Files
model-platform/migrations/versions/20260724_0001_v1_schema_baseline.py
T

653 lines
51 KiB
Python

"""v1 schema baseline
Revision ID: 20260724_0001
Revises:
Create Date: 2026-07-24 05:57:13.620909
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision: str = '20260724_0001'
down_revision: str | Sequence[str] | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('consumer_inbox',
sa.Column('consumer_name', sa.String(length=128), nullable=False),
sa.Column('event_id', sa.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='数据库事件处理批次标识'),
sa.Column('processed_at', mysql.DATETIME(fsp=3), nullable=True),
sa.Column('error_message', sa.String(length=2000), nullable=True),
sa.PrimaryKeyConstraint('consumer_name', 'event_id'),
comment='消费者幂等 Inbox,防止数据库事件重复处理'
)
op.create_index('idx_consumer_inbox_status', 'consumer_inbox', ['consumer_name', 'process_status', 'created_at'], unique=False)
op.create_table('outbox_events',
sa.Column('event_id', sa.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.PrimaryKeyConstraint('event_id'),
comment='事务 Outbox;由 Schedule Executor 直接轮询处理'
)
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', sa.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.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('roles',
sa.Column('role_id', sa.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.PrimaryKeyConstraint('role_id'),
comment='角色'
)
op.create_index('uk_roles_code', 'roles', ['role_code'], unique=True)
op.create_table('role_permissions',
sa.Column('role_id', sa.CHAR(length=26), nullable=False),
sa.Column('permission_id', sa.CHAR(length=26), nullable=False),
sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False),
sa.ForeignKeyConstraint(['permission_id'], ['permissions.permission_id'], name='fk_role_permissions_permission', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['role_id'], ['roles.role_id'], name='fk_role_permissions_role', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('role_id', 'permission_id'),
comment='角色权限'
)
op.create_index('fk_role_permissions_permission', 'role_permissions', ['permission_id'], unique=False)
op.create_table('users',
sa.Column('user_id', sa.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', sa.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('deleted_at', mysql.DATETIME(fsp=3), nullable=True),
sa.ForeignKeyConstraint(['platform_role_id'], ['roles.role_id'], name='fk_users_platform_role', ondelete='SET NULL'),
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('workspaces',
sa.Column('workspace_id', sa.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', sa.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='RustFS bucket'),
sa.Column('artifact_prefix', sa.String(length=512), nullable=True, comment='RustFS object key prefix'),
sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['users.user_id'], name='fk_workspaces_created_by', ondelete='RESTRICT'),
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)
op.create_table('audit_logs',
sa.Column('audit_id', mysql.BIGINT(), nullable=False),
sa.Column('action_code', sa.String(length=128), nullable=False),
sa.Column('target_type', sa.String(length=64), nullable=False),
sa.Column('operation_status', sa.String(length=16), server_default=sa.text("'success'"), nullable=False),
sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False),
sa.Column('workspace_id', sa.CHAR(length=26), nullable=True),
sa.Column('actor_user_id', sa.CHAR(length=26), nullable=True),
sa.Column('target_id', sa.String(length=128), nullable=True),
sa.Column('client_ip', sa.String(length=45), nullable=True),
sa.Column('user_agent', sa.String(length=1000), nullable=True),
sa.Column('detail_json', sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(['actor_user_id'], ['users.user_id'], name='fk_audit_actor', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_audit_workspace', ondelete='SET NULL'),
sa.PrimaryKeyConstraint('audit_id'),
comment='操作审计日志'
)
op.create_index('idx_audit_action_time', 'audit_logs', ['action_code', 'created_at'], unique=False)
op.create_index('idx_audit_actor_time', 'audit_logs', ['actor_user_id', 'created_at'], unique=False)
op.create_index('idx_audit_workspace_time', 'audit_logs', ['workspace_id', 'created_at'], unique=False)
op.create_table('runtime_instances',
sa.Column('runtime_id', sa.CHAR(length=26), nullable=False),
sa.Column('workspace_id', sa.CHAR(length=26), nullable=False),
sa.Column('runtime_type', sa.String(length=24), server_default=sa.text("'jupyter'"), nullable=False),
sa.Column('runtime_provider', sa.String(length=24), nullable=False, comment='process/docker/kubernetes'),
sa.Column('proxy_base_path', sa.String(length=512), nullable=False),
sa.Column('desired_state', sa.String(length=16), server_default=sa.text("'running'"), nullable=False),
sa.Column('actual_state', sa.String(length=24), server_default=sa.text("'provisioning'"), nullable=False, comment='provisioning/starting/running/unhealthy/stopping/stopped/failed'),
sa.Column('state_version', mysql.INTEGER(), server_default=sa.text('0'), nullable=False, comment='乐观锁版本'),
sa.Column('started_by', sa.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', sa.CHAR(length=26), nullable=True, comment='为空表示 Workspace 级 Runtime'),
sa.Column('runtime_ref', sa.String(length=255), nullable=True, comment='PID/container ID/pod UID'),
sa.Column('host_node', sa.String(length=255), nullable=True),
sa.Column('internal_url', sa.String(length=1000), nullable=True),
sa.Column('started_at', mysql.DATETIME(fsp=3), nullable=True),
sa.Column('last_heartbeat_at', mysql.DATETIME(fsp=3), nullable=True),
sa.Column('lease_expires_at', mysql.DATETIME(fsp=3), nullable=True),
sa.Column('stopped_at', mysql.DATETIME(fsp=3), nullable=True),
sa.Column('error_message', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], name='fk_runtime_owner', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['started_by'], ['users.user_id'], name='fk_runtime_started_by', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_runtime_workspace', ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('runtime_id'),
comment='Jupyter/未来 VS Code、OpenCode Runtime 实例'
)
op.create_index('fk_runtime_started_by', 'runtime_instances', ['started_by'], unique=False)
op.create_index('idx_runtime_lease', 'runtime_instances', ['actual_state', 'lease_expires_at'], unique=False)
op.create_index('idx_runtime_owner_state', 'runtime_instances', ['owner_user_id', 'actual_state'], unique=False)
op.create_index('idx_runtime_workspace_state', 'runtime_instances', ['workspace_id', 'runtime_type', 'actual_state'], unique=False)
op.create_table('schedules',
sa.Column('schedule_id', sa.CHAR(length=26), nullable=False),
sa.Column('workspace_id', sa.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', sa.CHAR(length=26), nullable=False),
sa.Column('updated_by', sa.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('deleted_at', mysql.DATETIME(fsp=3), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['users.user_id'], name='fk_schedules_created_by', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['updated_by'], ['users.user_id'], name='fk_schedules_updated_by', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_schedules_workspace', ondelete='RESTRICT'),
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('storage_objects',
sa.Column('storage_object_id', sa.CHAR(length=26), nullable=False),
sa.Column('workspace_id', sa.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='workspace_fs/rustfs'),
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', sa.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', sa.CHAR(length=26), nullable=True),
sa.Column('parent_object_id', sa.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', sa.CHAR(length=64), nullable=True, comment='SHA-256 hex'),
sa.Column('object_etag', sa.String(length=255), nullable=True),
sa.Column('deleted_at', mysql.DATETIME(fsp=3), nullable=True),
sa.ForeignKeyConstraint(['created_by'], ['users.user_id'], name='fk_storage_created_by', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], name='fk_storage_owner', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['parent_object_id'], ['storage_objects.storage_object_id'], name='fk_storage_parent', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_storage_workspace', ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('storage_object_id'),
comment='Workspace 文件和 RustFS 对象的统一元数据'
)
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('workspace_members',
sa.Column('workspace_id', sa.CHAR(length=26), nullable=False),
sa.Column('user_id', sa.CHAR(length=26), nullable=False),
sa.Column('role_id', sa.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.ForeignKeyConstraint(['role_id'], ['roles.role_id'], name='fk_workspace_members_role', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='fk_workspace_members_user', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_workspace_members_workspace', ondelete='CASCADE'),
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('data_resources',
sa.Column('resource_id', sa.CHAR(length=26), nullable=False),
sa.Column('workspace_id', sa.CHAR(length=26), nullable=False),
sa.Column('storage_object_id', sa.CHAR(length=26), nullable=False),
sa.Column('owner_user_id', sa.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('deleted_at', mysql.DATETIME(fsp=3), nullable=True),
sa.ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], name='fk_data_resources_owner', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['storage_object_id'], ['storage_objects.storage_object_id'], name='fk_data_resources_object', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_data_resources_workspace', ondelete='RESTRICT'),
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('edit_sessions',
sa.Column('edit_session_id', sa.CHAR(length=26), nullable=False),
sa.Column('workspace_id', sa.CHAR(length=26), nullable=False),
sa.Column('storage_object_id', sa.CHAR(length=26), nullable=False),
sa.Column('user_id', sa.CHAR(length=26), nullable=False),
sa.Column('lock_key', sa.String(length=512), nullable=False),
sa.Column('lock_token_hash', sa.BINARY(length=32), nullable=False, comment='不保存原始 token'),
sa.Column('session_status', sa.String(length=16), server_default=sa.text("'active'"), nullable=False, comment='active/closed/expired/failed'),
sa.Column('started_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False),
sa.Column('last_heartbeat_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False),
sa.Column('expires_at', mysql.DATETIME(fsp=3), nullable=False),
sa.Column('runtime_id', sa.CHAR(length=26), nullable=True),
sa.Column('jupyter_session_id', sa.String(length=255), nullable=True),
sa.Column('ended_at', mysql.DATETIME(fsp=3), nullable=True),
sa.Column('end_reason', sa.String(length=64), nullable=True),
sa.ForeignKeyConstraint(['runtime_id'], ['runtime_instances.runtime_id'], name='fk_edit_sessions_runtime', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['storage_object_id'], ['storage_objects.storage_object_id'], name='fk_edit_sessions_object', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='fk_edit_sessions_user', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_edit_sessions_workspace', ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('edit_session_id'),
comment='编辑会话与数据库租约;MySQL 为锁状态权威'
)
op.create_index('fk_edit_sessions_workspace', 'edit_sessions', ['workspace_id'], unique=False)
op.create_index('idx_edit_sessions_object', 'edit_sessions', ['storage_object_id', 'session_status', 'expires_at'], unique=False)
op.create_index('idx_edit_sessions_runtime', 'edit_sessions', ['runtime_id', 'session_status'], unique=False)
op.create_index('idx_edit_sessions_user', 'edit_sessions', ['user_id', 'session_status'], unique=False)
op.create_table('schedule_runs',
sa.Column('run_id', sa.CHAR(length=26), nullable=False),
sa.Column('schedule_id', sa.CHAR(length=26), nullable=False),
sa.Column('workspace_id', sa.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', sa.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', sa.CHAR(length=26), nullable=True),
sa.Column('result_object_id', sa.CHAR(length=26), nullable=True),
sa.ForeignKeyConstraint(['logs_object_id'], ['storage_objects.storage_object_id'], name='fk_schedule_runs_logs', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['result_object_id'], ['storage_objects.storage_object_id'], name='fk_schedule_runs_result', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['schedule_id'], ['schedules.schedule_id'], name='fk_schedule_runs_schedule', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['triggered_by'], ['users.user_id'], name='fk_schedule_runs_user', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_schedule_runs_workspace', ondelete='RESTRICT'),
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('scripts',
sa.Column('script_id', sa.CHAR(length=26), nullable=False),
sa.Column('workspace_id', sa.CHAR(length=26), nullable=False),
sa.Column('current_object_id', sa.CHAR(length=26), nullable=False, comment='当前工作副本'),
sa.Column('owner_user_id', sa.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('deleted_at', mysql.DATETIME(fsp=3), nullable=True),
sa.ForeignKeyConstraint(['current_object_id'], ['storage_objects.storage_object_id'], name='fk_scripts_current_object', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], name='fk_scripts_owner', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_scripts_workspace', ondelete='RESTRICT'),
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_table('upload_sessions',
sa.Column('upload_id', sa.CHAR(length=26), nullable=False),
sa.Column('workspace_id', sa.CHAR(length=26), nullable=False),
sa.Column('user_id', sa.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', sa.CHAR(length=64), nullable=True),
sa.Column('content_type', sa.String(length=255), nullable=True),
sa.Column('storage_object_id', sa.CHAR(length=26), nullable=True),
sa.Column('completed_at', mysql.DATETIME(fsp=3), nullable=True),
sa.ForeignKeyConstraint(['storage_object_id'], ['storage_objects.storage_object_id'], name='fk_upload_sessions_storage_object', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], name='fk_upload_sessions_user', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_upload_sessions_workspace', ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('upload_id'),
comment='RustFS 预签名上传会话;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('workspace_operations',
sa.Column('operation_id', sa.CHAR(length=26), nullable=False),
sa.Column('workspace_id', sa.CHAR(length=26), nullable=False),
sa.Column('operation_type', sa.String(length=24), nullable=False, comment='open/close/mount/unmount/start/stop/restart/recycle'),
sa.Column('operation_status', sa.String(length=24), server_default=sa.text("'pending'"), nullable=False, comment='pending/running/succeeded/failed/cancelled'),
sa.Column('state_version', mysql.INTEGER(), server_default=sa.text('0'), nullable=False, comment='乐观锁版本'),
sa.Column('requested_by', sa.CHAR(length=26), nullable=False),
sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False),
sa.Column('runtime_id', sa.CHAR(length=26), nullable=True),
sa.Column('request_id', sa.String(length=128), nullable=True),
sa.Column('started_at', mysql.DATETIME(fsp=3), nullable=True),
sa.Column('finished_at', mysql.DATETIME(fsp=3), nullable=True),
sa.Column('error_code', sa.String(length=64), nullable=True),
sa.Column('error_message', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['requested_by'], ['users.user_id'], name='fk_workspace_operations_user', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['runtime_id'], ['runtime_instances.runtime_id'], name='fk_workspace_operations_runtime', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_workspace_operations_workspace', ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('operation_id'),
comment='无状态 Backend 的 Workspace/Jupyter 异步操作记录'
)
op.create_index('fk_workspace_operations_user', 'workspace_operations', ['requested_by'], unique=False)
op.create_index('idx_workspace_operations_runtime', 'workspace_operations', ['runtime_id', 'created_at'], unique=False)
op.create_index('idx_workspace_operations_workspace', 'workspace_operations', ['workspace_id', 'operation_status', 'created_at'], unique=False)
op.create_index('uk_workspace_operations_request', 'workspace_operations', ['request_id'], unique=True)
op.create_table('notebook_snapshots',
sa.Column('snapshot_id', sa.CHAR(length=26), nullable=False),
sa.Column('workspace_id', sa.CHAR(length=26), nullable=False),
sa.Column('script_id', sa.CHAR(length=26), nullable=False),
sa.Column('source_object_id', sa.CHAR(length=26), nullable=False),
sa.Column('artifact_object_id', sa.CHAR(length=26), nullable=False),
sa.Column('snapshot_name', sa.String(length=255), nullable=False),
sa.Column('content_hash', sa.CHAR(length=64), nullable=False),
sa.Column('outputs_stripped', mysql.TINYINT(display_width=1), server_default=sa.text('1'), nullable=False),
sa.Column('created_by', sa.CHAR(length=26), 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=1000), nullable=True),
sa.ForeignKeyConstraint(['artifact_object_id'], ['storage_objects.storage_object_id'], name='fk_snapshots_artifact_object', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['created_by'], ['users.user_id'], name='fk_snapshots_created_by', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['script_id'], ['scripts.script_id'], name='fk_snapshots_script', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['source_object_id'], ['storage_objects.storage_object_id'], name='fk_snapshots_source_object', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_snapshots_workspace', ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('snapshot_id'),
comment='Notebook 开发快照,append-only'
)
op.create_index('fk_snapshots_created_by', 'notebook_snapshots', ['created_by'], unique=False)
op.create_index('fk_snapshots_source_object', 'notebook_snapshots', ['source_object_id'], unique=False)
op.create_index('idx_snapshots_workspace_created', 'notebook_snapshots', ['workspace_id', 'created_at'], unique=False)
op.create_index('uk_snapshots_artifact', 'notebook_snapshots', ['artifact_object_id'], unique=True)
op.create_index('uk_snapshots_script_hash', 'notebook_snapshots', ['script_id', 'content_hash'], unique=True)
op.create_table('versions',
sa.Column('versions_id', sa.CHAR(length=26), nullable=False, comment='稳定版本唯一 ID'),
sa.Column('workspace_id', sa.CHAR(length=26), nullable=False),
sa.Column('script_id', sa.CHAR(length=26), nullable=False),
sa.Column('source_object_id', sa.CHAR(length=26), nullable=False, comment='发布时的源对象'),
sa.Column('artifact_object_id', sa.CHAR(length=26), nullable=False, comment='RustFS 不可变版本制品'),
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', sa.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', sa.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.ForeignKeyConstraint(['artifact_object_id'], ['storage_objects.storage_object_id'], name='fk_versions_artifact_object', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['created_by'], ['users.user_id'], name='fk_versions_created_by', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['script_id'], ['scripts.script_id'], name='fk_versions_script', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['source_object_id'], ['storage_objects.storage_object_id'], name='fk_versions_source_object', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_versions_workspace', ondelete='RESTRICT'),
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('experiments',
sa.Column('experiment_id', sa.CHAR(length=26), nullable=False),
sa.Column('workspace_id', sa.CHAR(length=26), nullable=False),
sa.Column('owner_user_id', sa.CHAR(length=26), nullable=False),
sa.Column('experiment_name', sa.String(length=255), nullable=False),
sa.Column('source_type', sa.String(length=24), nullable=False, comment='python/notebook/schedule/rerun'),
sa.Column('experiment_status', sa.String(length=24), server_default=sa.text("'queued'"), nullable=False),
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('script_id', sa.CHAR(length=26), nullable=True),
sa.Column('versions_id', sa.CHAR(length=26), nullable=True, comment='工作副本运行时可为空'),
sa.Column('schedule_run_id', sa.CHAR(length=26), nullable=True),
sa.Column('parent_experiment_id', sa.CHAR(length=26), nullable=True),
sa.Column('parameters_json', sa.JSON(), nullable=True),
sa.Column('environment_json', sa.JSON(), nullable=True),
sa.Column('result_summary', sa.String(length=2000), nullable=True),
sa.Column('logs_object_id', sa.CHAR(length=26), nullable=True),
sa.Column('result_object_id', sa.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('deleted_at', mysql.DATETIME(fsp=3), nullable=True),
sa.ForeignKeyConstraint(['logs_object_id'], ['storage_objects.storage_object_id'], name='fk_experiments_logs', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['owner_user_id'], ['users.user_id'], name='fk_experiments_owner', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['parent_experiment_id'], ['experiments.experiment_id'], name='fk_experiments_parent', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['result_object_id'], ['storage_objects.storage_object_id'], name='fk_experiments_result', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['schedule_run_id'], ['schedule_runs.run_id'], name='fk_experiments_schedule_run', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['script_id'], ['scripts.script_id'], name='fk_experiments_script', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['versions_id'], ['versions.versions_id'], name='fk_experiments_version', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['workspace_id'], ['workspaces.workspace_id'], name='fk_experiments_workspace', ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('experiment_id'),
comment='实验记录'
)
op.create_index('fk_experiments_logs', 'experiments', ['logs_object_id'], unique=False)
op.create_index('fk_experiments_parent', 'experiments', ['parent_experiment_id'], unique=False)
op.create_index('fk_experiments_result', 'experiments', ['result_object_id'], unique=False)
op.create_index('fk_experiments_script', 'experiments', ['script_id'], unique=False)
op.create_index('idx_experiments_owner', 'experiments', ['owner_user_id', 'created_at'], unique=False)
op.create_index('idx_experiments_schedule_run', 'experiments', ['schedule_run_id'], unique=False)
op.create_index('idx_experiments_version', 'experiments', ['versions_id'], unique=False)
op.create_index('idx_experiments_workspace', 'experiments', ['workspace_id', 'experiment_status', 'created_at'], unique=False)
op.create_table('schedule_nodes',
sa.Column('node_id', sa.CHAR(length=26), nullable=False),
sa.Column('schedule_id', sa.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', sa.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.ForeignKeyConstraint(['schedule_id'], ['schedules.schedule_id'], name='fk_schedule_nodes_schedule', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['versions_id'], ['versions.versions_id'], name='fk_schedule_nodes_version', ondelete='RESTRICT'),
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('experiment_metrics',
sa.Column('metric_id', mysql.BIGINT(), nullable=False),
sa.Column('experiment_id', sa.CHAR(length=26), nullable=False),
sa.Column('metric_name', sa.String(length=128), nullable=False),
sa.Column('recorded_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False),
sa.Column('metric_value', sa.Double(asdecimal=True), nullable=True),
sa.Column('metric_text', sa.String(length=1000), nullable=True),
sa.Column('step_no', sa.BigInteger(), nullable=True),
sa.ForeignKeyConstraint(['experiment_id'], ['experiments.experiment_id'], name='fk_experiment_metrics_experiment', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('metric_id'),
comment='实验指标,支持筛选和曲线'
)
op.create_index('idx_experiment_metrics_lookup', 'experiment_metrics', ['experiment_id', 'metric_name', 'step_no'], unique=False)
op.create_table('experiment_resources',
sa.Column('experiment_id', sa.CHAR(length=26), nullable=False),
sa.Column('resource_id', sa.CHAR(length=26), nullable=False),
sa.Column('resource_role', sa.String(length=16), server_default=sa.text("'input'"), nullable=False, comment='input/output'),
sa.Column('created_at', mysql.DATETIME(fsp=3), server_default=sa.text('CURRENT_TIMESTAMP(3)'), nullable=False),
sa.ForeignKeyConstraint(['experiment_id'], ['experiments.experiment_id'], name='fk_experiment_resources_experiment', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['resource_id'], ['data_resources.resource_id'], name='fk_experiment_resources_resource', ondelete='RESTRICT'),
sa.PrimaryKeyConstraint('experiment_id', 'resource_id', 'resource_role'),
comment='实验与数据资源'
)
op.create_index('fk_experiment_resources_resource', 'experiment_resources', ['resource_id'], unique=False)
op.create_table('schedule_edges',
sa.Column('edge_id', sa.CHAR(length=26), nullable=False),
sa.Column('schedule_id', sa.CHAR(length=26), nullable=False),
sa.Column('source_node_id', sa.CHAR(length=26), nullable=False),
sa.Column('target_node_id', sa.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.ForeignKeyConstraint(['schedule_id'], ['schedules.schedule_id'], name='fk_schedule_edges_schedule', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['source_node_id'], ['schedule_nodes.node_id'], name='fk_schedule_edges_source', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['target_node_id'], ['schedule_nodes.node_id'], name='fk_schedule_edges_target', ondelete='CASCADE'),
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', sa.CHAR(length=26), nullable=False),
sa.Column('run_id', sa.CHAR(length=26), nullable=False),
sa.Column('node_id', sa.CHAR(length=26), nullable=False),
sa.Column('versions_id', sa.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', sa.CHAR(length=26), nullable=True),
sa.Column('result_object_id', sa.CHAR(length=26), nullable=True),
sa.ForeignKeyConstraint(['logs_object_id'], ['storage_objects.storage_object_id'], name='fk_node_runs_logs', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['node_id'], ['schedule_nodes.node_id'], name='fk_node_runs_node', ondelete='RESTRICT'),
sa.ForeignKeyConstraint(['result_object_id'], ['storage_objects.storage_object_id'], name='fk_node_runs_result', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['run_id'], ['schedule_runs.run_id'], name='fk_node_runs_run', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['versions_id'], ['versions.versions_id'], name='fk_node_runs_version', ondelete='RESTRICT'),
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)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# MySQL removes indexes with their table. Dropping FK-supporting indexes
# explicitly first raises error 1553, so tables are removed in reverse
# dependency order and MySQL performs the index cleanup.
op.drop_table('schedule_node_runs')
op.drop_table('schedule_edges')
op.drop_table('experiment_resources')
op.drop_table('experiment_metrics')
op.drop_table('schedule_nodes')
op.drop_table('experiments')
op.drop_table('versions')
op.drop_table('notebook_snapshots')
op.drop_table('workspace_operations')
op.drop_table('upload_sessions')
op.drop_table('scripts')
op.drop_table('schedule_runs')
op.drop_table('edit_sessions')
op.drop_table('data_resources')
op.drop_table('workspace_members')
op.drop_table('storage_objects')
op.drop_table('schedules')
op.drop_table('runtime_instances')
op.drop_table('audit_logs')
op.drop_table('workspaces')
op.drop_table('users')
op.drop_table('role_permissions')
op.drop_table('roles')
op.drop_table('permissions')
op.drop_table('outbox_events')
op.drop_table('consumer_inbox')
# ### end Alembic commands ###