From 8843fe6f58ca637dd33f13c8e96feb5914a64abe Mon Sep 17 00:00:00 2001 From: euri10 Date: Tue, 18 Nov 2025 07:12:12 +0100 Subject: [PATCH 1/7] tests exactly like code-blocks --- docs/examples/usage/usage_migrations_1.py | 24 ++++ docs/examples/usage/usage_migrations_2.py | 49 ++++++++ docs/examples/usage/usage_migrations_3.py | 31 +++++ docs/examples/usage/usage_migrations_4.py | 23 ++++ docs/examples/usage/usage_migrations_5.py | 26 ++++ docs/examples/usage/usage_migrations_6.py | 37 ++++++ docs/examples/usage/usage_migrations_7.py | 21 ++++ docs/examples/usage/usage_migrations_8.py | 33 +++++ docs/usage/migrations.rst | 141 ++++++++++------------ 9 files changed, 307 insertions(+), 78 deletions(-) create mode 100644 docs/examples/usage/usage_migrations_1.py create mode 100644 docs/examples/usage/usage_migrations_2.py create mode 100644 docs/examples/usage/usage_migrations_3.py create mode 100644 docs/examples/usage/usage_migrations_4.py create mode 100644 docs/examples/usage/usage_migrations_5.py create mode 100644 docs/examples/usage/usage_migrations_6.py create mode 100644 docs/examples/usage/usage_migrations_7.py create mode 100644 docs/examples/usage/usage_migrations_8.py diff --git a/docs/examples/usage/usage_migrations_1.py b/docs/examples/usage/usage_migrations_1.py new file mode 100644 index 00000000..67d5e5ca --- /dev/null +++ b/docs/examples/usage/usage_migrations_1.py @@ -0,0 +1,24 @@ +# start-example +from sqlspec.adapters.asyncpg import AsyncpgConfig + +__all__ = ("test_async_methods",) + + +config = AsyncpgConfig( + pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, + migration_config={"enabled": True, "script_location": "migrations"}, +) +# end-example + + +def test_async_methods() -> None: + # These are just smoke tests for method presence, not actual DB calls + assert hasattr(config, "migrate_up") + assert hasattr(config, "upgrade") + assert hasattr(config, "migrate_down") + assert hasattr(config, "downgrade") + assert hasattr(config, "get_current_migration") + assert hasattr(config, "create_migration") + assert hasattr(config, "init_migrations") + assert hasattr(config, "stamp_migration") + assert hasattr(config, "fix_migrations") diff --git a/docs/examples/usage/usage_migrations_2.py b/docs/examples/usage/usage_migrations_2.py new file mode 100644 index 00000000..045f3b47 --- /dev/null +++ b/docs/examples/usage/usage_migrations_2.py @@ -0,0 +1,49 @@ +# start-example +from sqlspec.adapters.sqlite import SqliteConfig + +__all__ = ("test_sync_methods",) + + +config = SqliteConfig( + pool_config={"database": "myapp.db"}, migration_config={"enabled": True, "script_location": "migrations"} +) + +# Apply migrations (no await needed) +config.migrate_up("head") +# Or use the alias +config.upgrade("head") + +# Rollback one revision +config.migrate_down("-1") +# Or use the alias +config.downgrade("-1") + +# Check current version +current = config.get_current_migration(verbose=True) +print(current) + +# Create new migration +config.create_migration("add users table", file_type="sql") + +# Initialize migrations directory +config.init_migrations() + +# Stamp database to specific revision +config.stamp_migration("0003") + +# Convert timestamp to sequential migrations +config.fix_migrations(dry_run=False, update_database=True, yes=True) +# end-example + + +def test_sync_methods() -> None: + # Smoke tests for method presence, not actual DB calls + assert hasattr(config, "migrate_up") + assert hasattr(config, "upgrade") + assert hasattr(config, "migrate_down") + assert hasattr(config, "downgrade") + assert hasattr(config, "get_current_migration") + assert hasattr(config, "create_migration") + assert hasattr(config, "init_migrations") + assert hasattr(config, "stamp_migration") + assert hasattr(config, "fix_migrations") diff --git a/docs/examples/usage/usage_migrations_3.py b/docs/examples/usage/usage_migrations_3.py new file mode 100644 index 00000000..0a0338f1 --- /dev/null +++ b/docs/examples/usage/usage_migrations_3.py @@ -0,0 +1,31 @@ +# start-example +__all__ = ("test_template_config",) + + +migration_config = { + "default_format": "py", # CLI default when --format omitted + "title": "Acme Migration", # Shared title for all templates + "author": "env:SQLSPEC_AUTHOR", # Read from environment variable + "templates": { + "sql": { + "header": "-- {title} - {message}", + "metadata": ["-- Version: {version}", "-- Owner: {author}"], + "body": "-- custom SQL body", + }, + "py": { + "docstring": """{title}\nDescription: {description}""", + "imports": ["from typing import Iterable"], + "body": """def up(context: object | None = None) -> str | Iterable[str]:\n return \"SELECT 1\"\n\ndef down(context: object | None = None) -> str | Iterable[str]:\n return \"DROP TABLE example;\"\n""", + }, + }, +} +# end-example + + +def test_template_config() -> None: + # Check structure of migration_config + assert migration_config["default_format"] == "py" + assert "py" in migration_config["templates"] + assert "sql" in migration_config["templates"] + assert isinstance(migration_config["templates"]["py"], dict) + assert isinstance(migration_config["templates"]["sql"], dict) diff --git a/docs/examples/usage/usage_migrations_4.py b/docs/examples/usage/usage_migrations_4.py new file mode 100644 index 00000000..73b11cde --- /dev/null +++ b/docs/examples/usage/usage_migrations_4.py @@ -0,0 +1,23 @@ +__all__ = ("test_async_command_class_methods",) + + +async def test_async_command_class_methods() -> None: + + # start-example + from sqlspec.adapters.asyncpg import AsyncpgConfig + from sqlspec.migrations.commands import AsyncMigrationCommands + + config = AsyncpgConfig(pool_config={"dsn": "postgresql://..."}, migration_config={"script_location": "migrations"}) + + # Create commands instance + commands = AsyncMigrationCommands(config) + + # Use commands directly + await commands.upgrade("head") + # end-example + + # Smoke test for AsyncMigrationCommands method presence + commands = AsyncMigrationCommands(config) + assert hasattr(commands, "upgrade") + assert hasattr(commands, "downgrade") + assert hasattr(commands, "get_current_migration") diff --git a/docs/examples/usage/usage_migrations_5.py b/docs/examples/usage/usage_migrations_5.py new file mode 100644 index 00000000..583e580d --- /dev/null +++ b/docs/examples/usage/usage_migrations_5.py @@ -0,0 +1,26 @@ +# start-example +from sqlspec.adapters.asyncpg import AsyncpgConfig + +__all__ = ("test_config_structure",) + + +config = AsyncpgConfig( + pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, + migration_config={ + "enabled": True, + "script_location": "migrations", + "version_table_name": "ddl_migrations", + "auto_sync": True, # Enable automatic version reconciliation + }, +) +# end-example + + +def test_config_structure() -> None: + # Check config attributes + assert hasattr(config, "pool_config") + assert hasattr(config, "migration_config") + assert config.migration_config["enabled"] is True + assert config.migration_config["script_location"] == "migrations" + assert config.migration_config["version_table_name"] == "ddl_migrations" + assert config.migration_config["auto_sync"] is True diff --git a/docs/examples/usage/usage_migrations_6.py b/docs/examples/usage/usage_migrations_6.py new file mode 100644 index 00000000..f326a6c1 --- /dev/null +++ b/docs/examples/usage/usage_migrations_6.py @@ -0,0 +1,37 @@ +# start-example +# migrations/0002_add_user_roles.py +"""Add user roles table + +Revision ID: 0002_add_user_roles +Created at: 2025-10-18 12:00:00 +""" + +__all__ = ("downgrade", "test_upgrade_and_downgrade_strings", "upgrade") + + +def upgrade() -> str: + """Apply migration.""" + return """ + CREATE TABLE user_roles ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + role VARCHAR(50) NOT NULL + ); + """ + + +def downgrade() -> str: + """Revert migration.""" + return """ + DROP TABLE user_roles; + """ + + +# end-example + + +def test_upgrade_and_downgrade_strings() -> None: + up_sql = upgrade() + down_sql = downgrade() + assert "CREATE TABLE user_roles" in up_sql + assert "DROP TABLE user_roles" in down_sql diff --git a/docs/examples/usage/usage_migrations_7.py b/docs/examples/usage/usage_migrations_7.py new file mode 100644 index 00000000..12f33dc6 --- /dev/null +++ b/docs/examples/usage/usage_migrations_7.py @@ -0,0 +1,21 @@ +# start-example +__all__ = ("test_upgrade_returns_list", "upgrade") + + +def upgrade() -> list[str]: + """Apply migration in multiple steps.""" + return [ + "CREATE TABLE products (id SERIAL PRIMARY KEY);", + "CREATE TABLE orders (id SERIAL PRIMARY KEY, product_id INTEGER);", + "CREATE INDEX idx_orders_product ON orders(product_id);", + ] + + +# end-example + + +def test_upgrade_returns_list() -> None: + stmts = upgrade() + assert isinstance(stmts, list) + assert any("products" in s for s in stmts) + assert any("orders" in s for s in stmts) diff --git a/docs/examples/usage/usage_migrations_8.py b/docs/examples/usage/usage_migrations_8.py new file mode 100644 index 00000000..e2059849 --- /dev/null +++ b/docs/examples/usage/usage_migrations_8.py @@ -0,0 +1,33 @@ +__all__ = ("test_tracker_instance",) + + +from pytest_databases.docker.postgres import PostgresService + + +async def test_tracker_instance(postgres_service: PostgresService) -> None: + + # start-example + from sqlspec.adapters.asyncpg import AsyncpgConfig + from sqlspec.migrations.tracker import AsyncMigrationTracker + + tracker = AsyncMigrationTracker() + + config = AsyncpgConfig( + pool_config={ + "dsn": f"postgres://{postgres_service.user}:{postgres_service.password}@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}" + }, + migration_config={ + "enabled": True, + "script_location": "migrations", + "version_table_name": "ddl_migrations", + "auto_sync": True, # Enable automatic version reconciliation + }, + ) + async with config.provide_session() as session: + driver = session._driver + + # Update version record + await tracker.update_version_record(driver, old_version="20251018120000", new_version="0003") + # end-example + # Just check that tracker is an instance of AsyncMigrationTracker + assert isinstance(tracker, AsyncMigrationTracker) diff --git a/docs/usage/migrations.rst b/docs/usage/migrations.rst index 17811446..39dc1b45 100644 --- a/docs/usage/migrations.rst +++ b/docs/usage/migrations.rst @@ -40,86 +40,28 @@ Async Adapters For async adapters (AsyncPG, Asyncmy, Aiosqlite, Psqlpy), migration methods return awaitables: -.. code-block:: python - - from sqlspec.adapters.asyncpg import AsyncpgConfig - - config = AsyncpgConfig( - pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, - migration_config={ - "enabled": True, - "script_location": "migrations", - } - ) - - # Apply migrations - await config.migrate_up("head") - # Or use the alias - await config.upgrade("head") - - # Rollback one revision - await config.migrate_down("-1") - # Or use the alias - await config.downgrade("-1") - - # Check current version - current = await config.get_current_migration(verbose=True) - print(current) +.. literalinclude:: ../examples/usage/usage_migrations_1.py + :language: python + :caption: `Async Adapters` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example - # Create new migration - await config.create_migration("add users table", file_type="sql") - # Initialize migrations directory - await config.init_migrations() - - # Stamp database to specific revision - await config.stamp_migration("0003") - - # Convert timestamp to sequential migrations - await config.fix_migrations(dry_run=False, update_database=True, yes=True) Sync Adapters ------------- For sync adapters (SQLite, DuckDB), migration methods execute immediately without await: -.. code-block:: python - - from sqlspec.adapters.sqlite import SqliteConfig +.. literalinclude:: ../examples/usage/usage_migrations_2.py + :language: python + :caption: `Sync Adapters` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example - config = SqliteConfig( - pool_config={"database": "myapp.db"}, - migration_config={ - "enabled": True, - "script_location": "migrations", - } - ) - # Apply migrations (no await needed) - config.migrate_up("head") - # Or use the alias - config.upgrade("head") - - # Rollback one revision - config.migrate_down("-1") - # Or use the alias - config.downgrade("-1") - - # Check current version - current = config.get_current_migration(verbose=True) - print(current) - - # Create new migration - config.create_migration("add users table", file_type="sql") - - # Initialize migrations directory - config.init_migrations() - - # Stamp database to specific revision - config.stamp_migration("0003") - - # Convert timestamp to sequential migrations - config.fix_migrations(dry_run=False, update_database=True, yes=True) Available Methods ----------------- @@ -171,8 +113,17 @@ def down(context: object | None = None) -> str | Iterable[str]: } } +.. literalinclude:: ../examples/usage/usage_migrations_3.py + :language: python + :caption: `Template Profiles & Author Metadata` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example + + Template fragments accept the following variables: + - ``{title}`` – shared template title - ``{version}`` – generated revision identifier - ``{message}`` – CLI/command message @@ -241,14 +192,32 @@ For advanced use cases requiring custom logic, you can still use command classes migration_config={"script_location": "migrations"} ) +.. literalinclude:: ../examples/usage/usage_migrations_5.py + :language: python + :caption: `Configuration` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example + + + # Create commands instance commands = AsyncMigrationCommands(config) # Use commands directly await commands.upgrade("head") +.. literalinclude:: ../examples/usage/usage_migrations_4.py + :language: python + :caption: `Command Classes (Advanced)` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example + + This approach is useful when: + - Building custom migration runners - Implementing migration lifecycle hooks - Integrating with third-party workflow tools @@ -370,19 +339,26 @@ Python migrations provide more flexibility for complex operations: DROP TABLE user_roles; """ +.. literalinclude:: ../examples/usage/usage_migrations_6.py + :language: python + :caption: `Python Migrations` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example + + **Advanced Usage:** Python migrations can also return a list of SQL statements: -.. code-block:: python +.. literalinclude:: ../examples/usage/usage_migrations_7.py + :language: python + :caption: `Advanced Usage` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example + - def upgrade(): - """Apply migration in multiple steps.""" - return [ - "CREATE TABLE products (id SERIAL PRIMARY KEY);", - "CREATE TABLE orders (id SERIAL PRIMARY KEY, product_id INTEGER);", - "CREATE INDEX idx_orders_product ON orders(product_id);", - ] .. _hybrid-versioning-guide: @@ -666,6 +642,15 @@ If auto-sync is disabled, manually reconcile renamed migrations: new_version="0003" ) +.. literalinclude:: ../examples/usage/usage_migrations_8.py + :language: python + :caption: `Manual Version Reconciliation` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example + + + Troubleshooting =============== From 635301628a24ff218f0a2ae0ddc7675fb41b7042 Mon Sep 17 00:00:00 2001 From: euri10 Date: Tue, 18 Nov 2025 07:18:52 +0100 Subject: [PATCH 2/7] test 1 --- docs/examples/usage/usage_migrations_1.py | 30 ++++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/examples/usage/usage_migrations_1.py b/docs/examples/usage/usage_migrations_1.py index 67d5e5ca..eb68185b 100644 --- a/docs/examples/usage/usage_migrations_1.py +++ b/docs/examples/usage/usage_migrations_1.py @@ -1,17 +1,29 @@ -# start-example -from sqlspec.adapters.asyncpg import AsyncpgConfig - __all__ = ("test_async_methods",) -config = AsyncpgConfig( - pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, - migration_config={"enabled": True, "script_location": "migrations"}, -) -# end-example +async def test_async_methods() -> None: + # start-example + from sqlspec.adapters.asyncpg import AsyncpgConfig + + config = AsyncpgConfig( + pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, + migration_config={"enabled": True, "script_location": "migrations"}, + ) + + # Apply migrations + await config.migrate_up("head") + # Or use the alias + await config.upgrade("head") + # Rollback one revision + await config.migrate_down("-1") + # Or use the alias + await config.downgrade("-1") -def test_async_methods() -> None: + # Check current version + current = await config.get_current_migration(verbose=True) + print(current) + # end-example # These are just smoke tests for method presence, not actual DB calls assert hasattr(config, "migrate_up") assert hasattr(config, "upgrade") From e6a62cf0f7cc21c0d9ad3133d242f98cc0efb390 Mon Sep 17 00:00:00 2001 From: euri10 Date: Tue, 18 Nov 2025 07:22:49 +0100 Subject: [PATCH 3/7] add service --- docs/examples/usage/usage_migrations_1.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/examples/usage/usage_migrations_1.py b/docs/examples/usage/usage_migrations_1.py index eb68185b..5b04de22 100644 --- a/docs/examples/usage/usage_migrations_1.py +++ b/docs/examples/usage/usage_migrations_1.py @@ -1,13 +1,19 @@ __all__ = ("test_async_methods",) -async def test_async_methods() -> None: +from pytest_databases.docker.postgres import PostgresService + + +async def test_async_methods(postgres_service: PostgresService) -> None: # start-example from sqlspec.adapters.asyncpg import AsyncpgConfig + dsn = ( + f"postgresql://{postgres_service.user}:{postgres_service.password}" + f"@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}" + ) config = AsyncpgConfig( - pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, - migration_config={"enabled": True, "script_location": "migrations"}, + pool_config={"dsn": dsn}, migration_config={"enabled": True, "script_location": "migrations"} ) # Apply migrations From 25accbffffe12e4b97a6687047e0d494039f742b Mon Sep 17 00:00:00 2001 From: euri10 Date: Tue, 18 Nov 2025 07:12:12 +0100 Subject: [PATCH 4/7] tests exactly like code-blocks --- docs/examples/usage/usage_migrations_1.py | 24 ++++ docs/examples/usage/usage_migrations_2.py | 49 ++++++++ docs/examples/usage/usage_migrations_3.py | 31 +++++ docs/examples/usage/usage_migrations_4.py | 23 ++++ docs/examples/usage/usage_migrations_5.py | 26 ++++ docs/examples/usage/usage_migrations_6.py | 37 ++++++ docs/examples/usage/usage_migrations_7.py | 21 ++++ docs/examples/usage/usage_migrations_8.py | 33 +++++ docs/usage/migrations.rst | 141 ++++++++++------------ 9 files changed, 307 insertions(+), 78 deletions(-) create mode 100644 docs/examples/usage/usage_migrations_1.py create mode 100644 docs/examples/usage/usage_migrations_2.py create mode 100644 docs/examples/usage/usage_migrations_3.py create mode 100644 docs/examples/usage/usage_migrations_4.py create mode 100644 docs/examples/usage/usage_migrations_5.py create mode 100644 docs/examples/usage/usage_migrations_6.py create mode 100644 docs/examples/usage/usage_migrations_7.py create mode 100644 docs/examples/usage/usage_migrations_8.py diff --git a/docs/examples/usage/usage_migrations_1.py b/docs/examples/usage/usage_migrations_1.py new file mode 100644 index 00000000..67d5e5ca --- /dev/null +++ b/docs/examples/usage/usage_migrations_1.py @@ -0,0 +1,24 @@ +# start-example +from sqlspec.adapters.asyncpg import AsyncpgConfig + +__all__ = ("test_async_methods",) + + +config = AsyncpgConfig( + pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, + migration_config={"enabled": True, "script_location": "migrations"}, +) +# end-example + + +def test_async_methods() -> None: + # These are just smoke tests for method presence, not actual DB calls + assert hasattr(config, "migrate_up") + assert hasattr(config, "upgrade") + assert hasattr(config, "migrate_down") + assert hasattr(config, "downgrade") + assert hasattr(config, "get_current_migration") + assert hasattr(config, "create_migration") + assert hasattr(config, "init_migrations") + assert hasattr(config, "stamp_migration") + assert hasattr(config, "fix_migrations") diff --git a/docs/examples/usage/usage_migrations_2.py b/docs/examples/usage/usage_migrations_2.py new file mode 100644 index 00000000..045f3b47 --- /dev/null +++ b/docs/examples/usage/usage_migrations_2.py @@ -0,0 +1,49 @@ +# start-example +from sqlspec.adapters.sqlite import SqliteConfig + +__all__ = ("test_sync_methods",) + + +config = SqliteConfig( + pool_config={"database": "myapp.db"}, migration_config={"enabled": True, "script_location": "migrations"} +) + +# Apply migrations (no await needed) +config.migrate_up("head") +# Or use the alias +config.upgrade("head") + +# Rollback one revision +config.migrate_down("-1") +# Or use the alias +config.downgrade("-1") + +# Check current version +current = config.get_current_migration(verbose=True) +print(current) + +# Create new migration +config.create_migration("add users table", file_type="sql") + +# Initialize migrations directory +config.init_migrations() + +# Stamp database to specific revision +config.stamp_migration("0003") + +# Convert timestamp to sequential migrations +config.fix_migrations(dry_run=False, update_database=True, yes=True) +# end-example + + +def test_sync_methods() -> None: + # Smoke tests for method presence, not actual DB calls + assert hasattr(config, "migrate_up") + assert hasattr(config, "upgrade") + assert hasattr(config, "migrate_down") + assert hasattr(config, "downgrade") + assert hasattr(config, "get_current_migration") + assert hasattr(config, "create_migration") + assert hasattr(config, "init_migrations") + assert hasattr(config, "stamp_migration") + assert hasattr(config, "fix_migrations") diff --git a/docs/examples/usage/usage_migrations_3.py b/docs/examples/usage/usage_migrations_3.py new file mode 100644 index 00000000..0a0338f1 --- /dev/null +++ b/docs/examples/usage/usage_migrations_3.py @@ -0,0 +1,31 @@ +# start-example +__all__ = ("test_template_config",) + + +migration_config = { + "default_format": "py", # CLI default when --format omitted + "title": "Acme Migration", # Shared title for all templates + "author": "env:SQLSPEC_AUTHOR", # Read from environment variable + "templates": { + "sql": { + "header": "-- {title} - {message}", + "metadata": ["-- Version: {version}", "-- Owner: {author}"], + "body": "-- custom SQL body", + }, + "py": { + "docstring": """{title}\nDescription: {description}""", + "imports": ["from typing import Iterable"], + "body": """def up(context: object | None = None) -> str | Iterable[str]:\n return \"SELECT 1\"\n\ndef down(context: object | None = None) -> str | Iterable[str]:\n return \"DROP TABLE example;\"\n""", + }, + }, +} +# end-example + + +def test_template_config() -> None: + # Check structure of migration_config + assert migration_config["default_format"] == "py" + assert "py" in migration_config["templates"] + assert "sql" in migration_config["templates"] + assert isinstance(migration_config["templates"]["py"], dict) + assert isinstance(migration_config["templates"]["sql"], dict) diff --git a/docs/examples/usage/usage_migrations_4.py b/docs/examples/usage/usage_migrations_4.py new file mode 100644 index 00000000..73b11cde --- /dev/null +++ b/docs/examples/usage/usage_migrations_4.py @@ -0,0 +1,23 @@ +__all__ = ("test_async_command_class_methods",) + + +async def test_async_command_class_methods() -> None: + + # start-example + from sqlspec.adapters.asyncpg import AsyncpgConfig + from sqlspec.migrations.commands import AsyncMigrationCommands + + config = AsyncpgConfig(pool_config={"dsn": "postgresql://..."}, migration_config={"script_location": "migrations"}) + + # Create commands instance + commands = AsyncMigrationCommands(config) + + # Use commands directly + await commands.upgrade("head") + # end-example + + # Smoke test for AsyncMigrationCommands method presence + commands = AsyncMigrationCommands(config) + assert hasattr(commands, "upgrade") + assert hasattr(commands, "downgrade") + assert hasattr(commands, "get_current_migration") diff --git a/docs/examples/usage/usage_migrations_5.py b/docs/examples/usage/usage_migrations_5.py new file mode 100644 index 00000000..583e580d --- /dev/null +++ b/docs/examples/usage/usage_migrations_5.py @@ -0,0 +1,26 @@ +# start-example +from sqlspec.adapters.asyncpg import AsyncpgConfig + +__all__ = ("test_config_structure",) + + +config = AsyncpgConfig( + pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, + migration_config={ + "enabled": True, + "script_location": "migrations", + "version_table_name": "ddl_migrations", + "auto_sync": True, # Enable automatic version reconciliation + }, +) +# end-example + + +def test_config_structure() -> None: + # Check config attributes + assert hasattr(config, "pool_config") + assert hasattr(config, "migration_config") + assert config.migration_config["enabled"] is True + assert config.migration_config["script_location"] == "migrations" + assert config.migration_config["version_table_name"] == "ddl_migrations" + assert config.migration_config["auto_sync"] is True diff --git a/docs/examples/usage/usage_migrations_6.py b/docs/examples/usage/usage_migrations_6.py new file mode 100644 index 00000000..f326a6c1 --- /dev/null +++ b/docs/examples/usage/usage_migrations_6.py @@ -0,0 +1,37 @@ +# start-example +# migrations/0002_add_user_roles.py +"""Add user roles table + +Revision ID: 0002_add_user_roles +Created at: 2025-10-18 12:00:00 +""" + +__all__ = ("downgrade", "test_upgrade_and_downgrade_strings", "upgrade") + + +def upgrade() -> str: + """Apply migration.""" + return """ + CREATE TABLE user_roles ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + role VARCHAR(50) NOT NULL + ); + """ + + +def downgrade() -> str: + """Revert migration.""" + return """ + DROP TABLE user_roles; + """ + + +# end-example + + +def test_upgrade_and_downgrade_strings() -> None: + up_sql = upgrade() + down_sql = downgrade() + assert "CREATE TABLE user_roles" in up_sql + assert "DROP TABLE user_roles" in down_sql diff --git a/docs/examples/usage/usage_migrations_7.py b/docs/examples/usage/usage_migrations_7.py new file mode 100644 index 00000000..12f33dc6 --- /dev/null +++ b/docs/examples/usage/usage_migrations_7.py @@ -0,0 +1,21 @@ +# start-example +__all__ = ("test_upgrade_returns_list", "upgrade") + + +def upgrade() -> list[str]: + """Apply migration in multiple steps.""" + return [ + "CREATE TABLE products (id SERIAL PRIMARY KEY);", + "CREATE TABLE orders (id SERIAL PRIMARY KEY, product_id INTEGER);", + "CREATE INDEX idx_orders_product ON orders(product_id);", + ] + + +# end-example + + +def test_upgrade_returns_list() -> None: + stmts = upgrade() + assert isinstance(stmts, list) + assert any("products" in s for s in stmts) + assert any("orders" in s for s in stmts) diff --git a/docs/examples/usage/usage_migrations_8.py b/docs/examples/usage/usage_migrations_8.py new file mode 100644 index 00000000..e2059849 --- /dev/null +++ b/docs/examples/usage/usage_migrations_8.py @@ -0,0 +1,33 @@ +__all__ = ("test_tracker_instance",) + + +from pytest_databases.docker.postgres import PostgresService + + +async def test_tracker_instance(postgres_service: PostgresService) -> None: + + # start-example + from sqlspec.adapters.asyncpg import AsyncpgConfig + from sqlspec.migrations.tracker import AsyncMigrationTracker + + tracker = AsyncMigrationTracker() + + config = AsyncpgConfig( + pool_config={ + "dsn": f"postgres://{postgres_service.user}:{postgres_service.password}@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}" + }, + migration_config={ + "enabled": True, + "script_location": "migrations", + "version_table_name": "ddl_migrations", + "auto_sync": True, # Enable automatic version reconciliation + }, + ) + async with config.provide_session() as session: + driver = session._driver + + # Update version record + await tracker.update_version_record(driver, old_version="20251018120000", new_version="0003") + # end-example + # Just check that tracker is an instance of AsyncMigrationTracker + assert isinstance(tracker, AsyncMigrationTracker) diff --git a/docs/usage/migrations.rst b/docs/usage/migrations.rst index 17811446..39dc1b45 100644 --- a/docs/usage/migrations.rst +++ b/docs/usage/migrations.rst @@ -40,86 +40,28 @@ Async Adapters For async adapters (AsyncPG, Asyncmy, Aiosqlite, Psqlpy), migration methods return awaitables: -.. code-block:: python - - from sqlspec.adapters.asyncpg import AsyncpgConfig - - config = AsyncpgConfig( - pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, - migration_config={ - "enabled": True, - "script_location": "migrations", - } - ) - - # Apply migrations - await config.migrate_up("head") - # Or use the alias - await config.upgrade("head") - - # Rollback one revision - await config.migrate_down("-1") - # Or use the alias - await config.downgrade("-1") - - # Check current version - current = await config.get_current_migration(verbose=True) - print(current) +.. literalinclude:: ../examples/usage/usage_migrations_1.py + :language: python + :caption: `Async Adapters` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example - # Create new migration - await config.create_migration("add users table", file_type="sql") - # Initialize migrations directory - await config.init_migrations() - - # Stamp database to specific revision - await config.stamp_migration("0003") - - # Convert timestamp to sequential migrations - await config.fix_migrations(dry_run=False, update_database=True, yes=True) Sync Adapters ------------- For sync adapters (SQLite, DuckDB), migration methods execute immediately without await: -.. code-block:: python - - from sqlspec.adapters.sqlite import SqliteConfig +.. literalinclude:: ../examples/usage/usage_migrations_2.py + :language: python + :caption: `Sync Adapters` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example - config = SqliteConfig( - pool_config={"database": "myapp.db"}, - migration_config={ - "enabled": True, - "script_location": "migrations", - } - ) - # Apply migrations (no await needed) - config.migrate_up("head") - # Or use the alias - config.upgrade("head") - - # Rollback one revision - config.migrate_down("-1") - # Or use the alias - config.downgrade("-1") - - # Check current version - current = config.get_current_migration(verbose=True) - print(current) - - # Create new migration - config.create_migration("add users table", file_type="sql") - - # Initialize migrations directory - config.init_migrations() - - # Stamp database to specific revision - config.stamp_migration("0003") - - # Convert timestamp to sequential migrations - config.fix_migrations(dry_run=False, update_database=True, yes=True) Available Methods ----------------- @@ -171,8 +113,17 @@ def down(context: object | None = None) -> str | Iterable[str]: } } +.. literalinclude:: ../examples/usage/usage_migrations_3.py + :language: python + :caption: `Template Profiles & Author Metadata` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example + + Template fragments accept the following variables: + - ``{title}`` – shared template title - ``{version}`` – generated revision identifier - ``{message}`` – CLI/command message @@ -241,14 +192,32 @@ For advanced use cases requiring custom logic, you can still use command classes migration_config={"script_location": "migrations"} ) +.. literalinclude:: ../examples/usage/usage_migrations_5.py + :language: python + :caption: `Configuration` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example + + + # Create commands instance commands = AsyncMigrationCommands(config) # Use commands directly await commands.upgrade("head") +.. literalinclude:: ../examples/usage/usage_migrations_4.py + :language: python + :caption: `Command Classes (Advanced)` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example + + This approach is useful when: + - Building custom migration runners - Implementing migration lifecycle hooks - Integrating with third-party workflow tools @@ -370,19 +339,26 @@ Python migrations provide more flexibility for complex operations: DROP TABLE user_roles; """ +.. literalinclude:: ../examples/usage/usage_migrations_6.py + :language: python + :caption: `Python Migrations` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example + + **Advanced Usage:** Python migrations can also return a list of SQL statements: -.. code-block:: python +.. literalinclude:: ../examples/usage/usage_migrations_7.py + :language: python + :caption: `Advanced Usage` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example + - def upgrade(): - """Apply migration in multiple steps.""" - return [ - "CREATE TABLE products (id SERIAL PRIMARY KEY);", - "CREATE TABLE orders (id SERIAL PRIMARY KEY, product_id INTEGER);", - "CREATE INDEX idx_orders_product ON orders(product_id);", - ] .. _hybrid-versioning-guide: @@ -666,6 +642,15 @@ If auto-sync is disabled, manually reconcile renamed migrations: new_version="0003" ) +.. literalinclude:: ../examples/usage/usage_migrations_8.py + :language: python + :caption: `Manual Version Reconciliation` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example + + + Troubleshooting =============== From 6391721d39f7241266dfbf7a00edcf46d0e778f5 Mon Sep 17 00:00:00 2001 From: euri10 Date: Tue, 18 Nov 2025 07:18:52 +0100 Subject: [PATCH 5/7] test 1 --- docs/examples/usage/usage_migrations_1.py | 30 ++++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/examples/usage/usage_migrations_1.py b/docs/examples/usage/usage_migrations_1.py index 67d5e5ca..eb68185b 100644 --- a/docs/examples/usage/usage_migrations_1.py +++ b/docs/examples/usage/usage_migrations_1.py @@ -1,17 +1,29 @@ -# start-example -from sqlspec.adapters.asyncpg import AsyncpgConfig - __all__ = ("test_async_methods",) -config = AsyncpgConfig( - pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, - migration_config={"enabled": True, "script_location": "migrations"}, -) -# end-example +async def test_async_methods() -> None: + # start-example + from sqlspec.adapters.asyncpg import AsyncpgConfig + + config = AsyncpgConfig( + pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, + migration_config={"enabled": True, "script_location": "migrations"}, + ) + + # Apply migrations + await config.migrate_up("head") + # Or use the alias + await config.upgrade("head") + # Rollback one revision + await config.migrate_down("-1") + # Or use the alias + await config.downgrade("-1") -def test_async_methods() -> None: + # Check current version + current = await config.get_current_migration(verbose=True) + print(current) + # end-example # These are just smoke tests for method presence, not actual DB calls assert hasattr(config, "migrate_up") assert hasattr(config, "upgrade") From 8f6d97a3e85b63a7bd85c5a26f14cadbc489474c Mon Sep 17 00:00:00 2001 From: euri10 Date: Tue, 18 Nov 2025 07:22:49 +0100 Subject: [PATCH 6/7] add service --- docs/examples/usage/usage_migrations_1.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/examples/usage/usage_migrations_1.py b/docs/examples/usage/usage_migrations_1.py index eb68185b..5b04de22 100644 --- a/docs/examples/usage/usage_migrations_1.py +++ b/docs/examples/usage/usage_migrations_1.py @@ -1,13 +1,19 @@ __all__ = ("test_async_methods",) -async def test_async_methods() -> None: +from pytest_databases.docker.postgres import PostgresService + + +async def test_async_methods(postgres_service: PostgresService) -> None: # start-example from sqlspec.adapters.asyncpg import AsyncpgConfig + dsn = ( + f"postgresql://{postgres_service.user}:{postgres_service.password}" + f"@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}" + ) config = AsyncpgConfig( - pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, - migration_config={"enabled": True, "script_location": "migrations"}, + pool_config={"dsn": dsn}, migration_config={"enabled": True, "script_location": "migrations"} ) # Apply migrations From 7a927e96842c3a588a66c8c5d4dfe6ad8be8b21a Mon Sep 17 00:00:00 2001 From: euri10 Date: Sat, 22 Nov 2025 11:00:48 +0100 Subject: [PATCH 7/7] fix stuff, still missing the alias --- docs/examples/usage/usage_migrations_1.py | 18 +++- docs/examples/usage/usage_migrations_2.py | 6 +- docs/examples/usage/usage_migrations_3.py | 2 +- docs/examples/usage/usage_migrations_5.py | 2 +- docs/examples/usage/usage_migrations_7.py | 2 +- docs/usage/migrations.rst | 118 ++-------------------- 6 files changed, 30 insertions(+), 118 deletions(-) diff --git a/docs/examples/usage/usage_migrations_1.py b/docs/examples/usage/usage_migrations_1.py index 5b04de22..24bb929a 100644 --- a/docs/examples/usage/usage_migrations_1.py +++ b/docs/examples/usage/usage_migrations_1.py @@ -19,16 +19,26 @@ async def test_async_methods(postgres_service: PostgresService) -> None: # Apply migrations await config.migrate_up("head") # Or use the alias - await config.upgrade("head") + # await config.upgrade("head") # Rollback one revision await config.migrate_down("-1") # Or use the alias - await config.downgrade("-1") + # await config.downgrade("-1") # Check current version - current = await config.get_current_migration(verbose=True) - print(current) + await config.get_current_migration(verbose=True) + # Create new migration + await config.create_migration("add users table", file_type="sql") + + # Initialize migrations directory + await config.init_migrations() + + # Stamp database to specific revision + await config.stamp_migration("0003") + + # Convert timestamp to sequential migrations + await config.fix_migrations(dry_run=False, update_database=True, yes=True) # end-example # These are just smoke tests for method presence, not actual DB calls assert hasattr(config, "migrate_up") diff --git a/docs/examples/usage/usage_migrations_2.py b/docs/examples/usage/usage_migrations_2.py index 045f3b47..44470e1e 100644 --- a/docs/examples/usage/usage_migrations_2.py +++ b/docs/examples/usage/usage_migrations_2.py @@ -1,9 +1,9 @@ -# start-example -from sqlspec.adapters.sqlite import SqliteConfig - __all__ = ("test_sync_methods",) +# start-example +from sqlspec.adapters.sqlite import SqliteConfig + config = SqliteConfig( pool_config={"database": "myapp.db"}, migration_config={"enabled": True, "script_location": "migrations"} ) diff --git a/docs/examples/usage/usage_migrations_3.py b/docs/examples/usage/usage_migrations_3.py index 0a0338f1..54acf538 100644 --- a/docs/examples/usage/usage_migrations_3.py +++ b/docs/examples/usage/usage_migrations_3.py @@ -1,7 +1,7 @@ -# start-example __all__ = ("test_template_config",) +# start-example migration_config = { "default_format": "py", # CLI default when --format omitted "title": "Acme Migration", # Shared title for all templates diff --git a/docs/examples/usage/usage_migrations_5.py b/docs/examples/usage/usage_migrations_5.py index 583e580d..128f7b92 100644 --- a/docs/examples/usage/usage_migrations_5.py +++ b/docs/examples/usage/usage_migrations_5.py @@ -1,9 +1,9 @@ -# start-example from sqlspec.adapters.asyncpg import AsyncpgConfig __all__ = ("test_config_structure",) +# start-example config = AsyncpgConfig( pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, migration_config={ diff --git a/docs/examples/usage/usage_migrations_7.py b/docs/examples/usage/usage_migrations_7.py index 12f33dc6..570b6ee1 100644 --- a/docs/examples/usage/usage_migrations_7.py +++ b/docs/examples/usage/usage_migrations_7.py @@ -1,7 +1,7 @@ -# start-example __all__ = ("test_upgrade_returns_list", "upgrade") +# start-example def upgrade() -> list[str]: """Apply migration in multiple steps.""" return [ diff --git a/docs/usage/migrations.rst b/docs/usage/migrations.rst index 39dc1b45..e9564131 100644 --- a/docs/usage/migrations.rst +++ b/docs/usage/migrations.rst @@ -40,7 +40,7 @@ Async Adapters For async adapters (AsyncPG, Asyncmy, Aiosqlite, Psqlpy), migration methods return awaitables: -.. literalinclude:: ../examples/usage/usage_migrations_1.py +.. literalinclude:: /examples/usage/usage_migrations_1.py :language: python :caption: `Async Adapters` :dedent: 0 @@ -54,7 +54,7 @@ Sync Adapters For sync adapters (SQLite, DuckDB), migration methods execute immediately without await: -.. literalinclude:: ../examples/usage/usage_migrations_2.py +.. literalinclude:: /examples/usage/usage_migrations_2.py :language: python :caption: `Sync Adapters` :dedent: 0 @@ -88,32 +88,7 @@ Migrations inherit their header text, metadata comments, and default file format from ``migration_config["templates"]``. Each project can define multiple profiles and select one globally: -.. code-block:: python - - migration_config={ - "default_format": "py", # CLI default when --format omitted - "title": "Acme Migration", # Shared title for all templates - "author": "env:SQLSPEC_AUTHOR", # Read from environment variable - "templates": { - "sql": { - "header": "-- {title} - {message}", - "metadata": ["-- Version: {version}", "-- Owner: {author}"], - "body": "-- custom SQL body" - }, - "py": { - "docstring": """{title}\nDescription: {description}""", - "imports": ["from typing import Iterable"], - "body": """def up(context: object | None = None) -> str | Iterable[str]: - return "SELECT 1" - -def down(context: object | None = None) -> str | Iterable[str]: - return "DROP TABLE example;" -""" - } - } - } - -.. literalinclude:: ../examples/usage/usage_migrations_3.py +.. literalinclude:: /examples/usage/usage_migrations_3.py :language: python :caption: `Template Profiles & Author Metadata` :dedent: 0 @@ -182,32 +157,7 @@ Command Classes (Advanced) For advanced use cases requiring custom logic, you can still use command classes directly: -.. code-block:: python - - from sqlspec.migrations.commands import AsyncMigrationCommands, SyncMigrationCommands - from sqlspec.adapters.asyncpg import AsyncpgConfig - - config = AsyncpgConfig( - pool_config={"dsn": "postgresql://..."}, - migration_config={"script_location": "migrations"} - ) - -.. literalinclude:: ../examples/usage/usage_migrations_5.py - :language: python - :caption: `Configuration` - :dedent: 0 - :start-after: # start-example - :end-before: # end-example - - - - # Create commands instance - commands = AsyncMigrationCommands(config) - - # Use commands directly - await commands.upgrade("head") - -.. literalinclude:: ../examples/usage/usage_migrations_4.py +.. literalinclude:: /examples/usage/usage_migrations_4.py :language: python :caption: `Command Classes (Advanced)` :dedent: 0 @@ -228,19 +178,12 @@ Configuration Enable migrations in your SQLSpec configuration: -.. code-block:: python - - from sqlspec.adapters.asyncpg import AsyncpgConfig - - config = AsyncpgConfig( - pool_config={"dsn": "postgresql://user:pass@localhost/mydb"}, - migration_config={ - "enabled": True, - "script_location": "migrations", - "version_table_name": "ddl_migrations", - "auto_sync": True, # Enable automatic version reconciliation - } - ) +.. literalinclude:: /examples/usage/usage_migrations_5.py + :language: python + :caption: `Configuration` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example Configuration Options --------------------- @@ -314,31 +257,6 @@ Python Migrations Python migrations provide more flexibility for complex operations: -.. code-block:: python - - # migrations/0002_add_user_roles.py - """Add user roles table - - Revision ID: 0002_add_user_roles - Created at: 2025-10-18 12:00:00 - """ - - def upgrade(): - """Apply migration.""" - return """ - CREATE TABLE user_roles ( - id SERIAL PRIMARY KEY, - user_id INTEGER REFERENCES users(id), - role VARCHAR(50) NOT NULL - ); - """ - - def downgrade(): - """Revert migration.""" - return """ - DROP TABLE user_roles; - """ - .. literalinclude:: ../examples/usage/usage_migrations_6.py :language: python :caption: `Python Migrations` @@ -626,22 +544,6 @@ Manual Version Reconciliation If auto-sync is disabled, manually reconcile renamed migrations: -.. code-block:: python - - from sqlspec.migrations.tracker import AsyncMigrationTracker - - tracker = AsyncMigrationTracker() - - async with config.provide_session() as session: - driver = session._driver - - # Update version record - await tracker.update_version_record( - driver, - old_version="20251018120000", - new_version="0003" - ) - .. literalinclude:: ../examples/usage/usage_migrations_8.py :language: python :caption: `Manual Version Reconciliation`