@cosmicdrift/kumiko-framework 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/package.json +7 -7
  2. package/src/__tests__/{anonymous-access.integration.ts → anonymous-access.integration.test.ts} +12 -9
  3. package/src/__tests__/{error-contract.integration.ts → error-contract.integration.test.ts} +5 -4
  4. package/src/__tests__/{field-access.integration.ts → field-access.integration.test.ts} +3 -3
  5. package/src/__tests__/{full-stack.integration.ts → full-stack.integration.test.ts} +7 -16
  6. package/src/__tests__/{ownership.integration.ts → ownership.integration.test.ts} +3 -2
  7. package/src/__tests__/{raw-table.integration.ts → raw-table.integration.test.ts} +18 -30
  8. package/src/__tests__/{reference-data.integration.ts → reference-data.integration.test.ts} +24 -11
  9. package/src/__tests__/{transition-guard.integration.ts → transition-guard.integration.test.ts} +12 -10
  10. package/src/api/__tests__/api.test.ts +1 -1
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +1 -1
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +1 -1
  13. package/src/api/__tests__/{batch.integration.ts → batch.integration.test.ts} +30 -30
  14. package/src/api/__tests__/body-limit.test.ts +1 -1
  15. package/src/api/__tests__/csrf-middleware.test.ts +1 -1
  16. package/src/api/__tests__/{dispatcher-live.integration.ts → dispatcher-live.integration.test.ts} +10 -9
  17. package/src/api/__tests__/metrics-endpoint.test.ts +1 -1
  18. package/src/api/__tests__/{nested-write.integration.ts → nested-write.integration.test.ts} +13 -16
  19. package/src/api/__tests__/readiness.test.ts +1 -1
  20. package/src/api/__tests__/request-id-middleware.test.ts +1 -1
  21. package/src/api/__tests__/sse-broker.test.ts +12 -12
  22. package/src/api/__tests__/sse-route.test.ts +1 -1
  23. package/src/api/readiness.ts +2 -2
  24. package/src/auth/__tests__/roles.test.ts +2 -2
  25. package/src/bun-db/__tests__/PATTERN.md +73 -0
  26. package/src/bun-db/__tests__/_helpers.ts +103 -0
  27. package/src/bun-db/__tests__/batch-methods.integration.test.ts +143 -0
  28. package/src/bun-db/__tests__/batch-methods.test.ts +20 -0
  29. package/src/bun-db/__tests__/bun-test-db.ts +19 -0
  30. package/src/bun-db/__tests__/bun-test-stack.ts +6 -0
  31. package/src/bun-db/__tests__/column-types.integration.test.ts +132 -0
  32. package/src/bun-db/__tests__/compound-types.integration.test.ts +134 -0
  33. package/src/bun-db/__tests__/jsonb-edge-cases.integration.test.ts +235 -0
  34. package/src/bun-db/__tests__/smoke.integration.test.ts +43 -0
  35. package/src/bun-db/__tests__/sql-methods.integration.test.ts +231 -0
  36. package/src/bun-db/__tests__/where-patterns.integration.test.ts +185 -0
  37. package/src/bun-db/connection.ts +84 -0
  38. package/src/bun-db/index.ts +31 -0
  39. package/src/bun-db/query.ts +845 -0
  40. package/src/compliance/__tests__/duration-spec.test.ts +1 -1
  41. package/src/compliance/__tests__/profiles.test.ts +1 -1
  42. package/src/compliance/__tests__/sub-processors.test.ts +1 -1
  43. package/src/db/__tests__/{apply-entity-event-tenant.integration.ts → apply-entity-event-tenant.integration.test.ts} +13 -11
  44. package/src/db/__tests__/big-int-field.test.ts +15 -14
  45. package/src/db/__tests__/column-ddl.integration.test.ts +113 -0
  46. package/src/db/__tests__/compound-types.test.ts +1 -1
  47. package/src/db/__tests__/{config-seed.integration.ts → config-seed.integration.test.ts} +32 -27
  48. package/src/db/__tests__/connection-options.test.ts +1 -1
  49. package/src/db/__tests__/dialect-instant.test.ts +1 -1
  50. package/src/db/__tests__/encryption.test.ts +1 -1
  51. package/src/db/__tests__/{drizzle-table-types.test.ts → entity-table-types.test.ts} +16 -16
  52. package/src/db/__tests__/{event-store-executor-list.integration.ts → event-store-executor-list.integration.test.ts} +12 -7
  53. package/src/db/__tests__/{event-store-executor.integration.ts → event-store-executor.integration.test.ts} +19 -12
  54. package/src/db/__tests__/{implicit-projection-equivalence.integration.ts → implicit-projection-equivalence.integration.test.ts} +35 -29
  55. package/src/db/__tests__/located-timestamp.test.ts +1 -1
  56. package/src/db/__tests__/money.test.ts +1 -1
  57. package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts} +18 -11
  58. package/src/db/__tests__/parse-auto-verb.test.ts +1 -1
  59. package/src/db/__tests__/{required-not-null-migration-safety.integration.ts → required-not-null-migration-safety.integration.test.ts} +28 -24
  60. package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts} +32 -28
  61. package/src/db/__tests__/sql-inventory.test.ts +56 -0
  62. package/src/db/__tests__/table-builder-indexes.test.ts +30 -11
  63. package/src/db/__tests__/table-builder-required.test.ts +20 -22
  64. package/src/db/__tests__/{tenant-db.integration.ts → tenant-db.integration.test.ts} +106 -144
  65. package/src/db/__tests__/{unique-violation-mapping.integration.ts → unique-violation-mapping.integration.test.ts} +13 -8
  66. package/src/db/api.ts +46 -0
  67. package/src/db/apply-entity-event.ts +45 -36
  68. package/src/db/assert-exists-in.ts +5 -16
  69. package/src/db/bun-provider.ts +37 -0
  70. package/src/db/config-seed.ts +4 -4
  71. package/src/db/connection.ts +14 -57
  72. package/src/db/cursor.ts +5 -56
  73. package/src/db/dialect.ts +472 -99
  74. package/src/db/eagerload.ts +5 -12
  75. package/src/db/entity-table-meta.ts +390 -0
  76. package/src/db/event-store-executor.ts +158 -100
  77. package/src/db/index.ts +33 -5
  78. package/src/db/migrate-generator.ts +350 -0
  79. package/src/db/migrate-runner.ts +206 -0
  80. package/src/db/postgres-provider.ts +25 -0
  81. package/src/db/queries/entity-read.ts +15 -0
  82. package/src/db/queries/es-ops.ts +17 -0
  83. package/src/db/queries/event-consumer.ts +170 -0
  84. package/src/db/queries/event-store-admin.ts +127 -0
  85. package/src/db/queries/event-store.ts +155 -0
  86. package/src/db/queries/projection-rebuild.ts +59 -0
  87. package/src/db/queries/raw-sql.ts +15 -0
  88. package/src/db/queries/schema-drift.ts +35 -0
  89. package/src/db/queries/seed-context.ts +58 -0
  90. package/src/db/queries/table-ops.ts +11 -0
  91. package/src/db/queries/test-stack.ts +56 -0
  92. package/src/db/query-api.ts +22 -0
  93. package/src/db/query.ts +30 -0
  94. package/src/db/reference-data.ts +19 -22
  95. package/src/db/render-ddl.ts +57 -0
  96. package/src/db/row-helpers.ts +3 -52
  97. package/src/db/schema-inspection.ts +17 -4
  98. package/src/db/sql-inventory.ts +208 -0
  99. package/src/db/table-builder.ts +48 -40
  100. package/src/db/tenant-db.ts +105 -326
  101. package/src/engine/__tests__/auth-claims-registrar.test.ts +1 -1
  102. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +3 -3
  103. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +1 -1
  104. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +5 -5
  105. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +3 -3
  106. package/src/engine/__tests__/boot-validator.test.ts +4 -3
  107. package/src/engine/__tests__/build-app-schema.test.ts +1 -1
  108. package/src/engine/__tests__/build-target.test.ts +1 -1
  109. package/src/engine/__tests__/claim-keys.test.ts +1 -1
  110. package/src/engine/__tests__/codemod-pipeline.test.ts +3 -3
  111. package/src/engine/__tests__/config-helpers.test.ts +1 -1
  112. package/src/engine/__tests__/effective-features.test.ts +1 -1
  113. package/src/engine/__tests__/engine.test.ts +1 -1
  114. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  115. package/src/engine/__tests__/event-helpers.test.ts +3 -3
  116. package/src/engine/__tests__/extends-registrar.test.ts +4 -4
  117. package/src/engine/__tests__/factories-long-text.test.ts +1 -1
  118. package/src/engine/__tests__/factories-time.test.ts +1 -1
  119. package/src/engine/__tests__/field-predicates.test.ts +1 -1
  120. package/src/engine/__tests__/hook-phases.test.ts +1 -1
  121. package/src/engine/__tests__/identifiers.test.ts +1 -1
  122. package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
  123. package/src/engine/__tests__/nav.test.ts +1 -1
  124. package/src/engine/__tests__/ownership.test.ts +10 -11
  125. package/src/engine/__tests__/parse-ref-target.test.ts +1 -1
  126. package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
  127. package/src/engine/__tests__/{pipeline-handler.integration.ts → pipeline-handler.integration.test.ts} +38 -52
  128. package/src/engine/__tests__/{pipeline-observability.integration.ts → pipeline-observability.integration.test.ts} +1 -1
  129. package/src/engine/__tests__/{pipeline-performance.integration.ts → pipeline-performance.integration.test.ts} +1 -1
  130. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
  131. package/src/engine/__tests__/post-query-hook.test.ts +1 -1
  132. package/src/engine/__tests__/projection-helpers.test.ts +25 -17
  133. package/src/engine/__tests__/projection.test.ts +4 -4
  134. package/src/engine/__tests__/qualified-name.test.ts +1 -1
  135. package/src/engine/__tests__/raw-table.test.ts +9 -8
  136. package/src/engine/__tests__/resolve-config-or-param.test.ts +5 -5
  137. package/src/engine/__tests__/run-in.test.ts +1 -1
  138. package/src/engine/__tests__/schema-builder.test.ts +1 -1
  139. package/src/engine/__tests__/screen.test.ts +1 -1
  140. package/src/engine/__tests__/search-payload-extension.test.ts +3 -3
  141. package/src/engine/__tests__/state-machine.test.ts +1 -1
  142. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +7 -7
  143. package/src/engine/__tests__/steps-aggregate-create.test.ts +4 -4
  144. package/src/engine/__tests__/steps-aggregate-update.test.ts +3 -3
  145. package/src/engine/__tests__/steps-call-feature.test.ts +5 -5
  146. package/src/engine/__tests__/steps-mail-send.test.ts +7 -7
  147. package/src/engine/__tests__/steps-read.test.ts +34 -40
  148. package/src/engine/__tests__/steps-resolver-utils.test.ts +6 -6
  149. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +24 -19
  150. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +28 -17
  151. package/src/engine/__tests__/steps-webhook-send.test.ts +6 -6
  152. package/src/engine/__tests__/steps-workflow.test.ts +7 -7
  153. package/src/engine/__tests__/system-user.test.ts +1 -1
  154. package/src/engine/__tests__/validate-projection-allowlist.test.ts +4 -5
  155. package/src/engine/__tests__/validation-hooks.test.ts +1 -1
  156. package/src/engine/__tests__/visual-tree-patterns.test.ts +1 -1
  157. package/src/engine/boot-validator/entity-handler.ts +3 -3
  158. package/src/engine/boot-validator/ownership.ts +1 -1
  159. package/src/engine/define-feature.ts +1 -2
  160. package/src/engine/entity-handlers.ts +5 -5
  161. package/src/engine/factories.ts +1 -1
  162. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -1
  163. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
  164. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +2 -2
  165. package/src/engine/feature-ast/__tests__/parse.test.ts +1 -1
  166. package/src/engine/feature-ast/__tests__/patch.test.ts +1 -1
  167. package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
  168. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +1 -1
  169. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +1 -1
  170. package/src/engine/ownership.ts +113 -41
  171. package/src/engine/pattern-library/__tests__/library.test.ts +2 -2
  172. package/src/engine/projection-helpers.ts +2 -11
  173. package/src/engine/registry.ts +2 -2
  174. package/src/engine/steps/read-find-many.ts +13 -13
  175. package/src/engine/steps/read-find-one.ts +7 -9
  176. package/src/engine/steps/unsafe-projection-delete.ts +4 -5
  177. package/src/engine/steps/unsafe-projection-upsert.ts +63 -31
  178. package/src/engine/types/feature.ts +7 -2
  179. package/src/engine/types/fields.ts +4 -5
  180. package/src/engine/types/step.ts +10 -10
  181. package/src/engine/validate-projection-allowlist.ts +23 -3
  182. package/src/entrypoint/__tests__/{entrypoint-job-wiring.integration.ts → entrypoint-job-wiring.integration.test.ts} +4 -3
  183. package/src/entrypoint/__tests__/{split-deploy.integration.ts → split-deploy.integration.test.ts} +4 -3
  184. package/src/env/__tests__/compose-env-schema.test.ts +1 -1
  185. package/src/env/__tests__/dry-run.test.ts +1 -1
  186. package/src/errors/__tests__/classes.test.ts +1 -1
  187. package/src/errors/__tests__/write-failures.test.ts +1 -1
  188. package/src/es-ops/__tests__/{context.integration.ts → context.integration.test.ts} +43 -29
  189. package/src/es-ops/__tests__/{runner.integration.ts → runner.integration.test.ts} +25 -23
  190. package/src/es-ops/__tests__/runner.test.ts +29 -19
  191. package/src/es-ops/context.ts +9 -43
  192. package/src/es-ops/operations-schema.ts +2 -2
  193. package/src/es-ops/runner.ts +12 -26
  194. package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts} +71 -45
  195. package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts} +7 -5
  196. package/src/event-store/__tests__/{get-stream-version-perf.integration.ts → get-stream-version-perf.integration.test.ts} +5 -3
  197. package/src/event-store/__tests__/{perf.integration.ts → perf.integration.test.ts} +24 -16
  198. package/src/event-store/__tests__/{snapshot.integration.ts → snapshot.integration.test.ts} +34 -28
  199. package/src/event-store/__tests__/{upcaster-dead-letter.integration.ts → upcaster-dead-letter.integration.test.ts} +11 -12
  200. package/src/event-store/__tests__/{upcaster.integration.ts → upcaster.integration.test.ts} +19 -32
  201. package/src/event-store/admin-api.ts +55 -83
  202. package/src/event-store/archive.ts +15 -39
  203. package/src/event-store/event-store.ts +92 -86
  204. package/src/event-store/events-schema.ts +2 -1
  205. package/src/event-store/index.ts +1 -0
  206. package/src/event-store/snapshot.ts +26 -24
  207. package/src/event-store/upcaster-dead-letter.ts +19 -18
  208. package/src/files/__tests__/content-disposition.test.ts +1 -1
  209. package/src/files/__tests__/{file-field-pipeline.integration.ts → file-field-pipeline.integration.test.ts} +8 -5
  210. package/src/files/__tests__/file-handle.test.ts +1 -1
  211. package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +32 -17
  212. package/src/files/__tests__/read-stream.test.ts +1 -1
  213. package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts} +26 -30
  214. package/src/files/__tests__/write-stream.test.ts +1 -1
  215. package/src/files/__tests__/zip-stream.test.ts +1 -1
  216. package/src/files/file-ref-table.ts +2 -2
  217. package/src/files/file-routes.ts +7 -9
  218. package/src/files/storage-tracking.ts +9 -17
  219. package/src/i18n/__tests__/i18n.test.ts +1 -1
  220. package/src/jobs/__tests__/{job-event-trigger.integration.ts → job-event-trigger.integration.test.ts} +6 -3
  221. package/src/jobs/__tests__/{job-multi-trigger.integration.ts → job-multi-trigger.integration.test.ts} +6 -3
  222. package/src/jobs/__tests__/{jobs.integration.ts → jobs.integration.test.ts} +5 -7
  223. package/src/lifecycle/__tests__/{lifecycle-server.integration.ts → lifecycle-server.integration.test.ts} +1 -1
  224. package/src/lifecycle/__tests__/lifecycle.test.ts +6 -6
  225. package/src/lifecycle/__tests__/signal-handlers.test.ts +6 -6
  226. package/src/logging/__tests__/pino-trace-bridge.test.ts +1 -1
  227. package/src/migrations/__tests__/compare-snapshots.test.ts +1 -1
  228. package/src/migrations/__tests__/{detect-drift.integration.ts → detect-drift.integration.test.ts} +34 -26
  229. package/src/migrations/__tests__/{detect-projections-to-rebuild.integration.ts → detect-projections-to-rebuild.integration.test.ts} +1 -1
  230. package/src/migrations/__tests__/rebuild-marker.test.ts +1 -1
  231. package/src/migrations/projection-detection.ts +12 -1
  232. package/src/migrations/schema-drift.ts +7 -23
  233. package/src/observability/__tests__/console-provider.test.ts +1 -1
  234. package/src/observability/__tests__/metric-validator.test.ts +1 -1
  235. package/src/observability/__tests__/noop-provider.test.ts +1 -1
  236. package/src/observability/__tests__/{observability.integration.ts → observability.integration.test.ts} +5 -8
  237. package/src/observability/__tests__/prometheus-meter.test.ts +1 -1
  238. package/src/observability/__tests__/recording-meter.test.ts +1 -1
  239. package/src/observability/__tests__/recording-tracer.test.ts +1 -1
  240. package/src/observability/__tests__/sensitive-filter.test.ts +1 -1
  241. package/src/pipeline/__tests__/{archive-stream.integration.ts → archive-stream.integration.test.ts} +3 -3
  242. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +9 -9
  243. package/src/pipeline/__tests__/{cascade-handler.integration.ts → cascade-handler.integration.test.ts} +18 -15
  244. package/src/pipeline/__tests__/cascade-handler.test.ts +1 -1
  245. package/src/pipeline/__tests__/{causation-chain.integration.ts → causation-chain.integration.test.ts} +12 -13
  246. package/src/pipeline/__tests__/{ctx-bridge.integration.ts → ctx-bridge.integration.test.ts} +12 -11
  247. package/src/pipeline/__tests__/dispatcher.test.ts +2 -2
  248. package/src/pipeline/__tests__/{distributed-lock.integration.ts → distributed-lock.integration.test.ts} +1 -1
  249. package/src/pipeline/__tests__/{domain-events-projections.integration.ts → domain-events-projections.integration.test.ts} +13 -15
  250. package/src/pipeline/__tests__/{event-dedup.integration.ts → event-dedup.integration.test.ts} +1 -1
  251. package/src/pipeline/__tests__/{event-define-event-strict.integration.ts → event-define-event-strict.integration.test.ts} +6 -16
  252. package/src/pipeline/__tests__/{event-dispatcher-lifecycle.integration.ts → event-dispatcher-lifecycle.integration.test.ts} +1 -1
  253. package/src/pipeline/__tests__/{event-dispatcher-multi-instance.integration.ts → event-dispatcher-multi-instance.integration.test.ts} +3 -2
  254. package/src/pipeline/__tests__/{event-dispatcher-pg-listen.integration.ts → event-dispatcher-pg-listen.integration.test.ts} +1 -1
  255. package/src/pipeline/__tests__/{event-dispatcher-recovery.integration.ts → event-dispatcher-recovery.integration.test.ts} +2 -2
  256. package/src/pipeline/__tests__/{event-dispatcher-second-audit.integration.ts → event-dispatcher-second-audit.integration.test.ts} +17 -16
  257. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +14 -12
  258. package/src/pipeline/__tests__/{event-dispatcher.integration.ts → event-dispatcher.integration.test.ts} +8 -15
  259. package/src/pipeline/__tests__/{event-retention.integration.ts → event-retention.integration.test.ts} +28 -25
  260. package/src/pipeline/__tests__/{fetch-for-writing.integration.ts → fetch-for-writing.integration.test.ts} +6 -6
  261. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +4 -4
  262. package/src/pipeline/__tests__/{load-aggregate-query.integration.ts → load-aggregate-query.integration.test.ts} +9 -5
  263. package/src/pipeline/__tests__/{msp-error-mode.integration.ts → msp-error-mode.integration.test.ts} +1 -1
  264. package/src/pipeline/__tests__/{msp-multi-hop.integration.ts → msp-multi-hop.integration.test.ts} +9 -8
  265. package/src/pipeline/__tests__/{msp-rebuild.integration.ts → msp-rebuild.integration.test.ts} +47 -55
  266. package/src/pipeline/__tests__/{multi-stream-projection.integration.ts → multi-stream-projection.integration.test.ts} +19 -53
  267. package/src/pipeline/__tests__/{perf-rebuild.integration.ts → perf-rebuild.integration.test.ts} +36 -34
  268. package/src/pipeline/__tests__/{post-query-hook.integration.ts → post-query-hook.integration.test.ts} +1 -1
  269. package/src/pipeline/__tests__/{projection-rebuild.integration.ts → projection-rebuild.integration.test.ts} +21 -30
  270. package/src/pipeline/__tests__/{query-projection.integration.ts → query-projection.integration.test.ts} +6 -5
  271. package/src/pipeline/__tests__/{redis-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
  272. package/src/pipeline/cascade-handler.ts +13 -21
  273. package/src/pipeline/dispatcher.ts +43 -48
  274. package/src/pipeline/event-consumer-state.ts +11 -2
  275. package/src/pipeline/event-dispatcher.ts +86 -146
  276. package/src/pipeline/event-retention.ts +14 -24
  277. package/src/pipeline/msp-rebuild.ts +54 -78
  278. package/src/pipeline/projection-rebuild.ts +65 -67
  279. package/src/pipeline/projection-state.ts +2 -2
  280. package/src/random/__tests__/generate.test.ts +13 -13
  281. package/src/rate-limit/__tests__/{dispatcher-l3.integration.ts → dispatcher-l3.integration.test.ts} +1 -1
  282. package/src/rate-limit/__tests__/{middleware.integration.ts → middleware.integration.test.ts} +1 -1
  283. package/src/rate-limit/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +1 -1
  284. package/src/redis/__tests__/redis-options.test.ts +1 -1
  285. package/src/search/__tests__/{meilisearch-adapter.integration.ts → meilisearch-adapter.integration.test.ts} +1 -1
  286. package/src/search/__tests__/search-adapter.test.ts +1 -1
  287. package/src/secrets/__tests__/dek-cache.test.ts +1 -3
  288. package/src/secrets/__tests__/env-master-key-provider.test.ts +1 -1
  289. package/src/secrets/__tests__/envelope.test.ts +1 -1
  290. package/src/secrets/__tests__/leak-guard.test.ts +1 -1
  291. package/src/secrets/__tests__/rotation.test.ts +1 -1
  292. package/src/stack/db.ts +25 -48
  293. package/src/stack/push-entity-projection-tables.ts +2 -4
  294. package/src/stack/table-helpers.ts +98 -61
  295. package/src/stack/test-stack.ts +8 -7
  296. package/src/testing/__tests__/db-cleanup.test.ts +40 -0
  297. package/src/testing/__tests__/e2e-generator.test.ts +1 -1
  298. package/src/testing/__tests__/{ensure-entity-table.integration.ts → ensure-entity-table.integration.test.ts} +7 -14
  299. package/src/testing/db-cleanup.ts +44 -0
  300. package/src/testing/expect-error.ts +1 -1
  301. package/src/testing/index.ts +2 -0
  302. package/src/testing/multipart-helper.ts +94 -0
  303. package/src/testing/shared-entities.ts +5 -5
  304. package/src/time/__tests__/polyfill.test.ts +1 -1
  305. package/src/time/__tests__/tz-context.test.ts +1 -1
  306. package/src/utils/__tests__/assert.test.ts +1 -1
  307. package/src/utils/__tests__/env-parse.test.ts +1 -1
  308. package/CHANGELOG.md +0 -472
  309. package/src/db/__tests__/cursor.test.ts +0 -41
  310. package/src/db/__tests__/db-helpers.test.ts +0 -369
  311. package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
  312. package/src/db/__tests__/row-helpers.test.ts +0 -59
  313. package/src/engine/steps/_drizzle-boundary.ts +0 -19
  314. package/src/files/__tests__/file-field-column.integration.ts +0 -103
@@ -1,6 +1,15 @@
1
- import { and, asc, eq, gt, sql } from "drizzle-orm";
2
1
  import { requestContext } from "../api/request-context";
3
2
  import type { DbConnection, DbTx, PgClient } from "../db/connection";
3
+ import {
4
+ advanceConsumerPastEventReturning,
5
+ insertConsumerIfAbsent,
6
+ markConsumerProcessing,
7
+ selectConsumerForUpdateSkipLocked,
8
+ updateConsumerDeliveryOutcome,
9
+ updateConsumerStatusReturning,
10
+ } from "../db/queries/event-consumer";
11
+ import { selectEventsHeadId, selectNextEventIdAfter } from "../db/queries/event-store";
12
+ import { coerceRow, extractTableInfo, selectMany } from "../db/query";
4
13
  import type { AppContext } from "../engine/types";
5
14
  import { SYSTEM_TENANT_ID } from "../engine/types/identifiers";
6
15
  import {
@@ -154,7 +163,30 @@ const DEFAULT_MAX_ATTEMPTS = 10;
154
163
  // dispatcher's main pass logic stays under ~50 LOC. Every helper takes an
155
164
  // explicit `tx` — none of them use the outer dispatcher's closure state.
156
165
 
157
- type ConsumerStateRow = typeof eventConsumerStateTable.$inferSelect;
166
+ type ConsumerStateRowShape = {
167
+ readonly name: string;
168
+ readonly instanceId: string;
169
+ readonly lastProcessedEventId: bigint;
170
+ readonly status: string;
171
+ readonly attempts: number;
172
+ readonly lastError: string | null;
173
+ readonly updatedAt: Temporal.Instant;
174
+ };
175
+ type ConsumerStateRow = ConsumerStateRowShape;
176
+
177
+ type StoredEventRow = {
178
+ readonly id: bigint;
179
+ readonly aggregateId: string;
180
+ readonly aggregateType: string;
181
+ readonly tenantId: string;
182
+ readonly version: number;
183
+ readonly type: string;
184
+ readonly eventVersion: number;
185
+ readonly payload: Record<string, unknown>;
186
+ readonly metadata: import("../event-store/event-store").EventMetadata;
187
+ readonly createdAt: Temporal.Instant;
188
+ readonly createdBy: string;
189
+ };
158
190
 
159
191
  type AcquireOutcome =
160
192
  | { readonly state: ConsumerStateRow; readonly skip: null }
@@ -180,16 +212,13 @@ async function acquireConsumerState(
180
212
  name: string,
181
213
  instanceId: string,
182
214
  ): Promise<AcquireOutcome> {
183
- const [state] = (await tx
184
- .select()
185
- .from(eventConsumerStateTable)
186
- .where(
187
- and(
188
- eq(eventConsumerStateTable.name, name),
189
- eq(eventConsumerStateTable.instanceId, instanceId),
190
- ),
191
- )
192
- .for("update", { skipLocked: true })) as [ConsumerStateRow | undefined]; // @cast-boundary db-row
215
+ const rawState = await selectConsumerForUpdateSkipLocked(tx, name, instanceId);
216
+
217
+ if (!rawState) {
218
+ return { state: null, skip: "not_registered" };
219
+ }
220
+
221
+ const state = coerceRow(rawState, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow;
193
222
 
194
223
  if (!state) {
195
224
  // Either the row never existed (no pre-reg, no ensureRegistered) or
@@ -218,12 +247,7 @@ async function preRegisterConsumers(
218
247
  ): Promise<void> {
219
248
  for (const consumer of consumers) {
220
249
  const instanceId = consumerInstanceId(consumer, dispatcherInstanceId);
221
- await db
222
- .insert(eventConsumerStateTable)
223
- .values({ name: consumer.name, instanceId, status: "idle" })
224
- .onConflictDoNothing({
225
- target: [eventConsumerStateTable.name, eventConsumerStateTable.instanceId],
226
- });
250
+ await insertConsumerIfAbsent(db, consumer.name, instanceId);
227
251
  }
228
252
  }
229
253
 
@@ -249,28 +273,20 @@ function consumerInstanceId(
249
273
  // lock already guarantees single-writer semantics; this is purely
250
274
  // informational (and resets on commit to idle/dead via persistConsumerOutcome).
251
275
  async function markProcessing(tx: DbTx, name: string, instanceId: string): Promise<void> {
252
- await tx
253
- .update(eventConsumerStateTable)
254
- .set({ status: "processing", updatedAt: sql`now()` })
255
- .where(
256
- and(
257
- eq(eventConsumerStateTable.name, name),
258
- eq(eventConsumerStateTable.instanceId, instanceId),
259
- ),
260
- );
276
+ await markConsumerProcessing(tx, name, instanceId);
261
277
  }
262
278
 
263
279
  async function fetchPendingEvents(
264
280
  tx: DbTx,
265
281
  cursor: bigint,
266
282
  batchSize: number,
267
- ): Promise<ReadonlyArray<typeof eventsTable.$inferSelect>> {
268
- return (await tx
269
- .select()
270
- .from(eventsTable)
271
- .where(gt(eventsTable.id, cursor))
272
- .orderBy(asc(eventsTable.id))
273
- .limit(batchSize)) as ReadonlyArray<typeof eventsTable.$inferSelect>; // @cast-boundary db-row
283
+ ): Promise<ReadonlyArray<StoredEventRow>> {
284
+ return (await selectMany(
285
+ tx,
286
+ eventsTable,
287
+ { id: { gt: cursor } },
288
+ { orderBy: { col: "id", direction: "asc" }, limit: batchSize },
289
+ )) as ReadonlyArray<StoredEventRow>; // @cast-boundary db-row
274
290
  }
275
291
 
276
292
  type DeliveryOutcome = {
@@ -282,7 +298,7 @@ type DeliveryOutcome = {
282
298
  readonly failed: number;
283
299
  };
284
300
 
285
- function rowToStoredEvent(row: typeof eventsTable.$inferSelect): StoredEvent {
301
+ function rowToStoredEvent(row: StoredEventRow): StoredEvent {
286
302
  return {
287
303
  id: String(row.id),
288
304
  aggregateId: row.aggregateId,
@@ -305,7 +321,7 @@ function rowToStoredEvent(row: typeof eventsTable.$inferSelect): StoredEvent {
305
321
  // restartConsumer / skipPoisonEvent).
306
322
  async function deliverEvents(
307
323
  consumer: EventConsumer,
308
- events: ReadonlyArray<typeof eventsTable.$inferSelect>,
324
+ events: ReadonlyArray<StoredEventRow>,
309
325
  context: AppContext,
310
326
  maxAttempts: number,
311
327
  state: ConsumerStateRow,
@@ -376,21 +392,7 @@ async function persistConsumerOutcome(
376
392
  instanceId: string,
377
393
  outcome: DeliveryOutcome,
378
394
  ): Promise<void> {
379
- await tx
380
- .update(eventConsumerStateTable)
381
- .set({
382
- lastProcessedEventId: outcome.cursor,
383
- attempts: outcome.attempts,
384
- status: outcome.deadLettered ? "dead" : "idle",
385
- lastError: outcome.lastError,
386
- updatedAt: sql`now()`,
387
- })
388
- .where(
389
- and(
390
- eq(eventConsumerStateTable.name, name),
391
- eq(eventConsumerStateTable.instanceId, instanceId),
392
- ),
393
- );
395
+ await updateConsumerDeliveryOutcome(tx, name, instanceId, outcome);
394
396
  }
395
397
 
396
398
  // Emit the lag gauge inside the consumer pass's tx so ops sees a snapshot
@@ -403,13 +405,7 @@ async function emitLagFromTx(
403
405
  cursor: bigint,
404
406
  meter: Meter,
405
407
  ): Promise<void> {
406
- const result = await tx.execute(
407
- sql`SELECT COALESCE(MAX(id), 0)::bigint AS head FROM kumiko_events`,
408
- );
409
- // @cast-boundary db-row — raw drizzle.execute() COALESCE-aggregate row
410
- const rows = Array.isArray(result) ? (result as Array<{ head?: bigint | string | null }>) : []; // @cast-boundary db-row
411
- const raw = rows[0]?.head;
412
- const head = typeof raw === "bigint" ? raw : BigInt(raw ?? 0);
408
+ const head = await selectEventsHeadId(tx);
413
409
  const lag = head > cursor ? Number(head - cursor) : 0;
414
410
  emitEventConsumerLag(meter, { consumer: consumerName, instanceId }, lag);
415
411
  }
@@ -535,7 +531,7 @@ export function createEventDispatcher(options: EventDispatcherOptions): EventDis
535
531
  });
536
532
 
537
533
  try {
538
- await db.transaction(async (tx) => {
534
+ await db.begin(async (tx: DbTx) => {
539
535
  const acquired = await acquireConsumerState(tx, consumer.name, instanceId);
540
536
  // skip: another instance holds the lock, or the consumer is
541
537
  // disabled/dead. Nothing to deliver this pass.
@@ -701,9 +697,7 @@ export function createEventDispatcher(options: EventDispatcherOptions): EventDis
701
697
  // lastError=null, status="idle". For events that will
702
698
  // never succeed (broken payload, removed feature code).
703
699
 
704
- function normalizeConsumerState(
705
- row: typeof eventConsumerStateTable.$inferSelect,
706
- ): ConsumerRecoveryState {
700
+ function normalizeConsumerState(row: ConsumerStateRowShape): ConsumerRecoveryState {
707
701
  return {
708
702
  name: row.name,
709
703
  instanceId: row.instanceId,
@@ -735,16 +729,11 @@ async function requireConsumerRow(
735
729
  db: DbConnection,
736
730
  name: string,
737
731
  instanceId: string,
738
- ): Promise<typeof eventConsumerStateTable.$inferSelect> {
739
- const [row] = await db
740
- .select()
741
- .from(eventConsumerStateTable)
742
- .where(
743
- and(
744
- eq(eventConsumerStateTable.name, name),
745
- eq(eventConsumerStateTable.instanceId, instanceId),
746
- ),
747
- );
732
+ ): Promise<ConsumerStateRowShape> {
733
+ const [row] = await selectMany<ConsumerStateRow>(db, eventConsumerStateTable, {
734
+ name,
735
+ instanceId,
736
+ });
748
737
  if (!row) {
749
738
  throw new Error(
750
739
  `Consumer "${name}" (instance_id="${instanceId}") has no state row — it hasn't run yet, the name is misspelled, or the instance is misspelled. ` +
@@ -765,16 +754,9 @@ export async function restartConsumer(
765
754
  `Consumer "${name}" (instance_id="${instanceId}") is not dead (status="${before.status}"). Restart only applies to dead consumers; use "enable" for a disabled one.`,
766
755
  );
767
756
  }
768
- const [updated] = await db
769
- .update(eventConsumerStateTable)
770
- .set({ status: "idle", attempts: 0, lastError: null, updatedAt: sql`now()` })
771
- .where(
772
- and(
773
- eq(eventConsumerStateTable.name, name),
774
- eq(eventConsumerStateTable.instanceId, instanceId),
775
- ),
776
- )
777
- .returning();
757
+ const raw = await updateConsumerStatusReturning(db, name, instanceId, "idle");
758
+ const updated =
759
+ raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
778
760
  if (!updated) {
779
761
  throw new Error(
780
762
  `Consumer "${name}" (instance_id="${instanceId}") vanished between read and write — retry.`,
@@ -789,16 +771,9 @@ export async function disableConsumer(
789
771
  instanceId: string = SHARED_INSTANCE_SENTINEL,
790
772
  ): Promise<ConsumerRecoveryState> {
791
773
  await requireConsumerRow(db, name, instanceId);
792
- const [updated] = await db
793
- .update(eventConsumerStateTable)
794
- .set({ status: "disabled", updatedAt: sql`now()` })
795
- .where(
796
- and(
797
- eq(eventConsumerStateTable.name, name),
798
- eq(eventConsumerStateTable.instanceId, instanceId),
799
- ),
800
- )
801
- .returning();
774
+ const raw = await updateConsumerStatusReturning(db, name, instanceId, "disabled");
775
+ const updated =
776
+ raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
802
777
  if (!updated) {
803
778
  throw new Error(
804
779
  `Consumer "${name}" (instance_id="${instanceId}") vanished between read and write — retry.`,
@@ -818,16 +793,9 @@ export async function enableConsumer(
818
793
  `Consumer "${name}" (instance_id="${instanceId}") is not disabled (status="${before.status}"). Enable only flips disabled → idle; use "restart" for a dead consumer.`,
819
794
  );
820
795
  }
821
- const [updated] = await db
822
- .update(eventConsumerStateTable)
823
- .set({ status: "idle", attempts: 0, lastError: null, updatedAt: sql`now()` })
824
- .where(
825
- and(
826
- eq(eventConsumerStateTable.name, name),
827
- eq(eventConsumerStateTable.instanceId, instanceId),
828
- ),
829
- )
830
- .returning();
796
+ const raw = await updateConsumerStatusReturning(db, name, instanceId, "idle");
797
+ const updated =
798
+ raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
831
799
  if (!updated) {
832
800
  throw new Error(
833
801
  `Consumer "${name}" (instance_id="${instanceId}") vanished between read and write — retry.`,
@@ -846,48 +814,25 @@ export async function skipPoisonEvent(
846
814
  instanceId: string = SHARED_INSTANCE_SENTINEL,
847
815
  ): Promise<ConsumerRecoveryState & { readonly skippedEventId: bigint | null }> {
848
816
  const before = await requireConsumerRow(db, name, instanceId);
849
- return db.transaction(async (tx) => {
850
- const [poison] = (await tx
851
- .select({ id: eventsTable.id })
852
- .from(eventsTable)
853
- .where(gt(eventsTable.id, before.lastProcessedEventId))
854
- .orderBy(asc(eventsTable.id))
855
- .limit(1)) as ReadonlyArray<{ id: bigint }>; // @cast-boundary db-row
856
- if (!poison) {
857
- const [unchanged] = await tx
858
- .select()
859
- .from(eventConsumerStateTable)
860
- .where(
861
- and(
862
- eq(eventConsumerStateTable.name, name),
863
- eq(eventConsumerStateTable.instanceId, instanceId),
864
- ),
865
- );
817
+ return db.begin(async (tx: DbTx) => {
818
+ const poisonId = await selectNextEventIdAfter(tx, before.lastProcessedEventId);
819
+ if (poisonId === null) {
820
+ const [unchanged] = await selectMany<ConsumerStateRow>(tx, eventConsumerStateTable, {
821
+ name,
822
+ instanceId,
823
+ });
866
824
  if (!unchanged)
867
825
  throw new Error(`Consumer "${name}" (instance_id="${instanceId}") vanished — retry.`);
868
826
  return { ...normalizeConsumerState(unchanged), skippedEventId: null };
869
827
  }
870
- const [updated] = await tx
871
- .update(eventConsumerStateTable)
872
- .set({
873
- lastProcessedEventId: poison.id,
874
- status: "idle",
875
- attempts: 0,
876
- lastError: null,
877
- updatedAt: sql`now()`,
878
- })
879
- .where(
880
- and(
881
- eq(eventConsumerStateTable.name, name),
882
- eq(eventConsumerStateTable.instanceId, instanceId),
883
- ),
884
- )
885
- .returning();
828
+ const raw = await advanceConsumerPastEventReturning(tx, name, instanceId, poisonId);
829
+ const updated =
830
+ raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
886
831
  if (!updated)
887
832
  throw new Error(
888
833
  `Consumer "${name}" (instance_id="${instanceId}") vanished mid-skip — retry.`,
889
834
  );
890
- return { ...normalizeConsumerState(updated), skippedEventId: poison.id };
835
+ return { ...normalizeConsumerState(updated), skippedEventId: poisonId };
891
836
  });
892
837
  }
893
838
 
@@ -905,15 +850,10 @@ export async function getConsumerState(
905
850
  readonly lastError: string | null;
906
851
  readonly updatedAt: Temporal.Instant;
907
852
  } | null> {
908
- const [row] = await db
909
- .select()
910
- .from(eventConsumerStateTable)
911
- .where(
912
- and(
913
- eq(eventConsumerStateTable.name, name),
914
- eq(eventConsumerStateTable.instanceId, instanceId),
915
- ),
916
- );
853
+ const [row] = await selectMany<ConsumerStateRow>(db, eventConsumerStateTable, {
854
+ name,
855
+ instanceId,
856
+ });
917
857
  if (!row) return null;
918
858
  return {
919
859
  name: row.name,
@@ -947,7 +887,7 @@ export async function listConsumersWithState(
947
887
  readonly lastError: string | null;
948
888
  }>
949
889
  > {
950
- const stateRows = await db.select().from(eventConsumerStateTable);
890
+ const stateRows = await selectMany<ConsumerStateRow>(db, eventConsumerStateTable);
951
891
  const registered = new Set(registeredNames);
952
892
 
953
893
  // Materialize one output row per (name, instance_id). Registered names
@@ -1,5 +1,6 @@
1
- import { and, getTableName, inArray, lt, sql } from "drizzle-orm";
2
1
  import type { DbConnection } from "../db/connection";
2
+ import { lockEventConsumersShareMode } from "../db/queries/event-consumer";
3
+ import { deleteMany, selectMany, transaction } from "../db/query";
3
4
  import { eventsTable } from "../event-store";
4
5
  import { eventConsumerStateTable } from "./event-consumer-state";
5
6
 
@@ -83,7 +84,7 @@ export async function pruneEvents(
83
84
  const aggregateTypes = options.aggregateTypes;
84
85
  const dryRun = options.dryRun === true;
85
86
 
86
- return db.transaction(async (tx) => {
87
+ return transaction(db, async (tx) => {
87
88
  // Serialise against consumer-bootstrap INSERTs. Without this, the race
88
89
  // is: prune reads consumers (snapshot misses a consumer bootstrapping
89
90
  // in a parallel tx) → consumer commits its row with
@@ -96,21 +97,13 @@ export async function pruneEvents(
96
97
  // (cursor advances) do too, but prune is measured in milliseconds and
97
98
  // pausing cursor advances for that window is cheap insurance against
98
99
  // a silent data-loss bug.
99
- //
100
- // Drizzle can't express LOCK TABLE — drop to raw SQL with the table
101
- // name identifier so a future table-rename is caught at compile time.
102
- await tx.execute(sql.raw(`LOCK TABLE ${getTableName(eventConsumerStateTable)} IN SHARE MODE`));
100
+ await lockEventConsumersShareMode(tx);
103
101
 
104
102
  // Step 1 — collect candidate event ids.
105
- const candidates = await tx
106
- .select({ id: eventsTable.id })
107
- .from(eventsTable)
108
- .where(
109
- and(
110
- inArray(eventsTable.aggregateType, [...aggregateTypes]),
111
- lt(eventsTable.createdAt, cutoff),
112
- ),
113
- );
103
+ const candidates = await selectMany<{ id: bigint }>(tx, eventsTable, {
104
+ aggregateType: [...aggregateTypes],
105
+ createdAt: { lt: cutoff },
106
+ });
114
107
 
115
108
  if (candidates.length === 0) {
116
109
  return { deletedCount: 0, cutoff, aggregateTypes, dryRun };
@@ -127,10 +120,10 @@ export async function pruneEvents(
127
120
  // The SHARE lock above guarantees this SELECT sees a complete view:
128
121
  // no new consumer can INSERT a fresh-cursor row between here and the
129
122
  // DELETE below.
130
- const activeConsumers = await tx
131
- .select()
132
- .from(eventConsumerStateTable)
133
- .where(sql`${eventConsumerStateTable.status} <> 'disabled'`);
123
+ const activeConsumers = await selectMany<{
124
+ name: string;
125
+ lastProcessedEventId: bigint;
126
+ }>(tx, eventConsumerStateTable, { status: { ne: "disabled" } });
134
127
 
135
128
  for (const consumer of activeConsumers) {
136
129
  if (consumer.lastProcessedEventId < maxCandidateId) {
@@ -144,11 +137,8 @@ export async function pruneEvents(
144
137
 
145
138
  // Step 3 — actual delete, bounded to the candidate set.
146
139
  const candidateIds = candidates.map((c) => c.id);
147
- const deleted = await tx
148
- .delete(eventsTable)
149
- .where(inArray(eventsTable.id, candidateIds))
150
- .returning({ id: eventsTable.id });
140
+ await deleteMany(tx, eventsTable, { id: candidateIds });
151
141
 
152
- return { deletedCount: deleted.length, cutoff, aggregateTypes, dryRun: false };
142
+ return { deletedCount: candidateIds.length, cutoff, aggregateTypes, dryRun: false };
153
143
  });
154
144
  }
@@ -1,5 +1,12 @@
1
- import { and, asc, eq, getTableName, inArray, sql } from "drizzle-orm";
2
- import type { DbConnection, DbRunner } from "../db/connection";
1
+ import type { DbConnection, DbRunner, DbTx } from "../db/connection";
2
+ import {
3
+ markConsumerRebuildFailed,
4
+ resetConsumerForMspRebuild,
5
+ selectConsumerForUpdate,
6
+ updateConsumerRebuildCursor,
7
+ } from "../db/queries/event-consumer";
8
+ import { truncateTable } from "../db/queries/table-ops";
9
+ import { selectMany } from "../db/query";
3
10
  import type { Registry, TenantId } from "../engine/types";
4
11
  import { InternalError } from "../errors";
5
12
  import { eventsTable, type StoredEvent, upcastStoredEvent } from "../event-store";
@@ -7,7 +14,7 @@ import { loadAggregate, loadAggregateAsOf } from "../event-store/event-store";
7
14
  import { upcastStoredEvents } from "../event-store/upcaster";
8
15
  import { emitProjectionRebuild } from "../observability/standard-metrics";
9
16
  import type { Meter } from "../observability/types/metric";
10
- import { eventConsumerStateTable, SHARED_INSTANCE_SENTINEL } from "./event-consumer-state";
17
+ import { SHARED_INSTANCE_SENTINEL } from "./event-consumer-state";
11
18
  import type { MultiStreamApplyContext } from "./multi-stream-apply-context";
12
19
  import type { RebuildResult } from "./projection-rebuild";
13
20
 
@@ -101,57 +108,35 @@ export async function rebuildMultiStreamProjection(
101
108
  let lastProcessedEventId = 0n;
102
109
 
103
110
  try {
104
- await db.transaction(async (tx) => {
105
- // Upsert + lock the consumer row. Rebuild always targets the
106
- // SHARED-delivery shard: per-instance MSPs are side-effect-only (no
107
- // table, so the guard above refuses them anyway), and rebuild's
108
- // purpose is to rematerialize one persistent read-model, not fan
109
- // out a local cache reset across instances. The FOR UPDATE on the
110
- // next SELECT is what blocks concurrent rebuilds of the same MSP;
111
- // live dispatcher passes use SKIP LOCKED on this row and will bail
112
- // silently while we hold it.
113
- await tx
114
- .insert(eventConsumerStateTable)
115
- .values({
116
- name: mspName,
117
- instanceId: SHARED_INSTANCE_SENTINEL,
118
- lastProcessedEventId: 0n,
119
- status: "idle",
120
- })
121
- .onConflictDoUpdate({
122
- target: [eventConsumerStateTable.name, eventConsumerStateTable.instanceId],
123
- set: {
124
- lastProcessedEventId: 0n,
125
- status: "idle",
126
- attempts: 0,
127
- lastError: null,
128
- updatedAt: sql`now()`,
129
- },
130
- });
131
- await tx
132
- .select()
133
- .from(eventConsumerStateTable)
134
- .where(
135
- and(
136
- eq(eventConsumerStateTable.name, mspName),
137
- eq(eventConsumerStateTable.instanceId, SHARED_INSTANCE_SENTINEL),
138
- ),
139
- )
140
- .for("update");
111
+ await db.begin(async (tx: DbTx) => {
112
+ await resetConsumerForMspRebuild(tx, mspName, SHARED_INSTANCE_SENTINEL);
113
+ await selectConsumerForUpdate(tx, mspName, SHARED_INSTANCE_SENTINEL);
141
114
 
142
- // msp.table is narrowed by the upfront guard; the assertion here is
143
- // for TS inside the async closure (narrowing doesn't cross the
144
- // transaction boundary).
145
- const tableName = getTableName(msp.table as NonNullable<typeof msp.table>); // @cast-boundary db-operator
146
- await tx.execute(sql.raw(`TRUNCATE TABLE ${quoteIdent(tableName)}`));
115
+ const mspTable = msp.table as NonNullable<typeof msp.table>;
116
+ const tableName = getTableName(mspTable);
117
+ await truncateTable(tx, tableName);
147
118
 
148
119
  const subscribedTypes = Object.keys(msp.apply);
149
120
  if (subscribedTypes.length > 0) {
150
- const events = (await tx
151
- .select()
152
- .from(eventsTable)
153
- .where(inArray(eventsTable.type, subscribedTypes))
154
- .orderBy(asc(eventsTable.id))) as ReadonlyArray<typeof eventsTable.$inferSelect>; // @cast-boundary db-row
121
+ type EventRow = {
122
+ id: bigint;
123
+ aggregateId: string;
124
+ aggregateType: string;
125
+ tenantId: TenantId;
126
+ version: number;
127
+ type: string;
128
+ eventVersion: number;
129
+ payload: Record<string, unknown>;
130
+ metadata: import("../event-store/event-store").EventMetadata;
131
+ createdAt: Temporal.Instant;
132
+ createdBy: string;
133
+ };
134
+ const events = await selectMany<EventRow>(
135
+ tx,
136
+ eventsTable,
137
+ { type: [...subscribedTypes] },
138
+ { orderBy: { col: "id", direction: "asc" } },
139
+ );
155
140
 
156
141
  const upcasters = registry.getEventUpcasters();
157
142
  for (const row of events) {
@@ -170,44 +155,27 @@ export async function rebuildMultiStreamProjection(
170
155
  };
171
156
  const storedEvent = await upcastStoredEvent(raw, upcasters, {
172
157
  db: tx,
173
- tenantId: row.tenantId as TenantId, // @cast-boundary db-row
158
+ tenantId: row.tenantId,
174
159
  });
175
160
  const applyFn = msp.apply[row.type];
176
161
  if (!applyFn) continue;
177
- const rebuildCtx = createRebuildCtx(registry, tx, row.tenantId as TenantId); // @cast-boundary db-row
162
+ const rebuildCtx = createRebuildCtx(registry, tx, row.tenantId);
178
163
  await applyFn(storedEvent, tx, rebuildCtx);
179
164
  eventsProcessed++;
180
165
  lastProcessedEventId = row.id;
181
166
  }
182
167
  }
183
168
 
184
- await tx
185
- .update(eventConsumerStateTable)
186
- .set({
187
- lastProcessedEventId,
188
- status: "idle",
189
- attempts: 0,
190
- lastError: null,
191
- updatedAt: sql`now()`,
192
- })
193
- .where(
194
- and(
195
- eq(eventConsumerStateTable.name, mspName),
196
- eq(eventConsumerStateTable.instanceId, SHARED_INSTANCE_SENTINEL),
197
- ),
198
- );
169
+ await updateConsumerRebuildCursor(
170
+ tx,
171
+ mspName,
172
+ SHARED_INSTANCE_SENTINEL,
173
+ lastProcessedEventId,
174
+ );
199
175
  });
200
176
  } catch (e) {
201
177
  const message = e instanceof Error ? e.message : String(e);
202
- await db
203
- .update(eventConsumerStateTable)
204
- .set({ status: "dead", lastError: message, updatedAt: sql`now()` })
205
- .where(
206
- and(
207
- eq(eventConsumerStateTable.name, mspName),
208
- eq(eventConsumerStateTable.instanceId, SHARED_INSTANCE_SENTINEL),
209
- ),
210
- );
178
+ await markConsumerRebuildFailed(db, mspName, SHARED_INSTANCE_SENTINEL, message);
211
179
  if (deps.meter) {
212
180
  emitProjectionRebuild(
213
181
  deps.meter,
@@ -237,6 +205,14 @@ export async function rebuildMultiStreamProjection(
237
205
  return result;
238
206
  }
239
207
 
240
- function quoteIdent(name: string): string {
241
- return `"${name.replace(/"/g, '""')}"`;
208
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
209
+ function getTableName(table: unknown): string {
210
+ if (typeof table !== "object" || table === null) {
211
+ throw new InternalError({ message: "msp-rebuild: msp.table is not a pgTable object" });
212
+ }
213
+ const name = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
214
+ if (typeof name !== "string") {
215
+ throw new InternalError({ message: "msp-rebuild: msp.table missing drizzle name symbol" });
216
+ }
217
+ return name;
242
218
  }