立项配置版本管理优化(回滚逻辑、UI、下载功能)

This commit is contained in:
Cheng Zhou
2026-03-04 14:49:59 +08:00
parent 6c5e5770c6
commit 0cd03eefa8
16 changed files with 3528 additions and 418 deletions
@@ -0,0 +1,44 @@
"""add published project snapshot to study_setup_config_versions
Revision ID: 20260228_02
Revises: 20260228_01
Create Date: 2026-02-28 18:20:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "20260228_02"
down_revision: Union[str, None] = "20260228_01"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
tables = set(inspector.get_table_names())
if "study_setup_config_versions" not in tables:
return
columns = {col["name"] for col in inspector.get_columns("study_setup_config_versions")}
if "published_project_snapshot" not in columns:
op.add_column(
"study_setup_config_versions",
sa.Column("published_project_snapshot", postgresql.JSONB(astext_type=sa.Text()), nullable=True),
)
def downgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
tables = set(inspector.get_table_names())
if "study_setup_config_versions" not in tables:
return
columns = {col["name"] for col in inspector.get_columns("study_setup_config_versions")}
if "published_project_snapshot" in columns:
op.drop_column("study_setup_config_versions", "published_project_snapshot")
@@ -0,0 +1,235 @@
"""add setup config version branching metadata
Revision ID: 20260302_01
Revises: 20260228_02
Create Date: 2026-03-02 16:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "20260302_01"
down_revision: Union[str, None] = "20260228_02"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def _has_table(inspector: sa.Inspector, table_name: str) -> bool:
return table_name in set(inspector.get_table_names())
def _has_column(inspector: sa.Inspector, table_name: str, column_name: str) -> bool:
return column_name in {col["name"] for col in inspector.get_columns(table_name)}
def _has_unique_constraint(inspector: sa.Inspector, table_name: str, constraint_name: str) -> bool:
return constraint_name in {item["name"] for item in inspector.get_unique_constraints(table_name)}
def _has_foreign_key(inspector: sa.Inspector, table_name: str, fk_name: str) -> bool:
return fk_name in {item["name"] for item in inspector.get_foreign_keys(table_name)}
def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
if _has_table(inspector, "study_setup_config_versions"):
if not _has_column(inspector, "study_setup_config_versions", "branch_name"):
op.add_column("study_setup_config_versions", sa.Column("branch_name", sa.String(), nullable=True))
if not _has_column(inspector, "study_setup_config_versions", "branch_seq"):
op.add_column("study_setup_config_versions", sa.Column("branch_seq", sa.Integer(), nullable=True))
if not _has_column(inspector, "study_setup_config_versions", "version_label"):
op.add_column("study_setup_config_versions", sa.Column("version_label", sa.String(), nullable=True))
if not _has_column(inspector, "study_setup_config_versions", "parent_version_id"):
op.add_column("study_setup_config_versions", sa.Column("parent_version_id", postgresql.UUID(as_uuid=True), nullable=True))
if not _has_column(inspector, "study_setup_config_versions", "merged_from_version_id"):
op.add_column(
"study_setup_config_versions",
sa.Column("merged_from_version_id", postgresql.UUID(as_uuid=True), nullable=True),
)
op.execute("UPDATE study_setup_config_versions SET branch_name = 'main' WHERE branch_name IS NULL")
op.execute(
"UPDATE study_setup_config_versions SET branch_seq = COALESCE(display_version, version, 1) WHERE branch_seq IS NULL"
)
op.execute(
"UPDATE study_setup_config_versions SET version_label = ('v' || COALESCE(display_version, version, 1)::text) WHERE version_label IS NULL"
)
op.alter_column("study_setup_config_versions", "branch_name", nullable=False)
op.alter_column("study_setup_config_versions", "branch_seq", nullable=False)
op.alter_column("study_setup_config_versions", "version_label", nullable=False)
inspector = sa.inspect(bind)
if not _has_foreign_key(inspector, "study_setup_config_versions", "fk_setup_config_versions_parent_version"):
op.create_foreign_key(
"fk_setup_config_versions_parent_version",
"study_setup_config_versions",
"study_setup_config_versions",
["parent_version_id"],
["id"],
ondelete="SET NULL",
)
if not _has_foreign_key(inspector, "study_setup_config_versions", "fk_setup_config_versions_merged_from_version"):
op.create_foreign_key(
"fk_setup_config_versions_merged_from_version",
"study_setup_config_versions",
"study_setup_config_versions",
["merged_from_version_id"],
["id"],
ondelete="SET NULL",
)
if _has_unique_constraint(inspector, "study_setup_config_versions", "uq_setup_config_versions_study_display_version"):
op.drop_constraint(
"uq_setup_config_versions_study_display_version",
"study_setup_config_versions",
type_="unique",
)
inspector = sa.inspect(bind)
if not _has_unique_constraint(inspector, "study_setup_config_versions", "uq_setup_config_versions_study_branch_seq"):
op.create_unique_constraint(
"uq_setup_config_versions_study_branch_seq",
"study_setup_config_versions",
["study_id", "branch_name", "branch_seq"],
)
if not _has_unique_constraint(inspector, "study_setup_config_versions", "uq_setup_config_versions_study_version_label"):
op.create_unique_constraint(
"uq_setup_config_versions_study_version_label",
"study_setup_config_versions",
["study_id", "version_label"],
)
if _has_table(inspector, "study_setup_configs"):
if not _has_column(inspector, "study_setup_configs", "current_branch_name"):
op.add_column("study_setup_configs", sa.Column("current_branch_name", sa.String(), nullable=True))
if not _has_column(inspector, "study_setup_configs", "active_branch_base_version_id"):
op.add_column(
"study_setup_configs",
sa.Column("active_branch_base_version_id", postgresql.UUID(as_uuid=True), nullable=True),
)
if not _has_column(inspector, "study_setup_configs", "current_published_version_id"):
op.add_column(
"study_setup_configs",
sa.Column("current_published_version_id", postgresql.UUID(as_uuid=True), nullable=True),
)
op.execute("UPDATE study_setup_configs SET current_branch_name = 'main' WHERE current_branch_name IS NULL")
op.alter_column("study_setup_configs", "current_branch_name", nullable=False)
if _has_table(inspector, "study_setup_config_versions"):
op.execute(
"""
UPDATE study_setup_configs
SET current_published_version_id = (
SELECT v.id
FROM study_setup_config_versions AS v
WHERE v.study_id = study_setup_configs.study_id
AND study_setup_configs.published_config IS NOT NULL
AND v.config = study_setup_configs.published_config
ORDER BY
CASE
WHEN study_setup_configs.published_at IS NOT NULL
AND v.published_at = study_setup_configs.published_at THEN 0
ELSE 1
END,
v.version DESC
LIMIT 1
)
WHERE current_published_version_id IS NULL
"""
)
inspector = sa.inspect(bind)
if not _has_foreign_key(inspector, "study_setup_configs", "fk_setup_configs_active_branch_base_version"):
op.create_foreign_key(
"fk_setup_configs_active_branch_base_version",
"study_setup_configs",
"study_setup_config_versions",
["active_branch_base_version_id"],
["id"],
ondelete="SET NULL",
)
if not _has_foreign_key(inspector, "study_setup_configs", "fk_setup_configs_current_published_version"):
op.create_foreign_key(
"fk_setup_configs_current_published_version",
"study_setup_configs",
"study_setup_config_versions",
["current_published_version_id"],
["id"],
ondelete="SET NULL",
)
def downgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
if _has_table(inspector, "study_setup_configs"):
if _has_foreign_key(inspector, "study_setup_configs", "fk_setup_configs_current_published_version"):
op.drop_constraint(
"fk_setup_configs_current_published_version",
"study_setup_configs",
type_="foreignkey",
)
if _has_foreign_key(inspector, "study_setup_configs", "fk_setup_configs_active_branch_base_version"):
op.drop_constraint(
"fk_setup_configs_active_branch_base_version",
"study_setup_configs",
type_="foreignkey",
)
if _has_column(inspector, "study_setup_configs", "current_published_version_id"):
op.drop_column("study_setup_configs", "current_published_version_id")
if _has_column(inspector, "study_setup_configs", "active_branch_base_version_id"):
op.drop_column("study_setup_configs", "active_branch_base_version_id")
if _has_column(inspector, "study_setup_configs", "current_branch_name"):
op.drop_column("study_setup_configs", "current_branch_name")
inspector = sa.inspect(bind)
if _has_table(inspector, "study_setup_config_versions"):
if _has_unique_constraint(inspector, "study_setup_config_versions", "uq_setup_config_versions_study_version_label"):
op.drop_constraint(
"uq_setup_config_versions_study_version_label",
"study_setup_config_versions",
type_="unique",
)
if _has_unique_constraint(inspector, "study_setup_config_versions", "uq_setup_config_versions_study_branch_seq"):
op.drop_constraint(
"uq_setup_config_versions_study_branch_seq",
"study_setup_config_versions",
type_="unique",
)
if not _has_unique_constraint(inspector, "study_setup_config_versions", "uq_setup_config_versions_study_display_version"):
op.create_unique_constraint(
"uq_setup_config_versions_study_display_version",
"study_setup_config_versions",
["study_id", "display_version"],
)
if _has_foreign_key(inspector, "study_setup_config_versions", "fk_setup_config_versions_merged_from_version"):
op.drop_constraint(
"fk_setup_config_versions_merged_from_version",
"study_setup_config_versions",
type_="foreignkey",
)
if _has_foreign_key(inspector, "study_setup_config_versions", "fk_setup_config_versions_parent_version"):
op.drop_constraint(
"fk_setup_config_versions_parent_version",
"study_setup_config_versions",
type_="foreignkey",
)
if _has_column(inspector, "study_setup_config_versions", "merged_from_version_id"):
op.drop_column("study_setup_config_versions", "merged_from_version_id")
if _has_column(inspector, "study_setup_config_versions", "parent_version_id"):
op.drop_column("study_setup_config_versions", "parent_version_id")
if _has_column(inspector, "study_setup_config_versions", "version_label"):
op.drop_column("study_setup_config_versions", "version_label")
if _has_column(inspector, "study_setup_config_versions", "branch_seq"):
op.drop_column("study_setup_config_versions", "branch_seq")
if _has_column(inspector, "study_setup_config_versions", "branch_name"):
op.drop_column("study_setup_config_versions", "branch_name")
@@ -0,0 +1,71 @@
"""drop unique constraint on setup config version label
Revision ID: 20260302_02
Revises: 20260302_01
Create Date: 2026-03-02 20:20:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "20260302_02"
down_revision: Union[str, None] = "20260302_01"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def _has_table(inspector: sa.Inspector, table_name: str) -> bool:
return table_name in set(inspector.get_table_names())
def _has_unique_constraint(inspector: sa.Inspector, table_name: str, constraint_name: str) -> bool:
return constraint_name in {item["name"] for item in inspector.get_unique_constraints(table_name)}
def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
table_name = "study_setup_config_versions"
constraint_name = "uq_setup_config_versions_study_version_label"
if _has_table(inspector, table_name) and _has_unique_constraint(inspector, table_name, constraint_name):
op.drop_constraint(constraint_name, table_name, type_="unique")
if _has_table(inspector, table_name):
op.execute(
"""
WITH ranked AS (
SELECT
id,
display_version,
ROW_NUMBER() OVER (
PARTITION BY study_id, branch_name, display_version
ORDER BY published_at ASC, version ASC, created_at ASC
) AS rev_seq
FROM study_setup_config_versions
WHERE branch_name = 'main'
)
UPDATE study_setup_config_versions AS v
SET version_label = CASE
WHEN ranked.rev_seq <= 1 THEN ('v' || ranked.display_version::text)
ELSE ('v' || ranked.display_version::text || '-r' || ranked.rev_seq::text)
END
FROM ranked
WHERE v.id = ranked.id
"""
)
def downgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
table_name = "study_setup_config_versions"
constraint_name = "uq_setup_config_versions_study_version_label"
if _has_table(inspector, table_name) and not _has_unique_constraint(inspector, table_name, constraint_name):
op.create_unique_constraint(
constraint_name,
table_name,
["study_id", "version_label"],
)