事件的两种来源:触发器与 Record
上级:events
状态: 已在 v0.1.76 发布(2026-09-27)—— 设计与落地记录见 StandMeet 仓库的 docs/design/event-bus-outbox-webhooks.md。
事件只有两种来源:被监视的行变化时由数据库触发器写入,或者由用例为“没有哪一行单独表达的事实”调用 Record。两者都和变更在同一事务里写进同一张 events 表,消费方分不出来,也不需要分。
| 行变化触发器 | 经 Record 的域事件 | |
|---|---|---|
| 谁写 | Postgres,AFTER INSERT/UPDATE/DELETE | 用例,recorder.With(tx).Record(ctx, ownerID, type, subject, data) |
| 什么时候用 | 被监视的行变了 | 没有哪一行单独表达的事实 |
| 例子 | corpus.note.changed,data.op ∈ {created, updated, deleted} | access_request.created、booking.created、application.committed |
| 覆盖 | 结构上完整:写入路径不可能忘了发 | 显式调用;可接受,因为每个事件恰好只有一个产出它的用例 |
| 证明 | 触发器 UT 断言每个被监视列都出事件、空更新不出、payload 形状固定 | 每个用例一条 UT,断言事件在写入的同一事务里记下 |
行变化触发器
- 表:只有
corpus_notes发事件。其它表都声明-- events: none (原因);关于它们的语义事实(writing.published、code.issued等)走Record。 - 两个触发器:
AFTER INSERT OR DELETE,以及AFTER UPDATE … WHEN被监视的列变了。被监视的列:genre、title、body、tags、parent_id、published、show_as_source、aliases、excerpt、slug、archived、css_classes、lang。只刷了updated_at的更新不出事件。 - 主体:笔记 URI,在 SQL 里算(
corpus_note_uri、corpus_path_segment)。SQL 这份路径规则由 UTTestSQLPathSegmentMatchesGo对齐 Go 的规则。 - data:
op(created、updated、deleted)、note_id、genre、parent_id、published、was_published(取消发布也看得见)和path_changed(标题或父节点变了,索引要重建子树)。 - 触发器同时
pg_notify('standmeet_events')唤醒 relay(relay-claims-rows-not-cursor)。 - 捕获的字段定义在一个迁移和
schema.sql里;schema 一致性 UT(TestMigrationsAddNothingToASchemaSQLDatabase)保证两者一致。 - 这让
CreateWiki这一类 bug 不可能发生,而不是修一次:原来CreateWiki、CreateOutput从没调索引钩子。
已决定(2026-09-26):行变化捕获用带 WHEN 的数据库触发器;语义事件仍显式 Record。
每张表都要做决定
门禁 check-table-event-policy.sh:schema.sql 里任何 CREATE TABLE 既没声明 -- events: emit 也没声明 -- events: none (原因) 就报错;声明 emit 但没有触发器也报错。每张新表强制做决定,是必填声明不是排除清单。实现后 58 张表全部标注,只有 corpus_notes 发事件。见 no-bypass-by-structure。
数据模型(ER)
收拢前后:搜索索引
相关:event-model(两种来源写的是同一形状)、storage-bounds(WHEN 作为一条边界)。