From 0809a079c58516fe4ada46e7fb6dda56247d506e Mon Sep 17 00:00:00 2001 From: Xiaoxia AI Date: Sun, 21 Jun 2026 06:48:16 +0800 Subject: [PATCH] refactor(db): introduce alembic schema baseline --- alembic/env.py | 9 +- .../versions/001_current_schema_baseline.py | 261 ++++++++++++++++++ alembic/versions/001_initial_schema.py | 90 ------ .../versions/002_add_project_management.py | 91 ------ apps/api/app/config.py | 1 + apps/api/app/db.py | 3 +- apps/worker/worker_app/core/config.py | 1 + apps/worker/worker_app/db.py | 3 +- infra/docker/api.Dockerfile | 2 + infra/docker/deploy-staging.sh | 20 ++ infra/docker/worker.Dockerfile | 2 + requirements.txt | 1 + 12 files changed, 300 insertions(+), 184 deletions(-) create mode 100644 alembic/versions/001_current_schema_baseline.py delete mode 100644 alembic/versions/001_initial_schema.py delete mode 100644 alembic/versions/002_add_project_management.py diff --git a/alembic/env.py b/alembic/env.py index 1040294b2..f48af7da6 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -1,3 +1,4 @@ +import os from logging.config import fileConfig from sqlalchemy import engine_from_config @@ -12,6 +13,10 @@ from packages.adapters.sqlalchemy_impl.models import Base # access to the values within the .ini file in use. config = context.config +database_url = os.getenv("DATABASE_URL") +if database_url: + config.set_main_option("sqlalchemy.url", database_url) + # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: @@ -66,7 +71,9 @@ def run_migrations_online() -> None: with connectable.connect() as connection: context.configure( - connection=connection, target_metadata=target_metadata + connection=connection, + target_metadata=target_metadata, + compare_type=True, ) with context.begin_transaction(): diff --git a/alembic/versions/001_current_schema_baseline.py b/alembic/versions/001_current_schema_baseline.py new file mode 100644 index 000000000..305981f85 --- /dev/null +++ b/alembic/versions/001_current_schema_baseline.py @@ -0,0 +1,261 @@ +"""Current SQLAlchemy schema baseline. + +Revision ID: 001 +Revises: +Create Date: 2026-06-21 + +This revision represents the current runtime schema defined by +packages.adapters.sqlalchemy_impl.models. Existing staging databases should be +stamped to this revision after compatibility verification; fresh databases can +run this migration normally. +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = "001" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "users", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("email", sa.String(length=255), nullable=False), + sa.Column("username", sa.String(length=100), nullable=True), + sa.Column("display_name", sa.String(length=255), nullable=False), + sa.Column("password_hash", sa.String(length=255), nullable=False), + sa.Column("email_verified", sa.Boolean(), nullable=False), + sa.Column("email_verification_token", sa.String(length=255), nullable=True), + sa.Column("password_reset_token", sa.String(length=255), nullable=True), + sa.Column("password_reset_expires_at", sa.DateTime(), nullable=True), + sa.Column("last_login_at", sa.DateTime(), nullable=True), + sa.Column("last_login_ip", sa.String(length=50), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) + op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True) + + op.create_table( + "projects", + sa.Column("id", sa.String(length=32), nullable=False), + sa.Column("workspace_id", sa.String(length=32), nullable=False), + sa.Column("name", sa.String(length=100), nullable=False), + sa.Column("description", sa.Text(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_projects_workspace_id"), "projects", ["workspace_id"], unique=False) + + op.create_table( + "asset_libraries", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("workspace_id", sa.String(length=36), nullable=False), + sa.Column("project_id", sa.String(length=36), nullable=True), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("kind", sa.String(length=20), nullable=False), + sa.Column("asset_count", sa.Float(), nullable=False), + sa.Column("total_size", sa.Float(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_asset_libraries_kind"), "asset_libraries", ["kind"], unique=False) + op.create_index(op.f("ix_asset_libraries_project_id"), "asset_libraries", ["project_id"], unique=False) + op.create_index(op.f("ix_asset_libraries_workspace_id"), "asset_libraries", ["workspace_id"], unique=False) + + op.create_table( + "assets", + sa.Column("id", sa.String(length=36), nullable=False), + sa.Column("workspace_id", sa.String(length=36), nullable=False), + sa.Column("project_id", sa.String(length=36), nullable=False), + sa.Column("asset_library_id", sa.String(length=36), nullable=False), + sa.Column("name", sa.String(length=500), nullable=False), + sa.Column("file_type", sa.String(length=20), nullable=False), + sa.Column("file_size", sa.Float(), nullable=False), + sa.Column("file_url", sa.String(length=1000), nullable=False), + sa.Column("thumbnail_url", sa.String(length=1000), nullable=True), + sa.Column("duration", sa.Float(), nullable=True), + sa.Column("width", sa.Float(), nullable=True), + sa.Column("height", sa.Float(), nullable=True), + sa.Column("fps", sa.Float(), nullable=True), + sa.Column("codec", sa.String(length=50), nullable=True), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("classification_status", sa.String(length=20), nullable=False), + sa.Column("classification_result", sa.Text(), nullable=True), + sa.Column("quality_score", sa.Float(), nullable=True), + sa.Column("uploaded_by_user_id", sa.String(length=36), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_assets_asset_library_id"), "assets", ["asset_library_id"], unique=False) + op.create_index(op.f("ix_assets_classification_status"), "assets", ["classification_status"], unique=False) + op.create_index(op.f("ix_assets_created_at"), "assets", ["created_at"], unique=False) + op.create_index(op.f("ix_assets_file_type"), "assets", ["file_type"], unique=False) + op.create_index(op.f("ix_assets_project_id"), "assets", ["project_id"], unique=False) + op.create_index(op.f("ix_assets_status"), "assets", ["status"], unique=False) + op.create_index(op.f("ix_assets_workspace_id"), "assets", ["workspace_id"], unique=False) + + op.create_table( + "ingest_jobs", + sa.Column("id", sa.String(length=32), nullable=False), + sa.Column("workspace_id", sa.String(length=32), nullable=False), + sa.Column("project_id", sa.String(length=32), nullable=False), + sa.Column("library_id", sa.String(length=32), nullable=False), + sa.Column("storage_key", sa.String(length=255), nullable=False), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("error_message", sa.Text(), nullable=False), + sa.Column("result_asset_id", sa.String(length=32), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_ingest_jobs_library_id"), "ingest_jobs", ["library_id"], unique=False) + op.create_index(op.f("ix_ingest_jobs_project_id"), "ingest_jobs", ["project_id"], unique=False) + op.create_index(op.f("ix_ingest_jobs_workspace_id"), "ingest_jobs", ["workspace_id"], unique=False) + + op.create_table( + "classification_jobs", + sa.Column("id", sa.String(length=32), nullable=False), + sa.Column("workspace_id", sa.String(length=32), nullable=False), + sa.Column("project_id", sa.String(length=32), nullable=False), + sa.Column("asset_id", sa.String(length=32), nullable=False), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("classification", sa.String(length=50), nullable=False), + sa.Column("confidence", sa.Float(), nullable=False), + sa.Column("error_message", sa.Text(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_classification_jobs_asset_id"), "classification_jobs", ["asset_id"], unique=False) + op.create_index(op.f("ix_classification_jobs_project_id"), "classification_jobs", ["project_id"], unique=False) + op.create_index(op.f("ix_classification_jobs_workspace_id"), "classification_jobs", ["workspace_id"], unique=False) + + op.create_table( + "generation_tasks", + sa.Column("id", sa.String(length=32), nullable=False), + sa.Column("workspace_id", sa.String(length=32), nullable=False), + sa.Column("project_id", sa.String(length=32), nullable=False), + sa.Column("strategy_id", sa.String(length=32), nullable=False), + sa.Column("asset_library_id", sa.String(length=32), nullable=False), + sa.Column("voice_library_id", sa.String(length=32), nullable=False), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("progress", sa.Float(), nullable=False), + sa.Column("result_count", sa.Float(), nullable=False), + sa.Column("error_message", sa.Text(), nullable=False), + sa.Column("started_at", sa.DateTime(), nullable=True), + sa.Column("completed_at", sa.DateTime(), nullable=True), + sa.Column("created_by_user_id", sa.String(length=32), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_generation_tasks_asset_library_id"), "generation_tasks", ["asset_library_id"], unique=False) + op.create_index(op.f("ix_generation_tasks_project_id"), "generation_tasks", ["project_id"], unique=False) + op.create_index(op.f("ix_generation_tasks_status"), "generation_tasks", ["status"], unique=False) + op.create_index(op.f("ix_generation_tasks_workspace_id"), "generation_tasks", ["workspace_id"], unique=False) + + op.create_table( + "generated_videos", + sa.Column("id", sa.String(length=32), nullable=False), + sa.Column("workspace_id", sa.String(length=32), nullable=False), + sa.Column("project_id", sa.String(length=32), nullable=False), + sa.Column("generation_task_id", sa.String(length=32), nullable=False), + sa.Column("name", sa.String(length=255), nullable=False), + sa.Column("file_url", sa.String(length=1000), nullable=False), + sa.Column("file_size", sa.Float(), nullable=False), + sa.Column("duration", sa.Float(), nullable=False), + sa.Column("thumbnail_url", sa.String(length=1000), nullable=True), + sa.Column("width", sa.Float(), nullable=False), + sa.Column("height", sa.Float(), nullable=False), + sa.Column("fps", sa.Float(), nullable=False), + sa.Column("generated_at", sa.DateTime(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_generated_videos_generation_task_id"), "generated_videos", ["generation_task_id"], unique=False) + op.create_index(op.f("ix_generated_videos_project_id"), "generated_videos", ["project_id"], unique=False) + op.create_index(op.f("ix_generated_videos_workspace_id"), "generated_videos", ["workspace_id"], unique=False) + + op.create_table( + "tasks", + sa.Column("id", sa.String(length=32), nullable=False), + sa.Column("project_id", sa.String(length=32), nullable=False), + sa.Column("workspace_id", sa.String(length=32), nullable=False), + sa.Column("name", sa.String(length=200), nullable=False), + sa.Column("description", sa.Text(), nullable=False), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("priority", sa.String(length=20), nullable=False), + sa.Column("parent_task_id", sa.String(length=32), nullable=False), + sa.Column("assignee_user_id", sa.String(length=32), nullable=False), + sa.Column("progress", sa.Float(), nullable=False), + sa.Column("planned_start_date", sa.DateTime(), nullable=True), + sa.Column("planned_end_date", sa.DateTime(), nullable=True), + sa.Column("actual_start_date", sa.DateTime(), nullable=True), + sa.Column("actual_end_date", sa.DateTime(), nullable=True), + sa.Column("tags_json", sa.Text(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_tasks_parent_task_id"), "tasks", ["parent_task_id"], unique=False) + op.create_index(op.f("ix_tasks_project_id"), "tasks", ["project_id"], unique=False) + op.create_index(op.f("ix_tasks_status"), "tasks", ["status"], unique=False) + op.create_index(op.f("ix_tasks_workspace_id"), "tasks", ["workspace_id"], unique=False) + + op.create_table( + "milestones", + sa.Column("id", sa.String(length=32), nullable=False), + sa.Column("project_id", sa.String(length=32), nullable=False), + sa.Column("workspace_id", sa.String(length=32), nullable=False), + sa.Column("name", sa.String(length=200), nullable=False), + sa.Column("description", sa.Text(), nullable=False), + sa.Column("target_date", sa.DateTime(), nullable=True), + sa.Column("completed", sa.Boolean(), nullable=False), + sa.Column("completed_at", sa.DateTime(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_milestones_project_id"), "milestones", ["project_id"], unique=False) + op.create_index(op.f("ix_milestones_workspace_id"), "milestones", ["workspace_id"], unique=False) + + op.create_table( + "task_issues", + sa.Column("id", sa.String(length=32), nullable=False), + sa.Column("task_id", sa.String(length=32), nullable=False), + sa.Column("project_id", sa.String(length=32), nullable=False), + sa.Column("workspace_id", sa.String(length=32), nullable=False), + sa.Column("title", sa.String(length=200), nullable=False), + sa.Column("description", sa.Text(), nullable=False), + sa.Column("resolved", sa.Boolean(), nullable=False), + sa.Column("resolved_at", sa.DateTime(), nullable=True), + sa.Column("created_by_user_id", sa.String(length=32), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_task_issues_project_id"), "task_issues", ["project_id"], unique=False) + op.create_index(op.f("ix_task_issues_task_id"), "task_issues", ["task_id"], unique=False) + op.create_index(op.f("ix_task_issues_workspace_id"), "task_issues", ["workspace_id"], unique=False) + + +def downgrade() -> None: + op.drop_table("task_issues") + op.drop_table("milestones") + op.drop_table("tasks") + op.drop_table("generated_videos") + op.drop_table("generation_tasks") + op.drop_table("classification_jobs") + op.drop_table("ingest_jobs") + op.drop_table("assets") + op.drop_table("asset_libraries") + op.drop_table("projects") + op.drop_table("users") diff --git a/alembic/versions/001_initial_schema.py b/alembic/versions/001_initial_schema.py deleted file mode 100644 index c7fd76bed..000000000 --- a/alembic/versions/001_initial_schema.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Initial schema - -Revision ID: 001 -Revises: -Create Date: 2026-06-15 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '001' -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create projects table - op.create_table( - 'projects', - sa.Column('id', sa.String(32), nullable=False), - sa.Column('workspace_id', sa.String(32), nullable=False), - sa.Column('name', sa.String(100), nullable=False), - sa.Column('description', sa.Text(), nullable=False, server_default=''), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_projects_workspace_id'), 'projects', ['workspace_id'], unique=False) - - # Create asset_libraries table - op.create_table( - 'asset_libraries', - sa.Column('id', sa.String(32), nullable=False), - sa.Column('workspace_id', sa.String(32), nullable=False), - sa.Column('project_id', sa.String(32), nullable=False), - sa.Column('name', sa.String(100), nullable=False), - sa.Column('kind', sa.String(20), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_asset_libraries_workspace_id'), 'asset_libraries', ['workspace_id'], unique=False) - op.create_index(op.f('ix_asset_libraries_project_id'), 'asset_libraries', ['project_id'], unique=False) - - # Create assets table - op.create_table( - 'assets', - sa.Column('id', sa.String(32), nullable=False), - sa.Column('workspace_id', sa.String(32), nullable=False), - sa.Column('project_id', sa.String(32), nullable=False), - sa.Column('library_id', sa.String(32), nullable=False), - sa.Column('name', sa.String(100), nullable=False), - sa.Column('storage_key', sa.String(255), nullable=False), - sa.Column('mime_type', sa.String(100), nullable=False), - sa.Column('metadata_json', sa.Text(), nullable=False, server_default='{}'), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_assets_workspace_id'), 'assets', ['workspace_id'], unique=False) - op.create_index(op.f('ix_assets_project_id'), 'assets', ['project_id'], unique=False) - op.create_index(op.f('ix_assets_library_id'), 'assets', ['library_id'], unique=False) - - # Create ingest_jobs table - op.create_table( - 'ingest_jobs', - sa.Column('id', sa.String(32), nullable=False), - sa.Column('workspace_id', sa.String(32), nullable=False), - sa.Column('project_id', sa.String(32), nullable=False), - sa.Column('library_id', sa.String(32), nullable=False), - sa.Column('storage_key', sa.String(255), nullable=False), - sa.Column('status', sa.String(20), nullable=False, server_default='pending'), - sa.Column('error_message', sa.Text(), nullable=False, server_default=''), - sa.Column('result_asset_id', sa.String(32), nullable=False, server_default=''), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_ingest_jobs_workspace_id'), 'ingest_jobs', ['workspace_id'], unique=False) - op.create_index(op.f('ix_ingest_jobs_project_id'), 'ingest_jobs', ['project_id'], unique=False) - op.create_index(op.f('ix_ingest_jobs_library_id'), 'ingest_jobs', ['library_id'], unique=False) - - -def downgrade() -> None: - op.drop_table('ingest_jobs') - op.drop_table('assets') - op.drop_table('asset_libraries') - op.drop_table('projects') diff --git a/alembic/versions/002_add_project_management.py b/alembic/versions/002_add_project_management.py deleted file mode 100644 index 4f06e8472..000000000 --- a/alembic/versions/002_add_project_management.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Add project management tables - -Revision ID: 002 -Revises: 001 -Create Date: 2026-06-16 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '002' -down_revision: Union[str, None] = '001' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Create tasks table - op.create_table( - 'tasks', - sa.Column('id', sa.String(32), nullable=False), - sa.Column('project_id', sa.String(32), nullable=False), - sa.Column('workspace_id', sa.String(32), nullable=False), - sa.Column('name', sa.String(200), nullable=False), - sa.Column('description', sa.Text(), nullable=False, server_default=''), - sa.Column('status', sa.String(20), nullable=False, server_default='pending'), - sa.Column('priority', sa.String(20), nullable=False, server_default='medium'), - sa.Column('parent_task_id', sa.String(32), nullable=False, server_default=''), - sa.Column('assignee_user_id', sa.String(32), nullable=False, server_default=''), - sa.Column('progress', sa.Float(), nullable=False, server_default='0'), - sa.Column('planned_start_date', sa.DateTime(), nullable=True), - sa.Column('planned_end_date', sa.DateTime(), nullable=True), - sa.Column('actual_start_date', sa.DateTime(), nullable=True), - sa.Column('actual_end_date', sa.DateTime(), nullable=True), - sa.Column('tags_json', sa.Text(), nullable=False, server_default='[]'), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_tasks_project_id'), 'tasks', ['project_id'], unique=False) - op.create_index(op.f('ix_tasks_workspace_id'), 'tasks', ['workspace_id'], unique=False) - op.create_index(op.f('ix_tasks_parent_task_id'), 'tasks', ['parent_task_id'], unique=False) - op.create_index(op.f('ix_tasks_status'), 'tasks', ['status'], unique=False) - - # Create milestones table - op.create_table( - 'milestones', - sa.Column('id', sa.String(32), nullable=False), - sa.Column('project_id', sa.String(32), nullable=False), - sa.Column('workspace_id', sa.String(32), nullable=False), - sa.Column('name', sa.String(200), nullable=False), - sa.Column('description', sa.Text(), nullable=False, server_default=''), - sa.Column('target_date', sa.DateTime(), nullable=True), - sa.Column('completed', sa.Boolean(), nullable=False, server_default='0'), - sa.Column('completed_at', sa.DateTime(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_milestones_project_id'), 'milestones', ['project_id'], unique=False) - op.create_index(op.f('ix_milestones_workspace_id'), 'milestones', ['workspace_id'], unique=False) - - # Create task_issues table - op.create_table( - 'task_issues', - sa.Column('id', sa.String(32), nullable=False), - sa.Column('task_id', sa.String(32), nullable=False), - sa.Column('project_id', sa.String(32), nullable=False), - sa.Column('workspace_id', sa.String(32), nullable=False), - sa.Column('title', sa.String(200), nullable=False), - sa.Column('description', sa.Text(), nullable=False, server_default=''), - sa.Column('resolved', sa.Boolean(), nullable=False, server_default='0'), - sa.Column('resolved_at', sa.DateTime(), nullable=True), - sa.Column('created_by_user_id', sa.String(32), nullable=False, server_default=''), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_task_issues_task_id'), 'task_issues', ['task_id'], unique=False) - op.create_index(op.f('ix_task_issues_project_id'), 'task_issues', ['project_id'], unique=False) - op.create_index(op.f('ix_task_issues_workspace_id'), 'task_issues', ['workspace_id'], unique=False) - - -def downgrade() -> None: - op.drop_table('task_issues') - op.drop_table('milestones') - op.drop_table('tasks') diff --git a/apps/api/app/config.py b/apps/api/app/config.py index daf68834c..1450e6b9b 100644 --- a/apps/api/app/config.py +++ b/apps/api/app/config.py @@ -19,6 +19,7 @@ class Settings(BaseSettings): DATABASE_POOL_TIMEOUT: int = 30 DATABASE_POOL_RECYCLE: int = 3600 USE_IN_MEMORY_DB: bool = False + AUTO_CREATE_SCHEMA: bool = False REDIS_URL: str = "redis://localhost:6379/0" REDIS_MAX_CONNECTIONS: int = 50 diff --git a/apps/api/app/db.py b/apps/api/app/db.py index a4849c142..74b1dd937 100644 --- a/apps/api/app/db.py +++ b/apps/api/app/db.py @@ -13,7 +13,8 @@ engine, SessionLocal = build_session_factory( pool_timeout=settings.DATABASE_POOL_TIMEOUT, pool_recycle=settings.DATABASE_POOL_RECYCLE, ) -initialize_database(engine) +if settings.AUTO_CREATE_SCHEMA: + initialize_database(engine) def get_db() -> Generator[Session, None, None]: diff --git a/apps/worker/worker_app/core/config.py b/apps/worker/worker_app/core/config.py index a79244bdd..4e5658c3c 100644 --- a/apps/worker/worker_app/core/config.py +++ b/apps/worker/worker_app/core/config.py @@ -14,6 +14,7 @@ class WorkerSettings(BaseSettings): database_max_overflow: int = 40 database_pool_timeout: int = 30 database_pool_recycle: int = 3600 + auto_create_schema: bool = False model_config = SettingsConfigDict( env_file=".env", diff --git a/apps/worker/worker_app/db.py b/apps/worker/worker_app/db.py index 78aaee433..260f57af3 100644 --- a/apps/worker/worker_app/db.py +++ b/apps/worker/worker_app/db.py @@ -10,4 +10,5 @@ engine, SessionLocal = build_session_factory( pool_timeout=settings.database_pool_timeout, pool_recycle=settings.database_pool_recycle, ) -initialize_database(engine) +if settings.auto_create_schema: + initialize_database(engine) diff --git a/infra/docker/api.Dockerfile b/infra/docker/api.Dockerfile index 78f7a3354..b479ec936 100644 --- a/infra/docker/api.Dockerfile +++ b/infra/docker/api.Dockerfile @@ -8,6 +8,8 @@ RUN python -m pip install --upgrade pip setuptools wheel && \ pip install --no-cache-dir --default-timeout=120 --retries 10 -r requirements.txt COPY apps/api /app/apps/api COPY packages /app/packages +COPY alembic /app/alembic +COPY alembic.ini /app/alembic.ini WORKDIR /app/apps/api EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/infra/docker/deploy-staging.sh b/infra/docker/deploy-staging.sh index 28cb33a74..ea2dbb939 100644 --- a/infra/docker/deploy-staging.sh +++ b/infra/docker/deploy-staging.sh @@ -36,5 +36,25 @@ export COMPOSE_DOCKER_CLI_BUILD=0 docker compose build --pull=false api docker compose build --pull=false worker +docker compose run --rm --no-deps api sh -c ' + cd /app && + python - <<"PY" +import os +from sqlalchemy import create_engine, text +url = os.environ["DATABASE_URL"] +engine = create_engine(url) +with engine.begin() as conn: + has_version = conn.execute(text("SELECT to_regclass('public.alembic_version')")).scalar() is not None + has_tables = conn.execute(text("SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'public' AND tablename != 'alembic_version'")).scalar() + if has_tables and not has_version: + raise SystemExit("STAMP_REQUIRED") +PY + status=$? + if [ "$status" -eq 0 ]; then + alembic upgrade head + else + alembic stamp head && alembic upgrade head + fi +' docker compose up -d api worker web docker compose ps diff --git a/infra/docker/worker.Dockerfile b/infra/docker/worker.Dockerfile index b76bf2ad0..866b94ff7 100644 --- a/infra/docker/worker.Dockerfile +++ b/infra/docker/worker.Dockerfile @@ -13,5 +13,7 @@ RUN set -eux; \ pip install --no-cache-dir --default-timeout=120 --retries 10 -r requirements.txt COPY apps/worker /app/apps/worker COPY packages /app/packages +COPY alembic /app/alembic +COPY alembic.ini /app/alembic.ini WORKDIR /app/apps/worker CMD ["celery", "-A", "worker_app.celery_app.celery_app", "worker", "--loglevel=info"] diff --git a/requirements.txt b/requirements.txt index 9c3357e23..dcf12d7cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ pydantic-settings==2.6.0 psycopg2-binary==2.9.9 psycopg[binary]==3.1.18 sqlalchemy==2.0.35 +alembic==1.13.3 # 认证 pyjwt==2.9.0