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.
@@ -1352,11 +1352,11 @@ export class QueryInterface {
1352
1352
  const deferred = single
1353
1353
  ? this.buildFindUnique(baseArgs)
1354
1354
  : this.buildFindMany(baseArgs);
1355
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
1355
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1356
1356
  const rows = deferred.transform(result);
1357
1357
  const entities = single ? (rows ? [rows] : []) : rows;
1358
1358
  if (entities.length > 0) {
1359
- await loadRelationsBatched(this.batchedContext(args.timeout, skip, args.includePii === true), entities, batchedWith, args.timeout);
1359
+ await loadRelationsBatched(this.batchedContext(args.timeout, skip, args.includePii === true, args.forceCustomPlan === true), entities, batchedWith, args.timeout);
1360
1360
  }
1361
1361
  stripFields(entities, proj.strip);
1362
1362
  return single ? (entities[0] ?? null) : entities;
@@ -1372,7 +1372,7 @@ export class QueryInterface {
1372
1372
  * and unlimited-warnings silenced, a relation load must fetch every matching
1373
1373
  * child, and the per-relation `limit` is applied client-side by the loader.
1374
1374
  */
1375
- batchedContext(timeout, skip, includePii) {
1375
+ batchedContext(timeout, skip, includePii, forceCustomPlan = false) {
1376
1376
  const childOptions = {
1377
1377
  ...this.options,
1378
1378
  defaultLimit: undefined,
@@ -1382,7 +1382,11 @@ export class QueryInterface {
1382
1382
  parentMeta: this.tableMeta,
1383
1383
  schema: this.schema,
1384
1384
  makeChild: (table) => new QueryInterface(this.pool, table, this.schema, [], childOptions),
1385
- exec: (sql, params, preparedName) => this.queryWithTimeout(sql, params, timeout, preparedName),
1385
+ // The per-query `forceCustomPlan` opt-in covers the relation follow-ups
1386
+ // too: a batched load re-issues the SAME tenant-shaped predicate one
1387
+ // level down, so leaving those named would keep exactly the plan-cache
1388
+ // exposure the caller asked to be rid of.
1389
+ exec: (sql, params, preparedName) => this.queryWithTimeout(sql, params, timeout, this.preparedNameFor({ forceCustomPlan }, preparedName)),
1386
1390
  quote: (name) => this.q(name),
1387
1391
  buildInClause: (expr, paramRef, negated) => this.inClause(expr, paramRef, negated),
1388
1392
  inClauseParam: (values) => this.inParam(values),
@@ -1434,10 +1438,10 @@ export class QueryInterface {
1434
1438
  const { baseArgs, strip } = this.prepareBatchedBase(args, withClause);
1435
1439
  // baseArgs.with is always undefined here; the cast just bridges the R generic.
1436
1440
  const deferred = this.buildFindMany(baseArgs);
1437
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
1441
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1438
1442
  const entities = deferred.transform(result);
1439
1443
  if (entities.length > 0) {
1440
- await loadRelationsBatched(this.batchedContext(args.timeout, skip, args.includePii === true), entities, withClause, args.timeout);
1444
+ await loadRelationsBatched(this.batchedContext(args.timeout, skip, args.includePii === true, args.forceCustomPlan === true), entities, withClause, args.timeout);
1441
1445
  }
1442
1446
  stripFields(entities, strip);
1443
1447
  return entities;
@@ -1593,6 +1597,69 @@ export class QueryInterface {
1593
1597
  // Listener errors must never crash a query
1594
1598
  }
1595
1599
  }
1600
+ /**
1601
+ * Resolve the prepared-statement name a read should execute under, honouring
1602
+ * the per-query {@link FindManyArgs.forceCustomPlan} opt-in.
1603
+ *
1604
+ * `forceCustomPlan: true` returns `undefined`, which sends the statement
1605
+ * UNNAMED. The mechanism is NOT "PostgreSQL treats an unnamed statement as a
1606
+ * one-shot plan that never enters the plan cache": the backend builds and
1607
+ * saves a `CachedPlanSource` for the unnamed statement too. It works because
1608
+ * node-postgres only skips Parse for a statement it has already parsed BY
1609
+ * NAME (`Query.hasBeenParsed` is `this.name && connection.parsedStatements[this.name]`),
1610
+ * so an unnamed statement is re-Parsed on every execution, each Parse
1611
+ * replaces the unnamed cached plan source with a fresh one whose custom-plan
1612
+ * counter is zero, and the five-execution threshold that precedes promotion
1613
+ * is never reached. Every execution is therefore planned with the real
1614
+ * parameter values.
1615
+ *
1616
+ * No GUC is set, no `SET LOCAL` is emitted, no transaction is opened, and no
1617
+ * extra round trip is added, which is exactly why the opt-in can be per query
1618
+ * while the client-level `planCacheMode` (a connection parameter) cannot be.
1619
+ *
1620
+ * The refusal is deliberately here, at the one seam every read execution
1621
+ * passes through, rather than in each build method: the flag changes NOTHING
1622
+ * about the SQL text, so a build-time check would have had to be repeated in
1623
+ * every builder and could still be bypassed by a hand-executed
1624
+ * `DeferredQuery`.
1625
+ *
1626
+ * Engines whose dialect does not report {@link Dialect.supportsPlanCacheMode}
1627
+ * throw {@link UnsupportedFeatureError} (E017): the flag names a PostgreSQL
1628
+ * plan-cache guarantee, and an engine with no such cache cannot make it.
1629
+ * The same flag left unset (or `false`) is accepted everywhere.
1630
+ *
1631
+ * THE ONE COMBINATION THAT IS REFUSED RATHER THAN HONOURED. A client-level
1632
+ * `planCacheMode: 'force_generic_plan'` DEFEATS this option, and that was
1633
+ * MEASURED rather than reasoned about: on PostgreSQL 16.14, five executions
1634
+ * of one unnamed statement read 19,107 buffers with that setting in force and
1635
+ * 55 buffers with the same connection set back to `auto`, against 19,107 for
1636
+ * the named statement. So the setting governs the unnamed statement too, and
1637
+ * withholding the name buys nothing against it. Accepting the flag there
1638
+ * would report a guarantee the very next execution breaks, so the
1639
+ * contradiction throws {@link ValidationError} (E003) naming both settings.
1640
+ * Turbine can only see the setting IT applied: a `plan_cache_mode` installed
1641
+ * by the caller's own `SET`, by `ALTER ROLE`, or by a pooler is invisible
1642
+ * here and is not refused.
1643
+ */
1644
+ preparedNameFor(args, name) {
1645
+ if (args?.forceCustomPlan !== true)
1646
+ return name;
1647
+ if (this.dialect.supportsPlanCacheMode !== true) {
1648
+ throw new 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 ' +
1649
+ 'engine has no such cache to keep it out of. Remove the option, or set it only on PostgreSQL queries.');
1650
+ }
1651
+ if (this.options?.planCacheMode === 'force_generic_plan') {
1652
+ throw new ValidationError('[turbine] forceCustomPlan: true cannot be honoured on a client configured with ' +
1653
+ "planCacheMode: 'force_generic_plan'. That setting is a connection parameter and it governs UNNAMED " +
1654
+ 'statements as well as named ones, so the mechanism this option uses (withholding the ' +
1655
+ 'prepared-statement name, so the driver re-parses the statement on every execution and it is planned ' +
1656
+ 'with its real values) is ' +
1657
+ 'overridden by it and the query would be planned generically anyway. Leave the client on the default ' +
1658
+ '(`planCacheMode` unset, or `auto`) and force the custom plan per query: that is the combination that ' +
1659
+ 'expresses "custom here, auto there".');
1660
+ }
1661
+ return undefined;
1662
+ }
1596
1663
  /**
1597
1664
  * Execute a pool.query with an optional timeout.
1598
1665
  * If timeout is set, races the query against a timer and rejects on expiry.
@@ -1741,7 +1808,7 @@ export class QueryInterface {
1741
1808
  }
1742
1809
  }
1743
1810
  const deferred = this.buildFindUnique(args);
1744
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
1811
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1745
1812
  return deferred.transform(result);
1746
1813
  });
1747
1814
  }
@@ -1762,11 +1829,11 @@ export class QueryInterface {
1762
1829
  const proj = includeKeysForBatching(args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, args.includePii));
1763
1830
  const baseArgs = { ...args, with: undefined, select: proj.select, omit: proj.omit };
1764
1831
  const deferred = this.buildFindUnique(baseArgs);
1765
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
1832
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1766
1833
  const entity = deferred.transform(result);
1767
1834
  if (!entity)
1768
1835
  return null;
1769
- await loadRelationsBatched(this.batchedContext(args.timeout, args.skipGlobalFilters, args.includePii === true), [entity], withClause, args.timeout);
1836
+ await loadRelationsBatched(this.batchedContext(args.timeout, args.skipGlobalFilters, args.includePii === true, args.forceCustomPlan === true), [entity], withClause, args.timeout);
1770
1837
  stripFields([entity], proj.strip);
1771
1838
  return entity;
1772
1839
  }
@@ -1937,7 +2004,7 @@ export class QueryInterface {
1937
2004
  }
1938
2005
  }
1939
2006
  const deferred = this.buildFindMany(args);
1940
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, deferred.preparedName);
2007
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
1941
2008
  return deferred.transform(result);
1942
2009
  });
1943
2010
  }
@@ -2388,7 +2455,13 @@ export class QueryInterface {
2388
2455
  limit: batchSize + 1,
2389
2456
  });
2390
2457
  this.currentAction = 'findManyStream';
2391
- const speculativeResult = await this.queryWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout);
2458
+ // Streaming is ALREADY immune to the generic-plan cliff: the speculative
2459
+ // fetch has never passed a prepared name, and the cursor path runs through
2460
+ // DECLARE, so neither statement enters the plan cache. `preparedNameFor` is
2461
+ // still called with no name so that `forceCustomPlan: true` is VALIDATED on
2462
+ // an engine that cannot honour it here either, rather than being quietly
2463
+ // satisfied by an accident of this code path.
2464
+ const speculativeResult = await this.queryWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout, this.preparedNameFor(args, undefined));
2392
2465
  if (speculativeResult.rows.length <= batchSize) {
2393
2466
  // Small drain, yield all rows and return, no cursor needed
2394
2467
  for (const row of speculativeResult.rows) {
@@ -2451,7 +2524,7 @@ export class QueryInterface {
2451
2524
  }
2452
2525
  }
2453
2526
  const deferred = this.buildFindFirst(args);
2454
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, deferred.preparedName);
2527
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
2455
2528
  return deferred.transform(result);
2456
2529
  });
2457
2530
  }
@@ -2476,7 +2549,7 @@ export class QueryInterface {
2476
2549
  async findFirstOrThrow(args) {
2477
2550
  return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
2478
2551
  const deferred = this.buildFindFirstOrThrow(args);
2479
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, deferred.preparedName);
2552
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
2480
2553
  return deferred.transform(result);
2481
2554
  });
2482
2555
  }
@@ -2506,7 +2579,7 @@ export class QueryInterface {
2506
2579
  async findUniqueOrThrow(args) {
2507
2580
  return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
2508
2581
  const deferred = this.buildFindUniqueOrThrow(args);
2509
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
2582
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
2510
2583
  return deferred.transform(result);
2511
2584
  });
2512
2585
  }
@@ -2691,7 +2764,7 @@ export class QueryInterface {
2691
2764
  async count(args) {
2692
2765
  return this.executeWithMiddleware('count', (args ?? {}), async () => {
2693
2766
  const deferred = this.buildCount(args);
2694
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, deferred.preparedName);
2767
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
2695
2768
  return deferred.transform(result);
2696
2769
  });
2697
2770
  }
@@ -2735,7 +2808,7 @@ export class QueryInterface {
2735
2808
  async groupBy(args) {
2736
2809
  return this.executeWithMiddleware('groupBy', args, async () => {
2737
2810
  const deferred = this.buildGroupBy(args);
2738
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
2811
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
2739
2812
  return deferred.transform(result);
2740
2813
  });
2741
2814
  }
@@ -2754,7 +2827,7 @@ export class QueryInterface {
2754
2827
  async aggregate(args) {
2755
2828
  return this.executeWithMiddleware('aggregate', args, async () => {
2756
2829
  const deferred = this.buildAggregate(args);
2757
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
2830
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
2758
2831
  return deferred.transform(result);
2759
2832
  });
2760
2833
  }
@@ -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';
@@ -6,5 +6,6 @@
6
6
  * former monolithic `import { … } from './query.js'`.
7
7
  */
8
8
  export { postgresDialect } from '../dialect.js';
9
+ 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';
9
10
  export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
10
11
  export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, } from './builder.js';
@@ -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,209 @@
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
+ export const FIND_UNIQUE_OPTIONS = {
44
+ where: 'prisma',
45
+ select: 'prisma',
46
+ omit: 'prisma',
47
+ // Prisma spells this `include`; forwarding `with` would collide with the
48
+ // translated projection and carry turbine relation names into a Prisma call.
49
+ with: 'nativeAlias',
50
+ // Same key on both surfaces, DIFFERENT value domains ('query' | 'join' vs
51
+ // 'join' | 'batched' | 'auto' | 'flatten'), so the value needs mapping.
52
+ relationLoadStrategy: 'prisma',
53
+ timeout: 'native',
54
+ stableRelationOrder: 'native',
55
+ skipGlobalFilters: 'native',
56
+ includePii: 'native',
57
+ forceCustomPlan: 'native',
58
+ };
59
+ export const FIND_MANY_OPTIONS = {
60
+ where: 'prisma',
61
+ select: 'prisma',
62
+ omit: 'prisma',
63
+ orderBy: 'prisma',
64
+ cursor: 'prisma',
65
+ take: 'prisma',
66
+ distinct: 'prisma',
67
+ relationLoadStrategy: 'prisma',
68
+ with: 'nativeAlias',
69
+ limit: 'nativeAlias',
70
+ offset: 'nativeAlias',
71
+ timeout: 'native',
72
+ stableRelationOrder: 'native',
73
+ skipGlobalFilters: 'native',
74
+ warnOnUnlimited: 'native',
75
+ includePii: 'native',
76
+ forceCustomPlan: 'native',
77
+ };
78
+ export const FIND_MANY_STREAM_OPTIONS = {
79
+ ...FIND_MANY_OPTIONS,
80
+ // No streaming delegate exists on the compat surface, so this is not a known
81
+ // key there and passing it is reported rather than quietly ignored.
82
+ batchSize: 'internal',
83
+ };
84
+ export const CREATE_OPTIONS = {
85
+ data: 'prisma',
86
+ timeout: 'native',
87
+ };
88
+ export const CREATE_MANY_OPTIONS = {
89
+ data: 'prisma',
90
+ skipDuplicates: 'prisma',
91
+ timeout: 'native',
92
+ };
93
+ export const UPDATE_OPTIONS = {
94
+ where: 'prisma',
95
+ data: 'prisma',
96
+ // `{ field, expected }`, and `field` is a FIELD NAME, so it has to be renamed
97
+ // into turbine's naming space rather than copied. See THE ONE RULE above.
98
+ optimisticLock: 'prisma',
99
+ timeout: 'native',
100
+ allowFullTableScan: 'native',
101
+ skipGlobalFilters: 'native',
102
+ };
103
+ export const UPDATE_MANY_OPTIONS = {
104
+ where: 'prisma',
105
+ data: 'prisma',
106
+ timeout: 'native',
107
+ allowFullTableScan: 'native',
108
+ skipGlobalFilters: 'native',
109
+ };
110
+ export const DELETE_OPTIONS = {
111
+ where: 'prisma',
112
+ timeout: 'native',
113
+ allowFullTableScan: 'native',
114
+ skipGlobalFilters: 'native',
115
+ };
116
+ export const DELETE_MANY_OPTIONS = {
117
+ where: 'prisma',
118
+ timeout: 'native',
119
+ allowFullTableScan: 'native',
120
+ skipGlobalFilters: 'native',
121
+ };
122
+ export const UPSERT_OPTIONS = {
123
+ where: 'prisma',
124
+ create: 'prisma',
125
+ update: 'prisma',
126
+ timeout: 'native',
127
+ skipGlobalFilters: 'native',
128
+ };
129
+ export const COUNT_OPTIONS = {
130
+ where: 'prisma',
131
+ timeout: 'native',
132
+ skipGlobalFilters: 'native',
133
+ forceCustomPlan: 'native',
134
+ };
135
+ export const AGGREGATE_OPTIONS = {
136
+ where: 'prisma',
137
+ _count: 'prisma',
138
+ _sum: 'prisma',
139
+ _avg: 'prisma',
140
+ _min: 'prisma',
141
+ _max: 'prisma',
142
+ timeout: 'native',
143
+ skipGlobalFilters: 'native',
144
+ includePii: 'native',
145
+ forceCustomPlan: 'native',
146
+ };
147
+ export const GROUP_BY_OPTIONS = {
148
+ by: 'prisma',
149
+ where: 'prisma',
150
+ having: 'prisma',
151
+ orderBy: 'prisma',
152
+ _count: 'prisma',
153
+ _sum: 'prisma',
154
+ _avg: 'prisma',
155
+ _min: 'prisma',
156
+ _max: 'prisma',
157
+ // `{ columns, orderBy }`, both in FIELD-NAME space. See THE ONE RULE.
158
+ distinctOn: 'prisma',
159
+ limit: 'nativeAlias',
160
+ offset: 'nativeAlias',
161
+ timeout: 'native',
162
+ skipGlobalFilters: 'native',
163
+ includePii: 'native',
164
+ forceCustomPlan: 'native',
165
+ };
166
+ /**
167
+ * Every table, so a test can assert the set is complete and well-formed without
168
+ * naming each one (a table stubbed out during a refactor shows up here).
169
+ */
170
+ export const ALL_OPTION_TABLES = {
171
+ findUnique: FIND_UNIQUE_OPTIONS,
172
+ findMany: FIND_MANY_OPTIONS,
173
+ findManyStream: FIND_MANY_STREAM_OPTIONS,
174
+ create: CREATE_OPTIONS,
175
+ createMany: CREATE_MANY_OPTIONS,
176
+ update: UPDATE_OPTIONS,
177
+ updateMany: UPDATE_MANY_OPTIONS,
178
+ delete: DELETE_OPTIONS,
179
+ deleteMany: DELETE_MANY_OPTIONS,
180
+ upsert: UPSERT_OPTIONS,
181
+ count: COUNT_OPTIONS,
182
+ aggregate: AGGREGATE_OPTIONS,
183
+ groupBy: GROUP_BY_OPTIONS,
184
+ };
185
+ /**
186
+ * Copy every `'native'` key present on `src` onto `dst`.
187
+ *
188
+ * Iterates `src` (a small caller-supplied object) rather than the table, so the
189
+ * cost is proportional to what was actually passed. `undefined` values are
190
+ * skipped: `{ ...maybeOpts }` routinely materializes keys with no value, and
191
+ * writing `undefined` through would be indistinguishable from passing it.
192
+ */
193
+ export function applyNativeOptions(table, src, dst) {
194
+ // Total on any input: a delegate whose args are optional can be called with
195
+ // none, and a diagnostic-adjacent helper must not be the thing that throws.
196
+ if (src === null || typeof src !== 'object')
197
+ return;
198
+ for (const key of Object.keys(src)) {
199
+ if (table[key] !== 'native')
200
+ continue;
201
+ const value = src[key];
202
+ if (value !== undefined)
203
+ dst[key] = value;
204
+ }
205
+ }
206
+ /** The keys of `table` with the given kind, as a set. */
207
+ export function optionKeysOfKind(table, ...kinds) {
208
+ return Object.keys(table).filter((k) => kinds.includes(table[k]));
209
+ }