Skip to content

fix(orm): grouped where(lambda) prefixes subgroup columns with the table (GH #205) - #206

Merged
tmgbedu merged 1 commit into
mainfrom
task/where-lambda-subgroup-1397
Aug 16, 2026
Merged

fix(orm): grouped where(lambda) prefixes subgroup columns with the table (GH #205)#206
tmgbedu merged 1 commit into
mainfrom
task/where-lambda-subgroup-1397

Conversation

@tmgbedu

@tmgbedu tmgbedu commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #205.

Problem

A grouped where(lambda q: ...) rendered invalid SQL — the subgroup's columns lost their table prefix:

Message.where("thread_id", "t").where(
    lambda q: q.where("role", "user").where("type", "text").or_where("id", ">=", 42)
).to_sql()
# ... AND (."role" = 'user' AND ."type" = 'text' OR ."id" >= '42')

Postgres rejects ."role" with syntax error at or near ".".

Root cause

QueryBuilder.new() returned self.connection.query() — a builder with _table = "" and no model — and where() builds the subgroup as column(self.new()). The grammar then rendered the subgroup columns with an empty table prefix.

Fix

new() now carries the current table into the nested builder:

def new(self):
    return self.connection.query().table(self._table)
  • The where(lambda ...) subgroup inherits the parent table, so columns render as "messages"."role" etc.
  • Callers that need a different table (e.g. BelongsToMany's exists/join subqueries) already chain .table(...) right after new(), so their behaviour is unchanged.
  • The model / global scopes are intentionally not copied, so those executed subqueries stay clean (no accidental scope leakage).

Tests

New regression tests in tests/masoniteorm/sqlite/builder/test_sqlite_query_builder.py covering the exact grouped AND/OR pattern from the issue:

  • to_sql()... AND ("users"."active" = '1' AND "users"."age" > '20' OR "users"."id" >= '42')
  • to_qmark() → correct ? placeholders and binding order ['Joe', 1, 20, 42]

Full ORM suite (excl. live-Postgres): 723 passed, 7 skipped. The generated SQL is now valid Postgres, so the CI Postgres job exercises it end-to-end.

Real-world validation (follow-up)

The where_raw workaround this unblocks lives in example/agents/app/repositories/conversation.py on the ai-package-fix branch (not on main), so its revert isn't part of this PR. The added regression test replicates that exact ("role" = ? AND "type" = ? OR "id" >= ?) shape; once this lands and ai-package-fix syncs, that line reverts to where(lambda q: q.where("role","user").where("type","text").or_where("id",">=",boundary)).

QueryBuilder.new() returned self.connection.query() — a table-less builder — so
a where(lambda q: ...) subgroup rendered its columns with an empty table prefix,
e.g. AND (."role" = 'user' ...), which Postgres rejects (syntax error at ".").

Carry the current table into the nested builder. Callers that want a different
table (e.g. BelongsToMany's exists/join subqueries) still override it with
.table(...) immediately after new(), so their behaviour is unchanged — and the
model/global scopes are deliberately not copied, keeping those subqueries clean.

Add regression tests for to_sql() and to_qmark() (SQL + binding order) covering
the exact grouped AND/OR pattern from GH #205.
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@tmgbedu

tmgbedu commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

✅ Code Review: APPROVE (posted as comment — GitHub blocks self-approval on this account)

Reviewed the diff, traced every new() caller, and exercised edge cases beyond the two new tests.

Correctness

new() now returns self.connection.query().table(self._table). The grouped-lambda path (column(self.new()) in where()) makes the nested builder inherit the parent table, so subgroup columns render as "users"."col" instead of the invalid table-less ."col" Postgres rejects.

Binding order

to_qmark() yields bindings in source order ['Joe', 1, 20, 42] — subgroup wheres are appended in-order into the parent _bindings; grouping doesn't reorder. Covered by the new qmark test.

Deliberate NOT-copying model/global scopes — confirmed safe

  • Pre-fix new() already returned a bare builder with no model, no scopes; this change only adds .table(), so scope/model behavior is unchanged — nothing newly omitted.
  • Correct for a where(lambda) subgroup: it only renders inline _wheres into the parent (never executed standalone), so re-applying global scopes there would be wrong. Not copying keeps scoped subqueries clean.
  • Both BelongsToMany callers (query_where_exists, query_has) chain .table(table) right after new(), overriding the carried table — behavior unchanged.

Edge cases I verified manually

  • Explicitly-qualified column in a subgroup is not double-prefixed: where('roles.name',…)"roles"."name".
  • Nested subgroups inherit the table at every level: ("users"."a" = '1' AND ("users"."b" = '2' OR "users"."c" = '3')).

Tests & CI

  • test_sqlite_query_builder.py → 25 passed (incl. 2 new regression tests). Full tests/masoniteorm → 726 passed, 7 skipped; the 17 failures are all local live-Postgres (role "app" does not exist) — unrelated. CI green: Pytest, Ruff, Pyright all pass; mergeState CLEAN.

Regression coverage matches the issue precisely. Recommend merge — holding for QA sign-off (task #1400) per PM instruction.

@tmgbedu
tmgbedu merged commit e7b400a into main Aug 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix grouped where(lambda) rendering invalid SQL: subgroup columns lose their table prefix

1 participant