196 lines
9.3 KiB
Python
196 lines
9.3 KiB
Python
"""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")
|