Build reusable bot framework
Some checks failed
CI / test (push) Has been cancelled
CI / compose-smoke (push) Has been cancelled

This commit is contained in:
Chelsea Lee
2026-07-19 21:53:24 -05:00
parent 7ecc1107b2
commit fbdf33e894
66 changed files with 8428 additions and 0 deletions

202
core/migrations/__init__.py Normal file
View File

@@ -0,0 +1,202 @@
"""Versioned PostgreSQL migrations for the framework and feature modules."""
from dataclasses import dataclass
from hashlib import sha256
from pathlib import Path
import re
import psycopg2.extras
from core import postgres
MIGRATION_NAME = re.compile(r"^(?P<version>\d+)(?:[_-].*)?\.sql$")
LOCK_NAME = "llm-bot-framework:schema-migrations"
class MigrationError(RuntimeError):
"""Raised when migration history is inconsistent or cannot be applied."""
@dataclass(frozen=True)
class Migration:
namespace: str
version: int
name: str
path: Path
checksum: str
sql: str
def _project_root(project_root=None):
if project_root is not None:
return Path(project_root).resolve()
return Path(__file__).resolve().parents[2]
def _read_namespace(namespace, directory):
migrations = []
if not directory.is_dir():
return migrations
for path in sorted(directory.glob("*.sql")):
match = MIGRATION_NAME.match(path.name)
if not match:
raise MigrationError(f"Invalid migration filename: {path}")
sql = path.read_text(encoding="utf-8")
migrations.append(
Migration(
namespace=namespace,
version=int(match.group("version")),
name=path.name,
path=path,
checksum=sha256(sql.encode("utf-8")).hexdigest(),
sql=sql,
)
)
return migrations
def discover_migrations(project_root=None):
"""Return core and feature migrations in deterministic application order."""
root = _project_root(project_root)
migrations = _read_namespace("core", root / "config" / "migrations")
modules_root = root / "modules"
if modules_root.is_dir():
for module_path in sorted(modules_root.iterdir(), key=lambda path: path.name):
if module_path.is_dir() and not module_path.name.startswith("_"):
migrations.extend(
_read_namespace(module_path.name, module_path / "migrations")
)
seen = set()
for migration in migrations:
key = (migration.namespace, migration.version)
if key in seen:
raise MigrationError(
f"Duplicate migration {migration.namespace}:{migration.version}"
)
seen.add(key)
return sorted(
migrations,
key=lambda item: (
item.namespace != "core",
item.namespace,
item.version,
item.name,
),
)
def _ensure_history_table(cursor):
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS schema_migrations (
namespace VARCHAR(255) NOT NULL,
version INTEGER NOT NULL,
checksum CHAR(64) NOT NULL,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (namespace, version)
)
"""
)
def _applied_migrations(cursor):
cursor.execute(
"""
SELECT namespace, version, checksum, applied_at
FROM schema_migrations
ORDER BY namespace, version
"""
)
return {
(row["namespace"], row["version"]): dict(row) for row in cursor.fetchall()
}
def upgrade(project_root=None):
"""Apply pending migrations transactionally under a PostgreSQL advisory lock."""
migrations = discover_migrations(project_root)
applied_now = []
with postgres.get_connection() as connection:
with connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
cursor.execute("SELECT pg_advisory_xact_lock(hashtext(%s))", (LOCK_NAME,))
_ensure_history_table(cursor)
applied = _applied_migrations(cursor)
for migration in migrations:
key = (migration.namespace, migration.version)
previous = applied.get(key)
if previous:
if previous["checksum"] != migration.checksum:
raise MigrationError(
"Applied migration checksum changed: "
f"{migration.namespace}:{migration.version}"
)
continue
cursor.execute(migration.sql)
cursor.execute(
"""
INSERT INTO schema_migrations (namespace, version, checksum)
VALUES (%s, %s, %s)
""",
(migration.namespace, migration.version, migration.checksum),
)
applied_now.append(migration)
return applied_now
def migration_status(project_root=None):
"""Return applied, pending, changed, and source-missing migration records."""
migrations = discover_migrations(project_root)
with postgres.get_connection() as connection:
with connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
cursor.execute(
"SELECT to_regclass('public.schema_migrations') AS table_name"
)
if cursor.fetchone()["table_name"] is None:
applied = {}
else:
applied = _applied_migrations(cursor)
status = []
source_keys = set()
for migration in migrations:
key = (migration.namespace, migration.version)
source_keys.add(key)
record = applied.get(key)
state = "pending"
applied_at = None
if record:
state = "applied" if record["checksum"] == migration.checksum else "changed"
applied_at = record["applied_at"]
status.append(
{
"namespace": migration.namespace,
"version": migration.version,
"name": migration.name,
"checksum": migration.checksum,
"applied_at": applied_at,
"state": state,
}
)
for key, record in sorted(applied.items()):
if key not in source_keys:
status.append(
{
"namespace": key[0],
"version": key[1],
"name": None,
"checksum": record["checksum"],
"applied_at": record["applied_at"],
"state": "missing",
}
)
return status