"""add tenant-scoped expense application memory Revision ID: 20260714_0005 Revises: 20260714_0004 Create Date: 2026-07-14 17:10:00 """ from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "20260714_0005" down_revision: str | None = "20260714_0004" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: op.create_unique_constraint( "uq_ai_decision_feedback_tenant_id", "ai_decision_feedback", ["tenant_id", "id"], ) op.create_unique_constraint( "uq_workflow_outcomes_tenant_id", "workflow_outcomes", ["tenant_id", "id"], ) op.create_table( "memory_entries", sa.Column("id", sa.String(length=36), nullable=False), sa.Column("tenant_id", sa.String(length=64), nullable=False), sa.Column( "scope_type", sa.String(length=20), server_default="user", nullable=False, ), sa.Column("scope_id", sa.String(length=120), nullable=False), sa.Column( "scene", sa.String(length=50), server_default="travel_application", nullable=False, ), sa.Column( "field_key", sa.String(length=60), server_default="transport_mode", nullable=False, ), sa.Column("generation", sa.Integer(), server_default="1", nullable=False), sa.Column("value_json", sa.JSON(), nullable=False), sa.Column("value_fingerprint", sa.String(length=80), nullable=False), sa.Column( "status", sa.String(length=20), server_default="candidate", nullable=False, ), sa.Column("evidence_count", sa.Integer(), server_default="0", nullable=False), sa.Column( "approved_evidence_count", sa.Integer(), server_default="0", nullable=False, ), sa.Column( "confidence", sa.Numeric(precision=5, scale=4), server_default="0", nullable=False, ), sa.Column( "last_evidence_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, ), sa.Column("candidate_expires_at", sa.DateTime(timezone=True), nullable=False), sa.Column("activated_at", sa.DateTime(timezone=True), nullable=True), sa.Column("active_expires_at", sa.DateTime(timezone=True), nullable=True), sa.Column("suppressed_at", sa.DateTime(timezone=True), nullable=True), sa.Column("expired_at", sa.DateTime(timezone=True), nullable=True), sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True), sa.Column("revoked_reason", sa.String(length=255), nullable=True), sa.Column("superseded_by_id", sa.String(length=36), nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, ), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, ), sa.CheckConstraint( "scope_type = 'user'", name="ck_memory_entries_scope_type", ), sa.CheckConstraint( "scene = 'travel_application'", name="ck_memory_entries_scene", ), sa.CheckConstraint( "field_key = 'transport_mode'", name="ck_memory_entries_field_key", ), sa.CheckConstraint( "status IN ('candidate', 'active', 'suppressed', 'expired', 'revoked')", name="ck_memory_entries_status", ), sa.CheckConstraint( "generation >= 1", name="ck_memory_entries_generation", ), sa.CheckConstraint( "evidence_count >= 0 AND approved_evidence_count >= 0 " "AND approved_evidence_count <= evidence_count", name="ck_memory_entries_evidence_counts", ), sa.CheckConstraint( "confidence >= 0 AND confidence <= 1", name="ck_memory_entries_confidence", ), sa.CheckConstraint( "candidate_expires_at > created_at", name="ck_memory_entries_candidate_expiry", ), sa.CheckConstraint( "active_expires_at IS NULL OR (" "activated_at IS NOT NULL AND active_expires_at > activated_at)", name="ck_memory_entries_active_expiry", ), sa.CheckConstraint( "status != 'active' OR (activated_at IS NOT NULL AND active_expires_at IS NOT NULL)", name="ck_memory_entries_active_fields", ), sa.CheckConstraint( "status != 'suppressed' OR suppressed_at IS NOT NULL", name="ck_memory_entries_suppressed_fields", ), sa.CheckConstraint( "status != 'expired' OR expired_at IS NOT NULL", name="ck_memory_entries_expired_fields", ), sa.CheckConstraint( "status != 'revoked' OR (revoked_at IS NOT NULL AND length(revoked_reason) > 0)", name="ck_memory_entries_revoked_fields", ), sa.CheckConstraint( "superseded_by_id IS NULL OR status IN ('suppressed', 'revoked')", name="ck_memory_entries_superseded_status", ), sa.CheckConstraint( "superseded_by_id IS NULL OR superseded_by_id != id", name="ck_memory_entries_not_self_superseded", ), sa.ForeignKeyConstraint( ["tenant_id", "superseded_by_id"], ["memory_entries.tenant_id", "memory_entries.id"], name="fk_memory_entries_tenant_superseded_by", ondelete="RESTRICT", ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint( "tenant_id", "id", name="uq_memory_entries_tenant_id", ), sa.UniqueConstraint( "tenant_id", "scope_type", "scope_id", "scene", "field_key", "generation", name="uq_memory_entries_generation", ), ) op.create_index( "ix_memory_entries_scope_lookup", "memory_entries", ["tenant_id", "scope_type", "scope_id", "scene", "field_key", "status"], ) op.create_index( "ix_memory_entries_status_expiry", "memory_entries", ["tenant_id", "status", "candidate_expires_at", "active_expires_at"], ) op.create_table( "memory_evidence_links", sa.Column("id", sa.String(length=36), nullable=False), sa.Column("tenant_id", sa.String(length=64), nullable=False), sa.Column("memory_entry_id", sa.String(length=36), nullable=False), sa.Column("expense_case_id", sa.String(length=36), nullable=False), sa.Column("decision_id", sa.String(length=36), nullable=False), sa.Column("feedback_id", sa.String(length=36), nullable=False), sa.Column("outcome_id", sa.String(length=36), nullable=False), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, ), sa.ForeignKeyConstraint( ["tenant_id", "memory_entry_id"], ["memory_entries.tenant_id", "memory_entries.id"], name="fk_memory_evidence_links_tenant_entry", ondelete="RESTRICT", ), sa.ForeignKeyConstraint( ["tenant_id", "expense_case_id"], ["expense_cases.tenant_id", "expense_cases.id"], name="fk_memory_evidence_links_tenant_case", ondelete="RESTRICT", ), sa.ForeignKeyConstraint( ["tenant_id", "decision_id"], ["ai_decisions.tenant_id", "ai_decisions.id"], name="fk_memory_evidence_links_tenant_decision", ondelete="RESTRICT", ), sa.ForeignKeyConstraint( ["tenant_id", "feedback_id"], ["ai_decision_feedback.tenant_id", "ai_decision_feedback.id"], name="fk_memory_evidence_links_tenant_feedback", ondelete="RESTRICT", ), sa.ForeignKeyConstraint( ["tenant_id", "outcome_id"], ["workflow_outcomes.tenant_id", "workflow_outcomes.id"], name="fk_memory_evidence_links_tenant_outcome", ondelete="RESTRICT", ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint( "tenant_id", "id", name="uq_memory_evidence_links_tenant_id", ), sa.UniqueConstraint( "tenant_id", "memory_entry_id", "expense_case_id", name="uq_memory_evidence_links_entry_case", ), ) op.create_index( "ix_memory_evidence_links_entry_time", "memory_evidence_links", ["tenant_id", "memory_entry_id", "created_at"], ) op.create_index( "ix_memory_evidence_links_sources", "memory_evidence_links", ["tenant_id", "decision_id", "feedback_id", "outcome_id"], ) def downgrade() -> None: op.drop_index( "ix_memory_evidence_links_sources", table_name="memory_evidence_links", ) op.drop_index( "ix_memory_evidence_links_entry_time", table_name="memory_evidence_links", ) op.drop_table("memory_evidence_links") op.drop_index( "ix_memory_entries_status_expiry", table_name="memory_entries", ) op.drop_index( "ix_memory_entries_scope_lookup", table_name="memory_entries", ) op.drop_table("memory_entries") op.drop_constraint( "uq_workflow_outcomes_tenant_id", "workflow_outcomes", type_="unique", ) op.drop_constraint( "uq_ai_decision_feedback_tenant_id", "ai_decision_feedback", type_="unique", )