diff --git a/example/config-app/uv.lock b/example/config-app/uv.lock index b5ded1c8..6868b878 100644 --- a/example/config-app/uv.lock +++ b/example/config-app/uv.lock @@ -162,7 +162,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.23.0" +version = "0.24.0" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, diff --git a/example/database-app/uv.lock b/example/database-app/uv.lock index 79b46c58..f2618dee 100644 --- a/example/database-app/uv.lock +++ b/example/database-app/uv.lock @@ -498,7 +498,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.23.0" +version = "0.24.0" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/Blueprint.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/Blueprint.py index b2d566eb..0acccff1 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/Blueprint.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/Blueprint.py @@ -743,7 +743,7 @@ def morphs(self, column, nullable=False, indexes=True): self._last_column = _columns return self - def to_sql(self): + async def to_sql(self): """Compiles the blueprint class into a sql statement. Returns: @@ -754,13 +754,10 @@ def to_sql(self): elif self._action == "create_table_if_not_exists": return self.platform().compile_create_sql(self.table, if_not_exists=True) else: - if not self._dry and self.table.from_table is None: - # get current table schema - table = self.platform().get_current_schema( + if self.table.from_table is None: + self.table.from_table = await self.platform().get_current_schema( self.connection, self.table.name, schema=self.schema ) - self.table.from_table = table - return self.platform().compile_alter_sql(self.table) def __enter__(self): @@ -781,12 +778,11 @@ def __exit__(self, exc_type, exc_value, exc_traceback): async def __aenter__(self): return self - # TODO: review async def __aexit__(self, exc_type, exc_value, exc_traceback): if self._dry: return - sql = self.to_sql() + sql = await self.to_sql() if isinstance(sql, list): for q in sql: await self.connection.statement(q, ()) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/MySQLPlatform.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/MySQLPlatform.py index 5c8119fc..e21764ef 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/MySQLPlatform.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/MySQLPlatform.py @@ -435,10 +435,10 @@ def compile_column_exists(self, table, column): def compile_get_all_tables(self, database, schema=None): return f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{database}'" - def get_current_schema(self, connection, table_name, schema=None): + async def get_current_schema(self, connection, table_name, schema=None): table = Table(table_name) sql = f"DESCRIBE {table_name}" - result = connection.query(sql, ()) + result = await connection.select(sql, ()) reversed_type_map = {v: k for k, v in self.type_map.items()} for column in result: diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/PostgresPlatform.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/PostgresPlatform.py index 1c3e3b04..5f0a7716 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/PostgresPlatform.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/PostgresPlatform.py @@ -492,7 +492,7 @@ def compile_column_exists(self, table, column): def compile_get_all_tables(self, database=None, schema=None): return f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{schema or 'public'}' AND table_catalog = '{database}' AND table_type = 'BASE TABLE'" - def get_current_schema(self, connection, table_name, schema=None): + async def get_current_schema(self, connection, table_name, schema=None): sql = self.table_information_string().format( table=table_name, schema=schema or "public" ) @@ -501,7 +501,7 @@ def get_current_schema(self, connection, table_name, schema=None): reversed_type_map.update(self.table_info_map) table = Table(table_name) - result = connection.query(sql, ()) + result = await connection.select(sql, ()) for column in result: column_type = reversed_type_map.get(column["data_type"].upper()) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/SQLitePlatform.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/SQLitePlatform.py index 5dcfcf21..d58dde0f 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/platforms/SQLitePlatform.py @@ -367,13 +367,13 @@ def columnize_names(self, columns): return names - def get_current_schema(self, connection, table_name, schema=None): + async def get_current_schema(self, connection, table_name, schema=None): sql = f"PRAGMA table_info({table_name})" reversed_type_map = {v: k for k, v in self.type_map.items()} table = Table(table_name) - result = connection.query(sql, ()) + result = await connection.select(sql, ()) for column in result: column_type = self.get_column_type( reversed_type_map, column["type"].upper() diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/schema.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/schema.py index 968bcad1..4d84c4c0 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/schema.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/schema/schema.py @@ -4,6 +4,48 @@ class Schema: + _type_hints_map = { + "string": str, + "char": str, + "big_increments": int, + "integer": int, + "big_integer": int, + "tiny_integer": int, + "small_integer": int, + "medium_integer": int, + "integer_unsigned": int, + "big_integer_unsigned": int, + "tiny_integer_unsigned": int, + "small_integer_unsigned": int, + "medium_integer_unsigned": int, + "increments": int, + "uuid": str, + "binary": bytes, + "boolean": bool, + "decimal": float, + "double": float, + "enum": str, + "text": str, + "tiny_text": str, + "float": float, + "geometry": str, + "json": dict, + "jsonb": bytes, + "inet": str, + "cidr": str, + "macaddr": str, + "long_text": str, + "point": str, + "time": str, + "timestamp": str, + "date": str, + "year": str, + "datetime": str, + "tiny_increments": int, + "unsigned": int, + "unsigned_integer": int, + } + def __init__(self, manager) -> None: self._manager = manager self._connection = None diff --git a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/migrations/2026_01_01_000001_add_body_to_posts_table.py b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/migrations/2026_01_01_000001_add_body_to_posts_table.py new file mode 100644 index 00000000..9d6176e2 --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/migrations/2026_01_01_000001_add_body_to_posts_table.py @@ -0,0 +1,11 @@ +from fastapi_startkit.masoniteorm import Migration + + +class AddBodyToPostsTable(Migration): + async def up(self): + async with await self.schema.table("posts") as table: + table.text("body").nullable() + + async def down(self): + async with await self.schema.table("posts") as table: + table.drop_column("body") \ No newline at end of file diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/schema/test_sqlite_schema_builder.py b/fastapi_startkit/tests/masoniteorm/sqlite/schema/test_sqlite_schema_builder.py index 8ae7ca76..906ac0d2 100644 --- a/fastapi_startkit/tests/masoniteorm/sqlite/schema/test_sqlite_schema_builder.py +++ b/fastapi_startkit/tests/masoniteorm/sqlite/schema/test_sqlite_schema_builder.py @@ -17,7 +17,7 @@ async def test_can_add_columns(self): self.assertEqual(len(blueprint.table.added_columns), 2) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'CREATE TABLE "users" ("name" VARCHAR(255) NOT NULL, "age" INTEGER NOT NULL)' ], @@ -32,7 +32,7 @@ async def test_can_add_tiny_text(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), ['CREATE TABLE "users" ("description" TEXT NOT NULL)'], ) @@ -45,7 +45,7 @@ async def test_can_add_unsigned_decimal(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), ['CREATE TABLE "users" ("amount" DECIMAL(19, 4) NOT NULL)'], ) @@ -59,7 +59,7 @@ async def test_can_create_table_if_not_exists(self): self.assertEqual(len(blueprint.table.added_columns), 2) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'CREATE TABLE IF NOT EXISTS "users" ("name" VARCHAR(255) NOT NULL, "age" INTEGER NOT NULL)' ], @@ -76,7 +76,7 @@ async def test_can_add_columns_with_constraint(self): self.assertEqual(len(blueprint.table.added_columns), 2) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'CREATE TABLE "users" ("name" VARCHAR(255) NOT NULL, "age" INTEGER NOT NULL, UNIQUE(name))' ], @@ -90,7 +90,7 @@ async def test_can_have_float_type(self): blueprint.float("amount") self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ """CREATE TABLE "users" (""" """\"amount" FLOAT(19, 4) NOT NULL)""" @@ -109,7 +109,7 @@ async def test_can_add_columns_with_foreign_key_constraint(self): self.assertEqual(len(blueprint.table.added_columns), 3) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'CREATE TABLE "users" ' '("name" VARCHAR(255) NOT NULL, ' @@ -134,7 +134,7 @@ async def test_can_add_columns_with_foreign_key_constraint_name(self): self.assertEqual(len(blueprint.table.added_columns), 3) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'CREATE TABLE "users" ' '("name" VARCHAR(255) NOT NULL, ' @@ -154,7 +154,7 @@ async def test_can_use_morphs_for_polymorphism_relationships(self): self.assertEqual(len(blueprint.table.added_columns), 2) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'CREATE TABLE "likes" ("record_id" INTEGER UNSIGNED NOT NULL, "record_type" VARCHAR NOT NULL)', 'CREATE INDEX likes_record_id_index ON "likes"(record_id)', @@ -181,7 +181,7 @@ async def test_can_advanced_table_creation(self): self.assertEqual(len(blueprint.table.added_columns), 11) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ """CREATE TABLE "users" ("id" INTEGER NOT NULL, "name" VARCHAR(255) NOT NULL, "gender" VARCHAR(255) CHECK(gender IN ('male', 'female')) NOT NULL, "email" VARCHAR(255) NOT NULL, """ """"password" VARCHAR(255) NOT NULL, "option" VARCHAR(255) NOT NULL DEFAULT 'ADMIN', "admin" INTEGER NOT NULL DEFAULT 0, "remember_token" VARCHAR(255) NULL, """ @@ -207,7 +207,7 @@ async def test_can_create_indexes(self): self.assertEqual(len(blueprint.table.added_columns), 0) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'CREATE INDEX users_name_index ON "users"(name)', 'CREATE INDEX active_idx ON "users"(active)', @@ -229,7 +229,7 @@ async def test_can_create_indexes_on_previous_column(self): self.assertEqual(len(blueprint.table.added_columns), 2) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'ALTER TABLE "users" ADD COLUMN "email" VARCHAR NOT NULL', 'ALTER TABLE "users" ADD COLUMN "active" VARCHAR NOT NULL', @@ -250,7 +250,7 @@ async def test_can_have_composite_keys(self): self.assertEqual(len(blueprint.table.added_columns), 3) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'CREATE TABLE "users" ' '("name" VARCHAR(255) NOT NULL, ' @@ -272,7 +272,7 @@ async def test_can_have_column_primary_key(self): self.assertEqual(len(blueprint.table.added_columns), 3) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'CREATE TABLE "users" ' '("name" VARCHAR(255) NOT NULL, ' @@ -309,7 +309,7 @@ async def test_can_advanced_table_creation2(self): self.assertEqual(len(blueprint.table.added_columns), 17) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'CREATE TABLE "users" ("id" INTEGER NOT NULL, "name" VARCHAR(255) NOT NULL, "duration" VARCHAR(255) NOT NULL, ' '"url" VARCHAR(255) NOT NULL, "payload" JSON NOT NULL, "birth" VARCHAR(4) NOT NULL, "last_address" VARCHAR(255) NULL, "route_origin" VARCHAR(255) NULL, "mac_address" VARCHAR(255) NULL, ' @@ -390,7 +390,7 @@ async def test_can_have_unsigned_columns(self): blueprint.medium_integer("medium_profile_id").unsigned() self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ """CREATE TABLE "users" (""" """"profile_id" INTEGER UNSIGNED NOT NULL, """ @@ -444,7 +444,7 @@ async def test_can_add_enum(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ "CREATE TABLE \"users\" (\"status\" VARCHAR(255) CHECK(status IN ('active', 'inactive')) NOT NULL DEFAULT 'active')" ], diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/schema/test_sqlite_schema_builder_alter.py b/fastapi_startkit/tests/masoniteorm/sqlite/schema/test_sqlite_schema_builder_alter.py index 005804c9..aea472a5 100644 --- a/fastapi_startkit/tests/masoniteorm/sqlite/schema/test_sqlite_schema_builder_alter.py +++ b/fastapi_startkit/tests/masoniteorm/sqlite/schema/test_sqlite_schema_builder_alter.py @@ -1,4 +1,4 @@ -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock from fastapi_startkit.masoniteorm.schema.Table import Table @@ -10,7 +10,7 @@ async def test_can_add_columns(self): mock_statement = AsyncMock() conn = self.schema.get_connection() conn.statement = mock_statement - conn.query = MagicMock(return_value=[]) + conn.select = AsyncMock(return_value=[]) async with await self.schema.table("users") as blueprint: blueprint.string("name") @@ -19,7 +19,7 @@ async def test_can_add_columns(self): self.assertEqual(len(blueprint.table.added_columns), 3) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'ALTER TABLE "users" ADD COLUMN "name" VARCHAR NOT NULL', """ALTER TABLE "users" ADD COLUMN "external_type" VARCHAR NOT NULL DEFAULT 'external'""", @@ -31,14 +31,14 @@ async def test_can_add_constraints(self): mock_statement = AsyncMock() conn = self.schema.get_connection() conn.statement = mock_statement - conn.query = MagicMock(return_value=[]) + conn.select = AsyncMock(return_value=[]) async with await self.schema.table("users") as blueprint: blueprint.unique("name", name="table_unique") self.assertEqual(len(blueprint.table.added_columns), 0) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), ['CREATE UNIQUE INDEX table_unique ON "users"(name)'], ) @@ -54,7 +54,7 @@ async def test_alter_rename(self): blueprint.table.from_table = table self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ "CREATE TEMPORARY TABLE __temp__users AS SELECT post FROM users", 'DROP TABLE "users"', @@ -78,7 +78,7 @@ async def test_alter_drop(self): blueprint.table.from_table = table self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ "CREATE TEMPORARY TABLE __temp__users AS SELECT name, email FROM users", 'DROP TABLE "users"', @@ -103,7 +103,7 @@ async def test_change(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual(len(blueprint.table.changed_columns), 1) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'ALTER TABLE "users" ADD COLUMN "name" VARCHAR NOT NULL', "CREATE TEMPORARY TABLE __temp__users AS SELECT age FROM users", @@ -144,7 +144,7 @@ async def test_timestamp_alter_add_nullable_column(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), ['ALTER TABLE "users" ADD COLUMN "due_date" TIMESTAMP NULL'], ) @@ -152,7 +152,7 @@ async def test_alter_drop_on_table_schema_table(self): mock_statement = AsyncMock() conn = self.schema.get_connection() conn.statement = mock_statement - conn.query = MagicMock(return_value=[]) + conn.select = AsyncMock(return_value=[]) async with await self.schema.table("table_schema") as blueprint: blueprint.drop_column("name") @@ -164,13 +164,13 @@ async def test_alter_add_primary(self): mock_statement = AsyncMock() conn = self.schema.get_connection() conn.statement = mock_statement - conn.query = MagicMock(return_value=[]) + conn.select = AsyncMock(return_value=[]) async with await self.schema.table("users") as blueprint: blueprint.primary("playlist_id") self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'ALTER TABLE "users" ADD CONSTRAINT users_playlist_id_primary PRIMARY KEY (playlist_id)' ], @@ -192,7 +192,7 @@ async def test_alter_add_column_and_foreign_key(self): blueprint.table.from_table = table self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ 'ALTER TABLE "users" ADD COLUMN "playlist_id" INTEGER UNSIGNED NULL REFERENCES "playlists"("id")', "CREATE TEMPORARY TABLE __temp__users AS SELECT age, email FROM users", @@ -219,7 +219,7 @@ async def test_alter_add_foreign_key_only(self): blueprint.table.from_table = table self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ "CREATE TEMPORARY TABLE __temp__users AS SELECT age, email FROM users", 'DROP TABLE "users"', @@ -234,19 +234,40 @@ async def test_can_add_column_enum(self): mock_statement = AsyncMock() conn = self.schema.get_connection() conn.statement = mock_statement - conn.query = MagicMock(return_value=[]) + conn.select = AsyncMock(return_value=[]) async with await self.schema.table("users") as blueprint: blueprint.enum("status", ["active", "inactive"]).default("active") self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ "ALTER TABLE \"users\" ADD COLUMN \"status\" VARCHAR CHECK('status' IN('active', 'inactive')) NOT NULL DEFAULT 'active'" ], ) + async def test_add_nullable_columns_without_from_table(self): + """Regression: adding nullable columns should use async get_current_schema, + not connection.query() which takes no arguments.""" + mock_statement = AsyncMock() + conn = self.schema.get_connection() + conn.statement = mock_statement + conn.select = AsyncMock(return_value=[]) + + async with await self.schema.table("clients") as blueprint: + blueprint.text("website").nullable() + blueprint.text("linkedin_url").nullable() + + self.assertEqual(len(blueprint.table.added_columns), 2) + self.assertEqual( + await blueprint.to_sql(), + [ + 'ALTER TABLE "clients" ADD COLUMN "website" TEXT NULL', + 'ALTER TABLE "clients" ADD COLUMN "linkedin_url" TEXT NULL', + ], + ) + async def test_can_change_column_enum(self): mock_statement = AsyncMock() self.schema.get_connection().statement = mock_statement @@ -257,7 +278,7 @@ async def test_can_change_column_enum(self): self.assertEqual(len(blueprint.table.changed_columns), 1) self.assertEqual( - blueprint.to_sql(), + await blueprint.to_sql(), [ "CREATE TEMPORARY TABLE __temp__users AS SELECT FROM users", 'DROP TABLE "users"', diff --git a/fastapi_startkit/uv.lock b/fastapi_startkit/uv.lock index f2d3b028..f1ae20be 100644 --- a/fastapi_startkit/uv.lock +++ b/fastapi_startkit/uv.lock @@ -443,7 +443,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.23.0" +version = "0.24.0" source = { editable = "." } dependencies = [ { name = "cleo" },