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
@@ -10,17 +10,37 @@
10
10
  * stay class-resident, reached through the ctx. See builder.ts for the thin
11
11
  * delegating methods and the async execute wrappers.
12
12
  */
13
+ import { maxBindParameters, } from '../dialect.js';
13
14
  import { NotFoundError, OptimisticLockError, UnsupportedFeatureError, ValidationError } from '../errors.js';
14
- import { camelToSnake, snakeToCamel } from '../schema.js';
15
- import { assertMutationWhereIdentifiesOneRow, expandCompoundUniqueWhere, upsertWhereMatchesCreate, } from './compound-unique.js';
16
- import { isUnmatchedPlainObject, UPDATE_OPERATOR_KEYS } from './filters.js';
15
+ import { camelToSnake } from '../schema.js';
16
+ import { isColumnRef } from './column-ref.js';
17
+ import { assertMutationWhereIdentifiesOneRow, expandCompoundUniqueWhere, otherCollidingUniqueKey, uniqueKeyNames, upsertCreateCollidingKey, upsertWhereMatchesCreate, } from './compound-unique.js';
18
+ import { compileSetExpr, expr as exprBuilders, exprNotAllowedMessage, isPlainExprShape, isSqlExpr, plainExprShapeMessage, resolveExcludedFromCreate, } from './expr.js';
19
+ import { isPlainRecord, isUnmatchedPlainObject, UPDATE_OPERATOR_KEYS } from './filters.js';
20
+ import { isJsonValue, serializeJsonValue } from './json-value.js';
21
+ import { pruneUndefinedArgs } from './normalize-args.js';
17
22
  import { resolveSkipGlobalFilters, resolveUnsafeFlag } from './types.js';
18
23
  import { canonicalWriteEntries, coerceTemporalValue, resolveColumnName } from './utils.js';
19
24
  import * as whereMod from './where.js';
20
25
  /**
21
- * Normalize one `data` value before it is bound as a write param.
26
+ * Normalize one `data` value before it is bound as a write param. This is THE
27
+ * write bind seam: create, createMany, update, updateMany, both halves of
28
+ * upsert, the cache-hit param collectors and (through those public methods)
29
+ * the nested-write engine all bind through it.
22
30
  *
23
- * Two value shapes need rewriting, both a JS `Date` on a temporal column:
31
+ * JSON columns (json / jsonb):
32
+ *
33
+ * - A JS array is serialized with `JSON.stringify`. The drivers' default
34
+ * array serialization is a Postgres ARRAY literal, so `[]` was stored as the
35
+ * object `{}`, `["a"]` failed with 22P02, and `createMany`'s UNNEST
36
+ * transpose flattened `[1, 2, 3]` into three rows.
37
+ * - A {@link jsonValue} wrapper is serialized with `JSON.stringify`, which is
38
+ * how a JSON string or a JSON `null` is written.
39
+ * - A JS string is RAW JSON TEXT and passes through untouched, so
40
+ * `JSON.stringify(value)` callers keep storing the parsed value. A plain
41
+ * object passes through too: every driver already serializes it as JSON.
42
+ *
43
+ * Temporal columns, a JS `Date`:
24
44
  *
25
45
  * 1. A time-of-day column (`time` / `timetz`), the driver serializes a `Date`
26
46
  * as a full ISO timestamp with the process offset and Postgres answers
@@ -30,45 +50,246 @@ import * as whereMod from './where.js';
30
50
  * non-UTC process the stored value is wrong and (because the read path
31
51
  * interprets an offset-less value as UTC) does not round-trip. Rewritten to
32
52
  * the UTC components, mirroring `parseDbDate`. See `toLocalDateTimeLiteral`
33
- * in query/utils.ts.
53
+ * in query/utils.ts. `timestamptz` is deliberately untouched: it stores a
54
+ * real instant and the driver's local-offset string is already correct.
55
+ *
56
+ * A plain object or an array bound to a known scalar column (text, int, uuid,
57
+ * ...) is refused with E003, and so is a `Map` or `Set` anywhere; see
58
+ * {@link assertScalarBindable} and {@link assertNotCollection}.
34
59
  *
35
- * `timestamptz` is deliberately untouched: it stores a real instant and the
36
- * driver's local-offset string is already correct for it.
60
+ * Expressions (`expr.*`, `col()`) never bind as a value: the update paths
61
+ * compile them before reaching here, so one that arrives is in a position that
62
+ * takes values only (create data, `{ set }`) and is refused. An unbranded
63
+ * object shaped like one is refused on a non-JSON column on every engine.
37
64
  *
38
- * Every other value, including a string that is already a valid literal, is
39
- * returned by identity, so this is a byte-for-byte no-op for every non-temporal
40
- * column.
65
+ * Every other value is returned by identity, so a primitive costs one check
66
+ * and no lookup. The exception is a string on an engine that defines the
67
+ * dialect's `checkTextWrite` (SQLite), which is checked against its column's
68
+ * type first; see {@link checkTextWrite}.
41
69
  */
42
70
  export function coerceWriteValue(qi, key, value) {
43
- // Cheap shape check first: the common path costs one check and no lookup.
44
- if (!(value instanceof Date) && !Array.isArray(value))
71
+ if (typeof value === 'string') {
72
+ if (qi.dialect.checkTextWrite !== undefined)
73
+ checkTextWrite(qi, key, value);
74
+ return value;
75
+ }
76
+ if (value === null || typeof value !== 'object')
45
77
  return value;
78
+ if (isSqlExpr(value) || isColumnRef(value)) {
79
+ throw new ValidationError(exprNotAllowedMessage(qi.table, key, value));
80
+ }
81
+ assertNotCollection(qi, key, value);
46
82
  // THE key-resolution rule, shared with `toColumn` (the SQL side of this very
47
- // statement) so the two can never disagree about which column a key names:
48
- // resolving through `columnMap` alone missed the snake_case COLUMN spelling
49
- // that the SQL builders accept, so those writes bound an unprocessed value
50
- // under an identical statement. Non-throwing, because this also runs on the
51
- // cache-HIT param-collect path, where an unknown key must not turn into a
52
- // different error than the build path already raises.
83
+ // statement) so the two can never disagree about which column a key names.
84
+ // Non-throwing, because this also runs on the cache-HIT param-collect path,
85
+ // where an unknown key must not turn into a different error than the build
86
+ // path already raises.
53
87
  const column = resolveColumnName(qi.tableMeta, key);
54
- if (!column)
88
+ const dbType = column ? qi.columnPgTypeMap.get(column) : undefined;
89
+ if (isJsonValue(value))
90
+ return jsonText(qi, key, value.value);
91
+ if (dbType !== undefined && whereMod.isJsonColumnType(qi, dbType)) {
92
+ return Array.isArray(value) ? jsonText(qi, key, value) : value;
93
+ }
94
+ if (isPlainExprShape(value)) {
95
+ throw new ValidationError(plainExprShapeMessage(qi.table, key, String(value.kind)));
96
+ }
97
+ assertScalarBindable(qi, key, dbType, value);
98
+ if (!(value instanceof Date) && !Array.isArray(value))
55
99
  return value;
56
- // Metadata generated by an older Turbine still carries per-column types
57
- // (`pgTypes` has always been part of TableMetadata), and a column missing
58
- // from the map resolves to undefined → no coercion, today's behavior.
59
- return coerceTemporalValue(qi.columnPgTypeMap.get(column), value, utcDateTimeWrites(qi));
100
+ // A column missing from the type map resolves to undefined → no coercion.
101
+ // The gate is where.ts's, so a written Date and a compared Date take the
102
+ // same form: a `date` column the UTC date part on every SQL engine, a
103
+ // zone-less `timestamp` the UTC literal on PostgreSQL only.
104
+ return coerceTemporalValue(dbType, value, whereMod.utcDateTimeRewrite(qi, dbType));
60
105
  }
61
106
  /**
62
- * Whether the zone-less `date` / `timestamp` UTC rewrite applies.
63
- *
64
- * PostgreSQL only: MySQL's `TIMESTAMP`/`DATETIME` and SQL Server's
65
- * `datetime2` are converted or bound by their own drivers, and MySQL in
66
- * particular reads a zone-less literal in the SESSION time zone, so a UTC
67
- * literal would be misread there. `utcTimestamps: false` opts out, it is the
68
- * same switch that turns off the UTC READ parsing, so the two stay symmetric.
107
+ * Ask the engine whether it stores `value` in the column `key` names as that
108
+ * column's type (the dialect's `checkTextWrite`); E003 when it would not.
109
+ * An unknown key is left to the build path's own error, as in
110
+ * {@link coerceWriteValue}.
111
+ */
112
+ function checkTextWrite(qi, key, value) {
113
+ const column = resolveColumnName(qi.tableMeta, key);
114
+ const columnType = column ? qi.columnPgTypeMap.get(column) : undefined;
115
+ if (!column || columnType === undefined)
116
+ return;
117
+ const refusal = qi.dialect.checkTextWrite?.(qi.table, column, columnType, value);
118
+ if (refusal !== undefined)
119
+ throw new ValidationError(refusal);
120
+ }
121
+ /** `JSON.stringify(value)` for a JSON column, or E003 when it has no JSON form. */
122
+ function jsonText(qi, key, value) {
123
+ assertNotCollection(qi, key, value);
124
+ const text = serializeJsonValue(value);
125
+ if (text === undefined) {
126
+ throw new ValidationError(`Cannot write "${qi.table}.${key}": the value has no JSON form (${describeValue(value)}). ` +
127
+ 'Convert it first, e.g. a bigint to a string or a number.');
128
+ }
129
+ return text;
130
+ }
131
+ function describeValue(value) {
132
+ return typeof value === 'bigint' ? 'it contains a bigint' : `got ${typeof value}`;
133
+ }
134
+ /**
135
+ * PostgreSQL column types on which a plain object or an array can only be a
136
+ * mistake. The driver serializes an object as JSON text and an array as an
137
+ * ARRAY literal, so on a text column `{ first, last }` was stored as
138
+ * `{"first":"A","last":"B"}` and `['a']` as `{"a"}`, silently. A type outside
139
+ * this list (enums, domains, extension and user types) is never refused here,
140
+ * because a domain over jsonb or an extension type may legitimately take one.
69
141
  */
70
- function utcDateTimeWrites(qi) {
71
- return qi.dialect.name === 'postgresql' && qi.utcTimestamps !== false;
142
+ const SCALAR_PG_TYPES = new Set([
143
+ 'text',
144
+ 'varchar',
145
+ 'character varying',
146
+ 'char',
147
+ 'character',
148
+ 'bpchar',
149
+ 'citext',
150
+ 'name',
151
+ 'uuid',
152
+ 'int2',
153
+ 'int4',
154
+ 'int8',
155
+ 'smallint',
156
+ 'integer',
157
+ 'bigint',
158
+ 'numeric',
159
+ 'decimal',
160
+ 'float4',
161
+ 'float8',
162
+ 'real',
163
+ 'double precision',
164
+ 'money',
165
+ 'bool',
166
+ 'boolean',
167
+ 'date',
168
+ 'time',
169
+ 'timetz',
170
+ 'timestamp',
171
+ 'timestamptz',
172
+ 'time without time zone',
173
+ 'time with time zone',
174
+ 'timestamp without time zone',
175
+ 'timestamp with time zone',
176
+ 'interval',
177
+ 'bytea',
178
+ 'inet',
179
+ 'cidr',
180
+ 'macaddr',
181
+ 'xml',
182
+ ]);
183
+ /**
184
+ * Column types, in every engine's spelling, that can only hold a scalar: a
185
+ * plain object or an array written to one is a mistake on every engine (the
186
+ * MySQL and SQLite shims stored it as JSON text, in an INTEGER column too).
187
+ */
188
+ const NON_TEXT_SCALAR_TYPES = new Set([
189
+ 'int',
190
+ 'integer',
191
+ 'tinyint',
192
+ 'smallint',
193
+ 'mediumint',
194
+ 'bigint',
195
+ 'int2',
196
+ 'int4',
197
+ 'int8',
198
+ 'numeric',
199
+ 'decimal',
200
+ 'dec',
201
+ 'real',
202
+ 'float',
203
+ 'double',
204
+ 'double precision',
205
+ 'money',
206
+ 'smallmoney',
207
+ 'bit',
208
+ 'bool',
209
+ 'boolean',
210
+ 'date',
211
+ 'time',
212
+ 'datetime',
213
+ 'datetime2',
214
+ 'smalldatetime',
215
+ 'datetimeoffset',
216
+ 'timestamp',
217
+ 'timestamptz',
218
+ 'year',
219
+ 'uuid',
220
+ 'uniqueidentifier',
221
+ 'blob',
222
+ 'tinyblob',
223
+ 'mediumblob',
224
+ 'longblob',
225
+ 'binary',
226
+ 'varbinary',
227
+ 'image',
228
+ ]);
229
+ /**
230
+ * MySQL's text types. MySQL has a JSON column type, so a text column is not
231
+ * where an object goes, as on PostgreSQL. SQLite and SQL Server have no JSON
232
+ * type: Turbine's own DDL stores json in TEXT / NVARCHAR(MAX), so an object
233
+ * written to a text column there is serialized as JSON on purpose.
234
+ */
235
+ const MYSQL_TEXT_TYPES = new Set([
236
+ 'char',
237
+ 'varchar',
238
+ 'tinytext',
239
+ 'text',
240
+ 'mediumtext',
241
+ 'longtext',
242
+ 'enum',
243
+ 'set',
244
+ ]);
245
+ /** A column type token, lowercased, without a length, precision or sign modifier. */
246
+ function baseColumnType(dbType) {
247
+ return dbType
248
+ .toLowerCase()
249
+ .replace(/\(\s*\d+(\s*,\s*\d+)?\s*\)/g, '')
250
+ .replace(/\b(unsigned|signed|zerofill)\b/g, '')
251
+ .replace(/\s+/g, ' ')
252
+ .trim();
253
+ }
254
+ /** Whether this engine's column of `dbType` refuses a plain object or an array. */
255
+ function refusesStructuredValue(qi, dbType) {
256
+ const columns = qi.dialect.capabilities.structuredValueColumns;
257
+ if (columns === 'pg-scalars')
258
+ return SCALAR_PG_TYPES.has(dbType.toLowerCase());
259
+ const t = baseColumnType(dbType);
260
+ return NON_TEXT_SCALAR_TYPES.has(t) || (columns === 'scalars-and-text' && MYSQL_TEXT_TYPES.has(t));
261
+ }
262
+ /**
263
+ * Refuse a `Map` or `Set` as a written value, on every column. Neither has a
264
+ * stored form: JSON.stringify writes `{}` for both, so the entries were lost
265
+ * without an error, on a json column too.
266
+ */
267
+ function assertNotCollection(qi, key, value) {
268
+ if (!(value instanceof Map) && !(value instanceof Set))
269
+ return;
270
+ const what = value instanceof Map ? 'a Map' : 'a Set';
271
+ throw new ValidationError(`Cannot write ${what} to "${qi.table}.${key}": it has no stored form, and JSON would write it as {} and lose ` +
272
+ `its entries. Convert it first (${value instanceof Map ? 'Object.fromEntries(map)' : '[...set]'}).`);
273
+ }
274
+ /**
275
+ * Refuse a plain object, or an array, bound to a known scalar column. Dates,
276
+ * Buffers, class instances (a Decimal wrapper, an interval) and every
277
+ * primitive are untouched, and so is every array column (`_text`) since its
278
+ * type is not a scalar one. Which text columns refuse one is per engine; see
279
+ * {@link MYSQL_TEXT_TYPES}.
280
+ */
281
+ function assertScalarBindable(qi, key, dbType, value) {
282
+ if (dbType === undefined)
283
+ return;
284
+ const isArray = Array.isArray(value);
285
+ if (!isArray && !isUnmatchedPlainObject(value))
286
+ return;
287
+ if (!refusesStructuredValue(qi, dbType))
288
+ return;
289
+ throw new ValidationError(`Cannot write ${isArray ? 'an array' : 'an object'} to "${qi.table}.${key}" (column type ${dbType}). ` +
290
+ 'Only json/jsonb columns take objects, and only array columns take arrays. ' +
291
+ 'To store JSON text in this column, pass `JSON.stringify(value)` or `jsonValue(value)`.' +
292
+ (isArray ? '' : ' Atomic operators (`{ set: v }`, `{ increment: n }`, ...) apply in update data only.'));
72
293
  }
73
294
  /**
74
295
  * Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
@@ -76,11 +297,157 @@ function utcDateTimeWrites(qi) {
76
297
  * return rows from non-RETURNING engines. Reuses the same parameterized WHERE
77
298
  * builder as reads, so no user value is interpolated.
78
299
  */
79
- export function buildReselectByWhere(qi, whereObj, projection) {
300
+ export function buildReselectByWhere(qi, whereObj, projection, lockRows = false) {
80
301
  const params = [];
81
302
  const clause = whereMod.buildWhereClause(qi, whereObj, params);
82
303
  const where = clause ? ` WHERE ${clause}` : '';
83
- return { sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}`, params };
304
+ return {
305
+ sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}${lockRows ? forUpdate(qi) : ''}`,
306
+ params,
307
+ };
308
+ }
309
+ /**
310
+ * Row-lock suffix for a read inside a reselect write's transaction, so a
311
+ * concurrent write of the same row waits instead of interleaving. Appended
312
+ * where the engine's reselect locks rows (MySQL); elsewhere nothing is.
313
+ */
314
+ function forUpdate(qi) {
315
+ return qi.dialect.capabilities.reselectLocksRows ? ' FOR UPDATE' : '';
316
+ }
317
+ /**
318
+ * Whether a write that sets `columns` may move a row out of `whereObj`, so the
319
+ * row must be re-read by its key rather than by `where`: a soft delete under a
320
+ * `deletedAt: null` global filter, `NOT: { status: 'done' }` with `status:
321
+ * 'done'`, a key change.
322
+ *
323
+ * Read off the compiled predicate, where every column it reads is quoted at
324
+ * any depth (`NOT`, `OR`, a relation filter's correlation, a column reference,
325
+ * the global filter), so no where shape is missed. A column of another table
326
+ * with the same name answers true, which costs one locking read and is never
327
+ * wrong.
328
+ */
329
+ function writeMayMoveRow(qi, whereObj, columns) {
330
+ if (columns.size === 0)
331
+ return false;
332
+ const clause = whereMod.buildWhereClause(qi, whereObj, []);
333
+ if (!clause)
334
+ return false;
335
+ for (const column of columns)
336
+ if (clause.includes(qi.q(column)))
337
+ return true;
338
+ return false;
339
+ }
340
+ /**
341
+ * The columns that identify a row after a write: the primary key, else the
342
+ * first unique key, else none.
343
+ */
344
+ function identityColumns(qi) {
345
+ return qi.tableMeta.primaryKey.length > 0 ? qi.tableMeta.primaryKey : (uniqueKeyNames(qi.tableMeta)[0] ?? []);
346
+ }
347
+ /**
348
+ * Lock the row(s) `whereObj` matches and return the first one's identity key as
349
+ * a `where`, with every key column `data` assigns replaced by its new value, so
350
+ * the row can be re-read after the write moves it. `null` when no row matched.
351
+ *
352
+ * Refused (E017) before anything is written: a table with no key to re-read by,
353
+ * a key column assigned by something whose result is not known here (an
354
+ * expression, `divide`, a non-integer operand), and, unless the call is a
355
+ * full-table write, a `where` that matched more than one row, which only an
356
+ * engine comparing a key loosely produces.
357
+ */
358
+ async function lockMovedRowKey(qi, exec, whereObj, data, operation, manyRows) {
359
+ const identity = identityColumns(qi);
360
+ if (identity.length === 0) {
361
+ throw new UnsupportedFeatureError(`${operation} on "${qi.table}" that changes a column its \`where\` reads`, qi.dialect.name, 'The written row is read back by a key, and the table has no primary key or unique key. Give the table a ' +
362
+ 'primary key, or leave the columns `where` reads out of data.');
363
+ }
364
+ const assigned = new Map();
365
+ for (const [key, value] of writeEntries(qi, data)) {
366
+ const column = resolveColumnName(qi.tableMeta, key);
367
+ if (column !== undefined && identity.includes(column))
368
+ assigned.set(column, [key, value]);
369
+ }
370
+ const params = [];
371
+ const clause = whereMod.buildWhereClause(qi, whereObj, params);
372
+ const sql = `SELECT ${identity.map((c) => qi.q(c)).join(', ')} FROM ${qi.q(qi.table)}${clause ? ` WHERE ${clause}` : ''}${forUpdate(qi)}`;
373
+ const rows = (await exec(sql, params)).rows;
374
+ const row = rows[0];
375
+ if (!row)
376
+ return null;
377
+ if (!manyRows)
378
+ assertOneRowByKey(qi, operation, identity, rows.length);
379
+ const key = {};
380
+ for (const column of identity) {
381
+ const entry = assigned.get(column);
382
+ const next = entry === undefined ? row[column] : movedKeyValue(qi, operation, entry[0], row[column], entry[1]);
383
+ key[fieldOf(qi, column)] = reselectKeyValue(qi, column, next);
384
+ }
385
+ return key;
386
+ }
387
+ /**
388
+ * The value a key column holds after `assigned` is written over `current`: a
389
+ * plain value or `{ set }` as given, and `increment` / `decrement` / `multiply`
390
+ * by an integer computed exactly on an integer key. Anything else is refused
391
+ * (E017) because its result, and so the row, cannot be known before the write.
392
+ */
393
+ function movedKeyValue(qi, operation, key, current, assigned) {
394
+ const op = setOperatorOf(qi, key, assigned);
395
+ if (op === null)
396
+ return assigned;
397
+ if (op === 'set')
398
+ return assigned.set;
399
+ const operand = op === 'expr' ? undefined : assigned[op];
400
+ const base = integerOf(current);
401
+ if (base !== undefined && typeof operand === 'number' && Number.isSafeInteger(operand) && op !== 'divide') {
402
+ const n = BigInt(operand);
403
+ const next = op === 'increment' ? base + n : op === 'decrement' ? base - n : base * n;
404
+ return typeof current === 'number' &&
405
+ next >= BigInt(Number.MIN_SAFE_INTEGER) &&
406
+ next <= BigInt(Number.MAX_SAFE_INTEGER)
407
+ ? Number(next)
408
+ : typeof current === 'bigint'
409
+ ? next
410
+ : next.toString();
411
+ }
412
+ throw new UnsupportedFeatureError(`${operation} on "${qi.table}" that changes the key "${key}" with ${op === 'expr' ? 'an expression' : `\`${op}\``}`, qi.dialect.name, 'The written row is read back by its key, and the new key is not known before the write. Read the row, then ' +
413
+ `write the new key with \`{ set: value }\`${op === 'expr' ? '' : ', or use an integer `increment`, `decrement` or `multiply`'}.`);
414
+ }
415
+ /** The columns the top level of `where` names, for a message. */
416
+ function whereColumns(qi, where) {
417
+ return Object.keys(where).flatMap((key) => {
418
+ const column = where[key] === undefined ? undefined : resolveColumnName(qi.tableMeta, key);
419
+ return column === undefined ? [] : [column];
420
+ });
421
+ }
422
+ /** An integer key value as a bigint, or `undefined` for anything else. */
423
+ function integerOf(value) {
424
+ if (typeof value === 'bigint')
425
+ return value;
426
+ if (typeof value === 'number')
427
+ return Number.isSafeInteger(value) ? BigInt(value) : undefined;
428
+ if (typeof value === 'string' && /^-?[0-9]+$/.test(value))
429
+ return BigInt(value);
430
+ return undefined;
431
+ }
432
+ /**
433
+ * `where` with each top-level key value bound as a reselect binds keys (see
434
+ * {@link whereMod.TEXT_COMPARED_TYPES}), or `where` itself when nothing changes.
435
+ */
436
+ function textSafeKeyWhere(qi, where) {
437
+ let out;
438
+ for (const [key, value] of Object.entries(where)) {
439
+ const column = resolveColumnName(qi.tableMeta, key);
440
+ if (column === undefined)
441
+ continue;
442
+ const plain = isPlainRecord(value) && Object.keys(value).length === 1 && 'equals' in value;
443
+ const raw = plain ? value.equals : value;
444
+ const bound = reselectKeyValue(qi, column, raw);
445
+ if (bound === raw)
446
+ continue;
447
+ out ??= { ...where };
448
+ out[key] = plain ? { equals: bound } : bound;
449
+ }
450
+ return out ?? where;
84
451
  }
85
452
  /**
86
453
  * Build the all-defaults INSERT for a `data` that names no column, via the
@@ -150,31 +517,181 @@ export function buildCreate(qi, args, projection) {
150
517
  };
151
518
  }
152
519
  /**
153
- * Build the `'reselect'` plan for {@link buildCreate}: run the INSERT, then
154
- * `SELECT * WHERE pk = ?`. Returns `undefined` (skipped) unless the active
155
- * dialect's result strategy is `'reselect'`, so the PostgreSQL/RETURNING path
156
- * pays nothing. Not yet wired to a real non-RETURNING engine.
520
+ * Build the `'reselect'` plan for {@link buildCreate} (MySQL): run the INSERT,
521
+ * then `SELECT ... WHERE <key> = ?` by a key whose value is known exactly
522
+ * (see {@link createReselectKey}). Returns `undefined` unless the dialect's
523
+ * result strategy is `'reselect'`, so the RETURNING engines pay nothing.
157
524
  */
158
525
  export function makeCreateReselect(qi, insertSql, insertParams, data, projection) {
159
526
  if (qi.dialect.resultStrategy !== 'reselect')
160
527
  return undefined;
528
+ const key = createReselectKey(qi, data);
161
529
  return async (exec) => {
162
530
  const writeResult = await exec(insertSql, insertParams);
163
- const insertId = qi.mutationInsertId(writeResult);
164
- const conds = [];
165
- const selParams = [];
166
- let idx = 1;
167
- for (const pk of qi.tableMeta.primaryKey) {
168
- const field = qi.tableMeta.reverseColumnMap[pk] ?? snakeToCamel(pk);
169
- // Same rewrite the INSERT's own params got: a temporal PK re-fetched with
170
- // a raw Date would not match the literal that was just written.
171
- selParams.push(coerceWriteValue(qi, field, data[field] ?? data[pk] ?? insertId));
172
- conds.push(`${qi.q(pk)} = ${qi.p(idx++)}`);
173
- }
174
- const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
175
- return exec(`SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}`, selParams);
531
+ const params = key.values.map((value) => value === INSERT_ID
532
+ ? generatedKey(qi, writeResult, key)
533
+ : value === ZERO_INSERT_ID
534
+ ? zeroKey(qi, writeResult)
535
+ : value);
536
+ const where = key.columns.map((c, i) => `${qi.q(c)} = ${qi.p(i + 1)}`).join(' AND ');
537
+ const result = await exec(`SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)} WHERE ${where}`, params);
538
+ assertOneRowByKey(qi, 'create', key.columns, result.rows.length);
539
+ return result;
176
540
  };
177
541
  }
542
+ /** Stands for the driver's generated insert id in a {@link ReselectKey}. */
543
+ const INSERT_ID = Symbol('turbine.insertId');
544
+ /**
545
+ * Stands for a 0 written to an AUTO_INCREMENT key. MySQL reads 0 there as
546
+ * "assign the next value" (unless the session's sql_mode has
547
+ * NO_AUTO_VALUE_ON_ZERO, which stores 0), so the row is re-read by the value
548
+ * the server reports it stored. Re-reading by 0 found nothing, and the create
549
+ * that had just inserted a row failed as "not found".
550
+ */
551
+ const ZERO_INSERT_ID = Symbol('turbine.zeroInsertId');
552
+ /**
553
+ * The key that finds the row a `create` inserted, decided before the INSERT is
554
+ * sent. Without RETURNING, MySQL reports only the AUTO_INCREMENT value it
555
+ * assigned (`insertId`, 0 when there is none), so a key the server fills any
556
+ * other way (`DEFAULT (UUID())`) cannot be read back: re-reading by 0 matched
557
+ * every CHAR key MySQL converts to the number 0, other tenants' rows included.
558
+ *
559
+ * In order: the primary key, when `data` supplies every member or the one it
560
+ * omits is server-numbered ({@link isServerNumberedKey}); else the first unique
561
+ * key `data` supplies in full. Anything else is refused (E017) before the
562
+ * INSERT, naming the fix. A NULL never identifies a row, so it counts as absent.
563
+ */
564
+ function createReselectKey(qi, data) {
565
+ const supplied = new Map();
566
+ for (const [key, value] of writeEntries(qi, data)) {
567
+ const column = resolveColumnName(qi.tableMeta, key);
568
+ // Same rewrite the INSERT's own params got, then the text-key rule.
569
+ if (column !== undefined && value !== null) {
570
+ supplied.set(column, reselectKeyValue(qi, column, coerceWriteValue(qi, key, value)));
571
+ }
572
+ }
573
+ const pk = qi.tableMeta.primaryKey;
574
+ const missing = pk.filter((c) => !supplied.has(c));
575
+ if (pk.length > 0 && (missing.length === 0 || (missing.length === 1 && isServerNumberedKey(qi, missing[0])))) {
576
+ return {
577
+ columns: pk,
578
+ values: pk.map((c) => {
579
+ if (!supplied.has(c))
580
+ return INSERT_ID;
581
+ const value = supplied.get(c);
582
+ return isZeroKeyValue(value) && isServerNumberedKey(qi, c) ? ZERO_INSERT_ID : value;
583
+ }),
584
+ };
585
+ }
586
+ for (const key of uniqueKeyNames(qi.tableMeta)) {
587
+ if (key.every((c) => supplied.has(c)))
588
+ return { columns: key, values: key.map((c) => supplied.get(c)) };
589
+ }
590
+ const fields = (missing.length > 0 ? missing : pk).map((c) => `"${fieldOf(qi, c)}"`).join(', ');
591
+ throw new UnsupportedFeatureError(pk.length > 0
592
+ ? `create on "${qi.table}" without ${fields} in data`
593
+ : `create on "${qi.table}", which has no primary key,`, qi.dialect.name, 'The inserted row is read back by a key, and the engine reports a generated value only for an ' +
594
+ 'AUTO_INCREMENT column. ' +
595
+ (pk.length > 0
596
+ ? `Pass ${fields} in data (for a UUID key, generate it client-side, e.g. crypto.randomUUID()), or make the key AUTO_INCREMENT.`
597
+ : 'Supply every column of a unique key in data, or give the table a primary key.'));
598
+ }
599
+ /**
600
+ * Whether the server numbers this key column itself, so the driver's insert id
601
+ * is its new value. `isGenerated` decides when the metadata records it;
602
+ * metadata that does not (hand-written, or an introspector that leaves it
603
+ * unset) counts an integer column with a default.
604
+ */
605
+ function isServerNumberedKey(qi, column) {
606
+ const col = qi.tableMeta.columns.find((c) => c.name === column);
607
+ if (!col)
608
+ return false;
609
+ if (col.isGenerated !== undefined)
610
+ return col.isGenerated;
611
+ return col.hasDefault && isIntegerColumnType(col.dialectType ?? col.pgType);
612
+ }
613
+ /**
614
+ * Whether a column's database type is an integer type, in the PostgreSQL,
615
+ * MySQL and code-first spellings. A MySQL display width or sign modifier
616
+ * (`int(11) unsigned`) is ignored, except that `tinyint(1)` is MySQL's
617
+ * BOOLEAN and is not counted.
618
+ */
619
+ function isIntegerColumnType(dbType) {
620
+ if (!dbType)
621
+ return false;
622
+ const t = dbType.trim().toLowerCase();
623
+ if (t === 'tinyint(1)')
624
+ return false;
625
+ const base = t
626
+ .replace(/\s+(unsigned|signed|zerofill)\b/g, '')
627
+ .replace(/\s*\(\s*\d+\s*\)$/, '')
628
+ .trim();
629
+ return INTEGER_COLUMN_TYPES.has(base);
630
+ }
631
+ const INTEGER_COLUMN_TYPES = new Set([
632
+ 'tinyint',
633
+ 'smallint',
634
+ 'mediumint',
635
+ 'int',
636
+ 'integer',
637
+ 'bigint',
638
+ 'int2',
639
+ 'int4',
640
+ 'int8',
641
+ 'serial',
642
+ 'bigserial',
643
+ 'smallserial',
644
+ ]);
645
+ function isZeroKeyValue(value) {
646
+ return value === 0 || value === 0n || (typeof value === 'string' && /^0+$/.test(value));
647
+ }
648
+ /**
649
+ * The stored value of a key written as 0 (see {@link ZERO_INSERT_ID}): the
650
+ * insert id the engine reports for the AUTO_INCREMENT column, which is the
651
+ * assigned value, or 0 when the server stored the 0 as written.
652
+ */
653
+ function zeroKey(qi, result) {
654
+ const id = qi.mutationInsertId(result);
655
+ const positive = (typeof id === 'number' && Number.isSafeInteger(id) && id > 0) ||
656
+ (typeof id === 'bigint' && id > 0n) ||
657
+ (typeof id === 'string' && /^[1-9][0-9]*$/.test(id));
658
+ return positive ? id : 0;
659
+ }
660
+ /** The insert id of `result` as a key value, or E017 when the engine reported none. */
661
+ function generatedKey(qi, result, key) {
662
+ const id = qi.mutationInsertId(result);
663
+ const positive = (typeof id === 'number' && Number.isSafeInteger(id) && id > 0) ||
664
+ (typeof id === 'bigint' && id > 0n) ||
665
+ (typeof id === 'string' && /^[1-9][0-9]*$/.test(id));
666
+ if (positive)
667
+ return id;
668
+ const column = key.columns[key.values.indexOf(INSERT_ID)];
669
+ throw new UnsupportedFeatureError(`create on "${qi.table}" without "${fieldOf(qi, column)}" in data`, qi.dialect.name, `The engine reported no generated value for "${fieldOf(qi, column)}", so the inserted row cannot be read back ` +
670
+ `and the insert is rolled back. Pass "${fieldOf(qi, column)}" in data, or make it AUTO_INCREMENT.`);
671
+ }
672
+ /**
673
+ * A key value as a reselect binds it: a number or bigint for a text-class
674
+ * column ({@link whereMod.TEXT_COMPARED_TYPES}) is sent as the text the INSERT
675
+ * stored, since MySQL compares `'0zz' = 0` as true. Converted on every engine,
676
+ * not only MySQL, because one caller (the identity read) is not MySQL-only.
677
+ */
678
+ function reselectKeyValue(qi, column, value) {
679
+ if (typeof value !== 'number' && typeof value !== 'bigint')
680
+ return value;
681
+ const type = qi.columnPgTypeMap.get(column);
682
+ return type !== undefined && whereMod.TEXT_COMPARED_TYPES.has(type.toLowerCase()) ? String(value) : value;
683
+ }
684
+ /**
685
+ * A re-read by a unique key finds one row. A second one means the engine
686
+ * matched the key loosely, so which row is the written one is unknown: refused
687
+ * (E017), which rolls the write back.
688
+ */
689
+ function assertOneRowByKey(qi, operation, columns, rows) {
690
+ if (rows <= 1)
691
+ return;
692
+ throw new UnsupportedFeatureError(`${operation} on "${qi.table}"`, qi.dialect.name, `Re-reading the written row by (${columns.map((c) => fieldOf(qi, c)).join(', ')}) matched ${rows} rows, so the ` +
693
+ 'written one cannot be told apart and the write is rolled back. Pass each key value with its column type.');
694
+ }
178
695
  /**
179
696
  * The `[key, value]` pairs a write's `data` object actually names, in the
180
697
  * table's own column order.
@@ -262,14 +779,81 @@ function assertUniformCreateManyRows(qi, rows, firstKeys) {
262
779
  function quoteList(fields) {
263
780
  return fields.map((f) => `"${f}"`).join(', ');
264
781
  }
265
- export function buildCreateMany(qi, args) {
782
+ /**
783
+ * Refuse a statement that carries more bind parameters than the engine takes.
784
+ * Only the row-major `VALUES` form can get there (PostgreSQL's UNNEST form
785
+ * binds one array per column), and past the limit the failure is a raw driver
786
+ * or protocol error. Executing in chunks would change what one `createMany`
787
+ * is (one statement, one RETURNING set), so the caller chooses the chunking.
788
+ */
789
+ function assertWithinBindLimit(qi, paramCount, rowCount, columnCount) {
790
+ const limit = maxBindParameters(qi.dialect);
791
+ if (limit === undefined || paramCount <= limit)
792
+ return;
793
+ const perChunk = Math.max(1, Math.floor(limit / Math.max(1, columnCount)));
794
+ throw new ValidationError(`createMany on "${qi.table}": ${rowCount} rows of ${columnCount} columns need ${paramCount} bind parameters, ` +
795
+ `over the ${qi.dialect.name} limit of ${limit} per statement. ` +
796
+ `Split the rows into chunks of at most ${perChunk} and call createMany once per chunk ` +
797
+ '(inside one `$transaction` if they must succeed or fail together).');
798
+ }
799
+ export function buildCreateMany(qi, args, overBindLimit = 'refuse', projection) {
800
+ const prepared = prepareCreateMany(qi, args, projection);
801
+ if ('deferred' in prepared)
802
+ return prepared.deferred;
803
+ return emitCreateMany(qi, prepared, prepared.rowValues, overBindLimit, true);
804
+ }
805
+ /**
806
+ * `createMany()` as the statements it runs: one when the rows fit one
807
+ * statement, else consecutive chunks that each do. The whole call is validated
808
+ * once, before any chunk is built, so a refusal (a duplicate conflict key in
809
+ * the last chunk included) sends nothing, and the chunks are sized before any
810
+ * statement is built, so no engine's own per-statement check refuses one.
811
+ *
812
+ * Built eagerly and synchronously: the chunks read state the call set on the
813
+ * builder (`skipGlobalFilters`), which a concurrent call could change across
814
+ * an await.
815
+ */
816
+ export function planCreateMany(qi, args, projection) {
817
+ const prepared = prepareCreateMany(qi, args, projection);
818
+ if ('deferred' in prepared)
819
+ return { chunks: [prepared.deferred], rowsPerChunk: args.data.length };
820
+ const rows = prepared.rowValues;
821
+ const size = rowsPerStatement(qi, prepared);
822
+ if (rows.length <= size) {
823
+ return { chunks: [emitCreateMany(qi, prepared, rows, 'allow', true)], rowsPerChunk: size };
824
+ }
825
+ if (prepared.conflict && !prepared.identity) {
826
+ throw new ValidationError(`createMany onConflict on "${qi.table}": ${rows.length} rows take more than one statement here (at most ` +
827
+ `${size} per statement), and the rows each statement writes cannot be identified (onConflict.update assigns ` +
828
+ "the key, or the only key is a PII column), so two rows equal under the key's comparison rules could merge " +
829
+ `across statements unnoticed. Pass at most ${size} rows per call.`);
830
+ }
831
+ const chunks = [];
832
+ for (let i = 0; i < rows.length; i += size) {
833
+ chunks.push(emitCreateMany(qi, prepared, rows.slice(i, i + size), 'refuse', i === 0));
834
+ }
835
+ return { chunks, rowsPerChunk: size };
836
+ }
837
+ /**
838
+ * Everything `createMany` checks about the call, over every row, or the whole
839
+ * answer when no row-values statement is needed (no rows, or rows of pure
840
+ * defaults).
841
+ */
842
+ function prepareCreateMany(qi, args, projection) {
266
843
  const qt = qi.q(qi.table);
844
+ // Validated on every call (UNSAFE only); it acts on onConflict's update.
845
+ qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
846
+ // The data-independent half of onConflict is checked before the empty-data
847
+ // shortcut, so a call is never valid with no rows and invalid with some.
848
+ const conflict = args.onConflict === undefined ? null : resolveConflictPlan(qi, args);
267
849
  if (args.data.length === 0) {
268
850
  return {
269
- sql: `SELECT * FROM ${qt} WHERE false`,
270
- params: [],
271
- transform: () => [],
272
- tag: `${qi.table}.createMany`,
851
+ deferred: {
852
+ sql: `SELECT * FROM ${qt} WHERE false`,
853
+ params: [],
854
+ transform: () => [],
855
+ tag: `${qi.table}.createMany`,
856
+ },
273
857
  };
274
858
  }
275
859
  assertWritable(qi, 'createMany');
@@ -278,22 +862,35 @@ export function buildCreateMany(qi, args) {
278
862
  }
279
863
  const keys = definedKeys(qi, args.data[0]);
280
864
  assertUniformCreateManyRows(qi, args.data, keys);
865
+ const columns = keys.map((k) => qi.toColumn(k));
866
+ if (conflict)
867
+ assertConflictTargetSupplied(qi, conflict, columns);
281
868
  // No column named by the first row: every row is pure defaults (the bulk
282
869
  // counterpart of `create({ data: {} })`, see buildDefaultValuesInsert). The
283
870
  // uniformity check above has already established that EVERY row is empty.
284
871
  if (keys.length === 0) {
285
872
  return {
286
- sql: buildDefaultValuesInsert(qi, args.data.length, args.skipDuplicates),
287
- params: [],
288
- transform: (result) => result.rows.map((row) => parseWriteRow(qi, row)),
289
- tag: `${qi.table}.createMany`,
873
+ deferred: {
874
+ sql: buildDefaultValuesInsert(qi, args.data.length, args.skipDuplicates, projection),
875
+ params: [],
876
+ transform: (result) => result.rows.map((row) => parseWriteRow(qi, row, projection)),
877
+ tag: `${qi.table}.createMany`,
878
+ },
290
879
  };
291
880
  }
292
- const columns = keys.map((k) => qi.toColumn(k));
293
881
  const rowValues = args.data.map((row) => {
294
882
  const record = row;
295
883
  return keys.map((key) => coerceWriteValue(qi, key, record[key]));
296
884
  });
885
+ let identity = null;
886
+ if (conflict) {
887
+ // Over every row, so a chunked call refuses before its first chunk is sent.
888
+ assertNoDuplicateKeys(qi, conflict, columns, rowValues);
889
+ if (qi.dialect.capabilities.upsertConflictTarget === 'any-unique-key') {
890
+ assertMysqlBulkUpsert(qi, conflict, columns, rowValues);
891
+ }
892
+ identity = conflictIdentity(qi, conflict, projection);
893
+ }
297
894
  // Use actual Postgres types for array casts in the default PostgreSQL dialect.
298
895
  // Enum columns cast to `"EnumName"[]`, the generic text[] fallback would
299
896
  // type the UNNEST output as text, which Postgres refuses to coerce to the
@@ -314,21 +911,450 @@ export function buildCreateMany(qi, args) {
314
911
  // directly. Detected from the column's own declared type rather than from
315
912
  // the shape of the first row's value, so a row whose array column happens to
316
913
  // be null or absent still takes the correct form.
914
+ //
915
+ // json/jsonb columns stay on UNNEST. coerceWriteValue has already turned
916
+ // every JS array bound to one into JSON TEXT, so the transpose has no
917
+ // nested array to flatten (a raw `[1, 2, 3]` there used to become three
918
+ // rows), and every other cell is a string, object or null that the driver
919
+ // serializes as one array element.
317
920
  const hasArrayColumn = columns.some((col) => (qi.columnPgTypeMap.get(col) ?? '').startsWith('_'));
318
- const built = qi.dialect.buildBulkInsertStatement({
319
- table: qt,
320
- columns: quotedColumns,
921
+ return {
922
+ conflict,
923
+ identity,
924
+ columns,
925
+ insert: {
926
+ table: qt,
927
+ columns: quotedColumns,
928
+ columnArrayTypes: typeCasts,
929
+ skipDuplicates: args.skipDuplicates,
930
+ returning: writeReturningColumns(qi, identity?.selection ?? projection),
931
+ requireRowValues: hasArrayColumn,
932
+ },
321
933
  rowValues,
322
- columnArrayTypes: typeCasts,
323
- skipDuplicates: args.skipDuplicates,
324
- returning: writeReturningColumns(qi),
325
- requireRowValues: hasArrayColumn,
934
+ projection,
935
+ };
936
+ }
937
+ /**
938
+ * One `createMany` statement over `rowValues` (all of a prepared call's rows,
939
+ * or a chunk of them). `first` marks the call's first statement, where an
940
+ * onConflict call starts counting the rows it wrote.
941
+ */
942
+ function emitCreateMany(qi, prepared, rowValues, overBindLimit, first) {
943
+ const { conflict, columns, projection } = prepared;
944
+ const insert = { ...prepared.insert, rowValues };
945
+ if (conflict) {
946
+ return buildBulkUpsert(qi, conflict, insert, columns, overBindLimit, projection, prepared.identity, first);
947
+ }
948
+ const built = qi.dialect.buildBulkInsertStatement(insert);
949
+ if (overBindLimit === 'refuse')
950
+ assertWithinBindLimit(qi, built.params.length, rowValues.length, columns.length);
951
+ return {
952
+ sql: built.sql,
953
+ params: built.params,
954
+ transform: (result) => result.rows.map((row) => parseWriteRow(qi, row, projection)),
955
+ tag: `${qi.table}.createMany`,
956
+ };
957
+ }
958
+ /**
959
+ * Rows one `createMany` statement may carry: the most that keep its bind
960
+ * parameters within the engine's limit and, on SQL Server, its rows within
961
+ * the 1000 a plain `INSERT ... VALUES` accepts. The parameter cost is measured
962
+ * from one-row and two-row builds, which separates the per-row parameters from
963
+ * the ones bound once per statement (an onConflict update's values); the
964
+ * PostgreSQL UNNEST form binds one array per column, so its rows never run out.
965
+ */
966
+ function rowsPerStatement(qi, prepared) {
967
+ // An onConflict statement (SQL Server's MERGE) reads its rows as a derived
968
+ // table, which the plain INSERT ... VALUES row cap does not bind.
969
+ const cap = prepared.conflict ? Infinity : qi.dialect.capabilities.maxInsertRows;
970
+ const limit = maxBindParameters(qi.dialect);
971
+ const rows = prepared.rowValues;
972
+ if (limit === undefined || rows.length < 2)
973
+ return cap;
974
+ const one = emitCreateMany(qi, prepared, rows.slice(0, 1), 'allow', true).params.length;
975
+ const perRow = emitCreateMany(qi, prepared, rows.slice(0, 2), 'allow', true).params.length - one;
976
+ if (perRow <= 0)
977
+ return cap;
978
+ const fixed = Math.max(0, one - perRow);
979
+ return Math.max(1, Math.min(cap, Math.floor((limit - fixed) / perRow)));
980
+ }
981
+ const ON_CONFLICT_KEYS = new Set(['target', 'update', 'where', 'ignore']);
982
+ /**
983
+ * Validate the data-independent half of `onConflict`: the engine supports it,
984
+ * its shape, its target names exactly one unique key, and its update and
985
+ * where are usable. Every refusal is E003 except an engine gap (E017).
986
+ */
987
+ function resolveConflictPlan(qi, args) {
988
+ const table = qi.table;
989
+ if (!qi.dialect.supportsBulkUpsert || typeof qi.dialect.buildBulkUpsertStatement !== 'function') {
990
+ throw new UnsupportedFeatureError('createMany onConflict', qi.dialect.name, 'This engine has no multi-row upsert statement. Call upsert() per row inside $transaction.');
991
+ }
992
+ if (args.skipDuplicates) {
993
+ throw new ValidationError(`createMany on "${table}": onConflict and skipDuplicates cannot be combined. skipDuplicates skips a ` +
994
+ 'conflict on ANY unique key, onConflict names one; to skip conflicts on that key, use ' +
995
+ 'onConflict: { target, ignore: true }.');
996
+ }
997
+ const oc = args.onConflict;
998
+ if (!isPlainRecord(oc)) {
999
+ throw new ValidationError(`createMany on "${table}": onConflict must be { target, update } or { target, ignore: true }.`);
1000
+ }
1001
+ const unknownKey = Object.keys(oc).find((k) => !ON_CONFLICT_KEYS.has(k) && oc[k] !== undefined);
1002
+ if (unknownKey !== undefined) {
1003
+ throw new ValidationError(`createMany on "${table}": onConflict has no option "${unknownKey}". It takes target, update, where and ignore.`);
1004
+ }
1005
+ const target = resolveConflictTarget(qi, oc.target);
1006
+ if (oc.ignore !== undefined && oc.ignore !== true) {
1007
+ throw new ValidationError(`createMany on "${table}": onConflict.ignore takes \`true\` only.`);
1008
+ }
1009
+ if (oc.ignore === true) {
1010
+ if (oc.update !== undefined || oc.where !== undefined) {
1011
+ throw new ValidationError(`createMany on "${table}": onConflict takes either ignore: true or update (with an optional where), not both.`);
1012
+ }
1013
+ return { target, update: null, where: null };
1014
+ }
1015
+ return { target, update: resolveConflictUpdate(qi, oc.update), where: resolveConflictWhere(qi, oc.where) };
1016
+ }
1017
+ function resolveConflictTarget(qi, raw) {
1018
+ const table = qi.table;
1019
+ if (!Array.isArray(raw) || raw.length === 0 || raw.some((f) => typeof f !== 'string')) {
1020
+ throw new ValidationError(`createMany on "${table}": onConflict.target must be a non-empty array of field names (the primary key or ` +
1021
+ `one unique key), e.g. target: ['email']. Valid targets: ${describeConflictTargets(qi)}.`);
1022
+ }
1023
+ const columns = [];
1024
+ for (const field of raw) {
1025
+ const column = resolveColumnName(qi.tableMeta, field);
1026
+ if (column === undefined) {
1027
+ throw new ValidationError(`createMany on "${table}": onConflict.target names "${field}", which is not a column. ` +
1028
+ `Valid targets: ${describeConflictTargets(qi)}.`);
1029
+ }
1030
+ if (columns.includes(column)) {
1031
+ throw new ValidationError(`createMany on "${table}": onConflict.target names "${field}" twice.`);
1032
+ }
1033
+ columns.push(column);
1034
+ }
1035
+ const wanted = new Set(columns);
1036
+ const sameSet = (set) => set.length === wanted.size && set.every((c) => wanted.has(c));
1037
+ if (uniqueKeyNames(qi.tableMeta).some(sameSet))
1038
+ return columns;
1039
+ const partial = qi.tableMeta.indexes.some((idx) => idx.unique && idx.partial && sameSet(idx.columns));
1040
+ throw new ValidationError(`createMany on "${table}": onConflict.target (${columns.map((c) => fieldOf(qi, c)).join(', ')}) ` +
1041
+ (partial
1042
+ ? 'is a PARTIAL unique index, which a conflict clause can only use by restating its predicate. '
1043
+ : 'is not the primary key or a unique key of the table, as the schema metadata knows it. ') +
1044
+ `Valid targets: ${describeConflictTargets(qi)}.`);
1045
+ }
1046
+ function fieldOf(qi, column) {
1047
+ return qi.tableMeta.reverseColumnMap[column] ?? column;
1048
+ }
1049
+ function describeConflictTargets(qi) {
1050
+ const keys = uniqueKeyNames(qi.tableMeta);
1051
+ if (keys.length === 0)
1052
+ return '(none: the table has no primary key or unique key in its metadata)';
1053
+ return keys.map((cols) => `[${cols.map((c) => `'${fieldOf(qi, c)}'`).join(', ')}]`).join(', ');
1054
+ }
1055
+ /** The conflict-UPDATE's data: the field-list shorthand becomes `{ f: expr.excluded(f) }`. */
1056
+ function resolveConflictUpdate(qi, raw) {
1057
+ const empty = new ValidationError(`createMany on "${qi.table}": onConflict.update is empty; use onConflict: { target, ignore: true } to skip ` +
1058
+ 'conflicting rows.');
1059
+ if (Array.isArray(raw)) {
1060
+ if (raw.length === 0)
1061
+ throw empty;
1062
+ const update = {};
1063
+ for (const field of raw) {
1064
+ if (typeof field !== 'string') {
1065
+ throw new ValidationError(`createMany on "${qi.table}": onConflict.update as a list takes field names, got ${typeof field}.`);
1066
+ }
1067
+ if (Object.hasOwn(update, field)) {
1068
+ throw new ValidationError(`createMany on "${qi.table}": onConflict.update names "${field}" twice.`);
1069
+ }
1070
+ update[field] = exprBuilders.excluded(field);
1071
+ }
1072
+ return update;
1073
+ }
1074
+ if (!isPlainRecord(raw)) {
1075
+ throw new ValidationError(`createMany on "${qi.table}": onConflict.update takes a list of field names or a { field: value } object.`);
1076
+ }
1077
+ if (!Object.values(raw).some((v) => v !== undefined))
1078
+ throw empty;
1079
+ assertNoGeneratedColumns(qi, raw, 'createMany');
1080
+ return raw;
1081
+ }
1082
+ /**
1083
+ * `onConflict.where`, with `undefined` keys pruned as in every `where`. One
1084
+ * whose every key was `undefined` is refused: dropping it would update the
1085
+ * rows it was written to protect.
1086
+ */
1087
+ function resolveConflictWhere(qi, raw) {
1088
+ if (raw === undefined)
1089
+ return null;
1090
+ if (!isPlainRecord(raw)) {
1091
+ throw new ValidationError(`createMany on "${qi.table}": onConflict.where must be a where object.`);
1092
+ }
1093
+ const pruned = pruneUndefinedArgs(qi.schema, qi.table, { where: raw }).where;
1094
+ if (Object.keys(pruned).length === 0) {
1095
+ if (Object.keys(raw).length > 0) {
1096
+ throw new ValidationError(`createMany on "${qi.table}": every key in onConflict.where was undefined, so it would restrict nothing. ` +
1097
+ 'Check the values you meant to filter on, or leave where out to update every conflicting row.');
1098
+ }
1099
+ return null;
1100
+ }
1101
+ return pruned;
1102
+ }
1103
+ /** Every row must supply every target column: a row without its key cannot conflict on it. */
1104
+ function assertConflictTargetSupplied(qi, conflict, columns) {
1105
+ const missing = conflict.target.filter((c) => !columns.includes(c));
1106
+ if (missing.length === 0)
1107
+ return;
1108
+ throw new ValidationError(`createMany onConflict on "${qi.table}": the target field ${missing.map((c) => `"${fieldOf(qi, c)}"`).join(', ')} ` +
1109
+ 'is missing from the rows. Every row must supply every target field; a key the database generates ' +
1110
+ '(an identity or serial id it has not assigned yet) cannot be a conflict target.');
1111
+ }
1112
+ /** A byte-equality token for one coerced key value. */
1113
+ function keyToken(value) {
1114
+ let s;
1115
+ if (value instanceof Date)
1116
+ s = value.toISOString();
1117
+ else if (value instanceof Uint8Array)
1118
+ s = Array.from(value, (b) => b.toString(16).padStart(2, '0')).join('');
1119
+ else
1120
+ s = String(value);
1121
+ return `${s.length}:${s}`;
1122
+ }
1123
+ /**
1124
+ * Refuse two rows of one call that share a conflict key, before any SQL. The
1125
+ * engines disagree on it (PostgreSQL raises 21000, SQL Server's MERGE 8672,
1126
+ * SQLite and MySQL apply the rows in sequence), and chunking at the bind
1127
+ * limit would turn the error into "the second chunk updates the first chunk's
1128
+ * row". Byte-equal on the coerced values, so a case-insensitive collation can
1129
+ * still let two rows through. A row with NULL in the key never conflicts.
1130
+ */
1131
+ function assertNoDuplicateKeys(qi, conflict, columns, rowValues) {
1132
+ const at = conflict.target.map((c) => columns.indexOf(c));
1133
+ const seen = new Map();
1134
+ for (let i = 0; i < rowValues.length; i++) {
1135
+ const row = rowValues[i];
1136
+ const parts = at.map((j) => row[j]);
1137
+ if (parts.some((v) => v === null || v === undefined))
1138
+ continue;
1139
+ const key = parts.map(keyToken).join('|');
1140
+ const first = seen.get(key);
1141
+ if (first !== undefined) {
1142
+ throw new ValidationError(`createMany onConflict on "${qi.table}": rows ${first} and ${i} share the conflict key ` +
1143
+ `(${conflict.target.map((c) => fieldOf(qi, c)).join(', ')}). One statement cannot insert or update one ` +
1144
+ 'key twice; merge those rows first.');
1145
+ }
1146
+ seen.set(key, i);
1147
+ }
1148
+ }
1149
+ /**
1150
+ * The identity for an onConflict call: the conflict target (the key that
1151
+ * collides) or the primary key, preferring one the caller's projection
1152
+ * returns. `null` when the written rows cannot be told
1153
+ * apart: each candidate is assigned by `onConflict.update` (so a row's key can
1154
+ * change between statements) or is PII, which a write never returns.
1155
+ */
1156
+ function conflictIdentity(qi, conflict, projection) {
1157
+ const assigned = conflict.update ? namedColumns(qi.tableMeta, conflict.update) : new Set();
1158
+ const pii = piiColumns(qi, qi.tableMeta);
1159
+ const usable = [conflict.target, qi.tableMeta.primaryKey].filter((key) => key.length > 0 && key.every((c) => !assigned.has(c) && !pii.has(c)));
1160
+ // One the caller's projection already returns, so the statement is unchanged.
1161
+ const columns = usable.find((key) => !projection || key.every((c) => projection.columns.includes(c))) ?? usable[0];
1162
+ if (!columns)
1163
+ return null;
1164
+ const missing = projection ? columns.filter((c) => !projection.columns.includes(c)) : [];
1165
+ if (missing.length === 0)
1166
+ return { columns, selection: projection, added: [], seen: new Set() };
1167
+ const wanted = new Set([...(projection?.columns ?? []), ...missing]);
1168
+ return {
1169
+ columns,
1170
+ selection: { columns: qi.tableMeta.allColumns.filter((c) => wanted.has(c)) },
1171
+ added: missing.map((c) => fieldOf(qi, c)),
1172
+ seen: new Set(),
1173
+ };
1174
+ }
1175
+ /** Refuse a table row that one onConflict call wrote twice (see {@link ConflictIdentity}). */
1176
+ function assertEachRowWrittenOnce(qi, conflict, identity, rows, first) {
1177
+ if (first)
1178
+ identity.seen.clear();
1179
+ for (const row of rows) {
1180
+ const values = identity.columns.map((c) => row[c]);
1181
+ // Every engine returns the identity columns (see conflictIdentity); a row
1182
+ // without them cannot be compared, so it is not counted.
1183
+ if (values.some((v) => v === undefined))
1184
+ continue;
1185
+ const token = values.map(keyToken).join('|');
1186
+ if (identity.seen.has(token))
1187
+ throw conflictKeySharedError(qi, conflict.target);
1188
+ identity.seen.add(token);
1189
+ }
1190
+ }
1191
+ /**
1192
+ * The E003 for two rows of one `createMany({ onConflict })` that share a key
1193
+ * only under the column's comparison rules (a case-insensitive collation, a
1194
+ * `citext` column), which the byte-equality check above cannot see.
1195
+ */
1196
+ function conflictKeySharedError(qi, target, cause) {
1197
+ return new ValidationError(`createMany onConflict on "${qi.table}": two rows in this call share the conflict key ` +
1198
+ `(${target.map((c) => fieldOf(qi, c)).join(', ')}) under the column's comparison rules (a case-insensitive ` +
1199
+ 'collation, for example), so the call would insert or update one row twice. Merge those rows first.', cause === undefined ? undefined : { cause });
1200
+ }
1201
+ /**
1202
+ * `err` from running a `createMany({ onConflict })`, with the engine's
1203
+ * "one statement touched one row twice" errors (PostgreSQL 21000, SQL Server
1204
+ * 8672) turned into the E003 that names the cause. Anything else is returned
1205
+ * as it was.
1206
+ */
1207
+ export function explainConflictKeyCollision(qi, args, err) {
1208
+ if (args.onConflict === undefined || !touchedRowTwice(err))
1209
+ return err;
1210
+ const raw = args.onConflict.target;
1211
+ const target = Array.isArray(raw)
1212
+ ? raw.map((f) => (typeof f === 'string' ? (resolveColumnName(qi.tableMeta, f) ?? f) : String(f)))
1213
+ : [];
1214
+ return conflictKeySharedError(qi, target, err);
1215
+ }
1216
+ /** PostgreSQL 21000 or SQL Server 8672 on `err` or a cause under it (Turbine wraps or scrubs driver errors). */
1217
+ function touchedRowTwice(err) {
1218
+ let e = err;
1219
+ for (let depth = 0; depth < 4 && e && typeof e === 'object'; depth++) {
1220
+ if (e.code === '21000' || e.sqlstate === '21000' || e.number === 8672)
1221
+ return true;
1222
+ e = e.cause;
1223
+ }
1224
+ return false;
1225
+ }
1226
+ /**
1227
+ * MySQL's ON DUPLICATE KEY UPDATE has no conflict target: it fires on
1228
+ * WHATEVER unique key a row violates, and with several matching keys it
1229
+ * updates one row. So the target must be the only key a row can violate, and
1230
+ * the shapes that need a predicate slot or a per-row outcome are refused.
1231
+ */
1232
+ function assertMysqlBulkUpsert(qi, conflict, columns, rowValues) {
1233
+ const fix = (why, hint) => new UnsupportedFeatureError(`createMany onConflict on "${qi.table}" ${why}`, qi.dialect.name, hint);
1234
+ if (conflict.update === null) {
1235
+ throw fix('with ignore: true', 'MySQL cannot tell the inserted rows from the skipped ones afterwards. Use onConflict: { target, update }.');
1236
+ }
1237
+ if (conflict.where !== null) {
1238
+ throw fix('with a where', 'ON DUPLICATE KEY UPDATE has no predicate slot.');
1239
+ }
1240
+ const gf = whereMod.resolveGlobalFilter(qi, qi.table);
1241
+ if (gf && whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, qi.q(qi.table), gf, [])) {
1242
+ throw fix('on a table with a global filter', "ON DUPLICATE KEY UPDATE cannot carry the filter, so another tenant's row could be updated. Call upsert() " +
1243
+ 'per row, or pass skipGlobalFilters: UNSAFE.');
1244
+ }
1245
+ // The single-row upsert's rule (otherCollidingUniqueKey), on column presence
1246
+ // so the verdict does not depend on the rows' values.
1247
+ const present = new Set(columns);
1248
+ const other = otherCollidingUniqueKey(qi.tableMeta, [conflict.target], (c) => (present.has(c) ? 'value' : 'absent'));
1249
+ if (other) {
1250
+ throw fix(`with target (${conflict.target.map((c) => fieldOf(qi, c)).join(', ')})`, `"${qi.table}" has another unique key (${other.map((c) => fieldOf(qi, c)).join(', ')}) that a row in data can ` +
1251
+ "collide on, and MySQL's ON DUPLICATE KEY UPDATE cannot be limited to one key, so it could update a row the " +
1252
+ 'target does not name. Call upsert() per row (it looks each row up first), or leave that key out of data.');
1253
+ }
1254
+ const at = conflict.target.map((c) => columns.indexOf(c));
1255
+ if (rowValues.some((row) => at.some((j) => row[j] === null || row[j] === undefined))) {
1256
+ throw fix('with a NULL in a target field', 'MySQL returns the written rows by re-reading them by key, and a NULL key reads back nothing. Supply every ' +
1257
+ 'target value.');
1258
+ }
1259
+ const numbered = at.filter((j) => isServerNumberedKey(qi, columns[j]));
1260
+ if (numbered.length > 0 && rowValues.some((row) => numbered.some((j) => isZeroKeyValue(row[j])))) {
1261
+ const field = fieldOf(qi, columns[numbered[0]]);
1262
+ throw fix(`with 0 in the AUTO_INCREMENT target field "${field}"`, 'MySQL assigns a new value for 0, so the row is inserted instead of conflicting, and it cannot be re-read ' +
1263
+ `by its key. Pass the row's real "${field}", or create() the rows that need a new one.`);
1264
+ }
1265
+ }
1266
+ /** The SELECT that re-reads a MySQL multi-row upsert's rows by their conflict keys. */
1267
+ function buildKeyReselect(qi, conflict, columns, rowValues, projection) {
1268
+ const at = conflict.target.map((c) => columns.indexOf(c));
1269
+ const params = [];
1270
+ const tuples = rowValues.map((row) => {
1271
+ const refs = at.map((j, k) => {
1272
+ params.push(reselectKeyValue(qi, conflict.target[k], row[j]));
1273
+ return qi.p(params.length);
1274
+ });
1275
+ return refs.length === 1 ? refs[0] : `(${refs.join(', ')})`;
326
1276
  });
1277
+ const keyCols = conflict.target.map((c) => qi.q(c));
1278
+ const lhs = keyCols.length === 1 ? keyCols[0] : `(${keyCols.join(', ')})`;
1279
+ return {
1280
+ sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)} WHERE ${lhs} IN (${tuples.join(', ')})`,
1281
+ params,
1282
+ };
1283
+ }
1284
+ function buildBulkUpsert(qi, conflict, insert, columns, overBindLimit, projection, identity, first) {
1285
+ // The duplicate-key and MySQL checks ran over every row in prepareCreateMany.
1286
+ const rowValues = insert.rowValues;
1287
+ // `updatedAt` columns are stamped on the conflict-UPDATE as update() stamps
1288
+ // them, unless the update names the column.
1289
+ const updateData = conflict.update ? applyUpdatedAtColumns(qi, conflict.update) : null;
1290
+ const insertColumns = new Set(columns);
1291
+ const gf = whereMod.resolveGlobalFilter(qi, qi.table);
1292
+ const built = qi.dialect.buildBulkUpsertStatement({
1293
+ ...insert,
1294
+ skipDuplicates: false,
1295
+ conflictColumns: conflict.target.map((c) => qi.q(c)),
1296
+ update: updateData
1297
+ ? (params, refs) => {
1298
+ const setClauses = buildSetClauses(qi, writeEntries(qi, updateData), params, {
1299
+ operation: 'createMany',
1300
+ target: refs.target,
1301
+ incoming: refs.incoming,
1302
+ insertColumns,
1303
+ });
1304
+ // The caller's predicate and the global filter both read the EXISTING
1305
+ // row, so a row they exclude is neither updated nor returned (and,
1306
+ // its key being taken, not inserted either). Inserts are not filtered,
1307
+ // as create() is not.
1308
+ const predicates = [];
1309
+ for (const where of [conflict.where, gf]) {
1310
+ if (!where)
1311
+ continue;
1312
+ const sql = whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, refs.target, where, params);
1313
+ if (sql)
1314
+ predicates.push(sql);
1315
+ }
1316
+ return {
1317
+ setClauses,
1318
+ where: predicates.length <= 1 ? (predicates[0] ?? null) : predicates.map((p) => `(${p})`).join(' AND '),
1319
+ };
1320
+ }
1321
+ : null,
1322
+ });
1323
+ if (overBindLimit === 'refuse') {
1324
+ assertWithinBindLimit(qi, built.params.length, rowValues.length, columns.length);
1325
+ }
327
1326
  return {
328
1327
  sql: built.sql,
329
1328
  params: built.params,
330
- transform: (result) => result.rows.map((row) => parseWriteRow(qi, row)),
1329
+ // The rows written: inserted or updated. A row `ignore` skipped, or one the
1330
+ // predicate or a global filter excluded, is not in the result, and the
1331
+ // order is the engine's, not the input's.
1332
+ transform: (result) => {
1333
+ if (identity)
1334
+ assertEachRowWrittenOnce(qi, conflict, identity, result.rows, first);
1335
+ const rows = result.rows.map((row) => parseWriteRow(qi, row, projection));
1336
+ if (identity && identity.added.length > 0) {
1337
+ for (const row of rows)
1338
+ for (const field of identity.added)
1339
+ delete row[field];
1340
+ }
1341
+ return rows;
1342
+ },
331
1343
  tag: `${qi.table}.createMany`,
1344
+ reselect: qi.dialect.resultStrategy === 'reselect'
1345
+ ? async (exec) => {
1346
+ await exec(built.sql, built.params);
1347
+ const sel = buildKeyReselect(qi, conflict, columns, rowValues, identity?.selection ?? projection);
1348
+ const result = await exec(sel.sql, sel.params);
1349
+ // Every row was inserted or updated and byte-equal keys were
1350
+ // refused up front, so fewer rows back means two rows' keys are
1351
+ // equal under the column's collation and MySQL applied the second
1352
+ // as an update of the first. Refused, which rolls the call back.
1353
+ if (result.rows.length < rowValues.length)
1354
+ throw conflictKeySharedError(qi, conflict.target);
1355
+ return result;
1356
+ }
1357
+ : undefined,
332
1358
  };
333
1359
  }
334
1360
  export function buildUpdate(qi, args, projection) {
@@ -394,8 +1420,7 @@ export function buildUpdate(qi, args, projection) {
394
1420
  : `u:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}${writeProjectionCacheSegment(projection)}`;
395
1421
  const params = [];
396
1422
  const buildSql = (freshParams) => {
397
- const setEntries = writeEntries(qi, dataObj);
398
- const setClauses = setEntries.map(([k, v]) => buildSetClause(qi, k, v, freshParams));
1423
+ const setClauses = buildSetClauses(qi, writeEntries(qi, dataObj), freshParams);
399
1424
  if (lock) {
400
1425
  const versionCol = qi.toSqlColumn(lock.field);
401
1426
  setClauses.push(`${versionCol} = ${versionCol} + 1`);
@@ -464,6 +1489,18 @@ export function buildUpdate(qi, args, projection) {
464
1489
  // Non-RETURNING engines: UPDATE, then re-fetch the row by the same where.
465
1490
  reselect: qi.dialect.resultStrategy === 'reselect'
466
1491
  ? async (exec) => {
1492
+ // Runs in one transaction (executeMutation), savepointed inside a
1493
+ // caller's. When the write may move the row out of `where` (it sets
1494
+ // a column `where` reads, the version column included), the row is
1495
+ // locked and re-read by its key, since `where` no longer finds it.
1496
+ const written = namedColumns(qi.tableMeta, dataObj);
1497
+ if (lock)
1498
+ written.add(qi.toColumn(lock.field));
1499
+ const key = writeMayMoveRow(qi, whereObj, written)
1500
+ ? await lockMovedRowKey(qi, exec, whereObj, dataObj, 'update', allowFullTableScan)
1501
+ : undefined;
1502
+ if (key === null)
1503
+ return { rows: [], rowCount: 0 };
467
1504
  const writeResult = await exec(sql, params, preparedName);
468
1505
  // Optimistic-lock conflict: the version-checked UPDATE matched no
469
1506
  // row. The re-fetch below uses `where` WITHOUT the version
@@ -477,14 +1514,21 @@ export function buildUpdate(qi, args, projection) {
477
1514
  expectedVersion: lock.expected,
478
1515
  });
479
1516
  }
480
- const sel = buildReselectByWhere(qi, whereObj, projection);
481
- return exec(sel.sql, sel.params);
1517
+ const sel = buildReselectByWhere(qi, key ?? whereObj, projection);
1518
+ const result = await exec(sel.sql, sel.params);
1519
+ // `update` writes one row; more than one here means the engine
1520
+ // matched the where loosely, and the write is rolled back.
1521
+ if (key || !allowFullTableScan) {
1522
+ assertOneRowByKey(qi, 'update', key ? identityColumns(qi) : whereColumns(qi, userWhere), result.rows.length);
1523
+ }
1524
+ return result;
482
1525
  }
483
1526
  : undefined,
484
1527
  };
485
1528
  }
486
1529
  export function buildDelete(qi, args, projection) {
487
1530
  assertWritable(qi, 'delete');
1531
+ assertNoOptimisticLock(qi, args, 'delete');
488
1532
  qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
489
1533
  // Prisma compound-unique selector → the column conjunction (before the guard).
490
1534
  const userWhere = expandCompoundUniqueWhere(qi.tableMeta, args.where);
@@ -533,21 +1577,72 @@ export function buildDelete(qi, args, projection) {
533
1577
  // by the same where, then run the DELETE, returning the captured row.
534
1578
  reselect: qi.dialect.resultStrategy === 'reselect'
535
1579
  ? async (exec) => {
536
- const sel = buildReselectByWhere(qi, whereObj, projection);
1580
+ // In one transaction (executeMutation): the row is locked while it
1581
+ // is read, and returned only if THIS delete removed it, so of two
1582
+ // concurrent deletes of one row exactly one reports it.
1583
+ const sel = buildReselectByWhere(qi, whereObj, projection, true);
537
1584
  const pre = await exec(sel.sql, sel.params);
538
- await exec(entry.sql, params, entry.name);
539
- return pre;
1585
+ // `delete` removes one row; refused before the DELETE when the
1586
+ // engine matched the where loosely.
1587
+ if (!allowFullTableScan)
1588
+ assertOneRowByKey(qi, 'delete', whereColumns(qi, userWhere), pre.rows.length);
1589
+ const deleted = await exec(entry.sql, params, entry.name);
1590
+ return (deleted.rowCount ?? 0) === 0 ? { rows: [], rowCount: 0 } : pre;
540
1591
  }
541
1592
  : undefined,
542
1593
  };
543
1594
  }
1595
+ /**
1596
+ * Refuse `limit` / `take` / `offset` / `skip` on `updateMany` / `deleteMany`.
1597
+ * No engine applies them to a bulk write here, so `deleteMany({ where, limit:
1598
+ * 1000 })` used to touch every matching row. Shared with the PowDB engine.
1599
+ */
1600
+ export function assertNoBulkWritePaging(table, operation, args) {
1601
+ const a = args;
1602
+ const key = ['limit', 'take', 'offset', 'skip'].find((k) => a[k] !== undefined);
1603
+ if (key === undefined)
1604
+ return;
1605
+ throw new ValidationError(`\`${key}\` is not supported on ${operation} for "${table}": it would apply to every row the \`where\` ` +
1606
+ `matches, not ${key === 'limit' || key === 'take' ? 'only the first ones' : 'skip any'}. To ${operation === 'deleteMany' ? 'delete' : 'update'} a bounded batch, ` +
1607
+ 'select the ids first (`findMany({ where, select: { id: true }, orderBy, limit })`), then pass ' +
1608
+ '`where: { id: { in: ids } }`.');
1609
+ }
1610
+ /**
1611
+ * Refuse `optimisticLock` on a write that does not implement it. It is an
1612
+ * `update` option; on a sibling method it used to be ignored, so a stale
1613
+ * write went through with no error, which is the one outcome the option
1614
+ * exists to prevent. The message names the equivalent for each method.
1615
+ */
1616
+ export function assertNoOptimisticLock(qi, args, operation) {
1617
+ const lock = args.optimisticLock;
1618
+ if (lock === undefined)
1619
+ return;
1620
+ const field = typeof lock?.field === 'string' ? lock.field : 'version';
1621
+ throw new ValidationError(`\`optimisticLock\` is an \`update\` option and ${operation} on "${qi.table}" does not apply it. ` +
1622
+ OPTIMISTIC_LOCK_ALTERNATIVE[operation](field));
1623
+ }
1624
+ const OPTIMISTIC_LOCK_ALTERNATIVE = {
1625
+ updateMany: (field) => `Put the expected version in \`where\` (\`${field}: expected\`) and bump it in \`data\` ` +
1626
+ `(\`${field}: { increment: 1 }\`); the returned count is the number of rows that were still at that version.`,
1627
+ upsert: () => 'Inside one `$transaction`, read the row by `where`, then call `update({ where, data, optimisticLock })` ' +
1628
+ 'when it exists or `create` when it does not.',
1629
+ delete: (field) => `Put the expected version in \`where\` beside the key (\`where: { id, ${field}: expected }\`): a row at ` +
1630
+ 'another version is not deleted, and the call throws NotFoundError.',
1631
+ deleteMany: (field) => `Put the expected version in \`where\` (\`${field}: expected\`); the returned count is the number of rows ` +
1632
+ 'that were still at that version.',
1633
+ };
544
1634
  export function upsertLookupReason(qi, args) {
1635
+ assertNoOptimisticLock(qi, args, 'upsert');
545
1636
  const where = (args.where ?? {});
546
1637
  const create = (args.create ?? {});
547
1638
  if (!upsertWhereMatchesCreate(qi.tableMeta, where, create))
548
1639
  return 'where-differs';
549
1640
  if (writeEntries(qi, (args.update ?? {})).length === 0)
550
1641
  return 'empty-update';
1642
+ if (qi.dialect.capabilities.upsertConflictTarget === 'any-unique-key' &&
1643
+ upsertCreateCollidingKey(qi.tableMeta, where, create)) {
1644
+ return 'other-unique-key';
1645
+ }
551
1646
  if (!qi.dialect.supportsUpsertUpdateWhere) {
552
1647
  qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
553
1648
  const filter = whereMod.resolveGlobalFilter(qi, qi.table);
@@ -558,8 +1653,13 @@ export function upsertLookupReason(qi, args) {
558
1653
  return null;
559
1654
  }
560
1655
  /** The refusal `buildUpsert` raises for each reason, naming the way forward. */
561
- function upsertStatementRefusal(qi, reason, where) {
1656
+ function upsertStatementRefusal(qi, reason, where, create) {
562
1657
  const call = 'Call `upsert()` unbatched: it looks the row up by `where` first.';
1658
+ if (reason === 'other-unique-key') {
1659
+ const key = upsertCreateCollidingKey(qi.tableMeta, where, create) ?? [];
1660
+ return new UnsupportedFeatureError(`a batched upsert on "${qi.table}" whose \`create\` also sets another unique key${key.length > 0 ? ` (${key.map((c) => fieldOf(qi, c)).join(', ')})` : ''},`, qi.dialect.name, 'ON DUPLICATE KEY UPDATE updates whichever row a unique key collides on first, which may not be the row `where` names. ' +
1661
+ call);
1662
+ }
563
1663
  if (reason === 'global-filter') {
564
1664
  return new UnsupportedFeatureError(`a batched upsert on "${qi.table}", which has a global filter,`, qi.dialect.name, `Its upsert statement cannot carry the filter. ${call} Or pass \`skipGlobalFilters: UNSAFE\`.`);
565
1665
  }
@@ -572,6 +1672,7 @@ function upsertStatementRefusal(qi, reason, where) {
572
1672
  }
573
1673
  export function buildUpsert(qi, args, projection) {
574
1674
  assertWritable(qi, 'upsert');
1675
+ assertNoOptimisticLock(qi, args, 'upsert');
575
1676
  assertNoGeneratedColumns(qi, args.create, 'upsert');
576
1677
  assertNoGeneratedColumns(qi, args.update, 'upsert');
577
1678
  qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
@@ -597,8 +1698,9 @@ export function buildUpsert(qi, args, projection) {
597
1698
  // `upsert()` never reaches here with a reason (it looks the row up instead),
598
1699
  // so this refuses only a batched upsert, which has no round trip to spare.
599
1700
  const lookupReason = upsertLookupReason(qi, args);
600
- if (lookupReason)
601
- throw upsertStatementRefusal(qi, lookupReason, upsertWhere);
1701
+ if (lookupReason) {
1702
+ throw upsertStatementRefusal(qi, lookupReason, upsertWhere, args.create);
1703
+ }
602
1704
  // Build the INSERT part from create data
603
1705
  const createEntries = writeEntries(qi, args.create);
604
1706
  const columns = createEntries.map(([k]) => qi.toSqlColumn(k));
@@ -609,18 +1711,30 @@ export function buildUpsert(qi, args, projection) {
609
1711
  const conflictKeys = Object.keys(upsertWhere).filter((k) => upsertWhere[k] !== undefined);
610
1712
  const conflictColumns = conflictKeys.map((k) => qi.toSqlColumn(k));
611
1713
  // The conflict-UPDATE SET goes through the SAME operator-aware compiler as
612
- // `update()` (buildSetClause), so `set` / `increment` / `decrement` /
1714
+ // `update()` (buildSetClauses), so `set` / `increment` / `decrement` /
613
1715
  // `multiply` / `divide` mean here what they mean there and a misspelled
614
1716
  // operator gets the same E003. This branch used to bind each value directly,
615
1717
  // which stored an operator OBJECT as the JSON text `{"set":"x"}` in a text
616
- // column with no error at all. buildSetClause pushes onto `params` as it
1718
+ // column with no error at all. buildSetClauses pushes onto `params` as it
617
1719
  // goes, so the numbering continues after the create params exactly as the
618
1720
  // hand-rolled `paramIdx` did. No SQL cache here (the statement is rebuilt per
619
1721
  // call), so the fingerprint / collect mirrors update() keeps are not needed.
620
- const updateEntries = writeEntries(qi, args.update);
1722
+ // `updatedAt` columns are stamped on the conflict-UPDATE exactly as the
1723
+ // lookup path's `update()` stamps them, so the column does not depend on
1724
+ // which path an upsert takes. (An empty `update` never reaches here: it is
1725
+ // a lookup that leaves the found row untouched, stamp included.)
1726
+ //
1727
+ // `expr.excluded(f)` is `create`'s value for `f` (the incoming row of a
1728
+ // one-row upsert IS `create`), resolved before compiling so this path and the
1729
+ // lookup path's plain `update()` write the same row.
1730
+ const update = resolveExcludedFromCreate(qi, args.update, args.create);
1731
+ const stampedUpdate = applyUpdatedAtColumns(qi, update);
1732
+ const updateEntries = writeEntries(qi, stampedUpdate);
621
1733
  const params = [...createParams];
622
- const refQualifier = upsertReferenceQualifier(qi);
623
- const setClauses = updateEntries.map(([k, v]) => buildSetClause(qi, k, v, params, refQualifier));
1734
+ const setClauses = buildSetClauses(qi, updateEntries, params, {
1735
+ operation: 'upsert',
1736
+ target: upsertTargetRef(qi),
1737
+ });
624
1738
  // Global filter → restrict the conflict-UPDATE (soft-delete / tenancy) so an
625
1739
  // upsert never resurrects a soft-deleted row or writes across tenants. Only
626
1740
  // on engines whose upsert can carry a predicate (Postgres, SQLite); the gf
@@ -689,38 +1803,46 @@ export function buildUpsert(qi, args, projection) {
689
1803
  // Non-RETURNING engines: run the upsert, then re-fetch by the where keys.
690
1804
  reselect: qi.dialect.resultStrategy === 'reselect'
691
1805
  ? async (exec) => {
1806
+ const lookup = (whereMod.mergeGlobalFilter(qi, textSafeKeyWhere(qi, upsertWhere)) ?? {});
1807
+ // An update that sets a column `where` reads moves the row out of
1808
+ // `where`'s reach: lock it and re-read it by its key. An insert
1809
+ // matches `where` (this path requires `where` pinned to `create`),
1810
+ // so a row that is not there yet is re-read by `where`.
1811
+ const key = writeMayMoveRow(qi, lookup, namedColumns(qi.tableMeta, stampedUpdate))
1812
+ ? await lockMovedRowKey(qi, exec, lookup, stampedUpdate, 'upsert', false)
1813
+ : undefined;
692
1814
  await exec(sql, params);
693
- const sel = buildReselectByWhere(qi, (whereMod.mergeGlobalFilter(qi, upsertWhere) ?? {}), projection);
694
- return exec(sel.sql, sel.params);
1815
+ const sel = buildReselectByWhere(qi, key ?? lookup, projection);
1816
+ const result = await exec(sel.sql, sel.params);
1817
+ assertOneRowByKey(qi, 'upsert', key ? identityColumns(qi) : whereColumns(qi, upsertWhere), result.rows.length);
1818
+ return result;
695
1819
  }
696
1820
  : undefined,
697
1821
  };
698
1822
  }
699
1823
  /**
700
- * The qualifier an upsert's arithmetic SET operator reads its column through.
1824
+ * The rendered reference to an upsert's TARGET row, which every column the
1825
+ * conflict-UPDATE reads goes through (an arithmetic operator's column, an
1826
+ * expression's `col()`, a case predicate).
701
1827
  *
702
1828
  * `INSERT ... ON CONFLICT DO UPDATE SET n = n + 1` is 42702 on PostgreSQL
703
1829
  * because `n` could be the target row's or `excluded`'s, so the reference is
704
1830
  * qualified with the table name on every engine whose upsert statement names
705
1831
  * the table directly (PostgreSQL, SQLite, MySQL all accept `"t"."n"` there).
706
- * SQL Server is the exception and keeps the bare reference: its `MERGE`
707
- * aliases the target as `T`, so the TABLE NAME does not resolve there at all,
708
- * and the alias belongs to that dialect's own `buildUpsertStatement` rather
709
- * than to this module. An arithmetic operator in an mssql upsert may therefore
710
- * still be refused by the server as an ambiguous column; that shape has never
711
- * worked (it previously bound the operator OBJECT as the value), and the sound
712
- * fix is for the dialect to publish its target alias, which is a change to
713
- * `dialect.ts`. It fails loudly either way, so no wrong answer is possible.
1832
+ * SQL Server's `MERGE` aliases its target, so the table name does not resolve
1833
+ * there and the dialect publishes the alias ({@link Dialect.upsertTargetAlias}).
714
1834
  * `set` and literal values reference no column and are unaffected everywhere.
715
1835
  *
716
- * Gated on the dialect NAME, never on hook presence: every engine dialect
717
- * spreads `postgresDialect`, so a presence test would say yes for all of them.
1836
+ * Read from a property only SQL Server sets, never inferred from a hook's
1837
+ * presence: every engine dialect spreads `postgresDialect`.
718
1838
  */
719
- function upsertReferenceQualifier(qi) {
720
- return qi.dialect.name === 'mssql' ? '' : `${qi.q(qi.table)}.`;
1839
+ function upsertTargetRef(qi) {
1840
+ return qi.dialect.upsertTargetAlias ?? qi.q(qi.table);
721
1841
  }
722
1842
  export function buildUpdateMany(qi, args) {
723
1843
  assertWritable(qi, 'updateMany');
1844
+ assertNoBulkWritePaging(qi.table, 'updateMany', args);
1845
+ assertNoOptimisticLock(qi, args, 'updateMany');
724
1846
  qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
725
1847
  const dataObj = applyUpdatedAtColumns(qi, args.data);
726
1848
  assertNoGeneratedColumns(qi, dataObj, 'updateMany');
@@ -743,8 +1865,7 @@ export function buildUpdateMany(qi, args) {
743
1865
  const ck = `um:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
744
1866
  const params = [];
745
1867
  const buildSql = (freshParams) => {
746
- const setEntries = writeEntries(qi, dataObj);
747
- const setClauses = setEntries.map(([k, v]) => buildSetClause(qi, k, v, freshParams));
1868
+ const setClauses = buildSetClauses(qi, writeEntries(qi, dataObj), freshParams, { operation: 'updateMany' });
748
1869
  const whereClause = whereMod.buildWhereClause(qi, whereObj, freshParams);
749
1870
  const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
750
1871
  return `UPDATE ${qi.q(qi.table)} SET ${setClauses.join(', ')}${whereSql}`;
@@ -763,6 +1884,8 @@ export function buildUpdateMany(qi, args) {
763
1884
  }
764
1885
  export function buildDeleteMany(qi, args) {
765
1886
  assertWritable(qi, 'deleteMany');
1887
+ assertNoBulkWritePaging(qi.table, 'deleteMany', args);
1888
+ assertNoOptimisticLock(qi, args, 'deleteMany');
766
1889
  qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
767
1890
  whereMod.assertMutationHasPredicate(qi, 'deleteMany', whereMod.userPredicateIsEmpty(qi, args.where) ? '' : ' WHERE x', resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan'));
768
1891
  const whereObj = (whereMod.mergeGlobalFilter(qi, args.where) ?? {});
@@ -788,8 +1911,8 @@ export function buildDeleteMany(qi, args) {
788
1911
  /**
789
1912
  * The snake_case names of a table's PII-tagged (`defineSchema` `pii: true`)
790
1913
  * columns. PII columns are excluded from default projections (findMany /
791
- * findUnique / relation subqueries / batched loads) unless the query opts in
792
- * via `includePii` or names the column explicitly in `select`. Returns an
1914
+ * findUnique / relation subqueries / batched loads) unless a read passes
1915
+ * `includePii: UNSAFE`; naming one in `select` without it is E003. Returns an
793
1916
  * empty set for any table with no PII column, so untagged schemas keep their
794
1917
  * byte-identical SQL.
795
1918
  */
@@ -822,8 +1945,8 @@ export function piiColumns(_qi, meta) {
822
1945
  * The camelCase field names of a table's PII-tagged columns: the read-side
823
1946
  * counterpart of {@link piiColumns} applied to already-parsed entities.
824
1947
  * Used to strip PII from a write's RETURNING/reselect row (writes accept no
825
- * `includePii`/`select`, so their returned row always applies the default
826
- * exclusion; you may still write PII fields freely).
1948
+ * `includePii`, and a PII column named in a write's `select` is E003, so their
1949
+ * returned row never carries one; you may still write PII fields freely).
827
1950
  */
828
1951
  export function piiFields(_qi, meta) {
829
1952
  const out = [];
@@ -857,8 +1980,9 @@ export function writeProjectionCacheSegment(projection) {
857
1980
  * column so the PII values never leave the database on a write. A PII-tagged
858
1981
  * PRIMARY KEY column is kept in the projection regardless (the returned row
859
1982
  * must stay addressable): tag sensitive data, not keys, a PII PK is
860
- * documented out of scope for stripping. Writes accept no `select`/`includePii`
861
- * (unlike reads), so this is the whole write-return policy at the SQL level;
1983
+ * documented out of scope for stripping. Writes accept no `includePii` and
1984
+ * their `select` cannot name a PII column, so this is the whole write-return
1985
+ * policy at the SQL level;
862
1986
  * {@link parseWriteRow} remains as a defense-in-depth strip (a no-op once the
863
1987
  * SQL already excludes the columns). Derived purely from static per-table
864
1988
  * schema metadata, so the write SQL cache needs no extra key segment.
@@ -945,8 +2069,8 @@ export function writeReselectSelection(qi, projection) {
945
2069
  */
946
2070
  export function parseWriteRow(qi, row, projection) {
947
2071
  const parsed = qi.parseRow(row, qi.table);
948
- // A column the caller named in `select` is the opt-in (see WriteProjection),
949
- // so the strip spares exactly the projected columns and nothing else.
2072
+ // A write's projection never names a PII column (E003, see WriteProjection),
2073
+ // so for PII this strip spares nothing; it stays as defense in depth.
950
2074
  const pii = piiColumns(qi, qi.tableMeta);
951
2075
  if (pii.size === 0)
952
2076
  return parsed;
@@ -987,85 +2111,260 @@ export function assertNoGeneratedColumns(qi, data, operation) {
987
2111
  }
988
2112
  }
989
2113
  }
2114
+ /** The SQL operator each arithmetic update operator compiles to. */
2115
+ const ARITHMETIC_SQL = { increment: '+', decrement: '-', multiply: '*', divide: '/' };
990
2116
  /**
991
- * Build a single SET clause entry for update/updateMany.
2117
+ * The SQL operator for one arithmetic update operator on one column.
992
2118
  *
993
- * Supports plain values and atomic operator objects ({ set, increment,
994
- * decrement, multiply, divide }). An operator object is detected ONLY when
995
- * it has EXACTLY one key that is one of the 5 operator keys, this avoids
996
- * misinterpreting JSON column values like `{ set: 'x' }` as operators
997
- * (real operator objects always have exactly one key, and a plain JSON
998
- * payload that happens to have a single `set` key is extremely unusual).
999
- * Multi-key objects are always treated as plain (JSON) values.
2119
+ * `divide` on an INTEGER column is the engine's truncating integer division
2120
+ * (`integerDivision`): `DIV` on MySQL, whose `/` is decimal division, so
2121
+ * `75 / 2` is 37.5 and the assignment back to an INT rounds it to 38, where
2122
+ * PostgreSQL's integer division truncates to 37. `DIV` truncates toward zero,
2123
+ * as PostgreSQL does. Decided by the COLUMN TYPE alone, never by
2124
+ * the bound divisor: the SQL text must not depend on a value, or the template
2125
+ * cache would serve one shape to the other.
2126
+ */
2127
+ function arithmeticSql(qi, op, column) {
2128
+ if (op === 'divide' && isIntegerColumnType(qi.columnPgTypeMap.get(column))) {
2129
+ return qi.dialect.capabilities.integerDivision;
2130
+ }
2131
+ return ARITHMETIC_SQL[op];
2132
+ }
2133
+ /**
2134
+ * The atomic operator a SET value names, or `null` when it is a plain value.
2135
+ * THE one rule, read by the SET build, its fingerprint and its cache-hit param
2136
+ * collector, so the three cannot disagree about whether a value is an
2137
+ * operator.
2138
+ *
2139
+ * A branded expression or `col()` reference is checked FIRST and answers
2140
+ * `'expr'`: it is a frozen plain object too, and must never reach the
2141
+ * operator test below. An operator is a plain object with EXACTLY one key from
2142
+ * {@link UPDATE_OPERATOR_KEYS}. On a json/jsonb column only `set` is an
2143
+ * operator: an object there IS the JSON value, so `{ increment: 1 }` is the
2144
+ * document `{"increment":1}`, and `{ set: doc }` is the way to write a
2145
+ * document that is itself shaped like `{ set: ... }`.
2146
+ */
2147
+ export function setOperatorOf(qi, key, value) {
2148
+ if (isSqlExpr(value) || isColumnRef(value))
2149
+ return 'expr';
2150
+ if (!isUnmatchedPlainObject(value))
2151
+ return null;
2152
+ const keys = Object.keys(value);
2153
+ if (keys.length !== 1 || !UPDATE_OPERATOR_KEYS.has(keys[0]))
2154
+ return null;
2155
+ const op = keys[0];
2156
+ if (op !== 'set' && isJsonWriteColumn(qi, key))
2157
+ return null;
2158
+ return op;
2159
+ }
2160
+ /** Whether `key` names a json/jsonb column (non-throwing; unknown keys are not). */
2161
+ function isJsonWriteColumn(qi, key) {
2162
+ const column = resolveColumnName(qi.tableMeta, key);
2163
+ const dbType = column ? qi.columnPgTypeMap.get(column) : undefined;
2164
+ return dbType !== undefined && whereMod.isJsonColumnType(qi, dbType);
2165
+ }
2166
+ const UPDATE_SET = { operation: 'update' };
2167
+ const NO_READS = new Set();
2168
+ /**
2169
+ * Compile a SET list: one clause per entry, in the order given (the canonical
2170
+ * column order of {@link writeEntries}), pushing params as it goes so later
2171
+ * clauses (WHERE, version check) continue the numbering.
2172
+ *
2173
+ * On MySQL the clause STRINGS are then reordered (see
2174
+ * {@link orderForLeftToRightSet}). Params are not: MySQL binds by name
2175
+ * (`:pN`), so a clause keeps its placeholders wherever it lands, and the
2176
+ * cache-hit collector ({@link collectSetParams}) keeps pushing in canonical
2177
+ * order.
2178
+ */
2179
+ function buildSetClauses(qi, entries, params, ctx = UPDATE_SET) {
2180
+ const compiled = entries.map(([k, v]) => compileSetEntry(qi, k, v, params, ctx));
2181
+ return qi.dialect.capabilities.setClauseOrder === 'left-to-right'
2182
+ ? orderForLeftToRightSet(qi, compiled)
2183
+ : compiled.map((c) => c.sql);
2184
+ }
2185
+ /**
2186
+ * Build a single SET clause entry.
2187
+ *
2188
+ * Supports plain values, atomic operator objects ({ set, increment,
2189
+ * decrement, multiply, divide }) and expressions (`expr.*`, `col()`), as
2190
+ * decided by {@link setOperatorOf}.
1000
2191
  *
1001
2192
  * Returns the SQL fragment (e.g., `"view_count" = "view_count" + $3`) and
1002
2193
  * pushes any required params onto the shared params array so that WHERE
1003
2194
  * clause numbering continues correctly afterward.
1004
2195
  *
1005
- * `refQualifier` prefixes the column REFERENCE an arithmetic operator reads
1006
- * from (`"view_count" = <qualifier>"view_count" + $n`). A plain `UPDATE` has
1007
- * one table in scope and passes nothing; `upsert` passes the table name,
1008
- * because inside `ON CONFLICT ... DO UPDATE SET` the target table and
1009
- * `excluded` are BOTH in scope and a bare reference is 42702 on PostgreSQL.
1010
- * The assignment target on the left stays bare on every engine (it can only
1011
- * ever be the target table's column), and `set` / literal values reference no
1012
- * column at all, so their SQL is byte-identical with or without a qualifier.
2196
+ * `ctx.target` qualifies the column REFERENCES the right-hand side reads
2197
+ * (`"view_count" = "users"."view_count" + $n`): absent for a plain `UPDATE`,
2198
+ * the table name in an upsert, because inside `ON CONFLICT ... DO UPDATE SET`
2199
+ * the target table and `excluded` are BOTH in scope and a bare reference is
2200
+ * 42702 on PostgreSQL. The assignment target on the left stays bare on every
2201
+ * engine (it can only ever be the target table's column), and `set` / literal
2202
+ * values reference no column at all, so their SQL is byte-identical with or
2203
+ * without a qualifier.
1013
2204
  */
1014
- export function buildSetClause(qi, key, value, params, refQualifier = '') {
2205
+ function compileSetEntry(qi, key, value, params, ctx) {
1015
2206
  const col = qi.toSqlColumn(key);
1016
- const ref = `${refQualifier}${col}`;
2207
+ const column = qi.toColumn(key);
2208
+ const op = setOperatorOf(qi, key, value);
2209
+ if (op === 'expr') {
2210
+ const compiled = compileSetExpr(exprEnv(qi, key, params, ctx), value);
2211
+ return { sql: `${col} = ${compiled.sql}`, column, reads: compiled.reads };
2212
+ }
1017
2213
  // Enum columns get an explicit `::"EnumName"` cast on their value bind
1018
2214
  // (see enumTypeForColumn); `''` everywhere else. Value-invariant, so the
1019
2215
  // SQL cache and collectSetParams are unaffected.
1020
- const cast = whereMod.enumCastSuffix(qi, qi.toColumn(key));
1021
- // Detect atomic-operator object: plain object (not null, not array, not
1022
- // Date, not Buffer) with EXACTLY one key matching an operator name.
1023
- if (value !== null &&
1024
- typeof value === 'object' &&
1025
- !Array.isArray(value) &&
1026
- !(value instanceof Date) &&
1027
- !Buffer.isBuffer(value)) {
1028
- const v = value;
1029
- const keys = Object.keys(v);
1030
- if (keys.length === 1 && UPDATE_OPERATOR_KEYS.has(keys[0])) {
1031
- const op = keys[0];
1032
- const opValue = v[op];
1033
- if (op === 'set') {
1034
- params.push(coerceWriteValue(qi, key, opValue));
1035
- return `${col} = ${qi.p(params.length)}${cast}`;
1036
- }
1037
- // Arithmetic operators: must be finite numbers
1038
- if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
1039
- throw new ValidationError(`update operator "${op}" on "${qi.table}.${key}" requires a finite number, got ${typeof opValue}`);
1040
- }
1041
- if (op === 'increment') {
1042
- params.push(opValue);
1043
- return `${col} = ${ref} + ${qi.p(params.length)}`;
1044
- }
1045
- if (op === 'decrement') {
1046
- params.push(opValue);
1047
- return `${col} = ${ref} - ${qi.p(params.length)}`;
1048
- }
1049
- if (op === 'multiply') {
1050
- params.push(opValue);
1051
- return `${col} = ${ref} * ${qi.p(params.length)}`;
1052
- }
1053
- if (op === 'divide') {
1054
- params.push(opValue);
1055
- return `${col} = ${ref} / ${qi.p(params.length)}`;
1056
- }
2216
+ const cast = whereMod.enumCastSuffix(qi, column);
2217
+ if (op === 'set') {
2218
+ params.push(coerceWriteValue(qi, key, value.set));
2219
+ return { sql: `${col} = ${qi.p(params.length)}${cast}`, column, reads: NO_READS };
2220
+ }
2221
+ if (op !== null) {
2222
+ const opValue = value[op];
2223
+ // Arithmetic operators: must be finite numbers
2224
+ if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
2225
+ throw new ValidationError(`update operator "${op}" on "${qi.table}.${key}" requires a finite number, got ${typeof opValue}`);
1057
2226
  }
1058
- // Fall through: multi-key objects or non-operator single-key objects
1059
- // are treated as plain values (e.g., JSONB column payloads), unless the
1060
- // shape can only be a misspelled atomic operator (see below).
1061
- assertBindableSetValue(qi, key, value);
2227
+ params.push(opValue);
2228
+ const ref = ctx.target ? `${ctx.target}.${col}` : col;
2229
+ // Reads only its own column, which no ordering needs to respect.
2230
+ return { sql: `${col} = ${ref} ${arithmeticSql(qi, op, column)} ${qi.p(params.length)}`, column, reads: NO_READS };
1062
2231
  }
2232
+ // A misspelled operator gets its own E003 before the bind seam's general
2233
+ // shape check.
2234
+ assertBindableSetValue(qi, key, value);
1063
2235
  // Plain value (including null, Date, Buffer, arrays, JSON objects)
1064
2236
  params.push(coerceWriteValue(qi, key, value));
1065
- return `${col} = ${qi.p(params.length)}${cast}`;
2237
+ return { sql: `${col} = ${qi.p(params.length)}${cast}`, column, reads: NO_READS };
2238
+ }
2239
+ /**
2240
+ * The environment one expression compiles in: how a value leaf binds, how a
2241
+ * column of the target row and of the incoming row render, and how a case
2242
+ * predicate compiles.
2243
+ */
2244
+ function exprEnv(qi, key, params, ctx) {
2245
+ const cast = exprValueCast(qi, key);
2246
+ const target = ctx.target;
2247
+ const incoming = ctx.incoming;
2248
+ return {
2249
+ qi,
2250
+ targetKey: key,
2251
+ operation: ctx.operation,
2252
+ bind: (value, arithmetic) => {
2253
+ params.push(coerceWriteValue(qi, key, value));
2254
+ return `${qi.p(params.length)}${arithmetic ? arithmeticValueCast(qi, key, cast, arithmetic) : cast}`;
2255
+ },
2256
+ column: (column) => (target ? `${target}.${qi.q(column)}` : qi.q(column)),
2257
+ excluded: incoming ? (column) => `${incoming}.${qi.q(column)}` : undefined,
2258
+ insertColumns: ctx.insertColumns,
2259
+ predicate: (when) => {
2260
+ const sql = target
2261
+ ? whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, target, when, params)
2262
+ : whereMod.buildWhereClause(qi, when, params);
2263
+ if (sql === null) {
2264
+ throw new ValidationError(`expr.case() on "${qi.table}": an arm's when compiles to no predicate.`);
2265
+ }
2266
+ return sql;
2267
+ },
2268
+ };
2269
+ }
2270
+ /**
2271
+ * A type token that is safe to write after `::` as is: a plain name, a
2272
+ * multi-word built-in (`double precision`), optionally with a precision.
2273
+ */
2274
+ const SAFE_CAST_TYPE = /^[A-Za-z_][A-Za-z0-9_]*( [A-Za-z_][A-Za-z0-9_]*)*(\(\d+( ?, ?\d+)?\))?$/;
2275
+ /**
2276
+ * The cast every bound value inside an expression carries on PostgreSQL: the
2277
+ * TARGET column's type. node-postgres sends parameters untyped, so in an
2278
+ * expression whose operands are all bound values PostgreSQL infers `text` and
2279
+ * fails (`CASE WHEN ... THEN $1 ELSE $2 END` into an int column is 42804, and
2280
+ * `$1 + $2` is 42725). Enums use the quoted enum cast; a type the metadata
2281
+ * does not name plainly, or one from another schema, gets none. Other engines
2282
+ * bind typed values and get none.
2283
+ */
2284
+ function exprValueCast(qi, key) {
2285
+ if (!qi.dialect.capabilities.paramTypeCasts)
2286
+ return '';
2287
+ const column = resolveColumnName(qi.tableMeta, key);
2288
+ if (column === undefined)
2289
+ return '';
2290
+ const enumCast = whereMod.enumCastSuffix(qi, column);
2291
+ if (enumCast)
2292
+ return enumCast;
2293
+ if (qi.crossSchemaTypeColumns.has(column))
2294
+ return '';
2295
+ const type = qi.columnPgTypeMap.get(column);
2296
+ if (type === undefined || type.startsWith('_') || !SAFE_CAST_TYPE.test(type))
2297
+ return '';
2298
+ return `::${type}`;
2299
+ }
2300
+ /**
2301
+ * The cast on a bound arithmetic operand on PostgreSQL: the operand's own
2302
+ * class, so `n / 2` stays integer division into a float column and `n * 1.5`
2303
+ * is not forced through an integer cast. An integer operand keeps the target
2304
+ * column's cast when that column is an integer one, `int8` otherwise; a
2305
+ * non-integer one is `numeric`. Other engines bind typed values and get none.
2306
+ */
2307
+ function arithmeticValueCast(qi, key, targetCast, cls) {
2308
+ if (!qi.dialect.capabilities.paramTypeCasts)
2309
+ return '';
2310
+ if (cls === 'dec')
2311
+ return '::numeric';
2312
+ const column = resolveColumnName(qi.tableMeta, key);
2313
+ return column !== undefined && targetCast !== '' && isIntegerColumnType(qi.columnPgTypeMap.get(column))
2314
+ ? targetCast
2315
+ : '::int8';
1066
2316
  }
1067
2317
  /**
1068
- * Reject a `data` value that reached {@link buildSetClause}'s plain-value
2318
+ * MySQL evaluates a single-table UPDATE's SET list LEFT TO RIGHT, and a later
2319
+ * assignment sees the NEW value of an earlier column; PostgreSQL, SQLite and
2320
+ * SQL Server read old values. So `{ a: col('b'), b: col('a') }` swaps there
2321
+ * and duplicates on MySQL. `ON DUPLICATE KEY UPDATE` behaves the same way.
2322
+ *
2323
+ * Order the list so every assignment that READS a column comes before the
2324
+ * assignment that WRITES it: a stable topological sort that keeps the
2325
+ * canonical order wherever the reads allow, so the order is a pure function of
2326
+ * the data's shape and the SQL-cache key (which already captures that shape)
2327
+ * stays sufficient. A cycle (a swap) has no such order and is refused.
2328
+ */
2329
+ function orderForLeftToRightSet(qi, entries) {
2330
+ if (entries.every((e) => e.reads.size === 0))
2331
+ return entries.map((e) => e.sql);
2332
+ const writer = new Map();
2333
+ entries.forEach((e, i) => {
2334
+ writer.set(e.column, i);
2335
+ });
2336
+ const before = entries.map(() => []);
2337
+ const pending = entries.map(() => 0);
2338
+ entries.forEach((e, i) => {
2339
+ for (const column of e.reads) {
2340
+ const j = writer.get(column);
2341
+ if (j === undefined || j === i)
2342
+ continue;
2343
+ before[i].push(j);
2344
+ pending[j]++;
2345
+ }
2346
+ });
2347
+ const ready = pending.flatMap((n, i) => (n === 0 ? [i] : []));
2348
+ const order = [];
2349
+ while (ready.length > 0) {
2350
+ ready.sort((a, b) => a - b);
2351
+ const i = ready.shift();
2352
+ order.push(i);
2353
+ for (const j of before[i])
2354
+ if (--pending[j] === 0)
2355
+ ready.push(j);
2356
+ }
2357
+ if (order.length < entries.length) {
2358
+ const stuck = entries
2359
+ .filter((_, i) => pending[i] > 0)
2360
+ .map((e) => qi.tableMeta.reverseColumnMap[e.column] ?? e.column);
2361
+ throw new UnsupportedFeatureError(`An update on "${qi.table}" that reads ${stuck.map((f) => `"${f}"`).join(' and ')} while assigning them`, qi.dialect.name, "MySQL evaluates SET left to right, so a later assignment sees the earlier one's new value and this " +
2362
+ 'cannot be expressed. Split it into two updates.');
2363
+ }
2364
+ return order.map((i) => entries[i].sql);
2365
+ }
2366
+ /**
2367
+ * Reject a `data` value that reached {@link buildSetClauses}'s plain-value
1069
2368
  * fallthrough as a plain object with exactly ONE key that is not a known
1070
2369
  * atomic operator. On a scalar column that shape can only be a misspelled
1071
2370
  * operator (`{ incremnt: 1 }`, `{ bogus: 'x' }`); binding it plainly writes the
@@ -1096,52 +2395,47 @@ export function assertBindableSetValue(qi, key, value) {
1096
2395
  }
1097
2396
  /**
1098
2397
  * Fingerprint SET clauses for update/updateMany.
1099
- * Captures key names + operator types (set/increment/etc) but not values.
2398
+ * Captures key names + operator types (set/increment/etc) but not values. An
2399
+ * expression contributes `k:expr(<shape>)` from the same walk that compiles
2400
+ * it, so a statement without one keeps its key byte-identical.
1100
2401
  */
1101
2402
  export function fingerprintSet(qi, data) {
1102
2403
  const entries = writeEntries(qi, data);
1103
2404
  const parts = [];
1104
2405
  for (const [k, v] of entries) {
1105
- if (v !== null &&
1106
- typeof v === 'object' &&
1107
- !Array.isArray(v) &&
1108
- !(v instanceof Date) &&
1109
- !(typeof Buffer !== 'undefined' && Buffer.isBuffer(v))) {
1110
- const keys = Object.keys(v);
1111
- if (keys.length === 1 && UPDATE_OPERATOR_KEYS.has(keys[0])) {
1112
- parts.push(`${k}:${keys[0]}`);
1113
- continue;
1114
- }
2406
+ const op = setOperatorOf(qi, k, v);
2407
+ if (op === 'expr') {
2408
+ const env = exprEnv(qi, k, [], UPDATE_SET);
2409
+ parts.push(`${k}:expr(${compileSetExpr(env, v).fingerprint})`);
2410
+ continue;
1115
2411
  }
1116
- parts.push(`${k}:eq`);
2412
+ parts.push(`${k}:${op ?? 'eq'}`);
1117
2413
  }
1118
2414
  return parts.join(',');
1119
2415
  }
1120
2416
  /**
1121
- * Collect SET params for update/updateMany. Mirrors buildSetClause param order.
2417
+ * Collect SET params for update/updateMany. Mirrors buildSetClauses param order.
1122
2418
  */
1123
2419
  export function collectSetParams(qi, data, params) {
1124
2420
  const entries = writeEntries(qi, data);
1125
2421
  for (const [k, v] of entries) {
1126
- if (v !== null &&
1127
- typeof v === 'object' &&
1128
- !Array.isArray(v) &&
1129
- !(v instanceof Date) &&
1130
- !(typeof Buffer !== 'undefined' && Buffer.isBuffer(v))) {
1131
- const obj = v;
1132
- const keys = Object.keys(obj);
1133
- if (keys.length === 1 && UPDATE_OPERATOR_KEYS.has(keys[0])) {
1134
- // `set` is the only operator that binds a user value of arbitrary type;
1135
- // the arithmetic operators are finite numbers, which coerceWriteValue
1136
- // returns untouched. Mirrors buildSetClause.
1137
- params.push(coerceWriteValue(qi, k, obj[keys[0]]));
1138
- continue;
1139
- }
1140
- // Mirror the build path's misspelled-operator guard: the SET fingerprint
1141
- // is `key:eq` for both a plain value and a bogus operator object, so a
1142
- // cache HIT would otherwise bind the object and skip the check.
1143
- assertBindableSetValue(qi, k, v);
2422
+ const op = setOperatorOf(qi, k, v);
2423
+ if (op === 'expr') {
2424
+ // The build's own walk: it binds the same leaves in the same order.
2425
+ compileSetExpr(exprEnv(qi, k, params, UPDATE_SET), v);
2426
+ continue;
2427
+ }
2428
+ if (op !== null) {
2429
+ // `set` is the only operator that binds a user value of arbitrary type;
2430
+ // the arithmetic operators are finite numbers, which coerceWriteValue
2431
+ // returns untouched. Mirrors buildSetClauses.
2432
+ params.push(coerceWriteValue(qi, k, v[op]));
2433
+ continue;
1144
2434
  }
2435
+ // Mirror the build path's misspelled-operator guard: the SET fingerprint
2436
+ // is `key:eq` for both a plain value and a bogus operator object, so a
2437
+ // cache HIT would otherwise bind the object and skip the check.
2438
+ assertBindableSetValue(qi, k, v);
1145
2439
  params.push(coerceWriteValue(qi, k, v));
1146
2440
  }
1147
2441
  }