@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
@@ -0,0 +1,845 @@
1
+ // Typed Query-API über Bun.sql. KEIN drizzle intern. Identische Signatur
2
+ // zur legacy `db/query-api.ts` — App-code-migration ist Import-Pfad-Wechsel,
3
+ // kein call-site-Refactor.
4
+ //
5
+ // API:
6
+ // selectMany<T>(db, table, where?, opts?) → readonly T[]
7
+ // fetchOne<T>(db, table, where) → T | undefined
8
+ // insertOne<T>(db, table, values) → T | undefined
9
+ // updateMany<T>(db, table, set, where) → readonly T[]
10
+ // deleteMany(db, table, where) → void
11
+ // countWhere(db, table, where?) → number
12
+ // upsertByPk / upsertOnConflict / incrementCounter
13
+ // deleteManyBatched(db, table, where, { limit }) → { deleted, batches }
14
+ // transaction<T>(db, fn) → T
15
+ //
16
+ // `table` kann sein:
17
+ // - EntityTableMeta (preferred, plain data)
18
+ // - drizzle pgTable (legacy, hat Symbol-based tableName) — extracted via
19
+ // drizzle's getTableName + getTableColumns (drizzle weiterhin als type-
20
+ // reference, NICHT als runtime-API-call)
21
+
22
+ import type { EntityTableMeta } from "../db/entity-table-meta";
23
+ import { toSnakeCase } from "../db/table-builder";
24
+ import { camelCase as envCamelCase } from "../env";
25
+
26
+ // Idempotent snake_case → camelCase. `env.camelCase` always lowercases first
27
+ // (designed for SHOUT_CASE input) — for already-camelCase keys (mock rows
28
+ // in tests, projection-aliased columns) it would silently produce "tenantid"
29
+ // instead of leaving "tenantId" alone. Guard with an underscore check so
30
+ // the conversion only fires when the key is actually snake-shaped.
31
+ function snakeToCamel(key: string): string {
32
+ if (!key.includes("_")) return key;
33
+ return envCamelCase(key);
34
+ }
35
+
36
+ import type { BunDbRunner } from "./connection";
37
+
38
+ // Drizzle-pgTable-Inspection via raw Symbol-access (kein drizzle-orm import).
39
+ // drizzle stores the table name unter `Symbol.for("kumiko:schema:Name")` und die
40
+ // column-map unter `Symbol.for("kumiko:schema:Columns")`.
41
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
42
+ const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
43
+
44
+ function getTableName(table: unknown): string | null {
45
+ if (typeof table !== "object" || table === null) return null;
46
+ const name = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
47
+ return typeof name === "string" ? name : null;
48
+ }
49
+
50
+ function extractDrizzleColumns(table: unknown): Map<string, { name: string; sqlType?: string }> {
51
+ const out = new Map<string, { name: string; sqlType?: string }>();
52
+ if (typeof table !== "object" || table === null) return out;
53
+ const cols = (table as Record<symbol, unknown>)[KUMIKO_COLUMNS_SYMBOL];
54
+ if (typeof cols !== "object" || cols === null) return out;
55
+ for (const [key, val] of Object.entries(cols as Record<string, unknown>)) {
56
+ if (typeof val !== "object" || val === null) continue;
57
+ const colObj = val as { name?: unknown; getSQLType?: () => string };
58
+ const colName = colObj.name;
59
+ if (typeof colName !== "string") continue;
60
+ // Drizzle's getSQLType uses `this` — call as method on colObj.
61
+ const sqlType = typeof colObj.getSQLType === "function" ? colObj.getSQLType() : undefined;
62
+ out.set(key, { name: colName, ...(sqlType !== undefined && { sqlType }) });
63
+ }
64
+ return out;
65
+ }
66
+
67
+ // `db` Input akzeptiert drei Shapes:
68
+ // 1. Bun.SQL connection (BunDbRunner) — neue Welt, native .unsafe + .begin
69
+ // 2. drizzle DbConnection (postgres-js-Wrapper) — legacy compat,
70
+ // raw postgres-js client liegt unter `db.$client.unsafe / .begin`
71
+ // 3. drizzle tx-handle — client liegt unter `tx.session.client`
72
+ // Shim extrahiert in allen Fällen ein `{unsafe, begin}`-Surface.
73
+ type RawClient = {
74
+ unsafe: <TRow = unknown>(sql: string, params?: readonly unknown[]) => Promise<readonly TRow[]>;
75
+ begin: <T>(fn: (tx: unknown) => Promise<T>) => Promise<T>;
76
+ };
77
+
78
+ export function asRawClient(db: unknown): RawClient {
79
+ const dbAny = db as Record<string, unknown>;
80
+ // Direct: Bun.SQL / postgres-js Sql / postgres-js TransactionSql. All three
81
+ // expose `.unsafe`; only Sql/Bun.SQL have `.begin` — TransactionSql uses
82
+ // `.savepoint` for nested-tx. We only require `.unsafe` here; callers that
83
+ // need `.begin` (e.g. `transaction()`) verify it themselves.
84
+ if (typeof dbAny["unsafe"] === "function") {
85
+ return dbAny as unknown as RawClient;
86
+ }
87
+ // TenantDb-shape: framework wrapper exposing the underlying runner as `.raw`.
88
+ // Callers that pass `ctx.db` instead of `ctx.db.raw` land here — unwrap once.
89
+ const raw = dbAny["raw"];
90
+ if (raw && typeof (raw as Record<string, unknown>)["unsafe"] === "function") {
91
+ return raw as unknown as RawClient;
92
+ }
93
+ // Drizzle DbConnection (legacy compat): $client = postgres-js Sql.
94
+ const $client = dbAny["$client"];
95
+ if ($client && typeof ($client as Record<string, unknown>)["unsafe"] === "function") {
96
+ return $client as unknown as RawClient;
97
+ }
98
+ // Drizzle pg-transaction (legacy compat): session.client = postgres-js Sql.
99
+ const session = dbAny["session"] as Record<string, unknown> | undefined;
100
+ const sessionClient = session?.["client"];
101
+ if (sessionClient && typeof (sessionClient as Record<string, unknown>)["unsafe"] === "function") {
102
+ return sessionClient as unknown as RawClient;
103
+ }
104
+ throw new Error(
105
+ "bun-db: db argument has no .unsafe() — pass Bun.SQL, postgres-js Sql, TenantDb, or a transaction handle.",
106
+ );
107
+ }
108
+
109
+ export type AnyDb = BunDbRunner | unknown;
110
+
111
+ // WhereValue: primitive für eq, array für IN, null für IS NULL, oder
112
+ // operator-object für range/comparisons.
113
+ export type WhereOperator = {
114
+ readonly gt?: unknown;
115
+ readonly gte?: unknown;
116
+ readonly lt?: unknown;
117
+ readonly lte?: unknown;
118
+ readonly ne?: unknown;
119
+ readonly in?: readonly unknown[];
120
+ readonly like?: string;
121
+ };
122
+ export type WhereValue = unknown | WhereOperator;
123
+ export type WhereObject = Record<string, WhereValue>;
124
+
125
+ function isWhereOperator(v: unknown): v is WhereOperator {
126
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
127
+ // Don't false-positive on Date / Temporal / other domain-objects.
128
+ // WhereOperator is plain object literal with at most these keys.
129
+ const keys = Object.keys(v);
130
+ if (keys.length === 0) return false;
131
+ const opKeys = ["gt", "gte", "lt", "lte", "ne", "in", "like"];
132
+ return keys.every((k) => opKeys.includes(k));
133
+ }
134
+ export type OrderByClause = {
135
+ readonly col: string;
136
+ readonly direction?: "asc" | "desc";
137
+ };
138
+
139
+ export type SelectOptions = {
140
+ readonly limit?: number;
141
+ // Single column or array for multi-column tie-breaks (e.g.
142
+ // [{col: "createdAt"}, {col: "id"}] for chronological-with-stable-id).
143
+ readonly orderBy?: OrderByClause | readonly OrderByClause[];
144
+ };
145
+
146
+ // Akzeptiert EITHER. Beide haben einen tableName und field→column-mapping.
147
+ // biome-ignore lint/suspicious/noExplicitAny: legacy drizzle pgTable surface
148
+ type TableLike = EntityTableMeta | any;
149
+
150
+ export type TableInfo = {
151
+ readonly name: string;
152
+ // field-name (camelCase oder snake_case) → snake_case column-name
153
+ readonly columnOf: (field: string) => string;
154
+ // pgType per column-name, for jsonb-cast detection
155
+ readonly pgTypeOf: (column: string) => string | undefined;
156
+ // Inverse of columnOf — snake_case DB column → JS field-name (camelCase).
157
+ // Used at the result boundary to rename row keys back to the API shape
158
+ // that callers consume (`row.aggregateId` instead of `row.aggregate_id`).
159
+ readonly fieldOf: (column: string) => string;
160
+ // Check if a field name or column name is known to this table
161
+ readonly hasColumn: (fieldOrColumn: string) => boolean;
162
+ };
163
+
164
+ export function extractTableInfo(table: TableLike): TableInfo {
165
+ // EntityTableMeta discriminator: hat source-property "managed" | "unmanaged"
166
+ if (
167
+ table !== null &&
168
+ typeof table === "object" &&
169
+ "source" in table &&
170
+ (table.source === "managed" || table.source === "unmanaged")
171
+ ) {
172
+ const meta = table as EntityTableMeta;
173
+ const colByField = new Map<string, string>();
174
+ const fieldByCol = new Map<string, string>();
175
+ const typeByCol = new Map<string, string>();
176
+ for (const c of meta.columns) {
177
+ typeByCol.set(c.name, c.pgType);
178
+ // EntityTableMeta column names are snake_case. Map snake → snake AND
179
+ // derive a camelCase JS field-name so result rows can be renamed back
180
+ // to the API shape (`aggregate_id` → `aggregateId`).
181
+ colByField.set(c.name, c.name);
182
+ const camel = snakeToCamel(c.name);
183
+ if (camel !== c.name) colByField.set(camel, c.name);
184
+ fieldByCol.set(c.name, camel === c.name ? c.name : camel);
185
+ }
186
+ return {
187
+ name: meta.tableName,
188
+ columnOf: (field) => colByField.get(field) ?? toSnakeCase(field),
189
+ pgTypeOf: (col) => typeByCol.get(col),
190
+ fieldOf: (col) => fieldByCol.get(col) ?? snakeToCamel(col),
191
+ hasColumn: (fieldOrColumn) => colByField.has(fieldOrColumn) || fieldByCol.has(fieldOrColumn),
192
+ };
193
+ }
194
+ // drizzle pgTable: tableName via Symbol.for("kumiko:schema:Name"), columns via
195
+ // enumerable properties (jeder col-object hat .name + .getSQLType()).
196
+ // Wir lesen Beide via raw Symbol/Property-access — kein drizzle-orm import.
197
+ const name = getTableName(table);
198
+ if (!name) {
199
+ throw new Error(
200
+ "bun-db.extractTableInfo: table-Argument ist weder EntityTableMeta noch drizzle pgTable",
201
+ );
202
+ }
203
+ const cols = extractDrizzleColumns(table);
204
+ const colByField = new Map<string, string>();
205
+ const fieldByCol = new Map<string, string>();
206
+ const typeByCol = new Map<string, string>();
207
+ for (const [field, { name: colName, sqlType }] of cols) {
208
+ colByField.set(field, colName);
209
+ fieldByCol.set(colName, field);
210
+ if (sqlType) typeByCol.set(colName, sqlType);
211
+ }
212
+ return {
213
+ name,
214
+ columnOf: (field) => colByField.get(field) ?? toSnakeCase(field),
215
+ pgTypeOf: (col) => typeByCol.get(col),
216
+ fieldOf: (col) => fieldByCol.get(col) ?? snakeToCamel(col),
217
+ hasColumn: (fieldOrColumn) => colByField.has(fieldOrColumn) || fieldByCol.has(fieldOrColumn),
218
+ };
219
+ }
220
+
221
+ function quoteIdent(name: string): string {
222
+ return `"${name.replace(/"/g, '""')}"`;
223
+ }
224
+
225
+ // --- Value coercion at the driver boundary ------------------------------
226
+ //
227
+ // postgres-js returns timestamptz as JS Date (or ISO string depending on
228
+ // driver config). Framework contract says timestamptz surfaces as
229
+ // Temporal.Instant — without coercion every read hands callers a Date and
230
+ // downstream code that does .epochMilliseconds / .add(...) crashes.
231
+ //
232
+ // Symmetric on write: Temporal.Instant doesn't bind directly into postgres-js
233
+ // params — convert to ISO string when the column pgType is timestamptz.
234
+
235
+ function isTemporalInstant(v: unknown): boolean {
236
+ return (
237
+ typeof v === "object" &&
238
+ v !== null &&
239
+ typeof (v as { epochNanoseconds?: unknown }).epochNanoseconds === "bigint"
240
+ );
241
+ }
242
+
243
+ function instantFromDriver(value: unknown): Temporal.Instant | null {
244
+ if (value === null || value === undefined) return null;
245
+ if (isTemporalInstant(value)) return value as Temporal.Instant;
246
+ // Number-coercion via +date — equivalent to .getTime() ohne Date-API-Methode
247
+ // (guard-no-date-api). Date → epochMilliseconds, dann zu Temporal.Instant.
248
+ if (value instanceof Date) return Temporal.Instant.fromEpochMilliseconds(+value);
249
+ if (typeof value === "string") {
250
+ try {
251
+ return Temporal.Instant.from(value);
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+ if (typeof value === "number") return Temporal.Instant.fromEpochMilliseconds(value);
257
+ return null;
258
+ }
259
+
260
+ // Walk the driver-row, applying three boundary-conversions per known column:
261
+ // - rename key snake_case → camelCase JS field-name (drizzle did this
262
+ // invisibly via its column-mapper; native dialect rebuild lost it)
263
+ // - parse jsonb string → object (postgres-js returns jsonb as text by
264
+ // default, not parsed JSON)
265
+ // - coerce timestamptz Date/string → Temporal.Instant
266
+ //
267
+ // Unknown columns (computed/aliased) pass through unchanged.
268
+ export function coerceRow<T extends Record<string, unknown>>(row: T, info: TableInfo): T {
269
+ const out: Record<string, unknown> = {};
270
+ let changed = false;
271
+ for (const key of Object.keys(row)) {
272
+ const pgType = info.pgTypeOf(key);
273
+ const value = row[key];
274
+ let coerced: unknown = value;
275
+ if (pgType === "timestamptz" || pgType === "timestamptz(3)") {
276
+ const t = instantFromDriver(value);
277
+ if (t !== null) coerced = t;
278
+ } else if (pgType === "jsonb" && typeof value === "string") {
279
+ try {
280
+ coerced = JSON.parse(value);
281
+ } catch {
282
+ // leave as string on parse error — caller decides
283
+ }
284
+ } else if ((pgType === "bigint" || pgType === "bigserial") && typeof value === "string") {
285
+ // postgres-js returns BIGINT as string to avoid JS-Number precision
286
+ // loss past 2^53. Framework contract: bigint columns surface as
287
+ // JS `bigint`. Drizzle's bigint customType did this conversion
288
+ // invisibly; the native dialect rebuild needs it explicit.
289
+ try {
290
+ coerced = BigInt(value);
291
+ } catch {
292
+ // leave as string on parse error
293
+ }
294
+ }
295
+ const fieldName = info.fieldOf(key);
296
+ if (fieldName !== key) changed = true;
297
+ if (coerced !== value) changed = true;
298
+ out[fieldName] = coerced;
299
+ }
300
+ return (changed ? out : row) as T;
301
+ }
302
+
303
+ function coerceRows<T extends Record<string, unknown>>(
304
+ rows: readonly T[],
305
+ info: TableInfo,
306
+ ): readonly T[] {
307
+ return rows.map((r) => coerceRow(r, info));
308
+ }
309
+
310
+ // Helper für jsonb-Werte: Bun.sql kann arrays/objects nicht direkt als
311
+ // jsonb binden — wir JSON.stringify + ::jsonb cast.
312
+ // Plus Temporal.Instant → ISO string coercion for timestamptz columns.
313
+ // SqlExpression (sql`now()`, sql`gen_random_uuid()`) wird als kind:"literal"
314
+ // returned — Caller embedded das inline statt einen $N-placeholder zu setzen.
315
+ type PreparedValue =
316
+ | { readonly kind: "param"; readonly sql: string; readonly bound: unknown }
317
+ | { readonly kind: "literal"; readonly literal: string };
318
+
319
+ function isSqlExpression(v: unknown): v is { kind: "sql-expr"; text: string } {
320
+ return typeof v === "object" && v !== null && (v as { kind?: unknown }).kind === "sql-expr";
321
+ }
322
+
323
+ function prepareValue(value: unknown, pgType: string | undefined): PreparedValue {
324
+ if (isSqlExpression(value)) {
325
+ return { kind: "literal", literal: value.text };
326
+ }
327
+ if (
328
+ pgType === "jsonb" &&
329
+ value !== null &&
330
+ typeof value === "object" &&
331
+ !isTemporalInstant(value)
332
+ ) {
333
+ return { kind: "param", sql: "::jsonb", bound: JSON.stringify(value) };
334
+ }
335
+ if ((pgType === "timestamptz" || pgType === "timestamptz(3)") && isTemporalInstant(value)) {
336
+ return { kind: "param", sql: "", bound: (value as Temporal.Instant).toString() };
337
+ }
338
+ return { kind: "param", sql: "", bound: value };
339
+ }
340
+
341
+ function buildWhereClause(
342
+ info: TableInfo,
343
+ where: WhereObject,
344
+ startIndex: number,
345
+ ): { sqlText: string; values: unknown[] } {
346
+ const conditions: string[] = [];
347
+ const values: unknown[] = [];
348
+ let idx = startIndex;
349
+ for (const [field, value] of Object.entries(where)) {
350
+ const col = info.columnOf(field);
351
+ const pgType = info.pgTypeOf(col);
352
+ if (value === null) {
353
+ conditions.push(`${quoteIdent(col)} IS NULL`);
354
+ } else if (Array.isArray(value)) {
355
+ if (value.length === 0) {
356
+ conditions.push("FALSE");
357
+ } else {
358
+ const parts: string[] = [];
359
+ for (const v of value) {
360
+ const p = prepareValue(v, pgType);
361
+ if (p.kind === "literal") {
362
+ parts.push(p.literal);
363
+ } else {
364
+ parts.push(`$${idx++}${p.sql}`);
365
+ values.push(p.bound);
366
+ }
367
+ }
368
+ conditions.push(`${quoteIdent(col)} IN (${parts.join(", ")})`);
369
+ }
370
+ } else if (isWhereOperator(value)) {
371
+ const opMap: Record<string, string> = {
372
+ gt: ">",
373
+ gte: ">=",
374
+ lt: "<",
375
+ lte: "<=",
376
+ ne: "<>",
377
+ like: "LIKE",
378
+ };
379
+ for (const [opKey, opSym] of Object.entries(opMap)) {
380
+ const opVal = (value as Record<string, unknown>)[opKey];
381
+ if (opVal === undefined) continue;
382
+ const p = prepareValue(opVal, pgType);
383
+ if (p.kind === "literal") {
384
+ conditions.push(`${quoteIdent(col)} ${opSym} ${p.literal}`);
385
+ } else {
386
+ conditions.push(`${quoteIdent(col)} ${opSym} $${idx++}${p.sql}`);
387
+ values.push(p.bound);
388
+ }
389
+ }
390
+ const inVal = (value as Record<string, unknown>)["in"];
391
+ if (Array.isArray(inVal)) {
392
+ if (inVal.length === 0) {
393
+ conditions.push("FALSE");
394
+ } else {
395
+ const parts: string[] = [];
396
+ for (const v of inVal) {
397
+ const p = prepareValue(v, pgType);
398
+ if (p.kind === "literal") {
399
+ parts.push(p.literal);
400
+ } else {
401
+ parts.push(`$${idx++}${p.sql}`);
402
+ values.push(p.bound);
403
+ }
404
+ }
405
+ conditions.push(`${quoteIdent(col)} IN (${parts.join(", ")})`);
406
+ }
407
+ }
408
+ } else {
409
+ const p = prepareValue(value, pgType);
410
+ if (p.kind === "literal") {
411
+ conditions.push(`${quoteIdent(col)} = ${p.literal}`);
412
+ } else {
413
+ conditions.push(`${quoteIdent(col)} = $${idx++}${p.sql}`);
414
+ values.push(p.bound);
415
+ }
416
+ }
417
+ }
418
+ return { sqlText: conditions.join(" AND "), values };
419
+ }
420
+
421
+ // biome-ignore lint/suspicious/noExplicitAny: opt-in default loosens row type for unannotated test fixtures
422
+ export async function selectMany<TRow = any>(
423
+ db: AnyDb,
424
+ table: TableLike,
425
+ where?: WhereObject,
426
+ options?: SelectOptions,
427
+ ): Promise<readonly TRow[]> {
428
+ const info = extractTableInfo(table);
429
+ let sqlText = `SELECT * FROM ${quoteIdent(info.name)}`;
430
+ let values: unknown[] = [];
431
+ if (where && Object.keys(where).length > 0) {
432
+ const w = buildWhereClause(info, where, 1);
433
+ sqlText += ` WHERE ${w.sqlText}`;
434
+ values = w.values;
435
+ }
436
+ if (options?.orderBy) {
437
+ const clauses = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
438
+ const parts = clauses.map((c) => {
439
+ const col = info.columnOf(c.col);
440
+ const dir = c.direction === "desc" ? "DESC" : "ASC";
441
+ return `${quoteIdent(col)} ${dir}`;
442
+ });
443
+ if (parts.length > 0) sqlText += ` ORDER BY ${parts.join(", ")}`;
444
+ }
445
+ if (options?.limit !== undefined) {
446
+ sqlText += ` LIMIT ${options.limit}`;
447
+ }
448
+ const raw = (await asRawClient(db).unsafe(sqlText, values)) as readonly Record<string, unknown>[];
449
+ return coerceRows(raw, info) as readonly TRow[];
450
+ }
451
+
452
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
453
+ export async function fetchOne<TRow = any>(
454
+ db: AnyDb,
455
+ table: TableLike,
456
+ where: WhereObject,
457
+ ): Promise<TRow | undefined> {
458
+ const rows = await selectMany<TRow>(db, table, where, { limit: 1 });
459
+ return rows[0];
460
+ }
461
+
462
+ // Bulk INSERT — same shape as insertOne but takes an array of rows and
463
+ // produces one multi-VALUES statement. Mirrors drizzle's
464
+ // `db.insert(t).values(rows[])`. Empty input is a no-op.
465
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
466
+ export async function insertMany<TRow = any>(
467
+ db: AnyDb,
468
+ table: TableLike,
469
+ rows: ReadonlyArray<Record<string, unknown>>,
470
+ ): Promise<readonly TRow[]> {
471
+ if (rows.length === 0) return [];
472
+ const info = extractTableInfo(table);
473
+ // Use the column-set from the first row; assume all rows share keys.
474
+ const firstRow = rows[0];
475
+ if (firstRow === undefined) return [];
476
+ // Filter out fields that don't exist as columns in the table (like drizzle did implicitly)
477
+ const fields = Object.keys(firstRow).filter((k) => info.hasColumn(k));
478
+ if (fields.length === 0) throw new Error("insertMany: empty row object");
479
+ const cols = fields.map((k) => quoteIdent(info.columnOf(k))).join(", ");
480
+ const params: unknown[] = [];
481
+ const valuesClauses: string[] = [];
482
+ for (const row of rows) {
483
+ const placeholders: string[] = [];
484
+ for (const f of fields) {
485
+ const col = info.columnOf(f);
486
+ const pgType = info.pgTypeOf(col);
487
+ const p = prepareValue(row[f], pgType);
488
+ if (p.kind === "literal") {
489
+ placeholders.push(p.literal);
490
+ } else {
491
+ params.push(p.bound);
492
+ placeholders.push(`$${params.length}${p.sql}`);
493
+ }
494
+ }
495
+ valuesClauses.push(`(${placeholders.join(", ")})`);
496
+ }
497
+ const sqlText = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES ${valuesClauses.join(", ")} RETURNING *`;
498
+ const raw = (await asRawClient(db).unsafe(sqlText, params)) as readonly Record<string, unknown>[];
499
+ return coerceRows(raw, info) as readonly TRow[];
500
+ }
501
+
502
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
503
+ export async function insertOne<TRow = any>(
504
+ db: AnyDb,
505
+ table: TableLike,
506
+ values: Record<string, unknown>,
507
+ ): Promise<TRow | undefined> {
508
+ const info = extractTableInfo(table);
509
+ const entries = Object.entries(values)
510
+ .filter(([k]) => info.hasColumn(k))
511
+ .map(([k, v]) => {
512
+ const col = info.columnOf(k);
513
+ const pgType = info.pgTypeOf(col);
514
+ return { col, prepared: prepareValue(v, pgType) };
515
+ });
516
+ if (entries.length === 0) throw new Error("insertOne: empty values object");
517
+ const cols = entries.map((e) => quoteIdent(e.col)).join(", ");
518
+ const params: unknown[] = [];
519
+ const placeholders = entries
520
+ .map((e) => {
521
+ if (e.prepared.kind === "literal") return e.prepared.literal;
522
+ params.push(e.prepared.bound);
523
+ return `$${params.length}${e.prepared.sql}`;
524
+ })
525
+ .join(", ");
526
+ const sqlText = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES (${placeholders}) RETURNING *`;
527
+ const rows = (await asRawClient(db).unsafe(sqlText, params)) as readonly Record<
528
+ string,
529
+ unknown
530
+ >[];
531
+ const first = rows[0];
532
+ if (!first) return undefined;
533
+ return coerceRow(first, info) as TRow;
534
+ }
535
+
536
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
537
+ export async function updateMany<TRow = any>(
538
+ db: AnyDb,
539
+ table: TableLike,
540
+ set: Record<string, unknown>,
541
+ where: WhereObject,
542
+ ): Promise<readonly TRow[]> {
543
+ const info = extractTableInfo(table);
544
+ const setEntries = Object.entries(set).map(([k, v]) => {
545
+ const col = info.columnOf(k);
546
+ const pgType = info.pgTypeOf(col);
547
+ return { col, prepared: prepareValue(v, pgType) };
548
+ });
549
+ if (setEntries.length === 0) throw new Error("updateMany: empty set object");
550
+ const values: unknown[] = [];
551
+ let idx = 1;
552
+ const setParts: string[] = [];
553
+ for (const e of setEntries) {
554
+ if (e.prepared.kind === "literal") {
555
+ setParts.push(`${quoteIdent(e.col)} = ${e.prepared.literal}`);
556
+ } else {
557
+ setParts.push(`${quoteIdent(e.col)} = $${idx++}${e.prepared.sql}`);
558
+ values.push(e.prepared.bound);
559
+ }
560
+ }
561
+ const w = buildWhereClause(info, where, idx);
562
+ let sqlText = `UPDATE ${quoteIdent(info.name)} SET ${setParts.join(", ")}`;
563
+ if (w.sqlText) {
564
+ sqlText += ` WHERE ${w.sqlText}`;
565
+ for (const v of w.values) values.push(v);
566
+ }
567
+ sqlText += " RETURNING *";
568
+ const raw = (await asRawClient(db).unsafe(sqlText, values)) as readonly Record<string, unknown>[];
569
+ return coerceRows(raw, info) as readonly TRow[];
570
+ }
571
+
572
+ export async function deleteMany(db: AnyDb, table: TableLike, where: WhereObject): Promise<void> {
573
+ const info = extractTableInfo(table);
574
+ const w = buildWhereClause(info, where, 1);
575
+ let sqlText = `DELETE FROM ${quoteIdent(info.name)}`;
576
+ if (w.sqlText) sqlText += ` WHERE ${w.sqlText}`;
577
+ await asRawClient(db).unsafe(sqlText, w.values);
578
+ }
579
+
580
+ type InsertEntry = {
581
+ readonly field: string;
582
+ readonly col: string;
583
+ readonly prepared: PreparedValue;
584
+ };
585
+
586
+ function insertEntries(info: TableInfo, values: Record<string, unknown>): InsertEntry[] {
587
+ return Object.entries(values)
588
+ .filter(([k]) => info.hasColumn(k))
589
+ .map(([k, v]) => {
590
+ const col = info.columnOf(k);
591
+ return { field: k, col, prepared: prepareValue(v, info.pgTypeOf(col)) };
592
+ });
593
+ }
594
+
595
+ function isEntityTableMeta(table: unknown): table is EntityTableMeta {
596
+ return (
597
+ typeof table === "object" &&
598
+ table !== null &&
599
+ "source" in table &&
600
+ (table.source === "managed" || table.source === "unmanaged") &&
601
+ "columns" in table
602
+ );
603
+ }
604
+
605
+ function resolveConflictColumns(
606
+ table: TableLike,
607
+ info: TableInfo,
608
+ conflictKeys: readonly string[] | undefined,
609
+ ): readonly string[] {
610
+ if (conflictKeys !== undefined && conflictKeys.length > 0) {
611
+ return conflictKeys.map((field) => info.columnOf(field));
612
+ }
613
+ if (isEntityTableMeta(table)) {
614
+ const pks = table.columns.filter((c) => c.primaryKey).map((c) => c.name);
615
+ if (pks.length > 0) return pks;
616
+ }
617
+ if (info.hasColumn("id")) return [info.columnOf("id")];
618
+ throw new Error("upsert: cannot infer conflict keys — pass conflictKeys explicitly");
619
+ }
620
+
621
+ function buildInsertSql(
622
+ info: TableInfo,
623
+ entries: readonly InsertEntry[],
624
+ ): { sqlPrefix: string; params: unknown[] } {
625
+ if (entries.length === 0) throw new Error("insert: empty values object");
626
+ const cols = entries.map((e) => quoteIdent(e.col)).join(", ");
627
+ const params: unknown[] = [];
628
+ const placeholders = entries
629
+ .map((e) => {
630
+ if (e.prepared.kind === "literal") return e.prepared.literal;
631
+ params.push(e.prepared.bound);
632
+ return `$${params.length}${e.prepared.sql}`;
633
+ })
634
+ .join(", ");
635
+ const sqlPrefix = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES (${placeholders})`;
636
+ return { sqlPrefix, params };
637
+ }
638
+
639
+ export type UpsertOnConflictOptions = {
640
+ readonly conflictKeys: readonly string[];
641
+ /** Columns to set on conflict. Default: EXCLUDED for every inserted non-key column. */
642
+ readonly update?: Record<string, unknown>;
643
+ };
644
+
645
+ export type DeleteManyBatchedOptions = {
646
+ readonly limit: number;
647
+ readonly maxBatches?: number;
648
+ /** Row id field for the LIMIT subquery. Default `id`. */
649
+ readonly idColumn?: string;
650
+ };
651
+
652
+ export type DeleteManyBatchedResult = {
653
+ readonly deleted: number;
654
+ readonly batches: number;
655
+ };
656
+
657
+ export async function countWhere(
658
+ db: AnyDb,
659
+ table: TableLike,
660
+ where: WhereObject = {},
661
+ ): Promise<number> {
662
+ const info = extractTableInfo(table);
663
+ let sqlText = `SELECT COUNT(*)::int AS count FROM ${quoteIdent(info.name)}`;
664
+ let values: unknown[] = [];
665
+ if (Object.keys(where).length > 0) {
666
+ const w = buildWhereClause(info, where, 1);
667
+ sqlText += ` WHERE ${w.sqlText}`;
668
+ values = w.values;
669
+ }
670
+ const rows = (await asRawClient(db).unsafe(sqlText, values)) as readonly { count: number }[];
671
+ return rows[0]?.count ?? 0;
672
+ }
673
+
674
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
675
+ export async function upsertOnConflict<TRow = any>(
676
+ db: AnyDb,
677
+ table: TableLike,
678
+ values: Record<string, unknown>,
679
+ options: UpsertOnConflictOptions,
680
+ ): Promise<TRow | undefined> {
681
+ const info = extractTableInfo(table);
682
+ const entries = insertEntries(info, values);
683
+ const conflictCols = resolveConflictColumns(table, info, options.conflictKeys);
684
+ const conflictSet = new Set(conflictCols);
685
+ const { sqlPrefix, params } = buildInsertSql(info, entries);
686
+
687
+ const updateParts: string[] = [];
688
+ let idx = params.length + 1;
689
+
690
+ if (options.update !== undefined) {
691
+ for (const [field, value] of Object.entries(options.update)) {
692
+ const col = info.columnOf(field);
693
+ const p = prepareValue(value, info.pgTypeOf(col));
694
+ if (p.kind === "literal") {
695
+ updateParts.push(`${quoteIdent(col)} = ${p.literal}`);
696
+ } else {
697
+ updateParts.push(`${quoteIdent(col)} = $${idx++}${p.sql}`);
698
+ params.push(p.bound);
699
+ }
700
+ }
701
+ } else {
702
+ for (const e of entries) {
703
+ if (conflictSet.has(e.col)) continue;
704
+ updateParts.push(`${quoteIdent(e.col)} = EXCLUDED.${quoteIdent(e.col)}`);
705
+ }
706
+ }
707
+
708
+ if (updateParts.length === 0) {
709
+ throw new Error("upsertOnConflict: nothing to update on conflict");
710
+ }
711
+
712
+ const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
713
+ const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO UPDATE SET ${updateParts.join(", ")} RETURNING *`;
714
+ const rows = (await asRawClient(db).unsafe(sqlText, params)) as readonly Record<
715
+ string,
716
+ unknown
717
+ >[];
718
+ const first = rows[0];
719
+ if (!first) return undefined;
720
+ return coerceRow(first, info) as TRow;
721
+ }
722
+
723
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
724
+ export async function upsertByPk<TRow = any>(
725
+ db: AnyDb,
726
+ table: TableLike,
727
+ values: Record<string, unknown>,
728
+ updateOnConflict?: Record<string, unknown>,
729
+ ): Promise<TRow | undefined> {
730
+ const info = extractTableInfo(table);
731
+ const conflictKeys = resolveConflictColumns(table, info, undefined);
732
+ const fieldKeys = conflictKeys.map((col) => info.fieldOf(col));
733
+ return upsertOnConflict<TRow>(db, table, values, {
734
+ conflictKeys: fieldKeys,
735
+ ...(updateOnConflict !== undefined ? { update: updateOnConflict } : {}),
736
+ });
737
+ }
738
+
739
+ export type IncrementCounterOptions = {
740
+ readonly conflictKeys?: readonly string[];
741
+ /** Extra SET clauses on conflict (e.g. lastUpdatedAt: sql\`NOW()\`). */
742
+ readonly set?: Record<string, unknown>;
743
+ };
744
+
745
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
746
+ export async function incrementCounter<TRow = any>(
747
+ db: AnyDb,
748
+ table: TableLike,
749
+ values: Record<string, unknown>,
750
+ increments: Record<string, number>,
751
+ options: IncrementCounterOptions = {},
752
+ ): Promise<TRow | undefined> {
753
+ const info = extractTableInfo(table);
754
+ const entries = insertEntries(info, values);
755
+ const conflictCols = resolveConflictColumns(table, info, options.conflictKeys);
756
+ const conflictSet = new Set(conflictCols);
757
+ const { sqlPrefix, params } = buildInsertSql(info, entries);
758
+ const tableQ = quoteIdent(info.name);
759
+
760
+ const updateParts: string[] = [];
761
+ let idx = params.length + 1;
762
+
763
+ for (const [field, delta] of Object.entries(increments)) {
764
+ const col = info.columnOf(field);
765
+ updateParts.push(`${quoteIdent(col)} = ${tableQ}.${quoteIdent(col)} + $${idx++}`);
766
+ params.push(delta);
767
+ }
768
+
769
+ if (options.set !== undefined) {
770
+ for (const [field, value] of Object.entries(options.set)) {
771
+ const col = info.columnOf(field);
772
+ const p = prepareValue(value, info.pgTypeOf(col));
773
+ if (p.kind === "literal") {
774
+ updateParts.push(`${quoteIdent(col)} = ${p.literal}`);
775
+ } else {
776
+ updateParts.push(`${quoteIdent(col)} = $${idx++}${p.sql}`);
777
+ params.push(p.bound);
778
+ }
779
+ }
780
+ }
781
+
782
+ for (const e of entries) {
783
+ if (conflictSet.has(e.col)) continue;
784
+ if (Object.hasOwn(increments, e.field)) continue;
785
+ if (options.set !== undefined && Object.hasOwn(options.set, e.field)) continue;
786
+ updateParts.push(`${quoteIdent(e.col)} = EXCLUDED.${quoteIdent(e.col)}`);
787
+ }
788
+
789
+ if (updateParts.length === 0) {
790
+ throw new Error("incrementCounter: nothing to update on conflict");
791
+ }
792
+
793
+ const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
794
+ const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO UPDATE SET ${updateParts.join(", ")} RETURNING *`;
795
+ const rows = (await asRawClient(db).unsafe(sqlText, params)) as readonly Record<
796
+ string,
797
+ unknown
798
+ >[];
799
+ const first = rows[0];
800
+ if (!first) return undefined;
801
+ return coerceRow(first, info) as TRow;
802
+ }
803
+
804
+ export async function deleteManyBatched(
805
+ db: AnyDb,
806
+ table: TableLike,
807
+ where: WhereObject,
808
+ options: DeleteManyBatchedOptions,
809
+ ): Promise<DeleteManyBatchedResult> {
810
+ const info = extractTableInfo(table);
811
+ const w = buildWhereClause(info, where, 1);
812
+ if (!w.sqlText) {
813
+ throw new Error("deleteManyBatched: where clause required — refusing unbounded batch delete");
814
+ }
815
+ const limit = options.limit;
816
+ if (!Number.isFinite(limit) || limit <= 0) {
817
+ throw new Error("deleteManyBatched: limit must be a positive number");
818
+ }
819
+ const idField = options.idColumn ?? "id";
820
+ const idCol = info.columnOf(idField);
821
+ const tableQ = quoteIdent(info.name);
822
+ const idQ = quoteIdent(idCol);
823
+ const maxBatches = options.maxBatches ?? Number.POSITIVE_INFINITY;
824
+
825
+ let deleted = 0;
826
+ let batches = 0;
827
+
828
+ while (batches < maxBatches) {
829
+ const limitIdx = w.values.length + 1;
830
+ const params = [...w.values, limit];
831
+ const sqlText = `DELETE FROM ${tableQ} WHERE ${idQ} IN (
832
+ SELECT ${idQ} FROM ${tableQ} WHERE ${w.sqlText} LIMIT $${limitIdx}
833
+ ) RETURNING ${idQ}`;
834
+ const rows = (await asRawClient(db).unsafe(sqlText, params)) as readonly unknown[];
835
+ batches++;
836
+ deleted += rows.length;
837
+ if (rows.length < limit) break;
838
+ }
839
+
840
+ return { deleted, batches };
841
+ }
842
+
843
+ export async function transaction<T>(db: AnyDb, fn: (tx: BunDbRunner) => Promise<T>): Promise<T> {
844
+ return (await asRawClient(db).begin(async (tx) => fn(tx as BunDbRunner))) as T;
845
+ }