diff --git a/docs/examples/arrow/arrow_basic_usage.py b/docs/examples/arrow/arrow_basic_usage.py index 04b4fa5a..3c5c6f70 100644 --- a/docs/examples/arrow/arrow_basic_usage.py +++ b/docs/examples/arrow/arrow_basic_usage.py @@ -75,11 +75,14 @@ async def example_adbc_native() -> None: # Example 2: PostgreSQL with Conversion Path async def example_postgres_conversion() -> None: """Demonstrate PostgreSQL adapter with dict → Arrow conversion.""" + import os + from sqlspec import SQLSpec from sqlspec.adapters.asyncpg import AsyncpgConfig + dsn = os.getenv("SQLSPEC_USAGE_PG_DSN", "postgresql://localhost/db") db_manager = SQLSpec() - asyncpg_db = db_manager.add_config(AsyncpgConfig(pool_config={"dsn": "postgresql://localhost/test"})) + asyncpg_db = db_manager.add_config(AsyncpgConfig(pool_config={"dsn": dsn})) async with db_manager.provide_session(asyncpg_db) as session: # Create test table with PostgreSQL-specific types diff --git a/docs/examples/patterns/configs/multi_adapter_registry.py b/docs/examples/patterns/configs/multi_adapter_registry.py index 2a322d9e..60a81823 100644 --- a/docs/examples/patterns/configs/multi_adapter_registry.py +++ b/docs/examples/patterns/configs/multi_adapter_registry.py @@ -1,5 +1,7 @@ """Show how to register multiple adapters on a single SQLSpec instance.""" +import os + from sqlspec import SQLSpec from sqlspec.adapters.aiosqlite import AiosqliteConfig from sqlspec.adapters.asyncpg import AsyncpgConfig, AsyncpgPoolConfig @@ -11,15 +13,12 @@ def build_registry() -> "SQLSpec": """Create a registry with both sync and async adapters.""" + dsn = os.getenv("SQLSPEC_USAGE_PG_DSN", "postgresql://localhost/db") registry = SQLSpec() registry.add_config(SqliteConfig(bind_key="sync_sqlite", pool_config={"database": ":memory:"})) registry.add_config(AiosqliteConfig(bind_key="async_sqlite", pool_config={"database": ":memory:"})) registry.add_config(DuckDBConfig(bind_key="duckdb_docs", pool_config={"database": ":memory:docs_duck"})) - registry.add_config( - AsyncpgConfig( - bind_key="asyncpg_docs", pool_config=AsyncpgPoolConfig(dsn="postgresql://user:pass@localhost:5432/db") - ) - ) + registry.add_config(AsyncpgConfig(bind_key="asyncpg_docs", pool_config=AsyncpgPoolConfig(dsn=dsn))) return registry diff --git a/docs/examples/usage/usage_migrations_1.py b/docs/examples/usage/usage_migrations_1.py new file mode 100644 index 00000000..7ac7c9e1 --- /dev/null +++ b/docs/examples/usage/usage_migrations_1.py @@ -0,0 +1,50 @@ +"""Async migration commands via config methods.""" + +import tempfile +from pathlib import Path + +import pytest +from pytest_databases.docker.postgres import PostgresService + +pytestmark = pytest.mark.xdist_group("postgres") + +__all__ = ("test_async_methods",) + + +async def test_async_methods(postgres_service: PostgresService) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + migration_dir = Path(temp_dir) / "migrations" + migration_dir.mkdir() + + # 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": dsn}, migration_config={"enabled": True, "script_location": str(migration_dir)} + ) + + # Initialize migrations directory (creates __init__.py if package=True) + await config.init_migrations() + + # Create new migration file + await config.create_migration("add users table", file_type="sql") + + # Apply migrations to head + await config.migrate_up("head") + + # Rollback one revision + await config.migrate_down("-1") + + # Check current version + await config.get_current_migration(verbose=True) + + # Stamp database to specific revision + await config.stamp_migration("0001") + + # Convert timestamp to sequential migrations + await config.fix_migrations(dry_run=True, update_database=False, yes=True) + # end-example diff --git a/docs/examples/usage/usage_migrations_10.py b/docs/examples/usage/usage_migrations_10.py new file mode 100644 index 00000000..40741acc --- /dev/null +++ b/docs/examples/usage/usage_migrations_10.py @@ -0,0 +1,51 @@ +"""Using AsyncMigrationTracker for version management.""" + +import tempfile +from pathlib import Path + +import pytest +from pytest_databases.docker.postgres import PostgresService + +pytestmark = pytest.mark.xdist_group("postgres") + +__all__ = ("test_tracker_instance",) + + +async def test_tracker_instance(postgres_service: PostgresService) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + migration_dir = Path(temp_dir) / "migrations" + migration_dir.mkdir() + + # start-example + from sqlspec.adapters.asyncpg import AsyncpgConfig + from sqlspec.migrations.tracker import AsyncMigrationTracker + + # Create tracker with custom table name + tracker = AsyncMigrationTracker(version_table_name="ddl_migrations") + + dsn = ( + f"postgresql://{postgres_service.user}:{postgres_service.password}" + f"@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}" + ) + config = AsyncpgConfig( + pool_config={"dsn": dsn}, + migration_config={ + "enabled": True, + "script_location": str(migration_dir), + "version_table_name": "ddl_migrations", + "auto_sync": True, # Enable automatic version reconciliation + }, + ) + + # Use the session to work with migrations + async with config.provide_session() as session: + # Ensure the tracking table exists + await tracker.ensure_tracking_table(session) + + # Get current version (None if no migrations applied) + current = await tracker.get_current_version(session) + print(f"Current version: {current}") + # end-example + + assert isinstance(tracker, AsyncMigrationTracker) + assert config.migration_config["version_table_name"] == "ddl_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..138fdaf7 --- /dev/null +++ b/docs/examples/usage/usage_migrations_2.py @@ -0,0 +1,44 @@ +"""Sync migration commands via config methods.""" + +import tempfile +from pathlib import Path + +__all__ = ("test_sync_methods",) + + +def test_sync_methods() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + migration_dir = Path(temp_dir) / "migrations" + migration_dir.mkdir() + temp_db = Path(temp_dir) / "test.db" + + # start-example + from sqlspec.adapters.sqlite import SqliteConfig + + config = SqliteConfig( + pool_config={"database": str(temp_db)}, + migration_config={"enabled": True, "script_location": str(migration_dir)}, + ) + + # Initialize migrations directory (creates __init__.py if package=True) + config.init_migrations() + + # Create new migration file + config.create_migration("add users table", file_type="sql") + + # Apply migrations to head (no await needed for sync) + config.migrate_up("head") + + # Rollback one revision + config.migrate_down("-1") + + # Check current version + current = config.get_current_migration(verbose=True) + print(current) + + # Stamp database to specific revision + config.stamp_migration("0001") + + # Convert timestamp to sequential migrations + config.fix_migrations(dry_run=True, update_database=False, yes=True) + # end-example diff --git a/docs/examples/usage/usage_migrations_3.py b/docs/examples/usage/usage_migrations_3.py new file mode 100644 index 00000000..54acf538 --- /dev/null +++ b/docs/examples/usage/usage_migrations_3.py @@ -0,0 +1,31 @@ +__all__ = ("test_template_config",) + + +# start-example +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..326e03a0 --- /dev/null +++ b/docs/examples/usage/usage_migrations_4.py @@ -0,0 +1,37 @@ +"""Using AsyncMigrationCommands directly.""" + +import os +import tempfile +from pathlib import Path + +__all__ = ("test_async_command_class_methods",) + + +async def test_async_command_class_methods() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + migration_dir = Path(temp_dir) / "migrations" + migration_dir.mkdir() + + # start-example + from sqlspec.adapters.asyncpg import AsyncpgConfig + from sqlspec.migrations.commands import AsyncMigrationCommands + + dsn = os.getenv("SQLSPEC_USAGE_PG_DSN", "postgresql://localhost/db") + config = AsyncpgConfig(pool_config={"dsn": dsn}, migration_config={"script_location": str(migration_dir)}) + + # Create commands instance + commands = AsyncMigrationCommands(config) + + # Use commands directly + await commands.init(str(migration_dir)) + await commands.upgrade("head") + # end-example + + # Smoke test for AsyncMigrationCommands method presence + assert hasattr(commands, "upgrade") + assert hasattr(commands, "downgrade") + assert hasattr(commands, "current") + assert hasattr(commands, "revision") + assert hasattr(commands, "stamp") + assert hasattr(commands, "fix") + assert hasattr(commands, "init") diff --git a/docs/examples/usage/usage_migrations_5.py b/docs/examples/usage/usage_migrations_5.py new file mode 100644 index 00000000..9b85602e --- /dev/null +++ b/docs/examples/usage/usage_migrations_5.py @@ -0,0 +1,29 @@ +import os + +from sqlspec.adapters.asyncpg import AsyncpgConfig + +__all__ = ("test_config_structure",) + + +# start-example +dsn = os.getenv("SQLSPEC_USAGE_PG_DSN", "postgresql://localhost/db") +config = AsyncpgConfig( + pool_config={"dsn": dsn}, + 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..e551a810 --- /dev/null +++ b/docs/examples/usage/usage_migrations_6.py @@ -0,0 +1,36 @@ +__all__ = ("downgrade", "test_upgrade_and_downgrade_strings", "upgrade") +# 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 +""" + + +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..570b6ee1 --- /dev/null +++ b/docs/examples/usage/usage_migrations_7.py @@ -0,0 +1,21 @@ +__all__ = ("test_upgrade_returns_list", "upgrade") + + +# start-example +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..c80a582e --- /dev/null +++ b/docs/examples/usage/usage_migrations_8.py @@ -0,0 +1,18 @@ +__all__ = ("test_version_comparison",) + + +def test_version_comparison() -> None: + + # start-example + from sqlspec.utils.version import parse_version + + v1 = parse_version("0001") + v2 = parse_version("20251018120000") + + # Sequential < Timestamp (by design) + assert v1 < v2 + + # Same type comparisons work naturally + assert parse_version("0001") < parse_version("0002") + assert parse_version("20251018120000") < parse_version("20251019120000") + # end-example diff --git a/docs/examples/usage/usage_migrations_9.py b/docs/examples/usage/usage_migrations_9.py new file mode 100644 index 00000000..9d25fb19 --- /dev/null +++ b/docs/examples/usage/usage_migrations_9.py @@ -0,0 +1,21 @@ +import os + +from sqlspec.adapters.asyncpg import AsyncpgConfig + +__all__ = ("test_extension_config",) + + +def test_extension_config() -> None: + # start-example + dsn = os.getenv("SQLSPEC_USAGE_PG_DSN", "postgresql://localhost/db") + config = AsyncpgConfig( + pool_config={"dsn": dsn}, + migration_config={ + "enabled": True, + "script_location": "migrations", + "include_extensions": ["litestar"], # Enable litestar extension migrations + }, + extension_config={"litestar": {"enable_repository_pattern": True, "enable_dto_generation": False}}, + ) + # end-example + assert config.extension_config diff --git a/docs/usage/migrations.rst b/docs/usage/migrations.rst index 17811446..e4d8885e 100644 --- a/docs/usage/migrations.rst +++ b/docs/usage/migrations.rst @@ -40,86 +40,24 @@ 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) - - # 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) +.. literalinclude:: /examples/usage/usage_migrations_1.py + :language: python + :caption: `async adapters` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example Sync Adapters ------------- For sync adapters (SQLite, DuckDB), migration methods execute immediately without await: -.. code-block:: python - - from sqlspec.adapters.sqlite import SqliteConfig - - 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) +.. literalinclude:: /examples/usage/usage_migrations_2.py + :language: python + :caption: `sync adapters` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example Available Methods ----------------- @@ -146,30 +84,12 @@ 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 + :language: python + :caption: `template profile` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example Template fragments accept the following variables: @@ -231,21 +151,12 @@ 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"} - ) - - # 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` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example This approach is useful when: @@ -259,19 +170,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 --------------------- @@ -345,44 +249,24 @@ 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` + :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: @@ -475,19 +359,12 @@ Version Comparison SQLSpec uses type-aware version comparison: -.. code-block:: python - - from sqlspec.utils.version import parse_version - - v1 = parse_version("0001") - v2 = parse_version("20251018120000") - - # Sequential < Timestamp (by design) - assert v1 < v2 - - # Same type comparisons work naturally - assert parse_version("0001") < parse_version("0002") - assert parse_version("20251018120000") < parse_version("20251019120000") +.. literalinclude:: /examples/usage/usage_migrations_8.py + :language: python + :caption: `version comparison` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example Migration Tracking ================== @@ -562,22 +439,12 @@ SQLSpec supports independent migration versioning for extensions and plugins. Configuration ------------- -.. code-block:: python - - config = AsyncpgConfig( - pool_config={"dsn": "postgresql://..."}, - migration_config={ - "enabled": True, - "script_location": "migrations", - "include_extensions": ["litestar"], # Enable litestar extension migrations - }, - extension_config={ - "litestar": { - "enable_repository_pattern": True, - "enable_dto_generation": False, - } - } - ) +.. literalinclude:: /examples/usage/usage_migrations_9.py + :language: python + :caption: `extension migrations` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example Directory Structure ------------------- @@ -650,21 +517,12 @@ 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_10.py + :language: python + :caption: `manual version` + :dedent: 0 + :start-after: # start-example + :end-before: # end-example Troubleshooting =============== @@ -785,4 +643,3 @@ See Also - :doc:`../usage/cli` - Complete CLI command reference - :doc:`../usage/configuration` - Migration configuration options -- :doc:`../reference/migrations_api` - Migration API reference