mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(alembic): A5 migration 013 drops stray role postgres enum
Smoke run 2 (2026-05-11) produced 'UndefinedFunctionError: operator does not exist: agentrole = role' because postgres had two enums (role, agentrole) for the same Python class. Information_schema check confirms no column uses role; migration drops it. Upgrade() raises if that ever stops being true. Downgrade() recreates the enum with the foundation's Role values. Investigation of WHY a second enum got created is tracked in spec E2; this migration handles the symptom. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md section A5.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""Drop stray `role` postgres enum.
|
||||
|
||||
Smoke run 2 (2026-05-11) surfaced `UndefinedFunctionError: operator
|
||||
does not exist: agentrole = role` because postgres ended up with two
|
||||
enums (`role` and `agentrole`) for the same Python `Role` class. Only
|
||||
agents.role column uses `agentrole`; nothing uses `role`. This migration
|
||||
drops the orphan.
|
||||
|
||||
The cause (E2 in the spec) is a SQLAlchemy parameter-binding edge case
|
||||
where a query parameter wasn't bound with the column's enum name,
|
||||
letting SQLAlchemy infer a new type from the Python class name.
|
||||
Investigation tracked in E2; this migration handles the symptom.
|
||||
|
||||
Revision ID: 013_drop_role_enum
|
||||
Revises: 012_align_agentrole_foundation
|
||||
Create Date: 2026-05-12
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import context, op
|
||||
from sqlalchemy import text
|
||||
|
||||
revision = "013_drop_role_enum"
|
||||
down_revision = "012_align_agentrole_foundation"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Drop the orphan `role` enum after confirming no column uses it."""
|
||||
if context.is_offline_mode():
|
||||
# Offline mode: emit the SQL but skip the safety check.
|
||||
op.execute("DROP TYPE IF EXISTS role")
|
||||
return
|
||||
|
||||
conn = op.get_bind()
|
||||
# Safety check: no column may still reference the type
|
||||
result = conn.execute(
|
||||
text(
|
||||
"SELECT column_name, table_name "
|
||||
"FROM information_schema.columns "
|
||||
"WHERE udt_name = 'role'"
|
||||
)
|
||||
)
|
||||
rows = list(result)
|
||||
if rows:
|
||||
raise RuntimeError(
|
||||
f"refusing to DROP TYPE role: columns still reference it: {rows}"
|
||||
)
|
||||
|
||||
conn.execute(text("DROP TYPE IF EXISTS role"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Recreate the enum with the foundation's Role values.
|
||||
|
||||
Used if we ever need to revert this migration. No rows reference
|
||||
the type after the upgrade ran, so recreation is a no-op for app
|
||||
state.
|
||||
"""
|
||||
op.execute(
|
||||
"CREATE TYPE role AS ENUM ("
|
||||
"'system', 'developer', 'qa', 'documenter', 'cell_pm', 'main_pm', "
|
||||
"'product_owner', 'head_marketing', 'auditor', 'ceo'"
|
||||
")"
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Wave A5 (2026-05-12): migration 013 drops the stray `role` postgres enum.
|
||||
|
||||
Smoke run 2 produced `UndefinedFunctionError: operator does not exist:
|
||||
agentrole = role` because postgres had two enums for the same Python
|
||||
class. The migration drops the unused one with a safety check.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_column_uses_role_type(db_session) -> None: # type: ignore[no-untyped-def]
|
||||
"""Before dropping, confirm no column actually uses the `role` type.
|
||||
|
||||
If this ever fails it means a column was added that references the
|
||||
`role` enum and the migration must NOT be allowed to drop it — that
|
||||
column would become orphaned.
|
||||
"""
|
||||
result = await db_session.execute(
|
||||
text(
|
||||
"SELECT column_name, table_name "
|
||||
"FROM information_schema.columns "
|
||||
"WHERE udt_name = 'role'"
|
||||
)
|
||||
)
|
||||
rows = list(result)
|
||||
assert rows == [], (
|
||||
f"columns still use `role` enum: {rows}; migration 013 cannot run"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_role_enum_dropped_after_upgrade(db_session) -> None: # type: ignore[no-untyped-def]
|
||||
"""After migration 013 runs, only `agentrole` remains; `role` is gone."""
|
||||
# This test runs against a db where migrations have been applied to head.
|
||||
# The conftest fixture should handle that — verify by reading the
|
||||
# existing tests' conftest.
|
||||
result = await db_session.execute(
|
||||
text(
|
||||
"SELECT typname FROM pg_type "
|
||||
"WHERE typname IN ('role', 'agentrole')"
|
||||
)
|
||||
)
|
||||
rows = {row[0] for row in result}
|
||||
assert "agentrole" in rows, "agentrole must remain (it's the live enum)"
|
||||
assert "role" not in rows, "stray `role` enum should be dropped"
|
||||
Reference in New Issue
Block a user