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
@@ -50,8 +50,12 @@ exports.buildReselectByWhere = buildReselectByWhere;
50
50
  exports.buildCreate = buildCreate;
51
51
  exports.makeCreateReselect = makeCreateReselect;
52
52
  exports.buildCreateMany = buildCreateMany;
53
+ exports.planCreateMany = planCreateMany;
54
+ exports.explainConflictKeyCollision = explainConflictKeyCollision;
53
55
  exports.buildUpdate = buildUpdate;
54
56
  exports.buildDelete = buildDelete;
57
+ exports.assertNoBulkWritePaging = assertNoBulkWritePaging;
58
+ exports.assertNoOptimisticLock = assertNoOptimisticLock;
55
59
  exports.upsertLookupReason = upsertLookupReason;
56
60
  exports.buildUpsert = buildUpsert;
57
61
  exports.buildUpdateMany = buildUpdateMany;
@@ -65,21 +69,41 @@ exports.writeReselectSelection = writeReselectSelection;
65
69
  exports.parseWriteRow = parseWriteRow;
66
70
  exports.assertWritable = assertWritable;
67
71
  exports.assertNoGeneratedColumns = assertNoGeneratedColumns;
68
- exports.buildSetClause = buildSetClause;
72
+ exports.setOperatorOf = setOperatorOf;
69
73
  exports.assertBindableSetValue = assertBindableSetValue;
70
74
  exports.fingerprintSet = fingerprintSet;
71
75
  exports.collectSetParams = collectSetParams;
76
+ const dialect_js_1 = require("../dialect.js");
72
77
  const errors_js_1 = require("../errors.js");
73
78
  const schema_js_1 = require("../schema.js");
79
+ const column_ref_js_1 = require("./column-ref.js");
74
80
  const compound_unique_js_1 = require("./compound-unique.js");
81
+ const expr_js_1 = require("./expr.js");
75
82
  const filters_js_1 = require("./filters.js");
83
+ const json_value_js_1 = require("./json-value.js");
84
+ const normalize_args_js_1 = require("./normalize-args.js");
76
85
  const types_js_1 = require("./types.js");
77
86
  const utils_js_1 = require("./utils.js");
78
87
  const whereMod = __importStar(require("./where.js"));
79
88
  /**
80
- * Normalize one `data` value before it is bound as a write param.
89
+ * Normalize one `data` value before it is bound as a write param. This is THE
90
+ * write bind seam: create, createMany, update, updateMany, both halves of
91
+ * upsert, the cache-hit param collectors and (through those public methods)
92
+ * the nested-write engine all bind through it.
81
93
  *
82
- * Two value shapes need rewriting, both a JS `Date` on a temporal column:
94
+ * JSON columns (json / jsonb):
95
+ *
96
+ * - A JS array is serialized with `JSON.stringify`. The drivers' default
97
+ * array serialization is a Postgres ARRAY literal, so `[]` was stored as the
98
+ * object `{}`, `["a"]` failed with 22P02, and `createMany`'s UNNEST
99
+ * transpose flattened `[1, 2, 3]` into three rows.
100
+ * - A {@link jsonValue} wrapper is serialized with `JSON.stringify`, which is
101
+ * how a JSON string or a JSON `null` is written.
102
+ * - A JS string is RAW JSON TEXT and passes through untouched, so
103
+ * `JSON.stringify(value)` callers keep storing the parsed value. A plain
104
+ * object passes through too: every driver already serializes it as JSON.
105
+ *
106
+ * Temporal columns, a JS `Date`:
83
107
  *
84
108
  * 1. A time-of-day column (`time` / `timetz`), the driver serializes a `Date`
85
109
  * as a full ISO timestamp with the process offset and Postgres answers
@@ -89,45 +113,246 @@ const whereMod = __importStar(require("./where.js"));
89
113
  * non-UTC process the stored value is wrong and (because the read path
90
114
  * interprets an offset-less value as UTC) does not round-trip. Rewritten to
91
115
  * the UTC components, mirroring `parseDbDate`. See `toLocalDateTimeLiteral`
92
- * in query/utils.ts.
116
+ * in query/utils.ts. `timestamptz` is deliberately untouched: it stores a
117
+ * real instant and the driver's local-offset string is already correct.
118
+ *
119
+ * A plain object or an array bound to a known scalar column (text, int, uuid,
120
+ * ...) is refused with E003, and so is a `Map` or `Set` anywhere; see
121
+ * {@link assertScalarBindable} and {@link assertNotCollection}.
93
122
  *
94
- * `timestamptz` is deliberately untouched: it stores a real instant and the
95
- * driver's local-offset string is already correct for it.
123
+ * Expressions (`expr.*`, `col()`) never bind as a value: the update paths
124
+ * compile them before reaching here, so one that arrives is in a position that
125
+ * takes values only (create data, `{ set }`) and is refused. An unbranded
126
+ * object shaped like one is refused on a non-JSON column on every engine.
96
127
  *
97
- * Every other value, including a string that is already a valid literal, is
98
- * returned by identity, so this is a byte-for-byte no-op for every non-temporal
99
- * column.
128
+ * Every other value is returned by identity, so a primitive costs one check
129
+ * and no lookup. The exception is a string on an engine that defines the
130
+ * dialect's `checkTextWrite` (SQLite), which is checked against its column's
131
+ * type first; see {@link checkTextWrite}.
100
132
  */
101
133
  function coerceWriteValue(qi, key, value) {
102
- // Cheap shape check first: the common path costs one check and no lookup.
103
- if (!(value instanceof Date) && !Array.isArray(value))
134
+ if (typeof value === 'string') {
135
+ if (qi.dialect.checkTextWrite !== undefined)
136
+ checkTextWrite(qi, key, value);
137
+ return value;
138
+ }
139
+ if (value === null || typeof value !== 'object')
104
140
  return value;
141
+ if ((0, expr_js_1.isSqlExpr)(value) || (0, column_ref_js_1.isColumnRef)(value)) {
142
+ throw new errors_js_1.ValidationError((0, expr_js_1.exprNotAllowedMessage)(qi.table, key, value));
143
+ }
144
+ assertNotCollection(qi, key, value);
105
145
  // THE key-resolution rule, shared with `toColumn` (the SQL side of this very
106
- // statement) so the two can never disagree about which column a key names:
107
- // resolving through `columnMap` alone missed the snake_case COLUMN spelling
108
- // that the SQL builders accept, so those writes bound an unprocessed value
109
- // under an identical statement. Non-throwing, because this also runs on the
110
- // cache-HIT param-collect path, where an unknown key must not turn into a
111
- // different error than the build path already raises.
146
+ // statement) so the two can never disagree about which column a key names.
147
+ // Non-throwing, because this also runs on the cache-HIT param-collect path,
148
+ // where an unknown key must not turn into a different error than the build
149
+ // path already raises.
112
150
  const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
113
- if (!column)
151
+ const dbType = column ? qi.columnPgTypeMap.get(column) : undefined;
152
+ if ((0, json_value_js_1.isJsonValue)(value))
153
+ return jsonText(qi, key, value.value);
154
+ if (dbType !== undefined && whereMod.isJsonColumnType(qi, dbType)) {
155
+ return Array.isArray(value) ? jsonText(qi, key, value) : value;
156
+ }
157
+ if ((0, expr_js_1.isPlainExprShape)(value)) {
158
+ throw new errors_js_1.ValidationError((0, expr_js_1.plainExprShapeMessage)(qi.table, key, String(value.kind)));
159
+ }
160
+ assertScalarBindable(qi, key, dbType, value);
161
+ if (!(value instanceof Date) && !Array.isArray(value))
114
162
  return value;
115
- // Metadata generated by an older Turbine still carries per-column types
116
- // (`pgTypes` has always been part of TableMetadata), and a column missing
117
- // from the map resolves to undefined → no coercion, today's behavior.
118
- return (0, utils_js_1.coerceTemporalValue)(qi.columnPgTypeMap.get(column), value, utcDateTimeWrites(qi));
163
+ // A column missing from the type map resolves to undefined → no coercion.
164
+ // The gate is where.ts's, so a written Date and a compared Date take the
165
+ // same form: a `date` column the UTC date part on every SQL engine, a
166
+ // zone-less `timestamp` the UTC literal on PostgreSQL only.
167
+ return (0, utils_js_1.coerceTemporalValue)(dbType, value, whereMod.utcDateTimeRewrite(qi, dbType));
119
168
  }
120
169
  /**
121
- * Whether the zone-less `date` / `timestamp` UTC rewrite applies.
122
- *
123
- * PostgreSQL only: MySQL's `TIMESTAMP`/`DATETIME` and SQL Server's
124
- * `datetime2` are converted or bound by their own drivers, and MySQL in
125
- * particular reads a zone-less literal in the SESSION time zone, so a UTC
126
- * literal would be misread there. `utcTimestamps: false` opts out, it is the
127
- * same switch that turns off the UTC READ parsing, so the two stay symmetric.
170
+ * Ask the engine whether it stores `value` in the column `key` names as that
171
+ * column's type (the dialect's `checkTextWrite`); E003 when it would not.
172
+ * An unknown key is left to the build path's own error, as in
173
+ * {@link coerceWriteValue}.
174
+ */
175
+ function checkTextWrite(qi, key, value) {
176
+ const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
177
+ const columnType = column ? qi.columnPgTypeMap.get(column) : undefined;
178
+ if (!column || columnType === undefined)
179
+ return;
180
+ const refusal = qi.dialect.checkTextWrite?.(qi.table, column, columnType, value);
181
+ if (refusal !== undefined)
182
+ throw new errors_js_1.ValidationError(refusal);
183
+ }
184
+ /** `JSON.stringify(value)` for a JSON column, or E003 when it has no JSON form. */
185
+ function jsonText(qi, key, value) {
186
+ assertNotCollection(qi, key, value);
187
+ const text = (0, json_value_js_1.serializeJsonValue)(value);
188
+ if (text === undefined) {
189
+ throw new errors_js_1.ValidationError(`Cannot write "${qi.table}.${key}": the value has no JSON form (${describeValue(value)}). ` +
190
+ 'Convert it first, e.g. a bigint to a string or a number.');
191
+ }
192
+ return text;
193
+ }
194
+ function describeValue(value) {
195
+ return typeof value === 'bigint' ? 'it contains a bigint' : `got ${typeof value}`;
196
+ }
197
+ /**
198
+ * PostgreSQL column types on which a plain object or an array can only be a
199
+ * mistake. The driver serializes an object as JSON text and an array as an
200
+ * ARRAY literal, so on a text column `{ first, last }` was stored as
201
+ * `{"first":"A","last":"B"}` and `['a']` as `{"a"}`, silently. A type outside
202
+ * this list (enums, domains, extension and user types) is never refused here,
203
+ * because a domain over jsonb or an extension type may legitimately take one.
128
204
  */
129
- function utcDateTimeWrites(qi) {
130
- return qi.dialect.name === 'postgresql' && qi.utcTimestamps !== false;
205
+ const SCALAR_PG_TYPES = new Set([
206
+ 'text',
207
+ 'varchar',
208
+ 'character varying',
209
+ 'char',
210
+ 'character',
211
+ 'bpchar',
212
+ 'citext',
213
+ 'name',
214
+ 'uuid',
215
+ 'int2',
216
+ 'int4',
217
+ 'int8',
218
+ 'smallint',
219
+ 'integer',
220
+ 'bigint',
221
+ 'numeric',
222
+ 'decimal',
223
+ 'float4',
224
+ 'float8',
225
+ 'real',
226
+ 'double precision',
227
+ 'money',
228
+ 'bool',
229
+ 'boolean',
230
+ 'date',
231
+ 'time',
232
+ 'timetz',
233
+ 'timestamp',
234
+ 'timestamptz',
235
+ 'time without time zone',
236
+ 'time with time zone',
237
+ 'timestamp without time zone',
238
+ 'timestamp with time zone',
239
+ 'interval',
240
+ 'bytea',
241
+ 'inet',
242
+ 'cidr',
243
+ 'macaddr',
244
+ 'xml',
245
+ ]);
246
+ /**
247
+ * Column types, in every engine's spelling, that can only hold a scalar: a
248
+ * plain object or an array written to one is a mistake on every engine (the
249
+ * MySQL and SQLite shims stored it as JSON text, in an INTEGER column too).
250
+ */
251
+ const NON_TEXT_SCALAR_TYPES = new Set([
252
+ 'int',
253
+ 'integer',
254
+ 'tinyint',
255
+ 'smallint',
256
+ 'mediumint',
257
+ 'bigint',
258
+ 'int2',
259
+ 'int4',
260
+ 'int8',
261
+ 'numeric',
262
+ 'decimal',
263
+ 'dec',
264
+ 'real',
265
+ 'float',
266
+ 'double',
267
+ 'double precision',
268
+ 'money',
269
+ 'smallmoney',
270
+ 'bit',
271
+ 'bool',
272
+ 'boolean',
273
+ 'date',
274
+ 'time',
275
+ 'datetime',
276
+ 'datetime2',
277
+ 'smalldatetime',
278
+ 'datetimeoffset',
279
+ 'timestamp',
280
+ 'timestamptz',
281
+ 'year',
282
+ 'uuid',
283
+ 'uniqueidentifier',
284
+ 'blob',
285
+ 'tinyblob',
286
+ 'mediumblob',
287
+ 'longblob',
288
+ 'binary',
289
+ 'varbinary',
290
+ 'image',
291
+ ]);
292
+ /**
293
+ * MySQL's text types. MySQL has a JSON column type, so a text column is not
294
+ * where an object goes, as on PostgreSQL. SQLite and SQL Server have no JSON
295
+ * type: Turbine's own DDL stores json in TEXT / NVARCHAR(MAX), so an object
296
+ * written to a text column there is serialized as JSON on purpose.
297
+ */
298
+ const MYSQL_TEXT_TYPES = new Set([
299
+ 'char',
300
+ 'varchar',
301
+ 'tinytext',
302
+ 'text',
303
+ 'mediumtext',
304
+ 'longtext',
305
+ 'enum',
306
+ 'set',
307
+ ]);
308
+ /** A column type token, lowercased, without a length, precision or sign modifier. */
309
+ function baseColumnType(dbType) {
310
+ return dbType
311
+ .toLowerCase()
312
+ .replace(/\(\s*\d+(\s*,\s*\d+)?\s*\)/g, '')
313
+ .replace(/\b(unsigned|signed|zerofill)\b/g, '')
314
+ .replace(/\s+/g, ' ')
315
+ .trim();
316
+ }
317
+ /** Whether this engine's column of `dbType` refuses a plain object or an array. */
318
+ function refusesStructuredValue(qi, dbType) {
319
+ const columns = qi.dialect.capabilities.structuredValueColumns;
320
+ if (columns === 'pg-scalars')
321
+ return SCALAR_PG_TYPES.has(dbType.toLowerCase());
322
+ const t = baseColumnType(dbType);
323
+ return NON_TEXT_SCALAR_TYPES.has(t) || (columns === 'scalars-and-text' && MYSQL_TEXT_TYPES.has(t));
324
+ }
325
+ /**
326
+ * Refuse a `Map` or `Set` as a written value, on every column. Neither has a
327
+ * stored form: JSON.stringify writes `{}` for both, so the entries were lost
328
+ * without an error, on a json column too.
329
+ */
330
+ function assertNotCollection(qi, key, value) {
331
+ if (!(value instanceof Map) && !(value instanceof Set))
332
+ return;
333
+ const what = value instanceof Map ? 'a Map' : 'a Set';
334
+ throw new errors_js_1.ValidationError(`Cannot write ${what} to "${qi.table}.${key}": it has no stored form, and JSON would write it as {} and lose ` +
335
+ `its entries. Convert it first (${value instanceof Map ? 'Object.fromEntries(map)' : '[...set]'}).`);
336
+ }
337
+ /**
338
+ * Refuse a plain object, or an array, bound to a known scalar column. Dates,
339
+ * Buffers, class instances (a Decimal wrapper, an interval) and every
340
+ * primitive are untouched, and so is every array column (`_text`) since its
341
+ * type is not a scalar one. Which text columns refuse one is per engine; see
342
+ * {@link MYSQL_TEXT_TYPES}.
343
+ */
344
+ function assertScalarBindable(qi, key, dbType, value) {
345
+ if (dbType === undefined)
346
+ return;
347
+ const isArray = Array.isArray(value);
348
+ if (!isArray && !(0, filters_js_1.isUnmatchedPlainObject)(value))
349
+ return;
350
+ if (!refusesStructuredValue(qi, dbType))
351
+ return;
352
+ throw new errors_js_1.ValidationError(`Cannot write ${isArray ? 'an array' : 'an object'} to "${qi.table}.${key}" (column type ${dbType}). ` +
353
+ 'Only json/jsonb columns take objects, and only array columns take arrays. ' +
354
+ 'To store JSON text in this column, pass `JSON.stringify(value)` or `jsonValue(value)`.' +
355
+ (isArray ? '' : ' Atomic operators (`{ set: v }`, `{ increment: n }`, ...) apply in update data only.'));
131
356
  }
132
357
  /**
133
358
  * Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
@@ -135,11 +360,157 @@ function utcDateTimeWrites(qi) {
135
360
  * return rows from non-RETURNING engines. Reuses the same parameterized WHERE
136
361
  * builder as reads, so no user value is interpolated.
137
362
  */
138
- function buildReselectByWhere(qi, whereObj, projection) {
363
+ function buildReselectByWhere(qi, whereObj, projection, lockRows = false) {
139
364
  const params = [];
140
365
  const clause = whereMod.buildWhereClause(qi, whereObj, params);
141
366
  const where = clause ? ` WHERE ${clause}` : '';
142
- return { sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}`, params };
367
+ return {
368
+ sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}${lockRows ? forUpdate(qi) : ''}`,
369
+ params,
370
+ };
371
+ }
372
+ /**
373
+ * Row-lock suffix for a read inside a reselect write's transaction, so a
374
+ * concurrent write of the same row waits instead of interleaving. Appended
375
+ * where the engine's reselect locks rows (MySQL); elsewhere nothing is.
376
+ */
377
+ function forUpdate(qi) {
378
+ return qi.dialect.capabilities.reselectLocksRows ? ' FOR UPDATE' : '';
379
+ }
380
+ /**
381
+ * Whether a write that sets `columns` may move a row out of `whereObj`, so the
382
+ * row must be re-read by its key rather than by `where`: a soft delete under a
383
+ * `deletedAt: null` global filter, `NOT: { status: 'done' }` with `status:
384
+ * 'done'`, a key change.
385
+ *
386
+ * Read off the compiled predicate, where every column it reads is quoted at
387
+ * any depth (`NOT`, `OR`, a relation filter's correlation, a column reference,
388
+ * the global filter), so no where shape is missed. A column of another table
389
+ * with the same name answers true, which costs one locking read and is never
390
+ * wrong.
391
+ */
392
+ function writeMayMoveRow(qi, whereObj, columns) {
393
+ if (columns.size === 0)
394
+ return false;
395
+ const clause = whereMod.buildWhereClause(qi, whereObj, []);
396
+ if (!clause)
397
+ return false;
398
+ for (const column of columns)
399
+ if (clause.includes(qi.q(column)))
400
+ return true;
401
+ return false;
402
+ }
403
+ /**
404
+ * The columns that identify a row after a write: the primary key, else the
405
+ * first unique key, else none.
406
+ */
407
+ function identityColumns(qi) {
408
+ return qi.tableMeta.primaryKey.length > 0 ? qi.tableMeta.primaryKey : ((0, compound_unique_js_1.uniqueKeyNames)(qi.tableMeta)[0] ?? []);
409
+ }
410
+ /**
411
+ * Lock the row(s) `whereObj` matches and return the first one's identity key as
412
+ * a `where`, with every key column `data` assigns replaced by its new value, so
413
+ * the row can be re-read after the write moves it. `null` when no row matched.
414
+ *
415
+ * Refused (E017) before anything is written: a table with no key to re-read by,
416
+ * a key column assigned by something whose result is not known here (an
417
+ * expression, `divide`, a non-integer operand), and, unless the call is a
418
+ * full-table write, a `where` that matched more than one row, which only an
419
+ * engine comparing a key loosely produces.
420
+ */
421
+ async function lockMovedRowKey(qi, exec, whereObj, data, operation, manyRows) {
422
+ const identity = identityColumns(qi);
423
+ if (identity.length === 0) {
424
+ throw new errors_js_1.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 ' +
425
+ 'primary key, or leave the columns `where` reads out of data.');
426
+ }
427
+ const assigned = new Map();
428
+ for (const [key, value] of writeEntries(qi, data)) {
429
+ const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
430
+ if (column !== undefined && identity.includes(column))
431
+ assigned.set(column, [key, value]);
432
+ }
433
+ const params = [];
434
+ const clause = whereMod.buildWhereClause(qi, whereObj, params);
435
+ const sql = `SELECT ${identity.map((c) => qi.q(c)).join(', ')} FROM ${qi.q(qi.table)}${clause ? ` WHERE ${clause}` : ''}${forUpdate(qi)}`;
436
+ const rows = (await exec(sql, params)).rows;
437
+ const row = rows[0];
438
+ if (!row)
439
+ return null;
440
+ if (!manyRows)
441
+ assertOneRowByKey(qi, operation, identity, rows.length);
442
+ const key = {};
443
+ for (const column of identity) {
444
+ const entry = assigned.get(column);
445
+ const next = entry === undefined ? row[column] : movedKeyValue(qi, operation, entry[0], row[column], entry[1]);
446
+ key[fieldOf(qi, column)] = reselectKeyValue(qi, column, next);
447
+ }
448
+ return key;
449
+ }
450
+ /**
451
+ * The value a key column holds after `assigned` is written over `current`: a
452
+ * plain value or `{ set }` as given, and `increment` / `decrement` / `multiply`
453
+ * by an integer computed exactly on an integer key. Anything else is refused
454
+ * (E017) because its result, and so the row, cannot be known before the write.
455
+ */
456
+ function movedKeyValue(qi, operation, key, current, assigned) {
457
+ const op = setOperatorOf(qi, key, assigned);
458
+ if (op === null)
459
+ return assigned;
460
+ if (op === 'set')
461
+ return assigned.set;
462
+ const operand = op === 'expr' ? undefined : assigned[op];
463
+ const base = integerOf(current);
464
+ if (base !== undefined && typeof operand === 'number' && Number.isSafeInteger(operand) && op !== 'divide') {
465
+ const n = BigInt(operand);
466
+ const next = op === 'increment' ? base + n : op === 'decrement' ? base - n : base * n;
467
+ return typeof current === 'number' &&
468
+ next >= BigInt(Number.MIN_SAFE_INTEGER) &&
469
+ next <= BigInt(Number.MAX_SAFE_INTEGER)
470
+ ? Number(next)
471
+ : typeof current === 'bigint'
472
+ ? next
473
+ : next.toString();
474
+ }
475
+ throw new errors_js_1.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 ' +
476
+ `write the new key with \`{ set: value }\`${op === 'expr' ? '' : ', or use an integer `increment`, `decrement` or `multiply`'}.`);
477
+ }
478
+ /** The columns the top level of `where` names, for a message. */
479
+ function whereColumns(qi, where) {
480
+ return Object.keys(where).flatMap((key) => {
481
+ const column = where[key] === undefined ? undefined : (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
482
+ return column === undefined ? [] : [column];
483
+ });
484
+ }
485
+ /** An integer key value as a bigint, or `undefined` for anything else. */
486
+ function integerOf(value) {
487
+ if (typeof value === 'bigint')
488
+ return value;
489
+ if (typeof value === 'number')
490
+ return Number.isSafeInteger(value) ? BigInt(value) : undefined;
491
+ if (typeof value === 'string' && /^-?[0-9]+$/.test(value))
492
+ return BigInt(value);
493
+ return undefined;
494
+ }
495
+ /**
496
+ * `where` with each top-level key value bound as a reselect binds keys (see
497
+ * {@link whereMod.TEXT_COMPARED_TYPES}), or `where` itself when nothing changes.
498
+ */
499
+ function textSafeKeyWhere(qi, where) {
500
+ let out;
501
+ for (const [key, value] of Object.entries(where)) {
502
+ const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
503
+ if (column === undefined)
504
+ continue;
505
+ const plain = (0, filters_js_1.isPlainRecord)(value) && Object.keys(value).length === 1 && 'equals' in value;
506
+ const raw = plain ? value.equals : value;
507
+ const bound = reselectKeyValue(qi, column, raw);
508
+ if (bound === raw)
509
+ continue;
510
+ out ??= { ...where };
511
+ out[key] = plain ? { equals: bound } : bound;
512
+ }
513
+ return out ?? where;
143
514
  }
144
515
  /**
145
516
  * Build the all-defaults INSERT for a `data` that names no column, via the
@@ -209,31 +580,181 @@ function buildCreate(qi, args, projection) {
209
580
  };
210
581
  }
211
582
  /**
212
- * Build the `'reselect'` plan for {@link buildCreate}: run the INSERT, then
213
- * `SELECT * WHERE pk = ?`. Returns `undefined` (skipped) unless the active
214
- * dialect's result strategy is `'reselect'`, so the PostgreSQL/RETURNING path
215
- * pays nothing. Not yet wired to a real non-RETURNING engine.
583
+ * Build the `'reselect'` plan for {@link buildCreate} (MySQL): run the INSERT,
584
+ * then `SELECT ... WHERE <key> = ?` by a key whose value is known exactly
585
+ * (see {@link createReselectKey}). Returns `undefined` unless the dialect's
586
+ * result strategy is `'reselect'`, so the RETURNING engines pay nothing.
216
587
  */
217
588
  function makeCreateReselect(qi, insertSql, insertParams, data, projection) {
218
589
  if (qi.dialect.resultStrategy !== 'reselect')
219
590
  return undefined;
591
+ const key = createReselectKey(qi, data);
220
592
  return async (exec) => {
221
593
  const writeResult = await exec(insertSql, insertParams);
222
- const insertId = qi.mutationInsertId(writeResult);
223
- const conds = [];
224
- const selParams = [];
225
- let idx = 1;
226
- for (const pk of qi.tableMeta.primaryKey) {
227
- const field = qi.tableMeta.reverseColumnMap[pk] ?? (0, schema_js_1.snakeToCamel)(pk);
228
- // Same rewrite the INSERT's own params got: a temporal PK re-fetched with
229
- // a raw Date would not match the literal that was just written.
230
- selParams.push(coerceWriteValue(qi, field, data[field] ?? data[pk] ?? insertId));
231
- conds.push(`${qi.q(pk)} = ${qi.p(idx++)}`);
232
- }
233
- const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
234
- return exec(`SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}`, selParams);
594
+ const params = key.values.map((value) => value === INSERT_ID
595
+ ? generatedKey(qi, writeResult, key)
596
+ : value === ZERO_INSERT_ID
597
+ ? zeroKey(qi, writeResult)
598
+ : value);
599
+ const where = key.columns.map((c, i) => `${qi.q(c)} = ${qi.p(i + 1)}`).join(' AND ');
600
+ const result = await exec(`SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)} WHERE ${where}`, params);
601
+ assertOneRowByKey(qi, 'create', key.columns, result.rows.length);
602
+ return result;
235
603
  };
236
604
  }
605
+ /** Stands for the driver's generated insert id in a {@link ReselectKey}. */
606
+ const INSERT_ID = Symbol('turbine.insertId');
607
+ /**
608
+ * Stands for a 0 written to an AUTO_INCREMENT key. MySQL reads 0 there as
609
+ * "assign the next value" (unless the session's sql_mode has
610
+ * NO_AUTO_VALUE_ON_ZERO, which stores 0), so the row is re-read by the value
611
+ * the server reports it stored. Re-reading by 0 found nothing, and the create
612
+ * that had just inserted a row failed as "not found".
613
+ */
614
+ const ZERO_INSERT_ID = Symbol('turbine.zeroInsertId');
615
+ /**
616
+ * The key that finds the row a `create` inserted, decided before the INSERT is
617
+ * sent. Without RETURNING, MySQL reports only the AUTO_INCREMENT value it
618
+ * assigned (`insertId`, 0 when there is none), so a key the server fills any
619
+ * other way (`DEFAULT (UUID())`) cannot be read back: re-reading by 0 matched
620
+ * every CHAR key MySQL converts to the number 0, other tenants' rows included.
621
+ *
622
+ * In order: the primary key, when `data` supplies every member or the one it
623
+ * omits is server-numbered ({@link isServerNumberedKey}); else the first unique
624
+ * key `data` supplies in full. Anything else is refused (E017) before the
625
+ * INSERT, naming the fix. A NULL never identifies a row, so it counts as absent.
626
+ */
627
+ function createReselectKey(qi, data) {
628
+ const supplied = new Map();
629
+ for (const [key, value] of writeEntries(qi, data)) {
630
+ const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
631
+ // Same rewrite the INSERT's own params got, then the text-key rule.
632
+ if (column !== undefined && value !== null) {
633
+ supplied.set(column, reselectKeyValue(qi, column, coerceWriteValue(qi, key, value)));
634
+ }
635
+ }
636
+ const pk = qi.tableMeta.primaryKey;
637
+ const missing = pk.filter((c) => !supplied.has(c));
638
+ if (pk.length > 0 && (missing.length === 0 || (missing.length === 1 && isServerNumberedKey(qi, missing[0])))) {
639
+ return {
640
+ columns: pk,
641
+ values: pk.map((c) => {
642
+ if (!supplied.has(c))
643
+ return INSERT_ID;
644
+ const value = supplied.get(c);
645
+ return isZeroKeyValue(value) && isServerNumberedKey(qi, c) ? ZERO_INSERT_ID : value;
646
+ }),
647
+ };
648
+ }
649
+ for (const key of (0, compound_unique_js_1.uniqueKeyNames)(qi.tableMeta)) {
650
+ if (key.every((c) => supplied.has(c)))
651
+ return { columns: key, values: key.map((c) => supplied.get(c)) };
652
+ }
653
+ const fields = (missing.length > 0 ? missing : pk).map((c) => `"${fieldOf(qi, c)}"`).join(', ');
654
+ throw new errors_js_1.UnsupportedFeatureError(pk.length > 0
655
+ ? `create on "${qi.table}" without ${fields} in data`
656
+ : `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 ' +
657
+ 'AUTO_INCREMENT column. ' +
658
+ (pk.length > 0
659
+ ? `Pass ${fields} in data (for a UUID key, generate it client-side, e.g. crypto.randomUUID()), or make the key AUTO_INCREMENT.`
660
+ : 'Supply every column of a unique key in data, or give the table a primary key.'));
661
+ }
662
+ /**
663
+ * Whether the server numbers this key column itself, so the driver's insert id
664
+ * is its new value. `isGenerated` decides when the metadata records it;
665
+ * metadata that does not (hand-written, or an introspector that leaves it
666
+ * unset) counts an integer column with a default.
667
+ */
668
+ function isServerNumberedKey(qi, column) {
669
+ const col = qi.tableMeta.columns.find((c) => c.name === column);
670
+ if (!col)
671
+ return false;
672
+ if (col.isGenerated !== undefined)
673
+ return col.isGenerated;
674
+ return col.hasDefault && isIntegerColumnType(col.dialectType ?? col.pgType);
675
+ }
676
+ /**
677
+ * Whether a column's database type is an integer type, in the PostgreSQL,
678
+ * MySQL and code-first spellings. A MySQL display width or sign modifier
679
+ * (`int(11) unsigned`) is ignored, except that `tinyint(1)` is MySQL's
680
+ * BOOLEAN and is not counted.
681
+ */
682
+ function isIntegerColumnType(dbType) {
683
+ if (!dbType)
684
+ return false;
685
+ const t = dbType.trim().toLowerCase();
686
+ if (t === 'tinyint(1)')
687
+ return false;
688
+ const base = t
689
+ .replace(/\s+(unsigned|signed|zerofill)\b/g, '')
690
+ .replace(/\s*\(\s*\d+\s*\)$/, '')
691
+ .trim();
692
+ return INTEGER_COLUMN_TYPES.has(base);
693
+ }
694
+ const INTEGER_COLUMN_TYPES = new Set([
695
+ 'tinyint',
696
+ 'smallint',
697
+ 'mediumint',
698
+ 'int',
699
+ 'integer',
700
+ 'bigint',
701
+ 'int2',
702
+ 'int4',
703
+ 'int8',
704
+ 'serial',
705
+ 'bigserial',
706
+ 'smallserial',
707
+ ]);
708
+ function isZeroKeyValue(value) {
709
+ return value === 0 || value === 0n || (typeof value === 'string' && /^0+$/.test(value));
710
+ }
711
+ /**
712
+ * The stored value of a key written as 0 (see {@link ZERO_INSERT_ID}): the
713
+ * insert id the engine reports for the AUTO_INCREMENT column, which is the
714
+ * assigned value, or 0 when the server stored the 0 as written.
715
+ */
716
+ function zeroKey(qi, result) {
717
+ const id = qi.mutationInsertId(result);
718
+ const positive = (typeof id === 'number' && Number.isSafeInteger(id) && id > 0) ||
719
+ (typeof id === 'bigint' && id > 0n) ||
720
+ (typeof id === 'string' && /^[1-9][0-9]*$/.test(id));
721
+ return positive ? id : 0;
722
+ }
723
+ /** The insert id of `result` as a key value, or E017 when the engine reported none. */
724
+ function generatedKey(qi, result, key) {
725
+ const id = qi.mutationInsertId(result);
726
+ const positive = (typeof id === 'number' && Number.isSafeInteger(id) && id > 0) ||
727
+ (typeof id === 'bigint' && id > 0n) ||
728
+ (typeof id === 'string' && /^[1-9][0-9]*$/.test(id));
729
+ if (positive)
730
+ return id;
731
+ const column = key.columns[key.values.indexOf(INSERT_ID)];
732
+ throw new errors_js_1.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 ` +
733
+ `and the insert is rolled back. Pass "${fieldOf(qi, column)}" in data, or make it AUTO_INCREMENT.`);
734
+ }
735
+ /**
736
+ * A key value as a reselect binds it: a number or bigint for a text-class
737
+ * column ({@link whereMod.TEXT_COMPARED_TYPES}) is sent as the text the INSERT
738
+ * stored, since MySQL compares `'0zz' = 0` as true. Converted on every engine,
739
+ * not only MySQL, because one caller (the identity read) is not MySQL-only.
740
+ */
741
+ function reselectKeyValue(qi, column, value) {
742
+ if (typeof value !== 'number' && typeof value !== 'bigint')
743
+ return value;
744
+ const type = qi.columnPgTypeMap.get(column);
745
+ return type !== undefined && whereMod.TEXT_COMPARED_TYPES.has(type.toLowerCase()) ? String(value) : value;
746
+ }
747
+ /**
748
+ * A re-read by a unique key finds one row. A second one means the engine
749
+ * matched the key loosely, so which row is the written one is unknown: refused
750
+ * (E017), which rolls the write back.
751
+ */
752
+ function assertOneRowByKey(qi, operation, columns, rows) {
753
+ if (rows <= 1)
754
+ return;
755
+ throw new errors_js_1.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 ` +
756
+ 'written one cannot be told apart and the write is rolled back. Pass each key value with its column type.');
757
+ }
237
758
  /**
238
759
  * The `[key, value]` pairs a write's `data` object actually names, in the
239
760
  * table's own column order.
@@ -321,14 +842,81 @@ function assertUniformCreateManyRows(qi, rows, firstKeys) {
321
842
  function quoteList(fields) {
322
843
  return fields.map((f) => `"${f}"`).join(', ');
323
844
  }
324
- function buildCreateMany(qi, args) {
845
+ /**
846
+ * Refuse a statement that carries more bind parameters than the engine takes.
847
+ * Only the row-major `VALUES` form can get there (PostgreSQL's UNNEST form
848
+ * binds one array per column), and past the limit the failure is a raw driver
849
+ * or protocol error. Executing in chunks would change what one `createMany`
850
+ * is (one statement, one RETURNING set), so the caller chooses the chunking.
851
+ */
852
+ function assertWithinBindLimit(qi, paramCount, rowCount, columnCount) {
853
+ const limit = (0, dialect_js_1.maxBindParameters)(qi.dialect);
854
+ if (limit === undefined || paramCount <= limit)
855
+ return;
856
+ const perChunk = Math.max(1, Math.floor(limit / Math.max(1, columnCount)));
857
+ throw new errors_js_1.ValidationError(`createMany on "${qi.table}": ${rowCount} rows of ${columnCount} columns need ${paramCount} bind parameters, ` +
858
+ `over the ${qi.dialect.name} limit of ${limit} per statement. ` +
859
+ `Split the rows into chunks of at most ${perChunk} and call createMany once per chunk ` +
860
+ '(inside one `$transaction` if they must succeed or fail together).');
861
+ }
862
+ function buildCreateMany(qi, args, overBindLimit = 'refuse', projection) {
863
+ const prepared = prepareCreateMany(qi, args, projection);
864
+ if ('deferred' in prepared)
865
+ return prepared.deferred;
866
+ return emitCreateMany(qi, prepared, prepared.rowValues, overBindLimit, true);
867
+ }
868
+ /**
869
+ * `createMany()` as the statements it runs: one when the rows fit one
870
+ * statement, else consecutive chunks that each do. The whole call is validated
871
+ * once, before any chunk is built, so a refusal (a duplicate conflict key in
872
+ * the last chunk included) sends nothing, and the chunks are sized before any
873
+ * statement is built, so no engine's own per-statement check refuses one.
874
+ *
875
+ * Built eagerly and synchronously: the chunks read state the call set on the
876
+ * builder (`skipGlobalFilters`), which a concurrent call could change across
877
+ * an await.
878
+ */
879
+ function planCreateMany(qi, args, projection) {
880
+ const prepared = prepareCreateMany(qi, args, projection);
881
+ if ('deferred' in prepared)
882
+ return { chunks: [prepared.deferred], rowsPerChunk: args.data.length };
883
+ const rows = prepared.rowValues;
884
+ const size = rowsPerStatement(qi, prepared);
885
+ if (rows.length <= size) {
886
+ return { chunks: [emitCreateMany(qi, prepared, rows, 'allow', true)], rowsPerChunk: size };
887
+ }
888
+ if (prepared.conflict && !prepared.identity) {
889
+ throw new errors_js_1.ValidationError(`createMany onConflict on "${qi.table}": ${rows.length} rows take more than one statement here (at most ` +
890
+ `${size} per statement), and the rows each statement writes cannot be identified (onConflict.update assigns ` +
891
+ "the key, or the only key is a PII column), so two rows equal under the key's comparison rules could merge " +
892
+ `across statements unnoticed. Pass at most ${size} rows per call.`);
893
+ }
894
+ const chunks = [];
895
+ for (let i = 0; i < rows.length; i += size) {
896
+ chunks.push(emitCreateMany(qi, prepared, rows.slice(i, i + size), 'refuse', i === 0));
897
+ }
898
+ return { chunks, rowsPerChunk: size };
899
+ }
900
+ /**
901
+ * Everything `createMany` checks about the call, over every row, or the whole
902
+ * answer when no row-values statement is needed (no rows, or rows of pure
903
+ * defaults).
904
+ */
905
+ function prepareCreateMany(qi, args, projection) {
325
906
  const qt = qi.q(qi.table);
907
+ // Validated on every call (UNSAFE only); it acts on onConflict's update.
908
+ qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
909
+ // The data-independent half of onConflict is checked before the empty-data
910
+ // shortcut, so a call is never valid with no rows and invalid with some.
911
+ const conflict = args.onConflict === undefined ? null : resolveConflictPlan(qi, args);
326
912
  if (args.data.length === 0) {
327
913
  return {
328
- sql: `SELECT * FROM ${qt} WHERE false`,
329
- params: [],
330
- transform: () => [],
331
- tag: `${qi.table}.createMany`,
914
+ deferred: {
915
+ sql: `SELECT * FROM ${qt} WHERE false`,
916
+ params: [],
917
+ transform: () => [],
918
+ tag: `${qi.table}.createMany`,
919
+ },
332
920
  };
333
921
  }
334
922
  assertWritable(qi, 'createMany');
@@ -337,22 +925,35 @@ function buildCreateMany(qi, args) {
337
925
  }
338
926
  const keys = definedKeys(qi, args.data[0]);
339
927
  assertUniformCreateManyRows(qi, args.data, keys);
928
+ const columns = keys.map((k) => qi.toColumn(k));
929
+ if (conflict)
930
+ assertConflictTargetSupplied(qi, conflict, columns);
340
931
  // No column named by the first row: every row is pure defaults (the bulk
341
932
  // counterpart of `create({ data: {} })`, see buildDefaultValuesInsert). The
342
933
  // uniformity check above has already established that EVERY row is empty.
343
934
  if (keys.length === 0) {
344
935
  return {
345
- sql: buildDefaultValuesInsert(qi, args.data.length, args.skipDuplicates),
346
- params: [],
347
- transform: (result) => result.rows.map((row) => parseWriteRow(qi, row)),
348
- tag: `${qi.table}.createMany`,
936
+ deferred: {
937
+ sql: buildDefaultValuesInsert(qi, args.data.length, args.skipDuplicates, projection),
938
+ params: [],
939
+ transform: (result) => result.rows.map((row) => parseWriteRow(qi, row, projection)),
940
+ tag: `${qi.table}.createMany`,
941
+ },
349
942
  };
350
943
  }
351
- const columns = keys.map((k) => qi.toColumn(k));
352
944
  const rowValues = args.data.map((row) => {
353
945
  const record = row;
354
946
  return keys.map((key) => coerceWriteValue(qi, key, record[key]));
355
947
  });
948
+ let identity = null;
949
+ if (conflict) {
950
+ // Over every row, so a chunked call refuses before its first chunk is sent.
951
+ assertNoDuplicateKeys(qi, conflict, columns, rowValues);
952
+ if (qi.dialect.capabilities.upsertConflictTarget === 'any-unique-key') {
953
+ assertMysqlBulkUpsert(qi, conflict, columns, rowValues);
954
+ }
955
+ identity = conflictIdentity(qi, conflict, projection);
956
+ }
356
957
  // Use actual Postgres types for array casts in the default PostgreSQL dialect.
357
958
  // Enum columns cast to `"EnumName"[]`, the generic text[] fallback would
358
959
  // type the UNNEST output as text, which Postgres refuses to coerce to the
@@ -373,21 +974,450 @@ function buildCreateMany(qi, args) {
373
974
  // directly. Detected from the column's own declared type rather than from
374
975
  // the shape of the first row's value, so a row whose array column happens to
375
976
  // be null or absent still takes the correct form.
977
+ //
978
+ // json/jsonb columns stay on UNNEST. coerceWriteValue has already turned
979
+ // every JS array bound to one into JSON TEXT, so the transpose has no
980
+ // nested array to flatten (a raw `[1, 2, 3]` there used to become three
981
+ // rows), and every other cell is a string, object or null that the driver
982
+ // serializes as one array element.
376
983
  const hasArrayColumn = columns.some((col) => (qi.columnPgTypeMap.get(col) ?? '').startsWith('_'));
377
- const built = qi.dialect.buildBulkInsertStatement({
378
- table: qt,
379
- columns: quotedColumns,
984
+ return {
985
+ conflict,
986
+ identity,
987
+ columns,
988
+ insert: {
989
+ table: qt,
990
+ columns: quotedColumns,
991
+ columnArrayTypes: typeCasts,
992
+ skipDuplicates: args.skipDuplicates,
993
+ returning: writeReturningColumns(qi, identity?.selection ?? projection),
994
+ requireRowValues: hasArrayColumn,
995
+ },
380
996
  rowValues,
381
- columnArrayTypes: typeCasts,
382
- skipDuplicates: args.skipDuplicates,
383
- returning: writeReturningColumns(qi),
384
- requireRowValues: hasArrayColumn,
997
+ projection,
998
+ };
999
+ }
1000
+ /**
1001
+ * One `createMany` statement over `rowValues` (all of a prepared call's rows,
1002
+ * or a chunk of them). `first` marks the call's first statement, where an
1003
+ * onConflict call starts counting the rows it wrote.
1004
+ */
1005
+ function emitCreateMany(qi, prepared, rowValues, overBindLimit, first) {
1006
+ const { conflict, columns, projection } = prepared;
1007
+ const insert = { ...prepared.insert, rowValues };
1008
+ if (conflict) {
1009
+ return buildBulkUpsert(qi, conflict, insert, columns, overBindLimit, projection, prepared.identity, first);
1010
+ }
1011
+ const built = qi.dialect.buildBulkInsertStatement(insert);
1012
+ if (overBindLimit === 'refuse')
1013
+ assertWithinBindLimit(qi, built.params.length, rowValues.length, columns.length);
1014
+ return {
1015
+ sql: built.sql,
1016
+ params: built.params,
1017
+ transform: (result) => result.rows.map((row) => parseWriteRow(qi, row, projection)),
1018
+ tag: `${qi.table}.createMany`,
1019
+ };
1020
+ }
1021
+ /**
1022
+ * Rows one `createMany` statement may carry: the most that keep its bind
1023
+ * parameters within the engine's limit and, on SQL Server, its rows within
1024
+ * the 1000 a plain `INSERT ... VALUES` accepts. The parameter cost is measured
1025
+ * from one-row and two-row builds, which separates the per-row parameters from
1026
+ * the ones bound once per statement (an onConflict update's values); the
1027
+ * PostgreSQL UNNEST form binds one array per column, so its rows never run out.
1028
+ */
1029
+ function rowsPerStatement(qi, prepared) {
1030
+ // An onConflict statement (SQL Server's MERGE) reads its rows as a derived
1031
+ // table, which the plain INSERT ... VALUES row cap does not bind.
1032
+ const cap = prepared.conflict ? Infinity : qi.dialect.capabilities.maxInsertRows;
1033
+ const limit = (0, dialect_js_1.maxBindParameters)(qi.dialect);
1034
+ const rows = prepared.rowValues;
1035
+ if (limit === undefined || rows.length < 2)
1036
+ return cap;
1037
+ const one = emitCreateMany(qi, prepared, rows.slice(0, 1), 'allow', true).params.length;
1038
+ const perRow = emitCreateMany(qi, prepared, rows.slice(0, 2), 'allow', true).params.length - one;
1039
+ if (perRow <= 0)
1040
+ return cap;
1041
+ const fixed = Math.max(0, one - perRow);
1042
+ return Math.max(1, Math.min(cap, Math.floor((limit - fixed) / perRow)));
1043
+ }
1044
+ const ON_CONFLICT_KEYS = new Set(['target', 'update', 'where', 'ignore']);
1045
+ /**
1046
+ * Validate the data-independent half of `onConflict`: the engine supports it,
1047
+ * its shape, its target names exactly one unique key, and its update and
1048
+ * where are usable. Every refusal is E003 except an engine gap (E017).
1049
+ */
1050
+ function resolveConflictPlan(qi, args) {
1051
+ const table = qi.table;
1052
+ if (!qi.dialect.supportsBulkUpsert || typeof qi.dialect.buildBulkUpsertStatement !== 'function') {
1053
+ throw new errors_js_1.UnsupportedFeatureError('createMany onConflict', qi.dialect.name, 'This engine has no multi-row upsert statement. Call upsert() per row inside $transaction.');
1054
+ }
1055
+ if (args.skipDuplicates) {
1056
+ throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict and skipDuplicates cannot be combined. skipDuplicates skips a ` +
1057
+ 'conflict on ANY unique key, onConflict names one; to skip conflicts on that key, use ' +
1058
+ 'onConflict: { target, ignore: true }.');
1059
+ }
1060
+ const oc = args.onConflict;
1061
+ if (!(0, filters_js_1.isPlainRecord)(oc)) {
1062
+ throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict must be { target, update } or { target, ignore: true }.`);
1063
+ }
1064
+ const unknownKey = Object.keys(oc).find((k) => !ON_CONFLICT_KEYS.has(k) && oc[k] !== undefined);
1065
+ if (unknownKey !== undefined) {
1066
+ throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict has no option "${unknownKey}". It takes target, update, where and ignore.`);
1067
+ }
1068
+ const target = resolveConflictTarget(qi, oc.target);
1069
+ if (oc.ignore !== undefined && oc.ignore !== true) {
1070
+ throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict.ignore takes \`true\` only.`);
1071
+ }
1072
+ if (oc.ignore === true) {
1073
+ if (oc.update !== undefined || oc.where !== undefined) {
1074
+ throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict takes either ignore: true or update (with an optional where), not both.`);
1075
+ }
1076
+ return { target, update: null, where: null };
1077
+ }
1078
+ return { target, update: resolveConflictUpdate(qi, oc.update), where: resolveConflictWhere(qi, oc.where) };
1079
+ }
1080
+ function resolveConflictTarget(qi, raw) {
1081
+ const table = qi.table;
1082
+ if (!Array.isArray(raw) || raw.length === 0 || raw.some((f) => typeof f !== 'string')) {
1083
+ throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict.target must be a non-empty array of field names (the primary key or ` +
1084
+ `one unique key), e.g. target: ['email']. Valid targets: ${describeConflictTargets(qi)}.`);
1085
+ }
1086
+ const columns = [];
1087
+ for (const field of raw) {
1088
+ const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, field);
1089
+ if (column === undefined) {
1090
+ throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict.target names "${field}", which is not a column. ` +
1091
+ `Valid targets: ${describeConflictTargets(qi)}.`);
1092
+ }
1093
+ if (columns.includes(column)) {
1094
+ throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict.target names "${field}" twice.`);
1095
+ }
1096
+ columns.push(column);
1097
+ }
1098
+ const wanted = new Set(columns);
1099
+ const sameSet = (set) => set.length === wanted.size && set.every((c) => wanted.has(c));
1100
+ if ((0, compound_unique_js_1.uniqueKeyNames)(qi.tableMeta).some(sameSet))
1101
+ return columns;
1102
+ const partial = qi.tableMeta.indexes.some((idx) => idx.unique && idx.partial && sameSet(idx.columns));
1103
+ throw new errors_js_1.ValidationError(`createMany on "${table}": onConflict.target (${columns.map((c) => fieldOf(qi, c)).join(', ')}) ` +
1104
+ (partial
1105
+ ? 'is a PARTIAL unique index, which a conflict clause can only use by restating its predicate. '
1106
+ : 'is not the primary key or a unique key of the table, as the schema metadata knows it. ') +
1107
+ `Valid targets: ${describeConflictTargets(qi)}.`);
1108
+ }
1109
+ function fieldOf(qi, column) {
1110
+ return qi.tableMeta.reverseColumnMap[column] ?? column;
1111
+ }
1112
+ function describeConflictTargets(qi) {
1113
+ const keys = (0, compound_unique_js_1.uniqueKeyNames)(qi.tableMeta);
1114
+ if (keys.length === 0)
1115
+ return '(none: the table has no primary key or unique key in its metadata)';
1116
+ return keys.map((cols) => `[${cols.map((c) => `'${fieldOf(qi, c)}'`).join(', ')}]`).join(', ');
1117
+ }
1118
+ /** The conflict-UPDATE's data: the field-list shorthand becomes `{ f: expr.excluded(f) }`. */
1119
+ function resolveConflictUpdate(qi, raw) {
1120
+ const empty = new errors_js_1.ValidationError(`createMany on "${qi.table}": onConflict.update is empty; use onConflict: { target, ignore: true } to skip ` +
1121
+ 'conflicting rows.');
1122
+ if (Array.isArray(raw)) {
1123
+ if (raw.length === 0)
1124
+ throw empty;
1125
+ const update = {};
1126
+ for (const field of raw) {
1127
+ if (typeof field !== 'string') {
1128
+ throw new errors_js_1.ValidationError(`createMany on "${qi.table}": onConflict.update as a list takes field names, got ${typeof field}.`);
1129
+ }
1130
+ if (Object.hasOwn(update, field)) {
1131
+ throw new errors_js_1.ValidationError(`createMany on "${qi.table}": onConflict.update names "${field}" twice.`);
1132
+ }
1133
+ update[field] = expr_js_1.expr.excluded(field);
1134
+ }
1135
+ return update;
1136
+ }
1137
+ if (!(0, filters_js_1.isPlainRecord)(raw)) {
1138
+ throw new errors_js_1.ValidationError(`createMany on "${qi.table}": onConflict.update takes a list of field names or a { field: value } object.`);
1139
+ }
1140
+ if (!Object.values(raw).some((v) => v !== undefined))
1141
+ throw empty;
1142
+ assertNoGeneratedColumns(qi, raw, 'createMany');
1143
+ return raw;
1144
+ }
1145
+ /**
1146
+ * `onConflict.where`, with `undefined` keys pruned as in every `where`. One
1147
+ * whose every key was `undefined` is refused: dropping it would update the
1148
+ * rows it was written to protect.
1149
+ */
1150
+ function resolveConflictWhere(qi, raw) {
1151
+ if (raw === undefined)
1152
+ return null;
1153
+ if (!(0, filters_js_1.isPlainRecord)(raw)) {
1154
+ throw new errors_js_1.ValidationError(`createMany on "${qi.table}": onConflict.where must be a where object.`);
1155
+ }
1156
+ const pruned = (0, normalize_args_js_1.pruneUndefinedArgs)(qi.schema, qi.table, { where: raw }).where;
1157
+ if (Object.keys(pruned).length === 0) {
1158
+ if (Object.keys(raw).length > 0) {
1159
+ throw new errors_js_1.ValidationError(`createMany on "${qi.table}": every key in onConflict.where was undefined, so it would restrict nothing. ` +
1160
+ 'Check the values you meant to filter on, or leave where out to update every conflicting row.');
1161
+ }
1162
+ return null;
1163
+ }
1164
+ return pruned;
1165
+ }
1166
+ /** Every row must supply every target column: a row without its key cannot conflict on it. */
1167
+ function assertConflictTargetSupplied(qi, conflict, columns) {
1168
+ const missing = conflict.target.filter((c) => !columns.includes(c));
1169
+ if (missing.length === 0)
1170
+ return;
1171
+ throw new errors_js_1.ValidationError(`createMany onConflict on "${qi.table}": the target field ${missing.map((c) => `"${fieldOf(qi, c)}"`).join(', ')} ` +
1172
+ 'is missing from the rows. Every row must supply every target field; a key the database generates ' +
1173
+ '(an identity or serial id it has not assigned yet) cannot be a conflict target.');
1174
+ }
1175
+ /** A byte-equality token for one coerced key value. */
1176
+ function keyToken(value) {
1177
+ let s;
1178
+ if (value instanceof Date)
1179
+ s = value.toISOString();
1180
+ else if (value instanceof Uint8Array)
1181
+ s = Array.from(value, (b) => b.toString(16).padStart(2, '0')).join('');
1182
+ else
1183
+ s = String(value);
1184
+ return `${s.length}:${s}`;
1185
+ }
1186
+ /**
1187
+ * Refuse two rows of one call that share a conflict key, before any SQL. The
1188
+ * engines disagree on it (PostgreSQL raises 21000, SQL Server's MERGE 8672,
1189
+ * SQLite and MySQL apply the rows in sequence), and chunking at the bind
1190
+ * limit would turn the error into "the second chunk updates the first chunk's
1191
+ * row". Byte-equal on the coerced values, so a case-insensitive collation can
1192
+ * still let two rows through. A row with NULL in the key never conflicts.
1193
+ */
1194
+ function assertNoDuplicateKeys(qi, conflict, columns, rowValues) {
1195
+ const at = conflict.target.map((c) => columns.indexOf(c));
1196
+ const seen = new Map();
1197
+ for (let i = 0; i < rowValues.length; i++) {
1198
+ const row = rowValues[i];
1199
+ const parts = at.map((j) => row[j]);
1200
+ if (parts.some((v) => v === null || v === undefined))
1201
+ continue;
1202
+ const key = parts.map(keyToken).join('|');
1203
+ const first = seen.get(key);
1204
+ if (first !== undefined) {
1205
+ throw new errors_js_1.ValidationError(`createMany onConflict on "${qi.table}": rows ${first} and ${i} share the conflict key ` +
1206
+ `(${conflict.target.map((c) => fieldOf(qi, c)).join(', ')}). One statement cannot insert or update one ` +
1207
+ 'key twice; merge those rows first.');
1208
+ }
1209
+ seen.set(key, i);
1210
+ }
1211
+ }
1212
+ /**
1213
+ * The identity for an onConflict call: the conflict target (the key that
1214
+ * collides) or the primary key, preferring one the caller's projection
1215
+ * returns. `null` when the written rows cannot be told
1216
+ * apart: each candidate is assigned by `onConflict.update` (so a row's key can
1217
+ * change between statements) or is PII, which a write never returns.
1218
+ */
1219
+ function conflictIdentity(qi, conflict, projection) {
1220
+ const assigned = conflict.update ? namedColumns(qi.tableMeta, conflict.update) : new Set();
1221
+ const pii = piiColumns(qi, qi.tableMeta);
1222
+ const usable = [conflict.target, qi.tableMeta.primaryKey].filter((key) => key.length > 0 && key.every((c) => !assigned.has(c) && !pii.has(c)));
1223
+ // One the caller's projection already returns, so the statement is unchanged.
1224
+ const columns = usable.find((key) => !projection || key.every((c) => projection.columns.includes(c))) ?? usable[0];
1225
+ if (!columns)
1226
+ return null;
1227
+ const missing = projection ? columns.filter((c) => !projection.columns.includes(c)) : [];
1228
+ if (missing.length === 0)
1229
+ return { columns, selection: projection, added: [], seen: new Set() };
1230
+ const wanted = new Set([...(projection?.columns ?? []), ...missing]);
1231
+ return {
1232
+ columns,
1233
+ selection: { columns: qi.tableMeta.allColumns.filter((c) => wanted.has(c)) },
1234
+ added: missing.map((c) => fieldOf(qi, c)),
1235
+ seen: new Set(),
1236
+ };
1237
+ }
1238
+ /** Refuse a table row that one onConflict call wrote twice (see {@link ConflictIdentity}). */
1239
+ function assertEachRowWrittenOnce(qi, conflict, identity, rows, first) {
1240
+ if (first)
1241
+ identity.seen.clear();
1242
+ for (const row of rows) {
1243
+ const values = identity.columns.map((c) => row[c]);
1244
+ // Every engine returns the identity columns (see conflictIdentity); a row
1245
+ // without them cannot be compared, so it is not counted.
1246
+ if (values.some((v) => v === undefined))
1247
+ continue;
1248
+ const token = values.map(keyToken).join('|');
1249
+ if (identity.seen.has(token))
1250
+ throw conflictKeySharedError(qi, conflict.target);
1251
+ identity.seen.add(token);
1252
+ }
1253
+ }
1254
+ /**
1255
+ * The E003 for two rows of one `createMany({ onConflict })` that share a key
1256
+ * only under the column's comparison rules (a case-insensitive collation, a
1257
+ * `citext` column), which the byte-equality check above cannot see.
1258
+ */
1259
+ function conflictKeySharedError(qi, target, cause) {
1260
+ return new errors_js_1.ValidationError(`createMany onConflict on "${qi.table}": two rows in this call share the conflict key ` +
1261
+ `(${target.map((c) => fieldOf(qi, c)).join(', ')}) under the column's comparison rules (a case-insensitive ` +
1262
+ 'collation, for example), so the call would insert or update one row twice. Merge those rows first.', cause === undefined ? undefined : { cause });
1263
+ }
1264
+ /**
1265
+ * `err` from running a `createMany({ onConflict })`, with the engine's
1266
+ * "one statement touched one row twice" errors (PostgreSQL 21000, SQL Server
1267
+ * 8672) turned into the E003 that names the cause. Anything else is returned
1268
+ * as it was.
1269
+ */
1270
+ function explainConflictKeyCollision(qi, args, err) {
1271
+ if (args.onConflict === undefined || !touchedRowTwice(err))
1272
+ return err;
1273
+ const raw = args.onConflict.target;
1274
+ const target = Array.isArray(raw)
1275
+ ? raw.map((f) => (typeof f === 'string' ? ((0, utils_js_1.resolveColumnName)(qi.tableMeta, f) ?? f) : String(f)))
1276
+ : [];
1277
+ return conflictKeySharedError(qi, target, err);
1278
+ }
1279
+ /** PostgreSQL 21000 or SQL Server 8672 on `err` or a cause under it (Turbine wraps or scrubs driver errors). */
1280
+ function touchedRowTwice(err) {
1281
+ let e = err;
1282
+ for (let depth = 0; depth < 4 && e && typeof e === 'object'; depth++) {
1283
+ if (e.code === '21000' || e.sqlstate === '21000' || e.number === 8672)
1284
+ return true;
1285
+ e = e.cause;
1286
+ }
1287
+ return false;
1288
+ }
1289
+ /**
1290
+ * MySQL's ON DUPLICATE KEY UPDATE has no conflict target: it fires on
1291
+ * WHATEVER unique key a row violates, and with several matching keys it
1292
+ * updates one row. So the target must be the only key a row can violate, and
1293
+ * the shapes that need a predicate slot or a per-row outcome are refused.
1294
+ */
1295
+ function assertMysqlBulkUpsert(qi, conflict, columns, rowValues) {
1296
+ const fix = (why, hint) => new errors_js_1.UnsupportedFeatureError(`createMany onConflict on "${qi.table}" ${why}`, qi.dialect.name, hint);
1297
+ if (conflict.update === null) {
1298
+ throw fix('with ignore: true', 'MySQL cannot tell the inserted rows from the skipped ones afterwards. Use onConflict: { target, update }.');
1299
+ }
1300
+ if (conflict.where !== null) {
1301
+ throw fix('with a where', 'ON DUPLICATE KEY UPDATE has no predicate slot.');
1302
+ }
1303
+ const gf = whereMod.resolveGlobalFilter(qi, qi.table);
1304
+ if (gf && whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, qi.q(qi.table), gf, [])) {
1305
+ 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() " +
1306
+ 'per row, or pass skipGlobalFilters: UNSAFE.');
1307
+ }
1308
+ // The single-row upsert's rule (otherCollidingUniqueKey), on column presence
1309
+ // so the verdict does not depend on the rows' values.
1310
+ const present = new Set(columns);
1311
+ const other = (0, compound_unique_js_1.otherCollidingUniqueKey)(qi.tableMeta, [conflict.target], (c) => (present.has(c) ? 'value' : 'absent'));
1312
+ if (other) {
1313
+ 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 ` +
1314
+ "collide on, and MySQL's ON DUPLICATE KEY UPDATE cannot be limited to one key, so it could update a row the " +
1315
+ 'target does not name. Call upsert() per row (it looks each row up first), or leave that key out of data.');
1316
+ }
1317
+ const at = conflict.target.map((c) => columns.indexOf(c));
1318
+ if (rowValues.some((row) => at.some((j) => row[j] === null || row[j] === undefined))) {
1319
+ 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 ' +
1320
+ 'target value.');
1321
+ }
1322
+ const numbered = at.filter((j) => isServerNumberedKey(qi, columns[j]));
1323
+ if (numbered.length > 0 && rowValues.some((row) => numbered.some((j) => isZeroKeyValue(row[j])))) {
1324
+ const field = fieldOf(qi, columns[numbered[0]]);
1325
+ 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 ' +
1326
+ `by its key. Pass the row's real "${field}", or create() the rows that need a new one.`);
1327
+ }
1328
+ }
1329
+ /** The SELECT that re-reads a MySQL multi-row upsert's rows by their conflict keys. */
1330
+ function buildKeyReselect(qi, conflict, columns, rowValues, projection) {
1331
+ const at = conflict.target.map((c) => columns.indexOf(c));
1332
+ const params = [];
1333
+ const tuples = rowValues.map((row) => {
1334
+ const refs = at.map((j, k) => {
1335
+ params.push(reselectKeyValue(qi, conflict.target[k], row[j]));
1336
+ return qi.p(params.length);
1337
+ });
1338
+ return refs.length === 1 ? refs[0] : `(${refs.join(', ')})`;
385
1339
  });
1340
+ const keyCols = conflict.target.map((c) => qi.q(c));
1341
+ const lhs = keyCols.length === 1 ? keyCols[0] : `(${keyCols.join(', ')})`;
1342
+ return {
1343
+ sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)} WHERE ${lhs} IN (${tuples.join(', ')})`,
1344
+ params,
1345
+ };
1346
+ }
1347
+ function buildBulkUpsert(qi, conflict, insert, columns, overBindLimit, projection, identity, first) {
1348
+ // The duplicate-key and MySQL checks ran over every row in prepareCreateMany.
1349
+ const rowValues = insert.rowValues;
1350
+ // `updatedAt` columns are stamped on the conflict-UPDATE as update() stamps
1351
+ // them, unless the update names the column.
1352
+ const updateData = conflict.update ? applyUpdatedAtColumns(qi, conflict.update) : null;
1353
+ const insertColumns = new Set(columns);
1354
+ const gf = whereMod.resolveGlobalFilter(qi, qi.table);
1355
+ const built = qi.dialect.buildBulkUpsertStatement({
1356
+ ...insert,
1357
+ skipDuplicates: false,
1358
+ conflictColumns: conflict.target.map((c) => qi.q(c)),
1359
+ update: updateData
1360
+ ? (params, refs) => {
1361
+ const setClauses = buildSetClauses(qi, writeEntries(qi, updateData), params, {
1362
+ operation: 'createMany',
1363
+ target: refs.target,
1364
+ incoming: refs.incoming,
1365
+ insertColumns,
1366
+ });
1367
+ // The caller's predicate and the global filter both read the EXISTING
1368
+ // row, so a row they exclude is neither updated nor returned (and,
1369
+ // its key being taken, not inserted either). Inserts are not filtered,
1370
+ // as create() is not.
1371
+ const predicates = [];
1372
+ for (const where of [conflict.where, gf]) {
1373
+ if (!where)
1374
+ continue;
1375
+ const sql = whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, refs.target, where, params);
1376
+ if (sql)
1377
+ predicates.push(sql);
1378
+ }
1379
+ return {
1380
+ setClauses,
1381
+ where: predicates.length <= 1 ? (predicates[0] ?? null) : predicates.map((p) => `(${p})`).join(' AND '),
1382
+ };
1383
+ }
1384
+ : null,
1385
+ });
1386
+ if (overBindLimit === 'refuse') {
1387
+ assertWithinBindLimit(qi, built.params.length, rowValues.length, columns.length);
1388
+ }
386
1389
  return {
387
1390
  sql: built.sql,
388
1391
  params: built.params,
389
- transform: (result) => result.rows.map((row) => parseWriteRow(qi, row)),
1392
+ // The rows written: inserted or updated. A row `ignore` skipped, or one the
1393
+ // predicate or a global filter excluded, is not in the result, and the
1394
+ // order is the engine's, not the input's.
1395
+ transform: (result) => {
1396
+ if (identity)
1397
+ assertEachRowWrittenOnce(qi, conflict, identity, result.rows, first);
1398
+ const rows = result.rows.map((row) => parseWriteRow(qi, row, projection));
1399
+ if (identity && identity.added.length > 0) {
1400
+ for (const row of rows)
1401
+ for (const field of identity.added)
1402
+ delete row[field];
1403
+ }
1404
+ return rows;
1405
+ },
390
1406
  tag: `${qi.table}.createMany`,
1407
+ reselect: qi.dialect.resultStrategy === 'reselect'
1408
+ ? async (exec) => {
1409
+ await exec(built.sql, built.params);
1410
+ const sel = buildKeyReselect(qi, conflict, columns, rowValues, identity?.selection ?? projection);
1411
+ const result = await exec(sel.sql, sel.params);
1412
+ // Every row was inserted or updated and byte-equal keys were
1413
+ // refused up front, so fewer rows back means two rows' keys are
1414
+ // equal under the column's collation and MySQL applied the second
1415
+ // as an update of the first. Refused, which rolls the call back.
1416
+ if (result.rows.length < rowValues.length)
1417
+ throw conflictKeySharedError(qi, conflict.target);
1418
+ return result;
1419
+ }
1420
+ : undefined,
391
1421
  };
392
1422
  }
393
1423
  function buildUpdate(qi, args, projection) {
@@ -453,8 +1483,7 @@ function buildUpdate(qi, args, projection) {
453
1483
  : `u:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}${writeProjectionCacheSegment(projection)}`;
454
1484
  const params = [];
455
1485
  const buildSql = (freshParams) => {
456
- const setEntries = writeEntries(qi, dataObj);
457
- const setClauses = setEntries.map(([k, v]) => buildSetClause(qi, k, v, freshParams));
1486
+ const setClauses = buildSetClauses(qi, writeEntries(qi, dataObj), freshParams);
458
1487
  if (lock) {
459
1488
  const versionCol = qi.toSqlColumn(lock.field);
460
1489
  setClauses.push(`${versionCol} = ${versionCol} + 1`);
@@ -523,6 +1552,18 @@ function buildUpdate(qi, args, projection) {
523
1552
  // Non-RETURNING engines: UPDATE, then re-fetch the row by the same where.
524
1553
  reselect: qi.dialect.resultStrategy === 'reselect'
525
1554
  ? async (exec) => {
1555
+ // Runs in one transaction (executeMutation), savepointed inside a
1556
+ // caller's. When the write may move the row out of `where` (it sets
1557
+ // a column `where` reads, the version column included), the row is
1558
+ // locked and re-read by its key, since `where` no longer finds it.
1559
+ const written = namedColumns(qi.tableMeta, dataObj);
1560
+ if (lock)
1561
+ written.add(qi.toColumn(lock.field));
1562
+ const key = writeMayMoveRow(qi, whereObj, written)
1563
+ ? await lockMovedRowKey(qi, exec, whereObj, dataObj, 'update', allowFullTableScan)
1564
+ : undefined;
1565
+ if (key === null)
1566
+ return { rows: [], rowCount: 0 };
526
1567
  const writeResult = await exec(sql, params, preparedName);
527
1568
  // Optimistic-lock conflict: the version-checked UPDATE matched no
528
1569
  // row. The re-fetch below uses `where` WITHOUT the version
@@ -536,14 +1577,21 @@ function buildUpdate(qi, args, projection) {
536
1577
  expectedVersion: lock.expected,
537
1578
  });
538
1579
  }
539
- const sel = buildReselectByWhere(qi, whereObj, projection);
540
- return exec(sel.sql, sel.params);
1580
+ const sel = buildReselectByWhere(qi, key ?? whereObj, projection);
1581
+ const result = await exec(sel.sql, sel.params);
1582
+ // `update` writes one row; more than one here means the engine
1583
+ // matched the where loosely, and the write is rolled back.
1584
+ if (key || !allowFullTableScan) {
1585
+ assertOneRowByKey(qi, 'update', key ? identityColumns(qi) : whereColumns(qi, userWhere), result.rows.length);
1586
+ }
1587
+ return result;
541
1588
  }
542
1589
  : undefined,
543
1590
  };
544
1591
  }
545
1592
  function buildDelete(qi, args, projection) {
546
1593
  assertWritable(qi, 'delete');
1594
+ assertNoOptimisticLock(qi, args, 'delete');
547
1595
  qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
548
1596
  // Prisma compound-unique selector → the column conjunction (before the guard).
549
1597
  const userWhere = (0, compound_unique_js_1.expandCompoundUniqueWhere)(qi.tableMeta, args.where);
@@ -592,21 +1640,72 @@ function buildDelete(qi, args, projection) {
592
1640
  // by the same where, then run the DELETE, returning the captured row.
593
1641
  reselect: qi.dialect.resultStrategy === 'reselect'
594
1642
  ? async (exec) => {
595
- const sel = buildReselectByWhere(qi, whereObj, projection);
1643
+ // In one transaction (executeMutation): the row is locked while it
1644
+ // is read, and returned only if THIS delete removed it, so of two
1645
+ // concurrent deletes of one row exactly one reports it.
1646
+ const sel = buildReselectByWhere(qi, whereObj, projection, true);
596
1647
  const pre = await exec(sel.sql, sel.params);
597
- await exec(entry.sql, params, entry.name);
598
- return pre;
1648
+ // `delete` removes one row; refused before the DELETE when the
1649
+ // engine matched the where loosely.
1650
+ if (!allowFullTableScan)
1651
+ assertOneRowByKey(qi, 'delete', whereColumns(qi, userWhere), pre.rows.length);
1652
+ const deleted = await exec(entry.sql, params, entry.name);
1653
+ return (deleted.rowCount ?? 0) === 0 ? { rows: [], rowCount: 0 } : pre;
599
1654
  }
600
1655
  : undefined,
601
1656
  };
602
1657
  }
1658
+ /**
1659
+ * Refuse `limit` / `take` / `offset` / `skip` on `updateMany` / `deleteMany`.
1660
+ * No engine applies them to a bulk write here, so `deleteMany({ where, limit:
1661
+ * 1000 })` used to touch every matching row. Shared with the PowDB engine.
1662
+ */
1663
+ function assertNoBulkWritePaging(table, operation, args) {
1664
+ const a = args;
1665
+ const key = ['limit', 'take', 'offset', 'skip'].find((k) => a[k] !== undefined);
1666
+ if (key === undefined)
1667
+ return;
1668
+ throw new errors_js_1.ValidationError(`\`${key}\` is not supported on ${operation} for "${table}": it would apply to every row the \`where\` ` +
1669
+ `matches, not ${key === 'limit' || key === 'take' ? 'only the first ones' : 'skip any'}. To ${operation === 'deleteMany' ? 'delete' : 'update'} a bounded batch, ` +
1670
+ 'select the ids first (`findMany({ where, select: { id: true }, orderBy, limit })`), then pass ' +
1671
+ '`where: { id: { in: ids } }`.');
1672
+ }
1673
+ /**
1674
+ * Refuse `optimisticLock` on a write that does not implement it. It is an
1675
+ * `update` option; on a sibling method it used to be ignored, so a stale
1676
+ * write went through with no error, which is the one outcome the option
1677
+ * exists to prevent. The message names the equivalent for each method.
1678
+ */
1679
+ function assertNoOptimisticLock(qi, args, operation) {
1680
+ const lock = args.optimisticLock;
1681
+ if (lock === undefined)
1682
+ return;
1683
+ const field = typeof lock?.field === 'string' ? lock.field : 'version';
1684
+ throw new errors_js_1.ValidationError(`\`optimisticLock\` is an \`update\` option and ${operation} on "${qi.table}" does not apply it. ` +
1685
+ OPTIMISTIC_LOCK_ALTERNATIVE[operation](field));
1686
+ }
1687
+ const OPTIMISTIC_LOCK_ALTERNATIVE = {
1688
+ updateMany: (field) => `Put the expected version in \`where\` (\`${field}: expected\`) and bump it in \`data\` ` +
1689
+ `(\`${field}: { increment: 1 }\`); the returned count is the number of rows that were still at that version.`,
1690
+ upsert: () => 'Inside one `$transaction`, read the row by `where`, then call `update({ where, data, optimisticLock })` ' +
1691
+ 'when it exists or `create` when it does not.',
1692
+ delete: (field) => `Put the expected version in \`where\` beside the key (\`where: { id, ${field}: expected }\`): a row at ` +
1693
+ 'another version is not deleted, and the call throws NotFoundError.',
1694
+ deleteMany: (field) => `Put the expected version in \`where\` (\`${field}: expected\`); the returned count is the number of rows ` +
1695
+ 'that were still at that version.',
1696
+ };
603
1697
  function upsertLookupReason(qi, args) {
1698
+ assertNoOptimisticLock(qi, args, 'upsert');
604
1699
  const where = (args.where ?? {});
605
1700
  const create = (args.create ?? {});
606
1701
  if (!(0, compound_unique_js_1.upsertWhereMatchesCreate)(qi.tableMeta, where, create))
607
1702
  return 'where-differs';
608
1703
  if (writeEntries(qi, (args.update ?? {})).length === 0)
609
1704
  return 'empty-update';
1705
+ if (qi.dialect.capabilities.upsertConflictTarget === 'any-unique-key' &&
1706
+ (0, compound_unique_js_1.upsertCreateCollidingKey)(qi.tableMeta, where, create)) {
1707
+ return 'other-unique-key';
1708
+ }
610
1709
  if (!qi.dialect.supportsUpsertUpdateWhere) {
611
1710
  qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
612
1711
  const filter = whereMod.resolveGlobalFilter(qi, qi.table);
@@ -617,8 +1716,13 @@ function upsertLookupReason(qi, args) {
617
1716
  return null;
618
1717
  }
619
1718
  /** The refusal `buildUpsert` raises for each reason, naming the way forward. */
620
- function upsertStatementRefusal(qi, reason, where) {
1719
+ function upsertStatementRefusal(qi, reason, where, create) {
621
1720
  const call = 'Call `upsert()` unbatched: it looks the row up by `where` first.';
1721
+ if (reason === 'other-unique-key') {
1722
+ const key = (0, compound_unique_js_1.upsertCreateCollidingKey)(qi.tableMeta, where, create) ?? [];
1723
+ return new errors_js_1.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. ' +
1724
+ call);
1725
+ }
622
1726
  if (reason === 'global-filter') {
623
1727
  return new errors_js_1.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\`.`);
624
1728
  }
@@ -631,6 +1735,7 @@ function upsertStatementRefusal(qi, reason, where) {
631
1735
  }
632
1736
  function buildUpsert(qi, args, projection) {
633
1737
  assertWritable(qi, 'upsert');
1738
+ assertNoOptimisticLock(qi, args, 'upsert');
634
1739
  assertNoGeneratedColumns(qi, args.create, 'upsert');
635
1740
  assertNoGeneratedColumns(qi, args.update, 'upsert');
636
1741
  qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
@@ -656,8 +1761,9 @@ function buildUpsert(qi, args, projection) {
656
1761
  // `upsert()` never reaches here with a reason (it looks the row up instead),
657
1762
  // so this refuses only a batched upsert, which has no round trip to spare.
658
1763
  const lookupReason = upsertLookupReason(qi, args);
659
- if (lookupReason)
660
- throw upsertStatementRefusal(qi, lookupReason, upsertWhere);
1764
+ if (lookupReason) {
1765
+ throw upsertStatementRefusal(qi, lookupReason, upsertWhere, args.create);
1766
+ }
661
1767
  // Build the INSERT part from create data
662
1768
  const createEntries = writeEntries(qi, args.create);
663
1769
  const columns = createEntries.map(([k]) => qi.toSqlColumn(k));
@@ -668,18 +1774,30 @@ function buildUpsert(qi, args, projection) {
668
1774
  const conflictKeys = Object.keys(upsertWhere).filter((k) => upsertWhere[k] !== undefined);
669
1775
  const conflictColumns = conflictKeys.map((k) => qi.toSqlColumn(k));
670
1776
  // The conflict-UPDATE SET goes through the SAME operator-aware compiler as
671
- // `update()` (buildSetClause), so `set` / `increment` / `decrement` /
1777
+ // `update()` (buildSetClauses), so `set` / `increment` / `decrement` /
672
1778
  // `multiply` / `divide` mean here what they mean there and a misspelled
673
1779
  // operator gets the same E003. This branch used to bind each value directly,
674
1780
  // which stored an operator OBJECT as the JSON text `{"set":"x"}` in a text
675
- // column with no error at all. buildSetClause pushes onto `params` as it
1781
+ // column with no error at all. buildSetClauses pushes onto `params` as it
676
1782
  // goes, so the numbering continues after the create params exactly as the
677
1783
  // hand-rolled `paramIdx` did. No SQL cache here (the statement is rebuilt per
678
1784
  // call), so the fingerprint / collect mirrors update() keeps are not needed.
679
- const updateEntries = writeEntries(qi, args.update);
1785
+ // `updatedAt` columns are stamped on the conflict-UPDATE exactly as the
1786
+ // lookup path's `update()` stamps them, so the column does not depend on
1787
+ // which path an upsert takes. (An empty `update` never reaches here: it is
1788
+ // a lookup that leaves the found row untouched, stamp included.)
1789
+ //
1790
+ // `expr.excluded(f)` is `create`'s value for `f` (the incoming row of a
1791
+ // one-row upsert IS `create`), resolved before compiling so this path and the
1792
+ // lookup path's plain `update()` write the same row.
1793
+ const update = (0, expr_js_1.resolveExcludedFromCreate)(qi, args.update, args.create);
1794
+ const stampedUpdate = applyUpdatedAtColumns(qi, update);
1795
+ const updateEntries = writeEntries(qi, stampedUpdate);
680
1796
  const params = [...createParams];
681
- const refQualifier = upsertReferenceQualifier(qi);
682
- const setClauses = updateEntries.map(([k, v]) => buildSetClause(qi, k, v, params, refQualifier));
1797
+ const setClauses = buildSetClauses(qi, updateEntries, params, {
1798
+ operation: 'upsert',
1799
+ target: upsertTargetRef(qi),
1800
+ });
683
1801
  // Global filter → restrict the conflict-UPDATE (soft-delete / tenancy) so an
684
1802
  // upsert never resurrects a soft-deleted row or writes across tenants. Only
685
1803
  // on engines whose upsert can carry a predicate (Postgres, SQLite); the gf
@@ -748,38 +1866,46 @@ function buildUpsert(qi, args, projection) {
748
1866
  // Non-RETURNING engines: run the upsert, then re-fetch by the where keys.
749
1867
  reselect: qi.dialect.resultStrategy === 'reselect'
750
1868
  ? async (exec) => {
1869
+ const lookup = (whereMod.mergeGlobalFilter(qi, textSafeKeyWhere(qi, upsertWhere)) ?? {});
1870
+ // An update that sets a column `where` reads moves the row out of
1871
+ // `where`'s reach: lock it and re-read it by its key. An insert
1872
+ // matches `where` (this path requires `where` pinned to `create`),
1873
+ // so a row that is not there yet is re-read by `where`.
1874
+ const key = writeMayMoveRow(qi, lookup, namedColumns(qi.tableMeta, stampedUpdate))
1875
+ ? await lockMovedRowKey(qi, exec, lookup, stampedUpdate, 'upsert', false)
1876
+ : undefined;
751
1877
  await exec(sql, params);
752
- const sel = buildReselectByWhere(qi, (whereMod.mergeGlobalFilter(qi, upsertWhere) ?? {}), projection);
753
- return exec(sel.sql, sel.params);
1878
+ const sel = buildReselectByWhere(qi, key ?? lookup, projection);
1879
+ const result = await exec(sel.sql, sel.params);
1880
+ assertOneRowByKey(qi, 'upsert', key ? identityColumns(qi) : whereColumns(qi, upsertWhere), result.rows.length);
1881
+ return result;
754
1882
  }
755
1883
  : undefined,
756
1884
  };
757
1885
  }
758
1886
  /**
759
- * The qualifier an upsert's arithmetic SET operator reads its column through.
1887
+ * The rendered reference to an upsert's TARGET row, which every column the
1888
+ * conflict-UPDATE reads goes through (an arithmetic operator's column, an
1889
+ * expression's `col()`, a case predicate).
760
1890
  *
761
1891
  * `INSERT ... ON CONFLICT DO UPDATE SET n = n + 1` is 42702 on PostgreSQL
762
1892
  * because `n` could be the target row's or `excluded`'s, so the reference is
763
1893
  * qualified with the table name on every engine whose upsert statement names
764
1894
  * the table directly (PostgreSQL, SQLite, MySQL all accept `"t"."n"` there).
765
- * SQL Server is the exception and keeps the bare reference: its `MERGE`
766
- * aliases the target as `T`, so the TABLE NAME does not resolve there at all,
767
- * and the alias belongs to that dialect's own `buildUpsertStatement` rather
768
- * than to this module. An arithmetic operator in an mssql upsert may therefore
769
- * still be refused by the server as an ambiguous column; that shape has never
770
- * worked (it previously bound the operator OBJECT as the value), and the sound
771
- * fix is for the dialect to publish its target alias, which is a change to
772
- * `dialect.ts`. It fails loudly either way, so no wrong answer is possible.
1895
+ * SQL Server's `MERGE` aliases its target, so the table name does not resolve
1896
+ * there and the dialect publishes the alias ({@link Dialect.upsertTargetAlias}).
773
1897
  * `set` and literal values reference no column and are unaffected everywhere.
774
1898
  *
775
- * Gated on the dialect NAME, never on hook presence: every engine dialect
776
- * spreads `postgresDialect`, so a presence test would say yes for all of them.
1899
+ * Read from a property only SQL Server sets, never inferred from a hook's
1900
+ * presence: every engine dialect spreads `postgresDialect`.
777
1901
  */
778
- function upsertReferenceQualifier(qi) {
779
- return qi.dialect.name === 'mssql' ? '' : `${qi.q(qi.table)}.`;
1902
+ function upsertTargetRef(qi) {
1903
+ return qi.dialect.upsertTargetAlias ?? qi.q(qi.table);
780
1904
  }
781
1905
  function buildUpdateMany(qi, args) {
782
1906
  assertWritable(qi, 'updateMany');
1907
+ assertNoBulkWritePaging(qi.table, 'updateMany', args);
1908
+ assertNoOptimisticLock(qi, args, 'updateMany');
783
1909
  qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
784
1910
  const dataObj = applyUpdatedAtColumns(qi, args.data);
785
1911
  assertNoGeneratedColumns(qi, dataObj, 'updateMany');
@@ -802,8 +1928,7 @@ function buildUpdateMany(qi, args) {
802
1928
  const ck = `um:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
803
1929
  const params = [];
804
1930
  const buildSql = (freshParams) => {
805
- const setEntries = writeEntries(qi, dataObj);
806
- const setClauses = setEntries.map(([k, v]) => buildSetClause(qi, k, v, freshParams));
1931
+ const setClauses = buildSetClauses(qi, writeEntries(qi, dataObj), freshParams, { operation: 'updateMany' });
807
1932
  const whereClause = whereMod.buildWhereClause(qi, whereObj, freshParams);
808
1933
  const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
809
1934
  return `UPDATE ${qi.q(qi.table)} SET ${setClauses.join(', ')}${whereSql}`;
@@ -822,6 +1947,8 @@ function buildUpdateMany(qi, args) {
822
1947
  }
823
1948
  function buildDeleteMany(qi, args) {
824
1949
  assertWritable(qi, 'deleteMany');
1950
+ assertNoBulkWritePaging(qi.table, 'deleteMany', args);
1951
+ assertNoOptimisticLock(qi, args, 'deleteMany');
825
1952
  qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
826
1953
  whereMod.assertMutationHasPredicate(qi, 'deleteMany', whereMod.userPredicateIsEmpty(qi, args.where) ? '' : ' WHERE x', (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan'));
827
1954
  const whereObj = (whereMod.mergeGlobalFilter(qi, args.where) ?? {});
@@ -847,8 +1974,8 @@ function buildDeleteMany(qi, args) {
847
1974
  /**
848
1975
  * The snake_case names of a table's PII-tagged (`defineSchema` `pii: true`)
849
1976
  * columns. PII columns are excluded from default projections (findMany /
850
- * findUnique / relation subqueries / batched loads) unless the query opts in
851
- * via `includePii` or names the column explicitly in `select`. Returns an
1977
+ * findUnique / relation subqueries / batched loads) unless a read passes
1978
+ * `includePii: UNSAFE`; naming one in `select` without it is E003. Returns an
852
1979
  * empty set for any table with no PII column, so untagged schemas keep their
853
1980
  * byte-identical SQL.
854
1981
  */
@@ -881,8 +2008,8 @@ function piiColumns(_qi, meta) {
881
2008
  * The camelCase field names of a table's PII-tagged columns: the read-side
882
2009
  * counterpart of {@link piiColumns} applied to already-parsed entities.
883
2010
  * Used to strip PII from a write's RETURNING/reselect row (writes accept no
884
- * `includePii`/`select`, so their returned row always applies the default
885
- * exclusion; you may still write PII fields freely).
2011
+ * `includePii`, and a PII column named in a write's `select` is E003, so their
2012
+ * returned row never carries one; you may still write PII fields freely).
886
2013
  */
887
2014
  function piiFields(_qi, meta) {
888
2015
  const out = [];
@@ -916,8 +2043,9 @@ function writeProjectionCacheSegment(projection) {
916
2043
  * column so the PII values never leave the database on a write. A PII-tagged
917
2044
  * PRIMARY KEY column is kept in the projection regardless (the returned row
918
2045
  * must stay addressable): tag sensitive data, not keys, a PII PK is
919
- * documented out of scope for stripping. Writes accept no `select`/`includePii`
920
- * (unlike reads), so this is the whole write-return policy at the SQL level;
2046
+ * documented out of scope for stripping. Writes accept no `includePii` and
2047
+ * their `select` cannot name a PII column, so this is the whole write-return
2048
+ * policy at the SQL level;
921
2049
  * {@link parseWriteRow} remains as a defense-in-depth strip (a no-op once the
922
2050
  * SQL already excludes the columns). Derived purely from static per-table
923
2051
  * schema metadata, so the write SQL cache needs no extra key segment.
@@ -1004,8 +2132,8 @@ function writeReselectSelection(qi, projection) {
1004
2132
  */
1005
2133
  function parseWriteRow(qi, row, projection) {
1006
2134
  const parsed = qi.parseRow(row, qi.table);
1007
- // A column the caller named in `select` is the opt-in (see WriteProjection),
1008
- // so the strip spares exactly the projected columns and nothing else.
2135
+ // A write's projection never names a PII column (E003, see WriteProjection),
2136
+ // so for PII this strip spares nothing; it stays as defense in depth.
1009
2137
  const pii = piiColumns(qi, qi.tableMeta);
1010
2138
  if (pii.size === 0)
1011
2139
  return parsed;
@@ -1046,85 +2174,260 @@ function assertNoGeneratedColumns(qi, data, operation) {
1046
2174
  }
1047
2175
  }
1048
2176
  }
2177
+ /** The SQL operator each arithmetic update operator compiles to. */
2178
+ const ARITHMETIC_SQL = { increment: '+', decrement: '-', multiply: '*', divide: '/' };
2179
+ /**
2180
+ * The SQL operator for one arithmetic update operator on one column.
2181
+ *
2182
+ * `divide` on an INTEGER column is the engine's truncating integer division
2183
+ * (`integerDivision`): `DIV` on MySQL, whose `/` is decimal division, so
2184
+ * `75 / 2` is 37.5 and the assignment back to an INT rounds it to 38, where
2185
+ * PostgreSQL's integer division truncates to 37. `DIV` truncates toward zero,
2186
+ * as PostgreSQL does. Decided by the COLUMN TYPE alone, never by
2187
+ * the bound divisor: the SQL text must not depend on a value, or the template
2188
+ * cache would serve one shape to the other.
2189
+ */
2190
+ function arithmeticSql(qi, op, column) {
2191
+ if (op === 'divide' && isIntegerColumnType(qi.columnPgTypeMap.get(column))) {
2192
+ return qi.dialect.capabilities.integerDivision;
2193
+ }
2194
+ return ARITHMETIC_SQL[op];
2195
+ }
2196
+ /**
2197
+ * The atomic operator a SET value names, or `null` when it is a plain value.
2198
+ * THE one rule, read by the SET build, its fingerprint and its cache-hit param
2199
+ * collector, so the three cannot disagree about whether a value is an
2200
+ * operator.
2201
+ *
2202
+ * A branded expression or `col()` reference is checked FIRST and answers
2203
+ * `'expr'`: it is a frozen plain object too, and must never reach the
2204
+ * operator test below. An operator is a plain object with EXACTLY one key from
2205
+ * {@link UPDATE_OPERATOR_KEYS}. On a json/jsonb column only `set` is an
2206
+ * operator: an object there IS the JSON value, so `{ increment: 1 }` is the
2207
+ * document `{"increment":1}`, and `{ set: doc }` is the way to write a
2208
+ * document that is itself shaped like `{ set: ... }`.
2209
+ */
2210
+ function setOperatorOf(qi, key, value) {
2211
+ if ((0, expr_js_1.isSqlExpr)(value) || (0, column_ref_js_1.isColumnRef)(value))
2212
+ return 'expr';
2213
+ if (!(0, filters_js_1.isUnmatchedPlainObject)(value))
2214
+ return null;
2215
+ const keys = Object.keys(value);
2216
+ if (keys.length !== 1 || !filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0]))
2217
+ return null;
2218
+ const op = keys[0];
2219
+ if (op !== 'set' && isJsonWriteColumn(qi, key))
2220
+ return null;
2221
+ return op;
2222
+ }
2223
+ /** Whether `key` names a json/jsonb column (non-throwing; unknown keys are not). */
2224
+ function isJsonWriteColumn(qi, key) {
2225
+ const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
2226
+ const dbType = column ? qi.columnPgTypeMap.get(column) : undefined;
2227
+ return dbType !== undefined && whereMod.isJsonColumnType(qi, dbType);
2228
+ }
2229
+ const UPDATE_SET = { operation: 'update' };
2230
+ const NO_READS = new Set();
1049
2231
  /**
1050
- * Build a single SET clause entry for update/updateMany.
2232
+ * Compile a SET list: one clause per entry, in the order given (the canonical
2233
+ * column order of {@link writeEntries}), pushing params as it goes so later
2234
+ * clauses (WHERE, version check) continue the numbering.
1051
2235
  *
1052
- * Supports plain values and atomic operator objects ({ set, increment,
1053
- * decrement, multiply, divide }). An operator object is detected ONLY when
1054
- * it has EXACTLY one key that is one of the 5 operator keys, this avoids
1055
- * misinterpreting JSON column values like `{ set: 'x' }` as operators
1056
- * (real operator objects always have exactly one key, and a plain JSON
1057
- * payload that happens to have a single `set` key is extremely unusual).
1058
- * Multi-key objects are always treated as plain (JSON) values.
2236
+ * On MySQL the clause STRINGS are then reordered (see
2237
+ * {@link orderForLeftToRightSet}). Params are not: MySQL binds by name
2238
+ * (`:pN`), so a clause keeps its placeholders wherever it lands, and the
2239
+ * cache-hit collector ({@link collectSetParams}) keeps pushing in canonical
2240
+ * order.
2241
+ */
2242
+ function buildSetClauses(qi, entries, params, ctx = UPDATE_SET) {
2243
+ const compiled = entries.map(([k, v]) => compileSetEntry(qi, k, v, params, ctx));
2244
+ return qi.dialect.capabilities.setClauseOrder === 'left-to-right'
2245
+ ? orderForLeftToRightSet(qi, compiled)
2246
+ : compiled.map((c) => c.sql);
2247
+ }
2248
+ /**
2249
+ * Build a single SET clause entry.
2250
+ *
2251
+ * Supports plain values, atomic operator objects ({ set, increment,
2252
+ * decrement, multiply, divide }) and expressions (`expr.*`, `col()`), as
2253
+ * decided by {@link setOperatorOf}.
1059
2254
  *
1060
2255
  * Returns the SQL fragment (e.g., `"view_count" = "view_count" + $3`) and
1061
2256
  * pushes any required params onto the shared params array so that WHERE
1062
2257
  * clause numbering continues correctly afterward.
1063
2258
  *
1064
- * `refQualifier` prefixes the column REFERENCE an arithmetic operator reads
1065
- * from (`"view_count" = <qualifier>"view_count" + $n`). A plain `UPDATE` has
1066
- * one table in scope and passes nothing; `upsert` passes the table name,
1067
- * because inside `ON CONFLICT ... DO UPDATE SET` the target table and
1068
- * `excluded` are BOTH in scope and a bare reference is 42702 on PostgreSQL.
1069
- * The assignment target on the left stays bare on every engine (it can only
1070
- * ever be the target table's column), and `set` / literal values reference no
1071
- * column at all, so their SQL is byte-identical with or without a qualifier.
2259
+ * `ctx.target` qualifies the column REFERENCES the right-hand side reads
2260
+ * (`"view_count" = "users"."view_count" + $n`): absent for a plain `UPDATE`,
2261
+ * the table name in an upsert, because inside `ON CONFLICT ... DO UPDATE SET`
2262
+ * the target table and `excluded` are BOTH in scope and a bare reference is
2263
+ * 42702 on PostgreSQL. The assignment target on the left stays bare on every
2264
+ * engine (it can only ever be the target table's column), and `set` / literal
2265
+ * values reference no column at all, so their SQL is byte-identical with or
2266
+ * without a qualifier.
1072
2267
  */
1073
- function buildSetClause(qi, key, value, params, refQualifier = '') {
2268
+ function compileSetEntry(qi, key, value, params, ctx) {
1074
2269
  const col = qi.toSqlColumn(key);
1075
- const ref = `${refQualifier}${col}`;
2270
+ const column = qi.toColumn(key);
2271
+ const op = setOperatorOf(qi, key, value);
2272
+ if (op === 'expr') {
2273
+ const compiled = (0, expr_js_1.compileSetExpr)(exprEnv(qi, key, params, ctx), value);
2274
+ return { sql: `${col} = ${compiled.sql}`, column, reads: compiled.reads };
2275
+ }
1076
2276
  // Enum columns get an explicit `::"EnumName"` cast on their value bind
1077
2277
  // (see enumTypeForColumn); `''` everywhere else. Value-invariant, so the
1078
2278
  // SQL cache and collectSetParams are unaffected.
1079
- const cast = whereMod.enumCastSuffix(qi, qi.toColumn(key));
1080
- // Detect atomic-operator object: plain object (not null, not array, not
1081
- // Date, not Buffer) with EXACTLY one key matching an operator name.
1082
- if (value !== null &&
1083
- typeof value === 'object' &&
1084
- !Array.isArray(value) &&
1085
- !(value instanceof Date) &&
1086
- !Buffer.isBuffer(value)) {
1087
- const v = value;
1088
- const keys = Object.keys(v);
1089
- if (keys.length === 1 && filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0])) {
1090
- const op = keys[0];
1091
- const opValue = v[op];
1092
- if (op === 'set') {
1093
- params.push(coerceWriteValue(qi, key, opValue));
1094
- return `${col} = ${qi.p(params.length)}${cast}`;
1095
- }
1096
- // Arithmetic operators: must be finite numbers
1097
- if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
1098
- throw new errors_js_1.ValidationError(`update operator "${op}" on "${qi.table}.${key}" requires a finite number, got ${typeof opValue}`);
1099
- }
1100
- if (op === 'increment') {
1101
- params.push(opValue);
1102
- return `${col} = ${ref} + ${qi.p(params.length)}`;
1103
- }
1104
- if (op === 'decrement') {
1105
- params.push(opValue);
1106
- return `${col} = ${ref} - ${qi.p(params.length)}`;
1107
- }
1108
- if (op === 'multiply') {
1109
- params.push(opValue);
1110
- return `${col} = ${ref} * ${qi.p(params.length)}`;
1111
- }
1112
- if (op === 'divide') {
1113
- params.push(opValue);
1114
- return `${col} = ${ref} / ${qi.p(params.length)}`;
1115
- }
2279
+ const cast = whereMod.enumCastSuffix(qi, column);
2280
+ if (op === 'set') {
2281
+ params.push(coerceWriteValue(qi, key, value.set));
2282
+ return { sql: `${col} = ${qi.p(params.length)}${cast}`, column, reads: NO_READS };
2283
+ }
2284
+ if (op !== null) {
2285
+ const opValue = value[op];
2286
+ // Arithmetic operators: must be finite numbers
2287
+ if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
2288
+ throw new errors_js_1.ValidationError(`update operator "${op}" on "${qi.table}.${key}" requires a finite number, got ${typeof opValue}`);
1116
2289
  }
1117
- // Fall through: multi-key objects or non-operator single-key objects
1118
- // are treated as plain values (e.g., JSONB column payloads), unless the
1119
- // shape can only be a misspelled atomic operator (see below).
1120
- assertBindableSetValue(qi, key, value);
2290
+ params.push(opValue);
2291
+ const ref = ctx.target ? `${ctx.target}.${col}` : col;
2292
+ // Reads only its own column, which no ordering needs to respect.
2293
+ return { sql: `${col} = ${ref} ${arithmeticSql(qi, op, column)} ${qi.p(params.length)}`, column, reads: NO_READS };
1121
2294
  }
2295
+ // A misspelled operator gets its own E003 before the bind seam's general
2296
+ // shape check.
2297
+ assertBindableSetValue(qi, key, value);
1122
2298
  // Plain value (including null, Date, Buffer, arrays, JSON objects)
1123
2299
  params.push(coerceWriteValue(qi, key, value));
1124
- return `${col} = ${qi.p(params.length)}${cast}`;
2300
+ return { sql: `${col} = ${qi.p(params.length)}${cast}`, column, reads: NO_READS };
2301
+ }
2302
+ /**
2303
+ * The environment one expression compiles in: how a value leaf binds, how a
2304
+ * column of the target row and of the incoming row render, and how a case
2305
+ * predicate compiles.
2306
+ */
2307
+ function exprEnv(qi, key, params, ctx) {
2308
+ const cast = exprValueCast(qi, key);
2309
+ const target = ctx.target;
2310
+ const incoming = ctx.incoming;
2311
+ return {
2312
+ qi,
2313
+ targetKey: key,
2314
+ operation: ctx.operation,
2315
+ bind: (value, arithmetic) => {
2316
+ params.push(coerceWriteValue(qi, key, value));
2317
+ return `${qi.p(params.length)}${arithmetic ? arithmeticValueCast(qi, key, cast, arithmetic) : cast}`;
2318
+ },
2319
+ column: (column) => (target ? `${target}.${qi.q(column)}` : qi.q(column)),
2320
+ excluded: incoming ? (column) => `${incoming}.${qi.q(column)}` : undefined,
2321
+ insertColumns: ctx.insertColumns,
2322
+ predicate: (when) => {
2323
+ const sql = target
2324
+ ? whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, target, when, params)
2325
+ : whereMod.buildWhereClause(qi, when, params);
2326
+ if (sql === null) {
2327
+ throw new errors_js_1.ValidationError(`expr.case() on "${qi.table}": an arm's when compiles to no predicate.`);
2328
+ }
2329
+ return sql;
2330
+ },
2331
+ };
2332
+ }
2333
+ /**
2334
+ * A type token that is safe to write after `::` as is: a plain name, a
2335
+ * multi-word built-in (`double precision`), optionally with a precision.
2336
+ */
2337
+ const SAFE_CAST_TYPE = /^[A-Za-z_][A-Za-z0-9_]*( [A-Za-z_][A-Za-z0-9_]*)*(\(\d+( ?, ?\d+)?\))?$/;
2338
+ /**
2339
+ * The cast every bound value inside an expression carries on PostgreSQL: the
2340
+ * TARGET column's type. node-postgres sends parameters untyped, so in an
2341
+ * expression whose operands are all bound values PostgreSQL infers `text` and
2342
+ * fails (`CASE WHEN ... THEN $1 ELSE $2 END` into an int column is 42804, and
2343
+ * `$1 + $2` is 42725). Enums use the quoted enum cast; a type the metadata
2344
+ * does not name plainly, or one from another schema, gets none. Other engines
2345
+ * bind typed values and get none.
2346
+ */
2347
+ function exprValueCast(qi, key) {
2348
+ if (!qi.dialect.capabilities.paramTypeCasts)
2349
+ return '';
2350
+ const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
2351
+ if (column === undefined)
2352
+ return '';
2353
+ const enumCast = whereMod.enumCastSuffix(qi, column);
2354
+ if (enumCast)
2355
+ return enumCast;
2356
+ if (qi.crossSchemaTypeColumns.has(column))
2357
+ return '';
2358
+ const type = qi.columnPgTypeMap.get(column);
2359
+ if (type === undefined || type.startsWith('_') || !SAFE_CAST_TYPE.test(type))
2360
+ return '';
2361
+ return `::${type}`;
2362
+ }
2363
+ /**
2364
+ * The cast on a bound arithmetic operand on PostgreSQL: the operand's own
2365
+ * class, so `n / 2` stays integer division into a float column and `n * 1.5`
2366
+ * is not forced through an integer cast. An integer operand keeps the target
2367
+ * column's cast when that column is an integer one, `int8` otherwise; a
2368
+ * non-integer one is `numeric`. Other engines bind typed values and get none.
2369
+ */
2370
+ function arithmeticValueCast(qi, key, targetCast, cls) {
2371
+ if (!qi.dialect.capabilities.paramTypeCasts)
2372
+ return '';
2373
+ if (cls === 'dec')
2374
+ return '::numeric';
2375
+ const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
2376
+ return column !== undefined && targetCast !== '' && isIntegerColumnType(qi.columnPgTypeMap.get(column))
2377
+ ? targetCast
2378
+ : '::int8';
1125
2379
  }
1126
2380
  /**
1127
- * Reject a `data` value that reached {@link buildSetClause}'s plain-value
2381
+ * MySQL evaluates a single-table UPDATE's SET list LEFT TO RIGHT, and a later
2382
+ * assignment sees the NEW value of an earlier column; PostgreSQL, SQLite and
2383
+ * SQL Server read old values. So `{ a: col('b'), b: col('a') }` swaps there
2384
+ * and duplicates on MySQL. `ON DUPLICATE KEY UPDATE` behaves the same way.
2385
+ *
2386
+ * Order the list so every assignment that READS a column comes before the
2387
+ * assignment that WRITES it: a stable topological sort that keeps the
2388
+ * canonical order wherever the reads allow, so the order is a pure function of
2389
+ * the data's shape and the SQL-cache key (which already captures that shape)
2390
+ * stays sufficient. A cycle (a swap) has no such order and is refused.
2391
+ */
2392
+ function orderForLeftToRightSet(qi, entries) {
2393
+ if (entries.every((e) => e.reads.size === 0))
2394
+ return entries.map((e) => e.sql);
2395
+ const writer = new Map();
2396
+ entries.forEach((e, i) => {
2397
+ writer.set(e.column, i);
2398
+ });
2399
+ const before = entries.map(() => []);
2400
+ const pending = entries.map(() => 0);
2401
+ entries.forEach((e, i) => {
2402
+ for (const column of e.reads) {
2403
+ const j = writer.get(column);
2404
+ if (j === undefined || j === i)
2405
+ continue;
2406
+ before[i].push(j);
2407
+ pending[j]++;
2408
+ }
2409
+ });
2410
+ const ready = pending.flatMap((n, i) => (n === 0 ? [i] : []));
2411
+ const order = [];
2412
+ while (ready.length > 0) {
2413
+ ready.sort((a, b) => a - b);
2414
+ const i = ready.shift();
2415
+ order.push(i);
2416
+ for (const j of before[i])
2417
+ if (--pending[j] === 0)
2418
+ ready.push(j);
2419
+ }
2420
+ if (order.length < entries.length) {
2421
+ const stuck = entries
2422
+ .filter((_, i) => pending[i] > 0)
2423
+ .map((e) => qi.tableMeta.reverseColumnMap[e.column] ?? e.column);
2424
+ throw new errors_js_1.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 " +
2425
+ 'cannot be expressed. Split it into two updates.');
2426
+ }
2427
+ return order.map((i) => entries[i].sql);
2428
+ }
2429
+ /**
2430
+ * Reject a `data` value that reached {@link buildSetClauses}'s plain-value
1128
2431
  * fallthrough as a plain object with exactly ONE key that is not a known
1129
2432
  * atomic operator. On a scalar column that shape can only be a misspelled
1130
2433
  * operator (`{ incremnt: 1 }`, `{ bogus: 'x' }`); binding it plainly writes the
@@ -1155,52 +2458,47 @@ function assertBindableSetValue(qi, key, value) {
1155
2458
  }
1156
2459
  /**
1157
2460
  * Fingerprint SET clauses for update/updateMany.
1158
- * Captures key names + operator types (set/increment/etc) but not values.
2461
+ * Captures key names + operator types (set/increment/etc) but not values. An
2462
+ * expression contributes `k:expr(<shape>)` from the same walk that compiles
2463
+ * it, so a statement without one keeps its key byte-identical.
1159
2464
  */
1160
2465
  function fingerprintSet(qi, data) {
1161
2466
  const entries = writeEntries(qi, data);
1162
2467
  const parts = [];
1163
2468
  for (const [k, v] of entries) {
1164
- if (v !== null &&
1165
- typeof v === 'object' &&
1166
- !Array.isArray(v) &&
1167
- !(v instanceof Date) &&
1168
- !(typeof Buffer !== 'undefined' && Buffer.isBuffer(v))) {
1169
- const keys = Object.keys(v);
1170
- if (keys.length === 1 && filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0])) {
1171
- parts.push(`${k}:${keys[0]}`);
1172
- continue;
1173
- }
2469
+ const op = setOperatorOf(qi, k, v);
2470
+ if (op === 'expr') {
2471
+ const env = exprEnv(qi, k, [], UPDATE_SET);
2472
+ parts.push(`${k}:expr(${(0, expr_js_1.compileSetExpr)(env, v).fingerprint})`);
2473
+ continue;
1174
2474
  }
1175
- parts.push(`${k}:eq`);
2475
+ parts.push(`${k}:${op ?? 'eq'}`);
1176
2476
  }
1177
2477
  return parts.join(',');
1178
2478
  }
1179
2479
  /**
1180
- * Collect SET params for update/updateMany. Mirrors buildSetClause param order.
2480
+ * Collect SET params for update/updateMany. Mirrors buildSetClauses param order.
1181
2481
  */
1182
2482
  function collectSetParams(qi, data, params) {
1183
2483
  const entries = writeEntries(qi, data);
1184
2484
  for (const [k, v] of entries) {
1185
- if (v !== null &&
1186
- typeof v === 'object' &&
1187
- !Array.isArray(v) &&
1188
- !(v instanceof Date) &&
1189
- !(typeof Buffer !== 'undefined' && Buffer.isBuffer(v))) {
1190
- const obj = v;
1191
- const keys = Object.keys(obj);
1192
- if (keys.length === 1 && filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0])) {
1193
- // `set` is the only operator that binds a user value of arbitrary type;
1194
- // the arithmetic operators are finite numbers, which coerceWriteValue
1195
- // returns untouched. Mirrors buildSetClause.
1196
- params.push(coerceWriteValue(qi, k, obj[keys[0]]));
1197
- continue;
1198
- }
1199
- // Mirror the build path's misspelled-operator guard: the SET fingerprint
1200
- // is `key:eq` for both a plain value and a bogus operator object, so a
1201
- // cache HIT would otherwise bind the object and skip the check.
1202
- assertBindableSetValue(qi, k, v);
2485
+ const op = setOperatorOf(qi, k, v);
2486
+ if (op === 'expr') {
2487
+ // The build's own walk: it binds the same leaves in the same order.
2488
+ (0, expr_js_1.compileSetExpr)(exprEnv(qi, k, params, UPDATE_SET), v);
2489
+ continue;
2490
+ }
2491
+ if (op !== null) {
2492
+ // `set` is the only operator that binds a user value of arbitrary type;
2493
+ // the arithmetic operators are finite numbers, which coerceWriteValue
2494
+ // returns untouched. Mirrors buildSetClauses.
2495
+ params.push(coerceWriteValue(qi, k, v[op]));
2496
+ continue;
1203
2497
  }
2498
+ // Mirror the build path's misspelled-operator guard: the SET fingerprint
2499
+ // is `key:eq` for both a plain value and a bogus operator object, so a
2500
+ // cache HIT would otherwise bind the object and skip the check.
2501
+ assertBindableSetValue(qi, k, v);
1204
2502
  params.push(coerceWriteValue(qi, k, v));
1205
2503
  }
1206
2504
  }