跳转至

12 · PostgreSQL、Drizzle 与迁移

PostgreSQLDrizzle ORM210 MigrationsConstraintsEmbedded Mode

一句话结论: PostgreSQL 不是 Paperclip 的被动存储,而是并发控制、幂等、租户边界和恢复的事实层;Drizzle 提供类型化访问,真正的演进与复杂约束仍由 SQL migration、条件索引和事务承担。

1. 数据层规模

快照中:

  • packages/db/src/schema/ 有 111 个 TypeScript schema 文件;
  • packages/db/src/migrations/ 有 210 个 SQL migration;
  • 另有 migration journal、检查、reconcile 和 safety helper;
  • Server 直接使用 Drizzle 与必要的原生 SQL 片段。

表的数量反映了建模选择:请求、运行、会话、事件、租约、审批、成本、文档 revision、plugin state 等都不是 JSON 塞在一个总表里。

2. 为什么选择真正的 PostgreSQL

系统依赖的能力包括:

  • transaction 与 row lock;
  • 条件/部分唯一索引;
  • JSONB;
  • GIN/trigram 搜索索引;
  • timestamp with timezone;
  • RETURNING;
  • 复杂 join/aggregate;
  • 多连接并发与 crash durability。

本地默认模式也运行 embedded PostgreSQL,而不是换一套语义不同的轻量数据库,从而减少“开发能跑、部署并发坏掉”的差异。

3. Schema 分域

可把 111 个 schema 文件归入:

域 代表表
组织与访问 companies、agents、memberships、roles、permission grants、API keys
工作 goals、projects、issues、relations、comments、labels、interactions、watchdogs
执行 wakeup requests、heartbeat runs/events、task sessions、runtime state
Workspace project/execution workspaces、operations、environments、leases、runtime services
治理 approvals、budgets、costs、decisions、activity、attention
内容与交付 documents/revisions/annotations、assets、attachments、work products
Skills/Automation company skills/policies、routines、pipelines/cases/events
Tools/Secrets tool access 全组表、secrets/versions/bindings/access events
Plugins plugin config/state/entities/database/jobs/webhooks/logs/managed resources

这种分域让外键和索引能表达业务边界,代价是跨域 mutation 必须认真设计 transaction。

4. Company scope 的数据库表达

绝大多数业务表有 company_id 并建立 company-first 索引:

(company_id, status)
(company_id, updated_at)
(company_id, project_id, ...)

外键通常连接具体对象 ID,但服务仍验证双方 company 一致。为什么不全靠复合外键?

  • Drizzle/查询复杂度会显著增加;
  • UUID 已保证对象唯一;
  • 一些历史表允许关联对象删除后 set null;
  • 应用需要给出更清楚的跨 Company 错误/隐藏存在性。

因此是“schema scope + service invariant”双层防线。

5. 条件唯一索引承担业务幂等

典型模式:

CREATE UNIQUE INDEX ...
ON issues(company_id, origin_kind, origin_id, origin_fingerprint)
WHERE origin_kind IS NOT NULL AND ...;

用途包括:

  • routine/recovery/watchdog 自动 Issue;
  • active origin 只允许一个;
  • pipeline parent request key;
  • automation triggering event;
  • default/active binding;
  • document revision number。

部分索引允许保留历史 terminal/retired rows,同时只约束“当前活动集合”。这比删除历史来腾出唯一键更适合审计系统。

6. CAS:用影响行数表达状态竞争

Drizzle 条件 update + returning 广泛用于:

  • Issue checkout;
  • Run queued → running → terminal;
  • Approval pending → outcome;
  • lease claim/release;
  • OAuth refresh lease;
  • recovery ownership;
  • plugin job claim。

通用模式:

UPDATE ... WHERE id = ? AND state IN (expected) RETURNING ...
  ├─ 1 row: 当前请求赢得转换
  └─ 0 row: reload → idempotent / stale-adopt / conflict

它比把整个业务都放在长 transaction 的悲观锁中更轻量,且容易为 worker queue 使用。

7. 什么时候使用 FOR UPDATE

需要读取多字段、做多步关联更新且必须锁住当前 row 时,服务使用 transaction + row lock,例如 Issue release 或某些 pipeline transition。

选择原则:

  • 单对象、单状态转换:优先 CAS;
  • 需要先计算、再更新多个相关事实:transaction + row lock;
  • 仍要有 unique/check 作为最后防线。

锁并不能替代幂等键;transaction retry 后仍可能重复外部副作用,因此外部调用通常放在可重放边界之外或配合 ledger。

8. JSONB 用在“可扩展配置”,不替代核心字段

JSONB 常见于:

  • adapter/runtime config;
  • issue execution settings/source trust;
  • tool transport/policy config;
  • plugin state/entity data;
  • pipeline stage/case fields;
  • run context/result metadata。

高频查询和并发控制字段仍单独成列:status、assignee、run ID、company、timestamp、lease、risk 等。

这是好的平衡:边缘 provider 配置可扩展,核心状态仍可索引和约束。

9. 时间字段是恢复证据

createdAt/updatedAt 只是基础,运行系统还需要:

  • queued/started/finished;
  • locked/acquired/lastUsed/expires/released;
  • scheduled retry;
  • revoked/archived/retired;
  • health checked;
  • last event/output;
  • source occurrence time。

Recovery 不应只看 updatedAt 推断所有事情。专用时间字段说明“哪种事实最后发生在何时”。

10. Embedded 与 External 的连接层

Embedded

  • 数据目录位于 Paperclip instance root;
  • CLI/Server 管理 Postgres 进程和端口;
  • 使用同一迁移和 driver;
  • backup/doctor 可以检查本地进程与文件。

External

  • 通过 connection string;
  • 可分离 migration URL;
  • 连接池、connect/idle timeout、prepared statement 根据部署配置;
  • 生产要自行提供备份、HA 和网络安全。

两条路径在 service 以上尽量完全一致。

11. Migration 启动协议

flowchart TD
  START["Server start"] --> LIST["load bundled migration journal"]
  LIST --> DB["inspect applied migrations"]
  DB --> COMP{"compatible / linear?"}
  COMP -->|no| FAIL["fail with diagnostic"]
  COMP -->|yes| MISS{"missing migrations?"}
  MISS -->|no| READY["DB ready"]
  MISS -->|yes| POLICY{"auto-apply allowed?"}
  POLICY -->|no| FAIL
  POLICY -->|yes| APPLY["apply in order + journal"]
  APPLY --> VERIFY["reinspect / reconcile"]
  VERIFY --> READY

Schema TS 是当前目标结构,SQL migration 是如何从旧结构安全到达目标。两者必须同时维护。

12. 迁移设计常见阶段

对非空新字段/唯一约束,安全顺序通常是:

  1. 添加 nullable 或有兼容默认的新列;
  2. backfill 历史 rows;
  3. 更新应用双读/双写或完成切换;
  4. 添加索引/约束;
  5. 后续 migration 移除旧字段/兼容代码。

Paperclip 有长时间运行的 session、queued run 和插件数据,不能假设升级时数据库没有活动状态。

Documents 对 title/body 使用 trigram/GIN 以支持全文模糊搜索。日志和附件则不直接无限塞入主 row:

  • run log 使用 log store/metadata;
  • assets 由 storage provider 保存,DB 保存元数据; -大文件 preview 有大小上限;
  • company search 使用独立提取、rate limit 和 scope。

把大内容与核心 transaction row 分开能降低写放大和锁竞争。

14. Backup 与恢复

配置支持定时 database backup、retention 和目录。CLI 有 db:backup 与 doctor 检查。生产 external DB 仍应使用平台级备份;应用内 backup 不是 HA 替代品。

备份要与以下内容一起考虑:

  • local/S3 asset storage;
  • secrets master key 或外部 vault;
  • plugin external resources;
  • active environment/provider leases。

仅恢复 SQL 可能得到“记录存在但外部产物已失”的不完整实例。

15. Portability 不是数据库 dump

Company export/import 使用业务 manifest:

  • 选择性包含 agents/goals/projects/issues/skills 等;
  • scrub secret value;
  • 记录 source/target、content hash 和相对引用;
  • collision strategy;
  • 保留/重映射 sidebar 和关系;
  • preview 后才 import。

这是迁移业务组织,而不是复制运行中 lease、API key 和本机绝对路径。

16. 数据层现实风险

Schema 很宽、服务很大

Issue/Run/Tool 表不断承载新能力,读写投影需要控制,避免 select * 让敏感/大字段进入每条请求。

条件索引需要测试 migration reality

只看 Drizzle schema 不足以确认旧数据库已有该索引;release verify 和 migration E2E 很重要。

单数据库不等于单事务世界

Adapter、provider、filesystem、Git 和 plugin worker 都在 DB 事务之外。系统需要 operation/ledger + reconciliation 处理跨资源最终一致性。

17. 可复用经验

  1. 数据库是并发事实层;类型和 ORM 不能替代约束。
  2. 部分唯一索引特别适合“历史可保留、活动对象唯一”。
  3. 单状态转换用 CAS,多对象一致性用 transaction/row lock。
  4. JSONB 放可扩展配置,核心查询/锁字段保持结构化列。
  5. Embedded 开发模式尽量使用与生产相同的数据库语义。
  6. 外部副作用用 ledger/reconciliation 跨越事务边界。
  7. Company portability 应导出业务模型,不应复制敏感运行状态。

源码入口

  • packages/db/src/schema/index.ts
  • packages/db/src/migrations/
  • packages/db/src/client.ts(或 DB package connection exports)
  • server/src/db/migrate.ts 及 migration helpers
  • server/src/embedded-postgres.ts 相关启动模块
  • doc/DATABASE.md、docs/deploy/database.md
  • server/src/services/company-portability.ts
  • cli/src/commands/db-backup.ts

数据写入后怎样快速呈现在 Board 上?下一章:13 · React UI 与实时数据链路。