From 5b88f81e4b8c813b6bbebd53794d6683e1cda249 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:00:57 +0800 Subject: [PATCH] update: alembic baseline --- .../d4e5f6a7b8c9_squashed_baseline.py | 658 ++++++++++++++++++ 1 file changed, 658 insertions(+) create mode 100644 migrations/versions/d4e5f6a7b8c9_squashed_baseline.py diff --git a/migrations/versions/d4e5f6a7b8c9_squashed_baseline.py b/migrations/versions/d4e5f6a7b8c9_squashed_baseline.py new file mode 100644 index 0000000..6566288 --- /dev/null +++ b/migrations/versions/d4e5f6a7b8c9_squashed_baseline.py @@ -0,0 +1,658 @@ +"""squashed baseline — full schema + seed data in one migration + +Single baseline migration combining the previous 5-step chain: + + 8d86e2f82860 initial baseline (20 tables) + b71c4f2a9d10 seed demo users / workspaces / roles / members + 9a1b2c3d4e5f enable password login for seeded users + a2b3c4d5e6f7 add storage_objects.trash_key + c3d4e5f6a7b8 add upload_sessions object-metadata columns + +The column additions from the later migrations are folded directly into +the CREATE TABLE statements, so this file is a from-scratch schema. + +Revision ID: d4e5f6a7b8c9 +Revises: (none) +Create Date: 2026-08-05 +""" + +from collections.abc import Sequence +import os + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +from common.auth.passwords import hash_password + +# revision identifiers, used by Alembic. +revision: str = "d4e5f6a7b8c9" +down_revision: str | Sequence[str] | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + """Full schema from scratch (all 20 tables, current model state).""" + op.create_table('consumer_inbox', + sa.Column('consumer_name', sa.String(length=128), nullable=False), + sa.Column('event_id', mysql.CHAR(length=26), nullable=False), + sa.Column('process_status', sa.String(length=16), server_default=sa.text("'processing'"), nullable=False, comment='processing/succeeded/failed'), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('message_id', sa.String(length=128), nullable=True, comment='Inbox message ID'), + sa.Column('processed_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('error_message', sa.String(length=2000), nullable=True), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('consumer_name', 'event_id'), + comment='消费者幂等 Inbox,防止 Stream 重投导致重复执行' + ) + op.create_index('idx_consumer_inbox_status', 'consumer_inbox', ['consumer_name', 'process_status', 'created_at'], unique=False) + op.create_table('data_resources', + sa.Column('resource_id', mysql.CHAR(length=26), nullable=False), + sa.Column('workspace_id', mysql.CHAR(length=26), nullable=False), + sa.Column('storage_object_id', mysql.CHAR(length=26), nullable=False), + sa.Column('owner_user_id', mysql.CHAR(length=26), nullable=False), + sa.Column('resource_name', sa.String(length=255), nullable=False), + sa.Column('visibility', sa.String(length=16), server_default=sa.text("'private'"), nullable=False), + sa.Column('status', sa.String(length=16), server_default=sa.text("'active'"), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('description', sa.String(length=1000), nullable=True), + sa.Column('schema_json', sa.JSON(), nullable=True, comment='字段结构、行数等可选元数据'), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('resource_id'), + comment='数据资源' + ) + op.create_index('idx_data_resources_owner', 'data_resources', ['owner_user_id', 'status'], unique=False) + op.create_index('idx_data_resources_workspace', 'data_resources', ['workspace_id', 'visibility', 'status'], unique=False) + op.create_index('uk_data_resources_object', 'data_resources', ['storage_object_id'], unique=True) + op.create_table('outbox_events', + sa.Column('event_id', mysql.CHAR(length=26), nullable=False), + sa.Column('aggregate_type', sa.String(length=64), nullable=False), + sa.Column('aggregate_id', sa.String(length=128), nullable=False), + sa.Column('event_type', sa.String(length=128), nullable=False), + sa.Column('schema_version', mysql.SMALLINT(), server_default=sa.text('1'), nullable=False), + sa.Column('payload_json', sa.JSON(), nullable=False), + sa.Column('event_status', sa.String(length=16), server_default=sa.text("'pending'"), nullable=False, comment='pending/published/failed'), + sa.Column('available_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('retry_count', mysql.INTEGER(), server_default=sa.text('0'), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('trace_id', sa.String(length=64), nullable=True), + sa.Column('idempotency_key', sa.String(length=128), nullable=True), + sa.Column('published_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('last_error', sa.String(length=2000), nullable=True), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('event_id'), + comment='事务 Outbox;提交后发布到内部事件总线' + ) + op.create_index('idx_outbox_aggregate', 'outbox_events', ['aggregate_type', 'aggregate_id', 'created_at'], unique=False) + op.create_index('idx_outbox_idempotency', 'outbox_events', ['idempotency_key'], unique=False) + op.create_index('idx_outbox_pending', 'outbox_events', ['event_status', 'available_at', 'created_at'], unique=False) + op.create_table('permissions', + sa.Column('permission_id', mysql.CHAR(length=26), nullable=False), + sa.Column('permission_code', sa.String(length=128), nullable=False), + sa.Column('permission_name', sa.String(length=100), nullable=False), + sa.Column('module_code', sa.String(length=64), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('description', sa.String(length=500), nullable=True), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('permission_id'), + comment='权限点' + ) + op.create_index('idx_permissions_module', 'permissions', ['module_code'], unique=False) + op.create_index('uk_permissions_code', 'permissions', ['permission_code'], unique=True) + op.create_table('role_permissions', + sa.Column('role_id', mysql.CHAR(length=26), nullable=False), + sa.Column('permission_id', mysql.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('role_id', 'permission_id'), + comment='角色权限' + ) + op.create_index('fk_role_permissions_permission', 'role_permissions', ['permission_id'], unique=False) + op.create_table('roles', + sa.Column('role_id', mysql.CHAR(length=26), nullable=False), + sa.Column('role_code', sa.String(length=64), nullable=False), + sa.Column('role_name', sa.String(length=100), nullable=False), + sa.Column('role_scope', sa.String(length=16), nullable=False, comment='platform/workspace'), + sa.Column('is_builtin', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('description', sa.String(length=500), nullable=True), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('role_id'), + comment='角色' + ) + op.create_index('uk_roles_code', 'roles', ['role_code'], unique=True) + op.create_table('schedule_edges', + sa.Column('edge_id', mysql.CHAR(length=26), nullable=False), + sa.Column('schedule_id', mysql.CHAR(length=26), nullable=False), + sa.Column('source_node_id', mysql.CHAR(length=26), nullable=False), + sa.Column('target_node_id', mysql.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('condition_expr', sa.String(length=1000), nullable=True), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('edge_id'), + comment='DAG 有向边' + ) + op.create_index('fk_schedule_edges_source', 'schedule_edges', ['source_node_id'], unique=False) + op.create_index('idx_schedule_edges_target', 'schedule_edges', ['target_node_id'], unique=False) + op.create_index('uk_schedule_edges_pair', 'schedule_edges', ['schedule_id', 'source_node_id', 'target_node_id'], unique=True) + op.create_table('schedule_node_runs', + sa.Column('node_run_id', mysql.CHAR(length=26), nullable=False), + sa.Column('run_id', mysql.CHAR(length=26), nullable=False), + sa.Column('node_id', mysql.CHAR(length=26), nullable=False), + sa.Column('versions_id', mysql.CHAR(length=26), nullable=False), + sa.Column('attempt_no', mysql.INTEGER(), server_default=sa.text('1'), nullable=False), + sa.Column('node_status', sa.String(length=24), server_default=sa.text("'queued'"), nullable=False, comment='queued/running/succeeded/failed/skipped/cancelled/timed_out'), + sa.Column('state_version', mysql.INTEGER(), server_default=sa.text('0'), nullable=False, comment='乐观锁版本'), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('started_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('finished_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('duration_ms', mysql.BIGINT(), nullable=True), + sa.Column('exit_code', sa.Integer(), nullable=True), + sa.Column('message', sa.String(length=2000), nullable=True), + sa.Column('metrics_json', sa.JSON(), nullable=True), + sa.Column('logs_object_id', mysql.CHAR(length=26), nullable=True), + sa.Column('result_object_id', mysql.CHAR(length=26), nullable=True), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('node_run_id'), + comment='调度节点运行与重试' + ) + op.create_index('fk_node_runs_logs', 'schedule_node_runs', ['logs_object_id'], unique=False) + op.create_index('fk_node_runs_node', 'schedule_node_runs', ['node_id'], unique=False) + op.create_index('fk_node_runs_result', 'schedule_node_runs', ['result_object_id'], unique=False) + op.create_index('idx_node_runs_status', 'schedule_node_runs', ['run_id', 'node_status'], unique=False) + op.create_index('idx_node_runs_version', 'schedule_node_runs', ['versions_id'], unique=False) + op.create_index('uk_node_runs_attempt', 'schedule_node_runs', ['run_id', 'node_id', 'attempt_no'], unique=True) + op.create_table('schedule_nodes', + sa.Column('node_id', mysql.CHAR(length=26), nullable=False), + sa.Column('schedule_id', mysql.CHAR(length=26), nullable=False), + sa.Column('node_key', sa.String(length=64), nullable=False, comment='画布内稳定标识'), + sa.Column('node_name', sa.String(length=255), nullable=False), + sa.Column('versions_id', mysql.CHAR(length=26), nullable=False), + sa.Column('timeout_seconds', mysql.INTEGER(), server_default=sa.text('600'), nullable=False), + sa.Column('retry_count', mysql.INTEGER(), server_default=sa.text('0'), nullable=False), + sa.Column('retry_interval_sec', mysql.INTEGER(), server_default=sa.text('5'), nullable=False), + sa.Column('position_x', sa.DECIMAL(precision=10, scale=2), server_default=sa.text('0.00'), nullable=False), + sa.Column('position_y', sa.DECIMAL(precision=10, scale=2), server_default=sa.text('0.00'), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('arguments_json', sa.JSON(), nullable=True), + sa.Column('env_refs_json', sa.JSON(), nullable=True, comment='只存密钥引用,不存明文密钥'), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('node_id'), + comment='DAG 节点,必须引用稳定版本' + ) + op.create_index('idx_schedule_nodes_version', 'schedule_nodes', ['versions_id'], unique=False) + op.create_index('uk_schedule_nodes_key', 'schedule_nodes', ['schedule_id', 'node_key'], unique=True) + op.create_table('schedule_runs', + sa.Column('run_id', mysql.CHAR(length=26), nullable=False), + sa.Column('schedule_id', mysql.CHAR(length=26), nullable=False), + sa.Column('workspace_id', mysql.CHAR(length=26), nullable=False), + sa.Column('workflow_version', mysql.INTEGER(), nullable=False), + sa.Column('trigger_type', sa.String(length=16), nullable=False, comment='manual/cron/api/retry'), + sa.Column('idempotency_key', sa.String(length=128), nullable=False), + sa.Column('run_status', sa.String(length=24), server_default=sa.text("'queued'"), nullable=False, comment='queued/running/succeeded/failed/cancelled/timed_out'), + sa.Column('state_version', mysql.INTEGER(), server_default=sa.text('0'), nullable=False, comment='乐观锁版本'), + sa.Column('schedule_snapshot', sa.JSON(), nullable=False, comment='执行时 DAG 快照'), + sa.Column('queued_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('triggered_by', mysql.CHAR(length=26), nullable=True), + sa.Column('started_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('finished_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('duration_ms', mysql.BIGINT(), nullable=True), + sa.Column('error_code', sa.String(length=64), nullable=True), + sa.Column('error_message', sa.Text(), nullable=True), + sa.Column('logs_object_id', mysql.CHAR(length=26), nullable=True), + sa.Column('result_object_id', mysql.CHAR(length=26), nullable=True), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('run_id'), + comment='调度运行' + ) + op.create_index('fk_schedule_runs_logs', 'schedule_runs', ['logs_object_id'], unique=False) + op.create_index('fk_schedule_runs_result', 'schedule_runs', ['result_object_id'], unique=False) + op.create_index('fk_schedule_runs_user', 'schedule_runs', ['triggered_by'], unique=False) + op.create_index('idx_schedule_runs_schedule', 'schedule_runs', ['schedule_id', 'created_at'], unique=False) + op.create_index('idx_schedule_runs_status', 'schedule_runs', ['run_status', 'queued_at'], unique=False) + op.create_index('idx_schedule_runs_workspace_status', 'schedule_runs', ['workspace_id', 'run_status', 'queued_at'], unique=False) + op.create_index('uk_schedule_runs_idempotency', 'schedule_runs', ['idempotency_key'], unique=True) + op.create_table('schedules', + sa.Column('schedule_id', mysql.CHAR(length=26), nullable=False), + sa.Column('workspace_id', mysql.CHAR(length=26), nullable=False), + sa.Column('schedule_name', sa.String(length=255), nullable=False), + sa.Column('trigger_type', sa.String(length=16), server_default=sa.text("'cron'"), nullable=False, comment='manual/cron/api'), + sa.Column('timezone', sa.String(length=64), server_default=sa.text("'Asia/Shanghai'"), nullable=False), + sa.Column('enabled', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('workflow_version', mysql.INTEGER(), server_default=sa.text('1'), nullable=False), + sa.Column('max_concurrency', mysql.INTEGER(), server_default=sa.text('1'), nullable=False), + sa.Column('failure_policy', sa.String(length=24), server_default=sa.text("'stop'"), nullable=False, comment='stop/continue'), + sa.Column('created_by', mysql.CHAR(length=26), nullable=False), + sa.Column('updated_by', mysql.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('description', sa.String(length=1000), nullable=True), + sa.Column('cron_expression', sa.String(length=128), nullable=True), + sa.Column('last_run_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('next_run_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('schedule_id'), + comment='调度方案' + ) + op.create_index('fk_schedules_created_by', 'schedules', ['created_by'], unique=False) + op.create_index('fk_schedules_updated_by', 'schedules', ['updated_by'], unique=False) + op.create_index('idx_schedules_due', 'schedules', ['enabled', 'next_run_at'], unique=False) + op.create_index('idx_schedules_workspace', 'schedules', ['workspace_id', 'enabled', 'updated_at'], unique=False) + op.create_table('scripts', + sa.Column('script_id', mysql.CHAR(length=26), nullable=False), + sa.Column('workspace_id', mysql.CHAR(length=26), nullable=False), + sa.Column('current_object_id', mysql.CHAR(length=26), nullable=False, comment='当前工作副本'), + sa.Column('owner_user_id', mysql.CHAR(length=26), nullable=False), + sa.Column('script_name', sa.String(length=255), nullable=False), + sa.Column('script_type', sa.String(length=16), nullable=False, comment='python/notebook'), + sa.Column('visibility', sa.String(length=16), server_default=sa.text("'private'"), nullable=False), + sa.Column('status', sa.String(length=16), server_default=sa.text("'active'"), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('is_locked', mysql.TINYINT(display_width=1), server_default=sa.text('1'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('script_id'), + comment='可执行 Python/Notebook 脚本' + ) + op.create_index('idx_scripts_owner', 'scripts', ['owner_user_id', 'status'], unique=False) + op.create_index('idx_scripts_workspace', 'scripts', ['workspace_id', 'script_type', 'visibility', 'status'], unique=False) + op.create_index('uk_scripts_current_object', 'scripts', ['current_object_id'], unique=True) + op.create_index('uk_scripts_workspace_name', 'scripts', ['workspace_id', 'script_name', 'script_type'], unique=True) + op.create_table('storage_objects', + sa.Column('storage_object_id', mysql.CHAR(length=26), nullable=False), + sa.Column('workspace_id', mysql.CHAR(length=26), nullable=False), + sa.Column('object_type', sa.String(length=16), nullable=False, comment='file/directory'), + sa.Column('usage_type', sa.String(length=32), nullable=False, comment='working_copy/public_script/data_resource/version_artifact/snapshot/run_log/run_result'), + sa.Column('storage_backend', sa.String(length=16), nullable=False, comment='s3'), + sa.Column('storage_uri', sa.String(length=1500), nullable=False), + sa.Column('file_name', sa.String(length=255), nullable=False), + sa.Column('size_bytes', mysql.BIGINT(), server_default=sa.text('0'), nullable=False), + sa.Column('visibility', sa.String(length=16), server_default=sa.text("'private'"), nullable=False, comment='private/workspace/public'), + sa.Column('is_immutable', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('object_status', sa.String(length=24), server_default=sa.text("'available'"), nullable=False, comment='uploading/available/deleting/deleted/failed'), + sa.Column('created_by', mysql.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('owner_user_id', mysql.CHAR(length=26), nullable=True), + sa.Column('parent_object_id', mysql.CHAR(length=26), nullable=True), + sa.Column('relative_path', sa.String(length=1024), nullable=True, comment='Workspace 相对路径'), + sa.Column('path_hash', sa.BINARY(length=32), nullable=True, comment='SHA-256(relative_path),由应用写入'), + sa.Column('bucket_name', sa.String(length=128), nullable=True), + sa.Column('object_key', sa.String(length=1024), nullable=True), + sa.Column('object_key_hash', sa.BINARY(length=32), nullable=True, comment='SHA-256(object_key),由应用写入'), + sa.Column('file_extension', sa.String(length=32), nullable=True), + sa.Column('mime_type', sa.String(length=255), nullable=True), + sa.Column('content_hash', mysql.CHAR(length=64), nullable=True, comment='SHA-256 hex'), + sa.Column('object_etag', sa.String(length=255), nullable=True), + sa.Column('trash_key', sa.String(length=1100), nullable=True, comment='Path inside the trash bucket where soft-deleted bytes are stored'), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('storage_object_id'), + comment='Workspace 文件和 S3 对象的统一元数据' + ) + op.create_index('fk_storage_created_by', 'storage_objects', ['created_by'], unique=False) + op.create_index('idx_storage_content_hash', 'storage_objects', ['content_hash'], unique=False) + op.create_index('idx_storage_owner', 'storage_objects', ['owner_user_id', 'object_status'], unique=False) + op.create_index('idx_storage_parent', 'storage_objects', ['parent_object_id'], unique=False) + op.create_index('idx_storage_workspace_usage', 'storage_objects', ['workspace_id', 'usage_type', 'object_status'], unique=False) + op.create_index('uk_storage_bucket_key', 'storage_objects', ['storage_backend', 'bucket_name', 'object_key_hash'], unique=True) + op.create_index('uk_storage_workspace_path', 'storage_objects', ['workspace_id', 'storage_backend', 'path_hash'], unique=True) + op.create_table('upload_sessions', + sa.Column('upload_id', mysql.CHAR(length=26), nullable=False), + sa.Column('workspace_id', mysql.CHAR(length=26), nullable=False), + sa.Column('user_id', mysql.CHAR(length=26), nullable=False), + sa.Column('idempotency_key', sa.String(length=128), nullable=False), + sa.Column('bucket_name', sa.String(length=128), nullable=False), + sa.Column('object_key', sa.String(length=1024), nullable=False), + sa.Column('object_key_hash', sa.BINARY(length=32), nullable=False), + sa.Column('upload_status', sa.String(length=24), server_default=sa.text("'created'"), nullable=False, comment='created/uploading/completed/expired/aborted/failed'), + sa.Column('expires_at', mysql.DATETIME(fsp=3), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('multipart_upload_id', sa.String(length=255), nullable=True), + sa.Column('expected_size_bytes', mysql.BIGINT(), nullable=True), + sa.Column('expected_hash', mysql.CHAR(length=64), nullable=True), + sa.Column('content_type', sa.String(length=255), nullable=True), + sa.Column('storage_object_id', mysql.CHAR(length=26), nullable=True), + sa.Column('completed_at', mysql.DATETIME(fsp=3), nullable=True), + # Object-metadata columns added by c3d4e5f6a7b8 (server-proxied upload). + sa.Column('file_name', sa.String(length=255), nullable=False, server_default=''), + sa.Column('usage_type', sa.String(length=32), nullable=False, server_default='working_copy', comment='data_resource/version_artifact/snapshot/run_log/run_result/working_copy/public_script'), + sa.Column('visibility', sa.String(length=16), nullable=False, server_default='private', comment='private/workspace/public'), + sa.Column('is_immutable', mysql.TINYINT(display_width=1), nullable=False, server_default='0'), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('upload_id'), + comment='S3 上传会话;URL 本身不持久化' + ) + op.create_index('fk_upload_sessions_storage_object', 'upload_sessions', ['storage_object_id'], unique=False) + op.create_index('fk_upload_sessions_user', 'upload_sessions', ['user_id'], unique=False) + op.create_index('idx_upload_sessions_expiry', 'upload_sessions', ['upload_status', 'expires_at'], unique=False) + op.create_index('idx_upload_sessions_object_key', 'upload_sessions', ['bucket_name', 'object_key_hash'], unique=False) + op.create_index('idx_upload_sessions_workspace', 'upload_sessions', ['workspace_id', 'user_id', 'created_at'], unique=False) + op.create_index('uk_upload_sessions_idempotency', 'upload_sessions', ['idempotency_key'], unique=True) + op.create_table('users', + sa.Column('user_id', mysql.CHAR(length=26), nullable=False), + sa.Column('username', sa.String(length=64), nullable=False), + sa.Column('display_name', sa.String(length=100), nullable=False), + sa.Column('password_hash', sa.String(length=255), nullable=False), + sa.Column('status', sa.String(length=16), server_default=sa.text("'active'"), nullable=False, comment='active/disabled/locked'), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('email', sa.String(length=255), nullable=True), + sa.Column('platform_role_id', mysql.CHAR(length=26), nullable=True), + sa.Column('avatar_uri', sa.String(length=1000), nullable=True), + sa.Column('last_login_at', mysql.DATETIME(fsp=3), nullable=True), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('user_id'), + comment='平台用户' + ) + op.create_index('fk_users_platform_role', 'users', ['platform_role_id'], unique=False) + op.create_index('idx_users_status', 'users', ['status'], unique=False) + op.create_index('uk_users_email', 'users', ['email'], unique=True) + op.create_index('uk_users_username', 'users', ['username'], unique=True) + op.create_table('versions', + sa.Column('versions_id', mysql.CHAR(length=26), nullable=False, comment='稳定版本唯一 ID'), + sa.Column('workspace_id', mysql.CHAR(length=26), nullable=False), + sa.Column('script_id', mysql.CHAR(length=26), nullable=False), + sa.Column('source_object_id', mysql.CHAR(length=26), nullable=False, comment='发布时的源对象'), + sa.Column('artifact_object_id', mysql.CHAR(length=26), nullable=False, comment='S3 不可变版本制品'), + sa.Column('version_no', mysql.INTEGER(), nullable=False), + sa.Column('version_label', sa.String(length=32), nullable=False, comment='例如 v1.0'), + sa.Column('source_path', sa.String(length=1024), nullable=False, comment='发布时路径快照'), + sa.Column('artifact_path', sa.String(length=1500), nullable=False), + sa.Column('content_hash', mysql.CHAR(length=64), nullable=False), + sa.Column('file_size_bytes', mysql.BIGINT(), server_default=sa.text('0'), nullable=False), + sa.Column('visibility', sa.String(length=16), server_default=sa.text("'private'"), nullable=False), + sa.Column('created_by', mysql.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('release_note', sa.String(length=1000), nullable=True), + sa.Column('schedule_hidden_at', mysql.DATETIME(fsp=3), nullable=True, comment='从调度稳定版本列表移除的时间;不影响版本和运行历史'), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('versions_id'), + comment='不可变稳定版本;调度节点必须引用 versions_id' + ) + op.create_index('fk_versions_source_object', 'versions', ['source_object_id'], unique=False) + op.create_index('idx_versions_creator', 'versions', ['created_by', 'created_at'], unique=False) + op.create_index('idx_versions_workspace_created', 'versions', ['workspace_id', 'created_at'], unique=False) + op.create_index('uk_versions_artifact', 'versions', ['artifact_object_id'], unique=True) + op.create_index('uk_versions_script_hash', 'versions', ['script_id', 'content_hash'], unique=True) + op.create_index('uk_versions_script_no', 'versions', ['script_id', 'version_no'], unique=True) + op.create_table('workspace_members', + sa.Column('workspace_id', mysql.CHAR(length=26), nullable=False), + sa.Column('user_id', mysql.CHAR(length=26), nullable=False), + sa.Column('role_id', mysql.CHAR(length=26), nullable=False), + sa.Column('member_status', sa.String(length=16), server_default=sa.text("'active'"), nullable=False), + sa.Column('joined_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('workspace_id', 'user_id'), + comment='Workspace 成员与角色' + ) + op.create_index('idx_workspace_members_role', 'workspace_members', ['role_id'], unique=False) + op.create_index('idx_workspace_members_user', 'workspace_members', ['user_id', 'member_status'], unique=False) + op.create_table('workspaces', + sa.Column('workspace_id', mysql.CHAR(length=26), nullable=False), + sa.Column('workspace_code', sa.String(length=64), nullable=False), + sa.Column('workspace_name', sa.String(length=150), nullable=False), + sa.Column('active_root_uri', sa.String(length=1500), nullable=False, comment='活动工作区,建议 NFS/PVC/file URI'), + sa.Column('quota_bytes', mysql.BIGINT(), server_default=sa.text('0'), nullable=False, comment='0 表示不限额'), + sa.Column('used_bytes', mysql.BIGINT(), server_default=sa.text('0'), nullable=False), + sa.Column('status', sa.String(length=24), server_default=sa.text("'active'"), nullable=False, comment='creating/active/suspended/deleting/deleted'), + sa.Column('created_by', mysql.CHAR(length=26), nullable=False), + sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('updated_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)'), nullable=False), + sa.Column('description', sa.String(length=1000), nullable=True), + sa.Column('artifact_bucket', sa.String(length=128), nullable=True, comment='S3 bucket'), + sa.Column('artifact_prefix', sa.String(length=512), nullable=True, comment='S3 object key prefix'), + sa.Column('is_deleted', mysql.TINYINT(display_width=1), server_default=sa.text('0'), nullable=False), + sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True), + sa.PrimaryKeyConstraint('workspace_id'), + comment='Workspace' + ) + op.create_index('fk_workspaces_created_by', 'workspaces', ['created_by'], unique=False) + op.create_index('idx_workspaces_status', 'workspaces', ['status'], unique=False) + op.create_index('uk_workspaces_code', 'workspaces', ['workspace_code'], unique=True) + + # ── seed data (from b71c4f2a9d10) ────────────────────────────── + ADMIN_ROLE_ID = "0000000000000000000000000A" + DEVELOPER_ROLE_ID = "0000000000000000000000000B" + USERS = ( + ("0000000000RF6FG1SDBXG59S13", "admin-zhang", "张三", ADMIN_ROLE_ID), + ("0000000000H2QYCGPCWQM1JSGS", "admin-li", "李四", ADMIN_ROLE_ID), + ("0000000000RWG40ESZPGJT629J", "dev-wang", "王五", DEVELOPER_ROLE_ID), + ("00000000004CQV7WASJA6N6FW4", "dev-zhao", "赵六", DEVELOPER_ROLE_ID), + ) + WORKSPACES = ( + ("00000000000BM630VT9ARVFZPC", "model-development", "模型开发 Workspace"), + ("0000000000AE0NC0V5T424KK86", "risk-validation", "风险验证 Workspace"), + ) + + roles = sa.table( + "roles", + sa.column("role_id", sa.String), + sa.column("role_code", sa.String), + sa.column("role_name", sa.String), + sa.column("role_scope", sa.String), + sa.column("is_builtin", sa.Integer), + sa.column("description", sa.String), + ) + users = sa.table( + "users", + sa.column("user_id", sa.String), + sa.column("username", sa.String), + sa.column("display_name", sa.String), + sa.column("password_hash", sa.String), + sa.column("status", sa.String), + sa.column("email", sa.String), + sa.column("platform_role_id", sa.String), + ) + workspaces = sa.table( + "workspaces", + sa.column("workspace_id", sa.String), + sa.column("workspace_code", sa.String), + sa.column("workspace_name", sa.String), + sa.column("active_root_uri", sa.String), + sa.column("status", sa.String), + sa.column("created_by", sa.String), + sa.column("description", sa.String), + ) + members = sa.table( + "workspace_members", + sa.column("workspace_id", sa.String), + sa.column("user_id", sa.String), + sa.column("role_id", sa.String), + sa.column("member_status", sa.String), + ) + + op.bulk_insert( + roles, + [ + { + "role_id": ADMIN_ROLE_ID, + "role_code": "admin", + "role_name": "管理员", + "role_scope": "workspace", + "is_builtin": 1, + "description": "Self-hosted workspace administrator", + }, + { + "role_id": DEVELOPER_ROLE_ID, + "role_code": "developer", + "role_name": "开发人员", + "role_scope": "workspace", + "is_builtin": 1, + "description": "Self-hosted workspace developer", + }, + ], + ) + op.bulk_insert( + users, + [ + { + "user_id": user_id, + "username": username, + "display_name": display_name, + "password_hash": "demo-login-disabled", + "status": "active", + "email": f"{username}@model-platform.local", + "platform_role_id": role_id, + } + for user_id, username, display_name, role_id in USERS + ], + ) + op.bulk_insert( + workspaces, + [ + { + "workspace_id": workspace_id, + "workspace_code": workspace_code, + "workspace_name": workspace_name, + "active_root_uri": f"s3://workspaces/{workspace_id}/", + "status": "active", + "created_by": USERS[0][0], + "description": "Self-hosted demo workspace", + } + for workspace_id, workspace_code, workspace_name in WORKSPACES + ], + ) + op.bulk_insert( + members, + [ + { + "workspace_id": workspace_id, + "user_id": user_id, + "role_id": role_id, + "member_status": "active", + } + for workspace_id, _, _ in WORKSPACES + for user_id, _, _, role_id in USERS + ], + ) + + # ── enable demo password login (from 9a1b2c3d4e5f) ───────────── + password = os.environ.get("INITIAL_ADMIN_PASSWORD", "admin12345") + seeded_user_ids = ( + "0000000000RF6FG1SDBXG59S13", + "0000000000H2QYCGPCWQM1JSGS", + "0000000000RWG40ESZPGJT629J", + "00000000004CQV7WASJA6N6FW4", + ) + users_update = sa.table( + "users", + sa.column("user_id", sa.String), + sa.column("password_hash", sa.String), + ) + op.execute( + users_update.update() + .where(users_update.c.user_id.in_(seeded_user_ids)) + .where(users_update.c.password_hash == "demo-login-disabled") + .values(password_hash=hash_password(password)) + ) + + +def downgrade() -> None: + """Drop everything (reverse of upgrade).""" + op.drop_index('uk_workspaces_code', table_name='workspaces') + op.drop_index('idx_workspaces_status', table_name='workspaces') + op.drop_index('fk_workspaces_created_by', table_name='workspaces') + op.drop_table('workspaces') + op.drop_index('idx_workspace_members_user', table_name='workspace_members') + op.drop_index('idx_workspace_members_role', table_name='workspace_members') + op.drop_table('workspace_members') + op.drop_index('uk_versions_script_no', table_name='versions') + op.drop_index('uk_versions_script_hash', table_name='versions') + op.drop_index('uk_versions_artifact', table_name='versions') + op.drop_index('idx_versions_workspace_created', table_name='versions') + op.drop_index('idx_versions_creator', table_name='versions') + op.drop_index('fk_versions_source_object', table_name='versions') + op.drop_table('versions') + op.drop_index('uk_users_username', table_name='users') + op.drop_index('uk_users_email', table_name='users') + op.drop_index('idx_users_status', table_name='users') + op.drop_index('fk_users_platform_role', table_name='users') + op.drop_table('users') + op.drop_index('uk_upload_sessions_idempotency', table_name='upload_sessions') + op.drop_index('idx_upload_sessions_workspace', table_name='upload_sessions') + op.drop_index('idx_upload_sessions_object_key', table_name='upload_sessions') + op.drop_index('idx_upload_sessions_expiry', table_name='upload_sessions') + op.drop_index('fk_upload_sessions_user', table_name='upload_sessions') + op.drop_index('fk_upload_sessions_storage_object', table_name='upload_sessions') + op.drop_table('upload_sessions') + op.drop_index('uk_storage_workspace_path', table_name='storage_objects') + op.drop_index('uk_storage_bucket_key', table_name='storage_objects') + op.drop_index('idx_storage_workspace_usage', table_name='storage_objects') + op.drop_index('idx_storage_parent', table_name='storage_objects') + op.drop_index('idx_storage_owner', table_name='storage_objects') + op.drop_index('idx_storage_content_hash', table_name='storage_objects') + op.drop_index('fk_storage_created_by', table_name='storage_objects') + op.drop_table('storage_objects') + op.drop_index('uk_scripts_workspace_name', table_name='scripts') + op.drop_index('uk_scripts_current_object', table_name='scripts') + op.drop_index('idx_scripts_workspace', table_name='scripts') + op.drop_index('idx_scripts_owner', table_name='scripts') + op.drop_table('scripts') + op.drop_index('idx_schedules_workspace', table_name='schedules') + op.drop_index('idx_schedules_due', table_name='schedules') + op.drop_index('fk_schedules_updated_by', table_name='schedules') + op.drop_index('fk_schedules_created_by', table_name='schedules') + op.drop_table('schedules') + op.drop_index('uk_schedule_runs_idempotency', table_name='schedule_runs') + op.drop_index('idx_schedule_runs_workspace_status', table_name='schedule_runs') + op.drop_index('idx_schedule_runs_status', table_name='schedule_runs') + op.drop_index('idx_schedule_runs_schedule', table_name='schedule_runs') + op.drop_index('fk_schedule_runs_user', table_name='schedule_runs') + op.drop_index('fk_schedule_runs_result', table_name='schedule_runs') + op.drop_index('fk_schedule_runs_logs', table_name='schedule_runs') + op.drop_table('schedule_runs') + op.drop_index('uk_schedule_nodes_key', table_name='schedule_nodes') + op.drop_index('idx_schedule_nodes_version', table_name='schedule_nodes') + op.drop_table('schedule_nodes') + op.drop_index('uk_node_runs_attempt', table_name='schedule_node_runs') + op.drop_index('idx_node_runs_version', table_name='schedule_node_runs') + op.drop_index('idx_node_runs_status', table_name='schedule_node_runs') + op.drop_index('fk_node_runs_result', table_name='schedule_node_runs') + op.drop_index('fk_node_runs_node', table_name='schedule_node_runs') + op.drop_index('fk_node_runs_logs', table_name='schedule_node_runs') + op.drop_table('schedule_node_runs') + op.drop_index('uk_schedule_edges_pair', table_name='schedule_edges') + op.drop_index('idx_schedule_edges_target', table_name='schedule_edges') + op.drop_index('fk_schedule_edges_source', table_name='schedule_edges') + op.drop_table('schedule_edges') + op.drop_index('uk_roles_code', table_name='roles') + op.drop_table('roles') + op.drop_index('fk_role_permissions_permission', table_name='role_permissions') + op.drop_table('role_permissions') + op.drop_index('uk_permissions_code', table_name='permissions') + op.drop_index('idx_permissions_module', table_name='permissions') + op.drop_table('permissions') + op.drop_index('idx_outbox_pending', table_name='outbox_events') + op.drop_index('idx_outbox_idempotency', table_name='outbox_events') + op.drop_index('idx_outbox_aggregate', table_name='outbox_events') + op.drop_table('outbox_events') + op.drop_index('uk_data_resources_object', table_name='data_resources') + op.drop_index('idx_data_resources_workspace', table_name='data_resources') + op.drop_index('idx_data_resources_owner', table_name='data_resources') + op.drop_table('data_resources') + op.drop_index('idx_consumer_inbox_status', table_name='consumer_inbox') + op.drop_table('consumer_inbox') \ No newline at end of file