turbine-orm 0.81.0 → 0.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +26 -25
  2. package/dist/checkout.d.ts +27 -2
  3. package/dist/checkout.js +75 -4
  4. package/dist/cjs/checkout.d.ts +27 -2
  5. package/dist/cjs/checkout.js +76 -3
  6. package/dist/cjs/cli/code-first-tags.d.ts +62 -0
  7. package/dist/cjs/cli/code-first-tags.js +124 -0
  8. package/dist/cjs/cli/compile-query.js +9 -0
  9. package/dist/cjs/cli/config.d.ts +35 -6
  10. package/dist/cjs/cli/config.js +17 -1
  11. package/dist/cjs/cli/destructive.d.ts +7 -4
  12. package/dist/cjs/cli/destructive.js +224 -20
  13. package/dist/cjs/cli/error-catalog.js +36 -0
  14. package/dist/cjs/cli/error-text.d.ts +15 -0
  15. package/dist/cjs/cli/error-text.js +40 -0
  16. package/dist/cjs/cli/index.d.ts +62 -4
  17. package/dist/cjs/cli/index.js +568 -108
  18. package/dist/cjs/cli/local-server-guard.d.ts +53 -0
  19. package/dist/cjs/cli/local-server-guard.js +119 -0
  20. package/dist/cjs/cli/local-server-output.d.ts +41 -0
  21. package/dist/cjs/cli/local-server-output.js +103 -0
  22. package/dist/cjs/cli/mcp.d.ts +8 -0
  23. package/dist/cjs/cli/mcp.js +121 -32
  24. package/dist/cjs/cli/migrate.d.ts +113 -5
  25. package/dist/cjs/cli/migrate.js +270 -21
  26. package/dist/cjs/cli/observe.d.ts +6 -1
  27. package/dist/cjs/cli/observe.js +85 -55
  28. package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
  29. package/dist/cjs/cli/pii-predicate-guard.js +98 -37
  30. package/dist/cjs/cli/prisma-resolve.js +17 -13
  31. package/dist/cjs/cli/prisma-schema.js +12 -4
  32. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  33. package/dist/cjs/cli/studio.d.ts +14 -2
  34. package/dist/cjs/cli/studio.js +244 -69
  35. package/dist/cjs/cli/ui.d.ts +1 -1
  36. package/dist/cjs/client.d.ts +558 -85
  37. package/dist/cjs/client.js +1270 -395
  38. package/dist/cjs/connection-guard.d.ts +13 -0
  39. package/dist/cjs/connection-guard.js +28 -0
  40. package/dist/cjs/connection-url.d.ts +51 -16
  41. package/dist/cjs/connection-url.js +67 -34
  42. package/dist/cjs/diagnostics.d.ts +81 -0
  43. package/dist/cjs/diagnostics.js +147 -0
  44. package/dist/cjs/dialect.d.ts +449 -20
  45. package/dist/cjs/dialect.js +117 -76
  46. package/dist/cjs/engine-config.d.ts +7 -6
  47. package/dist/cjs/errors.d.ts +126 -6
  48. package/dist/cjs/errors.js +256 -13
  49. package/dist/cjs/generate.d.ts +57 -7
  50. package/dist/cjs/generate.js +635 -279
  51. package/dist/cjs/global-filters-config.d.ts +59 -0
  52. package/dist/cjs/global-filters-config.js +367 -0
  53. package/dist/cjs/index.d.ts +9 -4
  54. package/dist/cjs/index.js +10 -2
  55. package/dist/cjs/introspect.d.ts +31 -6
  56. package/dist/cjs/introspect.js +108 -50
  57. package/dist/cjs/log-codes.d.ts +12 -0
  58. package/dist/cjs/log-codes.js +77 -0
  59. package/dist/cjs/logger.d.ts +87 -0
  60. package/dist/cjs/logger.js +98 -0
  61. package/dist/cjs/mssql.d.ts +8 -5
  62. package/dist/cjs/mssql.js +352 -65
  63. package/dist/cjs/mysql.d.ts +3 -5
  64. package/dist/cjs/mysql.js +420 -70
  65. package/dist/cjs/nested-write.d.ts +3 -0
  66. package/dist/cjs/nested-write.js +179 -114
  67. package/dist/cjs/pipeline-submittable.d.ts +12 -0
  68. package/dist/cjs/pipeline-submittable.js +118 -44
  69. package/dist/cjs/pipeline.js +21 -26
  70. package/dist/cjs/powdb-introspect.d.ts +5 -2
  71. package/dist/cjs/powdb-introspect.js +20 -15
  72. package/dist/cjs/powdb-shared.d.ts +36 -5
  73. package/dist/cjs/powdb-shared.js +67 -8
  74. package/dist/cjs/powdb.d.ts +4 -0
  75. package/dist/cjs/powdb.js +183 -51
  76. package/dist/cjs/powql.d.ts +394 -51
  77. package/dist/cjs/powql.js +1829 -284
  78. package/dist/cjs/prisma-compat.d.ts +23 -15
  79. package/dist/cjs/prisma-compat.js +507 -372
  80. package/dist/cjs/query/aggregates.d.ts +17 -1
  81. package/dist/cjs/query/aggregates.js +261 -29
  82. package/dist/cjs/query/batched-loader.d.ts +25 -6
  83. package/dist/cjs/query/batched-loader.js +103 -20
  84. package/dist/cjs/query/builder.d.ts +325 -184
  85. package/dist/cjs/query/builder.js +1452 -490
  86. package/dist/cjs/query/column-ref.d.ts +61 -0
  87. package/dist/cjs/query/column-ref.js +73 -0
  88. package/dist/cjs/query/compound-unique.d.ts +21 -0
  89. package/dist/cjs/query/compound-unique.js +98 -18
  90. package/dist/cjs/query/deferred.d.ts +53 -16
  91. package/dist/cjs/query/expr.d.ts +217 -0
  92. package/dist/cjs/query/expr.js +859 -0
  93. package/dist/cjs/query/filters.d.ts +155 -53
  94. package/dist/cjs/query/filters.js +613 -76
  95. package/dist/cjs/query/index.d.ts +2 -0
  96. package/dist/cjs/query/index.js +7 -1
  97. package/dist/cjs/query/json-value.d.ts +43 -0
  98. package/dist/cjs/query/json-value.js +59 -0
  99. package/dist/cjs/query/normalize-args.d.ts +87 -0
  100. package/dist/cjs/query/normalize-args.js +527 -0
  101. package/dist/cjs/query/option-surface.d.ts +16 -4
  102. package/dist/cjs/query/option-surface.js +57 -14
  103. package/dist/cjs/query/relation-names.d.ts +14 -0
  104. package/dist/cjs/query/relation-names.js +77 -18
  105. package/dist/cjs/query/relations.d.ts +30 -3
  106. package/dist/cjs/query/relations.js +219 -63
  107. package/dist/cjs/query/row-lock.d.ts +49 -0
  108. package/dist/cjs/query/row-lock.js +138 -0
  109. package/dist/cjs/query/sql-comment.d.ts +161 -0
  110. package/dist/cjs/query/sql-comment.js +381 -0
  111. package/dist/cjs/query/stream-idle.d.ts +132 -0
  112. package/dist/cjs/query/stream-idle.js +212 -0
  113. package/dist/cjs/query/table-client.d.ts +101 -0
  114. package/dist/cjs/query/table-client.js +74 -0
  115. package/dist/cjs/query/types.d.ts +677 -298
  116. package/dist/cjs/query/utils.d.ts +105 -6
  117. package/dist/cjs/query/utils.js +426 -19
  118. package/dist/cjs/query/warn-registry.d.ts +18 -0
  119. package/dist/cjs/query/warn-registry.js +18 -0
  120. package/dist/cjs/query/where-compile.d.ts +138 -14
  121. package/dist/cjs/query/where-compile.js +513 -29
  122. package/dist/cjs/query/where.d.ts +159 -59
  123. package/dist/cjs/query/where.js +716 -246
  124. package/dist/cjs/query/writes.d.ts +112 -50
  125. package/dist/cjs/query/writes.js +1498 -200
  126. package/dist/cjs/query-events.d.ts +4 -2
  127. package/dist/cjs/query-events.js +3 -1
  128. package/dist/cjs/realtime.d.ts +7 -3
  129. package/dist/cjs/realtime.js +13 -2
  130. package/dist/cjs/schema-builder.d.ts +55 -4
  131. package/dist/cjs/schema-builder.js +382 -18
  132. package/dist/cjs/schema-metadata.d.ts +21 -1
  133. package/dist/cjs/schema-metadata.js +29 -7
  134. package/dist/cjs/schema-sql.d.ts +75 -5
  135. package/dist/cjs/schema-sql.js +497 -45
  136. package/dist/cjs/schema.d.ts +26 -5
  137. package/dist/cjs/schema.js +110 -13
  138. package/dist/cjs/sql-placeholders.d.ts +56 -0
  139. package/dist/cjs/sql-placeholders.js +204 -0
  140. package/dist/cjs/sqlite.d.ts +75 -20
  141. package/dist/cjs/sqlite.js +593 -95
  142. package/dist/cjs/supplied-pool-types.d.ts +48 -0
  143. package/dist/cjs/supplied-pool-types.js +256 -0
  144. package/dist/cjs/transaction-control.d.ts +51 -0
  145. package/dist/cjs/transaction-control.js +316 -0
  146. package/dist/cjs/transaction-state.d.ts +342 -0
  147. package/dist/cjs/transaction-state.js +707 -0
  148. package/dist/cjs/typed-sql.d.ts +37 -3
  149. package/dist/cjs/typed-sql.js +102 -8
  150. package/dist/cli/code-first-tags.d.ts +62 -0
  151. package/dist/cli/code-first-tags.js +120 -0
  152. package/dist/cli/compile-query.js +9 -0
  153. package/dist/cli/config.d.ts +35 -6
  154. package/dist/cli/config.js +16 -1
  155. package/dist/cli/destructive.d.ts +7 -4
  156. package/dist/cli/destructive.js +224 -20
  157. package/dist/cli/error-catalog.js +36 -0
  158. package/dist/cli/error-text.d.ts +15 -0
  159. package/dist/cli/error-text.js +37 -0
  160. package/dist/cli/index.d.ts +62 -4
  161. package/dist/cli/index.js +569 -112
  162. package/dist/cli/local-server-guard.d.ts +53 -0
  163. package/dist/cli/local-server-guard.js +113 -0
  164. package/dist/cli/local-server-output.d.ts +41 -0
  165. package/dist/cli/local-server-output.js +98 -0
  166. package/dist/cli/mcp.d.ts +8 -0
  167. package/dist/cli/mcp.js +121 -34
  168. package/dist/cli/migrate.d.ts +113 -5
  169. package/dist/cli/migrate.js +261 -21
  170. package/dist/cli/observe.d.ts +6 -1
  171. package/dist/cli/observe.js +84 -54
  172. package/dist/cli/pii-predicate-guard.d.ts +43 -23
  173. package/dist/cli/pii-predicate-guard.js +100 -35
  174. package/dist/cli/prisma-resolve.js +15 -11
  175. package/dist/cli/prisma-schema.js +12 -4
  176. package/dist/cli/studio-ui.generated.js +1 -1
  177. package/dist/cli/studio.d.ts +14 -2
  178. package/dist/cli/studio.js +246 -71
  179. package/dist/cli/ui.d.ts +1 -1
  180. package/dist/client.d.ts +558 -85
  181. package/dist/client.js +1275 -400
  182. package/dist/connection-guard.d.ts +13 -0
  183. package/dist/connection-guard.js +27 -0
  184. package/dist/connection-url.d.ts +51 -16
  185. package/dist/connection-url.js +63 -34
  186. package/dist/diagnostics.d.ts +81 -0
  187. package/dist/diagnostics.js +144 -0
  188. package/dist/dialect.d.ts +449 -20
  189. package/dist/dialect.js +115 -43
  190. package/dist/engine-config.d.ts +7 -6
  191. package/dist/errors.d.ts +126 -6
  192. package/dist/errors.js +252 -12
  193. package/dist/generate.d.ts +57 -7
  194. package/dist/generate.js +635 -281
  195. package/dist/global-filters-config.d.ts +59 -0
  196. package/dist/global-filters-config.js +361 -0
  197. package/dist/index.d.ts +9 -4
  198. package/dist/index.js +4 -2
  199. package/dist/introspect.d.ts +31 -6
  200. package/dist/introspect.js +106 -50
  201. package/dist/log-codes.d.ts +12 -0
  202. package/dist/log-codes.js +74 -0
  203. package/dist/logger.d.ts +87 -0
  204. package/dist/logger.js +93 -0
  205. package/dist/mssql.d.ts +8 -5
  206. package/dist/mssql.js +354 -67
  207. package/dist/mysql.d.ts +3 -5
  208. package/dist/mysql.js +423 -73
  209. package/dist/nested-write.d.ts +3 -0
  210. package/dist/nested-write.js +177 -112
  211. package/dist/pipeline-submittable.d.ts +12 -0
  212. package/dist/pipeline-submittable.js +117 -44
  213. package/dist/pipeline.js +21 -26
  214. package/dist/powdb-introspect.d.ts +5 -2
  215. package/dist/powdb-introspect.js +21 -16
  216. package/dist/powdb-shared.d.ts +36 -5
  217. package/dist/powdb-shared.js +65 -8
  218. package/dist/powdb.d.ts +4 -0
  219. package/dist/powdb.js +183 -51
  220. package/dist/powql.d.ts +394 -51
  221. package/dist/powql.js +1836 -291
  222. package/dist/prisma-compat.d.ts +23 -15
  223. package/dist/prisma-compat.js +508 -373
  224. package/dist/query/aggregates.d.ts +17 -1
  225. package/dist/query/aggregates.js +263 -32
  226. package/dist/query/batched-loader.d.ts +25 -6
  227. package/dist/query/batched-loader.js +101 -21
  228. package/dist/query/builder.d.ts +325 -184
  229. package/dist/query/builder.js +1458 -495
  230. package/dist/query/column-ref.d.ts +61 -0
  231. package/dist/query/column-ref.js +66 -0
  232. package/dist/query/compound-unique.d.ts +21 -0
  233. package/dist/query/compound-unique.js +97 -19
  234. package/dist/query/deferred.d.ts +53 -16
  235. package/dist/query/expr.d.ts +217 -0
  236. package/dist/query/expr.js +848 -0
  237. package/dist/query/filters.d.ts +155 -53
  238. package/dist/query/filters.js +597 -72
  239. package/dist/query/index.d.ts +2 -0
  240. package/dist/query/index.js +4 -0
  241. package/dist/query/json-value.d.ts +43 -0
  242. package/dist/query/json-value.js +54 -0
  243. package/dist/query/normalize-args.d.ts +87 -0
  244. package/dist/query/normalize-args.js +517 -0
  245. package/dist/query/option-surface.d.ts +16 -4
  246. package/dist/query/option-surface.js +56 -13
  247. package/dist/query/relation-names.d.ts +14 -0
  248. package/dist/query/relation-names.js +77 -19
  249. package/dist/query/relations.d.ts +30 -3
  250. package/dist/query/relations.js +220 -67
  251. package/dist/query/row-lock.d.ts +49 -0
  252. package/dist/query/row-lock.js +131 -0
  253. package/dist/query/sql-comment.d.ts +161 -0
  254. package/dist/query/sql-comment.js +374 -0
  255. package/dist/query/stream-idle.d.ts +132 -0
  256. package/dist/query/stream-idle.js +203 -0
  257. package/dist/query/table-client.d.ts +101 -0
  258. package/dist/query/table-client.js +70 -0
  259. package/dist/query/types.d.ts +677 -298
  260. package/dist/query/utils.d.ts +105 -6
  261. package/dist/query/utils.js +414 -18
  262. package/dist/query/warn-registry.d.ts +18 -0
  263. package/dist/query/warn-registry.js +18 -0
  264. package/dist/query/where-compile.d.ts +138 -14
  265. package/dist/query/where-compile.js +508 -31
  266. package/dist/query/where.d.ts +159 -59
  267. package/dist/query/where.js +711 -248
  268. package/dist/query/writes.d.ts +112 -50
  269. package/dist/query/writes.js +1496 -202
  270. package/dist/query-events.d.ts +4 -2
  271. package/dist/query-events.js +3 -1
  272. package/dist/realtime.d.ts +7 -3
  273. package/dist/realtime.js +13 -2
  274. package/dist/schema-builder.d.ts +55 -4
  275. package/dist/schema-builder.js +381 -18
  276. package/dist/schema-metadata.d.ts +21 -1
  277. package/dist/schema-metadata.js +28 -7
  278. package/dist/schema-sql.d.ts +75 -5
  279. package/dist/schema-sql.js +494 -44
  280. package/dist/schema.d.ts +26 -5
  281. package/dist/schema.js +109 -13
  282. package/dist/sql-placeholders.d.ts +56 -0
  283. package/dist/sql-placeholders.js +200 -0
  284. package/dist/sqlite.d.ts +75 -20
  285. package/dist/sqlite.js +597 -99
  286. package/dist/supplied-pool-types.d.ts +48 -0
  287. package/dist/supplied-pool-types.js +247 -0
  288. package/dist/transaction-control.d.ts +51 -0
  289. package/dist/transaction-control.js +311 -0
  290. package/dist/transaction-state.d.ts +342 -0
  291. package/dist/transaction-state.js +696 -0
  292. package/dist/typed-sql.d.ts +37 -3
  293. package/dist/typed-sql.js +100 -8
  294. package/package.json +7 -4
  295. package/skills/turbine-orm/SKILL.md +56 -6
@@ -0,0 +1,848 @@
1
+ /**
2
+ * turbine-orm: SQL expressions in update data.
3
+ *
4
+ * `expr.greatest(col('viewCount'), 100)` and friends are branded node objects,
5
+ * and `expr.excluded('events')` names the incoming row of an upsert. Both are
6
+ * written into the SQL TEXT, so both carry a symbol brand: `JSON.parse` cannot
7
+ * produce one, so request data can never become an expression (the same
8
+ * reasoning as `col()` and `UNSAFE`). `Symbol.for` so the ESM and CJS copies of
9
+ * the package recognize each other's nodes. A plain look-alike object
10
+ * (`{ kind: 'greatest', args: [...] }`) is a VALUE, and on a non-JSON column
11
+ * the write path refuses it.
12
+ *
13
+ * The public types live in `query/types.ts` (`SqlExpr`, `ExcludedRef`). This
14
+ * module holds the node constructors, their validation, and
15
+ * {@link compileSetExpr}: the ONE traversal that produces an expression's SQL,
16
+ * its bound parameters and its cache fingerprint together, so the SET build,
17
+ * the cache-hit param collector and the fingerprint cannot disagree (the
18
+ * failure class behind the where-clause cache bugs).
19
+ *
20
+ * Where expressions are accepted: `update`, `updateMany`, `upsert`'s `update`,
21
+ * `createMany`'s `onConflict.update`, and nested update data (which runs
22
+ * through `update()`). Not in `create` / `createMany` data, where the write
23
+ * bind seam refuses a node.
24
+ */
25
+ import { UnsupportedFeatureError, ValidationError } from '../errors.js';
26
+ import { isColumnRef, isPlainColumnRefShape } from './column-ref.js';
27
+ import { isPlainRecord } from './filters.js';
28
+ import { pruneUndefinedArgs } from './normalize-args.js';
29
+ import { resolveColumnName } from './utils.js';
30
+ import { fingerprintWhere } from './where.js';
31
+ import { walkWhere } from './where-compile.js';
32
+ /** The brand every {@link SqlExpr} node carries. */
33
+ export const SQL_EXPR = Symbol.for('turbine-orm.sqlExpr');
34
+ /** The brand an {@link ExcludedRef} carries (the incoming row of an upsert). */
35
+ export const EXCLUDED_REF = Symbol.for('turbine-orm.excludedRef');
36
+ const EXPR_KINDS = new Set([
37
+ 'add',
38
+ 'sub',
39
+ 'mul',
40
+ 'div',
41
+ 'coalesce',
42
+ 'greatest',
43
+ 'least',
44
+ 'case',
45
+ 'now',
46
+ 'excluded',
47
+ ]);
48
+ /** Operand count bounds for coalesce / greatest / least. */
49
+ const MIN_VARIADIC = 2;
50
+ const MAX_VARIADIC = 8;
51
+ /** Arm count bounds for case. */
52
+ const MAX_CASE_ARMS = 16;
53
+ /** Nesting bound, so a hostile tree cannot exhaust the stack. */
54
+ const MAX_EXPR_DEPTH = 32;
55
+ // ---------------------------------------------------------------------------
56
+ // Constructors
57
+ // ---------------------------------------------------------------------------
58
+ function makeNode(kind, args, extra) {
59
+ const node = { kind, args: Object.freeze([...args]), ...extra };
60
+ // Non-enumerable, like `col()`'s brand: the node logs and compares as
61
+ // `{ kind, args }`, and no `Object.keys` walker ever sees the symbol.
62
+ Object.defineProperty(node, SQL_EXPR, { value: true, enumerable: false });
63
+ return Object.freeze(node);
64
+ }
65
+ function describeOperand(value) {
66
+ if (Array.isArray(value))
67
+ return 'an array';
68
+ if (typeof value === 'object' && value !== null) {
69
+ if (isPlainExprShape(value))
70
+ return 'a plain { kind, args } object (not built by expr)';
71
+ return isPlainRecord(value) ? 'a plain object' : `a ${value.constructor?.name ?? 'object'} instance`;
72
+ }
73
+ if (typeof value === 'number')
74
+ return String(value);
75
+ return typeof value;
76
+ }
77
+ /** Refuse an operand no engine can bind as one value. Validated at construction, so the error names the call. */
78
+ function assertOperand(kind, position, value) {
79
+ const at = `expr.${kind}(): operand ${position + 1}`;
80
+ if (value === undefined) {
81
+ throw new ValidationError(`${at} is undefined. Pass null for SQL NULL.`);
82
+ }
83
+ if (value === null || isColumnRef(value) || isSqlExpr(value))
84
+ return;
85
+ switch (typeof value) {
86
+ case 'string':
87
+ case 'boolean':
88
+ case 'bigint':
89
+ return;
90
+ case 'number':
91
+ if (Number.isFinite(value))
92
+ return;
93
+ throw new ValidationError(`${at} is ${value}; a bound number must be finite.`);
94
+ }
95
+ if (value instanceof Date) {
96
+ if (!Number.isNaN(value.getTime()))
97
+ return;
98
+ throw new ValidationError(`${at} is an Invalid Date.`);
99
+ }
100
+ if (value instanceof Uint8Array)
101
+ return;
102
+ if (isPlainColumnRefShape(value)) {
103
+ const name = JSON.stringify(value.col);
104
+ throw new ValidationError(`${at} is a plain { col: ${name} } object, not a column reference. Build one with col(${name}) ` +
105
+ "(import { col } from 'turbine-orm').");
106
+ }
107
+ throw new ValidationError(`${at} is ${describeOperand(value)}. An operand is a scalar (string, number, bigint, boolean, Date, ` +
108
+ 'Buffer or null), col(field), or a node built by expr; a plain object is never an expression.');
109
+ }
110
+ /** Arithmetic takes numbers only: a string, boolean or Date is refused at construction. */
111
+ function assertNumericOperand(kind, position, value) {
112
+ assertOperand(kind, position, value);
113
+ const t = typeof value;
114
+ if (t === 'string' || t === 'boolean' || value instanceof Date || value instanceof Uint8Array) {
115
+ throw new ValidationError(`expr.${kind}(): operand ${position + 1} is a ${value instanceof Date ? 'Date' : t}. Arithmetic takes ` +
116
+ 'numbers, bigints, col() of a numeric column, or numeric expressions. Date arithmetic is not supported.');
117
+ }
118
+ if (isSqlExpr(value) && value.kind === 'now') {
119
+ throw new ValidationError(`expr.${kind}(): operand ${position + 1} is expr.now(). Date arithmetic is not supported; ` +
120
+ 'compute the timestamp in JavaScript and pass it as a value.');
121
+ }
122
+ }
123
+ function arithmetic(kind, a, b) {
124
+ assertNumericOperand(kind, 0, a);
125
+ assertNumericOperand(kind, 1, b);
126
+ return makeNode(kind, [a, b]);
127
+ }
128
+ function variadic(kind, operands) {
129
+ if (operands.length < MIN_VARIADIC || operands.length > MAX_VARIADIC) {
130
+ throw new ValidationError(`expr.${kind}() takes ${MIN_VARIADIC} to ${MAX_VARIADIC} operands, got ${operands.length}.`);
131
+ }
132
+ operands.forEach((v, i) => {
133
+ assertOperand(kind, i, v);
134
+ });
135
+ return makeNode(kind, operands);
136
+ }
137
+ function caseNode(arms, otherwise) {
138
+ if (!Array.isArray(arms) || arms.length === 0 || arms.length > MAX_CASE_ARMS) {
139
+ throw new ValidationError(`expr.case() takes an array of 1 to ${MAX_CASE_ARMS} arms ({ when, then }), got ` +
140
+ `${Array.isArray(arms) ? `${arms.length} arms` : describeOperand(arms)}.`);
141
+ }
142
+ const frozen = arms.map((arm, i) => {
143
+ if (!isPlainRecord(arm)) {
144
+ throw new ValidationError(`expr.case(): arm ${i + 1} is ${describeOperand(arm)}; write { when, then }.`);
145
+ }
146
+ const extra = Object.keys(arm).filter((k) => k !== 'when' && k !== 'then');
147
+ if (extra.length > 0) {
148
+ throw new ValidationError(`expr.case(): arm ${i + 1} has unknown key "${extra[0]}"; an arm is { when, then }.`);
149
+ }
150
+ if (!isPlainRecord(arm.when)) {
151
+ throw new ValidationError(`expr.case(): arm ${i + 1}'s when is ${describeOperand(arm.when)}; write a predicate object like \`where\`.`);
152
+ }
153
+ if (!('then' in arm)) {
154
+ throw new ValidationError(`expr.case(): arm ${i + 1} has no then. Pass null for SQL NULL.`);
155
+ }
156
+ assertOperand('case', i, arm.then);
157
+ // biome-ignore lint/suspicious/noThenProperty: SQL's CASE WHEN ... THEN; a value operand, never a function, so never thenable
158
+ return Object.freeze({ when: arm.when, then: arm.then });
159
+ });
160
+ if (otherwise !== undefined)
161
+ assertOperand('case', arms.length, otherwise);
162
+ return makeNode('case', frozen, otherwise === undefined ? undefined : { else: otherwise });
163
+ }
164
+ /**
165
+ * Builders for SQL expressions in update data. Each returns a frozen, branded
166
+ * node; the SQL is generated per engine when the update compiles.
167
+ *
168
+ * ```ts
169
+ * import { col, expr } from 'turbine-orm';
170
+ * await db.posts.update({
171
+ * where: { id },
172
+ * data: {
173
+ * viewCount: expr.greatest(col('viewCount'), 100),
174
+ * score: expr.add(col('score'), expr.mul(col('bonus'), 2)),
175
+ * editedAt: expr.now(),
176
+ * },
177
+ * });
178
+ * ```
179
+ */
180
+ export const expr = Object.freeze({
181
+ /** `a + b`. Numbers, bigints, numeric columns and numeric expressions. */
182
+ add(a, b) {
183
+ return arithmetic('add', a, b);
184
+ },
185
+ /** `a - b`. */
186
+ sub(a, b) {
187
+ return arithmetic('sub', a, b);
188
+ },
189
+ /** `a * b`. */
190
+ mul(a, b) {
191
+ return arithmetic('mul', a, b);
192
+ },
193
+ /**
194
+ * `a / b`. When both operands are integers (an integer column, an integer
195
+ * number or bigint, or an integer expression) the division truncates toward
196
+ * zero on every engine, as the `divide` update operator does on an integer
197
+ * column. With a non-integer operand it is a decimal division.
198
+ */
199
+ div(a, b) {
200
+ return arithmetic('div', a, b);
201
+ },
202
+ /** The first non-NULL operand (2 to 8). */
203
+ coalesce(a, b, ...rest) {
204
+ return variadic('coalesce', [a, b, ...rest]);
205
+ },
206
+ /**
207
+ * The largest operand (2 to 8), IGNORING NULLs on every engine (PostgreSQL's
208
+ * meaning; MySQL and SQLite are rewritten to match). NULL only when every
209
+ * operand is NULL.
210
+ */
211
+ greatest(a, b, ...rest) {
212
+ return variadic('greatest', [a, b, ...rest]);
213
+ },
214
+ /** The smallest operand (2 to 8), ignoring NULLs on every engine. */
215
+ least(a, b, ...rest) {
216
+ return variadic('least', [a, b, ...rest]);
217
+ },
218
+ /**
219
+ * `CASE WHEN ... THEN ... ELSE ... END`. Each arm's `when` is a predicate on
220
+ * the current row's columns written like `where` (no relation filters); the
221
+ * first matching arm wins. Without `otherwise` an unmatched row gets NULL.
222
+ */
223
+ case(arms, otherwise) {
224
+ return caseNode(arms, otherwise);
225
+ },
226
+ /**
227
+ * The database server's current time, stored in the zone convention Turbine
228
+ * reads with: a zone-less `timestamp` / `date` gets the UTC wall clock (unless
229
+ * the client sets `utcTimestamps: false`). On PostgreSQL it is the
230
+ * TRANSACTION's start time; on MySQL, the statement's. Temporal columns only.
231
+ */
232
+ now() {
233
+ return makeNode('now', []);
234
+ },
235
+ /**
236
+ * The incoming row's value for `field`: inside `upsert`'s `update` (the value
237
+ * `create` supplies) and `createMany`'s `onConflict.update` (each row's own
238
+ * value). Refused anywhere else.
239
+ */
240
+ excluded(field) {
241
+ if (typeof field !== 'string' || field.length === 0) {
242
+ throw new ValidationError('expr.excluded() takes the name of a field on the table, as a non-empty string.');
243
+ }
244
+ const node = { kind: 'excluded', args: Object.freeze([]), field };
245
+ Object.defineProperty(node, SQL_EXPR, { value: true, enumerable: false });
246
+ Object.defineProperty(node, EXCLUDED_REF, { value: true, enumerable: false });
247
+ return Object.freeze(node);
248
+ },
249
+ });
250
+ /** True only for a node built by {@link expr} (any loaded copy of the package). */
251
+ export function isSqlExpr(value) {
252
+ return (typeof value === 'object' &&
253
+ value !== null &&
254
+ value[SQL_EXPR] === true &&
255
+ typeof value.kind === 'string');
256
+ }
257
+ /** True only for `expr.excluded(...)`. */
258
+ export function isExcludedRef(value) {
259
+ return isSqlExpr(value) && value[EXCLUDED_REF] === true;
260
+ }
261
+ /**
262
+ * True for an UNBRANDED object shaped like a node (`{ kind: 'greatest', args }`),
263
+ * which is what parsed request data produces. The write path refuses it on a
264
+ * non-JSON column rather than storing it as JSON text.
265
+ */
266
+ export function isPlainExprShape(value) {
267
+ if (!isPlainRecord(value) || isSqlExpr(value))
268
+ return false;
269
+ const kind = value.kind;
270
+ return (typeof kind === 'string' && EXPR_KINDS.has(kind) && (Array.isArray(value.args) || typeof value.field === 'string'));
271
+ }
272
+ /** The refusal for {@link isPlainExprShape}. */
273
+ export function plainExprShapeMessage(table, key, kind) {
274
+ return (`A plain { kind: ${JSON.stringify(kind)}, ... } object written to "${table}.${key}" is not an expression. ` +
275
+ `Build one with expr.${EXPR_KINDS.has(kind) ? kind : 'add'}(...) (import { expr } from 'turbine-orm'): ` +
276
+ 'expressions carry a symbol brand so that parsed request data can never become SQL. ' +
277
+ 'To store JSON text in this column, pass `JSON.stringify(value)` or `jsonValue(value)`.');
278
+ }
279
+ /** The refusal for a node at a position that takes values only (create data, `{ set }`). */
280
+ export function exprNotAllowedMessage(table, key, value) {
281
+ const what = isColumnRef(value) ? `col(${JSON.stringify(value.col)})` : `expr.${value.kind}()`;
282
+ return (`${what} cannot be written to "${table}.${key}" here: expressions and column references are accepted as the ` +
283
+ "direct value of a field in update, updateMany, upsert's update and createMany's onConflict.update, " +
284
+ 'not in create / createMany data and not inside an operator such as { set }.');
285
+ }
286
+ const NUMERIC_TYPES = new Set([
287
+ 'int2',
288
+ 'int4',
289
+ 'int8',
290
+ 'smallint',
291
+ 'integer',
292
+ 'int',
293
+ 'bigint',
294
+ 'tinyint',
295
+ 'mediumint',
296
+ 'serial',
297
+ 'bigserial',
298
+ 'smallserial',
299
+ 'numeric',
300
+ 'decimal',
301
+ 'dec',
302
+ 'fixed',
303
+ 'float4',
304
+ 'float8',
305
+ 'real',
306
+ 'double precision',
307
+ 'double',
308
+ 'float',
309
+ 'money',
310
+ 'smallmoney',
311
+ 'number',
312
+ ]);
313
+ /** The integer subset of {@link NUMERIC_TYPES}. */
314
+ const INTEGER_TYPES = new Set([
315
+ 'int2',
316
+ 'int4',
317
+ 'int8',
318
+ 'smallint',
319
+ 'integer',
320
+ 'int',
321
+ 'bigint',
322
+ 'tinyint',
323
+ 'mediumint',
324
+ 'serial',
325
+ 'bigserial',
326
+ 'smallserial',
327
+ ]);
328
+ const TEMPORAL_TYPES = {
329
+ timestamptz: 'tz',
330
+ 'timestamp with time zone': 'tz',
331
+ datetimeoffset: 'tz',
332
+ timestamp: 'timestamp',
333
+ 'timestamp without time zone': 'timestamp',
334
+ datetime: 'timestamp',
335
+ datetime2: 'timestamp',
336
+ smalldatetime: 'timestamp',
337
+ date: 'date',
338
+ time: 'time',
339
+ timetz: 'time',
340
+ 'time with time zone': 'time',
341
+ 'time without time zone': 'time',
342
+ };
343
+ /** A column type token, lowercased, without precision or `unsigned`. */
344
+ function normalizeType(dbType) {
345
+ return dbType
346
+ .toLowerCase()
347
+ .replace(/\(\s*\d+(\s*,\s*\d+)?\s*\)/g, '')
348
+ .replace(/\bunsigned\b|\bzerofill\b/g, '')
349
+ .replace(/\s+/g, ' ')
350
+ .trim();
351
+ }
352
+ function columnClass(dbType) {
353
+ if (dbType === undefined)
354
+ return 'unknown';
355
+ const t = normalizeType(dbType);
356
+ if (t === 'json' || t === 'jsonb')
357
+ return 'json';
358
+ if (t.startsWith('_') || t.endsWith('[]'))
359
+ return 'array';
360
+ if (NUMERIC_TYPES.has(t))
361
+ return 'numeric';
362
+ if (Object.hasOwn(TEMPORAL_TYPES, t))
363
+ return 'temporal';
364
+ return 'other';
365
+ }
366
+ /** The {@link NumClass} of a column of this type. */
367
+ function numClassOfType(dbType) {
368
+ if (dbType === undefined)
369
+ return 'unknown';
370
+ const t = normalizeType(dbType);
371
+ if (INTEGER_TYPES.has(t))
372
+ return 'int';
373
+ return NUMERIC_TYPES.has(t) ? 'dec' : 'other';
374
+ }
375
+ /** The {@link NumClass} of a bound value. */
376
+ function numClassOfValue(value) {
377
+ if (value === null)
378
+ return 'null';
379
+ if (typeof value === 'bigint')
380
+ return 'int';
381
+ if (typeof value === 'number')
382
+ return Number.isInteger(value) ? 'int' : 'dec';
383
+ return 'other';
384
+ }
385
+ /**
386
+ * The class of a result computed from these operands: anything non-numeric
387
+ * wins, then a non-integer, then an untyped column; NULL is neutral.
388
+ */
389
+ function combineClasses(classes) {
390
+ let out = 'null';
391
+ for (const c of classes) {
392
+ if (c === 'other')
393
+ return 'other';
394
+ if (c === 'dec' || (c === 'unknown' && out !== 'dec') || (c === 'int' && out === 'null'))
395
+ out = c;
396
+ }
397
+ return out;
398
+ }
399
+ function temporalKind(dbType) {
400
+ if (dbType === undefined)
401
+ return undefined;
402
+ const t = normalizeType(dbType);
403
+ return Object.hasOwn(TEMPORAL_TYPES, t) ? TEMPORAL_TYPES[t] : undefined;
404
+ }
405
+ function columnType(qi, column) {
406
+ return qi.columnPgTypeMap.get(column);
407
+ }
408
+ /**
409
+ * Refuse an expression assigned to a column it cannot target in this release
410
+ * (json/jsonb and array columns). Shared by the walker and the single-row
411
+ * upsert's `excluded` resolution, so both paths refuse the same columns.
412
+ */
413
+ export function assertExprTarget(qi, key, value, operation) {
414
+ const column = resolveColumnName(qi.tableMeta, key);
415
+ if (column === undefined) {
416
+ // The SET builder's own unknown-column error names the key; reach it.
417
+ return qi.toColumn(key);
418
+ }
419
+ const cls = columnClass(columnType(qi, column));
420
+ if (cls === 'json' || cls === 'array') {
421
+ // A bare copy of the incoming row's same-typed column (the onConflict
422
+ // field list, `update: ['payload']`) applies no operator, so it is a plain
423
+ // assignment on any column type.
424
+ if (isBareStructuredCopy(qi, column, value))
425
+ return column;
426
+ const what = isColumnRef(value) ? `col(${JSON.stringify(value.col)})` : `expr.${value.kind}()`;
427
+ throw new ValidationError(`${what} in ${operation} data cannot target "${qi.table}.${key}": it is ${cls === 'json' ? 'a json/jsonb' : 'an array'} ` +
428
+ 'column, and expressions on json and array columns are not supported. Write a plain value instead.');
429
+ }
430
+ return column;
431
+ }
432
+ /** `expr.excluded(f)` assigned whole to a column of exactly f's declared type. */
433
+ function isBareStructuredCopy(qi, column, value) {
434
+ if (!isExcludedRef(value))
435
+ return false;
436
+ const source = resolveColumnName(qi.tableMeta, value.field);
437
+ const type = columnType(qi, column);
438
+ return source !== undefined && type !== undefined && columnType(qi, source) === type;
439
+ }
440
+ /**
441
+ * Compile one expression (or `col()` reference) assigned to `env.targetKey`.
442
+ * The single traversal: SQL, params (through `env.bind`, in emission order)
443
+ * and fingerprint come out of one walk.
444
+ *
445
+ * What an expression stores is the same on every engine, by one rule:
446
+ * arithmetic is integer arithmetic when both operands are integers (integer
447
+ * columns, integer numbers, bigints, integer expressions), and integer
448
+ * division truncates toward zero, as `{ divide }` does on an integer column.
449
+ * Any other numeric operand makes it decimal. A non-integer result is refused
450
+ * on an integer column, because the engines disagree on converting one (round
451
+ * half away from zero, round half to even, truncate, or store it unconverted).
452
+ * The class is decided from the operands' types and the bound values' kinds,
453
+ * both part of the fingerprint, so the SQL text never depends on anything the
454
+ * cache key does not capture.
455
+ */
456
+ export function compileSetExpr(env, value) {
457
+ const { qi } = env;
458
+ assertEngine(qi);
459
+ const column = assertExprTarget(qi, env.targetKey, value, env.operation);
460
+ const state = { env, targetType: columnType(qi, column), reads: new Set() };
461
+ const part = walkOperand(state, value, false, 0);
462
+ if (part.cls === 'dec' && numClassOfType(state.targetType) === 'int') {
463
+ const what = isColumnRef(value) ? `col(${JSON.stringify(value.col)})` : `expr.${value.kind}()`;
464
+ throw new ValidationError(`${what} in ${env.operation} data on "${qi.table}.${env.targetKey}" computes a non-integer value (an operand ` +
465
+ `is a non-integer number or a non-integer column), and "${env.targetKey}" is an integer column ` +
466
+ `(${state.targetType}). Engines convert such a value differently, so it is refused. Use integer operands ` +
467
+ "(expr.div truncates: expr.div(expr.mul(col('n'), 3), 2) is n * 1.5 truncated), or compute the value in " +
468
+ 'JavaScript.');
469
+ }
470
+ return { sql: part.sql, fingerprint: part.fp, reads: state.reads };
471
+ }
472
+ function assertEngine(qi) {
473
+ if (qi.dialect.capabilities.updateExpressions)
474
+ return;
475
+ throw new UnsupportedFeatureError('update expressions (expr / col() in update data)', qi.dialect.name, 'Compute the value in JavaScript and write it as a plain value.');
476
+ }
477
+ function walkOperand(state, value, inArithmetic, depth) {
478
+ if (depth > MAX_EXPR_DEPTH) {
479
+ throw new ValidationError(`An update expression is nested more than ${MAX_EXPR_DEPTH} levels deep.`);
480
+ }
481
+ if (isColumnRef(value)) {
482
+ const column = resolveOperandColumn(state, value.col, 'col', inArithmetic);
483
+ state.reads.add(column);
484
+ return { sql: state.env.column(column), fp: `c:${column}`, cls: numClassOfType(columnType(state.env.qi, column)) };
485
+ }
486
+ if (isSqlExpr(value))
487
+ return walkNode(state, value, inArithmetic, depth);
488
+ // A value leaf. Its shape was validated when the node was built; the bind
489
+ // seam coerces it for the TARGET column, exactly as a plain value would be.
490
+ // The constructors check their own operands, so this catches the one shape
491
+ // they cannot see: a non-number reaching arithmetic through coalesce,
492
+ // greatest, least or a case arm (`expr.add(expr.coalesce('x', 1), 2)`).
493
+ if (inArithmetic &&
494
+ (typeof value === 'string' || typeof value === 'boolean' || value instanceof Date || value instanceof Uint8Array)) {
495
+ throw new ValidationError(`An arithmetic operand in ${state.env.operation} data on "${state.env.qi.table}.${state.env.targetKey}" is a ` +
496
+ `${value instanceof Date ? 'Date' : value instanceof Uint8Array ? 'Uint8Array' : typeof value}. ` +
497
+ 'Arithmetic takes numbers, bigints, col() of a numeric column, or numeric expressions.');
498
+ }
499
+ // The class is part of the fingerprint: it decides PostgreSQL's cast on an
500
+ // arithmetic operand and MySQL's DIV, so two calls differing only in it must
501
+ // not share a cached statement.
502
+ const cls = numClassOfValue(value);
503
+ const arithmetic = inArithmetic ? (cls === 'dec' ? 'dec' : 'int') : undefined;
504
+ return { sql: state.env.bind(value, arithmetic), fp: `?${cls}`, cls };
505
+ }
506
+ /** Resolve a field an operand names, refusing columns no expression may read. */
507
+ function resolveOperandColumn(state, field, how, inArithmetic) {
508
+ const { qi } = state.env;
509
+ const call = how === 'col' ? `col(${JSON.stringify(field)})` : `expr.excluded(${JSON.stringify(field)})`;
510
+ const column = resolveColumnName(qi.tableMeta, field);
511
+ if (column === undefined) {
512
+ throw new ValidationError(`${call} in ${state.env.operation} data on "${qi.table}" names no column. ` +
513
+ `Known fields: ${Object.keys(qi.tableMeta.columnMap).join(', ') || '(none)'}.`);
514
+ }
515
+ const cls = columnClass(columnType(qi, column));
516
+ if (cls === 'json' || cls === 'array') {
517
+ throw new ValidationError(`${call} on "${qi.table}" reads ${cls === 'json' ? 'a json/jsonb' : 'an array'} column; expressions over json and ` +
518
+ 'array columns are not supported.');
519
+ }
520
+ if (inArithmetic && cls !== 'numeric' && cls !== 'unknown') {
521
+ throw new ValidationError(`${call} on "${qi.table}" is used in arithmetic, but "${field}" is not a numeric column ` +
522
+ `(${columnType(qi, column)}). Arithmetic takes numeric columns only.`);
523
+ }
524
+ return column;
525
+ }
526
+ const ARITHMETIC_SQL = { add: '+', sub: '-', mul: '*', div: '/' };
527
+ function walkNode(state, node, inArithmetic, depth) {
528
+ const { env } = state;
529
+ const { qi } = env;
530
+ switch (node.kind) {
531
+ case 'add':
532
+ case 'sub':
533
+ case 'mul':
534
+ case 'div': {
535
+ const cls = columnClass(state.targetType);
536
+ if (!inArithmetic && cls !== 'numeric' && cls !== 'unknown') {
537
+ throw new ValidationError(`expr.${node.kind}() in ${env.operation} data on "${qi.table}.${env.targetKey}": arithmetic needs a numeric ` +
538
+ `column, and this one is ${state.targetType}.`);
539
+ }
540
+ const [a, b] = node.args.map((arg) => walkOperand(state, arg, true, depth + 1));
541
+ const result = combineClasses([a.cls, b.cls]);
542
+ // Integer division is the engine's truncating operator: DIV on MySQL,
543
+ // whose `/` is decimal division even between integers (truncating
544
+ // toward zero, like PostgreSQL's, SQLite's and SQL Server's `/`).
545
+ const op = node.kind === 'div' && result === 'int' ? qi.dialect.capabilities.integerDivision : ARITHMETIC_SQL[node.kind];
546
+ return { sql: `(${a.sql} ${op} ${b.sql})`, fp: `${node.kind}(${a.fp},${b.fp})`, cls: result };
547
+ }
548
+ case 'coalesce': {
549
+ if (node.args.length > MIN_VARIADIC)
550
+ qi.markVariableArity();
551
+ const parts = node.args.map((arg) => walkOperand(state, arg, inArithmetic, depth + 1));
552
+ return {
553
+ sql: `COALESCE(${parts.map((p) => p.sql).join(', ')})`,
554
+ fp: `coalesce(${parts.map((p) => p.fp).join(',')})`,
555
+ cls: combineClasses(parts.map((p) => p.cls)),
556
+ };
557
+ }
558
+ case 'greatest':
559
+ case 'least': {
560
+ if (node.args.length > MIN_VARIADIC)
561
+ qi.markVariableArity();
562
+ const parts = node.args.map((arg) => walkOperand(state, arg, inArithmetic, depth + 1));
563
+ return {
564
+ sql: renderExtreme(qi, node.kind, parts.map((p) => p.sql)),
565
+ fp: `${node.kind}(${parts.map((p) => p.fp).join(',')})`,
566
+ cls: combineClasses(parts.map((p) => p.cls)),
567
+ };
568
+ }
569
+ case 'case':
570
+ return walkCase(state, node, inArithmetic, depth);
571
+ case 'now': {
572
+ if (inArithmetic) {
573
+ throw new ValidationError('expr.now() cannot be an arithmetic operand: date arithmetic is not supported.');
574
+ }
575
+ const rendered = renderNow(state);
576
+ return { sql: rendered, fp: `now:${rendered}`, cls: 'other' };
577
+ }
578
+ case 'excluded': {
579
+ const field = node.field;
580
+ if (!env.excluded) {
581
+ throw new ValidationError(`expr.excluded(${JSON.stringify(field)}) names the incoming row of an upsert (upsert's update, or ` +
582
+ `createMany's onConflict.update); it has no meaning in ${env.operation} data.`);
583
+ }
584
+ // Depth 0 is the whole assigned value: a bare copy, which assertExprTarget
585
+ // has already allowed on a same-typed json or array column.
586
+ const column = depth === 0 && isBareStructuredCopy(qi, assertExprTarget(qi, env.targetKey, node, env.operation), node)
587
+ ? resolveColumnName(qi.tableMeta, field)
588
+ : resolveOperandColumn(state, field, 'excluded', inArithmetic);
589
+ if (env.insertColumns && !env.insertColumns.has(column)) {
590
+ throw new ValidationError(`expr.excluded(${JSON.stringify(field)}) on "${qi.table}" names a field the incoming rows do not supply. ` +
591
+ 'Add it to every row of data, or read the existing value with col() instead.');
592
+ }
593
+ return { sql: env.excluded(column), fp: `x:${column}`, cls: numClassOfType(columnType(qi, column)) };
594
+ }
595
+ default:
596
+ throw new ValidationError(`Unknown expression kind "${String(node.kind)}".`);
597
+ }
598
+ }
599
+ function walkCase(state, node, inArithmetic, depth) {
600
+ const { env } = state;
601
+ const { qi } = env;
602
+ // The arm count is the caller's array length, written into the SQL text, so
603
+ // the statement is not a fixed shape (see BuilderCtx.markVariableArity).
604
+ qi.markVariableArity();
605
+ const sqlParts = [];
606
+ const fpParts = [];
607
+ const classes = [];
608
+ node.args.forEach((arm, i) => {
609
+ const when = caseWhen(state, arm.when, i);
610
+ const predicate = env.predicate(when);
611
+ const then = walkOperand(state, arm.then, inArithmetic, depth + 1);
612
+ sqlParts.push(`WHEN ${predicate} THEN ${then.sql}`);
613
+ classes.push(then.cls);
614
+ fpParts.push(`w:${fingerprintWhere(qi, when)}=>${then.fp}`);
615
+ });
616
+ const hasElse = node.else !== undefined;
617
+ const otherwise = hasElse ? walkOperand(state, node.else, inArithmetic, depth + 1) : undefined;
618
+ if (otherwise)
619
+ classes.push(otherwise.cls);
620
+ return {
621
+ sql: `CASE ${sqlParts.join(' ')}${otherwise ? ` ELSE ${otherwise.sql}` : ''} END`,
622
+ fp: `case(${fpParts.join(';')}|e:${otherwise ? otherwise.fp : '-'})`,
623
+ cls: combineClasses(classes),
624
+ };
625
+ }
626
+ /**
627
+ * Prune and validate one arm's `when`: `undefined` keys drop out (the C1 rule
628
+ * for every `where` position), an arm left with no predicate is refused, and
629
+ * relation filters are refused. The columns it reads are recorded.
630
+ */
631
+ function caseWhen(state, when, index) {
632
+ const { qi } = state.env;
633
+ const pruned = pruneUndefinedArgs(qi.schema, qi.table, { where: when }).where;
634
+ if (!isPlainRecord(pruned) || Object.keys(pruned).length === 0) {
635
+ throw new ValidationError(`expr.case(): arm ${index + 1}'s when on "${qi.table}" has no predicate${pruned === when ? '' : ' (every key was undefined)'}. Every arm needs a condition; use the second argument of expr.case() for the fallback value.`);
636
+ }
637
+ collectPredicateReads(state, pruned, 0);
638
+ return pruned;
639
+ }
640
+ function collectPredicateReads(state, where, depth) {
641
+ const { qi } = state.env;
642
+ if (depth > MAX_EXPR_DEPTH) {
643
+ throw new ValidationError(`An expr.case() predicate is nested more than ${MAX_EXPR_DEPTH} levels deep.`);
644
+ }
645
+ for (const event of walkWhere(qi.whereHost, where)) {
646
+ switch (event.kind) {
647
+ case 'or':
648
+ case 'and':
649
+ for (const c of event.conditions)
650
+ collectPredicateReads(state, c, depth + 1);
651
+ break;
652
+ case 'not':
653
+ collectPredicateReads(state, event.condition, depth + 1);
654
+ break;
655
+ case 'relation':
656
+ throw new ValidationError(`expr.case() on "${qi.table}": "${event.key}" is a relation filter. A case arm's when takes predicates ` +
657
+ "on the row's own columns only.");
658
+ case 'scalar': {
659
+ const column = resolveColumnName(qi.tableMeta, event.key);
660
+ if (column !== undefined)
661
+ state.reads.add(column);
662
+ collectRefReads(state, event.value, 0);
663
+ break;
664
+ }
665
+ }
666
+ }
667
+ }
668
+ /** Record the columns `col()` references inside an operator object read. */
669
+ function collectRefReads(state, value, depth) {
670
+ if (depth > 4)
671
+ return;
672
+ if (isColumnRef(value)) {
673
+ const column = resolveColumnName(state.env.qi.tableMeta, value.col);
674
+ if (column !== undefined)
675
+ state.reads.add(column);
676
+ return;
677
+ }
678
+ if (isPlainRecord(value))
679
+ for (const v of Object.values(value))
680
+ collectRefReads(state, v, depth + 1);
681
+ }
682
+ // ---------------------------------------------------------------------------
683
+ // Per-engine rendering
684
+ // ---------------------------------------------------------------------------
685
+ /**
686
+ * GREATEST / LEAST that ignore NULLs on every engine. PostgreSQL's ignore NULL
687
+ * arguments natively. MySQL's and SQLite's return NULL when ANY argument is
688
+ * NULL, so each position i becomes COALESCE(arg_i, <the others>): when any
689
+ * argument is non-NULL every position is non-NULL and each non-NULL argument
690
+ * still appears as itself in its own position, so the extreme is the extreme of
691
+ * the non-NULL arguments; when all are NULL the result is NULL. SQL Server
692
+ * before 2022 has no GREATEST, and MAX over a VALUES list ignores NULLs.
693
+ *
694
+ * Each argument was compiled once, so a bound value repeated across positions
695
+ * reuses ONE placeholder (every engine here binds by number or by name).
696
+ */
697
+ function renderExtreme(qi, kind, args) {
698
+ switch (qi.dialect.name) {
699
+ case 'postgresql':
700
+ return `${kind === 'greatest' ? 'GREATEST' : 'LEAST'}(${args.join(', ')})`;
701
+ case 'mssql':
702
+ return (`(SELECT ${kind === 'greatest' ? 'MAX' : 'MIN'}(turbine_v) FROM ` +
703
+ `(VALUES ${args.map((a) => `(${a})`).join(', ')}) AS turbine_ext(turbine_v))`);
704
+ case 'mysql':
705
+ case 'sqlite': {
706
+ const fn = qi.dialect.name === 'sqlite' ? (kind === 'greatest' ? 'max' : 'min') : kind.toUpperCase();
707
+ const positions = args.map((a, i) => `COALESCE(${[a, ...args.filter((_, j) => j !== i)].join(', ')})`);
708
+ return `${fn}(${positions.join(', ')})`;
709
+ }
710
+ default:
711
+ throw unspelledExpression(qi, `expr.${kind}()`);
712
+ }
713
+ }
714
+ /**
715
+ * The server's current time for the target column, in the convention the read
716
+ * path uses. Zone-less `timestamp` / `date` columns take the UTC wall clock
717
+ * (PostgreSQL with `utcTimestamps: false` takes the session's local time, which
718
+ * is what that setting reads back), `timestamptz` takes the instant, and on
719
+ * SQLite, where Turbine binds a Date as ISO-8601 UTC text, the same text (the
720
+ * `YYYY-MM-DD` UTC day on a date column).
721
+ */
722
+ function renderNow(state) {
723
+ const { qi, targetKey, operation } = state.env;
724
+ const kind = temporalKind(state.targetType);
725
+ if (kind === undefined || kind === 'time') {
726
+ throw new ValidationError(`expr.now() in ${operation} data cannot target "${qi.table}.${targetKey}"` +
727
+ (state.targetType === undefined ? ': the column has no known type. ' : ` (${state.targetType}): `) +
728
+ 'it fits timestamp, timestamptz, datetime and date columns only.');
729
+ }
730
+ const utc = qi.utcTimestamps !== false;
731
+ switch (qi.dialect.name) {
732
+ case 'postgresql':
733
+ if (kind === 'tz')
734
+ return 'now()';
735
+ if (kind === 'date')
736
+ return utc ? "(now() AT TIME ZONE 'UTC')::date" : 'CURRENT_DATE';
737
+ return utc ? "(now() AT TIME ZONE 'UTC')" : 'LOCALTIMESTAMP';
738
+ case 'mysql':
739
+ return kind === 'date' ? 'UTC_DATE()' : 'UTC_TIMESTAMP(3)';
740
+ case 'sqlite':
741
+ // The text a bound Date takes on SQLite: the UTC day on a date column.
742
+ return kind === 'date' ? "strftime('%Y-%m-%d', 'now')" : "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
743
+ case 'mssql':
744
+ if (kind === 'tz')
745
+ return 'SYSDATETIMEOFFSET()';
746
+ if (kind === 'date')
747
+ return 'CAST(SYSUTCDATETIME() AS date)';
748
+ return 'SYSUTCDATETIME()';
749
+ default:
750
+ throw unspelledExpression(qi, 'expr.now()');
751
+ }
752
+ }
753
+ /**
754
+ * GREATEST / LEAST and now() are still spelled per engine NAME (until they
755
+ * become dialect hooks), so a dialect under any other name is refused rather
756
+ * than handed one engine's spelling as a fallback. That includes a dialect
757
+ * spread from a shipped engine dialect under a new name: its capabilities say
758
+ * it takes update expressions, but nothing here knows how it spells these
759
+ * three. Every other expression is rendered from capabilities.
760
+ */
761
+ function unspelledExpression(qi, what) {
762
+ return new UnsupportedFeatureError(`${what} in update data`, qi.dialect.name, `${what} is spelled per engine, and only postgresql, mysql, sqlite and mssql have a spelling. ` +
763
+ 'Compute the value in JavaScript and write it as a plain value.');
764
+ }
765
+ // ---------------------------------------------------------------------------
766
+ // Single-row upsert: `excluded` is `create`'s value
767
+ // ---------------------------------------------------------------------------
768
+ /**
769
+ * Rewrite every `expr.excluded(f)` in a single-row upsert's `update` to the
770
+ * value `create` supplies for `f`. The incoming row of a one-row upsert IS
771
+ * `create`, so the statement path, the lookup-first path (a plain update) and
772
+ * a nested upsert store the same row. A top-level reference becomes
773
+ * `{ set: value }`, which writes the value exactly as a plain value would be
774
+ * written, on every column type (json and array columns take `set` too). A
775
+ * reference inside an expression becomes a value operand, checked as the
776
+ * node's constructor checks one; the expression is then compiled by the
777
+ * update, with the column-type rules every expression gets. Needs only the
778
+ * table's name and metadata, so the nested-write engine shares it. Returns
779
+ * `update` by reference when it holds no reference.
780
+ */
781
+ export function resolveExcludedFromCreate(target, update, create) {
782
+ if (!update || !Object.values(update).some(holdsExcluded))
783
+ return update;
784
+ const createByColumn = new Map();
785
+ for (const [key, value] of Object.entries(create ?? {})) {
786
+ if (value === undefined)
787
+ continue;
788
+ const column = resolveColumnName(target.tableMeta, key);
789
+ if (column !== undefined)
790
+ createByColumn.set(column, value);
791
+ }
792
+ const incomingValue = (ref) => {
793
+ const column = resolveColumnName(target.tableMeta, ref.field);
794
+ if (column === undefined || !createByColumn.has(column)) {
795
+ throw new ValidationError(`expr.excluded(${JSON.stringify(ref.field)}) in upsert's update on "${target.table}" names a field ` +
796
+ '`create` does not supply. It reads the incoming row, which is `create`.');
797
+ }
798
+ return createByColumn.get(column);
799
+ };
800
+ const out = {};
801
+ for (const [key, value] of Object.entries(update)) {
802
+ if (!holdsExcluded(value)) {
803
+ out[key] = value;
804
+ continue;
805
+ }
806
+ out[key] = isExcludedRef(value)
807
+ ? { set: incomingValue(value) }
808
+ : replaceExcluded(value, incomingValue, 0);
809
+ }
810
+ return out;
811
+ }
812
+ function holdsExcluded(value, depth = 0) {
813
+ if (depth > MAX_EXPR_DEPTH || !isSqlExpr(value))
814
+ return false;
815
+ if (isExcludedRef(value))
816
+ return true;
817
+ if (value.kind === 'case') {
818
+ const node = value;
819
+ return node.args.some((arm) => holdsExcluded(arm.then, depth + 1)) || holdsExcluded(node.else, depth + 1);
820
+ }
821
+ return value.args.some((arg) => holdsExcluded(arg, depth + 1));
822
+ }
823
+ function replaceExcluded(node, incomingValue, depth) {
824
+ if (depth > MAX_EXPR_DEPTH) {
825
+ throw new ValidationError(`An update expression is nested more than ${MAX_EXPR_DEPTH} levels deep.`);
826
+ }
827
+ // The value `create` supplies takes the reference's place, so it must pass
828
+ // the check the node's constructor applies to an operand in that position.
829
+ const arithmeticNode = node.kind === 'add' || node.kind === 'sub' || node.kind === 'mul' || node.kind === 'div';
830
+ const swap = (operand, position) => {
831
+ if (isExcludedRef(operand)) {
832
+ const value = incomingValue(operand);
833
+ if (arithmeticNode)
834
+ assertNumericOperand(node.kind, position, value);
835
+ else
836
+ assertOperand(node.kind, position, value);
837
+ return value;
838
+ }
839
+ return isSqlExpr(operand) ? replaceExcluded(operand, incomingValue, depth + 1) : operand;
840
+ };
841
+ if (node.kind === 'case') {
842
+ const c = node;
843
+ // biome-ignore lint/suspicious/noThenProperty: SQL's CASE WHEN ... THEN; a value operand, never a function
844
+ const arms = c.args.map((arm, i) => Object.freeze({ when: arm.when, then: swap(arm.then, i) }));
845
+ return makeNode('case', arms, c.else === undefined ? undefined : { else: swap(c.else, arms.length) });
846
+ }
847
+ return makeNode(node.kind, node.args.map(swap));
848
+ }