From a81feaea8049187f91bfd0d699ed2a3b4f63ac37 Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Fri, 15 May 2026 14:45:50 -0700 Subject: [PATCH 1/2] feat: migrations fix --- docker-compose.yml | 2 + .../2026_04_26_110113_create_users.py | 2 +- ...26_04_27_145006_create_categories_table.py | 2 +- ...2026_04_27_145006_create_profiles_table.py | 2 +- .../2026_04_27_145007_create_courses_table.py | 2 +- ...6_04_27_145008_create_course_user_table.py | 2 +- .../2026_04_27_145008_create_lessons_table.py | 2 +- .../2026_04_27_145009_create_reviews_table.py | 2 +- .../databases/seeds/user_seeder.py | 4 +- example/database-app/uv.lock | 2 +- .../src/fastapi_startkit/__init__.py | 1 + .../src/fastapi_startkit/commands/__init__.py | 1 - .../src/fastapi_startkit/console/__init__.py | 7 + .../{console.py => console/application.py} | 15 +- .../can_override_config.py} | 0 .../can_override_default_options.py} | 0 .../src/fastapi_startkit/console/command.py | 14 + .../{commands => console}/publish_command.py | 2 +- .../fastapi/commands/serve_command.py | 2 +- .../fastapi_startkit/masoniteorm/__init__.py | 4 +- .../masoniteorm/commands/Command.py | 6 - .../masoniteorm/commands/DBMigrateCommand.py | 54 +-- .../masoniteorm/commands/DBSeedCommand.py | 2 +- .../masoniteorm/commands/Entry.py | 43 --- .../commands/MakeMigrationCommand.py | 2 +- .../masoniteorm/commands/MakeModelCommand.py | 2 +- .../commands/MakeModelDocstringCommand.py | 2 +- .../commands/MakeObserverCommand.py | 2 +- .../masoniteorm/commands/MakeSeedCommand.py | 2 +- .../commands/MigrateFreshCommand.py | 18 +- .../commands/MigrateRefreshCommand.py | 2 +- .../commands/MigrateResetCommand.py | 29 +- .../commands/MigrateRollbackCommand.py | 37 +-- .../commands/MigrateStatusCommand.py | 33 +- .../masoniteorm/commands/ShellCommand.py | 2 +- .../commands/stubs/create_migration.stub | 2 +- .../masoniteorm/config/database.py | 2 +- .../masoniteorm/migrations/Migration.py | 296 +---------------- .../masoniteorm/migrations/Migrator.py | 307 ++++++++++++++++++ .../masoniteorm/migrations/__init__.py | 3 + .../masoniteorm/models/MigrationModel.py | 2 +- .../masoniteorm/providers/DatabaseProvider.py | 6 +- .../providers/app_provider.py | 2 +- .../tests/masoniteorm/commands/__init__.py | 0 .../masoniteorm/commands/fixtures/__init__.py | 0 .../masoniteorm/commands/fixtures/app.py | 28 ++ .../commands/fixtures/databases/__init__.py | 0 .../2026_01_01_000000_create_posts_table.py | 12 + .../fixtures/databases/migrations/__init__.py | 0 .../commands/test_migrate_commands.py | 115 +++++++ .../tests/masoniteorm/sqlite/fixtures/db.py | 1 + .../2026_01_01_000000_create_posts_table.py | 12 + .../sqlite/fixtures/migrations/__init__.py | 0 .../migrations/test_sqlite_migration.py | 43 +++ .../schema/test_sqlite_schema_builder.py | 3 + fastapi_startkit/uv.lock | 2 +- mysql-init.sql | 3 + taskiq_poc.py | 142 ++++++++ 58 files changed, 836 insertions(+), 449 deletions(-) delete mode 100644 fastapi_startkit/src/fastapi_startkit/commands/__init__.py create mode 100644 fastapi_startkit/src/fastapi_startkit/console/__init__.py rename fastapi_startkit/src/fastapi_startkit/{console.py => console/application.py} (63%) rename fastapi_startkit/src/fastapi_startkit/{masoniteorm/commands/CanOverrideConfig.py => console/can_override_config.py} (100%) rename fastapi_startkit/src/fastapi_startkit/{masoniteorm/commands/CanOverrideOptionsDefault.py => console/can_override_default_options.py} (100%) create mode 100644 fastapi_startkit/src/fastapi_startkit/console/command.py rename fastapi_startkit/src/fastapi_startkit/{commands => console}/publish_command.py (97%) delete mode 100644 fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/Command.py delete mode 100644 fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/Entry.py create mode 100644 fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migrator.py create mode 100644 fastapi_startkit/tests/masoniteorm/commands/__init__.py create mode 100644 fastapi_startkit/tests/masoniteorm/commands/fixtures/__init__.py create mode 100644 fastapi_startkit/tests/masoniteorm/commands/fixtures/app.py create mode 100644 fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/__init__.py create mode 100644 fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/migrations/2026_01_01_000000_create_posts_table.py create mode 100644 fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/migrations/__init__.py create mode 100644 fastapi_startkit/tests/masoniteorm/commands/test_migrate_commands.py create mode 100644 fastapi_startkit/tests/masoniteorm/sqlite/fixtures/migrations/2026_01_01_000000_create_posts_table.py create mode 100644 fastapi_startkit/tests/masoniteorm/sqlite/fixtures/migrations/__init__.py create mode 100644 fastapi_startkit/tests/masoniteorm/sqlite/migrations/test_sqlite_migration.py create mode 100644 mysql-init.sql create mode 100644 taskiq_poc.py diff --git a/docker-compose.yml b/docker-compose.yml index 1a70a7c0..d2920ab8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: MYSQL_PASSWORD: secret ports: - "3306:3306" + volumes: + - ./mysql-init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: [ "CMD", "mysqladmin", "ping", "--silent" ] interval: 5s diff --git a/example/database-app/databases/migrations/2026_04_26_110113_create_users.py b/example/database-app/databases/migrations/2026_04_26_110113_create_users.py index 82cb2f23..310864a3 100644 --- a/example/database-app/databases/migrations/2026_04_26_110113_create_users.py +++ b/example/database-app/databases/migrations/2026_04_26_110113_create_users.py @@ -1,6 +1,6 @@ """Create-users Migration.""" -from fastapi_startkit.masoniteorm.migrations import Migration +from fastapi_startkit.masoniteorm import Migration class CreateUsers(Migration): diff --git a/example/database-app/databases/migrations/2026_04_27_145006_create_categories_table.py b/example/database-app/databases/migrations/2026_04_27_145006_create_categories_table.py index 3385379e..aac570b2 100644 --- a/example/database-app/databases/migrations/2026_04_27_145006_create_categories_table.py +++ b/example/database-app/databases/migrations/2026_04_27_145006_create_categories_table.py @@ -1,6 +1,6 @@ """CreateCategoriesTable Migration.""" -from fastapi_startkit.masoniteorm.migrations import Migration +from fastapi_startkit.masoniteorm import Migration class CreateCategoriesTable(Migration): diff --git a/example/database-app/databases/migrations/2026_04_27_145006_create_profiles_table.py b/example/database-app/databases/migrations/2026_04_27_145006_create_profiles_table.py index 2b7e9f9e..6a600d3b 100644 --- a/example/database-app/databases/migrations/2026_04_27_145006_create_profiles_table.py +++ b/example/database-app/databases/migrations/2026_04_27_145006_create_profiles_table.py @@ -1,6 +1,6 @@ """CreateProfilesTable Migration.""" -from fastapi_startkit.masoniteorm.migrations import Migration +from fastapi_startkit.masoniteorm import Migration class CreateProfilesTable(Migration): diff --git a/example/database-app/databases/migrations/2026_04_27_145007_create_courses_table.py b/example/database-app/databases/migrations/2026_04_27_145007_create_courses_table.py index 15c0b735..b26ffb06 100644 --- a/example/database-app/databases/migrations/2026_04_27_145007_create_courses_table.py +++ b/example/database-app/databases/migrations/2026_04_27_145007_create_courses_table.py @@ -1,6 +1,6 @@ """CreateCoursesTable Migration.""" -from fastapi_startkit.masoniteorm.migrations import Migration +from fastapi_startkit.masoniteorm import Migration class CreateCoursesTable(Migration): diff --git a/example/database-app/databases/migrations/2026_04_27_145008_create_course_user_table.py b/example/database-app/databases/migrations/2026_04_27_145008_create_course_user_table.py index c1871638..e5c0a108 100644 --- a/example/database-app/databases/migrations/2026_04_27_145008_create_course_user_table.py +++ b/example/database-app/databases/migrations/2026_04_27_145008_create_course_user_table.py @@ -1,6 +1,6 @@ """CreateCourseUserTable Migration.""" -from fastapi_startkit.masoniteorm.migrations import Migration +from fastapi_startkit.masoniteorm import Migration class CreateCourseUserTable(Migration): diff --git a/example/database-app/databases/migrations/2026_04_27_145008_create_lessons_table.py b/example/database-app/databases/migrations/2026_04_27_145008_create_lessons_table.py index 7d9ebfce..3f9582ab 100644 --- a/example/database-app/databases/migrations/2026_04_27_145008_create_lessons_table.py +++ b/example/database-app/databases/migrations/2026_04_27_145008_create_lessons_table.py @@ -1,6 +1,6 @@ """CreateLessonsTable Migration.""" -from fastapi_startkit.masoniteorm.migrations import Migration +from fastapi_startkit.masoniteorm import Migration class CreateLessonsTable(Migration): diff --git a/example/database-app/databases/migrations/2026_04_27_145009_create_reviews_table.py b/example/database-app/databases/migrations/2026_04_27_145009_create_reviews_table.py index d6837241..ee8ee462 100644 --- a/example/database-app/databases/migrations/2026_04_27_145009_create_reviews_table.py +++ b/example/database-app/databases/migrations/2026_04_27_145009_create_reviews_table.py @@ -1,6 +1,6 @@ """CreateReviewsTable Migration.""" -from fastapi_startkit.masoniteorm.migrations import Migration +from fastapi_startkit.masoniteorm import Migration class CreateReviewsTable(Migration): diff --git a/example/database-app/databases/seeds/user_seeder.py b/example/database-app/databases/seeds/user_seeder.py index 1cb64209..737612cc 100644 --- a/example/database-app/databases/seeds/user_seeder.py +++ b/example/database-app/databases/seeds/user_seeder.py @@ -46,5 +46,5 @@ async def run(self): for data in users: profile_data = data.pop("profile") - user, _ = await User.first_or_create({"email": data["email"]}, data) - await Profile.first_or_create({"user_id": user.id}, {"user_id": user.id, **profile_data}) \ No newline at end of file + user= await User.first_or_create({"email": data["email"]}, data) + await Profile.first_or_create({"user_id": user.id}, {"user_id": user.id, **profile_data}) diff --git a/example/database-app/uv.lock b/example/database-app/uv.lock index 56a1b993..5a426a93 100644 --- a/example/database-app/uv.lock +++ b/example/database-app/uv.lock @@ -498,7 +498,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.20.0" +version = "0.21.0" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, diff --git a/fastapi_startkit/src/fastapi_startkit/__init__.py b/fastapi_startkit/src/fastapi_startkit/__init__.py index 87cf3e61..892fc268 100644 --- a/fastapi_startkit/src/fastapi_startkit/__init__.py +++ b/fastapi_startkit/src/fastapi_startkit/__init__.py @@ -1,4 +1,5 @@ from .application import Application +from .console import ConsoleApplication from .configuration.config import Config __all__ = ["Application", "Config"] diff --git a/fastapi_startkit/src/fastapi_startkit/commands/__init__.py b/fastapi_startkit/src/fastapi_startkit/commands/__init__.py deleted file mode 100644 index 8b137891..00000000 --- a/fastapi_startkit/src/fastapi_startkit/commands/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fastapi_startkit/src/fastapi_startkit/console/__init__.py b/fastapi_startkit/src/fastapi_startkit/console/__init__.py new file mode 100644 index 00000000..c98daee3 --- /dev/null +++ b/fastapi_startkit/src/fastapi_startkit/console/__init__.py @@ -0,0 +1,7 @@ +from .application import ConsoleApplication +from .command import Command + +__all__ = [ + "ConsoleApplication", + "Command" +] diff --git a/fastapi_startkit/src/fastapi_startkit/console.py b/fastapi_startkit/src/fastapi_startkit/console/application.py similarity index 63% rename from fastapi_startkit/src/fastapi_startkit/console.py rename to fastapi_startkit/src/fastapi_startkit/console/application.py index 1b4f6ff6..bc853124 100644 --- a/fastapi_startkit/src/fastapi_startkit/console.py +++ b/fastapi_startkit/src/fastapi_startkit/console/application.py @@ -1,19 +1,26 @@ +from typing import TYPE_CHECKING + from cleo.application import Application as BaseApplication from cleo.io.io import IO -from fastapi_startkit.application import Application + +if TYPE_CHECKING: + from fastapi_startkit.application import Application class ConsoleApplication(BaseApplication): - def __init__(self, app: Application): + def __init__(self, app: "Application"): super().__init__() self.app = app # Register commands from Application for command in self.app.commands: if isinstance(command, type): - self.add(command()) + instance = command() else: - self.add(command) + instance = command + + instance.set_container(self.app) + self.add(instance) def render_error(self, error: Exception, io: IO) -> None: self.app.exception_manager.report(error) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/CanOverrideConfig.py b/fastapi_startkit/src/fastapi_startkit/console/can_override_config.py similarity index 100% rename from fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/CanOverrideConfig.py rename to fastapi_startkit/src/fastapi_startkit/console/can_override_config.py diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/CanOverrideOptionsDefault.py b/fastapi_startkit/src/fastapi_startkit/console/can_override_default_options.py similarity index 100% rename from fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/CanOverrideOptionsDefault.py rename to fastapi_startkit/src/fastapi_startkit/console/can_override_default_options.py diff --git a/fastapi_startkit/src/fastapi_startkit/console/command.py b/fastapi_startkit/src/fastapi_startkit/console/command.py new file mode 100644 index 00000000..f67f592a --- /dev/null +++ b/fastapi_startkit/src/fastapi_startkit/console/command.py @@ -0,0 +1,14 @@ +from typing import TYPE_CHECKING + +from .can_override_config import CanOverrideConfig +from .can_override_default_options import CanOverrideOptionsDefault + +if TYPE_CHECKING: + from fastapi_startkit import Application + + +class Command(CanOverrideOptionsDefault, CanOverrideConfig): + container: "Application" + + def set_container(self, container: "Application") -> None: + self.container = container diff --git a/fastapi_startkit/src/fastapi_startkit/commands/publish_command.py b/fastapi_startkit/src/fastapi_startkit/console/publish_command.py similarity index 97% rename from fastapi_startkit/src/fastapi_startkit/commands/publish_command.py rename to fastapi_startkit/src/fastapi_startkit/console/publish_command.py index 5bbe2282..928e0516 100644 --- a/fastapi_startkit/src/fastapi_startkit/commands/publish_command.py +++ b/fastapi_startkit/src/fastapi_startkit/console/publish_command.py @@ -2,7 +2,7 @@ import shutil from typing import TYPE_CHECKING -from cleo.commands.command import Command +from fastapi_startkit.console import Command from cleo.helpers import option from fastapi_startkit.helpers.string import Str diff --git a/fastapi_startkit/src/fastapi_startkit/fastapi/commands/serve_command.py b/fastapi_startkit/src/fastapi_startkit/fastapi/commands/serve_command.py index 128012a9..9ba13166 100644 --- a/fastapi_startkit/src/fastapi_startkit/fastapi/commands/serve_command.py +++ b/fastapi_startkit/src/fastapi_startkit/fastapi/commands/serve_command.py @@ -1,4 +1,4 @@ -from cleo.commands.command import Command +from fastapi_startkit.console.command import Command from cleo.helpers import option diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/__init__.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/__init__.py index 18a6579e..731e8c69 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/__init__.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/__init__.py @@ -1,6 +1,8 @@ from .config.config import MySQLConfig, PostgresConfig, SQLiteConfig from .facades import DB +from .migrations.Migration import Migration +from .migrations.Migrator import Migrator from .models import Model from .providers import DatabaseProvider -__all__ = ["DatabaseProvider", "PostgresConfig", "MySQLConfig", "SQLiteConfig", "Model", "DB"] +__all__ = ["DatabaseProvider", "PostgresConfig", "MySQLConfig", "SQLiteConfig", "Model", "DB", "Migration", "Migrator"] diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/Command.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/Command.py deleted file mode 100644 index a4d13a10..00000000 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/Command.py +++ /dev/null @@ -1,6 +0,0 @@ -from .CanOverrideConfig import CanOverrideConfig -from .CanOverrideOptionsDefault import CanOverrideOptionsDefault - - -class Command(CanOverrideOptionsDefault, CanOverrideConfig): - pass diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/DBMigrateCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/DBMigrateCommand.py index b8750f00..2d3e790e 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/DBMigrateCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/DBMigrateCommand.py @@ -1,7 +1,10 @@ import os -from .Command import Command + from cleo.helpers import option +from fastapi_startkit.console import Command +from fastapi_startkit.masoniteorm.migrations.Migrator import Migrator + class DBMigrateCommand(Command): name = "db:migrate" @@ -28,12 +31,6 @@ class DBMigrateCommand(Command): flag=True, description="Force migrations without prompt in production", ), - option( - "show", - "s", - flag=True, - description="Shows the output of SQL for migrations that would be running", - ), option( "directory", "d", @@ -49,29 +46,42 @@ def handle(self): return asyncio.run(self.handle_async()) async def handle_async(self): - from ..migrations import Migration + self.confirm_to_proceed() - # prompt user for confirmation in production - if os.getenv("APP_ENV") == "production" and not self.option("force"): - answer = "" - while answer not in ["y", "n"]: - answer = input( - "Do you want to run migrations in PRODUCTION ? (y/n)\n" - ).lower() - if answer != "y": - self.info("Migrations cancelled") - exit(0) - migration = Migration( + directory = self.resolve_migration_path() + + migration = Migrator( command_class=self, connection=self.option("connection"), - migration_directory=self.option("directory"), + migration_directory=directory, ) + await migration.create_table_if_not_exists() if not await migration.get_unran_migrations(): self.info("Nothing To Migrate!") return migration_name = self.option("migration") - show_output = self.option("show") - await migration.migrate(migration=migration_name, output=show_output) + await migration.migrate(migration=migration_name) + + def resolve_migration_path(self) -> str: + path = self.option('directory') + + config = self.container.make('config').get('database.migrations') + default_directory = config.get('directory') + + migration_directory = path or default_directory + return self.container.use_base_path(migration_directory) + + def confirm_to_proceed(self) -> None: + # prompt user for confirmation in production + if os.getenv("APP_ENV") == "production" and not self.option("force"): + answer = "" + while answer not in ["y", "n"]: + answer = input( + "Do you want to run migrations in PRODUCTION ? (y/n)\n" + ).lower() + if answer != "y": + self.info("Migrations cancelled") + exit(0) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/DBSeedCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/DBSeedCommand.py index 3b90c96a..ed6a2610 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/DBSeedCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/DBSeedCommand.py @@ -1,7 +1,7 @@ from cleo.helpers import argument, option from inflection import camelize, underscore -from .Command import Command +from fastapi_startkit.console import Command class DBSeedCommand(Command): diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/Entry.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/Entry.py deleted file mode 100644 index 67c83ea3..00000000 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/Entry.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Craft Command. - -This module is really used for backup only if the masonite CLI cannot import this for you. -This can be used by running "python craft". This module is not ran when the CLI can -successfully import commands for you. -""" - -from cleo.application import Application - -from . import ( - MakeMigrationCommand, - MakeModelCommand, - MakeModelDocstringCommand, - MakeObserverCommand, - MakeSeedCommand, - DBMigrateCommand, - MigrateFreshCommand, - MigrateRefreshCommand, - MigrateResetCommand, - MigrateRollbackCommand, - MigrateStatusCommand, - DBSeedCommand, - ShellCommand, -) - -application = Application("ORM Version:", "0.1") - -application.add(DBMigrateCommand()) -application.add(MigrateRollbackCommand()) -application.add(MigrateRefreshCommand()) -application.add(MigrateFreshCommand()) -application.add(MakeMigrationCommand()) -application.add(MakeModelCommand()) -application.add(MakeModelDocstringCommand()) -application.add(MakeObserverCommand()) -application.add(MigrateResetCommand()) -application.add(MigrateStatusCommand()) -application.add(MakeSeedCommand()) -application.add(DBSeedCommand()) -application.add(ShellCommand()) - -if __name__ == "__main__": - application.run() diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeMigrationCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeMigrationCommand.py index 6b79baf7..4e32b378 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeMigrationCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeMigrationCommand.py @@ -4,7 +4,7 @@ from inflection import tableize from cleo.helpers import argument, option from fastapi_startkit.helpers.string import Str -from .Command import Command +from fastapi_startkit.console import Command class MakeMigrationCommand(Command): diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeModelCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeModelCommand.py index 4666602f..d0fafecb 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeModelCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeModelCommand.py @@ -1,7 +1,7 @@ import os import pathlib from inflection import camelize, tableize, underscore -from .Command import Command +from fastapi_startkit.console import Command from cleo.helpers import argument, option diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeModelDocstringCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeModelDocstringCommand.py index 732e6daa..38401ea6 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeModelDocstringCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeModelDocstringCommand.py @@ -1,6 +1,6 @@ from cleo.helpers import argument, option from ..config import load_config -from .Command import Command +from fastapi_startkit.console import Command class MakeModelDocstringCommand(Command): diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeObserverCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeObserverCommand.py index e46071d3..da2ea3e8 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeObserverCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeObserverCommand.py @@ -2,7 +2,7 @@ import os import pathlib from inflection import camelize, underscore -from .Command import Command +from fastapi_startkit.console import Command class MakeObserverCommand(Command): diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeSeedCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeSeedCommand.py index 8349088d..3033ec03 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeSeedCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeSeedCommand.py @@ -2,7 +2,7 @@ import os import pathlib from inflection import camelize, underscore -from .Command import Command +from fastapi_startkit.console import Command class MakeSeedCommand(Command): diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateFreshCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateFreshCommand.py index babfdc22..4aa8d494 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateFreshCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateFreshCommand.py @@ -1,5 +1,6 @@ from cleo.helpers import option -from .Command import Command +from fastapi_startkit.console import Command +from fastapi_startkit.masoniteorm.migrations.Migrator import Migrator class MigrateFreshCommand(Command): @@ -49,12 +50,12 @@ def handle(self): return asyncio.run(self.handle_async()) async def handle_async(self): - from ..migrations import Migration + directory = self.resolve_migration_path() - migration = Migration( + migration = Migrator( command_class=self, connection=self.option("connection"), - migration_directory=self.option("directory"), + migration_directory=directory, ) await migration.fresh(ignore_fk=not self.option("no-fk")) @@ -69,3 +70,12 @@ async def handle_async(self): "seed:run", f"{self.option('seed')} --directory {self.option('seed-directory')} --connection {self.option('connection')}", ) + + def resolve_migration_path(self) -> str: + path = self.option('directory') + + config = self.container.make('config').get('database.migrations') + default_directory = config.get('directory') + + migration_directory = path or default_directory + return self.container.use_base_path(migration_directory) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateRefreshCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateRefreshCommand.py index c7a89836..1885e04a 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateRefreshCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateRefreshCommand.py @@ -1,5 +1,5 @@ from cleo.helpers import option -from .Command import Command +from fastapi_startkit.console import Command class MigrateRefreshCommand(Command): diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateResetCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateResetCommand.py index 3a1fd848..8aac6687 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateResetCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateResetCommand.py @@ -1,9 +1,10 @@ from cleo.helpers import option -from .Command import Command +from fastapi_startkit.console import Command +from fastapi_startkit.masoniteorm.migrations.Migrator import Migrator class MigrateResetCommand(Command): - name = "migrate:reset" + name = "db:migrate:reset" description = "Reset migrations." options = [ @@ -21,13 +22,6 @@ class MigrateResetCommand(Command): default="default", description="The connection you want to run migrations on", ), - option( - "schema", - None, - flag=False, - default=None, - description="Sets the schema to be migrated", - ), option( "directory", "d", @@ -43,14 +37,21 @@ def handle(self): return asyncio.run(self.handle_async()) async def handle_async(self): - from ..migrations import Migration + directory = self.resolve_migration_path() - migration = Migration( + migration = Migrator( command_class=self, connection=self.option("connection"), - migration_directory=self.option("directory"), - config_path=self.option("config"), - schema=self.option("schema"), + migration_directory=directory, ) await migration.reset(self.option("migration")) + + def resolve_migration_path(self) -> str: + path = self.option('directory') + + config = self.container.make('config').get('database.migrations') + default_directory = config.get('directory') + + migration_directory = path or default_directory + return self.container.use_base_path(migration_directory) \ No newline at end of file diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateRollbackCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateRollbackCommand.py index da874193..1d359163 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateRollbackCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateRollbackCommand.py @@ -1,5 +1,6 @@ from cleo.helpers import option -from .Command import Command +from fastapi_startkit.console import Command +from fastapi_startkit.masoniteorm.migrations.Migrator import Migrator class MigrateRollbackCommand(Command): @@ -21,19 +22,6 @@ class MigrateRollbackCommand(Command): default="default", description="The connection you want to run migrations on", ), - option( - "show", - "s", - flag=True, - description="Shows the output of SQL for migrations that would be running", - ), - option( - "schema", - None, - flag=False, - default=None, - description="Sets the schema to be migrated", - ), option( "directory", "d", @@ -49,12 +37,21 @@ def handle(self): return asyncio.run(self.handle_async()) async def handle_async(self): - from ..migrations import Migration + directory = self.resolve_migration_path() - await Migration( + migration = Migrator( command_class=self, connection=self.option("connection"), - migration_directory=self.option("directory"), - config_path=self.option("config"), - schema=self.option("schema"), - ).rollback(migration=self.option("migration"), output=self.option("show")) + migration_directory=directory, + ) + + await migration.rollback(migration=self.option("migration")) + + def resolve_migration_path(self) -> str: + path = self.option('directory') + + config = self.container.make('config').get('database.migrations') + default_directory = config.get('directory') + + migration_directory = path or default_directory + return self.container.use_base_path(migration_directory) \ No newline at end of file diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateStatusCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateStatusCommand.py index 66d4ae27..6f8ed8dd 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateStatusCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateStatusCommand.py @@ -1,6 +1,6 @@ from cleo.helpers import option -from ..migrations import Migration -from .Command import Command +from fastapi_startkit.console import Command +from fastapi_startkit.masoniteorm.migrations.Migrator import Migrator class MigrateStatusCommand(Command): @@ -15,13 +15,6 @@ class MigrateStatusCommand(Command): default="default", description="The connection you want to run migrations on", ), - option( - "schema", - None, - flag=False, - default=None, - description="Sets the schema to be migrated", - ), option( "directory", "d", @@ -37,16 +30,17 @@ def handle(self): return asyncio.run(self.handle_async()) async def handle_async(self): - migration = Migration( + directory = self.resolve_migration_path() + + migration = Migrator( command_class=self, connection=self.option("connection"), - migration_directory=self.option("directory"), - config_path=self.option("config"), - schema=self.option("schema"), + migration_directory=directory, ) + await migration.create_table_if_not_exists() table = self.table() - table.set_header_row(["Ran?", "Migration", "Batch"]) + table.set_headers(["Ran?", "Migration", "Batch"]) migrations = [] ran_migrations = await migration.get_ran_migrations() @@ -74,4 +68,13 @@ async def handle_async(self): table.set_rows(migrations) - table.render(self.io) + table.render() + + def resolve_migration_path(self) -> str: + path = self.option('directory') + + config = self.container.make('config').get('database.migrations') + default_directory = config.get('directory') + + migration_directory = path or default_directory + return self.container.use_base_path(migration_directory) \ No newline at end of file diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/ShellCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/ShellCommand.py index 0a33b466..b13b7eea 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/ShellCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/ShellCommand.py @@ -4,7 +4,7 @@ import shlex import subprocess from collections import OrderedDict -from .Command import Command +from fastapi_startkit.console import Command class ShellCommand(Command): diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/stubs/create_migration.stub b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/stubs/create_migration.stub index fe3573a3..4bceea2f 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/stubs/create_migration.stub +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/stubs/create_migration.stub @@ -1,6 +1,6 @@ """__MIGRATION_NAME__ Migration.""" -from fastapi_startkit.masoniteorm.migrations import Migration +from fastapi_startkit.masoniteorm import Migration class __MIGRATION_NAME__(Migration): diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/config/database.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/config/database.py index 0db7e1d7..c175aa43 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/config/database.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/config/database.py @@ -20,5 +20,5 @@ class DatabaseConfig: ) migrations: Dict[str, Dict[str, Any]] = field( - default_factory=lambda: {"table": "migrations", "path": "databases/migrations"} + default_factory=lambda: {"table": "migrations", "directory": "databases/migrations"} ) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migration.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migration.py index a88597df..e63605bc 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migration.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migration.py @@ -1,297 +1,11 @@ -import os -from os import listdir -from os.path import isfile, join -from pydoc import locate -from timeit import default_timer as timer +from typing import TYPE_CHECKING -from inflection import camelize - -from ..models.MigrationModel import MigrationModel +if TYPE_CHECKING: + from fastapi_startkit.masoniteorm import DatabaseManager class Migration: - db_manager: "DatabaseManager" - - def __init__( - self, - connection="default", - command_class=None, - migration_directory="databases/migrations", - ): + def __init__(self, connection=None, schema=None): self.connection = connection - self.migration_directory = migration_directory - self.last_migrations_ran = [] - self.command_class = command_class - - self.schema = self.db_manager.get_schema_builder() - - self.migration_model = MigrationModel.on(self.connection) - - async def create_table_if_not_exists(self): - async with await self.schema.create_table_if_not_exists("migrations") as table: - table.increments("migration_id") - table.string("migration") - table.integer("batch") - - async def get_unran_migrations(self): - directory_path = os.path.join(os.getcwd(), self.migration_directory) - all_migrations = [ - f.replace(".py", "") - for f in listdir(directory_path) - if isfile(join(directory_path, f)) - and f != "__init__.py" - and not f.startswith(".") - ] - all_migrations.sort() - unran_migrations = [] - database_migrations = await self.migration_model.get() - for migration in all_migrations: - if migration not in database_migrations.pluck("migration"): - unran_migrations.append(migration) - return unran_migrations - - async def get_rollback_migrations(self): - all_migrations = await self.migration_model.all() - return ( - await self.migration_model.where("batch", all_migrations.max("batch")) - .order_by("migration_id", "desc") - .get() - ).pluck("migration") - - async def get_all_migrations(self, reverse=False): - if reverse: - return ( - await self.migration_model.order_by("migration_id", "desc").get() - ).pluck("migration") - - return (await self.migration_model.all()).pluck("migration") - - async def get_last_batch_number(self): - return (await self.migration_model.all()).max("batch") or 0 - - async def delete_migration(self, file_path): - return await self.migration_model.where("migration", file_path).delete() - - def locate(self, file_name): - migration_name = camelize("_".join(file_name.split("_")[4:]).replace(".py", "")) - file_name = file_name.replace(".py", "") - migration_directory = self.migration_directory.replace("/", ".").replace( - "\\", "." - ) - return locate(f"{migration_directory}.{file_name}.{migration_name}") - - async def get_ran_migrations(self): - directory_path = os.path.join(os.getcwd(), self.migration_directory) - all_migrations = [ - f.replace(".py", "") - for f in listdir(directory_path) - if isfile(join(directory_path, f)) - and f != "__init__.py" - and not f.startswith(".") - ] - all_migrations.sort() - ran = [] - - database_migrations = await self.migration_model.all() - for migration in all_migrations: - matched_migration = database_migrations.where( - "migration", migration - ).first() - if matched_migration: - ran.append( - { - "migration_file": matched_migration.migration, - "batch": matched_migration.batch, - } - ) - return ran - - async def migrate(self, migration="all", output=False): - default_migrations = await self.get_unran_migrations() - migrations = default_migrations if migration == "all" else [migration] - - batch = int(await self.get_last_batch_number() or 0) + 1 - - for migration in migrations: - try: - migration_class = self.locate(migration) - except TypeError: - migration_class = None - - if migration_class is None: - if self.command_class: - self.command_class.line(f"Not Found: {migration}") - continue - - self.last_migrations_ran.append(migration) - if self.command_class: - self.command_class.line( - f"Migrating: {migration}" - ) - - migration_class = migration_class(connection=self.connection) - - if output: - migration_class.schema.dry() - start = timer() - await migration_class.up() - duration = "{:.2f}".format(timer() - start) - - if output: - if self.command_class: - table = self.command_class.table() - table.set_header_row(["SQL"]) - sql = migration_class.schema._blueprint.to_sql() - if isinstance(sql, list): - sql = ",".join(sql) - table.set_rows([[sql]]) - table.render(self.command_class.io) - continue - else: - print(migration_class.schema._blueprint.to_sql()) - - if self.command_class: - self.command_class.line( - f"Migrated: {migration} ({duration}s)" - ) - - await self.migration_model.create( - {"batch": batch, "migration": migration.replace(".py", "")} - ) - - async def rollback(self, migration="all", output=False): - default_migrations = await self.get_rollback_migrations() - migrations = default_migrations if migration == "all" else [migration] - - for migration in migrations: - if migration.endswith(".py"): - migration = migration.replace(".py", "") - - if self.command_class: - self.command_class.line( - f"Rolling back: {migration}" - ) - - try: - migration_class = self.locate(migration) - except TypeError: - self.command_class.line(f"Not Found: {migration}") - continue - - migration_class = migration_class( - connection=self.connection, schema=self.schema_name - ) - - if output: - migration_class.schema.dry() - - start = timer() - await migration_class.down() - duration = "{:.2f}".format(timer() - start) - - if output: - if self.command_class: - table = self.command_class.table() - table.set_header_row(["SQL"]) - if ( - hasattr(migration_class.schema, "_blueprint") - and migration_class.schema._blueprint - ): - sql = migration_class.schema._blueprint.to_sql() - if isinstance(sql, list): - sql = ",".join(sql) - - table.set_rows([[sql]]) - elif migration_class.schema._sql: - table.set_rows([[migration_class.schema._sql]]) - table.render(self.command_class.io) - continue - else: - print(migration_class.schema._blueprint.to_sql()) - - await self.delete_migration(migration) - - if self.command_class: - self.command_class.line( - f"Rolled back: {migration} ({duration}s)" - ) - - async def delete_migrations(self, migrations=None): - return await self.migration_model.where_in( - "migration", migrations or [] - ).delete() - - async def delete_last_batch(self): - return await self.migration_model.where( - "batch", await self.get_last_batch_number() - ).delete() - - async def reset(self, migration="all"): - default_migrations = await self.get_all_migrations(reverse=True) - migrations = default_migrations if migration == "all" else [migration] - - if not len(migrations): - if self.command_class: - self.command_class.line("Nothing to reset") - else: - print("Nothing to reset") - - for migration in migrations: - if self.command_class: - self.command_class.line( - f"Rolling back: {migration}" - ) - - try: - migration_instance = self.locate(migration)( - connection=self.connection, schema=self.schema_name - ) - await migration_instance.down() - except TypeError: - self.command_class.line(f"Not Found: {migration}") - continue - - # raise MigrationNotFound(f"Could not find {migration}") - - await self.delete_migration(migration) - - if self.command_class: - self.command_class.line( - f"Rolled back: {migration}" - ) - - await self.delete_migrations([migration]) - - if self.command_class: - self.command_class.line("") - - async def refresh(self, migration="all"): - await self.reset(migration) - await self.migrate(migration) - - async def drop_all_tables(self, ignore_fk=False): - if self.command_class: - self.command_class.line("Dropping all tables") - - if ignore_fk: - await self.schema.disable_foreign_key_constraints() - - for table in await self.schema.get_all_tables(): - await self.schema.drop(table) - - if ignore_fk: - await self.schema.enable_foreign_key_constraints() - - if self.command_class: - self.command_class.line("All tables dropped") - - async def fresh(self, ignore_fk=False, migration="all"): - await self.drop_all_tables(ignore_fk=ignore_fk) - await self.create_table_if_not_exists() - - if not await self.get_unran_migrations(): - if self.command_class: - self.command_class.line("Nothing to migrate") - return - await self.migrate(migration) + self.schema = schema diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migrator.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migrator.py new file mode 100644 index 00000000..2d87f6f3 --- /dev/null +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migrator.py @@ -0,0 +1,307 @@ +import os +from dumpdie import dd +from os import listdir +from os.path import isfile, join +from pydoc import locate +from timeit import default_timer as timer +from typing import TYPE_CHECKING + +from inflection import camelize + +from ..models.MigrationModel import MigrationModel + +if TYPE_CHECKING: + from fastapi_startkit.masoniteorm import DatabaseManager + + +class Migrator: + db_manager: "DatabaseManager" + + def __init__( + self, + migration_directory: str, + connection="default", + command_class=None, + ): + self.connection = connection + self.migration_directory = migration_directory + + self.last_migrations_ran = [] + self.command_class = command_class + + self.schema = self.db_manager.get_schema_builder() + + self.migration_model = MigrationModel.on(self.connection) + + async def create_table_if_not_exists(self): + async with await self.schema.create_table_if_not_exists("migrations") as table: + table.increments("id") + table.string("migration") + table.integer("batch") + + async def get_unran_migrations(self): + directory_path = self.migration_directory + all_migrations = [ + f.replace(".py", "") + for f in listdir(directory_path) + if isfile(join(directory_path, f)) + and f != "__init__.py" + and not f.startswith(".") + ] + all_migrations.sort() + unran_migrations = [] + database_migrations = await self.migration_model.get() + for migration in all_migrations: + if migration not in database_migrations.pluck("migration"): + unran_migrations.append(migration) + return unran_migrations + + async def get_rollback_migrations(self): + all_migrations = await self.migration_model.all() + return ( + await self.migration_model.where("batch", all_migrations.max("batch")) + .order_by("id", "desc") + .get() + ).pluck("migration") + + async def get_all_migrations(self, reverse=False): + if reverse: + return ( + await self.migration_model.new_query().order_by("id", "desc").get() + ).pluck("migration") + + return (await self.migration_model.all()).pluck("migration") + + async def get_last_batch_number(self): + return (await self.migration_model.all()).max("batch") or 0 + + async def delete_migration(self, file_path): + return await self.migration_model.where("migration", file_path).delete() + + def locate(self, file_name): + import sys + migration_name = camelize("_".join(file_name.split("_")[4:]).replace(".py", "")) + file_name = file_name.replace(".py", "") + migration_directory = str(self.migration_directory) + for sys_path in sorted(sys.path, key=len, reverse=True): + if sys_path and migration_directory.startswith(sys_path): + migration_directory = migration_directory[len(sys_path):].lstrip("/\\") + break + migration_directory = migration_directory.replace("/", ".").replace("\\", ".") + return locate(f"{migration_directory}.{file_name}.{migration_name}") + + async def get_ran_migrations(self): + directory_path = self.migration_directory + all_migrations = [ + f.replace(".py", "") + for f in listdir(directory_path) + if isfile(join(directory_path, f)) + and f != "__init__.py" + and not f.startswith(".") + ] + all_migrations.sort() + ran = [] + + database_migrations = await self.migration_model.all() + for migration in all_migrations: + matched_migration = database_migrations.where( + "migration", migration + ).first() + if matched_migration: + ran.append( + { + "migration_file": matched_migration.migration, + "batch": matched_migration.batch, + } + ) + return ran + + async def migrate(self, migration="all", output=False): + default_migrations = await self.get_unran_migrations() + migrations = default_migrations if migration == "all" else [migration] + + batch = int(await self.get_last_batch_number() or 0) + 1 + + for migration in migrations: + try: + migration_class = self.locate(migration) + except TypeError: + migration_class = None + + if migration_class is None: + if self.command_class: + self.command_class.line(f"Not Found: {migration}") + continue + + self.last_migrations_ran.append(migration) + if self.command_class: + self.command_class.line( + f"Migrating: {migration}" + ) + + migration_class = migration_class(connection=self.connection, schema=self.schema) + + if output: + migration_class.schema.dry() + start = timer() + await migration_class.up() + duration = "{:.2f}".format(timer() - start) + + if output: + if self.command_class: + table = self.command_class.table() + table.set_headers(["SQL"]) + sql = migration_class.schema._blueprint.to_sql() + if isinstance(sql, list): + sql = ",".join(sql) + table.set_rows([[sql]]) + table.render() + continue + else: + print(migration_class.schema._blueprint.to_sql()) + + if self.command_class: + self.command_class.line( + f"Migrated: {migration} ({duration}s)" + ) + + await self.migration_model.create( + {"batch": batch, "migration": migration.replace(".py", "")} + ) + + async def rollback(self, migration="all", output=False): + default_migrations = await self.get_rollback_migrations() + migrations = default_migrations if migration == "all" else [migration] + + for migration in migrations: + if migration.endswith(".py"): + migration = migration.replace(".py", "") + + if self.command_class: + self.command_class.line( + f"Rolling back: {migration}" + ) + + try: + migration_class = self.locate(migration) + except TypeError: + self.command_class.line(f"Not Found: {migration}") + continue + + migration_class = migration_class( + connection=self.connection, schema=self.schema + ) + + if output: + migration_class.schema.dry() + + start = timer() + await migration_class.down() + duration = "{:.2f}".format(timer() - start) + + if output: + if self.command_class: + table = self.command_class.table() + table.set_headers(["SQL"]) + if ( + hasattr(migration_class.schema, "_blueprint") + and migration_class.schema._blueprint + ): + sql = migration_class.schema._blueprint.to_sql() + if isinstance(sql, list): + sql = ",".join(sql) + + table.set_rows([[sql]]) + elif migration_class.schema._sql: + table.set_rows([[migration_class.schema._sql]]) + table.render() + continue + else: + print(migration_class.schema._blueprint.to_sql()) + + await self.delete_migration(migration) + + if self.command_class: + self.command_class.line( + f"Rolled back: {migration} ({duration}s)" + ) + + async def delete_migrations(self, migrations=None): + return await self.migration_model.where_in( + "migration", migrations or [] + ).delete() + + async def delete_last_batch(self): + return await self.migration_model.where( + "batch", await self.get_last_batch_number() + ).delete() + + async def reset(self, migration="all"): + default_migrations = await self.get_all_migrations(reverse=True) + migrations = default_migrations if migration == "all" else [migration] + + if not len(migrations): + if self.command_class: + self.command_class.line("Nothing to reset") + else: + print("Nothing to reset") + + for migration in migrations: + if self.command_class: + self.command_class.line( + f"Rolling back: {migration}" + ) + + try: + migration_instance = self.locate(migration)( + connection=self.connection, schema=self.schema + ) + await migration_instance.down() + except TypeError: + self.command_class.line(f"Not Found: {migration}") + continue + + # raise MigrationNotFound(f"Could not find {migration}") + + await self.delete_migration(migration) + + if self.command_class: + self.command_class.line( + f"Rolled back: {migration}" + ) + + await self.delete_migrations([migration]) + + if self.command_class: + self.command_class.line("") + + async def refresh(self, migration="all"): + await self.reset(migration) + await self.migrate(migration) + + async def drop_all_tables(self, ignore_fk=False): + if self.command_class: + self.command_class.line("Dropping all tables") + + if ignore_fk: + await self.schema.disable_foreign_key_constraints() + + for table in await self.schema.get_all_tables(): + await self.schema.drop(table) + + if ignore_fk: + await self.schema.enable_foreign_key_constraints() + + if self.command_class: + self.command_class.line("All tables dropped") + + async def fresh(self, ignore_fk=False, migration="all"): + await self.drop_all_tables(ignore_fk=ignore_fk) + await self.create_table_if_not_exists() + + if not await self.get_unran_migrations(): + if self.command_class: + self.command_class.line("Nothing to migrate") + return + + await self.migrate(migration) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/__init__.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/__init__.py index 5213e909..362177bd 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/__init__.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/__init__.py @@ -1 +1,4 @@ from .Migration import Migration +from .Migrator import Migrator + +__all__ = ["Migration", "Migrator"] diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/MigrationModel.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/MigrationModel.py index a13e62e6..b25d8562 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/MigrationModel.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/MigrationModel.py @@ -4,7 +4,7 @@ class MigrationModel(Model): __table__ = "migrations" __timestamps__ = False - __primary_key__ = "migration_id" + __primary_key__ = "id" migration: str batch: int diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/providers/DatabaseProvider.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/providers/DatabaseProvider.py index aa9c0c25..15d4b7b8 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/providers/DatabaseProvider.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/providers/DatabaseProvider.py @@ -12,8 +12,8 @@ ) from fastapi_startkit.masoniteorm.connections.factory import ConnectionFactory from fastapi_startkit.masoniteorm.connections.manager import DatabaseManager -from fastapi_startkit.masoniteorm.migrations import Migration -from fastapi_startkit.masoniteorm.models import Model +from fastapi_startkit.masoniteorm import Migrator +from fastapi_startkit.masoniteorm import Model from fastapi_startkit.providers.Provider import Provider @@ -30,7 +30,7 @@ def register(self): self.app.bind("schema", db.get_schema_builder()) Model.db_manager = db - Migration.db_manager = db + Migrator.db_manager = db def boot(self) -> None: self.publishes( diff --git a/fastapi_startkit/src/fastapi_startkit/providers/app_provider.py b/fastapi_startkit/src/fastapi_startkit/providers/app_provider.py index f809e81c..f6b984ce 100644 --- a/fastapi_startkit/src/fastapi_startkit/providers/app_provider.py +++ b/fastapi_startkit/src/fastapi_startkit/providers/app_provider.py @@ -1,4 +1,4 @@ -from fastapi_startkit.commands.publish_command import PublishCommand +from fastapi_startkit.console.publish_command import PublishCommand from fastapi_startkit.providers import Provider diff --git a/fastapi_startkit/tests/masoniteorm/commands/__init__.py b/fastapi_startkit/tests/masoniteorm/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastapi_startkit/tests/masoniteorm/commands/fixtures/__init__.py b/fastapi_startkit/tests/masoniteorm/commands/fixtures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastapi_startkit/tests/masoniteorm/commands/fixtures/app.py b/fastapi_startkit/tests/masoniteorm/commands/fixtures/app.py new file mode 100644 index 00000000..43004174 --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/commands/fixtures/app.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, Any + +from fastapi_startkit.application import Application +from fastapi_startkit.masoniteorm import SQLiteConfig +from fastapi_startkit.masoniteorm.providers import DatabaseProvider + +BASE_DIR = Path(__file__).parent +DB_PATH = BASE_DIR / "test.sqlite3" + + +def create_app() -> Application: + return Application( + base_path=BASE_DIR, + providers=[ + (DatabaseProvider, { + "default": "sqlite", + "connections": { + "sqlite": SQLiteConfig( + driver="sqlite", + url=f"sqlite+aiosqlite:///{DB_PATH}", + options=None, + ), + } + }), + ], + ) diff --git a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/__init__.py b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/migrations/2026_01_01_000000_create_posts_table.py b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/migrations/2026_01_01_000000_create_posts_table.py new file mode 100644 index 00000000..a30e13eb --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/migrations/2026_01_01_000000_create_posts_table.py @@ -0,0 +1,12 @@ +from fastapi_startkit.masoniteorm import Migration + + +class CreatePostsTable(Migration): + async def up(self): + async with await self.schema.create("posts") as table: + table.id() + table.string("title") + table.timestamps() + + async def down(self): + await self.schema.drop("posts") diff --git a/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/migrations/__init__.py b/fastapi_startkit/tests/masoniteorm/commands/fixtures/databases/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastapi_startkit/tests/masoniteorm/commands/test_migrate_commands.py b/fastapi_startkit/tests/masoniteorm/commands/test_migrate_commands.py new file mode 100644 index 00000000..64f7614e --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/commands/test_migrate_commands.py @@ -0,0 +1,115 @@ +import asyncio +import unittest + +from cleo.testers.command_tester import CommandTester + +from fastapi_startkit.masoniteorm.commands.DBMigrateCommand import DBMigrateCommand +from fastapi_startkit.masoniteorm.commands.MigrateFreshCommand import MigrateFreshCommand +from fastapi_startkit.masoniteorm.commands.MigrateResetCommand import MigrateResetCommand +from fastapi_startkit.masoniteorm.commands.MigrateRollbackCommand import MigrateRollbackCommand +from fastapi_startkit.masoniteorm.commands.MigrateStatusCommand import MigrateStatusCommand +from fastapi_startkit.masoniteorm.migrations.Migrator import Migrator +from .fixtures.app import create_app, DB_PATH + + +class TestMigrateCommands(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.app = create_app() + + def setUp(self): + asyncio.run(self._reset_db()) + + def tearDown(self): + if DB_PATH.exists(): + DB_PATH.unlink() + + async def _reset_db(self): + db = self.app.make("db") + await db.clear() + schema = db.get_schema_builder() + for table in await schema.get_all_tables(): + await schema.drop_table_if_exists(table) + + async def _migrate(self): + db = self.app.make("db") + await db.clear() + migrations_dir = self.app.use_base_path("databases/migrations") + migrator = Migrator( + connection="sqlite", + migration_directory=migrations_dir, + ) + await migrator.create_table_if_not_exists() + await migrator.migrate() + + def _make_command(self, command_class): + cmd = command_class() + cmd.set_container(self.app) + return cmd + + def test_migrate_runs_pending_migrations(self): + cmd = self._make_command(DBMigrateCommand) + tester = CommandTester(cmd) + tester.execute("--connection sqlite") + output = tester.io.fetch_output() + self.assertIn("Migrated:", output) + self.assertIn("create_posts_table", output) + + def test_migrate_reports_nothing_to_migrate(self): + cmd = self._make_command(DBMigrateCommand) + tester = CommandTester(cmd) + tester.execute("--connection sqlite") + tester.io.fetch_output() + + tester.execute("--connection sqlite") + output = tester.io.fetch_output() + self.assertIn("Nothing To Migrate!", output) + + def test_status_shows_unran_migrations(self): + cmd = self._make_command(MigrateStatusCommand) + tester = CommandTester(cmd) + tester.execute("--connection sqlite") + output = tester.io.fetch_output() + self.assertIn("create_posts_table", output) + self.assertIn("N", output) + + def test_status_shows_ran_migrations(self): + asyncio.run(self._migrate()) + + cmd = self._make_command(MigrateStatusCommand) + tester = CommandTester(cmd) + tester.execute("--connection sqlite") + output = tester.io.fetch_output() + self.assertIn("create_posts_table", output) + self.assertIn("Y", output) + + def test_rollback_rolls_back_last_batch(self): + asyncio.run(self._migrate()) + + cmd = self._make_command(MigrateRollbackCommand) + tester = CommandTester(cmd) + tester.execute("--connection sqlite") + output = tester.io.fetch_output() + self.assertIn("Rolled back:", output) + self.assertIn("create_posts_table", output) + + def test_reset_rolls_back_all_migrations(self): + asyncio.run(self._migrate()) + + cmd = self._make_command(MigrateResetCommand) + tester = CommandTester(cmd) + tester.execute("--connection sqlite") + output = tester.io.fetch_output() + self.assertIn("Rolled back:", output) + self.assertIn("create_posts_table", output) + + def test_fresh_drops_all_tables_and_remigrates(self): + asyncio.run(self._migrate()) + + cmd = self._make_command(MigrateFreshCommand) + tester = CommandTester(cmd) + tester.execute("--connection sqlite") + output = tester.io.fetch_output() + self.assertIn("Dropping all tables", output) + self.assertIn("Migrated:", output) + self.assertIn("create_posts_table", output) diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/fixtures/db.py b/fastapi_startkit/tests/masoniteorm/sqlite/fixtures/db.py index 25a88f94..d71315df 100644 --- a/fastapi_startkit/tests/masoniteorm/sqlite/fixtures/db.py +++ b/fastapi_startkit/tests/masoniteorm/sqlite/fixtures/db.py @@ -6,6 +6,7 @@ ConnectionFactory(), { "default": "sqlite", + "migrations": "fixtures/migrations", "connections": { "sqlite": { "driver": "sqlite", diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/fixtures/migrations/2026_01_01_000000_create_posts_table.py b/fastapi_startkit/tests/masoniteorm/sqlite/fixtures/migrations/2026_01_01_000000_create_posts_table.py new file mode 100644 index 00000000..9964a31b --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/sqlite/fixtures/migrations/2026_01_01_000000_create_posts_table.py @@ -0,0 +1,12 @@ +from fastapi_startkit.masoniteorm.migrations import Migration + + +class CreatePostsTable(Migration): + async def up(self): + async with await self.schema.create("posts") as table: + table.id() + table.string("title") + table.timestamps() + + async def down(self): + await self.schema.drop("posts") \ No newline at end of file diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/fixtures/migrations/__init__.py b/fastapi_startkit/tests/masoniteorm/sqlite/fixtures/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/migrations/test_sqlite_migration.py b/fastapi_startkit/tests/masoniteorm/sqlite/migrations/test_sqlite_migration.py new file mode 100644 index 00000000..f7adfd65 --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/sqlite/migrations/test_sqlite_migration.py @@ -0,0 +1,43 @@ +from unittest import IsolatedAsyncioTestCase + +from fastapi_startkit.masoniteorm.migrations.Migration import Migration +from fastapi_startkit.masoniteorm.models.MigrationModel import MigrationModel + +from tests.masoniteorm.sqlite.fixtures.db import DB + + +class TestMigration(IsolatedAsyncioTestCase): + async def asyncSetUp(self): + Migration.db_manager = DB + MigrationModel.db_manager = DB + self.migration = Migration(connection="sqlite") + self.schema = DB.get_schema_builder() + await self.migration.fresh() + + async def asyncTearDown(self): + await DB.clear() + + async def test_fresh_creates_migrations_table(self): + tables = await self.schema.get_all_tables() + self.assertIn("migrations", tables) + + async def test_fresh_runs_all_migrations(self): + tables = await self.schema.get_all_tables() + self.assertIn("posts", tables) + + async def test_migrations_table_primary_key_is_id(self): + record = await MigrationModel.create( + {"migration": "2026_01_01_000000_create_posts_table", "batch": 1} + ) + + self.assertIsNotNone(record.id) + self.assertIsInstance(record.id, int) + + async def test_migration_record_is_fetchable_by_id(self): + record = await MigrationModel.where( + "migration", "2026_01_01_000000_create_posts_table" + ).first() + + self.assertIsNotNone(record) + self.assertEqual(record.batch, 1) + self.assertIsNotNone(record.id) 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 3bbc95cd..8ae7ca76 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 @@ -1,5 +1,8 @@ from unittest.mock import AsyncMock, MagicMock +from fastapi_startkit.masoniteorm.migrations.Migration import Migration +from fastapi_startkit.masoniteorm.models.MigrationModel import MigrationModel + from ..test_case import TestCase diff --git a/fastapi_startkit/uv.lock b/fastapi_startkit/uv.lock index 49ee40c0..2065bd39 100644 --- a/fastapi_startkit/uv.lock +++ b/fastapi_startkit/uv.lock @@ -443,7 +443,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.20.0" +version = "0.21.0" source = { editable = "." } dependencies = [ { name = "cleo" }, diff --git a/mysql-init.sql b/mysql-init.sql new file mode 100644 index 00000000..b7f4c42b --- /dev/null +++ b/mysql-init.sql @@ -0,0 +1,3 @@ +CREATE DATABASE IF NOT EXISTS database_app; +GRANT ALL PRIVILEGES ON database_app.* TO 'app'@'%'; +FLUSH PRIVILEGES; \ No newline at end of file diff --git a/taskiq_poc.py b/taskiq_poc.py new file mode 100644 index 00000000..9e4b7c0e --- /dev/null +++ b/taskiq_poc.py @@ -0,0 +1,142 @@ +import asyncio +import json +from datetime import datetime, timedelta +from typing import Any, List, Optional, Union, Type, TypeVar +from taskiq import AsyncBroker, InMemoryBroker, TaskiqResult + +# --- Mocking external dependencies for the PoC --- + +class NowMock: + """Mock for pendulum.now() or similar""" + def __init__(self, dt: datetime = None): + self.dt = dt or datetime.now() + + def add(self, minutes: int = 0, seconds: int = 0) -> datetime: + return self.dt + timedelta(minutes=minutes, seconds=seconds) + +def now(): + return NowMock() + +# --- The Core Logic --- + +T = TypeVar("T", bound="Queue") + +class Queue: + """ + Base class for Taskiq tasks with a class-based syntax. + """ + queue: str = 'default' + timeout: int = 60 + max_exceptions: int = 3 + tries: int | None = None + rate_limit: str | None = None + backoff: int | list[int] = 5 + + _broker: AsyncBroker = None + _taskiq_task = None + + + @classmethod + def bind(cls, broker: AsyncBroker): + """ + Registers this class as a task in the given broker. + """ + cls._broker = broker + + async def _task_runner(*args, **kwargs): + # 1. Instantiate the class on the worker side + instance = cls(*args, **kwargs) + # 2. Execute the handle method + return await instance.handle() + + # Register the task with proper labels so taskiq respects them + # This ensures that 'timeout', 'max_retry', etc. are actually used by the broker. + cls._taskiq_task = broker.register_task( + _task_runner, + task_name=f"{cls.__module__}.{cls.__name__}", + queue_name=cls.queue, + ).with_labels( + timeout=cls.timeout, + max_retry=cls.max_exceptions, + backoff=cls.backoff, + rate_limit=cls.rate_limit, + ) + + print(f"Registered task: {cls.__name__} on queue: {cls.queue}") + + @classmethod + async def dispatch(cls, *args, **kwargs): + """ + Dispatches the task to the broker using the class itself. + """ + if not cls._taskiq_task: + raise RuntimeError( + f"Task {cls.__name__} is not bound to a broker. " + "Call ClassifyTask.bind(broker) first." + ) + + print(f"Dispatching {cls.__name__} with args: {args} {kwargs}") + + # Send the arguments directly to the taskiq task + return await cls._taskiq_task.kiq(*args, **kwargs) + + async def handle(self): + """ + The actual logic to be executed by the worker. + """ + raise NotImplementedError("Subclasses must implement the handle() method.") + + +# --- The Requested POC Implementation --- + +class ClassifyTask(Queue): + queue: str = 'default' + timeout: int = 60 # Default timeout in seconds + max_exceptions: int = 3 # Maximum number of exceptions before task is marked as failed + + tries: int | None = None + rate_limit: str | None = '20/m' + backoff: int | list[int] = 5 + + @property + def retry_until(self): + return now().add(minutes=1) + + def __init__(self, user_id: int): + self.user_id = user_id + + async def handle(self): + print(f"[Worker] Classifying data for user_id: {self.user_id}") + print(f"[Worker] Task settings: queue={self.queue}, timeout={self.timeout}") + # Simulate some work + await asyncio.sleep(0.1) + print(f"[Worker] Finished processing for user_id: {self.user_id}") + + +# --- Running the POC --- + +async def main(): + # 1. Setup a broker (using InMemoryBroker for testing) + broker = InMemoryBroker() + + # 2. Bind the task class to the broker + # In a real app, you might automate this for all Queue subclasses + ClassifyTask.bind(broker) + + # 3. Startup the broker (simulating worker/client initialization) + await broker.startup() + + # 4. Dispatch a task + # This now uses the class method directly + handle = await ClassifyTask.dispatch(user_id=42) + + print(f"Task dispatched! Task ID: {handle.task_id}") + + # 5. Wait for the result (since it's an InMemoryBroker, it runs immediately) + result = await handle.wait_result(timeout=2) + print(f"Task result: {result.return_value}") + + await broker.shutdown() + +if __name__ == "__main__": + asyncio.run(main()) From ca4635e7a446c31b64f331bbad244ced78d5cda4 Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Fri, 15 May 2026 14:55:22 -0700 Subject: [PATCH 2/2] feat: fix --- example/config-app/uv.lock | 2 +- .../commands/MigrateRefreshCommand.py | 27 ++++++------ .../masoniteorm/migrations/Migration.py | 2 +- .../masoniteorm/migrations/Migrator.py | 2 - .../masoniteorm/testing/transaction.py | 7 ++- .../migrations/test_sqlite_migration.py | 43 ------------------- 6 files changed, 21 insertions(+), 62 deletions(-) delete mode 100644 fastapi_startkit/tests/masoniteorm/sqlite/migrations/test_sqlite_migration.py diff --git a/example/config-app/uv.lock b/example/config-app/uv.lock index 9cdb486a..e2c55a3d 100644 --- a/example/config-app/uv.lock +++ b/example/config-app/uv.lock @@ -162,7 +162,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.20.0" +version = "0.21.0" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateRefreshCommand.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateRefreshCommand.py index 1885e04a..fcfd369f 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateRefreshCommand.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateRefreshCommand.py @@ -1,9 +1,10 @@ from cleo.helpers import option from fastapi_startkit.console import Command +from fastapi_startkit.masoniteorm.migrations.Migrator import Migrator class MigrateRefreshCommand(Command): - name = "migrate:refresh" + name = "db:migrate:refresh" description = "Refresh migrations." options = [ @@ -21,13 +22,6 @@ class MigrateRefreshCommand(Command): default="default", description="The connection you want to run migrations on", ), - option( - "schema", - None, - flag=False, - default=None, - description="Sets the schema to be migrated", - ), option( "directory", "d", @@ -57,14 +51,12 @@ def handle(self): return asyncio.run(self.handle_async()) async def handle_async(self): - from ..migrations import Migration + directory = self.resolve_migration_path() - migration = Migration( + migration = Migrator( command_class=self, connection=self.option("connection"), - migration_directory=self.option("directory"), - config_path=self.option("config"), - schema=self.option("schema"), + migration_directory=directory, ) await migration.refresh(self.option("migration")) @@ -79,3 +71,12 @@ async def handle_async(self): "seed:run", f"{self.option('seed')} --directory {self.option('seed-directory')} --connection {self.option('connection')}", ) + + def resolve_migration_path(self) -> str: + path = self.option('directory') + + config = self.container.make('config').get('database.migrations') + default_directory = config.get('directory') + + migration_directory = path or default_directory + return self.container.use_base_path(migration_directory) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migration.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migration.py index e63605bc..f2a8208b 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migration.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migration.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from fastapi_startkit.masoniteorm import DatabaseManager + pass class Migration: diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migrator.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migrator.py index 2d87f6f3..37870bc1 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migrator.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/migrations/Migrator.py @@ -1,5 +1,3 @@ -import os -from dumpdie import dd from os import listdir from os.path import isfile, join from pydoc import locate diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/testing/transaction.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/testing/transaction.py index 54308a68..cb275e3f 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/testing/transaction.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/testing/transaction.py @@ -19,7 +19,10 @@ async def asyncStartTestRun(self): @staticmethod async def migrate_database(): if not RefreshDatabase.migrated: - from fastapi_startkit.masoniteorm.migrations import Migration + from fastapi_startkit.masoniteorm.migrations import Migrator + from fastapi_startkit.application import app as get_app - await Migration(migration_directory="databases/migrations").fresh(ignore_fk=True) + migration_dir = get_app().use_base_path("databases/migrations") + migrator = Migrator(migration_directory=migration_dir) + await migrator.fresh(ignore_fk=True) RefreshDatabase.migrated = True diff --git a/fastapi_startkit/tests/masoniteorm/sqlite/migrations/test_sqlite_migration.py b/fastapi_startkit/tests/masoniteorm/sqlite/migrations/test_sqlite_migration.py deleted file mode 100644 index f7adfd65..00000000 --- a/fastapi_startkit/tests/masoniteorm/sqlite/migrations/test_sqlite_migration.py +++ /dev/null @@ -1,43 +0,0 @@ -from unittest import IsolatedAsyncioTestCase - -from fastapi_startkit.masoniteorm.migrations.Migration import Migration -from fastapi_startkit.masoniteorm.models.MigrationModel import MigrationModel - -from tests.masoniteorm.sqlite.fixtures.db import DB - - -class TestMigration(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - Migration.db_manager = DB - MigrationModel.db_manager = DB - self.migration = Migration(connection="sqlite") - self.schema = DB.get_schema_builder() - await self.migration.fresh() - - async def asyncTearDown(self): - await DB.clear() - - async def test_fresh_creates_migrations_table(self): - tables = await self.schema.get_all_tables() - self.assertIn("migrations", tables) - - async def test_fresh_runs_all_migrations(self): - tables = await self.schema.get_all_tables() - self.assertIn("posts", tables) - - async def test_migrations_table_primary_key_is_id(self): - record = await MigrationModel.create( - {"migration": "2026_01_01_000000_create_posts_table", "batch": 1} - ) - - self.assertIsNotNone(record.id) - self.assertIsInstance(record.id, int) - - async def test_migration_record_is_fetchable_by_id(self): - record = await MigrationModel.where( - "migration", "2026_01_01_000000_create_posts_table" - ).first() - - self.assertIsNotNone(record) - self.assertEqual(record.batch, 1) - self.assertIsNotNone(record.id)