From f40e979c534a511a96911aa7aefeb8958f2379d2 Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Mon, 25 May 2026 12:36:02 +0800 Subject: [PATCH] =?UTF-8?q?refactor(db):=20=E6=8A=BD=E5=87=BA=20GUID=20?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=B9=B6=E5=85=BC=E5=AE=B9=20SQLite=20?= =?UTF-8?q?=E7=9A=84=20JSONB=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - base_class.py 内置 GUID(TypeDecorator),PostgreSQL 走原生 UUID, SQLite 走 CHAR(36),UUID 在绑定/读取时统一转换。 - study_setup_config 与 study_setup_config_version 改用 JSONB().with_variant 以便测试库使用 JSON 字段,生产仍保留 JSONB。 Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/db/base_class.py | 40 +++++++++++++++++++ backend/app/models/study_setup_config.py | 10 +++-- .../app/models/study_setup_config_version.py | 8 ++-- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/backend/app/db/base_class.py b/backend/app/db/base_class.py index fa2b68a5..92ce9cf3 100644 --- a/backend/app/db/base_class.py +++ b/backend/app/db/base_class.py @@ -1,5 +1,45 @@ +from __future__ import annotations + +import uuid + +from sqlalchemy import String, TypeDecorator +from sqlalchemy.dialects.postgresql import UUID as PG_UUID from sqlalchemy.orm import DeclarativeBase +class GUID(TypeDecorator): + """Platform-independent UUID type. + + PostgreSQL keeps native UUID columns; SQLite test databases store canonical + UUID strings so bind/result behavior stays consistent. + """ + + impl = String + cache_ok = True + + def load_dialect_impl(self, dialect): + if dialect.name == "sqlite": + return dialect.type_descriptor(String(36)) + return dialect.type_descriptor(PG_UUID(as_uuid=True)) + + def process_bind_param(self, value, dialect): + if value is None: + return None + if dialect.name == "sqlite": + return str(value) + return value + + def process_result_value(self, value, dialect): + if value is None: + return None + if dialect.name == "sqlite" and isinstance(value, str): + return uuid.UUID(value) + return value + + class Base(DeclarativeBase): + type_annotation_map = { + uuid.UUID: GUID(), + } + pass diff --git a/backend/app/models/study_setup_config.py b/backend/app/models/study_setup_config.py index 0fb8ad27..26dda9d8 100644 --- a/backend/app/models/study_setup_config.py +++ b/backend/app/models/study_setup_config.py @@ -4,12 +4,14 @@ from typing import Optional import uuid from datetime import datetime -from sqlalchemy import DateTime, ForeignKey, Integer, UniqueConstraint, func +from sqlalchemy import DateTime, ForeignKey, Integer, JSON, UniqueConstraint, func from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import Mapped, mapped_column from app.db.base_class import Base +JSONB_TYPE = JSON().with_variant(JSONB, "postgresql") + class StudySetupConfig(Base): __tablename__ = "study_setup_configs" @@ -18,10 +20,10 @@ class StudySetupConfig(Base): id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), nullable=False, index=True) version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) - config: Mapped[dict] = mapped_column(JSONB, nullable=False) + config: Mapped[dict] = mapped_column(JSONB_TYPE, nullable=False) publish_status: Mapped[str] = mapped_column(default="DRAFT", nullable=False) - published_config: Mapped[dict | None] = mapped_column(JSONB, nullable=True) - published_project_snapshot: Mapped[dict | None] = mapped_column(JSONB, nullable=True) + published_config: Mapped[dict | None] = mapped_column(JSONB_TYPE, nullable=True) + published_project_snapshot: Mapped[dict | None] = mapped_column(JSONB_TYPE, nullable=True) current_branch_name: Mapped[str] = mapped_column(nullable=False, default="main") active_branch_base_version_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("study_setup_config_versions.id", ondelete="SET NULL"), nullable=True diff --git a/backend/app/models/study_setup_config_version.py b/backend/app/models/study_setup_config_version.py index 52d6fca1..c74a2a25 100644 --- a/backend/app/models/study_setup_config_version.py +++ b/backend/app/models/study_setup_config_version.py @@ -4,12 +4,14 @@ from typing import Optional import uuid from datetime import datetime -from sqlalchemy import DateTime, ForeignKey, Integer, UniqueConstraint, func +from sqlalchemy import DateTime, ForeignKey, Integer, JSON, UniqueConstraint, func from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import Mapped, mapped_column from app.db.base_class import Base +JSONB_TYPE = JSON().with_variant(JSONB, "postgresql") + class StudySetupConfigVersion(Base): __tablename__ = "study_setup_config_versions" @@ -35,8 +37,8 @@ class StudySetupConfigVersion(Base): merged_from_version_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("study_setup_config_versions.id", ondelete="SET NULL"), nullable=True ) - config: Mapped[dict] = mapped_column(JSONB, nullable=False) - published_project_snapshot: Mapped[dict | None] = mapped_column(JSONB, nullable=True) + config: Mapped[dict] = mapped_column(JSONB_TYPE, nullable=False) + published_project_snapshot: Mapped[dict | None] = mapped_column(JSONB_TYPE, nullable=True) published_by: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) published_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())