#!/usr/bin/env python3 """Explicit task-v2 schema migration. Default is read-only; --apply backs up first.""" import argparse import datetime import json from pathlib import Path import sqlite3 def migrate(database: Path, apply: bool = False) -> dict: if not database.is_file(): raise ValueError("database must already exist") with sqlite3.connect(database.resolve().as_uri() + ("?mode=rw" if apply else "?mode=ro"), uri=True) as db: if not db.execute("pragma table_info(gallery)").fetchall(): raise ValueError("required table missing: gallery") duplicates = db.execute("select count(*) from (select gid from gallery group by gid having count(*)>1)").fetchone()[0] if duplicates: raise ValueError("duplicate gallery gid; reconcile before migration") present = bool(db.execute("pragma table_info(task_operation)").fetchall()) result = {"apply": apply, "create_operation_table": not present} if not apply: return result stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%S%fZ") backup = database.with_name(database.name + ".before-task-v2." + stamp) with sqlite3.connect(backup) as copy: db.backup(copy) backup.chmod(0o600) sql = Path(__file__).with_name("migrate_task_operation_v2.sql").read_text() try: db.executescript("BEGIN IMMEDIATE;\n" + sql + "\nCOMMIT;") except BaseException: db.rollback() raise result["backup"] = str(backup) result["operation_rows"] = db.execute("select count(*) from task_operation").fetchone()[0] return result if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("database", type=Path) parser.add_argument("--apply", action="store_true") arguments = parser.parse_args() print(json.dumps(migrate(arguments.database, arguments.apply), ensure_ascii=False))