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.
- package/README.md +18 -16
- package/dist/cjs/cli/index.js +109 -16
- package/dist/cjs/cli/migrate.js +78 -3
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +333 -22
- package/dist/cjs/cli/ui.js +7 -1
- package/dist/cjs/dialect.js +1 -1
- package/dist/cjs/generate.js +23 -2
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/mssql.js +22 -5
- package/dist/cjs/powdb.js +41 -1
- package/dist/cjs/powql.js +80 -25
- package/dist/cjs/query/aggregates.js +683 -0
- package/dist/cjs/query/batched-loader.js +2 -0
- package/dist/cjs/query/builder.js +297 -4504
- package/dist/cjs/query/filters.js +12 -0
- package/dist/cjs/query/relations.js +1698 -0
- package/dist/cjs/query/where-compile.js +180 -0
- package/dist/cjs/query/where.js +1491 -0
- package/dist/cjs/query/writes.js +680 -0
- package/dist/cjs/schema-builder.js +6 -0
- package/dist/cjs/schema-metadata.js +4 -0
- package/dist/cjs/schema-sql.js +265 -3
- package/dist/cjs/sqlite.js +1 -1
- package/dist/cli/index.d.ts +8 -2
- package/dist/cli/index.js +111 -18
- package/dist/cli/migrate.d.ts +24 -1
- package/dist/cli/migrate.js +77 -3
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +46 -13
- package/dist/cli/studio.js +331 -23
- package/dist/cli/ui.js +7 -1
- package/dist/dialect.d.ts +15 -6
- package/dist/dialect.js +1 -1
- package/dist/generate.js +23 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.js +22 -5
- package/dist/powdb.d.ts +20 -0
- package/dist/powdb.js +40 -0
- package/dist/powql.d.ts +33 -1
- package/dist/powql.js +80 -25
- package/dist/query/aggregates.d.ts +74 -0
- package/dist/query/aggregates.js +641 -0
- package/dist/query/batched-loader.d.ts +6 -0
- package/dist/query/batched-loader.js +2 -0
- package/dist/query/builder.d.ts +62 -829
- package/dist/query/builder.js +302 -4509
- package/dist/query/deferred.d.ts +7 -0
- package/dist/query/filters.d.ts +7 -0
- package/dist/query/filters.js +11 -0
- package/dist/query/relations.d.ts +441 -0
- package/dist/query/relations.js +1627 -0
- package/dist/query/types.d.ts +15 -0
- package/dist/query/where-compile.d.ts +139 -0
- package/dist/query/where-compile.js +175 -0
- package/dist/query/where.d.ts +494 -0
- package/dist/query/where.js +1431 -0
- package/dist/query/writes.d.ts +131 -0
- package/dist/query/writes.js +626 -0
- package/dist/schema-builder.d.ts +18 -3
- package/dist/schema-builder.js +6 -0
- package/dist/schema-metadata.js +4 -0
- package/dist/schema-sql.d.ts +60 -3
- package/dist/schema-sql.js +261 -4
- package/dist/schema.d.ts +10 -0
- package/dist/sqlite.js +1 -1
- 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
|
+
}
|