fix(db): 时间列 TIMESTAMPTZ + 热路径索引 + 状态 CHECK 约束
P1-1 base.py 时间列改 timezone=True;迁移把 16 表 created_at/updated_at ALTER 为 TIMESTAMPTZ(手写 USING ... AT TIME ZONE 'UTC')。 P1-3 新增 8 个索引(chapter_reviews/chapter_digests 复合、owner_id、 usage_ledger、rules + jobs/rules 部分索引)。 P2 chapters/foreshadow status CHECK 约束。 迁移链 ad2c4c663daf → b1a2c3d4e5f6 → c2b3d4e5f6a7,upgrade/downgrade 往返无漂移。
This commit is contained in:
@@ -11,7 +11,10 @@ from typing import Any
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
CheckConstraint,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
LargeBinary,
|
||||
Text,
|
||||
@@ -37,7 +40,7 @@ class User(UuidPk, CreatedAt, Base):
|
||||
|
||||
class Project(UuidPk, TimestampedMixin, Base):
|
||||
__tablename__ = "projects"
|
||||
owner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
|
||||
owner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
|
||||
title: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
genre: Mapped[str | None] = mapped_column(Text)
|
||||
logline: Mapped[str | None] = mapped_column(Text)
|
||||
@@ -92,6 +95,13 @@ class Outline(UuidPk, Base):
|
||||
|
||||
class ChapterDigest(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "chapter_digests"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"ix_chapter_digests_project_chapter",
|
||||
"project_id",
|
||||
text("chapter_no DESC"),
|
||||
),
|
||||
)
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
@@ -101,7 +111,13 @@ class ChapterDigest(UuidPk, CreatedAt, Base):
|
||||
|
||||
class Foreshadow(UuidPk, Base):
|
||||
__tablename__ = "foreshadow"
|
||||
__table_args__ = (UniqueConstraint("project_id", "code"),)
|
||||
__table_args__ = (
|
||||
UniqueConstraint("project_id", "code"),
|
||||
CheckConstraint(
|
||||
"status IN ('OPEN', 'PARTIAL', 'CLOSED', 'OVERDUE')",
|
||||
name="ck_foreshadow_status",
|
||||
),
|
||||
)
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
@@ -116,7 +132,10 @@ class Foreshadow(UuidPk, Base):
|
||||
links: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
progress: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
nullable=False, server_default=text("now()"), onupdate=text("now()")
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=text("now()"),
|
||||
onupdate=text("now()"),
|
||||
)
|
||||
|
||||
|
||||
@@ -132,8 +151,15 @@ class StyleFingerprint(UuidPk, CreatedAt, Base):
|
||||
|
||||
class Rule(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "rules"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"ix_rules_global",
|
||||
"id",
|
||||
postgresql_where=text("project_id IS NULL"),
|
||||
),
|
||||
)
|
||||
project_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE")
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
level: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
@@ -141,7 +167,13 @@ class Rule(UuidPk, CreatedAt, Base):
|
||||
|
||||
class Chapter(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "chapters"
|
||||
__table_args__ = (UniqueConstraint("project_id", "chapter_no", "version"),)
|
||||
__table_args__ = (
|
||||
UniqueConstraint("project_id", "chapter_no", "version"),
|
||||
CheckConstraint(
|
||||
"status IN ('draft', 'accepted')",
|
||||
name="ck_chapters_status",
|
||||
),
|
||||
)
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
@@ -172,6 +204,14 @@ class ChapterInjection(UuidPk, TimestampedMixin, Base):
|
||||
|
||||
class ChapterReview(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "chapter_reviews"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"ix_chapter_reviews_project_chapter",
|
||||
"project_id",
|
||||
"chapter_no",
|
||||
text("created_at DESC"),
|
||||
),
|
||||
)
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
@@ -217,8 +257,8 @@ class TierRouting(UuidPk, Base):
|
||||
|
||||
class UsageLedger(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "usage_ledger"
|
||||
owner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
|
||||
project_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("projects.id"))
|
||||
owner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
|
||||
project_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("projects.id"), index=True)
|
||||
provider: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
model: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
input_tokens: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
@@ -246,6 +286,13 @@ class Skill(UuidPk, CreatedAt, Base):
|
||||
|
||||
class Job(UuidPk, TimestampedMixin, Base):
|
||||
__tablename__ = "jobs"
|
||||
__table_args__ = (
|
||||
Index(
|
||||
"ix_jobs_status_running",
|
||||
"status",
|
||||
postgresql_where=text("status = 'running'"),
|
||||
),
|
||||
)
|
||||
project_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user