@cosmicdrift/kumiko-framework 0.387.0 → 0.389.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 (375) hide show
  1. package/AGENTS.md +9 -1
  2. package/agents/examples/anonymous-access.md +1 -1
  3. package/agents/examples/apex-landing.md +204 -0
  4. package/agents/examples/api-worker-split.md +64 -0
  5. package/agents/examples/app-config.md +14 -0
  6. package/agents/examples/auth-mfa.md +115 -0
  7. package/agents/examples/env-composition.md +28 -1
  8. package/agents/examples/golden-path.md +13 -1
  9. package/agents/examples/legal-pages.md +1 -1
  10. package/agents/examples/lifecycle-hooks.md +55 -2
  11. package/agents/examples/list-table-options.md +466 -0
  12. package/agents/examples/pdf-audit-export.md +4 -2
  13. package/agents/examples/prod-boot-options.md +16 -0
  14. package/agents/examples/rate-limiting.md +48 -0
  15. package/agents/examples/record-detail-layout.md +692 -0
  16. package/agents/examples/seed-migration.md +212 -0
  17. package/agents/examples/state-machine.md +3 -2
  18. package/agents/examples/tenant-isolation.md +73 -1
  19. package/agents/examples/wizard-form.md +941 -0
  20. package/agents/examples/writeform-section.md +548 -0
  21. package/agents/examples.md +1 -0
  22. package/agents/features.md +19 -2
  23. package/agents/widgets.md +2 -0
  24. package/dist/api/api-constants.d.ts +1 -0
  25. package/dist/api/api-constants.js +2 -0
  26. package/dist/api/auth-routes-shared.d.ts +2 -0
  27. package/dist/api/auth-routes-shared.js +5 -0
  28. package/dist/api/client-error-route.js +10 -5
  29. package/dist/api/entity-read-access.d.ts +2 -1
  30. package/dist/api/entity-read-access.js +14 -0
  31. package/dist/api/export/export-cells-reference.d.ts +9 -0
  32. package/dist/api/export/export-cells-reference.js +21 -0
  33. package/dist/api/export/export-cells.d.ts +11 -0
  34. package/dist/api/export/export-cells.js +136 -0
  35. package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
  36. package/dist/api/export/export-ciphertext-guard.js +27 -0
  37. package/dist/api/export/export-columns.d.ts +29 -0
  38. package/dist/api/export/export-columns.js +85 -0
  39. package/dist/api/export/export-errors.d.ts +4 -0
  40. package/dist/api/export/export-errors.js +14 -0
  41. package/dist/api/export/export-route.d.ts +4 -0
  42. package/dist/api/export/export-route.js +74 -0
  43. package/dist/api/export/export-rows.d.ts +13 -0
  44. package/dist/api/export/export-rows.js +62 -0
  45. package/dist/api/export/export-screen.d.ts +12 -0
  46. package/dist/api/export/export-screen.js +47 -0
  47. package/dist/api/export/export-xlsx-writer.d.ts +4 -0
  48. package/dist/api/export/export-xlsx-writer.js +21 -0
  49. package/dist/api/extra-route.d.ts +4 -0
  50. package/dist/api/index.d.ts +3 -1
  51. package/dist/api/index.js +3 -1
  52. package/dist/api/pii-leak-guard.d.ts +3 -0
  53. package/dist/api/pii-leak-guard.js +3 -2
  54. package/dist/api/request-context.d.ts +1 -1
  55. package/dist/api/request-context.js +1 -1
  56. package/dist/api/routes.d.ts +16 -0
  57. package/dist/api/routes.js +2 -2
  58. package/dist/api/server-boot-checks.d.ts +10 -0
  59. package/dist/api/server-boot-checks.js +157 -0
  60. package/dist/api/server-event-dispatcher.d.ts +26 -0
  61. package/dist/api/server-event-dispatcher.js +208 -0
  62. package/dist/api/server-extra-routes.d.ts +11 -0
  63. package/dist/api/server-extra-routes.js +206 -0
  64. package/dist/api/server-middleware.d.ts +20 -0
  65. package/dist/api/server-middleware.js +211 -0
  66. package/dist/api/server-routes.d.ts +19 -0
  67. package/dist/api/server-routes.js +299 -0
  68. package/dist/api/server-websocket-routes.d.ts +4 -0
  69. package/dist/api/server-websocket-routes.js +108 -0
  70. package/dist/api/server-wiring.d.ts +32 -0
  71. package/dist/api/server-wiring.js +238 -0
  72. package/dist/api/server.d.ts +10 -15
  73. package/dist/api/server.js +48 -1251
  74. package/dist/api/session-cookies.js +4 -4
  75. package/dist/api/sse-route.d.ts +2 -1
  76. package/dist/api/sse-route.js +27 -13
  77. package/dist/config/index.d.ts +3 -1
  78. package/dist/crypto/owner-transition-rekey.d.ts +5 -1
  79. package/dist/crypto/owner-transition-rekey.js +1 -1
  80. package/dist/crypto/pg-kms-adapter.d.ts +1 -0
  81. package/dist/crypto/pg-kms-adapter.js +12 -18
  82. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  83. package/dist/crypto/pii-field-encryption.js +3 -1
  84. package/dist/db/aggregate-sql.d.ts +23 -0
  85. package/dist/db/aggregate-sql.js +165 -0
  86. package/dist/db/closed-connection.d.ts +9 -0
  87. package/dist/db/closed-connection.js +88 -0
  88. package/dist/db/connection.d.ts +4 -5
  89. package/dist/db/connection.js +29 -20
  90. package/dist/db/dialect.js +1 -1
  91. package/dist/db/distinct-on.d.ts +1 -1
  92. package/dist/db/driver-row-coercion.d.ts +2 -0
  93. package/dist/db/driver-row-coercion.js +186 -0
  94. package/dist/db/driver-value-preparation.d.ts +16 -0
  95. package/dist/db/driver-value-preparation.js +101 -0
  96. package/dist/db/entity-field-encryption.d.ts +1 -0
  97. package/dist/db/entity-field-encryption.js +1 -1
  98. package/dist/db/entity-table-meta.js +103 -110
  99. package/dist/db/event-store-executor-context.d.ts +2 -1
  100. package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
  101. package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
  102. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  103. package/dist/db/event-store-executor-read-aggregate.js +143 -0
  104. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  105. package/dist/db/event-store-executor-read-filter.js +206 -0
  106. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  107. package/dist/db/event-store-executor-read-paging.js +99 -0
  108. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  109. package/dist/db/event-store-executor-read-rows.js +32 -0
  110. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  111. package/dist/db/event-store-executor-read-search.js +221 -0
  112. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  113. package/dist/db/event-store-executor-read-sort.js +130 -0
  114. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  115. package/dist/db/event-store-executor-read-where.js +81 -0
  116. package/dist/db/event-store-executor-read.d.ts +2 -9
  117. package/dist/db/event-store-executor-read.js +48 -701
  118. package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
  119. package/dist/db/event-store-executor-write-atomic.js +41 -0
  120. package/dist/db/event-store-executor-write-steps.d.ts +17 -0
  121. package/dist/db/event-store-executor-write-steps.js +95 -0
  122. package/dist/db/event-store-executor-write.js +405 -509
  123. package/dist/db/event-store-executor.js +2 -1
  124. package/dist/db/executor-managed-tables.js +1 -1
  125. package/dist/db/index.d.ts +7 -3
  126. package/dist/db/index.js +6 -2
  127. package/dist/db/list-aggregate-labels.d.ts +14 -0
  128. package/dist/db/list-aggregate-labels.js +26 -0
  129. package/dist/db/migrate-generator.js +71 -86
  130. package/dist/db/migrate-runner.js +7 -4
  131. package/dist/db/pg-error.js +26 -21
  132. package/dist/db/queries/backfill-pii.js +3 -6
  133. package/dist/db/queries/database-now.js +2 -1
  134. package/dist/db/queries/shadow-swap.js +2 -3
  135. package/dist/db/query.d.ts +16 -15
  136. package/dist/db/query.js +76 -804
  137. package/dist/db/reference-data.js +19 -2
  138. package/dist/db/statement-timeout.js +2 -1
  139. package/dist/db/table-builder.js +113 -166
  140. package/dist/db/table-info.d.ts +10 -0
  141. package/dist/db/table-info.js +45 -0
  142. package/dist/db/temporal-value-guards.d.ts +6 -0
  143. package/dist/db/temporal-value-guards.js +15 -0
  144. package/dist/db/tenant-db-runner.d.ts +1 -0
  145. package/dist/db/tenant-db-runner.js +3 -0
  146. package/dist/db/tenant-db-upsert.d.ts +5 -0
  147. package/dist/db/tenant-db-upsert.js +91 -0
  148. package/dist/db/tenant-db.js +50 -10
  149. package/dist/db/where-clause.d.ts +6 -0
  150. package/dist/db/where-clause.js +236 -0
  151. package/dist/derivatives/delete-stored-file.js +2 -1
  152. package/dist/engine/boot-validator/boot-check.js +1 -1
  153. package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
  154. package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
  155. package/dist/engine/boot-validator/detail-screens.js +2 -2
  156. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  157. package/dist/engine/boot-validator/entity-handler.js +17 -0
  158. package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
  159. package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
  160. package/dist/engine/boot-validator/form-derivations.js +35 -1
  161. package/dist/engine/boot-validator/i18n-keys.js +5 -3
  162. package/dist/engine/boot-validator/index.js +2 -0
  163. package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
  164. package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
  165. package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
  166. package/dist/engine/boot-validator/list-column-layout.js +41 -0
  167. package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
  168. package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
  169. package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
  170. package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
  171. package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
  172. package/dist/engine/boot-validator/list-table-export-options.js +27 -0
  173. package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
  174. package/dist/engine/boot-validator/list-table-options.js +77 -0
  175. package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
  176. package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
  177. package/dist/engine/boot-validator/pii-retention.js +233 -155
  178. package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
  179. package/dist/engine/boot-validator/projection-list-screens.js +3 -13
  180. package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
  181. package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
  182. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  183. package/dist/engine/boot-validator/screens-actions.js +8 -1
  184. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  185. package/dist/engine/boot-validator/screens-forms.js +2 -6
  186. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  187. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  188. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  189. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  190. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  191. package/dist/engine/boot-validator/screens-kind-entity-list.js +235 -0
  192. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  193. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  194. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  195. package/dist/engine/boot-validator/screens-kind-projection-list.js +56 -0
  196. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  197. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  198. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  199. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  200. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  201. package/dist/engine/boot-validator/screens-lists.js +24 -11
  202. package/dist/engine/boot-validator/screens.d.ts +0 -2
  203. package/dist/engine/boot-validator/screens.js +99 -860
  204. package/dist/engine/build-app-schema.d.ts +2 -1
  205. package/dist/engine/build-app-schema.js +65 -97
  206. package/dist/engine/build-config-feature-schema.js +6 -1
  207. package/dist/engine/define-feature.js +13 -2
  208. package/dist/engine/define-handler.js +1 -0
  209. package/dist/engine/entity-handlers.d.ts +13 -0
  210. package/dist/engine/entity-handlers.js +10 -21
  211. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  212. package/dist/engine/entity-list-runtime-options.js +18 -0
  213. package/dist/engine/feature-ast/extractors/handlers.js +1 -0
  214. package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
  215. package/dist/engine/feature-ast/extractors/index.js +1 -1
  216. package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
  217. package/dist/engine/feature-ast/extractors/round1.js +6 -0
  218. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  219. package/dist/engine/feature-ast/index.d.ts +1 -1
  220. package/dist/engine/feature-ast/parse.js +3 -1
  221. package/dist/engine/feature-ast/patch.d.ts +2 -0
  222. package/dist/engine/feature-ast/patch.js +5 -182
  223. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
  224. package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
  225. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  226. package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
  227. package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
  228. package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
  229. package/dist/engine/feature-ast/patterns.d.ts +14 -1
  230. package/dist/engine/feature-ast/patterns.js +44 -51
  231. package/dist/engine/feature-ast/render.js +52 -87
  232. package/dist/engine/feature-builder-state.d.ts +1 -0
  233. package/dist/engine/feature-builder-state.js +1 -0
  234. package/dist/engine/feature-entity-handlers.js +3 -0
  235. package/dist/engine/feature-manifest.d.ts +1 -0
  236. package/dist/engine/feature-manifest.js +1 -0
  237. package/dist/engine/field-access.d.ts +1 -0
  238. package/dist/engine/field-access.js +12 -0
  239. package/dist/engine/form-derivations.js +18 -7
  240. package/dist/engine/handler-field-references.js +1 -1
  241. package/dist/engine/index.d.ts +2 -2
  242. package/dist/engine/index.js +1 -1
  243. package/dist/engine/pattern-library/library.js +2 -1
  244. package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
  245. package/dist/engine/pattern-library/static-schemas.js +9 -0
  246. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  247. package/dist/engine/project-app-schema-for-roles.js +6 -4
  248. package/dist/engine/registry-ingest.js +1 -1
  249. package/dist/engine/schema-builder.js +228 -212
  250. package/dist/engine/screen-filter-ops.js +2 -0
  251. package/dist/engine/screen-helpers.js +1 -1
  252. package/dist/engine/screen-kind.d.ts +7 -0
  253. package/dist/engine/screen-kind.js +1 -0
  254. package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
  255. package/dist/engine/types/feature.d.ts +1 -0
  256. package/dist/engine/types/index.d.ts +4 -3
  257. package/dist/engine/types/screen.d.ts +1 -1
  258. package/dist/engine/types/screen.js +1 -1
  259. package/dist/engine/validate-projection-allowlist.js +2 -1
  260. package/dist/entrypoint/index.d.ts +6 -1
  261. package/dist/entrypoint/index.js +10 -1
  262. package/dist/errors/docs-url.d.ts +1 -1
  263. package/dist/errors/docs-url.js +22 -2
  264. package/dist/es-ops/context.js +1 -1
  265. package/dist/event-store/admin-api.js +1 -1
  266. package/dist/event-store/event-store.d.ts +1 -0
  267. package/dist/event-store/event-store.js +17 -8
  268. package/dist/event-store/provenance-append.js +1 -1
  269. package/dist/files/bind-pending-file-refs.js +3 -3
  270. package/dist/files/create-file-ref-from-bytes.js +7 -1
  271. package/dist/files/create-file-ref.d.ts +3 -0
  272. package/dist/files/create-file-ref.js +11 -2
  273. package/dist/files/file-handle.js +2 -3
  274. package/dist/files/file-routes.d.ts +2 -0
  275. package/dist/files/file-routes.js +40 -10
  276. package/dist/files/provider-resolver.d.ts +1 -0
  277. package/dist/files/provider-resolver.js +1 -1
  278. package/dist/files/record-write-check.d.ts +6 -0
  279. package/dist/files/record-write-check.js +13 -0
  280. package/dist/files/resolve-content-type.js +43 -0
  281. package/dist/files/upload-ledger.d.ts +2 -2
  282. package/dist/files/upload-ledger.js +11 -4
  283. package/dist/i18n/required-surface-keys.js +189 -208
  284. package/dist/internal/stack-support.d.ts +2 -1
  285. package/dist/internal/stack-support.js +1 -1
  286. package/dist/internal/testing.js +1 -1
  287. package/dist/jobs/job-runner-context.d.ts +32 -0
  288. package/dist/jobs/job-runner-context.js +305 -0
  289. package/dist/jobs/job-runner-execution.d.ts +7 -0
  290. package/dist/jobs/job-runner-execution.js +160 -0
  291. package/dist/jobs/job-runner-gates.d.ts +19 -0
  292. package/dist/jobs/job-runner-gates.js +198 -0
  293. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  294. package/dist/jobs/job-runner-helpers.js +61 -2
  295. package/dist/jobs/job-runner-scope.d.ts +44 -0
  296. package/dist/jobs/job-runner-scope.js +102 -0
  297. package/dist/jobs/job-runner-types.d.ts +12 -0
  298. package/dist/jobs/job-runner.d.ts +0 -1
  299. package/dist/jobs/job-runner.js +50 -657
  300. package/dist/observability/escape-hatch-report.d.ts +3 -0
  301. package/dist/observability/escape-hatch-report.js +21 -3
  302. package/dist/pipeline/after-rollback-sink.d.ts +6 -0
  303. package/dist/pipeline/after-rollback-sink.js +32 -0
  304. package/dist/pipeline/cascade-handler.js +165 -138
  305. package/dist/pipeline/dispatch-batch.js +8 -1
  306. package/dist/pipeline/dispatch-query.js +10 -3
  307. package/dist/pipeline/dispatch-shared.d.ts +13 -3
  308. package/dist/pipeline/dispatch-shared.js +62 -710
  309. package/dist/pipeline/dispatch-write.js +296 -189
  310. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  311. package/dist/pipeline/event-dispatcher-delivery.js +6 -6
  312. package/dist/pipeline/event-dispatcher.d.ts +1 -0
  313. package/dist/pipeline/event-dispatcher.js +6 -9
  314. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  315. package/dist/pipeline/handler-context-auth.js +54 -0
  316. package/dist/pipeline/handler-context-config.d.ts +14 -0
  317. package/dist/pipeline/handler-context-config.js +113 -0
  318. package/dist/pipeline/handler-context-db.d.ts +24 -0
  319. package/dist/pipeline/handler-context-db.js +149 -0
  320. package/dist/pipeline/handler-context-dispatch.d.ts +23 -0
  321. package/dist/pipeline/handler-context-dispatch.js +101 -0
  322. package/dist/pipeline/handler-context-events.d.ts +41 -0
  323. package/dist/pipeline/handler-context-events.js +239 -0
  324. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  325. package/dist/pipeline/handler-context-notify.js +55 -0
  326. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  327. package/dist/pipeline/handler-context-observability.js +21 -0
  328. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  329. package/dist/pipeline/handler-context-scope.js +88 -0
  330. package/dist/pipeline/notify-scope.js +12 -1
  331. package/dist/pipeline/pre-handler-gates.js +20 -4
  332. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  333. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  334. package/dist/pipeline/projection-rebuild.js +2 -1
  335. package/dist/pipeline/system-hooks.d.ts +3 -1
  336. package/dist/pipeline/system-hooks.js +55 -6
  337. package/dist/pipeline/write-result-masking.js +13 -10
  338. package/dist/schema-cli/apply-command.d.ts +2 -0
  339. package/dist/schema-cli/apply-command.js +97 -0
  340. package/dist/schema-cli/baseline-command.d.ts +2 -0
  341. package/dist/schema-cli/baseline-command.js +33 -0
  342. package/dist/schema-cli/generate-command.d.ts +2 -0
  343. package/dist/schema-cli/generate-command.js +90 -0
  344. package/dist/schema-cli/shared.d.ts +39 -0
  345. package/dist/schema-cli/shared.js +95 -0
  346. package/dist/schema-cli/status-command.d.ts +2 -0
  347. package/dist/schema-cli/status-command.js +73 -0
  348. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  349. package/dist/schema-cli/validate-boot-input.js +130 -0
  350. package/dist/schema-cli/validate-command.d.ts +2 -0
  351. package/dist/schema-cli/validate-command.js +130 -0
  352. package/dist/schema-cli.d.ts +2 -18
  353. package/dist/schema-cli.js +47 -604
  354. package/dist/search/meilisearch-adapter.js +2 -1
  355. package/dist/search/reindex-entity.js +15 -5
  356. package/dist/ui-types/index.d.ts +2 -2
  357. package/dist/ui-types/index.js +1 -1
  358. package/dist/upgrade-changelog.d.ts +2 -0
  359. package/dist/upgrade-changelog.js +14 -0
  360. package/dist/upgrade-cli.d.ts +3 -12
  361. package/dist/upgrade-cli.js +171 -197
  362. package/dist/upgrade-marker-parse.d.ts +32 -0
  363. package/dist/upgrade-marker-parse.js +73 -0
  364. package/package.json +9 -6
  365. package/src/changes.json +555 -0
  366. package/src/scripts/codemod/codemod-support.ts +1 -1
  367. package/src/scripts/codemod/db-factory.ts +1 -1
  368. package/src/scripts/codemod/feature-layout.ts +10 -2
  369. package/src/scripts/codemod/object-form.ts +41 -4
  370. package/src/scripts/codemod/package-merge.ts +58 -8
  371. package/src/scripts/codemod/pii-personal-migration.ts +175 -145
  372. package/src/scripts/codemod/testing-injection.ts +122 -42
  373. package/src/scripts/codemod/testing-move.ts +29 -7
  374. package/dist/engine/provider-plugin-context.d.ts +0 -3
  375. package/dist/engine/provider-plugin-context.js +0 -28
package/dist/db/query.js CHANGED
@@ -1,4 +1,4 @@
1
- // Typed query helpers over the postgres-js driver: EntityTableMeta in, rows out.
1
+ // Typed query helpers over the Bun.SQL driver: EntityTableMeta in, rows out.
2
2
  //
3
3
  // API:
4
4
  // selectMany<T>(db, table, where?, opts?) → readonly T[]
@@ -12,33 +12,19 @@
12
12
  // transaction<T>(db, fn) → T
13
13
  //
14
14
  // `table` is an EntityTableMeta; the legacy table objects carrying it under a Symbol are accepted too.
15
- // Static import (not getTemporal()): coercion works without an installed
16
- // global and returns the same Temporal classes as the rest of the repo.
17
- import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
18
- import { isOneOf, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
19
- import { requestContext } from "../api/request-context.js";
20
- import { computeBlindIndex, configuredBlindIndexKey } from "../crypto/blind-index.js";
21
- import { isPiiCiphertext } from "../crypto/pii-ciphertext-format.js";
22
- import { camelCase as envCamelCase } from "../env/index.js";
15
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
16
+ import { quoteIdent } from "../crypto/ciphertext-pattern.js";
23
17
  import { InternalError } from "../errors/index.js";
24
- import { createFallbackLogger } from "../logging/index.js";
25
- import { parseJsonSafe } from "../utils/safe-json.js";
26
- import { SQL_EXPR_BRAND } from "./dialect.js";
18
+ import { aggregateDimensionSql, aggregateGroupOrderLimitSql, aggregateKeyOf, aggregateMeasureSql, assertValidAggregateLimits, } from "./aggregate-sql.js";
19
+ import { isClosedConnectionError, isPooledClient, logClosedConnection, poolMaxOf, } from "./closed-connection.js";
27
20
  import { buildDistinctOnClause } from "./distinct-on.js";
21
+ import { coerceDriverRow } from "./driver-row-coercion.js";
22
+ import { prepareValue } from "./driver-value-preparation.js";
28
23
  import { executorManagedWriteDenied } from "./executor-managed-tables.js";
29
24
  import { extractPgError } from "./pg-error.js";
30
- import { toSnakeCase } from "./table-builder.js";
25
+ import { extractTableInfo } from "./table-info.js";
31
26
  import { asEntityTableMeta, requireEntityTableMeta } from "./table-introspection.js";
32
- // Idempotent snake_case → camelCase. `env.camelCase` always lowercases first
33
- // (designed for SHOUT_CASE input) — for already-camelCase keys (mock rows
34
- // in tests, projection-aliased columns) it would silently produce "tenantid"
35
- // instead of leaving "tenantId" alone. Guard with an underscore check so
36
- // the conversion only fires when the key is actually snake-shaped.
37
- function snakeToCamel(key) {
38
- if (!key.includes("_"))
39
- return key;
40
- return envCamelCase(key);
41
- }
27
+ import { buildWhereClause } from "./where-clause.js";
42
28
  // table() (dialect) stores the canonical, shadow-proof EntityTableMeta under
43
29
  // `Symbol.for("kumiko:schema:Meta")`. The column handles on the table object are
44
30
  // enumerable props, so an entity field named `source`/`columns`/`tableName`/…
@@ -53,9 +39,9 @@ function isRawClient(value) {
53
39
  return typeof propertyOf(value, "unsafe") === "function";
54
40
  }
55
41
  export function asRawClient(db) {
56
- // Direct: postgres-js Sql / TransactionSql. Both
57
- // expose `.unsafe`; only Sql has `.begin` — TransactionSql uses
58
- // `.savepoint` for nested-tx. We only require `.unsafe` here; callers that
42
+ // Direct: Bun.SQL pool / transaction handle. Both
43
+ // expose `.unsafe`; only the pool starts a transaction — a transaction handle uses
44
+ // `.savepoint` for nesting. We only require `.unsafe` here; callers that
59
45
  // need `.begin` (e.g. `transaction()`) verify it themselves.
60
46
  if (isRawClient(db))
61
47
  return db;
@@ -66,15 +52,15 @@ export function asRawClient(db) {
66
52
  "tenant filter — use a ctx.db.<method>, or ctx.db.unsafeRaw() with escapeHatch: " +
67
53
  "{ reason } on the handler/hook (r.systemScope() features: ctx.systemDb.unsafeRaw(reason)).");
68
54
  }
69
- // Wrapper handle: $client = postgres-js Sql.
55
+ // Wrapper handle: $client = Bun.SQL pool.
70
56
  const $client = propertyOf(db, "$client");
71
57
  if (isRawClient($client))
72
58
  return $client;
73
- // Wrapper transaction handle: session.client = postgres-js Sql.
59
+ // Wrapper transaction handle: session.client = Bun.SQL pool.
74
60
  const sessionClient = propertyOf(propertyOf(db, "session"), "client");
75
61
  if (isRawClient(sessionClient))
76
62
  return sessionClient;
77
- throw new Error("asRawClient: db argument has no .unsafe() — pass a postgres-js Sql or a transaction handle.");
63
+ throw new Error("asRawClient: db argument has no .unsafe() — pass a Bun.SQL pool or a transaction handle.");
78
64
  }
79
65
  export function runnerCanBeginTransaction(db) {
80
66
  return hasBegin(asRawClient(db));
@@ -86,10 +72,31 @@ function savepointRunnerOf(sp) {
86
72
  export async function runInSavepoint(tx, fn) {
87
73
  const raw = asRawClient(tx);
88
74
  if (typeof raw.savepoint !== "function") {
89
- throw new Error("runInSavepoint: transaction handle has no savepoint() — pass the driver tx (postgres.js TransactionSql).");
75
+ throw new Error("runInSavepoint: transaction handle has no savepoint() — pass the driver tx (Bun.SQL TransactionSQL).");
90
76
  }
91
77
  return raw.savepoint((sp) => fn(savepointRunnerOf(sp)));
92
78
  }
79
+ // Same error-confinement as runInSavepoint, but for call sites that don't
80
+ // know whether `db` is a bare pool connection or an active transaction
81
+ // (e.g. the CRUD executor, invoked both from a dispatcher tx and directly
82
+ // against the pool by seeds/tests). A pool connection has no savepoint() —
83
+ // and doesn't need one, since each statement there is its own auto-committed
84
+ // unit and a failed statement can't poison anything downstream.
85
+ //
86
+ // `.savepoint` being present isn't proof the transaction is still open: a
87
+ // committed transaction handle still exposes `.savepoint`, and calling it
88
+ // fails (Bun.SQL: ERR_POSTGRES_INVALID_TRANSACTION_STATE, Postgres: 25P01)
89
+ // since there's no BEGIN left to nest into. That is now a programming error, not something
90
+ // to paper over by re-running `fn` without the savepoint's isolation.
91
+ const CONNECTION_CLOSED_CODE = "ERR_POSTGRES_CONNECTION_CLOSED";
92
+ const SAVEPOINT_ON_COMMITTED_CODES = new Set([
93
+ "25P01",
94
+ "ERR_POSTGRES_INVALID_TRANSACTION_STATE",
95
+ ]);
96
+ function isSavepointOnDeadTransactionCode(code) {
97
+ return (code !== undefined &&
98
+ (code === CONNECTION_CLOSED_CODE || SAVEPOINT_ON_COMMITTED_CODES.has(code)));
99
+ }
93
100
  export async function runInSavepointIfSupported(db, fn) {
94
101
  const raw = asRawClient(db);
95
102
  if (typeof raw.savepoint !== "function")
@@ -104,17 +111,17 @@ export async function runInSavepointIfSupported(db, fn) {
104
111
  }
105
112
  catch (e) {
106
113
  const code = extractPgError(e)?.code;
107
- if (!fnStarted && (code === "25P01" || code === "ERR_POSTGRES_CONNECTION_CLOSED")) {
114
+ if (!fnStarted && isSavepointOnDeadTransactionCode(code)) {
108
115
  throw new InternalError({
109
- message: code === "25P01"
110
- ? "runInSavepointIfSupported: savepoint on a transaction that has already committed — " +
111
- "this is almost always database work in an afterCommit hook. afterCommit is the default " +
112
- "phase of r.hook/postSave/postDelete. Use HookPhases.inTransaction for writes that must " +
113
- "roll back with the trigger, or ctx.dbOutsideTransaction for work that must survive a rollback."
114
- : "runInSavepointIfSupported: the transaction handle's connection is gone — the transaction " +
116
+ message: code === CONNECTION_CLOSED_CODE
117
+ ? "runInSavepointIfSupported: the transaction handle's connection is gone — the transaction " +
115
118
  "already committed (almost always database work in an afterCommit hook) or the connection dropped. " +
116
119
  "afterCommit is the default phase of r.hook/postSave/postDelete. Use HookPhases.inTransaction " +
117
- "for writes that must roll back with the trigger, or ctx.dbOutsideTransaction for work that must survive a rollback.",
120
+ "for writes that must roll back with the trigger, or ctx.dbOutsideTransaction for work that must survive a rollback."
121
+ : "runInSavepointIfSupported: savepoint on a transaction that has already committed — " +
122
+ "this is almost always database work in an afterCommit hook. afterCommit is the default " +
123
+ "phase of r.hook/postSave/postDelete. Use HookPhases.inTransaction for writes that must " +
124
+ "roll back with the trigger, or ctx.dbOutsideTransaction for work that must survive a rollback.",
118
125
  cause: e instanceof Error ? e : undefined,
119
126
  });
120
127
  }
@@ -145,677 +152,20 @@ function assertNotTenantScoped(db, fnName) {
145
152
  if (tenantDbDelegate(db) !== undefined) {
146
153
  throw new Error(`${fnName}: received a tenant-scoped db but this helper does not apply the tenant filter. ` +
147
154
  "Pass ctx.db.unsafeRaw() (with escapeHatch: { reason } on the handler/hook) for an " +
148
- "explicit cross-tenant op, or use a scoped TenantDb method.");
149
- }
150
- }
151
- const WHERE_OPERATOR_KEYS = [
152
- "gt",
153
- "gte",
154
- "lt",
155
- "lte",
156
- "ne",
157
- "in",
158
- "notIn",
159
- "like",
160
- "jsonText",
161
- ];
162
- const _whereOperatorKeysExhaustive = true;
163
- void _whereOperatorKeysExhaustive;
164
- function isJsonTextMatchList(value) {
165
- return Array.isArray(value);
166
- }
167
- function isWhereOperator(v) {
168
- if (v === null || typeof v !== "object" || Array.isArray(v))
169
- return false;
170
- // Don't false-positive on Date / Temporal / other domain-objects.
171
- // WhereOperator is plain object literal with at most these keys.
172
- const keys = Object.keys(v);
173
- if (keys.length === 0)
174
- return false;
175
- return keys.every((k) => isOneOf(WHERE_OPERATOR_KEYS, k));
176
- }
177
- export function extractTableInfo(table) {
178
- const meta = asEntityTableMeta(table);
179
- if (!meta) {
180
- throw new Error("extractTableInfo: table is not a kumiko EntityTableMeta — " +
181
- "build it via buildEntityTable / deriveEntityTableMeta / table().");
155
+ "explicit cross-tenant op, or use a scoped TenantDb method " +
156
+ "(for an upsert: ctx.db.upsert(table, values, { conflictTarget })).");
182
157
  }
183
- const colByField = new Map();
184
- const fieldByCol = new Map();
185
- const typeByCol = new Map();
186
- const bigintModeByCol = new Map();
187
- for (const c of meta.columns) {
188
- typeByCol.set(c.name, c.pgType);
189
- if (c.bigintJsMode !== undefined)
190
- bigintModeByCol.set(c.name, c.bigintJsMode);
191
- // EntityTableMeta column names are snake_case. Map snake → snake AND
192
- // derive a camelCase JS field-name so result rows can be renamed back
193
- // to the API shape (`aggregate_id` → `aggregateId`).
194
- colByField.set(c.name, c.name);
195
- const camel = snakeToCamel(c.name);
196
- if (camel !== c.name)
197
- colByField.set(camel, c.name);
198
- fieldByCol.set(c.name, camel === c.name ? c.name : camel);
199
- }
200
- return {
201
- name: meta.tableName,
202
- columnOf: (field) => colByField.get(field) ?? toSnakeCase(field),
203
- pgTypeOf: (col) => typeByCol.get(col),
204
- bigintJsModeOf: (col) => bigintModeByCol.get(col),
205
- fieldOf: (col) => fieldByCol.get(col) ?? snakeToCamel(col),
206
- hasColumn: (fieldOrColumn) => colByField.has(fieldOrColumn) || fieldByCol.has(fieldOrColumn),
207
- };
208
- }
209
- function quoteIdent(name) {
210
- return `"${name.replace(/"/g, '""')}"`;
211
- }
212
- // --- Value coercion at the driver boundary ------------------------------
213
- //
214
- // postgres-js returns timestamptz as JS Date (or ISO string depending on
215
- // driver config). Framework contract says timestamptz surfaces as
216
- // Temporal.Instant — without coercion every read hands callers a Date and
217
- // downstream code that does .epochMilliseconds / .add(...) crashes.
218
- //
219
- // Symmetric on write: Temporal.Instant doesn't bind directly into postgres-js
220
- // params — convert to ISO string when the column pgType is timestamptz.
221
- function isTemporalInstant(v) {
222
- return propertyOf(v, Symbol.toStringTag) === "Temporal.Instant";
223
158
  }
224
- // Postgres bigint → JS: safe integers become `number` (matches createBigIntField /
225
- // drizzle mode:"number"); values outside ±2^53 stay `bigint` (money cents, raw
226
- // unmanaged columns). Uses BigInt equality check so 9007199254740993n never
227
- // silently rounds to a safe integer.
228
- function coerceBigIntFromDriver(value) {
229
- let bi;
230
- if (typeof value === "string") {
231
- try {
232
- bi = BigInt(value);
233
- }
234
- catch {
235
- return value;
236
- }
237
- }
238
- else {
239
- bi = value;
240
- }
241
- const min = BigInt(Number.MIN_SAFE_INTEGER);
242
- const max = BigInt(Number.MAX_SAFE_INTEGER);
243
- if (bi >= min && bi <= max) {
244
- const n = Number(bi);
245
- if (BigInt(n) === bi)
246
- return n;
247
- }
248
- return bi;
249
- }
250
- function instantFromDriver(value) {
251
- if (value === null || value === undefined)
252
- return null;
253
- if (isTemporalInstant(value))
254
- return value;
255
- // Number coercion via +date — equivalent to .getTime() without a Date-API
256
- // method (guard-no-date-api). Date → epoch milliseconds, then to Temporal.Instant.
257
- if (value instanceof Date)
258
- return Temporal.Instant.fromEpochMilliseconds(+value);
259
- if (typeof value === "string") {
260
- try {
261
- return Temporal.Instant.from(value);
262
- }
263
- catch {
264
- return null;
265
- }
266
- }
267
- if (typeof value === "number")
268
- return Temporal.Instant.fromEpochMilliseconds(value);
269
- return null;
270
- }
271
- function isTemporalPlainDate(v) {
272
- return propertyOf(v, Symbol.toStringTag) === "Temporal.PlainDate";
273
- }
274
- function isTemporalPlainDateTimeLike(v) {
275
- const tag = propertyOf(v, Symbol.toStringTag);
276
- return ((tag === "Temporal.PlainDateTime" || tag === "Temporal.ZonedDateTime") &&
277
- typeof v === "object" &&
278
- v !== null &&
279
- "toPlainDate" in v &&
280
- typeof v.toPlainDate === "function");
281
- }
282
- // Verified empirically: a `date`
283
- // column comes back as a native JS Date, but ALWAYS anchored at UTC
284
- // midnight regardless of process TZ — epochMilliseconds is stable across
285
- // zones, only the local wall-clock read of that Date would drift the day
286
- // (e.g. America/Los_Angeles reads back the 14th for a stored 15th). Routing
287
- // through Instant→ZonedDateTimeISO("UTC")→PlainDate reads the calendar day
288
- // back deterministically without ever touching a local Date getter — the
289
- // same +value idiom instantFromDriver uses (guard-no-date-api: no
290
- // `.getTime()`), just anchored to a fixed zone instead of process-local.
291
- // A real `date` column always arrives as a UTC-midnight Date. Anything else means the column is
292
- // still timestamptz (declared `date` before its ALTER ran); the UTC anchor then silently shifts
293
- // the day, so say so once per column instead of returning the wrong day quietly.
294
- const warnedNonMidnightDateColumns = new Set();
295
- const MS_PER_DAY = 86_400_000;
296
- const dateColumnDriftLog = createFallbackLogger("postgres-js");
297
- function warnIfNotDateColumn(epochMs, column) {
298
- // skip: midnight value is a correct date column, and an already-warned column is deduped
299
- if (epochMs % MS_PER_DAY === 0 || warnedNonMidnightDateColumns.has(column))
300
- return;
301
- warnedNonMidnightDateColumns.add(column);
302
- dateColumnDriftLog.warn(`column "${column}" is declared as date but returned a non-midnight timestamp; it is probably still timestamptz. Run the ALTER ... TYPE date migration, otherwise days can shift.`);
303
- }
304
- function plainDateFromDriver(value, column) {
305
- if (value === null || value === undefined)
306
- return null;
307
- if (isTemporalPlainDate(value))
308
- return value;
309
- if (typeof value === "string") {
310
- const isoDay = /^\d{4}-\d{2}-\d{2}/.exec(value)?.[0];
311
- if (isoDay === undefined)
312
- return null;
313
- try {
314
- return Temporal.PlainDate.from(isoDay);
315
- }
316
- catch {
317
- return null;
318
- }
319
- }
320
- if (value instanceof Date) {
321
- warnIfNotDateColumn(+value, column);
322
- return Temporal.Instant.fromEpochMilliseconds(+value).toZonedDateTimeISO("UTC").toPlainDate();
323
- }
324
- return null;
325
- }
326
- // Walk the driver-row, applying three boundary-conversions per known column:
327
- // - rename key snake_case → camelCase JS field-name (drizzle did this
328
- // invisibly via its column-mapper; native dialect rebuild lost it)
329
- // - parse jsonb string → object (postgres-js returns jsonb as text by
330
- // default, not parsed JSON)
331
- // - coerce timestamptz Date/string → Temporal.Instant
332
- //
333
- // Unknown columns (computed/aliased) pass through unchanged.
334
159
  export function coerceRow(row, info) {
335
160
  // @cast-boundary db-row — the driver row (raw column names/types) becomes the caller's entity row shape here, and only here
336
161
  return coerceDriverRow(row, info);
337
162
  }
338
- function coerceDriverRow(row, info) {
339
- const out = {};
340
- let changed = false;
341
- for (const key of Object.keys(row)) {
342
- // Blind-index columns are pure lookup artifacts — the
343
- // deterministic HMAC must never leak into caller/API responses.
344
- if ((key.endsWith("_bidx") || key.endsWith("Bidx")) && info.hasColumn(key)) {
345
- changed = true;
346
- continue;
347
- }
348
- const pgType = info.pgTypeOf(key);
349
- const value = row[key];
350
- let coerced = value;
351
- if (pgType === "timestamptz" || pgType === "timestamptz(3)") {
352
- const t = instantFromDriver(value);
353
- if (t !== null)
354
- coerced = t;
355
- }
356
- else if (pgType === "date") {
357
- const d = plainDateFromDriver(value, key);
358
- if (d !== null)
359
- coerced = d;
360
- }
361
- else if (pgType === "jsonb" && typeof value === "string") {
362
- coerced = parseJsonSafe(value, value);
363
- }
364
- else if ((pgType === "bigint" || pgType === "bigserial") &&
365
- (typeof value === "string" || typeof value === "bigint")) {
366
- const jsMode = info.bigintJsModeOf(key);
367
- if (jsMode === "number") {
368
- coerced = coerceBigIntFromDriver(value);
369
- }
370
- else if (typeof value === "string") {
371
- try {
372
- coerced = BigInt(value);
373
- }
374
- catch {
375
- // leave as string on parse error
376
- }
377
- }
378
- else {
379
- coerced = value;
380
- }
381
- }
382
- else if ((pgType === "integer" || pgType === "int4" || pgType === "smallint" || pgType === "int2") &&
383
- (typeof value === "bigint" || typeof value === "string")) {
384
- // Drivers may return int4 as bigint or numeric string.
385
- // Drizzle coerced to number for numberField columns — match that.
386
- const n = Number(value);
387
- if (!Number.isNaN(n))
388
- coerced = n;
389
- }
390
- else if (typeof pgType === "string" &&
391
- pgType.startsWith("numeric") &&
392
- (typeof value === "string" || typeof value === "bigint")) {
393
- // pg returns numeric(p,s) as a string to preserve precision; the
394
- // decimal-field contract surfaces it as JS number (safe ≤ 2^53). A
395
- // non-numeric string would be DB corruption — leave it untouched so it
396
- // surfaces loud downstream rather than becoming a silent NaN.
397
- const n = Number(value);
398
- if (!Number.isNaN(n))
399
- coerced = n;
400
- }
401
- const fieldName = info.fieldOf(key);
402
- if (fieldName !== key)
403
- changed = true;
404
- if (coerced !== value)
405
- changed = true;
406
- out[fieldName] = coerced;
407
- }
408
- return changed ? out : row;
409
- }
410
163
  function coerceRows(rows, info) {
411
164
  return rows.map((r) => coerceRow(r, info));
412
165
  }
413
- // Checks the brand Symbol, not the `kind` string — a client-supplied jsonb
414
- // value can fake `kind: "sql-expr"` over JSON but can never carry a Symbol,
415
- // so request data can't be smuggled in as a raw SQL literal.
416
- function isSqlExpression(v) {
417
- return typeof v === "object" && v !== null && SQL_EXPR_BRAND in v;
418
- }
419
- // A `date` column takes a plain "yyyy-mm-dd" string (or PlainDate.toString())
420
- // as-is — no Instant detour. Postgres parses a DATE literal without ever
421
- // consulting the session timezone, so this is TZ-independent by construction
422
- // (verified empirically — see plainDateFromDriver above for the read side).
423
- // Compat cushion: code (this field used to be an Instant) may still
424
- // hand a Temporal.Instant into a where-filter or write on a `date` column —
425
- // same UTC anchor as plainDateFromDriver's read side, so a filter built with
426
- // `Temporal.Now.instant()` keeps working instead of binding a raw Instant the
427
- // driver can't serialize.
428
- function prepareDateValue(value) {
429
- if (isTemporalPlainDate(value)) {
430
- return { kind: "param", sql: "", bound: value.toString() };
431
- }
432
- if (isTemporalPlainDateTimeLike(value)) {
433
- return { kind: "param", sql: "", bound: value.toPlainDate().toString() };
434
- }
435
- if (isTemporalInstant(value)) {
436
- const day = value.toZonedDateTimeISO("UTC").toPlainDate();
437
- return { kind: "param", sql: "", bound: day.toString() };
438
- }
439
- return undefined;
440
- }
441
- // structured values bind directly with an ::jsonb cast; JSON.stringify first
442
- // would produce a JSON-string scalar instead of the structured value.
443
- function prepareJsonbValue(value) {
444
- if (typeof value === "boolean") {
445
- return { kind: "param", sql: "::text::jsonb", bound: JSON.stringify(value) };
446
- }
447
- if (typeof value !== "object" || isTemporalInstant(value)) {
448
- return undefined;
449
- }
450
- if (!Array.isArray(value)) {
451
- return { kind: "param", sql: "::jsonb", bound: value };
452
- }
453
- // All-boolean arrays are inferred as boolean[] by postgres-js; route via text::jsonb.
454
- if (value.length > 0 && value.every((entry) => typeof entry === "boolean")) {
455
- return { kind: "param", sql: "::text::jsonb", bound: JSON.stringify(value) };
456
- }
457
- return { kind: "param", sql: "::jsonb", bound: value };
458
- }
459
- const INTEGER_PG_TYPES = new Set([
460
- "integer",
461
- "int4",
462
- "smallint",
463
- "int2",
464
- "bigint",
465
- "int8",
466
- "serial",
467
- "bigserial",
468
- ]);
469
- // Postgres rounds sub-millisecond params into timestamptz(3); truncating to ms here keeps the
470
- // stored instant identical across drivers and equal to what postgres.js sends via Date.
471
- export function instantToTimestamptzParam(instant) {
472
- return Temporal.Instant.fromEpochMilliseconds(instant.epochMilliseconds).toString();
473
- }
474
- function prepareDriverNeutralScalar(value, pgType) {
475
- if ((pgType === "timestamptz" || pgType === "timestamptz(3)") && isTemporalInstant(value)) {
476
- return { kind: "param", sql: "", bound: instantToTimestamptzParam(value) };
477
- }
478
- // postgres.js sends text, so Postgres rejects 3.5 for an integer column instead of rounding it.
479
- if (typeof value === "number" && pgType !== undefined && INTEGER_PG_TYPES.has(pgType)) {
480
- return { kind: "param", sql: "", bound: String(value) };
481
- }
482
- return undefined;
483
- }
484
- function prepareValue(value, pgType) {
485
- if (isSqlExpression(value)) {
486
- return { kind: "literal", literal: value.text };
487
- }
488
- // A PII-encrypted jsonb field is a JSON string scalar; a raw string bound to
489
- // ::jsonb would be parsed as JSON text (or double-encoded).
490
- if (pgType === "jsonb" && isPiiCiphertext(value)) {
491
- return { kind: "param", sql: "::text::jsonb", bound: JSON.stringify(value) };
492
- }
493
- if (pgType === "jsonb" && value !== null) {
494
- const prepared = prepareJsonbValue(value);
495
- if (prepared)
496
- return prepared;
497
- }
498
- const scalar = prepareDriverNeutralScalar(value, pgType);
499
- if (scalar)
500
- return scalar;
501
- if (pgType === "date") {
502
- const prepared = prepareDateValue(value);
503
- if (prepared)
504
- return prepared;
505
- }
506
- return { kind: "param", sql: "", bound: value };
507
- }
508
- function buildWhereClause(info, where, startIndex, qualifier) {
509
- const conditions = [];
510
- const values = [];
511
- let idx = startIndex;
512
- // Single place that renders a column reference, so a join qualifier reaches
513
- // every arm (blind index, jsonb containment, IS NULL, jsonText) alike.
514
- const ref = (column) => qualifier === undefined ? quoteIdent(column) : `${quoteIdent(qualifier)}.${quoteIdent(column)}`;
515
- // multiSelect (and any other jsonb-array-of-scalars column) stores an
516
- // array — a filter value is one option, not the whole array, so eq/ne/in
517
- // must check array containment (`@>`) instead of scalar `=`/`<>`/`IN`
518
- // against the jsonb column, which Postgres rejects outright.
519
- // Mirrors event-store-executor-read.ts's applyFilter — keep in lock-step.
520
- function jsonbContainsAny(col, candidates) {
521
- if (candidates.length === 0)
522
- return "FALSE";
523
- const parts = [];
524
- for (const v of candidates) {
525
- const p = prepareJsonbValue([v]);
526
- // prepareJsonbValue never returns kind:"literal" (only prepareValue's
527
- // isSqlExpression branch does) — narrow anyway, PreparedValue is a union.
528
- if (p && p.kind === "param") {
529
- parts.push(`${ref(col)} @> $${idx++}${p.sql}`);
530
- values.push(p.bound);
531
- }
532
- // A jsonb column holding the scalar itself is never `@>` an array of it.
533
- if (isJsonbScalar(v)) {
534
- parts.push(`${ref(col)} = $${idx++}::text::jsonb`);
535
- values.push(JSON.stringify(v));
536
- }
537
- }
538
- return parts.length > 0 ? `(${parts.join(" OR ")})` : "FALSE";
539
- }
540
- function isJsonbScalar(value) {
541
- return value !== null && typeof value !== "object";
542
- }
543
- for (const [field, value] of Object.entries(where)) {
544
- const col = info.columnOf(field);
545
- const pgType = info.pgTypeOf(col);
546
- if (value === null) {
547
- conditions.push(`${ref(col)} IS NULL`);
548
- }
549
- else if (Array.isArray(value)) {
550
- if (value.length === 0) {
551
- conditions.push("FALSE");
552
- }
553
- else if (pgType === "jsonb") {
554
- conditions.push(jsonbContainsAny(col, value));
555
- }
556
- else {
557
- const parts = [];
558
- for (const v of value) {
559
- const p = prepareValue(v, pgType);
560
- if (p.kind === "literal") {
561
- parts.push(p.literal);
562
- }
563
- else {
564
- parts.push(`$${idx++}${p.sql}`);
565
- values.push(p.bound);
566
- }
567
- }
568
- conditions.push(`${ref(col)} IN (${parts.join(", ")})`);
569
- }
570
- }
571
- else if (isWhereOperator(value)) {
572
- if (value.jsonText !== undefined) {
573
- if (Object.keys(value).length > 1) {
574
- throw new Error(`where: jsonText on column "${col}" cannot be combined with other operators`);
575
- }
576
- if (pgType !== "jsonb") {
577
- throw new Error(`where: jsonText requires a jsonb column, "${col}" is ${pgType}`);
578
- }
579
- const matches = isJsonTextMatchList(value.jsonText) ? value.jsonText : [value.jsonText];
580
- if (matches.length === 0) {
581
- conditions.push("FALSE");
582
- continue;
583
- }
584
- for (const match of matches) {
585
- if (match === null ||
586
- typeof match !== "object" ||
587
- !Array.isArray(match.keys) ||
588
- match.keys.length === 0 ||
589
- !match.keys.every((k) => typeof k === "string") ||
590
- typeof match.eq !== "string") {
591
- throw new Error(`where: jsonText on column "${col}" needs non-empty string keys and a string eq`);
592
- }
593
- const lookups = match.keys.map((key) => {
594
- values.push(key);
595
- return `${ref(col)}->>$${idx++}::text`;
596
- });
597
- const lhs = lookups.length === 1 ? lookups[0] : `COALESCE(${lookups.join(", ")})`;
598
- values.push(match.eq);
599
- conditions.push(`(${lhs} = $${idx++})`);
600
- }
601
- continue;
602
- }
603
- if (value.ne === null && Object.keys(value).length === 1) {
604
- conditions.push(`${ref(col)} IS NOT NULL`);
605
- continue;
606
- }
607
- const opMap = {
608
- gt: ">",
609
- gte: ">=",
610
- lt: "<",
611
- lte: "<=",
612
- ne: "<>",
613
- like: "LIKE",
614
- };
615
- for (const [opKey, opSym] of Object.entries(opMap)) {
616
- const opVal = Reflect.get(value, opKey);
617
- if (opVal === undefined)
618
- continue;
619
- // Mirrors applyMultiSelectFilter: ordering/pattern ops have no meaning on a jsonb
620
- // option array (`like` would crash as `jsonb ~~ jsonb`), so match nothing.
621
- if (pgType === "jsonb" && opKey !== "ne") {
622
- conditions.push("FALSE");
623
- continue;
624
- }
625
- // Mirrors applyMultiSelectFilter: an array `ne` means "does not contain all of these".
626
- if (opKey === "ne" && pgType === "jsonb" && Array.isArray(opVal)) {
627
- if (opVal.length === 0) {
628
- conditions.push("FALSE");
629
- continue;
630
- }
631
- const p = prepareJsonbValue(opVal);
632
- if (p && p.kind === "param") {
633
- conditions.push(`(${ref(col)} IS NULL OR NOT (${ref(col)} @> $${idx++}${p.sql}))`);
634
- values.push(p.bound);
635
- continue;
636
- }
637
- }
638
- if (opKey === "ne" && pgType === "jsonb" && isJsonbScalar(opVal)) {
639
- const p = prepareJsonbValue([opVal]);
640
- if (p && p.kind === "param") {
641
- conditions.push(`(${ref(col)} IS NULL OR NOT (${ref(col)} @> $${idx++}${p.sql} OR ${ref(col)} = $${idx++}::text::jsonb))`);
642
- values.push(p.bound, JSON.stringify(opVal));
643
- continue;
644
- }
645
- }
646
- const p = prepareValue(opVal, pgType);
647
- if (p.kind === "literal") {
648
- conditions.push(`${ref(col)} ${opSym} ${p.literal}`);
649
- }
650
- else {
651
- conditions.push(`${ref(col)} ${opSym} $${idx++}${p.sql}`);
652
- values.push(p.bound);
653
- }
654
- }
655
- const inVal = value.in;
656
- if (Array.isArray(inVal)) {
657
- if (pgType === "jsonb") {
658
- conditions.push(jsonbContainsAny(col, inVal));
659
- }
660
- else if (inVal.length === 0) {
661
- conditions.push("FALSE");
662
- }
663
- else {
664
- const parts = [];
665
- for (const v of inVal) {
666
- const p = prepareValue(v, pgType);
667
- if (p.kind === "literal") {
668
- parts.push(p.literal);
669
- }
670
- else {
671
- parts.push(`$${idx++}${p.sql}`);
672
- values.push(p.bound);
673
- }
674
- }
675
- conditions.push(`${ref(col)} IN (${parts.join(", ")})`);
676
- }
677
- }
678
- const notInVal = value.notIn;
679
- if (Array.isArray(notInVal)) {
680
- if (pgType === "jsonb") {
681
- throw new Error(`where: notIn is not supported on jsonb column "${col}"`);
682
- }
683
- const parts = [];
684
- for (const v of notInVal) {
685
- const p = prepareValue(v, pgType);
686
- if (p.kind === "literal") {
687
- parts.push(p.literal);
688
- }
689
- else {
690
- parts.push(`$${idx++}${p.sql}`);
691
- values.push(p.bound);
692
- }
693
- }
694
- conditions.push(parts.length === 0 ? "TRUE" : `${ref(col)} NOT IN (${parts.join(", ")})`);
695
- }
696
- }
697
- else if (pgType === "jsonb" && isJsonbScalar(value)) {
698
- conditions.push(jsonbContainsAny(col, [value]));
699
- }
700
- else {
701
- const p = prepareValue(value, pgType);
702
- if (p.kind === "literal") {
703
- conditions.push(`${ref(col)} = ${p.literal}`);
704
- }
705
- else {
706
- // Blind-index OR rewrite: if the column has a bidx
707
- // counterpart (suffix convention `<col>_bidx`, framework-reserved)
708
- // and a key is configured, equality matches both arms —
709
- // plaintext legacy rows via the plaintext arm, encrypted rows
710
- // via the HMAC. The `kumiko-pii:` prefix on ciphertexts makes
711
- // a random match on the plaintext arm practically impossible.
712
- const bidxKey = configuredBlindIndexKey();
713
- const bidxCol = `${col}_bidx`;
714
- if (bidxKey !== undefined &&
715
- typeof value === "string" &&
716
- !col.endsWith("_bidx") &&
717
- info.hasColumn(bidxCol)) {
718
- conditions.push(`(${ref(col)} = $${idx++} OR ${ref(bidxCol)} = $${idx++})`);
719
- values.push(p.bound, computeBlindIndex(bidxKey, value));
720
- }
721
- else {
722
- conditions.push(`${ref(col)} = $${idx++}${p.sql}`);
723
- values.push(p.bound);
724
- }
725
- }
726
- }
727
- }
728
- return { sqlText: conditions.join(" AND "), values };
729
- }
730
- // A pool can briefly keep handing out connections the server just closed.
731
- // Each failed attempt discards one, so retries are bounded by pool size.
732
- const CLOSED_CONNECTION_CODES = new Set([
733
- "CONNECTION_CLOSED", // postgres-js
734
- "57P01", // PG SQLSTATE: admin_shutdown / terminated backend
735
- ]);
736
- export function isClosedConnectionError(err) {
737
- const code = extractPgError(err)?.code;
738
- return code !== undefined && CLOSED_CONNECTION_CODES.has(code);
739
- }
740
- // Tx/reserved handles are pinned to one physical connection — a retry there
741
- // would run on the same dead connection, not swap in a fresh one.
742
- function isPooledClient(raw) {
743
- if (raw === null || (typeof raw !== "object" && typeof raw !== "function"))
744
- return false;
745
- return (typeof propertyOf(raw, "begin") === "function" &&
746
- typeof propertyOf(raw, "savepoint") !== "function" &&
747
- typeof propertyOf(raw, "release") !== "function");
748
- }
749
- const closedConnectionLog = createFallbackLogger("postgres-js");
750
- // The driver rejects in-flight queries with this AbortError (no code) when the
751
- // pool closes under them, so isClosedConnectionError can't see it.
752
- const DRIVER_CLOSED_ABORT_MESSAGE = "The connection was closed.";
753
- function isDriverClosedAbort(err) {
754
- return (err instanceof Error && err.name === "AbortError" && err.message === DRIVER_CLOSED_ABORT_MESSAGE);
755
- }
756
- function isHandleLike(raw) {
757
- return raw !== null && (typeof raw === "object" || typeof raw === "function");
758
- }
759
- // postgres-js pool and tx handles both carry `typed`.
760
- function driverOf(raw) {
761
- if (!isHandleLike(raw))
762
- return "unknown";
763
- if (typeof raw["typed"] === "function")
764
- return "postgres-js";
765
- return "unknown";
766
- }
767
- function positiveNumber(v) {
768
- return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined;
769
- }
770
- // postgres-js configures timeouts in seconds.
771
- function timeoutOptionMs(options, driver, optionName) {
772
- if (driver !== "postgres-js")
773
- return undefined;
774
- const seconds = positiveNumber(options[optionName]);
775
- return seconds === undefined ? undefined : seconds * 1000;
776
- }
777
- function errorProp(err, name) {
778
- return isHandleLike(err) ? err[name] : undefined;
779
- }
780
- // Deliberately excludes SQL text and parameters: app SQL can carry PII.
781
- export function describeClosedConnectionFailure(raw, err, attempts) {
782
- if (!isClosedConnectionError(err) && !isDriverClosedAbort(err))
783
- return undefined;
784
- const driver = driverOf(raw);
785
- const rawOptions = isHandleLike(raw) ? raw["options"] : undefined;
786
- const options = isHandleLike(rawOptions) ? rawOptions : {};
787
- return {
788
- driver,
789
- pooled: isPooledClient(raw),
790
- attempts,
791
- poolMax: positiveNumber(options["max"]),
792
- idleTimeoutMs: timeoutOptionMs(options, driver, "idle_timeout"),
793
- maxLifetimeMs: timeoutOptionMs(options, driver, "max_lifetime"),
794
- connectionTimeoutMs: timeoutOptionMs(options, driver, "connect_timeout"),
795
- errorName: errorProp(err, "name"),
796
- errorCode: extractPgError(err)?.code ?? errorProp(err, "code"),
797
- errorErrno: errorProp(err, "errno"),
798
- errorMessage: errorProp(err, "message"),
799
- };
800
- }
801
- function logClosedConnection(raw, err, attempts, context) {
802
- const failure = describeClosedConnectionFailure(raw, err, attempts);
803
- // skip: not a closed-connection error, nothing to diagnose
804
- if (!failure)
805
- return;
806
- // Request identity lets this line be joined to the route's `handler failed` log.
807
- const request = requestContext.get();
808
- closedConnectionLog.warn("closed connection surfaced", {
809
- ...failure,
810
- ...context,
811
- requestId: request?.requestId,
812
- correlationId: request?.correlationId,
813
- handler: request?.handler,
814
- });
815
- }
816
166
  // Write and other non-retried paths: log when a closed-connection error leaves
817
167
  // the db layer, rethrow unchanged.
818
- async function unsafeLoggingClosedConnection(db, sqlText, params, context) {
168
+ export async function unsafeLoggingClosedConnection(db, sqlText, params, context) {
819
169
  const raw = asRawClient(db);
820
170
  try {
821
171
  return await raw.unsafe(sqlText, params);
@@ -825,11 +175,6 @@ async function unsafeLoggingClosedConnection(db, sqlText, params, context) {
825
175
  throw err;
826
176
  }
827
177
  }
828
- const DEFAULT_POOL_MAX = 10;
829
- function poolMaxOf(raw) {
830
- const max = propertyOf(propertyOf(raw, "options"), "max");
831
- return typeof max === "number" && Number.isInteger(max) && max > 0 ? max : DEFAULT_POOL_MAX;
832
- }
833
178
  // Exported so raw-SQL query modules outside the db layer (e.g. bundled-features'
834
179
  // db/queries/*.ts) can opt into the same closed-connection retry instead of
835
180
  // calling asRawClient(db).unsafe(...) directly and losing it.
@@ -1130,14 +475,14 @@ export async function deleteMany(db, table, where) {
1130
475
  operation: "deleteMany",
1131
476
  table: info.name,
1132
477
  });
1133
- // postgres-js reports the affected-row count of a DELETE on the result, no RETURNING needed.
478
+ // Bun.SQL reports the affected-row count of a DELETE on the result, no RETURNING needed.
1134
479
  const count = propertyOf(result, "count");
1135
480
  if (typeof count !== "number") {
1136
481
  throw new Error(`deleteMany(${info.name}): the driver result carries no affected-row count`);
1137
482
  }
1138
483
  return count;
1139
484
  }
1140
- function insertEntries(info, values) {
485
+ export function insertEntries(info, values) {
1141
486
  return Object.entries(values)
1142
487
  .filter(([k]) => info.hasColumn(k))
1143
488
  .map(([k, v]) => {
@@ -1163,7 +508,31 @@ export function resolveConflictColumns(table, info, conflictKeys) {
1163
508
  return [info.columnOf("id")];
1164
509
  throw new Error("upsert: cannot infer conflict keys — pass conflictKeys explicitly");
1165
510
  }
1166
- function buildInsertSql(info, entries) {
511
+ // Appends bound values to `params`; `skipCols` only applies to the default EXCLUDED set.
512
+ export function conflictUpdateParts(info, entries, update, skipCols, params) {
513
+ const parts = [];
514
+ if (update === undefined) {
515
+ for (const e of entries) {
516
+ if (skipCols.has(e.col))
517
+ continue;
518
+ parts.push(`${quoteIdent(e.col)} = EXCLUDED.${quoteIdent(e.col)}`);
519
+ }
520
+ return parts;
521
+ }
522
+ for (const [field, value] of Object.entries(update)) {
523
+ const col = info.columnOf(field);
524
+ const p = prepareValue(value, info.pgTypeOf(col));
525
+ if (p.kind === "literal") {
526
+ parts.push(`${quoteIdent(col)} = ${p.literal}`);
527
+ }
528
+ else {
529
+ params.push(p.bound);
530
+ parts.push(`${quoteIdent(col)} = $${params.length}${p.sql}`);
531
+ }
532
+ }
533
+ return parts;
534
+ }
535
+ export function buildInsertSql(info, entries) {
1167
536
  if (entries.length === 0)
1168
537
  throw new Error("insert: empty values object");
1169
538
  const cols = entries.map((e) => quoteIdent(e.col)).join(", ");
@@ -1191,82 +560,6 @@ export async function countWhere(db, table, where = {}) {
1191
560
  const rows = await unsafeReadRetrying(db, sqlText, values);
1192
561
  return rows[0]?.count ?? 0;
1193
562
  }
1194
- const AGGREGATE_BUCKET_UNITS = ["hour", "day"];
1195
- const AGGREGATE_ORDER_DIRECTIONS = ["asc", "desc"];
1196
- function requireAggregateColumn(info, field, role) {
1197
- if (!info.hasColumn(field)) {
1198
- throw new Error(`aggregateWhere: ${role} "${field}" is not a column of ${info.name}`);
1199
- }
1200
- return quoteIdent(info.columnOf(field));
1201
- }
1202
- export function isTimestamptzType(pgType) {
1203
- return pgType === "timestamptz" || pgType === "timestamptz(3)";
1204
- }
1205
- function aggregateKeyOf(raw) {
1206
- if (raw === null || raw === undefined)
1207
- return null;
1208
- if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean")
1209
- return raw;
1210
- if (typeof raw === "bigint")
1211
- return Number(raw);
1212
- if (raw instanceof Date)
1213
- return raw.toISOString();
1214
- return String(raw);
1215
- }
1216
- function aggregateMeasureSql(info, measure) {
1217
- switch (measure.fn) {
1218
- case "count":
1219
- return "COUNT(*)";
1220
- case "countDistinct":
1221
- return `COUNT(DISTINCT ${requireAggregateColumn(info, measure.field, "measure field")})`;
1222
- case "sum":
1223
- return `COALESCE(SUM(${requireAggregateColumn(info, measure.field, "measure field")}), 0)`;
1224
- case "avg":
1225
- return `AVG(${requireAggregateColumn(info, measure.field, "measure field")})`;
1226
- default:
1227
- throw new Error(`aggregateWhere: unknown measure fn "${String(Reflect.get(measure, "fn"))}"`);
1228
- }
1229
- }
1230
- function aggregateDimensionSql(info, dimension, index, params) {
1231
- const column = requireAggregateColumn(info, dimension.field, "groupBy field");
1232
- if (!("bucket" in dimension))
1233
- return `${column} AS k${index}`;
1234
- if (!AGGREGATE_BUCKET_UNITS.includes(dimension.bucket)) {
1235
- throw new Error(`aggregateWhere: unknown bucket "${String(dimension.bucket)}"`);
1236
- }
1237
- if (!isTimestamptzType(info.pgTypeOf(info.columnOf(dimension.field)))) {
1238
- throw new Error(`aggregateWhere: bucket on "${dimension.field}" requires a timestamptz column`);
1239
- }
1240
- params.push(dimension.timeZone);
1241
- return `(EXTRACT(EPOCH FROM date_trunc('${dimension.bucket}', ${column}, $${params.length}::text)) * 1000)::float8 AS k${index}`;
1242
- }
1243
- function assertValidAggregateLimits(spec) {
1244
- if (spec.limit !== undefined && (!Number.isInteger(spec.limit) || spec.limit < 0)) {
1245
- throw new Error(`aggregateWhere: limit must be a non-negative integer, got ${spec.limit}`);
1246
- }
1247
- if (spec.orderByValue !== undefined && !AGGREGATE_ORDER_DIRECTIONS.includes(spec.orderByValue)) {
1248
- throw new Error(`aggregateWhere: unknown orderByValue "${String(spec.orderByValue)}"`);
1249
- }
1250
- if (spec.orderByKeys !== undefined && !AGGREGATE_ORDER_DIRECTIONS.includes(spec.orderByKeys)) {
1251
- throw new Error(`aggregateWhere: unknown orderByKeys "${String(spec.orderByKeys)}"`);
1252
- }
1253
- }
1254
- function aggregateGroupOrderLimitSql(spec, dimensionCount) {
1255
- let sqlText = "";
1256
- if (dimensionCount > 0) {
1257
- const positions = Array.from({ length: dimensionCount }, (_, index) => String(index + 1));
1258
- sqlText += ` GROUP BY ${positions.join(", ")}`;
1259
- const keyDirection = spec.orderByKeys === "desc" ? "DESC" : "ASC";
1260
- const keyParts = positions.map((position) => `${position} ${keyDirection}`);
1261
- const orderParts = spec.orderByValue === undefined
1262
- ? keyParts
1263
- : [`${dimensionCount + 1} ${spec.orderByValue === "desc" ? "DESC" : "ASC"}`, ...keyParts];
1264
- sqlText += ` ORDER BY ${orderParts.join(", ")}`;
1265
- }
1266
- if (spec.limit !== undefined)
1267
- sqlText += ` LIMIT ${spec.limit}`;
1268
- return sqlText;
1269
- }
1270
563
  export async function aggregateWhere(db, table, spec, where = {}) {
1271
564
  const scoped = tenantDbDelegate(db);
1272
565
  if (scoped) {
@@ -1309,28 +602,7 @@ async function upsertOnConflictChecked(db, table, values, options) {
1309
602
  const conflictCols = resolveConflictColumns(table, info, options.conflictKeys);
1310
603
  const conflictSet = new Set(conflictCols);
1311
604
  const { sqlPrefix, params } = buildInsertSql(info, entries);
1312
- const updateParts = [];
1313
- let idx = params.length + 1;
1314
- if (options.update !== undefined) {
1315
- for (const [field, value] of Object.entries(options.update)) {
1316
- const col = info.columnOf(field);
1317
- const p = prepareValue(value, info.pgTypeOf(col));
1318
- if (p.kind === "literal") {
1319
- updateParts.push(`${quoteIdent(col)} = ${p.literal}`);
1320
- }
1321
- else {
1322
- updateParts.push(`${quoteIdent(col)} = $${idx++}${p.sql}`);
1323
- params.push(p.bound);
1324
- }
1325
- }
1326
- }
1327
- else {
1328
- for (const e of entries) {
1329
- if (conflictSet.has(e.col))
1330
- continue;
1331
- updateParts.push(`${quoteIdent(e.col)} = EXCLUDED.${quoteIdent(e.col)}`);
1332
- }
1333
- }
605
+ const updateParts = conflictUpdateParts(info, entries, options.update, conflictSet, params);
1334
606
  if (updateParts.length === 0) {
1335
607
  throw new Error("upsertOnConflict: nothing to update on conflict");
1336
608
  }