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
@@ -49,6 +49,7 @@ var __importStar = (this && this.__importStar) || (function () {
49
49
  Object.defineProperty(exports, "__esModule", { value: true });
50
50
  exports.resolveProjection = resolveProjection;
51
51
  exports.resolveColumns = resolveColumns;
52
+ exports.relationOptions = relationOptions;
52
53
  exports.withFingerprint = withFingerprint;
53
54
  exports.collectWithParams = collectWithParams;
54
55
  exports.collectRelationSubqueryParams = collectRelationSubqueryParams;
@@ -254,6 +255,46 @@ function resolveProjection(qi, table, meta, select, omit, includePii) {
254
255
  function resolveColumns(qi, select, omit, includePii) {
255
256
  return resolveProjection(qi, qi.table, qi.tableMeta, select, omit, includePii);
256
257
  }
258
+ /**
259
+ * The one place `with: { posts: true }` becomes an options object.
260
+ *
261
+ * `true` is not a third kind of relation spec, it is shorthand for "include
262
+ * this relation with no options", i.e. `{}`. Every walker over a `with` tree
263
+ * needs the same reading of it, and there are SIX of them: the build path
264
+ * ({@link buildRelationSubquery} / {@link buildManyToManySubquery}), the
265
+ * param-collect mirror ({@link collectRelationSubqueryParams}), the cache
266
+ * fingerprint ({@link withFingerprint}), the decode shape
267
+ * ({@link buildRelationShape}), the projection resolver
268
+ * ({@link resolveTargetColumns}) and the flatten planner.
269
+ *
270
+ * Each of them USED TO spell the shorthand out for itself, with a
271
+ * `spec !== true && spec.x` guard per option on the build side and a single
272
+ * `if (spec === true) return` early exit on the collect side. Those two
273
+ * readings are not the same, and the difference was a live bug: since global
274
+ * filters reached relation subqueries the build path has emitted the target's
275
+ * filter UNCONDITIONALLY (a `with` must never surface rows the filter hides),
276
+ * while the collect path's early exit returned before pushing its params. A
277
+ * value-bearing filter on a relation target therefore compiled a `$N` that no
278
+ * value backed, and `with: { posts: true }` failed at bind time with
279
+ * "bind message supplies N parameters, but prepared statement requires N+1"
280
+ * while the identical query written `with: { posts: {} }` worked.
281
+ *
282
+ * Returning ONE frozen empty options object removes the branch instead of
283
+ * duplicating it: below this call there is no `true` case left to keep in
284
+ * sync, so a future option added to one walker cannot be forgotten by the
285
+ * shorthand in another. Frozen and shared because it is read-only by
286
+ * construction (every consumer only reads `select`/`omit`/`where`/`orderBy`/
287
+ * `limit`/`with`), so one instance serves every call and allocates nothing on
288
+ * the hot path.
289
+ */
290
+ const EMPTY_WITH_OPTIONS = Object.freeze({});
291
+ /**
292
+ * Read a relation spec as options. See {@link EMPTY_WITH_OPTIONS} for why this
293
+ * is a shared authority rather than a guard repeated per walker.
294
+ */
295
+ function relationOptions(spec) {
296
+ return spec === true ? EMPTY_WITH_OPTIONS : spec;
297
+ }
257
298
  /**
258
299
  * Produce a fingerprint for a `with` clause tree. Recursion mirrors
259
300
  * buildSelectWithRelations / buildRelationSubquery.
@@ -290,11 +331,7 @@ function withFingerprint(qi, withClause, table, depth = 0) {
290
331
  parts.push(`unknown:${relName}`);
291
332
  continue;
292
333
  }
293
- if (spec === true) {
294
- parts.push(relName);
295
- continue;
296
- }
297
- const opts = spec;
334
+ const opts = relationOptions(spec);
298
335
  const subParts = [];
299
336
  // select/omit shape
300
337
  if (opts.select) {
@@ -372,8 +409,10 @@ function collectWithParams(qi, withClause, params, table, flattenPlan) {
372
409
  * Collect params from a single relation subquery. Mirrors buildRelationSubquery.
373
410
  */
374
411
  function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, depth = 0) {
375
- if (spec === true)
376
- return; // No params for default include
412
+ // `true` IS `{}` (see {@link relationOptions}). This used to be an early
413
+ // `return`, which skipped the target's global-filter params the build path
414
+ // always pushes.
415
+ const opts = relationOptions(spec);
377
416
  const targetTable = relDef.to;
378
417
  const targetMeta = qi.schema.tables[targetTable];
379
418
  if (!targetMeta)
@@ -388,19 +427,19 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
388
427
  // orderBy params → where params → limit param → nested-with params
389
428
  // (always, both paths).
390
429
  if (relDef.type === 'manyToMany') {
391
- const m2mOrderEntries = spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
430
+ const m2mOrderEntries = opts.orderBy ? (0, filters_js_1.orderByEntries)(opts.orderBy).filter(([, dir]) => dir !== undefined) : [];
392
431
  if (nativeOrderPath && m2mOrderEntries.length > 0) {
393
432
  collectRelationOrderParams(qi, targetTable, targetMeta, m2mOrderEntries, params);
394
433
  }
395
- if (spec.where) {
396
- whereMod.collectAliasWhereParams(qi, targetTable, targetMeta, spec.where, params);
434
+ if (opts.where) {
435
+ whereMod.collectAliasWhereParams(qi, targetTable, targetMeta, opts.where, params);
397
436
  }
398
437
  whereMod.collectTargetGlobalFilterAlias(qi, targetTable, params);
399
- if (spec.limit !== undefined && !qi.dialect.inlineLimitOffset) {
400
- params.push(qi.paginationValue(spec.limit, 'relation limit'));
438
+ if (opts.limit !== undefined && !qi.dialect.inlineLimitOffset) {
439
+ params.push(qi.paginationValue(opts.limit, 'relation limit'));
401
440
  }
402
- if (spec.with) {
403
- for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(spec.with)) {
441
+ if (opts.with) {
442
+ for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
404
443
  const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
405
444
  if (!nestedRelDef)
406
445
  continue;
@@ -410,12 +449,12 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
410
449
  return;
411
450
  }
412
451
  // Mirrors buildRelationSubquery's willWrap: `orderBy: {}` is treated as absent.
413
- const relOrderEntries = spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
452
+ const relOrderEntries = opts.orderBy ? (0, filters_js_1.orderByEntries)(opts.orderBy).filter(([, dir]) => dir !== undefined) : [];
414
453
  const hasOrder = relOrderEntries.length > 0;
415
- const willWrap = relDef.type === 'hasMany' && (spec.limit !== undefined || hasOrder);
454
+ const willWrap = relDef.type === 'hasMany' && (opts.limit !== undefined || hasOrder);
416
455
  // Non-wrapped path: nested relations BEFORE where/limit
417
- if (!willWrap && spec.with) {
418
- for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(spec.with)) {
456
+ if (!willWrap && opts.with) {
457
+ for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
419
458
  const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
420
459
  if (!nestedRelDef)
421
460
  continue;
@@ -429,8 +468,8 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
429
468
  collectRelationOrderParams(qi, targetTable, targetMeta, relOrderEntries, params);
430
469
  }
431
470
  // where params, mirrors buildAliasWhere push order
432
- if (spec.where) {
433
- whereMod.collectAliasWhereParams(qi, targetTable, targetMeta, spec.where, params);
471
+ if (opts.where) {
472
+ whereMod.collectAliasWhereParams(qi, targetTable, targetMeta, opts.where, params);
434
473
  }
435
474
  // Global filter on the target, mirrors targetGlobalFilterAlias in
436
475
  // buildRelationSubquery (pushed after spec.where, before limit).
@@ -439,12 +478,12 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
439
478
  // buildRelationSubquery). belongsTo/hasOne ignore limit (always LIMIT 1), so
440
479
  // pushing one here would orphan a param and desync the collect path.
441
480
  // `limit: 0` pushes (LIMIT 0 is honored), so check !== undefined.
442
- if (relDef.type === 'hasMany' && spec.limit !== undefined && !qi.dialect.inlineLimitOffset) {
443
- params.push(qi.paginationValue(spec.limit, 'relation limit'));
481
+ if (relDef.type === 'hasMany' && opts.limit !== undefined && !qi.dialect.inlineLimitOffset) {
482
+ params.push(qi.paginationValue(opts.limit, 'relation limit'));
444
483
  }
445
484
  // Wrapped path: nested relations AFTER where/limit (inside inner subquery)
446
- if (willWrap && spec.with) {
447
- for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(spec.with)) {
485
+ if (willWrap && opts.with) {
486
+ for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
448
487
  const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
449
488
  if (!nestedRelDef)
450
489
  continue;
@@ -813,13 +852,12 @@ function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lateralSin
813
852
  // landing here on such a table is an orderBy VALUE of the wrong shape on a
814
853
  // scalar column, which deserves to be named rather than reported as a
815
854
  // missing relation.
816
- const known = Object.keys(ownerMeta.relations);
817
855
  const isColumn = (0, utils_js_1.resolveColumnName)(ownerMeta, relName) !== undefined;
818
856
  throw new errors_js_1.RelationError(isColumn
819
857
  ? `[turbine] orderBy on "${ownerTable}.${relName}" got a relation-shaped value, but "${relName}" is a ` +
820
858
  `column. Order a column with 'asc' / 'desc' (or { sort, nulls }); the object form is for relations.`
821
859
  : `[turbine] Unknown relation "${relName}" in orderBy on table "${ownerTable}". ` +
822
- (known.length > 0 ? `Available: ${known.join(', ')}` : `"${ownerTable}" has no relations.`));
860
+ (0, utils_js_1.availableClause)(Object.keys(ownerMeta.relations), `"${ownerTable}" has no relations.`));
823
861
  }
824
862
  // Pick-row ordering (`{ pick, by }`): order by a value from ONE related
825
863
  // row: a correlated scalar subquery with its own ORDER BY … LIMIT 1.
@@ -1398,8 +1436,9 @@ function parseNestedRow(qi, row, table, fromJson = false) {
1398
1436
  * second implementation.
1399
1437
  */
1400
1438
  function resolveTargetColumns(qi, spec, targetMeta, includePii, targetTable = targetMeta.name) {
1401
- const select = spec === true ? undefined : spec.select;
1402
- const omit = spec === true ? undefined : spec.omit;
1439
+ const opts = relationOptions(spec);
1440
+ const select = opts.select;
1441
+ const omit = opts.omit;
1403
1442
  return resolveProjection(qi, targetTable, targetMeta, select, omit, includePii) ?? targetMeta.allColumns;
1404
1443
  }
1405
1444
  /**
@@ -1554,8 +1593,9 @@ function buildRelationShape(qi, relDef, spec, parentMeta, includePii) {
1554
1593
  const targetColumns = resolveTargetColumns(qi, spec, targetMeta, includePii, relDef.to);
1555
1594
  const keys = targetColumns.map((col) => targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col));
1556
1595
  const nested = {};
1557
- if (spec !== true && spec.with) {
1558
- for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(spec.with)) {
1596
+ const nestedWith = relationOptions(spec).with;
1597
+ if (nestedWith) {
1598
+ for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(nestedWith)) {
1559
1599
  const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
1560
1600
  if (!nestedRelDef)
1561
1601
  continue;
@@ -1792,15 +1832,13 @@ function planFlattenNode(qi, counter, relName, relDef, spec, depth, path, includ
1792
1832
  : `its correlation column(s) on "${relDef.to}" are not provably unique (no primary key, ` +
1793
1833
  'unique constraint or non-partial unique index covers them), so a join could multiply parent rows');
1794
1834
  }
1795
- const opts = spec === true ? undefined : spec;
1796
- if (opts) {
1797
- if (opts.limit !== undefined)
1798
- return decline('it declares a `limit`');
1799
- if (opts.orderBy && (0, filters_js_1.orderByEntries)(opts.orderBy).some(([, dir]) => dir !== undefined)) {
1800
- return decline('it declares an `orderBy`');
1801
- }
1835
+ const opts = relationOptions(spec);
1836
+ if (opts.limit !== undefined)
1837
+ return decline('it declares a `limit`');
1838
+ if (opts.orderBy && (0, filters_js_1.orderByEntries)(opts.orderBy).some(([, dir]) => dir !== undefined)) {
1839
+ return decline('it declares an `orderBy`');
1802
1840
  }
1803
- const nestedEntries = opts?.with ? (0, filters_js_1.sortedEntries)(opts.with) : [];
1841
+ const nestedEntries = opts.with ? (0, filters_js_1.sortedEntries)(opts.with) : [];
1804
1842
  for (const [nestedRelName] of nestedEntries) {
1805
1843
  if (nestedRelName === '_count')
1806
1844
  return decline('its nested `with` uses `_count`');
@@ -2001,8 +2039,9 @@ function emitFlattenInner(qi, node, params, innerSelects, innerJoins, innerWhere
2001
2039
  // than dropping the parent row, matching what the correlated subquery's
2002
2040
  // `LIMIT 1` did when it returned NULL.
2003
2041
  const filters = innerWhere;
2004
- if (node.spec !== true && node.spec.where) {
2005
- const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, srcAlias, node.spec.where, params);
2042
+ const nodeWhere = relationOptions(node.spec).where;
2043
+ if (nodeWhere) {
2044
+ const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, srcAlias, nodeWhere, params);
2006
2045
  if (extra)
2007
2046
  filters.push(extra);
2008
2047
  }
@@ -2068,8 +2107,9 @@ function projectFlattenNode(qi, node, outerAlias, selectSink) {
2068
2107
  }
2069
2108
  /** Param-collect mirror of {@link emitFlattenNode}. */
2070
2109
  function collectFlattenNodeParams(qi, node, params) {
2071
- if (node.spec !== true && node.spec.where) {
2072
- whereMod.collectAliasWhereParams(qi, node.targetTable, node.targetMeta, node.spec.where, params);
2110
+ const nodeWhere = relationOptions(node.spec).where;
2111
+ if (nodeWhere) {
2112
+ whereMod.collectAliasWhereParams(qi, node.targetTable, node.targetMeta, nodeWhere, params);
2073
2113
  }
2074
2114
  whereMod.collectTargetGlobalFilterAlias(qi, node.targetTable, params);
2075
2115
  for (const slot of node.slots) {
@@ -2218,7 +2258,7 @@ function buildSelectWithRelations(qi, table, withClause, params, columnsList, de
2218
2258
  const relDef = (0, utils_js_1.ownLookup)(meta.relations, relName);
2219
2259
  if (!relDef) {
2220
2260
  throw new errors_js_1.RelationError(`[turbine] Unknown relation "${relName}" on table "${table}". ` +
2221
- `Available: ${Object.keys(meta.relations).join(', ')}`);
2261
+ (0, utils_js_1.availableClause)(Object.keys(meta.relations), `"${table}" has no relations.`));
2222
2262
  }
2223
2263
  // `relationLoadStrategy: 'flatten'`: an eligible to-one relation becomes a
2224
2264
  // LEFT JOIN + prefixed scalar projection instead of a per-parent-row
@@ -2352,6 +2392,11 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2352
2392
  const targetMeta = qi.schema.tables[targetTable];
2353
2393
  if (!targetMeta)
2354
2394
  throw new errors_js_1.RelationError(`[turbine] Unknown relation target "${targetTable}"`);
2395
+ // `true` IS `{}`; below this line there is no `true` case, which is what
2396
+ // keeps this walk and collectRelationSubqueryParams reading one shape.
2397
+ // The raw `spec` is still what crosses the dialect seam below, since
2398
+ // RelationSubqueryContext is a published contract.
2399
+ const opts = relationOptions(spec);
2355
2400
  // Dev-only: correlated relation loading probes the child table once per parent
2356
2401
  // row, so a missing FK index multiplies into per-parent full-table scans (a
2357
2402
  // batched-loader ORM pays the same missing index only once, which is why
@@ -2400,9 +2445,33 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2400
2445
  depth: currentDepth,
2401
2446
  path: currentPath,
2402
2447
  quote: (name) => qi.q(name),
2403
- buildWhere: (whereAlias) => (spec !== true && spec.where
2404
- ? whereMod.buildAliasWhere(qi, targetTable, targetMeta, whereAlias, spec.where, params)
2405
- : '') ?? '',
2448
+ // The relation target's WHERE, which is the caller's `spec.where` AND the
2449
+ // target table's GLOBAL FILTER. Both, not just the first.
2450
+ //
2451
+ // A dialect that overrides buildRelationSubquery takes over the whole
2452
+ // subquery, so the generic builder returns at this seam BEFORE it would
2453
+ // have applied targetGlobalFilterAlias itself. The override cannot apply
2454
+ // the filter on its own: RelationSubqueryContext deliberately hands it no
2455
+ // BuilderCtx, so `targetGlobalFilterAlias` is not reachable from a dialect
2456
+ // file. That left SQL Server's `FOR JSON PATH` path emitting a correlation
2457
+ // predicate and nothing else, so a `with` returned rows a tenancy or
2458
+ // soft-delete filter is supposed to hide, while the collect mirror still
2459
+ // pushed the filter's params. Wrong rows AND an orphan param, on every
2460
+ // spec shape.
2461
+ //
2462
+ // Folding it in here rather than at each override keeps ONE authority for
2463
+ // "what does this relation's WHERE contain", and lands the params exactly
2464
+ // where collectRelationSubqueryParams already expects them: after
2465
+ // spec.where, before the limit.
2466
+ buildWhere: (whereAlias) => {
2467
+ const userWhere = (opts.where
2468
+ ? whereMod.buildAliasWhere(qi, targetTable, targetMeta, whereAlias, opts.where, params)
2469
+ : '') ?? '';
2470
+ const gf = whereMod.targetGlobalFilterAlias(qi, targetTable, whereAlias, params);
2471
+ if (userWhere && gf)
2472
+ return `${userWhere} AND ${gf}`;
2473
+ return userWhere || gf || '';
2474
+ },
2406
2475
  recurse: (nRelDef, nSpec, nParent, nDepth, nPath) => buildRelationSubquery(qi, nRelDef, nSpec, params, nParent, aliasCounter, nDepth, nPath, includePii),
2407
2476
  });
2408
2477
  }
@@ -2414,8 +2483,8 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2414
2483
  // An orderBy with no defined entries (`orderBy: {}`) is treated as absent -
2415
2484
  // it must neither trigger the wrap (dropping nested relations) nor render a
2416
2485
  // dangling `ORDER BY `. `limit: 0` is meaningful (LIMIT 0) and DOES wrap.
2417
- const relOrderEntries = spec !== true && spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
2418
- const willWrap = relDef.type === 'hasMany' && spec !== true && (spec.limit !== undefined || relOrderEntries.length > 0);
2486
+ const relOrderEntries = opts.orderBy ? (0, filters_js_1.orderByEntries)(opts.orderBy).filter(([, dir]) => dir !== undefined) : [];
2487
+ const willWrap = relDef.type === 'hasMany' && (opts.limit !== undefined || relOrderEntries.length > 0);
2419
2488
  // manyToMany takes a dedicated JOIN-through-junction path. Nested relations,
2420
2489
  // where, orderBy, and select/omit are handled there (the target alias is the
2421
2490
  // row source, exactly like hasMany), so short-circuit before the hasMany logic.
@@ -2423,12 +2492,12 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2423
2492
  return buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCounter, currentDepth, currentPath, alias, targetMeta, targetColumns, includePii);
2424
2493
  }
2425
2494
  // Nested relations, only in the non-wrapped path (wrapped path builds them separately)
2426
- if (!willWrap && spec !== true && spec.with) {
2427
- for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(spec.with)) {
2495
+ if (!willWrap && opts.with) {
2496
+ for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
2428
2497
  const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
2429
2498
  if (!nestedRelDef) {
2430
2499
  throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
2431
- `Available: ${Object.keys(targetMeta.relations).join(', ')}`);
2500
+ (0, utils_js_1.availableClause)(Object.keys(targetMeta.relations), `"${relDef.to}" has no relations.`));
2432
2501
  }
2433
2502
  // Recursively build nested subquery, passing THIS alias as the parent reference
2434
2503
  const nestedSubquery = buildRelationSubquery(qi, nestedRelDef, nestedSpec, params, alias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name], includePii);
@@ -2465,8 +2534,8 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2465
2534
  }
2466
2535
  // Additional filters, full scalar where surface (equality, null, operator
2467
2536
  // objects, OR/AND/NOT), properly parameterized against this alias.
2468
- if (spec !== true && spec.where) {
2469
- const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, alias, spec.where, params);
2537
+ if (opts.where) {
2538
+ const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, alias, opts.where, params);
2470
2539
  if (extra)
2471
2540
  whereClause += ` AND ${extra}`;
2472
2541
  }
@@ -2483,8 +2552,8 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2483
2552
  // (and shifts every later placeholder by one). To-one relations ignore limit.
2484
2553
  // `limit: 0` is honored (LIMIT 0 → empty array), so check !== undefined.
2485
2554
  let limitClause = '';
2486
- if (relDef.type === 'hasMany' && spec !== true && spec.limit !== undefined) {
2487
- limitClause = ` LIMIT ${qi.paginationRef(spec.limit, params, 'relation limit')}`;
2555
+ if (relDef.type === 'hasMany' && opts.limit !== undefined) {
2556
+ limitClause = ` LIMIT ${qi.paginationRef(opts.limit, params, 'relation limit')}`;
2488
2557
  }
2489
2558
  if (relDef.type === 'hasMany') {
2490
2559
  // When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
@@ -2497,12 +2566,12 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
2497
2566
  // For the json_build_object, reference the inner alias, only include resolved columns
2498
2567
  const innerJsonPairs = jsonScalarPairs(qi, targetMeta, targetColumns, innerAlias);
2499
2568
  // Build nested relation subqueries referencing innerAlias
2500
- if (spec !== true && spec.with) {
2501
- for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(spec.with)) {
2569
+ if (opts.with) {
2570
+ for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
2502
2571
  const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
2503
2572
  if (!nestedRelDef) {
2504
2573
  throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
2505
- `Available: ${Object.keys(targetMeta.relations).join(', ')}`);
2574
+ (0, utils_js_1.availableClause)(Object.keys(targetMeta.relations), `"${relDef.to}" has no relations.`));
2506
2575
  }
2507
2576
  const nestedSub = buildRelationSubquery(qi, nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name], includePii);
2508
2577
  const fallback = nestedRelDef.type === 'hasMany' ? qi.dialect.emptyJsonArrayLiteral : qi.dialect.nullJsonLiteral;
@@ -2547,6 +2616,9 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
2547
2616
  if (!relDef.through) {
2548
2617
  throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relDef.name}" is missing a \`through\` junction descriptor.`);
2549
2618
  }
2619
+ // `true` IS `{}`; see {@link relationOptions}. Same reading as
2620
+ // buildRelationSubquery and collectRelationSubqueryParams' m2m branch.
2621
+ const opts = relationOptions(spec);
2550
2622
  const targetTable = relDef.to;
2551
2623
  const qTarget = qi.q(targetTable);
2552
2624
  const qJunction = qi.q(relDef.through.table);
@@ -2582,15 +2654,15 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
2582
2654
  // `orderBy: {}` (no defined entries) is treated as absent: it must not
2583
2655
  // render a dangling `ORDER BY `. Param pushes here land BEFORE the
2584
2656
  // spec.where params, mirrored by collectRelationSubqueryParams' m2m branch.
2585
- const relOrderEntries = spec !== true && spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
2657
+ const relOrderEntries = opts.orderBy ? (0, filters_js_1.orderByEntries)(opts.orderBy).filter(([, dir]) => dir !== undefined) : [];
2586
2658
  let orderClause = '';
2587
2659
  if (relOrderEntries.length > 0) {
2588
2660
  orderClause = buildRelationOrderClause(qi, targetTable, targetMeta, talias, relOrderEntries, params);
2589
2661
  }
2590
2662
  // Additional WHERE filters on the target, full scalar where surface,
2591
2663
  // properly parameterized against the target alias.
2592
- if (spec !== true && spec.where) {
2593
- const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, talias, spec.where, params);
2664
+ if (opts.where) {
2665
+ const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, talias, opts.where, params);
2594
2666
  if (extra)
2595
2667
  whereClause += ` AND ${extra}`;
2596
2668
  }
@@ -2601,8 +2673,8 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
2601
2673
  whereClause += ` AND ${gfExtra}`;
2602
2674
  // LIMIT, `limit: 0` is honored (LIMIT 0 → empty array)
2603
2675
  let limitClause = '';
2604
- if (spec !== true && spec.limit !== undefined) {
2605
- limitClause = ` LIMIT ${qi.paginationRef(spec.limit, params, 'relation limit')}`;
2676
+ if (opts.limit !== undefined) {
2677
+ limitClause = ` LIMIT ${qi.paginationRef(opts.limit, params, 'relation limit')}`;
2606
2678
  }
2607
2679
  const fromJoin = `FROM ${qTarget} ${talias} JOIN ${qJunction} ${jalias} ON ${joinOn}`;
2608
2680
  // When LIMIT or ORDER BY is present, wrap the joined rows in an inner subquery
@@ -2613,12 +2685,12 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
2613
2685
  `${fromJoin} WHERE ${whereClause}${orderClause}${limitClause}`;
2614
2686
  const innerJsonPairs = jsonScalarPairs(qi, targetMeta, targetColumns, innerAlias);
2615
2687
  // Nested relations reference the inner alias.
2616
- if (spec !== true && spec.with) {
2617
- for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(spec.with)) {
2688
+ if (opts.with) {
2689
+ for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
2618
2690
  const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
2619
2691
  if (!nestedRelDef) {
2620
2692
  throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
2621
- `Available: ${Object.keys(targetMeta.relations).join(', ')}`);
2693
+ (0, utils_js_1.availableClause)(Object.keys(targetMeta.relations), `"${relDef.to}" has no relations.`));
2622
2694
  }
2623
2695
  const nestedSub = buildRelationSubquery(qi, nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name], includePii);
2624
2696
  const fallback = nestedRelDef.type === 'belongsTo' || nestedRelDef.type === 'hasOne'
@@ -2633,12 +2705,12 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
2633
2705
  // Simple path: build the json object pairs directly off the target alias,
2634
2706
  // including any nested relations (correlated to the target alias).
2635
2707
  const jsonPairs = jsonScalarPairs(qi, targetMeta, targetColumns, talias);
2636
- if (spec !== true && spec.with) {
2637
- for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(spec.with)) {
2708
+ if (opts.with) {
2709
+ for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
2638
2710
  const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
2639
2711
  if (!nestedRelDef) {
2640
2712
  throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
2641
- `Available: ${Object.keys(targetMeta.relations).join(', ')}`);
2713
+ (0, utils_js_1.availableClause)(Object.keys(targetMeta.relations), `"${relDef.to}" has no relations.`));
2642
2714
  }
2643
2715
  const nestedSub = buildRelationSubquery(qi, nestedRelDef, nestedSpec, params, talias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name], includePii);
2644
2716
  const fallback = nestedRelDef.type === 'belongsTo' || nestedRelDef.type === 'hasOne'
@@ -677,6 +677,19 @@ export declare function unknownFieldMessage(table: string, field: string, meta:
677
677
  columnMap: Record<string, string>;
678
678
  relations?: Record<string, unknown>;
679
679
  }): string;
680
+ /**
681
+ * The trailing "Available: a, b, c" clause of a name-not-found error, or
682
+ * `emptySentence` when there is nothing to list.
683
+ *
684
+ * The empty branch is the whole reason this exists. Fifteen error sites
685
+ * interpolated `Object.keys(...).join(', ')` directly, and on a table with no
686
+ * relations (or a schema with no tables) that renders a dangling
687
+ * `Available: ` with nothing after the colon, which reads as a broken error
688
+ * message rather than as an answer. One site already got this right by hand;
689
+ * the other fourteen are now the same function, so the next one cannot get it
690
+ * wrong by omission.
691
+ */
692
+ export declare function availableClause(names: readonly string[], emptySentence: string): string;
680
693
  /**
681
694
  * The error text for a RELATION named inside `select` / `omit`.
682
695
  *
@@ -45,6 +45,7 @@ exports.coerceJsonWireValue = coerceJsonWireValue;
45
45
  exports.closestName = closestName;
46
46
  exports.suggestKey = suggestKey;
47
47
  exports.unknownFieldMessage = unknownFieldMessage;
48
+ exports.availableClause = availableClause;
48
49
  exports.relationInProjectionMessage = relationInProjectionMessage;
49
50
  exports.warnRedundantSortTerm = warnRedundantSortTerm;
50
51
  exports.selectNamesNothingMessage = selectNamesNothingMessage;
@@ -1467,6 +1468,21 @@ function unknownFieldMessage(table, field, meta) {
1467
1468
  ` Known columns: ${columns.join(', ') || '(none)'}.` +
1468
1469
  (relations.length ? ` Known relations (valid in \`where\` and \`with\`): ${relations.join(', ')}.` : ''));
1469
1470
  }
1471
+ /**
1472
+ * The trailing "Available: a, b, c" clause of a name-not-found error, or
1473
+ * `emptySentence` when there is nothing to list.
1474
+ *
1475
+ * The empty branch is the whole reason this exists. Fifteen error sites
1476
+ * interpolated `Object.keys(...).join(', ')` directly, and on a table with no
1477
+ * relations (or a schema with no tables) that renders a dangling
1478
+ * `Available: ` with nothing after the colon, which reads as a broken error
1479
+ * message rather than as an answer. One site already got this right by hand;
1480
+ * the other fourteen are now the same function, so the next one cannot get it
1481
+ * wrong by omission.
1482
+ */
1483
+ function availableClause(names, emptySentence) {
1484
+ return names.length > 0 ? `Available: ${names.join(', ')}` : emptySentence;
1485
+ }
1470
1486
  /**
1471
1487
  * The error text for a RELATION named inside `select` / `omit`.
1472
1488
  *
@@ -143,7 +143,7 @@ export type TurbineHttpOptions = Omit<TurbineConfig, 'pool'>;
143
143
  * ```ts
144
144
  * import { Pool } from '@neondatabase/serverless';
145
145
  * import { turbineHttp } from 'turbine-orm/serverless';
146
- * import type { TurbineClient } from './generated/turbine';
146
+ * import type { TurbineClient } from './generated/turbine/index.js';
147
147
  * import { SCHEMA } from './generated/turbine/metadata.js';
148
148
  *
149
149
  * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
@@ -126,7 +126,7 @@ const client_js_1 = require("./client.js");
126
126
  * ```ts
127
127
  * import { Pool } from '@neondatabase/serverless';
128
128
  * import { turbineHttp } from 'turbine-orm/serverless';
129
- * import type { TurbineClient } from './generated/turbine';
129
+ * import type { TurbineClient } from './generated/turbine/index.js';
130
130
  * import { SCHEMA } from './generated/turbine/metadata.js';
131
131
  *
132
132
  * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
@@ -34,7 +34,7 @@
34
34
  * - **Case-insensitive matching** uses `COLLATE NOCASE`, which is **ASCII-only**
35
35
  * (no Unicode case folding).
36
36
  *
37
- * ## Example, `:memory:` database
37
+ * ## Example, `:memory:` database, from a generated schema
38
38
  *
39
39
  * ```ts
40
40
  * import { turbineSqlite } from 'turbine-orm/sqlite';
@@ -44,6 +44,38 @@
44
44
  * const users = await db.users.findMany({ with: { posts: true }, limit: 10 });
45
45
  * await db.disconnect();
46
46
  * ```
47
+ *
48
+ * ## Example, SQLite with no Postgres anywhere
49
+ *
50
+ * The snippet above needs `turbine generate`, which reads a live **Postgres**
51
+ * catalog, so it is the wrong starting point if SQLite is your only database.
52
+ * Describe the schema in code instead: `schemaToSQL` emits the DDL and
53
+ * `schemaDefToMetadata` derives the runtime metadata (relations included, from
54
+ * the same `references:`), both pure functions with no database involved.
55
+ *
56
+ * ```ts
57
+ * import { defineSchema, schemaDefToMetadata, schemaToSQL } from 'turbine-orm';
58
+ * import { sqliteDialect, turbineSqlite } from 'turbine-orm/sqlite';
59
+ *
60
+ * const schema = defineSchema({
61
+ * users: { id: { type: 'serial', primaryKey: true }, email: { type: 'text', notNull: true } },
62
+ * posts: {
63
+ * id: { type: 'serial', primaryKey: true },
64
+ * userId: { type: 'integer', notNull: true, references: 'users.id' },
65
+ * title: { type: 'text', notNull: true },
66
+ * },
67
+ * });
68
+ *
69
+ * const db = turbineSqlite(':memory:', schemaDefToMetadata(schema));
70
+ * for (const stmt of schemaToSQL(schema, { dialect: sqliteDialect })) {
71
+ * await db.raw([stmt] as never);
72
+ * }
73
+ *
74
+ * // `db.table(...)`, not `db.users`: the typed property accessors are emitted
75
+ * // by `turbine generate`, and this path skips it.
76
+ * const users = await db.table('users').findMany({ with: { posts: true }, orderBy: { id: 'asc' } });
77
+ * await db.disconnect();
78
+ * ```
47
79
  */
48
80
  import type { DatabaseSync } from 'node:sqlite';
49
81
  import { type PgCompatPool, type PgCompatPoolClient, TurbineClient } from './client.js';