turbine-orm 0.19.2 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +99 -18
  2. package/dist/adapters/cockroachdb.js +4 -9
  3. package/dist/cjs/adapters/cockroachdb.js +4 -9
  4. package/dist/cjs/cli/index.js +6 -3
  5. package/dist/cjs/cli/observe-ui.js +12 -2
  6. package/dist/cjs/cli/observe.js +6 -7
  7. package/dist/cjs/cli/studio.js +6 -7
  8. package/dist/cjs/client.js +44 -22
  9. package/dist/cjs/dialect.js +116 -0
  10. package/dist/cjs/errors.js +19 -1
  11. package/dist/cjs/generate.js +4 -0
  12. package/dist/cjs/index.js +3 -2
  13. package/dist/cjs/introspect.js +20 -0
  14. package/dist/cjs/mssql.js +1338 -0
  15. package/dist/cjs/mysql.js +1052 -0
  16. package/dist/cjs/powdb.js +851 -0
  17. package/dist/cjs/powql.js +903 -0
  18. package/dist/cjs/query/builder.js +293 -73
  19. package/dist/cjs/sqlite.js +849 -0
  20. package/dist/cjs/typed-sql.js +9 -7
  21. package/dist/cli/index.js +6 -3
  22. package/dist/cli/observe-ui.d.ts +1 -1
  23. package/dist/cli/observe-ui.js +12 -2
  24. package/dist/cli/observe.js +7 -8
  25. package/dist/cli/studio.js +7 -8
  26. package/dist/cli/ui.d.ts +1 -1
  27. package/dist/client.d.ts +23 -1
  28. package/dist/client.js +45 -23
  29. package/dist/dialect.d.ts +258 -1
  30. package/dist/dialect.js +83 -0
  31. package/dist/errors.d.ts +12 -0
  32. package/dist/errors.js +17 -0
  33. package/dist/generate.js +4 -0
  34. package/dist/index.d.ts +3 -3
  35. package/dist/index.js +1 -1
  36. package/dist/introspect.d.ts +18 -0
  37. package/dist/introspect.js +19 -0
  38. package/dist/mssql.d.ts +233 -0
  39. package/dist/mssql.js +1298 -0
  40. package/dist/mysql.d.ts +174 -0
  41. package/dist/mysql.js +1012 -0
  42. package/dist/powdb.d.ts +338 -0
  43. package/dist/powdb.js +802 -0
  44. package/dist/powql.d.ts +153 -0
  45. package/dist/powql.js +900 -0
  46. package/dist/query/builder.d.ts +105 -0
  47. package/dist/query/builder.js +294 -74
  48. package/dist/query/index.d.ts +1 -1
  49. package/dist/sqlite.d.ts +144 -0
  50. package/dist/sqlite.js +842 -0
  51. package/dist/typed-sql.d.ts +7 -5
  52. package/dist/typed-sql.js +9 -7
  53. package/package.json +45 -1
@@ -337,6 +337,89 @@ class QueryInterface {
337
337
  p(index) {
338
338
  return this.dialect.paramPlaceholder(index);
339
339
  }
340
+ /**
341
+ * Cast an aggregate expression to an integer/float result type through the
342
+ * active dialect. PostgreSQL keeps the historical postfix cast (`expr::int` /
343
+ * `expr::float`); SQLite (no `::` operator) maps to `CAST(expr AS INTEGER/REAL)`.
344
+ * Falls back to the Postgres postfix cast for dialects without the hook.
345
+ */
346
+ castAgg(expr, target) {
347
+ return this.dialect.castAggregate?.(expr, target) ?? `${expr}::${target}`;
348
+ }
349
+ /**
350
+ * Build the trailing pagination clause for an OUTER SELECT. PostgreSQL/MySQL/
351
+ * SQLite use ` LIMIT <ph>` and/or ` OFFSET <ph>`. SQL Server has no `LIMIT`, so
352
+ * its dialect implements {@link Dialect.buildLimitOffset} to emit
353
+ * `[ORDER BY (SELECT NULL)] OFFSET <off> ROWS [FETCH NEXT <lim> ROWS ONLY]`.
354
+ * Param-push order (limit before offset) is owned by the caller and unchanged —
355
+ * this only varies the SQL text, so PG output stays byte-identical.
356
+ */
357
+ buildPagination(limitPh, offsetPh, hasOrderBy) {
358
+ if (this.dialect.buildLimitOffset) {
359
+ return this.dialect.buildLimitOffset({ limitPlaceholder: limitPh, offsetPlaceholder: offsetPh, hasOrderBy });
360
+ }
361
+ let s = '';
362
+ if (limitPh !== undefined)
363
+ s += ` LIMIT ${limitPh}`;
364
+ if (offsetPh !== undefined)
365
+ s += ` OFFSET ${offsetPh}`;
366
+ return s;
367
+ }
368
+ /**
369
+ * The single-row limit appended to findUnique / findFirst-style lookups. ` LIMIT 1`
370
+ * for PG/MySQL/SQLite; SQL Server routes through {@link Dialect.buildLimitOffset}
371
+ * (literal `1`, no params) → ` ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1
372
+ * ROWS ONLY`. No params are pushed, so the collect path is unaffected.
373
+ */
374
+ limitOneClause() {
375
+ if (this.dialect.buildLimitOffset) {
376
+ return this.dialect.buildLimitOffset({ limitPlaceholder: '1', offsetPlaceholder: undefined, hasOrderBy: false });
377
+ }
378
+ return ' LIMIT 1';
379
+ }
380
+ /**
381
+ * Validate a LIMIT/OFFSET value as a non-negative integer and return it as an
382
+ * inline SQL literal. Used only on `dialect.inlineLimitOffset` engines (MySQL).
383
+ * The input is always a Turbine-controlled pagination value, never a raw user
384
+ * string — and this guard guarantees the output is `String` of a validated
385
+ * integer, so inlining cannot inject SQL.
386
+ */
387
+ limitOffsetLiteral(value) {
388
+ const n = Number(value);
389
+ if (!Number.isInteger(n) || n < 0) {
390
+ throw new errors_js_1.ValidationError(`LIMIT/OFFSET must be a non-negative integer, received: ${String(value)}`);
391
+ }
392
+ return String(n);
393
+ }
394
+ /**
395
+ * Resolve a LIMIT/OFFSET value to either an inline literal (no param pushed, on
396
+ * `dialect.inlineLimitOffset` engines) or a bound placeholder (the value is
397
+ * pushed to `params`). Build and collect paths both gate on the same flag so
398
+ * the param order stays mirrored; PG/SQLite/SQL Server keep parameterizing and
399
+ * stay byte-identical.
400
+ */
401
+ paginationRef(value, params) {
402
+ if (this.dialect.inlineLimitOffset) {
403
+ return this.limitOffsetLiteral(value);
404
+ }
405
+ params.push(Number(value));
406
+ return this.p(params.length);
407
+ }
408
+ /**
409
+ * Build an `IN` / `NOT IN` predicate through the active dialect. PostgreSQL
410
+ * keeps the array-param form (`expr = ANY($n)` / `expr != ALL($n)`); other
411
+ * engines (SQLite) use a length-independent single-placeholder form. Paired
412
+ * with {@link inParam}, which supplies the single bound value.
413
+ */
414
+ inClause(expr, paramRef, negated) {
415
+ if (this.dialect.buildInClause)
416
+ return this.dialect.buildInClause(expr, paramRef, negated);
417
+ return negated ? `${expr} != ALL(${paramRef})` : `${expr} = ANY(${paramRef})`;
418
+ }
419
+ /** The single bound parameter for an `IN` list (PG: the array; SQLite: a JSON string). */
420
+ inParam(values) {
421
+ return this.dialect.inClauseParam ? this.dialect.inClauseParam(values) : values;
422
+ }
340
423
  /**
341
424
  * Return cache hit/miss statistics for this QueryInterface instance.
342
425
  * Useful for monitoring and benchmarking.
@@ -437,6 +520,49 @@ class QueryInterface {
437
520
  clearTimeout(timer);
438
521
  }
439
522
  }
523
+ /**
524
+ * Execute a write `DeferredQuery` (create/update/delete/upsert) according to
525
+ * the active dialect's {@link Dialect.resultStrategy}, then apply its
526
+ * transform.
527
+ *
528
+ * - `'returning'` / `'output'`: the statement returns its own affected rows
529
+ * (`RETURNING *` / `OUTPUT INSERTED.*`). Byte-identical to the historical
530
+ * single `queryWithTimeout` + `transform(result)` path — the PostgreSQL
531
+ * route is unchanged.
532
+ * - `'reselect'`: the engine cannot return rows from a write, so the build
533
+ * method attached a {@link DeferredQuery.reselect} plan that runs the
534
+ * write and a follow-up SELECT; the SELECT's rows feed the transform.
535
+ */
536
+ async executeMutation(deferred, timeout) {
537
+ if (this.dialect.resultStrategy === 'reselect' && deferred.reselect) {
538
+ const exec = (sql, params, preparedName) => this.queryWithTimeout(sql, params, timeout, preparedName);
539
+ const result = await deferred.reselect(exec);
540
+ return deferred.transform(result);
541
+ }
542
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, timeout, deferred.preparedName);
543
+ return deferred.transform(result);
544
+ }
545
+ /**
546
+ * Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
547
+ * by a write's `where` clause. Used by the `'reselect'` result strategy to
548
+ * return rows from non-RETURNING engines. Reuses the same parameterized WHERE
549
+ * builder as reads, so no user value is interpolated.
550
+ */
551
+ buildReselectByWhere(whereObj) {
552
+ const params = [];
553
+ const clause = this.buildWhereClause(whereObj, params);
554
+ const where = clause ? ` WHERE ${clause}` : '';
555
+ return { sql: `SELECT * FROM ${this.q(this.table)}${where}`, params };
556
+ }
557
+ /**
558
+ * Best-effort extraction of an auto-generated primary key from a write
559
+ * result for `'reselect'` engines (e.g. mysql2's `insertId`). Returns
560
+ * `undefined` when the driver exposes no such field.
561
+ */
562
+ mutationInsertId(result) {
563
+ const r = result;
564
+ return r.insertId ?? r.lastID;
565
+ }
440
566
  /**
441
567
  * Execute a query through the middleware chain.
442
568
  * If no middlewares are registered, executes directly.
@@ -507,7 +633,7 @@ class QueryInterface {
507
633
  const whereSql = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
508
634
  const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
509
635
  void tempParams; // params are positional, SQL is value-invariant
510
- return `SELECT ${selectExpr} FROM ${qt}${whereSql} LIMIT 1`;
636
+ return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
511
637
  });
512
638
  // Collect params (same order as build)
513
639
  for (const k of whereKeys) {
@@ -532,7 +658,7 @@ class QueryInterface {
532
658
  const whereSql = clause ? ` WHERE ${clause}` : '';
533
659
  const qt = this.q(this.table);
534
660
  const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
535
- return `SELECT ${selectExpr} FROM ${qt}${whereSql} LIMIT 1`;
661
+ return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
536
662
  });
537
663
  // Collect params
538
664
  this.collectWhereParams(whereObj, params);
@@ -557,7 +683,7 @@ class QueryInterface {
557
683
  const clause = this.buildWhereClause(whereObj, freshParams);
558
684
  const whereSql = clause ? ` WHERE ${clause}` : '';
559
685
  const selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList);
560
- return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql} LIMIT 1`;
686
+ return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}${this.limitOneClause()}`;
561
687
  });
562
688
  // Collect params in exact build order: where first, then with-clause relations
563
689
  this.collectWhereParams(whereObj, params);
@@ -717,14 +843,18 @@ class QueryInterface {
717
843
  // vector at the correct position (after cursor params, before LIMIT).
718
844
  sql += ` ORDER BY ${this.buildOrderBy(args.orderBy, freshParams)}`;
719
845
  }
846
+ // Pagination — push params in the same order the collect path mirrors
847
+ // (limit before offset); the SQL TEXT shape is dialect-owned via
848
+ // buildPagination (PG: ` LIMIT $n`/` OFFSET $n`; SQL Server: OFFSET/FETCH).
849
+ let limitPh;
850
+ let offsetPh;
720
851
  if (effectiveLimit !== undefined) {
721
- freshParams.push(Number(effectiveLimit));
722
- sql += ` LIMIT ${this.p(freshParams.length)}`;
852
+ limitPh = this.paginationRef(effectiveLimit, freshParams);
723
853
  }
724
854
  if (args?.offset !== undefined) {
725
- freshParams.push(Number(args.offset));
726
- sql += ` OFFSET ${this.p(freshParams.length)}`;
855
+ offsetPh = this.paginationRef(args.offset, freshParams);
727
856
  }
857
+ sql += this.buildPagination(limitPh, offsetPh, !!args?.orderBy);
728
858
  return sql;
729
859
  });
730
860
  // Collect params in exact build order:
@@ -748,12 +878,13 @@ class QueryInterface {
748
878
  if (args?.orderBy) {
749
879
  this.collectOrderByParams(args.orderBy, params);
750
880
  }
751
- // 5. LIMIT param
752
- if (effectiveLimit !== undefined) {
881
+ // 5. LIMIT param — skipped when the dialect inlines pagination (build path
882
+ // mirrors via paginationRef → no placeholder, no param).
883
+ if (effectiveLimit !== undefined && !this.dialect.inlineLimitOffset) {
753
884
  params.push(Number(effectiveLimit));
754
885
  }
755
- // 6. OFFSET param
756
- if (args?.offset !== undefined) {
886
+ // 6. OFFSET param — same inline gate as LIMIT above.
887
+ if (args?.offset !== undefined && !this.dialect.inlineLimitOffset) {
757
888
  params.push(Number(args.offset));
758
889
  }
759
890
  return {
@@ -813,34 +944,19 @@ class QueryInterface {
813
944
  }
814
945
  // --- Overflow: fall back to cursor path from scratch ---
815
946
  const deferred = this.buildFindMany(args);
816
- // Acquire a dedicated connection — cursors require a single connection in a transaction
947
+ // Acquire a dedicated connection — cursors require a single connection in a
948
+ // transaction. The dialect owns the streaming SQL (Postgres: BEGIN → DECLARE
949
+ // … NO SCROLL CURSOR FOR → FETCH n → CLOSE → COMMIT, ROLLBACK on error); we
950
+ // just parse + yield the row batches it produces.
817
951
  const client = await this.pool.connect();
818
- const cursorName = `turbine_cursor_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
819
- const quotedCursor = this.q(cursorName);
820
952
  try {
821
- await client.query('BEGIN');
822
- await client.query(`DECLARE ${quotedCursor} NO SCROLL CURSOR FOR ${deferred.sql}`, deferred.params);
823
- while (true) {
824
- const batch = await client.query(`FETCH ${batchSize} FROM ${quotedCursor}`);
825
- if (batch.rows.length === 0)
826
- break;
827
- for (const row of batch.rows) {
953
+ for await (const batch of this.dialect.openStream(client, deferred.sql, deferred.params, batchSize)) {
954
+ for (const row of batch) {
828
955
  yield (hasRelations ? this.parseNestedRow(row, this.table) : this.parseRow(row, this.table));
829
956
  }
830
- if (batch.rows.length < batchSize)
831
- break;
832
957
  }
833
- await client.query(`CLOSE ${quotedCursor}`);
834
- await client.query('COMMIT');
835
958
  }
836
959
  catch (err) {
837
- // Rollback on error (also closes cursor implicitly)
838
- try {
839
- await client.query('ROLLBACK');
840
- }
841
- catch {
842
- // Connection may already be broken — ignore rollback error
843
- }
844
960
  // Wrap pg constraint errors so streaming surfaces typed errors like the rest of the API
845
961
  throw (0, errors_js_1.wrapPgError)(err);
846
962
  }
@@ -942,8 +1058,7 @@ class QueryInterface {
942
1058
  return this.nestedCreate(args);
943
1059
  }
944
1060
  const deferred = this.buildCreate(args);
945
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
946
- return deferred.transform(result);
1061
+ return this.executeMutation(deferred, args.timeout);
947
1062
  });
948
1063
  }
949
1064
  buildCreate(args) {
@@ -972,6 +1087,33 @@ class QueryInterface {
972
1087
  return this.parseRow(row, this.table);
973
1088
  },
974
1089
  tag: `${this.table}.create`,
1090
+ // Non-RETURNING engines: INSERT, then re-fetch the new row by primary key
1091
+ // (provided value, else the driver's generated insert id).
1092
+ reselect: this.makeCreateReselect(sql, params, args.data),
1093
+ };
1094
+ }
1095
+ /**
1096
+ * Build the `'reselect'` plan for {@link buildCreate}: run the INSERT, then
1097
+ * `SELECT * WHERE pk = ?`. Returns `undefined` (skipped) unless the active
1098
+ * dialect's result strategy is `'reselect'`, so the PostgreSQL/RETURNING path
1099
+ * pays nothing. Not yet wired to a real non-RETURNING engine.
1100
+ */
1101
+ makeCreateReselect(insertSql, insertParams, data) {
1102
+ if (this.dialect.resultStrategy !== 'reselect')
1103
+ return undefined;
1104
+ return async (exec) => {
1105
+ const writeResult = await exec(insertSql, insertParams);
1106
+ const insertId = this.mutationInsertId(writeResult);
1107
+ const conds = [];
1108
+ const selParams = [];
1109
+ let idx = 1;
1110
+ for (const pk of this.tableMeta.primaryKey) {
1111
+ const field = this.tableMeta.reverseColumnMap[pk] ?? (0, schema_js_1.snakeToCamel)(pk);
1112
+ selParams.push(data[field] ?? data[pk] ?? insertId);
1113
+ conds.push(`${this.q(pk)} = ${this.p(idx++)}`);
1114
+ }
1115
+ const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
1116
+ return exec(`SELECT * FROM ${this.q(this.table)}${where}`, selParams);
975
1117
  };
976
1118
  }
977
1119
  // -------------------------------------------------------------------------
@@ -1027,8 +1169,7 @@ class QueryInterface {
1027
1169
  return this.nestedUpdate(args);
1028
1170
  }
1029
1171
  const deferred = this.buildUpdate(args);
1030
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
1031
- return deferred.transform(result);
1172
+ return this.executeMutation(deferred, args.timeout);
1032
1173
  });
1033
1174
  }
1034
1175
  buildUpdate(args) {
@@ -1056,7 +1197,12 @@ class QueryInterface {
1056
1197
  whereSql = whereSql ? `${whereSql} AND ${versionCheck}` : ` WHERE ${versionCheck}`;
1057
1198
  }
1058
1199
  this.assertMutationHasPredicate('update', whereSql, args.allowFullTableScan);
1059
- return `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql}${this.dialect.buildReturningClause('*')}`;
1200
+ // Engines that inject their returning shape MID-statement (SQL Server
1201
+ // `OUTPUT INSERTED.*` between SET and WHERE) override buildUpdateStatement;
1202
+ // absent → the trailing-clause PG/SQLite/MySQL form (byte-identical).
1203
+ return this.dialect.buildUpdateStatement
1204
+ ? this.dialect.buildUpdateStatement({ table: this.q(this.table), setClauses, whereSql, returning: '*' })
1205
+ : `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql}${this.dialect.buildReturningClause('*')}`;
1060
1206
  };
1061
1207
  let sql;
1062
1208
  let preparedName;
@@ -1100,6 +1246,26 @@ class QueryInterface {
1100
1246
  },
1101
1247
  tag: `${this.table}.update`,
1102
1248
  preparedName,
1249
+ // Non-RETURNING engines: UPDATE, then re-fetch the row by the same where.
1250
+ reselect: this.dialect.resultStrategy === 'reselect'
1251
+ ? async (exec) => {
1252
+ const writeResult = await exec(sql, params, preparedName);
1253
+ // Optimistic-lock conflict: the version-checked UPDATE matched no
1254
+ // row. The re-fetch below uses `where` WITHOUT the version
1255
+ // predicate, so it would return the stale row and silently mask
1256
+ // the conflict — detect it from affected-rows here instead, to
1257
+ // match the OptimisticLockError thrown on RETURNING/OUTPUT engines.
1258
+ if (lock && (writeResult.rowCount ?? 0) === 0) {
1259
+ throw new errors_js_1.OptimisticLockError({
1260
+ table: this.table,
1261
+ versionField: lock.field,
1262
+ expectedVersion: lock.expected,
1263
+ });
1264
+ }
1265
+ const sel = this.buildReselectByWhere(whereObj);
1266
+ return exec(sel.sql, sel.params);
1267
+ }
1268
+ : undefined,
1103
1269
  };
1104
1270
  }
1105
1271
  // -------------------------------------------------------------------------
@@ -1131,19 +1297,19 @@ class QueryInterface {
1131
1297
  async runInImplicitTx(fn) {
1132
1298
  const client = await this.pool.connect();
1133
1299
  try {
1134
- await client.query('BEGIN');
1300
+ await client.query(this.dialect.beginStatement());
1135
1301
  const { TransactionClient } = await Promise.resolve().then(() => __importStar(require('../client.js')));
1136
1302
  // biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
1137
1303
  const tx = new TransactionClient(client, this.schema, this.middlewares, this.options);
1138
1304
  // biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
1139
1305
  const ctx = { schema: this.schema, tx: tx };
1140
1306
  const result = await fn(ctx);
1141
- await client.query('COMMIT');
1307
+ await client.query(this.dialect.commitStatement());
1142
1308
  return result;
1143
1309
  }
1144
1310
  catch (err) {
1145
1311
  try {
1146
- await client.query('ROLLBACK');
1312
+ await client.query(this.dialect.rollbackStatement());
1147
1313
  }
1148
1314
  catch {
1149
1315
  // Best-effort rollback — connection may have died.
@@ -1176,8 +1342,7 @@ class QueryInterface {
1176
1342
  async delete(args) {
1177
1343
  return this.executeWithMiddleware('delete', args, async () => {
1178
1344
  const deferred = this.buildDelete(args);
1179
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
1180
- return deferred.transform(result);
1345
+ return this.executeMutation(deferred, args.timeout);
1181
1346
  });
1182
1347
  }
1183
1348
  buildDelete(args) {
@@ -1192,7 +1357,11 @@ class QueryInterface {
1192
1357
  const clause = this.buildWhereClause(whereObj, freshParams);
1193
1358
  const whereSql = clause ? ` WHERE ${clause}` : '';
1194
1359
  this.assertMutationHasPredicate('delete', whereSql, args.allowFullTableScan);
1195
- return `DELETE FROM ${this.q(this.table)}${whereSql}${this.dialect.buildReturningClause('*')}`;
1360
+ // SQL Server injects `OUTPUT DELETED.*` between `DELETE FROM <t>` and WHERE;
1361
+ // absent override → the trailing-clause PG/SQLite/MySQL form (byte-identical).
1362
+ return this.dialect.buildDeleteStatement
1363
+ ? this.dialect.buildDeleteStatement({ table: this.q(this.table), whereSql, returning: '*' })
1364
+ : `DELETE FROM ${this.q(this.table)}${whereSql}${this.dialect.buildReturningClause('*')}`;
1196
1365
  });
1197
1366
  // On cache hit, still validate the predicate
1198
1367
  if (whereFp === '') {
@@ -1215,6 +1384,16 @@ class QueryInterface {
1215
1384
  },
1216
1385
  tag: `${this.table}.delete`,
1217
1386
  preparedName: entry.name,
1387
+ // Non-RETURNING engines: the row is gone after DELETE, so pre-SELECT it
1388
+ // by the same where, then run the DELETE, returning the captured row.
1389
+ reselect: this.dialect.resultStrategy === 'reselect'
1390
+ ? async (exec) => {
1391
+ const sel = this.buildReselectByWhere(whereObj);
1392
+ const pre = await exec(sel.sql, sel.params);
1393
+ await exec(entry.sql, params, entry.name);
1394
+ return pre;
1395
+ }
1396
+ : undefined,
1218
1397
  };
1219
1398
  }
1220
1399
  // -------------------------------------------------------------------------
@@ -1223,8 +1402,7 @@ class QueryInterface {
1223
1402
  async upsert(args) {
1224
1403
  return this.executeWithMiddleware('upsert', args, async () => {
1225
1404
  const deferred = this.buildUpsert(args);
1226
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
1227
- return deferred.transform(result);
1405
+ return this.executeMutation(deferred, args.timeout);
1228
1406
  });
1229
1407
  }
1230
1408
  buildUpsert(args) {
@@ -1270,6 +1448,14 @@ class QueryInterface {
1270
1448
  return this.parseRow(row, this.table);
1271
1449
  },
1272
1450
  tag: `${this.table}.upsert`,
1451
+ // Non-RETURNING engines: run the upsert, then re-fetch by the where keys.
1452
+ reselect: this.dialect.resultStrategy === 'reselect'
1453
+ ? async (exec) => {
1454
+ await exec(sql, params);
1455
+ const sel = this.buildReselectByWhere(args.where);
1456
+ return exec(sel.sql, sel.params);
1457
+ }
1458
+ : undefined,
1273
1459
  };
1274
1460
  }
1275
1461
  // -------------------------------------------------------------------------
@@ -1364,7 +1550,7 @@ class QueryInterface {
1364
1550
  const freshParams = [];
1365
1551
  const clause = args?.where ? this.buildWhereClause(whereObj, freshParams) : null;
1366
1552
  const whereSql = clause ? ` WHERE ${clause}` : '';
1367
- return `SELECT COUNT(*)::int AS count FROM ${this.q(this.table)}${whereSql}`;
1553
+ return `SELECT ${this.castAgg('COUNT(*)', 'int')} AS count FROM ${this.q(this.table)}${whereSql}`;
1368
1554
  });
1369
1555
  if (args?.where) {
1370
1556
  this.collectWhereParams(whereObj, params);
@@ -1404,7 +1590,7 @@ class QueryInterface {
1404
1590
  // _count
1405
1591
  if (args._count === true || args._count === undefined) {
1406
1592
  // default: always include count
1407
- selectExprs.push('COUNT(*)::int AS _count');
1593
+ selectExprs.push(`${this.castAgg('COUNT(*)', 'int')} AS _count`);
1408
1594
  }
1409
1595
  // _sum
1410
1596
  if (args._sum) {
@@ -1420,7 +1606,7 @@ class QueryInterface {
1420
1606
  for (const [field, enabled] of Object.entries(args._avg)) {
1421
1607
  if (enabled) {
1422
1608
  const col = this.toColumn(field);
1423
- selectExprs.push(`AVG(${this.q(col)})::float AS ${this.q(`_avg_${col}`)}`);
1609
+ selectExprs.push(`${this.castAgg(`AVG(${this.q(col)})`, 'float')} AS ${this.q(`_avg_${col}`)}`);
1424
1610
  }
1425
1611
  }
1426
1612
  }
@@ -1621,12 +1807,12 @@ class QueryInterface {
1621
1807
  clauses.push(`${expr} <= ${this.p(params.length)}`);
1622
1808
  }
1623
1809
  if (op.in !== undefined) {
1624
- params.push(op.in);
1625
- clauses.push(`${expr} = ANY(${this.p(params.length)})`);
1810
+ params.push(this.inParam(op.in));
1811
+ clauses.push(this.inClause(expr, this.p(params.length), false));
1626
1812
  }
1627
1813
  if (op.notIn !== undefined) {
1628
- params.push(op.notIn);
1629
- clauses.push(`${expr} != ALL(${this.p(params.length)})`);
1814
+ params.push(this.inParam(op.notIn));
1815
+ clauses.push(this.inClause(expr, this.p(params.length), true));
1630
1816
  }
1631
1817
  return clauses;
1632
1818
  }
@@ -1664,13 +1850,13 @@ class QueryInterface {
1664
1850
  const selectExprs = [];
1665
1851
  // _count
1666
1852
  if (args._count === true) {
1667
- selectExprs.push('COUNT(*)::int AS _count');
1853
+ selectExprs.push(`${this.castAgg('COUNT(*)', 'int')} AS _count`);
1668
1854
  }
1669
1855
  else if (args._count && typeof args._count === 'object') {
1670
1856
  for (const [field, enabled] of Object.entries(args._count)) {
1671
1857
  if (enabled) {
1672
1858
  const col = this.toColumn(field);
1673
- selectExprs.push(`COUNT(${this.q(col)})::int AS ${this.q(`_count_${col}`)}`);
1859
+ selectExprs.push(`${this.castAgg(`COUNT(${this.q(col)})`, 'int')} AS ${this.q(`_count_${col}`)}`);
1674
1860
  }
1675
1861
  }
1676
1862
  }
@@ -1688,7 +1874,7 @@ class QueryInterface {
1688
1874
  for (const [field, enabled] of Object.entries(args._avg)) {
1689
1875
  if (enabled) {
1690
1876
  const col = this.toColumn(field);
1691
- selectExprs.push(`AVG(${this.q(col)})::float AS ${this.q(`_avg_${col}`)}`);
1877
+ selectExprs.push(`${this.castAgg(`AVG(${this.q(col)})`, 'float')} AS ${this.q(`_avg_${col}`)}`);
1692
1878
  }
1693
1879
  }
1694
1880
  }
@@ -1711,7 +1897,7 @@ class QueryInterface {
1711
1897
  }
1712
1898
  }
1713
1899
  if (selectExprs.length === 0) {
1714
- selectExprs.push('COUNT(*)::int AS _count');
1900
+ selectExprs.push(`${this.castAgg('COUNT(*)', 'int')} AS _count`);
1715
1901
  }
1716
1902
  const sql = `SELECT ${selectExprs.join(', ')} FROM ${this.q(this.table)}${whereSql}`;
1717
1903
  return {
@@ -2205,9 +2391,9 @@ class QueryInterface {
2205
2391
  if (op.not !== undefined && op.not !== null)
2206
2392
  params.push(op.not);
2207
2393
  if (op.in !== undefined)
2208
- params.push(op.in);
2394
+ params.push(this.inParam(op.in));
2209
2395
  if (op.notIn !== undefined)
2210
- params.push(op.notIn);
2396
+ params.push(this.inParam(op.notIn));
2211
2397
  if (op.contains !== undefined)
2212
2398
  params.push(`%${(0, utils_js_1.escapeLike)(op.contains)}%`);
2213
2399
  if (op.startsWith !== undefined)
@@ -2372,7 +2558,7 @@ class QueryInterface {
2372
2558
  if (spec.where) {
2373
2559
  this.collectAliasWhereParams(targetTable, targetMeta, spec.where, params);
2374
2560
  }
2375
- if (spec.limit !== undefined) {
2561
+ if (spec.limit !== undefined && !this.dialect.inlineLimitOffset) {
2376
2562
  params.push(Number(spec.limit));
2377
2563
  }
2378
2564
  if (spec.with) {
@@ -2405,7 +2591,7 @@ class QueryInterface {
2405
2591
  // buildRelationSubquery). belongsTo/hasOne ignore limit (always LIMIT 1), so
2406
2592
  // pushing one here would orphan a param and desync the collect path.
2407
2593
  // `limit: 0` pushes (LIMIT 0 is honored), so check !== undefined.
2408
- if (relDef.type === 'hasMany' && spec.limit !== undefined) {
2594
+ if (relDef.type === 'hasMany' && spec.limit !== undefined && !this.dialect.inlineLimitOffset) {
2409
2595
  params.push(Number(spec.limit));
2410
2596
  }
2411
2597
  // Wrapped path: nested relations AFTER where/limit (inside inner subquery)
@@ -2924,12 +3110,12 @@ class QueryInterface {
2924
3110
  }
2925
3111
  }
2926
3112
  if (op.in !== undefined) {
2927
- params.push(op.in);
2928
- clauses.push(`${column} = ANY(${this.p(params.length)})`);
3113
+ params.push(this.inParam(op.in));
3114
+ clauses.push(this.inClause(column, this.p(params.length), false));
2929
3115
  }
2930
3116
  if (op.notIn !== undefined) {
2931
- params.push(op.notIn);
2932
- clauses.push(`${column} != ALL(${this.p(params.length)})`);
3117
+ params.push(this.inParam(op.notIn));
3118
+ clauses.push(this.inClause(column, this.p(params.length), true));
2933
3119
  }
2934
3120
  const buildLikeClause = (paramRef) => op.mode === 'insensitive' ? this.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
2935
3121
  if (op.contains !== undefined) {
@@ -3000,6 +3186,9 @@ class QueryInterface {
3000
3186
  * non-vector column — a user-supplied string can never become a SQL operator.
3001
3187
  */
3002
3188
  vectorOperator(field, rawColumn, metric) {
3189
+ if (!this.dialect.supportsVector) {
3190
+ throw new errors_js_1.UnsupportedFeatureError('pgvector distance operations', this.dialect.name, 'Vector search requires PostgreSQL with the pgvector extension.');
3191
+ }
3003
3192
  const colType = this.getColumnPgType(rawColumn);
3004
3193
  if (colType !== 'vector') {
3005
3194
  throw new errors_js_1.ValidationError(`[turbine] Column "${field}" on table "${this.table}" is not a vector column ` +
@@ -3020,6 +3209,9 @@ class QueryInterface {
3020
3209
  * placeholder string.
3021
3210
  */
3022
3211
  pushVectorParam(field, _rawColumn, to, params) {
3212
+ if (!this.dialect.supportsVector) {
3213
+ throw new errors_js_1.UnsupportedFeatureError('pgvector distance operations', this.dialect.name, 'Vector search requires PostgreSQL with the pgvector extension.');
3214
+ }
3023
3215
  if (!Array.isArray(to) || to.length === 0) {
3024
3216
  throw new errors_js_1.ValidationError(`[turbine] Vector distance on "${field}" requires a non-empty array of numbers for "to".`);
3025
3217
  }
@@ -3330,6 +3522,32 @@ class QueryInterface {
3330
3522
  .map(([k]) => targetMeta.columnMap[k] ?? (0, schema_js_1.camelToSnake)(k)));
3331
3523
  targetColumns = targetMeta.allColumns.filter((col) => !omittedFields.has(col));
3332
3524
  }
3525
+ // Engine override seam (additive): a dialect whose JSON-aggregation shape does
3526
+ // not map onto buildJsonObject/buildJsonArrayAgg (SQL Server FOR JSON PATH) owns
3527
+ // the WHOLE subquery. Absent for PG/MySQL/SQLite → the native path below runs
3528
+ // unchanged (byte-identical output, all their tests stay green). The override
3529
+ // pushes params per the documented RelationSubqueryContext ordering contract,
3530
+ // which mirrors collectRelationSubqueryParams so the SQL cache / pipeline stay
3531
+ // in sync.
3532
+ if (this.dialect.buildRelationSubquery) {
3533
+ return this.dialect.buildRelationSubquery({
3534
+ relDef,
3535
+ spec,
3536
+ params,
3537
+ parentRef,
3538
+ alias,
3539
+ targetTable,
3540
+ targetMeta,
3541
+ targetColumns,
3542
+ depth: currentDepth,
3543
+ path: currentPath,
3544
+ quote: (name) => this.q(name),
3545
+ buildWhere: (whereAlias) => (spec !== true && spec.where
3546
+ ? this.buildAliasWhere(targetTable, targetMeta, whereAlias, spec.where, params)
3547
+ : '') ?? '',
3548
+ recurse: (nRelDef, nSpec, nParent, nDepth, nPath) => this.buildRelationSubquery(nRelDef, nSpec, params, nParent, aliasCounter, nDepth, nPath),
3549
+ });
3550
+ }
3333
3551
  // Build JSON object pairs for resolved columns
3334
3552
  const jsonPairs = targetColumns.map((col) => [
3335
3553
  targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col),
@@ -3361,7 +3579,7 @@ class QueryInterface {
3361
3579
  const nestedSubquery = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, alias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
3362
3580
  // Use '[]'::json for hasMany (empty array), NULL for belongsTo/hasOne (no object)
3363
3581
  const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
3364
- jsonPairs.push([nestedRelName, `COALESCE((${nestedSubquery}), ${fallback})`]);
3582
+ jsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSubquery, fallback)]);
3365
3583
  }
3366
3584
  }
3367
3585
  const jsonObj = this.dialect.buildJsonObject(jsonPairs);
@@ -3409,8 +3627,7 @@ class QueryInterface {
3409
3627
  // `limit: 0` is honored (LIMIT 0 → empty array), so check !== undefined.
3410
3628
  let limitClause = '';
3411
3629
  if (relDef.type === 'hasMany' && spec !== true && spec.limit !== undefined) {
3412
- params.push(Number(spec.limit));
3413
- limitClause = ` LIMIT ${this.p(params.length)}`;
3630
+ limitClause = ` LIMIT ${this.paginationRef(spec.limit, params)}`;
3414
3631
  }
3415
3632
  if (relDef.type === 'hasMany') {
3416
3633
  // When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
@@ -3435,13 +3652,17 @@ class QueryInterface {
3435
3652
  }
3436
3653
  const nestedSub = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
3437
3654
  const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
3438
- innerJsonPairs.push([nestedRelName, `COALESCE((${nestedSub}), ${fallback})`]);
3655
+ innerJsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSub, fallback)]);
3439
3656
  }
3440
3657
  }
3441
3658
  const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
3442
3659
  return `SELECT ${this.dialect.buildJsonArrayAgg(innerJsonObj)} FROM (${innerSql}) ${innerAlias}`;
3443
3660
  }
3444
- return `SELECT ${this.dialect.buildJsonArrayAgg(jsonObj, orderClause.trim() || undefined)} FROM ${qTarget} ${alias} WHERE ${whereClause}`;
3661
+ // Inline ORDER BY only when the dialect's array-agg supports it (PG). For
3662
+ // hasMany this path is reached only when there is no orderClause, so the
3663
+ // argument is `undefined` either way — keeping PG output byte-identical.
3664
+ const inlineOrder = this.dialect.aggSupportsInlineOrderBy ? orderClause.trim() || undefined : undefined;
3665
+ return `SELECT ${this.dialect.buildJsonArrayAgg(jsonObj, inlineOrder)} FROM ${qTarget} ${alias} WHERE ${whereClause}`;
3445
3666
  }
3446
3667
  // belongsTo / hasOne — return single object
3447
3668
  return `SELECT ${jsonObj} FROM ${qTarget} ${alias} WHERE ${whereClause} LIMIT 1`;
@@ -3529,8 +3750,7 @@ class QueryInterface {
3529
3750
  // LIMIT — `limit: 0` is honored (LIMIT 0 → empty array)
3530
3751
  let limitClause = '';
3531
3752
  if (spec !== true && spec.limit !== undefined) {
3532
- params.push(Number(spec.limit));
3533
- limitClause = ` LIMIT ${this.p(params.length)}`;
3753
+ limitClause = ` LIMIT ${this.paginationRef(spec.limit, params)}`;
3534
3754
  }
3535
3755
  const fromJoin = `FROM ${qTarget} ${talias} JOIN ${qJunction} ${jalias} ON ${joinOn}`;
3536
3756
  // When LIMIT or ORDER BY is present, wrap the joined rows in an inner subquery
@@ -3555,7 +3775,7 @@ class QueryInterface {
3555
3775
  const fallback = nestedRelDef.type === 'belongsTo' || nestedRelDef.type === 'hasOne'
3556
3776
  ? this.dialect.nullJsonLiteral
3557
3777
  : this.dialect.emptyJsonArrayLiteral;
3558
- innerJsonPairs.push([nestedRelName, `COALESCE((${nestedSub}), ${fallback})`]);
3778
+ innerJsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSub, fallback)]);
3559
3779
  }
3560
3780
  }
3561
3781
  const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
@@ -3578,7 +3798,7 @@ class QueryInterface {
3578
3798
  const fallback = nestedRelDef.type === 'belongsTo' || nestedRelDef.type === 'hasOne'
3579
3799
  ? this.dialect.nullJsonLiteral
3580
3800
  : this.dialect.emptyJsonArrayLiteral;
3581
- jsonPairs.push([nestedRelName, `COALESCE((${nestedSub}), ${fallback})`]);
3801
+ jsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSub, fallback)]);
3582
3802
  }
3583
3803
  }
3584
3804
  const jsonObj = this.dialect.buildJsonObject(jsonPairs);