From 1107a49a18d39f132266a1a3e4bea527e9622dc4 Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Sat, 11 Jul 2026 23:31:35 -0700 Subject: [PATCH 1/2] feat(orm): select_sub subquery-as-column + Laravel-style add_select + table() - select_sub(subquery, alias): the subquery-as-column primitive. Renders (subquery) AS alias with bindings in SELECT position; accepts a builder or a callable that builds one. Matches Laravel selectSub(query, as). - add_select(*columns): Laravel's variadic column-adder. Appends plain string columns with a dedup guard; an associative {alias: subquery} entry (string key + queryable value) delegates to select_sub, seeding select({table}.*) first when nothing is selected yet. A string value under a string key stays a plain column. - table(name): sets the target table on a bare builder; backs the callable form of select_sub. - Migrate the *Through/BelongsToMany with-count callers from the old add_select(alias, callable) shape to select_sub(callable, alias). - process_columns: route the subquery alias through subquery_alias_string() instead of a hardcoded AS (single source of alias emission; SQL unchanged). Existing string-based select() is unchanged. Covered by grammar-level to_sql()/to_qmark() with binding-order assertions across all four dialects, plus a real sqlite suite incl. the Laravel addSelect({alias: subquery}) acceptance case. End-to-end relationship with-count remains gated on the async count() fix (#886). --- .../masoniteorm/models/builder.py | 57 ++++++ .../masoniteorm/query/grammars/BaseGrammar.py | 2 +- .../relationships/BelongsToMany.py | 4 +- .../relationships/HasManyThrough.py | 4 +- .../relationships/HasOneThrough.py | 4 +- .../query/grammars/test_select_sub_grammar.py | 187 ++++++++++++++++++ .../sqlite/builder/test_sqlite_select_sub.py | 161 +++++++++++++++ 7 files changed, 412 insertions(+), 7 deletions(-) create mode 100644 fastapi_startkit/tests/masoniteorm/query/grammars/test_select_sub_grammar.py create mode 100644 fastapi_startkit/tests/masoniteorm/sqlite/builder/test_sqlite_select_sub.py diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py index 5d818105..432a144d 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/builder.py @@ -80,6 +80,63 @@ def select(self, *args) -> "QueryBuilder": self._columns += (SelectExpression(column),) return self + @staticmethod + def _is_queryable(value) -> bool: + """A subquery source: a builder or a callable that returns one.""" + return isinstance(value, QueryBuilder) or (callable(value) and not isinstance(value, str)) + + def select_sub(self, subquery, alias: str) -> "QueryBuilder": + """Add a subquery as an aliased column: ``(subquery) AS alias``. + + ``subquery`` is a ``QueryBuilder`` or a callable that receives a fresh + builder and returns one. Mirrors Laravel's ``selectSub($query, $as)``. + """ + if callable(subquery) and not isinstance(subquery, QueryBuilder): + subquery = subquery(QueryBuilder(self.connection, self.grammar, self.processor)) + if not isinstance(subquery, QueryBuilder): + raise TypeError("select_sub() expects a QueryBuilder subquery or a callable returning one.") + self._columns += (SubGroupExpression(subquery, alias),) + return self + + def add_select(self, *columns) -> "QueryBuilder": + """Append columns to the current selection — Laravel's ``addSelect``. + + Accepts plain string columns (variadic, or a single list) appended with + a dedup guard. An associative ``{alias: subquery}`` entry — a string key + with a *queryable* value — adds a correlated column via + :meth:`select_sub`, seeding the selection with ``{table}.*`` first when + nothing is selected yet (so the base columns are not lost). A string + value under a string key is still treated as a plain column. + """ + if len(columns) == 1 and isinstance(columns[0], (list, tuple)): + columns = tuple(columns[0]) + + for column in columns: + pairs = column.items() if isinstance(column, dict) else ((None, column),) + for alias, value in pairs: + if self._is_queryable(value): + if not isinstance(alias, str): + raise TypeError( + "add_select() received a subquery without a string alias; " + "pass {alias: subquery} or use select_sub(subquery, alias)." + ) + if not self._columns: + self.select(f"{self._table}.*") + self.select_sub(value, alias) + continue + + already_selected = any( + isinstance(existing, SelectExpression) and existing.alias is None and existing.column == value + for existing in self._columns + ) + if not already_selected: + self._columns += (SelectExpression(value),) + return self + + def table(self, name: str) -> "QueryBuilder": + self._table = name + return self + def limit(self, limit: int) -> "QueryBuilder": self._limit = limit return self diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/query/grammars/BaseGrammar.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/query/grammars/BaseGrammar.py index 44822a51..f617fa88 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/query/grammars/BaseGrammar.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/query/grammars/BaseGrammar.py @@ -826,7 +826,7 @@ def process_columns(self, separator="", action="select", qmark=False): self.add_binding(*column.builder._bindings) else: builder_sql = column.builder.to_sql() - sql += f"({builder_sql}) AS {column.alias}, " + sql += f"({builder_sql}) " + self.subquery_alias_string().format(alias=column.alias) + ", " continue sql += self._table_column_string(column, alias=alias, separator=separator) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/BelongsToMany.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/BelongsToMany.py index 61c6a3c9..8248d287 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/BelongsToMany.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/BelongsToMany.py @@ -460,8 +460,7 @@ def get_with_count_query(self, builder, callback): if not builder._columns: builder = builder.select("*") - return_query = builder.add_select( - f"{query.get_table_name()}_count", + return_query = builder.select_sub( lambda q: ( q.count("*") .where_column( @@ -477,6 +476,7 @@ def get_with_count_query(self, builder, callback): ), ) ), + f"{query.get_table_name()}_count", ) return return_query diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/HasManyThrough.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/HasManyThrough.py index ed9ace29..54313efe 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/HasManyThrough.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/HasManyThrough.py @@ -178,8 +178,7 @@ def get_with_count_query(self, current_builder, callback): if not current_builder._columns: current_builder.select("*") - return_query = current_builder.add_select( - f"{self.attribute}_count", + return_query = current_builder.select_sub( lambda q: ( q.count("*") .join( @@ -201,6 +200,7 @@ def get_with_count_query(self, current_builder, callback): ), ) ), + f"{self.attribute}_count", ) return return_query diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/HasOneThrough.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/HasOneThrough.py index 2cb07761..5279dc89 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/HasOneThrough.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/relationships/HasOneThrough.py @@ -204,8 +204,7 @@ def get_with_count_query(self, current_builder, callback): if not current_builder._columns: current_builder.select("*") - return_query = current_builder.add_select( - f"{self.attribute}_count", + return_query = current_builder.select_sub( lambda q: ( q.count("*") .join( @@ -227,6 +226,7 @@ def get_with_count_query(self, current_builder, callback): ), ) ), + f"{self.attribute}_count", ) return return_query diff --git a/fastapi_startkit/tests/masoniteorm/query/grammars/test_select_sub_grammar.py b/fastapi_startkit/tests/masoniteorm/query/grammars/test_select_sub_grammar.py new file mode 100644 index 00000000..4c4edcd7 --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/query/grammars/test_select_sub_grammar.py @@ -0,0 +1,187 @@ +import unittest + +from fastapi_startkit.masoniteorm.models.builder import QueryBuilder +from fastapi_startkit.masoniteorm.query.grammars.SQLiteGrammar import SQLiteGrammar +from fastapi_startkit.masoniteorm.query.grammars.MySQLGrammar import MySQLGrammar +from fastapi_startkit.masoniteorm.query.grammars.PostgresGrammar import PostgresGrammar +from fastapi_startkit.masoniteorm.query.grammars.MSSQLGrammar import MSSQLGrammar + +GRAMMARS = { + "sqlite": SQLiteGrammar, + "mysql": MySQLGrammar, + "postgres": PostgresGrammar, + "mssql": MSSQLGrammar, +} + +EXPECTED = { + "sqlite": { + "select_sub": 'SELECT "posts"."id", (SELECT "categories"."name" FROM "categories" WHERE categories.id = posts.category_id LIMIT 1) AS cat_name FROM "posts"', + "variadic": 'SELECT "posts"."id", "posts"."title", "posts"."category_id" FROM "posts"', + "assoc_empty": 'SELECT "posts".*, (SELECT "categories"."name" FROM "categories" WHERE categories.id = posts.category_id LIMIT 1) AS cat FROM "posts"', + "assoc_after_select": 'SELECT "posts"."id", (SELECT "categories"."name" FROM "categories" WHERE categories.id = posts.category_id LIMIT 1) AS cat FROM "posts"', + "select_sub_qmark": 'SELECT "posts"."id", (SELECT "categories"."name" FROM "categories" WHERE "categories"."active" = ? LIMIT 1) AS cat FROM "posts" WHERE "posts"."status" = ?', + }, + "mysql": { + "select_sub": "SELECT `posts`.`id`, (SELECT `categories`.`name` FROM `categories` WHERE categories.id = posts.category_id LIMIT 1) AS cat_name FROM `posts`", + "variadic": "SELECT `posts`.`id`, `posts`.`title`, `posts`.`category_id` FROM `posts`", + "assoc_empty": "SELECT `posts`.*, (SELECT `categories`.`name` FROM `categories` WHERE categories.id = posts.category_id LIMIT 1) AS cat FROM `posts`", + "assoc_after_select": "SELECT `posts`.`id`, (SELECT `categories`.`name` FROM `categories` WHERE categories.id = posts.category_id LIMIT 1) AS cat FROM `posts`", + "select_sub_qmark": "SELECT `posts`.`id`, (SELECT `categories`.`name` FROM `categories` WHERE `categories`.`active` = ? LIMIT 1) AS cat FROM `posts` WHERE `posts`.`status` = ?", + }, + "postgres": { + "select_sub": 'SELECT "posts"."id", (SELECT "categories"."name" FROM "categories" WHERE categories.id = posts.category_id LIMIT 1) AS cat_name FROM "posts"', + "variadic": 'SELECT "posts"."id", "posts"."title", "posts"."category_id" FROM "posts"', + "assoc_empty": 'SELECT "posts".*, (SELECT "categories"."name" FROM "categories" WHERE categories.id = posts.category_id LIMIT 1) AS cat FROM "posts"', + "assoc_after_select": 'SELECT "posts"."id", (SELECT "categories"."name" FROM "categories" WHERE categories.id = posts.category_id LIMIT 1) AS cat FROM "posts"', + "select_sub_qmark": 'SELECT "posts"."id", (SELECT "categories"."name" FROM "categories" WHERE "categories"."active" = ? LIMIT 1) AS cat FROM "posts" WHERE "posts"."status" = ?', + }, + "mssql": { + "select_sub": "SELECT [posts].[id], (SELECT TOP 1 [categories].[name] FROM [categories] WHERE categories.id = posts.category_id) AS cat_name FROM [posts]", + "variadic": "SELECT [posts].[id], [posts].[title], [posts].[category_id] FROM [posts]", + "assoc_empty": "SELECT [posts].*, (SELECT TOP 1 [categories].[name] FROM [categories] WHERE categories.id = posts.category_id) AS cat FROM [posts]", + "assoc_after_select": "SELECT [posts].[id], (SELECT TOP 1 [categories].[name] FROM [categories] WHERE categories.id = posts.category_id) AS cat FROM [posts]", + "select_sub_qmark": "SELECT [posts].[id], (SELECT TOP 1 [categories].[name] FROM [categories] WHERE [categories].[active] = ?) AS cat FROM [posts] WHERE [posts].[status] = ?", + }, +} + + +def qb(grammar, table): + q = QueryBuilder(connection=None, grammar=grammar, processor=None) + q._table = table + return q + + +def category_subquery(grammar): + return qb(grammar, "categories").select("name").where_column("categories.id", "posts.category_id").limit(1) + + +class TestSelectSubGrammar(unittest.TestCase): + """Grammar-level parity for select_sub (subquery-as-column) across all dialects.""" + + def test_select_sub_builder(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + sql = qb(grammar, "posts").select("id").select_sub(category_subquery(grammar), "cat_name").to_sql() + self.assertEqual(sql, EXPECTED[name]["select_sub"]) + + def test_select_sub_callable(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + sql = ( + qb(grammar, "posts") + .select("id") + .select_sub( + lambda q: ( + q.table("categories") + .select("name") + .where_column("categories.id", "posts.category_id") + .limit(1) + ), + "cat_name", + ) + .to_sql() + ) + self.assertEqual(sql, EXPECTED[name]["select_sub"]) + + def test_select_sub_rejects_non_builder(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + with self.assertRaises(TypeError): + qb(grammar, "posts").select_sub("not-a-builder", "cat") + + def test_select_sub_qmark_binding_order(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + sub = qb(grammar, "categories").select("name").where("categories.active", 1).limit(1) + q = qb(grammar, "posts").select("id").select_sub(sub, "cat").where("posts.status", "active") + self.assertEqual(q.to_qmark(), EXPECTED[name]["select_sub_qmark"]) + # SELECT-clause subquery binding precedes the WHERE binding. + self.assertEqual(list(q.get_bindings()), [1, "active"]) + + +class TestAddSelectGrammar(unittest.TestCase): + """add_select() is Laravel's variadic column-adder, not a subquery method.""" + + def test_add_select_variadic_strings(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + sql = qb(grammar, "posts").select("id").add_select("title", "category_id").to_sql() + self.assertEqual(sql, EXPECTED[name]["variadic"]) + + def test_add_select_single_list(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + sql = qb(grammar, "posts").select("id").add_select(["title", "category_id"]).to_sql() + self.assertEqual(sql, EXPECTED[name]["variadic"]) + + def test_add_select_dedup_guard(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + # 'title' already selected -> not duplicated + sql = qb(grammar, "posts").select("id", "title").add_select("title", "category_id").to_sql() + self.assertEqual(sql, EXPECTED[name]["variadic"]) + + def test_add_select_assoc_seeds_table_star_when_empty(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + sql = qb(grammar, "posts").add_select({"cat": category_subquery(grammar)}).to_sql() + self.assertEqual(sql, EXPECTED[name]["assoc_empty"]) + + def test_add_select_assoc_keeps_existing_columns(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + sql = qb(grammar, "posts").select("id").add_select({"cat": category_subquery(grammar)}).to_sql() + self.assertEqual(sql, EXPECTED[name]["assoc_after_select"]) + + def test_add_select_assoc_callable(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + sql = ( + qb(grammar, "posts") + .add_select( + { + "cat": lambda q: ( + q.table("categories") + .select("name") + .where_column("categories.id", "posts.category_id") + .limit(1) + ) + } + ) + .to_sql() + ) + self.assertEqual(sql, EXPECTED[name]["assoc_empty"]) + + def test_add_select_string_value_under_string_key_is_plain_column(self): + # Laravel: only string-key + queryable-value delegates; a string value stays a column. + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + sql = qb(grammar, "posts").select("id").add_select({"ignored_key": "title"}).to_sql() + plain = qb(grammar, "posts").select("id").add_select("title").to_sql() + self.assertEqual(sql, plain) + + def test_add_select_list_mixed_columns_and_subquery(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + sql = qb(grammar, "posts").add_select(["id", {"cat": category_subquery(grammar)}]).to_sql() + self.assertIn("AS cat", sql) + # both the plain column and the subquery column are present + self.assertIn("id", sql) + + def test_add_select_bare_subquery_without_alias_raises(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + with self.assertRaises(TypeError): + qb(grammar, "posts").add_select(qb(grammar, "categories").select("name")) + + def test_add_select_assoc_qmark_binding_order(self): + for name, grammar in GRAMMARS.items(): + with self.subTest(grammar=name): + sub = qb(grammar, "categories").select("name").where("categories.active", 1).limit(1) + q = qb(grammar, "posts").select("id").add_select({"cat": sub}).where("posts.status", "active") + self.assertEqual(q.to_qmark(), EXPECTED[name]["select_sub_qmark"]) + self.assertEqual(list(q.get_bindings()), [1, "active"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/builder/test_sqlite_select_sub.py b/fastapi_startkit/tests/masoniteorm/sqlite/builder/test_sqlite_select_sub.py new file mode 100644 index 00000000..90b4c88d --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/sqlite/builder/test_sqlite_select_sub.py @@ -0,0 +1,161 @@ +from fastapi_startkit.masoniteorm import Model + +from ..fixtures.db import DB +from ..test_case import TestCase + + +class Category(Model): + __table__ = "categories" + __timestamps__ = None + id: int + name: str + + +class Post(Model): + __table__ = "posts" + __timestamps__ = None + id: int + title: str + category_id: int + + +class Account(Model): + __table__ = "accounts" + __timestamps__ = None + id: int + name: str + + +class Login(Model): + __table__ = "logins" + __timestamps__ = None + id: int + user_id: int + created_at: str + + +def category_name_subquery(): + return Category.query().select("name").where_column("categories.id", "posts.category_id").limit(1) + + +class TestSqliteSelectSub(TestCase): + async def asyncSetUp(self): + await super().asyncSetUp() + conn = DB.connection("default") + await conn.execute("CREATE TABLE categories (id INTEGER PRIMARY KEY, name TEXT)") + await conn.execute("CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, category_id INTEGER)") + await conn.execute("INSERT INTO categories (id, name) VALUES (1, 'Zebra'), (2, 'Apple'), (3, 'Mango')") + await conn.execute( + "INSERT INTO posts (id, title, category_id) VALUES (1, 'z', 1), (2, 'a', 2), (3, 'm', 3), (4, 'a2', 2)" + ) + await conn.execute("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT)") + await conn.execute("CREATE TABLE logins (id INTEGER PRIMARY KEY, user_id INTEGER, created_at TEXT)") + await conn.execute("INSERT INTO accounts (id, name) VALUES (1, 'Alice'), (2, 'Bob')") + await conn.execute( + "INSERT INTO logins (id, user_id, created_at) VALUES " + "(1, 1, '2024-01-01'), (2, 1, '2024-03-01'), (3, 2, '2024-02-01')" + ) + + async def test_select_sub_with_builder(self): + posts = await ( + Post.query() + .select("id", "title") + .select_sub(category_name_subquery(), "category_name") + .order_by("id") + .get() + ) + pairs = [(p.id, p.serialize()["category_name"]) for p in posts] + self.assertEqual(pairs, [(1, "Zebra"), (2, "Apple"), (3, "Mango"), (4, "Apple")]) + + async def test_select_sub_with_callable(self): + posts = await ( + Post.query() + .select("id") + .select_sub( + lambda q: ( + q.table("categories").select("name").where_column("categories.id", "posts.category_id").limit(1) + ), + "category_name", + ) + .order_by("id") + .get() + ) + self.assertEqual([p.serialize()["category_name"] for p in posts], ["Zebra", "Apple", "Mango", "Apple"]) + + async def test_add_select_variadic_columns(self): + posts = await Post.query().select("id").add_select("title", "category_id").order_by("id").get() + self.assertEqual( + [(p.id, p.title, p.category_id) for p in posts], [(1, "z", 1), (2, "a", 2), (3, "m", 3), (4, "a2", 2)] + ) + + async def test_add_select_associative_subquery_acceptance_case(self): + # Mirrors Laravel: addSelect(['last_login_at' => Logins::select('created_at') + # ->whereColumn('user_id','users.id')->latest()->take(1)]) — take(1) => limit(1). + accounts = ( + await Account.query() + .add_select( + { + "last_login_at": Login.query() + .select("created_at") + .where_column("user_id", "accounts.id") + .latest() + .limit(1) + } + ) + .order_by("id") + .get() + ) + result = {a.name: a.serialize()["last_login_at"] for a in accounts} + self.assertEqual(result, {"Alice": "2024-03-01", "Bob": "2024-02-01"}) + + async def test_add_select_associative_seeds_table_star(self): + # No prior select(): base columns must still be present (seeded via {table}.*). + accounts = ( + await Account.query() + .add_select( + { + "last_login_at": Login.query() + .select("created_at") + .where_column("user_id", "accounts.id") + .latest() + .limit(1) + } + ) + .order_by("id") + .get() + ) + self.assertEqual([a.name for a in accounts], ["Alice", "Bob"]) + + async def test_table_setter_overrides_target_table(self): + rows = await Post.query().table("categories").select("name").order_by("id").get() + self.assertEqual([r.serialize()["name"] for r in rows], ["Zebra", "Apple", "Mango"]) + + async def test_select_sub_with_join_shaped_callable_compiles_and_runs(self): + # The shape the migrated *Through/BelongsToMany with-count callers now use: + # select_sub(callable that sets table + joins + where_column, alias). This + # asserts the real correlated subquery-as-column compiles (JOIN, correlated + # column, alias all present) and executes. The relationship methods' own + # end-to-end with-count is fixed separately — it needs a synchronous COUNT + # subquery instead of the async count() coroutine (see #886). + query = ( + Post.query() + .select("posts.id") + .select_sub( + lambda q: ( + q.table("categories") + .select("name") + .join("posts", "posts.category_id", "=", "categories.id") + .where_column("categories.id", "posts.category_id") + .limit(1) + ), + "joined_name", + ) + ) + sql = query.to_sql() + self.assertIn("JOIN", sql) + self.assertIn("categories.id = posts.category_id", sql) + self.assertIn("AS joined_name", sql) + + rows = await query.order_by("posts.id").get() + self.assertEqual(len(rows), 4) + self.assertTrue(all("joined_name" in r.serialize() for r in rows)) From da46c3114fdb452e66dfc3fdf1068a20b982a3a5 Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Sun, 12 Jul 2026 10:22:51 -0700 Subject: [PATCH 2/2] test(orm): build select_sub fixtures via Schema + ORM insert Replace raw conn.execute CREATE TABLE / INSERT with the framework's Schema builder and Model.insert() for the categories/posts and accounts/logins fixtures. Same data and same assertions (incl. the Laravel addSelect last_login_at acceptance case). --- .../sqlite/builder/test_sqlite_select_sub.py | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/builder/test_sqlite_select_sub.py b/fastapi_startkit/tests/masoniteorm/sqlite/builder/test_sqlite_select_sub.py index 90b4c88d..a0b9ee90 100644 --- a/fastapi_startkit/tests/masoniteorm/sqlite/builder/test_sqlite_select_sub.py +++ b/fastapi_startkit/tests/masoniteorm/sqlite/builder/test_sqlite_select_sub.py @@ -1,6 +1,5 @@ from fastapi_startkit.masoniteorm import Model -from ..fixtures.db import DB from ..test_case import TestCase @@ -41,19 +40,39 @@ def category_name_subquery(): class TestSqliteSelectSub(TestCase): async def asyncSetUp(self): await super().asyncSetUp() - conn = DB.connection("default") - await conn.execute("CREATE TABLE categories (id INTEGER PRIMARY KEY, name TEXT)") - await conn.execute("CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT, category_id INTEGER)") - await conn.execute("INSERT INTO categories (id, name) VALUES (1, 'Zebra'), (2, 'Apple'), (3, 'Mango')") - await conn.execute( - "INSERT INTO posts (id, title, category_id) VALUES (1, 'z', 1), (2, 'a', 2), (3, 'm', 3), (4, 'a2', 2)" + async with await self.schema.create("categories") as table: + table.id() + table.string("name") + async with await self.schema.create("posts") as table: + table.id() + table.string("title") + table.integer("category_id") + async with await self.schema.create("accounts") as table: + table.id() + table.string("name") + async with await self.schema.create("logins") as table: + table.id() + table.integer("user_id") + table.string("created_at") + + await Category.query().insert( + [{"id": 1, "name": "Zebra"}, {"id": 2, "name": "Apple"}, {"id": 3, "name": "Mango"}] ) - await conn.execute("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT)") - await conn.execute("CREATE TABLE logins (id INTEGER PRIMARY KEY, user_id INTEGER, created_at TEXT)") - await conn.execute("INSERT INTO accounts (id, name) VALUES (1, 'Alice'), (2, 'Bob')") - await conn.execute( - "INSERT INTO logins (id, user_id, created_at) VALUES " - "(1, 1, '2024-01-01'), (2, 1, '2024-03-01'), (3, 2, '2024-02-01')" + await Post.query().insert( + [ + {"id": 1, "title": "z", "category_id": 1}, + {"id": 2, "title": "a", "category_id": 2}, + {"id": 3, "title": "m", "category_id": 3}, + {"id": 4, "title": "a2", "category_id": 2}, + ] + ) + await Account.query().insert([{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]) + await Login.query().insert( + [ + {"id": 1, "user_id": 1, "created_at": "2024-01-01"}, + {"id": 2, "user_id": 1, "created_at": "2024-03-01"}, + {"id": 3, "user_id": 2, "created_at": "2024-02-01"}, + ] ) async def test_select_sub_with_builder(self):