Files
ss-tools/backend/alembic/versions/v1w2x3y4z5a6_add_scenario_registry_tables.py

183 lines
8.8 KiB
Python

# #region Alembic.ScenarioRegistryTables [C:3] [TYPE Module] [SEMANTICS alembic,scenario,registry,revision,lifecycle,staleness]
# @ingroup Alembic
# @BRIEF Add 042 scenario registry tables: entries, revisions, staleness signals, lifecycle audit.
# @LAYER Database
"""add scenario registry tables (042 dashboard-scenario-registry)
Revision ID: v1w2x3y4z5a6
Revises: u1v2w3x4y5z6
Create Date: 2026-08-19
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "v1w2x3y4z5a6"
down_revision: str | Sequence[str] | None = "u1v2w3x4y5z6"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Create scenario_registry_entries, scenario_revisions, scenario_staleness_signals, scenario_lifecycle_audit.
entry.current_revision_id is a plain indexed pointer (no FK): a circular FK
(revisions.scenario_id -> entries) would break sequential inserts under SQLite
FK enforcement and is unnecessary — revisions are append-only and pointer
integrity is owned by the activation service.
"""
# ── scenario_registry_entries ──
op.create_table(
"scenario_registry_entries",
sa.Column("scenario_id", sa.String(length=36), nullable=False),
sa.Column("scenario_key", sa.String(length=255), nullable=False),
sa.Column("name", sa.String(length=255), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("dashboard_id", sa.Integer(), nullable=False),
sa.Column("environment_ids", sa.JSON(), nullable=False),
sa.Column("owner_id", sa.String(length=128), nullable=False),
sa.Column("owner_username", sa.String(length=128), nullable=False),
sa.Column("tags", sa.JSON(), nullable=False),
sa.Column("metadata_version", sa.String(length=64), nullable=False),
sa.Column("current_revision_id", sa.String(length=36), nullable=True),
sa.Column("lifecycle_status", sa.String(length=32), nullable=False),
sa.Column("validation_status", sa.String(length=32), nullable=False),
sa.Column("last_run_id", sa.String(length=36), nullable=True),
sa.Column("last_successful_run_id", sa.String(length=36), nullable=True),
sa.Column("health", sa.String(length=16), nullable=False),
sa.Column("baseline_compatibility", sa.String(length=64), nullable=True),
sa.Column("source_scenario_id", sa.String(length=36), nullable=True),
sa.Column("source_revision_id", sa.String(length=36), nullable=True),
sa.Column("last_modified_at", sa.DateTime(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint("scenario_id"),
)
op.create_index("ix_scenario_registry_entries_key", "scenario_registry_entries", ["scenario_key"])
op.create_index(
"ix_scenario_registry_entries_dashboard", "scenario_registry_entries", ["dashboard_id"]
)
op.create_index(
"ix_scenario_registry_entries_owner", "scenario_registry_entries", ["owner_username"]
)
op.create_index(
"ix_scenario_registry_entries_lifecycle", "scenario_registry_entries", ["lifecycle_status"]
)
op.create_index(
"ix_scenario_registry_entries_current_revision",
"scenario_registry_entries",
["current_revision_id"],
)
# ── scenario_revisions ──
op.create_table(
"scenario_revisions",
sa.Column("revision_id", sa.String(length=36), nullable=False),
sa.Column("scenario_id", sa.String(length=36), nullable=False),
sa.Column("content_hash", sa.String(length=64), nullable=False),
sa.Column("parent_revision_id", sa.String(length=36), nullable=True),
sa.Column("graph_snapshot", sa.JSON(), nullable=False),
sa.Column("execution_template_hash", sa.String(length=64), nullable=False),
sa.Column("template_version", sa.String(length=32), nullable=False),
sa.Column("schema_version", sa.Integer(), nullable=False),
sa.Column("compatibility_family", sa.String(length=64), nullable=False),
sa.Column("change_summary", sa.JSON(), nullable=False),
sa.Column("created_by", sa.String(length=128), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("activation_status", sa.String(length=16), nullable=False),
sa.Column("activated_by", sa.String(length=128), nullable=True),
sa.Column("activated_at", sa.DateTime(), nullable=True),
sa.Column("activation_agent_action_id", sa.String(length=128), nullable=True),
sa.ForeignKeyConstraint(["scenario_id"], ["scenario_registry_entries.scenario_id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["parent_revision_id"], ["scenario_revisions.revision_id"], ondelete="SET NULL"),
sa.PrimaryKeyConstraint("revision_id"),
)
op.create_index("ix_scenario_revisions_scenario", "scenario_revisions", ["scenario_id"])
op.create_index(
"ix_scenario_revisions_activation", "scenario_revisions", ["activation_status"]
)
# ── entry.current_revision_id is a plain indexed pointer (no FK) — see upgrade() rationale ──
# ── scenario_staleness_signals ──
op.create_table(
"scenario_staleness_signals",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("scenario_id", sa.String(length=36), nullable=False),
sa.Column("source_type", sa.String(length=64), nullable=False),
sa.Column("source_fingerprint", sa.String(length=128), nullable=False),
sa.Column("kind", sa.String(length=32), nullable=False),
sa.Column("severity", sa.String(length=16), nullable=False),
sa.Column("reason", sa.Text(), nullable=False),
sa.Column("affected_ref", sa.String(length=255), nullable=False),
sa.Column("detected_at", sa.DateTime(), nullable=False),
sa.Column("resolved_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(["scenario_id"], ["scenario_registry_entries.scenario_id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"scenario_id",
"source_type",
"source_fingerprint",
"affected_ref",
name="uq_scenario_staleness_signal_identity",
),
)
op.create_index(
"ix_scenario_staleness_signals_scenario", "scenario_staleness_signals", ["scenario_id"]
)
# ── scenario_lifecycle_audit ──
op.create_table(
"scenario_lifecycle_audit",
sa.Column("id", sa.String(length=36), nullable=False),
sa.Column("scenario_id", sa.String(length=36), nullable=False),
sa.Column("from_state", sa.String(length=32), nullable=True),
sa.Column("to_state", sa.String(length=32), nullable=False),
sa.Column("actor_id", sa.String(length=128), nullable=False),
sa.Column("actor_type", sa.String(length=16), nullable=False),
sa.Column("reason", sa.Text(), nullable=True),
sa.Column("agent_action_id", sa.String(length=128), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(["scenario_id"], ["scenario_registry_entries.scenario_id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"ix_scenario_lifecycle_audit_scenario", "scenario_lifecycle_audit", ["scenario_id"]
)
op.create_index(
"ix_scenario_lifecycle_audit_created", "scenario_lifecycle_audit", ["created_at"]
)
def downgrade() -> None:
"""Drop the scenario registry tables (reverse order of FK dependencies)."""
op.drop_index("ix_scenario_lifecycle_audit_created", table_name="scenario_lifecycle_audit")
op.drop_index("ix_scenario_lifecycle_audit_scenario", table_name="scenario_lifecycle_audit")
op.drop_table("scenario_lifecycle_audit")
op.drop_index(
"ix_scenario_staleness_signals_scenario", table_name="scenario_staleness_signals"
)
op.drop_table("scenario_staleness_signals")
op.drop_index(
"ix_scenario_registry_entries_current_revision",
table_name="scenario_registry_entries",
)
op.drop_index("ix_scenario_revisions_activation", table_name="scenario_revisions")
op.drop_index("ix_scenario_revisions_scenario", table_name="scenario_revisions")
op.drop_table("scenario_revisions")
op.drop_index("ix_scenario_registry_entries_lifecycle", table_name="scenario_registry_entries")
op.drop_index("ix_scenario_registry_entries_owner", table_name="scenario_registry_entries")
op.drop_index(
"ix_scenario_registry_entries_dashboard", table_name="scenario_registry_entries"
)
op.drop_index("ix_scenario_registry_entries_key", table_name="scenario_registry_entries")
op.drop_table("scenario_registry_entries")
# #endregion Alembic.ScenarioRegistryTables