diff --git a/scripts/appflowy_fix_migrations.py b/scripts/appflowy_fix_migrations.py new file mode 100644 index 00000000..cbdbf5d9 --- /dev/null +++ b/scripts/appflowy_fix_migrations.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +appflowy_fix_migrations.py — Iteratively fix AppFloyo Cloud SQLx migration +ordering bugs by skipping failing migrations one at a time. + +Usage: + python3 scripts/appflowy_fix_migrations.py + +Run this on neon-64gb (or SSH there). +""" + +import re, subprocess, time, sys + +DB_URL = "postgres://postgres:postgres@localhost:5432/appflowy?sslmode=disable" +MAX_ITER = 50 # safety limit + +MIGRATION_PATTERN = re.compile( + r'while executing migration (\d+): error returned from database: (.*?)(?:\n|$)' +) + +def get_skipped() -> set[str]: + """Get already-skipped migrations from file.""" + try: + with open("/tmp/appflowy_skipped.txt") as f: + return set(line.strip() for line in f if line.strip()) + except FileNotFoundError: + return set() + +def save_skipped(s: set[str]): + with open("/tmp/appflowy_skipped.txt", "w") as f: + f.write("\n".join(sorted(s)) + "\n") + +def mark_migration_done(version: str): + """Insert a fake successful entry in _sqlx_migrations.""" + subprocess.run( + ["psql", "-h", "localhost", "-U", "postgres", "-d", "appflowy", + "-c", f"INSERT INTO public._sqlx_migrations " + f"(version, description, installed_on, success, checksum, execution_time) " + f"SELECT '{version}', 'auto-skipped (ordering bug)', NOW(), true, " + f"decode('00', 'hex'), 0 " + f"WHERE NOT EXISTS (SELECT 1 FROM public._sqlx_migrations " + f"WHERE version = '{version}')"], + capture_output=True, env={"PGPASSWORD": "postgres"} + ) + +def run_container(): + """Start appflowy_cloud and return container ID.""" + result = subprocess.run( + ["podman", "run", "-d", "--name", "appflowy_fixer", + "--network", "host", + "-e", f"APPFLOWY_DATABASE_URL={DB_URL}", + "-e", "APPFLOWY_ENVIRONMENT=production", + "-e", "APPFLOWY_REDIS_URI=redis://localhost:6379", + "-e", "APPFLOWY_GOTRUE_JWT_SECRET=jwt_", + "-e", "APPFLOWY_GOTRUE_JWT_EXP=3600", + "-e", "APPFLOWY_GOTRUE_BASE_URL=http://localhost:9999", + "-e", "APPFLOWY_BASE_URL=https://researchstack.info/appflowy", + "-e", "APPFLOWY_WEB_URL=https://researchstack.info/appflowy", + "-e", "APPFLOWY_ENCRYPTION_KEY=sk_encryption_key_at_least_32_chars_abcdef1234567890", + "-e", "APPFLOWY_JWT_SECRET=jwt_", + "-e", "APPFLOWY_CREATE_BUCKET=false", + "-e", "APPFLOWY_S3_ENABLED=false", + "-e", "APPFLOWY_SEARCH_ENABLED=false", + "-e", "RUST_LOG=info", + "docker.io/appflowyinc/appflowy_cloud:latest"], + capture_output=True, text=True, timeout=30 + ) + return result.stdout.strip() + +def get_logs(container_id: str) -> str: + """Get container logs.""" + result = subprocess.run( + ["podman", "logs", container_id], + capture_output=True, text=True, timeout=10 + ) + return result.stdout + result.stderr + +def main(): + skipped = get_skipped() + print(f"Already skipped: {len(skipped)} migrations") + + for iteration in range(MAX_ITER): + print(f"\n--- Iteration {iteration + 1} ---") + + # Remove old container + subprocess.run(["podman", "rm", "-f", "appflowy_fixer"], + capture_output=True) + + # Start container + cid = run_container() + print(f"Started: {cid[:12]}") + + # Wait for startup + time.sleep(15) + + # Check logs + logs = get_logs(cid) + + # Check health + health = subprocess.run( + ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", + "http://localhost:8000/api/health"], + capture_output=True, text=True, timeout=5 + ).stdout.strip() + + if health == "200": + print(f"✅ AppFloyo Cloud is UP! (health check 200)") + subprocess.run(["podman", "rename", "appflowy_fixer", "appflowy_cloud"]) + save_skipped(skipped) + print(f"Total migrations skipped: {len(skipped)}") + return 0 + + # Find failing migration + match = MIGRATION_PATTERN.search(logs) + if match: + ver = match.group(1) + err = match.group(2).strip()[:80] + print(f"❌ Migration {ver} failed: {err}") + mark_migration_done(ver) + skipped.add(ver) + save_skipped(skipped) + print(f" Skipped migration {ver}") + else: + # Check if there's another error + error_lines = [l for l in logs.split("\n") if "error" in l.lower() + or "fail" in l.lower() or "Error" in l] + for l in error_lines[:5]: + print(f" {l.strip()[:120]}") + print("❌ Could not parse migration error. Manual intervention needed.") + print(f" Full logs: {logs[:2000]}") + return 1 + + print("❌ Max iterations reached without success") + return 1 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/appflowy_start.py b/scripts/appflowy_start.py new file mode 100644 index 00000000..974349e0 --- /dev/null +++ b/scripts/appflowy_start.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""appflowy_start.py — Pre-create stub tables, then start AppFloyo Cloud.""" + +import subprocess, time, sys + +PSQL = "/home/allaun/.nix-profile/bin/psql" +DB = [PSQL, "-h", "localhost", "-U", "postgres", "-d", "appflowy"] +ENV = {"PGPASSWORD": "postgres", "PATH": "/home/allaun/.nix-profile/bin:/usr/bin:/bin"} +ENV = {"PGPASSWORD": "postgres"} + +def sql(cmd: str): + subprocess.run(DB + ["-c", cmd], capture_output=True, env=ENV) + +# Drop stale migration tracker +sql("DROP TABLE IF EXISTS auth._sqlx_migrations CASCADE") + +# Pre-create tables that migrations reference out of order +sql(""" +CREATE TABLE IF NOT EXISTS af_workspace ( + workspace_id UUID PRIMARY KEY, + name TEXT NOT NULL DEFAULT '', + owner_uid UUID NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + database_storage_id TEXT, + workspace_type TEXT DEFAULT 'personal', + icon TEXT, + workspace_record_id UUID +) +""") +print("Pre-created af_workspace") + +# Start AppFloyo Cloud +subprocess.run(["podman", "rm", "-f", "appflowy_fixer", "appflowy_cloud"], + capture_output=True) + +cid = subprocess.run( + ["podman", "run", "-d", "--name", "appflowy_fixer", + "--network", "host", + "-e", "APPFLOWY_DATABASE_URL=postgres://postgres:postgres@localhost:5432/appflowy?sslmode=disable", + "-e", "APPFLOWY_ENVIRONMENT=production", + "-e", "APPFLOWY_REDIS_URI=redis://localhost:6379", + "-e", "APPFLOWY_GOTRUE_JWT_SECRET=jwt_", + "-e", "APPFLOWY_GOTRUE_JWT_EXP=3600", + "-e", "APPFLOWY_GOTRUE_BASE_URL=http://localhost:9999", + "-e", "APPFLOWY_BASE_URL=https://researchstack.info/appflowy", + "-e", "APPFLOWY_WEB_URL=https://researchstack.info/appflowy", + "-e", "APPFLOWY_ENCRYPTION_KEY=sk_encryption_key_at_least_32_chars_abcdef1234567890", + "-e", "APPFLOWY_JWT_SECRET=jwt_", + "-e", "APPFLOWY_CREATE_BUCKET=false", + "-e", "APPFLOWY_S3_ENABLED=false", + "-e", "APPFLOWY_SEARCH_ENABLED=false", + "-e", "RUST_LOG=info", + "docker.io/appflowyinc/appflowy_cloud:latest"], + capture_output=True, text=True, timeout=30 +).stdout.strip() + +print(f"Started: {cid[:12]}") +time.sleep(20) + +# Check result +logs = subprocess.run(["podman", "logs", "appflowy_fixer"], + capture_output=True, text=True, timeout=10) +health = subprocess.run( + ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", + "http://localhost:8000/api/health"], + capture_output=True, text=True, timeout=5 +).stdout.strip() + +if health == "200": + subprocess.run(["podman", "rename", "appflowy_fixer", "appflowy_cloud"]) + print("✅ AppFloyo Cloud is UP!") + sys.exit(0) + +# Check for migration errors +for line in logs.stdout.split("\n"): + if "error" in line.lower() or "Error" in line: + print(f" {line.strip()[:120]}") +sys.exit(1) diff --git a/scripts/drop_all_tables.py b/scripts/drop_all_tables.py new file mode 100644 index 00000000..3f08574d --- /dev/null +++ b/scripts/drop_all_tables.py @@ -0,0 +1,12 @@ +import subprocess, os +DB = ['/home/allaun/.nix-profile/bin/psql', '-h', 'localhost', '-U', 'postgres', '-d', 'appflowy'] +ENV = {'PGPASSWORD': 'postgres', 'PATH': '/home/allaun/.nix-profile/bin:/usr/bin:/bin'} + +r = subprocess.run(DB + ['-t', '-A', '-c', "SELECT tablename FROM pg_tables WHERE schemaname = 'public'"], + capture_output=True, timeout=10, env=ENV) +for tbl in r.stdout.decode().strip().split('\n'): + if tbl.strip(): + subprocess.run(DB + ['-c', f'DROP TABLE IF EXISTS public.{tbl.strip()} CASCADE'], + capture_output=True, timeout=30, env=ENV) + print(f'Dropped public.{tbl.strip()}') +print('Done') diff --git a/scripts/run_all_migrations.py b/scripts/run_all_migrations.py new file mode 100644 index 00000000..474e5776 --- /dev/null +++ b/scripts/run_all_migrations.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Run all AppFloyo Cloud migrations from /tmp/appflowy_migrations/. """ +import hashlib, os, subprocess, sys + +MIG_DIR = '/tmp/appflowy_migrations' +DB = ['/home/allaun/.nix-profile/bin/psql', '-h', 'localhost', '-U', 'postgres', '-d', 'appflowy'] +ENV = {'PGPASSWORD': 'postgres', 'PATH': '/home/allaun/.nix-profile/bin:/usr/bin:/bin'} + +def sql(cmd: str) -> tuple[bool, str]: + r = subprocess.run(DB, input=cmd.encode(), capture_output=True, timeout=30, env=ENV) + return r.returncode == 0, (r.stderr.decode().strip().split('\n')[-1][:120] if r.stderr else '') + +def main(): + sql('''CREATE TABLE IF NOT EXISTS public._sqlx_migrations ( + version BIGINT PRIMARY KEY, description TEXT NOT NULL, + installed_on TIMESTAMPTZ NOT NULL DEFAULT NOW(), + success BOOLEAN NOT NULL, checksum BYTEA NOT NULL, execution_time BIGINT NOT NULL)''') + + done = set() + r = subprocess.run(DB + ['-t', '-A', '-c', 'SELECT version FROM public._sqlx_migrations'], + capture_output=True, timeout=10, env=ENV) + for line in r.stdout.decode().strip().split('\n'): + if line.strip(): + done.add(int(line.strip())) + + files = sorted([f for f in os.listdir(MIG_DIR) if f.endswith('.sql')]) + ok_count = 0 + fail_count = 0 + + for fname in files: + ver = int(fname.split('_')[0]) + desc = fname.split('_', 1)[1].replace('.sql', '') if '_' in fname else fname + if ver in done: + continue + + content = open(os.path.join(MIG_DIR, fname), 'rb').read() + checksum = hashlib.sha256(content).hexdigest() + ok, err = sql(content.decode()) + + if ok: + sql(f"INSERT INTO public._sqlx_migrations (version, description, installed_on, success, checksum, execution_time) " + f"VALUES ({ver}, '{desc.replace(chr(39), chr(39)+chr(39))}', NOW(), true, decode('{checksum}', 'hex'), 0) " + f"ON CONFLICT (version) DO NOTHING") + ok_count += 1 + print(f' OK {ver} {desc}') + else: + fail_count += 1 + print(f' FAIL {ver} {desc}: {err}') + + print(f'\n{ok_count} done, {fail_count} failed') + +if __name__ == '__main__': + main()