"""foundation: users, groups, group_members, shares Revision ID: 0001 Revises: Create Date: 2026-07-19 The M0 identity + sharing-ACL spine. No notes yet (those arrive in M1); the shares table is polymorphic so notes reuse it without a schema retrofit. """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import CITEXT, UUID revision = "0001" down_revision = None branch_labels = None depends_on = None def upgrade() -> None: op.execute("CREATE EXTENSION IF NOT EXISTS citext") op.create_table( "users", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("email", CITEXT(), nullable=False, unique=True), sa.Column("email_verified", sa.Boolean(), nullable=False, server_default=sa.false()), sa.Column("password_hash", sa.Text(), nullable=True), sa.Column("display_name", sa.Text(), nullable=False), sa.Column("avatar_path", sa.Text(), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) op.create_index("ix_users_email", "users", ["email"]) op.create_table( "groups", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("name", sa.Text(), nullable=False), sa.Column("owner_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), ) op.create_table( "group_members", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("group_id", UUID(as_uuid=True), sa.ForeignKey("groups.id", ondelete="CASCADE"), nullable=False), sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False), sa.Column("role", sa.Text(), nullable=False, server_default="member"), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.UniqueConstraint("group_id", "user_id", name="uq_group_members_group_user"), ) op.create_index("ix_group_members_user", "group_members", ["user_id"]) op.create_table( "shares", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("resource_type", sa.Text(), nullable=False), sa.Column("resource_id", UUID(as_uuid=True), nullable=False), sa.Column("shared_with_user_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=True), sa.Column("shared_with_group_id", UUID(as_uuid=True), sa.ForeignKey("groups.id", ondelete="CASCADE"), nullable=True), sa.Column("permission", sa.Text(), nullable=False, server_default="view"), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.CheckConstraint( "(shared_with_user_id IS NOT NULL) <> (shared_with_group_id IS NOT NULL)", name="ck_shares_one_target", ), ) op.create_index("ix_shares_resource", "shares", ["resource_type", "resource_id"]) def downgrade() -> None: op.drop_index("ix_shares_resource", table_name="shares") op.drop_table("shares") op.drop_index("ix_group_members_user", table_name="group_members") op.drop_table("group_members") op.drop_table("groups") op.drop_index("ix_users_email", table_name="users") op.drop_table("users") # citext extension is left installed — it may be in use elsewhere.