From f497fa7f9d4739d270faa3d20a6529e14775a90a Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Sun, 16 Aug 2026 16:36:03 -0700 Subject: [PATCH] fix(orm): grouped where(lambda) prefixes subgroup columns with the table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../masoniteorm/models/builder.py | 6 ++++- .../builder/test_sqlite_query_builder.py | 27 +++++++++++++++++++ fastapi_startkit/uv.lock | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py index 5e793f73..94fd34a5 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py @@ -466,7 +466,11 @@ async def chunk_by_id_desc(self, count: int, column: str = None, alias: str = No yield results def new(self): - return self.connection.query() + # Carry the current table so a nested builder (e.g. a where(lambda ...) + # subgroup) prefixes its columns correctly instead of rendering a + # table-less ."column". Callers that want a different table override it + # with .table(...) as usual. + return self.connection.query().table(self._table) def invalid_operator(self, operator): """Determine whether an operator is not supported by the builder.""" diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/builder/test_sqlite_query_builder.py b/fastapi_startkit/tests/masoniteorm/sqlite/builder/test_sqlite_query_builder.py index 41094d1c..cc0dcecd 100644 --- a/fastapi_startkit/tests/masoniteorm/sqlite/builder/test_sqlite_query_builder.py +++ b/fastapi_startkit/tests/masoniteorm/sqlite/builder/test_sqlite_query_builder.py @@ -134,3 +134,30 @@ async def test_delete_with_multiple_wheres(self): sql, bindings = mock_delete.call_args[0] self.assertEqual(sql, 'DELETE FROM "users" WHERE "age" = ? AND "profile" = ?') self.assertEqual(list(bindings), [20, 1]) + + async def test_where_grouped_lambda_prefixes_subgroup_columns(self): + sql = ( + User.query() + .where("name", "Joe") + .where(lambda q: q.where("active", 1).where("age", ">", 20).or_where("id", ">=", 42)) + .to_sql() + ) + self.assertEqual( + sql, + 'SELECT * FROM "users" WHERE "users"."name" = \'Joe\' AND ' + '("users"."active" = \'1\' AND "users"."age" > \'20\' OR "users"."id" >= \'42\')', + ) + + async def test_where_grouped_lambda_to_qmark_binding_order(self): + builder = ( + User.query() + .where("name", "Joe") + .where(lambda q: q.where("active", 1).where("age", ">", 20).or_where("id", ">=", 42)) + ) + sql = builder.to_qmark() + self.assertEqual( + sql, + 'SELECT * FROM "users" WHERE "users"."name" = ? AND ' + '("users"."active" = ? AND "users"."age" > ? OR "users"."id" >= ?)', + ) + self.assertEqual(list(builder._bindings), ["Joe", 1, 20, 42]) diff --git a/fastapi_startkit/uv.lock b/fastapi_startkit/uv.lock index 5808c35b..c9ea4cba 100644 --- a/fastapi_startkit/uv.lock +++ b/fastapi_startkit/uv.lock @@ -527,7 +527,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.48.0" +version = "0.50.0" source = { editable = "." } dependencies = [ { name = "cleo" },