turbine-orm 0.50.0 → 0.51.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 (186) hide show
  1. package/README.md +66 -66
  2. package/dist/adapters/cockroachdb.d.ts +5 -5
  3. package/dist/adapters/cockroachdb.js +10 -10
  4. package/dist/adapters/index.d.ts +5 -5
  5. package/dist/adapters/index.js +7 -7
  6. package/dist/adapters/yugabytedb.d.ts +7 -7
  7. package/dist/adapters/yugabytedb.js +10 -10
  8. package/dist/cjs/adapters/cockroachdb.d.ts +5 -5
  9. package/dist/cjs/adapters/cockroachdb.js +10 -10
  10. package/dist/cjs/adapters/index.d.ts +5 -5
  11. package/dist/cjs/adapters/index.js +7 -7
  12. package/dist/cjs/adapters/yugabytedb.d.ts +7 -7
  13. package/dist/cjs/adapters/yugabytedb.js +10 -10
  14. package/dist/cjs/cli/config.d.ts +13 -2
  15. package/dist/cjs/cli/config.js +3 -2
  16. package/dist/cjs/cli/destructive.d.ts +1 -1
  17. package/dist/cjs/cli/destructive.js +1 -1
  18. package/dist/cjs/cli/index.d.ts +10 -10
  19. package/dist/cjs/cli/index.js +49 -45
  20. package/dist/cjs/cli/loader.d.ts +7 -7
  21. package/dist/cjs/cli/loader.js +9 -9
  22. package/dist/cjs/cli/mcp.js +4 -4
  23. package/dist/cjs/cli/migrate.d.ts +5 -5
  24. package/dist/cjs/cli/migrate.js +11 -11
  25. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  26. package/dist/cjs/cli/ui.d.ts +2 -2
  27. package/dist/cjs/cli/ui.js +2 -2
  28. package/dist/cjs/client.d.ts +49 -38
  29. package/dist/cjs/client.js +57 -56
  30. package/dist/cjs/dialect.d.ts +62 -18
  31. package/dist/cjs/dialect.js +40 -2
  32. package/dist/cjs/errors.d.ts +5 -5
  33. package/dist/cjs/errors.js +11 -11
  34. package/dist/cjs/generate.d.ts +6 -6
  35. package/dist/cjs/generate.js +31 -29
  36. package/dist/cjs/index-advisor.d.ts +5 -5
  37. package/dist/cjs/index-advisor.js +0 -0
  38. package/dist/cjs/index.d.ts +1 -1
  39. package/dist/cjs/index.js +7 -7
  40. package/dist/cjs/introspect.d.ts +35 -9
  41. package/dist/cjs/introspect.js +83 -32
  42. package/dist/cjs/mssql.d.ts +11 -11
  43. package/dist/cjs/mssql.js +64 -29
  44. package/dist/cjs/mysql.d.ts +8 -8
  45. package/dist/cjs/mysql.js +61 -23
  46. package/dist/cjs/nested-write.d.ts +21 -2
  47. package/dist/cjs/nested-write.js +51 -14
  48. package/dist/cjs/optional-peer-import.cjs +7 -7
  49. package/dist/cjs/optional-peer-import.d.cts +7 -7
  50. package/dist/cjs/pipeline-submittable.d.ts +2 -2
  51. package/dist/cjs/pipeline-submittable.js +6 -6
  52. package/dist/cjs/pipeline.d.ts +1 -1
  53. package/dist/cjs/pipeline.js +4 -4
  54. package/dist/cjs/powdb-introspect.d.ts +1 -1
  55. package/dist/cjs/powdb-introspect.js +1 -1
  56. package/dist/cjs/powdb.d.ts +28 -28
  57. package/dist/cjs/powdb.js +66 -66
  58. package/dist/cjs/powql.d.ts +27 -27
  59. package/dist/cjs/powql.js +73 -52
  60. package/dist/cjs/query/aggregates.d.ts +1 -1
  61. package/dist/cjs/query/aggregates.js +5 -5
  62. package/dist/cjs/query/batched-loader.d.ts +11 -11
  63. package/dist/cjs/query/batched-loader.js +24 -24
  64. package/dist/cjs/query/builder.d.ts +39 -21
  65. package/dist/cjs/query/builder.js +99 -57
  66. package/dist/cjs/query/compound-unique.d.ts +1 -1
  67. package/dist/cjs/query/compound-unique.js +0 -0
  68. package/dist/cjs/query/deferred.d.ts +12 -6
  69. package/dist/cjs/query/deferred.js +1 -1
  70. package/dist/cjs/query/filters.d.ts +31 -11
  71. package/dist/cjs/query/filters.js +67 -14
  72. package/dist/cjs/query/index.d.ts +1 -1
  73. package/dist/cjs/query/index.js +1 -1
  74. package/dist/cjs/query/relations.d.ts +9 -9
  75. package/dist/cjs/query/relations.js +164 -57
  76. package/dist/cjs/query/types.d.ts +86 -35
  77. package/dist/cjs/query/types.js +1 -1
  78. package/dist/cjs/query/utils.d.ts +27 -10
  79. package/dist/cjs/query/utils.js +86 -14
  80. package/dist/cjs/query/where.d.ts +47 -28
  81. package/dist/cjs/query/where.js +130 -31
  82. package/dist/cjs/query/writes.d.ts +24 -5
  83. package/dist/cjs/query/writes.js +102 -13
  84. package/dist/cjs/realtime.d.ts +7 -7
  85. package/dist/cjs/realtime.js +9 -9
  86. package/dist/cjs/schema-builder.d.ts +18 -7
  87. package/dist/cjs/schema-builder.js +17 -10
  88. package/dist/cjs/schema-metadata.d.ts +3 -3
  89. package/dist/cjs/schema-metadata.js +9 -9
  90. package/dist/cjs/schema-sql.d.ts +9 -9
  91. package/dist/cjs/schema-sql.js +20 -20
  92. package/dist/cjs/schema.d.ts +19 -9
  93. package/dist/cjs/schema.js +6 -6
  94. package/dist/cjs/serverless.d.ts +15 -15
  95. package/dist/cjs/serverless.js +16 -16
  96. package/dist/cjs/sqlite.d.ts +8 -8
  97. package/dist/cjs/sqlite.js +53 -22
  98. package/dist/cjs/typed-sql.d.ts +4 -4
  99. package/dist/cjs/typed-sql.js +5 -5
  100. package/dist/cli/config.d.ts +13 -2
  101. package/dist/cli/config.js +3 -2
  102. package/dist/cli/destructive.d.ts +1 -1
  103. package/dist/cli/destructive.js +1 -1
  104. package/dist/cli/index.d.ts +10 -10
  105. package/dist/cli/index.js +49 -45
  106. package/dist/cli/loader.d.ts +7 -7
  107. package/dist/cli/loader.js +9 -9
  108. package/dist/cli/mcp.js +4 -4
  109. package/dist/cli/migrate.d.ts +5 -5
  110. package/dist/cli/migrate.js +11 -11
  111. package/dist/cli/studio-ui.generated.js +1 -1
  112. package/dist/cli/ui.d.ts +2 -2
  113. package/dist/cli/ui.js +2 -2
  114. package/dist/client.d.ts +49 -38
  115. package/dist/client.js +57 -56
  116. package/dist/dialect.d.ts +62 -18
  117. package/dist/dialect.js +40 -2
  118. package/dist/errors.d.ts +5 -5
  119. package/dist/errors.js +11 -11
  120. package/dist/generate.d.ts +6 -6
  121. package/dist/generate.js +31 -29
  122. package/dist/index-advisor.d.ts +5 -5
  123. package/dist/index-advisor.js +0 -0
  124. package/dist/index.d.ts +1 -1
  125. package/dist/index.js +7 -7
  126. package/dist/introspect.d.ts +35 -9
  127. package/dist/introspect.js +82 -32
  128. package/dist/mssql.d.ts +11 -11
  129. package/dist/mssql.js +64 -29
  130. package/dist/mysql.d.ts +8 -8
  131. package/dist/mysql.js +61 -23
  132. package/dist/nested-write.d.ts +21 -2
  133. package/dist/nested-write.js +51 -14
  134. package/dist/optional-peer-import.cjs +7 -7
  135. package/dist/optional-peer-import.d.cts +7 -7
  136. package/dist/pipeline-submittable.d.ts +2 -2
  137. package/dist/pipeline-submittable.js +6 -6
  138. package/dist/pipeline.d.ts +1 -1
  139. package/dist/pipeline.js +4 -4
  140. package/dist/powdb-introspect.d.ts +1 -1
  141. package/dist/powdb-introspect.js +1 -1
  142. package/dist/powdb.d.ts +28 -28
  143. package/dist/powdb.js +66 -66
  144. package/dist/powql.d.ts +27 -27
  145. package/dist/powql.js +73 -52
  146. package/dist/query/aggregates.d.ts +1 -1
  147. package/dist/query/aggregates.js +5 -5
  148. package/dist/query/batched-loader.d.ts +11 -11
  149. package/dist/query/batched-loader.js +24 -24
  150. package/dist/query/builder.d.ts +39 -21
  151. package/dist/query/builder.js +100 -58
  152. package/dist/query/compound-unique.d.ts +1 -1
  153. package/dist/query/compound-unique.js +0 -0
  154. package/dist/query/deferred.d.ts +12 -6
  155. package/dist/query/deferred.js +1 -1
  156. package/dist/query/filters.d.ts +31 -11
  157. package/dist/query/filters.js +66 -13
  158. package/dist/query/index.d.ts +1 -1
  159. package/dist/query/index.js +1 -1
  160. package/dist/query/relations.d.ts +9 -9
  161. package/dist/query/relations.js +165 -58
  162. package/dist/query/types.d.ts +86 -35
  163. package/dist/query/types.js +1 -1
  164. package/dist/query/utils.d.ts +27 -10
  165. package/dist/query/utils.js +84 -14
  166. package/dist/query/where.d.ts +47 -28
  167. package/dist/query/where.js +129 -32
  168. package/dist/query/writes.d.ts +24 -5
  169. package/dist/query/writes.js +101 -13
  170. package/dist/realtime.d.ts +7 -7
  171. package/dist/realtime.js +9 -9
  172. package/dist/schema-builder.d.ts +18 -7
  173. package/dist/schema-builder.js +17 -10
  174. package/dist/schema-metadata.d.ts +3 -3
  175. package/dist/schema-metadata.js +9 -9
  176. package/dist/schema-sql.d.ts +9 -9
  177. package/dist/schema-sql.js +20 -20
  178. package/dist/schema.d.ts +19 -9
  179. package/dist/schema.js +6 -6
  180. package/dist/serverless.d.ts +15 -15
  181. package/dist/serverless.js +16 -16
  182. package/dist/sqlite.d.ts +8 -8
  183. package/dist/sqlite.js +53 -22
  184. package/dist/typed-sql.d.ts +4 -4
  185. package/dist/typed-sql.js +5 -5
  186. package/package.json +2 -2
@@ -1,14 +1,14 @@
1
1
  /**
2
- * turbine-orm — Query builder
2
+ * turbine-orm, Query builder
3
3
  *
4
4
  * Each table accessor (db.users, db.posts, etc.) returns a QueryInterface<T>
5
5
  * that builds parameterized SQL and executes it through the connection pool.
6
6
  *
7
7
  * Nested relations use json_build_object + json_agg subqueries for single-query
8
- * resolution — a PostgreSQL-native approach that eliminates N+1 query patterns.
8
+ * resolution, a PostgreSQL-native approach that eliminates N+1 query patterns.
9
9
  *
10
10
  * Schema-driven: all column names, types, and relations come from introspected
11
- * metadata — nothing is hardcoded.
11
+ * metadata, nothing is hardcoded.
12
12
  */
13
13
  import { postgresDialect } from '../dialect.js';
14
14
  import { NotFoundError, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError } from '../errors.js';
@@ -20,7 +20,7 @@ import { defaultProjectionFields, includeKeysForBatching, loadRelationsBatched,
20
20
  import { expandCompoundUniqueWhere } from './compound-unique.js';
21
21
  import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, orderByEntries, sortedEntries, } from './filters.js';
22
22
  import * as relationsMod from './relations.js';
23
- import { LRUCache, ownLookup, parseDbDate, sqlToPreparedName } from './utils.js';
23
+ import { LRUCache, ownLookup, parseDbDate, sqlToPreparedName, unknownFieldMessage, } from './utils.js';
24
24
  import { shouldWarnOnce, WARN_NS } from './warn-registry.js';
25
25
  import * as whereMod from './where.js';
26
26
  import * as writesMod from './writes.js';
@@ -239,7 +239,7 @@ function isEmptyOrderBy(orderBy) {
239
239
  }
240
240
  return orderBy === undefined || orderBy === null;
241
241
  }
242
- // biome-ignore lint/complexity/noBannedTypes: {} means "no relations known" — intentional for untyped table access
242
+ // biome-ignore lint/complexity/noBannedTypes: {} means "no relations known", intentional for untyped table access
243
243
  export class QueryInterface {
244
244
  pool;
245
245
  table;
@@ -263,6 +263,7 @@ export class QueryInterface {
263
263
  middlewares;
264
264
  defaultLimit;
265
265
  warnOnUnlimited;
266
+ scopedConnect;
266
267
  utcTimestamps;
267
268
  preparedStatementsEnabled;
268
269
  /**
@@ -310,7 +311,7 @@ export class QueryInterface {
310
311
  globalFilters;
311
312
  /**
312
313
  * Tracks tables that have already triggered an unlimited-query warning so
313
- * the user is not spammed once per row. Per-instance state — each
314
+ * the user is not spammed once per row. Per-instance state, each
314
315
  * QueryInterface is bound to a single table, so this set will only ever
315
316
  * contain at most one entry, but using a Set keeps the API consistent with
316
317
  * the audit's "Set<string>" guidance and leaves room for future
@@ -325,7 +326,7 @@ export class QueryInterface {
325
326
  columnArrayTypeMap;
326
327
  /**
327
328
  * Columns whose type lives in a DIFFERENT schema than the introspected one
328
- * (ColumnMetadata.pgTypeSchema is recorded only in that case) — such columns
329
+ * (ColumnMetadata.pgTypeSchema is recorded only in that case), such columns
329
330
  * must never receive this schema's `::"enum"` cast (see enumTypeForColumn).
330
331
  */
331
332
  crossSchemaTypeColumns;
@@ -339,7 +340,7 @@ export class QueryInterface {
339
340
  camelDateFieldCache = new Map();
340
341
  /** True when this QI runs inside an active transaction (set via _txScoped option). */
341
342
  txScoped;
342
- /** Original options reference — forwarded to child QIs in nested writes. */
343
+ /** Original options reference, forwarded to child QIs in nested writes. */
343
344
  options;
344
345
  /** Set by executeWithMiddleware so queryWithTimeout can include it in events. */
345
346
  currentAction = 'raw';
@@ -355,7 +356,7 @@ export class QueryInterface {
355
356
  /**
356
357
  * The active query's `skipGlobalFilters` opt-out, set at the top of each
357
358
  * `build*` method and read deep in the (synchronous) SQL-build + param-collect
358
- * tree — so relation subqueries, relation filters, `_count`, and relation
359
+ * tree, so relation subqueries, relation filters, `_count`, and relation
359
360
  * `orderBy` all see it without threading it through dozens of signatures.
360
361
  * Only load-bearing when {@link globalFilters} is configured; build+collect are
361
362
  * synchronous per call, so this transient is never observed across an await.
@@ -399,8 +400,8 @@ export class QueryInterface {
399
400
  // `warnOnUnlimited: false`, per table with `warnOnUnlimited: { users:
400
401
  // false }` (unlisted tables keep the default), or per call via
401
402
  // `findMany({ warnOnUnlimited: false })`.
402
- // Per-table maps accept BOTH key forms — the snake_case table name
403
- // (`user_profiles`) and the camelCase accessor (`userProfiles`) — since
403
+ // Per-table maps accept BOTH key forms, the snake_case table name
404
+ // (`user_profiles`) and the camelCase accessor (`userProfiles`), since
404
405
  // users naturally key by the accessor they type everywhere else. The
405
406
  // snake_case entry wins when both are present.
406
407
  const warnOpt = options?.warnOnUnlimited;
@@ -408,6 +409,7 @@ export class QueryInterface {
408
409
  typeof warnOpt === 'object' && warnOpt !== null
409
410
  ? (warnOpt[table] ?? warnOpt[snakeToCamel(table)]) !== false
410
411
  : warnOpt !== false;
412
+ this.scopedConnect = options?.scopedConnect === true;
411
413
  this.utcTimestamps = options?.utcTimestamps !== false;
412
414
  this.preparedStatementsEnabled = options?.preparedStatements ?? true;
413
415
  // SQL template cache capacity. `sqlCacheSize: 0` disables caching entirely
@@ -518,7 +520,7 @@ export class QueryInterface {
518
520
  * SQLite use ` LIMIT <ph>` and/or ` OFFSET <ph>`. SQL Server has no `LIMIT`, so
519
521
  * its dialect implements {@link Dialect.buildLimitOffset} to emit
520
522
  * `[ORDER BY (SELECT NULL)] OFFSET <off> ROWS [FETCH NEXT <lim> ROWS ONLY]`.
521
- * Param-push order (limit before offset) is owned by the caller and unchanged —
523
+ * Param-push order (limit before offset) is owned by the caller and unchanged -
522
524
  * this only varies the SQL text, so PG output stays byte-identical.
523
525
  */
524
526
  buildPagination(limitPh, offsetPh, hasOrderBy) {
@@ -948,7 +950,7 @@ export class QueryInterface {
948
950
  * join for a to-one relation.
949
951
  *
950
952
  * Resolution order:
951
- * 1. an explicit `autoToOneJoinMaxRows` — an instruction, used verbatim
953
+ * 1. an explicit `autoToOneJoinMaxRows`, an instruction, used verbatim
952
954
  * (no clamping: the caller has measured their own workload);
953
955
  * 2. the configured `autoRoundTripMs` divided by
954
956
  * {@link AUTO_JOIN_PENALTY_MS_PER_ROW}, clamped to
@@ -962,7 +964,7 @@ export class QueryInterface {
962
964
  * put, so any single constant is wrong for someone by more than the margin it
963
965
  * is trying to save. Placing the switch AT the break-even is also what removes
964
966
  * the old cliff: two plans that cost the same at the boundary make the regret
965
- * there ~1.0x, rising only as the true row count moves away from it — where
967
+ * there ~1.0x, rising only as the true row count moves away from it, where
966
968
  * the previous fixed 1000 put its WORST case (1.44x measured) immediately
967
969
  * below its own switch point.
968
970
  *
@@ -1126,7 +1128,7 @@ export class QueryInterface {
1126
1128
  * Build the {@link RelationLoadContext} the batched loader needs, closing over
1127
1129
  * this interface's pool/dialect/executor. Child readers are constructed on the
1128
1130
  * SAME pool (so they join an active transaction) with `defaultLimit` cleared
1129
- * and unlimited-warnings silenced — a relation load must fetch every matching
1131
+ * and unlimited-warnings silenced, a relation load must fetch every matching
1130
1132
  * child, and the per-relation `limit` is applied client-side by the loader.
1131
1133
  */
1132
1134
  batchedContext(timeout, skip, includePii) {
@@ -1358,7 +1360,7 @@ export class QueryInterface {
1358
1360
  async queryWithTimeout(sql, params, timeout, preparedName) {
1359
1361
  const start = performance.now();
1360
1362
  const action = this.currentAction;
1361
- // Build the query argument — use object form with `name` for prepared
1363
+ // Build the query argument, use object form with `name` for prepared
1362
1364
  // statements, or the plain (text, values) form otherwise.
1363
1365
  const usePrepared = preparedName && this.preparedStatementsEnabled;
1364
1366
  const exec = usePrepared
@@ -1401,7 +1403,7 @@ export class QueryInterface {
1401
1403
  *
1402
1404
  * - `'returning'` / `'output'`: the statement returns its own affected rows
1403
1405
  * (`RETURNING *` / `OUTPUT INSERTED.*`). Byte-identical to the historical
1404
- * single `queryWithTimeout` + `transform(result)` path — the PostgreSQL
1406
+ * single `queryWithTimeout` + `transform(result)` path, the PostgreSQL
1405
1407
  * route is unchanged.
1406
1408
  * - `'reselect'`: the engine cannot return rows from a write, so the build
1407
1409
  * method attached a {@link DeferredQuery.reselect} plan that runs the
@@ -1455,7 +1457,7 @@ export class QueryInterface {
1455
1457
  *
1456
1458
  * Middleware can inspect and log query parameters, measure timing, and
1457
1459
  * transform the result returned by `next()`. Note: query SQL is generated
1458
- * BEFORE middleware runs — `params.args` is a read-only snapshot, and
1460
+ * BEFORE middleware runs, `params.args` is a read-only snapshot, and
1459
1461
  * mutating it does NOT change the executed SQL. Cross-cutting filters
1460
1462
  * (e.g. soft deletes) belong in the query itself: pass an explicit
1461
1463
  * `where: { deletedAt: null }` or wrap the table accessor in a small helper.
@@ -1473,7 +1475,7 @@ export class QueryInterface {
1473
1475
  const mw = this.middlewares[index++];
1474
1476
  return mw(p, next);
1475
1477
  }
1476
- // End of chain — execute the actual query
1478
+ // End of chain, execute the actual query
1477
1479
  return executor();
1478
1480
  };
1479
1481
  return next(params);
@@ -1525,7 +1527,7 @@ export class QueryInterface {
1525
1527
  stripFields([entity], proj.strip);
1526
1528
  return entity;
1527
1529
  }
1528
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
1530
+ // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
1529
1531
  buildFindUnique(args) {
1530
1532
  this.currentSkip = args.skipGlobalFilters;
1531
1533
  // Prisma compound-unique selector expansion (before global-filter merge and
@@ -1559,7 +1561,7 @@ export class QueryInterface {
1559
1561
  const ck = `fu:${whereFingerprint}|c=${colKey}|w=${withFp}|pii=${includePii ? 1 : 0}${this.globalFilterCacheSegment()}`;
1560
1562
  const params = [];
1561
1563
  // Check if all where values are simple (plain equality, no operators/null/OR).
1562
- // Keys are sorted to match fingerprintWhere — insertion order here would let
1564
+ // Keys are sorted to match fingerprintWhere, insertion order here would let
1563
1565
  // permuted where literals share a cache entry with misaligned params.
1564
1566
  const whereKeys = Object.keys(whereObj)
1565
1567
  .filter((k) => whereObj[k] !== undefined)
@@ -1663,7 +1665,7 @@ export class QueryInterface {
1663
1665
  if (args?.with) {
1664
1666
  const depth = this.measureWithDepth(args.with);
1665
1667
  if (depth > 5 && shouldWarnOnce(WARN_NS.deepWith, this.table)) {
1666
- console.warn(`[turbine] Deep with clause (depth ${depth}) on "${this.tableMeta.name}" — ` +
1668
+ console.warn(`[turbine] Deep with clause (depth ${depth}) on "${this.tableMeta.name}", ` +
1667
1669
  'consider splitting into separate queries for better performance.');
1668
1670
  }
1669
1671
  }
@@ -1763,12 +1765,53 @@ export class QueryInterface {
1763
1765
  const hasExplicitLimit = args?.limit !== undefined || args?.take !== undefined || args?.cursor !== undefined;
1764
1766
  if (hasExplicitLimit)
1765
1767
  return;
1768
+ if (this.whereMatchesAtMostOneRow(args?.where))
1769
+ return;
1766
1770
  if (this.warnedTables.has(this.table))
1767
1771
  return;
1768
1772
  this.warnedTables.add(this.table);
1769
1773
  console.warn(`[turbine] warning: findMany on "${this.table}" has no limit: this will fetch every row. ` +
1770
1774
  'Pass `limit`, or silence with `warnOnUnlimited: false` (per call, per table, or in config).');
1771
1775
  }
1776
+ /**
1777
+ * Whether `where` can match at most one row, because it pins every column of
1778
+ * the primary key or of some unique column set to a literal value.
1779
+ *
1780
+ * The unlimited-read warning is about accidentally fetching a whole table,
1781
+ * so firing it on `findMany({ where: { id: 1 } })` is noise: that query is
1782
+ * bounded by a uniqueness constraint just as firmly as by a `limit`, and a
1783
+ * warning that cries wolf on correct code trains people to disable it.
1784
+ *
1785
+ * Deliberately conservative. Only DIRECT equality on a literal counts: an
1786
+ * operator object (`{ id: { in: [...] } }`, `{ id: { gt: 1 } }`) can match
1787
+ * many rows, and any `OR` / `NOT` / relation filter can widen the result, so
1788
+ * anything that is not a plain scalar equality leaves the warning in place.
1789
+ * A compound-unique SELECTOR (`{ orgId_userId: {...} }`) is expanded first,
1790
+ * so both spellings are recognized.
1791
+ */
1792
+ whereMatchesAtMostOneRow(where) {
1793
+ if (where === null || typeof where !== 'object' || Array.isArray(where))
1794
+ return false;
1795
+ const expanded = expandCompoundUniqueWhere(this.tableMeta, where);
1796
+ const pinned = new Set();
1797
+ for (const [field, value] of Object.entries(expanded)) {
1798
+ if (value === undefined)
1799
+ continue;
1800
+ // A plain scalar (or a Date) is an equality. `null` is NOT: `col IS NULL`
1801
+ // is not a uniqueness match, since SQL uniqueness permits many nulls.
1802
+ const isScalarEquality = value !== null && (typeof value !== 'object' || value instanceof Date) && typeof value !== 'function';
1803
+ if (!isScalarEquality)
1804
+ return false;
1805
+ const column = ownLookup(this.tableMeta.columnMap, field);
1806
+ if (!column)
1807
+ return false;
1808
+ pinned.add(column);
1809
+ }
1810
+ if (pinned.size === 0)
1811
+ return false;
1812
+ const covers = (columns) => !!columns && columns.length > 0 && columns.every((c) => pinned.has(c));
1813
+ return covers(this.tableMeta.primaryKey) || (this.tableMeta.uniqueColumns ?? []).some(covers);
1814
+ }
1772
1815
  /**
1773
1816
  * Recursively measure the maximum depth of a `with` clause tree.
1774
1817
  * Used by the dev-only deep-with warning guard.
@@ -1783,7 +1826,7 @@ export class QueryInterface {
1783
1826
  }
1784
1827
  return maxDepth;
1785
1828
  }
1786
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
1829
+ // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
1787
1830
  buildFindMany(args) {
1788
1831
  this.currentSkip = args?.skipGlobalFilters;
1789
1832
  // Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
@@ -1815,7 +1858,7 @@ export class QueryInterface {
1815
1858
  // path re-orders in an outer wrapper (`... AS "<table>_distinct" ORDER BY
1816
1859
  // <userOrder>`) where a correlated relation subquery (pick-row, `_count`,
1817
1860
  // to-one relation ordering) would reference the parent table name out of
1818
- // scope — a guaranteed "missing FROM-clause entry" crash on Postgres.
1861
+ // scope, a guaranteed "missing FROM-clause entry" crash on Postgres.
1819
1862
  // Checked BEFORE the SQL cache so build and warm-cache paths throw
1820
1863
  // identically (same rule as the vector guard inside the distinct branch).
1821
1864
  if (args?.distinct && args.distinct.length > 0 && args.orderBy) {
@@ -1874,7 +1917,7 @@ export class QueryInterface {
1874
1917
  // own cache-key segment: a cached no-PII statement must never serve an
1875
1918
  // `includePii` call, nor vice versa.
1876
1919
  // `relationLoadStrategy: 'flatten'` compiles eligible to-one relations to
1877
- // LEFT JOINs instead of correlated subqueries — a completely different
1920
+ // LEFT JOINs instead of correlated subqueries, a completely different
1878
1921
  // statement for the same `with` shape, which `withFp` (strategy-blind)
1879
1922
  // does not distinguish. So the plan gets its own cache-key segment, exactly
1880
1923
  // like `pii=`: a join-planned template must never serve a flatten-planned
@@ -1925,7 +1968,7 @@ export class QueryInterface {
1925
1968
  // where → cursor order (the collect path mirrors this exactly).
1926
1969
  let tail = freshWhereSql;
1927
1970
  if (args?.cursor) {
1928
- // Sorted (canonical) order — MUST match cursorFp and the cache-hit collect below.
1971
+ // Sorted (canonical) order, MUST match cursorFp and the cache-hit collect below.
1929
1972
  const cursorEntries = sortedEntries(args.cursor).filter(([, v]) => v !== undefined);
1930
1973
  if (cursorEntries.length > 0) {
1931
1974
  // Resolve the seek direction per cursor field from the flattened
@@ -1975,7 +2018,7 @@ export class QueryInterface {
1975
2018
  : '';
1976
2019
  sql = `SELECT ${distinctPrefix}${selectClause} FROM ${qt}${relationJoins.join('')}${lateralJoins.join('')}${tail}${orderBySql}`;
1977
2020
  }
1978
- // Pagination — push params in the same order the collect path mirrors
2021
+ // Pagination, push params in the same order the collect path mirrors
1979
2022
  // (limit before offset); the SQL TEXT shape is dialect-owned via
1980
2023
  // buildPagination (PG: ` LIMIT $n`/` OFFSET $n`; SQL Server: OFFSET/FETCH).
1981
2024
  let limitPh;
@@ -1999,7 +2042,7 @@ export class QueryInterface {
1999
2042
  if (args?.with) {
2000
2043
  this.collectWithParams(args.with, params, undefined, flattenPlan);
2001
2044
  }
2002
- // 3. Cursor params — sorted (canonical) order, matching cursorFp and the build path.
2045
+ // 3. Cursor params, sorted (canonical) order, matching cursorFp and the build path.
2003
2046
  if (args?.cursor) {
2004
2047
  const cursorEntries = sortedEntries(args.cursor).filter(([, v]) => v !== undefined);
2005
2048
  for (const [, v] of cursorEntries) {
@@ -2007,11 +2050,11 @@ export class QueryInterface {
2007
2050
  }
2008
2051
  }
2009
2052
  // 4. ORDER BY params (vector KNN ordering binds a `$n::vector` query vector).
2010
- // Mirrors buildOrderBy's push order — between cursor and LIMIT.
2053
+ // Mirrors buildOrderBy's push order, between cursor and LIMIT.
2011
2054
  if (args?.orderBy) {
2012
2055
  this.collectOrderByParams(args.orderBy, params);
2013
2056
  }
2014
- // 5. LIMIT param — skipped when the dialect inlines pagination (build path
2057
+ // 5. LIMIT param, skipped when the dialect inlines pagination (build path
2015
2058
  // mirrors via paginationRef → no placeholder, no param).
2016
2059
  if (effectiveLimit !== undefined && !this.dialect.inlineLimitOffset) {
2017
2060
  // Validate here too: on a cache HIT the build path never runs, and a
@@ -2019,7 +2062,7 @@ export class QueryInterface {
2019
2062
  // i.e. no limit at all).
2020
2063
  params.push(this.paginationValue(effectiveLimit, 'limit'));
2021
2064
  }
2022
- // 6. OFFSET param — same inline gate as LIMIT above.
2065
+ // 6. OFFSET param, same inline gate as LIMIT above.
2023
2066
  if (args?.offset !== undefined && !this.dialect.inlineLimitOffset) {
2024
2067
  params.push(this.paginationValue(args.offset, 'skip/offset'));
2025
2068
  }
@@ -2035,7 +2078,7 @@ export class QueryInterface {
2035
2078
  };
2036
2079
  }
2037
2080
  // -------------------------------------------------------------------------
2038
- // findManyStream — async iterable using PostgreSQL cursors
2081
+ // findManyStream, async iterable using PostgreSQL cursors
2039
2082
  // -------------------------------------------------------------------------
2040
2083
  /**
2041
2084
  * Stream rows from a findMany query using PostgreSQL cursors.
@@ -2074,8 +2117,8 @@ export class QueryInterface {
2074
2117
  const hasRelations = !!args?.with;
2075
2118
  // Build the positional-aware relation parser once for the whole stream.
2076
2119
  // Same flatten plan buildFindMany compiles below. The plan is a pure
2077
- // function of the schema, the `with` shape and `includePii` — never of
2078
- // `limit` — so the batch-size override the speculative fetch applies cannot
2120
+ // function of the schema, the `with` shape and `includePii`, never of
2121
+ // `limit`, so the batch-size override the speculative fetch applies cannot
2079
2122
  // change it, and the stream's parser matches the emitted SQL.
2080
2123
  const streamFlattenPlan = hasRelations ? this.planFlatten(args, args?.includePii === true) : null;
2081
2124
  const parseWith = hasRelations
@@ -2089,7 +2132,7 @@ export class QueryInterface {
2089
2132
  this.currentAction = 'findManyStream';
2090
2133
  const speculativeResult = await this.queryWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout);
2091
2134
  if (speculativeResult.rows.length <= batchSize) {
2092
- // Small drain — yield all rows and return, no cursor needed
2135
+ // Small drain, yield all rows and return, no cursor needed
2093
2136
  for (const row of speculativeResult.rows) {
2094
2137
  yield (parseWith ? parseWith(row) : this.parseRow(row, this.table));
2095
2138
  }
@@ -2129,7 +2172,7 @@ export class QueryInterface {
2129
2172
  }
2130
2173
  }
2131
2174
  // -------------------------------------------------------------------------
2132
- // findFirst — like findMany but returns a single row or null
2175
+ // findFirst, like findMany but returns a single row or null
2133
2176
  // -------------------------------------------------------------------------
2134
2177
  async findFirst(args) {
2135
2178
  return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
@@ -2154,7 +2197,7 @@ export class QueryInterface {
2154
2197
  return deferred.transform(result);
2155
2198
  });
2156
2199
  }
2157
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
2200
+ // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
2158
2201
  buildFindFirst(args) {
2159
2202
  // Reuse findMany's SQL builder but force LIMIT 1
2160
2203
  const findManyArgs = { ...args, limit: 1 };
@@ -2170,7 +2213,7 @@ export class QueryInterface {
2170
2213
  };
2171
2214
  }
2172
2215
  // -------------------------------------------------------------------------
2173
- // findFirstOrThrow — like findFirst but throws if no record found
2216
+ // findFirstOrThrow, like findFirst but throws if no record found
2174
2217
  // -------------------------------------------------------------------------
2175
2218
  async findFirstOrThrow(args) {
2176
2219
  return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
@@ -2179,7 +2222,7 @@ export class QueryInterface {
2179
2222
  return deferred.transform(result);
2180
2223
  });
2181
2224
  }
2182
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
2225
+ // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
2183
2226
  buildFindFirstOrThrow(args) {
2184
2227
  const inner = this.buildFindFirst(args);
2185
2228
  return {
@@ -2200,7 +2243,7 @@ export class QueryInterface {
2200
2243
  };
2201
2244
  }
2202
2245
  // -------------------------------------------------------------------------
2203
- // findUniqueOrThrow — like findUnique but throws if no record found
2246
+ // findUniqueOrThrow, like findUnique but throws if no record found
2204
2247
  // -------------------------------------------------------------------------
2205
2248
  async findUniqueOrThrow(args) {
2206
2249
  return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
@@ -2209,7 +2252,7 @@ export class QueryInterface {
2209
2252
  return deferred.transform(result);
2210
2253
  });
2211
2254
  }
2212
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
2255
+ // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
2213
2256
  buildFindUniqueOrThrow(args) {
2214
2257
  const inner = this.buildFindUnique(args);
2215
2258
  return {
@@ -2242,7 +2285,7 @@ export class QueryInterface {
2242
2285
  });
2243
2286
  }
2244
2287
  // -------------------------------------------------------------------------
2245
- // createMany — uses UNNEST for performance
2288
+ // createMany, uses UNNEST for performance
2246
2289
  // -------------------------------------------------------------------------
2247
2290
  async createMany(args) {
2248
2291
  return this.executeWithMiddleware('createMany', args, async () => {
@@ -2308,7 +2351,7 @@ export class QueryInterface {
2308
2351
  // implicit transaction.
2309
2352
  this.pool);
2310
2353
  // biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
2311
- const ctx = { schema: this.schema, tx: tx };
2354
+ const ctx = { schema: this.schema, tx: tx, scopedConnect: this.scopedConnect };
2312
2355
  const result = await fn(ctx);
2313
2356
  await client.query(this.dialect.commitStatement());
2314
2357
  return result;
@@ -2356,7 +2399,7 @@ export class QueryInterface {
2356
2399
  });
2357
2400
  }
2358
2401
  // -------------------------------------------------------------------------
2359
- // upsert — INSERT ... ON CONFLICT ... DO UPDATE
2402
+ // upsert, INSERT ... ON CONFLICT ... DO UPDATE
2360
2403
  // -------------------------------------------------------------------------
2361
2404
  async upsert(args) {
2362
2405
  return this.executeWithMiddleware('upsert', args, async () => {
@@ -2365,7 +2408,7 @@ export class QueryInterface {
2365
2408
  });
2366
2409
  }
2367
2410
  // -------------------------------------------------------------------------
2368
- // updateMany — UPDATE ... WHERE ... returning count
2411
+ // updateMany, UPDATE ... WHERE ... returning count
2369
2412
  // -------------------------------------------------------------------------
2370
2413
  async updateMany(args) {
2371
2414
  return this.executeWithMiddleware('updateMany', args, async () => {
@@ -2375,7 +2418,7 @@ export class QueryInterface {
2375
2418
  });
2376
2419
  }
2377
2420
  // -------------------------------------------------------------------------
2378
- // deleteMany — DELETE ... WHERE ... returning count
2421
+ // deleteMany, DELETE ... WHERE ... returning count
2379
2422
  // -------------------------------------------------------------------------
2380
2423
  async deleteMany(args) {
2381
2424
  return this.executeWithMiddleware('deleteMany', args, async () => {
@@ -2448,7 +2491,7 @@ export class QueryInterface {
2448
2491
  return aggMod.buildAggregate(this.ctx, args);
2449
2492
  }
2450
2493
  // -------------------------------------------------------------------------
2451
- // aggregate — standalone aggregation without groupBy
2494
+ // aggregate, standalone aggregation without groupBy
2452
2495
  // -------------------------------------------------------------------------
2453
2496
  async aggregate(args) {
2454
2497
  return this.executeWithMiddleware('aggregate', args, async () => {
@@ -2584,7 +2627,7 @@ export class QueryInterface {
2584
2627
  // Fall back to camelToSnake ONLY if that snake_cased name also exists as a
2585
2628
  // real column on the table. This preserves the convenience of writing
2586
2629
  // `userId` when the schema exposes `user_id` under an unusual field name,
2587
- // but rejects arbitrary strings — closing the defense-in-depth gap for
2630
+ // but rejects arbitrary strings, closing the defense-in-depth gap for
2588
2631
  // SQL injection and catching typos like `where: { emial: 'x' }` with a
2589
2632
  // clear error instead of a cryptic Postgres "column does not exist".
2590
2633
  const snake = camelToSnake(field);
@@ -2594,15 +2637,14 @@ export class QueryInterface {
2594
2637
  if (this.tableMeta.allColumns?.includes(snake)) {
2595
2638
  return snake;
2596
2639
  }
2597
- throw new ValidationError(`[turbine] Unknown field "${field}" on table "${this.table}". ` +
2598
- `Known fields: ${Object.keys(this.tableMeta.columnMap).join(', ') || '(none)'}.`);
2640
+ throw new ValidationError(unknownFieldMessage(this.table, field, this.tableMeta));
2599
2641
  }
2600
2642
  /** Convert camelCase field name to a double-quoted SQL identifier */
2601
2643
  toSqlColumn(field) {
2602
2644
  return this.q(this.toColumn(field));
2603
2645
  }
2604
2646
  // =========================================================================
2605
- // Fingerprinting — value-invariant shape keys for SQL cache lookup
2647
+ // Fingerprinting, value-invariant shape keys for SQL cache lookup
2606
2648
  // =========================================================================
2607
2649
  // ---------------------------------------------------------------------------
2608
2650
  // WHERE-clause compilation (extracted to where.ts).
@@ -2699,7 +2741,7 @@ export class QueryInterface {
2699
2741
  }
2700
2742
  }
2701
2743
  // -------------------------------------------------------------------------
2702
- // Global filters (soft-delete / multi-tenancy — WS-G)
2744
+ // Global filters (soft-delete / multi-tenancy, WS-G)
2703
2745
  //
2704
2746
  // A configured global filter for a table is AND-merged into the compiled WHERE
2705
2747
  // of every query on that table (via {@link mergeGlobalFilter}, so the merge is
@@ -2707,12 +2749,12 @@ export class QueryInterface {
2707
2749
  // subquery targeting it (rendered at build time against the subquery's alias/
2708
2750
  // table by the `*GlobalFilterAlias`/`*GlobalFilterExists` helpers, with the
2709
2751
  // shape folded into the SQL-cache key via {@link globalFilterCacheSegment}).
2710
- // Function filters are evaluated per resolve — at query-build time — enabling
2752
+ // Function filters are evaluated per resolve, at query-build time, enabling
2711
2753
  // per-request tenancy via a closure. They must return a STABLE shape (same
2712
2754
  // keys/operators); only values may vary between calls.
2713
2755
  // -------------------------------------------------------------------------
2714
2756
  // -------------------------------------------------------------------------
2715
- // Scoped (non-top-level) WHERE compilation — relation EXISTS sub-wheres and
2757
+ // Scoped (non-top-level) WHERE compilation, relation EXISTS sub-wheres and
2716
2758
  // relation `with`-clause `where`s. All three consumers below drive the SAME
2717
2759
  // canonical `walkWhere` the top level uses (bound to the SCOPE's target
2718
2760
  // table), so their key order + combinator structure + relation detection can
@@ -2728,12 +2770,12 @@ export class QueryInterface {
2728
2770
  /**
2729
2771
  * Build ORDER BY clause from an object.
2730
2772
  *
2731
- * Each value is either a plain direction (`'asc'`/`'desc'`) or — for pgvector
2732
- * columns — a `{ distance: { to, metric, direction? } }` KNN ordering object.
2773
+ * Each value is either a plain direction (`'asc'`/`'desc'`) or, for pgvector
2774
+ * columns, a `{ distance: { to, metric, direction? } }` KNN ordering object.
2733
2775
  * Vector ordering binds the query vector as a `$n::vector` param, so a `params`
2734
2776
  * array MUST be supplied when a vector ordering may be present (top-level
2735
2777
  * findMany path). When `params` is omitted (groupBy / relation path) a vector
2736
- * ordering throws — KNN ordering is only supported at the top level.
2778
+ * ordering throws, KNN ordering is only supported at the top level.
2737
2779
  */
2738
2780
  // -------------------------------------------------------------------------
2739
2781
  // pgvector helpers (similarity search)
@@ -2766,7 +2808,7 @@ export class QueryInterface {
2766
2808
  // columns (`date[]`, `timestamp[]`, `timestamptz[]`), for which the
2767
2809
  // driver already hands back a `Date[]`. Coercing it ran
2768
2810
  // `new Date(String(theArray))` and replaced the whole array with a
2769
- // single Invalid Date — the column was unreadable on every strategy.
2811
+ // single Invalid Date, the column was unreadable on every strategy.
2770
2812
  // The join strategy's string arrays are handled upstream instead, by
2771
2813
  // the JSON-wire decode in relations.ts.
2772
2814
  if ((dateCols.has(col) || camelDateFields.has(field)) &&
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Prisma-style compound-unique `where` selectors.
2
+ * turbine-orm, Prisma-style compound-unique `where` selectors.
3
3
  *
4
4
  * Prisma lets a `findUnique`-family `where` address a multi-column unique
5
5
  * constraint through a single synthetic key holding the member columns:
Binary file
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Deferred query + QueryInterface option types
2
+ * turbine-orm, Deferred query + QueryInterface option types
3
3
  *
4
4
  * Split from builder.ts so the class file focuses on SQL assembly / execution.
5
5
  */
@@ -27,7 +27,7 @@ export interface DeferredQuery<T> {
27
27
  preparedName?: string;
28
28
  /**
29
29
  * Execution plan for dialects whose {@link Dialect.resultStrategy} is
30
- * `'reselect'` (no RETURNING — e.g. MySQL). Owns the statement ordering: it
30
+ * `'reselect'` (no RETURNING, e.g. MySQL). Owns the statement ordering: it
31
31
  * runs the write and the follow-up row-fetching SELECT(s) via `exec`, and
32
32
  * resolves the result whose rows {@link DeferredQuery.transform} consumes.
33
33
  * Absent for `'returning'`/`'output'` dialects (the statement returns its own
@@ -35,7 +35,7 @@ export interface DeferredQuery<T> {
35
35
  */
36
36
  reselect?: (exec: ReselectExecutor) => Promise<pg.QueryResult>;
37
37
  }
38
- /** Middleware function type — imported from client to avoid circular deps */
38
+ /** Middleware function type, imported from client to avoid circular deps */
39
39
  export type MiddlewareFn = (params: {
40
40
  model: string;
41
41
  action: string;
@@ -75,13 +75,19 @@ export interface QueryInterfaceOptions {
75
75
  * queries are surfaced loudly during development. Pass `false` to silence
76
76
  * the warning entirely (e.g. for CLI tooling that intentionally streams
77
77
  * full tables), or a per-table map (`{ userProfiles: false }`) to silence
78
- * only the tables that intentionally read full sets — unlisted tables keep
78
+ * only the tables that intentionally read full sets, unlisted tables keep
79
79
  * the default. Map keys accept BOTH the camelCase accessor name
80
80
  * (`userProfiles`) and the snake_case table name (`user_profiles`); the
81
81
  * snake_case entry wins if both are present. Individual calls can also
82
82
  * override via `findMany({ warnOnUnlimited: false })`.
83
83
  */
84
84
  warnOnUnlimited?: boolean | Record<string, boolean>;
85
+ /**
86
+ * Refuse a nested `connect` / `connectOrCreate` that would re-parent a
87
+ * to-many child already owned by a different parent. Off by default; see
88
+ * {@link import('../nested-write.js').NestedWriteContext.scopedConnect}.
89
+ */
90
+ scopedConnect?: boolean;
85
91
  /**
86
92
  * Enable prepared statements. When true, queries are submitted with a
87
93
  * `{ name, text, values }` object to the pg driver, which caches the
@@ -196,7 +202,7 @@ export interface QueryInterfaceOptions {
196
202
  autoRoundTripMs?: number;
197
203
  /**
198
204
  * How nested-relation subqueries encode each row's JSON: `'object'` (default,
199
- * `json_build_object`) or `'positional'` (`json_build_array`, key-less — see
205
+ * `json_build_object`) or `'positional'` (`json_build_array`, key-less, see
200
206
  * {@link Dialect.buildJsonArray}). Positional is Postgres-only in v1; a
201
207
  * `with` clause on any other dialect throws `UnsupportedFeatureError` (E017).
202
208
  */
@@ -208,7 +214,7 @@ export interface QueryInterfaceOptions {
208
214
  * {@link GlobalFilters}.
209
215
  */
210
216
  globalFilters?: GlobalFilters;
211
- /** @internal Set by TransactionClient — signals that this QI runs inside an active transaction. */
217
+ /** @internal Set by TransactionClient, signals that this QI runs inside an active transaction. */
212
218
  _txScoped?: boolean;
213
219
  /** @internal Callback from TurbineClient for query event emission. */
214
220
  _onQuery?: (event: QueryEvent) => void;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Deferred query + QueryInterface option types
2
+ * turbine-orm, Deferred query + QueryInterface option types
3
3
  *
4
4
  * Split from builder.ts so the class file focuses on SQL assembly / execution.
5
5
  */