Kansho/backend/db_init.py
Lars 79e4fd0d4b
All checks were successful
Deploy Development / deploy (push) Successful in 56s
Test Suite / pytest-backend (push) Successful in 2m43s
Test Suite / smoke-dev (push) Successful in 0s
Test Suite / frontend-build (push) Successful in 15s
Load the test schema once and truncate data between files.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 15:07:33 +02:00

168 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Wait for PostgreSQL, load schema.sql, apply numbered SQL migrations. Fail-fast."""
from __future__ import annotations
import os
import re
import sys
import time
from pathlib import Path
from sql_compat import postgres_connect_kwargs, split_sql, sqlite_schema_to_postgres, use_postgres
SCHEMA_PATH = Path(__file__).resolve().parent / "schema.sql"
MIGRATIONS_DIR = Path(__file__).resolve().parent / "migrations"
SQLITE_HISTORY_MIGRATIONS = (
"001_frame",
"002_platform",
"003_dialogue_memory",
"004_mvp_journal",
"005_provider_settings",
"006_writing_profile_dialogue_style",
"007_journal_day_scratch",
"008_journal_source_refs",
"009_conversation_signals",
"010_profile_governance",
"011_profile_review",
"012_profile_shell",
"013_journal_generate_narration",
"014_identity_registry",
"015_journal_generation_settings",
"016_generation_instruction_fragments",
"017_generation_guidelines",
"018_debug_runs",
"019_debug_run_placement",
"020_voice_style_context",
"021_voice_legacy_immutable",
)
_LEADING_DIGITS = re.compile(r"^(\d{3})_.*\.sql$")
_schema_ready = False
def mark_schema_dirty() -> None:
"""Call after DROP SCHEMA so the next init_db reloads schema.sql."""
global _schema_ready
_schema_ready = False
def _connect_raw():
import psycopg
kwargs = postgres_connect_kwargs()
if "conninfo" in kwargs:
return psycopg.connect(kwargs["conninfo"], autocommit=False)
return psycopg.connect(autocommit=False, **kwargs)
def wait_for_postgres(max_retries: int = 30) -> None:
print("Checking PostgreSQL connection...")
last_error = None
for attempt in range(1, max_retries + 1):
try:
conn = _connect_raw()
conn.close()
print("PostgreSQL ready")
return
except Exception as exc: # noqa: BLE001 — fail-fast after retries
last_error = exc
print(f" waiting ({attempt}/{max_retries})")
time.sleep(2)
print(f"PostgreSQL not ready: {last_error}")
sys.exit(1)
def _table_exists(conn, name: str) -> bool:
row = conn.execute(
"""
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = %s
""",
(name,),
).fetchone()
return row is not None
def _load_schema(conn) -> None:
sql = sqlite_schema_to_postgres(SCHEMA_PATH.read_text(encoding="utf-8"))
for statement in split_sql(sql):
conn.execute(statement)
print("Schema loaded from schema.sql (Postgres dialect)")
def _applied_ids(conn) -> set[str]:
rows = conn.execute("SELECT id FROM schema_migrations").fetchall()
return {row[0] for row in rows}
def _record(conn, migration_id: str) -> None:
conn.execute(
"INSERT INTO schema_migrations (id) VALUES (%s) ON CONFLICT (id) DO NOTHING",
(migration_id,),
)
def _migration_files() -> list[tuple[str, Path]]:
if not MIGRATIONS_DIR.is_dir():
return []
rows: list[tuple[str, Path]] = []
for path in sorted(MIGRATIONS_DIR.iterdir()):
match = _LEADING_DIGITS.match(path.name)
if not match or path.suffix != ".sql":
continue
rows.append((path.stem, path))
return rows
def apply_schema_and_migrations() -> None:
conn = _connect_raw()
try:
if not _table_exists(conn, "schema_migrations"):
_load_schema(conn)
conn.commit()
applied = _applied_ids(conn)
if "001_frame" not in applied:
for migration_id in SQLITE_HISTORY_MIGRATIONS:
_record(conn, migration_id)
conn.commit()
print("Recorded SQLite history 001-021 as applied (greenfield Postgres schema)")
applied = _applied_ids(conn)
for stem, path in _migration_files():
if stem in applied:
continue
sql = path.read_text(encoding="utf-8")
for statement in split_sql(sql):
conn.execute(statement)
_record(conn, stem)
conn.commit()
print(f"Applied migration {stem}")
except Exception as exc:
conn.rollback()
print(f"Database initialization failed: {exc}")
raise
finally:
conn.close()
def ensure_postgres_ready() -> None:
global _schema_ready
if not use_postgres():
return
if _schema_ready:
return
wait_for_postgres()
try:
apply_schema_and_migrations()
except Exception:
sys.exit(1)
_schema_ready = True
def main() -> None:
os.environ.setdefault("KANSHO_DB_BACKEND", "postgres")
ensure_postgres_ready()
print("db_init complete")
if __name__ == "__main__":
main()