feat: project skeleton
- infra (k8s, kind, helm, docker) backbone is implemented - security: implementation + unit tests are done
This commit is contained in:
119
migrations/migrate.py
Normal file
119
migrations/migrate.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Simple migration runner using asyncpg.
|
||||
Tracks applied migrations in a _migrations table.
|
||||
|
||||
Usage:
|
||||
DATABASE_URL=postgresql://user:pass@localhost/db uv run python migrations/migrate.py apply
|
||||
DATABASE_URL=postgresql://user:pass@localhost/db uv run python migrations/migrate.py status
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import asyncpg
|
||||
|
||||
MIGRATIONS_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
async def ensure_migrations_table(conn: asyncpg.Connection) -> None:
|
||||
"""Create the migrations tracking table if it doesn't exist."""
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS _migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
async def get_applied_migrations(conn: asyncpg.Connection) -> set[str]:
|
||||
"""Get the set of already applied migration names."""
|
||||
rows = await conn.fetch("SELECT name FROM _migrations")
|
||||
return {row["name"] for row in rows}
|
||||
|
||||
|
||||
async def get_pending_migrations(conn: asyncpg.Connection) -> list[Path]:
|
||||
"""Get list of migration files that haven't been applied yet."""
|
||||
applied = await get_applied_migrations(conn)
|
||||
sql_files = sorted(MIGRATIONS_DIR.glob("*.sql"))
|
||||
return [f for f in sql_files if f.name not in applied]
|
||||
|
||||
|
||||
async def apply_migration(conn: asyncpg.Connection, migration_file: Path) -> None:
|
||||
"""Apply a single migration file within a transaction."""
|
||||
sql = migration_file.read_text()
|
||||
async with conn.transaction():
|
||||
await conn.execute(sql)
|
||||
await conn.execute(
|
||||
"INSERT INTO _migrations (name) VALUES ($1)",
|
||||
migration_file.name
|
||||
)
|
||||
print(f"Applied: {migration_file.name}")
|
||||
|
||||
|
||||
async def migrate(database_url: str) -> None:
|
||||
"""Apply all pending migrations."""
|
||||
conn = await asyncpg.connect(database_url)
|
||||
try:
|
||||
await ensure_migrations_table(conn)
|
||||
pending = await get_pending_migrations(conn)
|
||||
|
||||
if not pending:
|
||||
print("No pending migrations.")
|
||||
return
|
||||
|
||||
for migration_file in pending:
|
||||
await apply_migration(conn, migration_file)
|
||||
|
||||
print(f"Applied {len(pending)} migration(s).")
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def status(database_url: str) -> None:
|
||||
"""Show migration status."""
|
||||
conn = await asyncpg.connect(database_url)
|
||||
try:
|
||||
await ensure_migrations_table(conn)
|
||||
applied = await get_applied_migrations(conn)
|
||||
pending = await get_pending_migrations(conn)
|
||||
|
||||
print("Applied migrations:")
|
||||
for name in sorted(applied):
|
||||
print(f" [x] {name}")
|
||||
|
||||
print("\nPending migrations:")
|
||||
for f in pending:
|
||||
print(f" [ ] {f.name}")
|
||||
|
||||
if not applied and not pending:
|
||||
print(" (none)")
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
database_url = os.environ.get("DATABASE_URL")
|
||||
if not database_url:
|
||||
print("Error: DATABASE_URL environment variable is required")
|
||||
sys.exit(1)
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python migrate.py [apply|status]")
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
if command == "apply":
|
||||
asyncio.run(migrate(database_url))
|
||||
elif command == "status":
|
||||
asyncio.run(status(database_url))
|
||||
else:
|
||||
print(f"Unknown command: {command}")
|
||||
print("Usage: python migrate.py [apply|status]")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user