turbine-orm 0.55.0 → 0.57.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 (52) hide show
  1. package/README.md +16 -1
  2. package/dist/cjs/cli/index.d.ts +3 -1
  3. package/dist/cjs/cli/index.js +341 -14
  4. package/dist/cjs/client.d.ts +16 -1
  5. package/dist/cjs/client.js +7 -39
  6. package/dist/cjs/dialect.d.ts +9 -0
  7. package/dist/cjs/index-stats.d.ts +46 -0
  8. package/dist/cjs/index-stats.js +42 -1
  9. package/dist/cjs/plan-divergence.d.ts +511 -0
  10. package/dist/cjs/plan-divergence.js +790 -0
  11. package/dist/cjs/powql.d.ts +11 -0
  12. package/dist/cjs/powql.js +22 -0
  13. package/dist/cjs/prisma-compat.d.ts +32 -1
  14. package/dist/cjs/prisma-compat.js +297 -41
  15. package/dist/cjs/query/builder.d.ts +45 -0
  16. package/dist/cjs/query/builder.js +90 -17
  17. package/dist/cjs/query/deferred.d.ts +9 -0
  18. package/dist/cjs/query/index.d.ts +2 -0
  19. package/dist/cjs/query/index.js +18 -1
  20. package/dist/cjs/query/option-surface.d.ts +100 -0
  21. package/dist/cjs/query/option-surface.js +214 -0
  22. package/dist/cjs/query/types.d.ts +140 -0
  23. package/dist/cjs/query/utils.d.ts +30 -0
  24. package/dist/cjs/query/utils.js +67 -3
  25. package/dist/cjs/query/warn-registry.d.ts +8 -0
  26. package/dist/cjs/query/warn-registry.js +8 -0
  27. package/dist/cli/index.d.ts +3 -1
  28. package/dist/cli/index.js +341 -14
  29. package/dist/client.d.ts +16 -1
  30. package/dist/client.js +8 -40
  31. package/dist/dialect.d.ts +9 -0
  32. package/dist/index-stats.d.ts +46 -0
  33. package/dist/index-stats.js +42 -1
  34. package/dist/plan-divergence.d.ts +511 -0
  35. package/dist/plan-divergence.js +783 -0
  36. package/dist/powql.d.ts +11 -0
  37. package/dist/powql.js +22 -0
  38. package/dist/prisma-compat.d.ts +32 -1
  39. package/dist/prisma-compat.js +297 -41
  40. package/dist/query/builder.d.ts +45 -0
  41. package/dist/query/builder.js +90 -17
  42. package/dist/query/deferred.d.ts +9 -0
  43. package/dist/query/index.d.ts +2 -0
  44. package/dist/query/index.js +1 -0
  45. package/dist/query/option-surface.d.ts +100 -0
  46. package/dist/query/option-surface.js +209 -0
  47. package/dist/query/types.d.ts +140 -0
  48. package/dist/query/utils.d.ts +30 -0
  49. package/dist/query/utils.js +66 -3
  50. package/dist/query/warn-registry.d.ts +8 -0
  51. package/dist/query/warn-registry.js +8 -0
  52. package/package.json +1 -1
@@ -712,6 +712,51 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
712
712
  */
713
713
  resetUnlimitedWarnings(): void;
714
714
  private emitQueryEvent;
715
+ /**
716
+ * Resolve the prepared-statement name a read should execute under, honouring
717
+ * the per-query {@link FindManyArgs.forceCustomPlan} opt-in.
718
+ *
719
+ * `forceCustomPlan: true` returns `undefined`, which sends the statement
720
+ * UNNAMED. The mechanism is NOT "PostgreSQL treats an unnamed statement as a
721
+ * one-shot plan that never enters the plan cache": the backend builds and
722
+ * saves a `CachedPlanSource` for the unnamed statement too. It works because
723
+ * node-postgres only skips Parse for a statement it has already parsed BY
724
+ * NAME (`Query.hasBeenParsed` is `this.name && connection.parsedStatements[this.name]`),
725
+ * so an unnamed statement is re-Parsed on every execution, each Parse
726
+ * replaces the unnamed cached plan source with a fresh one whose custom-plan
727
+ * counter is zero, and the five-execution threshold that precedes promotion
728
+ * is never reached. Every execution is therefore planned with the real
729
+ * parameter values.
730
+ *
731
+ * No GUC is set, no `SET LOCAL` is emitted, no transaction is opened, and no
732
+ * extra round trip is added, which is exactly why the opt-in can be per query
733
+ * while the client-level `planCacheMode` (a connection parameter) cannot be.
734
+ *
735
+ * The refusal is deliberately here, at the one seam every read execution
736
+ * passes through, rather than in each build method: the flag changes NOTHING
737
+ * about the SQL text, so a build-time check would have had to be repeated in
738
+ * every builder and could still be bypassed by a hand-executed
739
+ * `DeferredQuery`.
740
+ *
741
+ * Engines whose dialect does not report {@link Dialect.supportsPlanCacheMode}
742
+ * throw {@link UnsupportedFeatureError} (E017): the flag names a PostgreSQL
743
+ * plan-cache guarantee, and an engine with no such cache cannot make it.
744
+ * The same flag left unset (or `false`) is accepted everywhere.
745
+ *
746
+ * THE ONE COMBINATION THAT IS REFUSED RATHER THAN HONOURED. A client-level
747
+ * `planCacheMode: 'force_generic_plan'` DEFEATS this option, and that was
748
+ * MEASURED rather than reasoned about: on PostgreSQL 16.14, five executions
749
+ * of one unnamed statement read 19,107 buffers with that setting in force and
750
+ * 55 buffers with the same connection set back to `auto`, against 19,107 for
751
+ * the named statement. So the setting governs the unnamed statement too, and
752
+ * withholding the name buys nothing against it. Accepting the flag there
753
+ * would report a guarantee the very next execution breaks, so the
754
+ * contradiction throws {@link ValidationError} (E003) naming both settings.
755
+ * Turbine can only see the setting IT applied: a `plan_cache_mode` installed
756
+ * by the caller's own `SET`, by `ALTER ROLE`, or by a pooler is invisible
757
+ * here and is not refused.
758
+ */
759
+ private preparedNameFor;
715
760
  /**
716
761
  * Execute a pool.query with an optional timeout.
717
762
  * If timeout is set, races the query against a timer and rejects on expiry.
@@ -1388,11 +1388,11 @@ class QueryInterface {
1388
1388
  const deferred = single
1389
1389
  ? this.buildFindUnique(baseArgs)
1390
1390
  : this.buildFindMany(baseArgs);
1391
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
1391
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1392
1392
  const rows = deferred.transform(result);
1393
1393
  const entities = single ? (rows ? [rows] : []) : rows;
1394
1394
  if (entities.length > 0) {
1395
- await (0, batched_loader_js_1.loadRelationsBatched)(this.batchedContext(args.timeout, skip, args.includePii === true), entities, batchedWith, args.timeout);
1395
+ await (0, batched_loader_js_1.loadRelationsBatched)(this.batchedContext(args.timeout, skip, args.includePii === true, args.forceCustomPlan === true), entities, batchedWith, args.timeout);
1396
1396
  }
1397
1397
  (0, batched_loader_js_1.stripFields)(entities, proj.strip);
1398
1398
  return single ? (entities[0] ?? null) : entities;
@@ -1408,7 +1408,7 @@ class QueryInterface {
1408
1408
  * and unlimited-warnings silenced, a relation load must fetch every matching
1409
1409
  * child, and the per-relation `limit` is applied client-side by the loader.
1410
1410
  */
1411
- batchedContext(timeout, skip, includePii) {
1411
+ batchedContext(timeout, skip, includePii, forceCustomPlan = false) {
1412
1412
  const childOptions = {
1413
1413
  ...this.options,
1414
1414
  defaultLimit: undefined,
@@ -1418,7 +1418,11 @@ class QueryInterface {
1418
1418
  parentMeta: this.tableMeta,
1419
1419
  schema: this.schema,
1420
1420
  makeChild: (table) => new QueryInterface(this.pool, table, this.schema, [], childOptions),
1421
- exec: (sql, params, preparedName) => this.queryWithTimeout(sql, params, timeout, preparedName),
1421
+ // The per-query `forceCustomPlan` opt-in covers the relation follow-ups
1422
+ // too: a batched load re-issues the SAME tenant-shaped predicate one
1423
+ // level down, so leaving those named would keep exactly the plan-cache
1424
+ // exposure the caller asked to be rid of.
1425
+ exec: (sql, params, preparedName) => this.queryWithTimeout(sql, params, timeout, this.preparedNameFor({ forceCustomPlan }, preparedName)),
1422
1426
  quote: (name) => this.q(name),
1423
1427
  buildInClause: (expr, paramRef, negated) => this.inClause(expr, paramRef, negated),
1424
1428
  inClauseParam: (values) => this.inParam(values),
@@ -1470,10 +1474,10 @@ class QueryInterface {
1470
1474
  const { baseArgs, strip } = this.prepareBatchedBase(args, withClause);
1471
1475
  // baseArgs.with is always undefined here; the cast just bridges the R generic.
1472
1476
  const deferred = this.buildFindMany(baseArgs);
1473
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
1477
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1474
1478
  const entities = deferred.transform(result);
1475
1479
  if (entities.length > 0) {
1476
- await (0, batched_loader_js_1.loadRelationsBatched)(this.batchedContext(args.timeout, skip, args.includePii === true), entities, withClause, args.timeout);
1480
+ await (0, batched_loader_js_1.loadRelationsBatched)(this.batchedContext(args.timeout, skip, args.includePii === true, args.forceCustomPlan === true), entities, withClause, args.timeout);
1477
1481
  }
1478
1482
  (0, batched_loader_js_1.stripFields)(entities, strip);
1479
1483
  return entities;
@@ -1629,6 +1633,69 @@ class QueryInterface {
1629
1633
  // Listener errors must never crash a query
1630
1634
  }
1631
1635
  }
1636
+ /**
1637
+ * Resolve the prepared-statement name a read should execute under, honouring
1638
+ * the per-query {@link FindManyArgs.forceCustomPlan} opt-in.
1639
+ *
1640
+ * `forceCustomPlan: true` returns `undefined`, which sends the statement
1641
+ * UNNAMED. The mechanism is NOT "PostgreSQL treats an unnamed statement as a
1642
+ * one-shot plan that never enters the plan cache": the backend builds and
1643
+ * saves a `CachedPlanSource` for the unnamed statement too. It works because
1644
+ * node-postgres only skips Parse for a statement it has already parsed BY
1645
+ * NAME (`Query.hasBeenParsed` is `this.name && connection.parsedStatements[this.name]`),
1646
+ * so an unnamed statement is re-Parsed on every execution, each Parse
1647
+ * replaces the unnamed cached plan source with a fresh one whose custom-plan
1648
+ * counter is zero, and the five-execution threshold that precedes promotion
1649
+ * is never reached. Every execution is therefore planned with the real
1650
+ * parameter values.
1651
+ *
1652
+ * No GUC is set, no `SET LOCAL` is emitted, no transaction is opened, and no
1653
+ * extra round trip is added, which is exactly why the opt-in can be per query
1654
+ * while the client-level `planCacheMode` (a connection parameter) cannot be.
1655
+ *
1656
+ * The refusal is deliberately here, at the one seam every read execution
1657
+ * passes through, rather than in each build method: the flag changes NOTHING
1658
+ * about the SQL text, so a build-time check would have had to be repeated in
1659
+ * every builder and could still be bypassed by a hand-executed
1660
+ * `DeferredQuery`.
1661
+ *
1662
+ * Engines whose dialect does not report {@link Dialect.supportsPlanCacheMode}
1663
+ * throw {@link UnsupportedFeatureError} (E017): the flag names a PostgreSQL
1664
+ * plan-cache guarantee, and an engine with no such cache cannot make it.
1665
+ * The same flag left unset (or `false`) is accepted everywhere.
1666
+ *
1667
+ * THE ONE COMBINATION THAT IS REFUSED RATHER THAN HONOURED. A client-level
1668
+ * `planCacheMode: 'force_generic_plan'` DEFEATS this option, and that was
1669
+ * MEASURED rather than reasoned about: on PostgreSQL 16.14, five executions
1670
+ * of one unnamed statement read 19,107 buffers with that setting in force and
1671
+ * 55 buffers with the same connection set back to `auto`, against 19,107 for
1672
+ * the named statement. So the setting governs the unnamed statement too, and
1673
+ * withholding the name buys nothing against it. Accepting the flag there
1674
+ * would report a guarantee the very next execution breaks, so the
1675
+ * contradiction throws {@link ValidationError} (E003) naming both settings.
1676
+ * Turbine can only see the setting IT applied: a `plan_cache_mode` installed
1677
+ * by the caller's own `SET`, by `ALTER ROLE`, or by a pooler is invisible
1678
+ * here and is not refused.
1679
+ */
1680
+ preparedNameFor(args, name) {
1681
+ if (args?.forceCustomPlan !== true)
1682
+ return name;
1683
+ if (this.dialect.supportsPlanCacheMode !== true) {
1684
+ throw new errors_js_1.UnsupportedFeatureError('The forceCustomPlan query option', this.dialect.name, 'Forcing a per-query custom plan means keeping the statement out of the PostgreSQL plan cache, and this ' +
1685
+ 'engine has no such cache to keep it out of. Remove the option, or set it only on PostgreSQL queries.');
1686
+ }
1687
+ if (this.options?.planCacheMode === 'force_generic_plan') {
1688
+ throw new errors_js_1.ValidationError('[turbine] forceCustomPlan: true cannot be honoured on a client configured with ' +
1689
+ "planCacheMode: 'force_generic_plan'. That setting is a connection parameter and it governs UNNAMED " +
1690
+ 'statements as well as named ones, so the mechanism this option uses (withholding the ' +
1691
+ 'prepared-statement name, so the driver re-parses the statement on every execution and it is planned ' +
1692
+ 'with its real values) is ' +
1693
+ 'overridden by it and the query would be planned generically anyway. Leave the client on the default ' +
1694
+ '(`planCacheMode` unset, or `auto`) and force the custom plan per query: that is the combination that ' +
1695
+ 'expresses "custom here, auto there".');
1696
+ }
1697
+ return undefined;
1698
+ }
1632
1699
  /**
1633
1700
  * Execute a pool.query with an optional timeout.
1634
1701
  * If timeout is set, races the query against a timer and rejects on expiry.
@@ -1777,7 +1844,7 @@ class QueryInterface {
1777
1844
  }
1778
1845
  }
1779
1846
  const deferred = this.buildFindUnique(args);
1780
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
1847
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1781
1848
  return deferred.transform(result);
1782
1849
  });
1783
1850
  }
@@ -1798,11 +1865,11 @@ class QueryInterface {
1798
1865
  const proj = (0, batched_loader_js_1.includeKeysForBatching)(args.select, args.omit, needed, (0, batched_loader_js_1.defaultProjectionFields)(this.tableMeta, args.includePii));
1799
1866
  const baseArgs = { ...args, with: undefined, select: proj.select, omit: proj.omit };
1800
1867
  const deferred = this.buildFindUnique(baseArgs);
1801
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
1868
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1802
1869
  const entity = deferred.transform(result);
1803
1870
  if (!entity)
1804
1871
  return null;
1805
- await (0, batched_loader_js_1.loadRelationsBatched)(this.batchedContext(args.timeout, args.skipGlobalFilters, args.includePii === true), [entity], withClause, args.timeout);
1872
+ await (0, batched_loader_js_1.loadRelationsBatched)(this.batchedContext(args.timeout, args.skipGlobalFilters, args.includePii === true, args.forceCustomPlan === true), [entity], withClause, args.timeout);
1806
1873
  (0, batched_loader_js_1.stripFields)([entity], proj.strip);
1807
1874
  return entity;
1808
1875
  }
@@ -1973,7 +2040,7 @@ class QueryInterface {
1973
2040
  }
1974
2041
  }
1975
2042
  const deferred = this.buildFindMany(args);
1976
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, deferred.preparedName);
2043
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
1977
2044
  return deferred.transform(result);
1978
2045
  });
1979
2046
  }
@@ -2424,7 +2491,13 @@ class QueryInterface {
2424
2491
  limit: batchSize + 1,
2425
2492
  });
2426
2493
  this.currentAction = 'findManyStream';
2427
- const speculativeResult = await this.queryWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout);
2494
+ // Streaming is ALREADY immune to the generic-plan cliff: the speculative
2495
+ // fetch has never passed a prepared name, and the cursor path runs through
2496
+ // DECLARE, so neither statement enters the plan cache. `preparedNameFor` is
2497
+ // still called with no name so that `forceCustomPlan: true` is VALIDATED on
2498
+ // an engine that cannot honour it here either, rather than being quietly
2499
+ // satisfied by an accident of this code path.
2500
+ const speculativeResult = await this.queryWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout, this.preparedNameFor(args, undefined));
2428
2501
  if (speculativeResult.rows.length <= batchSize) {
2429
2502
  // Small drain, yield all rows and return, no cursor needed
2430
2503
  for (const row of speculativeResult.rows) {
@@ -2487,7 +2560,7 @@ class QueryInterface {
2487
2560
  }
2488
2561
  }
2489
2562
  const deferred = this.buildFindFirst(args);
2490
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, deferred.preparedName);
2563
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
2491
2564
  return deferred.transform(result);
2492
2565
  });
2493
2566
  }
@@ -2512,7 +2585,7 @@ class QueryInterface {
2512
2585
  async findFirstOrThrow(args) {
2513
2586
  return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
2514
2587
  const deferred = this.buildFindFirstOrThrow(args);
2515
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, deferred.preparedName);
2588
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
2516
2589
  return deferred.transform(result);
2517
2590
  });
2518
2591
  }
@@ -2542,7 +2615,7 @@ class QueryInterface {
2542
2615
  async findUniqueOrThrow(args) {
2543
2616
  return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
2544
2617
  const deferred = this.buildFindUniqueOrThrow(args);
2545
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
2618
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
2546
2619
  return deferred.transform(result);
2547
2620
  });
2548
2621
  }
@@ -2727,7 +2800,7 @@ class QueryInterface {
2727
2800
  async count(args) {
2728
2801
  return this.executeWithMiddleware('count', (args ?? {}), async () => {
2729
2802
  const deferred = this.buildCount(args);
2730
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, deferred.preparedName);
2803
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
2731
2804
  return deferred.transform(result);
2732
2805
  });
2733
2806
  }
@@ -2771,7 +2844,7 @@ class QueryInterface {
2771
2844
  async groupBy(args) {
2772
2845
  return this.executeWithMiddleware('groupBy', args, async () => {
2773
2846
  const deferred = this.buildGroupBy(args);
2774
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
2847
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
2775
2848
  return deferred.transform(result);
2776
2849
  });
2777
2850
  }
@@ -2790,7 +2863,7 @@ class QueryInterface {
2790
2863
  async aggregate(args) {
2791
2864
  return this.executeWithMiddleware('aggregate', args, async () => {
2792
2865
  const deferred = this.buildAggregate(args);
2793
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
2866
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
2794
2867
  return deferred.transform(result);
2795
2868
  });
2796
2869
  }
@@ -127,6 +127,15 @@ export interface QueryInterfaceOptions {
127
127
  * (serverless drivers may not support named statements).
128
128
  */
129
129
  preparedStatements?: boolean;
130
+ /**
131
+ * The client-level `plan_cache_mode` this interface's connections were opened
132
+ * with, forwarded from `TurbineConfig.planCacheMode` purely so the per-query
133
+ * `forceCustomPlan` option can REFUSE the one combination it cannot honour
134
+ * (`'force_generic_plan'`, see `preparedNameFor` in builder.ts). Nothing else
135
+ * reads it, and it is never emitted into SQL: the setting itself travels as a
136
+ * connection parameter, set by client.ts when it opens the pool.
137
+ */
138
+ planCacheMode?: 'auto' | 'force_custom_plan' | 'force_generic_plan';
130
139
  /**
131
140
  * Enable the SQL template cache. When true, repeated queries with the
132
141
  * same shape (same keys, operators, relations, different values) reuse
@@ -8,6 +8,8 @@
8
8
  export type { AggregateArgs, AggregateResult, ArrayFilter, ColumnRef, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByAggregateSpec, GroupByArgs, GroupByDistinctOn, GroupByResult, HavingClause, JsonFilter, JsonPathAggregateTarget, JsonPathGroupKey, JsonPathOrderBy, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderByObject, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, RelationPickBy, RelationPickOrderBy, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithOrderByObject, WithResult, } from './types.js';
9
9
  export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, InsertStatementInput, UpsertStatementInput, } from '../dialect.js';
10
10
  export { postgresDialect } from '../dialect.js';
11
+ export type { OptionKind, OptionTable } from './option-surface.js';
12
+ export { AGGREGATE_OPTIONS, ALL_OPTION_TABLES, applyNativeOptions, COUNT_OPTIONS, CREATE_MANY_OPTIONS, CREATE_OPTIONS, DELETE_MANY_OPTIONS, DELETE_OPTIONS, FIND_MANY_OPTIONS, FIND_MANY_STREAM_OPTIONS, FIND_UNIQUE_OPTIONS, GROUP_BY_OPTIONS, optionKeysOfKind, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './option-surface.js';
11
13
  export type { SqlCacheEntry } from './utils.js';
12
14
  export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
13
15
  export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, TemporalInfinityReading, } from './builder.js';
@@ -7,9 +7,26 @@
7
7
  * former monolithic `import { … } from './query.js'`.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.sqlToPreparedName = exports.quoteIdent = exports.OPERATOR_KEYS = exports.LRUCache = exports.fnv1a64Hex = exports.escSingleQuote = exports.escapeLike = exports.buildCorrelation = exports.postgresDialect = void 0;
10
+ exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.sqlToPreparedName = exports.quoteIdent = exports.OPERATOR_KEYS = exports.LRUCache = exports.fnv1a64Hex = exports.escSingleQuote = exports.escapeLike = exports.buildCorrelation = exports.UPSERT_OPTIONS = exports.UPDATE_OPTIONS = exports.UPDATE_MANY_OPTIONS = exports.optionKeysOfKind = exports.GROUP_BY_OPTIONS = exports.FIND_UNIQUE_OPTIONS = exports.FIND_MANY_STREAM_OPTIONS = exports.FIND_MANY_OPTIONS = exports.DELETE_OPTIONS = exports.DELETE_MANY_OPTIONS = exports.CREATE_OPTIONS = exports.CREATE_MANY_OPTIONS = exports.COUNT_OPTIONS = exports.applyNativeOptions = exports.ALL_OPTION_TABLES = exports.AGGREGATE_OPTIONS = exports.postgresDialect = void 0;
11
11
  var dialect_js_1 = require("../dialect.js");
12
12
  Object.defineProperty(exports, "postgresDialect", { enumerable: true, get: function () { return dialect_js_1.postgresDialect; } });
13
+ var option_surface_js_1 = require("./option-surface.js");
14
+ Object.defineProperty(exports, "AGGREGATE_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.AGGREGATE_OPTIONS; } });
15
+ Object.defineProperty(exports, "ALL_OPTION_TABLES", { enumerable: true, get: function () { return option_surface_js_1.ALL_OPTION_TABLES; } });
16
+ Object.defineProperty(exports, "applyNativeOptions", { enumerable: true, get: function () { return option_surface_js_1.applyNativeOptions; } });
17
+ Object.defineProperty(exports, "COUNT_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.COUNT_OPTIONS; } });
18
+ Object.defineProperty(exports, "CREATE_MANY_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.CREATE_MANY_OPTIONS; } });
19
+ Object.defineProperty(exports, "CREATE_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.CREATE_OPTIONS; } });
20
+ Object.defineProperty(exports, "DELETE_MANY_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.DELETE_MANY_OPTIONS; } });
21
+ Object.defineProperty(exports, "DELETE_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.DELETE_OPTIONS; } });
22
+ Object.defineProperty(exports, "FIND_MANY_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.FIND_MANY_OPTIONS; } });
23
+ Object.defineProperty(exports, "FIND_MANY_STREAM_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.FIND_MANY_STREAM_OPTIONS; } });
24
+ Object.defineProperty(exports, "FIND_UNIQUE_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.FIND_UNIQUE_OPTIONS; } });
25
+ Object.defineProperty(exports, "GROUP_BY_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.GROUP_BY_OPTIONS; } });
26
+ Object.defineProperty(exports, "optionKeysOfKind", { enumerable: true, get: function () { return option_surface_js_1.optionKeysOfKind; } });
27
+ Object.defineProperty(exports, "UPDATE_MANY_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.UPDATE_MANY_OPTIONS; } });
28
+ Object.defineProperty(exports, "UPDATE_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.UPDATE_OPTIONS; } });
29
+ Object.defineProperty(exports, "UPSERT_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.UPSERT_OPTIONS; } });
13
30
  var utils_js_1 = require("./utils.js");
14
31
  Object.defineProperty(exports, "buildCorrelation", { enumerable: true, get: function () { return utils_js_1.buildCorrelation; } });
15
32
  Object.defineProperty(exports, "escapeLike", { enumerable: true, get: function () { return utils_js_1.escapeLike; } });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * turbine-orm, the query-argument OPTION SURFACE as runtime data.
3
+ *
4
+ * ## Why this file exists
5
+ *
6
+ * TypeScript erases interfaces, so `FindManyArgs` does not exist at runtime and
7
+ * any layer that has to decide, key by key, what to do with an args object has
8
+ * to keep its own hand-written list. `turbine-orm/prisma-compat` is exactly
9
+ * such a layer: it builds a FRESH turbine args object out of Prisma-shaped
10
+ * input and copies over the keys it recognizes. Every time core gained a
11
+ * query-level option, that ad-hoc allowlist silently failed to gain it, and the
12
+ * option was accepted by the caller's type-checker and then dropped on the
13
+ * floor. There was no feedback of any kind: no error, no warning, no test.
14
+ *
15
+ * These tables are the fix. Each one is a `Record<keyof SomeArgs<Row>,
16
+ * OptionKind>`, the same mechanism `TURBINE_CONFIG_KEYS` (client.ts) uses for
17
+ * the client-config surface, and it binds the compiler in BOTH directions:
18
+ *
19
+ * - add an option to an arg interface and this file stops compiling until a
20
+ * human classifies it ("Property 'fooMode' is missing in type ..."), so an
21
+ * option can no longer be stranded BY OMISSION;
22
+ * - list a key here that is not on the interface and it fails as an excess
23
+ * property, so a table can never drift into describing an option that does
24
+ * not exist.
25
+ *
26
+ * It deliberately does NOT make "add the option in one place" sufficient: it
27
+ * makes the second edit a BUILD FAILURE rather than a silent drop. That trade is
28
+ * intentional. A passthrough-by-default translator would satisfy the shorter
29
+ * wording and be actively wrong, because two of the options below carry FIELD
30
+ * NAMES in their values (`optimisticLock.field`, `distinctOn.columns`), which a
31
+ * compat layer must rename before core ever sees them. Copying those blind
32
+ * works on a schema whose names happen to coincide and breaks on one that
33
+ * renames a column, i.e. it makes the failure mode depend on the schema.
34
+ *
35
+ * ## THE ONE RULE for classifying a new option
36
+ *
37
+ * Classify a key `'native'` ONLY when its value contains no field, relation,
38
+ * column, or model NAME. If the value names anything in the schema, it is
39
+ * `'prisma'`: a name-translating consumer has to walk it by hand.
40
+ *
41
+ * @module
42
+ */
43
+ import type { AggregateArgs, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, UpdateArgs, UpdateManyArgs, UpsertArgs } from './types.js';
44
+ /**
45
+ * How a name-translating consumer (today: `turbine-orm/prisma-compat`) must
46
+ * handle one key of a turbine query-arg interface.
47
+ *
48
+ * - `'prisma'`, the key is a Prisma concept too, or its VALUE carries names
49
+ * that live in the caller's naming space. Translated by hand; NEVER copied
50
+ * verbatim.
51
+ * - `'native'`, turbine-only and its value is opaque to naming (a boolean, a
52
+ * number, a list of table names). Copied through untouched.
53
+ * - `'nativeAlias'`, the turbine SPELLING of a concept the caller's surface
54
+ * already has under another name (`with`/`limit`/`offset` vs
55
+ * `include`/`take`/`skip`). Refused, because forwarding it would collide with
56
+ * the translated key and would carry turbine relation names into a call
57
+ * written in the caller's names. The diagnostic names the right key instead.
58
+ * - `'internal'`, not reachable through the compat surface at all
59
+ * (`batchSize` belongs to a streaming method compat does not expose), so it
60
+ * is not part of any known set and passing it is reported as unknown.
61
+ */
62
+ export type OptionKind = 'prisma' | 'native' | 'nativeAlias' | 'internal';
63
+ /**
64
+ * The generic parameter the tables are instantiated at. `keyof FindManyArgs<T>`
65
+ * is the literal union of the DECLARED key names regardless of `T`, so a
66
+ * neutral row type keeps the tables stable and free of entity coupling.
67
+ */
68
+ type Row = Record<string, unknown>;
69
+ /** One option table: every declared key of one arg interface, classified. */
70
+ export type OptionTable<A> = Readonly<Record<keyof A, OptionKind>>;
71
+ export declare const FIND_UNIQUE_OPTIONS: OptionTable<FindUniqueArgs<Row>>;
72
+ export declare const FIND_MANY_OPTIONS: OptionTable<FindManyArgs<Row>>;
73
+ export declare const FIND_MANY_STREAM_OPTIONS: OptionTable<FindManyStreamArgs<Row>>;
74
+ export declare const CREATE_OPTIONS: OptionTable<CreateArgs<Row>>;
75
+ export declare const CREATE_MANY_OPTIONS: OptionTable<CreateManyArgs<Row>>;
76
+ export declare const UPDATE_OPTIONS: OptionTable<UpdateArgs<Row>>;
77
+ export declare const UPDATE_MANY_OPTIONS: OptionTable<UpdateManyArgs<Row>>;
78
+ export declare const DELETE_OPTIONS: OptionTable<DeleteArgs<Row>>;
79
+ export declare const DELETE_MANY_OPTIONS: OptionTable<DeleteManyArgs<Row>>;
80
+ export declare const UPSERT_OPTIONS: OptionTable<UpsertArgs<Row>>;
81
+ export declare const COUNT_OPTIONS: OptionTable<CountArgs<Row>>;
82
+ export declare const AGGREGATE_OPTIONS: OptionTable<AggregateArgs<Row>>;
83
+ export declare const GROUP_BY_OPTIONS: OptionTable<GroupByArgs<Row>>;
84
+ /**
85
+ * Every table, so a test can assert the set is complete and well-formed without
86
+ * naming each one (a table stubbed out during a refactor shows up here).
87
+ */
88
+ export declare const ALL_OPTION_TABLES: Readonly<Record<string, Readonly<Record<string, OptionKind>>>>;
89
+ /**
90
+ * Copy every `'native'` key present on `src` onto `dst`.
91
+ *
92
+ * Iterates `src` (a small caller-supplied object) rather than the table, so the
93
+ * cost is proportional to what was actually passed. `undefined` values are
94
+ * skipped: `{ ...maybeOpts }` routinely materializes keys with no value, and
95
+ * writing `undefined` through would be indistinguishable from passing it.
96
+ */
97
+ export declare function applyNativeOptions(table: Readonly<Record<string, OptionKind>>, src: Record<string, unknown>, dst: Record<string, unknown>): void;
98
+ /** The keys of `table` with the given kind, as a set. */
99
+ export declare function optionKeysOfKind(table: Readonly<Record<string, OptionKind>>, ...kinds: OptionKind[]): string[];
100
+ export {};
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm, the query-argument OPTION SURFACE as runtime data.
4
+ *
5
+ * ## Why this file exists
6
+ *
7
+ * TypeScript erases interfaces, so `FindManyArgs` does not exist at runtime and
8
+ * any layer that has to decide, key by key, what to do with an args object has
9
+ * to keep its own hand-written list. `turbine-orm/prisma-compat` is exactly
10
+ * such a layer: it builds a FRESH turbine args object out of Prisma-shaped
11
+ * input and copies over the keys it recognizes. Every time core gained a
12
+ * query-level option, that ad-hoc allowlist silently failed to gain it, and the
13
+ * option was accepted by the caller's type-checker and then dropped on the
14
+ * floor. There was no feedback of any kind: no error, no warning, no test.
15
+ *
16
+ * These tables are the fix. Each one is a `Record<keyof SomeArgs<Row>,
17
+ * OptionKind>`, the same mechanism `TURBINE_CONFIG_KEYS` (client.ts) uses for
18
+ * the client-config surface, and it binds the compiler in BOTH directions:
19
+ *
20
+ * - add an option to an arg interface and this file stops compiling until a
21
+ * human classifies it ("Property 'fooMode' is missing in type ..."), so an
22
+ * option can no longer be stranded BY OMISSION;
23
+ * - list a key here that is not on the interface and it fails as an excess
24
+ * property, so a table can never drift into describing an option that does
25
+ * not exist.
26
+ *
27
+ * It deliberately does NOT make "add the option in one place" sufficient: it
28
+ * makes the second edit a BUILD FAILURE rather than a silent drop. That trade is
29
+ * intentional. A passthrough-by-default translator would satisfy the shorter
30
+ * wording and be actively wrong, because two of the options below carry FIELD
31
+ * NAMES in their values (`optimisticLock.field`, `distinctOn.columns`), which a
32
+ * compat layer must rename before core ever sees them. Copying those blind
33
+ * works on a schema whose names happen to coincide and breaks on one that
34
+ * renames a column, i.e. it makes the failure mode depend on the schema.
35
+ *
36
+ * ## THE ONE RULE for classifying a new option
37
+ *
38
+ * Classify a key `'native'` ONLY when its value contains no field, relation,
39
+ * column, or model NAME. If the value names anything in the schema, it is
40
+ * `'prisma'`: a name-translating consumer has to walk it by hand.
41
+ *
42
+ * @module
43
+ */
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.ALL_OPTION_TABLES = exports.GROUP_BY_OPTIONS = exports.AGGREGATE_OPTIONS = exports.COUNT_OPTIONS = exports.UPSERT_OPTIONS = exports.DELETE_MANY_OPTIONS = exports.DELETE_OPTIONS = exports.UPDATE_MANY_OPTIONS = exports.UPDATE_OPTIONS = exports.CREATE_MANY_OPTIONS = exports.CREATE_OPTIONS = exports.FIND_MANY_STREAM_OPTIONS = exports.FIND_MANY_OPTIONS = exports.FIND_UNIQUE_OPTIONS = void 0;
46
+ exports.applyNativeOptions = applyNativeOptions;
47
+ exports.optionKeysOfKind = optionKeysOfKind;
48
+ exports.FIND_UNIQUE_OPTIONS = {
49
+ where: 'prisma',
50
+ select: 'prisma',
51
+ omit: 'prisma',
52
+ // Prisma spells this `include`; forwarding `with` would collide with the
53
+ // translated projection and carry turbine relation names into a Prisma call.
54
+ with: 'nativeAlias',
55
+ // Same key on both surfaces, DIFFERENT value domains ('query' | 'join' vs
56
+ // 'join' | 'batched' | 'auto' | 'flatten'), so the value needs mapping.
57
+ relationLoadStrategy: 'prisma',
58
+ timeout: 'native',
59
+ stableRelationOrder: 'native',
60
+ skipGlobalFilters: 'native',
61
+ includePii: 'native',
62
+ forceCustomPlan: 'native',
63
+ };
64
+ exports.FIND_MANY_OPTIONS = {
65
+ where: 'prisma',
66
+ select: 'prisma',
67
+ omit: 'prisma',
68
+ orderBy: 'prisma',
69
+ cursor: 'prisma',
70
+ take: 'prisma',
71
+ distinct: 'prisma',
72
+ relationLoadStrategy: 'prisma',
73
+ with: 'nativeAlias',
74
+ limit: 'nativeAlias',
75
+ offset: 'nativeAlias',
76
+ timeout: 'native',
77
+ stableRelationOrder: 'native',
78
+ skipGlobalFilters: 'native',
79
+ warnOnUnlimited: 'native',
80
+ includePii: 'native',
81
+ forceCustomPlan: 'native',
82
+ };
83
+ exports.FIND_MANY_STREAM_OPTIONS = {
84
+ ...exports.FIND_MANY_OPTIONS,
85
+ // No streaming delegate exists on the compat surface, so this is not a known
86
+ // key there and passing it is reported rather than quietly ignored.
87
+ batchSize: 'internal',
88
+ };
89
+ exports.CREATE_OPTIONS = {
90
+ data: 'prisma',
91
+ timeout: 'native',
92
+ };
93
+ exports.CREATE_MANY_OPTIONS = {
94
+ data: 'prisma',
95
+ skipDuplicates: 'prisma',
96
+ timeout: 'native',
97
+ };
98
+ exports.UPDATE_OPTIONS = {
99
+ where: 'prisma',
100
+ data: 'prisma',
101
+ // `{ field, expected }`, and `field` is a FIELD NAME, so it has to be renamed
102
+ // into turbine's naming space rather than copied. See THE ONE RULE above.
103
+ optimisticLock: 'prisma',
104
+ timeout: 'native',
105
+ allowFullTableScan: 'native',
106
+ skipGlobalFilters: 'native',
107
+ };
108
+ exports.UPDATE_MANY_OPTIONS = {
109
+ where: 'prisma',
110
+ data: 'prisma',
111
+ timeout: 'native',
112
+ allowFullTableScan: 'native',
113
+ skipGlobalFilters: 'native',
114
+ };
115
+ exports.DELETE_OPTIONS = {
116
+ where: 'prisma',
117
+ timeout: 'native',
118
+ allowFullTableScan: 'native',
119
+ skipGlobalFilters: 'native',
120
+ };
121
+ exports.DELETE_MANY_OPTIONS = {
122
+ where: 'prisma',
123
+ timeout: 'native',
124
+ allowFullTableScan: 'native',
125
+ skipGlobalFilters: 'native',
126
+ };
127
+ exports.UPSERT_OPTIONS = {
128
+ where: 'prisma',
129
+ create: 'prisma',
130
+ update: 'prisma',
131
+ timeout: 'native',
132
+ skipGlobalFilters: 'native',
133
+ };
134
+ exports.COUNT_OPTIONS = {
135
+ where: 'prisma',
136
+ timeout: 'native',
137
+ skipGlobalFilters: 'native',
138
+ forceCustomPlan: 'native',
139
+ };
140
+ exports.AGGREGATE_OPTIONS = {
141
+ where: 'prisma',
142
+ _count: 'prisma',
143
+ _sum: 'prisma',
144
+ _avg: 'prisma',
145
+ _min: 'prisma',
146
+ _max: 'prisma',
147
+ timeout: 'native',
148
+ skipGlobalFilters: 'native',
149
+ includePii: 'native',
150
+ forceCustomPlan: 'native',
151
+ };
152
+ exports.GROUP_BY_OPTIONS = {
153
+ by: 'prisma',
154
+ where: 'prisma',
155
+ having: 'prisma',
156
+ orderBy: 'prisma',
157
+ _count: 'prisma',
158
+ _sum: 'prisma',
159
+ _avg: 'prisma',
160
+ _min: 'prisma',
161
+ _max: 'prisma',
162
+ // `{ columns, orderBy }`, both in FIELD-NAME space. See THE ONE RULE.
163
+ distinctOn: 'prisma',
164
+ limit: 'nativeAlias',
165
+ offset: 'nativeAlias',
166
+ timeout: 'native',
167
+ skipGlobalFilters: 'native',
168
+ includePii: 'native',
169
+ forceCustomPlan: 'native',
170
+ };
171
+ /**
172
+ * Every table, so a test can assert the set is complete and well-formed without
173
+ * naming each one (a table stubbed out during a refactor shows up here).
174
+ */
175
+ exports.ALL_OPTION_TABLES = {
176
+ findUnique: exports.FIND_UNIQUE_OPTIONS,
177
+ findMany: exports.FIND_MANY_OPTIONS,
178
+ findManyStream: exports.FIND_MANY_STREAM_OPTIONS,
179
+ create: exports.CREATE_OPTIONS,
180
+ createMany: exports.CREATE_MANY_OPTIONS,
181
+ update: exports.UPDATE_OPTIONS,
182
+ updateMany: exports.UPDATE_MANY_OPTIONS,
183
+ delete: exports.DELETE_OPTIONS,
184
+ deleteMany: exports.DELETE_MANY_OPTIONS,
185
+ upsert: exports.UPSERT_OPTIONS,
186
+ count: exports.COUNT_OPTIONS,
187
+ aggregate: exports.AGGREGATE_OPTIONS,
188
+ groupBy: exports.GROUP_BY_OPTIONS,
189
+ };
190
+ /**
191
+ * Copy every `'native'` key present on `src` onto `dst`.
192
+ *
193
+ * Iterates `src` (a small caller-supplied object) rather than the table, so the
194
+ * cost is proportional to what was actually passed. `undefined` values are
195
+ * skipped: `{ ...maybeOpts }` routinely materializes keys with no value, and
196
+ * writing `undefined` through would be indistinguishable from passing it.
197
+ */
198
+ function applyNativeOptions(table, src, dst) {
199
+ // Total on any input: a delegate whose args are optional can be called with
200
+ // none, and a diagnostic-adjacent helper must not be the thing that throws.
201
+ if (src === null || typeof src !== 'object')
202
+ return;
203
+ for (const key of Object.keys(src)) {
204
+ if (table[key] !== 'native')
205
+ continue;
206
+ const value = src[key];
207
+ if (value !== undefined)
208
+ dst[key] = value;
209
+ }
210
+ }
211
+ /** The keys of `table` with the given kind, as a set. */
212
+ function optionKeysOfKind(table, ...kinds) {
213
+ return Object.keys(table).filter((k) => kinds.includes(table[k]));
214
+ }