turbine-orm 0.21.0 → 0.23.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.
@@ -0,0 +1,1262 @@
1
+ "use strict";
2
+ /**
3
+ * PowqlInterface — Turbine's PowQL query generator (the PowDB analogue of
4
+ * {@link QueryInterface}). It exposes the same public method surface as the SQL
5
+ * `QueryInterface` (`findMany`, `create`, `update`, …) but emits **PowQL** — a
6
+ * pipeline language, not SQL — executed through {@link PowdbPool}.
7
+ *
8
+ * It is a *parallel* implementation rather than a `Dialect` of the SQL builder:
9
+ * PowQL's grammar (`T filter <e> order <k> { .col }`) shares no surface with
10
+ * `SELECT … FROM … WHERE`, so the SQL `Dialect` seam cannot express it. Keeping
11
+ * it separate also means the four SQL engines are untouched.
12
+ *
13
+ * Behavioural deltas from the SQL path, all driven by PowDB's wire reality (see
14
+ * `docs/strategy/powdb-parity-matrix.md`, every row verified against a live
15
+ * server):
16
+ * - `create`/`createMany`/`update`/`delete` use PowDB 0.7.0's trailing
17
+ * `returning` keyword (`RETURNING *`, all columns) to surface affected rows
18
+ * in one round-trip. `upsert` is the lone exception — its statement does not
19
+ * accept `returning`, so it reselects the row by PK (a composite-PK upsert
20
+ * reselects-or-writes inside one flat transaction).
21
+ * - The PK is server-assigned when the column is `isGenerated` (PowDB's `auto`
22
+ * int — read back via `returning`); otherwise a defaulted **string** PK is
23
+ * generated client-side (UUID).
24
+ * - `with` (nested relations) uses **batched N+1 loaders** — D round-trips for
25
+ * depth D, not one query — including manyToMany (junction → targets).
26
+ * - **Relation filters** (`some`/`none`/`every`, all cardinalities incl. m2m)
27
+ * are resolved client-side to a literal `in (…)` list, never an IN-subquery:
28
+ * PowDB's executor caches a subquery's result by plan shape and would return
29
+ * a stale prior result for a later subquery of the same shape.
30
+ * - **Nested writes** (relation ops in `create`/`update` data) run through the
31
+ * shared nested-write engine as one flat top-level transaction (PowDB is
32
+ * single-writer, no savepoints).
33
+ * - pgvector / JSON / array filters and cursor streaming throw
34
+ * {@link UnsupportedFeatureError} (E017) — they have no PowDB equivalent.
35
+ *
36
+ * @module
37
+ */
38
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
39
+ if (k2 === undefined) k2 = k;
40
+ var desc = Object.getOwnPropertyDescriptor(m, k);
41
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
42
+ desc = { enumerable: true, get: function() { return m[k]; } };
43
+ }
44
+ Object.defineProperty(o, k2, desc);
45
+ }) : (function(o, m, k, k2) {
46
+ if (k2 === undefined) k2 = k;
47
+ o[k2] = m[k];
48
+ }));
49
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
50
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
51
+ }) : function(o, v) {
52
+ o["default"] = v;
53
+ });
54
+ var __importStar = (this && this.__importStar) || (function () {
55
+ var ownKeys = function(o) {
56
+ ownKeys = Object.getOwnPropertyNames || function (o) {
57
+ var ar = [];
58
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
59
+ return ar;
60
+ };
61
+ return ownKeys(o);
62
+ };
63
+ return function (mod) {
64
+ if (mod && mod.__esModule) return mod;
65
+ var result = {};
66
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
67
+ __setModuleDefault(result, mod);
68
+ return result;
69
+ };
70
+ })();
71
+ Object.defineProperty(exports, "__esModule", { value: true });
72
+ exports.PowqlInterface = void 0;
73
+ const node_crypto_1 = require("node:crypto");
74
+ const errors_js_1 = require("./errors.js");
75
+ const nested_write_js_1 = require("./nested-write.js");
76
+ const powdb_js_1 = require("./powdb.js");
77
+ const utils_js_1 = require("./query/utils.js");
78
+ const schema_js_1 = require("./schema.js");
79
+ /**
80
+ * Max parent keys per relation-loader `in (…)` query. A `with` over a large
81
+ * parent set is split into batches of this size so a single query never exceeds
82
+ * PowDB's per-statement param / row limits; the batches' children are merged
83
+ * before grouping. Mirrors the chunking the parity matrix documents.
84
+ */
85
+ const MAX_RELATION_KEYS = 1000;
86
+ /** Operator keys recognised inside a `WhereOperator` object. */
87
+ const OPERATOR_KEYS = new Set([
88
+ 'equals',
89
+ 'gt',
90
+ 'gte',
91
+ 'lt',
92
+ 'lte',
93
+ 'not',
94
+ 'in',
95
+ 'notIn',
96
+ 'contains',
97
+ 'startsWith',
98
+ 'endsWith',
99
+ 'mode',
100
+ ]);
101
+ /** Filters that have no PowDB representation and must throw E017. */
102
+ function rejectUnsupportedFilter(value, field) {
103
+ if ('distance' in value || 'metric' in value) {
104
+ throw new errors_js_1.UnsupportedFeatureError('pgvector distance filters', 'PowDB', `field "${field}"`);
105
+ }
106
+ if ('path' in value || 'hasKey' in value) {
107
+ throw new errors_js_1.UnsupportedFeatureError('JSON path/key filters', 'PowDB', `field "${field}"`);
108
+ }
109
+ if ('hasEvery' in value || 'hasSome' in value || 'isEmpty' in value) {
110
+ throw new errors_js_1.UnsupportedFeatureError('array filters', 'PowDB', `field "${field}"`);
111
+ }
112
+ if ('search' in value) {
113
+ throw new errors_js_1.UnsupportedFeatureError('full-text search filters', 'PowDB', `field "${field}"`);
114
+ }
115
+ }
116
+ /**
117
+ * The PowQL query interface. Constructed by `turbinePowDB` via the
118
+ * `queryInterfaceFactory` seam and cast to `QueryInterface<object>` so
119
+ * `TurbineClient.table()` can return it transparently.
120
+ */
121
+ class PowqlInterface {
122
+ pool;
123
+ table;
124
+ schema;
125
+ middlewares;
126
+ options;
127
+ meta;
128
+ defaultLimit;
129
+ warnOnUnlimited;
130
+ onQuery;
131
+ currentAction = 'raw';
132
+ warnedUnlimited = false;
133
+ constructor(pool, table, schema, middlewares = [], options = {}) {
134
+ this.pool = pool;
135
+ this.table = table;
136
+ this.schema = schema;
137
+ this.middlewares = middlewares;
138
+ this.options = options;
139
+ const meta = schema.tables[table];
140
+ if (!meta) {
141
+ throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}". Available: ${Object.keys(schema.tables).join(', ')}`);
142
+ }
143
+ this.meta = meta;
144
+ this.defaultLimit = options.defaultLimit;
145
+ this.warnOnUnlimited = options.warnOnUnlimited !== false;
146
+ this.onQuery = options._onQuery;
147
+ }
148
+ // -------------------------------------------------------------------------
149
+ // Column / value helpers
150
+ // -------------------------------------------------------------------------
151
+ /** Resolve a camelCase field name (or raw snake) to its column metadata. */
152
+ column(field) {
153
+ const snake = this.meta.columnMap[field] ?? field;
154
+ const col = this.meta.columns.find((c) => c.name === snake || c.field === field);
155
+ if (!col) {
156
+ throw new errors_js_1.ValidationError(`[turbine] Unknown column "${field}" on table "${this.table}". Known: ${this.meta.columns
157
+ .map((c) => c.field)
158
+ .join(', ')}`);
159
+ }
160
+ return col;
161
+ }
162
+ /** PowQL column reference (`.snake_name`) for a field. */
163
+ ref(field) {
164
+ return `.${this.column(field).name}`;
165
+ }
166
+ /**
167
+ * Push a value into the param array and return its `$N` placeholder. When the
168
+ * value targets a `float` column it is wrapped in {@link PowdbFloatParam} so
169
+ * the *embedded* literal encoder emits a float-form literal even for an
170
+ * integer value (PowQL's `42` is an int literal, `42.0` a float). The
171
+ * networked driver unwraps the marker back to the plain number in
172
+ * {@link toPowdbParam}, so the wire param is unchanged.
173
+ */
174
+ param(value, params, col) {
175
+ const tagged = col && typeof value === 'number' && this.isFloatCol(col) ? new powdb_js_1.PowdbFloatParam(value) : value;
176
+ params.push(tagged);
177
+ return `$${params.length}`;
178
+ }
179
+ /**
180
+ * Render a value for a write *assignment* (`col := …`). Every value — float
181
+ * columns included — is sent as a positional `$N` param. PowDB ≥ 0.7.0 fixed
182
+ * the int→float UPDATE coercion bug (`score := $n` with an integer param now
183
+ * reads back the integer value, not the raw i64 bits), so the float-literal
184
+ * inlining workaround Turbine carried for ≤ 0.6.2 is gone. Marks the column as
185
+ * float-typed for the *embedded* literal encoder (which materializes params
186
+ * into PowQL text) so an integer-valued float still encodes as a float literal
187
+ * and coercion stays unambiguous. Non-finite floats are still rejected.
188
+ */
189
+ writeRef(value, col, params) {
190
+ if (typeof value === 'number' && !Number.isFinite(value) && this.isFloatCol(col)) {
191
+ throw new errors_js_1.ValidationError(`[turbine] Non-finite value for float column "${col.name}" on "${this.table}".`);
192
+ }
193
+ return this.param(value, params, col);
194
+ }
195
+ isFloatCol(col) {
196
+ try {
197
+ return (0, powdb_js_1.powqlColumnType)(col) === 'float';
198
+ }
199
+ catch {
200
+ return false;
201
+ }
202
+ }
203
+ /** A predicate that is always false — the empty-`in` / contradiction sentinel. */
204
+ alwaysFalse() {
205
+ const pk = this.meta.primaryKey[0] ?? this.meta.columns[0]?.name;
206
+ return `(.${pk} is null and .${pk} is not null)`;
207
+ }
208
+ // -------------------------------------------------------------------------
209
+ // WHERE builder
210
+ // -------------------------------------------------------------------------
211
+ /**
212
+ * Compile a {@link WhereClause} into a PowQL filter expression, pushing every
213
+ * value as a positional `$N` param. Returns `''` when there are no conditions.
214
+ */
215
+ buildWhere(where, params) {
216
+ if (!where)
217
+ return '';
218
+ const parts = [];
219
+ for (const [key, value] of Object.entries(where)) {
220
+ if (value === undefined)
221
+ continue;
222
+ if (key === 'AND') {
223
+ const sub = value.map((w) => this.buildWhere(w, params)).filter(Boolean);
224
+ if (sub.length)
225
+ parts.push(`(${sub.join(' and ')})`);
226
+ }
227
+ else if (key === 'OR') {
228
+ const sub = value.map((w) => this.buildWhere(w, params)).filter(Boolean);
229
+ if (sub.length)
230
+ parts.push(`(${sub.join(' or ')})`);
231
+ }
232
+ else if (key === 'NOT') {
233
+ const sub = this.buildWhere(value, params);
234
+ if (sub)
235
+ parts.push(`not (${sub})`);
236
+ }
237
+ else if (this.meta.relations[key]) {
238
+ // Relation filters are pre-resolved to scalar in/notIn by
239
+ // resolveRelationFilters() before buildWhere runs — reaching here means
240
+ // a caller skipped that step (an internal bug, not user error).
241
+ throw new errors_js_1.ValidationError(`[turbine] internal: relation filter "${key}" reached buildWhere unresolved (missing resolveRelationFilters()).`);
242
+ }
243
+ else {
244
+ parts.push(this.buildFieldCondition(key, value, params));
245
+ }
246
+ }
247
+ return parts.join(' and ');
248
+ }
249
+ /** Build a single `field: value | operator` condition. */
250
+ buildFieldCondition(field, value, params) {
251
+ const ref = this.ref(field);
252
+ if (value === null)
253
+ return `${ref} is null`;
254
+ if (value instanceof Date || typeof value !== 'object') {
255
+ return `${ref} = ${this.param(value, params)}`;
256
+ }
257
+ const op = value;
258
+ rejectUnsupportedFilter(op, field);
259
+ if (!Object.keys(op).some((k) => OPERATOR_KEYS.has(k))) {
260
+ // A bare object that is not an operator set — equality by value.
261
+ return `${ref} = ${this.param(value, params)}`;
262
+ }
263
+ const insensitive = op.mode === 'insensitive';
264
+ const lhs = insensitive ? `lower(${ref})` : ref;
265
+ const conds = [];
266
+ for (const [opName, opVal] of Object.entries(op)) {
267
+ if (opVal === undefined || opName === 'mode')
268
+ continue;
269
+ switch (opName) {
270
+ case 'equals':
271
+ conds.push(opVal === null ? `${ref} is null` : `${lhs} = ${this.bind(opVal, params, insensitive)}`);
272
+ break;
273
+ case 'not':
274
+ conds.push(opVal === null ? `${ref} is not null` : `not (${lhs} = ${this.bind(opVal, params, insensitive)})`);
275
+ break;
276
+ case 'gt':
277
+ conds.push(`${lhs} > ${this.bind(opVal, params, insensitive)}`);
278
+ break;
279
+ case 'gte':
280
+ conds.push(`${lhs} >= ${this.bind(opVal, params, insensitive)}`);
281
+ break;
282
+ case 'lt':
283
+ conds.push(`${lhs} < ${this.bind(opVal, params, insensitive)}`);
284
+ break;
285
+ case 'lte':
286
+ conds.push(`${lhs} <= ${this.bind(opVal, params, insensitive)}`);
287
+ break;
288
+ case 'in':
289
+ conds.push(this.buildInList(lhs, opVal, params, insensitive, false));
290
+ break;
291
+ case 'notIn':
292
+ conds.push(this.buildInList(lhs, opVal, params, insensitive, true));
293
+ break;
294
+ case 'contains':
295
+ conds.push(`${lhs} like ${this.bindLike(`%${(0, utils_js_1.escapeLike)(String(opVal))}%`, params, insensitive)}`);
296
+ break;
297
+ case 'startsWith':
298
+ conds.push(`${lhs} like ${this.bindLike(`${(0, utils_js_1.escapeLike)(String(opVal))}%`, params, insensitive)}`);
299
+ break;
300
+ case 'endsWith':
301
+ conds.push(`${lhs} like ${this.bindLike(`%${(0, utils_js_1.escapeLike)(String(opVal))}`, params, insensitive)}`);
302
+ break;
303
+ default:
304
+ throw new errors_js_1.ValidationError(`[turbine] Unsupported operator "${opName}" on PowDB.`);
305
+ }
306
+ }
307
+ return conds.length > 1 ? `(${conds.join(' and ')})` : (conds[0] ?? this.alwaysFalse());
308
+ }
309
+ /** Bind a value, lowercasing for case-insensitive comparisons. */
310
+ bind(value, params, insensitive) {
311
+ const ph = this.param(value, params);
312
+ return insensitive ? `lower(${ph})` : ph;
313
+ }
314
+ /** Bind a LIKE pattern (already escaped), lowercasing for insensitive mode. */
315
+ bindLike(pattern, params, insensitive) {
316
+ const ph = this.param(pattern, params);
317
+ return insensitive ? `lower(${ph})` : ph;
318
+ }
319
+ /** `lhs [not] in ($1, $2, …)` — empty list collapses to a constant. */
320
+ buildInList(lhs, values, params, insensitive, negate) {
321
+ if (!Array.isArray(values) || values.length === 0) {
322
+ // `in []` matches nothing; `not in []` matches everything.
323
+ return negate ? '(1 = 1)' : this.alwaysFalse();
324
+ }
325
+ const items = values.map((v) => this.bind(v, params, insensitive)).join(', ');
326
+ return `${lhs} ${negate ? 'not in' : 'in'} (${items})`;
327
+ }
328
+ /**
329
+ * Pre-resolve every relation filter (`some`/`none`/`every`) in a where clause
330
+ * into a plain scalar `in`/`notIn` condition on the **local key**, by running
331
+ * the inner predicate as its own query and materializing the matching keys as
332
+ * a literal list. The compiled where is then relation-free and `buildWhere`
333
+ * emits only `in (<literal list>)`.
334
+ *
335
+ * Why not an IN-subquery (`.k in (Target filter <e> { .fk })`)? PowDB's
336
+ * executor caches a subquery's result by **plan shape, ignoring the literal**,
337
+ * so a second subquery of the same shape with a different value returns the
338
+ * first one's stale rows (reproduced live on the embedded engine; the
339
+ * single-statement literal `in (list)` form is always correct). Resolving
340
+ * client-side trades extra round-trips for correctness, and recurses — nested
341
+ * relation filters in the inner predicate resolve when the target query runs.
342
+ */
343
+ async resolveRelationFilters(where, timeout) {
344
+ if (!where)
345
+ return where;
346
+ const scalar = {};
347
+ const relConds = [];
348
+ for (const [key, value] of Object.entries(where)) {
349
+ if (value === undefined)
350
+ continue;
351
+ if (key === 'AND' || key === 'OR') {
352
+ scalar[key] = await Promise.all(value.map((w) => this.resolveRelationFilters(w, timeout)));
353
+ }
354
+ else if (key === 'NOT') {
355
+ scalar[key] = await this.resolveRelationFilters(value, timeout);
356
+ }
357
+ else if (this.meta.relations[key]) {
358
+ relConds.push(await this.resolveRelationCondition(this.meta.relations[key], value, timeout));
359
+ }
360
+ else {
361
+ scalar[key] = value;
362
+ }
363
+ }
364
+ if (!relConds.length)
365
+ return scalar;
366
+ const hasScalar = Object.keys(scalar).length > 0;
367
+ return { AND: [...(hasScalar ? [scalar] : []), ...relConds] };
368
+ }
369
+ /** Resolve one hasMany/hasOne/belongsTo filter to `{ localField: { in|notIn: [...] } }`. */
370
+ async resolveRelationCondition(rel, filter, timeout) {
371
+ const mode = filter.some ? 'some' : filter.none ? 'none' : filter.every ? 'every' : null;
372
+ if (!mode)
373
+ return {};
374
+ const innerWhere = filter[mode];
375
+ const innerEmpty = !innerWhere || Object.keys(innerWhere).length === 0;
376
+ if (rel.type === 'manyToMany') {
377
+ return this.resolveManyToManyCondition(rel, mode, innerWhere, innerEmpty, timeout);
378
+ }
379
+ const fk = (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
380
+ const rk = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
381
+ if (fk.length > 1 || rk.length > 1) {
382
+ throw new errors_js_1.UnsupportedFeatureError('composite-key relation filters', 'PowDB', `relation "${rel.name}" uses a composite key — PowQL has no tuple-\`in\` to express it`);
383
+ }
384
+ const targetMeta = this.schema.tables[rel.to];
385
+ if (!targetMeta)
386
+ throw new errors_js_1.ValidationError(`[turbine] Relation "${rel.name}" targets unknown table "${rel.to}".`);
387
+ // hasMany/hasOne: localKey = our referenceKey, collect the child's foreignKey.
388
+ // belongsTo: localKey = our foreignKey, collect the target's referenceKey.
389
+ const localCol = rel.type === 'belongsTo' ? fk[0] : rk[0];
390
+ const childCol = rel.type === 'belongsTo' ? rk[0] : fk[0];
391
+ const localField = this.meta.reverseColumnMap[localCol] ?? localCol;
392
+ const childField = targetMeta.reverseColumnMap[childCol] ?? childCol;
393
+ const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
394
+ const collect = async (w) => {
395
+ const rows = await targetQi.findMany({
396
+ where: w,
397
+ select: { [childField]: true },
398
+ timeout,
399
+ });
400
+ return [...new Set(rows.map((r) => r[childField]).filter((v) => v != null))];
401
+ };
402
+ if (mode === 'some')
403
+ return { [localField]: { in: await collect(innerWhere) } };
404
+ if (mode === 'none')
405
+ return { [localField]: { notIn: await collect(innerWhere) } };
406
+ // every: a parent qualifies unless it has a child FAILING the predicate.
407
+ if (innerEmpty)
408
+ return {}; // every:{} ⇒ trivially true for all parents
409
+ return { [localField]: { notIn: await collect({ NOT: innerWhere }) } };
410
+ }
411
+ /** Resolve a manyToMany filter through the junction to `{ sourceRefField: { in|notIn: [...] } }`. */
412
+ async resolveManyToManyCondition(rel, mode, innerWhere, innerEmpty, timeout) {
413
+ const through = rel.through;
414
+ if (!through)
415
+ throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${rel.name}" is missing its junction (\`through\`).`);
416
+ const sourceJ = (0, schema_js_1.normalizeKeyColumns)(through.sourceKey);
417
+ const targetJ = (0, schema_js_1.normalizeKeyColumns)(through.targetKey);
418
+ const sourceRef = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
419
+ const targetMeta = this.schema.tables[rel.to];
420
+ if (!targetMeta)
421
+ throw new errors_js_1.ValidationError(`[turbine] Relation "${rel.name}" targets unknown table "${rel.to}".`);
422
+ if (sourceJ.length > 1 || targetJ.length > 1 || sourceRef.length > 1 || targetMeta.primaryKey.length > 1) {
423
+ throw new errors_js_1.UnsupportedFeatureError('composite-key manyToMany filters', 'PowDB', `relation "${rel.name}" — PowQL has no tuple-\`in\` for composite junction/target keys`);
424
+ }
425
+ const sourceJCol = sourceJ[0];
426
+ const targetJCol = targetJ[0];
427
+ const sourceRefCol = sourceRef[0];
428
+ const targetPkCol = targetMeta.primaryKey[0];
429
+ const sourceRefField = this.meta.reverseColumnMap[sourceRefCol] ?? sourceRefCol;
430
+ const sourceRefColMeta = this.meta.columns.find((c) => c.name === sourceRefCol);
431
+ const targetPkField = targetMeta.reverseColumnMap[targetPkCol] ?? targetPkCol;
432
+ const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
433
+ const collectTargetPks = async (w) => {
434
+ const rows = await targetQi.findMany({
435
+ where: w,
436
+ select: { [targetPkField]: true },
437
+ timeout,
438
+ });
439
+ return [...new Set(rows.map((r) => r[targetPkField]).filter((v) => v != null))];
440
+ };
441
+ // Junction source keys linking any of `targetPks` (literal IN-list — never a subquery).
442
+ const sourcesForTargets = async (targetPks) => {
443
+ if (!targetPks.length)
444
+ return [];
445
+ const out = new Set();
446
+ for (let i = 0; i < targetPks.length; i += MAX_RELATION_KEYS) {
447
+ const chunk = targetPks.slice(i, i + MAX_RELATION_KEYS);
448
+ const params = [];
449
+ const ph = chunk.map((v) => this.param(v, params)).join(', ');
450
+ const { rows } = await this.exec(`${through.table} filter .${targetJCol} in (${ph}) { .${sourceJCol} }`, params, timeout);
451
+ for (const r of rows) {
452
+ const v = r[sourceJCol];
453
+ if (v != null)
454
+ out.add(sourceRefColMeta ? coerceScalar(String(v), sourceRefColMeta.tsType) : v);
455
+ }
456
+ }
457
+ return [...out];
458
+ };
459
+ if (mode === 'some') {
460
+ return { [sourceRefField]: { in: await sourcesForTargets(await collectTargetPks(innerWhere)) } };
461
+ }
462
+ if (mode === 'none') {
463
+ return { [sourceRefField]: { notIn: await sourcesForTargets(await collectTargetPks(innerWhere)) } };
464
+ }
465
+ // every: exclude parents that link a target FAILING the predicate.
466
+ if (innerEmpty)
467
+ return {}; // every:{} ⇒ trivially true
468
+ const failing = await sourcesForTargets(await collectTargetPks({ NOT: innerWhere }));
469
+ return { [sourceRefField]: { notIn: failing } };
470
+ }
471
+ // -------------------------------------------------------------------------
472
+ // Projection / order
473
+ // -------------------------------------------------------------------------
474
+ /** Resolve the set of columns to project, honouring `select` / `omit`. */
475
+ projectedColumns(select, omit) {
476
+ let cols = this.meta.columns.map((c) => c.name);
477
+ if (select && Object.keys(select).length) {
478
+ const picked = new Set(Object.entries(select)
479
+ .filter(([, v]) => v)
480
+ .map(([k]) => this.column(k).name));
481
+ // Always keep the PK so reselect / relation stitching has a key to work with.
482
+ for (const pk of this.meta.primaryKey)
483
+ picked.add(pk);
484
+ cols = cols.filter((c) => picked.has(c));
485
+ }
486
+ if (omit && Object.keys(omit).length) {
487
+ const dropped = new Set(Object.entries(omit)
488
+ .filter(([, v]) => v)
489
+ .map(([k]) => this.column(k).name));
490
+ cols = cols.filter((c) => !dropped.has(c));
491
+ }
492
+ return cols;
493
+ }
494
+ /** `{ .c1, .c2, … }` projection clause. */
495
+ projection(cols) {
496
+ return `{ ${cols.map((c) => `.${c}`).join(', ')} }`;
497
+ }
498
+ /** `order .c1 asc, .c2 desc` clause (empty string when no orderBy). */
499
+ buildOrder(orderBy) {
500
+ if (!orderBy)
501
+ return '';
502
+ const keys = Object.entries(orderBy).filter(([, dir]) => dir !== undefined);
503
+ if (!keys.length)
504
+ return '';
505
+ const parts = keys.map(([field, dir]) => {
506
+ if (dir && typeof dir === 'object') {
507
+ throw new errors_js_1.UnsupportedFeatureError('vector / distance ordering', 'PowDB', `field "${field}"`);
508
+ }
509
+ return `${this.ref(field)} ${dir === 'desc' ? 'desc' : 'asc'}`;
510
+ });
511
+ return ` order ${parts.join(', ')}`;
512
+ }
513
+ // -------------------------------------------------------------------------
514
+ // Execution plumbing
515
+ // -------------------------------------------------------------------------
516
+ /** Run PowQL with optional timeout, emitting a query event either way. */
517
+ async exec(powql, params, timeout) {
518
+ const start = performance.now();
519
+ const run = this.pool.query(powql, params);
520
+ try {
521
+ const result = timeout
522
+ ? await Promise.race([
523
+ run,
524
+ new Promise((_, reject) => setTimeout(() => reject(new errors_js_1.TimeoutError(timeout)), timeout)),
525
+ ])
526
+ : await run;
527
+ this.emit(powql, params, performance.now() - start, result.rowCount ?? result.rows.length);
528
+ return result;
529
+ }
530
+ catch (err) {
531
+ this.emit(powql, params, performance.now() - start, 0, err);
532
+ throw err;
533
+ }
534
+ }
535
+ emit(sql, params, duration, rows, error) {
536
+ if (!this.onQuery)
537
+ return;
538
+ try {
539
+ this.onQuery({
540
+ sql,
541
+ params,
542
+ duration,
543
+ model: this.table,
544
+ action: this.currentAction,
545
+ rows,
546
+ timestamp: new Date(),
547
+ error,
548
+ });
549
+ }
550
+ catch {
551
+ /* listener errors never crash a query */
552
+ }
553
+ }
554
+ /** Run a method body through the middleware chain (mirrors QueryInterface). */
555
+ async withMiddleware(action, args, executor) {
556
+ this.currentAction = action;
557
+ if (this.middlewares.length === 0)
558
+ return executor();
559
+ let index = 0;
560
+ const next = async (p) => {
561
+ if (index < this.middlewares.length)
562
+ return this.middlewares[index++](p, next);
563
+ return executor();
564
+ };
565
+ return next({ model: this.table, action, args: { ...args } });
566
+ }
567
+ /** Map raw rows to typed entities. */
568
+ shape(rows) {
569
+ return rows.map((r) => (0, powdb_js_1.rowToEntity)(r, this.meta));
570
+ }
571
+ // -------------------------------------------------------------------------
572
+ // Reads
573
+ // -------------------------------------------------------------------------
574
+ async findMany(args = {}) {
575
+ return this.withMiddleware('findMany', args, async () => {
576
+ const rows = await this.runFind(args);
577
+ const entities = this.shape(rows);
578
+ if (args.with)
579
+ await this.loadRelations(entities, args.with, args.timeout);
580
+ return entities;
581
+ });
582
+ }
583
+ /** Build + run the flat findMany select; returns raw rows. */
584
+ async runFind(args) {
585
+ if (args.cursor) {
586
+ throw new errors_js_1.UnsupportedFeatureError('cursor pagination', 'PowDB', 'use limit/offset instead');
587
+ }
588
+ const params = [];
589
+ const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
590
+ const where = this.buildWhere(resolvedWhere, params);
591
+ const cols = this.projectedColumns(args.select, args.omit);
592
+ const distinct = args.distinct?.length ? ' distinct' : '';
593
+ const filter = where ? ` filter ${where}` : '';
594
+ const order = this.buildOrder(args.orderBy);
595
+ const limit = args.limit ?? args.take ?? this.defaultLimit;
596
+ if (limit === undefined && this.warnOnUnlimited && !this.warnedUnlimited) {
597
+ this.warnedUnlimited = true;
598
+ console.warn(`[turbine] findMany on "${this.table}" has no limit — this scans the whole table.`);
599
+ }
600
+ const limitClause = limit !== undefined ? ` limit ${this.param(limit, params)}` : '';
601
+ const offsetClause = args.offset ? ` offset ${this.param(args.offset, params)}` : '';
602
+ const powql = `${this.table}${distinct}${filter}${order}${limitClause}${offsetClause} ${this.projection(cols)}`;
603
+ const { rows } = await this.exec(powql, params, args.timeout);
604
+ return rows;
605
+ }
606
+ async findUnique(args) {
607
+ return this.withMiddleware('findUnique', args, async () => {
608
+ const rows = await this.runFind({ ...args, limit: 1 });
609
+ if (!rows.length)
610
+ return null;
611
+ const entities = this.shape(rows);
612
+ if (args.with)
613
+ await this.loadRelations(entities, args.with, args.timeout);
614
+ return entities[0];
615
+ });
616
+ }
617
+ async findFirst(args = {}) {
618
+ return this.withMiddleware('findFirst', args, async () => {
619
+ const rows = await this.runFind({ ...args, limit: 1 });
620
+ if (!rows.length)
621
+ return null;
622
+ const entities = this.shape(rows);
623
+ if (args.with)
624
+ await this.loadRelations(entities, args.with, args.timeout);
625
+ return entities[0];
626
+ });
627
+ }
628
+ async findUniqueOrThrow(args) {
629
+ const row = await this.findUnique(args);
630
+ if (!row)
631
+ throw new errors_js_1.NotFoundError({ table: this.table, where: args.where });
632
+ return row;
633
+ }
634
+ async findFirstOrThrow(args = {}) {
635
+ const row = await this.findFirst(args);
636
+ if (!row)
637
+ throw new errors_js_1.NotFoundError({ table: this.table, where: (args.where ?? {}) });
638
+ return row;
639
+ }
640
+ // -------------------------------------------------------------------------
641
+ // Nested relations — batched N+1 loaders (hasMany / hasOne / belongsTo)
642
+ // -------------------------------------------------------------------------
643
+ /** Load each requested relation for `parents` and attach it onto each row. */
644
+ async loadRelations(parents, withClause, timeout, depth = 0) {
645
+ if (depth >= 10) {
646
+ throw new errors_js_1.ValidationError(`[turbine] Nested 'with' on PowDB exceeded depth 10 (relation cycle?).`);
647
+ }
648
+ if (!parents.length)
649
+ return;
650
+ for (const [relName, opt] of Object.entries(withClause)) {
651
+ if (!opt)
652
+ continue;
653
+ const rel = this.meta.relations[relName];
654
+ if (!rel)
655
+ throw new errors_js_1.ValidationError(`[turbine] Unknown relation "${relName}" on "${this.table}".`);
656
+ if (rel.type === 'manyToMany') {
657
+ await this.loadManyToMany(parents, rel, relName, opt, timeout);
658
+ continue;
659
+ }
660
+ const fk = (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
661
+ const rk = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
662
+ if (fk.length > 1 || rk.length > 1) {
663
+ throw new errors_js_1.UnsupportedFeatureError('composite-key nested reads', 'PowDB', `relation "${relName}"`);
664
+ }
665
+ const options = (opt === true ? {} : opt);
666
+ const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
667
+ const targetMeta = this.schema.tables[rel.to];
668
+ // Local key on the parent, remote key on the target child.
669
+ const parentKeyCol = rel.type === 'belongsTo' ? fk[0] : rk[0];
670
+ const childKeyCol = rel.type === 'belongsTo' ? rk[0] : fk[0];
671
+ const parentKeyField = this.meta.reverseColumnMap[parentKeyCol] ?? parentKeyCol;
672
+ const childKeyField = targetMeta.reverseColumnMap[childKeyCol] ?? childKeyCol;
673
+ const keys = [
674
+ ...new Set(parents.map((p) => p[parentKeyField]).filter((k) => k != null)),
675
+ ];
676
+ const childByKey = new Map();
677
+ // Chunk the key set so a single `in (…)` never exceeds PowDB's
678
+ // per-statement param / row limits; merge each chunk's children.
679
+ for (let i = 0; i < keys.length; i += MAX_RELATION_KEYS) {
680
+ const chunk = keys.slice(i, i + MAX_RELATION_KEYS);
681
+ const childWhere = {
682
+ ...options.where,
683
+ [childKeyField]: { in: chunk },
684
+ };
685
+ const children = (await targetQi.findMany({
686
+ ...options,
687
+ where: childWhere,
688
+ with: options.with,
689
+ timeout: options.timeout ?? timeout,
690
+ }));
691
+ for (const child of children) {
692
+ const k = child[childKeyField];
693
+ const bucket = childByKey.get(k);
694
+ if (bucket)
695
+ bucket.push(child);
696
+ else
697
+ childByKey.set(k, [child]);
698
+ }
699
+ }
700
+ const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
701
+ for (const parent of parents) {
702
+ const k = parent[parentKeyField];
703
+ const matches = childByKey.get(k) ?? [];
704
+ parent[relName] = single ? (matches[0] ?? null) : matches;
705
+ }
706
+ }
707
+ }
708
+ /**
709
+ * manyToMany nested read — a three-hop batched loader (no `json_agg`/join
710
+ * pushdown): (1) read the junction rows for all parents in `sourceKey in (…)`
711
+ * chunks, (2) read the target rows for the collected `targetKey`s, (3) stitch
712
+ * each parent → its junction rows → its targets in memory. Mirrors the
713
+ * single-key N+1 loaders; the junction's source/target columns must be single
714
+ * (composite junction keys would need PowQL tuple-`in`, which it lacks).
715
+ */
716
+ async loadManyToMany(parents, rel, relName, opt, timeout) {
717
+ const through = rel.through;
718
+ if (!through)
719
+ throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relName}" is missing its junction (\`through\`).`);
720
+ const sourceJ = (0, schema_js_1.normalizeKeyColumns)(through.sourceKey);
721
+ const targetJ = (0, schema_js_1.normalizeKeyColumns)(through.targetKey);
722
+ const sourceRef = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
723
+ const targetMeta = this.schema.tables[rel.to];
724
+ if (!targetMeta)
725
+ throw new errors_js_1.ValidationError(`[turbine] Relation "${relName}" targets unknown table "${rel.to}".`);
726
+ if (sourceJ.length > 1 || targetJ.length > 1 || sourceRef.length > 1 || targetMeta.primaryKey.length > 1) {
727
+ throw new errors_js_1.UnsupportedFeatureError('composite-key manyToMany', 'PowDB', `relation "${relName}" — PowQL has no tuple-\`in\`, so composite junction/target keys can't be loaded`);
728
+ }
729
+ const sourceJCol = sourceJ[0];
730
+ const targetJCol = targetJ[0];
731
+ const sourceRefCol = sourceRef[0];
732
+ const targetPkCol = targetMeta.primaryKey[0];
733
+ const parentRefField = this.meta.reverseColumnMap[sourceRefCol] ?? sourceRefCol;
734
+ const targetPkField = targetMeta.reverseColumnMap[targetPkCol] ?? targetPkCol;
735
+ const targetPkColMeta = targetMeta.columns.find((c) => c.name === targetPkCol);
736
+ const parentKeys = [
737
+ ...new Set(parents.map((p) => p[parentRefField]).filter((k) => k != null)),
738
+ ];
739
+ if (!parentKeys.length) {
740
+ for (const parent of parents)
741
+ parent[relName] = [];
742
+ return;
743
+ }
744
+ // (1) Junction rows: sourceKeyVal(String) → [targetKeyVal(String)].
745
+ const targetsBySource = new Map();
746
+ const allTargetVals = new Set();
747
+ for (let i = 0; i < parentKeys.length; i += MAX_RELATION_KEYS) {
748
+ const chunk = parentKeys.slice(i, i + MAX_RELATION_KEYS);
749
+ const params = [];
750
+ const placeholders = chunk.map((v) => this.param(v, params)).join(', ');
751
+ const powql = `${through.table} filter .${sourceJCol} in (${placeholders}) { .${sourceJCol}, .${targetJCol} }`;
752
+ const { rows } = await this.exec(powql, params, timeout);
753
+ for (const row of rows) {
754
+ const sv = String(row[sourceJCol]);
755
+ const tv = String(row[targetJCol]);
756
+ const bucket = targetsBySource.get(sv);
757
+ if (bucket)
758
+ bucket.push(tv);
759
+ else
760
+ targetsBySource.set(sv, [tv]);
761
+ allTargetVals.add(tv);
762
+ }
763
+ }
764
+ // (2) Target rows by PK, honouring the relation's own where/with/select/…
765
+ const options = (opt === true ? {} : opt);
766
+ const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
767
+ const targetByPk = new Map();
768
+ const targetValList = [...allTargetVals].map((v) => targetPkColMeta ? coerceScalar(v, targetPkColMeta.tsType) : v);
769
+ for (let i = 0; i < targetValList.length; i += MAX_RELATION_KEYS) {
770
+ const chunk = targetValList.slice(i, i + MAX_RELATION_KEYS);
771
+ const where = {
772
+ ...options.where,
773
+ [targetPkField]: { in: chunk },
774
+ };
775
+ const targets = (await targetQi.findMany({
776
+ ...options,
777
+ where,
778
+ with: options.with,
779
+ timeout: options.timeout ?? timeout,
780
+ }));
781
+ for (const t of targets)
782
+ targetByPk.set(String(t[targetPkField]), t);
783
+ }
784
+ // (3) Stitch: each parent → its junction targets (m2m is always a list).
785
+ for (const parent of parents) {
786
+ const sv = String(parent[parentRefField]);
787
+ const tvs = targetsBySource.get(sv) ?? [];
788
+ const children = [];
789
+ for (const tv of tvs) {
790
+ const child = targetByPk.get(tv);
791
+ if (child)
792
+ children.push(child);
793
+ }
794
+ parent[relName] = children;
795
+ }
796
+ }
797
+ // -------------------------------------------------------------------------
798
+ // Writes (reselect — PowDB has no RETURNING)
799
+ // -------------------------------------------------------------------------
800
+ /** Split `data` into scalar assignments; reject relation (nested-write) keys. */
801
+ scalarData(data) {
802
+ const out = [];
803
+ for (const [field, value] of Object.entries(data)) {
804
+ if (value === undefined)
805
+ continue;
806
+ if (this.meta.relations[field]) {
807
+ throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}" — nested writes need create()/update(), not createMany()/upsert()`);
808
+ }
809
+ out.push({ col: this.column(field), value });
810
+ }
811
+ return out;
812
+ }
813
+ /**
814
+ * Fill in a client-generated UUID for a defaulted **string** PK that wasn't
815
+ * supplied. A server-generated PK ({@link ColumnMetadata.isGenerated}, e.g. an
816
+ * `int` column with PowDB's `auto` modifier) is left untouched — PowDB assigns
817
+ * it and the trailing `returning` reads it back — as is any non-string PK.
818
+ */
819
+ applyPkDefault(data) {
820
+ const out = { ...data };
821
+ for (const pk of this.meta.primaryKey) {
822
+ const field = this.meta.reverseColumnMap[pk] ?? pk;
823
+ const col = this.meta.columns.find((c) => c.name === pk);
824
+ if (out[field] == null &&
825
+ col?.hasDefault &&
826
+ !col.isGenerated &&
827
+ col.tsType.replace(/\s*\|\s*null$/, '').trim() === 'string') {
828
+ out[field] = (0, node_crypto_1.randomUUID)();
829
+ }
830
+ }
831
+ return out;
832
+ }
833
+ async create(args) {
834
+ return this.withMiddleware('create', args, async () => {
835
+ if ((0, nested_write_js_1.hasRelationFields)(args.data, this.meta)) {
836
+ return this.nestedCreate(args);
837
+ }
838
+ const data = this.applyPkDefault(args.data);
839
+ const assigns = this.scalarData(data);
840
+ const params = [];
841
+ const body = assigns.map((a) => `${a.col.name} := ${this.writeRef(a.value, a.col, params)}`).join(', ');
842
+ // `returning` surfaces the inserted row (all columns, schema order) in one round-trip.
843
+ const { rows } = await this.exec(`insert ${this.table} { ${body} } returning`, params, args.timeout);
844
+ const row = rows.length ? this.shape(rows)[0] : null;
845
+ if (!row)
846
+ throw new errors_js_1.NotFoundError({ table: this.table, where: data });
847
+ return row;
848
+ });
849
+ }
850
+ async createMany(args) {
851
+ return this.withMiddleware('createMany', args, async () => {
852
+ const inputs = args.data.map((d) => this.applyPkDefault(d));
853
+ if (!inputs.length)
854
+ return [];
855
+ const params = [];
856
+ const tuples = inputs.map((d) => {
857
+ const assigns = this.scalarData(d);
858
+ return `{ ${assigns.map((a) => `${a.col.name} := ${this.writeRef(a.value, a.col, params)}`).join(', ')} }`;
859
+ });
860
+ // Multi-row insert with `returning` hands back every inserted row in one round-trip.
861
+ const { rows } = await this.exec(`insert ${this.table} ${tuples.join(', ')} returning`, params, args.timeout);
862
+ return this.shape(rows);
863
+ });
864
+ }
865
+ async update(args) {
866
+ return this.withMiddleware('update', args, async () => {
867
+ if ((0, nested_write_js_1.hasRelationFields)(args.data, this.meta)) {
868
+ return this.nestedUpdate(args);
869
+ }
870
+ const params = [];
871
+ const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
872
+ const where = this.buildWhere(resolvedWhere, params);
873
+ this.assertCompiledWhere(where, false, 'update');
874
+ const setClause = this.buildUpdateAssignments(args.data, params);
875
+ // `returning` hands back the post-update row(s); take the first (single-row contract).
876
+ const { rows } = await this.exec(`${this.table} filter ${where} update { ${setClause} } returning`, params, args.timeout);
877
+ const row = rows.length ? this.shape(rows)[0] : null;
878
+ if (!row)
879
+ throw new errors_js_1.NotFoundError({ table: this.table, where: args.where });
880
+ return row;
881
+ });
882
+ }
883
+ async updateMany(args) {
884
+ return this.withMiddleware('updateMany', args, async () => {
885
+ const params = [];
886
+ const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
887
+ const where = this.buildWhere(resolvedWhere, params);
888
+ this.assertCompiledWhere(where, args.allowFullTableScan, 'updateMany');
889
+ const setClause = this.buildUpdateAssignments(args.data, params);
890
+ const filter = where ? ` filter ${where}` : '';
891
+ const { rowCount } = await this.exec(`${this.table}${filter} update { ${setClause} }`, params, args.timeout);
892
+ return { count: rowCount };
893
+ });
894
+ }
895
+ /** Compile `data` into PowQL update assignments, including atomic operators. */
896
+ buildUpdateAssignments(data, params) {
897
+ const parts = [];
898
+ for (const [field, value] of Object.entries(data)) {
899
+ if (value === undefined)
900
+ continue;
901
+ if (this.meta.relations[field]) {
902
+ throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}" — nested writes need create()/update(), not updateMany()/upsert()`);
903
+ }
904
+ const colMeta = this.column(field);
905
+ const ref = this.ref(field);
906
+ const col = colMeta.name;
907
+ if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
908
+ const opObj = value;
909
+ if ('set' in opObj)
910
+ parts.push(`${col} := ${this.writeRef(opObj.set, colMeta, params)}`);
911
+ else if ('increment' in opObj)
912
+ parts.push(`${col} := ${ref} + ${this.writeRef(opObj.increment, colMeta, params)}`);
913
+ else if ('decrement' in opObj)
914
+ parts.push(`${col} := ${ref} - ${this.writeRef(opObj.decrement, colMeta, params)}`);
915
+ else if ('multiply' in opObj)
916
+ parts.push(`${col} := ${ref} * ${this.writeRef(opObj.multiply, colMeta, params)}`);
917
+ else if ('divide' in opObj)
918
+ parts.push(`${col} := ${ref} / ${this.writeRef(opObj.divide, colMeta, params)}`);
919
+ else
920
+ throw new errors_js_1.ValidationError(`[turbine] Unsupported update operator on "${field}".`);
921
+ }
922
+ else {
923
+ parts.push(`${col} := ${this.writeRef(value, colMeta, params)}`);
924
+ }
925
+ }
926
+ if (!parts.length)
927
+ throw new errors_js_1.ValidationError(`[turbine] update on "${this.table}" has no fields to set.`);
928
+ return parts.join(', ');
929
+ }
930
+ // -------------------------------------------------------------------------
931
+ // Nested writes — create/update whose `data` carries relation ops (create,
932
+ // connect, connectOrCreate, disconnect, set, delete, update, upsert). Reuses
933
+ // the engine-agnostic nested-write engine (it only needs ctx.schema + the
934
+ // table accessors PowqlInterface already provides). Runs as ONE flat top-level
935
+ // PowDB transaction — single global write lock, no savepoints, so the whole
936
+ // tree commits or rolls back together (mirrors the SQL path's coverage:
937
+ // hasMany / hasOne / belongsTo; manyToMany nested writes are not handled by
938
+ // the shared engine on any backend).
939
+ // -------------------------------------------------------------------------
940
+ isTxScoped() {
941
+ return this.options._txScoped === true;
942
+ }
943
+ async nestedCreate(args) {
944
+ const data = args.data;
945
+ if (this.isTxScoped()) {
946
+ return (0, nested_write_js_1.executeNestedCreate)(this.buildNestedCtx(), this.table, data);
947
+ }
948
+ return this.runInImplicitTx((ctx) => (0, nested_write_js_1.executeNestedCreate)(ctx, this.table, data));
949
+ }
950
+ async nestedUpdate(args) {
951
+ const data = args.data;
952
+ const where = args.where;
953
+ if (this.isTxScoped()) {
954
+ return (0, nested_write_js_1.executeNestedUpdate)(this.buildNestedCtx(), this.table, where, data);
955
+ }
956
+ return this.runInImplicitTx((ctx) => (0, nested_write_js_1.executeNestedUpdate)(ctx, this.table, where, data));
957
+ }
958
+ /** Open a flat PowDB transaction on a pinned connection and run `fn` inside it. */
959
+ async runInImplicitTx(fn) {
960
+ // Route tx keywords through the dialect (like the SQL path) so this never
961
+ // drifts from `powdbDialect`; falls back to the literal lowercase keywords.
962
+ const d = this.options.dialect;
963
+ const client = await this.pool.connect();
964
+ try {
965
+ await client.query(d?.beginStatement?.() ?? 'begin');
966
+ const { TransactionClient } = await Promise.resolve().then(() => __importStar(require('./client.js')));
967
+ const tx = new TransactionClient(client, this.schema, this.middlewares, this.options);
968
+ const ctx = { schema: this.schema, tx: tx };
969
+ const result = await fn(ctx);
970
+ await client.query(d?.commitStatement?.() ?? 'commit');
971
+ return result;
972
+ }
973
+ catch (err) {
974
+ try {
975
+ await client.query(d?.rollbackStatement?.() ?? 'rollback');
976
+ }
977
+ catch {
978
+ /* best-effort — the connection may be gone */
979
+ }
980
+ throw err;
981
+ }
982
+ finally {
983
+ client.release();
984
+ }
985
+ }
986
+ /** Already inside a transaction: build a context whose table accessors reuse the pinned pool. */
987
+ buildNestedCtx() {
988
+ const opts = { ...this.options, _txScoped: true };
989
+ const tx = {
990
+ table: (name) => new PowqlInterface(this.pool, name, this.schema, this.middlewares, opts),
991
+ };
992
+ return { schema: this.schema, tx: tx };
993
+ }
994
+ async delete(args) {
995
+ return this.withMiddleware('delete', args, async () => {
996
+ const params = [];
997
+ const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
998
+ const where = this.buildWhere(resolvedWhere, params);
999
+ this.assertCompiledWhere(where, false, 'delete');
1000
+ // `returning` hands back the deleted row(s) — no separate pre-image reselect needed.
1001
+ const { rows } = await this.exec(`${this.table} filter ${where} delete returning`, params, args.timeout);
1002
+ const row = rows.length ? this.shape(rows)[0] : null;
1003
+ if (!row)
1004
+ throw new errors_js_1.NotFoundError({ table: this.table, where: args.where });
1005
+ return row;
1006
+ });
1007
+ }
1008
+ async deleteMany(args) {
1009
+ return this.withMiddleware('deleteMany', args, async () => {
1010
+ const params = [];
1011
+ const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
1012
+ const where = this.buildWhere(resolvedWhere, params);
1013
+ this.assertCompiledWhere(where, args.allowFullTableScan, 'deleteMany');
1014
+ const filter = where ? ` filter ${where}` : '';
1015
+ const { rowCount } = await this.exec(`${this.table}${filter} delete`, params, args.timeout);
1016
+ return { count: rowCount };
1017
+ });
1018
+ }
1019
+ async upsert(args) {
1020
+ return this.withMiddleware('upsert', args, async () => {
1021
+ const createData = this.applyPkDefault(args.create);
1022
+ const pkCol = this.meta.primaryKey[0];
1023
+ if (this.meta.primaryKey.length !== 1 || !pkCol) {
1024
+ // PowQL's native `upsert … on .col` takes a single conflict column, so a
1025
+ // composite PK falls back to an atomic reselect-or-write transaction.
1026
+ return this.upsertComposite(createData, args.update);
1027
+ }
1028
+ const params = [];
1029
+ const createBody = this.scalarData(createData)
1030
+ .map((a) => `${a.col.name} := ${this.writeRef(a.value, a.col, params)}`)
1031
+ .join(', ');
1032
+ const updateBody = this.buildUpdateAssignments(args.update, params);
1033
+ // PowDB 0.7.0's `upsert` statement does NOT accept a trailing `returning`
1034
+ // (verified: "unexpected trailing token … 'returning'"), because it is one
1035
+ // atomic insert-or-update, not two branches. So upsert alone keeps the
1036
+ // reselect-by-PK fetch; create/update/delete all use `returning`.
1037
+ await this.exec(`upsert ${this.table} on .${pkCol} { ${createBody} } on conflict { ${updateBody} }`, params, args.timeout);
1038
+ const pkField = this.meta.reverseColumnMap[pkCol] ?? pkCol;
1039
+ const row = await this.reselectByPk(createData[pkField], args.timeout);
1040
+ if (!row)
1041
+ throw new errors_js_1.NotFoundError({ table: this.table, where: createData });
1042
+ return row;
1043
+ });
1044
+ }
1045
+ /**
1046
+ * Composite-key upsert: PowQL's `upsert … on .col` only takes one conflict
1047
+ * column, so reselect by the full composite PK and update-or-create inside one
1048
+ * flat transaction (PowDB single-writer makes the read-then-write safe from
1049
+ * concurrent writers; the transaction makes it atomic with the write).
1050
+ */
1051
+ async upsertComposite(createData, updateData) {
1052
+ // Accept either the camelCase field or the snake_case column in `create`
1053
+ // (create() resolves both), and key the where by field name.
1054
+ const pkPairs = this.meta.primaryKey.map((pk) => {
1055
+ const field = this.meta.reverseColumnMap[pk] ?? pk;
1056
+ return { field, value: createData[field] ?? createData[pk] };
1057
+ });
1058
+ if (pkPairs.some((p) => p.value == null)) {
1059
+ throw new errors_js_1.ValidationError(`[turbine] upsert on "${this.table}" needs every composite-PK field in \`create\` (${pkPairs
1060
+ .map((p) => p.field)
1061
+ .join(', ')}).`);
1062
+ }
1063
+ const pkWhere = Object.fromEntries(pkPairs.map((p) => [p.field, p.value]));
1064
+ const run = async (ctx) => {
1065
+ const tbl = ctx.tx.table(this.table);
1066
+ const existing = await tbl.findUnique({ where: pkWhere });
1067
+ return existing
1068
+ ? (await tbl.update({ where: pkWhere, data: updateData }))
1069
+ : (await tbl.create({ data: createData }));
1070
+ };
1071
+ return this.isTxScoped() ? run(this.buildNestedCtx()) : this.runInImplicitTx(run);
1072
+ }
1073
+ // -------------------------------------------------------------------------
1074
+ // Aggregates
1075
+ // -------------------------------------------------------------------------
1076
+ async count(args = {}) {
1077
+ return this.withMiddleware('count', (args ?? {}), async () => {
1078
+ const params = [];
1079
+ const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
1080
+ const where = this.buildWhere(resolvedWhere, params);
1081
+ const filter = where ? ` filter ${where}` : '';
1082
+ const { rows } = await this.exec(`count(${this.table}${filter})`, params, args.timeout);
1083
+ return Number((rows[0]?.value ?? rows[0]?.count ?? 0));
1084
+ });
1085
+ }
1086
+ async aggregate(args) {
1087
+ return this.withMiddleware('aggregate', args, async () => {
1088
+ // One scalar query per aggregate — PowDB's bare-projection aggregate is broken.
1089
+ const result = {};
1090
+ const filterParams = [];
1091
+ const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
1092
+ const where = this.buildWhere(resolvedWhere, filterParams);
1093
+ const filter = where ? ` filter ${where}` : '';
1094
+ const scalar = async (expr) => {
1095
+ const params = [...filterParams];
1096
+ const { rows } = await this.exec(expr, params, args.timeout);
1097
+ const v = rows[0]?.value;
1098
+ return v == null || v === 'null' ? null : Number(v);
1099
+ };
1100
+ if (args._count) {
1101
+ if (args._count === true) {
1102
+ result._count = (await scalar(`count(${this.table}${filter})`)) ?? 0;
1103
+ }
1104
+ else {
1105
+ const counts = {};
1106
+ for (const field of Object.keys(args._count).filter((f) => args._count[f])) {
1107
+ counts[field] = (await scalar(`count(${this.table}${filter} { ${this.ref(field)} })`)) ?? 0;
1108
+ }
1109
+ result._count = counts;
1110
+ }
1111
+ }
1112
+ for (const fn of ['_sum', '_avg', '_min', '_max']) {
1113
+ const spec = args[fn];
1114
+ if (!spec)
1115
+ continue;
1116
+ const acc = {};
1117
+ for (const field of Object.keys(spec).filter((f) => spec[f])) {
1118
+ const powfn = fn.slice(1); // sum/avg/min/max
1119
+ acc[field] = await scalar(`${powfn}(${this.table}${filter} { ${this.ref(field)} })`);
1120
+ }
1121
+ result[fn] = acc;
1122
+ }
1123
+ return result;
1124
+ });
1125
+ }
1126
+ async groupBy(args) {
1127
+ return this.withMiddleware('groupBy', args, async () => {
1128
+ const params = [];
1129
+ const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
1130
+ const where = this.buildWhere(resolvedWhere, params);
1131
+ const filter = where ? ` filter ${where}` : '';
1132
+ const groupKeys = args.by.map((f) => this.ref(f));
1133
+ // Safe aliases — PowQL rejects reserved-word aliases (e.g. `count:`).
1134
+ const aliasMap = [];
1135
+ let n = 0;
1136
+ const proj = args.by.map((f) => this.ref(f));
1137
+ if (args._count) {
1138
+ aliasMap.push({ alias: `agg_${n++}`, fn: 'count', field: null, outKey: '_count' });
1139
+ }
1140
+ for (const fn of ['_sum', '_avg', '_min', '_max']) {
1141
+ const spec = args[fn];
1142
+ if (!spec)
1143
+ continue;
1144
+ for (const field of Object.keys(spec).filter((f) => spec[f])) {
1145
+ aliasMap.push({ alias: `agg_${n++}`, fn: fn.slice(1), field, outKey: `${fn}:${field}` });
1146
+ }
1147
+ }
1148
+ for (const a of aliasMap) {
1149
+ proj.push(`${a.alias}: ${a.fn}(${a.field ? this.ref(a.field) : `.${this.meta.primaryKey[0]}`})`);
1150
+ }
1151
+ const having = this.buildHaving(args.having, params);
1152
+ const order = this.buildOrder(args.orderBy);
1153
+ const powql = `${this.table}${filter} group ${groupKeys.join(', ')}${having}${order} { ${proj.join(', ')} }`;
1154
+ const { rows } = await this.exec(powql, params, args.timeout);
1155
+ // Reshape: group keys → camel fields + coerced; aggregates → nested {_sum:{field}}.
1156
+ return rows.map((raw) => {
1157
+ const out = {};
1158
+ for (const f of args.by) {
1159
+ const col = this.column(f);
1160
+ out[f] =
1161
+ typeof raw[col.name] === 'string' ? coerceScalar(raw[col.name], col.tsType) : raw[col.name];
1162
+ }
1163
+ for (const a of aliasMap) {
1164
+ const val = raw[a.alias];
1165
+ const num = val == null || val === 'null' ? null : Number(val);
1166
+ if (a.outKey === '_count')
1167
+ out._count = num ?? 0;
1168
+ else {
1169
+ const [bucket, field] = a.outKey.split(':');
1170
+ out[bucket] ??= {};
1171
+ out[bucket][field] = num;
1172
+ }
1173
+ }
1174
+ return out;
1175
+ });
1176
+ });
1177
+ }
1178
+ /** `having <expr>` over group aggregates (count/sum/avg/min/max). */
1179
+ buildHaving(having, params) {
1180
+ if (!having)
1181
+ return '';
1182
+ const conds = [];
1183
+ const cmp = (expr, filter) => {
1184
+ if (typeof filter === 'number')
1185
+ return `${expr} = ${this.param(filter, params)}`;
1186
+ const f = filter;
1187
+ const ops = { equals: '=', gt: '>', gte: '>=', lt: '<', lte: '<=', not: '!=' };
1188
+ return Object.entries(f)
1189
+ .filter(([k]) => ops[k])
1190
+ .map(([k, v]) => `${expr} ${ops[k]} ${this.param(v, params)}`)
1191
+ .join(' and ');
1192
+ };
1193
+ for (const [key, spec] of Object.entries(having)) {
1194
+ if (spec == null)
1195
+ continue;
1196
+ if (key === '_count') {
1197
+ conds.push(cmp(`count(.${this.meta.primaryKey[0]})`, spec));
1198
+ }
1199
+ else {
1200
+ for (const [fn, filter] of Object.entries(spec)) {
1201
+ if (filter == null)
1202
+ continue;
1203
+ conds.push(cmp(`${fn.slice(1)}(${this.ref(key)})`, filter));
1204
+ }
1205
+ }
1206
+ }
1207
+ return conds.length ? ` having ${conds.join(' and ')}` : '';
1208
+ }
1209
+ // -------------------------------------------------------------------------
1210
+ // Streaming / unsupported
1211
+ // -------------------------------------------------------------------------
1212
+ // biome-ignore lint/correctness/useYield: intentionally throws before yielding — PowDB has no server cursor.
1213
+ async *findManyStream() {
1214
+ throw new errors_js_1.UnsupportedFeatureError('cursor streaming (findManyStream)', 'PowDB', 'PowDB has no server-side cursor; page with findMany({ limit, offset }) instead');
1215
+ }
1216
+ // -------------------------------------------------------------------------
1217
+ // Reselect helper (upsert only — PowDB's upsert has no `returning`)
1218
+ // -------------------------------------------------------------------------
1219
+ /** Reselect a single row by its single-column primary key value. */
1220
+ async reselectByPk(pkValue, timeout) {
1221
+ const pkField = this.meta.reverseColumnMap[this.meta.primaryKey[0]] ?? this.meta.primaryKey[0];
1222
+ const rows = await this.runFind({
1223
+ where: { [pkField]: pkValue },
1224
+ limit: 1,
1225
+ timeout,
1226
+ });
1227
+ return rows.length ? this.shape(rows)[0] : null;
1228
+ }
1229
+ /**
1230
+ * Empty-where guard — blocks accidental whole-table writes. Mirrors the SQL
1231
+ * path's `assertMutationHasPredicate` (query/builder.ts): it gates on the
1232
+ * *compiled* PowQL filter fragment, NOT the shape of the `where` object. A
1233
+ * `where` whose conditions all evaporate during compilation — `{}`,
1234
+ * `{ id: undefined }`, `{ OR: [] }`, `{ AND: [] }`, `{ NOT: {} }`,
1235
+ * `{ OR: [{ f: undefined }] }` — compiles to the empty string and is refused,
1236
+ * because emitting a filter-less write would hit every row.
1237
+ */
1238
+ assertCompiledWhere(compiledWhere, allow, action) {
1239
+ if (allow === true)
1240
+ return;
1241
+ if (compiledWhere.length > 0)
1242
+ return;
1243
+ throw new errors_js_1.ValidationError(`[turbine] ${action} on "${this.table}" refused: the \`where\` clause is empty. ` +
1244
+ `Pass \`allowFullTableScan: true\` to opt in, or check that your filter values are defined.`);
1245
+ }
1246
+ }
1247
+ exports.PowqlInterface = PowqlInterface;
1248
+ /** Coerce a group-key scalar string by the column's TS type (numbers/bools). */
1249
+ function coerceScalar(raw, tsType) {
1250
+ const ts = tsType.replace(/\s*\|\s*null$/i, '').trim();
1251
+ if (raw === 'null')
1252
+ return null;
1253
+ if (ts === 'number' || ts === 'bigint') {
1254
+ const n = Number(raw);
1255
+ return Number.isFinite(n) ? n : raw;
1256
+ }
1257
+ if (ts === 'boolean')
1258
+ return raw === 'true';
1259
+ if (ts === 'Date')
1260
+ return new Date(Number(raw) / 1000);
1261
+ return raw;
1262
+ }