2026-09-27·by Sijie Wang#standmeet#architecture#design#events

two-sources-of-events

事件的两种来源:触发器与 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 这份路径规则由 UT TestSQLPathSegmentMatchesGo 对齐 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 作为一条边界)。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

two-sources-of-events