run_setup: refuse an existing installation; remove --clean-slate
- Removed --clean-slate: it dropped every schema in the database, including user data. Nothing used it. - Pre-check (same rule as the Python client's init_postgres_db()): refuse if meta or log exists, series is not empty, or the prevent_series_drops trigger exists, naming the installed version. User schemas and tables are allowed and never touched; an empty series schema is reused. - --drop-existing drops every registered series schema plus meta and log, and refuses if a series schema holds non-series objects. - All files run in one transaction: a failure leaves nothing behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
eb0f93e032
commit
9032fd3a45
2 changed files with 109 additions and 48 deletions
145
run_setup.py
145
run_setup.py
|
|
@ -22,8 +22,12 @@ Connection settings come from (highest precedence first):
|
|||
Usage:
|
||||
python run_setup.py # use db_config.json / defaults
|
||||
python run_setup.py --host 192.168.1.7 --user claude --password claudepwd
|
||||
python run_setup.py --drop-existing # drop meta+series schemas first
|
||||
python run_setup.py --clean-slate # drop ALL user objects first
|
||||
python run_setup.py --drop-existing # drop the existing installation first
|
||||
# (all series!); user schemas untouched
|
||||
|
||||
A database that already holds the library's objects (meta, log, a non-empty
|
||||
series schema, the event trigger) is refused -- there are no migrations.
|
||||
User schemas and tables may exist and are never touched.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
|
@ -82,8 +86,7 @@ def execute_sql_file(conn, sql_file: Path):
|
|||
try:
|
||||
sql = sql_file.read_text(encoding="utf-8")
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(sql)
|
||||
conn.commit()
|
||||
cur.execute(sql) # committed by the caller, all files at once
|
||||
return True, f"[OK] Executed {sql_file.name}"
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
|
|
@ -122,23 +125,72 @@ def verify_setup(conn) -> None:
|
|||
print(f"[OK] schema_version: {row[0]} ({row[1]})")
|
||||
|
||||
|
||||
def clean_slate(conn) -> None:
|
||||
print("Performing clean slate...")
|
||||
# Pre-check, shared rule with the Python client's init_postgres_db():
|
||||
# provisioning needs the library's own objects to be ABSENT. User schemas and
|
||||
# tables are irrelevant and never touched. Never drop-and-recreate: an
|
||||
# existing installation is refused, because there are no migrations and
|
||||
# re-provisioning deletes all series.
|
||||
PRECHECK_SQL = """
|
||||
SELECT 'schema ' || nspname || ' exists'
|
||||
FROM pg_namespace WHERE nspname IN ('meta', 'log')
|
||||
UNION ALL
|
||||
SELECT 'schema series is not empty (' || count(*) || ' objects)'
|
||||
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname = 'series' AND c.relkind IN ('r', 'p', 'v', 'm', 'f', 'S')
|
||||
HAVING count(*) > 0
|
||||
UNION ALL
|
||||
SELECT 'event trigger prevent_series_drops exists'
|
||||
FROM pg_event_trigger WHERE evtname = 'prevent_series_drops'
|
||||
"""
|
||||
|
||||
|
||||
def existing_installation(conn) -> list:
|
||||
"""What would stop a fresh install; [] means go ahead."""
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT schema_name FROM information_schema.schemata "
|
||||
"WHERE schema_name NOT IN ('pg_catalog','information_schema','pg_toast','public') "
|
||||
"AND schema_name NOT LIKE 'pg_temp_%' AND schema_name NOT LIKE 'pg_toast_temp_%'"
|
||||
)
|
||||
for (schema,) in cur.fetchall():
|
||||
print(f" drop schema {schema}")
|
||||
cur.execute(PRECHECK_SQL)
|
||||
found = [r[0] for r in cur.fetchall()]
|
||||
cur.execute("SELECT to_regclass('meta.schema_version') IS NOT NULL")
|
||||
if cur.fetchone()[0]:
|
||||
cur.execute("SELECT string_agg(version, ', ') FROM meta.schema_version")
|
||||
version = cur.fetchone()[0]
|
||||
found.append(f"installed schema version: {version or 'none recorded'}")
|
||||
return found
|
||||
|
||||
|
||||
def drop_existing(conn) -> None:
|
||||
"""Drop the library's objects: every registered series schema, meta, log,
|
||||
the event trigger. Refuses if a series schema holds non-series objects --
|
||||
those are user data, which this never deletes."""
|
||||
with conn.cursor() as cur:
|
||||
schemas = ['series']
|
||||
cur.execute("SELECT to_regclass('meta.series_schemas') IS NOT NULL")
|
||||
if cur.fetchone()[0]:
|
||||
cur.execute("SELECT name FROM meta.series_schemas")
|
||||
schemas = sorted({r[0] for r in cur.fetchall()} | {'series'})
|
||||
cur.execute("SELECT to_regclass('meta.series') IS NOT NULL")
|
||||
has_registry = cur.fetchone()[0]
|
||||
stray = []
|
||||
for schema in schemas:
|
||||
cur.execute(
|
||||
"SELECT c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace "
|
||||
"WHERE n.nspname = %s AND c.relkind IN ('r','p','v','m','f','S')", (schema,))
|
||||
names = [r[0] for r in cur.fetchall()]
|
||||
if has_registry:
|
||||
cur.execute("SELECT table_name FROM meta.series WHERE db_schema = %s", (schema,))
|
||||
names = sorted(set(names) - {r[0] for r in cur.fetchall()})
|
||||
stray += [f"{schema}.{n}" for n in names]
|
||||
if stray:
|
||||
raise RuntimeError(
|
||||
"refusing to drop: these objects in series schemas are not series "
|
||||
f"(user data?): {', '.join(stray)}. Move or drop them first.")
|
||||
cur.execute("DROP EVENT TRIGGER IF EXISTS prevent_series_drops")
|
||||
for schema in schemas:
|
||||
cur.execute(f"DROP SCHEMA IF EXISTS {schema} CASCADE")
|
||||
cur.execute("SELECT evtname FROM pg_event_trigger")
|
||||
for (trig,) in cur.fetchall():
|
||||
cur.execute(f"DROP EVENT TRIGGER IF EXISTS {trig}")
|
||||
cur.execute("CREATE SCHEMA IF NOT EXISTS public")
|
||||
conn.commit()
|
||||
print("[OK] clean slate done\n")
|
||||
print(f" dropped schema {schema}")
|
||||
cur.execute("DROP SCHEMA IF EXISTS meta CASCADE")
|
||||
cur.execute("DROP SCHEMA IF EXISTS log CASCADE")
|
||||
conn.commit()
|
||||
print("[OK] existing installation dropped\n")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
|
@ -149,9 +201,8 @@ def main() -> int:
|
|||
p.add_argument("--user")
|
||||
p.add_argument("--password")
|
||||
p.add_argument("--drop-existing", action="store_true",
|
||||
help="Drop meta and series schemas before creating (destructive)")
|
||||
p.add_argument("--clean-slate", action="store_true",
|
||||
help="Drop ALL user objects before creating (very destructive)")
|
||||
help="Drop the existing installation first: every series schema "
|
||||
"(ALL series data), meta, log. User schemas are untouched.")
|
||||
p.add_argument("--yes", action="store_true", help="Skip confirmation prompts")
|
||||
args = p.parse_args()
|
||||
|
||||
|
|
@ -170,8 +221,8 @@ def main() -> int:
|
|||
print(f"Target : {cfg['host']}:{cfg['port']}/{cfg['database']} (user {cfg['user']})")
|
||||
print(f"SQL dir: {SQL_DIR}")
|
||||
|
||||
if (args.clean_slate or args.drop_existing) and not args.yes:
|
||||
what = "ALL user objects" if args.clean_slate else "meta and series schemas"
|
||||
if args.drop_existing and not args.yes:
|
||||
what = "the existing installation, including ALL series data"
|
||||
if input(f"This will DROP {what}. Continue? (yes/no): ").lower() != "yes":
|
||||
print("Aborted.")
|
||||
return 1
|
||||
|
|
@ -186,32 +237,34 @@ def main() -> int:
|
|||
return 1
|
||||
|
||||
try:
|
||||
if args.clean_slate:
|
||||
clean_slate(conn)
|
||||
elif args.drop_existing:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("DROP SCHEMA IF EXISTS meta CASCADE")
|
||||
cur.execute("DROP SCHEMA IF EXISTS series CASCADE")
|
||||
cur.execute("DROP SCHEMA IF EXISTS log CASCADE")
|
||||
cur.execute("DROP EVENT TRIGGER IF EXISTS prevent_series_drops")
|
||||
conn.commit()
|
||||
print("[OK] existing schemas dropped\n")
|
||||
if args.drop_existing:
|
||||
try:
|
||||
drop_existing(conn)
|
||||
except RuntimeError as e:
|
||||
print(f"[FAIL] {e}")
|
||||
return 1
|
||||
|
||||
failed = []
|
||||
existing = existing_installation(conn)
|
||||
if existing:
|
||||
print("[FAIL] the database already holds a tsapi installation:")
|
||||
for line in existing:
|
||||
print(f" - {line}")
|
||||
print(" There are no migrations. Re-provision with --drop-existing "
|
||||
"(deletes all series), or use another database.")
|
||||
return 1
|
||||
|
||||
missing = [n for n in SQL_FILES if not (SQL_DIR / n).exists()]
|
||||
if missing:
|
||||
print(f"[FAIL] missing {', '.join(missing)}")
|
||||
return 1
|
||||
# One transaction: a failure part-way leaves the database untouched.
|
||||
for name in SQL_FILES:
|
||||
f = SQL_DIR / name
|
||||
if not f.exists():
|
||||
print(f"[FAIL] missing {name}")
|
||||
failed.append(name)
|
||||
continue
|
||||
ok, msg = execute_sql_file(conn, f)
|
||||
ok, msg = execute_sql_file(conn, SQL_DIR / name)
|
||||
print(msg)
|
||||
if not ok:
|
||||
failed.append(name)
|
||||
|
||||
if failed:
|
||||
print(f"\n[FAIL] {len(failed)} file(s) failed: {', '.join(failed)}")
|
||||
return 1
|
||||
print(f"\n[FAIL] {name} failed; nothing was installed")
|
||||
return 1
|
||||
conn.commit()
|
||||
verify_setup(conn)
|
||||
print("\n[SUCCESS] schema setup complete")
|
||||
return 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue