turbine-orm 0.75.0 → 0.76.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 (78) hide show
  1. package/README.md +48 -7
  2. package/dist/cjs/cli/compile-query.d.ts +22 -2
  3. package/dist/cjs/cli/compile-query.js +49 -5
  4. package/dist/cjs/cli/config.d.ts +2 -0
  5. package/dist/cjs/cli/config.js +1 -1
  6. package/dist/cjs/cli/destructive.js +78 -43
  7. package/dist/cjs/cli/index.d.ts +95 -1
  8. package/dist/cjs/cli/index.js +609 -145
  9. package/dist/cjs/cli/mcp.js +30 -1
  10. package/dist/cjs/cli/pii-predicate-guard.d.ts +25 -0
  11. package/dist/cjs/cli/pii-predicate-guard.js +72 -12
  12. package/dist/cjs/cli/rate-limit.js +38 -1
  13. package/dist/cjs/cli/studio.js +26 -5
  14. package/dist/cjs/cli/ui.d.ts +33 -0
  15. package/dist/cjs/cli/ui.js +53 -7
  16. package/dist/cjs/client.d.ts +13 -1
  17. package/dist/cjs/client.js +1 -1
  18. package/dist/cjs/errors.d.ts +12 -1
  19. package/dist/cjs/errors.js +11 -2
  20. package/dist/cjs/generate.d.ts +26 -0
  21. package/dist/cjs/generate.js +174 -27
  22. package/dist/cjs/index.d.ts +1 -1
  23. package/dist/cjs/index.js +1 -1
  24. package/dist/cjs/introspect.d.ts +17 -0
  25. package/dist/cjs/introspect.js +100 -1
  26. package/dist/cjs/mssql.d.ts +18 -0
  27. package/dist/cjs/mssql.js +20 -1
  28. package/dist/cjs/pipeline.js +44 -6
  29. package/dist/cjs/powql.js +51 -17
  30. package/dist/cjs/query/batched-loader.js +3 -3
  31. package/dist/cjs/query/builder.js +1 -1
  32. package/dist/cjs/query/relations.d.ts +5 -0
  33. package/dist/cjs/query/relations.js +141 -69
  34. package/dist/cjs/query/utils.d.ts +13 -0
  35. package/dist/cjs/query/utils.js +16 -0
  36. package/dist/cjs/serverless.d.ts +1 -1
  37. package/dist/cjs/serverless.js +1 -1
  38. package/dist/cjs/sqlite.d.ts +33 -1
  39. package/dist/cjs/sqlite.js +84 -3
  40. package/dist/cli/compile-query.d.ts +22 -2
  41. package/dist/cli/compile-query.js +50 -6
  42. package/dist/cli/config.d.ts +2 -0
  43. package/dist/cli/config.js +1 -1
  44. package/dist/cli/destructive.js +78 -43
  45. package/dist/cli/index.d.ts +95 -1
  46. package/dist/cli/index.js +604 -147
  47. package/dist/cli/mcp.js +30 -1
  48. package/dist/cli/pii-predicate-guard.d.ts +25 -0
  49. package/dist/cli/pii-predicate-guard.js +73 -13
  50. package/dist/cli/rate-limit.js +38 -1
  51. package/dist/cli/studio.js +27 -6
  52. package/dist/cli/ui.d.ts +33 -0
  53. package/dist/cli/ui.js +51 -7
  54. package/dist/client.d.ts +13 -1
  55. package/dist/client.js +1 -1
  56. package/dist/errors.d.ts +12 -1
  57. package/dist/errors.js +11 -2
  58. package/dist/generate.d.ts +26 -0
  59. package/dist/generate.js +172 -27
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js +1 -1
  62. package/dist/introspect.d.ts +17 -0
  63. package/dist/introspect.js +98 -1
  64. package/dist/mssql.d.ts +18 -0
  65. package/dist/mssql.js +20 -1
  66. package/dist/pipeline.js +44 -6
  67. package/dist/powql.js +53 -19
  68. package/dist/query/batched-loader.js +4 -4
  69. package/dist/query/builder.js +2 -2
  70. package/dist/query/relations.d.ts +5 -0
  71. package/dist/query/relations.js +141 -70
  72. package/dist/query/utils.d.ts +13 -0
  73. package/dist/query/utils.js +15 -0
  74. package/dist/serverless.d.ts +1 -1
  75. package/dist/serverless.js +1 -1
  76. package/dist/sqlite.d.ts +33 -1
  77. package/dist/sqlite.js +85 -4
  78. package/package.json +2 -2
package/dist/pipeline.js CHANGED
@@ -18,6 +18,7 @@
18
18
  * Sequential fallback covers HTTP-based drivers (Neon HTTP, Vercel Postgres, Cloudflare
19
19
  * Hyperdrive), mock pools in tests, and any pool that doesn't expose pg internals.
20
20
  */
21
+ import { postgresDialect } from './dialect.js';
21
22
  import { PipelineError, wrapPgError } from './errors.js';
22
23
  import { runPipelined, supportsExtendedPipeline } from './pipeline-submittable.js';
23
24
  /**
@@ -28,7 +29,7 @@ import { runPipelined, supportsExtendedPipeline } from './pipeline-submittable.j
28
29
  * The caller is responsible for acquiring the client and releasing it after
29
30
  * this function completes (in the finally block).
30
31
  */
31
- async function runSequential(client, queries, options = {}) {
32
+ async function runSequential(client, queries, dialect, options = {}) {
32
33
  const { transactional = true } = options;
33
34
  if (!transactional)
34
35
  return runIndependent(client, queries);
@@ -40,7 +41,7 @@ async function runSequential(client, queries, options = {}) {
40
41
  // `TURBINE_E004` nor a retryable flag for the one failure that is most
41
42
  // worth retrying.
42
43
  try {
43
- await client.query('BEGIN');
44
+ await client.query(dialect.beginStatement());
44
45
  }
45
46
  catch (err) {
46
47
  throw wrapPgError(err);
@@ -57,7 +58,7 @@ async function runSequential(client, queries, options = {}) {
57
58
  results.push(q.transform(raw));
58
59
  }
59
60
  try {
60
- await client.query('COMMIT');
61
+ await client.query(dialect.commitStatement());
61
62
  }
62
63
  catch (err) {
63
64
  throw wrapPgError(err);
@@ -66,7 +67,7 @@ async function runSequential(client, queries, options = {}) {
66
67
  }
67
68
  catch (err) {
68
69
  try {
69
- await client.query('ROLLBACK');
70
+ await client.query(dialect.rollbackStatement());
70
71
  }
71
72
  catch {
72
73
  // Best-effort rollback
@@ -74,6 +75,40 @@ async function runSequential(client, queries, options = {}) {
74
75
  throw err;
75
76
  }
76
77
  }
78
+ /**
79
+ * The dialect whose transaction keywords this batch must use.
80
+ *
81
+ * `executePipeline` is handed a POOL and nothing else, so the pool is the only
82
+ * place the engine can be read from; the engine pool shims are the objects that
83
+ * know their own dialect, and one of them (`MssqlPool`) publishes it for exactly
84
+ * this. Anything else, a real `pg.Pool`, a serverless HTTP pool, a test mock,
85
+ * keeps PostgreSQL, which is what every one of them already spoke.
86
+ *
87
+ * This existed as three hard-coded strings, `BEGIN` / `COMMIT` / `ROLLBACK`,
88
+ * which is the one engine-specific decision in this file and the one it was not
89
+ * making. On SQL Server a bare `BEGIN` opens a statement BLOCK, not a
90
+ * transaction: `MssqlTxClient` matches the dialect's `BEGIN TRANSACTION` and
91
+ * nothing else, so a bare `BEGIN` missed that branch, reached the server as a
92
+ * block opener with no `END`, and was rejected; the `COMMIT` and `ROLLBACK`
93
+ * that followed then found no open transaction and silently did nothing. A
94
+ * pipeline that documents itself as atomic was neither atomic nor rolled back.
95
+ *
96
+ * Duck-typed rather than `instanceof`, deliberately: importing an engine module
97
+ * here would pull an optional peer's whole module graph into the Postgres path.
98
+ * The three methods tested are exactly the three called below, so a partial
99
+ * object can never be accepted and then fail at the call site.
100
+ */
101
+ function poolDialect(pool) {
102
+ const candidate = pool.dialect;
103
+ if (candidate !== null &&
104
+ typeof candidate === 'object' &&
105
+ typeof candidate.beginStatement === 'function' &&
106
+ typeof candidate.commitStatement === 'function' &&
107
+ typeof candidate.rollbackStatement === 'function') {
108
+ return candidate;
109
+ }
110
+ return postgresDialect;
111
+ }
77
112
  /**
78
113
  * Sequential fallback for `{ transactional: false }`: each query is
79
114
  * INDEPENDENT, which is what that option promises and what the real pipeline
@@ -177,8 +212,11 @@ export async function executePipeline(pool, queries, options) {
177
212
  const results = await runPipelined(client, queries, pipelineOptions);
178
213
  return results;
179
214
  }
180
- // Sequential fallback, reuses the same client
181
- return await runSequential(client, queries, options);
215
+ // Sequential fallback, reuses the same client. This is the path every
216
+ // non-Postgres engine takes (none of their clients expose pg's wire
217
+ // internals), so it is the one that has to speak the engine's transaction
218
+ // keywords rather than Postgres's.
219
+ return await runSequential(client, queries, poolDialect(pool), options);
182
220
  }
183
221
  finally {
184
222
  client.release();
package/dist/powql.js CHANGED
@@ -40,14 +40,14 @@ import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './
40
40
  import { ALL_POWDB_CAPABILITIES, coerceNativeValue, isJsonColumn, isPowdbDatetimeColumn, isStaleFramePowdbError, PowdbFloatParam, PowdbJsonParam, powqlColumnType, quotePowqlDotted, quotePowqlIdent, requireCapability, rowToEntity, } from './powdb.js';
41
41
  import { assertAggregatePiiOptIn } from './query/aggregates.js';
42
42
  import { assertWhereIdentifiesOneRow, expandCompoundUniqueWhere } from './query/compound-unique.js';
43
- import { isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/filters.js';
43
+ import { ARRAY_OPERATOR_KEYS, isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/filters.js';
44
44
  import { warnUnknownQueryOptions } from './query/option-surface.js';
45
45
  import { applyStableRelationOrderTo, normalizeWithClause } from './query/relation-names.js';
46
46
  // The privilege sentinel and its resolver: `includePii` / `allowFullTableScan`
47
47
  // are unlocked ONLY by the UNSAFE symbol, on this engine exactly as on the SQL
48
48
  // engines, so a spread request body cannot turn either on here either.
49
49
  import { assertDirectionToken, resolveSkipGlobalFilters, resolveUnsafeFlag, UNSAFE } from './query/types.js';
50
- import { escapeLike, normalizePagination, ownLookup, relationInProjectionMessage, resolveColumnName, resolveRelationDef, selectNamesNothingMessage, selectOmitExclusiveMessage, } from './query/utils.js';
50
+ import { availableClause, escapeLike, normalizePagination, ownLookup, relationInProjectionMessage, resolveColumnName, resolveRelationDef, selectNamesNothingMessage, selectOmitExclusiveMessage, } from './query/utils.js';
51
51
  import { assertJsonFilterKeys, jsonStringEntries, resolveGlobalFilterFrom } from './query/where.js';
52
52
  import { normalizeKeyColumns, snakeToCamel, } from './schema.js';
53
53
  /**
@@ -149,8 +149,17 @@ function rejectUnsupportedFilter(value, field) {
149
149
  if ('path' in value || 'hasKey' in value) {
150
150
  throw new UnsupportedFeatureError('JSON path/key filters', 'PowDB', `field "${field}"`);
151
151
  }
152
- if ('hasEvery' in value || 'hasSome' in value || 'isEmpty' in value) {
153
- throw new UnsupportedFeatureError('array filters', 'PowDB', `field "${field}"`);
152
+ // Enumerated from the SHARED {@link ARRAY_OPERATOR_KEYS} rather than
153
+ // re-listed here, because the hand-written copy had already drifted from it:
154
+ // `has` was missing, so `{ tags: { has: 'x' } }` was not recognised as an
155
+ // array filter at all, fell through to the bare-object equality branch, and
156
+ // bound the whole `{ has: 'x' }` OBJECT as a scalar parameter. A silent wrong
157
+ // answer where its three siblings raise E017. Two lists is how that happened;
158
+ // reading the one the SQL side reads is what stops it happening again.
159
+ for (const key of ARRAY_OPERATOR_KEYS) {
160
+ if (key in value) {
161
+ throw new UnsupportedFeatureError('array filters', 'PowDB', `field "${field}"`);
162
+ }
154
163
  }
155
164
  if ('search' in value) {
156
165
  throw new UnsupportedFeatureError('full-text search filters', 'PowDB', `field "${field}"`);
@@ -180,7 +189,7 @@ export class PowqlInterface {
180
189
  this.options = options;
181
190
  const meta = schema.tables[table];
182
191
  if (!meta) {
183
- throw new ValidationError(`[turbine] Unknown table "${table}". Available: ${Object.keys(schema.tables).join(', ')}`);
192
+ throw new ValidationError(`[turbine] Unknown table "${table}". ${availableClause(Object.keys(schema.tables), 'The schema has no tables.')}`);
184
193
  }
185
194
  this.meta = meta;
186
195
  this.defaultLimit = options.defaultLimit;
@@ -1153,8 +1162,18 @@ export class PowqlInterface {
1153
1162
  stripWritePii(entity) {
1154
1163
  if (!entity)
1155
1164
  return entity;
1165
+ // Same PRIMARY-KEY exemption as the SQL engines' `piiColumns` / `piiFields`
1166
+ // (query/writes.ts), and for the same measured reason: the returned row has
1167
+ // to stay ADDRESSABLE. Without it a PII-tagged key column came back deleted,
1168
+ // so the caller's follow-up `update`/`delete` built its where from a partial
1169
+ // key, the empty-where guard did not fire (the other member was present),
1170
+ // and the write hit every row sharing the remaining member. The policy is
1171
+ // "tag sensitive data, not keys": a PII PK is documented out of scope for
1172
+ // stripping. This docstring already cross-referenced the SQL rule; only the
1173
+ // exemption itself was missing.
1174
+ const pk = new Set(this.meta.primaryKey);
1156
1175
  for (const col of this.meta.columns) {
1157
- if (col.pii)
1176
+ if (col.pii && !pk.has(col.name))
1158
1177
  delete entity[col.field];
1159
1178
  }
1160
1179
  return entity;
@@ -1422,18 +1441,40 @@ export class PowqlInterface {
1422
1441
  if (args.cursor) {
1423
1442
  throw new UnsupportedFeatureError('cursor pagination', 'PowDB', 'use limit/offset instead');
1424
1443
  }
1444
+ // `distinct` names COLUMNS: Postgres compiles it to `SELECT DISTINCT ON
1445
+ // (col)`, one row per distinct value of those columns. PowQL's `distinct`
1446
+ // keyword is ROW-WIDE (`SELECT DISTINCT *`) and takes no column list, and
1447
+ // the language has no window functions to rebuild per-column distinct with,
1448
+ // so there is nothing faithful to emit. Accepting the option and emitting
1449
+ // the row-wide keyword returned a DIFFERENT row set under the same
1450
+ // argument, with no error and no warning, which is the one outcome the
1451
+ // dialect seam exists to prevent.
1452
+ //
1453
+ // Refused with the same E017 the other non-Postgres engines already raise
1454
+ // (query/builder.ts's `dialect.name !== 'postgresql'` gate) and in the same
1455
+ // words, so `distinct` now means one thing across every engine and matches
1456
+ // what the README documents. Refused BEFORE the column names are resolved,
1457
+ // matching that gate's position, so `distinct: ['nope']` reports the
1458
+ // unsupported feature rather than the typo on every engine alike.
1459
+ if (args.distinct?.length) {
1460
+ throw new UnsupportedFeatureError('DISTINCT ON (findMany distinct)', 'PowDB', "findMany({ distinct }) requires PostgreSQL: PowQL's `distinct` is row-wide, not per-column. " +
1461
+ 'Group in application code, or use groupBy({ by }) for one row per combination.');
1462
+ }
1425
1463
  const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
1426
1464
  const { cols, forcedPk } = this.projectionPlan(args.select, args.omit, resolveUnsafeFlag(args.includePii, 'includePii'));
1427
1465
  // Partition the `with` clause: nested-projection blocks vs loader residue.
1428
- // A parent `distinct` never nests (distinct over a row containing a JSON
1429
- // array is not a defined comparison), and a relation whose field name
1430
- // collides with a projected parent column stays on the loaders (its block
1431
- // key would duplicate the column's).
1466
+ // A relation whose field name collides with a projected parent column stays
1467
+ // on the loaders (its block key would duplicate the column's). A parent
1468
+ // `distinct` used to be a third exclusion here (distinct over a row
1469
+ // containing a JSON array is not a defined comparison); the refusal above
1470
+ // now settles that one statement earlier, so the branch cannot be reached
1471
+ // with a `distinct` present and testing for it again would be dead code
1472
+ // implying the option is still accepted.
1432
1473
  const withClause = args.with;
1433
1474
  const nestedPlans = [];
1434
1475
  const linkPlans = [];
1435
1476
  let residualWith = withClause;
1436
- if (withClause && !args.distinct?.length && this.nestedProjectionsPreferred(args)) {
1477
+ if (withClause && this.nestedProjectionsPreferred(args)) {
1437
1478
  const residue = {};
1438
1479
  for (const [relName, opt] of Object.entries(withClause)) {
1439
1480
  if (!opt)
@@ -1464,13 +1505,6 @@ export class PowqlInterface {
1464
1505
  const alias = nest ? 't0' : undefined;
1465
1506
  let where = this.buildWhere(resolvedWhere, params, alias);
1466
1507
  where = this.applyGlobalFilter(where, params, args.skipGlobalFilters, alias);
1467
- // PowQL's `distinct` is row-wide, so these names never reach the emitted
1468
- // statement. They are still caller-supplied names, and a name resolves or
1469
- // throws: reading the array for its LENGTH alone let `distinct: ['nope']`
1470
- // succeed here while every SQL engine refuses it. Validation only.
1471
- for (const key of args.distinct ?? [])
1472
- this.column(key);
1473
- const distinct = args.distinct?.length ? ' distinct' : '';
1474
1508
  const filter = where ? ` filter ${where}` : '';
1475
1509
  const order = this.buildOrder(args.orderBy, params, alias);
1476
1510
  const limit = this.effectiveLimit(args);
@@ -1498,7 +1532,7 @@ export class PowqlInterface {
1498
1532
  else {
1499
1533
  projection = this.projection(cols);
1500
1534
  }
1501
- const powql = `${this.qt}${nest ? ' as t0' : ''}${distinct}${filter}${order}${limitClause}${offsetClause} ${projection}`;
1535
+ const powql = `${this.qt}${nest ? ' as t0' : ''}${filter}${order}${limitClause}${offsetClause} ${projection}`;
1502
1536
  return { powql, resolvedWhere, nestedPlans, linkPlans, residualWith, forcedPk };
1503
1537
  }
1504
1538
  /** Build + run the findMany select; returns raw rows, the serving wire, the resolved where, and the `with` partition. */
@@ -56,7 +56,7 @@
56
56
  import { CircularRelationError, RelationError, UnsupportedFeatureError, ValidationError } from '../errors.js';
57
57
  import { normalizeKeyColumns } from '../schema.js';
58
58
  import { dedupeOrderEntries, isOrderBySpec, isRelationPickOrderBy, orderByEntries, sortedEntries } from './filters.js';
59
- import { markInternalCombinator, ownLookup, resolveColumnName, selectNamesNothingMessage, selectOmitExclusiveMessage, sqlToPreparedName, } from './utils.js';
59
+ import { availableClause, markInternalCombinator, ownLookup, resolveColumnName, selectNamesNothingMessage, selectOmitExclusiveMessage, sqlToPreparedName, } from './utils.js';
60
60
  /**
61
61
  * Max parent keys per follow-up query. On Postgres the whole key set travels as
62
62
  * ONE array parameter (`= ANY($1)`), so this is not a bind-parameter limit, it
@@ -419,7 +419,7 @@ export function resolveCountRelations(parentMeta, countSpec) {
419
419
  const rel = ownLookup(parentMeta.relations, relName);
420
420
  if (!rel) {
421
421
  throw new RelationError(`[turbine] Unknown relation "${relName}" in _count on table "${parentMeta.name}". ` +
422
- `Available: ${Object.keys(parentMeta.relations).join(', ')}`);
422
+ availableClause(Object.keys(parentMeta.relations), 'It has no relations.'));
423
423
  }
424
424
  if (!isToMany(rel)) {
425
425
  throw new ValidationError(`[turbine] _count is only supported for to-many relations; "${relName}" on ` +
@@ -495,7 +495,7 @@ export async function loadRelationsBatched(ctx, parents, withClause, timeout, de
495
495
  // E005 for this exact shape (relations.ts), and under 'auto' the two
496
496
  // must refuse identically or the error CODE depends on table size.
497
497
  throw new RelationError(`[turbine] Unknown relation "${relName}" on table "${ctx.parentMeta.name}". ` +
498
- `Available: ${Object.keys(ctx.parentMeta.relations).join(', ')}`);
498
+ availableClause(Object.keys(ctx.parentMeta.relations), 'It has no relations.'));
499
499
  }
500
500
  resolved.push({ relName, rel, options: spec === true ? {} : spec });
501
501
  }
@@ -523,7 +523,7 @@ export async function loadRelationsBatched(ctx, parents, withClause, timeout, de
523
523
  // not a line in a correctness fix. Until then both strategies say no.
524
524
  if (hasCount && depth > 0) {
525
525
  throw new RelationError(`[turbine] Unknown relation "_count" on table "${ctx.parentMeta.name}". ` +
526
- `Available: ${Object.keys(ctx.parentMeta.relations).join(', ')}. ` +
526
+ `${availableClause(Object.keys(ctx.parentMeta.relations), 'It has no relations.')} ` +
527
527
  '(`_count` is supported on the top-level `with` only, on every relationLoadStrategy.)');
528
528
  }
529
529
  // Fix key order BEFORE anything is awaited: the loads below all write their
@@ -23,7 +23,7 @@ import { warnUnknownQueryOptions } from './option-surface.js';
23
23
  import { applyStableRelationOrderTo, normalizeWithClause } from './relation-names.js';
24
24
  import * as relationsMod from './relations.js';
25
25
  import { resolveSkipGlobalFilters, resolveUnsafeFlag, UNSAFE } from './types.js';
26
- import { isTemporalInfinity, LRUCache, normalizePagination, ownLookup, parseDbDate, resolveColumnName, resolveRelation, resolveRelationDef, sqlToPreparedName, unknownFieldMessage, warnRedundantSortTerm, } from './utils.js';
26
+ import { availableClause, isTemporalInfinity, LRUCache, normalizePagination, ownLookup, parseDbDate, resolveColumnName, resolveRelation, resolveRelationDef, sqlToPreparedName, unknownFieldMessage, warnRedundantSortTerm, } from './utils.js';
27
27
  import { shouldWarnOnce, WARN_NS } from './warn-registry.js';
28
28
  import * as whereMod from './where.js';
29
29
  import * as writesMod from './writes.js';
@@ -669,7 +669,7 @@ export class QueryInterface {
669
669
  this.schema = schema;
670
670
  const meta = schema.tables[table];
671
671
  if (!meta) {
672
- throw new ValidationError(`[turbine] Unknown table "${table}". Available: ${Object.keys(schema.tables).join(', ')}`);
672
+ throw new ValidationError(`[turbine] Unknown table "${table}". ${availableClause(Object.keys(schema.tables), 'The schema has no tables.')}`);
673
673
  }
674
674
  this.tableMeta = meta;
675
675
  this.middlewares = middlewares ?? [];
@@ -71,6 +71,11 @@ export declare function resolveProjection(qi: BuilderCtx, table: string, meta: T
71
71
  * the existing call sites in builder.ts.
72
72
  */
73
73
  export declare function resolveColumns(qi: BuilderCtx, select?: Record<string, boolean>, omit?: Record<string, boolean>, includePii?: boolean): string[] | null;
74
+ /**
75
+ * Read a relation spec as options. See {@link EMPTY_WITH_OPTIONS} for why this
76
+ * is a shared authority rather than a guard repeated per walker.
77
+ */
78
+ export declare function relationOptions(spec: true | WithOptions): WithOptions;
74
79
  /**
75
80
  * Produce a fingerprint for a `with` clause tree. Recursion mirrors
76
81
  * buildSelectWithRelations / buildRelationSubquery.