@cosmicdrift/kumiko-framework 0.14.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 +6 -6
  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 -474
  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
@@ -0,0 +1,350 @@
1
+ // Build-time Generator: EntityTableMeta[] → reviewbares SQL-Migration-File.
2
+ // Ersatz für `drizzle-kit generate`.
3
+ //
4
+ // **NO-MAGIC-ON-DATA Kernprinzip** (siehe drizzle-replacement.md):
5
+ // - Generator emittiert SQL-Files für PR-Review/Edit
6
+ // - Generator wird NIE zur App-Runtime aufgerufen
7
+ // - Output ist Start-Form; App-Author darf das SQL hand-editieren
8
+ // (BRIN-Index, partial-Index, performance-tuning) bevor committed
9
+ // - Append-only: bestehende Migration-Files werden NIE überschrieben.
10
+ // Neue Schema-Diffs landen in einem NEUEN File mit incrementierter
11
+ // Sequence-Number.
12
+ //
13
+ // **DESTRUCTIVE-ops** (DROP TABLE/COLUMN, INDEX-removal) werden als
14
+ // AUSKOMMENTIERTE statements emittiert — Reviewer muss explizit
15
+ // uncommenten + ggf. Data-Backup-Schritt vorlauf einfügen. Default:
16
+ // niemals automatisch droppen.
17
+
18
+ import { readFileSync, writeFileSync } from "node:fs";
19
+
20
+ import type { ColumnMeta, EntityTableMeta, IndexMeta } from "./entity-table-meta";
21
+ import { renderTableDdl } from "./render-ddl";
22
+
23
+ const SNAPSHOT_VERSION = 1 as const;
24
+
25
+ export type Snapshot = {
26
+ readonly version: typeof SNAPSHOT_VERSION;
27
+ readonly generatedAt: string; // ISO-8601
28
+ readonly tables: readonly EntityTableMeta[];
29
+ };
30
+
31
+ export type ColumnChange = {
32
+ readonly name: string;
33
+ readonly nullabilityChanged?: { readonly from: boolean; readonly to: boolean };
34
+ readonly defaultChanged?: { readonly from: string | undefined; readonly to: string | undefined };
35
+ readonly typeChanged?: { readonly from: string; readonly to: string };
36
+ };
37
+
38
+ export type TableDiff = {
39
+ readonly tableName: string;
40
+ readonly newColumns: readonly ColumnMeta[];
41
+ readonly droppedColumns: readonly string[];
42
+ readonly changedColumns: readonly ColumnChange[];
43
+ readonly newIndexes: readonly IndexMeta[];
44
+ readonly droppedIndexes: readonly string[];
45
+ };
46
+
47
+ export type SchemaDiff = {
48
+ readonly newTables: readonly EntityTableMeta[];
49
+ readonly droppedTables: readonly string[];
50
+ readonly changedTables: readonly TableDiff[];
51
+ };
52
+
53
+ export function snapshotFromMetas(metas: readonly EntityTableMeta[]): Snapshot {
54
+ // Stable ordering by tableName so the snapshot.json diff in PRs is
55
+ // meaningful (table-add isn't a noisy re-sort of everything).
56
+ const sorted = [...metas].sort((a, b) => a.tableName.localeCompare(b.tableName));
57
+ return {
58
+ version: SNAPSHOT_VERSION,
59
+ generatedAt: new Date().toISOString(),
60
+ tables: sorted,
61
+ };
62
+ }
63
+
64
+ export function loadSnapshotJson(path: string): Snapshot | null {
65
+ try {
66
+ const text = readFileSync(path, "utf8");
67
+ const parsed = JSON.parse(text) as { version?: unknown };
68
+ if (parsed.version !== SNAPSHOT_VERSION) {
69
+ throw new Error(
70
+ `Snapshot at ${path} has version ${String(parsed.version)}, expected ${SNAPSHOT_VERSION}`,
71
+ );
72
+ }
73
+ return parsed as Snapshot;
74
+ } catch (err) {
75
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
76
+ throw err;
77
+ }
78
+ }
79
+
80
+ export function writeSnapshotJson(path: string, snapshot: Snapshot): void {
81
+ writeFileSync(path, `${JSON.stringify(snapshot, null, 2)}\n`);
82
+ }
83
+
84
+ function indexMetaKey(idx: IndexMeta): string {
85
+ // Index-Identität via Name (PG-Constraint: unique innerhalb der DB)
86
+ return idx.name;
87
+ }
88
+
89
+ function columnsByName(meta: EntityTableMeta): Map<string, ColumnMeta> {
90
+ const m = new Map<string, ColumnMeta>();
91
+ for (const c of meta.columns) m.set(c.name, c);
92
+ return m;
93
+ }
94
+
95
+ function indexesByName(meta: EntityTableMeta): Map<string, IndexMeta> {
96
+ const m = new Map<string, IndexMeta>();
97
+ for (const i of meta.indexes) m.set(indexMetaKey(i), i);
98
+ return m;
99
+ }
100
+
101
+ function diffOneTable(prev: EntityTableMeta, next: EntityTableMeta): TableDiff | null {
102
+ const prevCols = columnsByName(prev);
103
+ const nextCols = columnsByName(next);
104
+
105
+ const newColumns: ColumnMeta[] = [];
106
+ const droppedColumns: string[] = [];
107
+ const changedColumns: ColumnChange[] = [];
108
+
109
+ for (const [name, nextCol] of nextCols) {
110
+ const prevCol = prevCols.get(name);
111
+ if (!prevCol) {
112
+ newColumns.push(nextCol);
113
+ continue;
114
+ }
115
+ const change: ColumnChange = { name };
116
+ if (prevCol.notNull !== nextCol.notNull) {
117
+ Object.assign(change, {
118
+ nullabilityChanged: { from: prevCol.notNull, to: nextCol.notNull },
119
+ });
120
+ }
121
+ if (prevCol.defaultSql !== nextCol.defaultSql) {
122
+ Object.assign(change, {
123
+ defaultChanged: { from: prevCol.defaultSql, to: nextCol.defaultSql },
124
+ });
125
+ }
126
+ if (prevCol.pgType !== nextCol.pgType) {
127
+ Object.assign(change, {
128
+ typeChanged: { from: prevCol.pgType, to: nextCol.pgType },
129
+ });
130
+ }
131
+ if (change.nullabilityChanged || change.defaultChanged || change.typeChanged) {
132
+ changedColumns.push(change);
133
+ }
134
+ }
135
+ for (const name of prevCols.keys()) {
136
+ if (!nextCols.has(name)) droppedColumns.push(name);
137
+ }
138
+
139
+ const prevIdx = indexesByName(prev);
140
+ const nextIdx = indexesByName(next);
141
+ const newIndexes: IndexMeta[] = [];
142
+ const droppedIndexes: string[] = [];
143
+ for (const [name, idx] of nextIdx) {
144
+ if (!prevIdx.has(name)) newIndexes.push(idx);
145
+ }
146
+ for (const name of prevIdx.keys()) {
147
+ if (!nextIdx.has(name)) droppedIndexes.push(name);
148
+ }
149
+
150
+ const isEmpty =
151
+ newColumns.length === 0 &&
152
+ droppedColumns.length === 0 &&
153
+ changedColumns.length === 0 &&
154
+ newIndexes.length === 0 &&
155
+ droppedIndexes.length === 0;
156
+ if (isEmpty) return null;
157
+ return {
158
+ tableName: prev.tableName,
159
+ newColumns,
160
+ droppedColumns,
161
+ changedColumns,
162
+ newIndexes,
163
+ droppedIndexes,
164
+ };
165
+ }
166
+
167
+ export function diffSnapshots(prev: Snapshot | null, next: Snapshot): SchemaDiff {
168
+ const prevByName = new Map<string, EntityTableMeta>();
169
+ if (prev) {
170
+ for (const t of prev.tables) prevByName.set(t.tableName, t);
171
+ }
172
+ const nextByName = new Map<string, EntityTableMeta>();
173
+ for (const t of next.tables) nextByName.set(t.tableName, t);
174
+
175
+ const newTables: EntityTableMeta[] = [];
176
+ const droppedTables: string[] = [];
177
+ const changedTables: TableDiff[] = [];
178
+
179
+ for (const [name, nextMeta] of nextByName) {
180
+ const prevMeta = prevByName.get(name);
181
+ if (!prevMeta) {
182
+ newTables.push(nextMeta);
183
+ continue;
184
+ }
185
+ const td = diffOneTable(prevMeta, nextMeta);
186
+ if (td) changedTables.push(td);
187
+ }
188
+ for (const name of prevByName.keys()) {
189
+ if (!nextByName.has(name)) droppedTables.push(name);
190
+ }
191
+
192
+ return { newTables, droppedTables, changedTables };
193
+ }
194
+
195
+ // --- SQL-Render ---------------------------------------------------------
196
+
197
+ function quoteIdent(name: string): string {
198
+ return `"${name.replace(/"/g, '""')}"`;
199
+ }
200
+
201
+ function renderColumnInline(col: ColumnMeta): string {
202
+ const parts: string[] = [quoteIdent(col.name), col.pgType];
203
+ if (col.defaultSql !== undefined) parts.push(`DEFAULT ${col.defaultSql}`);
204
+ if (col.notNull) parts.push("NOT NULL");
205
+ return parts.join(" ");
206
+ }
207
+
208
+ function renderAddColumn(tableName: string, col: ColumnMeta): string {
209
+ return `ALTER TABLE ${quoteIdent(tableName)} ADD COLUMN ${renderColumnInline(col)};`;
210
+ }
211
+
212
+ function renderColumnChange(tableName: string, change: ColumnChange): readonly string[] {
213
+ const out: string[] = [];
214
+ const col = quoteIdent(change.name);
215
+ const tbl = quoteIdent(tableName);
216
+ if (change.nullabilityChanged) {
217
+ const op = change.nullabilityChanged.to ? "SET NOT NULL" : "DROP NOT NULL";
218
+ out.push(`ALTER TABLE ${tbl} ALTER COLUMN ${col} ${op};`);
219
+ }
220
+ if (change.defaultChanged) {
221
+ if (change.defaultChanged.to !== undefined) {
222
+ out.push(`ALTER TABLE ${tbl} ALTER COLUMN ${col} SET DEFAULT ${change.defaultChanged.to};`);
223
+ } else {
224
+ out.push(`ALTER TABLE ${tbl} ALTER COLUMN ${col} DROP DEFAULT;`);
225
+ }
226
+ }
227
+ if (change.typeChanged) {
228
+ // pg ALTER TYPE braucht oft USING-clause für nicht-implicit-castable
229
+ // type-changes. Wir emittieren das als Reviewer-Kommentar + raw cast —
230
+ // App-Author muss prüfen ob das gewünscht ist.
231
+ out.push(
232
+ `-- WARN: column-type-change ${change.typeChanged.from} → ${change.typeChanged.to}. Review USING-clause if needed.`,
233
+ );
234
+ out.push(`ALTER TABLE ${tbl} ALTER COLUMN ${col} TYPE ${change.typeChanged.to};`);
235
+ }
236
+ return out;
237
+ }
238
+
239
+ function renderIndex(tableName: string, idx: IndexMeta): string {
240
+ const kind = idx.unique === true ? "UNIQUE INDEX" : "INDEX";
241
+ const colList = idx.columns.map(quoteIdent).join(", ");
242
+ const where = idx.whereSql !== undefined ? ` WHERE ${idx.whereSql}` : "";
243
+ return `CREATE ${kind} IF NOT EXISTS ${quoteIdent(idx.name)} ON ${quoteIdent(tableName)} (${colList})${where};`;
244
+ }
245
+
246
+ // Render the diff as a SQL-file content with header-comment + grouped
247
+ // statements. Destructive operations (DROP TABLE/COLUMN, DROP INDEX where
248
+ // the index serves performance) are emitted as commented-out statements
249
+ // for explicit reviewer-action.
250
+ export function renderMigrationSql(
251
+ diff: SchemaDiff,
252
+ options: { readonly name: string; readonly sequenceNumber: number },
253
+ ): string {
254
+ const lines: string[] = [];
255
+ const seq = options.sequenceNumber.toString().padStart(4, "0");
256
+ lines.push(`-- Migration ${seq}_${options.name}`);
257
+ lines.push(`-- Generated: ${new Date().toISOString()}`);
258
+ lines.push("-- ");
259
+ lines.push("-- This file is generated by `kumiko migrate generate`. You may");
260
+ lines.push("-- hand-edit it before committing — add partial-indexes, BRIN-");
261
+ lines.push("-- variants, performance-tuning. After commit + apply, NEVER edit");
262
+ lines.push("-- this file again (production-DBs already ran it). Write a new");
263
+ lines.push("-- migration instead.");
264
+ lines.push("");
265
+
266
+ if (diff.newTables.length > 0) {
267
+ lines.push("-- === New tables ===");
268
+ for (const tbl of diff.newTables) {
269
+ lines.push(...renderTableDdl(tbl));
270
+ lines.push("");
271
+ }
272
+ }
273
+
274
+ if (diff.changedTables.length > 0) {
275
+ lines.push("-- === Changed tables ===");
276
+ for (const td of diff.changedTables) {
277
+ lines.push(`-- ${td.tableName}`);
278
+ for (const col of td.newColumns) {
279
+ lines.push(renderAddColumn(td.tableName, col));
280
+ }
281
+ for (const ch of td.changedColumns) {
282
+ for (const stmt of renderColumnChange(td.tableName, ch)) lines.push(stmt);
283
+ }
284
+ for (const idx of td.newIndexes) {
285
+ lines.push(renderIndex(td.tableName, idx));
286
+ }
287
+ for (const name of td.droppedIndexes) {
288
+ lines.push(`-- (review) DROP INDEX IF EXISTS ${quoteIdent(name)};`);
289
+ }
290
+ for (const colName of td.droppedColumns) {
291
+ lines.push(
292
+ `-- DESTRUCTIVE: ALTER TABLE ${quoteIdent(td.tableName)} DROP COLUMN ${quoteIdent(colName)}; -- uncomment + ensure backup`,
293
+ );
294
+ }
295
+ lines.push("");
296
+ }
297
+ }
298
+
299
+ if (diff.droppedTables.length > 0) {
300
+ lines.push("-- === Dropped tables ===");
301
+ for (const name of diff.droppedTables) {
302
+ lines.push(
303
+ `-- DESTRUCTIVE: DROP TABLE IF EXISTS ${quoteIdent(name)}; -- uncomment + ensure backup + run during maintenance window`,
304
+ );
305
+ }
306
+ lines.push("");
307
+ }
308
+
309
+ if (
310
+ diff.newTables.length === 0 &&
311
+ diff.changedTables.length === 0 &&
312
+ diff.droppedTables.length === 0
313
+ ) {
314
+ lines.push("-- No schema changes detected.");
315
+ }
316
+
317
+ return lines.join("\n");
318
+ }
319
+
320
+ // --- High-level entry point --------------------------------------------
321
+
322
+ export type GenerateMigrationInput = {
323
+ readonly metas: readonly EntityTableMeta[];
324
+ readonly prevSnapshot: Snapshot | null;
325
+ readonly name: string;
326
+ readonly sequenceNumber: number;
327
+ };
328
+
329
+ export type GenerateMigrationOutput = {
330
+ readonly filename: string;
331
+ readonly sqlContent: string;
332
+ readonly snapshot: Snapshot;
333
+ readonly diff: SchemaDiff;
334
+ };
335
+
336
+ export function generateMigration(input: GenerateMigrationInput): GenerateMigrationOutput {
337
+ const nextSnapshot = snapshotFromMetas(input.metas);
338
+ const diff = diffSnapshots(input.prevSnapshot, nextSnapshot);
339
+ const sqlContent = renderMigrationSql(diff, {
340
+ name: input.name,
341
+ sequenceNumber: input.sequenceNumber,
342
+ });
343
+ const seq = input.sequenceNumber.toString().padStart(4, "0");
344
+ return {
345
+ filename: `${seq}_${input.name}.sql`,
346
+ sqlContent,
347
+ snapshot: nextSnapshot,
348
+ diff,
349
+ };
350
+ }
@@ -0,0 +1,206 @@
1
+ // Migrate-Runner: applied checked-in SQL-Files gegen eine DB.
2
+ // Ersatz für `drizzle-kit migrate` als Runtime-Komponente.
3
+ //
4
+ // **NO-MAGIC-ON-DATA Kernprinzip** (siehe drizzle-replacement.md):
5
+ // - Runner liest NUR checked-in `<dir>/*.sql` Files
6
+ // - Kein Reach-back zu EntityDefinition zur Runtime
7
+ // - Kein Auto-Apply-on-App-Boot — Deploy-Skript oder Init-Container ruft
8
+ // `kumiko migrate apply` als eigenen Schritt
9
+ // - Schema-Drift wird durch Checksum-Mismatch fail-loud detektiert
10
+ //
11
+ // Tracking-Table `_kumiko_migrations(id, applied_at, checksum)`. id = filename
12
+ // ohne .sql-Endung. Checksum = sha256 vom File-Content beim Apply. Wenn ein
13
+ // schon-appliedes File nachträglich editiert wird, schlägt der nächste Apply
14
+ // fehl mit klarer Fehlermeldung (production-DBs haben das alte SQL drin —
15
+ // edit nachträglich = production-state inkonsistent mit committed-state).
16
+
17
+ import { createHash } from "node:crypto";
18
+ import { readdirSync, readFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+
21
+ import type { DbConnection, DbRunner } from "./connection";
22
+
23
+ // Adapter: extract raw postgres-js client from drizzle DbConnection,
24
+ // or use Bun.sql instance directly. Either way `.unsafe()` is the
25
+ // runtime call.
26
+ function rawClient(db: DbRunner): {
27
+ unsafe: (sql: string, params?: readonly unknown[]) => Promise<readonly Record<string, unknown>[]>;
28
+ begin: <T>(fn: (tx: unknown) => Promise<T>) => Promise<T>;
29
+ } {
30
+ const dbAny = db as unknown as Record<string, unknown>;
31
+ if (typeof dbAny["unsafe"] === "function" && typeof dbAny["begin"] === "function") {
32
+ return dbAny as never;
33
+ }
34
+ const $client = dbAny["$client"];
35
+ if ($client && typeof ($client as Record<string, unknown>)["unsafe"] === "function") {
36
+ return $client as never;
37
+ }
38
+ const session = dbAny["session"] as Record<string, unknown> | undefined;
39
+ const sessionClient = session?.["client"];
40
+ if (sessionClient && typeof (sessionClient as Record<string, unknown>)["unsafe"] === "function") {
41
+ return sessionClient as never;
42
+ }
43
+ throw new Error(
44
+ "migrate-runner: db argument has no .unsafe() (need Bun.SQL or drizzle DbConnection)",
45
+ );
46
+ }
47
+
48
+ export type Migration = {
49
+ readonly id: string; // filename ohne .sql
50
+ readonly checksum: string; // sha256-hex vom File-Content
51
+ readonly statements: readonly string[];
52
+ };
53
+
54
+ export type AppliedMigration = {
55
+ readonly id: string;
56
+ readonly checksum: string;
57
+ };
58
+
59
+ export type ApplyResult = {
60
+ readonly applied: readonly string[];
61
+ readonly skipped: readonly string[];
62
+ };
63
+
64
+ // Advisory-lock key (random 32-bit int, stable). Verhindert dass zwei
65
+ // gleichzeitig bootende Pods beide migrate apply laufen lassen (zweiter
66
+ // blockiert bis erster fertig).
67
+ const ADVISORY_LOCK_KEY = 0x6b756d69; // "kumi" in ASCII
68
+
69
+ const MIGRATIONS_TABLE_DDL = `
70
+ CREATE TABLE IF NOT EXISTS "_kumiko_migrations" (
71
+ "id" text PRIMARY KEY NOT NULL,
72
+ "checksum" text NOT NULL,
73
+ "applied_at" timestamptz NOT NULL DEFAULT now()
74
+ )
75
+ `.trim();
76
+
77
+ // Splittet SQL-File-Text in einzelne Statements. Pragma: simpler `;`-Split,
78
+ // reicht weil unsere generierten SQL-Files keine eingebetteten `;` in String-
79
+ // Literalen haben. App-Author der hand-editiert + tricky SQL einfügt sollte
80
+ // das wissen — sonst pg-Parser einziehen.
81
+ export function splitSqlStatements(sqlText: string): readonly string[] {
82
+ return sqlText
83
+ .split(";")
84
+ .map((s) => s.replace(/--[^\n]*/g, "").trim())
85
+ .filter((s) => s.length > 0)
86
+ .map((s) => `${s};`);
87
+ }
88
+
89
+ function sha256Hex(content: string): string {
90
+ return createHash("sha256").update(content).digest("hex");
91
+ }
92
+
93
+ // Liest <dir>/*.sql, sortiert lex (z.B. 0001_init.sql, 0002_add_locale.sql),
94
+ // returnt Migration[] mit id + checksum + statements.
95
+ export function loadMigrationsFromDir(dir: string): readonly Migration[] {
96
+ const files = readdirSync(dir)
97
+ .filter((f) => f.endsWith(".sql"))
98
+ .sort();
99
+ return files.map((file) => {
100
+ const content = readFileSync(join(dir, file), "utf8");
101
+ return {
102
+ id: file.replace(/\.sql$/, ""),
103
+ checksum: sha256Hex(content),
104
+ statements: splitSqlStatements(content),
105
+ };
106
+ });
107
+ }
108
+
109
+ // Raw-SQL via postgres-js or Bun.sql .unsafe() — same shape for both.
110
+ async function executeRaw(db: DbRunner, sqlText: string): Promise<void> {
111
+ await rawClient(db).unsafe(sqlText);
112
+ }
113
+
114
+ async function fetchAppliedMigrations(db: DbConnection): Promise<readonly AppliedMigration[]> {
115
+ const result = await rawClient(db).unsafe(
116
+ `SELECT id, checksum FROM "_kumiko_migrations" ORDER BY id`,
117
+ );
118
+ const rows = Array.isArray(result) ? result : [];
119
+ const applied: AppliedMigration[] = [];
120
+ for (const row of rows) {
121
+ if (
122
+ typeof row === "object" &&
123
+ row !== null &&
124
+ typeof (row as { id?: unknown }).id === "string" &&
125
+ typeof (row as { checksum?: unknown }).checksum === "string"
126
+ ) {
127
+ applied.push({
128
+ id: (row as { id: string }).id,
129
+ checksum: (row as { checksum: string }).checksum,
130
+ });
131
+ }
132
+ }
133
+ return applied;
134
+ }
135
+
136
+ export class MigrationChecksumMismatchError extends Error {
137
+ constructor(
138
+ public readonly migrationId: string,
139
+ public readonly expected: string,
140
+ public readonly actual: string,
141
+ ) {
142
+ super(
143
+ `Migration "${migrationId}" was edited after being applied. ` +
144
+ `DB has checksum ${expected.slice(0, 12)}…, file has ${actual.slice(0, 12)}…. ` +
145
+ `Production-DBs already ran the original SQL — editing applied migrations ` +
146
+ `causes schema-drift. Write a NEW migration file instead (NNNN+1_<change>.sql).`,
147
+ );
148
+ this.name = "MigrationChecksumMismatchError";
149
+ }
150
+ }
151
+
152
+ // Apply pending migrations. Idempotent: schon applied migrations werden via
153
+ // _kumiko_migrations-Lookup übersprungen. Checksum-Mismatch wirft fail-loud.
154
+ // Advisory-Lock verhindert concurrent-apply-races.
155
+ export async function runMigrations(
156
+ db: DbConnection,
157
+ migrations: readonly Migration[],
158
+ ): Promise<ApplyResult> {
159
+ await executeRaw(db, MIGRATIONS_TABLE_DDL);
160
+ await executeRaw(db, `SELECT pg_advisory_lock(${ADVISORY_LOCK_KEY})`);
161
+ try {
162
+ const applied = new Map(
163
+ (await fetchAppliedMigrations(db)).map((a) => [a.id, a.checksum] as const),
164
+ );
165
+
166
+ const appliedIds: string[] = [];
167
+ const skippedIds: string[] = [];
168
+
169
+ for (const m of migrations) {
170
+ const prevChecksum = applied.get(m.id);
171
+ if (prevChecksum !== undefined) {
172
+ if (prevChecksum !== m.checksum) {
173
+ throw new MigrationChecksumMismatchError(m.id, prevChecksum, m.checksum);
174
+ }
175
+ skippedIds.push(m.id);
176
+ continue;
177
+ }
178
+ // Apply file content + INSERT tracking-row in einer TX. Wenn ein
179
+ // Statement bricht, Rollback inkl. Tracking-Row → kein partial-apply
180
+ // stuck in den books.
181
+ const client = rawClient(db);
182
+ await client.begin(async (tx) => {
183
+ const txClient = tx as { unsafe: (s: string, p?: readonly unknown[]) => Promise<unknown> };
184
+ for (const stmt of m.statements) {
185
+ await txClient.unsafe(stmt);
186
+ }
187
+ await txClient.unsafe(
188
+ `INSERT INTO "_kumiko_migrations" ("id", "checksum") VALUES ($1, $2)`,
189
+ [m.id, m.checksum],
190
+ );
191
+ });
192
+ appliedIds.push(m.id);
193
+ }
194
+
195
+ return { applied: appliedIds, skipped: skippedIds };
196
+ } finally {
197
+ await executeRaw(db, `SELECT pg_advisory_unlock(${ADVISORY_LOCK_KEY})`);
198
+ }
199
+ }
200
+
201
+ // Convenience: load + run in einem Call. Wird vom `kumiko migrate apply`
202
+ // CLI-Command + von Test-Setup-Helfern verwendet.
203
+ export async function runMigrationsFromDir(db: DbConnection, dir: string): Promise<ApplyResult> {
204
+ const migrations = loadMigrationsFromDir(dir);
205
+ return runMigrations(db, migrations);
206
+ }
@@ -0,0 +1,25 @@
1
+ // PostgreSQL Provider: postgres-js.
2
+ // Standard-Provider — stabil, keine bekannten Protocol-Level-Bugs.
3
+ // `asRawClient(db)` wrappt .unsafe und .begin transparent.
4
+
5
+ import postgres from "postgres";
6
+ import type { DbConnection, DbConnectionOptions } from "./api";
7
+
8
+ export function createPgConnection(url: string, options: DbConnectionOptions = {}): DbConnection {
9
+ const pgOptions: Parameters<typeof postgres>[1] = {};
10
+ if (options.maxConnections !== undefined) pgOptions.max = options.maxConnections;
11
+ if (options.idleTimeoutSeconds !== undefined) pgOptions.idle_timeout = options.idleTimeoutSeconds;
12
+ if (options.connectTimeoutSeconds !== undefined) {
13
+ pgOptions.connect_timeout = options.connectTimeoutSeconds;
14
+ }
15
+
16
+ const client = postgres(url, pgOptions);
17
+
18
+ return {
19
+ db: client,
20
+ client,
21
+ close: async () => {
22
+ await client.end();
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,15 @@
1
+ import type { AnyDb } from "../query";
2
+ import { executeRawQuery } from "./raw-sql";
3
+ import { quoteTableIdent } from "./table-ops";
4
+
5
+ export async function selectRowForUpdateById(
6
+ db: AnyDb,
7
+ tableName: string,
8
+ id: string | number,
9
+ ): Promise<readonly Record<string, unknown>[]> {
10
+ return executeRawQuery(
11
+ db,
12
+ `SELECT * FROM ${quoteTableIdent(tableName)} WHERE "id" = $1 FOR UPDATE`,
13
+ [id],
14
+ );
15
+ }
@@ -0,0 +1,17 @@
1
+ import type { AnyDb } from "../query";
2
+ import { asRawClient } from "../query";
3
+
4
+ /** Stabiler Lock-Key für pg_advisory_xact_lock — multi-replica seed boots. */
5
+ export const ES_OPS_ADVISORY_LOCK_KEY = 0x65_73_6f_70; // 'esop'
6
+
7
+ export async function acquireEsOpsAdvisoryLock(db: AnyDb): Promise<void> {
8
+ await asRawClient(db).unsafe(`SELECT pg_advisory_xact_lock($1)`, [ES_OPS_ADVISORY_LOCK_KEY]);
9
+ }
10
+
11
+ export async function esOperationExists(db: AnyDb, operationId: string): Promise<boolean> {
12
+ const rows = (await asRawClient(db).unsafe(
13
+ `SELECT 1 FROM "kumiko_es_operations" WHERE id = $1 LIMIT 1`,
14
+ [operationId],
15
+ )) as readonly unknown[];
16
+ return rows.length > 0;
17
+ }