"""Framework tables — users, roles, user_roles, permissions, audit_log, version_history. Revision ID: 0002_framework_tables Revises: 0001_baseline Create Date: 2026-09-30 Milestone 0.2 baseline. Creates the tables that every domain entity relies on via the vendored OWC framework. Deliberately kept close to OWC's own baseline so schema drift between the two systems stays reviewable. Per repo rule (CONTRIBUTING.md): this file is immutable once merged. Every subsequent change is a new revision, never an edit here. """ from __future__ import annotations from collections.abc import Sequence import sqlalchemy as sa from alembic import op revision: str = "0002_framework_tables" down_revision: str | Sequence[str] | None = "0001_baseline" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: op.create_table( "roles", sa.Column("id", sa.Integer(), primary_key=True), sa.Column("code", sa.String(length=64), nullable=False), sa.Column("description", sa.String(length=255), nullable=True), sa.UniqueConstraint("code", name="uq_roles_code"), ) op.create_index("ix_roles_code", "roles", ["code"], unique=True) op.create_table( "users", sa.Column("id", sa.Integer(), primary_key=True), sa.Column("username", sa.String(length=64), nullable=False), sa.Column("email", sa.String(length=255), nullable=True), sa.Column("full_name", sa.String(length=255), nullable=True), sa.Column("password_hash", sa.String(length=255), nullable=False), sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")), sa.Column("mfa_verified", sa.Boolean(), nullable=False, server_default=sa.text("false")), sa.Column( "assurance_level", sa.String(length=32), nullable=False, server_default=sa.text("'standard'"), ), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.UniqueConstraint("username", name="uq_users_username"), sa.UniqueConstraint("email", name="uq_users_email"), ) op.create_index("ix_users_username", "users", ["username"], unique=True) op.create_index("ix_users_email", "users", ["email"], unique=True) op.create_table( "user_roles", sa.Column("id", sa.Integer(), primary_key=True), sa.Column( "user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ), sa.Column( "role_id", sa.Integer(), sa.ForeignKey("roles.id", ondelete="CASCADE"), nullable=False, ), sa.UniqueConstraint("user_id", "role_id", name="uq_user_role"), ) op.create_index("ix_user_roles_user_id", "user_roles", ["user_id"]) op.create_index("ix_user_roles_role_id", "user_roles", ["role_id"]) op.create_table( "permissions", sa.Column("id", sa.Integer(), primary_key=True), sa.Column( "role_id", sa.Integer(), sa.ForeignKey("roles.id", ondelete="CASCADE"), nullable=False, ), sa.Column("entity", sa.String(length=64), nullable=False), sa.Column("action", sa.String(length=64), nullable=False), sa.Column("level", sa.String(length=4), nullable=False, server_default=sa.text("'C'")), sa.UniqueConstraint( "role_id", "entity", "action", name="uq_permission_role_entity_action" ), ) op.create_index("ix_permissions_role_id", "permissions", ["role_id"]) op.create_index("ix_permissions_entity", "permissions", ["entity"]) op.create_index("ix_permissions_action", "permissions", ["action"]) op.create_table( "audit_log", sa.Column("id", sa.Integer(), primary_key=True), sa.Column("ts", sa.DateTime(timezone=True), nullable=False), sa.Column( "actor_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ), sa.Column("actor_username", sa.String(length=64), nullable=True), sa.Column("action", sa.String(length=64), nullable=False), sa.Column("entity", sa.String(length=64), nullable=False), sa.Column("entity_id", sa.Integer(), nullable=True), sa.Column("before", sa.JSON(), nullable=True), sa.Column("after", sa.JSON(), nullable=True), sa.Column("ip", sa.String(length=64), nullable=True), ) op.create_index("ix_audit_log_ts", "audit_log", ["ts"]) op.create_index("ix_audit_log_actor_id", "audit_log", ["actor_id"]) op.create_index("ix_audit_log_action", "audit_log", ["action"]) op.create_index("ix_audit_log_entity", "audit_log", ["entity"]) op.create_index("ix_audit_log_entity_id", "audit_log", ["entity_id"]) op.create_table( "version_history", sa.Column("id", sa.Integer(), primary_key=True), sa.Column("ts", sa.DateTime(timezone=True), nullable=False), sa.Column("entity", sa.String(length=64), nullable=False), sa.Column("entity_id", sa.Integer(), nullable=False), sa.Column("version", sa.Integer(), nullable=False), sa.Column( "actor_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ), sa.Column("snapshot", sa.JSON(), nullable=False), ) op.create_index("ix_version_history_ts", "version_history", ["ts"]) op.create_index("ix_version_history_entity", "version_history", ["entity"]) op.create_index("ix_version_history_entity_id", "version_history", ["entity_id"]) def downgrade() -> None: for name in ( "version_history", "audit_log", "permissions", "user_roles", "users", "roles", ): op.drop_table(name)