"""Initial OpenCloud schema. Revision ID: 20260718_0001 Revises: Create Date: 2026-07-18 """ from typing import Sequence from alembic import op import sqlalchemy as sa revision: str = "20260718_0001" down_revision: str | None = None branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: op.create_table( "users", sa.Column("id", sa.Uuid(), nullable=False), sa.Column("email", sa.Text(), nullable=False), sa.Column("password_hash", sa.Text(), nullable=False), sa.Column("email_verified_at", sa.DateTime(timezone=True), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("email"), ) op.create_table( "cloud_types", sa.Column("id", sa.SmallInteger(), autoincrement=False, nullable=False), sa.Column("name", sa.Text(), nullable=False), sa.Column("name_en", sa.Text(), nullable=False), sa.Column("genus", sa.Text(), nullable=False), sa.Column("rarity", sa.Text(), nullable=False), sa.Column("description", sa.Text(), nullable=True), sa.Column("icon_url", sa.Text(), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.CheckConstraint("rarity IN ('common', 'uncommon', 'rare')", name="ck_cloud_types_rarity"), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("name"), sa.UniqueConstraint("name_en"), ) op.create_table( "profiles", sa.Column("id", sa.Uuid(), nullable=False), sa.Column("username", sa.Text(), nullable=False), sa.Column("avatar_path", sa.Text(), nullable=True), sa.Column("role", sa.Text(), server_default="user", nullable=False), sa.Column("is_disabled", sa.Boolean(), server_default=sa.text("false"), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.CheckConstraint("length(trim(username)) >= 2", name="ck_profiles_username_length"), sa.CheckConstraint("role IN ('user', 'admin')", name="ck_profiles_role"), sa.ForeignKeyConstraint(["id"], ["users.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("username"), ) op.create_table( "refresh_sessions", sa.Column("id", sa.Uuid(), nullable=False), sa.Column("user_id", sa.Uuid(), nullable=False), sa.Column("token_hash", sa.Text(), nullable=False), sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True), sa.Column("replaced_by_id", sa.Uuid(), nullable=True), sa.Column("user_agent", sa.Text(), nullable=True), sa.Column("ip_address", sa.Text(), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.ForeignKeyConstraint(["replaced_by_id"], ["refresh_sessions.id"], ondelete="SET NULL"), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("token_hash"), ) op.create_index("ix_refresh_sessions_user_id", "refresh_sessions", ["user_id"]) op.create_index("ix_refresh_sessions_replaced_by_id", "refresh_sessions", ["replaced_by_id"]) op.create_index( "ix_refresh_sessions_active_user", "refresh_sessions", ["user_id", "expires_at"], postgresql_where=sa.text("revoked_at IS NULL"), ) op.create_table( "email_tokens", sa.Column("id", sa.Uuid(), nullable=False), sa.Column("user_id", sa.Uuid(), nullable=False), sa.Column("purpose", sa.Text(), nullable=False), sa.Column("token_hash", sa.Text(), nullable=False), sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), sa.Column("used_at", sa.DateTime(timezone=True), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.CheckConstraint("purpose IN ('confirm_email', 'reset_password')", name="ck_email_tokens_purpose"), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("token_hash"), ) op.create_index("ix_email_tokens_user_purpose", "email_tokens", ["user_id", "purpose"]) op.create_table( "clouds", sa.Column("id", sa.Uuid(), nullable=False), sa.Column("user_id", sa.Uuid(), nullable=False), sa.Column("cloud_type_id", sa.SmallInteger(), nullable=True), sa.Column("custom_cloud_type", sa.Text(), nullable=True), sa.Column("image_path", sa.Text(), nullable=False), sa.Column("thumbnail_path", sa.Text(), nullable=False), sa.Column("latitude", sa.Numeric(4, 2), nullable=True), sa.Column("longitude", sa.Numeric(5, 2), nullable=True), sa.Column("location_name", sa.Text(), nullable=True), sa.Column("description", sa.Text(), nullable=True), sa.Column("captured_at", sa.DateTime(timezone=True), nullable=True), sa.Column("status", sa.Text(), server_default="pending", nullable=False), sa.Column("is_hidden", sa.Boolean(), server_default=sa.text("false"), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.CheckConstraint("status IN ('pending', 'approved', 'rejected')", name="ck_clouds_status"), sa.CheckConstraint( "((latitude IS NULL AND longitude IS NULL) OR (latitude IS NOT NULL AND longitude IS NOT NULL))", name="ck_clouds_coordinate_pair", ), sa.CheckConstraint("latitude IS NULL OR latitude BETWEEN -90 AND 90", name="ck_clouds_latitude"), sa.CheckConstraint("longitude IS NULL OR longitude BETWEEN -180 AND 180", name="ck_clouds_longitude"), sa.CheckConstraint( "((cloud_type_id IS NOT NULL AND custom_cloud_type IS NULL) OR " "(cloud_type_id IS NULL AND custom_cloud_type IS NOT NULL AND length(trim(custom_cloud_type)) > 0))", name="ck_clouds_type_choice", ), sa.ForeignKeyConstraint(["cloud_type_id"], ["cloud_types.id"], ondelete="SET NULL"), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("image_path"), sa.UniqueConstraint("thumbnail_path"), ) op.create_index("ix_clouds_user_id", "clouds", ["user_id"]) op.create_index("ix_clouds_cloud_type_id", "clouds", ["cloud_type_id"]) op.create_index( "ix_clouds_user_captured", "clouds", ["user_id", sa.text("captured_at DESC"), sa.text("created_at DESC")], ) op.create_index("ix_clouds_status_created", "clouds", ["status", sa.text("created_at DESC")]) op.create_index( "ix_clouds_public_gallery", "clouds", [sa.text("created_at DESC"), sa.text("id DESC")], postgresql_where=sa.text("status = 'approved' AND is_hidden = false"), ) op.create_index( "ix_clouds_public_map", "clouds", ["captured_at"], postgresql_where=sa.text( "status = 'approved' AND is_hidden = false AND latitude IS NOT NULL AND longitude IS NOT NULL" ), ) op.create_index( "ix_clouds_public_type", "clouds", ["cloud_type_id", sa.text("captured_at DESC")], postgresql_where=sa.text("status = 'approved' AND is_hidden = false"), ) op.create_table( "user_collections", sa.Column("id", sa.Uuid(), nullable=False), sa.Column("user_id", sa.Uuid(), nullable=False), sa.Column("cloud_type_id", sa.SmallInteger(), nullable=False), sa.Column("first_cloud_id", sa.Uuid(), nullable=True), sa.Column("unlocked_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False), sa.ForeignKeyConstraint(["cloud_type_id"], ["cloud_types.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["first_cloud_id"], ["clouds.id"], ondelete="SET NULL"), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint("user_id", "cloud_type_id", name="uq_user_collections_user_type"), ) op.create_index("ix_user_collections_user_id", "user_collections", ["user_id"]) op.create_index("ix_user_collections_cloud_type_id", "user_collections", ["cloud_type_id"]) op.create_index("ix_user_collections_first_cloud_id", "user_collections", ["first_cloud_id"]) def downgrade() -> None: op.drop_table("user_collections") op.drop_table("clouds") op.drop_table("email_tokens") op.drop_table("refresh_sessions") op.drop_table("profiles") op.drop_table("cloud_types") op.drop_table("users")