Files

127 lines
4.3 KiB
Python
Raw Permalink Normal View History

2026-05-11 02:15:47 +02:00
"""Verify postgres agentrole + team enums match foundation/identity.
Run: `uv run python scripts/verify_postgres_enums.py`
2026-06-29 05:38:21 +02:00
Exit codes:
0 — match, OR skip (postgres unreachable / DB not migrated: both enum
types absent). A skip is not a failure: the gate only fails on real
drift against a schema that actually has the enum types.
1 — drift (the enum types exist but their labels differ from foundation).
An Alembic migration must be written to align postgres with foundation.
2026-05-11 02:15:47 +02:00
2026-06-29 05:38:21 +02:00
Reads DB connection from roboco.config. Skip semantics live IN the script
(exit 0) so the Makefile gate can't mask a real drift exit as "skipped".
2026-05-11 02:15:47 +02:00
"""
from __future__ import annotations
import asyncio
import sys
import asyncpg
from roboco.config import settings
from roboco.foundation import identity
async def fetch_enum_values(conn: asyncpg.Connection, enum_name: str) -> set[str]:
rows = await conn.fetch(
"""
SELECT e.enumlabel
FROM pg_enum e
JOIN pg_type t ON e.enumtypid = t.oid
WHERE t.typname = $1
ORDER BY e.enumsortorder
""",
enum_name,
)
return {r["enumlabel"] for r in rows}
2026-06-29 05:38:21 +02:00
async def type_exists(conn: asyncpg.Connection, enum_name: str) -> bool:
"""True iff a postgres enum type named ``enum_name`` exists."""
return bool(
await conn.fetchval(
"SELECT EXISTS (SELECT 1 FROM pg_type WHERE typname = $1)", enum_name
)
)
def should_skip_for_unmigrated(agentrole_exists: bool, team_exists: bool) -> bool:
"""Skip only when BOTH enum types are absent (DB schema not migrated).
A partial schema (one type present, the other absent) is itself drift —
don't skip; let ``enum_drift`` report it. An empty/unmigrated DB (both
absent) has no migrated target to check, so skipping is correct.
"""
return not agentrole_exists and not team_exists
def enum_drift(
db_roles: set[str],
db_teams: set[str],
foundation_roles: set[str],
foundation_teams: set[str],
) -> tuple[bool, list[str]]:
"""Compare DB enum labels to foundation; return (has_drift, messages)."""
messages: list[str] = []
missing_roles = foundation_roles - db_roles
extra_roles = db_roles - foundation_roles
missing_teams = foundation_teams - db_teams
extra_teams = db_teams - foundation_teams
if missing_roles:
messages.append(f"DRIFT: postgres agentrole missing: {sorted(missing_roles)}")
if extra_roles:
messages.append(f"DRIFT: postgres agentrole has extra: {sorted(extra_roles)}")
if missing_teams:
messages.append(f"DRIFT: postgres team missing: {sorted(missing_teams)}")
if extra_teams:
messages.append(f"DRIFT: postgres team has extra: {sorted(extra_teams)}")
return (bool(messages), messages)
2026-05-11 02:15:47 +02:00
async def main() -> int:
try:
conn = await asyncpg.connect(
host=settings.database_host,
port=settings.database_port,
user=settings.database_user,
password=settings.database_password,
database=settings.database_name,
)
except (OSError, asyncpg.PostgresError) as exc:
print(f"postgres unreachable: {exc}")
2026-06-29 05:38:21 +02:00
print(" (skipped — postgres unreachable)")
return 0
2026-05-11 02:15:47 +02:00
try:
2026-06-29 05:38:21 +02:00
agentrole_exists = await type_exists(conn, "agentrole")
team_exists = await type_exists(conn, "team")
if should_skip_for_unmigrated(agentrole_exists, team_exists):
print(
" (skipped — DB schema not migrated: agentrole/team enum types absent)"
)
return 0
2026-05-11 02:15:47 +02:00
agentrole_db = await fetch_enum_values(conn, "agentrole")
team_db = await fetch_enum_values(conn, "team")
finally:
await conn.close()
foundation_roles = {r.value for r in identity.Role}
foundation_teams = {t.value for t in identity.Team}
2026-06-29 05:38:21 +02:00
has_drift, messages = enum_drift(
agentrole_db, team_db, foundation_roles, foundation_teams
)
if has_drift:
for line in messages:
print(line)
2026-05-11 02:15:47 +02:00
print("Write an Alembic migration to align postgres with foundation.")
return 1
print(
f"OK: agentrole has {len(agentrole_db)} values; team has {len(team_db)} values."
)
return 0
if __name__ == "__main__":
sys.exit(asyncio.run(main()))