turbine-orm 0.35.0 → 0.36.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 (68) hide show
  1. package/README.md +18 -16
  2. package/dist/cjs/cli/index.js +109 -16
  3. package/dist/cjs/cli/migrate.js +78 -3
  4. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  5. package/dist/cjs/cli/studio.js +333 -22
  6. package/dist/cjs/cli/ui.js +7 -1
  7. package/dist/cjs/dialect.js +1 -1
  8. package/dist/cjs/generate.js +23 -2
  9. package/dist/cjs/index.js +2 -1
  10. package/dist/cjs/mssql.js +22 -5
  11. package/dist/cjs/powdb.js +41 -1
  12. package/dist/cjs/powql.js +80 -25
  13. package/dist/cjs/query/aggregates.js +683 -0
  14. package/dist/cjs/query/batched-loader.js +2 -0
  15. package/dist/cjs/query/builder.js +297 -4504
  16. package/dist/cjs/query/filters.js +12 -0
  17. package/dist/cjs/query/relations.js +1698 -0
  18. package/dist/cjs/query/where-compile.js +180 -0
  19. package/dist/cjs/query/where.js +1491 -0
  20. package/dist/cjs/query/writes.js +680 -0
  21. package/dist/cjs/schema-builder.js +6 -0
  22. package/dist/cjs/schema-metadata.js +4 -0
  23. package/dist/cjs/schema-sql.js +265 -3
  24. package/dist/cjs/sqlite.js +1 -1
  25. package/dist/cli/index.d.ts +8 -2
  26. package/dist/cli/index.js +111 -18
  27. package/dist/cli/migrate.d.ts +24 -1
  28. package/dist/cli/migrate.js +77 -3
  29. package/dist/cli/studio-ui.generated.js +1 -1
  30. package/dist/cli/studio.d.ts +46 -13
  31. package/dist/cli/studio.js +331 -23
  32. package/dist/cli/ui.js +7 -1
  33. package/dist/dialect.d.ts +15 -6
  34. package/dist/dialect.js +1 -1
  35. package/dist/generate.js +23 -2
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/mssql.js +22 -5
  39. package/dist/powdb.d.ts +20 -0
  40. package/dist/powdb.js +40 -0
  41. package/dist/powql.d.ts +33 -1
  42. package/dist/powql.js +80 -25
  43. package/dist/query/aggregates.d.ts +74 -0
  44. package/dist/query/aggregates.js +641 -0
  45. package/dist/query/batched-loader.d.ts +6 -0
  46. package/dist/query/batched-loader.js +2 -0
  47. package/dist/query/builder.d.ts +62 -829
  48. package/dist/query/builder.js +302 -4509
  49. package/dist/query/deferred.d.ts +7 -0
  50. package/dist/query/filters.d.ts +7 -0
  51. package/dist/query/filters.js +11 -0
  52. package/dist/query/relations.d.ts +441 -0
  53. package/dist/query/relations.js +1627 -0
  54. package/dist/query/types.d.ts +15 -0
  55. package/dist/query/where-compile.d.ts +139 -0
  56. package/dist/query/where-compile.js +175 -0
  57. package/dist/query/where.d.ts +494 -0
  58. package/dist/query/where.js +1431 -0
  59. package/dist/query/writes.d.ts +131 -0
  60. package/dist/query/writes.js +626 -0
  61. package/dist/schema-builder.d.ts +18 -3
  62. package/dist/schema-builder.js +6 -0
  63. package/dist/schema-metadata.js +4 -0
  64. package/dist/schema-sql.d.ts +60 -3
  65. package/dist/schema-sql.js +261 -4
  66. package/dist/schema.d.ts +10 -0
  67. package/dist/sqlite.js +1 -1
  68. package/package.json +2 -2
@@ -0,0 +1,680 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm: write compilation (extracted from builder.ts)
4
+ *
5
+ * SQL builders for the mutating operations (create / createMany / update /
6
+ * delete / upsert / updateMany / deleteMany) plus the write-projection helpers
7
+ * (writeReturningColumns / writeReselectSelection / parseWriteRow, the PII
8
+ * column set, optimistic-lock and atomic-operator SET clauses). All functions
9
+ * take a {@link BuilderCtx} first argument; WHERE compilation is reused from
10
+ * where.ts (via `whereMod`), and the cache / dialect / row-parse primitives
11
+ * stay class-resident, reached through the ctx. See builder.ts for the thin
12
+ * delegating methods and the async execute wrappers.
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.buildReselectByWhere = buildReselectByWhere;
49
+ exports.buildCreate = buildCreate;
50
+ exports.makeCreateReselect = makeCreateReselect;
51
+ exports.buildCreateMany = buildCreateMany;
52
+ exports.buildUpdate = buildUpdate;
53
+ exports.buildDelete = buildDelete;
54
+ exports.buildUpsert = buildUpsert;
55
+ exports.buildUpdateMany = buildUpdateMany;
56
+ exports.buildDeleteMany = buildDeleteMany;
57
+ exports.piiColumns = piiColumns;
58
+ exports.piiFields = piiFields;
59
+ exports.writeReturningColumns = writeReturningColumns;
60
+ exports.writeReselectSelection = writeReselectSelection;
61
+ exports.parseWriteRow = parseWriteRow;
62
+ exports.assertWritable = assertWritable;
63
+ exports.assertNoGeneratedColumns = assertNoGeneratedColumns;
64
+ exports.buildSetClause = buildSetClause;
65
+ exports.fingerprintSet = fingerprintSet;
66
+ exports.collectSetParams = collectSetParams;
67
+ const errors_js_1 = require("../errors.js");
68
+ const schema_js_1 = require("../schema.js");
69
+ const filters_js_1 = require("./filters.js");
70
+ const whereMod = __importStar(require("./where.js"));
71
+ /**
72
+ * Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
73
+ * by a write's `where` clause. Used by the `'reselect'` result strategy to
74
+ * return rows from non-RETURNING engines. Reuses the same parameterized WHERE
75
+ * builder as reads, so no user value is interpolated.
76
+ */
77
+ function buildReselectByWhere(qi, whereObj) {
78
+ const params = [];
79
+ const clause = whereMod.buildWhereClause(qi, whereObj, params);
80
+ const where = clause ? ` WHERE ${clause}` : '';
81
+ return { sql: `SELECT ${writeReselectSelection(qi)} FROM ${qi.q(qi.table)}${where}`, params };
82
+ }
83
+ function buildCreate(qi, args) {
84
+ assertWritable(qi, 'create');
85
+ assertNoGeneratedColumns(qi, args.data, 'create');
86
+ const entries = Object.entries(args.data).filter(([, v]) => v !== undefined);
87
+ const columns = entries.map(([k]) => qi.toSqlColumn(k));
88
+ const params = entries.map(([, v]) => v);
89
+ // Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
90
+ const placeholders = entries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
91
+ const sql = qi.dialect.buildInsertStatement({
92
+ table: qi.q(qi.table),
93
+ columns,
94
+ valuePlaceholders: placeholders,
95
+ returning: writeReturningColumns(qi),
96
+ });
97
+ return {
98
+ sql,
99
+ params,
100
+ transform: (result) => {
101
+ const row = result.rows[0];
102
+ if (!row) {
103
+ throw new errors_js_1.NotFoundError({
104
+ table: qi.table,
105
+ operation: 'create',
106
+ message: `[turbine] create on "${qi.table}" returned no row from RETURNING *; this should never happen.`,
107
+ });
108
+ }
109
+ return parseWriteRow(qi, row);
110
+ },
111
+ tag: `${qi.table}.create`,
112
+ // Non-RETURNING engines: INSERT, then re-fetch the new row by primary key
113
+ // (provided value, else the driver's generated insert id).
114
+ reselect: makeCreateReselect(qi, sql, params, args.data),
115
+ };
116
+ }
117
+ /**
118
+ * Build the `'reselect'` plan for {@link buildCreate}: run the INSERT, then
119
+ * `SELECT * WHERE pk = ?`. Returns `undefined` (skipped) unless the active
120
+ * dialect's result strategy is `'reselect'`, so the PostgreSQL/RETURNING path
121
+ * pays nothing. Not yet wired to a real non-RETURNING engine.
122
+ */
123
+ function makeCreateReselect(qi, insertSql, insertParams, data) {
124
+ if (qi.dialect.resultStrategy !== 'reselect')
125
+ return undefined;
126
+ return async (exec) => {
127
+ const writeResult = await exec(insertSql, insertParams);
128
+ const insertId = qi.mutationInsertId(writeResult);
129
+ const conds = [];
130
+ const selParams = [];
131
+ let idx = 1;
132
+ for (const pk of qi.tableMeta.primaryKey) {
133
+ const field = qi.tableMeta.reverseColumnMap[pk] ?? (0, schema_js_1.snakeToCamel)(pk);
134
+ selParams.push(data[field] ?? data[pk] ?? insertId);
135
+ conds.push(`${qi.q(pk)} = ${qi.p(idx++)}`);
136
+ }
137
+ const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
138
+ return exec(`SELECT ${writeReselectSelection(qi)} FROM ${qi.q(qi.table)}${where}`, selParams);
139
+ };
140
+ }
141
+ function buildCreateMany(qi, args) {
142
+ const qt = qi.q(qi.table);
143
+ if (args.data.length === 0) {
144
+ return {
145
+ sql: `SELECT * FROM ${qt} WHERE false`,
146
+ params: [],
147
+ transform: () => [],
148
+ tag: `${qi.table}.createMany`,
149
+ };
150
+ }
151
+ assertWritable(qi, 'createMany');
152
+ for (const row of args.data) {
153
+ assertNoGeneratedColumns(qi, row, 'createMany');
154
+ }
155
+ const keys = Object.keys(args.data[0]).filter((k) => args.data[0][k] !== undefined);
156
+ const columns = keys.map((k) => qi.toColumn(k));
157
+ const rowValues = args.data.map((row) => {
158
+ const record = row;
159
+ return keys.map((key) => record[key]);
160
+ });
161
+ // Use actual Postgres types for array casts in the default PostgreSQL dialect.
162
+ // Enum columns cast to `"EnumName"[]` — the generic text[] fallback would
163
+ // type the UNNEST output as text, which Postgres refuses to coerce to the
164
+ // enum ("column X is of type Y but expression is of type text").
165
+ const typeCasts = columns.map((col) => {
166
+ const enumType = whereMod.enumTypeForColumn(qi, col);
167
+ return enumType ? `${qi.q(enumType)}[]` : whereMod.getColumnArrayType(qi, col);
168
+ });
169
+ const quotedColumns = columns.map((c) => qi.q(c));
170
+ const built = qi.dialect.buildBulkInsertStatement({
171
+ table: qt,
172
+ columns: quotedColumns,
173
+ rowValues,
174
+ columnArrayTypes: typeCasts,
175
+ skipDuplicates: args.skipDuplicates,
176
+ returning: writeReturningColumns(qi),
177
+ });
178
+ return {
179
+ sql: built.sql,
180
+ params: built.params,
181
+ transform: (result) => result.rows.map((row) => parseWriteRow(qi, row)),
182
+ tag: `${qi.table}.createMany`,
183
+ };
184
+ }
185
+ function buildUpdate(qi, args) {
186
+ assertWritable(qi, 'update');
187
+ qi.currentSkip = args.skipGlobalFilters;
188
+ const dataObj = args.data;
189
+ assertNoGeneratedColumns(qi, dataObj, 'update');
190
+ const userWhere = args.where;
191
+ const lock = args.optimisticLock;
192
+ // The empty-`where` guard checks the USER predicate only — a global filter
193
+ // must never turn an unguarded mass update into an allowed one.
194
+ const userHasPredicate = !whereMod.userPredicateIsEmpty(qi, userWhere) || !!lock;
195
+ whereMod.assertMutationHasPredicate(qi, 'update', userHasPredicate ? ' WHERE x' : '', args.allowFullTableScan);
196
+ // The SQL is built from the global-filter-merged where (soft-delete keeps an
197
+ // update from touching already-deleted rows).
198
+ const whereObj = (whereMod.mergeGlobalFilter(qi, userWhere) ?? {});
199
+ const setFp = fingerprintSet(qi, dataObj);
200
+ const whereFp = whereMod.fingerprintWhere(qi, whereObj);
201
+ const ck = lock ? null : `u:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
202
+ const params = [];
203
+ const buildSql = (freshParams) => {
204
+ const setEntries = Object.entries(dataObj).filter(([, v]) => v !== undefined);
205
+ const setClauses = setEntries.map(([k, v]) => buildSetClause(qi, k, v, freshParams));
206
+ if (lock) {
207
+ const versionCol = qi.toSqlColumn(lock.field);
208
+ setClauses.push(`${versionCol} = ${versionCol} + 1`);
209
+ }
210
+ const whereClause = whereMod.buildWhereClause(qi, whereObj, freshParams);
211
+ let whereSql = whereClause ? ` WHERE ${whereClause}` : '';
212
+ if (lock) {
213
+ const versionCol = qi.toSqlColumn(lock.field);
214
+ freshParams.push(lock.expected);
215
+ const versionCheck = `${versionCol} = ${qi.p(freshParams.length)}`;
216
+ whereSql = whereSql ? `${whereSql} AND ${versionCheck}` : ` WHERE ${versionCheck}`;
217
+ }
218
+ // Engines that inject their returning shape MID-statement (SQL Server
219
+ // `OUTPUT INSERTED.*` between SET and WHERE) override buildUpdateStatement;
220
+ // absent → the trailing-clause PG/SQLite/MySQL form (byte-identical).
221
+ // `returning` excludes PII columns on tagged tables (else '*').
222
+ const returning = writeReturningColumns(qi);
223
+ return qi.dialect.buildUpdateStatement
224
+ ? qi.dialect.buildUpdateStatement({ table: qi.q(qi.table), setClauses, whereSql, returning })
225
+ : `UPDATE ${qi.q(qi.table)} SET ${setClauses.join(', ')}${whereSql}${qi.dialect.buildReturningClause(returning)}`;
226
+ };
227
+ let sql;
228
+ let preparedName;
229
+ let cacheEntry;
230
+ if (ck) {
231
+ cacheEntry = qi.acquireSql(ck, buildSql);
232
+ sql = cacheEntry.sql;
233
+ preparedName = cacheEntry.name;
234
+ }
235
+ else {
236
+ // optimisticLock path: value-variant version check → uncacheable, no cross-check.
237
+ sql = buildSql([]);
238
+ }
239
+ // Collect params: SET first, then WHERE, then version check (same order as fresh build)
240
+ collectSetParams(qi, dataObj, params);
241
+ whereMod.collectWhereParams(qi, whereObj, params);
242
+ if (lock) {
243
+ params.push(lock.expected);
244
+ }
245
+ if (ck && cacheEntry) {
246
+ qi.crossCheckCache('update', ck, cacheEntry, buildSql, params);
247
+ }
248
+ return {
249
+ sql,
250
+ params,
251
+ transform: (result) => {
252
+ const row = result.rows[0];
253
+ if (!row) {
254
+ if (lock) {
255
+ throw new errors_js_1.OptimisticLockError({
256
+ table: qi.table,
257
+ versionField: lock.field,
258
+ expectedVersion: lock.expected,
259
+ });
260
+ }
261
+ throw new errors_js_1.NotFoundError({
262
+ table: qi.table,
263
+ where: args.where,
264
+ operation: 'update',
265
+ });
266
+ }
267
+ return parseWriteRow(qi, row);
268
+ },
269
+ tag: `${qi.table}.update`,
270
+ preparedName,
271
+ // Non-RETURNING engines: UPDATE, then re-fetch the row by the same where.
272
+ reselect: qi.dialect.resultStrategy === 'reselect'
273
+ ? async (exec) => {
274
+ const writeResult = await exec(sql, params, preparedName);
275
+ // Optimistic-lock conflict: the version-checked UPDATE matched no
276
+ // row. The re-fetch below uses `where` WITHOUT the version
277
+ // predicate, so it would return the stale row and silently mask
278
+ // the conflict — detect it from affected-rows here instead, to
279
+ // match the OptimisticLockError thrown on RETURNING/OUTPUT engines.
280
+ if (lock && (writeResult.rowCount ?? 0) === 0) {
281
+ throw new errors_js_1.OptimisticLockError({
282
+ table: qi.table,
283
+ versionField: lock.field,
284
+ expectedVersion: lock.expected,
285
+ });
286
+ }
287
+ const sel = buildReselectByWhere(qi, whereObj);
288
+ return exec(sel.sql, sel.params);
289
+ }
290
+ : undefined,
291
+ };
292
+ }
293
+ function buildDelete(qi, args) {
294
+ assertWritable(qi, 'delete');
295
+ qi.currentSkip = args.skipGlobalFilters;
296
+ // Guard the USER predicate (a global filter must not satisfy the guard).
297
+ whereMod.assertMutationHasPredicate(qi, 'delete', whereMod.userPredicateIsEmpty(qi, args.where) ? '' : ' WHERE x', args.allowFullTableScan);
298
+ const whereObj = (whereMod.mergeGlobalFilter(qi, args.where) ?? {});
299
+ const whereFp = whereMod.fingerprintWhere(qi, whereObj);
300
+ const ck = `d:${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
301
+ const params = [];
302
+ const buildSql = (freshParams) => {
303
+ const clause = whereMod.buildWhereClause(qi, whereObj, freshParams);
304
+ const whereSql = clause ? ` WHERE ${clause}` : '';
305
+ // SQL Server injects `OUTPUT DELETED.*` between `DELETE FROM <t>` and WHERE;
306
+ // absent override → the trailing-clause PG/SQLite/MySQL form (byte-identical).
307
+ // `returning` excludes PII columns on tagged tables (else '*').
308
+ const returning = writeReturningColumns(qi);
309
+ return qi.dialect.buildDeleteStatement
310
+ ? qi.dialect.buildDeleteStatement({ table: qi.q(qi.table), whereSql, returning })
311
+ : `DELETE FROM ${qi.q(qi.table)}${whereSql}${qi.dialect.buildReturningClause(returning)}`;
312
+ };
313
+ const entry = qi.acquireSql(ck, buildSql);
314
+ whereMod.collectWhereParams(qi, whereObj, params);
315
+ qi.crossCheckCache('delete', ck, entry, buildSql, params);
316
+ return {
317
+ sql: entry.sql,
318
+ params,
319
+ transform: (result) => {
320
+ const row = result.rows[0];
321
+ if (!row) {
322
+ throw new errors_js_1.NotFoundError({
323
+ table: qi.table,
324
+ where: args.where,
325
+ operation: 'delete',
326
+ });
327
+ }
328
+ return parseWriteRow(qi, row);
329
+ },
330
+ tag: `${qi.table}.delete`,
331
+ preparedName: entry.name,
332
+ // Non-RETURNING engines: the row is gone after DELETE, so pre-SELECT it
333
+ // by the same where, then run the DELETE, returning the captured row.
334
+ reselect: qi.dialect.resultStrategy === 'reselect'
335
+ ? async (exec) => {
336
+ const sel = buildReselectByWhere(qi, whereObj);
337
+ const pre = await exec(sel.sql, sel.params);
338
+ await exec(entry.sql, params, entry.name);
339
+ return pre;
340
+ }
341
+ : undefined,
342
+ };
343
+ }
344
+ function buildUpsert(qi, args) {
345
+ assertWritable(qi, 'upsert');
346
+ assertNoGeneratedColumns(qi, args.create, 'upsert');
347
+ assertNoGeneratedColumns(qi, args.update, 'upsert');
348
+ qi.currentSkip = args.skipGlobalFilters;
349
+ // Build the INSERT part from create data
350
+ const createEntries = Object.entries(args.create).filter(([, v]) => v !== undefined);
351
+ const columns = createEntries.map(([k]) => qi.toSqlColumn(k));
352
+ const createParams = createEntries.map(([, v]) => v);
353
+ // Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
354
+ const placeholders = createEntries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
355
+ // The conflict target comes from `where` keys — must be unique/PK columns
356
+ const conflictKeys = Object.keys(args.where).filter((k) => args.where[k] !== undefined);
357
+ const conflictColumns = conflictKeys.map((k) => qi.toSqlColumn(k));
358
+ // Build the UPDATE SET part
359
+ const updateEntries = Object.entries(args.update).filter(([, v]) => v !== undefined);
360
+ let paramIdx = createParams.length + 1;
361
+ const setClauses = updateEntries.map(([k]) => {
362
+ const clause = `${qi.toSqlColumn(k)} = ${qi.p(paramIdx)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`;
363
+ paramIdx++;
364
+ return clause;
365
+ });
366
+ const updateParams = updateEntries.map(([, v]) => v);
367
+ const params = [...createParams, ...updateParams];
368
+ // Global filter → restrict the conflict-UPDATE (soft-delete / tenancy) so an
369
+ // upsert never resurrects a soft-deleted row or writes across tenants. Only
370
+ // on engines whose upsert can carry a predicate (Postgres); the gf params
371
+ // continue the placeholder numbering after create+update params.
372
+ let updateWhere;
373
+ if (qi.dialect.supportsUpsertUpdateWhere) {
374
+ const gf = whereMod.resolveGlobalFilter(qi, qi.table);
375
+ if (gf)
376
+ updateWhere = whereMod.buildWhereClause(qi, gf, params) ?? undefined;
377
+ }
378
+ const sql = qi.dialect.buildUpsertStatement({
379
+ table: qi.q(qi.table),
380
+ insertColumns: columns,
381
+ valuePlaceholders: placeholders,
382
+ conflictColumns,
383
+ updateSetClauses: setClauses,
384
+ updateWhere,
385
+ returning: writeReturningColumns(qi),
386
+ });
387
+ return {
388
+ sql,
389
+ params,
390
+ transform: (result) => {
391
+ const row = result.rows[0];
392
+ if (!row) {
393
+ throw new errors_js_1.NotFoundError({
394
+ table: qi.table,
395
+ where: args.where,
396
+ operation: 'upsert',
397
+ message: `[turbine] upsert on "${qi.table}" returned no row from RETURNING *; this should never happen.`,
398
+ });
399
+ }
400
+ return parseWriteRow(qi, row);
401
+ },
402
+ tag: `${qi.table}.upsert`,
403
+ // Non-RETURNING engines: run the upsert, then re-fetch by the where keys.
404
+ reselect: qi.dialect.resultStrategy === 'reselect'
405
+ ? async (exec) => {
406
+ await exec(sql, params);
407
+ const sel = buildReselectByWhere(qi, (whereMod.mergeGlobalFilter(qi, args.where) ?? {}));
408
+ return exec(sel.sql, sel.params);
409
+ }
410
+ : undefined,
411
+ };
412
+ }
413
+ function buildUpdateMany(qi, args) {
414
+ assertWritable(qi, 'updateMany');
415
+ qi.currentSkip = args.skipGlobalFilters;
416
+ const dataObj = args.data;
417
+ assertNoGeneratedColumns(qi, dataObj, 'updateMany');
418
+ whereMod.assertMutationHasPredicate(qi, 'updateMany', whereMod.userPredicateIsEmpty(qi, args.where) ? '' : ' WHERE x', args.allowFullTableScan);
419
+ const whereObj = (whereMod.mergeGlobalFilter(qi, args.where) ?? {});
420
+ const setFp = fingerprintSet(qi, dataObj);
421
+ const whereFp = whereMod.fingerprintWhere(qi, whereObj);
422
+ const ck = `um:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
423
+ const params = [];
424
+ const buildSql = (freshParams) => {
425
+ const setEntries = Object.entries(dataObj).filter(([, v]) => v !== undefined);
426
+ const setClauses = setEntries.map(([k, v]) => buildSetClause(qi, k, v, freshParams));
427
+ const whereClause = whereMod.buildWhereClause(qi, whereObj, freshParams);
428
+ const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
429
+ return `UPDATE ${qi.q(qi.table)} SET ${setClauses.join(', ')}${whereSql}`;
430
+ };
431
+ const entry = qi.acquireSql(ck, buildSql);
432
+ collectSetParams(qi, dataObj, params);
433
+ whereMod.collectWhereParams(qi, whereObj, params);
434
+ qi.crossCheckCache('updateMany', ck, entry, buildSql, params);
435
+ return {
436
+ sql: entry.sql,
437
+ params,
438
+ transform: (result) => ({ count: result.rowCount ?? 0 }),
439
+ tag: `${qi.table}.updateMany`,
440
+ preparedName: entry.name,
441
+ };
442
+ }
443
+ function buildDeleteMany(qi, args) {
444
+ assertWritable(qi, 'deleteMany');
445
+ qi.currentSkip = args.skipGlobalFilters;
446
+ whereMod.assertMutationHasPredicate(qi, 'deleteMany', whereMod.userPredicateIsEmpty(qi, args.where) ? '' : ' WHERE x', args.allowFullTableScan);
447
+ const whereObj = (whereMod.mergeGlobalFilter(qi, args.where) ?? {});
448
+ const whereFp = whereMod.fingerprintWhere(qi, whereObj);
449
+ const ck = `dm:${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
450
+ const params = [];
451
+ const buildSql = (freshParams) => {
452
+ const clause = whereMod.buildWhereClause(qi, whereObj, freshParams);
453
+ const whereSql = clause ? ` WHERE ${clause}` : '';
454
+ return `DELETE FROM ${qi.q(qi.table)}${whereSql}`;
455
+ };
456
+ const entry = qi.acquireSql(ck, buildSql);
457
+ whereMod.collectWhereParams(qi, whereObj, params);
458
+ qi.crossCheckCache('deleteMany', ck, entry, buildSql, params);
459
+ return {
460
+ sql: entry.sql,
461
+ params,
462
+ transform: (result) => ({ count: result.rowCount ?? 0 }),
463
+ tag: `${qi.table}.deleteMany`,
464
+ preparedName: entry.name,
465
+ };
466
+ }
467
+ /**
468
+ * The snake_case names of a table's PII-tagged (`defineSchema` `pii: true`)
469
+ * columns. PII columns are excluded from default projections (findMany /
470
+ * findUnique / relation subqueries / batched loads) unless the query opts in
471
+ * via `includePii` or names the column explicitly in `select`. Returns an
472
+ * empty set for any table with no PII column, so untagged schemas keep their
473
+ * byte-identical SQL.
474
+ */
475
+ function piiColumns(_qi, meta) {
476
+ const out = new Set();
477
+ for (const col of meta.columns) {
478
+ if (col.pii)
479
+ out.add(col.name);
480
+ }
481
+ return out;
482
+ }
483
+ /**
484
+ * The camelCase field names of a table's PII-tagged columns: the read-side
485
+ * counterpart of {@link piiColumns} applied to already-parsed entities.
486
+ * Used to strip PII from a write's RETURNING/reselect row (writes accept no
487
+ * `includePii`/`select`, so their returned row always applies the default
488
+ * exclusion; you may still write PII fields freely).
489
+ */
490
+ function piiFields(_qi, meta) {
491
+ const out = [];
492
+ for (const col of meta.columns) {
493
+ if (col.pii)
494
+ out.push(col.field);
495
+ }
496
+ return out;
497
+ }
498
+ /**
499
+ * The `RETURNING` / `OUTPUT` selection for a write on this table. A table with
500
+ * no PII column returns `'*'` (every column — byte-identical SQL to before);
501
+ * a table WITH PII columns returns an explicit quoted list of every non-PII
502
+ * column so the PII values never leave the database on a write. A PII-tagged
503
+ * PRIMARY KEY column is kept in the projection regardless (the returned row
504
+ * must stay addressable): tag sensitive data, not keys — a PII PK is
505
+ * documented out of scope for stripping. Writes accept no `select`/`includePii`
506
+ * (unlike reads), so this is the whole write-return policy at the SQL level;
507
+ * {@link parseWriteRow} remains as a defense-in-depth strip (a no-op once the
508
+ * SQL already excludes the columns). Derived purely from static per-table
509
+ * schema metadata, so the write SQL cache needs no extra key segment.
510
+ */
511
+ function writeReturningColumns(qi) {
512
+ const piiCols = piiColumns(qi, qi.tableMeta);
513
+ if (piiCols.size === 0)
514
+ return '*';
515
+ const pk = new Set(qi.tableMeta.primaryKey);
516
+ return qi.tableMeta.allColumns.filter((col) => !piiCols.has(col) || pk.has(col)).map((col) => qi.q(col));
517
+ }
518
+ /**
519
+ * String form of {@link writeReturningColumns} for a `SELECT` list (the
520
+ * `'reselect'` result strategy re-fetches via a SELECT, not RETURNING).
521
+ * `'*'` when there is no PII column; otherwise the comma-joined quoted list.
522
+ */
523
+ function writeReselectSelection(qi) {
524
+ const cols = writeReturningColumns(qi);
525
+ return cols === '*' ? '*' : cols.join(', ');
526
+ }
527
+ /**
528
+ * Parse a write's returned row (create/update/upsert/delete), then strip the
529
+ * table's PII fields: the write-side read policy. On PII-tagged tables the
530
+ * statement's RETURNING/OUTPUT already omits these columns (see
531
+ * {@link writeReturningColumns}), so this strip is defense-in-depth and a
532
+ * no-op. Untagged tables incur only one `for` over a zero-length field list,
533
+ * so behavior is unchanged.
534
+ */
535
+ function parseWriteRow(qi, row) {
536
+ const parsed = qi.parseRow(row, qi.table);
537
+ for (const field of piiFields(qi, qi.tableMeta)) {
538
+ delete parsed[field];
539
+ }
540
+ return parsed;
541
+ }
542
+ /**
543
+ * Reject any write against a view (H4). Views are introspected with
544
+ * `isView: true` and are read-only in every engine; a write raises a
545
+ * {@link ValidationError} (E003) rather than emitting SQL Postgres would
546
+ * reject (or, worse, silently applying to an updatable view).
547
+ */
548
+ function assertWritable(qi, operation) {
549
+ if (qi.tableMeta.isView) {
550
+ throw new errors_js_1.ValidationError(`[turbine] Cannot ${operation} "${qi.table}": it is a view (read-only). ` +
551
+ 'Views support reads (findMany/findFirst/…) but not writes.');
552
+ }
553
+ }
554
+ /**
555
+ * Reject a write whose `data` names a `GENERATED ALWAYS AS (...) STORED`
556
+ * column (H3). Postgres computes these from other columns and errors if you
557
+ * try to write them; we fail early with a clear {@link ValidationError} (E003)
558
+ * instead of surfacing a cryptic driver error. Undefined values are ignored
559
+ * (they're stripped from the statement anyway).
560
+ */
561
+ function assertNoGeneratedColumns(qi, data, operation) {
562
+ for (const [key, value] of Object.entries(data)) {
563
+ if (value === undefined)
564
+ continue;
565
+ const col = qi.tableMeta.columns.find((c) => c.field === key || c.name === key || c.name === (0, schema_js_1.camelToSnake)(key));
566
+ if (col?.isGeneratedStored) {
567
+ throw new errors_js_1.ValidationError(`[turbine] Cannot ${operation} "${qi.table}": column "${key}" is a GENERATED ALWAYS AS (…) STORED ` +
568
+ 'column whose value the database computes — remove it from your data.');
569
+ }
570
+ }
571
+ }
572
+ /**
573
+ * Build a single SET clause entry for update/updateMany.
574
+ *
575
+ * Supports plain values and atomic operator objects ({ set, increment,
576
+ * decrement, multiply, divide }). An operator object is detected ONLY when
577
+ * it has EXACTLY one key that is one of the 5 operator keys — this avoids
578
+ * misinterpreting JSON column values like `{ set: 'x' }` as operators
579
+ * (real operator objects always have exactly one key, and a plain JSON
580
+ * payload that happens to have a single `set` key is extremely unusual).
581
+ * Multi-key objects are always treated as plain (JSON) values.
582
+ *
583
+ * Returns the SQL fragment (e.g., `"view_count" = "view_count" + $3`) and
584
+ * pushes any required params onto the shared params array so that WHERE
585
+ * clause numbering continues correctly afterward.
586
+ */
587
+ function buildSetClause(qi, key, value, params) {
588
+ const col = qi.toSqlColumn(key);
589
+ // Enum columns get an explicit `::"EnumName"` cast on their value bind
590
+ // (see enumTypeForColumn); `''` everywhere else. Value-invariant, so the
591
+ // SQL cache and collectSetParams are unaffected.
592
+ const cast = whereMod.enumCastSuffix(qi, qi.toColumn(key));
593
+ // Detect atomic-operator object: plain object (not null, not array, not
594
+ // Date, not Buffer) with EXACTLY one key matching an operator name.
595
+ if (value !== null &&
596
+ typeof value === 'object' &&
597
+ !Array.isArray(value) &&
598
+ !(value instanceof Date) &&
599
+ !Buffer.isBuffer(value)) {
600
+ const v = value;
601
+ const keys = Object.keys(v);
602
+ if (keys.length === 1 && filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0])) {
603
+ const op = keys[0];
604
+ const opValue = v[op];
605
+ if (op === 'set') {
606
+ params.push(opValue);
607
+ return `${col} = ${qi.p(params.length)}${cast}`;
608
+ }
609
+ // Arithmetic operators: must be finite numbers
610
+ if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
611
+ throw new errors_js_1.ValidationError(`[turbine] update operator "${op}" on "${qi.table}.${key}" requires a finite number, got ${typeof opValue}`);
612
+ }
613
+ if (op === 'increment') {
614
+ params.push(opValue);
615
+ return `${col} = ${col} + ${qi.p(params.length)}`;
616
+ }
617
+ if (op === 'decrement') {
618
+ params.push(opValue);
619
+ return `${col} = ${col} - ${qi.p(params.length)}`;
620
+ }
621
+ if (op === 'multiply') {
622
+ params.push(opValue);
623
+ return `${col} = ${col} * ${qi.p(params.length)}`;
624
+ }
625
+ if (op === 'divide') {
626
+ params.push(opValue);
627
+ return `${col} = ${col} / ${qi.p(params.length)}`;
628
+ }
629
+ }
630
+ // Fall through: multi-key objects or non-operator single-key objects
631
+ // are treated as plain values (e.g., JSONB column payloads).
632
+ }
633
+ // Plain value (including null, Date, Buffer, arrays, JSON objects)
634
+ params.push(value);
635
+ return `${col} = ${qi.p(params.length)}${cast}`;
636
+ }
637
+ /**
638
+ * Fingerprint SET clauses for update/updateMany.
639
+ * Captures key names + operator types (set/increment/etc) but not values.
640
+ */
641
+ function fingerprintSet(_qi, data) {
642
+ const entries = Object.entries(data).filter(([, v]) => v !== undefined);
643
+ const parts = [];
644
+ for (const [k, v] of entries) {
645
+ if (v !== null &&
646
+ typeof v === 'object' &&
647
+ !Array.isArray(v) &&
648
+ !(v instanceof Date) &&
649
+ !(typeof Buffer !== 'undefined' && Buffer.isBuffer(v))) {
650
+ const keys = Object.keys(v);
651
+ if (keys.length === 1 && filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0])) {
652
+ parts.push(`${k}:${keys[0]}`);
653
+ continue;
654
+ }
655
+ }
656
+ parts.push(`${k}:eq`);
657
+ }
658
+ return parts.join(',');
659
+ }
660
+ /**
661
+ * Collect SET params for update/updateMany. Mirrors buildSetClause param order.
662
+ */
663
+ function collectSetParams(_qi, data, params) {
664
+ const entries = Object.entries(data).filter(([, v]) => v !== undefined);
665
+ for (const [, v] of entries) {
666
+ if (v !== null &&
667
+ typeof v === 'object' &&
668
+ !Array.isArray(v) &&
669
+ !(v instanceof Date) &&
670
+ !(typeof Buffer !== 'undefined' && Buffer.isBuffer(v))) {
671
+ const obj = v;
672
+ const keys = Object.keys(obj);
673
+ if (keys.length === 1 && filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0])) {
674
+ params.push(obj[keys[0]]);
675
+ continue;
676
+ }
677
+ }
678
+ params.push(v);
679
+ }
680
+ }