Rewrite durable task lifecycle, gallery indexing and subscription publication
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Add stable user identity to subscription bindings and access records.
|
||||
|
||||
The default invocation only reports schema changes. --apply creates a consistent
|
||||
SQLite backup before the transaction. Run against an isolated database first.
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
|
||||
|
||||
def migrate(db_path: Path, apply: bool = False) -> dict:
|
||||
if not db_path.is_file():
|
||||
raise ValueError("database must already exist")
|
||||
uri = db_path.resolve().as_uri() + ("?mode=rw" if apply else "?mode=ro")
|
||||
with sqlite3.connect(uri, uri=True) as db:
|
||||
changes = []
|
||||
for table in ("sub_bind", "sub_update_record"):
|
||||
columns = {row[1] for row in db.execute(f'PRAGMA table_info("{table}")')}
|
||||
if not columns:
|
||||
raise ValueError(f"required table missing: {table}")
|
||||
if "user_id" not in columns:
|
||||
changes.append(f"add {table}.user_id")
|
||||
duplicates = db.execute(
|
||||
"select count(*) from (select username from User group by username having count(*) > 1)"
|
||||
).fetchone()[0]
|
||||
if duplicates:
|
||||
raise ValueError("ambiguous duplicate usernames; resolve identities before migration")
|
||||
result = {"apply": apply, "schema_changes": changes}
|
||||
if not apply:
|
||||
return result
|
||||
stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
|
||||
backup = db_path.with_name(db_path.name + ".before-user-links." + stamp)
|
||||
with sqlite3.connect(backup) as snapshot:
|
||||
db.backup(snapshot)
|
||||
backup.chmod(0o600)
|
||||
db.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
for change in changes:
|
||||
table = change.split()[1].split(".")[0]
|
||||
db.execute(f'ALTER TABLE "{table}" ADD COLUMN user_id INTEGER REFERENCES User(id)')
|
||||
for table in ("sub_bind", "sub_update_record"):
|
||||
db.execute(f'UPDATE "{table}" SET user_id=(SELECT id FROM User WHERE username="{table}".user) WHERE user_id IS NULL')
|
||||
db.execute("CREATE UNIQUE INDEX IF NOT EXISTS User_username_uindex ON User(username)")
|
||||
db.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_sub_bind_user_id ON sub_bind(user_id) WHERE user_id IS NOT NULL")
|
||||
db.execute("CREATE INDEX IF NOT EXISTS idx_sub_record_user_id ON sub_update_record(user_id, id)")
|
||||
db.commit()
|
||||
except BaseException:
|
||||
db.rollback()
|
||||
raise
|
||||
result["backup"] = str(backup)
|
||||
result["unresolved_bindings"] = db.execute("select count(*) from sub_bind where user_id is null").fetchone()[0]
|
||||
result["unresolved_records"] = db.execute("select count(*) from sub_update_record where user_id is null").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")
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(migrate(args.database, args.apply), ensure_ascii=False))
|
||||
Reference in New Issue
Block a user