refactor(db): 抽出 GUID 类型并兼容 SQLite 的 JSONB 字段

- 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) <noreply@anthropic.com>
This commit is contained in:
Cheng Zhou
2026-05-25 12:36:02 +08:00
parent ffcf217142
commit f40e979c53
3 changed files with 51 additions and 7 deletions
+40
View File
@@ -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 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): class Base(DeclarativeBase):
type_annotation_map = {
uuid.UUID: GUID(),
}
pass pass
+6 -4
View File
@@ -4,12 +4,14 @@ from typing import Optional
import uuid import uuid
from datetime import datetime 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.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.db.base_class import Base from app.db.base_class import Base
JSONB_TYPE = JSON().with_variant(JSONB, "postgresql")
class StudySetupConfig(Base): class StudySetupConfig(Base):
__tablename__ = "study_setup_configs" __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) 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) 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) 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) publish_status: Mapped[str] = mapped_column(default="DRAFT", nullable=False)
published_config: 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, 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") current_branch_name: Mapped[str] = mapped_column(nullable=False, default="main")
active_branch_base_version_id: Mapped[Optional[uuid.UUID]] = mapped_column( 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 UUID(as_uuid=True), ForeignKey("study_setup_config_versions.id", ondelete="SET NULL"), nullable=True
@@ -4,12 +4,14 @@ from typing import Optional
import uuid import uuid
from datetime import datetime 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.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.db.base_class import Base from app.db.base_class import Base
JSONB_TYPE = JSON().with_variant(JSONB, "postgresql")
class StudySetupConfigVersion(Base): class StudySetupConfigVersion(Base):
__tablename__ = "study_setup_config_versions" __tablename__ = "study_setup_config_versions"
@@ -35,8 +37,8 @@ class StudySetupConfigVersion(Base):
merged_from_version_id: Mapped[Optional[uuid.UUID]] = mapped_column( 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 UUID(as_uuid=True), ForeignKey("study_setup_config_versions.id", ondelete="SET NULL"), nullable=True
) )
config: Mapped[dict] = mapped_column(JSONB, nullable=False) config: Mapped[dict] = mapped_column(JSONB_TYPE, nullable=False)
published_project_snapshot: Mapped[dict | None] = mapped_column(JSONB, nullable=True) 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_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()) 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()) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())