@blamejs/blamejs-shop 0.4.30 → 0.4.32

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 (338) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/checkout.js +8 -0
  4. package/lib/order.js +71 -11
  5. package/lib/vendor/MANIFEST.json +392 -278
  6. package/lib/vendor/blamejs/.github/workflows/ci.yml +34 -3
  7. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +21 -4
  8. package/lib/vendor/blamejs/.gitignore +6 -0
  9. package/lib/vendor/blamejs/CHANGELOG.md +26 -0
  10. package/lib/vendor/blamejs/MIGRATING.md +43 -0
  11. package/lib/vendor/blamejs/README.md +8 -6
  12. package/lib/vendor/blamejs/SECURITY.md +19 -3
  13. package/lib/vendor/blamejs/api-snapshot.json +2190 -664
  14. package/lib/vendor/blamejs/docker/caddy/localstack.Caddyfile +19 -0
  15. package/lib/vendor/blamejs/docker/init/generate-certs.sh +1 -1
  16. package/lib/vendor/blamejs/docker/otel/config.yaml +42 -0
  17. package/lib/vendor/blamejs/docker/otel/export/.gitkeep +0 -0
  18. package/lib/vendor/blamejs/docker/postgres/initdb/10-replication.sh +15 -0
  19. package/lib/vendor/blamejs/docker/postgres/replica-entrypoint.sh +38 -0
  20. package/lib/vendor/blamejs/docker/toxiproxy/toxiproxy.json +14 -0
  21. package/lib/vendor/blamejs/docker-compose.test.yml +209 -0
  22. package/lib/vendor/blamejs/examples/wiki/lib/page-generator.js +132 -0
  23. package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +221 -61
  24. package/lib/vendor/blamejs/examples/wiki/lib/source-doc-parser.js +144 -9
  25. package/lib/vendor/blamejs/examples/wiki/test/e2e.js +99 -0
  26. package/lib/vendor/blamejs/fuzz/guard-sql.fuzz.js +36 -0
  27. package/lib/vendor/blamejs/index.js +4 -0
  28. package/lib/vendor/blamejs/lib/agent-envelope-mac.js +104 -0
  29. package/lib/vendor/blamejs/lib/agent-event-bus.js +105 -4
  30. package/lib/vendor/blamejs/lib/agent-posture-chain.js +8 -42
  31. package/lib/vendor/blamejs/lib/ai-content-detect.js +9 -10
  32. package/lib/vendor/blamejs/lib/api-key.js +158 -77
  33. package/lib/vendor/blamejs/lib/atomic-file.js +62 -4
  34. package/lib/vendor/blamejs/lib/audit-chain.js +47 -11
  35. package/lib/vendor/blamejs/lib/audit-sign.js +77 -2
  36. package/lib/vendor/blamejs/lib/audit-tools.js +79 -51
  37. package/lib/vendor/blamejs/lib/audit.js +259 -123
  38. package/lib/vendor/blamejs/lib/auth/oauth.js +53 -9
  39. package/lib/vendor/blamejs/lib/auth/openid-federation.js +108 -47
  40. package/lib/vendor/blamejs/lib/auth/saml.js +6 -8
  41. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +31 -5
  42. package/lib/vendor/blamejs/lib/backup/index.js +45 -10
  43. package/lib/vendor/blamejs/lib/break-glass.js +355 -147
  44. package/lib/vendor/blamejs/lib/cache.js +174 -105
  45. package/lib/vendor/blamejs/lib/chain-writer.js +38 -16
  46. package/lib/vendor/blamejs/lib/cli.js +19 -14
  47. package/lib/vendor/blamejs/lib/cluster-provider-db.js +130 -104
  48. package/lib/vendor/blamejs/lib/cluster-storage.js +119 -22
  49. package/lib/vendor/blamejs/lib/cluster.js +119 -71
  50. package/lib/vendor/blamejs/lib/codepoint-class.js +23 -0
  51. package/lib/vendor/blamejs/lib/compliance.js +206 -4
  52. package/lib/vendor/blamejs/lib/consent.js +82 -29
  53. package/lib/vendor/blamejs/lib/constants.js +27 -11
  54. package/lib/vendor/blamejs/lib/crypto-field.js +916 -156
  55. package/lib/vendor/blamejs/lib/db-declare-row-policy.js +35 -22
  56. package/lib/vendor/blamejs/lib/db-file-lifecycle.js +3 -2
  57. package/lib/vendor/blamejs/lib/db-query.js +882 -260
  58. package/lib/vendor/blamejs/lib/db-schema.js +228 -44
  59. package/lib/vendor/blamejs/lib/db.js +249 -99
  60. package/lib/vendor/blamejs/lib/dsr.js +385 -55
  61. package/lib/vendor/blamejs/lib/error-page.js +14 -1
  62. package/lib/vendor/blamejs/lib/external-db-migrate.js +239 -137
  63. package/lib/vendor/blamejs/lib/external-db.js +549 -34
  64. package/lib/vendor/blamejs/lib/file-upload.js +52 -7
  65. package/lib/vendor/blamejs/lib/framework-error.js +20 -1
  66. package/lib/vendor/blamejs/lib/framework-files.js +73 -0
  67. package/lib/vendor/blamejs/lib/framework-schema.js +695 -394
  68. package/lib/vendor/blamejs/lib/gate-contract.js +659 -1
  69. package/lib/vendor/blamejs/lib/guard-agent-registry.js +26 -44
  70. package/lib/vendor/blamejs/lib/guard-all.js +1 -0
  71. package/lib/vendor/blamejs/lib/guard-auth.js +42 -112
  72. package/lib/vendor/blamejs/lib/guard-cidr.js +33 -154
  73. package/lib/vendor/blamejs/lib/guard-csv.js +46 -113
  74. package/lib/vendor/blamejs/lib/guard-domain.js +34 -157
  75. package/lib/vendor/blamejs/lib/guard-dsn.js +27 -43
  76. package/lib/vendor/blamejs/lib/guard-email.js +47 -69
  77. package/lib/vendor/blamejs/lib/guard-envelope.js +19 -32
  78. package/lib/vendor/blamejs/lib/guard-event-bus-payload.js +24 -42
  79. package/lib/vendor/blamejs/lib/guard-event-bus-topic.js +25 -43
  80. package/lib/vendor/blamejs/lib/guard-filename.js +42 -106
  81. package/lib/vendor/blamejs/lib/guard-graphql.js +42 -123
  82. package/lib/vendor/blamejs/lib/guard-html.js +53 -108
  83. package/lib/vendor/blamejs/lib/guard-idempotency-key.js +24 -42
  84. package/lib/vendor/blamejs/lib/guard-image.js +46 -103
  85. package/lib/vendor/blamejs/lib/guard-imap-command.js +18 -32
  86. package/lib/vendor/blamejs/lib/guard-jmap.js +16 -30
  87. package/lib/vendor/blamejs/lib/guard-json.js +38 -108
  88. package/lib/vendor/blamejs/lib/guard-jsonpath.js +38 -171
  89. package/lib/vendor/blamejs/lib/guard-jwt.js +49 -179
  90. package/lib/vendor/blamejs/lib/guard-list-id.js +25 -41
  91. package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +27 -43
  92. package/lib/vendor/blamejs/lib/guard-mail-compose.js +24 -42
  93. package/lib/vendor/blamejs/lib/guard-mail-move.js +26 -44
  94. package/lib/vendor/blamejs/lib/guard-mail-query.js +28 -46
  95. package/lib/vendor/blamejs/lib/guard-mail-reply.js +24 -42
  96. package/lib/vendor/blamejs/lib/guard-mail-sieve.js +24 -42
  97. package/lib/vendor/blamejs/lib/guard-managesieve-command.js +17 -31
  98. package/lib/vendor/blamejs/lib/guard-markdown.js +37 -104
  99. package/lib/vendor/blamejs/lib/guard-message-id.js +26 -45
  100. package/lib/vendor/blamejs/lib/guard-mime.js +39 -151
  101. package/lib/vendor/blamejs/lib/guard-oauth.js +54 -135
  102. package/lib/vendor/blamejs/lib/guard-pdf.js +45 -101
  103. package/lib/vendor/blamejs/lib/guard-pop3-command.js +21 -31
  104. package/lib/vendor/blamejs/lib/guard-posture-chain.js +24 -42
  105. package/lib/vendor/blamejs/lib/guard-regex.js +33 -107
  106. package/lib/vendor/blamejs/lib/guard-saga-config.js +24 -42
  107. package/lib/vendor/blamejs/lib/guard-shell.js +42 -172
  108. package/lib/vendor/blamejs/lib/guard-smtp-command.js +48 -54
  109. package/lib/vendor/blamejs/lib/guard-snapshot-envelope.js +24 -42
  110. package/lib/vendor/blamejs/lib/guard-sql.js +1491 -0
  111. package/lib/vendor/blamejs/lib/guard-stream-args.js +24 -43
  112. package/lib/vendor/blamejs/lib/guard-svg.js +47 -65
  113. package/lib/vendor/blamejs/lib/guard-template.js +35 -172
  114. package/lib/vendor/blamejs/lib/guard-tenant-id.js +26 -45
  115. package/lib/vendor/blamejs/lib/guard-time.js +32 -154
  116. package/lib/vendor/blamejs/lib/guard-trace-context.js +25 -44
  117. package/lib/vendor/blamejs/lib/guard-uuid.js +32 -153
  118. package/lib/vendor/blamejs/lib/guard-xml.js +38 -113
  119. package/lib/vendor/blamejs/lib/guard-yaml.js +51 -163
  120. package/lib/vendor/blamejs/lib/http-client.js +37 -9
  121. package/lib/vendor/blamejs/lib/inbox.js +120 -107
  122. package/lib/vendor/blamejs/lib/legal-hold.js +121 -50
  123. package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +47 -31
  124. package/lib/vendor/blamejs/lib/log-stream-otlp.js +32 -18
  125. package/lib/vendor/blamejs/lib/mail-auth.js +236 -0
  126. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +2 -6
  127. package/lib/vendor/blamejs/lib/mail-dkim.js +1 -0
  128. package/lib/vendor/blamejs/lib/mail-greylist.js +2 -6
  129. package/lib/vendor/blamejs/lib/mail-helo.js +2 -6
  130. package/lib/vendor/blamejs/lib/mail-journal.js +85 -64
  131. package/lib/vendor/blamejs/lib/mail-rbl.js +2 -6
  132. package/lib/vendor/blamejs/lib/mail-scan.js +2 -6
  133. package/lib/vendor/blamejs/lib/mail-server-jmap.js +117 -12
  134. package/lib/vendor/blamejs/lib/mail-server-mx.js +276 -7
  135. package/lib/vendor/blamejs/lib/mail-spam-score.js +2 -6
  136. package/lib/vendor/blamejs/lib/mail-store.js +293 -154
  137. package/lib/vendor/blamejs/lib/mail.js +8 -4
  138. package/lib/vendor/blamejs/lib/middleware/body-parser.js +71 -25
  139. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +19 -8
  140. package/lib/vendor/blamejs/lib/middleware/dpop.js +10 -1
  141. package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +17 -7
  142. package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +75 -51
  143. package/lib/vendor/blamejs/lib/middleware/rate-limit.js +102 -32
  144. package/lib/vendor/blamejs/lib/middleware/security-headers.js +21 -5
  145. package/lib/vendor/blamejs/lib/migrations.js +108 -66
  146. package/lib/vendor/blamejs/lib/network-heartbeat.js +7 -0
  147. package/lib/vendor/blamejs/lib/network-proxy.js +24 -1
  148. package/lib/vendor/blamejs/lib/nonce-store.js +31 -9
  149. package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +9 -4
  150. package/lib/vendor/blamejs/lib/object-store/azure-blob.js +57 -3
  151. package/lib/vendor/blamejs/lib/object-store/gcs.js +4 -1
  152. package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -2
  153. package/lib/vendor/blamejs/lib/object-store/sigv4.js +38 -6
  154. package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +9 -1
  155. package/lib/vendor/blamejs/lib/observability.js +124 -0
  156. package/lib/vendor/blamejs/lib/otel-export.js +12 -3
  157. package/lib/vendor/blamejs/lib/outbox.js +184 -83
  158. package/lib/vendor/blamejs/lib/parsers/safe-xml.js +47 -7
  159. package/lib/vendor/blamejs/lib/pqc-agent.js +44 -0
  160. package/lib/vendor/blamejs/lib/pubsub-cluster.js +42 -20
  161. package/lib/vendor/blamejs/lib/queue-local.js +225 -140
  162. package/lib/vendor/blamejs/lib/queue-redis.js +9 -1
  163. package/lib/vendor/blamejs/lib/queue-sqs.js +6 -0
  164. package/lib/vendor/blamejs/lib/queue.js +7 -0
  165. package/lib/vendor/blamejs/lib/redact.js +68 -11
  166. package/lib/vendor/blamejs/lib/redis-client.js +160 -31
  167. package/lib/vendor/blamejs/lib/request-helpers.js +7 -0
  168. package/lib/vendor/blamejs/lib/retention.js +101 -40
  169. package/lib/vendor/blamejs/lib/router.js +212 -5
  170. package/lib/vendor/blamejs/lib/safe-dns.js +29 -45
  171. package/lib/vendor/blamejs/lib/safe-ical.js +18 -33
  172. package/lib/vendor/blamejs/lib/safe-icap.js +27 -43
  173. package/lib/vendor/blamejs/lib/safe-sieve.js +21 -40
  174. package/lib/vendor/blamejs/lib/safe-sql.js +212 -3
  175. package/lib/vendor/blamejs/lib/safe-url.js +170 -3
  176. package/lib/vendor/blamejs/lib/safe-vcard.js +18 -33
  177. package/lib/vendor/blamejs/lib/scheduler.js +35 -12
  178. package/lib/vendor/blamejs/lib/seeders.js +122 -74
  179. package/lib/vendor/blamejs/lib/session-stores.js +42 -14
  180. package/lib/vendor/blamejs/lib/session.js +175 -77
  181. package/lib/vendor/blamejs/lib/sql.js +3842 -0
  182. package/lib/vendor/blamejs/lib/sse.js +26 -0
  183. package/lib/vendor/blamejs/lib/ssrf-guard.js +151 -4
  184. package/lib/vendor/blamejs/lib/static.js +177 -34
  185. package/lib/vendor/blamejs/lib/subject.js +96 -49
  186. package/lib/vendor/blamejs/lib/vault/index.js +3 -2
  187. package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -2
  188. package/lib/vendor/blamejs/lib/vault/rotate.js +168 -108
  189. package/lib/vendor/blamejs/lib/vault-aad.js +6 -0
  190. package/lib/vendor/blamejs/lib/vendor-data.js +2 -0
  191. package/lib/vendor/blamejs/lib/websocket.js +35 -5
  192. package/lib/vendor/blamejs/lib/worker-pool.js +11 -0
  193. package/lib/vendor/blamejs/package.json +2 -2
  194. package/lib/vendor/blamejs/release-notes/v0.14.x.json +1503 -0
  195. package/lib/vendor/blamejs/release-notes/v0.15.0.json +77 -0
  196. package/lib/vendor/blamejs/release-notes/v0.15.1.json +22 -0
  197. package/lib/vendor/blamejs/release-notes/v0.15.2.json +22 -0
  198. package/lib/vendor/blamejs/release-notes/v0.15.3.json +39 -0
  199. package/lib/vendor/blamejs/release-notes/v0.15.4.json +39 -0
  200. package/lib/vendor/blamejs/release-notes/v0.15.5.json +22 -0
  201. package/lib/vendor/blamejs/release-notes/v0.15.6.json +59 -0
  202. package/lib/vendor/blamejs/scripts/check-services.js +21 -0
  203. package/lib/vendor/blamejs/scripts/gen-migrating.js +51 -0
  204. package/lib/vendor/blamejs/scripts/release.js +398 -38
  205. package/lib/vendor/blamejs/test/00-primitives.js +117 -0
  206. package/lib/vendor/blamejs/test/10-state.js +140 -14
  207. package/lib/vendor/blamejs/test/20-db.js +65 -2
  208. package/lib/vendor/blamejs/test/helpers/db.js +9 -0
  209. package/lib/vendor/blamejs/test/helpers/drivers.js +27 -15
  210. package/lib/vendor/blamejs/test/helpers/services.js +21 -0
  211. package/lib/vendor/blamejs/test/integration/audit-actor-binding-pg.test.js +246 -0
  212. package/lib/vendor/blamejs/test/integration/audit-chain-external-db.test.js +517 -0
  213. package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +639 -0
  214. package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +832 -0
  215. package/lib/vendor/blamejs/test/integration/backup-restore-objectstore.test.js +453 -0
  216. package/lib/vendor/blamejs/test/integration/data-layer-cluster-mysql.test.js +649 -0
  217. package/lib/vendor/blamejs/test/integration/data-layer-cluster-pg.test.js +770 -0
  218. package/lib/vendor/blamejs/test/integration/data-layer-mysql-privacy.test.js +630 -0
  219. package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +610 -0
  220. package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +577 -0
  221. package/lib/vendor/blamejs/test/integration/data-layer-postgres.test.js +771 -0
  222. package/lib/vendor/blamejs/test/integration/db-layer-mysql.test.js +549 -0
  223. package/lib/vendor/blamejs/test/integration/db-layer-postgres.test.js +598 -0
  224. package/lib/vendor/blamejs/test/integration/distributed-scheduler-fencing-pg.test.js +602 -0
  225. package/lib/vendor/blamejs/test/integration/external-db-postgres.test.js +576 -0
  226. package/lib/vendor/blamejs/test/integration/framework-schema-mysql.test.js +353 -0
  227. package/lib/vendor/blamejs/test/integration/log-stream-cloudwatch.test.js +224 -0
  228. package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +142 -17
  229. package/lib/vendor/blamejs/test/integration/network-heartbeat.test.js +25 -10
  230. package/lib/vendor/blamejs/test/integration/object-store-azure.test.js +101 -0
  231. package/lib/vendor/blamejs/test/integration/object-store-gcs.test.js +239 -0
  232. package/lib/vendor/blamejs/test/integration/object-store-sigv4.test.js +35 -16
  233. package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +291 -0
  234. package/lib/vendor/blamejs/test/integration/pubsub.test.js +14 -0
  235. package/lib/vendor/blamejs/test/integration/queue-sqs.test.js +322 -0
  236. package/lib/vendor/blamejs/test/integration/redis-reconnect-toxiproxy.test.js +300 -0
  237. package/lib/vendor/blamejs/test/integration/sql-fts5-catalog-sqlite.test.js +154 -0
  238. package/lib/vendor/blamejs/test/integration/tls-classical-downgrade-audit.test.js +71 -0
  239. package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +175 -12
  240. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-exclusive-temp.test.js +216 -0
  241. package/lib/vendor/blamejs/test/layer-0-primitives/audit-checkpoint-false-rollback.test.js +203 -0
  242. package/lib/vendor/blamejs/test/layer-0-primitives/audit-query-self-log.test.js +126 -0
  243. package/lib/vendor/blamejs/test/layer-0-primitives/audit-safeemit-redacts-secrets.test.js +196 -0
  244. package/lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js +197 -0
  245. package/lib/vendor/blamejs/test/layer-0-primitives/audit-verifybundle-tamper.test.js +209 -0
  246. package/lib/vendor/blamejs/test/layer-0-primitives/azure-blob-key-encoding.test.js +121 -0
  247. package/lib/vendor/blamejs/test/layer-0-primitives/backup-residency-posture.test.js +168 -0
  248. package/lib/vendor/blamejs/test/layer-0-primitives/backup-scheduletest-drill.test.js +318 -0
  249. package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +233 -7
  250. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +1120 -14
  251. package/lib/vendor/blamejs/test/layer-0-primitives/compliance.test.js +229 -0
  252. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +24 -7
  253. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-dual-read-migrate.test.js +165 -0
  254. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-per-row-key.test.js +350 -0
  255. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +27 -9
  256. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-upgrade-dialect.test.js +76 -0
  257. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-interop-oracles.test.js +392 -0
  258. package/lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js +159 -0
  259. package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +180 -1
  260. package/lib/vendor/blamejs/test/layer-0-primitives/db-query-cross-schema.test.js +5 -2
  261. package/lib/vendor/blamejs/test/layer-0-primitives/db-query-sealed-field-in.test.js +101 -0
  262. package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +128 -0
  263. package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +38 -5
  264. package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-reconcile-emittable.test.js +127 -0
  265. package/lib/vendor/blamejs/test/layer-0-primitives/db-stream-and-payload-shape.test.js +267 -0
  266. package/lib/vendor/blamejs/test/layer-0-primitives/db-worm.test.js +150 -0
  267. package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +30 -0
  268. package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +46 -0
  269. package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +218 -0
  270. package/lib/vendor/blamejs/test/layer-0-primitives/erase-posture-vacuum.test.js +210 -0
  271. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +4 -1
  272. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +48 -2
  273. package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +237 -5
  274. package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +20 -9
  275. package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +193 -0
  276. package/lib/vendor/blamejs/test/layer-0-primitives/guard-csv.test.js +90 -0
  277. package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +85 -0
  278. package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +10 -6
  279. package/lib/vendor/blamejs/test/layer-0-primitives/inbox.test.js +15 -4
  280. package/lib/vendor/blamejs/test/layer-0-primitives/legal-hold.test.js +146 -0
  281. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +189 -0
  282. package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +3 -1
  283. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +123 -4
  284. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +207 -2
  285. package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +74 -0
  286. package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +43 -0
  287. package/lib/vendor/blamejs/test/layer-0-primitives/otel-export.test.js +133 -0
  288. package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +101 -0
  289. package/lib/vendor/blamejs/test/layer-0-primitives/outbox-inflight-reaper.test.js +136 -0
  290. package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +83 -0
  291. package/lib/vendor/blamejs/test/layer-0-primitives/passkey-real-vectors.test.js +429 -0
  292. package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +21 -11
  293. package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +40 -0
  294. package/lib/vendor/blamejs/test/layer-0-primitives/redact-dlp.test.js +83 -0
  295. package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +113 -0
  296. package/lib/vendor/blamejs/test/layer-0-primitives/retention-dryrun-no-vacuum.test.js +99 -0
  297. package/lib/vendor/blamejs/test/layer-0-primitives/router-use-path-scope.test.js +255 -0
  298. package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +309 -0
  299. package/lib/vendor/blamejs/test/layer-0-primitives/safe-xml.test.js +143 -0
  300. package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +287 -0
  301. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc-ecdsa-p1363.test.js +79 -0
  302. package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +50 -0
  303. package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +31 -4
  304. package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +45 -0
  305. package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +49 -0
  306. package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +595 -0
  307. package/lib/vendor/blamejs/test/layer-0-primitives/sse-backpressure.test.js +91 -0
  308. package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +69 -0
  309. package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +194 -2
  310. package/lib/vendor/blamejs/test/layer-0-primitives/websocket-extension-header.test.js +88 -0
  311. package/lib/vendor/blamejs/test/layer-0-primitives/worker-pool-recycle-race.test.js +66 -0
  312. package/lib/vendor/blamejs/test/layer-1-state/api-key.test.js +84 -0
  313. package/lib/vendor/blamejs/test/layer-5-integration/external-db-residency.test.js +638 -0
  314. package/lib/vendor/blamejs/test/layer-5-integration/guard-host-integration.test.js +21 -0
  315. package/lib/vendor/blamejs/test/smoke.js +79 -21
  316. package/package.json +1 -1
  317. package/lib/vendor/blamejs/release-notes/v0.14.0.json +0 -43
  318. package/lib/vendor/blamejs/release-notes/v0.14.1.json +0 -60
  319. package/lib/vendor/blamejs/release-notes/v0.14.10.json +0 -54
  320. package/lib/vendor/blamejs/release-notes/v0.14.11.json +0 -72
  321. package/lib/vendor/blamejs/release-notes/v0.14.12.json +0 -95
  322. package/lib/vendor/blamejs/release-notes/v0.14.13.json +0 -52
  323. package/lib/vendor/blamejs/release-notes/v0.14.14.json +0 -31
  324. package/lib/vendor/blamejs/release-notes/v0.14.16.json +0 -45
  325. package/lib/vendor/blamejs/release-notes/v0.14.17.json +0 -57
  326. package/lib/vendor/blamejs/release-notes/v0.14.18.json +0 -127
  327. package/lib/vendor/blamejs/release-notes/v0.14.19.json +0 -61
  328. package/lib/vendor/blamejs/release-notes/v0.14.2.json +0 -18
  329. package/lib/vendor/blamejs/release-notes/v0.14.20.json +0 -73
  330. package/lib/vendor/blamejs/release-notes/v0.14.21.json +0 -98
  331. package/lib/vendor/blamejs/release-notes/v0.14.22.json +0 -91
  332. package/lib/vendor/blamejs/release-notes/v0.14.3.json +0 -18
  333. package/lib/vendor/blamejs/release-notes/v0.14.4.json +0 -18
  334. package/lib/vendor/blamejs/release-notes/v0.14.5.json +0 -18
  335. package/lib/vendor/blamejs/release-notes/v0.14.6.json +0 -60
  336. package/lib/vendor/blamejs/release-notes/v0.14.7.json +0 -77
  337. package/lib/vendor/blamejs/release-notes/v0.14.8.json +0 -27
  338. package/lib/vendor/blamejs/release-notes/v0.14.9.json +0 -40
@@ -0,0 +1,3842 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.sql
4
+ * @nav Validation
5
+ * @title SQL Builder
6
+ * @order 90
7
+ * @featured true
8
+ *
9
+ * @intro
10
+ * Chainable SQL builder that makes hand-rolled SQL impossible. Every
11
+ * table and column name is quoted by construction through
12
+ * `b.safeSql`; every value is a bound `?` placeholder, never
13
+ * string-interpolated. The builder emits BARE logical table names and
14
+ * `?` placeholders - `b.clusterStorage` rewrites bare framework tables
15
+ * to their cluster-prefixed names and translates `?` to `$N` for
16
+ * Postgres at execute time - so one query text runs unchanged against
17
+ * the local SQLite single-node backend and the operator-supplied
18
+ * external Postgres / MySQL in cluster mode.
19
+ *
20
+ * The terminal call is `.toSql()` returning `{ sql, params }`. Pass
21
+ * that straight to `b.clusterStorage.execute(sql, params)`. The
22
+ * builder never touches the database itself - it is a pure SQL-string
23
+ * composer, which keeps it free of the residency / sealed-column
24
+ * write-path concerns that `db.from(...)` (the executing query
25
+ * builder, `lib/db-query.js`) owns.
26
+ *
27
+ * Only `upsert` emits dialect-final syntax (Postgres / SQLite
28
+ * `ON CONFLICT ... DO UPDATE`, MySQL `ON DUPLICATE KEY UPDATE`); every
29
+ * other verb stays `?`-placeholder + double-quote and defers the
30
+ * dialect rewrite to `b.clusterStorage`. Joins, common-table
31
+ * expressions, scalar and `IN`/`EXISTS` subqueries, grouping,
32
+ * aggregates, and `RETURNING` are all composable. DDL builders
33
+ * (`createTable` / `createIndex` / `alterTable` / `dropTable`) reuse
34
+ * the framework's own type map so operator app-schema tables get the
35
+ * same quote-by-construction guarantee the framework tables get.
36
+ *
37
+ * Safety defaults are not opt-in: `update` and `delete` THROW without a
38
+ * `where()` unless `allowNoWhere` is set; a column-membership gate
39
+ * refuses unknown columns; `LIKE` auto-escapes `%` / `_` / `\` and
40
+ * emits the matching `ESCAPE`; raw fragments pass through `b.guardSql`
41
+ * (strict by default on the request path) plus the placeholder-count
42
+ * and embedded-literal scanners.
43
+ *
44
+ * @card
45
+ * Chainable SQL builder - every identifier quoted by construction, every value a bound placeholder, dialect-aware upsert.
46
+ */
47
+
48
+ var safeSql = require("./safe-sql");
49
+ var frameworkSchema = require("./framework-schema");
50
+ var safeJson = require("./safe-json");
51
+ var safeJsonPath = require("./safe-jsonpath");
52
+ var lazyRequire = require("./lazy-require");
53
+ var C = require("./constants");
54
+ var { FrameworkError } = require("./framework-error");
55
+
56
+ // Output-validation bounds (enforced by _assertEmittable on every build).
57
+ // Two scopes: statement-level (the whole emitted text + total bind count)
58
+ // and column-level (each individual bound value).
59
+ //
60
+ // MAX_SQL_BYTES: a runaway/DoS ceiling on the emitted statement text - a
61
+ // build this large is a bug (an unbatched bulk insert, a pathological
62
+ // IN-list), never a legitimate single statement. MAX_BIND_PARAMS: the
63
+ // wire-protocol bind-parameter ceiling - Postgres + MySQL cap a statement
64
+ // at 65535 parameters (exceeding it is a hard driver error), and SQLite's
65
+ // default SQLITE_MAX_VARIABLE_NUMBER is 32766 since 3.32; catching it at
66
+ // build surfaces a clear builder error instead of a cryptic driver crash.
67
+ //
68
+ // MAX_PARAM_BYTES: the per-value (column-level) ceiling. A single bound
69
+ // value can be pathologically large WITHOUT tripping MAX_SQL_BYTES, because
70
+ // bound values ride the wire separately - they are not interpolated into
71
+ // the statement text. 64 MiB is MySQL's default max_allowed_packet, the
72
+ // tightest per-value wire boundary across the supported drivers; a value
73
+ // larger than this is a buffer-overflow-class mistake (an unintended whole-
74
+ // file / whole-buffer bind), never a legitimate single column.
75
+ var MAX_SQL_BYTES = C.BYTES.mib(4);
76
+ var MAX_BIND_PARAMS = 65535;
77
+ var MAX_PARAM_BYTES = C.BYTES.mib(64);
78
+
79
+ // b.guardSql is the residual-raw-surface guard (whereRaw / setRaw /
80
+ // having-raw / join-raw / on-raw). It is lazy-required so b.sql does not
81
+ // hard-depend on the guard at module load (the guard module composes
82
+ // gate-contract + db-query helpers and is loaded on first raw use), and
83
+ // so a circular load between the two never wedges boot. The guard is
84
+ // applied by DEFAULT on every raw fragment - strict on the request path
85
+ // - never behind a config flag (security defaults are wired in, not
86
+ // opt-in). Operators with a deliberately benign single-statement read
87
+ // fragment relax via `{ guardProfile: "balanced" }`; the structurally
88
+ // unambiguous refusals (stacked statements, invalid encoding) never
89
+ // relax regardless of profile.
90
+ var guardSql = lazyRequire(function () { return require("./guard-sql"); });
91
+
92
+ /**
93
+ * @primitive b.sql.SqlBuilderError
94
+ * @signature b.sql.SqlBuilderError
95
+ * @since 0.14.29
96
+ * @status stable
97
+ * @related b.safeSql.SafeSqlError, b.sql.select, b.sql.upsert
98
+ *
99
+ * Error thrown by every `b.sql` builder on a bad call shape - an unknown
100
+ * dialect, an invalid identifier, an unconditional `update`/`delete`, a
101
+ * placeholder-count mismatch, an empty value set, a conflicting upsert
102
+ * action, and so on. Extends `FrameworkError` and is always permanent:
103
+ * these are programming / config errors caught at SQL-composition time,
104
+ * well before the query reaches a driver, so retrying never makes them
105
+ * valid. The throw IS the security signal.
106
+ *
107
+ * Carries a stable `.code` with a `sql-builder/` prefix
108
+ * (`sql-builder/bad-dialect`, `sql-builder/no-where`,
109
+ * `sql-builder/placeholder-mismatch`, `sql-builder/empty-values`,
110
+ * `sql-builder/conflict-action`, `sql-builder/unknown-column`, ...) - the
111
+ * slash style mirrors `SafeSqlError`'s codes and stays distinct from the
112
+ * dot-style codes `b.guardSql` raises.
113
+ *
114
+ * @example
115
+ * var b = require("@blamejs/core");
116
+ * try {
117
+ * b.sql.update("users").set({ active: false }).toSql();
118
+ * } catch (e) {
119
+ * e instanceof b.sql.SqlBuilderError; // -> true
120
+ * e.code; // -> "sql-builder/no-where"
121
+ * }
122
+ */
123
+ // Mirrors the in-file error-class convention used by sibling composition
124
+ // modules that subclass FrameworkError directly (safe-sql.js
125
+ // SafeSqlError, cluster-storage.js ClusterStorageError) rather than
126
+ // routing through framework-error.defineClass. An integrator who would
127
+ // rather register it centrally adds
128
+ // `defineClass("SqlBuilderError", { alwaysPermanent: true })` to
129
+ // framework-error.js and re-points this require; the public shape
130
+ // (name / code / permanent / isSqlBuilderError) is identical either way.
131
+ class SqlBuilderError extends FrameworkError {
132
+ constructor(message, code) {
133
+ super(message);
134
+ this.name = "SqlBuilderError";
135
+ this.code = code || "sql-builder/invalid";
136
+ this.permanent = true;
137
+ this.isSqlBuilderError = true;
138
+ }
139
+ }
140
+
141
+ function _err(message, code) {
142
+ return new SqlBuilderError(message, code);
143
+ }
144
+
145
+ // ---- Dialects -------------------------------------------------------
146
+
147
+ var DIALECTS = Object.freeze({ postgres: true, sqlite: true, mysql: true });
148
+
149
+ function _normDialect(dialect) {
150
+ if (dialect === undefined || dialect === null) return "sqlite";
151
+ if (typeof dialect !== "string" || DIALECTS[dialect] !== true) {
152
+ throw _err("dialect must be one of postgres | sqlite | mysql (got " +
153
+ JSON.stringify(dialect) + ")", "sql-builder/bad-dialect");
154
+ }
155
+ return dialect;
156
+ }
157
+
158
+ // MySQL quotes identifiers with backticks; Postgres + SQLite share the
159
+ // SQL-standard double-quote. The quoting below agrees with the framework
160
+ // DDL builder (framework-schema.js), which double-quotes on both backend
161
+ // dialects for the same casing-preservation reason.
162
+ //
163
+ // Validate then wrap an identifier in dialect quotes. The builder accepts
164
+ // reserved-word identifiers BY DESIGN: quoting a name is exactly what
165
+ // makes `from` / `select` / `count` / `key` usable as a real column or
166
+ // table, which is the framework's stated rationale for quote-by-
167
+ // construction (framework-schema.js DDL builder makes the same point).
168
+ // Every identifier the builder emits is quoted through the framework's
169
+ // single identifier primitive - b.safeSql.quoteIdentifier - with
170
+ // allowReserved on: quoting is exactly what makes a SQL-keyword column
171
+ // (e.g. `from`) safe in identifier position, and the builder admits
172
+ // reserved names by design. quoteIdentifier still enforces shape /
173
+ // length / null-byte / sqlite_-prefix rules, so nothing is weakened and
174
+ // the builder composes the primitive rather than reinventing quoting.
175
+ function _quoteId(name, dialect) {
176
+ return safeSql.quoteIdentifier(name, dialect, { allowReserved: true });
177
+ }
178
+
179
+ // ---- DDL logical-type map -------------------------------------------
180
+ //
181
+ // The framework's own DDL builder (framework-schema.js `_types`) is the
182
+ // single source of truth for the two column types that diverge across
183
+ // dialects - the integer and binary tokens. b.sql consumes that map for
184
+ // operator app-schema parity rather than forking it: postgres BIGINT /
185
+ // BYTEA, sqlite INTEGER / BLOB. _types covers postgres + sqlite only
186
+ // (the framework's two backend dialects); MySQL is a b.sql-only DDL
187
+ // target, so its divergent tokens are mapped here. JSON diverges three
188
+ // ways (postgres JSONB / mysql JSON / sqlite TEXT) and is handled in
189
+ // _ddlType; the remaining tokens (TEXT / BOOLEAN / REAL / NUMERIC /
190
+ // TIMESTAMP) resolve uniformly. If framework-schema later exports `_types`, this
191
+ // reads it directly; until then the postgres/sqlite INT/BLOB values are
192
+ // kept byte-identical to framework-schema._types so there is exactly one
193
+ // definition of each token in the shipped tree.
194
+ var _schemaTypes = (typeof frameworkSchema._types === "function")
195
+ ? frameworkSchema._types
196
+ : function (dialect) {
197
+ if (dialect === "postgres") return { INT: "BIGINT", BLOB: "BYTEA" };
198
+ if (dialect === "sqlite") return { INT: "INTEGER", BLOB: "BLOB" };
199
+ throw _err("framework type map has no entry for dialect '" + dialect + "'",
200
+ "sql-builder/bad-dialect");
201
+ };
202
+
203
+ // Logical type vocabulary -> dialect-final SQL type token. INT/BLOB
204
+ // delegate to the framework map (or its MySQL extension); the rest are
205
+ // dialect-invariant. Callers pass a logical name (case-insensitive) OR a
206
+ // verbatim type string - a string the vocabulary does not recognise is
207
+ // emitted as-is so operators can declare a dialect-specific type the map
208
+ // does not enumerate (it is still placed after a quoted column name, so
209
+ // no identifier injection is possible).
210
+ function _ddlType(logical, dialect) {
211
+ if (typeof logical !== "string" || logical.length === 0) {
212
+ throw _err("column type must be a non-empty string", "sql-builder/bad-type");
213
+ }
214
+ var key = logical.toUpperCase();
215
+ var divergent;
216
+ if (key === "INT" || key === "INTEGER" || key === "BIGINT") {
217
+ divergent = (dialect === "mysql") ? { INT: "BIGINT" } : _schemaTypes(dialect);
218
+ return divergent.INT;
219
+ }
220
+ if (key === "BLOB" || key === "BYTEA" || key === "BINARY") {
221
+ divergent = (dialect === "mysql") ? { BLOB: "LONGBLOB" } : _schemaTypes(dialect);
222
+ return divergent.BLOB;
223
+ }
224
+ if (key === "TEXT" || key === "STRING") return "TEXT";
225
+ if (key === "BOOLEAN" || key === "BOOL") return "BOOLEAN";
226
+ if (key === "REAL" || key === "FLOAT" || key === "DOUBLE") return "REAL";
227
+ if (key === "NUMERIC" || key === "DECIMAL") return "NUMERIC";
228
+ if (key === "TIMESTAMP") return "TIMESTAMP";
229
+ if (key === "JSON") {
230
+ return dialect === "postgres" ? "JSONB" : (dialect === "mysql" ? "JSON" : "TEXT");
231
+ }
232
+ // Unrecognised: a verbatim dialect-specific type (VARCHAR(255), GEOGRAPHY,
233
+ // NUMERIC(10,2), DOUBLE PRECISION, TIMESTAMP WITH TIME ZONE, MySQL
234
+ // ENUM('a','b') / SET(...), ...). It follows a quoted identifier so it is in
235
+ // type position, never identifier position. Injection safety for the type
236
+ // token is enforced at the statement level: createTable / alterTable route
237
+ // the finished DDL through _assertCatalogEmittable, whose quote-aware
238
+ // single-statement scan refuses a top-level ';', a comment marker, an
239
+ // unbalanced quote, an unbalanced paren, and a NUL - while CORRECTLY allowing
240
+ // those same characters when they sit inside a balanced quoted label (e.g.
241
+ // ENUM('needs;review')). A non-quote-aware pre-scan here would over-reject
242
+ // such valid labels, so the one quote-aware gate is the right place to check.
243
+ return logical;
244
+ }
245
+
246
+ // ---- Operators ------------------------------------------------------
247
+ //
248
+ // Shared with the executing query builder (lib/db-query.js ALLOWED_OPS):
249
+ // comparison, IS/IS NOT, LIKE/NOT LIKE, IN/NOT IN, BETWEEN, and the
250
+ // Postgres JSONB containment + key-existence operators. Operator-supplied
251
+ // op strings are validated against this allowlist so no operator token
252
+ // reaches the SQL except one of these exact strings.
253
+ var ALLOWED_OPS = Object.freeze({
254
+ "=": true, "!=": true, "<>": true, "<": true, "<=": true, ">": true, ">=": true,
255
+ "IS": true, "IS NOT": true, "LIKE": true, "NOT LIKE": true,
256
+ "IN": true, "NOT IN": true, "BETWEEN": true,
257
+ "@>": true, "?": true, "?|": true, "?&": true,
258
+ // sqlite FTS5 full-text match - `<fts-table-or-column> MATCH ?`. The
259
+ // operand (the FTS5 query expression) ALWAYS binds as a single `?`;
260
+ // build-gated to the sqlite dialect in _cmp (no Postgres / MySQL form).
261
+ "MATCH": true,
262
+ });
263
+
264
+ var JOIN_KINDS = Object.freeze({
265
+ INNER: "INNER JOIN", LEFT: "LEFT JOIN", RIGHT: "RIGHT JOIN",
266
+ FULL: "FULL JOIN", CROSS: "CROSS JOIN",
267
+ });
268
+
269
+ // ---- Identifier helpers ---------------------------------------------
270
+
271
+ function _validateColumn(col) {
272
+ if (typeof col !== "string" || col.length === 0) {
273
+ throw _err("column name must be a non-empty string", "sql-builder/bad-column");
274
+ }
275
+ // Routes through safeSql so the shape / length / reserved-word /
276
+ // null-byte rules are the framework's single identifier policy.
277
+ safeSql.validateIdentifier(col, { allowReserved: true });
278
+ return col;
279
+ }
280
+
281
+ // ---- Table reference ------------------------------------------------
282
+ //
283
+ // Bare DEFAULT logical names stay UNQUOTED so clusterStorage.resolveTables
284
+ // can rewrite them to the cluster-prefixed form (a quoted name would not
285
+ // match its bare-identifier scan). A custom prefix or a schema qualifier
286
+ // is validated + quoted at build time - an invalid identifier throws
287
+ // here, at config time, where the operator catches the typo at boot.
288
+ // Two-segment qualified names (schema.table) are the maximum.
289
+ function _normTableRef(name, opts) {
290
+ opts = opts || {};
291
+ if (name instanceof TableRef) return name;
292
+ if (typeof name !== "string" || name.length === 0) {
293
+ throw _err("table name must be a non-empty string", "sql-builder/bad-table");
294
+ }
295
+ var schema = opts.schema || null;
296
+ var table = name;
297
+ if (schema === null && name.indexOf(".") !== -1) {
298
+ var dotParts = name.split(".");
299
+ if (dotParts.length !== 2 || dotParts[0].length === 0 || dotParts[1].length === 0) {
300
+ throw _err("schema-qualified table must be exactly 'schema.table' (got '" +
301
+ name + "')", "sql-builder/bad-table");
302
+ }
303
+ schema = dotParts[0];
304
+ table = dotParts[1];
305
+ }
306
+ return new TableRef(table, {
307
+ schema: schema,
308
+ prefix: opts.prefix !== undefined ? opts.prefix : (opts.tablePrefix || null),
309
+ alias: opts.alias || null,
310
+ quoteName: opts.quoteName === true,
311
+ });
312
+ }
313
+
314
+ /**
315
+ * @primitive b.sql.table
316
+ * @signature b.sql.table(name, opts?)
317
+ * @since 0.14.29
318
+ * @status stable
319
+ * @related b.sql.select, b.clusterStorage.resolveTables
320
+ *
321
+ * Build a table reference. A bare default logical name
322
+ * (`b.sql.table("audit_log")`) stays UNQUOTED in the emitted SQL so
323
+ * `b.clusterStorage` can rewrite it to the cluster-prefixed name. A
324
+ * schema qualifier (`{ schema: "public" }` or the dotted form
325
+ * `"public.users"`) or an operator app-table `prefix` is validated and
326
+ * quoted at build time - a bad identifier throws immediately. The
327
+ * `prefix` here is operator app-table namespacing, distinct from the
328
+ * framework's internal `_blamejs_` prefix; it is prepended to the table
329
+ * name and the whole result is quoted as one identifier. At most two
330
+ * segments (schema.table). An `alias` is quoted and appended for joins.
331
+ *
332
+ * @opts
333
+ * schema: string, // schema qualifier, quoted at build time
334
+ * prefix: string, // operator app-table namespace, prepended then quoted
335
+ * alias: string, // table alias, used to disambiguate joins
336
+ *
337
+ * @example
338
+ * var b = require("@blamejs/core");
339
+ * b.sql.table("audit_log").toString("sqlite");
340
+ * // -> "audit_log" (bare default - clusterStorage rewrites)
341
+ *
342
+ * b.sql.table("users", { schema: "public" }).toString("postgres");
343
+ * // -> '"public"."users"'
344
+ *
345
+ * b.sql.table("orders", { prefix: "shopX_" }).toString("sqlite");
346
+ * // -> '"shopX_orders"'
347
+ */
348
+ function table(name, opts) {
349
+ return _normTableRef(name, opts);
350
+ }
351
+
352
+ class TableRef {
353
+ constructor(name, opts) {
354
+ opts = opts || {};
355
+ if (typeof name !== "string" || name.length === 0) {
356
+ throw _err("table name must be a non-empty string", "sql-builder/bad-table");
357
+ }
358
+ this._schema = opts.schema || null;
359
+ this._prefix = opts.prefix || null;
360
+ this._alias = opts.alias || null;
361
+ // quoteName forces a bare default name to be quoted. The bare-default
362
+ // name normally stays UNQUOTED so b.clusterStorage's resolveTables can
363
+ // rewrite it to the cluster-prefixed form (a quoted name would not
364
+ // match its bare-identifier scan). A LOCAL-only consumer that does NO
365
+ // cluster rewrite (the executing query builder's single-node sqlite
366
+ // path) opts into quoting so a reserved-word / case-sensitive table
367
+ // name still emits a valid `"name"` identifier - quoting is exactly
368
+ // what makes a SQL-keyword table name safe in identifier position.
369
+ this._quoteName = opts.quoteName === true;
370
+ // A custom prefix is validated as an identifier and prepended; the
371
+ // combined name is then a single quoted identifier. The bare default
372
+ // (no prefix, no schema) stays unquoted for clusterStorage.
373
+ if (this._prefix !== null) {
374
+ _validateColumn(this._prefix);
375
+ this._name = this._prefix + name;
376
+ this._bare = false;
377
+ } else {
378
+ this._name = name;
379
+ this._bare = this._schema === null && !this._quoteName;
380
+ }
381
+ if (this._schema !== null) safeSql.validateIdentifier(this._schema, { allowReserved: true });
382
+ if (this._alias !== null) safeSql.validateIdentifier(this._alias, { allowReserved: true });
383
+ // Validate the base name shape even for the bare default - an
384
+ // attacker-influenced logical name still must be a real identifier.
385
+ safeSql.validateIdentifier(this._name, { allowReserved: true });
386
+ }
387
+
388
+ // The reference as it appears in FROM / INTO / UPDATE / JOIN. Bare
389
+ // default names stay unquoted (clusterStorage rewrite target); custom
390
+ // / schema-qualified names are quoted. Alias is never part of the
391
+ // resolution target - added separately where an alias is legal.
392
+ ref(dialect) {
393
+ if (this._schema !== null) {
394
+ return _quoteId(this._schema, dialect) + "." + _quoteId(this._name, dialect);
395
+ }
396
+ if (this._bare) return this._name;
397
+ return _quoteId(this._name, dialect);
398
+ }
399
+
400
+ // ref() plus a quoted alias, for FROM / JOIN where an alias is legal.
401
+ refWithAlias(dialect) {
402
+ var base = this.ref(dialect);
403
+ return this._alias !== null ? base + " " + _quoteId(this._alias, dialect) : base;
404
+ }
405
+
406
+ // The identifier columns are qualified against - the alias when set,
407
+ // else the resolution target.
408
+ qualifier(dialect) {
409
+ if (this._alias !== null) return _quoteId(this._alias, dialect);
410
+ return this.ref(dialect);
411
+ }
412
+
413
+ toString(dialect) {
414
+ return this.refWithAlias(_normDialect(dialect));
415
+ }
416
+ }
417
+
418
+ // ---- Allowlisted SQL function literals ------------------------------
419
+ //
420
+ // A small set of nullary, side-effect-free SQL function tokens an operator
421
+ // commonly wants in INSERT VALUES / SET RHS (a server-side timestamp) but
422
+ // which CANNOT be a bound `?` parameter (a `?` binds a value; these emit a
423
+ // keyword the engine evaluates). Rather than open a raw-fragment hole on
424
+ // the values path, b.sql.fn(name) wraps EXACTLY one of these allowlisted
425
+ // tokens - an unknown name throws, so no arbitrary expression reaches a
426
+ // VALUES / SET position. The token is dialect-checked at emit (NOW() is
427
+ // Postgres / MySQL; CURRENT_TIMESTAMP is portable). It is NOT a value -
428
+ // it consumes no `?` and contributes no param.
429
+ var SQL_FUNCTIONS = Object.freeze({
430
+ "NOW": { sql: "NOW()", dialects: { postgres: true, mysql: true } },
431
+ "CURRENT_TIMESTAMP": { sql: "CURRENT_TIMESTAMP", dialects: { postgres: true, sqlite: true, mysql: true } },
432
+ "CURRENT_DATE": { sql: "CURRENT_DATE", dialects: { postgres: true, sqlite: true, mysql: true } },
433
+ "CURRENT_TIME": { sql: "CURRENT_TIME", dialects: { postgres: true, sqlite: true, mysql: true } },
434
+ });
435
+
436
+ class SqlFunction {
437
+ constructor(name) {
438
+ if (typeof name !== "string") {
439
+ throw _err("b.sql.fn(name): name must be a string", "sql-builder/bad-fn");
440
+ }
441
+ var key = name.toUpperCase();
442
+ if (SQL_FUNCTIONS[key] === undefined) {
443
+ throw _err("b.sql.fn(name): '" + name + "' is not an allowlisted SQL function " +
444
+ "(NOW / CURRENT_TIMESTAMP / CURRENT_DATE / CURRENT_TIME); a bound value uses a ? " +
445
+ "placeholder, an arbitrary expression uses a guarded raw fragment", "sql-builder/bad-fn");
446
+ }
447
+ this._key = key;
448
+ this.isSqlFunction = true;
449
+ }
450
+ // The SQL token for the builder's dialect; throws when the function is
451
+ // not available on that backend (NOW() on sqlite has no portable form).
452
+ toSqlToken(dialect) {
453
+ var def = SQL_FUNCTIONS[this._key];
454
+ if (def.dialects[dialect] !== true) {
455
+ throw _err("b.sql.fn('" + this._key + "') is not available on " + dialect +
456
+ " (use CURRENT_TIMESTAMP for a portable server timestamp)", "sql-builder/fn-unsupported");
457
+ }
458
+ return def.sql;
459
+ }
460
+ }
461
+
462
+ /**
463
+ * @primitive b.sql.fn
464
+ * @signature b.sql.fn(name)
465
+ * @since 0.15.0
466
+ * @status stable
467
+ * @related b.sql.insert, b.sql.update, b.sql.cast
468
+ *
469
+ * Wrap an allowlisted, nullary, side-effect-free SQL function token for use
470
+ * as an INSERT `values()` / UPDATE `set()` right-hand side - a value
471
+ * position that must emit a keyword the engine evaluates server-side (a
472
+ * `NOW()` timestamp) rather than a bound `?` parameter. The allowlist is
473
+ * exactly `NOW` / `CURRENT_TIMESTAMP` / `CURRENT_DATE` / `CURRENT_TIME`; an
474
+ * unknown name throws, so no arbitrary expression reaches a VALUES / SET
475
+ * position. The token is dialect-checked at emit (`NOW()` is Postgres /
476
+ * MySQL; `CURRENT_TIMESTAMP` is portable). The wrapped function consumes
477
+ * no `?` and contributes no param.
478
+ *
479
+ * @example
480
+ * var b = require("@blamejs/core");
481
+ * b.sql.insert("events")
482
+ * .values({ topic: "x", at: b.sql.fn("CURRENT_TIMESTAMP") })
483
+ * .toSql();
484
+ * // -> { sql: 'INSERT INTO events ("topic", "at") VALUES (?, CURRENT_TIMESTAMP)',
485
+ * // params: ["x"] }
486
+ */
487
+ function fn(name) { return new SqlFunction(name); }
488
+
489
+ // ---- Allowlisted column casts ---------------------------------------
490
+ //
491
+ // A `col::type` / `?::type` cast applies an allowlisted target type to a
492
+ // quoted column or a bound `?` placeholder. The cast TYPE is matched
493
+ // against a fixed vocabulary (no operator-supplied type token reaches the
494
+ // SQL), and the LHS is either a quoted identifier or a single bound
495
+ // placeholder - never raw text. Postgres `::` is the canonical form; the
496
+ // same vocabulary maps to a portable form where one exists (jsonb -> json
497
+ // on a non-Postgres backend that has it; interval has no portable cast and
498
+ // is Postgres-only).
499
+ var CAST_TYPES = Object.freeze({
500
+ "jsonb": { postgres: "jsonb", mysql: "json", sqlite: null },
501
+ "json": { postgres: "json", mysql: "json", sqlite: null },
502
+ "interval": { postgres: "interval", mysql: null, sqlite: null },
503
+ "uuid": { postgres: "uuid", mysql: null, sqlite: null },
504
+ "text": { postgres: "text", mysql: "char", sqlite: "text" },
505
+ "int": { postgres: "integer", mysql: "signed", sqlite: "integer" },
506
+ "bigint": { postgres: "bigint", mysql: "signed", sqlite: "integer" },
507
+ "timestamptz": { postgres: "timestamptz", mysql: null, sqlite: null },
508
+ "boolean": { postgres: "boolean", mysql: null, sqlite: null },
509
+ });
510
+
511
+ function _castType(type, dialect) {
512
+ if (typeof type !== "string" || type.length === 0) {
513
+ throw _err("cast type must be a non-empty string", "sql-builder/bad-cast");
514
+ }
515
+ var key = type.toLowerCase();
516
+ if (CAST_TYPES[key] === undefined) {
517
+ throw _err("cast type '" + type + "' is not on the allowlist (jsonb / json / " +
518
+ "interval / uuid / text / int / bigint / timestamptz / boolean)", "sql-builder/bad-cast");
519
+ }
520
+ var target = CAST_TYPES[key][dialect];
521
+ if (target === null || target === undefined) {
522
+ throw _err("cast to '" + type + "' has no portable form on " + dialect +
523
+ " (it is Postgres-only)", "sql-builder/cast-unsupported");
524
+ }
525
+ return target;
526
+ }
527
+
528
+ // Render the dialect-correct cast suffix for a bound `?` placeholder or a
529
+ // quoted column. Postgres uses the `::type` operator; MySQL has no `::`,
530
+ // so a cast there wraps in CAST(<lhs> AS <type>). SQLite is weakly typed -
531
+ // the small set of casts portable to sqlite (text / int) emit
532
+ // CAST(<lhs> AS <type>) too; a sqlite-unsupported cast already threw in
533
+ // _castType.
534
+ function _renderCast(lhs, type, dialect) {
535
+ var target = _castType(type, dialect);
536
+ if (dialect === "postgres") return lhs + "::" + target;
537
+ return "CAST(" + lhs + " AS " + target + ")";
538
+ }
539
+
540
+ // A value wrapped for binding-with-cast: the value binds as a single `?`
541
+ // and the placeholder carries the dialect cast (`?::jsonb` on Postgres).
542
+ class CastValue {
543
+ constructor(value, type) {
544
+ // Eager allowlist-membership check so a typo'd type token fails at the
545
+ // call site (the entry-point THROW tier), not deep inside a later
546
+ // toSql(). The dialect-portability check (interval / uuid are
547
+ // Postgres-only) stays at render time, where the target dialect is known.
548
+ if (typeof type !== "string" || type.length === 0) {
549
+ throw _err("cast type must be a non-empty string", "sql-builder/bad-cast");
550
+ }
551
+ if (CAST_TYPES[type.toLowerCase()] === undefined) {
552
+ throw _err("cast type '" + type + "' is not on the allowlist (jsonb / json / " +
553
+ "interval / uuid / text / int / bigint / timestamptz / boolean)", "sql-builder/bad-cast");
554
+ }
555
+ this.value = value;
556
+ this.type = type;
557
+ this.isCastValue = true;
558
+ }
559
+ }
560
+
561
+ /**
562
+ * @primitive b.sql.cast
563
+ * @signature b.sql.cast(value, type)
564
+ * @since 0.15.0
565
+ * @status stable
566
+ * @related b.sql.insert, b.sql.update, b.sql.fn
567
+ *
568
+ * Wrap a value so it binds as a single `?` placeholder carrying a
569
+ * dialect-correct cast - `?::jsonb` on Postgres, `CAST(? AS json)` on
570
+ * MySQL. The cast TYPE is matched against a fixed allowlist (`jsonb` /
571
+ * `json` / `interval` / `uuid` / `text` / `int` / `bigint` / `timestamptz`
572
+ * / `boolean`); an unknown type, or one with no portable form on the
573
+ * target dialect (`interval` / `uuid` are Postgres-only), throws at build.
574
+ * Use it for an INSERT `values()` / UPDATE `set()` cell that must coerce a
575
+ * bound string into a typed column (a JSON string into a `jsonb` column, a
576
+ * duration string into an `interval`).
577
+ *
578
+ * @example
579
+ * var b = require("@blamejs/core");
580
+ * b.sql.insert("docs", { dialect: "postgres" })
581
+ * .values({ id: 1, meta: b.sql.cast('{"a":1}', "jsonb") })
582
+ * .toSql();
583
+ * // -> { sql: 'INSERT INTO docs ("id", "meta") VALUES (?, ?::jsonb)',
584
+ * // params: [1, '{"a":1}'] }
585
+ */
586
+ function cast(value, type) { return new CastValue(value, type); }
587
+
588
+ // Render a single value cell for an INSERT VALUES / UPDATE SET RHS.
589
+ // Returns { sql, params } where sql is `?` for a bound value, `?::type`
590
+ // for a CastValue, or the dialect function token for a SqlFunction (no
591
+ // param). The single choke-point both insert and update value paths use,
592
+ // so the allowlisted-function / cast handling lives in one place.
593
+ function _renderValueCell(value, dialect) {
594
+ if (value instanceof SqlFunction) {
595
+ return { sql: value.toSqlToken(dialect), params: [] };
596
+ }
597
+ if (value instanceof CastValue) {
598
+ return { sql: _renderCast("?", value.type, dialect), params: [value.value] };
599
+ }
600
+ return { sql: "?", params: [value] };
601
+ }
602
+
603
+ // ---- Value-binding helpers ------------------------------------------
604
+ //
605
+ // Every value is pushed to a params array and represented in the SQL by
606
+ // a `?`. A b.sql builder used as a subquery contributes its placeholders
607
+ // in left-to-right order, so a params array concatenation is always
608
+ // correct as long as fragments are appended in emission order.
609
+
610
+ // A column reference qualified column expression. Accepts "col" or
611
+ // "alias.col" / "table.col" - both segments validated + quoted.
612
+ function _qualifiedColumn(expr, dialect) {
613
+ if (typeof expr !== "string" || expr.length === 0) {
614
+ throw _err("column expression must be a non-empty string", "sql-builder/bad-column");
615
+ }
616
+ if (expr.indexOf(".") !== -1) {
617
+ var parts = expr.split(".");
618
+ if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) {
619
+ throw _err("qualified column must be 'qualifier.column' (got '" + expr + "')",
620
+ "sql-builder/bad-column");
621
+ }
622
+ safeSql.validateIdentifier(parts[0], { allowReserved: true });
623
+ safeSql.validateIdentifier(parts[1], { allowReserved: true });
624
+ return _quoteId(parts[0], dialect) + "." + _quoteId(parts[1], dialect);
625
+ }
626
+ _validateColumn(expr);
627
+ return _quoteId(expr, dialect);
628
+ }
629
+
630
+ // LIKE auto-escape. Escapes %, _, and the escape char itself in an
631
+ // operator-supplied LIKE value so a stray % can't widen the match into a
632
+ // full-table disclosure. The escape char is `~`, NOT backslash: MySQL with
633
+ // the default sql_mode treats backslash as a string-literal escape, so
634
+ // `ESCAPE '\'` reads as an unterminated literal and parse-errors - `~` is
635
+ // parser-mode-independent across SQLite / Postgres / MySQL. Mirrors
636
+ // db-query.js's LIKE handling.
637
+ function _escapeLike(value) {
638
+ return String(value).replace(/[~%_]/g, "~$&");
639
+ }
640
+
641
+ // Compose a sub-builder into a parent statement. The builder quotes
642
+ // identifiers eagerly (at the columns() / where() call), so a sub built with
643
+ // a different dialect than the parent has already baked in the wrong quote
644
+ // char - splicing it would emit mixed quoting the wrong backend mis-reads
645
+ // (a default-sqlite sub's "id" inside a mysql parent, or a mysql sub's `id`
646
+ // inside a postgres parent). Refuse the mismatch loudly at build rather than
647
+ // ship a corrupt statement; the operator builds the sub with the matching
648
+ // { dialect } so the whole statement is one dialect. A sub left at the
649
+ // default (sqlite) composes cleanly into a default (sqlite) parent.
650
+ function _composeSub(subBuilder, parentDialect) {
651
+ if (subBuilder._dialect !== parentDialect) {
652
+ throw _err("sub-query dialect '" + subBuilder._dialect + "' does not match the " +
653
+ "parent statement's dialect '" + parentDialect + "' - build the composed " +
654
+ "sub-query with { dialect: '" + parentDialect + "' } so the whole statement " +
655
+ "is one dialect", "sql-builder/dialect-mismatch");
656
+ }
657
+ return subBuilder.toSql();
658
+ }
659
+
660
+ // The Postgres JSONB operators. Two shared dialect-design gates compose
661
+ // over this set so every emission site (the value-comparison _cmp path,
662
+ // scalar-subquery comparison, join-ON) enforces the same rule.
663
+ var JSONB_OPS = Object.freeze({ "@>": true, "?": true, "?|": true, "?&": true });
664
+
665
+ // Build-time refusal: a JSONB operator on a non-Postgres builder would emit
666
+ // jsonb_exists* / @> to a backend that has neither (downstream regression).
667
+ function _assertJsonbDialect(op, dialect) {
668
+ if (JSONB_OPS[op] === true && dialect !== "postgres") {
669
+ throw _err("the '" + op + "' JSONB operator is Postgres-only (no portable " +
670
+ "SQLite / MySQL equivalent); build this query with { dialect: 'postgres' }",
671
+ "sql-builder/jsonb-postgres-only");
672
+ }
673
+ }
674
+
675
+ // Build-time refusal: a JSONB operator in a position that has no
676
+ // jsonb_exists* rewrite (scalar-subquery comparison, join-ON) - the bare
677
+ // operator would splice in and the wrong backend mis-reads it (and a bare
678
+ // `?` collides with the placeholder marker even on Postgres).
679
+ function _refuseJsonbOp(op, position) {
680
+ if (JSONB_OPS[op] === true) {
681
+ throw _err("the '" + op + "' JSONB operator is not supported in " + position +
682
+ "; use where(col, '" + op + "', value) on a Postgres builder",
683
+ "sql-builder/jsonb-bad-position");
684
+ }
685
+ }
686
+
687
+ // ---- Condition tree (WHERE / HAVING / JOIN-ON) ----------------------
688
+ //
689
+ // A predicate group is an ordered list of leaves joined by AND / OR.
690
+ // Each leaf carries its own SQL fragment + params; nesting is a leaf
691
+ // whose fragment is a parenthesised sub-group. This is the structure
692
+ // every where/having/on clause and every whereGroup closure builds.
693
+
694
+ class Predicate {
695
+ constructor(owner, joinerDefault) {
696
+ this._owner = owner; // the builder, for the column gate
697
+ this._joiner = joinerDefault || "AND";
698
+ this._parts = []; // [{ joiner, sql, params }]
699
+ }
700
+
701
+ _gate(col) {
702
+ if (this._owner && typeof this._owner._assertColumnMember === "function") {
703
+ this._owner._assertColumnMember(col, "where");
704
+ }
705
+ }
706
+
707
+ _dialect() {
708
+ return this._owner ? this._owner._dialect : "sqlite";
709
+ }
710
+
711
+ _add(joiner, sql, params) {
712
+ this._parts.push({ joiner: joiner, sql: sql, params: params || [] });
713
+ return this;
714
+ }
715
+
716
+ // Core comparison. op validated against ALLOWED_OPS; the JSONB key-
717
+ // existence operators emit the jsonb_exists* function family (see below).
718
+ _cmp(joiner, col, op, value) {
719
+ if (ALLOWED_OPS[op] !== true) {
720
+ throw _err("invalid where operator '" + op + "'", "sql-builder/bad-operator");
721
+ }
722
+ this._gate(col);
723
+ var dialect = this._dialect();
724
+ var qc = _qualifiedColumn(col, dialect);
725
+
726
+ // Dialect-design gate: the JSONB containment (@>) + key-existence (?, ?|,
727
+ // ?&) operators are Postgres-only - the JSONB type, the jsonb_exists*
728
+ // functions, and @> containment have no portable SQLite / MySQL form.
729
+ // Emitting them for a non-Postgres backend silently regresses downstream
730
+ // (no such function / unknown operator at execute), so refuse at build.
731
+ _assertJsonbDialect(op, dialect);
732
+
733
+ // JSONB / JSON-path injection guard + placeholder-safe emission
734
+ // (inherited + hardened from the executing query builder). The Postgres
735
+ // JSONB containment (@>) and key-existence (?, ?|, ?&) operators take an
736
+ // operator-supplied operand the engine compares verbatim; route it
737
+ // through safeJsonPath so NUL / control / bidi / zero-width characters a
738
+ // driver might silently strip can't smuggle into the JSON-shape compare.
739
+ //
740
+ // The key-existence operators are emitted as the jsonb_exists* FUNCTION
741
+ // family, not the literal `?` / `?|` / `?&` operator: a literal `?`
742
+ // collides with the `?` bind-placeholder marker, so placeholderize would
743
+ // rewrite the operator itself to `$N` and corrupt the query. The operand
744
+ // always binds via a single `?` placeholder.
745
+ if (op === "@>") {
746
+ if (typeof value === "string") {
747
+ var parsedContainment;
748
+ try { parsedContainment = safeJson.parse(value); }
749
+ catch (e) {
750
+ throw _err("where '@>' value: invalid JSON string: " + ((e && e.message) || String(e)),
751
+ "sql-builder/bad-jsonb-value");
752
+ }
753
+ safeJsonPath.validateContainment(parsedContainment);
754
+ } else {
755
+ safeJsonPath.validateContainment(value);
756
+ // Bind the canonical-shape JSON so the driver sees the bytes we
757
+ // just walked end-to-end.
758
+ value = JSON.stringify(value);
759
+ }
760
+ } else if (op === "?") {
761
+ if (typeof value !== "string") {
762
+ throw _err("where '?' requires a string key (got " + (typeof value) + ")",
763
+ "sql-builder/bad-jsonb-key");
764
+ }
765
+ safeJsonPath.validateKey(value);
766
+ return this._add(joiner, "jsonb_exists(" + qc + ", ?)", [value]);
767
+ } else if (op === "?|" || op === "?&") {
768
+ if (!Array.isArray(value) || value.length === 0) {
769
+ throw _err("'" + op + "' requires a non-empty array of keys", "sql-builder/bad-jsonb-keys");
770
+ }
771
+ for (var ki = 0; ki < value.length; ki += 1) safeJsonPath.validateKey(value[ki]);
772
+ var jsonbExistsFn = op === "?|" ? "jsonb_exists_any" : "jsonb_exists_all";
773
+ return this._add(joiner, jsonbExistsFn + "(" + qc + ", ?)", [value.slice()]);
774
+ }
775
+
776
+ if (op === "IN" || op === "NOT IN") {
777
+ if (value instanceof Builder) {
778
+ var sub = _composeSub(value, this._dialect());
779
+ return this._add(joiner, qc + " " + op + " (" + sub.sql + ")", sub.params);
780
+ }
781
+ if (!Array.isArray(value) || value.length === 0) {
782
+ throw _err(op + " requires a non-empty array of values (or a subquery builder)",
783
+ "sql-builder/empty-in");
784
+ }
785
+ var holders = value.map(function () { return "?"; }).join(", ");
786
+ return this._add(joiner, qc + " " + op + " (" + holders + ")", value.slice());
787
+ }
788
+
789
+ if (op === "BETWEEN") {
790
+ if (!Array.isArray(value) || value.length !== 2) {
791
+ throw _err("BETWEEN requires a [low, high] pair", "sql-builder/bad-between");
792
+ }
793
+ return this._add(joiner, qc + " BETWEEN ? AND ?", [value[0], value[1]]);
794
+ }
795
+
796
+ if ((op === "IS" || op === "IS NOT") && value === null) {
797
+ // IS NULL / IS NOT NULL - no placeholder, no param.
798
+ return this._add(joiner, qc + " " + op + " NULL", []);
799
+ }
800
+
801
+ if ((op === "LIKE" || op === "NOT LIKE") && typeof value === "string") {
802
+ return this._add(joiner, qc + " " + op + " ? ESCAPE '~'", [_escapeLike(value)]);
803
+ }
804
+
805
+ // sqlite FTS5 `<fts-table-or-column> MATCH ?`. The full-text query
806
+ // expression ALWAYS binds as a single `?` - never interpolated - so an
807
+ // operator-supplied search term cannot reshape the statement. MATCH has
808
+ // no portable Postgres / MySQL form (Postgres uses to_tsvector @@
809
+ // to_tsquery; MySQL uses MATCH ... AGAINST with different grammar), so
810
+ // refuse a non-sqlite dialect at build, the config-time tier.
811
+ if (op === "MATCH") {
812
+ if (dialect !== "sqlite") {
813
+ throw _err("the MATCH full-text operator is sqlite-FTS5-only (no portable " +
814
+ "Postgres / MySQL form); build this query with { dialect: 'sqlite' }",
815
+ "sql-builder/match-sqlite-only");
816
+ }
817
+ if (typeof value !== "string" || value.length === 0) {
818
+ throw _err("MATCH requires a non-empty FTS5 query string", "sql-builder/bad-match");
819
+ }
820
+ return this._add(joiner, qc + " MATCH ?", [value]);
821
+ }
822
+
823
+ return this._add(joiner, qc + " " + op + " ?", [value]);
824
+ }
825
+
826
+ // where(field, val) / where(field, op, val) / where({ ... }).
827
+ where(fieldOrObj, op, value) {
828
+ if (fieldOrObj && typeof fieldOrObj === "object" && !(fieldOrObj instanceof Builder)) {
829
+ var self = this;
830
+ Object.keys(fieldOrObj).forEach(function (k) { self._cmp("AND", k, "=", fieldOrObj[k]); });
831
+ return this;
832
+ }
833
+ if (arguments.length === 2) return this._cmp("AND", fieldOrObj, "=", op);
834
+ return this._cmp("AND", fieldOrObj, op, value);
835
+ }
836
+ andWhere(fieldOrObj, op, value) { return this.where(fieldOrObj, op, value); }
837
+ orWhere(fieldOrObj, op, value) {
838
+ if (fieldOrObj && typeof fieldOrObj === "object" && !(fieldOrObj instanceof Builder)) {
839
+ var self = this;
840
+ Object.keys(fieldOrObj).forEach(function (k) { self._cmp("OR", k, "=", fieldOrObj[k]); });
841
+ return this;
842
+ }
843
+ if (arguments.length === 2) return this._cmp("OR", fieldOrObj, "=", op);
844
+ return this._cmp("OR", fieldOrObj, op, value);
845
+ }
846
+
847
+ whereOp(col, op, value) { return this._cmp("AND", col, op, value); }
848
+ orWhereOp(col, op, value) { return this._cmp("OR", col, op, value); }
849
+
850
+ // LIKE with caller-controlled match mode. The structured LIKE in _cmp
851
+ // escapes the WHOLE bound value (so a `%` in the value is a literal
852
+ // percent, never a wildcard) - correct for an exact compare but it
853
+ // can't express a "contains" / "starts-with" search where the wrapping
854
+ // `%` MUST stay a live wildcard while the user's own `%` / `_` stay
855
+ // escaped. This helper bridges that: it escapes the user term's
856
+ // metacharacters with `~` (matching _cmp's escape char) and then adds
857
+ // the LIVE wrapping wildcard per mode, binding the assembled pattern.
858
+ // It emits the SAME `col LIKE ? ESCAPE '~'` form _cmp does - the ESCAPE
859
+ // literal is builder-emitted (not a raw fragment), so it is not subject
860
+ // to the raw-fragment guard's embedded-literal refusal. Mode is
861
+ // "substring" (default, `%term%`), "prefix" (`term%`), or "exact"
862
+ // (`term`, equivalent to a structured LIKE but spelled as a search).
863
+ whereLike(col, term, mode) { return this._like("AND", col, term, mode); }
864
+ orWhereLike(col, term, mode) { return this._like("OR", col, term, mode); }
865
+ _like(joiner, col, term, mode) {
866
+ if (typeof term !== "string") {
867
+ throw _err("whereLike requires a string term (got " + (typeof term) + ")",
868
+ "sql-builder/bad-like-term");
869
+ }
870
+ this._gate(col);
871
+ var qc = _qualifiedColumn(col, this._dialect());
872
+ var escaped = _escapeLike(term);
873
+ var pattern;
874
+ var m = mode || "substring";
875
+ if (m === "exact") pattern = escaped;
876
+ else if (m === "prefix") pattern = escaped + "%";
877
+ else if (m === "substring") pattern = "%" + escaped + "%";
878
+ else throw _err("whereLike mode must be 'substring' | 'prefix' | 'exact'",
879
+ "sql-builder/bad-like-mode");
880
+ return this._add(joiner, qc + " LIKE ? ESCAPE '~'", [pattern]);
881
+ }
882
+
883
+ // sqlite FTS5 full-text MATCH. The target is the FTS virtual-table name
884
+ // (the recommended shape - `<fts> MATCH ?` inside an IN-subquery - since
885
+ // FTS5 MATCH binds to the virtual table, and an aliased / joined column
886
+ // ref is parsed as an ordinary column and fails) or a single FTS column.
887
+ // The query expression binds as one `?`. sqlite-only (enforced in _cmp);
888
+ // the column gate is bypassed because the target is a table identifier,
889
+ // not a member of the builder's declared column set.
890
+ whereMatch(target, expr) {
891
+ if (this._dialect() !== "sqlite") {
892
+ throw _err("whereMatch (FTS5 MATCH) is sqlite-only; build with { dialect: 'sqlite' }",
893
+ "sql-builder/match-sqlite-only");
894
+ }
895
+ if (typeof expr !== "string" || expr.length === 0) {
896
+ throw _err("whereMatch requires a non-empty FTS5 query string", "sql-builder/bad-match");
897
+ }
898
+ return this._add("AND", _qualifiedColumn(target, "sqlite") + " MATCH ?", [expr]);
899
+ }
900
+ orWhereMatch(target, expr) {
901
+ if (this._dialect() !== "sqlite") {
902
+ throw _err("orWhereMatch (FTS5 MATCH) is sqlite-only; build with { dialect: 'sqlite' }",
903
+ "sql-builder/match-sqlite-only");
904
+ }
905
+ if (typeof expr !== "string" || expr.length === 0) {
906
+ throw _err("orWhereMatch requires a non-empty FTS5 query string", "sql-builder/bad-match");
907
+ }
908
+ return this._add("OR", _qualifiedColumn(target, "sqlite") + " MATCH ?", [expr]);
909
+ }
910
+
911
+ // sqlite `<col> IN (SELECT value FROM json_each(?))`. The JSON-array
912
+ // STRING binds as one `?` and sqlite's table-valued json_each unrolls it
913
+ // to a one-column row set - the placeholder-safe way to test membership
914
+ // in a variable-length set without expanding to one `?` per element (and
915
+ // without the Postgres-only `= ANY(?)` array bind). The bound value MUST
916
+ // be a JSON array string (json_each errors at execute on a non-array).
917
+ // sqlite-only (json_each is a sqlite extension); the column is gated +
918
+ // quoted by construction.
919
+ whereInJsonEach(col, jsonArrayString) {
920
+ if (this._dialect() !== "sqlite") {
921
+ throw _err("whereInJsonEach (json_each table-valued function) is sqlite-only; " +
922
+ "use whereInArray on Postgres", "sql-builder/json-each-sqlite-only");
923
+ }
924
+ if (typeof jsonArrayString !== "string" || jsonArrayString.length === 0) {
925
+ throw _err("whereInJsonEach requires a JSON-array string", "sql-builder/bad-json-each");
926
+ }
927
+ this._gate(col);
928
+ var qc = _qualifiedColumn(col, "sqlite");
929
+ return this._add("AND", qc + " IN (SELECT value FROM json_each(?))", [jsonArrayString]);
930
+ }
931
+
932
+ whereIn(col, values) { return this._cmp("AND", col, "IN", values); }
933
+ whereNotIn(col, values) { return this._cmp("AND", col, "NOT IN", values); }
934
+ orWhereIn(col, values) { return this._cmp("OR", col, "IN", values); }
935
+
936
+ // Postgres `col = ANY(?)` - the whole array binds as ONE parameter
937
+ // (a single `?`), in contrast to `whereIn` which expands to one `?`
938
+ // per element. This is the form a Postgres driver wants for a
939
+ // variable-length id set without a placeholder explosion (and it
940
+ // stays a single bind under the 65535-param wire ceiling). On a
941
+ // non-Postgres dialect there is no `= ANY(array)` value form, so it
942
+ // degrades to the portable expanded `IN (?, ?, ...)` automatically -
943
+ // every element still binds, the contract is identical. The array is
944
+ // bound, never interpolated.
945
+ whereInArray(col, values) { return this._inArray("AND", col, values); }
946
+ orWhereInArray(col, values) { return this._inArray("OR", col, values); }
947
+ _inArray(joiner, col, values) {
948
+ if (!Array.isArray(values) || values.length === 0) {
949
+ throw _err("whereInArray requires a non-empty array of values", "sql-builder/empty-in");
950
+ }
951
+ this._gate(col);
952
+ var qc = _qualifiedColumn(col, this._dialect());
953
+ if (this._dialect() === "postgres") {
954
+ // The whole array is one bound value (one `?`); the driver expands
955
+ // it to a Postgres array operand at execute.
956
+ return this._add(joiner, qc + " = ANY(?)", [values.slice()]);
957
+ }
958
+ var holders = values.map(function () { return "?"; }).join(", ");
959
+ return this._add(joiner, qc + " IN (" + holders + ")", values.slice());
960
+ }
961
+
962
+ whereNull(col) { return this._cmp("AND", col, "IS", null); }
963
+ whereNotNull(col) { return this._cmp("AND", col, "IS NOT", null); }
964
+ orWhereNull(col) { return this._cmp("OR", col, "IS", null); }
965
+
966
+ whereBetween(col, low, high) { return this._cmp("AND", col, "BETWEEN", [low, high]); }
967
+
968
+ // Nested group: where(qb => qb.where(...).orWhere(...)) -> "( ... )".
969
+ whereGroup(closure) { return this._group("AND", closure); }
970
+ orWhereGroup(closure) { return this._group("OR", closure); }
971
+ _group(joiner, closure) {
972
+ if (typeof closure !== "function") {
973
+ throw _err("whereGroup(closure): expected a function", "sql-builder/bad-closure");
974
+ }
975
+ var sub = new Predicate(this._owner, "AND");
976
+ closure(sub);
977
+ var built = sub.build();
978
+ if (!built.sql) return this;
979
+ return this._add(joiner, "(" + built.sql + ")", built.params);
980
+ }
981
+
982
+ // Subquery EXISTS / NOT EXISTS.
983
+ whereExists(subBuilder) { return this._exists("AND", "EXISTS", subBuilder); }
984
+ whereNotExists(subBuilder) { return this._exists("AND", "NOT EXISTS", subBuilder); }
985
+ orWhereExists(subBuilder) { return this._exists("OR", "EXISTS", subBuilder); }
986
+ _exists(joiner, kw, subBuilder) {
987
+ if (!(subBuilder instanceof Builder)) {
988
+ throw _err(kw + " requires a b.sql subquery builder", "sql-builder/bad-subquery");
989
+ }
990
+ var sub = _composeSub(subBuilder, this._dialect());
991
+ return this._add(joiner, kw + " (" + sub.sql + ")", sub.params);
992
+ }
993
+
994
+ // Scalar-subquery comparison: whereSub("col", "=", subBuilder).
995
+ whereSub(col, op, subBuilder) {
996
+ if (ALLOWED_OPS[op] !== true) {
997
+ throw _err("invalid where operator '" + op + "'", "sql-builder/bad-operator");
998
+ }
999
+ // JSONB operators have no jsonb_exists* rewrite in scalar-subquery
1000
+ // position (only the value-comparison where() path emits it), so refuse
1001
+ // them here rather than splice a bare ?/?|/?&/@> a backend mis-reads.
1002
+ _refuseJsonbOp(op, "a scalar-subquery comparison");
1003
+ if (!(subBuilder instanceof Builder)) {
1004
+ throw _err("whereSub requires a b.sql subquery builder", "sql-builder/bad-subquery");
1005
+ }
1006
+ this._gate(col);
1007
+ var sub = _composeSub(subBuilder, this._dialect());
1008
+ return this._add("AND", _qualifiedColumn(col, this._dialect()) + " " + op +
1009
+ " (" + sub.sql + ")", sub.params);
1010
+ }
1011
+
1012
+ // Raw fragment, guarded by b.guardSql + the embedded-literal +
1013
+ // placeholder-count scanners. The fragment must be a value expression
1014
+ // (no statement terminator, no verb, no string literal) and every
1015
+ // value must be a `?` bound through params.
1016
+ whereRaw(sql, params, opts) { return this._raw("AND", sql, params, opts); }
1017
+ orWhereRaw(sql, params, opts) { return this._raw("OR", sql, params, opts); }
1018
+ _raw(joiner, sql, params, opts) {
1019
+ var checked = _checkRawFragment(sql, params, opts, "whereRaw");
1020
+ return this._add(joiner, "(" + checked.sql + ")", checked.params);
1021
+ }
1022
+
1023
+ build() {
1024
+ if (this._parts.length === 0) return { sql: "", params: [] };
1025
+ var sql = this._parts[0].sql;
1026
+ var params = this._parts[0].params.slice();
1027
+ for (var i = 1; i < this._parts.length; i++) {
1028
+ sql += " " + this._parts[i].joiner + " " + this._parts[i].sql;
1029
+ for (var j = 0; j < this._parts[i].params.length; j++) params.push(this._parts[i].params[j]);
1030
+ }
1031
+ return { sql: sql, params: params };
1032
+ }
1033
+
1034
+ get length() { return this._parts.length; }
1035
+ }
1036
+
1037
+ // ---- Raw-fragment guard ---------------------------------------------
1038
+ //
1039
+ // The single choke-point every raw escape hatch (whereRaw / setRaw /
1040
+ // havingRaw / joinRaw / on-raw / conflictWhere) passes through. Three
1041
+ // layers, all on by default:
1042
+ // 1. b.guardSql.validate - RFC/CVE defense for hostile raw SQL
1043
+ // (stacked statements, comment-smuggling, file/exec/exfil
1044
+ // primitives, invalid encoding). Strict profile on the request
1045
+ // path; { ok:false } refuses the fragment.
1046
+ // 2. embedded-literal refusal - a '...' literal is the signature of
1047
+ // operator input concatenated into the fragment; refuse unless the
1048
+ // caller explicitly opts in for a static operator-controlled
1049
+ // literal.
1050
+ // 3. placeholder-count - the number of `?` must equal params.length,
1051
+ // so no value is silently unbound or over-supplied.
1052
+ function _checkRawFragment(sql, params, opts, where) {
1053
+ opts = opts || {};
1054
+ if (typeof sql !== "string" || sql.length === 0) {
1055
+ throw _err(where + ": sql must be a non-empty string", "sql-builder/bad-raw");
1056
+ }
1057
+ var p = Array.isArray(params) ? params.slice() : (params == null ? [] : [params]);
1058
+
1059
+ // Guard against hostile raw SQL via b.guardSql.validate - the direct
1060
+ // content checker that returns { ok, issues }. Default strict (request
1061
+ // path); a benign single-statement read fragment can relax via
1062
+ // guardProfile, but the structurally unambiguous classes (stacked
1063
+ // statements, invalid encoding) refuse under every profile. The
1064
+ // fragment context requires the fragment to be a value expression
1065
+ // (any statement terminator / verb / dangerous token refuses).
1066
+ // allowLiterals propagates to b.guardSql too: its own embedded-string-
1067
+ // literal rule honours the same opt (a static operator-controlled
1068
+ // literal the caller deliberately allows), so a fragment opted in here
1069
+ // must not be refused by the guard's literal rule while the local
1070
+ // _assertRawNoStringLiteral pass is skipped - the two literal checks
1071
+ // stay consistent. The structurally unambiguous classes (stacked
1072
+ // statements, invalid encoding, dangerous primitives) refuse regardless.
1073
+ var profile = opts.guardProfile || "strict";
1074
+ var g = guardSql();
1075
+ if (g && typeof g.validate === "function") {
1076
+ var result = g.validate(sql, {
1077
+ profile: profile, context: "fragment", allowLiterals: opts.allowLiterals === true,
1078
+ });
1079
+ if (result && result.ok === false) {
1080
+ var first = (result.issues && result.issues[0]) || {};
1081
+ throw _err(where + ": raw fragment refused by b.guardSql (" +
1082
+ (first.code || "policy") + (first.snippet ? ": " + first.snippet : "") + ")",
1083
+ "sql-builder/guard-refused");
1084
+ }
1085
+ }
1086
+
1087
+ // Embedded-literal + placeholder-count scan (single linear pass over
1088
+ // the fragment, comment / quoted-identifier aware).
1089
+ if (opts.allowLiterals !== true) _assertRawNoStringLiteral(sql, where);
1090
+ // A bare Postgres JSONB key-existence operator (?| / ?&) in a raw fragment
1091
+ // is corrupted by clusterStorage.placeholderize (the ? is rewritten to $N
1092
+ // -> "tags $1| keys"); _countPlaceholders also miscounts the ? as a bind.
1093
+ // Refuse it - the structured where(col, '?|', keys) path emits the
1094
+ // placeholder-safe jsonb_exists_any() form instead.
1095
+ _assertNoRawJsonbKeyOp(sql, where);
1096
+ var holders = _countPlaceholders(sql);
1097
+ if (holders !== p.length) {
1098
+ throw _err(where + ": " + holders + " placeholder(s) in sql but " + p.length +
1099
+ " param(s) supplied", "sql-builder/placeholder-mismatch");
1100
+ }
1101
+ return { sql: sql, params: p };
1102
+ }
1103
+
1104
+ // Refuse a raw fragment that embeds a single-quoted string literal. A
1105
+ // '...' literal is the signature of operator input concatenated into the
1106
+ // builder (CWE-89). Double-quoted identifiers, line comments, and block
1107
+ // comments are skipped. Single linear pass, no backtracking regex. Same
1108
+ // shape as db-query.js's scanner.
1109
+ function _assertRawNoStringLiteral(sql, where) {
1110
+ var i = 0;
1111
+ var len = sql.length;
1112
+ while (i < len) {
1113
+ var ch = sql.charAt(i);
1114
+ var next = i + 1 < len ? sql.charAt(i + 1) : "";
1115
+ if (ch === '"') {
1116
+ i += 1;
1117
+ while (i < len) {
1118
+ if (sql.charAt(i) === '"') {
1119
+ if (sql.charAt(i + 1) === '"') { i += 2; continue; }
1120
+ i += 1; break;
1121
+ }
1122
+ i += 1;
1123
+ }
1124
+ continue;
1125
+ }
1126
+ if (ch === "-" && next === "-") {
1127
+ while (i < len && sql.charAt(i) !== "\n") i += 1;
1128
+ continue;
1129
+ }
1130
+ if (ch === "/" && next === "*") {
1131
+ i += 2;
1132
+ while (i < len && !(sql.charAt(i) === "*" && sql.charAt(i + 1) === "/")) i += 1;
1133
+ i += 2;
1134
+ continue;
1135
+ }
1136
+ if (ch === "'") {
1137
+ throw _err(where + ": raw SQL must not contain a string literal ('...') - bind " +
1138
+ "every value with a ? placeholder, or pass { allowLiterals: true } when the " +
1139
+ "literal is static and operator-controlled", "sql-builder/raw-literal");
1140
+ }
1141
+ i += 1;
1142
+ }
1143
+ }
1144
+
1145
+ // Refuse the two-char Postgres JSONB key-existence tokens (?| / ?&) in a raw
1146
+ // fragment. They are unambiguous (a `?` placeholder is never immediately
1147
+ // followed by `|` / `&`), can't be expressed safely under the ?->$N
1148
+ // placeholderize rewrite, and have a placeholder-safe structured form
1149
+ // (where(col, '?|', keys) -> jsonb_exists_any). Quote/comment-aware so a
1150
+ // `?|` inside an allowLiterals fragment's literal or comment is ignored.
1151
+ function _assertNoRawJsonbKeyOp(sql, where) {
1152
+ var i = 0;
1153
+ var len = sql.length;
1154
+ while (i < len) {
1155
+ var ch = sql.charAt(i);
1156
+ var next = i + 1 < len ? sql.charAt(i + 1) : "";
1157
+ if (ch === "'" || ch === '"' || ch === "`") {
1158
+ var q = ch;
1159
+ i += 1;
1160
+ while (i < len) {
1161
+ if (sql.charAt(i) === q) {
1162
+ if (sql.charAt(i + 1) === q) { i += 2; continue; }
1163
+ i += 1; break;
1164
+ }
1165
+ i += 1;
1166
+ }
1167
+ continue;
1168
+ }
1169
+ if (ch === "-" && next === "-") { while (i < len && sql.charAt(i) !== "\n") i += 1; continue; }
1170
+ if (ch === "/" && next === "*") {
1171
+ i += 2;
1172
+ while (i < len && !(sql.charAt(i) === "*" && sql.charAt(i + 1) === "/")) i += 1;
1173
+ i += 2;
1174
+ continue;
1175
+ }
1176
+ if (ch === "?" && (next === "|" || next === "&")) {
1177
+ throw _err(where + ": raw SQL must not contain the Postgres JSONB key-existence " +
1178
+ "operator '?" + next + "' (it collides with the ? bind placeholder) - use the " +
1179
+ "structured where(col, '?" + next + "', keys) form", "sql-builder/raw-jsonb-op");
1180
+ }
1181
+ i += 1;
1182
+ }
1183
+ }
1184
+
1185
+ // Placeholder counting (quote/comment-aware) is the scanner shared with the
1186
+ // residency write-gate; composed from safe-sql so the skip rules live in one
1187
+ // place. The output validator below uses it for placeholder/param parity.
1188
+ var _countPlaceholders = safeSql.countPlaceholders;
1189
+
1190
+ // Translate the builder's `?` placeholders to a dialect's positional form
1191
+ // (Postgres `$1..$N`; SQLite / MySQL keep `?`). Quote / comment-aware single
1192
+ // pass - a `?` inside a string literal, a quoted identifier, or a comment is
1193
+ // NOT a placeholder and is left untouched. This is the toExternalSql terminal
1194
+ // for code that hands the SQL to an operator-supplied driver directly (no
1195
+ // clusterStorage in the path to do the rewrite). The translator lives here -
1196
+ // the builder owns its driver-final output rendering - rather than reaching
1197
+ // into clusterStorage (which transitively requires this module).
1198
+ function _toPositional(sql, dialect) {
1199
+ if (dialect !== "postgres") return sql;
1200
+ var out = "";
1201
+ var n = 0;
1202
+ var i = 0;
1203
+ var len = sql.length;
1204
+ while (i < len) {
1205
+ var c = sql.charAt(i);
1206
+ var nx = i + 1 < len ? sql.charAt(i + 1) : "";
1207
+ if (c === "'" || c === '"' || c === "`") {
1208
+ out += c;
1209
+ i += 1;
1210
+ while (i < len) {
1211
+ var q = sql.charAt(i);
1212
+ if (q === c) {
1213
+ if (sql.charAt(i + 1) === c) { out += c + c; i += 2; continue; }
1214
+ out += c; i += 1; break;
1215
+ }
1216
+ out += q; i += 1;
1217
+ }
1218
+ continue;
1219
+ }
1220
+ if (c === "-" && nx === "-") {
1221
+ while (i < len && sql.charAt(i) !== "\n") { out += sql.charAt(i); i += 1; }
1222
+ continue;
1223
+ }
1224
+ if (c === "/" && nx === "*") {
1225
+ out += "/*"; i += 2;
1226
+ while (i < len && !(sql.charAt(i) === "*" && sql.charAt(i + 1) === "/")) { out += sql.charAt(i); i += 1; }
1227
+ if (i < len) { out += "*/"; i += 2; }
1228
+ continue;
1229
+ }
1230
+ if (c === "?") { n += 1; out += "$" + n; i += 1; continue; }
1231
+ out += c;
1232
+ i += 1;
1233
+ }
1234
+ return out;
1235
+ }
1236
+
1237
+ /**
1238
+ * @primitive b.sql.toExternalSql
1239
+ * @signature b.sql.toExternalSql(builtOrBuilder, dialect)
1240
+ * @since 0.15.0
1241
+ * @status stable
1242
+ * @related b.sql.select, b.sql.createTable, b.clusterStorage.placeholderize
1243
+ *
1244
+ * Translate a built statement to a driver's positional placeholder form for
1245
+ * code that hands the SQL to an operator-supplied driver DIRECTLY (no
1246
+ * `b.clusterStorage` in the path to rewrite). Accepts either a chainable
1247
+ * builder (any `b.sql.select` / `insert` / `update` / `delete` / `upsert`,
1248
+ * via its own `.toExternalSql()` method) OR a plain `{ sql, params }` result
1249
+ * from a DDL builder (`createTable` / `createIndex` / `alterTable` /
1250
+ * `dropTable` / the RLS + catalog builders). Postgres gets `$1..$N`; SQLite
1251
+ * and MySQL keep `?`. The `?`-by-construction invariant is unchanged - only
1252
+ * the emitted text differs at the last step.
1253
+ *
1254
+ * @example
1255
+ * var b = require("@blamejs/core");
1256
+ * var ddl = b.sql.toExternalSql(
1257
+ * b.sql.createIndex("idx_pending", "outbox", ["next_attempt_at"],
1258
+ * { dialect: "postgres", where: "status = 'pending'" }),
1259
+ * "postgres");
1260
+ * // -> { sql: 'CREATE INDEX IF NOT EXISTS "idx_pending" ON outbox ' +
1261
+ * // '("next_attempt_at") WHERE status = \'pending\'', params: [] }
1262
+ */
1263
+ function toExternalSql(builtOrBuilder, dialect) {
1264
+ if (builtOrBuilder instanceof Builder) return builtOrBuilder.toExternalSql(dialect);
1265
+ if (builtOrBuilder && typeof builtOrBuilder.sql === "string" &&
1266
+ Array.isArray(builtOrBuilder.params)) {
1267
+ var d = _normDialect(dialect);
1268
+ return { sql: _toPositional(builtOrBuilder.sql, d), params: builtOrBuilder.params };
1269
+ }
1270
+ throw _err("b.sql.toExternalSql expects a b.sql builder or a { sql, params } result",
1271
+ "sql-builder/bad-external-input");
1272
+ }
1273
+
1274
+ // Final output gate - every verb's toSql() routes through _emit() before
1275
+ // returning, so a builder bug or a tainted identifier can never reach the
1276
+ // driver. Three invariants over the assembled statement, reusing the same
1277
+ // quote/comment-aware scan:
1278
+ // 1. placeholder <-> param parity - a `?` without its bound param (or a
1279
+ // param without its `?`) silently shifts values into the wrong
1280
+ // columns, the highest-severity builder bug.
1281
+ // 2. exactly one statement - no top-level `;` outside literals/comments
1282
+ // (a stacked statement has no place in a single built statement).
1283
+ // 3. balanced parentheses at the top level (structural well-formedness).
1284
+ // String literals only legitimately appear via whereRaw { allowLiterals }
1285
+ // (already gated at fragment time), so the literal check stays there.
1286
+ function _assertEmittable(sql, params) {
1287
+ // ---- shape ----
1288
+ // A builder bug must never emit a non-string / empty statement, and
1289
+ // params must be an array - a misshapen output hides a real defect
1290
+ // rather than failing loudly at the driver.
1291
+ if (typeof sql !== "string" || sql.length === 0) {
1292
+ throw _err("toSql: emitted SQL must be a non-empty string (builder bug)",
1293
+ "sql-builder/empty-sql");
1294
+ }
1295
+ if (!Array.isArray(params)) {
1296
+ throw _err("toSql: params must be an array (builder bug)",
1297
+ "sql-builder/bad-params-shape");
1298
+ }
1299
+ // ---- size ----
1300
+ // Runaway / DoS ceiling on the statement text. A statement this large
1301
+ // is a bug (an unbatched bulk write, a pathological IN-list), never a
1302
+ // legitimate single query.
1303
+ if (sql.length > MAX_SQL_BYTES) {
1304
+ throw _err("toSql: emitted SQL is " + sql.length + " bytes, over the " +
1305
+ MAX_SQL_BYTES + "-byte cap - batch the operation rather than building " +
1306
+ "one oversized statement", "sql-builder/sql-too-large");
1307
+ }
1308
+ // ---- text validity (boundary-escape) ----
1309
+ // A NUL byte truncates the statement at C-string-based drivers and can't
1310
+ // be stored in Postgres text; lone UTF-16 surrogates encode to invalid
1311
+ // UTF-8 on the wire (the CVE-2025-1094 encoding-escape class). Neither
1312
+ // can legitimately appear in builder-emitted SQL.
1313
+ if (sql.indexOf("\u0000") !== -1) {
1314
+ throw _err("toSql: emitted SQL contains a NUL byte - rejected " +
1315
+ "(statement-truncation / boundary-escape risk)", "sql-builder/null-byte-sql");
1316
+ }
1317
+ if (typeof sql.isWellFormed === "function" && !sql.isWellFormed()) {
1318
+ throw _err("toSql: emitted SQL contains invalid Unicode (lone " +
1319
+ "surrogates) - rejected (would encode to invalid UTF-8 on the wire)",
1320
+ "sql-builder/invalid-encoding-sql");
1321
+ }
1322
+ var n = params.length;
1323
+ // ---- bind-parameter ceiling ----
1324
+ // The wire-protocol limit (Postgres/MySQL 65535; SQLite 32766). Past it
1325
+ // is a hard driver error; catch it here with a clear message. The usual
1326
+ // cause is an unbounded IN-list / bulk insert that should be chunked.
1327
+ if (n > MAX_BIND_PARAMS) {
1328
+ throw _err("toSql: " + n + " bind parameters exceeds the " + MAX_BIND_PARAMS +
1329
+ "-parameter wire limit - chunk the values (batch the IN-list / rows)",
1330
+ "sql-builder/too-many-params");
1331
+ }
1332
+ // ---- param-element shape ----
1333
+ // A param that is `undefined` / a function / a symbol is a caller
1334
+ // mistake (a typo'd variable, a method reference passed by accident);
1335
+ // drivers coerce these ambiguously (undefined -> NULL, function ->
1336
+ // "[Function]"), silently storing the wrong value. Bind a concrete
1337
+ // value (string / number / boolean / null / bigint / Buffer / Date /
1338
+ // a JSON-serializable object). null is valid SQL NULL.
1339
+ for (var pi = 0; pi < n; pi += 1) {
1340
+ var pv = params[pi];
1341
+ var pt = typeof pv;
1342
+ if (pv === undefined || pt === "function" || pt === "symbol") {
1343
+ throw _err("toSql: param[" + pi + "] is " +
1344
+ (pv === undefined ? "undefined" : pt) + " - bind a concrete value " +
1345
+ "(string / number / boolean / null / bigint / Buffer / Date / object); " +
1346
+ "use null for SQL NULL", "sql-builder/bad-param-value");
1347
+ }
1348
+ // ---- column-level (per-value) size boundary ----
1349
+ // Only strings / Buffers can carry an unbounded payload; everything
1350
+ // else (number / boolean / bigint / Date / null) is fixed-small. A
1351
+ // single value over the per-value ceiling is a buffer-overflow-class
1352
+ // mistake (a whole file / whole buffer bound by accident), distinct
1353
+ // from the total-statement and total-param caps above.
1354
+ if (pt === "string" || Buffer.isBuffer(pv)) {
1355
+ var vbytes = pt === "string" ? Buffer.byteLength(pv, "utf8") : pv.length;
1356
+ if (vbytes > MAX_PARAM_BYTES) {
1357
+ throw _err("toSql: param[" + pi + "] is " + vbytes + " bytes, over the " +
1358
+ MAX_PARAM_BYTES + "-byte per-value ceiling - stream large blobs " +
1359
+ "through chunked storage rather than binding one oversized column",
1360
+ "sql-builder/param-too-large");
1361
+ }
1362
+ }
1363
+ if (pt === "string") {
1364
+ // A bound string still rides the wire. A NUL byte cannot be stored
1365
+ // in a Postgres text column and truncates C-string-based drivers; a
1366
+ // lone UTF-16 surrogate encodes to invalid UTF-8 on the wire (the
1367
+ // text values that "jump out of boundaries"). Reject both here so a
1368
+ // malformed value fails loudly at build time, not as a corrupt store.
1369
+ if (pv.indexOf("\u0000") !== -1) {
1370
+ throw _err("toSql: param[" + pi + "] contains a NUL byte - rejected " +
1371
+ "(text-column / driver truncation, boundary-escape risk)",
1372
+ "sql-builder/null-byte-param");
1373
+ }
1374
+ if (typeof pv.isWellFormed === "function" && !pv.isWellFormed()) {
1375
+ throw _err("toSql: param[" + pi + "] contains invalid Unicode (lone " +
1376
+ "surrogates) - rejected (would encode to invalid UTF-8 on the wire)",
1377
+ "sql-builder/invalid-encoding-param");
1378
+ }
1379
+ }
1380
+ }
1381
+ // ---- placeholder <-> param parity ----
1382
+ var holders = _countPlaceholders(sql);
1383
+ if (holders !== n) {
1384
+ throw _err("toSql: placeholder/param count mismatch - " + holders +
1385
+ " '?' placeholder(s) but " + n + " param(s); emitting this would " +
1386
+ "misalign bound values across columns", "sql-builder/param-mismatch");
1387
+ }
1388
+ safeSql.assertSingleStatement(sql, {
1389
+ label: "toSql",
1390
+ makeError: function (m, suffix) { return _err(m, "sql-builder/" + suffix); },
1391
+ });
1392
+ }
1393
+
1394
+ // Terminal wrapper: validate then return the { sql, params } shape every
1395
+ // verb's toSql() emits.
1396
+ function _emit(sql, params) {
1397
+ _assertEmittable(sql, params);
1398
+ return { sql: sql, params: params };
1399
+ }
1400
+
1401
+ // ---- WITH (CTE) -----------------------------------------------------
1402
+ //
1403
+ // A CTE is a name + a subquery (a b.sql Builder whose toSql() composes,
1404
+ // params concatenated in CTE order before the main statement's params)
1405
+ // OR a guarded raw fragment. A statement carries an ordered list of
1406
+ // CTEs; withRecursive marks the WITH clause RECURSIVE.
1407
+
1408
+ function _cteFragment(cte, dialect) {
1409
+ var name = _quoteId(cte.name, dialect);
1410
+ if (cte.builder instanceof Builder) {
1411
+ // Render the CTE body under the OUTER statement's dialect, not the
1412
+ // sub-builder's own (default sqlite), so the name + body quote
1413
+ // consistently in one statement.
1414
+ var sub = _composeSub(cte.builder, dialect);
1415
+ return { sql: name + " AS (" + sub.sql + ")", params: sub.params };
1416
+ }
1417
+ // Raw CTE body - guarded like any raw fragment but allowed to be a
1418
+ // full SELECT/INSERT/UPDATE/DELETE statement (migration context).
1419
+ var checked = _checkRawFragment(cte.sql, cte.params, { guardProfile: cte.guardProfile || "balanced" },
1420
+ "with");
1421
+ return { sql: name + " AS (" + checked.sql + ")", params: checked.params };
1422
+ }
1423
+
1424
+ function _renderWith(ctes, recursive, dialect) {
1425
+ if (!ctes || ctes.length === 0) return { sql: "", params: [] };
1426
+ var fragments = [];
1427
+ var params = [];
1428
+ for (var i = 0; i < ctes.length; i++) {
1429
+ var f = _cteFragment(ctes[i], dialect);
1430
+ fragments.push(f.sql);
1431
+ for (var j = 0; j < f.params.length; j++) params.push(f.params[j]);
1432
+ }
1433
+ return {
1434
+ sql: "WITH " + (recursive ? "RECURSIVE " : "") + fragments.join(", ") + " ",
1435
+ params: params,
1436
+ };
1437
+ }
1438
+
1439
+ // ---- Base Builder ---------------------------------------------------
1440
+ //
1441
+ // Holds the shared dialect, table, CTE list, and column-membership gate.
1442
+ // Each verb is a subclass with its own clause set + toSql().
1443
+
1444
+ class Builder {
1445
+ constructor(verb, tableNameOrRef, opts) {
1446
+ opts = opts || {};
1447
+ this._verb = verb;
1448
+ this._dialect = _normDialect(opts.dialect);
1449
+ this._table = _normTableRef(tableNameOrRef, opts);
1450
+ this._ctes = [];
1451
+ this._cteRecursive = false;
1452
+
1453
+ // Column-membership gate. When the operator declares allowedColumns
1454
+ // (or a schema-declared set), an unknown column is refused before it
1455
+ // interpolates as an identifier (ORDER-BY / disclosure injection).
1456
+ this._allowedColumns = null;
1457
+ if (opts.allowedColumns) {
1458
+ if (!Array.isArray(opts.allowedColumns) || opts.allowedColumns.length === 0) {
1459
+ throw _err("allowedColumns must be a non-empty array", "sql-builder/bad-allowed-columns");
1460
+ }
1461
+ opts.allowedColumns.forEach(_validateColumn);
1462
+ this._allowedColumns = new Set(opts.allowedColumns);
1463
+ }
1464
+ this._columnGateMode = opts.columnGateMode || (this._allowedColumns ? "reject" : "off");
1465
+ }
1466
+
1467
+ // Restrict columns to an explicit subset (chainable form of the opt).
1468
+ allowedColumns(cols) {
1469
+ if (!Array.isArray(cols) || cols.length === 0) {
1470
+ throw _err("allowedColumns(cols): expected a non-empty array", "sql-builder/bad-allowed-columns");
1471
+ }
1472
+ cols.forEach(_validateColumn);
1473
+ this._allowedColumns = new Set(cols);
1474
+ if (this._columnGateMode === "off") this._columnGateMode = "reject";
1475
+ return this;
1476
+ }
1477
+
1478
+ columnGate(mode) {
1479
+ if (mode !== "reject" && mode !== "warn" && mode !== "off") {
1480
+ throw _err("columnGate mode must be 'reject' | 'warn' | 'off'", "sql-builder/bad-gate-mode");
1481
+ }
1482
+ this._columnGateMode = mode;
1483
+ return this;
1484
+ }
1485
+
1486
+ // Assert a column is a member of the gate set before it is quoted into
1487
+ // SQL. Always enforces an explicit allowedColumns set; "warn" mode
1488
+ // permits unknown columns (no audit sink here - this is a pure string
1489
+ // builder), "off" / no set skips. A qualified "alias.col" gates on the
1490
+ // bare column segment.
1491
+ _assertColumnMember(col, where) {
1492
+ if (this._columnGateMode === "off" || this._allowedColumns === null) return;
1493
+ var bare = col.indexOf(".") !== -1 ? col.split(".").pop() : col;
1494
+ if (this._allowedColumns.has(bare)) return;
1495
+ if (this._columnGateMode === "warn") return;
1496
+ throw _err("column '" + col + "' is not in the allowedColumns set" +
1497
+ (where ? " (" + where + ")" : ""), "sql-builder/unknown-column");
1498
+ }
1499
+
1500
+ // ---- WITH (shared by every verb) ----
1501
+ with(name, subqueryOrRaw, params, opts) {
1502
+ return this._pushCte(false, name, subqueryOrRaw, params, opts);
1503
+ }
1504
+ withRecursive(name, subqueryOrRaw, params, opts) {
1505
+ return this._pushCte(true, name, subqueryOrRaw, params, opts);
1506
+ }
1507
+ _pushCte(recursive, name, subqueryOrRaw, params, opts) {
1508
+ _validateColumn(name);
1509
+ if (recursive) this._cteRecursive = true;
1510
+ if (subqueryOrRaw instanceof Builder) {
1511
+ this._ctes.push({ name: name, builder: subqueryOrRaw });
1512
+ } else if (typeof subqueryOrRaw === "string") {
1513
+ this._ctes.push({
1514
+ name: name, sql: subqueryOrRaw, params: params,
1515
+ guardProfile: (opts && opts.guardProfile) || "balanced",
1516
+ });
1517
+ } else {
1518
+ throw _err("with(name, ...): second arg must be a b.sql builder or a raw SQL string",
1519
+ "sql-builder/bad-cte");
1520
+ }
1521
+ return this;
1522
+ }
1523
+
1524
+ // Subclasses implement _render() -> { sql, params } WITHOUT the WITH
1525
+ // prefix; toSql() prepends the rendered CTE clause.
1526
+ toSql() {
1527
+ var body = this._render();
1528
+ if (this._ctes.length === 0) return body;
1529
+ var withClause = _renderWith(this._ctes, this._cteRecursive, this._dialect);
1530
+ return {
1531
+ sql: withClause.sql + body.sql,
1532
+ params: withClause.params.concat(body.params),
1533
+ };
1534
+ }
1535
+
1536
+ // Driver-final form for code that targets an operator-supplied driver
1537
+ // DIRECTLY (b.externalDb.query / a transaction client), with no
1538
+ // b.clusterStorage in the path to rewrite placeholders. The builder
1539
+ // always composes `?` placeholders by construction; this terminal
1540
+ // translates them to the dialect's positional form at the boundary:
1541
+ // `$1..$N` for Postgres, left as `?` for SQLite / MySQL. The translation
1542
+ // is the SAME quote/comment-aware single pass clusterStorage uses, so a
1543
+ // `?` inside a string literal / quoted identifier / comment is never
1544
+ // rewritten. The `?`-by-construction invariant is unchanged - only the
1545
+ // emitted text differs at the very last step.
1546
+ toExternalSql(dialect) {
1547
+ var built = this.toSql();
1548
+ var d = _normDialect(dialect || this._dialect);
1549
+ return { sql: _toPositional(built.sql, d), params: built.params };
1550
+ }
1551
+ }
1552
+
1553
+ // ---- SELECT ---------------------------------------------------------
1554
+
1555
+ class SelectBuilder extends Builder {
1556
+ constructor(tableNameOrRef, opts) {
1557
+ super("select", tableNameOrRef, opts);
1558
+ this._projection = []; // [{ sql, params }] - column / aggregate / scalar-subquery
1559
+ this._distinct = false;
1560
+ this._joins = []; // [{ sql, params }]
1561
+ this._where = new Predicate(this, "AND");
1562
+ this._groupBy = [];
1563
+ this._having = new Predicate(this, "AND");
1564
+ this._orderBy = [];
1565
+ this._limit = null;
1566
+ this._offset = null;
1567
+ this._lockMode = null; // null | "UPDATE" | "SHARE"
1568
+ this._lockSkipLocked = false;
1569
+ this._lockNoWait = false;
1570
+ }
1571
+
1572
+ distinct() { this._distinct = true; return this; }
1573
+
1574
+ // Projection: array of column names (or "alias.col"); each quoted.
1575
+ // Empty / unset -> "*".
1576
+ columns(cols) {
1577
+ if (!Array.isArray(cols)) throw _err("columns() expects an array", "sql-builder/bad-columns");
1578
+ var self = this;
1579
+ cols.forEach(function (c) {
1580
+ self._assertColumnMember(c, "select");
1581
+ self._projection.push({ sql: _qualifiedColumn(c, self._dialect), params: [] });
1582
+ });
1583
+ return this;
1584
+ }
1585
+ select(cols) { return this.columns(cols); }
1586
+
1587
+ // A guarded raw projection expression - a constant `1` presence sentinel
1588
+ // (`SELECT 1 ... WHERE ...` for an existence probe), a function-call
1589
+ // projection, or any value expression the structured column / aggregate
1590
+ // helpers don't cover. It rides the same b.guardSql raw-fragment gate as
1591
+ // whereRaw (no statement terminator, no embedded string literal unless
1592
+ // allowLiterals); any value binds via a `?` carried in params.
1593
+ selectRaw(expr, params, opts) {
1594
+ var checked = _checkRawFragment(expr, params, opts, "selectRaw");
1595
+ this._projection.push({ sql: checked.sql, params: checked.params });
1596
+ return this;
1597
+ }
1598
+
1599
+ // Aggregate helpers. alias is quoted; the aggregated column is quoted
1600
+ // (or "*" for count()).
1601
+ count(col, alias) { return this._agg("COUNT", col || "*", alias, false); }
1602
+ countDistinct(col, alias) { return this._agg("COUNT", col, alias, true); }
1603
+ max(col, alias) { return this._agg("MAX", col, alias, false); }
1604
+ min(col, alias) { return this._agg("MIN", col, alias, false); }
1605
+ sum(col, alias) { return this._agg("SUM", col, alias, false); }
1606
+ avg(col, alias) { return this._agg("AVG", col, alias, false); }
1607
+ _agg(fn, col, alias, distinct) {
1608
+ var inner;
1609
+ if (col === "*") {
1610
+ inner = "*";
1611
+ } else {
1612
+ this._assertColumnMember(col, fn.toLowerCase());
1613
+ inner = (distinct ? "DISTINCT " : "") + _qualifiedColumn(col, this._dialect);
1614
+ }
1615
+ var sql = fn + "(" + inner + ")";
1616
+ if (alias) { _validateColumn(alias); sql += " AS " + _quoteId(alias, this._dialect); }
1617
+ this._projection.push({ sql: sql, params: [] });
1618
+ return this;
1619
+ }
1620
+
1621
+ // Scalar subquery in the projection: selectSub(subBuilder, "alias").
1622
+ selectSub(subBuilder, alias) {
1623
+ if (!(subBuilder instanceof Builder)) {
1624
+ throw _err("selectSub requires a b.sql subquery builder", "sql-builder/bad-subquery");
1625
+ }
1626
+ _validateColumn(alias);
1627
+ var sub = _composeSub(subBuilder, this._dialect);
1628
+ this._projection.push({
1629
+ sql: "(" + sub.sql + ") AS " + _quoteId(alias, this._dialect),
1630
+ params: sub.params,
1631
+ });
1632
+ return this;
1633
+ }
1634
+
1635
+ // ---- JOINs ----
1636
+ join(tbl, onLeft, op, onRight) { return this._join("INNER", tbl, onLeft, op, onRight); }
1637
+ innerJoin(tbl, onLeft, op, onRight) { return this._join("INNER", tbl, onLeft, op, onRight); }
1638
+ leftJoin(tbl, onLeft, op, onRight) { return this._join("LEFT", tbl, onLeft, op, onRight); }
1639
+ rightJoin(tbl, onLeft, op, onRight) { return this._join("RIGHT", tbl, onLeft, op, onRight); }
1640
+ fullJoin(tbl, onLeft, op, onRight) { return this._join("FULL", tbl, onLeft, op, onRight); }
1641
+ crossJoin(tbl) { return this._join("CROSS", tbl, null, null, null); }
1642
+ _join(kind, tbl, onLeft, op, onRight) {
1643
+ var ref = _normTableRef(tbl, {});
1644
+ var clause = JOIN_KINDS[kind] + " " + ref.refWithAlias(this._dialect);
1645
+ if (kind !== "CROSS") {
1646
+ if (typeof onLeft !== "string" || typeof onRight !== "string") {
1647
+ throw _err(kind + " join requires onLeft + onRight column expressions",
1648
+ "sql-builder/bad-join-on");
1649
+ }
1650
+ var joinOp = op || "=";
1651
+ // The ON operator is a comparison; validate against the same
1652
+ // allowlist. Both operands are column expressions (quoted), never
1653
+ // bound values - a join condition compares columns, not literals.
1654
+ if (ALLOWED_OPS[joinOp] !== true) {
1655
+ throw _err("invalid join ON operator '" + joinOp + "'", "sql-builder/bad-operator");
1656
+ }
1657
+ // A JSONB operator in a join ON has no jsonb_exists* rewrite and a bare
1658
+ // `?` collides with the placeholder marker; refuse it here.
1659
+ _refuseJsonbOp(joinOp, "a join ON clause");
1660
+ clause += " ON " + _qualifiedColumn(onLeft, this._dialect) + " " + joinOp + " " +
1661
+ _qualifiedColumn(onRight, this._dialect);
1662
+ }
1663
+ this._joins.push({ sql: clause, params: [] });
1664
+ return this;
1665
+ }
1666
+
1667
+ // Raw join (guarded) - the full "<KIND> JOIN <tbl> ON <raw>" escape
1668
+ // hatch for join conditions the column-pair form can't express.
1669
+ joinRaw(sql, params, opts) {
1670
+ var checked = _checkRawFragment(sql, params, opts, "joinRaw");
1671
+ this._joins.push({ sql: checked.sql, params: checked.params });
1672
+ return this;
1673
+ }
1674
+
1675
+ // ---- WHERE (delegated to the Predicate) ----
1676
+ // where / andWhere / orWhere forward `arguments` rather than fixed
1677
+ // positional params: the Predicate distinguishes the 2-arg
1678
+ // where(field, value) shorthand from the 3-arg where(field, op, value)
1679
+ // form by arguments.length, so a fixed (a, b, c) signature here would
1680
+ // make a 2-arg call look like 3 (binding the value as the operator).
1681
+ where() { this._where.where.apply(this._where, arguments); return this; }
1682
+ andWhere() { this._where.andWhere.apply(this._where, arguments); return this; }
1683
+ orWhere() { this._where.orWhere.apply(this._where, arguments); return this; }
1684
+ whereOp(col, op, value) { this._where.whereOp(col, op, value); return this; }
1685
+ orWhereOp(col, op, value) { this._where.orWhereOp(col, op, value); return this; }
1686
+ whereIn(col, values) { this._where.whereIn(col, values); return this; }
1687
+ whereNotIn(col, values) { this._where.whereNotIn(col, values); return this; }
1688
+ orWhereIn(col, values) { this._where.orWhereIn(col, values); return this; }
1689
+ whereInArray(col, values) { this._where.whereInArray(col, values); return this; }
1690
+ orWhereInArray(col, values) { this._where.orWhereInArray(col, values); return this; }
1691
+ whereInJsonEach(col, jsonArrayString) { this._where.whereInJsonEach(col, jsonArrayString); return this; }
1692
+ whereMatch(target, expr) { this._where.whereMatch(target, expr); return this; }
1693
+ orWhereMatch(target, expr) { this._where.orWhereMatch(target, expr); return this; }
1694
+ whereNull(col) { this._where.whereNull(col); return this; }
1695
+ whereNotNull(col) { this._where.whereNotNull(col); return this; }
1696
+ orWhereNull(col) { this._where.orWhereNull(col); return this; }
1697
+ whereLike(col, term, mode) { this._where.whereLike(col, term, mode); return this; }
1698
+ orWhereLike(col, term, mode) { this._where.orWhereLike(col, term, mode); return this; }
1699
+ whereBetween(col, low, high) { this._where.whereBetween(col, low, high); return this; }
1700
+ whereGroup(closure) { this._where.whereGroup(closure); return this; }
1701
+ orWhereGroup(closure) { this._where.orWhereGroup(closure); return this; }
1702
+ whereExists(sub) { this._where.whereExists(sub); return this; }
1703
+ whereNotExists(sub) { this._where.whereNotExists(sub); return this; }
1704
+ whereSub(col, op, sub) { this._where.whereSub(col, op, sub); return this; }
1705
+ whereRaw(sql, params, opts) { this._where.whereRaw(sql, params, opts); return this; }
1706
+ orWhereRaw(sql, params, opts) { this._where.orWhereRaw(sql, params, opts); return this; }
1707
+
1708
+ // ---- Row locking (Postgres / MySQL 8+) ----
1709
+ // FOR UPDATE [SKIP LOCKED] - the competing-consumer claim idiom. SKIP
1710
+ // LOCKED lets parallel workers each grab a disjoint row set without
1711
+ // blocking on each other's locks (the at-least-once outbox / job-queue
1712
+ // claim). It is Postgres / MySQL-only; SQLite is a single writer and
1713
+ // has no row lock, so the builder REFUSES forUpdate on a sqlite dialect
1714
+ // at build (emitting unsupported syntax would be a silent driver error)
1715
+ // - the caller branches on dialect and uses a plain transaction-scoped
1716
+ // SELECT for sqlite, exactly as the publisher does.
1717
+ forUpdate(opts) { return this._lock("UPDATE", opts); }
1718
+ forShare(opts) { return this._lock("SHARE", opts); }
1719
+ _lock(mode, opts) {
1720
+ opts = opts || {};
1721
+ if (this._dialect === "sqlite") {
1722
+ throw _err("forUpdate / forShare row locking is Postgres / MySQL-only " +
1723
+ "(SQLite is a single writer with no row lock); branch on dialect and use a " +
1724
+ "transaction-scoped SELECT for sqlite", "sql-builder/lock-unsupported");
1725
+ }
1726
+ this._lockMode = mode;
1727
+ this._lockSkipLocked = opts.skipLocked === true;
1728
+ this._lockNoWait = opts.noWait === true;
1729
+ if (this._lockSkipLocked && this._lockNoWait) {
1730
+ throw _err("forUpdate: skipLocked and noWait are mutually exclusive", "sql-builder/bad-lock");
1731
+ }
1732
+ return this;
1733
+ }
1734
+
1735
+ // ---- GROUP BY / HAVING ----
1736
+ groupBy(cols) {
1737
+ var arr = Array.isArray(cols) ? cols : [cols];
1738
+ var self = this;
1739
+ arr.forEach(function (c) {
1740
+ self._assertColumnMember(c, "groupBy");
1741
+ self._groupBy.push(_qualifiedColumn(c, self._dialect));
1742
+ });
1743
+ return this;
1744
+ }
1745
+ having() { this._having.where.apply(this._having, arguments); return this; }
1746
+ orHaving() { this._having.orWhere.apply(this._having, arguments); return this; }
1747
+ havingRaw(sql, params, opts) { this._having.whereRaw(sql, params, opts); return this; }
1748
+
1749
+ // ---- ORDER BY / LIMIT / OFFSET ----
1750
+ orderBy(col, direction) {
1751
+ this._assertColumnMember(col, "orderBy");
1752
+ var dir = (direction || "asc").toLowerCase();
1753
+ if (dir !== "asc" && dir !== "desc") {
1754
+ throw _err("orderBy direction must be 'asc' or 'desc'", "sql-builder/bad-direction");
1755
+ }
1756
+ this._orderBy.push(_qualifiedColumn(col, this._dialect) + " " + dir.toUpperCase());
1757
+ return this;
1758
+ }
1759
+ limit(n) {
1760
+ if (!Number.isInteger(n) || n < 0) {
1761
+ throw _err("limit must be a non-negative integer", "sql-builder/bad-limit");
1762
+ }
1763
+ this._limit = n;
1764
+ return this;
1765
+ }
1766
+ offset(n) {
1767
+ if (!Number.isInteger(n) || n < 0) {
1768
+ throw _err("offset must be a non-negative integer", "sql-builder/bad-offset");
1769
+ }
1770
+ this._offset = n;
1771
+ return this;
1772
+ }
1773
+
1774
+ _render() {
1775
+ var dialect = this._dialect;
1776
+ var params = [];
1777
+ var projSql;
1778
+ if (this._projection.length === 0) {
1779
+ projSql = "*";
1780
+ } else {
1781
+ var pieces = [];
1782
+ for (var p = 0; p < this._projection.length; p++) {
1783
+ pieces.push(this._projection[p].sql);
1784
+ for (var pp = 0; pp < this._projection[p].params.length; pp++) {
1785
+ params.push(this._projection[p].params[pp]);
1786
+ }
1787
+ }
1788
+ projSql = pieces.join(", ");
1789
+ }
1790
+
1791
+ var sql = "SELECT " + (this._distinct ? "DISTINCT " : "") + projSql +
1792
+ " FROM " + this._table.refWithAlias(dialect);
1793
+
1794
+ for (var j = 0; j < this._joins.length; j++) {
1795
+ sql += " " + this._joins[j].sql;
1796
+ for (var jp = 0; jp < this._joins[j].params.length; jp++) params.push(this._joins[j].params[jp]);
1797
+ }
1798
+
1799
+ var w = this._where.build();
1800
+ if (w.sql) { sql += " WHERE " + w.sql; for (var wi = 0; wi < w.params.length; wi++) params.push(w.params[wi]); }
1801
+
1802
+ if (this._groupBy.length > 0) sql += " GROUP BY " + this._groupBy.join(", ");
1803
+
1804
+ var h = this._having.build();
1805
+ if (h.sql) { sql += " HAVING " + h.sql; for (var hi = 0; hi < h.params.length; hi++) params.push(h.params[hi]); }
1806
+
1807
+ if (this._orderBy.length > 0) sql += " ORDER BY " + this._orderBy.join(", ");
1808
+ if (this._limit !== null) sql += " LIMIT " + this._limit;
1809
+ if (this._offset !== null) sql += " OFFSET " + this._offset;
1810
+
1811
+ if (this._lockMode !== null) {
1812
+ sql += " FOR " + this._lockMode;
1813
+ if (this._lockSkipLocked) sql += " SKIP LOCKED";
1814
+ else if (this._lockNoWait) sql += " NOWAIT";
1815
+ }
1816
+
1817
+ return _emit(sql, params);
1818
+ }
1819
+ }
1820
+
1821
+ // ---- INSERT ---------------------------------------------------------
1822
+
1823
+ class InsertBuilder extends Builder {
1824
+ constructor(tableNameOrRef, opts) {
1825
+ super("insert", tableNameOrRef, opts);
1826
+ this._columns = null;
1827
+ this._rows = []; // array of value arrays, aligned to _columns
1828
+ this._returning = null;
1829
+ }
1830
+
1831
+ columns(cols) {
1832
+ if (!Array.isArray(cols) || cols.length === 0) {
1833
+ throw _err("columns() expects a non-empty array", "sql-builder/bad-columns");
1834
+ }
1835
+ var self = this;
1836
+ cols.forEach(function (c) { self._assertColumnMember(c, "insert"); _validateColumn(c); });
1837
+ this._columns = cols.slice();
1838
+ return this;
1839
+ }
1840
+
1841
+ // values(obj) - one row from a column->value map (sets _columns from
1842
+ // the keys if not already set). values([obj, obj]) - multiple rows.
1843
+ // values(array) - one row aligned to a prior columns() call.
1844
+ values(rowOrRows) {
1845
+ if (Array.isArray(rowOrRows) && rowOrRows.length > 0 && typeof rowOrRows[0] === "object" &&
1846
+ rowOrRows[0] !== null && !Array.isArray(rowOrRows[0])) {
1847
+ // Array of row objects.
1848
+ var self = this;
1849
+ rowOrRows.forEach(function (r) { self._addRowObject(r); });
1850
+ return this;
1851
+ }
1852
+ if (Array.isArray(rowOrRows)) {
1853
+ // A single positional row aligned to columns().
1854
+ if (this._columns === null) {
1855
+ throw _err("values(array) requires a prior columns([...]) call", "sql-builder/no-columns");
1856
+ }
1857
+ if (rowOrRows.length !== this._columns.length) {
1858
+ throw _err("values(array): " + rowOrRows.length + " values but " +
1859
+ this._columns.length + " columns", "sql-builder/value-count");
1860
+ }
1861
+ this._rows.push(rowOrRows.slice());
1862
+ return this;
1863
+ }
1864
+ if (rowOrRows && typeof rowOrRows === "object") {
1865
+ this._addRowObject(rowOrRows);
1866
+ return this;
1867
+ }
1868
+ throw _err("values() requires a row object, an array of row objects, or a value array",
1869
+ "sql-builder/bad-values");
1870
+ }
1871
+
1872
+ _addRowObject(obj) {
1873
+ var keys = Object.keys(obj);
1874
+ if (keys.length === 0) throw _err("insert row object is empty", "sql-builder/empty-values");
1875
+ if (this._columns === null) {
1876
+ this.columns(keys);
1877
+ }
1878
+ var self = this;
1879
+ var row = this._columns.map(function (c) {
1880
+ if (!Object.prototype.hasOwnProperty.call(obj, c)) {
1881
+ throw _err("insert row is missing column '" + c + "'", "sql-builder/missing-column");
1882
+ }
1883
+ return obj[c];
1884
+ });
1885
+ // Reject extra keys not in the column set (silent-drop would lose data).
1886
+ keys.forEach(function (k) {
1887
+ if (self._columns.indexOf(k) === -1) {
1888
+ throw _err("insert row has column '" + k + "' not in the column set", "sql-builder/extra-column");
1889
+ }
1890
+ });
1891
+ this._rows.push(row);
1892
+ }
1893
+
1894
+ returning(cols) { this._returning = _normReturning(cols); return this; }
1895
+
1896
+ _render() {
1897
+ if (this._columns === null || this._rows.length === 0) {
1898
+ throw _err("insert requires columns + at least one values() row", "sql-builder/empty-values");
1899
+ }
1900
+ var dialect = this._dialect;
1901
+ var quotedCols = this._columns.map(function (c) { return _quoteId(c, dialect); }).join(", ");
1902
+ var holders = [];
1903
+ var params = [];
1904
+ // Each cell renders to `?` (bound), `?::type` (cast), or an allowlisted
1905
+ // SQL function token (NOW() / CURRENT_TIMESTAMP - no param). A
1906
+ // SqlFunction / CastValue cell is identical across rows for a given
1907
+ // column, but the cell is resolved per-row so a multi-row insert can
1908
+ // mix a literal in one row and a function in another.
1909
+ for (var r = 0; r < this._rows.length; r++) {
1910
+ var cells = [];
1911
+ for (var v = 0; v < this._rows[r].length; v++) {
1912
+ var rendered = _renderValueCell(this._rows[r][v], dialect);
1913
+ cells.push(rendered.sql);
1914
+ for (var rp = 0; rp < rendered.params.length; rp++) params.push(rendered.params[rp]);
1915
+ }
1916
+ holders.push("(" + cells.join(", ") + ")");
1917
+ }
1918
+ var sql = "INSERT INTO " + this._table.ref(dialect) + " (" + quotedCols + ") VALUES " +
1919
+ holders.join(", ");
1920
+ sql += _renderReturning(this._returning, dialect);
1921
+ return _emit(sql, params);
1922
+ }
1923
+ }
1924
+
1925
+ // ---- UPDATE ---------------------------------------------------------
1926
+
1927
+ class UpdateBuilder extends Builder {
1928
+ constructor(tableNameOrRef, opts) {
1929
+ super("update", tableNameOrRef, opts);
1930
+ this._set = []; // [{ sql, params }]
1931
+ this._where = new Predicate(this, "AND");
1932
+ this._returning = null;
1933
+ this._allowNoWhere = false;
1934
+ }
1935
+
1936
+ // set(obj) - column->value assignments. set(col, value) - single
1937
+ // assignment. A value may be a bound literal, a b.sql.cast(...) (binds
1938
+ // `?::type`), or a b.sql.fn(...) allowlisted SQL function (emits the
1939
+ // token, no param) - all routed through the single _renderValueCell
1940
+ // choke-point. A SqlFunction / CastValue is itself an object, so the
1941
+ // object-form detection excludes them explicitly.
1942
+ set(colOrObj, value) {
1943
+ var self = this;
1944
+ if (colOrObj && typeof colOrObj === "object" &&
1945
+ !(colOrObj instanceof SqlFunction) && !(colOrObj instanceof CastValue)) {
1946
+ var keys = Object.keys(colOrObj);
1947
+ if (keys.length === 0) throw _err("set object is empty", "sql-builder/empty-set");
1948
+ keys.forEach(function (k) {
1949
+ self._assertColumnMember(k, "update");
1950
+ var cell = _renderValueCell(colOrObj[k], self._dialect);
1951
+ self._set.push({ sql: _quoteId(k, self._dialect) + " = " + cell.sql, params: cell.params });
1952
+ });
1953
+ return this;
1954
+ }
1955
+ this._assertColumnMember(colOrObj, "update");
1956
+ var cell1 = _renderValueCell(value, this._dialect);
1957
+ this._set.push({ sql: _quoteId(colOrObj, this._dialect) + " = " + cell1.sql, params: cell1.params });
1958
+ return this;
1959
+ }
1960
+
1961
+ // setRaw(col, rawExpr, params) - assign a guarded raw expression
1962
+ // (e.g. "count" = "count" + ?). The column is quoted; the expression
1963
+ // is guarded + placeholder-checked.
1964
+ setRaw(col, expr, params, opts) {
1965
+ this._assertColumnMember(col, "update");
1966
+ var checked = _checkRawFragment(expr, params, opts, "setRaw");
1967
+ this._set.push({
1968
+ sql: _quoteId(col, this._dialect) + " = " + checked.sql,
1969
+ params: checked.params,
1970
+ });
1971
+ return this;
1972
+ }
1973
+
1974
+ allowNoWhere() { this._allowNoWhere = true; return this; }
1975
+
1976
+ where() { this._where.where.apply(this._where, arguments); return this; }
1977
+ andWhere() { this._where.andWhere.apply(this._where, arguments); return this; }
1978
+ orWhere() { this._where.orWhere.apply(this._where, arguments); return this; }
1979
+ whereOp(col, op, value) { this._where.whereOp(col, op, value); return this; }
1980
+ orWhereOp(col, op, value) { this._where.orWhereOp(col, op, value); return this; }
1981
+ whereIn(col, values) { this._where.whereIn(col, values); return this; }
1982
+ whereNotIn(col, values) { this._where.whereNotIn(col, values); return this; }
1983
+ orWhereIn(col, values) { this._where.orWhereIn(col, values); return this; }
1984
+ whereInArray(col, values) { this._where.whereInArray(col, values); return this; }
1985
+ orWhereInArray(col, values) { this._where.orWhereInArray(col, values); return this; }
1986
+ whereInJsonEach(col, jsonArrayString) { this._where.whereInJsonEach(col, jsonArrayString); return this; }
1987
+ whereMatch(target, expr) { this._where.whereMatch(target, expr); return this; }
1988
+ whereNull(col) { this._where.whereNull(col); return this; }
1989
+ whereNotNull(col) { this._where.whereNotNull(col); return this; }
1990
+ orWhereNull(col) { this._where.orWhereNull(col); return this; }
1991
+ whereLike(col, term, mode) { this._where.whereLike(col, term, mode); return this; }
1992
+ orWhereLike(col, term, mode) { this._where.orWhereLike(col, term, mode); return this; }
1993
+ whereSub(col, op, sub) { this._where.whereSub(col, op, sub); return this; }
1994
+ whereExists(sub) { this._where.whereExists(sub); return this; }
1995
+ whereNotExists(sub) { this._where.whereNotExists(sub); return this; }
1996
+ whereGroup(closure) { this._where.whereGroup(closure); return this; }
1997
+ orWhereGroup(closure) { this._where.orWhereGroup(closure); return this; }
1998
+ whereRaw(sql, params, opts) { this._where.whereRaw(sql, params, opts); return this; }
1999
+ orWhereRaw(sql, params, opts) { this._where.orWhereRaw(sql, params, opts); return this; }
2000
+
2001
+ returning(cols) { this._returning = _normReturning(cols); return this; }
2002
+
2003
+ _render() {
2004
+ if (this._set.length === 0) throw _err("update requires a set(...) call", "sql-builder/empty-set");
2005
+ if (this._where.length === 0 && !this._allowNoWhere) {
2006
+ throw _err("refusing unconditional update - call where(...) first or allowNoWhere()",
2007
+ "sql-builder/no-where");
2008
+ }
2009
+ var dialect = this._dialect;
2010
+ var params = [];
2011
+ var setPieces = [];
2012
+ for (var s = 0; s < this._set.length; s++) {
2013
+ setPieces.push(this._set[s].sql);
2014
+ for (var sp = 0; sp < this._set[s].params.length; sp++) params.push(this._set[s].params[sp]);
2015
+ }
2016
+ var sql = "UPDATE " + this._table.ref(dialect) + " SET " + setPieces.join(", ");
2017
+ var w = this._where.build();
2018
+ if (w.sql) { sql += " WHERE " + w.sql; for (var wi = 0; wi < w.params.length; wi++) params.push(w.params[wi]); }
2019
+ sql += _renderReturning(this._returning, dialect);
2020
+ return _emit(sql, params);
2021
+ }
2022
+ }
2023
+
2024
+ // ---- DELETE ---------------------------------------------------------
2025
+
2026
+ class DeleteBuilder extends Builder {
2027
+ constructor(tableNameOrRef, opts) {
2028
+ super("delete", tableNameOrRef, opts);
2029
+ this._where = new Predicate(this, "AND");
2030
+ this._returning = null;
2031
+ this._allowNoWhere = false;
2032
+ }
2033
+
2034
+ allowNoWhere() { this._allowNoWhere = true; return this; }
2035
+
2036
+ where() { this._where.where.apply(this._where, arguments); return this; }
2037
+ andWhere() { this._where.andWhere.apply(this._where, arguments); return this; }
2038
+ orWhere() { this._where.orWhere.apply(this._where, arguments); return this; }
2039
+ whereOp(col, op, value) { this._where.whereOp(col, op, value); return this; }
2040
+ orWhereOp(col, op, value) { this._where.orWhereOp(col, op, value); return this; }
2041
+ whereIn(col, values) { this._where.whereIn(col, values); return this; }
2042
+ whereNotIn(col, values) { this._where.whereNotIn(col, values); return this; }
2043
+ orWhereIn(col, values) { this._where.orWhereIn(col, values); return this; }
2044
+ whereInArray(col, values) { this._where.whereInArray(col, values); return this; }
2045
+ orWhereInArray(col, values) { this._where.orWhereInArray(col, values); return this; }
2046
+ whereInJsonEach(col, jsonArrayString) { this._where.whereInJsonEach(col, jsonArrayString); return this; }
2047
+ whereMatch(target, expr) { this._where.whereMatch(target, expr); return this; }
2048
+ whereNull(col) { this._where.whereNull(col); return this; }
2049
+ whereNotNull(col) { this._where.whereNotNull(col); return this; }
2050
+ orWhereNull(col) { this._where.orWhereNull(col); return this; }
2051
+ whereLike(col, term, mode) { this._where.whereLike(col, term, mode); return this; }
2052
+ orWhereLike(col, term, mode) { this._where.orWhereLike(col, term, mode); return this; }
2053
+ whereSub(col, op, sub) { this._where.whereSub(col, op, sub); return this; }
2054
+ whereExists(sub) { this._where.whereExists(sub); return this; }
2055
+ whereNotExists(sub) { this._where.whereNotExists(sub); return this; }
2056
+ whereGroup(closure) { this._where.whereGroup(closure); return this; }
2057
+ orWhereGroup(closure) { this._where.orWhereGroup(closure); return this; }
2058
+ whereRaw(sql, params, opts) { this._where.whereRaw(sql, params, opts); return this; }
2059
+ orWhereRaw(sql, params, opts) { this._where.orWhereRaw(sql, params, opts); return this; }
2060
+
2061
+ returning(cols) { this._returning = _normReturning(cols); return this; }
2062
+
2063
+ _render() {
2064
+ if (this._where.length === 0 && !this._allowNoWhere) {
2065
+ throw _err("refusing unconditional delete - call where(...) first or allowNoWhere()",
2066
+ "sql-builder/no-where");
2067
+ }
2068
+ var dialect = this._dialect;
2069
+ var params = [];
2070
+ var sql = "DELETE FROM " + this._table.ref(dialect);
2071
+ var w = this._where.build();
2072
+ if (w.sql) { sql += " WHERE " + w.sql; for (var wi = 0; wi < w.params.length; wi++) params.push(w.params[wi]); }
2073
+ sql += _renderReturning(this._returning, dialect);
2074
+ return _emit(sql, params);
2075
+ }
2076
+ }
2077
+
2078
+ // ---- UPSERT (the dialect-divergence centrepiece) --------------------
2079
+ //
2080
+ // The one verb that must emit dialect-final syntax - placeholderize +
2081
+ // resolveTables cannot synthesise a conflict clause.
2082
+ //
2083
+ // Postgres / SQLite:
2084
+ // INSERT INTO t (cols) VALUES (?...) ON CONFLICT (keys)
2085
+ // DO UPDATE SET col = EXCLUDED.col [WHERE <guard>] [RETURNING ...]
2086
+ // | DO NOTHING
2087
+ //
2088
+ // MySQL:
2089
+ // INSERT INTO t (cols) VALUES (?...) ON DUPLICATE KEY UPDATE
2090
+ // col = VALUES(col) (or IF(<guard>, VALUES(col), col)
2091
+ // when conflictWhere is present -
2092
+ // MySQL has no per-statement WHERE
2093
+ // on the conflict action)
2094
+ // No WHERE, no RETURNING. A readbackSql SELECT is auto-emitted so the
2095
+ // caller can fetch the upserted row the way RETURNING would have
2096
+ // surfaced it.
2097
+ //
2098
+ // All three conflict actions are required: doUpdate (re-bind specific
2099
+ // columns, optionally to an expression), doUpdateFromExcluded (set the
2100
+ // listed columns to the proposed row's values), and doNothing.
2101
+ class UpsertBuilder extends Builder {
2102
+ constructor(tableNameOrRef, opts) {
2103
+ super("upsert", tableNameOrRef, opts);
2104
+ this._columns = null;
2105
+ this._values = null; // single row, aligned to _columns
2106
+ this._conflictKeys = null;
2107
+ this._action = null; // "update" | "update-excluded" | "nothing"
2108
+ this._updateCols = null; // for update-excluded: [col, ...]
2109
+ this._updateExprs = null; // for update: { col: "?" | rawExpr } map, ordered
2110
+ this._updateParams = null; // params for the update expressions
2111
+ this._conflictWhere = null; // { sql, params } guarded fragment
2112
+ this._returning = null;
2113
+ }
2114
+
2115
+ columns(cols) {
2116
+ if (!Array.isArray(cols) || cols.length === 0) {
2117
+ throw _err("columns() expects a non-empty array", "sql-builder/bad-columns");
2118
+ }
2119
+ var self = this;
2120
+ cols.forEach(function (c) { self._assertColumnMember(c, "upsert"); _validateColumn(c); });
2121
+ this._columns = cols.slice();
2122
+ return this;
2123
+ }
2124
+
2125
+ values(obj) {
2126
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
2127
+ throw _err("upsert values() requires a single row object", "sql-builder/bad-values");
2128
+ }
2129
+ var keys = Object.keys(obj);
2130
+ if (keys.length === 0) throw _err("upsert row object is empty", "sql-builder/empty-values");
2131
+ if (this._columns === null) this.columns(keys);
2132
+ var self = this;
2133
+ this._values = this._columns.map(function (c) {
2134
+ if (!Object.prototype.hasOwnProperty.call(obj, c)) {
2135
+ throw _err("upsert row is missing column '" + c + "'", "sql-builder/missing-column");
2136
+ }
2137
+ return obj[c];
2138
+ });
2139
+ keys.forEach(function (k) {
2140
+ if (self._columns.indexOf(k) === -1) {
2141
+ throw _err("upsert row has column '" + k + "' not in the column set", "sql-builder/extra-column");
2142
+ }
2143
+ });
2144
+ return this;
2145
+ }
2146
+
2147
+ onConflict(keyCols) {
2148
+ var arr = Array.isArray(keyCols) ? keyCols : [keyCols];
2149
+ if (arr.length === 0) throw _err("onConflict requires at least one key column", "sql-builder/bad-conflict");
2150
+ arr.forEach(_validateColumn);
2151
+ this._conflictKeys = arr.slice();
2152
+ return this;
2153
+ }
2154
+
2155
+ // DO UPDATE SET col = EXCLUDED.col for each listed column.
2156
+ doUpdateFromExcluded(cols) {
2157
+ if (!Array.isArray(cols) || cols.length === 0) {
2158
+ throw _err("doUpdateFromExcluded requires a non-empty column array", "sql-builder/conflict-action");
2159
+ }
2160
+ var self = this;
2161
+ cols.forEach(function (c) { self._assertColumnMember(c, "upsert"); _validateColumn(c); });
2162
+ this._action = "update-excluded";
2163
+ this._updateCols = cols.slice();
2164
+ return this;
2165
+ }
2166
+
2167
+ // DO UPDATE SET col = <value-or-expr>. An array of columns sets each
2168
+ // to EXCLUDED.col (Postgres/SQLite) / VALUES(col) (MySQL). An object
2169
+ // { col: "?" } re-binds the column to a supplied param; { col: rawExpr }
2170
+ // sets it to a guarded raw expression. Pass exprParams for any `?` in
2171
+ // the object's expressions, in column order.
2172
+ doUpdate(colsOrMap, exprParams) {
2173
+ if (Array.isArray(colsOrMap)) return this.doUpdateFromExcluded(colsOrMap);
2174
+ if (!colsOrMap || typeof colsOrMap !== "object") {
2175
+ throw _err("doUpdate requires a column array or a { col: expr } map", "sql-builder/conflict-action");
2176
+ }
2177
+ var keys = Object.keys(colsOrMap);
2178
+ if (keys.length === 0) throw _err("doUpdate map is empty", "sql-builder/conflict-action");
2179
+ var self = this;
2180
+ keys.forEach(function (c) { self._assertColumnMember(c, "upsert"); _validateColumn(c); });
2181
+ this._action = "update";
2182
+ this._updateExprs = colsOrMap;
2183
+ this._updateParams = Array.isArray(exprParams) ? exprParams.slice()
2184
+ : (exprParams == null ? [] : [exprParams]);
2185
+ return this;
2186
+ }
2187
+
2188
+ doNothing() { this._action = "nothing"; return this; }
2189
+
2190
+ // The fenced WHERE on the conflict action (Postgres/SQLite); on MySQL
2191
+ // it folds into IF(<guard>, VALUES(col), col). Guarded raw fragment.
2192
+ //
2193
+ // opts.guardColumn names the column the fence protects - the column the
2194
+ // guard expression compares against (e.g. a monotonic fencing token).
2195
+ // On MySQL it is emitted LAST in the SET list so the IF on every other
2196
+ // column evaluates the guard against this column's PRE-UPDATE value
2197
+ // (MySQL evaluates the SET list left to right and a later assignment in
2198
+ // the same statement sees earlier columns' already-updated values - the
2199
+ // IF-eval-order hazard). Ignored on Postgres / SQLite, which apply the
2200
+ // WHERE atomically. When omitted the SET list keeps its declared order
2201
+ // (correct whenever the guard does not also appear as a SET target).
2202
+ conflictWhere(sql, params, opts) {
2203
+ var checked = _checkRawFragment(sql, params, opts, "conflictWhere");
2204
+ var guardColumn = opts && opts.guardColumn;
2205
+ if (guardColumn !== undefined && guardColumn !== null) {
2206
+ _validateColumn(guardColumn);
2207
+ checked.guardColumn = guardColumn;
2208
+ }
2209
+ this._conflictWhere = checked;
2210
+ return this;
2211
+ }
2212
+
2213
+ returning(cols) { this._returning = _normReturning(cols); return this; }
2214
+
2215
+ // Render the VALUES tuple through the same _renderValueCell choke-point
2216
+ // INSERT uses, so an upsert VALUES cell may be a bound literal, a
2217
+ // b.sql.cast(...) (`?::type`), or a b.sql.fn(...) allowlisted function
2218
+ // (`NOW()` / `CURRENT_TIMESTAMP`, no param). Without this the wrapper
2219
+ // objects would leak straight into params (a SqlFunction / CastValue is
2220
+ // an object, not a scalar) and the driver would mis-bind them.
2221
+ _renderValuesTuple(dialect) {
2222
+ var cells = [];
2223
+ var params = [];
2224
+ for (var i = 0; i < this._values.length; i += 1) {
2225
+ var rendered = _renderValueCell(this._values[i], dialect);
2226
+ cells.push(rendered.sql);
2227
+ for (var p = 0; p < rendered.params.length; p += 1) params.push(rendered.params[p]);
2228
+ }
2229
+ return { sql: cells.join(", "), params: params };
2230
+ }
2231
+
2232
+ _render() {
2233
+ if (this._columns === null || this._values === null) {
2234
+ throw _err("upsert requires columns + values()", "sql-builder/empty-values");
2235
+ }
2236
+ if (this._action === null) {
2237
+ throw _err("upsert requires a conflict action - doUpdate(...) / " +
2238
+ "doUpdateFromExcluded(...) / doNothing()", "sql-builder/conflict-action");
2239
+ }
2240
+ if (this._action !== "nothing" && this._conflictKeys === null && this._dialect !== "mysql") {
2241
+ throw _err("upsert doUpdate requires onConflict(keys) on " + this._dialect,
2242
+ "sql-builder/bad-conflict");
2243
+ }
2244
+ return this._dialect === "mysql" ? this._renderMysql() : this._renderStandard();
2245
+ }
2246
+
2247
+ // Postgres + SQLite: ON CONFLICT (keys) DO UPDATE ... [WHERE] [RETURNING].
2248
+ _renderStandard() {
2249
+ var dialect = this._dialect;
2250
+ var quotedCols = this._columns.map(function (c) { return _quoteId(c, dialect); }).join(", ");
2251
+ var tuple = this._renderValuesTuple(dialect);
2252
+ var params = tuple.params;
2253
+
2254
+ var sql = "INSERT INTO " + this._table.ref(dialect) + " (" + quotedCols + ") VALUES (" +
2255
+ tuple.sql + ")";
2256
+
2257
+ if (this._action === "nothing") {
2258
+ sql += " ON CONFLICT" + this._conflictTarget(dialect) + " DO NOTHING";
2259
+ } else {
2260
+ var setClause = this._buildStandardSet(dialect);
2261
+ sql += " ON CONFLICT" + this._conflictTarget(dialect) + " DO UPDATE SET " + setClause.sql;
2262
+ for (var i = 0; i < setClause.params.length; i++) params.push(setClause.params[i]);
2263
+ if (this._conflictWhere) {
2264
+ sql += " WHERE " + this._conflictWhere.sql;
2265
+ for (var w = 0; w < this._conflictWhere.params.length; w++) params.push(this._conflictWhere.params[w]);
2266
+ }
2267
+ }
2268
+ sql += _renderReturning(this._returning, dialect);
2269
+ return _emit(sql, params);
2270
+ }
2271
+
2272
+ _conflictTarget(dialect) {
2273
+ if (this._conflictKeys === null) return "";
2274
+ var keys = this._conflictKeys.map(function (k) { return _quoteId(k, dialect); }).join(", ");
2275
+ return " (" + keys + ")";
2276
+ }
2277
+
2278
+ _buildStandardSet(dialect) {
2279
+ var pieces = [];
2280
+ var params = [];
2281
+ if (this._action === "update-excluded") {
2282
+ for (var i = 0; i < this._updateCols.length; i++) {
2283
+ var c = this._updateCols[i];
2284
+ pieces.push(_quoteId(c, dialect) + " = EXCLUDED." + _quoteId(c, dialect));
2285
+ }
2286
+ } else {
2287
+ // action === "update": { col: expr } map. "?" re-binds to a param;
2288
+ // any other string is a guarded raw expression.
2289
+ var keys = Object.keys(this._updateExprs);
2290
+ var paramCursor = 0;
2291
+ for (var k = 0; k < keys.length; k++) {
2292
+ var col = keys[k];
2293
+ var expr = this._updateExprs[col];
2294
+ if (expr === "?") {
2295
+ pieces.push(_quoteId(col, dialect) + " = ?");
2296
+ params.push(this._updateParams[paramCursor]);
2297
+ paramCursor += 1;
2298
+ } else if (typeof expr === "string") {
2299
+ // Guarded raw expression (e.g. "EXCLUDED.\"count\" + 1"). Its
2300
+ // own `?` placeholders draw from _updateParams in order.
2301
+ var remaining = this._updateParams.slice(paramCursor);
2302
+ var needed = _countPlaceholders(expr);
2303
+ var exprParams = remaining.slice(0, needed);
2304
+ var checked = _checkRawFragment(expr, exprParams, { allowLiterals: false }, "doUpdate");
2305
+ pieces.push(_quoteId(col, dialect) + " = " + checked.sql);
2306
+ for (var ep = 0; ep < checked.params.length; ep++) params.push(checked.params[ep]);
2307
+ paramCursor += needed;
2308
+ } else {
2309
+ throw _err("doUpdate expression for '" + col + "' must be '?' or a raw SQL string",
2310
+ "sql-builder/conflict-action");
2311
+ }
2312
+ }
2313
+ }
2314
+ return { sql: pieces.join(", "), params: params };
2315
+ }
2316
+
2317
+ // MySQL: ON DUPLICATE KEY UPDATE col = VALUES(col). No WHERE, no
2318
+ // RETURNING. conflictWhere folds into IF(<guard>, VALUES(col), col).
2319
+ // The guard column is emitted LAST so MySQL's left-to-right evaluation
2320
+ // of the SET list sees the other columns' pre-guard values when the
2321
+ // guard references them (the IF-eval-order hazard). A readbackSql
2322
+ // SELECT is returned alongside so the caller can fetch the row that
2323
+ // RETURNING would have surfaced.
2324
+ _renderMysql() {
2325
+ var dialect = "mysql";
2326
+ var quotedCols = this._columns.map(function (c) { return _quoteId(c, dialect); }).join(", ");
2327
+ var tuple = this._renderValuesTuple(dialect);
2328
+ var params = tuple.params;
2329
+
2330
+ var sql = "INSERT INTO " + this._table.ref(dialect) + " (" + quotedCols + ") VALUES (" +
2331
+ tuple.sql + ")";
2332
+
2333
+ if (this._action === "nothing") {
2334
+ // MySQL has no DO NOTHING; the idiom is to no-op a key column.
2335
+ // Assign the first conflict / first column to itself so the row is
2336
+ // left unchanged on duplicate.
2337
+ var noopCol = (this._conflictKeys && this._conflictKeys[0]) || this._columns[0];
2338
+ sql += " ON DUPLICATE KEY UPDATE " + _quoteId(noopCol, dialect) + " = " +
2339
+ _quoteId(noopCol, dialect);
2340
+ } else {
2341
+ var setBuild = this._buildMysqlSet(dialect);
2342
+ sql += " ON DUPLICATE KEY UPDATE " + setBuild.sql;
2343
+ for (var i = 0; i < setBuild.params.length; i++) params.push(setBuild.params[i]);
2344
+ }
2345
+
2346
+ var out = _emit(sql, params);
2347
+ // RETURNING is unavailable on MySQL upsert - emit a readback SELECT
2348
+ // keyed on the conflict columns so the caller fetches the row. Validate
2349
+ // it through the same output gate.
2350
+ if (this._returning !== null) {
2351
+ var rb = this._buildReadback(dialect);
2352
+ _assertEmittable(rb.sql, rb.params);
2353
+ out.readbackSql = rb;
2354
+ }
2355
+ return out;
2356
+ }
2357
+
2358
+ _buildMysqlSet(dialect) {
2359
+ var guardSqlText = this._conflictWhere ? this._conflictWhere.sql : null;
2360
+ var guardParams = this._conflictWhere ? this._conflictWhere.params : [];
2361
+
2362
+ // Resolve the ordered (col, assignment-RHS) list WITHOUT the guard
2363
+ // wrap first; then, when a guard is present, wrap each RHS in
2364
+ // IF(<guard>, <rhs>, col) and order the guard column (a column the
2365
+ // guard references, if it is itself a set target) LAST.
2366
+ var assignments = []; // [{ col, rhs, rhsParams }]
2367
+ if (this._action === "update-excluded") {
2368
+ for (var i = 0; i < this._updateCols.length; i++) {
2369
+ var c = this._updateCols[i];
2370
+ assignments.push({ col: c, rhs: "VALUES(" + _quoteId(c, dialect) + ")", rhsParams: [] });
2371
+ }
2372
+ } else {
2373
+ var keys = Object.keys(this._updateExprs);
2374
+ var paramCursor = 0;
2375
+ for (var k = 0; k < keys.length; k++) {
2376
+ var col = keys[k];
2377
+ var expr = this._updateExprs[col];
2378
+ if (expr === "?") {
2379
+ assignments.push({ col: col, rhs: "?", rhsParams: [this._updateParams[paramCursor]] });
2380
+ paramCursor += 1;
2381
+ } else if (typeof expr === "string") {
2382
+ var needed = _countPlaceholders(expr);
2383
+ var exprParams = this._updateParams.slice(paramCursor, paramCursor + needed);
2384
+ var checked = _checkRawFragment(expr, exprParams, { allowLiterals: false }, "doUpdate");
2385
+ assignments.push({ col: col, rhs: checked.sql, rhsParams: checked.params });
2386
+ paramCursor += needed;
2387
+ } else {
2388
+ throw _err("doUpdate expression for '" + col + "' must be '?' or a raw SQL string",
2389
+ "sql-builder/conflict-action");
2390
+ }
2391
+ }
2392
+ }
2393
+
2394
+ var pieces = [];
2395
+ var params = [];
2396
+ if (guardSqlText === null) {
2397
+ for (var a = 0; a < assignments.length; a++) {
2398
+ pieces.push(_quoteId(assignments[a].col, dialect) + " = " + assignments[a].rhs);
2399
+ for (var ap = 0; ap < assignments[a].rhsParams.length; ap++) params.push(assignments[a].rhsParams[ap]);
2400
+ }
2401
+ return { sql: pieces.join(", "), params: params };
2402
+ }
2403
+
2404
+ // Guarded: col = IF(<guard>, <rhs>, col). The guard's own params are
2405
+ // bound once per assignment (the guard expression repeats per SET
2406
+ // target in MySQL's UPDATE list). The guard column - the column the
2407
+ // fenced comparison protects - is emitted last so the IF on the
2408
+ // other columns evaluates against this column's pre-update value.
2409
+ var guardColName = this._conflictWhere && this._conflictWhere.guardColumn
2410
+ ? this._conflictWhere.guardColumn : null;
2411
+ var ordered = assignments.slice();
2412
+ if (guardColName) {
2413
+ ordered.sort(function (x, y) {
2414
+ var xg = x.col === guardColName ? 1 : 0;
2415
+ var yg = y.col === guardColName ? 1 : 0;
2416
+ return xg - yg;
2417
+ });
2418
+ }
2419
+ for (var o = 0; o < ordered.length; o++) {
2420
+ var qc = _quoteId(ordered[o].col, dialect);
2421
+ pieces.push(qc + " = IF(" + guardSqlText + ", " + ordered[o].rhs + ", " + qc + ")");
2422
+ for (var gp = 0; gp < guardParams.length; gp++) params.push(guardParams[gp]);
2423
+ for (var rp = 0; rp < ordered[o].rhsParams.length; rp++) params.push(ordered[o].rhsParams[rp]);
2424
+ }
2425
+ return { sql: pieces.join(", "), params: params };
2426
+ }
2427
+
2428
+ // Readback SELECT for the MySQL upsert path - fetch the upserted row by
2429
+ // its conflict key(s) bound to the proposed values, projecting the
2430
+ // RETURNING column list (or "*").
2431
+ _buildReadback(dialect) {
2432
+ var keys = this._conflictKeys || [];
2433
+ if (keys.length === 0) {
2434
+ // No declared conflict key - read back by the full proposed row's
2435
+ // first column as a best-effort key.
2436
+ keys = [this._columns[0]];
2437
+ }
2438
+ var proj = (this._returning === "*" || this._returning === null)
2439
+ ? "*"
2440
+ : this._returning.map(function (c) { return _quoteId(c, dialect); }).join(", ");
2441
+ var sql = "SELECT " + proj + " FROM " + this._table.ref(dialect);
2442
+ var params = [];
2443
+ var conds = [];
2444
+ for (var i = 0; i < keys.length; i++) {
2445
+ var idx = this._columns.indexOf(keys[i]);
2446
+ if (idx === -1) {
2447
+ throw _err("upsert readback: conflict key '" + keys[i] + "' is not in the value set",
2448
+ "sql-builder/bad-conflict");
2449
+ }
2450
+ var keyVal = this._values[idx];
2451
+ if (keyVal instanceof SqlFunction) {
2452
+ // A server-evaluated function (NOW() / CURRENT_TIMESTAMP / ...) as a
2453
+ // conflict key has no stable readback identity: the row holds the value
2454
+ // the server computed at INSERT time, which a fresh evaluation in this
2455
+ // WHERE would never equal, so the readback would silently match zero
2456
+ // rows. Refuse rather than return a wrong (empty) result.
2457
+ throw _err("upsert readback: conflict key '" + keys[i] + "' is a " +
2458
+ "server-evaluated function (b.sql.fn) with no stable readback identity " +
2459
+ "- use a literal/cast conflict key or read the row back explicitly",
2460
+ "sql-builder/bad-conflict");
2461
+ }
2462
+ // Resolve the key value through the same cell renderer the VALUES tuple
2463
+ // uses, so a b.sql.cast(...) conflict key emits `col = CAST(? AS type)`
2464
+ // (Postgres `col = ?::type`) binding the inner value, and a plain scalar
2465
+ // still emits `col = ?` binding the value unchanged.
2466
+ var cell = _renderValueCell(keyVal, dialect);
2467
+ conds.push(_quoteId(keys[i], dialect) + " = " + cell.sql);
2468
+ for (var cp = 0; cp < cell.params.length; cp++) params.push(cell.params[cp]);
2469
+ }
2470
+ sql += " WHERE " + conds.join(" AND ");
2471
+ return { sql: sql, params: params };
2472
+ }
2473
+ }
2474
+
2475
+ // RETURNING normalization - "*" or an array of validated columns.
2476
+ function _normReturning(cols) {
2477
+ if (cols === "*" || cols === undefined || cols === null) return "*";
2478
+ var arr = Array.isArray(cols) ? cols : [cols];
2479
+ arr.forEach(_validateColumn);
2480
+ return arr.slice();
2481
+ }
2482
+
2483
+ function _renderReturning(returning, dialect) {
2484
+ if (returning === null) return "";
2485
+ // MySQL / MariaDB do not support RETURNING on INSERT / UPDATE / DELETE.
2486
+ // Emitting it would parse-error at the driver; refuse at build with a
2487
+ // clear message so the operator runs an explicit read-back SELECT.
2488
+ // (The upsert verb's MySQL path already auto-emits a readback instead of
2489
+ // reaching here.)
2490
+ if (dialect === "mysql") {
2491
+ throw _err("RETURNING is not supported on MySQL for this verb - run a " +
2492
+ "read-back SELECT on the affected key instead", "sql-builder/returning-unsupported");
2493
+ }
2494
+ if (returning === "*") return " RETURNING *";
2495
+ return " RETURNING " + returning.map(function (c) { return _quoteId(c, dialect); }).join(", ");
2496
+ }
2497
+
2498
+ // ---- DDL builders ---------------------------------------------------
2499
+ //
2500
+ // Operator app-schema parity: createTable / createIndex / alterTable /
2501
+ // dropTable, dialect-aware and quote-by-construction, reusing the
2502
+ // framework's own type vocabulary (no fork of the type map). DDL is
2503
+ // declarative - these return { sql } (no params) since DDL binds no
2504
+ // values.
2505
+
2506
+ /**
2507
+ * @primitive b.sql.createTable
2508
+ * @signature b.sql.createTable(name, columns, opts?)
2509
+ * @since 0.14.29
2510
+ * @status stable
2511
+ * @related b.sql.createIndex, b.sql.alterTable, b.sql.dropTable
2512
+ *
2513
+ * Build a `CREATE TABLE` statement with every identifier quoted by
2514
+ * construction and every column type drawn from the framework's own
2515
+ * type map (so an operator app-schema table is portable across the same
2516
+ * dialects the framework tables are). `columns` is an array of column
2517
+ * specs; each `{ name, type, constraints?, primaryKey?, notNull?,
2518
+ * unique?, default? }`. The `type` is a logical name (`int` / `text` /
2519
+ * `blob` / `boolean` / `real` / `numeric` / `timestamp` / `json`) mapped
2520
+ * to the dialect token, or a verbatim dialect type string. Emits
2521
+ * `IF NOT EXISTS` by default so re-running is idempotent.
2522
+ *
2523
+ * @opts
2524
+ * dialect: string, // postgres | sqlite | mysql (default sqlite)
2525
+ * ifNotExists: boolean, // default true
2526
+ * primaryKey: array, // composite PK column list (table-level)
2527
+ *
2528
+ * @example
2529
+ * var b = require("@blamejs/core");
2530
+ * b.sql.createTable("widget", [
2531
+ * { name: "id", type: "int", primaryKey: true },
2532
+ * { name: "name", type: "text", notNull: true },
2533
+ * ], { dialect: "postgres" }).sql;
2534
+ * // -> 'CREATE TABLE IF NOT EXISTS widget ("id" BIGINT PRIMARY KEY, "name" TEXT NOT NULL)'
2535
+ * // (the bare default table name is the clusterStorage rewrite
2536
+ * // target; pass a prefix or schema to quote it)
2537
+ */
2538
+ function createTable(name, columns, opts) {
2539
+ opts = opts || {};
2540
+ var dialect = _normDialect(opts.dialect);
2541
+ var ref = _normTableRef(name, opts);
2542
+ if (!Array.isArray(columns) || columns.length === 0) {
2543
+ throw _err("createTable requires a non-empty columns array", "sql-builder/bad-columns");
2544
+ }
2545
+ var pieces = columns.map(function (c) {
2546
+ if (typeof c !== "object" || c === null || typeof c.name !== "string") {
2547
+ throw _err("createTable column must be { name, type, ... }", "sql-builder/bad-column");
2548
+ }
2549
+ _validateColumn(c.name);
2550
+ var qn = _quoteId(c.name, dialect);
2551
+ // Auto-increment / identity PK. This MUST diverge by dialect or an app
2552
+ // developed on the default sqlite dialect (where INTEGER PRIMARY KEY is
2553
+ // a rowid alias that auto-increments implicitly) breaks on the
2554
+ // postgres / mysql backend the builder advertises portability to (a
2555
+ // plain BIGINT PRIMARY KEY there does NOT default a value). postgres ->
2556
+ // BIGSERIAL (implies the int type + sequence default); sqlite -> INTEGER
2557
+ // PRIMARY KEY AUTOINCREMENT (MUST be INTEGER, not BIGINT); mysql ->
2558
+ // BIGINT AUTO_INCREMENT. An identity column is the primary key and takes
2559
+ // no DEFAULT.
2560
+ if (c.autoIncrement || c.serial) {
2561
+ if (c.default !== undefined) {
2562
+ throw _err("createTable: auto-increment column '" + c.name +
2563
+ "' cannot also declare a default", "sql-builder/bad-column");
2564
+ }
2565
+ var idDef;
2566
+ if (dialect === "postgres") idDef = qn + " BIGSERIAL PRIMARY KEY";
2567
+ else if (dialect === "mysql") idDef = qn + " BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY";
2568
+ else idDef = qn + " INTEGER PRIMARY KEY AUTOINCREMENT";
2569
+ if (typeof c.constraints === "string" && c.constraints.length > 0) {
2570
+ var idCk = _checkRawFragment(c.constraints, [], { allowLiterals: true }, "createTable.constraints");
2571
+ idDef += " " + idCk.sql;
2572
+ }
2573
+ return idDef;
2574
+ }
2575
+ var def = qn + " " + _ddlType(c.type, dialect);
2576
+ if (c.primaryKey) def += " PRIMARY KEY";
2577
+ if (c.notNull) def += " NOT NULL";
2578
+ if (c.unique) def += " UNIQUE";
2579
+ if (c.default !== undefined) def += " DEFAULT " + _ddlDefault(c.default);
2580
+ // Foreign key: a quote-by-construction REFERENCES clause (string table
2581
+ // name or { table, column?, onDelete?, onUpdate? }). Identifiers are
2582
+ // validated + quoted; the referential actions are allowlisted.
2583
+ if (c.references !== undefined && c.references !== false) {
2584
+ def += _ddlReferences(c.references, dialect, opts);
2585
+ }
2586
+ if (typeof c.constraints === "string" && c.constraints.length > 0) {
2587
+ // Verbatim constraint clause (CHECK / REFERENCES). Guarded so an
2588
+ // operator-influenced constraint can't smuggle a statement.
2589
+ var checked = _checkRawFragment(c.constraints, [], { allowLiterals: true }, "createTable.constraints");
2590
+ def += " " + checked.sql;
2591
+ }
2592
+ return def;
2593
+ });
2594
+ if (Array.isArray(opts.primaryKey) && opts.primaryKey.length > 0) {
2595
+ // A column-level primary key (primaryKey / autoIncrement / serial) and a
2596
+ // composite opts.primaryKey are mutually exclusive: emitting both produces
2597
+ // two PRIMARY KEY clauses, which sqlite / Postgres / MySQL all reject at the
2598
+ // driver. Catch the contradiction at build time with a clear error rather
2599
+ // than a cryptic "multiple primary keys" failure mid-migration. Lives in the
2600
+ // shared composer so defineTable is covered too.
2601
+ var colHasPk = columns.some(function (c) {
2602
+ return c && (c.primaryKey || c.autoIncrement || c.serial);
2603
+ });
2604
+ if (colHasPk) {
2605
+ throw _err("createTable: a column-level primary key (primaryKey / " +
2606
+ "autoIncrement / serial) and a composite opts.primaryKey are mutually " +
2607
+ "exclusive", "sql-builder/bad-column");
2608
+ }
2609
+ opts.primaryKey.forEach(_validateColumn);
2610
+ pieces.push("PRIMARY KEY (" + opts.primaryKey.map(function (k) {
2611
+ return _quoteId(k, dialect);
2612
+ }).join(", ") + ")");
2613
+ }
2614
+ var ifNot = opts.ifNotExists === false ? "" : "IF NOT EXISTS ";
2615
+ var sql = "CREATE TABLE " + ifNot + ref.ref(dialect) + " (" + pieces.join(", ") + ")";
2616
+ // Route the finished DDL through the same emittable gate every SELECT /
2617
+ // INSERT / UPDATE / DELETE verb uses: it refuses a stacked top-level ';', a
2618
+ // NUL, an unterminated quote, and unbalanced parens - a defence-in-depth
2619
+ // backstop behind the per-column type / constraint guards.
2620
+ return _assertCatalogEmittable(sql, []);
2621
+ }
2622
+
2623
+ // DDL DEFAULT renderer - numeric / boolean / null inline; a string
2624
+ // default is emitted as a single-quoted SQL literal with the quote
2625
+ // doubled to escape it (DDL defaults are static, operator-controlled,
2626
+ // and never bound).
2627
+ function _ddlDefault(value) {
2628
+ if (value === null) return "NULL";
2629
+ if (typeof value === "number" && isFinite(value)) return String(value);
2630
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
2631
+ if (typeof value === "string") return "'" + value.replace(/'/g, "''") + "'";
2632
+ throw _err("createTable column default must be a string, number, boolean, or null",
2633
+ "sql-builder/bad-default");
2634
+ }
2635
+
2636
+ // Referential actions allowed on a foreign key (ON DELETE / ON UPDATE).
2637
+ var FK_ACTIONS = Object.freeze({
2638
+ "CASCADE": true, "SET NULL": true, "SET DEFAULT": true, "RESTRICT": true, "NO ACTION": true,
2639
+ });
2640
+
2641
+ // Quote-by-construction REFERENCES clause. `references` is a table-name
2642
+ // string (referenced column defaults to "id") or { table, column?, onDelete?,
2643
+ // onUpdate? }. The referenced table inherits the parent table's prefix /
2644
+ // schema so a prefixed deployment's FK target resolves to the same namespace.
2645
+ function _ddlReferences(references, dialect, opts) {
2646
+ var spec = typeof references === "string" ? { table: references } : references;
2647
+ if (!spec || typeof spec.table !== "string" || spec.table.length === 0) {
2648
+ throw _err("column 'references' must be a table name or { table, column?, onDelete?, onUpdate? }",
2649
+ "sql-builder/bad-references");
2650
+ }
2651
+ var refTable = _normTableRef(spec.table, opts || {});
2652
+ var refCol = spec.column || "id";
2653
+ _validateColumn(refCol);
2654
+ var out = " REFERENCES " + refTable.ref(dialect) + " (" + _quoteId(refCol, dialect) + ")";
2655
+ ["onDelete", "onUpdate"].forEach(function (k) {
2656
+ if (spec[k] === undefined || spec[k] === null) return;
2657
+ var action = String(spec[k]).toUpperCase();
2658
+ if (FK_ACTIONS[action] !== true) {
2659
+ throw _err("invalid " + k + " referential action '" + spec[k] +
2660
+ "' (CASCADE / SET NULL / SET DEFAULT / RESTRICT / NO ACTION)", "sql-builder/bad-fk-action");
2661
+ }
2662
+ out += (k === "onDelete" ? " ON DELETE " : " ON UPDATE ") + action;
2663
+ });
2664
+ return out;
2665
+ }
2666
+
2667
+ /**
2668
+ * @primitive b.sql.createIndex
2669
+ * @signature b.sql.createIndex(name, tableName, columns, opts?)
2670
+ * @since 0.14.29
2671
+ * @status stable
2672
+ * @related b.sql.createTable, b.sql.dropTable
2673
+ *
2674
+ * Build a `CREATE INDEX` statement, identifiers quoted by construction,
2675
+ * `IF NOT EXISTS` by default. `columns` is the indexed column list (each
2676
+ * quoted); `opts.unique` emits a `UNIQUE INDEX`.
2677
+ *
2678
+ * @opts
2679
+ * dialect: string, // postgres | sqlite | mysql (default sqlite)
2680
+ * unique: boolean, // default false
2681
+ * ifNotExists: boolean, // default true
2682
+ * where: string, // partial-index predicate (guarded raw fragment)
2683
+ * whereParams: Array, // bound params for the partial-index predicate
2684
+ *
2685
+ * A partial index (`opts.where`) narrows the index to rows matching a
2686
+ * boolean predicate - the publisher's pending-row index
2687
+ * (`WHERE status = 'pending'`) is the canonical case. The predicate rides
2688
+ * the same `b.guardSql`-gated raw-fragment path as `whereRaw` (a static
2689
+ * operator-controlled literal opts in via `allowLiterals`); MySQL has no
2690
+ * partial index, so it throws there.
2691
+ *
2692
+ * @example
2693
+ * var b = require("@blamejs/core");
2694
+ * b.sql.createIndex("idx_widget_name", "widget", ["name"],
2695
+ * { dialect: "sqlite", unique: true }).sql;
2696
+ * // -> 'CREATE UNIQUE INDEX IF NOT EXISTS "idx_widget_name" ON widget ("name")'
2697
+ * // (the index name is quoted; the bare default table stays the
2698
+ * // clusterStorage rewrite target)
2699
+ */
2700
+ function createIndex(name, tableName, columns, opts) {
2701
+ opts = opts || {};
2702
+ var dialect = _normDialect(opts.dialect);
2703
+ _validateColumn(name);
2704
+ var ref = _normTableRef(tableName, opts);
2705
+ if (!Array.isArray(columns) || columns.length === 0) {
2706
+ throw _err("createIndex requires a non-empty columns array", "sql-builder/bad-columns");
2707
+ }
2708
+ columns.forEach(_validateColumn);
2709
+ var ifNot = opts.ifNotExists === false ? "" : "IF NOT EXISTS ";
2710
+ var cols = columns.map(function (c) { return _quoteId(c, dialect); }).join(", ");
2711
+ var sql = "CREATE " + (opts.unique ? "UNIQUE " : "") + "INDEX " + ifNot +
2712
+ _quoteId(name, dialect) + " ON " + ref.ref(dialect) + " (" + cols + ")";
2713
+ var params = [];
2714
+ if (opts.where !== undefined && opts.where !== null) {
2715
+ if (dialect === "mysql") {
2716
+ throw _err("createIndex: partial index (where) is Postgres / SQLite-only " +
2717
+ "(MySQL has no partial index)", "sql-builder/partial-index-unsupported");
2718
+ }
2719
+ var checked = _checkRawFragment(opts.where, opts.whereParams,
2720
+ { allowLiterals: opts.allowLiterals !== false }, "createIndex.where");
2721
+ sql += " WHERE " + checked.sql;
2722
+ params = checked.params;
2723
+ }
2724
+ return { sql: sql, params: params };
2725
+ }
2726
+
2727
+ /**
2728
+ * @primitive b.sql.alterTable
2729
+ * @signature b.sql.alterTable(name, change, opts?)
2730
+ * @since 0.14.29
2731
+ * @status stable
2732
+ * @related b.sql.createTable, b.sql.dropTable
2733
+ *
2734
+ * Build an `ALTER TABLE` statement. `change` is one of
2735
+ * `{ addColumn: { name, type, ... } }`,
2736
+ * `{ dropColumn: "name" }`, or
2737
+ * `{ renameColumn: { from, to } }` - each identifier quoted, the
2738
+ * add-column type drawn from the framework type map.
2739
+ *
2740
+ * @opts
2741
+ * dialect: string, // postgres | sqlite | mysql (default sqlite)
2742
+ *
2743
+ * @example
2744
+ * var b = require("@blamejs/core");
2745
+ * b.sql.alterTable("widget", { addColumn: { name: "active", type: "boolean" } },
2746
+ * { dialect: "postgres" }).sql;
2747
+ * // -> 'ALTER TABLE widget ADD COLUMN "active" BOOLEAN'
2748
+ * // (bare default table name; the added column is quoted)
2749
+ */
2750
+ function alterTable(name, change, opts) {
2751
+ opts = opts || {};
2752
+ var dialect = _normDialect(opts.dialect);
2753
+ var ref = _normTableRef(name, opts);
2754
+ if (!change || typeof change !== "object") {
2755
+ throw _err("alterTable requires a change descriptor", "sql-builder/bad-alter");
2756
+ }
2757
+ var head = "ALTER TABLE " + ref.ref(dialect) + " ";
2758
+ if (change.addColumn) {
2759
+ var col = change.addColumn;
2760
+ if (typeof col.name !== "string") throw _err("addColumn requires a name", "sql-builder/bad-column");
2761
+ _validateColumn(col.name);
2762
+ var def = _quoteId(col.name, dialect) + " " + _ddlType(col.type, dialect);
2763
+ if (col.notNull) def += " NOT NULL";
2764
+ if (col.unique) def += " UNIQUE";
2765
+ if (col.default !== undefined) def += " DEFAULT " + _ddlDefault(col.default);
2766
+ return _assertCatalogEmittable(head + "ADD COLUMN " + def, []);
2767
+ }
2768
+ if (change.dropColumn) {
2769
+ _validateColumn(change.dropColumn);
2770
+ return _assertCatalogEmittable(head + "DROP COLUMN " + _quoteId(change.dropColumn, dialect), []);
2771
+ }
2772
+ if (change.renameColumn) {
2773
+ var rc = change.renameColumn;
2774
+ if (typeof rc.from !== "string" || typeof rc.to !== "string") {
2775
+ throw _err("renameColumn requires { from, to }", "sql-builder/bad-alter");
2776
+ }
2777
+ _validateColumn(rc.from);
2778
+ _validateColumn(rc.to);
2779
+ return _assertCatalogEmittable(
2780
+ head + "RENAME COLUMN " + _quoteId(rc.from, dialect) + " TO " + _quoteId(rc.to, dialect), []);
2781
+ }
2782
+ throw _err("alterTable change must be addColumn / dropColumn / renameColumn",
2783
+ "sql-builder/bad-alter");
2784
+ }
2785
+
2786
+ /**
2787
+ * @primitive b.sql.dropTable
2788
+ * @signature b.sql.dropTable(name, opts?)
2789
+ * @since 0.14.29
2790
+ * @status stable
2791
+ * @related b.sql.createTable, b.sql.alterTable
2792
+ *
2793
+ * Build a `DROP TABLE` statement, identifier quoted, `IF EXISTS` by
2794
+ * default so dropping a missing table is a no-op.
2795
+ *
2796
+ * @opts
2797
+ * dialect: string, // postgres | sqlite | mysql (default sqlite)
2798
+ * ifExists: boolean, // default true
2799
+ * cascade: boolean, // default false (Postgres CASCADE)
2800
+ *
2801
+ * @example
2802
+ * var b = require("@blamejs/core");
2803
+ * b.sql.dropTable("widget", { dialect: "postgres", cascade: true }).sql;
2804
+ * // -> 'DROP TABLE IF EXISTS widget CASCADE'
2805
+ * // (bare default table name; the clusterStorage rewrite target)
2806
+ */
2807
+ function dropTable(name, opts) {
2808
+ opts = opts || {};
2809
+ var dialect = _normDialect(opts.dialect);
2810
+ var ref = _normTableRef(name, opts);
2811
+ var ifExists = opts.ifExists === false ? "" : "IF EXISTS ";
2812
+ var sql = "DROP TABLE " + ifExists + ref.ref(dialect);
2813
+ if (opts.cascade && dialect === "postgres") sql += " CASCADE";
2814
+ return { sql: sql, params: [] };
2815
+ }
2816
+
2817
+ // ---- sqlite virtual table (FTS5) ------------------------------------
2818
+ //
2819
+ // The sqlite-only virtual-table DDL b.sql's general createTable has no
2820
+ // form for - the FTS5 full-text index the mail store's sealed-token
2821
+ // search runs MATCH against. The supported module is `fts5` (the only one
2822
+ // a framework primitive ships against); the column list + tokenizer
2823
+ // option are quoted / allowlisted by construction so no operator-supplied
2824
+ // token reaches the DDL raw.
2825
+
2826
+ // The tokenizers an operator may name - the fixed FTS5 built-in set. A
2827
+ // custom tokenizer (a loadable extension) is outside the framework's
2828
+ // supported surface and refused, so no arbitrary token reaches the
2829
+ // `tokenize = '...'` option.
2830
+ var FTS5_TOKENIZERS = Object.freeze({
2831
+ "unicode61": true, "ascii": true, "porter": true, "trigram": true,
2832
+ });
2833
+ // The tokenizer ARGUMENT tokens FTS5 accepts after the tokenizer name
2834
+ // (e.g. `unicode61 remove_diacritics 2`). A fixed allowlist so the whole
2835
+ // `tokenize` option is builder-controlled end to end.
2836
+ var FTS5_TOKENIZER_ARGS = Object.freeze({
2837
+ "remove_diacritics": true, "0": true, "1": true, "2": true,
2838
+ "categories": true, "tokenchars": true, "separators": true, "case_sensitive": true,
2839
+ });
2840
+
2841
+ /**
2842
+ * @primitive b.sql.createVirtualTable
2843
+ * @signature b.sql.createVirtualTable(name, opts)
2844
+ * @since 0.15.0
2845
+ * @status stable
2846
+ * @related b.sql.createTable, b.sql.select, b.sql.createIndex
2847
+ *
2848
+ * Build a sqlite `CREATE VIRTUAL TABLE ... USING fts5(...)` statement for
2849
+ * a full-text index - the construct `b.sql.createTable` has no form for.
2850
+ * `opts.columns` is the FTS5 column list; each entry is a column name (a
2851
+ * searched column) or `{ name, unindexed: true }` (a stored-but-not-
2852
+ * searched column, the join key). `opts.tokenize` names a built-in FTS5
2853
+ * tokenizer (`unicode61` / `ascii` / `porter` / `trigram`) and optional
2854
+ * allowlisted arguments (`remove_diacritics 2`); a custom / loadable
2855
+ * tokenizer is refused. Every column name is quoted by construction and
2856
+ * every tokenizer token is allowlisted, so no operator-supplied token
2857
+ * reaches the DDL raw. `IF NOT EXISTS` by default. sqlite-only (FTS5 is a
2858
+ * sqlite extension); a non-sqlite dialect throws at build.
2859
+ *
2860
+ * @opts
2861
+ * columns: Array, // FTS5 columns: "name" | { name, unindexed }
2862
+ * tokenize: string, // "unicode61 remove_diacritics 2" (built-in + allowlisted args)
2863
+ * ifNotExists: boolean, // default true
2864
+ *
2865
+ * @example
2866
+ * var b = require("@blamejs/core");
2867
+ * b.sql.createVirtualTable("mail_fts", {
2868
+ * columns: [{ name: "objectid", unindexed: true }, "subject_toks", "body_toks"],
2869
+ * tokenize: "unicode61 remove_diacritics 2",
2870
+ * }).sql;
2871
+ * // -> 'CREATE VIRTUAL TABLE IF NOT EXISTS "mail_fts" USING fts5(' +
2872
+ * // '"objectid" UNINDEXED, "subject_toks", "body_toks", ' +
2873
+ * // "tokenize = 'unicode61 remove_diacritics 2')"
2874
+ */
2875
+ function createVirtualTable(name, opts) {
2876
+ opts = opts || {};
2877
+ var dialect = _normDialect(opts.dialect || "sqlite");
2878
+ if (dialect !== "sqlite") {
2879
+ throw _err("createVirtualTable (USING fts5) is sqlite-only (FTS5 is a sqlite " +
2880
+ "extension); build it with { dialect: 'sqlite' }", "sql-builder/vtable-sqlite-only");
2881
+ }
2882
+ // The table identifier is QUOTED (this DDL targets a concrete sqlite
2883
+ // handle, not a clusterStorage-rewritten bare name).
2884
+ var ref = _normTableRef(name, Object.assign({}, opts, { quoteName: true }));
2885
+ if (!Array.isArray(opts.columns) || opts.columns.length === 0) {
2886
+ throw _err("createVirtualTable requires a non-empty columns array", "sql-builder/bad-columns");
2887
+ }
2888
+ var cols = opts.columns.map(function (c) {
2889
+ var colName = typeof c === "string" ? c : (c && c.name);
2890
+ _validateColumn(colName);
2891
+ var piece = _quoteId(colName, "sqlite");
2892
+ if (c && typeof c === "object" && c.unindexed === true) piece += " UNINDEXED";
2893
+ // Reject any other per-column option token (an arbitrary string would
2894
+ // splice into the DDL); only UNINDEXED is supported.
2895
+ if (c && typeof c === "object") {
2896
+ for (var k in c) {
2897
+ if (!Object.prototype.hasOwnProperty.call(c, k)) continue;
2898
+ if (k === "name" || k === "unindexed") continue;
2899
+ throw _err("createVirtualTable column option '" + k + "' is not supported " +
2900
+ "(only { name, unindexed } )", "sql-builder/bad-vtable-column");
2901
+ }
2902
+ }
2903
+ return piece;
2904
+ });
2905
+ var tokenizeClause = "";
2906
+ if (opts.tokenize !== undefined && opts.tokenize !== null) {
2907
+ tokenizeClause = ", tokenize = '" + _ftsTokenize(opts.tokenize) + "'";
2908
+ }
2909
+ var ifNot = opts.ifNotExists === false ? "" : "IF NOT EXISTS ";
2910
+ var sql = "CREATE VIRTUAL TABLE " + ifNot + ref.ref("sqlite") + " USING fts5(" +
2911
+ cols.join(", ") + tokenizeClause + ")";
2912
+ return { sql: sql, params: [] };
2913
+ }
2914
+
2915
+ // Validate + re-render an FTS5 tokenize spec from its allowlisted tokens.
2916
+ // The first token is the tokenizer name (built-in only); the rest are
2917
+ // allowlisted argument tokens. Returns the canonical space-joined string -
2918
+ // every token came off the allowlist, so the emitted `'...'` literal is
2919
+ // fully builder-controlled (no operator token reaches the DDL raw).
2920
+ function _ftsTokenize(spec) {
2921
+ if (typeof spec !== "string" || spec.length === 0) {
2922
+ throw _err("createVirtualTable tokenize must be a non-empty string", "sql-builder/bad-tokenize");
2923
+ }
2924
+ var tokens = spec.trim().split(/\s+/);
2925
+ if (FTS5_TOKENIZERS[tokens[0]] !== true) {
2926
+ throw _err("createVirtualTable tokenizer '" + tokens[0] + "' is not a built-in FTS5 " +
2927
+ "tokenizer (unicode61 / ascii / porter / trigram); a loadable tokenizer is refused",
2928
+ "sql-builder/bad-tokenize");
2929
+ }
2930
+ for (var i = 1; i < tokens.length; i += 1) {
2931
+ if (FTS5_TOKENIZER_ARGS[tokens[i]] !== true) {
2932
+ throw _err("createVirtualTable tokenize argument '" + tokens[i] + "' is not on the " +
2933
+ "allowlist", "sql-builder/bad-tokenize");
2934
+ }
2935
+ }
2936
+ return tokens.join(" ");
2937
+ }
2938
+
2939
+ // ---- Row-Level Security (Postgres RLS) ------------------------------
2940
+ //
2941
+ // Postgres-only: ENABLE ROW LEVEL SECURITY + CREATE POLICY + DROP POLICY.
2942
+ // Identifiers (schema / table / policy / role) are quoted by construction
2943
+ // through the framework's single identifier primitive; the USING /
2944
+ // WITH CHECK boolean predicates ride the EXISTING guardSql-gated raw-
2945
+ // fragment path (the same choke-point whereRaw / setRaw use), so an
2946
+ // operator-influenced predicate can't smuggle a stacked statement, a
2947
+ // string literal, or a dangerous primitive. SQLite + MySQL have no
2948
+ // portable RLS grammar, so every RLS builder refuses a non-Postgres
2949
+ // dialect at build time (config-time tier - the operator catches the
2950
+ // typo at boot, not at apply).
2951
+
2952
+ var RLS_COMMANDS = Object.freeze({
2953
+ ALL: true, SELECT: true, INSERT: true, UPDATE: true, DELETE: true,
2954
+ });
2955
+
2956
+ function _assertPostgresRls(dialect, what) {
2957
+ if (dialect !== "postgres") {
2958
+ throw _err(what + " is Postgres-only (SQLite / MySQL have no portable " +
2959
+ "row-level-security grammar); build it with { dialect: 'postgres' }",
2960
+ "sql-builder/rls-postgres-only");
2961
+ }
2962
+ }
2963
+
2964
+ // A USING / WITH CHECK predicate is a boolean value expression, routed
2965
+ // through the SAME raw-fragment guard whereRaw uses (b.guardSql strict +
2966
+ // the embedded-literal + placeholder-count scanners). It binds no params
2967
+ // by default - an RLS predicate references session GUCs / row columns, not
2968
+ // per-request bound values - but accepts a params array for the rare
2969
+ // parameterized predicate. Returns the checked { sql, params }.
2970
+ function _rlsPredicate(label, expr, params, opts) {
2971
+ return _checkRawFragment(expr, params, opts || {}, label);
2972
+ }
2973
+
2974
+ /**
2975
+ * @primitive b.sql.enableRowLevelSecurity
2976
+ * @signature b.sql.enableRowLevelSecurity(table, opts?)
2977
+ * @since 0.15.0
2978
+ * @status stable
2979
+ * @related b.sql.createPolicy, b.sql.dropPolicy, b.db.declareRowPolicy
2980
+ *
2981
+ * Build a Postgres `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` statement,
2982
+ * the table identifier quoted by construction (schema-qualified via
2983
+ * `{ schema }` or the dotted `"schema.table"` form). Postgres has no
2984
+ * `IF NOT EXISTS` for this verb; the declarative migration in
2985
+ * `b.db.declareRowPolicy` checks `pg_class.relrowsecurity` and skips the
2986
+ * ALTER when already enabled, so re-running a partially-applied migration
2987
+ * set does not fail. Refuses a non-Postgres dialect at build time.
2988
+ *
2989
+ * @opts
2990
+ * schema: string, // schema qualifier, quoted at build time
2991
+ * force: boolean, // default false - emit FORCE ROW LEVEL SECURITY
2992
+ *
2993
+ * @example
2994
+ * var b = require("@blamejs/core");
2995
+ * b.sql.enableRowLevelSecurity("sessions",
2996
+ * { schema: "public" }).sql;
2997
+ * // -> 'ALTER TABLE "public"."sessions" ENABLE ROW LEVEL SECURITY'
2998
+ */
2999
+ function enableRowLevelSecurity(name, opts) {
3000
+ opts = opts || {};
3001
+ var dialect = _normDialect(opts.dialect || "postgres");
3002
+ _assertPostgresRls(dialect, "enableRowLevelSecurity");
3003
+ // RLS targets a concrete table, so it is quoted (quoteName) rather than
3004
+ // emitted bare - there is no clusterStorage rewrite for a Postgres RLS
3005
+ // migration, which runs against the operator's external backend directly.
3006
+ var ref = _normTableRef(name, Object.assign({}, opts, { quoteName: true }));
3007
+ var sql = "ALTER TABLE " + ref.ref(dialect) + " " +
3008
+ (opts.force === true ? "FORCE" : "ENABLE") + " ROW LEVEL SECURITY";
3009
+ return { sql: sql, params: [] };
3010
+ }
3011
+
3012
+ /**
3013
+ * @primitive b.sql.disableRowLevelSecurity
3014
+ * @signature b.sql.disableRowLevelSecurity(table, opts?)
3015
+ * @since 0.15.0
3016
+ * @status stable
3017
+ * @related b.sql.enableRowLevelSecurity, b.sql.dropPolicy
3018
+ *
3019
+ * Build a Postgres `ALTER TABLE ... DISABLE ROW LEVEL SECURITY` statement
3020
+ * (the inverse of `enableRowLevelSecurity`), the table identifier quoted
3021
+ * by construction. Refuses a non-Postgres dialect at build time.
3022
+ *
3023
+ * @opts
3024
+ * schema: string, // schema qualifier, quoted at build time
3025
+ *
3026
+ * @example
3027
+ * var b = require("@blamejs/core");
3028
+ * b.sql.disableRowLevelSecurity("sessions", { schema: "public" }).sql;
3029
+ * // -> 'ALTER TABLE "public"."sessions" DISABLE ROW LEVEL SECURITY'
3030
+ */
3031
+ function disableRowLevelSecurity(name, opts) {
3032
+ opts = opts || {};
3033
+ var dialect = _normDialect(opts.dialect || "postgres");
3034
+ _assertPostgresRls(dialect, "disableRowLevelSecurity");
3035
+ var ref = _normTableRef(name, Object.assign({}, opts, { quoteName: true }));
3036
+ return { sql: "ALTER TABLE " + ref.ref(dialect) + " DISABLE ROW LEVEL SECURITY", params: [] };
3037
+ }
3038
+
3039
+ /**
3040
+ * @primitive b.sql.createPolicy
3041
+ * @signature b.sql.createPolicy(name, table, spec, opts?)
3042
+ * @since 0.15.0
3043
+ * @status stable
3044
+ * @related b.sql.enableRowLevelSecurity, b.sql.dropPolicy, b.db.declareRowPolicy
3045
+ *
3046
+ * Build a Postgres `CREATE POLICY` statement in canonical clause order:
3047
+ * `name -> table -> AS PERMISSIVE|RESTRICTIVE -> FOR <command> ->
3048
+ * TO <role> -> USING (<pred>) -> WITH CHECK (<pred>)`. The policy / table /
3049
+ * role identifiers are quoted by construction; the `using` and `withCheck`
3050
+ * boolean predicates ride the SAME `b.guardSql`-gated raw-fragment path as
3051
+ * `whereRaw` (strict profile by default, embedded-literal + placeholder-
3052
+ * count scanners), so an operator-influenced predicate cannot smuggle a
3053
+ * stacked statement or a dangerous primitive. Refuses a non-Postgres
3054
+ * dialect at build time.
3055
+ *
3056
+ * `spec.command` is one of `ALL` (default) / `SELECT` / `INSERT` /
3057
+ * `UPDATE` / `DELETE`; `spec.permissive` defaults `true` (a `PERMISSIVE`
3058
+ * policy OR-combines with peers; `false` emits `RESTRICTIVE`, which
3059
+ * AND-combines). `spec.role` is optional (omitted -> the policy applies to
3060
+ * every role). The predicates default to binding no params - an RLS
3061
+ * predicate references session GUCs / row columns - but a `usingParams` /
3062
+ * `withCheckParams` array binds values for a parameterized predicate.
3063
+ *
3064
+ * @opts
3065
+ * schema: string, // schema qualifier for the table
3066
+ * guardProfile: string, // raw-fragment guard profile (default "strict")
3067
+ *
3068
+ * @example
3069
+ * var b = require("@blamejs/core");
3070
+ * b.sql.createPolicy("tenant_isolation", "sessions", {
3071
+ * role: "app_user",
3072
+ * command: "ALL",
3073
+ * using: "tenant_id = current_setting('app.tenant_id')::uuid",
3074
+ * withCheck: "tenant_id = current_setting('app.tenant_id')::uuid",
3075
+ * }, { schema: "public" }).sql;
3076
+ * // -> 'CREATE POLICY "tenant_isolation" ON "public"."sessions" ' +
3077
+ * // 'AS PERMISSIVE FOR ALL TO "app_user" ' +
3078
+ * // "USING (tenant_id = current_setting('app.tenant_id')::uuid) " +
3079
+ * // "WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid)"
3080
+ * // (the static current_setting literal opts in via allowLiterals)
3081
+ */
3082
+ function createPolicy(name, table, spec, opts) {
3083
+ opts = opts || {};
3084
+ spec = spec || {};
3085
+ var dialect = _normDialect(opts.dialect || "postgres");
3086
+ _assertPostgresRls(dialect, "createPolicy");
3087
+ _validateColumn(name);
3088
+ var ref = _normTableRef(table, Object.assign({}, opts, { quoteName: true }));
3089
+
3090
+ var command = "ALL";
3091
+ if (spec.command !== undefined && spec.command !== null) {
3092
+ if (typeof spec.command !== "string" || RLS_COMMANDS[spec.command.toUpperCase()] !== true) {
3093
+ throw _err("createPolicy command must be ALL / SELECT / INSERT / UPDATE / DELETE (got " +
3094
+ JSON.stringify(spec.command) + ")", "sql-builder/bad-rls-command");
3095
+ }
3096
+ command = spec.command.toUpperCase();
3097
+ }
3098
+ var permissive = spec.permissive !== false;
3099
+
3100
+ if (spec.using === undefined || spec.using === null) {
3101
+ throw _err("createPolicy requires a 'using' boolean predicate", "sql-builder/bad-rls-predicate");
3102
+ }
3103
+ // The USING / WITH CHECK predicates are guarded raw fragments. RLS
3104
+ // predicates routinely carry a static, operator-controlled string
3105
+ // literal (current_setting('app.tenant_id')), so allowLiterals defaults
3106
+ // ON here - the literal is the policy author's, never per-request input;
3107
+ // every value-bearing operand still binds via a ? placeholder + params.
3108
+ var rawOpts = {
3109
+ guardProfile: opts.guardProfile || "strict",
3110
+ allowLiterals: spec.allowLiterals !== false,
3111
+ };
3112
+ var using = _rlsPredicate("createPolicy.using", spec.using, spec.usingParams, rawOpts);
3113
+ var withCheck = null;
3114
+ if (spec.withCheck !== undefined && spec.withCheck !== null) {
3115
+ withCheck = _rlsPredicate("createPolicy.withCheck", spec.withCheck, spec.withCheckParams, rawOpts);
3116
+ }
3117
+
3118
+ var sql = "CREATE POLICY " + _quoteId(name, dialect) + " ON " + ref.ref(dialect);
3119
+ sql += " AS " + (permissive ? "PERMISSIVE" : "RESTRICTIVE");
3120
+ sql += " FOR " + command;
3121
+ if (spec.role !== undefined && spec.role !== null) {
3122
+ _validateColumn(spec.role);
3123
+ sql += " TO " + _quoteId(spec.role, dialect);
3124
+ }
3125
+ var params = [];
3126
+ sql += " USING (" + using.sql + ")";
3127
+ for (var ui = 0; ui < using.params.length; ui += 1) params.push(using.params[ui]);
3128
+ if (withCheck) {
3129
+ sql += " WITH CHECK (" + withCheck.sql + ")";
3130
+ for (var wi = 0; wi < withCheck.params.length; wi += 1) params.push(withCheck.params[wi]);
3131
+ }
3132
+ return _emit(sql, params);
3133
+ }
3134
+
3135
+ /**
3136
+ * @primitive b.sql.dropPolicy
3137
+ * @signature b.sql.dropPolicy(name, table, opts?)
3138
+ * @since 0.15.0
3139
+ * @status stable
3140
+ * @related b.sql.createPolicy, b.sql.enableRowLevelSecurity
3141
+ *
3142
+ * Build a Postgres `DROP POLICY` statement, the policy + table identifiers
3143
+ * quoted by construction, `IF EXISTS` by default so dropping a missing
3144
+ * policy is a no-op (the migration down-path is idempotent). Refuses a
3145
+ * non-Postgres dialect at build time.
3146
+ *
3147
+ * @opts
3148
+ * schema: string, // schema qualifier for the table
3149
+ * ifExists: boolean, // default true
3150
+ *
3151
+ * @example
3152
+ * var b = require("@blamejs/core");
3153
+ * b.sql.dropPolicy("tenant_isolation", "sessions", { schema: "public" }).sql;
3154
+ * // -> 'DROP POLICY IF EXISTS "tenant_isolation" ON "public"."sessions"'
3155
+ */
3156
+ function dropPolicy(name, table, opts) {
3157
+ opts = opts || {};
3158
+ var dialect = _normDialect(opts.dialect || "postgres");
3159
+ _assertPostgresRls(dialect, "dropPolicy");
3160
+ _validateColumn(name);
3161
+ var ref = _normTableRef(table, Object.assign({}, opts, { quoteName: true }));
3162
+ var ifExists = opts.ifExists === false ? "" : "IF EXISTS ";
3163
+ return { sql: "DROP POLICY " + ifExists + _quoteId(name, dialect) + " ON " + ref.ref(dialect), params: [] };
3164
+ }
3165
+
3166
+ // ---- Catalog / PRAGMA (narrow audited sqlite-internal sub-API) ------
3167
+ //
3168
+ // b.safeSql.quoteIdentifier refuses an `sqlite_`-prefixed identifier BY
3169
+ // DESIGN (sql/internal-prefix) and _assertEmittable refuses a multi-verb
3170
+ // statement - both stay intact for every general caller. The vault key-
3171
+ // rotation pipeline (lib/vault/rotate.js) legitimately needs to read the
3172
+ // sqlite catalog (sqlite_master), introspect a table (PRAGMA table_info),
3173
+ // set journal mode + synchronous, checkpoint the WAL, and sample rows in
3174
+ // random order. None of those compose through the general builder. This
3175
+ // narrow sub-API allowlists EXACTLY those statements + verbs and nothing
3176
+ // else: every other sqlite_-prefixed identifier and every other PRAGMA
3177
+ // verb still refuses through the general quoteIdentifier / builder gate.
3178
+ //
3179
+ // Every emitter here returns the SAME { sql, params } shape the verbs do,
3180
+ // validated through a CATALOG-scoped output gate (_assertCatalogEmittable)
3181
+ // that allows the sqlite_master / PRAGMA / RANDOM() forms the general
3182
+ // _assertEmittable refuses while keeping NUL / surrogate / stacked-
3183
+ // statement / unterminated-quote refusals fully intact.
3184
+
3185
+ // The exact PRAGMA verbs this sub-API will emit. A verb not on this list
3186
+ // throws - the allowlist is the audit boundary, not a suggestion.
3187
+ var CATALOG_PRAGMA_VERBS = Object.freeze({
3188
+ "table_info": { kind: "introspect" }, // PRAGMA table_info("<table>")
3189
+ "journal_mode": { kind: "set-or-read" }, // PRAGMA journal_mode=WAL | PRAGMA journal_mode
3190
+ "synchronous": { kind: "set-or-read" }, // PRAGMA synchronous=NORMAL
3191
+ "wal_checkpoint": { kind: "checkpoint" }, // PRAGMA wal_checkpoint(TRUNCATE)
3192
+ });
3193
+ // Allowlisted argument tokens per set-or-read / checkpoint PRAGMA - a
3194
+ // fixed, operator-uninfluenced vocabulary so no arbitrary token reaches
3195
+ // the PRAGMA argument position.
3196
+ var PRAGMA_JOURNAL_MODES = Object.freeze({
3197
+ DELETE: true, TRUNCATE: true, PERSIST: true, MEMORY: true, WAL: true, OFF: true,
3198
+ });
3199
+ var PRAGMA_SYNC_LEVELS = Object.freeze({ OFF: true, NORMAL: true, FULL: true, EXTRA: true });
3200
+ var PRAGMA_CHECKPOINT_MODES = Object.freeze({ PASSIVE: true, FULL: true, RESTART: true, TRUNCATE: true });
3201
+
3202
+ // Quote an sqlite identifier WITH allowReserved (an internal table walk
3203
+ // can encounter any name). The sqlite_-prefix rule stays in force for the
3204
+ // general quoteIdentifier path; catalog identifiers that are themselves a
3205
+ // real user table go through the normal allowReserved quote (a user table
3206
+ // is never sqlite_-prefixed). The ONLY sqlite_-prefixed token this sub-API
3207
+ // emits is the fixed `sqlite_master` / `sqlite_schema` literal below -
3208
+ // never an operator-supplied name.
3209
+ function _catalogQuoteTable(name) {
3210
+ if (typeof name !== "string" || name.length === 0) {
3211
+ throw _err("catalog: table name must be a non-empty string", "sql-builder/bad-table");
3212
+ }
3213
+ // A catalog walk reads a name OUT of sqlite_master, so the live table
3214
+ // name is already a validated existing identifier; still route it through
3215
+ // the framework quote primitive (shape / length / NUL rules) with
3216
+ // allowReserved on. An sqlite_-prefixed user table cannot exist (sqlite
3217
+ // reserves the prefix), so the general internal-prefix refusal correctly
3218
+ // rejects it if one is ever passed - the catalog sub-API never relaxes
3219
+ // that for an operator-supplied name.
3220
+ return safeSql.quoteIdentifier(name, "sqlite", { allowReserved: true });
3221
+ }
3222
+
3223
+ // Output gate for the catalog sub-API. Keeps every boundary-escape
3224
+ // refusal _assertEmittable has (NUL, lone surrogate, stacked top-level ';',
3225
+ // unterminated quote, param/placeholder parity) but does NOT run the
3226
+ // single-verb / identifier-shape assumptions the general gate makes about
3227
+ // the builder verbs - a PRAGMA / catalog statement is its own shape.
3228
+ function _assertCatalogEmittable(sql, params) {
3229
+ if (typeof sql !== "string" || sql.length === 0) {
3230
+ throw _err("catalog: emitted SQL must be a non-empty string (builder bug)",
3231
+ "sql-builder/empty-sql");
3232
+ }
3233
+ if (!Array.isArray(params)) {
3234
+ throw _err("catalog: params must be an array (builder bug)", "sql-builder/bad-params-shape");
3235
+ }
3236
+ if (sql.indexOf("\u0000") !== -1) {
3237
+ throw _err("catalog: emitted SQL contains a NUL byte - rejected",
3238
+ "sql-builder/null-byte-sql");
3239
+ }
3240
+ if (typeof sql.isWellFormed === "function" && !sql.isWellFormed()) {
3241
+ throw _err("catalog: emitted SQL contains invalid Unicode (lone surrogates) - rejected",
3242
+ "sql-builder/invalid-encoding-sql");
3243
+ }
3244
+ var holders = _countPlaceholders(sql);
3245
+ if (holders !== params.length) {
3246
+ throw _err("catalog: placeholder/param count mismatch - " + holders + " '?' but " +
3247
+ params.length + " param(s)", "sql-builder/param-mismatch");
3248
+ }
3249
+ // Quote/comment-aware single-statement + balanced-paren scan, identical
3250
+ // to _assertEmittable's tail. A stacked top-level ';' / unterminated
3251
+ // quote is refused here too.
3252
+ safeSql.assertSingleStatement(sql, {
3253
+ label: "catalog",
3254
+ makeError: function (m, suffix) { return _err(m, "sql-builder/" + suffix); },
3255
+ });
3256
+ return { sql: sql, params: params };
3257
+ }
3258
+
3259
+ // The audited catalog/PRAGMA sub-API. Every method returns { sql, params }.
3260
+ var catalog = Object.freeze({
3261
+ /**
3262
+ * @primitive b.sql.catalog.listTables
3263
+ * @signature b.sql.catalog.listTables()
3264
+ * @since 0.15.0
3265
+ * @status stable
3266
+ * @related b.sql.catalog.tableInfo, b.sql.catalog.tableExists
3267
+ *
3268
+ * Build the sqlite catalog query that lists every user table -
3269
+ * `SELECT name FROM sqlite_master WHERE type='table' AND
3270
+ * name NOT LIKE 'sqlite_%'`. This is the ONLY general path that emits an
3271
+ * `sqlite_master` reference; the framework's `b.safeSql.quoteIdentifier`
3272
+ * refuses an `sqlite_`-prefixed identifier for every other caller, so a
3273
+ * `sqlite_master` scan cannot be hand-built through the normal builder.
3274
+ * The `sqlite_%` LIKE pattern is a builder-emitted static literal (not
3275
+ * operator input). sqlite-internal; no dialect option.
3276
+ *
3277
+ * @example
3278
+ * var b = require("@blamejs/core");
3279
+ * var q = b.sql.catalog.listTables();
3280
+ * // -> { sql: "SELECT name FROM sqlite_master WHERE type = 'table' " +
3281
+ * // "AND name NOT LIKE 'sqlite_%'", params: [] }
3282
+ */
3283
+ listTables: function () {
3284
+ var sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'";
3285
+ return _assertCatalogEmittable(sql, []);
3286
+ },
3287
+
3288
+ /**
3289
+ * @primitive b.sql.catalog.tableExists
3290
+ * @signature b.sql.catalog.tableExists(name)
3291
+ * @since 0.15.0
3292
+ * @status stable
3293
+ * @related b.sql.catalog.listTables, b.sql.catalog.tableInfo
3294
+ *
3295
+ * Build the sqlite catalog existence probe for one table -
3296
+ * `SELECT name FROM sqlite_master WHERE type='table' AND name = ?`, the
3297
+ * table name BOUND as a `?` parameter (never interpolated). Returns one
3298
+ * row when the table exists, none otherwise.
3299
+ *
3300
+ * @example
3301
+ * var b = require("@blamejs/core");
3302
+ * b.sql.catalog.tableExists("audit_log");
3303
+ * // -> { sql: "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
3304
+ * // params: ["audit_log"] }
3305
+ */
3306
+ tableExists: function (name) {
3307
+ if (typeof name !== "string" || name.length === 0) {
3308
+ throw _err("catalog.tableExists: name must be a non-empty string", "sql-builder/bad-table");
3309
+ }
3310
+ var sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?";
3311
+ return _assertCatalogEmittable(sql, [name]);
3312
+ },
3313
+
3314
+ /**
3315
+ * @primitive b.sql.catalog.tableInfo
3316
+ * @signature b.sql.catalog.tableInfo(name)
3317
+ * @since 0.15.0
3318
+ * @status stable
3319
+ * @related b.sql.catalog.listTables, b.sql.pragma
3320
+ *
3321
+ * Build a `PRAGMA table_info("<table>")` statement, the table name
3322
+ * quoted by construction through `b.safeSql`. PRAGMA does not bind a
3323
+ * parameter in its argument position, so the name is quoted (shape /
3324
+ * length / NUL-validated), never string-interpolated raw. sqlite-only.
3325
+ *
3326
+ * @example
3327
+ * var b = require("@blamejs/core");
3328
+ * b.sql.catalog.tableInfo("audit_log").sql;
3329
+ * // -> 'PRAGMA table_info("audit_log")'
3330
+ */
3331
+ tableInfo: function (name) {
3332
+ var sql = "PRAGMA table_info(" + _catalogQuoteTable(name) + ")";
3333
+ return _assertCatalogEmittable(sql, []);
3334
+ },
3335
+
3336
+ /**
3337
+ * @primitive b.sql.catalog.sampleRandom
3338
+ * @signature b.sql.catalog.sampleRandom(table, columns?, opts?)
3339
+ * @since 0.15.0
3340
+ * @status stable
3341
+ * @related b.sql.select, b.sql.catalog.tableInfo
3342
+ *
3343
+ * Build a `SELECT <cols> FROM "<table>" ORDER BY RANDOM() LIMIT ?`
3344
+ * row-sampler, identifiers quoted by construction and the limit BOUND as
3345
+ * a `?` parameter. `RANDOM()` ordering is the audited sqlite sampler form
3346
+ * the general `b.sql.select` builder has no clause for (it is used to
3347
+ * pick representative rows for verification, not cryptographic
3348
+ * randomness). `columns` defaults to `*`. sqlite-only.
3349
+ *
3350
+ * @opts
3351
+ * limit: number, // bound LIMIT (required > 0)
3352
+ *
3353
+ * @example
3354
+ * var b = require("@blamejs/core");
3355
+ * b.sql.catalog.sampleRandom("sessions", ["_id", "email"], { limit: 50 });
3356
+ * // -> { sql: 'SELECT "_id", "email" FROM "sessions" ORDER BY RANDOM() LIMIT ?',
3357
+ * // params: [50] }
3358
+ */
3359
+ /**
3360
+ * @primitive b.sql.catalog.changes
3361
+ * @signature b.sql.catalog.changes()
3362
+ * @since 0.15.0
3363
+ * @status stable
3364
+ * @related b.sql.catalog.listTables, b.sql.delete
3365
+ *
3366
+ * Build `SELECT changes() AS c` - the sqlite scalar that reports the row
3367
+ * count of the most recent INSERT / UPDATE / DELETE on the current
3368
+ * connection. `changes()` is a sqlite-internal function with no table to
3369
+ * select from, so the general builder (which requires a FROM table) has
3370
+ * no form for it; this audited builder emits the exact zero-parameter
3371
+ * probe the inbox sweep uses to learn how many rows a preceding DELETE
3372
+ * removed. sqlite-only; the column alias is `c`.
3373
+ *
3374
+ * @example
3375
+ * var b = require("@blamejs/core");
3376
+ * b.sql.catalog.changes().sql; // -> "SELECT changes() AS c"
3377
+ */
3378
+ changes: function () {
3379
+ return _assertCatalogEmittable("SELECT changes() AS c", []);
3380
+ },
3381
+
3382
+ sampleRandom: function (table, columns, opts) {
3383
+ opts = opts || {};
3384
+ var qt = _catalogQuoteTable(table);
3385
+ var proj = "*";
3386
+ if (columns !== undefined && columns !== null) {
3387
+ if (!Array.isArray(columns) || columns.length === 0) {
3388
+ throw _err("catalog.sampleRandom: columns must be a non-empty array (or omit for *)",
3389
+ "sql-builder/bad-columns");
3390
+ }
3391
+ proj = columns.map(function (c) {
3392
+ _validateColumn(c);
3393
+ return _quoteId(c, "sqlite");
3394
+ }).join(", ");
3395
+ }
3396
+ var limit = opts.limit;
3397
+ if (!Number.isInteger(limit) || limit <= 0) {
3398
+ throw _err("catalog.sampleRandom: opts.limit must be a positive integer", "sql-builder/bad-limit");
3399
+ }
3400
+ var sql = "SELECT " + proj + " FROM " + qt + " ORDER BY RANDOM() LIMIT ?";
3401
+ return _assertCatalogEmittable(sql, [limit]);
3402
+ },
3403
+ });
3404
+
3405
+ /**
3406
+ * @primitive b.sql.pragma
3407
+ * @signature b.sql.pragma(verb, arg?)
3408
+ * @since 0.15.0
3409
+ * @status stable
3410
+ * @related b.sql.catalog.tableInfo, b.sql.catalog.listTables
3411
+ *
3412
+ * Build a sqlite `PRAGMA` statement from a NARROW allowlist of verbs:
3413
+ * `journal_mode` (set `PRAGMA journal_mode=WAL` or read `PRAGMA
3414
+ * journal_mode`), `synchronous` (`PRAGMA synchronous=NORMAL`), and
3415
+ * `wal_checkpoint` (`PRAGMA wal_checkpoint(TRUNCATE)`). The argument is
3416
+ * matched against a fixed per-verb vocabulary - a journal mode / sync
3417
+ * level / checkpoint mode - so no operator-influenced token reaches the
3418
+ * PRAGMA argument position. A verb not on the allowlist throws; this is
3419
+ * the audit boundary the at-rest key-rotation pipeline routes its PRAGMA
3420
+ * statements through. Pass no `arg` to a set-or-read verb to read the
3421
+ * current value. sqlite-only.
3422
+ *
3423
+ * @opts
3424
+ * (none - the second positional is the allowlisted argument token)
3425
+ *
3426
+ * @example
3427
+ * var b = require("@blamejs/core");
3428
+ * b.sql.pragma("journal_mode", "WAL").sql; // -> 'PRAGMA journal_mode=WAL'
3429
+ * b.sql.pragma("synchronous", "NORMAL").sql; // -> 'PRAGMA synchronous=NORMAL'
3430
+ * b.sql.pragma("wal_checkpoint", "TRUNCATE").sql; // -> 'PRAGMA wal_checkpoint(TRUNCATE)'
3431
+ * b.sql.pragma("journal_mode").sql; // -> 'PRAGMA journal_mode' (read)
3432
+ */
3433
+ function pragma(verb, arg) {
3434
+ if (typeof verb !== "string" || CATALOG_PRAGMA_VERBS[verb] === undefined) {
3435
+ throw _err("pragma: verb '" + verb + "' is not on the allowlist (journal_mode / " +
3436
+ "synchronous / wal_checkpoint); a PRAGMA outside this set is refused by design",
3437
+ "sql-builder/bad-pragma");
3438
+ }
3439
+ var def = CATALOG_PRAGMA_VERBS[verb];
3440
+ if (def.kind === "introspect") {
3441
+ // table_info is reached via catalog.tableInfo (needs a quoted name).
3442
+ throw _err("pragma: use b.sql.catalog.tableInfo(name) for PRAGMA table_info",
3443
+ "sql-builder/bad-pragma");
3444
+ }
3445
+ if (def.kind === "checkpoint") {
3446
+ var ckMode = (arg === undefined || arg === null) ? "PASSIVE" : String(arg).toUpperCase();
3447
+ if (PRAGMA_CHECKPOINT_MODES[ckMode] !== true) {
3448
+ throw _err("pragma wal_checkpoint mode must be PASSIVE / FULL / RESTART / TRUNCATE (got " +
3449
+ JSON.stringify(arg) + ")", "sql-builder/bad-pragma-arg");
3450
+ }
3451
+ return _assertCatalogEmittable("PRAGMA wal_checkpoint(" + ckMode + ")", []);
3452
+ }
3453
+ // set-or-read: journal_mode / synchronous.
3454
+ if (arg === undefined || arg === null) {
3455
+ return _assertCatalogEmittable("PRAGMA " + verb, []);
3456
+ }
3457
+ var token = String(arg).toUpperCase();
3458
+ var vocab = verb === "journal_mode" ? PRAGMA_JOURNAL_MODES : PRAGMA_SYNC_LEVELS;
3459
+ if (vocab[token] !== true) {
3460
+ throw _err("pragma " + verb + " argument '" + arg + "' is not in the allowed vocabulary",
3461
+ "sql-builder/bad-pragma-arg");
3462
+ }
3463
+ return _assertCatalogEmittable("PRAGMA " + verb + "=" + token, []);
3464
+ }
3465
+
3466
+ // ---- Schema optimization (defineTable) ------------------------------
3467
+ //
3468
+ // PK / FK / index automation over createTable + createIndex. Each layer is
3469
+ // on by default and individually disablable.
3470
+
3471
+ // Naive pluralizer for FK table inference (entity -> table). Covers the
3472
+ // common English cases; an unusual plural is overridden with an explicit
3473
+ // `references`. consonant+y -> ies, sibilant -> es, else -> s.
3474
+ function _pluralize(s) {
3475
+ if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + "ies";
3476
+ if (/(?:s|x|z|ch|sh)$/i.test(s)) return s + "es";
3477
+ return s + "s";
3478
+ }
3479
+
3480
+ // Infer a foreign-key reference from a column name by convention: a column
3481
+ // named `<entity>Id` / `<entity>_id` references `<pluralize(entity)>(<pkCol>)`.
3482
+ // Returns null when the name does not match (or the entity part is empty,
3483
+ // e.g. a bare `id` / `_id`).
3484
+ function _inferFkRef(colName, pkCol) {
3485
+ var m = /^(.+?)(?:Id|_id)$/.exec(colName);
3486
+ if (!m || m[1].length === 0) return null;
3487
+ return { table: _pluralize(m[1]), column: pkCol };
3488
+ }
3489
+
3490
+ // Deterministic index name `idx_<table>_<cols>`, sanitized to an identifier
3491
+ // and capped at the dialect identifier limit (Postgres NAMEDATALEN 63) the
3492
+ // same way the query builder bounds every identifier. An over-long name is
3493
+ // truncated with a short stable checksum suffix so two long names can't
3494
+ // collide after truncation.
3495
+ function _indexName(table, cols) {
3496
+ var base = ("idx_" + table + "_" + cols.join("_")).replace(/[^A-Za-z0-9_]/g, "_");
3497
+ if (base.length > safeSql.MAX_IDENTIFIER_LENGTH) {
3498
+ var h = 0;
3499
+ for (var i = 0; i < base.length; i += 1) h = (h * 31 + base.charCodeAt(i)) >>> 0;
3500
+ base = base.slice(0, safeSql.MAX_IDENTIFIER_LENGTH - 9) + "_" + h.toString(36);
3501
+ }
3502
+ return base;
3503
+ }
3504
+
3505
+ /**
3506
+ * @primitive b.sql.defineTable
3507
+ * @signature b.sql.defineTable(name, spec, opts?)
3508
+ * @since 0.14.29
3509
+ * @status stable
3510
+ * @related b.sql.createTable, b.sql.createIndex, b.sql.select
3511
+ *
3512
+ * Declarative schema with built-in PK / FK / index optimization. Returns an
3513
+ * ordered `{ statements: [{ sql, params }, ...] }` bundle (the `CREATE TABLE`
3514
+ * first, then each `CREATE INDEX`) to run in sequence. Three automation
3515
+ * layers, each on by default and individually disablable:
3516
+ *
3517
+ * - **Primary key** - if no column declares `primaryKey` / `autoIncrement`
3518
+ * and `opts.primaryKey` is unset, an identity PK column (`opts.primaryKeyColumn`,
3519
+ * default `id`) is auto-added in the dialect-correct form (BIGSERIAL /
3520
+ * INTEGER AUTOINCREMENT / BIGINT AUTO_INCREMENT). Disable: `autoPrimaryKey: false`.
3521
+ * - **Foreign keys** - a column named `<entity>Id` / `<entity>_id` infers a
3522
+ * `REFERENCES <pluralize(entity)>(<pk>)` constraint. Override one column with
3523
+ * an explicit `references` (`"table"` or `{ table, column?, onDelete?,
3524
+ * onUpdate? }`) or opt it out with `references: false`. Disable all
3525
+ * inference: `autoForeignKeys: false`.
3526
+ * - **Indexes** - every FK column is auto-indexed (databases do not index
3527
+ * FK columns for you), as is any column flagged `index: true`
3528
+ * (`unique: true` is enforced inline). Add composite / custom indexes via
3529
+ * `opts.indexes`. Disable auto-indexing: `autoIndex: false`.
3530
+ *
3531
+ * Every index / FK column is gated against the table's declared column set -
3532
+ * the same column-namespace discipline the query builder applies with
3533
+ * `allowedColumns` - and every generated index name is bounded to the dialect
3534
+ * identifier limit.
3535
+ *
3536
+ * @opts
3537
+ * dialect: string, // postgres | sqlite | mysql (default sqlite)
3538
+ * prefix: string, // operator app-table namespace prefix
3539
+ * schema: string, // schema qualifier
3540
+ * autoPrimaryKey: boolean, // default true
3541
+ * primaryKeyColumn: string, // default "id"
3542
+ * autoForeignKeys: boolean, // default true (naming-convention inference)
3543
+ * autoIndex: boolean, // default true
3544
+ * indexes: array, // [{ columns: [...], unique?, name? }]
3545
+ *
3546
+ * @example
3547
+ * var b = require("@blamejs/core");
3548
+ * var ddl = b.sql.defineTable("orders", [
3549
+ * { name: "userId", type: "int" }, // -> FK users(id) + index
3550
+ * { name: "total", type: "numeric" },
3551
+ * { name: "email", type: "text", index: true },
3552
+ * ], { dialect: "postgres" });
3553
+ * ddl.statements.length;
3554
+ * // -> 3 (CREATE TABLE orders; CREATE INDEX on userId; CREATE INDEX on email)
3555
+ */
3556
+ function defineTable(name, spec, opts) {
3557
+ opts = opts || {};
3558
+ var dialect = _normDialect(opts.dialect);
3559
+ if (!Array.isArray(spec) || spec.length === 0) {
3560
+ throw _err("defineTable requires a non-empty columns spec array", "sql-builder/bad-columns");
3561
+ }
3562
+ var autoPk = opts.autoPrimaryKey !== false;
3563
+ var autoFk = opts.autoForeignKeys !== false;
3564
+ var autoIdx = opts.autoIndex !== false;
3565
+ var pkCol = opts.primaryKeyColumn || "id";
3566
+
3567
+ // Shallow-copy each spec so FK inference never mutates the caller's object.
3568
+ var cols = spec.map(function (c) {
3569
+ if (!c || typeof c !== "object" || typeof c.name !== "string") {
3570
+ throw _err("defineTable column must be { name, type, ... }", "sql-builder/bad-column");
3571
+ }
3572
+ return Object.assign({}, c);
3573
+ });
3574
+
3575
+ // PK automation.
3576
+ var declaredPk = cols.some(function (c) { return c.primaryKey || c.autoIncrement; }) ||
3577
+ (Array.isArray(opts.primaryKey) && opts.primaryKey.length > 0);
3578
+ if (autoPk && !declaredPk) cols.unshift({ name: pkCol, autoIncrement: true });
3579
+
3580
+ // Column namespace - index / FK columns must be members, the same gate the
3581
+ // query builder enforces with allowedColumns / _assertColumnMember.
3582
+ var declared = {};
3583
+ cols.forEach(function (c) { declared[c.name] = true; });
3584
+ function _assertMember(col, where) {
3585
+ if (declared[col] !== true) {
3586
+ throw _err("defineTable: " + where + " references column '" + col +
3587
+ "' which is not a declared column of '" + name + "'", "sql-builder/unknown-column");
3588
+ }
3589
+ }
3590
+
3591
+ // FK automation (convention-by-default + per-column override).
3592
+ var fkColumns = [];
3593
+ cols.forEach(function (c) {
3594
+ if (c.references === false) return; // opt-out
3595
+ if (c.references !== undefined) { fkColumns.push(c.name); return; } // explicit
3596
+ if (c.primaryKey || c.autoIncrement) return; // PK is not an FK
3597
+ if (autoFk) {
3598
+ var inferred = _inferFkRef(c.name, pkCol);
3599
+ if (inferred) { c.references = inferred; fkColumns.push(c.name); }
3600
+ }
3601
+ });
3602
+
3603
+ var statements = [createTable(name, cols, opts)];
3604
+
3605
+ // Index automation. Generated index names are bounded by _indexName.
3606
+ var indexed = {};
3607
+ function _pushIndex(indexCols, unique, explicitName) {
3608
+ indexCols.forEach(function (col) { _assertMember(col, "index"); });
3609
+ statements.push(createIndex(explicitName || _indexName(name, indexCols), name, indexCols,
3610
+ { dialect: dialect, unique: unique === true, prefix: opts.prefix, schema: opts.schema }));
3611
+ }
3612
+ if (autoIdx) {
3613
+ fkColumns.forEach(function (cn) {
3614
+ if (!indexed[cn]) { indexed[cn] = true; _pushIndex([cn], false, null); }
3615
+ });
3616
+ cols.forEach(function (c) {
3617
+ if (c.index === true && !c.unique && !c.primaryKey && !c.autoIncrement && !indexed[c.name]) {
3618
+ indexed[c.name] = true; _pushIndex([c.name], false, null);
3619
+ }
3620
+ });
3621
+ }
3622
+ // Explicit indexes are always honored (even with autoIndex off).
3623
+ if (Array.isArray(opts.indexes)) {
3624
+ opts.indexes.forEach(function (ix) {
3625
+ if (!ix || !Array.isArray(ix.columns) || ix.columns.length === 0) {
3626
+ throw _err("defineTable opts.indexes entry needs a non-empty columns array",
3627
+ "sql-builder/bad-index");
3628
+ }
3629
+ _pushIndex(ix.columns, ix.unique, ix.name);
3630
+ });
3631
+ }
3632
+
3633
+ return { statements: statements };
3634
+ }
3635
+
3636
+ // ---- Verb entry points ----------------------------------------------
3637
+
3638
+ /**
3639
+ * @primitive b.sql.select
3640
+ * @signature b.sql.select(table, opts?)
3641
+ * @since 0.14.29
3642
+ * @status stable
3643
+ * @related b.sql.insert, b.sql.update, b.sql.delete, b.sql.upsert
3644
+ *
3645
+ * Start a `SELECT` builder over `table` (a name, a `"schema.table"`, or a
3646
+ * `b.sql.table(...)` reference). Chain `columns` / aggregates /
3647
+ * `join` family / `where` family / `groupBy` / `having` / `orderBy` /
3648
+ * `limit` / `offset`, then call `toSql()` for `{ sql, params }`. Emits
3649
+ * bare default table names + `?` placeholders so `b.clusterStorage`
3650
+ * applies the cluster prefix + Postgres `$N` translation at execute time.
3651
+ *
3652
+ * @opts
3653
+ * dialect: string, // postgres | sqlite | mysql (default sqlite)
3654
+ * schema: string, // schema qualifier for the table
3655
+ * prefix: string, // operator app-table namespace prefix
3656
+ * alias: string, // table alias (for joins)
3657
+ * allowedColumns: array, // column-membership gate set
3658
+ * columnGateMode: string, // reject | warn | off
3659
+ *
3660
+ * @example
3661
+ * var b = require("@blamejs/core");
3662
+ * b.sql.select("users")
3663
+ * .columns(["id", "email"])
3664
+ * .where("status", "active")
3665
+ * .orderBy("createdAt", "desc")
3666
+ * .limit(10)
3667
+ * .toSql();
3668
+ * // -> { sql: 'SELECT "id", "email" FROM users WHERE "status" = ? ORDER BY "createdAt" DESC LIMIT 10',
3669
+ * // params: ["active"] }
3670
+ */
3671
+ function select(tableNameOrRef, opts) { return new SelectBuilder(tableNameOrRef, opts); }
3672
+
3673
+ /**
3674
+ * @primitive b.sql.insert
3675
+ * @signature b.sql.insert(table, opts?)
3676
+ * @since 0.14.29
3677
+ * @status stable
3678
+ * @related b.sql.select, b.sql.upsert, b.sql.update
3679
+ *
3680
+ * Start an `INSERT` builder. Provide rows via `columns([...])` +
3681
+ * `values([...])` (positional), `values({ ... })` (one row object), or
3682
+ * `values([{...}, {...}])` (multi-row). Optional `returning(cols)`. The
3683
+ * value set is fully bound - every value becomes a `?` placeholder.
3684
+ *
3685
+ * @opts
3686
+ * dialect: string, // postgres | sqlite | mysql (default sqlite)
3687
+ * schema: string, // schema qualifier
3688
+ * prefix: string, // operator app-table namespace prefix
3689
+ * allowedColumns: array, // column-membership gate set
3690
+ *
3691
+ * @example
3692
+ * var b = require("@blamejs/core");
3693
+ * b.sql.insert("users")
3694
+ * .values({ id: 1, email: "a@b.c" })
3695
+ * .returning(["id"])
3696
+ * .toSql();
3697
+ * // -> { sql: 'INSERT INTO users ("id", "email") VALUES (?, ?) RETURNING "id"',
3698
+ * // params: [1, "a@b.c"] }
3699
+ */
3700
+ function insert(tableNameOrRef, opts) { return new InsertBuilder(tableNameOrRef, opts); }
3701
+
3702
+ /**
3703
+ * @primitive b.sql.update
3704
+ * @signature b.sql.update(table, opts?)
3705
+ * @since 0.14.29
3706
+ * @status stable
3707
+ * @related b.sql.select, b.sql.insert, b.sql.delete
3708
+ *
3709
+ * Start an `UPDATE` builder. Set assignments via `set({ ... })` /
3710
+ * `set(col, val)` / `setRaw(col, expr, params)`; filter via the `where`
3711
+ * family. An update with no `where()` THROWS unless `allowNoWhere()` is
3712
+ * called - a deliberate full-table write must opt in. Optional
3713
+ * `returning(cols)`.
3714
+ *
3715
+ * @opts
3716
+ * dialect: string, // postgres | sqlite | mysql (default sqlite)
3717
+ * schema: string, // schema qualifier
3718
+ * prefix: string, // operator app-table namespace prefix
3719
+ * allowedColumns: array, // column-membership gate set
3720
+ *
3721
+ * @example
3722
+ * var b = require("@blamejs/core");
3723
+ * b.sql.update("users")
3724
+ * .set({ status: "inactive" })
3725
+ * .where("id", 1)
3726
+ * .toSql();
3727
+ * // -> { sql: 'UPDATE users SET "status" = ? WHERE "id" = ?', params: ["inactive", 1] }
3728
+ */
3729
+ function update(tableNameOrRef, opts) { return new UpdateBuilder(tableNameOrRef, opts); }
3730
+
3731
+ /**
3732
+ * @primitive b.sql.delete
3733
+ * @signature b.sql.delete(table, opts?)
3734
+ * @since 0.14.29
3735
+ * @status stable
3736
+ * @related b.sql.select, b.sql.update, b.sql.insert
3737
+ *
3738
+ * Start a `DELETE` builder. Filter via the `where` family. A delete with
3739
+ * no `where()` THROWS unless `allowNoWhere()` is called. Optional
3740
+ * `returning(cols)`.
3741
+ *
3742
+ * @opts
3743
+ * dialect: string, // postgres | sqlite | mysql (default sqlite)
3744
+ * schema: string, // schema qualifier
3745
+ * prefix: string, // operator app-table namespace prefix
3746
+ * allowedColumns: array, // column-membership gate set
3747
+ *
3748
+ * @example
3749
+ * var b = require("@blamejs/core");
3750
+ * b.sql.delete("sessions")
3751
+ * .where("expiresAt", "<", 1700000000)
3752
+ * .toSql();
3753
+ * // -> { sql: 'DELETE FROM sessions WHERE "expiresAt" < ?', params: [1700000000] }
3754
+ */
3755
+ function del(tableNameOrRef, opts) { return new DeleteBuilder(tableNameOrRef, opts); }
3756
+
3757
+ /**
3758
+ * @primitive b.sql.upsert
3759
+ * @signature b.sql.upsert(table, opts?)
3760
+ * @since 0.14.29
3761
+ * @status stable
3762
+ * @related b.sql.insert, b.sql.update, b.sql.select
3763
+ *
3764
+ * Start an `UPSERT` builder - the one verb that emits dialect-final
3765
+ * conflict syntax. Supply the row via `columns` + `values({...})`, the
3766
+ * conflict key via `onConflict(keys)`, and one conflict action:
3767
+ * `doUpdate(cols | { col: expr })`, `doUpdateFromExcluded(cols)`, or
3768
+ * `doNothing()`. Optional `conflictWhere(rawGuard, params, opts?)` fences
3769
+ * the update - pass `{ guardColumn: "<col>" }` to name the column the
3770
+ * fence protects so the MySQL fold emits it last (see below); optional
3771
+ * `returning(cols)`.
3772
+ *
3773
+ * On Postgres / SQLite `toSql()` returns
3774
+ * `{ sql, params }` emitting `ON CONFLICT (keys) DO UPDATE SET
3775
+ * col = EXCLUDED.col [WHERE ...] [RETURNING ...]`. On MySQL it returns
3776
+ * `{ sql, params, readbackSql }` emitting `ON DUPLICATE KEY UPDATE
3777
+ * col = VALUES(col)` (or `IF(guard, VALUES(col), col)` when
3778
+ * `conflictWhere` is set); MySQL evaluates the SET list left to right, so
3779
+ * when the fenced guard column is itself a SET target it must be assigned
3780
+ * last (each IF must see the guard column's pre-update value) - name it
3781
+ * via `conflictWhere(..., { guardColumn })` and the fold reorders it to
3782
+ * the end. MySQL has no per-statement WHERE / RETURNING on the conflict
3783
+ * action, so a readback `SELECT` keyed on the conflict columns is
3784
+ * returned for the caller to fetch the upserted row.
3785
+ *
3786
+ * @opts
3787
+ * dialect: string, // postgres | sqlite | mysql (default sqlite)
3788
+ * schema: string, // schema qualifier
3789
+ * prefix: string, // operator app-table namespace prefix
3790
+ * allowedColumns: array, // column-membership gate set
3791
+ *
3792
+ * @example
3793
+ * var b = require("@blamejs/core");
3794
+ * b.sql.upsert("audit_tip", { dialect: "postgres" })
3795
+ * .values({ id: 1, counter: 42 })
3796
+ * .onConflict(["id"])
3797
+ * .doUpdateFromExcluded(["counter"])
3798
+ * .toSql();
3799
+ * // -> { sql: 'INSERT INTO audit_tip ("id", "counter") VALUES (?, ?) ' +
3800
+ * // 'ON CONFLICT ("id") DO UPDATE SET "counter" = EXCLUDED."counter"',
3801
+ * // params: [1, 42] }
3802
+ */
3803
+ function upsert(tableNameOrRef, opts) { return new UpsertBuilder(tableNameOrRef, opts); }
3804
+
3805
+ module.exports = {
3806
+ // Verbs
3807
+ select: select,
3808
+ insert: insert,
3809
+ update: update,
3810
+ delete: del,
3811
+ upsert: upsert,
3812
+ // Table reference
3813
+ table: table,
3814
+ // Value-position helpers (INSERT values() / UPDATE set() right-hand side)
3815
+ fn: fn,
3816
+ cast: cast,
3817
+ // Driver-final positional translation (for direct-driver callers: a DDL
3818
+ // { sql, params } result or a chainable builder -> $1..$N on postgres).
3819
+ toExternalSql: toExternalSql,
3820
+ // DDL
3821
+ createTable: createTable,
3822
+ createIndex: createIndex,
3823
+ alterTable: alterTable,
3824
+ dropTable: dropTable,
3825
+ createVirtualTable: createVirtualTable,
3826
+ defineTable: defineTable,
3827
+ // Row-Level Security (Postgres)
3828
+ enableRowLevelSecurity: enableRowLevelSecurity,
3829
+ disableRowLevelSecurity: disableRowLevelSecurity,
3830
+ createPolicy: createPolicy,
3831
+ dropPolicy: dropPolicy,
3832
+ // Catalog / PRAGMA (narrow audited sqlite-internal sub-API)
3833
+ catalog: catalog,
3834
+ pragma: pragma,
3835
+ // Error class
3836
+ SqlBuilderError: SqlBuilderError,
3837
+ // Exposed for the integrator: the operator-facing builder bases +
3838
+ // operator allowlist, so wiki harvesters + adjacent lib code can
3839
+ // instanceof-check a builder and the must-compose detector can scope.
3840
+ Builder: Builder,
3841
+ ALLOWED_OPS: ALLOWED_OPS,
3842
+ };