From 28069f516ba76980ff4a5ae793461419831e2761 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:21:15 +0800 Subject: [PATCH] refactor: delete table --- backend/src/backend/scripts.py | 120 +-- common/src/common/db/models/__init__.py | 15 +- common/src/common/db/models/experiments.py | 112 -- common/src/common/db/models/runtime.py | 115 +- common/src/common/db/models/scripts.py | 32 - .../20260728_0002_demo_workspaces_users.py | 112 -- ...60728_0003_schedule_artifact_visibility.py | 41 - .../20260730_0004_remove_redis_lock_name.py | 49 - ...2860_initial_baseline_20_active_tables.py} | 988 ++++++++---------- 9 files changed, 510 insertions(+), 1074 deletions(-) delete mode 100644 common/src/common/db/models/experiments.py delete mode 100644 migrations/versions/20260728_0002_demo_workspaces_users.py delete mode 100644 migrations/versions/20260728_0003_schedule_artifact_visibility.py delete mode 100644 migrations/versions/20260730_0004_remove_redis_lock_name.py rename migrations/versions/{20260724_0001_v1_schema_baseline.py => 8d86e2f82860_initial_baseline_20_active_tables.py} (53%) diff --git a/backend/src/backend/scripts.py b/backend/src/backend/scripts.py index 06513db..db3791e 100644 --- a/backend/src/backend/scripts.py +++ b/backend/src/backend/scripts.py @@ -21,7 +21,6 @@ from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from common.db.models import ( - EditSessions, Scripts, StorageObjects, Versions, @@ -227,24 +226,31 @@ async def get_script_row( return script, storage_object -async def require_no_active_edit_session( - session: AsyncSession, - storage_object_ids: list[str], +def require_script_modify_access( + script: Scripts, + *, + user_id: str, + is_admin: bool, ) -> None: - if not storage_object_ids: + """Enforce the V3.1 §4 access rules for write operations on a script. + + Rules: + * admin: always allowed + * owner: always allowed + * non-owner: allowed iff ``is_locked`` is False + + Reads (``list`` / ``get``) intentionally do not call this helper — the + design contract is "everyone in the workspace can see the script + list, but only the owner (or admin) can mutate when locked". + """ + if is_admin or script.owner_user_id == user_id: return - active_session = await session.scalar( - select(EditSessions).where( - EditSessions.storage_object_id.in_(storage_object_ids), - EditSessions.session_status == "active", - EditSessions.expires_at > datetime.now(UTC).replace(tzinfo=None), - ) + if not script.is_locked: + return + raise HTTPException( + status.HTTP_403_FORBIDDEN, + "script is locked; only the owner (or an administrator) may modify it", ) - if active_session is not None: - raise HTTPException( - status.HTTP_409_CONFLICT, - "file is being edited; end the editing session before deletion", - ) async def create_script_record( @@ -542,10 +548,6 @@ async def delete_workspace_directory( ) ) ).all() - await require_no_active_edit_session( - session, - [script.current_object_id for script, _storage in rows], - ) for script, _storage in rows: await request.app.state.storage_client.delete_object( script.current_object_id @@ -624,14 +626,11 @@ async def update_script( session, for_update=True, ) - if ( - script.owner_user_id != context.user.user_id - and not context.is_admin - ): - raise HTTPException( - status.HTTP_403_FORBIDDEN, - "script can only be changed by its owner or an administrator", - ) + require_script_modify_access( + script, + user_id=context.user.user_id, + is_admin=context.is_admin, + ) content = validate_script_content(payload.content, script.script_type) if not storage_object.relative_path: raise HTTPException( @@ -677,17 +676,10 @@ async def delete_script( session, for_update=True, ) - if ( - script.owner_user_id != context.user.user_id - and not context.is_admin - ): - raise HTTPException( - status.HTTP_403_FORBIDDEN, - "script can only be deleted by its owner or an administrator", - ) - await require_no_active_edit_session( - session, - [script.current_object_id], + require_script_modify_access( + script, + user_id=context.user.user_id, + is_admin=context.is_admin, ) await request.app.state.storage_client.delete_object( script.current_object_id @@ -722,14 +714,11 @@ async def publish_version( session, for_update=True, ) - if ( - script.owner_user_id != context.user.user_id - and not context.is_admin - ): - raise HTTPException( - status.HTTP_403_FORBIDDEN, - "script can only be published by its owner or an administrator", - ) + require_script_modify_access( + script, + user_id=context.user.user_id, + is_admin=context.is_admin, + ) if ( payload.source_object_id and payload.source_object_id != script.current_object_id @@ -867,20 +856,31 @@ async def delete_version( context: RequestContext = Depends(request_context), session: AsyncSession = Depends(database_session), ) -> dict[str, Any]: - version = await session.get(Versions, versions_id) - if ( - version is None - or version.workspace_id != context.workspace.workspace_id - ): - raise HTTPException(status.HTTP_404_NOT_FOUND, "稳定版本不存在") - if ( - version.created_by != context.user.user_id - and not context.is_admin - ): - raise HTTPException( - status.HTTP_403_FORBIDDEN, - "仅稳定版本发布者或管理员可以删除", + # Join to Scripts so the lock + owner check rides on the script record, + # not on whoever happened to publish this specific version. Lock state + # is a property of the script as a whole (architecture V3.1 §4), not of + # any one version of it. + row = ( + await session.execute( + select(Versions, Scripts) + .join( + Scripts, + Scripts.script_id == Versions.script_id, + ) + .where( + Versions.versions_id == versions_id, + Versions.workspace_id == context.workspace.workspace_id, + ) ) + ).one_or_none() + if row is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "稳定版本不存在") + version, script = row + require_script_modify_access( + script, + user_id=context.user.user_id, + is_admin=context.is_admin, + ) version.schedule_hidden_at = datetime.now(UTC).replace(tzinfo=None) await session.flush() diff --git a/common/src/common/db/models/__init__.py b/common/src/common/db/models/__init__.py index cf0b853..15ad04a 100644 --- a/common/src/common/db/models/__init__.py +++ b/common/src/common/db/models/__init__.py @@ -1,13 +1,8 @@ from common.db.base import Base from common.db.models.audit import AuditLogs from common.db.models.events import ConsumerInbox, OutboxEvents -from common.db.models.experiments import ( - ExperimentMetrics, - ExperimentResources, - Experiments, -) from common.db.models.identity import Permissions, RolePermissions, Roles, Users -from common.db.models.runtime import EditSessions, RuntimeInstances, WorkspaceOperations +from common.db.models.runtime import WorkspaceOperations from common.db.models.schedules import ( ScheduleEdges, ScheduleNodeRuns, @@ -15,7 +10,7 @@ from common.db.models.schedules import ( ScheduleRuns, Schedules, ) -from common.db.models.scripts import NotebookSnapshots, Scripts, Versions +from common.db.models.scripts import Scripts, Versions from common.db.models.storage import DataResources, StorageObjects, UploadSessions from common.db.models.workspaces import WorkspaceMembers, Workspaces @@ -24,16 +19,10 @@ __all__ = [ "AuditLogs", "ConsumerInbox", "DataResources", - "EditSessions", - "ExperimentMetrics", - "ExperimentResources", - "Experiments", - "NotebookSnapshots", "OutboxEvents", "Permissions", "RolePermissions", "Roles", - "RuntimeInstances", "ScheduleEdges", "ScheduleNodeRuns", "ScheduleNodes", diff --git a/common/src/common/db/models/experiments.py b/common/src/common/db/models/experiments.py deleted file mode 100644 index 14b6c5d..0000000 --- a/common/src/common/db/models/experiments.py +++ /dev/null @@ -1,112 +0,0 @@ -import datetime -import decimal -from typing import Optional - -from sqlalchemy import BigInteger, Double, Index, JSON, String, text -from sqlalchemy.dialects.mysql import BIGINT, CHAR, DATETIME, INTEGER, TINYINT -from sqlalchemy.orm import Mapped, mapped_column - -from common.db.base import Base - - -class Experiments(Base): - __tablename__ = "experiments" - __table_args__ = ( - Index("fk_experiments_logs", "logs_object_id"), - Index("fk_experiments_parent", "parent_experiment_id"), - Index("fk_experiments_result", "result_object_id"), - Index("fk_experiments_script", "script_id"), - Index("idx_experiments_owner", "owner_user_id", "created_at"), - Index("idx_experiments_schedule_run", "schedule_run_id"), - Index("idx_experiments_version", "versions_id"), - Index( - "idx_experiments_workspace", - "workspace_id", - "experiment_status", - "created_at", - ), - {"comment": "实验记录"}, - ) - - experiment_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) - workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) - owner_user_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) - experiment_name: Mapped[str] = mapped_column(String(255), nullable=False) - source_type: Mapped[str] = mapped_column( - String(24), nullable=False, comment="python/notebook/schedule/rerun" - ) - experiment_status: Mapped[str] = mapped_column( - String(24), nullable=False, server_default=text("'queued'") - ) - state_version: Mapped[int] = mapped_column( - INTEGER, nullable=False, server_default=text("0"), comment="乐观锁版本" - ) - created_at: Mapped[datetime.datetime] = mapped_column( - DATETIME(fsp=3), nullable=False, server_default=text("CURRENT_TIMESTAMP(3)") - ) - script_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) - versions_id: Mapped[Optional[str]] = mapped_column( - CHAR(26), comment="工作副本运行时可为空" - ) - schedule_run_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) - parent_experiment_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) - parameters_json: Mapped[Optional[dict]] = mapped_column(JSON) - environment_json: Mapped[Optional[dict]] = mapped_column(JSON) - result_summary: Mapped[Optional[str]] = mapped_column(String(2000)) - logs_object_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) - result_object_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) - started_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) - finished_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) - duration_ms: Mapped[Optional[int]] = mapped_column(BIGINT) - is_deleted: Mapped[int] = mapped_column( - TINYINT(1), nullable=False, server_default=text("0") - ) - deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) - - -class ExperimentMetrics(Base): - __tablename__ = "experiment_metrics" - __table_args__ = ( - Index("idx_experiment_metrics_lookup", "experiment_id", "metric_name", "step_no"), - {"comment": "实验指标,支持筛选和曲线"}, - ) - - metric_id: Mapped[int] = mapped_column(BIGINT, primary_key=True) - experiment_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) - metric_name: Mapped[str] = mapped_column(String(128), nullable=False) - recorded_at: Mapped[datetime.datetime] = mapped_column( - DATETIME(fsp=3), nullable=False, server_default=text("CURRENT_TIMESTAMP(3)") - ) - metric_value: Mapped[Optional[decimal.Decimal]] = mapped_column( - Double(asdecimal=True) - ) - metric_text: Mapped[Optional[str]] = mapped_column(String(1000)) - step_no: Mapped[Optional[int]] = mapped_column(BigInteger) - is_deleted: Mapped[int] = mapped_column( - TINYINT(1), nullable=False, server_default=text("0") - ) - deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) - - -class ExperimentResources(Base): - __tablename__ = "experiment_resources" - __table_args__ = ( - Index("fk_experiment_resources_resource", "resource_id"), - {"comment": "实验与数据资源"}, - ) - - experiment_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) - resource_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) - resource_role: Mapped[str] = mapped_column( - String(16), - primary_key=True, - server_default=text("'input'"), - comment="input/output", - ) - created_at: Mapped[datetime.datetime] = mapped_column( - DATETIME(fsp=3), nullable=False, server_default=text("CURRENT_TIMESTAMP(3)") - ) - is_deleted: Mapped[int] = mapped_column( - TINYINT(1), nullable=False, server_default=text("0") - ) - deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) diff --git a/common/src/common/db/models/runtime.py b/common/src/common/db/models/runtime.py index c86b13e..1299077 100644 --- a/common/src/common/db/models/runtime.py +++ b/common/src/common/db/models/runtime.py @@ -1,126 +1,13 @@ import datetime from typing import Optional -from sqlalchemy import BINARY, Index, String, Text, text +from sqlalchemy import Index, String, Text, text from sqlalchemy.dialects.mysql import CHAR, DATETIME, INTEGER, TINYINT from sqlalchemy.orm import Mapped, mapped_column from common.db.base import Base -class RuntimeInstances(Base): - __tablename__ = "runtime_instances" - __table_args__ = ( - Index("fk_runtime_started_by", "started_by"), - Index("idx_runtime_lease", "actual_state", "lease_expires_at"), - Index("idx_runtime_owner_state", "owner_user_id", "actual_state"), - Index( - "idx_runtime_workspace_state", - "workspace_id", - "runtime_type", - "actual_state", - ), - {"comment": "Jupyter/未来 VS Code、OpenCode Runtime 实例"}, - ) - - runtime_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) - workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) - runtime_type: Mapped[str] = mapped_column( - String(24), nullable=False, server_default=text("'jupyter'") - ) - runtime_provider: Mapped[str] = mapped_column( - String(24), nullable=False, comment="process/docker/kubernetes" - ) - proxy_base_path: Mapped[str] = mapped_column(String(512), nullable=False) - desired_state: Mapped[str] = mapped_column( - String(16), nullable=False, server_default=text("'running'") - ) - actual_state: Mapped[str] = mapped_column( - String(24), - nullable=False, - server_default=text("'provisioning'"), - comment="provisioning/starting/running/unhealthy/stopping/stopped/failed", - ) - state_version: Mapped[int] = mapped_column( - INTEGER, nullable=False, server_default=text("0"), comment="乐观锁版本" - ) - started_by: Mapped[str] = mapped_column(CHAR(26), nullable=False) - created_at: Mapped[datetime.datetime] = mapped_column( - DATETIME(fsp=3), nullable=False, server_default=text("CURRENT_TIMESTAMP(3)") - ) - updated_at: Mapped[datetime.datetime] = mapped_column( - DATETIME(fsp=3), - nullable=False, - server_default=text("CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"), - ) - owner_user_id: Mapped[Optional[str]] = mapped_column( - CHAR(26), comment="为空表示 Workspace 级 Runtime" - ) - runtime_ref: Mapped[Optional[str]] = mapped_column( - String(255), comment="PID/container ID/pod UID" - ) - host_node: Mapped[Optional[str]] = mapped_column(String(255)) - internal_url: Mapped[Optional[str]] = mapped_column(String(1000)) - started_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) - last_heartbeat_at: Mapped[Optional[datetime.datetime]] = mapped_column( - DATETIME(fsp=3) - ) - lease_expires_at: Mapped[Optional[datetime.datetime]] = mapped_column( - DATETIME(fsp=3) - ) - stopped_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) - error_message: Mapped[Optional[str]] = mapped_column(Text) - is_deleted: Mapped[int] = mapped_column( - TINYINT(1), nullable=False, server_default=text("0") - ) - deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) - - -class EditSessions(Base): - __tablename__ = "edit_sessions" - __table_args__ = ( - Index("fk_edit_sessions_workspace", "workspace_id"), - Index( - "idx_edit_sessions_object", - "storage_object_id", - "session_status", - "expires_at", - ), - Index("idx_edit_sessions_runtime", "runtime_id", "session_status"), - Index("idx_edit_sessions_user", "user_id", "session_status"), - {"comment": "编辑会话审计"}, - ) - - edit_session_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) - workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) - storage_object_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) - user_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) - lock_token_hash: Mapped[bytes] = mapped_column( - BINARY(32), nullable=False, comment="不保存原始 token" - ) - session_status: Mapped[str] = mapped_column( - String(16), - nullable=False, - server_default=text("'active'"), - comment="active/closed/expired/failed", - ) - started_at: Mapped[datetime.datetime] = mapped_column( - DATETIME(fsp=3), nullable=False, server_default=text("CURRENT_TIMESTAMP(3)") - ) - last_heartbeat_at: Mapped[datetime.datetime] = mapped_column( - DATETIME(fsp=3), nullable=False, server_default=text("CURRENT_TIMESTAMP(3)") - ) - expires_at: Mapped[datetime.datetime] = mapped_column(DATETIME(fsp=3), nullable=False) - runtime_id: Mapped[Optional[str]] = mapped_column(CHAR(26)) - jupyter_session_id: Mapped[Optional[str]] = mapped_column(String(255)) - ended_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) - end_reason: Mapped[Optional[str]] = mapped_column(String(64)) - is_deleted: Mapped[int] = mapped_column( - TINYINT(1), nullable=False, server_default=text("0") - ) - deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) - - class WorkspaceOperations(Base): __tablename__ = "workspace_operations" __table_args__ = ( diff --git a/common/src/common/db/models/scripts.py b/common/src/common/db/models/scripts.py index 9a3fe01..153453b 100644 --- a/common/src/common/db/models/scripts.py +++ b/common/src/common/db/models/scripts.py @@ -63,38 +63,6 @@ class Scripts(Base): deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) -class NotebookSnapshots(Base): - __tablename__ = "notebook_snapshots" - __table_args__ = ( - Index("fk_snapshots_created_by", "created_by"), - Index("fk_snapshots_source_object", "source_object_id"), - Index("idx_snapshots_workspace_created", "workspace_id", "created_at"), - Index("uk_snapshots_artifact", "artifact_object_id", unique=True), - Index("uk_snapshots_script_hash", "script_id", "content_hash", unique=True), - {"comment": "Notebook 开发快照,append-only"}, - ) - - snapshot_id: Mapped[str] = mapped_column(CHAR(26), primary_key=True) - workspace_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) - script_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) - source_object_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) - artifact_object_id: Mapped[str] = mapped_column(CHAR(26), nullable=False) - snapshot_name: Mapped[str] = mapped_column(String(255), nullable=False) - content_hash: Mapped[str] = mapped_column(CHAR(64), nullable=False) - outputs_stripped: Mapped[int] = mapped_column( - TINYINT(1), nullable=False, server_default=text("1") - ) - created_by: Mapped[str] = mapped_column(CHAR(26), nullable=False) - created_at: Mapped[datetime.datetime] = mapped_column( - DATETIME(fsp=3), nullable=False, server_default=text("CURRENT_TIMESTAMP(3)") - ) - description: Mapped[Optional[str]] = mapped_column(String(1000)) - is_deleted: Mapped[int] = mapped_column( - TINYINT(1), nullable=False, server_default=text("0") - ) - deleted_at: Mapped[Optional[datetime.datetime]] = mapped_column(DATETIME(fsp=3)) - - class Versions(Base): __tablename__ = "versions" __table_args__ = ( diff --git a/migrations/versions/20260728_0002_demo_workspaces_users.py b/migrations/versions/20260728_0002_demo_workspaces_users.py deleted file mode 100644 index ec15f48..0000000 --- a/migrations/versions/20260728_0002_demo_workspaces_users.py +++ /dev/null @@ -1,112 +0,0 @@ -"""demo workspaces and users - -Revision ID: 20260728_0002 -Revises: 20260724_0001 -Create Date: 2026-07-28 -""" - -from collections.abc import Sequence - -from alembic import op - - -revision: str = "20260728_0002" -down_revision: str | Sequence[str] | None = "20260724_0001" -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -ADMIN_ROLE = "0000000000HNQN4KM476QNKW1C" -DEVELOPER_ROLE = "00000000005RCQ4GPGBK3WZYMM" -MODEL_WORKSPACE = "00000000000BM630VT9ARVFZPC" -RISK_WORKSPACE = "0000000000AE0NC0V5T424KK86" -ZHANG = "0000000000RF6FG1SDBXG59S13" -LI = "0000000000H2QYCGPCWQM1JSGS" -WANG = "0000000000RWG40ESZPGJT629J" -ZHAO = "00000000004CQV7WASJA6N6FW4" - - -def upgrade() -> None: - op.execute( - f""" - INSERT INTO roles - (role_id, role_code, role_name, role_scope, is_builtin) - VALUES - ('{ADMIN_ROLE}', 'admin', '管理员', 'workspace', 1), - ('{DEVELOPER_ROLE}', 'developer', '开发人员', 'workspace', 1) - ON DUPLICATE KEY UPDATE - role_name = VALUES(role_name), - role_scope = VALUES(role_scope) - """ - ) - op.execute( - f""" - INSERT INTO users - (user_id, username, display_name, password_hash, status, email) - VALUES - ('{ZHANG}', 'admin-zhang', '张三', 'demo-login-disabled', 'active', - 'zhangsan@example.local'), - ('{LI}', 'admin-li', '李四', 'demo-login-disabled', 'active', - 'lisi@example.local'), - ('{WANG}', 'dev-wang', '王五', 'demo-login-disabled', 'active', - 'wangwu@example.local'), - ('{ZHAO}', 'dev-zhao', '赵六', 'demo-login-disabled', 'active', - 'zhaoliu@example.local') - ON DUPLICATE KEY UPDATE - username = VALUES(username), - display_name = VALUES(display_name), - status = 'active', - email = VALUES(email) - """ - ) - op.execute( - f""" - INSERT INTO workspaces - (workspace_id, workspace_code, workspace_name, active_root_uri, - quota_bytes, used_bytes, status, created_by, description, - artifact_bucket, artifact_prefix) - VALUES - ('{MODEL_WORKSPACE}', 'model-dev', '模型开发 Workspace', - 'file:///workspace/workspaces/model-dev', 0, 0, 'active', - '{ZHANG}', '模型开发与脚本调度', 'model-platform', - 'workspaces/model-dev'), - ('{RISK_WORKSPACE}', 'risk-validation', '风险验证 Workspace', - 'file:///workspace/workspaces/risk-validation', 0, 0, 'active', - '{LI}', '风险模型验证与批处理', 'model-platform', - 'workspaces/risk-validation') - ON DUPLICATE KEY UPDATE - workspace_name = VALUES(workspace_name), - active_root_uri = VALUES(active_root_uri), - status = 'active', - description = VALUES(description) - """ - ) - values = [] - for workspace_id in (MODEL_WORKSPACE, RISK_WORKSPACE): - for user_id, role_id in ( - (ZHANG, ADMIN_ROLE), - (LI, ADMIN_ROLE), - (WANG, DEVELOPER_ROLE), - (ZHAO, DEVELOPER_ROLE), - ): - values.append( - f"('{workspace_id}', '{user_id}', '{role_id}', 'active')" - ) - op.execute( - """ - INSERT INTO workspace_members - (workspace_id, user_id, role_id, member_status) - VALUES - """ - + ",\n".join(values) - + """ - ON DUPLICATE KEY UPDATE - role_id = VALUES(role_id), - member_status = 'active' - """ - ) - - -def downgrade() -> None: - # 演示身份可能已产生业务数据,降级时保留,避免破坏外键引用。 - pass diff --git a/migrations/versions/20260728_0003_schedule_artifact_visibility.py b/migrations/versions/20260728_0003_schedule_artifact_visibility.py deleted file mode 100644 index 0cb4f09..0000000 --- a/migrations/versions/20260728_0003_schedule_artifact_visibility.py +++ /dev/null @@ -1,41 +0,0 @@ -"""decouple schedule artifact visibility from version history - -Revision ID: 20260728_0003 -Revises: 20260728_0002 -Create Date: 2026-07-28 -""" - -from collections.abc import Sequence - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql - - -revision: str = "20260728_0003" -down_revision: str | Sequence[str] | None = "20260728_0002" -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - op.add_column( - "versions", - sa.Column( - "schedule_hidden_at", - mysql.DATETIME(fsp=3), - nullable=True, - comment="从调度稳定版本列表移除的时间;不影响版本和运行历史", - ), - ) - op.create_index( - "idx_versions_schedule_visible", - "versions", - ["workspace_id", "schedule_hidden_at", "created_at"], - unique=False, - ) - - -def downgrade() -> None: - op.drop_index("idx_versions_schedule_visible", table_name="versions") - op.drop_column("versions", "schedule_hidden_at") diff --git a/migrations/versions/20260730_0004_remove_redis_lock_name.py b/migrations/versions/20260730_0004_remove_redis_lock_name.py deleted file mode 100644 index 3b1b3f0..0000000 --- a/migrations/versions/20260730_0004_remove_redis_lock_name.py +++ /dev/null @@ -1,49 +0,0 @@ -"""remove Redis-specific lock column naming - -Revision ID: 20260730_0004 -Revises: 20260728_0003 -Create Date: 2026-07-30 -""" - -from collections.abc import Sequence - -from alembic import context, op -import sqlalchemy as sa - -revision: str = "20260730_0004" -down_revision: str | None = "20260728_0003" -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def _column_names() -> set[str]: - inspector = sa.inspect(op.get_bind()) - return {item["name"] for item in inspector.get_columns("edit_sessions")} - - -def upgrade() -> None: - if context.is_offline_mode(): - return - columns = _column_names() - if "redis_lock_key" in columns and "lock_key" not in columns: - op.alter_column( - "edit_sessions", - "redis_lock_key", - new_column_name="lock_key", - existing_type=sa.String(length=512), - existing_nullable=False, - ) - - -def downgrade() -> None: - if context.is_offline_mode(): - return - columns = _column_names() - if "lock_key" in columns and "redis_lock_key" not in columns: - op.alter_column( - "edit_sessions", - "lock_key", - new_column_name="redis_lock_key", - existing_type=sa.String(length=512), - existing_nullable=False, - ) diff --git a/migrations/versions/20260724_0001_v1_schema_baseline.py b/migrations/versions/8d86e2f82860_initial_baseline_20_active_tables.py similarity index 53% rename from migrations/versions/20260724_0001_v1_schema_baseline.py rename to migrations/versions/8d86e2f82860_initial_baseline_20_active_tables.py index ddcf2dc..c9f4673 100644 --- a/migrations/versions/20260724_0001_v1_schema_baseline.py +++ b/migrations/versions/8d86e2f82860_initial_baseline_20_active_tables.py @@ -1,8 +1,8 @@ -"""v1 schema baseline +"""initial baseline (20 active tables) -Revision ID: 20260724_0001 +Revision ID: 8d86e2f82860 Revises: -Create Date: 2026-07-24 05:57:13.620909 +Create Date: 2026-07-31 13:17:52.047720 """ from collections.abc import Sequence @@ -12,7 +12,7 @@ import sqlalchemy as sa from sqlalchemy.dialects import mysql # revision identifiers, used by Alembic. -revision: str = '20260724_0001' +revision: str = '8d86e2f82860' down_revision: str | Sequence[str] | None = None branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None @@ -21,20 +21,62 @@ depends_on: str | Sequence[str] | None = None def upgrade() -> None: """Upgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### + 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', mysql.CHAR(length=26), nullable=True), + sa.Column('actor_user_id', mysql.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.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('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('consumer_inbox', sa.Column('consumer_name', sa.String(length=128), nullable=False), - sa.Column('event_id', sa.CHAR(length=26), 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='数据库事件处理批次标识'), + 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,防止数据库事件重复处理' + 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', sa.CHAR(length=26), nullable=False), + 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), @@ -48,26 +90,40 @@ def upgrade() -> None: 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;由 Schedule Executor 直接轮询处理' + 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', sa.CHAR(length=26), nullable=False), + 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', sa.CHAR(length=26), nullable=False), + 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'), @@ -75,509 +131,21 @@ def upgrade() -> None: 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('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('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.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.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 有向边' ) @@ -585,10 +153,10 @@ def upgrade() -> None: 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('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='乐观锁版本'), @@ -599,13 +167,10 @@ def upgrade() -> None: 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.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='调度节点运行与重试' ) @@ -615,38 +180,379 @@ def upgrade() -> None: 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='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', 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('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 文件和 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('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), + 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='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('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='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', 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('workspace_operations', + sa.Column('operation_id', mysql.CHAR(length=26), nullable=False), + sa.Column('workspace_id', mysql.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', 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('runtime_id', mysql.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.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('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('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='RustFS bucket'), + sa.Column('artifact_prefix', sa.String(length=512), nullable=True, comment='RustFS 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) # ### 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') + # ### commands auto generated by Alembic - please adjust! ### + 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('uk_workspace_operations_request', table_name='workspace_operations') + op.drop_index('idx_workspace_operations_workspace', table_name='workspace_operations') + op.drop_index('idx_workspace_operations_runtime', table_name='workspace_operations') + op.drop_index('fk_workspace_operations_user', table_name='workspace_operations') + op.drop_table('workspace_operations') + 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_table('role_permissions') + 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') + op.drop_index('idx_audit_workspace_time', table_name='audit_logs') + op.drop_index('idx_audit_actor_time', table_name='audit_logs') + op.drop_index('idx_audit_action_time', table_name='audit_logs') + op.drop_table('audit_logs') # ### end Alembic commands ###