turbine-orm 0.51.0 → 0.52.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 (74) hide show
  1. package/README.md +33 -5
  2. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  3. package/dist/cjs/client.d.ts +106 -2
  4. package/dist/cjs/client.js +111 -5
  5. package/dist/cjs/dialect.d.ts +33 -0
  6. package/dist/cjs/dialect.js +14 -0
  7. package/dist/cjs/engine-config.d.ts +49 -0
  8. package/dist/cjs/engine-config.js +19 -0
  9. package/dist/cjs/index-advisor.js +0 -0
  10. package/dist/cjs/index.d.ts +1 -1
  11. package/dist/cjs/index.js +3 -2
  12. package/dist/cjs/mssql.d.ts +8 -3
  13. package/dist/cjs/mssql.js +22 -3
  14. package/dist/cjs/mysql.d.ts +7 -3
  15. package/dist/cjs/mysql.js +20 -3
  16. package/dist/cjs/nested-write.d.ts +31 -0
  17. package/dist/cjs/nested-write.js +80 -2
  18. package/dist/cjs/powdb-introspect.d.ts +10 -1
  19. package/dist/cjs/powdb-introspect.js +10 -1
  20. package/dist/cjs/powdb.d.ts +116 -6
  21. package/dist/cjs/powdb.js +169 -10
  22. package/dist/cjs/powql.d.ts +161 -1
  23. package/dist/cjs/powql.js +299 -19
  24. package/dist/cjs/prisma-compat.d.ts +54 -8
  25. package/dist/cjs/prisma-compat.js +136 -20
  26. package/dist/cjs/query/batched-loader.d.ts +7 -0
  27. package/dist/cjs/query/batched-loader.js +97 -15
  28. package/dist/cjs/query/builder.d.ts +131 -5
  29. package/dist/cjs/query/builder.js +223 -19
  30. package/dist/cjs/query/compound-unique.js +0 -0
  31. package/dist/cjs/query/index.d.ts +1 -1
  32. package/dist/cjs/query/index.js +2 -1
  33. package/dist/cjs/query/warn-registry.d.ts +10 -0
  34. package/dist/cjs/query/warn-registry.js +10 -0
  35. package/dist/cjs/query/writes.js +115 -7
  36. package/dist/cjs/sqlite.d.ts +10 -4
  37. package/dist/cjs/sqlite.js +18 -4
  38. package/dist/cli/studio-ui.generated.js +1 -1
  39. package/dist/client.d.ts +106 -2
  40. package/dist/client.js +111 -5
  41. package/dist/dialect.d.ts +33 -0
  42. package/dist/dialect.js +14 -0
  43. package/dist/engine-config.d.ts +49 -0
  44. package/dist/engine-config.js +18 -0
  45. package/dist/index-advisor.js +0 -0
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.js +1 -1
  48. package/dist/mssql.d.ts +8 -3
  49. package/dist/mssql.js +22 -3
  50. package/dist/mysql.d.ts +7 -3
  51. package/dist/mysql.js +20 -3
  52. package/dist/nested-write.d.ts +31 -0
  53. package/dist/nested-write.js +79 -2
  54. package/dist/powdb-introspect.d.ts +10 -1
  55. package/dist/powdb-introspect.js +10 -1
  56. package/dist/powdb.d.ts +116 -6
  57. package/dist/powdb.js +167 -9
  58. package/dist/powql.d.ts +161 -1
  59. package/dist/powql.js +299 -19
  60. package/dist/prisma-compat.d.ts +54 -8
  61. package/dist/prisma-compat.js +136 -20
  62. package/dist/query/batched-loader.d.ts +7 -0
  63. package/dist/query/batched-loader.js +98 -16
  64. package/dist/query/builder.d.ts +131 -5
  65. package/dist/query/builder.js +222 -18
  66. package/dist/query/compound-unique.js +0 -0
  67. package/dist/query/index.d.ts +1 -1
  68. package/dist/query/index.js +1 -1
  69. package/dist/query/warn-registry.d.ts +10 -0
  70. package/dist/query/warn-registry.js +10 -0
  71. package/dist/query/writes.js +116 -8
  72. package/dist/sqlite.d.ts +10 -4
  73. package/dist/sqlite.js +19 -5
  74. package/package.json +3 -3
package/dist/cjs/powql.js CHANGED
@@ -69,7 +69,7 @@ var __importStar = (this && this.__importStar) || (function () {
69
69
  };
70
70
  })();
71
71
  Object.defineProperty(exports, "__esModule", { value: true });
72
- exports.PowqlInterface = void 0;
72
+ exports.PowqlInterface = exports.MAX_POWQL_DATETIME_TERMS = void 0;
73
73
  const node_crypto_1 = require("node:crypto");
74
74
  const errors_js_1 = require("./errors.js");
75
75
  const nested_write_js_1 = require("./nested-write.js");
@@ -87,6 +87,18 @@ const schema_js_1 = require("./schema.js");
87
87
  * before grouping. Mirrors the chunking the parity matrix documents.
88
88
  */
89
89
  const MAX_RELATION_KEYS = 1000;
90
+ /**
91
+ * Max values one `in` / `notIn` on a PowDB-native `datetime` column may carry.
92
+ *
93
+ * Such a list is never sent as a list: it is expanded into an equality chain
94
+ * (see {@link PowqlInterface.buildDatetimeInList}), and PowQL spends one level
95
+ * of its 64-level nesting budget per chain term. Measured on the 0.20.0 addon:
96
+ * 63 terms parse at the top level, 61 one level deep, whatever else the
97
+ * predicate contains. 32 leaves room for the surrounding filter, and is also the
98
+ * key-chunk size the relation loaders use for a datetime correlation column, so
99
+ * a loader can never build a chain the engine will reject.
100
+ */
101
+ exports.MAX_POWQL_DATETIME_TERMS = 32;
90
102
  /**
91
103
  * Read-shaped actions whose statement may be transparently replayed once on a
92
104
  * stale wire frame when `retryStaleReads` is enabled (see
@@ -296,6 +308,36 @@ class PowqlInterface {
296
308
  get capabilities() {
297
309
  return this.pool.capabilities ?? powdb_js_1.ALL_POWDB_CAPABILITIES;
298
310
  }
311
+ /**
312
+ * The `limit` a query actually emits: the explicit `limit`, Prisma's `take`
313
+ * alias, then the client-level `defaultLimit`. Shared by {@link buildFind} and
314
+ * the {@link findMany} zero short-circuit so the two can never disagree about
315
+ * which limit is in force.
316
+ */
317
+ effectiveLimit(args) {
318
+ return args.limit ?? args.take ?? this.defaultLimit;
319
+ }
320
+ /**
321
+ * Reject a negative `limit` / `offset` before it reaches the engine. PowDB
322
+ * casts both with `as usize` at execution, so below engine 0.20 a negative
323
+ * limit wrapped to `usize::MAX` and silently returned EVERY row, the opposite
324
+ * of what the caller asked for; 0.20 refuses it. Validating client-side makes
325
+ * the refusal identical on every engine version and names the argument.
326
+ *
327
+ * `limit: 0` is legal and means "no rows" (SQL `LIMIT 0`). It is not emitted:
328
+ * callers short-circuit it, because PowDB's projection fast path returned ONE
329
+ * row for `limit 0` below 0.20.
330
+ */
331
+ assertPagination(limit, offset, context) {
332
+ for (const [name, value] of [
333
+ ['limit', limit],
334
+ ['offset', offset],
335
+ ]) {
336
+ if (value !== undefined && value !== null && value < 0) {
337
+ throw new errors_js_1.ValidationError(`[turbine] ${context} on "${this.table}": \`${name}\` must not be negative (got ${value}).`);
338
+ }
339
+ }
340
+ }
299
341
  /** A predicate that is always false, the empty-`in` / contradiction sentinel. */
300
342
  alwaysFalse() {
301
343
  const pk = this.meta.primaryKey[0] ?? this.meta.columns[0]?.name;
@@ -353,6 +395,43 @@ class PowqlInterface {
353
395
  }
354
396
  return parts.join(' and ');
355
397
  }
398
+ /**
399
+ * Gate a predicate on a PowDB-native `datetime` column. Turbine binds a JS
400
+ * `Date` as an integer count of microseconds, and PowDB writes a timestamp
401
+ * literal as a plain integer, so every such predicate is a `DateTime` vs `Int`
402
+ * comparison. Below engine 0.20 that pairing was unhandled and fell back to
403
+ * comparing TYPE TAGS, so `>` matched every non-null row, `=` and `<` matched
404
+ * none, and the answer additionally changed with the column's access path
405
+ * (indexed vs scanned). 0.20 compares microseconds and every binary operator
406
+ * (`=`, `!=`, `<`, `<=`, `>`, `>=`) is correct.
407
+ *
408
+ * ONE gate covers the whole family, whatever spelling the caller used. `in` /
409
+ * `not in` are included because their still-broken LIST form is never emitted:
410
+ * {@link buildInList} expands a datetime list into the equality chain the
411
+ * engine does answer correctly, which is the same binary comparison this flag
412
+ * governs. So on >= 0.20 every path is served (direct predicates, relation
413
+ * filters, the batched loaders, nested projections, native joins, and the
414
+ * findUnique / update / delete / upsert by-key paths), and below 0.20 every
415
+ * path that compares against a literal is refused with one message, which
416
+ * names the read paths that do not.
417
+ *
418
+ * `is null` / `is not null` are never gated: they compare no literal and are
419
+ * correct on every version. Ordering, grouping and `min`/`max` are likewise
420
+ * unaffected (they compare datetimes against each other, never against an int),
421
+ * as are nested-projection and join correlations, which are column-to-column.
422
+ *
423
+ * Only PowDB's native `datetime` type is affected. Turbine's own DDL emits
424
+ * `int` epoch micros for a `Date` column, so a Turbine-provisioned database
425
+ * never reaches this: the exposed shape is a table created outside Turbine and
426
+ * read through `introspectPowdbDatabase`, or hand-written metadata.
427
+ */
428
+ assertDatetimePredicateSupported(col) {
429
+ if (!(0, powdb_js_1.isPowdbDatetimeColumn)(col))
430
+ return;
431
+ (0, powdb_js_1.requireCapability)(this.capabilities, 'datetimeCompare', `comparisons on the PowDB datetime column "${col.name}"`, 'Reading that column is unaffected, and so are ordering, grouping and `is null` checks; only comparing it ' +
432
+ 'against a bound timestamp needs the fix. A nested `with` (the default nested-projection / join paths) ' +
433
+ 'correlates column to column, so it loads the relation without any such comparison.');
434
+ }
356
435
  /** Build a single `field: value | operator` condition. */
357
436
  buildFieldCondition(field, value, params, alias) {
358
437
  const colMeta = this.column(field);
@@ -360,6 +439,7 @@ class PowqlInterface {
360
439
  if (value === null)
361
440
  return `${ref} is null`;
362
441
  if (value instanceof Date || typeof value !== 'object') {
442
+ this.assertDatetimePredicateSupported(colMeta);
363
443
  return `${ref} = ${this.param(value, params, colMeta)}`;
364
444
  }
365
445
  const op = value;
@@ -374,6 +454,7 @@ class PowqlInterface {
374
454
  rejectUnsupportedFilter(op, field);
375
455
  if (!Object.keys(op).some((k) => OPERATOR_KEYS.has(k))) {
376
456
  // A bare object that is not an operator set, equality by value.
457
+ this.assertDatetimePredicateSupported(colMeta);
377
458
  return `${ref} = ${this.param(value, params)}`;
378
459
  }
379
460
  const insensitive = op.mode === 'insensitive';
@@ -382,6 +463,17 @@ class PowqlInterface {
382
463
  for (const [opName, opVal] of Object.entries(op)) {
383
464
  if (opVal === undefined || opName === 'mode')
384
465
  continue;
466
+ // Every operator below compares the column against a bound literal, which
467
+ // is the shape a PowDB `datetime` column can only answer from engine 0.20
468
+ // (see assertDatetimePredicateSupported; `in`/`notIn` reach the engine as
469
+ // an equality chain, so they are the same comparison). The two exceptions
470
+ // compare nothing and stay allowed on every version: a null operand
471
+ // (`equals`/`not: null` → `is [not] null`) and an empty `in`/`notIn` list
472
+ // (a compile-time constant, never sent).
473
+ const comparesNothing = (opVal === null && (opName === 'equals' || opName === 'not')) ||
474
+ ((opName === 'in' || opName === 'notIn') && Array.isArray(opVal) && opVal.length === 0);
475
+ if (!comparesNothing)
476
+ this.assertDatetimePredicateSupported(colMeta);
385
477
  switch (opName) {
386
478
  case 'equals':
387
479
  conds.push(opVal === null ? `${ref} is null` : `${lhs} = ${this.bind(opVal, params, insensitive)}`);
@@ -406,10 +498,10 @@ class PowqlInterface {
406
498
  conds.push(`${lhs} <= ${this.bind(opVal, params, insensitive)}`);
407
499
  break;
408
500
  case 'in':
409
- conds.push(this.buildInList(lhs, opVal, params, insensitive, false));
501
+ conds.push(this.buildInList(lhs, opVal, params, insensitive, false, colMeta));
410
502
  break;
411
503
  case 'notIn':
412
- conds.push(this.buildInList(lhs, opVal, params, insensitive, true));
504
+ conds.push(this.buildInList(lhs, opVal, params, insensitive, true, colMeta));
413
505
  break;
414
506
  case 'contains':
415
507
  conds.push(`${lhs} like ${this.bindLike(`%${(0, utils_js_1.escapeLike)(String(opVal))}%`, params, insensitive)}`);
@@ -546,13 +638,20 @@ class PowqlInterface {
546
638
  const ph = this.param(pattern, params);
547
639
  return insensitive ? `lower(${ph})` : ph;
548
640
  }
549
- /** `lhs [not] in ($1, $2, …)`, empty list collapses to a constant. */
550
- buildInList(lhs, values, params, insensitive, negate) {
641
+ /**
642
+ * `lhs [not] in ($1, $2, )`, empty list collapses to a constant.
643
+ *
644
+ * A PowDB-native `datetime` column takes the expanded form instead (see
645
+ * {@link buildDatetimeInList}): its `in` list is still broken upstream at 0.20.
646
+ */
647
+ buildInList(lhs, values, params, insensitive, negate, col) {
551
648
  if (!Array.isArray(values) || values.length === 0) {
552
649
  // `in []` matches nothing; `not in []` matches everything (SQL parity requires a
553
650
  // missing-value row to match `notIn []`, so NO presence guard is appended here).
554
651
  return negate ? '(1 = 1)' : this.alwaysFalse();
555
652
  }
653
+ if (col && (0, powdb_js_1.isPowdbDatetimeColumn)(col))
654
+ return this.buildDatetimeInList(lhs, values, params, negate, col);
556
655
  const items = values.map((v) => this.bind(v, params, insensitive)).join(', ');
557
656
  if (negate) {
558
657
  // PowQL `not in` matches missing-value rows, so append `and lhs is not null`
@@ -561,6 +660,78 @@ class PowqlInterface {
561
660
  }
562
661
  return `${lhs} in (${items})`;
563
662
  }
663
+ /**
664
+ * `in` / `not in` on a PowDB-native `datetime` column, expanded into the
665
+ * equality chain the engine answers correctly:
666
+ *
667
+ * `in` → `(.ts = $1 or .ts = $2 …)`
668
+ * `notIn` → `(.ts != $1 and .ts != $2 … and .ts is not null)`
669
+ *
670
+ * PowQL's LIST form compares a datetime column against integer timestamp
671
+ * literals by TYPE TAG as of engine 0.20 (the 0.20 timestamp fix covered the
672
+ * binary operators only): measured on the 0.20.0 addon, `filter .ts in
673
+ * (<micros>, …)` matches nothing and `not in` matches everything, while the
674
+ * identical lists against an `int` control column answer correctly. The
675
+ * expanded chain uses the operators 0.20 DID fix, so it answers correctly and
676
+ * matches the int control exactly.
677
+ *
678
+ * This is what keeps the relation family coherent: relation filters and the
679
+ * batched loaders both compile to a key `in` list, so without the rewrite the
680
+ * same relation was refused through one strategy and served through another
681
+ * (nested projections and joins correlate column to column and never emit a
682
+ * list at all).
683
+ *
684
+ * The cost is the chain's width. PowQL bounds the SHAPE of the predicate tree,
685
+ * and a flat `or` / `and` chain spends one level per term against the same
686
+ * 64-level budget as nested parens (measured on 0.20: 63 terms at the top
687
+ * level, 61 one level deep), so the expansion is capped at
688
+ * {@link MAX_POWQL_DATETIME_TERMS} with headroom for whatever predicate it
689
+ * sits inside. The loaders chunk their key lists to that cap, so only a
690
+ * caller-written list (or a relation filter matching very many distinct
691
+ * timestamps) can exceed it, and that raises a typed E017 saying so rather
692
+ * than an engine parse failure.
693
+ */
694
+ buildDatetimeInList(lhs, values, params, negate, col) {
695
+ if (values.length > exports.MAX_POWQL_DATETIME_TERMS) {
696
+ throw new errors_js_1.UnsupportedFeatureError(`\`${negate ? 'notIn' : 'in'}\` with ${values.length} values on the PowDB datetime column "${col.name}"`, 'PowDB', `PowQL's \`in\` list still compares a datetime column against integer timestamp literals by type tag as of ` +
697
+ `engine 0.20, so Turbine expands it into an equality chain, which PowQL's nesting budget caps at ` +
698
+ `${exports.MAX_POWQL_DATETIME_TERMS} terms. Narrow the list (a \`gte\`/\`lte\` range over the same timestamps is ` +
699
+ 'one comparison), split the call and merge the results, or store the column as a PowQL `int` of epoch ' +
700
+ "microseconds, which is what Turbine's own DDL emits for a `Date` column. A relation filter reaches this " +
701
+ 'when the inner predicate matches more than that many distinct key timestamps.');
702
+ }
703
+ const terms = values.map((v) => `${lhs} ${negate ? '!=' : '='} ${this.param(v, params, col)}`);
704
+ // `!=` already excludes a missing-value row, but the trailing presence guard
705
+ // keeps the emitted predicate identical in meaning to the plain `not in`
706
+ // branch above (and to SQL null semantics) on every engine version.
707
+ if (negate)
708
+ terms.push(`${lhs} is not null`);
709
+ return `(${terms.join(negate ? ' and ' : ' or ')})`;
710
+ }
711
+ /**
712
+ * A literal `ref in (…)` clause for the hand-built key lists the relation
713
+ * loaders emit (they bypass {@link buildWhere}). Routes a PowDB-native
714
+ * `datetime` key column through the same equality-chain expansion the
715
+ * where-builder uses, so a datetime junction / correlation key behaves
716
+ * identically however the statement was assembled.
717
+ */
718
+ inClause(ref, values, params, col) {
719
+ if (col && (0, powdb_js_1.isPowdbDatetimeColumn)(col)) {
720
+ this.assertDatetimePredicateSupported(col);
721
+ return this.buildDatetimeInList(ref, values, params, false, col);
722
+ }
723
+ return `${ref} in (${values.map((v) => this.param(v, params, col)).join(', ')})`;
724
+ }
725
+ /**
726
+ * Key-chunk size for a relation loader. A PowDB-native `datetime` correlation
727
+ * column's `in` list is expanded into an equality chain, which PowQL's nesting
728
+ * budget bounds, so those keys chunk at {@link MAX_POWQL_DATETIME_TERMS}
729
+ * (more, smaller round-trips) instead of {@link MAX_RELATION_KEYS}.
730
+ */
731
+ keyChunkSize(meta, colName) {
732
+ const col = meta?.columns.find((c) => c.name === colName);
733
+ return col && (0, powdb_js_1.isPowdbDatetimeColumn)(col) ? exports.MAX_POWQL_DATETIME_TERMS : MAX_RELATION_KEYS;
734
+ }
564
735
  /**
565
736
  * Pre-resolve every relation filter (`some`/`none`/`every`) in a where clause
566
737
  * into a plain scalar `in`/`notIn` condition on the **local key**, by running
@@ -675,15 +846,18 @@ class PowqlInterface {
675
846
  return [...new Set(rows.map((r) => r[targetPkField]).filter((v) => v != null))];
676
847
  };
677
848
  // Junction source keys linking any of `targetPks` (literal IN-list, never a subquery).
849
+ const junctionMeta = this.schema.tables[through.table];
850
+ const targetJColMeta = junctionMeta?.columns.find((c) => c.name === targetJCol);
851
+ const junctionChunk = this.keyChunkSize(junctionMeta, targetJCol);
678
852
  const sourcesForTargets = async (targetPks) => {
679
853
  if (!targetPks.length)
680
854
  return [];
681
855
  const out = new Set();
682
- for (let i = 0; i < targetPks.length; i += MAX_RELATION_KEYS) {
683
- const chunk = targetPks.slice(i, i + MAX_RELATION_KEYS);
856
+ for (let i = 0; i < targetPks.length; i += junctionChunk) {
857
+ const chunk = targetPks.slice(i, i + junctionChunk);
684
858
  const params = [];
685
- const ph = chunk.map((v) => this.param(v, params)).join(', ');
686
- const { rows } = await this.exec(`${(0, powdb_js_1.quotePowqlIdent)(through.table)} filter .${targetJCol} in (${ph}) { .${sourceJCol} }`, params, timeout, 'findMany');
859
+ const keyClause = this.inClause(`.${targetJCol}`, chunk, params, targetJColMeta);
860
+ const { rows } = await this.exec(`${(0, powdb_js_1.quotePowqlIdent)(through.table)} filter ${keyClause} { .${sourceJCol} }`, params, timeout, 'findMany');
687
861
  for (const r of rows) {
688
862
  const v = r[sourceJCol];
689
863
  if (v != null)
@@ -984,6 +1158,13 @@ class PowqlInterface {
984
1158
  // -------------------------------------------------------------------------
985
1159
  async findMany(args = {}) {
986
1160
  return this.withMiddleware('findMany', args, async () => {
1161
+ // `limit: 0` means "no rows" (SQL `LIMIT 0`), and answering it client-side
1162
+ // is correct on every engine version: PowDB's projection fast path returned
1163
+ // ONE row for `limit 0` below 0.20. Validate first so a negative limit still
1164
+ // raises instead of falling through to a query.
1165
+ this.assertPagination(this.effectiveLimit(args), args.offset, 'findMany');
1166
+ if (this.effectiveLimit(args) === 0)
1167
+ return [];
987
1168
  const { rows, native, resolvedWhere, nestedPlans, linkPlans, residualWith } = await this.runFind(args, 'findMany');
988
1169
  const entities = this.shape(rows, native);
989
1170
  if (nestedPlans.length)
@@ -1055,7 +1236,8 @@ class PowqlInterface {
1055
1236
  const distinct = args.distinct?.length ? ' distinct' : '';
1056
1237
  const filter = where ? ` filter ${where}` : '';
1057
1238
  const order = this.buildOrder(args.orderBy, params, alias);
1058
- const limit = args.limit ?? args.take ?? this.defaultLimit;
1239
+ const limit = this.effectiveLimit(args);
1240
+ this.assertPagination(limit, args.offset, 'findMany');
1059
1241
  if (limit === undefined && this.warnOnUnlimited && !this.warnedUnlimited) {
1060
1242
  this.warnedUnlimited = true;
1061
1243
  console.warn(`[turbine] findMany on "${this.table}" has no limit: this scans the whole table.`);
@@ -1254,8 +1436,9 @@ class PowqlInterface {
1254
1436
  // cell) so a datetime correlation column stitches instead of silently
1255
1437
  // returning [].
1256
1438
  const childByKey = new Map();
1257
- for (let i = 0; i < keys.length; i += MAX_RELATION_KEYS) {
1258
- const chunk = keys.slice(i, i + MAX_RELATION_KEYS);
1439
+ const chunkSize = this.keyChunkSize(targetMeta, childKeyCol);
1440
+ for (let i = 0; i < keys.length; i += chunkSize) {
1441
+ const chunk = keys.slice(i, i + chunkSize);
1259
1442
  const childWhere = {
1260
1443
  ...fetchOptions.where,
1261
1444
  [childKeyField]: { in: chunk },
@@ -1331,13 +1514,16 @@ class PowqlInterface {
1331
1514
  return;
1332
1515
  }
1333
1516
  // (1) Junction rows: sourceKeyVal(String) → [targetKeyVal(String)].
1517
+ const junctionMeta = this.schema.tables[through.table];
1518
+ const sourceJColMeta = junctionMeta?.columns.find((c) => c.name === sourceJCol);
1334
1519
  const targetsBySource = new Map();
1335
1520
  const allTargetVals = new Set();
1336
- for (let i = 0; i < parentKeys.length; i += MAX_RELATION_KEYS) {
1337
- const chunk = parentKeys.slice(i, i + MAX_RELATION_KEYS);
1521
+ const junctionChunk = this.keyChunkSize(junctionMeta, sourceJCol);
1522
+ for (let i = 0; i < parentKeys.length; i += junctionChunk) {
1523
+ const chunk = parentKeys.slice(i, i + junctionChunk);
1338
1524
  const params = [];
1339
- const placeholders = chunk.map((v) => this.param(v, params)).join(', ');
1340
- const powql = `${(0, powdb_js_1.quotePowqlIdent)(through.table)} filter .${sourceJCol} in (${placeholders}) { .${sourceJCol}, .${targetJCol} }`;
1525
+ const keyClause = this.inClause(`.${sourceJCol}`, chunk, params, sourceJColMeta);
1526
+ const powql = `${(0, powdb_js_1.quotePowqlIdent)(through.table)} filter ${keyClause} { .${sourceJCol}, .${targetJCol} }`;
1341
1527
  const { rows } = await this.exec(powql, params, timeout, 'findMany');
1342
1528
  for (const row of rows) {
1343
1529
  const sv = String(row[sourceJCol]);
@@ -1355,8 +1541,9 @@ class PowqlInterface {
1355
1541
  const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
1356
1542
  const targetByPk = new Map();
1357
1543
  const targetValList = [...allTargetVals].map((v) => targetPkColMeta ? coerceScalar(v, targetPkColMeta.tsType) : v);
1358
- for (let i = 0; i < targetValList.length; i += MAX_RELATION_KEYS) {
1359
- const chunk = targetValList.slice(i, i + MAX_RELATION_KEYS);
1544
+ const targetChunk = this.keyChunkSize(targetMeta, targetPkCol);
1545
+ for (let i = 0; i < targetValList.length; i += targetChunk) {
1546
+ const chunk = targetValList.slice(i, i + targetChunk);
1360
1547
  const where = {
1361
1548
  ...options.where,
1362
1549
  [targetPkField]: { in: chunk },
@@ -1466,6 +1653,11 @@ class PowqlInterface {
1466
1653
  if ((options.limit !== undefined || options.offset) && (single || parentCount > MAX_RELATION_KEYS)) {
1467
1654
  return false;
1468
1655
  }
1656
+ // A `limit 0` relation stays off the join statement for the same reason it
1657
+ // stays off a nested projection: PowDB answered `limit 0` with one row below
1658
+ // engine 0.20. The loader resolves it client-side, correctly on every version.
1659
+ if (options.limit === 0)
1660
+ return false;
1469
1661
  return true;
1470
1662
  }
1471
1663
  /**
@@ -1507,6 +1699,7 @@ class PowqlInterface {
1507
1699
  const childCols = this.joinChildCols(targetQi, options, includePii);
1508
1700
  const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 'c', params, options.timeout ?? timeout);
1509
1701
  const order = targetQi.buildOrder(options.orderBy, params, 'c');
1702
+ this.assertPagination(options.limit, options.offset, `relation "${relName}"`);
1510
1703
  const limitClause = options.limit !== undefined ? ` limit ${this.param(options.limit, params)}` : '';
1511
1704
  const offsetClause = options.offset ? ` offset ${this.param(options.offset, params)}` : '';
1512
1705
  const proj = this.joinProjection(childCols, `p.${(0, powdb_js_1.quotePowqlIdent)(parentKeyCol)}`, 'c');
@@ -1661,6 +1854,10 @@ class PowqlInterface {
1661
1854
  * - m2m (the block takes exactly one child table; the junction-order
1662
1855
  * stitch has no nested equivalent), and composite relation keys;
1663
1856
  * - a to-one relation carrying `limit`/`offset` (the loaders' semantics);
1857
+ * - a relation `limit` of exactly 0 (a nested block would emit `limit 0`,
1858
+ * which PowDB's projection fast path answered with ONE row below engine
1859
+ * 0.20; the loader path resolves it client-side and is correct on every
1860
+ * version). A NEGATIVE relation limit is refused outright, not fallen back;
1664
1861
  * - `distinct` inside the relation options (no nested grammar for it);
1665
1862
  * - a projected child column whose tsType is `bigint` or `Uint8Array`
1666
1863
  * (values ride a JSON array, which cannot carry them losslessly);
@@ -1686,6 +1883,9 @@ class PowqlInterface {
1686
1883
  const single = rel.type === 'belongsTo' || rel.type === 'hasOne';
1687
1884
  if (single && (options.limit !== undefined || options.offset))
1688
1885
  return null;
1886
+ this.assertPagination(options.limit, options.offset, `relation "${relName}"`);
1887
+ if (options.limit === 0)
1888
+ return null;
1689
1889
  const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
1690
1890
  const cols = targetQi.projectedColumns(options.select, options.omit, includePii);
1691
1891
  const byName = new Map(targetQi.meta.columns.map((c) => [c.name, c]));
@@ -2020,6 +2220,52 @@ class PowqlInterface {
2020
2220
  return row;
2021
2221
  });
2022
2222
  }
2223
+ /**
2224
+ * Refuse a `createMany` whose rows do not all name the SAME fields.
2225
+ *
2226
+ * PowQL could express it: a multi-row insert carries one `{ col := … }` tuple
2227
+ * per row, each with its own column list, so a ragged call inserts every
2228
+ * value. The SQL engines cannot, their single statement takes its column list
2229
+ * from the first row, so a field only a later row names is dropped and a field
2230
+ * only the first row names is written as NULL over that column's default. They
2231
+ * refuse it (ValidationError, query/writes.ts), and PowDB matching that
2232
+ * refusal is what makes the call portable: a shape accepted here and rejected
2233
+ * by every other engine turns a PowDB-to-Postgres move into a hard error found
2234
+ * in production rather than at the first run.
2235
+ *
2236
+ * Runs AFTER `applyPkDefault`, on the rows as they will actually be written:
2237
+ * a defaulted string PK is filled in on every row, so `[{}, { name }]` is
2238
+ * refused for the missing `name`, not for the PK the client supplied itself.
2239
+ * Rows that all name the same fields (the overwhelmingly common shape,
2240
+ * including N rows of pure defaults) cost one `Object.keys` pass and emit
2241
+ * byte-identical PowQL.
2242
+ */
2243
+ assertUniformCreateManyRows(rows) {
2244
+ const definedKeys = (row) => Object.keys(row).filter((k) => row[k] !== undefined);
2245
+ const firstKeys = definedKeys(rows[0]);
2246
+ const expected = new Set(firstKeys);
2247
+ const quoteList = (names) => names.map((n) => `"${n}"`).join(', ');
2248
+ for (let i = 1; i < rows.length; i++) {
2249
+ const rowKeys = definedKeys(rows[i]);
2250
+ // No stranger and the same count means the same set (object keys are unique).
2251
+ const unexpected = rowKeys.filter((k) => !expected.has(k));
2252
+ if (unexpected.length === 0 && rowKeys.length === expected.size)
2253
+ continue;
2254
+ const present = new Set(rowKeys);
2255
+ const missing = firstKeys.filter((k) => !present.has(k));
2256
+ const parts = [];
2257
+ if (missing.length > 0)
2258
+ parts.push(`does not supply ${quoteList(missing)}`);
2259
+ if (unexpected.length > 0)
2260
+ parts.push(`supplies ${quoteList(unexpected)}, which the first row does not`);
2261
+ throw new errors_js_1.ValidationError(`[turbine] createMany on "${this.table}": row ${i} ${parts.join(' and ')}. ` +
2262
+ 'Every row must supply the same fields (a field set to `undefined` counts as omitted, exactly as it does ' +
2263
+ 'in `create`). PowQL itself would insert the ragged rows, but the SQL engines build ONE statement whose ' +
2264
+ "column list comes from the first row, so there a later row's extra field is dropped and a field it " +
2265
+ "omits is written as NULL over that column's default. Supply the field explicitly on every row, or " +
2266
+ 'split the call into one createMany per field set.');
2267
+ }
2268
+ }
2023
2269
  async createMany(args) {
2024
2270
  return this.withMiddleware('createMany', args, async () => {
2025
2271
  if (args.skipDuplicates) {
@@ -2030,6 +2276,7 @@ class PowqlInterface {
2030
2276
  const inputs = args.data.map((d) => this.applyPkDefault(d));
2031
2277
  if (!inputs.length)
2032
2278
  return [];
2279
+ this.assertUniformCreateManyRows(inputs);
2033
2280
  const params = [];
2034
2281
  const tuples = inputs.map((d) => {
2035
2282
  const assigns = this.scalarData(d);
@@ -2286,6 +2533,34 @@ class PowqlInterface {
2286
2533
  return Number((rows[0]?.value ?? rows[0]?.count ?? 0));
2287
2534
  });
2288
2535
  }
2536
+ /**
2537
+ * Gate ONE field of a per-field `_count`.
2538
+ *
2539
+ * `_count: { col: true }` compiles to `count(T { .col })`, which counts
2540
+ * NON-NULL values of the column (SQL's `COUNT(col)`) only from engine 0.20 on.
2541
+ * Below 0.20 both PowDB frontends ignored the projection and returned the ROW
2542
+ * count.
2543
+ *
2544
+ * That divergence only EXISTS on a nullable column: where the column is NOT
2545
+ * NULL the row count and the non-null count are the same number, so the
2546
+ * pre-0.20 answer was already the right one and the query keeps working. The
2547
+ * gate is therefore per-column, not per-call: refusing the whole feature would
2548
+ * take a correct, working call away from every user on the engine line Turbine
2549
+ * shipped against and hand them nothing.
2550
+ *
2551
+ * Nullability comes from the column metadata (`introspectPowdbDatabase` reads
2552
+ * PowDB's `required` modifier; `defineSchema` declares it). Metadata that
2553
+ * claims NOT NULL for a column the live catalog lets be null would count rows
2554
+ * instead of values below 0.20, the same drift any stale-metadata query has.
2555
+ */
2556
+ assertProjectedCountSupported(field) {
2557
+ const col = this.column(field);
2558
+ if (!col.nullable)
2559
+ return;
2560
+ (0, powdb_js_1.requireCapability)(this.capabilities, 'projectedCountNonNull', `per-field \`_count\` of the nullable column "${col.name}"`, 'Below that version the engine ignored the column projection and returned the ROW count, which differs from ' +
2561
+ 'every SQL engine exactly when the column is nullable. `_count: true` (a row count) and a per-field ' +
2562
+ '`_count` of a NOT NULL column are correct on every version and are never refused.');
2563
+ }
2289
2564
  async aggregate(args) {
2290
2565
  return this.withMiddleware('aggregate', args, async () => {
2291
2566
  // One scalar query per aggregate, PowDB's bare-projection aggregate is broken.
@@ -2307,6 +2582,7 @@ class PowqlInterface {
2307
2582
  else {
2308
2583
  const counts = {};
2309
2584
  for (const field of Object.keys(args._count).filter((f) => args._count[f])) {
2585
+ this.assertProjectedCountSupported(field);
2310
2586
  counts[field] = (await scalar(`count(${this.qt}${filter} { ${this.ref(field)} })`)) ?? 0;
2311
2587
  }
2312
2588
  result._count = counts;
@@ -2476,7 +2752,11 @@ class PowqlInterface {
2476
2752
  const having = this.buildHaving(args.having, params, aggInner);
2477
2753
  const order = this.buildGroupOrder(args.orderBy, byOrderExprs, aggOrderExprs);
2478
2754
  // LIMIT / OFFSET over the result groups, applied after ORDER BY (mirrors
2479
- // the SQL groupBy). offset 0 is a no-op, matching findMany.
2755
+ // the SQL groupBy). offset 0 is a no-op, matching findMany; `limit: 0` is
2756
+ // answered client-side (PowDB returned one row for it below engine 0.20).
2757
+ this.assertPagination(args.limit, args.offset, 'groupBy');
2758
+ if (args.limit === 0)
2759
+ return [];
2480
2760
  const limitClause = args.limit !== undefined ? ` limit ${this.param(args.limit, params)}` : '';
2481
2761
  const offsetClause = args.offset ? ` offset ${this.param(args.offset, params)}` : '';
2482
2762
  const powql = `${this.qt}${filter} group ${groupExprs.join(', ')}${having}${order}${limitClause}${offsetClause} { ${proj.join(', ')} }`;
@@ -25,7 +25,10 @@
25
25
  * , the un-awaited delegate calls defer to Turbine's `build*()` methods and
26
26
  * run atomically through the core batch `$transaction([...])` path.
27
27
  * - **Raw SQL**: `$queryRaw` / `$executeRaw` tagged templates (with
28
- * `Prisma.sql`-style nested-fragment flattening) and the `*Unsafe` variants.
28
+ * `Prisma.sql`-style nested-fragment flattening) and the `*Unsafe` variants,
29
+ * on the client AND on the transaction client, where they run on the
30
+ * transaction's own connection so a mixed raw + delegate `$transaction` stays
31
+ * atomic.
29
32
  * - **Result reshaping**: `_count` objects keyed back to Prisma relation names,
30
33
  * and to-one relations surfaced as `object | null`.
31
34
  *
@@ -128,6 +131,18 @@ export interface CompatQueryInterface {
128
131
  /** A transaction-scoped client handed to a `$transaction(callback)`. */
129
132
  export interface CompatTransactionClient {
130
133
  table(name: string): CompatQueryInterface;
134
+ /**
135
+ * Execute a prebuilt `(text, params)` statement on the TRANSACTION's own
136
+ * connection (core `TransactionClient.rawQuery`). It is what backs the
137
+ * transaction-scoped `$queryRaw` / `$executeRaw`; without it those methods
138
+ * would have to reach around the transaction to the pool, which silently
139
+ * breaks atomicity. Optional only so a test stub can omit it: when it is
140
+ * absent the raw methods throw instead of escaping the transaction.
141
+ */
142
+ rawQuery?(text: string, params?: readonly unknown[]): Promise<{
143
+ rows: unknown[];
144
+ rowCount: number | null;
145
+ }>;
131
146
  }
132
147
  /** The minimal `TurbineClient` surface the adapter consumes. */
133
148
  export interface CompatTurbineClient extends CompatTransactionClient {
@@ -135,6 +150,21 @@ export interface CompatTurbineClient extends CompatTransactionClient {
135
150
  $transaction<R>(fn: (tx: CompatTransactionClient) => Promise<R>, options?: unknown): Promise<R>;
136
151
  $transaction(queries: readonly DeferredQuery<unknown>[]): Promise<unknown[]>;
137
152
  }
153
+ /**
154
+ * Brand marking an object as a raw-SQL fragment whose `strings` are spliced
155
+ * VERBATIM into the emitted statement (see `flattenTemplate`). Deliberately a
156
+ * module-private `Symbol()` and NOT `Symbol.for(...)`: a registry symbol is
157
+ * reachable by name from anywhere in the process, so any dependency could mint
158
+ * an object that flattens as trusted SQL. With a private symbol the only way to
159
+ * obtain a fragment is to call `Prisma.sql` / `Prisma.join` / `Prisma.raw` from
160
+ * this module.
161
+ *
162
+ * The fragment check is fail-CLOSED: an object that does not carry this exact
163
+ * symbol is bound as a `$N` parameter, never spliced. That is also what makes
164
+ * the (contrived) dual-package case safe rather than dangerous, a fragment
165
+ * built by the ESM copy of this module and executed by the CJS copy binds as a
166
+ * parameter instead of composing.
167
+ */
138
168
  declare const SQL_FRAGMENT: unique symbol;
139
169
  /** A composable SQL fragment, the local stand-in for `Prisma.Sql`. */
140
170
  export interface Sql {
@@ -227,16 +257,24 @@ export interface PrismaModelDelegate<M extends PrismaModelTypes> {
227
257
  aggregate(args: Args): Promise<Record<string, unknown>>;
228
258
  groupBy(args: Args): Promise<Record<string, unknown>[]>;
229
259
  }
260
+ /**
261
+ * The four Prisma raw-SQL methods. Present on BOTH the client and the
262
+ * transaction-scoped client, exactly as in Prisma, so a migrated call site that
263
+ * mixes `$transaction` with raw SQL keeps working, and the raw statement runs on
264
+ * the transaction's own connection.
265
+ */
266
+ export interface PrismaCompatRawSurface {
267
+ $queryRaw<T = unknown>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
268
+ $queryRawUnsafe<T = unknown>(sql: string, ...params: unknown[]): Promise<T[]>;
269
+ $executeRaw(strings: TemplateStringsArray, ...values: unknown[]): Promise<number>;
270
+ $executeRawUnsafe(sql: string, ...params: unknown[]): Promise<number>;
271
+ }
230
272
  /** The client-level surface (`$transaction` / raw), added to the model map. */
231
- export interface PrismaCompatClientBase<S extends Record<string, PrismaModelTypes> = Record<string, PrismaModelTypes>> {
273
+ export interface PrismaCompatClientBase<S extends Record<string, PrismaModelTypes> = Record<string, PrismaModelTypes>> extends PrismaCompatRawSurface {
232
274
  $transaction<R>(fn: (tx: PrismaCompatTransactionClient<S>) => Promise<R>, options?: PrismaCompatTxOptions): Promise<R>;
233
275
  $transaction<P extends readonly PromiseLike<unknown>[]>(promises: readonly [...P]): Promise<{
234
276
  [K in keyof P]: Awaited<P[K]>;
235
277
  }>;
236
- $queryRaw<T = unknown>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
237
- $queryRawUnsafe<T = unknown>(sql: string, ...params: unknown[]): Promise<T[]>;
238
- $executeRaw(strings: TemplateStringsArray, ...values: unknown[]): Promise<number>;
239
- $executeRawUnsafe(sql: string, ...params: unknown[]): Promise<number>;
240
278
  $connect(): Promise<void>;
241
279
  $disconnect(): Promise<void>;
242
280
  }
@@ -246,10 +284,18 @@ export interface PrismaCompatTxOptions {
246
284
  timeout?: number;
247
285
  maxWait?: number;
248
286
  }
249
- /** The transaction-scoped client handed to a `$transaction(callback)`. */
287
+ /**
288
+ * The transaction-scoped client handed to a `$transaction(callback)`: a model
289
+ * delegate per Prisma model (under both spellings, as on the client) plus the
290
+ * raw-SQL surface, every one of them bound to the transaction's connection.
291
+ * Prisma's transaction client has no `$transaction` / `$connect` /
292
+ * `$disconnect`, and neither does this one.
293
+ */
250
294
  export type PrismaCompatTransactionClient<S extends Record<string, PrismaModelTypes> = Record<string, PrismaModelTypes>> = {
251
295
  [K in keyof S]: PrismaModelDelegate<S[K]>;
252
- };
296
+ } & {
297
+ [K in keyof S as Uncapitalize<K & string>]: PrismaModelDelegate<S[K]>;
298
+ } & PrismaCompatRawSurface;
253
299
  /**
254
300
  * The full typed compat client: a model delegate per Prisma model name, plus the
255
301
  * client-level `$transaction` / raw surface. Parameterize `S` with your