first commit
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from app import models # noqa: F401
|
||||
from app.config import settings
|
||||
from app.database import Base
|
||||
|
||||
|
||||
config = context.config
|
||||
config.set_main_option("sqlalchemy.url", settings.database_url)
|
||||
if config.config_file_name:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
context.configure(
|
||||
url=settings.database_url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
compare_type=True,
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection) -> None:
|
||||
context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_async_migrations() -> None:
|
||||
connectable = async_engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
asyncio.run(run_async_migrations())
|
||||
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,195 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user