turbine-orm 0.77.1 → 0.78.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 (93) hide show
  1. package/README.md +13 -9
  2. package/dist/cjs/cli/config.d.ts +7 -1
  3. package/dist/cjs/cli/config.js +11 -2
  4. package/dist/cjs/cli/destructive.d.ts +1 -1
  5. package/dist/cjs/cli/destructive.js +307 -9
  6. package/dist/cjs/cli/index.js +252 -42
  7. package/dist/cjs/cli/mcp.d.ts +23 -0
  8. package/dist/cjs/cli/mcp.js +190 -152
  9. package/dist/cjs/cli/migrate.d.ts +243 -3
  10. package/dist/cjs/cli/migrate.js +432 -43
  11. package/dist/cjs/cli/sql-statements.js +27 -0
  12. package/dist/cjs/cli/studio.d.ts +0 -1
  13. package/dist/cjs/cli/studio.js +9 -7
  14. package/dist/cjs/client.d.ts +8 -1
  15. package/dist/cjs/client.js +7 -0
  16. package/dist/cjs/connection-url.d.ts +82 -0
  17. package/dist/cjs/connection-url.js +187 -1
  18. package/dist/cjs/errors.d.ts +112 -12
  19. package/dist/cjs/errors.js +558 -114
  20. package/dist/cjs/generate.js +47 -15
  21. package/dist/cjs/index.d.ts +1 -1
  22. package/dist/cjs/introspect.d.ts +33 -0
  23. package/dist/cjs/introspect.js +54 -1
  24. package/dist/cjs/mssql.js +21 -1
  25. package/dist/cjs/nested-write.js +85 -14
  26. package/dist/cjs/pipeline-submittable.d.ts +2 -0
  27. package/dist/cjs/pipeline-submittable.js +88 -3
  28. package/dist/cjs/pipeline.js +13 -1
  29. package/dist/cjs/powdb-introspect.d.ts +5 -1
  30. package/dist/cjs/powdb-introspect.js +5 -1
  31. package/dist/cjs/powql.d.ts +14 -0
  32. package/dist/cjs/powql.js +44 -4
  33. package/dist/cjs/prisma-compat.js +95 -8
  34. package/dist/cjs/query/aggregates.js +56 -6
  35. package/dist/cjs/query/builder.d.ts +76 -13
  36. package/dist/cjs/query/builder.js +188 -58
  37. package/dist/cjs/query/compound-unique.d.ts +76 -0
  38. package/dist/cjs/query/compound-unique.js +129 -0
  39. package/dist/cjs/query/index.d.ts +1 -1
  40. package/dist/cjs/query/types.d.ts +65 -11
  41. package/dist/cjs/query/where.d.ts +85 -19
  42. package/dist/cjs/query/where.js +262 -47
  43. package/dist/cjs/query/writes.d.ts +11 -2
  44. package/dist/cjs/query/writes.js +116 -21
  45. package/dist/cjs/seed.d.ts +16 -0
  46. package/dist/cjs/seed.js +16 -0
  47. package/dist/cli/config.d.ts +7 -1
  48. package/dist/cli/config.js +11 -2
  49. package/dist/cli/destructive.d.ts +1 -1
  50. package/dist/cli/destructive.js +307 -9
  51. package/dist/cli/index.js +254 -44
  52. package/dist/cli/mcp.d.ts +23 -0
  53. package/dist/cli/mcp.js +187 -150
  54. package/dist/cli/migrate.d.ts +243 -3
  55. package/dist/cli/migrate.js +423 -45
  56. package/dist/cli/sql-statements.js +27 -0
  57. package/dist/cli/studio.d.ts +0 -1
  58. package/dist/cli/studio.js +10 -7
  59. package/dist/client.d.ts +8 -1
  60. package/dist/client.js +7 -0
  61. package/dist/connection-url.d.ts +82 -0
  62. package/dist/connection-url.js +183 -0
  63. package/dist/errors.d.ts +112 -12
  64. package/dist/errors.js +558 -114
  65. package/dist/generate.js +47 -15
  66. package/dist/index.d.ts +1 -1
  67. package/dist/introspect.d.ts +33 -0
  68. package/dist/introspect.js +53 -1
  69. package/dist/mssql.js +21 -1
  70. package/dist/nested-write.js +85 -14
  71. package/dist/pipeline-submittable.d.ts +2 -0
  72. package/dist/pipeline-submittable.js +87 -3
  73. package/dist/pipeline.js +14 -2
  74. package/dist/powdb-introspect.d.ts +5 -1
  75. package/dist/powdb-introspect.js +5 -1
  76. package/dist/powql.d.ts +14 -0
  77. package/dist/powql.js +45 -5
  78. package/dist/prisma-compat.js +96 -9
  79. package/dist/query/aggregates.js +56 -6
  80. package/dist/query/builder.d.ts +76 -13
  81. package/dist/query/builder.js +188 -58
  82. package/dist/query/compound-unique.d.ts +76 -0
  83. package/dist/query/compound-unique.js +126 -1
  84. package/dist/query/index.d.ts +1 -1
  85. package/dist/query/types.d.ts +65 -11
  86. package/dist/query/where.d.ts +85 -19
  87. package/dist/query/where.js +260 -47
  88. package/dist/query/writes.d.ts +11 -2
  89. package/dist/query/writes.js +117 -22
  90. package/dist/seed.d.ts +16 -0
  91. package/dist/seed.js +16 -0
  92. package/package.json +3 -3
  93. package/skills/turbine-orm/SKILL.md +37 -10
@@ -1172,9 +1172,11 @@ class QueryInterface {
1172
1172
  }
1173
1173
  /**
1174
1174
  * The field names a `cursor` actually seeks on (its own keys with a defined
1175
- * value), in the canonical sorted order the cursor conditions are built in.
1176
- * Empty for a missing cursor or one whose every value is `undefined` (which
1177
- * emits no seek condition at all, so it does not paginate).
1175
+ * value), in canonical sorted order. This is the diagnostic / implicit-
1176
+ * ordering view; the seek itself orders the fields by their `orderBy`
1177
+ * precedence (see {@link cursorSeekEntries}). Empty for a missing cursor or
1178
+ * one whose every value is `undefined` (which emits no seek condition at
1179
+ * all, so it does not paginate).
1178
1180
  */
1179
1181
  cursorFields(cursor) {
1180
1182
  if (!cursor || typeof cursor !== 'object')
@@ -1188,18 +1190,20 @@ class QueryInterface {
1188
1190
  * The ascending ordering implied by a `cursor`, or `undefined` when the shape
1189
1191
  * is too ambiguous to order safely.
1190
1192
  *
1191
- * A cursor seek emits `col > $n` per field (`<` when the orderBy says desc),
1192
- * so the ONLY ordering coherent with it is on the cursor's own field: ordering
1193
- * a seek on column X by column Y walks the table in an order the seek does not
1194
- * follow, which skips and repeats rows just as badly as no order at all. That
1195
- * is why this orders on the cursor field rather than blindly on the primary
1196
- * key when the two differ.
1193
+ * A cursor seek compares against the cursor's own field(s) (`col > $n`, `<`
1194
+ * when the orderBy says desc), so the ONLY ordering coherent with it is on
1195
+ * those fields: ordering a seek on column X by column Y walks the table in an
1196
+ * order the seek does not follow, which skips and repeats rows just as badly
1197
+ * as no order at all. That is why this orders on the cursor field rather than
1198
+ * blindly on the primary key when the two differ.
1197
1199
  *
1198
1200
  * Returns `undefined` (warn, inject nothing) for two shapes:
1199
- * - a MULTI-field cursor. `a > $1 AND b > $2` is a conjunction, not a proper
1200
- * composite keyset seek (`(a, b) > ($1, $2)`), so no single ORDER BY makes
1201
- * it correct. Injecting `(a asc, b asc)` would dress a broken seek up as a
1202
- * sound one.
1201
+ * - a MULTI-field cursor. The seek is a proper keyset predicate, but its
1202
+ * column PRECEDENCE comes from the `orderBy` ({@link cursorSeekEntries}),
1203
+ * and with no orderBy there is nothing to derive it from: the cursor
1204
+ * object's key order is canonicalized away, so `(a asc, b asc)` would be a
1205
+ * guess at which key the caller meant to lead. The caller must state the
1206
+ * order, and the unordered-page warning says so.
1203
1207
  * - a field that does not resolve to a real column. Column validation belongs
1204
1208
  * to the normal build path, which raises a precise error; synthesizing an
1205
1209
  * ORDER BY on it here would only change which error the caller sees.
@@ -1217,6 +1221,83 @@ class QueryInterface {
1217
1221
  }
1218
1222
  return { [field]: 'asc' };
1219
1223
  }
1224
+ /**
1225
+ * The fields a `cursor` seeks on, in KEYSET order with their seek direction.
1226
+ * THE single authority the cache fingerprint (`cur=`), the SQL build and the
1227
+ * cache-hit param collect all consume, so the three cannot disagree about
1228
+ * which value binds to which `$n`.
1229
+ *
1230
+ * Keyset order is the `orderBy` precedence: the predicate for
1231
+ * `orderBy: [{ viewCount }, { id }]` must test `view_count` first and `id`
1232
+ * only within equal `view_count`, whatever order the caller wrote the cursor
1233
+ * object in (its keys are canonicalized for the cache anyway). A cursor field
1234
+ * the orderBy does not name trails the named ones in sorted key order and
1235
+ * seeks ascending, the same default a single-field cursor with no orderBy has
1236
+ * always had.
1237
+ *
1238
+ * Directions are indexed by the RESOLVED COLUMN, never the caller's key: both
1239
+ * `cursor` and `orderBy` take either spelling, so a cursor written
1240
+ * `{ created_at }` against `orderBy: { createdAt: 'desc' }` used to miss the
1241
+ * lookup, default to ascending, and seek the wrong side of the page. The
1242
+ * `{ sort, nulls }` spec form is normalized for the same reason. A relation /
1243
+ * JSON-path / vector orderBy key resolves to no column and is skipped.
1244
+ *
1245
+ * An unknown cursor field throws the same E003 here that the build path
1246
+ * threw before this helper existed; the fingerprint simply meets it first.
1247
+ */
1248
+ cursorSeekEntries(cursor, orderBy) {
1249
+ const orderPos = new Map();
1250
+ const orderDesc = new Map();
1251
+ for (const [ok, od] of (0, filters_js_1.orderByEntries)(orderBy)) {
1252
+ const ocol = (0, utils_js_1.resolveColumnName)(this.tableMeta, ok);
1253
+ if (ocol === undefined)
1254
+ continue;
1255
+ if (!orderPos.has(ocol))
1256
+ orderPos.set(ocol, orderPos.size);
1257
+ // Last wins, matching object-key semantics (duplicates are refused
1258
+ // upstream anyway, so in practice there is exactly one).
1259
+ orderDesc.set(ocol, (0, filters_js_1.isOrderBySpec)(od) ? od.sort === 'desc' : od === 'desc');
1260
+ }
1261
+ const entries = (0, filters_js_1.sortedEntries)(cursor)
1262
+ .filter(([, v]) => v !== undefined)
1263
+ .map(([k, value]) => {
1264
+ const column = this.toColumn(k);
1265
+ return { column, value, desc: orderDesc.get(column) === true };
1266
+ });
1267
+ // Stable sort over the canonical (sorted-key) list: orderBy precedence
1268
+ // first, unnamed fields after it in their sorted order.
1269
+ return entries.sort((a, b) => (orderPos.get(a.column) ?? Number.MAX_SAFE_INTEGER) - (orderPos.get(b.column) ?? Number.MAX_SAFE_INTEGER));
1270
+ }
1271
+ /**
1272
+ * The keyset predicate for cursor terms in keyset order.
1273
+ *
1274
+ * One field is the plain `col > $1` (byte-identical to every single-field
1275
+ * cursor ever emitted). Two or more expand to
1276
+ * `(c1 > $1 OR (c1 = $1 AND c2 > $2) OR (c1 = $1 AND c2 = $2 AND c3 > $3))`,
1277
+ * with `<` for a `desc` field: the row-value form `(c1, c2) > ($1, $2)` is
1278
+ * shorter, but it cannot express a MIXED direction at all and SQL Server has
1279
+ * no row-value comparison, so the expanded form is the one path every engine
1280
+ * and every direction set share. Each value is bound ONCE and its `$n` is
1281
+ * referenced by every branch that needs it (legal on every supported
1282
+ * placeholder syntax), which is what keeps the param count equal to the field
1283
+ * count on both the build and the collect side.
1284
+ *
1285
+ * The former `c1 > $1 AND c2 > $2` was a conjunction, not a seek: it skipped
1286
+ * every row whose leading key EQUALED the cursor's, so a table with few
1287
+ * distinct leading values lost most of its rows across a page walk, silently.
1288
+ */
1289
+ keysetPredicate(terms) {
1290
+ const compare = (t) => `${t.column} ${t.desc ? '<' : '>'} ${t.placeholder}`;
1291
+ if (terms.length === 1)
1292
+ return compare(terms[0]);
1293
+ const branches = terms.map((term, i) => {
1294
+ if (i === 0)
1295
+ return compare(term);
1296
+ const prefix = terms.slice(0, i).map((t) => `${t.column} = ${t.placeholder}`);
1297
+ return `(${[...prefix, compare(term)].join(' AND ')})`;
1298
+ });
1299
+ return `(${branches.join(' OR ')})`;
1300
+ }
1220
1301
  /**
1221
1302
  * Whether a findMany paginates (`limit` / `take` / `offset` / `cursor`) but
1222
1303
  * declares no ordering, which makes the returned page NON-DETERMINISTIC:
@@ -1701,17 +1782,27 @@ class QueryInterface {
1701
1782
  // Query-level opt-in threaded onto every follow-up child `buildFindMany`,
1702
1783
  // so a batched load excludes/includes PII exactly as the join strategy.
1703
1784
  includePii,
1704
- tableGlobalFilter: (table, alias, precedingParams) => {
1785
+ // `ref` is the follow-up query's FROM-item AS THE SQL REFERS TO IT: the
1786
+ // quoted child table for a hasMany `_count`, the bare junction alias `t`
1787
+ // for the m2m one. Compiled through the rendered-reference scope, which
1788
+ // uses it verbatim for the column qualifier and for a nested relation
1789
+ // filter's correlation parent alike. It went through the BARE-alias scope
1790
+ // before, which quotes what it is given, so a `globalFilters` entry that
1791
+ // was itself a relation filter emitted `"""posts"""` into the EXISTS
1792
+ // body and the count failed 42P01. Reproduced live; the plain batched
1793
+ // relation fetch was never affected, because that one goes through the
1794
+ // child's own buildFindMany and merges the filter unqualified.
1795
+ tableGlobalFilter: (table, ref, precedingParams) => {
1705
1796
  const gf = this.resolveGlobalFilter(table, resolvedSkip);
1706
1797
  if (!gf)
1707
1798
  return null;
1708
1799
  const meta = this.schema.tables[table];
1709
1800
  if (!meta)
1710
1801
  return null;
1711
- // Seed the param array with `precedingParams` placeholders so
1712
- // buildAliasWhere numbers the gf params after the already-bound ones.
1802
+ // Seed the param array with `precedingParams` placeholders so the
1803
+ // builder numbers the gf params after the already-bound ones.
1713
1804
  const seeded = new Array(precedingParams).fill(undefined);
1714
- const clause = this.buildAliasWhere(table, meta, alias, gf, seeded);
1805
+ const clause = whereMod.buildRenderedRefWhere(this.ctx, table, meta, ref, gf, seeded);
1715
1806
  if (!clause)
1716
1807
  return null;
1717
1808
  return { clause, params: seeded.slice(precedingParams) };
@@ -2735,10 +2826,12 @@ class QueryInterface {
2735
2826
  })
2736
2827
  .join(',')
2737
2828
  : '';
2829
+ // Keyset order AND per-field direction: a multi-field cursor's predicate
2830
+ // is one OR-branch per field in orderBy precedence, and `<` / `>` are SQL
2831
+ // text, so `asc` and `desc` (or two precedences) can never share a template.
2738
2832
  const cursorFp = args?.cursor
2739
- ? Object.keys(args.cursor)
2740
- .filter((k) => args.cursor[k] !== undefined)
2741
- .sort()
2833
+ ? this.cursorSeekEntries(args.cursor, args.orderBy)
2834
+ .map((e) => `${e.column}:${e.desc ? 'd' : 'a'}`)
2742
2835
  .join(',')
2743
2836
  : '';
2744
2837
  // distinct must fingerprint in USER order: the SQL emits `DISTINCT ON` in
@@ -2848,39 +2941,16 @@ class QueryInterface {
2848
2941
  // where → cursor order (the collect path mirrors this exactly).
2849
2942
  let tail = freshWhereSql;
2850
2943
  if (args?.cursor) {
2851
- // Sorted (canonical) order, MUST match cursorFp and the cache-hit collect below.
2852
- const cursorEntries = (0, filters_js_1.sortedEntries)(args.cursor).filter(([, v]) => v !== undefined);
2853
- if (cursorEntries.length > 0) {
2854
- // Resolve the seek direction per cursor field from the flattened
2855
- // orderBy entries (last wins, matching object-key semantics), so both
2856
- // the object and array orderBy forms drive the cursor comparison.
2857
- //
2858
- // Indexed by the RESOLVED COLUMN, never the caller's key: both
2859
- // `cursor` and `orderBy` take either spelling, so a cursor written
2860
- // `{ created_at }` against `orderBy: { createdAt: 'desc' }` missed
2861
- // this lookup, defaulted to ascending, and emitted `created_at > $n`
2862
- // under `ORDER BY created_at DESC` — the wrong page, silently. Same
2863
- // failure the `{ sort, nulls }` normalization below prevents, reached
2864
- // through the spelling instead of the value shape. A relation /
2865
- // JSON-path / vector key resolves to no column and is skipped.
2866
- const orderDirByColumn = new Map();
2867
- for (const [ok, od] of (0, filters_js_1.orderByEntries)(args.orderBy)) {
2868
- const ocol = (0, utils_js_1.resolveColumnName)(this.tableMeta, ok);
2869
- if (ocol !== undefined)
2870
- orderDirByColumn.set(ocol, od);
2871
- }
2872
- const cursorConditions = cursorEntries.map(([k, v]) => {
2873
- const rawCol = this.toColumn(k);
2874
- const col = this.q(rawCol);
2875
- // orderBy values can be the { sort, nulls } spec form: normalize
2876
- // before comparing, or a desc spec would seek the ascending side.
2877
- const dir = orderDirByColumn.get(rawCol);
2878
- const desc = (0, filters_js_1.isOrderBySpec)(dir) ? dir.sort === 'desc' : dir === 'desc';
2879
- const op = desc ? '<' : '>';
2880
- freshParams.push(v);
2881
- return `${qt}.${col} ${op} ${this.p(freshParams.length)}`;
2944
+ // Keyset order (orderBy precedence), MUST match cursorFp and the
2945
+ // cache-hit collect below: both go through cursorSeekEntries.
2946
+ const seek = this.cursorSeekEntries(args.cursor, args.orderBy);
2947
+ if (seek.length > 0) {
2948
+ const terms = seek.map((e) => {
2949
+ freshParams.push(e.value);
2950
+ return { column: `${qt}.${this.q(e.column)}`, placeholder: this.p(freshParams.length), desc: e.desc };
2882
2951
  });
2883
- tail += freshWhereSql ? ` AND ${cursorConditions.join(' AND ')}` : ` WHERE ${cursorConditions.join(' AND ')}`;
2952
+ const predicate = this.keysetPredicate(terms);
2953
+ tail += freshWhereSql ? ` AND ${predicate}` : ` WHERE ${predicate}`;
2884
2954
  }
2885
2955
  }
2886
2956
  // ORDER BY is built AFTER the cursor pushes (param order
@@ -2899,10 +2969,40 @@ class QueryInterface {
2899
2969
  throw new errors_js_1.ValidationError('`distinct` cannot be combined with vector distance ordering.');
2900
2970
  }
2901
2971
  const userOrder = this.buildOrderBy(args.orderBy, freshParams);
2902
- const inner = `SELECT ${distinctPrefix}${selectClause} FROM ${qt}${tail} ORDER BY ${distinctCols
2972
+ // The outer level re-orders by the user's columns, which must therefore
2973
+ // exist in the derived table. A NARROWED projection (`select`, `omit`,
2974
+ // or the PII rule, which narrows with no user projection at all) left
2975
+ // them out, so the statement failed with 42703. Every order column the
2976
+ // projection omits is projected into the INNER query under its own
2977
+ // name, and the outer SELECT then lists the projected columns
2978
+ // explicitly instead of `*`, so the extra column orders the result and
2979
+ // never reaches a row (a PII column never even crosses the wire). The
2980
+ // full projection (`columnsList === null`, `"t".*`) and a projection
2981
+ // that already covers the order keeps the `SELECT *` wrapper byte for
2982
+ // byte. The user's order is rendered ONCE (params pushed once, as the
2983
+ // collect path mirrors) and its text reused at both levels.
2984
+ let innerSelect = selectClause;
2985
+ let outerSelect = '*';
2986
+ if (columnsList) {
2987
+ const projected = new Set(columnsList);
2988
+ const extras = [];
2989
+ for (const [key] of (0, filters_js_1.orderByEntries)(args.orderBy)) {
2990
+ // Relation and vector ordering are refused above, so every key
2991
+ // names a column of this table (a JSON-path entry's key is its
2992
+ // column); buildOrderBy has already validated it.
2993
+ const col = this.toColumn(key);
2994
+ if (!projected.has(col) && !extras.includes(col))
2995
+ extras.push(col);
2996
+ }
2997
+ if (extras.length > 0) {
2998
+ innerSelect = `${selectClause}, ${extras.map((c) => `${qt}.${this.q(c)}`).join(', ')}`;
2999
+ outerSelect = this.distinctOuterSelectList(columnsList, args.with).join(', ');
3000
+ }
3001
+ }
3002
+ const inner = `SELECT ${distinctPrefix}${innerSelect} FROM ${qt}${tail} ORDER BY ${distinctCols
2903
3003
  .map((c) => `${c} ASC`)
2904
3004
  .join(', ')}, ${userOrder}`;
2905
- sql = `SELECT * FROM (${inner}) AS ${this.q(`${this.table}_distinct`)} ORDER BY ${userOrder}`;
3005
+ sql = `SELECT ${outerSelect} FROM (${inner}) AS ${this.q(`${this.table}_distinct`)} ORDER BY ${userOrder}`;
2906
3006
  }
2907
3007
  else {
2908
3008
  // Pass freshParams so vector KNN ordering binds its `$n::vector` query
@@ -2937,11 +3037,11 @@ class QueryInterface {
2937
3037
  if (args?.with) {
2938
3038
  this.collectWithParams(args.with, params, undefined, flattenPlan);
2939
3039
  }
2940
- // 3. Cursor params, sorted (canonical) order, matching cursorFp and the build path.
3040
+ // 3. Cursor params, keyset order, matching cursorFp and the build path
3041
+ // (each value binds ONCE; the expanded predicate re-references its `$n`).
2941
3042
  if (args?.cursor) {
2942
- const cursorEntries = (0, filters_js_1.sortedEntries)(args.cursor).filter(([, v]) => v !== undefined);
2943
- for (const [, v] of cursorEntries) {
2944
- params.push(v);
3043
+ for (const e of this.cursorSeekEntries(args.cursor, args.orderBy)) {
3044
+ params.push(e.value);
2945
3045
  }
2946
3046
  }
2947
3047
  // 4. ORDER BY params (vector KNN ordering binds a `$n::vector` query vector).
@@ -3720,6 +3820,36 @@ class QueryInterface {
3720
3820
  toSqlColumn(field) {
3721
3821
  return this.q(this.toColumn(field));
3722
3822
  }
3823
+ /**
3824
+ * The explicit outer SELECT list of the `distinct` + `orderBy` wrapper, used
3825
+ * only when the inner derived table had to carry ORDER BY columns the
3826
+ * projection leaves out (see the distinct branch of {@link buildFindMany}):
3827
+ * the projected base columns, then one column per `with` relation, then one
3828
+ * per counted relation. These are the aliases `buildSelectWithRelations`
3829
+ * (relations.ts) gives the same columns, `AS "<relName>"` per entry in sorted
3830
+ * order and `AS "_count__<rel>"` per counted relation, over the SAME
3831
+ * `resolveCountRelations` it uses, so the two lists name the same columns.
3832
+ * `flatten` never reaches here (the strategy is gated off under `distinct`),
3833
+ * so no prefixed scalar aliases exist to enumerate. A drift between the two
3834
+ * would fail LOUDLY, as a 42703 on the outer list, never as a silently wrong
3835
+ * row, and the live distinct test pins the `with` + `_count` shape.
3836
+ */
3837
+ distinctOuterSelectList(columnsList, withClause) {
3838
+ const list = columnsList.map((c) => this.q(c));
3839
+ if (!withClause)
3840
+ return list;
3841
+ for (const [relName] of (0, filters_js_1.sortedEntries)(withClause)) {
3842
+ if (relName === '_count')
3843
+ continue;
3844
+ list.push(this.q(relName));
3845
+ }
3846
+ const countSpec = withClause._count;
3847
+ if (countSpec !== undefined) {
3848
+ for (const rel of (0, batched_loader_js_1.resolveCountRelations)(this.tableMeta, countSpec))
3849
+ list.push(this.q(`_count__${rel.name}`));
3850
+ }
3851
+ return list;
3852
+ }
3723
3853
  // =========================================================================
3724
3854
  // Fingerprinting, value-invariant shape keys for SQL cache lookup
3725
3855
  // =========================================================================
@@ -77,7 +77,70 @@ export declare function expandCompoundUniqueWhere(meta: TableMetadata, where: Re
77
77
  * person cannot take.
78
78
  */
79
79
  export declare function assertWhereIdentifiesOneRow(meta: TableMetadata, table: string, where: Record<string, unknown> | undefined): void;
80
+ /**
81
+ * The same refusal for the single-row WRITE methods, `update` and `delete`, and
82
+ * for `upsert`, whose `where` carries the same one-row contract by a different
83
+ * mechanism.
84
+ *
85
+ * `update` / `delete` emit `... WHERE <predicate> RETURNING *` and hand back
86
+ * `rows[0]`, so a `where` that matches many rows mutates EVERY one of them and
87
+ * reports one: the `findUnique` hazard (an arbitrary one of many) with a write
88
+ * attached. `upsert`'s `where` is not a predicate at all, it IS the conflict
89
+ * target, so a non-unique one has no single row to update on conflict and the
90
+ * emitted `ON CONFLICT (...)` names columns no unique index backs. Same rule,
91
+ * same sources of uniqueness, same null policy as
92
+ * {@link assertWhereIdentifiesOneRow}; only the sentence differs, because the
93
+ * consequence and the fix differ per operation. A reader who meant one row
94
+ * names a key; a reader who meant every matching row has `updateMany` /
95
+ * `deleteMany`, which report `{ count }` and never pretend to have touched one
96
+ * row.
97
+ *
98
+ * Called on the CALLER's where, before any global filter is merged in (a
99
+ * tenancy filter narrows, it does not identify). `update` / `delete` skip it
100
+ * under `allowFullTableScan: UNSAFE`, which already says "every row" in so many
101
+ * words, and their message names that hatch: a predicate CAN identify one row
102
+ * through a constraint this schema's metadata does not carry (a stale generated
103
+ * `metadata.ts`, a `defineSchema` that declares fewer uniques than the database
104
+ * has), and a rule with no way past it turns that into an unreachable method.
105
+ * `upsert` has no such option and its message therefore offers none.
106
+ */
107
+ export declare function assertMutationWhereIdentifiesOneRow(meta: TableMetadata, table: string, where: Record<string, unknown> | undefined, operation: 'update' | 'delete' | 'upsert'): void;
80
108
  export declare function uniqueKeyNames(meta: TableMetadata): string[][];
109
+ /**
110
+ * Brands a predicate Turbine wrote ENTIRELY itself to address the one row a
111
+ * DECLARED to-one relation points at, so the single-row write rule accepts it.
112
+ *
113
+ * A nested `disconnect: true` / `delete: true` on a `hasOne`, and a nested
114
+ * `update` on a `belongsTo` (whose argument is `{ data }`, with no `where` at
115
+ * all), give the caller no selector to write. The engine builds the predicate
116
+ * from the relation's own correlation key, and the thing that makes it one row
117
+ * is the relation's declared CARDINALITY, which is not in `primaryKey` /
118
+ * `uniqueColumns` / `indexes` and so is invisible to
119
+ * {@link whereIdentifiesOneRow}. Without this the rule refused those writes
120
+ * with "Name a unique key", advice the caller cannot take because they never
121
+ * wrote a `where`, on every schema whose `hasOne` FK is not ALSO declared
122
+ * unique: the `defineSchema` code-first path, the PowDB path, and any FK backed
123
+ * only by a partial unique index.
124
+ *
125
+ * Turbine believes a declared `hasOne` everywhere else (it reads the relation
126
+ * as an object rather than an array, and emits `LIMIT 1` for it), so believing
127
+ * it here is consistency, not a hole. The brand is applied at the one place the
128
+ * predicate is synthesized (`scopeWhereToParent` / the belongsTo correlation in
129
+ * nested-write.ts) and never to anything a caller supplied.
130
+ *
131
+ * A distinct Symbol rather than a second meaning for `markInternalCombinator`:
132
+ * that brand exists to keep a fixed-arity wrapper on a NAMED prepared
133
+ * statement, and folding "identifies one row" into it would mean any future
134
+ * fixed-arity wrapper silently switched this rule off. `Symbol.for` for the
135
+ * ESM/CJS cross-copy identity reason `INTERNAL_COMBINATOR` documents, and a
136
+ * SYMBOL so the `Object.keys` every where walker enumerates never sees it and
137
+ * no emitted SQL can change.
138
+ */
139
+ export declare const INTERNAL_ROW_SELECTOR: unique symbol;
140
+ /** Tag `where` as an engine-written single-row selector, and return it. */
141
+ export declare function markInternalRowSelector<T extends object>(where: T): T;
142
+ /** Did Turbine itself write this predicate to address one declared related row? */
143
+ export declare function isInternalRowSelector(where: unknown): boolean;
81
144
  /**
82
145
  * True when `where` pins every column of at least one unique key to a single
83
146
  * value, so the row it names is the row it gets.
@@ -91,5 +154,18 @@ export declare function uniqueKeyNames(meta: TableMetadata): string[][];
91
154
  * A NULL is not an identity. `WHERE email IS NULL` matches every row whose
92
155
  * email is null, which a UNIQUE constraint permits any number of, so a null
93
156
  * value satisfies no key here even on a unique column.
157
+ *
158
+ * The one combinator it does read through is Turbine's OWN: a nested write
159
+ * scopes a child selector to its parent by merging the parent correlation in,
160
+ * and when the two name the same column the merge is `{ AND: [selector,
161
+ * correlation] }` BRANDED via `markInternalCombinator` (query/utils.ts). That
162
+ * brand is a Symbol no request body can produce, the arity is fixed at two,
163
+ * and each branch only narrows the other, so the wrapper identifies a row
164
+ * exactly when one of its branches does. A caller-written `AND` is not read.
165
+ *
166
+ * The other thing it reads is {@link markInternalRowSelector}: a predicate the
167
+ * ENGINE wrote in full, for which there is no caller `where` this rule could be
168
+ * about. See that function for why the declaration, not the metadata, is the
169
+ * uniqueness source there.
94
170
  */
95
171
  export declare function whereIdentifiesOneRow(meta: TableMetadata, where: Record<string, unknown>): boolean;
@@ -40,9 +40,13 @@
40
40
  * {@link TableMetadata} in a module-level `WeakMap`.
41
41
  */
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.INTERNAL_ROW_SELECTOR = void 0;
43
44
  exports.expandCompoundUniqueWhere = expandCompoundUniqueWhere;
44
45
  exports.assertWhereIdentifiesOneRow = assertWhereIdentifiesOneRow;
46
+ exports.assertMutationWhereIdentifiesOneRow = assertMutationWhereIdentifiesOneRow;
45
47
  exports.uniqueKeyNames = uniqueKeyNames;
48
+ exports.markInternalRowSelector = markInternalRowSelector;
49
+ exports.isInternalRowSelector = isInternalRowSelector;
46
50
  exports.whereIdentifiesOneRow = whereIdentifiesOneRow;
47
51
  const errors_js_1 = require("../errors.js");
48
52
  const filters_js_1 = require("./filters.js");
@@ -245,6 +249,70 @@ function assertWhereIdentifiesOneRow(meta, table, where) {
245
249
  throw new errors_js_1.ValidationError(`findUnique on "${table}" refused: the \`where\` clause does not identify a single row, ` +
246
250
  `so this would return an arbitrary one of the rows that match. ${advice}`);
247
251
  }
252
+ /**
253
+ * The escape hatch sentence, on `update` / `delete` only.
254
+ *
255
+ * Stated with its cost rather than as an option to reach for: the same flag
256
+ * turns off the empty-`where` guard, so a predicate that is merely NARROW today
257
+ * and becomes EMPTY tomorrow (every value `undefined` on a request that omitted
258
+ * them) writes the whole table instead of being refused.
259
+ */
260
+ const UNSAFE_ESCAPE = 'If this predicate really does identify one row through a constraint this schema does not describe, ' +
261
+ '`allowFullTableScan: UNSAFE` writes it anyway, at the cost of the empty-`where` guard as well: an ' +
262
+ 'all-`undefined` `where` then matches every row instead of being refused.';
263
+ /**
264
+ * The same refusal for the single-row WRITE methods, `update` and `delete`, and
265
+ * for `upsert`, whose `where` carries the same one-row contract by a different
266
+ * mechanism.
267
+ *
268
+ * `update` / `delete` emit `... WHERE <predicate> RETURNING *` and hand back
269
+ * `rows[0]`, so a `where` that matches many rows mutates EVERY one of them and
270
+ * reports one: the `findUnique` hazard (an arbitrary one of many) with a write
271
+ * attached. `upsert`'s `where` is not a predicate at all, it IS the conflict
272
+ * target, so a non-unique one has no single row to update on conflict and the
273
+ * emitted `ON CONFLICT (...)` names columns no unique index backs. Same rule,
274
+ * same sources of uniqueness, same null policy as
275
+ * {@link assertWhereIdentifiesOneRow}; only the sentence differs, because the
276
+ * consequence and the fix differ per operation. A reader who meant one row
277
+ * names a key; a reader who meant every matching row has `updateMany` /
278
+ * `deleteMany`, which report `{ count }` and never pretend to have touched one
279
+ * row.
280
+ *
281
+ * Called on the CALLER's where, before any global filter is merged in (a
282
+ * tenancy filter narrows, it does not identify). `update` / `delete` skip it
283
+ * under `allowFullTableScan: UNSAFE`, which already says "every row" in so many
284
+ * words, and their message names that hatch: a predicate CAN identify one row
285
+ * through a constraint this schema's metadata does not carry (a stale generated
286
+ * `metadata.ts`, a `defineSchema` that declares fewer uniques than the database
287
+ * has), and a rule with no way past it turns that into an unreachable method.
288
+ * `upsert` has no such option and its message therefore offers none.
289
+ */
290
+ function assertMutationWhereIdentifiesOneRow(meta, table, where, operation) {
291
+ if (whereIdentifiesOneRow(meta, where ?? {}))
292
+ return;
293
+ const keys = describeUniqueKeys(meta);
294
+ if (operation === 'upsert') {
295
+ const advice = keys.length > 0
296
+ ? `Name a unique key (${keys.join(', ')}).`
297
+ : `Table "${table}" declares no primary key and no unique constraint, so no \`where\` can identify one row ` +
298
+ 'here and no upsert is possible on it: insert with `create`, or change matching rows with `updateMany`.';
299
+ throw new errors_js_1.ValidationError(`upsert on "${table}" refused: the \`where\` clause does not identify a single row. An upsert's \`where\` IS ` +
300
+ 'its conflict target, so there is no one row to update on conflict, and the `ON CONFLICT` this would emit ' +
301
+ `names columns no unique constraint backs. ${advice}`);
302
+ }
303
+ const many = operation === 'update' ? 'updateMany' : 'deleteMany';
304
+ const advice = keys.length > 0
305
+ ? `Name a unique key (${keys.join(', ')}), or use \`${many}\` if you meant "every row matching a filter".`
306
+ : `Table "${table}" declares no primary key and no unique constraint, so no \`where\` can identify one row ` +
307
+ `here. Use \`${many}\`, which reports how many rows it touched.`;
308
+ throw new errors_js_1.ValidationError(`${operation} on "${table}" refused: the \`where\` clause does not identify a single row, ` +
309
+ `so this would ${operation} every row that matches and return only one of them. ${advice} ${UNSAFE_ESCAPE}`);
310
+ }
311
+ /** Each unique key of `meta` rendered in field spelling, for an error message. */
312
+ function describeUniqueKeys(meta) {
313
+ const field = (c) => meta.reverseColumnMap[c] ?? c;
314
+ return uniqueKeyNames(meta).map((cols) => cols.length === 1 ? `\`${field(cols[0])}\`` : `\`{ ${cols.map(field).join(', ')} }\``);
315
+ }
248
316
  function uniqueKeyNames(meta) {
249
317
  return dedupeColumnSets(uniqueColumnSets(meta));
250
318
  }
@@ -274,6 +342,49 @@ function uniqueColumnSets(meta) {
274
342
  }
275
343
  return sets;
276
344
  }
345
+ // ---------------------------------------------------------------------------
346
+ // Engine-addressed row selectors
347
+ // ---------------------------------------------------------------------------
348
+ /**
349
+ * Brands a predicate Turbine wrote ENTIRELY itself to address the one row a
350
+ * DECLARED to-one relation points at, so the single-row write rule accepts it.
351
+ *
352
+ * A nested `disconnect: true` / `delete: true` on a `hasOne`, and a nested
353
+ * `update` on a `belongsTo` (whose argument is `{ data }`, with no `where` at
354
+ * all), give the caller no selector to write. The engine builds the predicate
355
+ * from the relation's own correlation key, and the thing that makes it one row
356
+ * is the relation's declared CARDINALITY, which is not in `primaryKey` /
357
+ * `uniqueColumns` / `indexes` and so is invisible to
358
+ * {@link whereIdentifiesOneRow}. Without this the rule refused those writes
359
+ * with "Name a unique key", advice the caller cannot take because they never
360
+ * wrote a `where`, on every schema whose `hasOne` FK is not ALSO declared
361
+ * unique: the `defineSchema` code-first path, the PowDB path, and any FK backed
362
+ * only by a partial unique index.
363
+ *
364
+ * Turbine believes a declared `hasOne` everywhere else (it reads the relation
365
+ * as an object rather than an array, and emits `LIMIT 1` for it), so believing
366
+ * it here is consistency, not a hole. The brand is applied at the one place the
367
+ * predicate is synthesized (`scopeWhereToParent` / the belongsTo correlation in
368
+ * nested-write.ts) and never to anything a caller supplied.
369
+ *
370
+ * A distinct Symbol rather than a second meaning for `markInternalCombinator`:
371
+ * that brand exists to keep a fixed-arity wrapper on a NAMED prepared
372
+ * statement, and folding "identifies one row" into it would mean any future
373
+ * fixed-arity wrapper silently switched this rule off. `Symbol.for` for the
374
+ * ESM/CJS cross-copy identity reason `INTERNAL_COMBINATOR` documents, and a
375
+ * SYMBOL so the `Object.keys` every where walker enumerates never sees it and
376
+ * no emitted SQL can change.
377
+ */
378
+ exports.INTERNAL_ROW_SELECTOR = Symbol.for('turbine.internalRowSelector');
379
+ /** Tag `where` as an engine-written single-row selector, and return it. */
380
+ function markInternalRowSelector(where) {
381
+ Object.defineProperty(where, exports.INTERNAL_ROW_SELECTOR, { value: true, enumerable: false, configurable: true });
382
+ return where;
383
+ }
384
+ /** Did Turbine itself write this predicate to address one declared related row? */
385
+ function isInternalRowSelector(where) {
386
+ return (typeof where === 'object' && where !== null && where[exports.INTERNAL_ROW_SELECTOR] === true);
387
+ }
277
388
  /**
278
389
  * True when `where` pins every column of at least one unique key to a single
279
390
  * value, so the row it names is the row it gets.
@@ -287,8 +398,26 @@ function uniqueColumnSets(meta) {
287
398
  * A NULL is not an identity. `WHERE email IS NULL` matches every row whose
288
399
  * email is null, which a UNIQUE constraint permits any number of, so a null
289
400
  * value satisfies no key here even on a unique column.
401
+ *
402
+ * The one combinator it does read through is Turbine's OWN: a nested write
403
+ * scopes a child selector to its parent by merging the parent correlation in,
404
+ * and when the two name the same column the merge is `{ AND: [selector,
405
+ * correlation] }` BRANDED via `markInternalCombinator` (query/utils.ts). That
406
+ * brand is a Symbol no request body can produce, the arity is fixed at two,
407
+ * and each branch only narrows the other, so the wrapper identifies a row
408
+ * exactly when one of its branches does. A caller-written `AND` is not read.
409
+ *
410
+ * The other thing it reads is {@link markInternalRowSelector}: a predicate the
411
+ * ENGINE wrote in full, for which there is no caller `where` this rule could be
412
+ * about. See that function for why the declaration, not the metadata, is the
413
+ * uniqueness source there.
290
414
  */
291
415
  function whereIdentifiesOneRow(meta, where) {
416
+ if (isInternalRowSelector(where))
417
+ return true;
418
+ if ((0, utils_js_1.isInternalCombinator)(where) && Array.isArray(where.AND)) {
419
+ return where.AND.some((branch) => typeof branch === 'object' && branch !== null && whereIdentifiesOneRow(meta, branch));
420
+ }
292
421
  const pinned = new Set();
293
422
  for (const [key, value] of Object.entries(where)) {
294
423
  if (!isPinnedToOneValue(value))
@@ -5,7 +5,7 @@
5
5
  * `import { … } from './query/index.js'` is a drop-in replacement for the
6
6
  * former monolithic `import { … } from './query.js'`.
7
7
  */
8
- export type { AggregateArgs, AggregateResult, ArrayFilter, ColumnRef, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByAggregateSpec, GroupByArgs, GroupByDistinctOn, GroupByResult, HavingClause, JsonEncoding, JsonFilter, JsonPathAggregateTarget, JsonPathGroupKey, JsonPathOrderBy, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderByObject, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, RelationPickBy, RelationPickOrderBy, ResolvedSkipGlobalFilters, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithOrderByObject, WithResult, } from './types.js';
8
+ export type { AggregateArgs, AggregateResult, ArrayFilter, ColumnRef, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByAggregateSpec, GroupByArgs, GroupByDistinctOn, GroupByResult, HavingClause, JsonEncoding, JsonFilter, JsonPathAggregateTarget, JsonPathGroupKey, JsonPathOrderBy, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderByObject, OrderBySpec, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, RelationOrderBy, RelationOrderByChain, RelationPickBy, RelationPickOrderBy, ResolvedSkipGlobalFilters, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithOrderByObject, WithResult, } from './types.js';
9
9
  export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, InsertStatementInput, UpsertStatementInput, } from '../dialect.js';
10
10
  export { postgresDialect } from '../dialect.js';
11
11
  export type { OptionKind, OptionTable } from './option-surface.js';