Skip to content

Commit fa50a04

Browse files
authored
docs: migration usage (#265)
Migration usage docs
1 parent 86cc802 commit fa50a04

13 files changed

+406
-209
lines changed

docs/examples/arrow/arrow_basic_usage.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,14 @@ async def example_adbc_native() -> None:
7575
# Example 2: PostgreSQL with Conversion Path
7676
async def example_postgres_conversion() -> None:
7777
"""Demonstrate PostgreSQL adapter with dict → Arrow conversion."""
78+
import os
79+
7880
from sqlspec import SQLSpec
7981
from sqlspec.adapters.asyncpg import AsyncpgConfig
8082

83+
dsn = os.getenv("SQLSPEC_USAGE_PG_DSN", "postgresql://localhost/db")
8184
db_manager = SQLSpec()
82-
asyncpg_db = db_manager.add_config(AsyncpgConfig(pool_config={"dsn": "postgresql://localhost/test"}))
85+
asyncpg_db = db_manager.add_config(AsyncpgConfig(pool_config={"dsn": dsn}))
8386

8487
async with db_manager.provide_session(asyncpg_db) as session:
8588
# Create test table with PostgreSQL-specific types

docs/examples/patterns/configs/multi_adapter_registry.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Show how to register multiple adapters on a single SQLSpec instance."""
22

3+
import os
4+
35
from sqlspec import SQLSpec
46
from sqlspec.adapters.aiosqlite import AiosqliteConfig
57
from sqlspec.adapters.asyncpg import AsyncpgConfig, AsyncpgPoolConfig
@@ -11,15 +13,12 @@
1113

1214
def build_registry() -> "SQLSpec":
1315
"""Create a registry with both sync and async adapters."""
16+
dsn = os.getenv("SQLSPEC_USAGE_PG_DSN", "postgresql://localhost/db")
1417
registry = SQLSpec()
1518
registry.add_config(SqliteConfig(bind_key="sync_sqlite", pool_config={"database": ":memory:"}))
1619
registry.add_config(AiosqliteConfig(bind_key="async_sqlite", pool_config={"database": ":memory:"}))
1720
registry.add_config(DuckDBConfig(bind_key="duckdb_docs", pool_config={"database": ":memory:docs_duck"}))
18-
registry.add_config(
19-
AsyncpgConfig(
20-
bind_key="asyncpg_docs", pool_config=AsyncpgPoolConfig(dsn="postgresql://user:pass@localhost:5432/db")
21-
)
22-
)
21+
registry.add_config(AsyncpgConfig(bind_key="asyncpg_docs", pool_config=AsyncpgPoolConfig(dsn=dsn)))
2322
return registry
2423

2524

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Async migration commands via config methods."""
2+
3+
import tempfile
4+
from pathlib import Path
5+
6+
import pytest
7+
from pytest_databases.docker.postgres import PostgresService
8+
9+
pytestmark = pytest.mark.xdist_group("postgres")
10+
11+
__all__ = ("test_async_methods",)
12+
13+
14+
async def test_async_methods(postgres_service: PostgresService) -> None:
15+
with tempfile.TemporaryDirectory() as temp_dir:
16+
migration_dir = Path(temp_dir) / "migrations"
17+
migration_dir.mkdir()
18+
19+
# start-example
20+
from sqlspec.adapters.asyncpg import AsyncpgConfig
21+
22+
dsn = (
23+
f"postgresql://{postgres_service.user}:{postgres_service.password}"
24+
f"@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}"
25+
)
26+
config = AsyncpgConfig(
27+
pool_config={"dsn": dsn}, migration_config={"enabled": True, "script_location": str(migration_dir)}
28+
)
29+
30+
# Initialize migrations directory (creates __init__.py if package=True)
31+
await config.init_migrations()
32+
33+
# Create new migration file
34+
await config.create_migration("add users table", file_type="sql")
35+
36+
# Apply migrations to head
37+
await config.migrate_up("head")
38+
39+
# Rollback one revision
40+
await config.migrate_down("-1")
41+
42+
# Check current version
43+
await config.get_current_migration(verbose=True)
44+
45+
# Stamp database to specific revision
46+
await config.stamp_migration("0001")
47+
48+
# Convert timestamp to sequential migrations
49+
await config.fix_migrations(dry_run=True, update_database=False, yes=True)
50+
# end-example
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Using AsyncMigrationTracker for version management."""
2+
3+
import tempfile
4+
from pathlib import Path
5+
6+
import pytest
7+
from pytest_databases.docker.postgres import PostgresService
8+
9+
pytestmark = pytest.mark.xdist_group("postgres")
10+
11+
__all__ = ("test_tracker_instance",)
12+
13+
14+
async def test_tracker_instance(postgres_service: PostgresService) -> None:
15+
with tempfile.TemporaryDirectory() as temp_dir:
16+
migration_dir = Path(temp_dir) / "migrations"
17+
migration_dir.mkdir()
18+
19+
# start-example
20+
from sqlspec.adapters.asyncpg import AsyncpgConfig
21+
from sqlspec.migrations.tracker import AsyncMigrationTracker
22+
23+
# Create tracker with custom table name
24+
tracker = AsyncMigrationTracker(version_table_name="ddl_migrations")
25+
26+
dsn = (
27+
f"postgresql://{postgres_service.user}:{postgres_service.password}"
28+
f"@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}"
29+
)
30+
config = AsyncpgConfig(
31+
pool_config={"dsn": dsn},
32+
migration_config={
33+
"enabled": True,
34+
"script_location": str(migration_dir),
35+
"version_table_name": "ddl_migrations",
36+
"auto_sync": True, # Enable automatic version reconciliation
37+
},
38+
)
39+
40+
# Use the session to work with migrations
41+
async with config.provide_session() as session:
42+
# Ensure the tracking table exists
43+
await tracker.ensure_tracking_table(session)
44+
45+
# Get current version (None if no migrations applied)
46+
current = await tracker.get_current_version(session)
47+
print(f"Current version: {current}")
48+
# end-example
49+
50+
assert isinstance(tracker, AsyncMigrationTracker)
51+
assert config.migration_config["version_table_name"] == "ddl_migrations"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Sync migration commands via config methods."""
2+
3+
import tempfile
4+
from pathlib import Path
5+
6+
__all__ = ("test_sync_methods",)
7+
8+
9+
def test_sync_methods() -> None:
10+
with tempfile.TemporaryDirectory() as temp_dir:
11+
migration_dir = Path(temp_dir) / "migrations"
12+
migration_dir.mkdir()
13+
temp_db = Path(temp_dir) / "test.db"
14+
15+
# start-example
16+
from sqlspec.adapters.sqlite import SqliteConfig
17+
18+
config = SqliteConfig(
19+
pool_config={"database": str(temp_db)},
20+
migration_config={"enabled": True, "script_location": str(migration_dir)},
21+
)
22+
23+
# Initialize migrations directory (creates __init__.py if package=True)
24+
config.init_migrations()
25+
26+
# Create new migration file
27+
config.create_migration("add users table", file_type="sql")
28+
29+
# Apply migrations to head (no await needed for sync)
30+
config.migrate_up("head")
31+
32+
# Rollback one revision
33+
config.migrate_down("-1")
34+
35+
# Check current version
36+
current = config.get_current_migration(verbose=True)
37+
print(current)
38+
39+
# Stamp database to specific revision
40+
config.stamp_migration("0001")
41+
42+
# Convert timestamp to sequential migrations
43+
config.fix_migrations(dry_run=True, update_database=False, yes=True)
44+
# end-example
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
__all__ = ("test_template_config",)
2+
3+
4+
# start-example
5+
migration_config = {
6+
"default_format": "py", # CLI default when --format omitted
7+
"title": "Acme Migration", # Shared title for all templates
8+
"author": "env:SQLSPEC_AUTHOR", # Read from environment variable
9+
"templates": {
10+
"sql": {
11+
"header": "-- {title} - {message}",
12+
"metadata": ["-- Version: {version}", "-- Owner: {author}"],
13+
"body": "-- custom SQL body",
14+
},
15+
"py": {
16+
"docstring": """{title}\nDescription: {description}""",
17+
"imports": ["from typing import Iterable"],
18+
"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""",
19+
},
20+
},
21+
}
22+
# end-example
23+
24+
25+
def test_template_config() -> None:
26+
# Check structure of migration_config
27+
assert migration_config["default_format"] == "py"
28+
assert "py" in migration_config["templates"]
29+
assert "sql" in migration_config["templates"]
30+
assert isinstance(migration_config["templates"]["py"], dict)
31+
assert isinstance(migration_config["templates"]["sql"], dict)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Using AsyncMigrationCommands directly."""
2+
3+
import os
4+
import tempfile
5+
from pathlib import Path
6+
7+
__all__ = ("test_async_command_class_methods",)
8+
9+
10+
async def test_async_command_class_methods() -> None:
11+
with tempfile.TemporaryDirectory() as temp_dir:
12+
migration_dir = Path(temp_dir) / "migrations"
13+
migration_dir.mkdir()
14+
15+
# start-example
16+
from sqlspec.adapters.asyncpg import AsyncpgConfig
17+
from sqlspec.migrations.commands import AsyncMigrationCommands
18+
19+
dsn = os.getenv("SQLSPEC_USAGE_PG_DSN", "postgresql://localhost/db")
20+
config = AsyncpgConfig(pool_config={"dsn": dsn}, migration_config={"script_location": str(migration_dir)})
21+
22+
# Create commands instance
23+
commands = AsyncMigrationCommands(config)
24+
25+
# Use commands directly
26+
await commands.init(str(migration_dir))
27+
await commands.upgrade("head")
28+
# end-example
29+
30+
# Smoke test for AsyncMigrationCommands method presence
31+
assert hasattr(commands, "upgrade")
32+
assert hasattr(commands, "downgrade")
33+
assert hasattr(commands, "current")
34+
assert hasattr(commands, "revision")
35+
assert hasattr(commands, "stamp")
36+
assert hasattr(commands, "fix")
37+
assert hasattr(commands, "init")
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import os
2+
3+
from sqlspec.adapters.asyncpg import AsyncpgConfig
4+
5+
__all__ = ("test_config_structure",)
6+
7+
8+
# start-example
9+
dsn = os.getenv("SQLSPEC_USAGE_PG_DSN", "postgresql://localhost/db")
10+
config = AsyncpgConfig(
11+
pool_config={"dsn": dsn},
12+
migration_config={
13+
"enabled": True,
14+
"script_location": "migrations",
15+
"version_table_name": "ddl_migrations",
16+
"auto_sync": True, # Enable automatic version reconciliation
17+
},
18+
)
19+
# end-example
20+
21+
22+
def test_config_structure() -> None:
23+
# Check config attributes
24+
assert hasattr(config, "pool_config")
25+
assert hasattr(config, "migration_config")
26+
assert config.migration_config["enabled"] is True
27+
assert config.migration_config["script_location"] == "migrations"
28+
assert config.migration_config["version_table_name"] == "ddl_migrations"
29+
assert config.migration_config["auto_sync"] is True
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
__all__ = ("downgrade", "test_upgrade_and_downgrade_strings", "upgrade")
2+
# start-example
3+
# migrations/0002_add_user_roles.py
4+
"""Add user roles table
5+
6+
Revision ID: 0002_add_user_roles
7+
Created at: 2025-10-18 12:00:00
8+
"""
9+
10+
11+
def upgrade() -> str:
12+
"""Apply migration."""
13+
return """
14+
CREATE TABLE user_roles (
15+
id SERIAL PRIMARY KEY,
16+
user_id INTEGER REFERENCES users(id),
17+
role VARCHAR(50) NOT NULL
18+
);
19+
"""
20+
21+
22+
def downgrade() -> str:
23+
"""Revert migration."""
24+
return """
25+
DROP TABLE user_roles;
26+
"""
27+
28+
29+
# end-example
30+
31+
32+
def test_upgrade_and_downgrade_strings() -> None:
33+
up_sql = upgrade()
34+
down_sql = downgrade()
35+
assert "CREATE TABLE user_roles" in up_sql
36+
assert "DROP TABLE user_roles" in down_sql
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
__all__ = ("test_upgrade_returns_list", "upgrade")
2+
3+
4+
# start-example
5+
def upgrade() -> list[str]:
6+
"""Apply migration in multiple steps."""
7+
return [
8+
"CREATE TABLE products (id SERIAL PRIMARY KEY);",
9+
"CREATE TABLE orders (id SERIAL PRIMARY KEY, product_id INTEGER);",
10+
"CREATE INDEX idx_orders_product ON orders(product_id);",
11+
]
12+
13+
14+
# end-example
15+
16+
17+
def test_upgrade_returns_list() -> None:
18+
stmts = upgrade()
19+
assert isinstance(stmts, list)
20+
assert any("products" in s for s in stmts)
21+
assert any("orders" in s for s in stmts)

0 commit comments

Comments
 (0)