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,15 +1,15 @@
1
1
  "use strict";
2
2
  /**
3
- * turbine-orm — Query builder
3
+ * turbine-orm, Query builder
4
4
  *
5
5
  * Each table accessor (db.users, db.posts, etc.) returns a QueryInterface<T>
6
6
  * that builds parameterized SQL and executes it through the connection pool.
7
7
  *
8
8
  * Nested relations use json_build_object + json_agg subqueries for single-query
9
- * resolution — a PostgreSQL-native approach that eliminates N+1 query patterns.
9
+ * resolution, a PostgreSQL-native approach that eliminates N+1 query patterns.
10
10
  *
11
11
  * Schema-driven: all column names, types, and relations come from introspected
12
- * metadata — nothing is hardcoded.
12
+ * metadata, nothing is hardcoded.
13
13
  */
14
14
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
15
  if (k2 === undefined) k2 = k;
@@ -275,7 +275,7 @@ function isEmptyOrderBy(orderBy) {
275
275
  }
276
276
  return orderBy === undefined || orderBy === null;
277
277
  }
278
- // biome-ignore lint/complexity/noBannedTypes: {} means "no relations known" — intentional for untyped table access
278
+ // biome-ignore lint/complexity/noBannedTypes: {} means "no relations known", intentional for untyped table access
279
279
  class QueryInterface {
280
280
  pool;
281
281
  table;
@@ -299,6 +299,7 @@ class QueryInterface {
299
299
  middlewares;
300
300
  defaultLimit;
301
301
  warnOnUnlimited;
302
+ scopedConnect;
302
303
  utcTimestamps;
303
304
  preparedStatementsEnabled;
304
305
  /**
@@ -346,7 +347,7 @@ class QueryInterface {
346
347
  globalFilters;
347
348
  /**
348
349
  * Tracks tables that have already triggered an unlimited-query warning so
349
- * the user is not spammed once per row. Per-instance state — each
350
+ * the user is not spammed once per row. Per-instance state, each
350
351
  * QueryInterface is bound to a single table, so this set will only ever
351
352
  * contain at most one entry, but using a Set keeps the API consistent with
352
353
  * the audit's "Set<string>" guidance and leaves room for future
@@ -361,7 +362,7 @@ class QueryInterface {
361
362
  columnArrayTypeMap;
362
363
  /**
363
364
  * Columns whose type lives in a DIFFERENT schema than the introspected one
364
- * (ColumnMetadata.pgTypeSchema is recorded only in that case) — such columns
365
+ * (ColumnMetadata.pgTypeSchema is recorded only in that case), such columns
365
366
  * must never receive this schema's `::"enum"` cast (see enumTypeForColumn).
366
367
  */
367
368
  crossSchemaTypeColumns;
@@ -375,7 +376,7 @@ class QueryInterface {
375
376
  camelDateFieldCache = new Map();
376
377
  /** True when this QI runs inside an active transaction (set via _txScoped option). */
377
378
  txScoped;
378
- /** Original options reference — forwarded to child QIs in nested writes. */
379
+ /** Original options reference, forwarded to child QIs in nested writes. */
379
380
  options;
380
381
  /** Set by executeWithMiddleware so queryWithTimeout can include it in events. */
381
382
  currentAction = 'raw';
@@ -391,7 +392,7 @@ class QueryInterface {
391
392
  /**
392
393
  * The active query's `skipGlobalFilters` opt-out, set at the top of each
393
394
  * `build*` method and read deep in the (synchronous) SQL-build + param-collect
394
- * tree — so relation subqueries, relation filters, `_count`, and relation
395
+ * tree, so relation subqueries, relation filters, `_count`, and relation
395
396
  * `orderBy` all see it without threading it through dozens of signatures.
396
397
  * Only load-bearing when {@link globalFilters} is configured; build+collect are
397
398
  * synchronous per call, so this transient is never observed across an await.
@@ -435,8 +436,8 @@ class QueryInterface {
435
436
  // `warnOnUnlimited: false`, per table with `warnOnUnlimited: { users:
436
437
  // false }` (unlisted tables keep the default), or per call via
437
438
  // `findMany({ warnOnUnlimited: false })`.
438
- // Per-table maps accept BOTH key forms — the snake_case table name
439
- // (`user_profiles`) and the camelCase accessor (`userProfiles`) — since
439
+ // Per-table maps accept BOTH key forms, the snake_case table name
440
+ // (`user_profiles`) and the camelCase accessor (`userProfiles`), since
440
441
  // users naturally key by the accessor they type everywhere else. The
441
442
  // snake_case entry wins when both are present.
442
443
  const warnOpt = options?.warnOnUnlimited;
@@ -444,6 +445,7 @@ class QueryInterface {
444
445
  typeof warnOpt === 'object' && warnOpt !== null
445
446
  ? (warnOpt[table] ?? warnOpt[(0, schema_js_1.snakeToCamel)(table)]) !== false
446
447
  : warnOpt !== false;
448
+ this.scopedConnect = options?.scopedConnect === true;
447
449
  this.utcTimestamps = options?.utcTimestamps !== false;
448
450
  this.preparedStatementsEnabled = options?.preparedStatements ?? true;
449
451
  // SQL template cache capacity. `sqlCacheSize: 0` disables caching entirely
@@ -554,7 +556,7 @@ class QueryInterface {
554
556
  * SQLite use ` LIMIT <ph>` and/or ` OFFSET <ph>`. SQL Server has no `LIMIT`, so
555
557
  * its dialect implements {@link Dialect.buildLimitOffset} to emit
556
558
  * `[ORDER BY (SELECT NULL)] OFFSET <off> ROWS [FETCH NEXT <lim> ROWS ONLY]`.
557
- * Param-push order (limit before offset) is owned by the caller and unchanged —
559
+ * Param-push order (limit before offset) is owned by the caller and unchanged -
558
560
  * this only varies the SQL text, so PG output stays byte-identical.
559
561
  */
560
562
  buildPagination(limitPh, offsetPh, hasOrderBy) {
@@ -984,7 +986,7 @@ class QueryInterface {
984
986
  * join for a to-one relation.
985
987
  *
986
988
  * Resolution order:
987
- * 1. an explicit `autoToOneJoinMaxRows` — an instruction, used verbatim
989
+ * 1. an explicit `autoToOneJoinMaxRows`, an instruction, used verbatim
988
990
  * (no clamping: the caller has measured their own workload);
989
991
  * 2. the configured `autoRoundTripMs` divided by
990
992
  * {@link AUTO_JOIN_PENALTY_MS_PER_ROW}, clamped to
@@ -998,7 +1000,7 @@ class QueryInterface {
998
1000
  * put, so any single constant is wrong for someone by more than the margin it
999
1001
  * is trying to save. Placing the switch AT the break-even is also what removes
1000
1002
  * the old cliff: two plans that cost the same at the boundary make the regret
1001
- * there ~1.0x, rising only as the true row count moves away from it — where
1003
+ * there ~1.0x, rising only as the true row count moves away from it, where
1002
1004
  * the previous fixed 1000 put its WORST case (1.44x measured) immediately
1003
1005
  * below its own switch point.
1004
1006
  *
@@ -1162,7 +1164,7 @@ class QueryInterface {
1162
1164
  * Build the {@link RelationLoadContext} the batched loader needs, closing over
1163
1165
  * this interface's pool/dialect/executor. Child readers are constructed on the
1164
1166
  * SAME pool (so they join an active transaction) with `defaultLimit` cleared
1165
- * and unlimited-warnings silenced — a relation load must fetch every matching
1167
+ * and unlimited-warnings silenced, a relation load must fetch every matching
1166
1168
  * child, and the per-relation `limit` is applied client-side by the loader.
1167
1169
  */
1168
1170
  batchedContext(timeout, skip, includePii) {
@@ -1394,7 +1396,7 @@ class QueryInterface {
1394
1396
  async queryWithTimeout(sql, params, timeout, preparedName) {
1395
1397
  const start = performance.now();
1396
1398
  const action = this.currentAction;
1397
- // Build the query argument — use object form with `name` for prepared
1399
+ // Build the query argument, use object form with `name` for prepared
1398
1400
  // statements, or the plain (text, values) form otherwise.
1399
1401
  const usePrepared = preparedName && this.preparedStatementsEnabled;
1400
1402
  const exec = usePrepared
@@ -1437,7 +1439,7 @@ class QueryInterface {
1437
1439
  *
1438
1440
  * - `'returning'` / `'output'`: the statement returns its own affected rows
1439
1441
  * (`RETURNING *` / `OUTPUT INSERTED.*`). Byte-identical to the historical
1440
- * single `queryWithTimeout` + `transform(result)` path — the PostgreSQL
1442
+ * single `queryWithTimeout` + `transform(result)` path, the PostgreSQL
1441
1443
  * route is unchanged.
1442
1444
  * - `'reselect'`: the engine cannot return rows from a write, so the build
1443
1445
  * method attached a {@link DeferredQuery.reselect} plan that runs the
@@ -1491,7 +1493,7 @@ class QueryInterface {
1491
1493
  *
1492
1494
  * Middleware can inspect and log query parameters, measure timing, and
1493
1495
  * transform the result returned by `next()`. Note: query SQL is generated
1494
- * BEFORE middleware runs — `params.args` is a read-only snapshot, and
1496
+ * BEFORE middleware runs, `params.args` is a read-only snapshot, and
1495
1497
  * mutating it does NOT change the executed SQL. Cross-cutting filters
1496
1498
  * (e.g. soft deletes) belong in the query itself: pass an explicit
1497
1499
  * `where: { deletedAt: null }` or wrap the table accessor in a small helper.
@@ -1509,7 +1511,7 @@ class QueryInterface {
1509
1511
  const mw = this.middlewares[index++];
1510
1512
  return mw(p, next);
1511
1513
  }
1512
- // End of chain — execute the actual query
1514
+ // End of chain, execute the actual query
1513
1515
  return executor();
1514
1516
  };
1515
1517
  return next(params);
@@ -1561,7 +1563,7 @@ class QueryInterface {
1561
1563
  (0, batched_loader_js_1.stripFields)([entity], proj.strip);
1562
1564
  return entity;
1563
1565
  }
1564
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
1566
+ // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
1565
1567
  buildFindUnique(args) {
1566
1568
  this.currentSkip = args.skipGlobalFilters;
1567
1569
  // Prisma compound-unique selector expansion (before global-filter merge and
@@ -1595,7 +1597,7 @@ class QueryInterface {
1595
1597
  const ck = `fu:${whereFingerprint}|c=${colKey}|w=${withFp}|pii=${includePii ? 1 : 0}${this.globalFilterCacheSegment()}`;
1596
1598
  const params = [];
1597
1599
  // Check if all where values are simple (plain equality, no operators/null/OR).
1598
- // Keys are sorted to match fingerprintWhere — insertion order here would let
1600
+ // Keys are sorted to match fingerprintWhere, insertion order here would let
1599
1601
  // permuted where literals share a cache entry with misaligned params.
1600
1602
  const whereKeys = Object.keys(whereObj)
1601
1603
  .filter((k) => whereObj[k] !== undefined)
@@ -1699,7 +1701,7 @@ class QueryInterface {
1699
1701
  if (args?.with) {
1700
1702
  const depth = this.measureWithDepth(args.with);
1701
1703
  if (depth > 5 && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.deepWith, this.table)) {
1702
- console.warn(`[turbine] Deep with clause (depth ${depth}) on "${this.tableMeta.name}" — ` +
1704
+ console.warn(`[turbine] Deep with clause (depth ${depth}) on "${this.tableMeta.name}", ` +
1703
1705
  'consider splitting into separate queries for better performance.');
1704
1706
  }
1705
1707
  }
@@ -1799,12 +1801,53 @@ class QueryInterface {
1799
1801
  const hasExplicitLimit = args?.limit !== undefined || args?.take !== undefined || args?.cursor !== undefined;
1800
1802
  if (hasExplicitLimit)
1801
1803
  return;
1804
+ if (this.whereMatchesAtMostOneRow(args?.where))
1805
+ return;
1802
1806
  if (this.warnedTables.has(this.table))
1803
1807
  return;
1804
1808
  this.warnedTables.add(this.table);
1805
1809
  console.warn(`[turbine] warning: findMany on "${this.table}" has no limit: this will fetch every row. ` +
1806
1810
  'Pass `limit`, or silence with `warnOnUnlimited: false` (per call, per table, or in config).');
1807
1811
  }
1812
+ /**
1813
+ * Whether `where` can match at most one row, because it pins every column of
1814
+ * the primary key or of some unique column set to a literal value.
1815
+ *
1816
+ * The unlimited-read warning is about accidentally fetching a whole table,
1817
+ * so firing it on `findMany({ where: { id: 1 } })` is noise: that query is
1818
+ * bounded by a uniqueness constraint just as firmly as by a `limit`, and a
1819
+ * warning that cries wolf on correct code trains people to disable it.
1820
+ *
1821
+ * Deliberately conservative. Only DIRECT equality on a literal counts: an
1822
+ * operator object (`{ id: { in: [...] } }`, `{ id: { gt: 1 } }`) can match
1823
+ * many rows, and any `OR` / `NOT` / relation filter can widen the result, so
1824
+ * anything that is not a plain scalar equality leaves the warning in place.
1825
+ * A compound-unique SELECTOR (`{ orgId_userId: {...} }`) is expanded first,
1826
+ * so both spellings are recognized.
1827
+ */
1828
+ whereMatchesAtMostOneRow(where) {
1829
+ if (where === null || typeof where !== 'object' || Array.isArray(where))
1830
+ return false;
1831
+ const expanded = (0, compound_unique_js_1.expandCompoundUniqueWhere)(this.tableMeta, where);
1832
+ const pinned = new Set();
1833
+ for (const [field, value] of Object.entries(expanded)) {
1834
+ if (value === undefined)
1835
+ continue;
1836
+ // A plain scalar (or a Date) is an equality. `null` is NOT: `col IS NULL`
1837
+ // is not a uniqueness match, since SQL uniqueness permits many nulls.
1838
+ const isScalarEquality = value !== null && (typeof value !== 'object' || value instanceof Date) && typeof value !== 'function';
1839
+ if (!isScalarEquality)
1840
+ return false;
1841
+ const column = (0, utils_js_1.ownLookup)(this.tableMeta.columnMap, field);
1842
+ if (!column)
1843
+ return false;
1844
+ pinned.add(column);
1845
+ }
1846
+ if (pinned.size === 0)
1847
+ return false;
1848
+ const covers = (columns) => !!columns && columns.length > 0 && columns.every((c) => pinned.has(c));
1849
+ return covers(this.tableMeta.primaryKey) || (this.tableMeta.uniqueColumns ?? []).some(covers);
1850
+ }
1808
1851
  /**
1809
1852
  * Recursively measure the maximum depth of a `with` clause tree.
1810
1853
  * Used by the dev-only deep-with warning guard.
@@ -1819,7 +1862,7 @@ class QueryInterface {
1819
1862
  }
1820
1863
  return maxDepth;
1821
1864
  }
1822
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
1865
+ // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
1823
1866
  buildFindMany(args) {
1824
1867
  this.currentSkip = args?.skipGlobalFilters;
1825
1868
  // Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
@@ -1851,7 +1894,7 @@ class QueryInterface {
1851
1894
  // path re-orders in an outer wrapper (`... AS "<table>_distinct" ORDER BY
1852
1895
  // <userOrder>`) where a correlated relation subquery (pick-row, `_count`,
1853
1896
  // to-one relation ordering) would reference the parent table name out of
1854
- // scope — a guaranteed "missing FROM-clause entry" crash on Postgres.
1897
+ // scope, a guaranteed "missing FROM-clause entry" crash on Postgres.
1855
1898
  // Checked BEFORE the SQL cache so build and warm-cache paths throw
1856
1899
  // identically (same rule as the vector guard inside the distinct branch).
1857
1900
  if (args?.distinct && args.distinct.length > 0 && args.orderBy) {
@@ -1910,7 +1953,7 @@ class QueryInterface {
1910
1953
  // own cache-key segment: a cached no-PII statement must never serve an
1911
1954
  // `includePii` call, nor vice versa.
1912
1955
  // `relationLoadStrategy: 'flatten'` compiles eligible to-one relations to
1913
- // LEFT JOINs instead of correlated subqueries — a completely different
1956
+ // LEFT JOINs instead of correlated subqueries, a completely different
1914
1957
  // statement for the same `with` shape, which `withFp` (strategy-blind)
1915
1958
  // does not distinguish. So the plan gets its own cache-key segment, exactly
1916
1959
  // like `pii=`: a join-planned template must never serve a flatten-planned
@@ -1961,7 +2004,7 @@ class QueryInterface {
1961
2004
  // where → cursor order (the collect path mirrors this exactly).
1962
2005
  let tail = freshWhereSql;
1963
2006
  if (args?.cursor) {
1964
- // Sorted (canonical) order — MUST match cursorFp and the cache-hit collect below.
2007
+ // Sorted (canonical) order, MUST match cursorFp and the cache-hit collect below.
1965
2008
  const cursorEntries = (0, filters_js_1.sortedEntries)(args.cursor).filter(([, v]) => v !== undefined);
1966
2009
  if (cursorEntries.length > 0) {
1967
2010
  // Resolve the seek direction per cursor field from the flattened
@@ -2011,7 +2054,7 @@ class QueryInterface {
2011
2054
  : '';
2012
2055
  sql = `SELECT ${distinctPrefix}${selectClause} FROM ${qt}${relationJoins.join('')}${lateralJoins.join('')}${tail}${orderBySql}`;
2013
2056
  }
2014
- // Pagination — push params in the same order the collect path mirrors
2057
+ // Pagination, push params in the same order the collect path mirrors
2015
2058
  // (limit before offset); the SQL TEXT shape is dialect-owned via
2016
2059
  // buildPagination (PG: ` LIMIT $n`/` OFFSET $n`; SQL Server: OFFSET/FETCH).
2017
2060
  let limitPh;
@@ -2035,7 +2078,7 @@ class QueryInterface {
2035
2078
  if (args?.with) {
2036
2079
  this.collectWithParams(args.with, params, undefined, flattenPlan);
2037
2080
  }
2038
- // 3. Cursor params — sorted (canonical) order, matching cursorFp and the build path.
2081
+ // 3. Cursor params, sorted (canonical) order, matching cursorFp and the build path.
2039
2082
  if (args?.cursor) {
2040
2083
  const cursorEntries = (0, filters_js_1.sortedEntries)(args.cursor).filter(([, v]) => v !== undefined);
2041
2084
  for (const [, v] of cursorEntries) {
@@ -2043,11 +2086,11 @@ class QueryInterface {
2043
2086
  }
2044
2087
  }
2045
2088
  // 4. ORDER BY params (vector KNN ordering binds a `$n::vector` query vector).
2046
- // Mirrors buildOrderBy's push order — between cursor and LIMIT.
2089
+ // Mirrors buildOrderBy's push order, between cursor and LIMIT.
2047
2090
  if (args?.orderBy) {
2048
2091
  this.collectOrderByParams(args.orderBy, params);
2049
2092
  }
2050
- // 5. LIMIT param — skipped when the dialect inlines pagination (build path
2093
+ // 5. LIMIT param, skipped when the dialect inlines pagination (build path
2051
2094
  // mirrors via paginationRef → no placeholder, no param).
2052
2095
  if (effectiveLimit !== undefined && !this.dialect.inlineLimitOffset) {
2053
2096
  // Validate here too: on a cache HIT the build path never runs, and a
@@ -2055,7 +2098,7 @@ class QueryInterface {
2055
2098
  // i.e. no limit at all).
2056
2099
  params.push(this.paginationValue(effectiveLimit, 'limit'));
2057
2100
  }
2058
- // 6. OFFSET param — same inline gate as LIMIT above.
2101
+ // 6. OFFSET param, same inline gate as LIMIT above.
2059
2102
  if (args?.offset !== undefined && !this.dialect.inlineLimitOffset) {
2060
2103
  params.push(this.paginationValue(args.offset, 'skip/offset'));
2061
2104
  }
@@ -2071,7 +2114,7 @@ class QueryInterface {
2071
2114
  };
2072
2115
  }
2073
2116
  // -------------------------------------------------------------------------
2074
- // findManyStream — async iterable using PostgreSQL cursors
2117
+ // findManyStream, async iterable using PostgreSQL cursors
2075
2118
  // -------------------------------------------------------------------------
2076
2119
  /**
2077
2120
  * Stream rows from a findMany query using PostgreSQL cursors.
@@ -2110,8 +2153,8 @@ class QueryInterface {
2110
2153
  const hasRelations = !!args?.with;
2111
2154
  // Build the positional-aware relation parser once for the whole stream.
2112
2155
  // Same flatten plan buildFindMany compiles below. The plan is a pure
2113
- // function of the schema, the `with` shape and `includePii` — never of
2114
- // `limit` — so the batch-size override the speculative fetch applies cannot
2156
+ // function of the schema, the `with` shape and `includePii`, never of
2157
+ // `limit`, so the batch-size override the speculative fetch applies cannot
2115
2158
  // change it, and the stream's parser matches the emitted SQL.
2116
2159
  const streamFlattenPlan = hasRelations ? this.planFlatten(args, args?.includePii === true) : null;
2117
2160
  const parseWith = hasRelations
@@ -2125,7 +2168,7 @@ class QueryInterface {
2125
2168
  this.currentAction = 'findManyStream';
2126
2169
  const speculativeResult = await this.queryWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout);
2127
2170
  if (speculativeResult.rows.length <= batchSize) {
2128
- // Small drain — yield all rows and return, no cursor needed
2171
+ // Small drain, yield all rows and return, no cursor needed
2129
2172
  for (const row of speculativeResult.rows) {
2130
2173
  yield (parseWith ? parseWith(row) : this.parseRow(row, this.table));
2131
2174
  }
@@ -2165,7 +2208,7 @@ class QueryInterface {
2165
2208
  }
2166
2209
  }
2167
2210
  // -------------------------------------------------------------------------
2168
- // findFirst — like findMany but returns a single row or null
2211
+ // findFirst, like findMany but returns a single row or null
2169
2212
  // -------------------------------------------------------------------------
2170
2213
  async findFirst(args) {
2171
2214
  return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
@@ -2190,7 +2233,7 @@ class QueryInterface {
2190
2233
  return deferred.transform(result);
2191
2234
  });
2192
2235
  }
2193
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
2236
+ // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
2194
2237
  buildFindFirst(args) {
2195
2238
  // Reuse findMany's SQL builder but force LIMIT 1
2196
2239
  const findManyArgs = { ...args, limit: 1 };
@@ -2206,7 +2249,7 @@ class QueryInterface {
2206
2249
  };
2207
2250
  }
2208
2251
  // -------------------------------------------------------------------------
2209
- // findFirstOrThrow — like findFirst but throws if no record found
2252
+ // findFirstOrThrow, like findFirst but throws if no record found
2210
2253
  // -------------------------------------------------------------------------
2211
2254
  async findFirstOrThrow(args) {
2212
2255
  return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
@@ -2215,7 +2258,7 @@ class QueryInterface {
2215
2258
  return deferred.transform(result);
2216
2259
  });
2217
2260
  }
2218
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
2261
+ // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
2219
2262
  buildFindFirstOrThrow(args) {
2220
2263
  const inner = this.buildFindFirst(args);
2221
2264
  return {
@@ -2236,7 +2279,7 @@ class QueryInterface {
2236
2279
  };
2237
2280
  }
2238
2281
  // -------------------------------------------------------------------------
2239
- // findUniqueOrThrow — like findUnique but throws if no record found
2282
+ // findUniqueOrThrow, like findUnique but throws if no record found
2240
2283
  // -------------------------------------------------------------------------
2241
2284
  async findUniqueOrThrow(args) {
2242
2285
  return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
@@ -2245,7 +2288,7 @@ class QueryInterface {
2245
2288
  return deferred.transform(result);
2246
2289
  });
2247
2290
  }
2248
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
2291
+ // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
2249
2292
  buildFindUniqueOrThrow(args) {
2250
2293
  const inner = this.buildFindUnique(args);
2251
2294
  return {
@@ -2278,7 +2321,7 @@ class QueryInterface {
2278
2321
  });
2279
2322
  }
2280
2323
  // -------------------------------------------------------------------------
2281
- // createMany — uses UNNEST for performance
2324
+ // createMany, uses UNNEST for performance
2282
2325
  // -------------------------------------------------------------------------
2283
2326
  async createMany(args) {
2284
2327
  return this.executeWithMiddleware('createMany', args, async () => {
@@ -2344,7 +2387,7 @@ class QueryInterface {
2344
2387
  // implicit transaction.
2345
2388
  this.pool);
2346
2389
  // biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
2347
- const ctx = { schema: this.schema, tx: tx };
2390
+ const ctx = { schema: this.schema, tx: tx, scopedConnect: this.scopedConnect };
2348
2391
  const result = await fn(ctx);
2349
2392
  await client.query(this.dialect.commitStatement());
2350
2393
  return result;
@@ -2392,7 +2435,7 @@ class QueryInterface {
2392
2435
  });
2393
2436
  }
2394
2437
  // -------------------------------------------------------------------------
2395
- // upsert — INSERT ... ON CONFLICT ... DO UPDATE
2438
+ // upsert, INSERT ... ON CONFLICT ... DO UPDATE
2396
2439
  // -------------------------------------------------------------------------
2397
2440
  async upsert(args) {
2398
2441
  return this.executeWithMiddleware('upsert', args, async () => {
@@ -2401,7 +2444,7 @@ class QueryInterface {
2401
2444
  });
2402
2445
  }
2403
2446
  // -------------------------------------------------------------------------
2404
- // updateMany — UPDATE ... WHERE ... returning count
2447
+ // updateMany, UPDATE ... WHERE ... returning count
2405
2448
  // -------------------------------------------------------------------------
2406
2449
  async updateMany(args) {
2407
2450
  return this.executeWithMiddleware('updateMany', args, async () => {
@@ -2411,7 +2454,7 @@ class QueryInterface {
2411
2454
  });
2412
2455
  }
2413
2456
  // -------------------------------------------------------------------------
2414
- // deleteMany — DELETE ... WHERE ... returning count
2457
+ // deleteMany, DELETE ... WHERE ... returning count
2415
2458
  // -------------------------------------------------------------------------
2416
2459
  async deleteMany(args) {
2417
2460
  return this.executeWithMiddleware('deleteMany', args, async () => {
@@ -2484,7 +2527,7 @@ class QueryInterface {
2484
2527
  return aggMod.buildAggregate(this.ctx, args);
2485
2528
  }
2486
2529
  // -------------------------------------------------------------------------
2487
- // aggregate — standalone aggregation without groupBy
2530
+ // aggregate, standalone aggregation without groupBy
2488
2531
  // -------------------------------------------------------------------------
2489
2532
  async aggregate(args) {
2490
2533
  return this.executeWithMiddleware('aggregate', args, async () => {
@@ -2620,7 +2663,7 @@ class QueryInterface {
2620
2663
  // Fall back to camelToSnake ONLY if that snake_cased name also exists as a
2621
2664
  // real column on the table. This preserves the convenience of writing
2622
2665
  // `userId` when the schema exposes `user_id` under an unusual field name,
2623
- // but rejects arbitrary strings — closing the defense-in-depth gap for
2666
+ // but rejects arbitrary strings, closing the defense-in-depth gap for
2624
2667
  // SQL injection and catching typos like `where: { emial: 'x' }` with a
2625
2668
  // clear error instead of a cryptic Postgres "column does not exist".
2626
2669
  const snake = (0, schema_js_1.camelToSnake)(field);
@@ -2630,15 +2673,14 @@ class QueryInterface {
2630
2673
  if (this.tableMeta.allColumns?.includes(snake)) {
2631
2674
  return snake;
2632
2675
  }
2633
- throw new errors_js_1.ValidationError(`[turbine] Unknown field "${field}" on table "${this.table}". ` +
2634
- `Known fields: ${Object.keys(this.tableMeta.columnMap).join(', ') || '(none)'}.`);
2676
+ throw new errors_js_1.ValidationError((0, utils_js_1.unknownFieldMessage)(this.table, field, this.tableMeta));
2635
2677
  }
2636
2678
  /** Convert camelCase field name to a double-quoted SQL identifier */
2637
2679
  toSqlColumn(field) {
2638
2680
  return this.q(this.toColumn(field));
2639
2681
  }
2640
2682
  // =========================================================================
2641
- // Fingerprinting — value-invariant shape keys for SQL cache lookup
2683
+ // Fingerprinting, value-invariant shape keys for SQL cache lookup
2642
2684
  // =========================================================================
2643
2685
  // ---------------------------------------------------------------------------
2644
2686
  // WHERE-clause compilation (extracted to where.ts).
@@ -2735,7 +2777,7 @@ class QueryInterface {
2735
2777
  }
2736
2778
  }
2737
2779
  // -------------------------------------------------------------------------
2738
- // Global filters (soft-delete / multi-tenancy — WS-G)
2780
+ // Global filters (soft-delete / multi-tenancy, WS-G)
2739
2781
  //
2740
2782
  // A configured global filter for a table is AND-merged into the compiled WHERE
2741
2783
  // of every query on that table (via {@link mergeGlobalFilter}, so the merge is
@@ -2743,12 +2785,12 @@ class QueryInterface {
2743
2785
  // subquery targeting it (rendered at build time against the subquery's alias/
2744
2786
  // table by the `*GlobalFilterAlias`/`*GlobalFilterExists` helpers, with the
2745
2787
  // shape folded into the SQL-cache key via {@link globalFilterCacheSegment}).
2746
- // Function filters are evaluated per resolve — at query-build time — enabling
2788
+ // Function filters are evaluated per resolve, at query-build time, enabling
2747
2789
  // per-request tenancy via a closure. They must return a STABLE shape (same
2748
2790
  // keys/operators); only values may vary between calls.
2749
2791
  // -------------------------------------------------------------------------
2750
2792
  // -------------------------------------------------------------------------
2751
- // Scoped (non-top-level) WHERE compilation — relation EXISTS sub-wheres and
2793
+ // Scoped (non-top-level) WHERE compilation, relation EXISTS sub-wheres and
2752
2794
  // relation `with`-clause `where`s. All three consumers below drive the SAME
2753
2795
  // canonical `walkWhere` the top level uses (bound to the SCOPE's target
2754
2796
  // table), so their key order + combinator structure + relation detection can
@@ -2764,12 +2806,12 @@ class QueryInterface {
2764
2806
  /**
2765
2807
  * Build ORDER BY clause from an object.
2766
2808
  *
2767
- * Each value is either a plain direction (`'asc'`/`'desc'`) or — for pgvector
2768
- * columns — a `{ distance: { to, metric, direction? } }` KNN ordering object.
2809
+ * Each value is either a plain direction (`'asc'`/`'desc'`) or, for pgvector
2810
+ * columns, a `{ distance: { to, metric, direction? } }` KNN ordering object.
2769
2811
  * Vector ordering binds the query vector as a `$n::vector` param, so a `params`
2770
2812
  * array MUST be supplied when a vector ordering may be present (top-level
2771
2813
  * findMany path). When `params` is omitted (groupBy / relation path) a vector
2772
- * ordering throws — KNN ordering is only supported at the top level.
2814
+ * ordering throws, KNN ordering is only supported at the top level.
2773
2815
  */
2774
2816
  // -------------------------------------------------------------------------
2775
2817
  // pgvector helpers (similarity search)
@@ -2802,7 +2844,7 @@ class QueryInterface {
2802
2844
  // columns (`date[]`, `timestamp[]`, `timestamptz[]`), for which the
2803
2845
  // driver already hands back a `Date[]`. Coercing it ran
2804
2846
  // `new Date(String(theArray))` and replaced the whole array with a
2805
- // single Invalid Date — the column was unreadable on every strategy.
2847
+ // single Invalid Date, the column was unreadable on every strategy.
2806
2848
  // The join strategy's string arrays are handled upstream instead, by
2807
2849
  // the JSON-wire decode in relations.ts.
2808
2850
  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,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * turbine-orm — Deferred query + QueryInterface option types
3
+ * turbine-orm, Deferred query + QueryInterface option types
4
4
  *
5
5
  * Split from builder.ts so the class file focuses on SQL assembly / execution.
6
6
  */