suvradeepp's picture
Deploy reno scheduling engine (FastAPI + Streamlit)
9c50399 verified
Raw
History Blame Contribute Delete
3.6 kB
"""Tiny forward/!down migration runner for SQLite.
Each file in /migrations has a ``-- up`` section and a ``-- down`` section.
Applied migrations are tracked in ``schema_migrations``.
Usage:
python -m scripts.migrate up # apply all pending
python -m scripts.migrate down # revert the most recent
python -m scripts.migrate down 0 # revert everything (to a clean DB)
python -m scripts.migrate status
"""
import os
import sys
from app import db
MIGRATIONS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "migrations")
def _parse(path: str) -> tuple[str, str]:
with open(path, encoding="utf-8") as f:
text = f.read()
lower = text.lower()
up_idx = lower.find("-- up")
down_idx = lower.find("-- down")
if up_idx == -1 or down_idx == -1:
raise ValueError(f"{path} must contain '-- up' and '-- down' markers")
up_sql = text[up_idx + len("-- up"):down_idx].strip()
down_sql = text[down_idx + len("-- down"):].strip()
return up_sql, down_sql
def _all_migrations() -> list[str]:
return sorted(f for f in os.listdir(MIGRATIONS_DIR) if f.endswith(".sql"))
def _ensure_table(conn) -> None:
conn.executescript(
"CREATE TABLE IF NOT EXISTS schema_migrations ("
" name TEXT PRIMARY KEY,"
" applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')));"
)
def _applied(conn) -> set[str]:
_ensure_table(conn)
return {r[0] for r in conn.execute("SELECT name FROM schema_migrations").fetchall()}
def up() -> list[str]:
done = []
conn = db.connect()
try:
applied = _applied(conn)
for name in _all_migrations():
if name in applied:
continue
up_sql, _ = _parse(os.path.join(MIGRATIONS_DIR, name))
conn.executescript(up_sql)
conn.execute("INSERT INTO schema_migrations (name) VALUES (?)", (name,))
conn.commit()
done.append(name)
return done
finally:
conn.close()
def down(target: int | None = None) -> list[str]:
"""Revert migrations. ``target`` is how many to keep applied (default: keep all-but-last)."""
reverted = []
conn = db.connect()
try:
applied = sorted(_applied(conn))
if target is None:
to_revert = applied[-1:] if applied else []
else:
to_revert = applied[target:]
for name in reversed(to_revert):
_, down_sql = _parse(os.path.join(MIGRATIONS_DIR, name))
conn.executescript(down_sql)
conn.execute("DELETE FROM schema_migrations WHERE name = ?", (name,))
conn.commit()
reverted.append(name)
return reverted
finally:
conn.close()
def status() -> None:
conn = db.connect()
try:
applied = _applied(conn)
for name in _all_migrations():
print(f"[{'x' if name in applied else ' '}] {name}")
finally:
conn.close()
def main(argv: list[str]) -> int:
cmd = argv[1] if len(argv) > 1 else "up"
if cmd == "up":
print("applied:", up() or "(nothing pending)")
elif cmd == "down":
tgt = int(argv[2]) if len(argv) > 2 else None
print("reverted:", down(tgt) or "(nothing to revert)")
elif cmd == "status":
status()
else:
print(__doc__)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))