12 · PostgreSQL、Drizzle 与迁移¶
一句话结论: 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 索引:
外键通常连接具体对象 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. 迁移设计常见阶段¶
对非空新字段/唯一约束,安全顺序通常是:
- 添加 nullable 或有兼容默认的新列;
- backfill 历史 rows;
- 更新应用双读/双写或完成切换;
- 添加索引/约束;
- 后续 migration 移除旧字段/兼容代码。
Paperclip 有长时间运行的 session、queued run 和插件数据,不能假设升级时数据库没有活动状态。
13. Search 与大字段¶
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. 可复用经验¶
- 数据库是并发事实层;类型和 ORM 不能替代约束。
- 部分唯一索引特别适合“历史可保留、活动对象唯一”。
- 单状态转换用 CAS,多对象一致性用 transaction/row lock。
- JSONB 放可扩展配置,核心查询/锁字段保持结构化列。
- Embedded 开发模式尽量使用与生产相同的数据库语义。
- 外部副作用用 ledger/reconciliation 跨越事务边界。
- Company portability 应导出业务模型,不应复制敏感运行状态。
源码入口¶
packages/db/src/schema/index.tspackages/db/src/migrations/packages/db/src/client.ts(或 DB package connection exports)server/src/db/migrate.ts及 migration helpersserver/src/embedded-postgres.ts相关启动模块doc/DATABASE.md、docs/deploy/database.mdserver/src/services/company-portability.tscli/src/commands/db-backup.ts
数据写入后怎样快速呈现在 Board 上?下一章:13 · React UI 与实时数据链路。