sunnyware / scripts /migrate.py
shamiur's picture
Part 2: config system + Neon DB pool + migrations (7/7 smoke pass)
440bb5b
Raw History Blame Contribute Delete
1.89 kB
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Shamiur Rashid Sunny
# SPDX-License-Identifier: AGPL-3.0-only
"""Idempotent migration runner for Neon Postgres."""
import asyncio
import os
import sys
from pathlib import Path
import asyncpg
from dotenv import load_dotenv
MIGRATIONS_DIR = Path(__file__).parent.parent / "migrations"
async def run_migrations():
load_dotenv()
url = os.getenv("NEON_DATABASE_URL")
if not url:
print("ERROR: NEON_DATABASE_URL not set")
return 1
print("-> Connecting to Neon...")
conn = await asyncpg.connect(url, statement_cache_size=0)
try:
await conn.execute("""
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
""")
applied = {
r["version"] for r in await conn.fetch("SELECT version FROM schema_migrations")
}
print(f"-> Already applied: {len(applied)} migration(s)")
files = sorted(MIGRATIONS_DIR.glob("*.sql"))
if not files:
print("-> No migration files found")
return 0
for f in files:
version = f.stem
if version in applied:
print(f" [skip] {version} (already applied)")
continue
print(f" -> Applying {version}...")
sql = f.read_text()
async with conn.transaction():
await conn.execute(sql)
await conn.execute(
"INSERT INTO schema_migrations (version) VALUES ($1)", version
)
print(f" [ok] {version} applied")
print("[done] All migrations complete")
return 0
finally:
await conn.close()
if __name__ == "__main__":
sys.exit(asyncio.run(run_migrations()))