turbine-orm 0.65.0 → 0.66.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 (142) hide show
  1. package/README.md +34 -32
  2. package/dist/adapters/cockroachdb.js +21 -3
  3. package/dist/adapters/index.d.ts +15 -0
  4. package/dist/adapters/yugabytedb.js +20 -3
  5. package/dist/cjs/adapters/cockroachdb.js +21 -3
  6. package/dist/cjs/adapters/index.d.ts +15 -0
  7. package/dist/cjs/adapters/yugabytedb.js +20 -3
  8. package/dist/cjs/cli/destructive.d.ts +18 -4
  9. package/dist/cjs/cli/destructive.js +230 -122
  10. package/dist/cjs/cli/index.d.ts +21 -4
  11. package/dist/cjs/cli/index.js +119 -22
  12. package/dist/cjs/cli/mcp.d.ts +28 -8
  13. package/dist/cjs/cli/mcp.js +170 -127
  14. package/dist/cjs/cli/migrate.d.ts +134 -13
  15. package/dist/cjs/cli/migrate.js +349 -241
  16. package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
  17. package/dist/cjs/cli/pii-predicate-guard.js +390 -0
  18. package/dist/cjs/cli/prisma-resolve.js +75 -4
  19. package/dist/cjs/cli/prisma-schema.d.ts +17 -1
  20. package/dist/cjs/cli/prisma-schema.js +83 -17
  21. package/dist/cjs/cli/sql-statements.d.ts +125 -0
  22. package/dist/cjs/cli/sql-statements.js +378 -0
  23. package/dist/cjs/cli/studio.js +49 -118
  24. package/dist/cjs/cli/ui.d.ts +1 -1
  25. package/dist/cjs/client.d.ts +43 -0
  26. package/dist/cjs/client.js +125 -6
  27. package/dist/cjs/dialect.d.ts +123 -0
  28. package/dist/cjs/dialect.js +33 -0
  29. package/dist/cjs/errors.d.ts +74 -1
  30. package/dist/cjs/errors.js +239 -25
  31. package/dist/cjs/index-advisor.d.ts +33 -1
  32. package/dist/cjs/index-advisor.js +32 -1
  33. package/dist/cjs/introspect.d.ts +48 -0
  34. package/dist/cjs/introspect.js +222 -91
  35. package/dist/cjs/mssql.js +43 -1
  36. package/dist/cjs/mysql.d.ts +5 -2
  37. package/dist/cjs/mysql.js +202 -17
  38. package/dist/cjs/nested-write.js +6 -1
  39. package/dist/cjs/pipeline-submittable.js +17 -3
  40. package/dist/cjs/pipeline.js +75 -9
  41. package/dist/cjs/powdb.d.ts +23 -0
  42. package/dist/cjs/powdb.js +33 -1
  43. package/dist/cjs/powql.d.ts +61 -9
  44. package/dist/cjs/powql.js +186 -49
  45. package/dist/cjs/prisma-compat.js +160 -41
  46. package/dist/cjs/query/aggregates.d.ts +1 -1
  47. package/dist/cjs/query/aggregates.js +80 -18
  48. package/dist/cjs/query/batched-loader.d.ts +10 -0
  49. package/dist/cjs/query/batched-loader.js +268 -7
  50. package/dist/cjs/query/builder.d.ts +73 -0
  51. package/dist/cjs/query/builder.js +225 -28
  52. package/dist/cjs/query/filters.d.ts +162 -0
  53. package/dist/cjs/query/filters.js +250 -1
  54. package/dist/cjs/query/relations.d.ts +10 -10
  55. package/dist/cjs/query/relations.js +93 -12
  56. package/dist/cjs/query/types.d.ts +14 -1
  57. package/dist/cjs/query/utils.d.ts +146 -2
  58. package/dist/cjs/query/utils.js +210 -4
  59. package/dist/cjs/query/warn-registry.d.ts +10 -0
  60. package/dist/cjs/query/warn-registry.js +10 -0
  61. package/dist/cjs/query/where-compile.d.ts +30 -0
  62. package/dist/cjs/query/where-compile.js +41 -0
  63. package/dist/cjs/query/where.d.ts +128 -13
  64. package/dist/cjs/query/where.js +215 -77
  65. package/dist/cjs/query/writes.d.ts +1 -1
  66. package/dist/cjs/query/writes.js +39 -15
  67. package/dist/cjs/schema-builder.d.ts +2 -1
  68. package/dist/cjs/schema-sql.d.ts +94 -4
  69. package/dist/cjs/schema-sql.js +506 -30
  70. package/dist/cjs/schema.d.ts +3 -1
  71. package/dist/cjs/sqlite.d.ts +6 -0
  72. package/dist/cjs/sqlite.js +151 -10
  73. package/dist/cjs/typed-sql.d.ts +29 -1
  74. package/dist/cjs/typed-sql.js +30 -12
  75. package/dist/cli/destructive.d.ts +18 -4
  76. package/dist/cli/destructive.js +229 -121
  77. package/dist/cli/index.d.ts +21 -4
  78. package/dist/cli/index.js +120 -24
  79. package/dist/cli/mcp.d.ts +28 -8
  80. package/dist/cli/mcp.js +172 -129
  81. package/dist/cli/migrate.d.ts +134 -13
  82. package/dist/cli/migrate.js +347 -238
  83. package/dist/cli/pii-predicate-guard.d.ts +112 -0
  84. package/dist/cli/pii-predicate-guard.js +386 -0
  85. package/dist/cli/prisma-resolve.js +75 -4
  86. package/dist/cli/prisma-schema.d.ts +17 -1
  87. package/dist/cli/prisma-schema.js +83 -17
  88. package/dist/cli/sql-statements.d.ts +125 -0
  89. package/dist/cli/sql-statements.js +373 -0
  90. package/dist/cli/studio.js +49 -118
  91. package/dist/cli/ui.d.ts +1 -1
  92. package/dist/client.d.ts +43 -0
  93. package/dist/client.js +126 -7
  94. package/dist/dialect.d.ts +123 -0
  95. package/dist/dialect.js +33 -0
  96. package/dist/errors.d.ts +74 -1
  97. package/dist/errors.js +228 -19
  98. package/dist/index-advisor.d.ts +33 -1
  99. package/dist/index-advisor.js +31 -1
  100. package/dist/introspect.d.ts +48 -0
  101. package/dist/introspect.js +221 -91
  102. package/dist/mssql.js +44 -2
  103. package/dist/mysql.d.ts +5 -2
  104. package/dist/mysql.js +203 -18
  105. package/dist/nested-write.js +7 -2
  106. package/dist/pipeline-submittable.js +18 -4
  107. package/dist/pipeline.js +76 -10
  108. package/dist/powdb.d.ts +23 -0
  109. package/dist/powdb.js +33 -2
  110. package/dist/powql.d.ts +61 -9
  111. package/dist/powql.js +187 -50
  112. package/dist/prisma-compat.js +160 -41
  113. package/dist/query/aggregates.d.ts +1 -1
  114. package/dist/query/aggregates.js +82 -20
  115. package/dist/query/batched-loader.d.ts +10 -0
  116. package/dist/query/batched-loader.js +270 -9
  117. package/dist/query/builder.d.ts +73 -0
  118. package/dist/query/builder.js +226 -30
  119. package/dist/query/filters.d.ts +162 -0
  120. package/dist/query/filters.js +246 -1
  121. package/dist/query/relations.d.ts +10 -10
  122. package/dist/query/relations.js +94 -14
  123. package/dist/query/types.d.ts +14 -1
  124. package/dist/query/utils.d.ts +146 -2
  125. package/dist/query/utils.js +204 -3
  126. package/dist/query/warn-registry.d.ts +10 -0
  127. package/dist/query/warn-registry.js +10 -0
  128. package/dist/query/where-compile.d.ts +30 -0
  129. package/dist/query/where-compile.js +40 -1
  130. package/dist/query/where.d.ts +128 -13
  131. package/dist/query/where.js +216 -80
  132. package/dist/query/writes.d.ts +1 -1
  133. package/dist/query/writes.js +40 -16
  134. package/dist/schema-builder.d.ts +2 -1
  135. package/dist/schema-sql.d.ts +94 -4
  136. package/dist/schema-sql.js +505 -30
  137. package/dist/schema.d.ts +3 -1
  138. package/dist/sqlite.d.ts +6 -0
  139. package/dist/sqlite.js +151 -10
  140. package/dist/typed-sql.d.ts +29 -1
  141. package/dist/typed-sql.js +30 -12
  142. package/package.json +6 -4
@@ -28,6 +28,12 @@ export interface ColumnNameSource {
28
28
  columnMap: Record<string, string>;
29
29
  reverseColumnMap?: Record<string, string>;
30
30
  allColumns?: string[];
31
+ /**
32
+ * The table's own name, present whenever the source is a real
33
+ * `TableMetadata`. Used for ERROR TEXT only, never for key resolution, so a
34
+ * hand-built source that omits it still resolves identically.
35
+ */
36
+ name?: string;
31
37
  }
32
38
  /**
33
39
  * Resolve a user-supplied key to its unquoted column name, or `undefined` when
@@ -53,6 +59,105 @@ export interface ColumnNameSource {
53
59
  * name (see {@link ownLookup}).
54
60
  */
55
61
  export declare function resolveColumnName(meta: ColumnNameSource, key: string): string | undefined;
62
+ /**
63
+ * THE canonical order for a caller-supplied set of columns: the table's own
64
+ * `allColumns` order, which is the order `omit` and the default projection
65
+ * already produce.
66
+ *
67
+ * ## The failure mode this exists to close, and it is NOT arity
68
+ *
69
+ * Every distinct SQL text Turbine emits is parsed on the server as a NAMED
70
+ * prepared statement and is never DEALLOCATEd. The arity rule
71
+ * (`markVariableArity`) bounds the shapes whose LENGTH the caller picks. It
72
+ * does nothing about the shapes whose length is fixed and whose ORDER the
73
+ * caller picks, and those grow the SQL text just as freely: `select: { a, b }`
74
+ * and `select: { b, a }` are the same query and two different statements.
75
+ *
76
+ * Measured against PostgreSQL 16 on a SEVEN-column table, one connection, with
77
+ * the arity fix already landed:
78
+ *
79
+ * baseline prepared= 0 CachedPlanSource= 0.0 MB
80
+ * after 5040 `select` permutations prepared= 5040 CachedPlanSource=39.4 MB
81
+ * after 300 varying-arity ORs prepared= 5040 CachedPlanSource=39.4 MB
82
+ * after 5040 `distinct` permutations prepared=10080 CachedPlanSource=59.1 MB
83
+ * 720 reordered PATCH bodies (update) prepared= 720 CachedPlanSource= 2.8 MB
84
+ *
85
+ * The write row is the realistic one: `JSON.parse` preserves insertion order,
86
+ * so `update({ where, data: JSON.parse(reqBody) })` hands a request body the
87
+ * SET-clause column order for free. No arrays, no unusual input, no opt-in.
88
+ *
89
+ * The reachable space is ORDERED subsets, sum over k of k! * C(n,k): 9.9e6 for
90
+ * 10 columns, 6.6e18 for 20. Canonicalizing collapses each permutation class to
91
+ * one statement, leaving the UNORDERED subsets (2^n) that `omit` has always
92
+ * had.
93
+ *
94
+ * ## Why ordering is the right remedy here rather than withholding the name
95
+ *
96
+ * A projection's SELECT-list order is not semantically meaningful to Turbine:
97
+ * rows are assembled by NAME (`parseRow`, `jsonScalarPairs` and
98
+ * `buildRelationShape` all key off the same resolved list), so reordering it
99
+ * changes no value. Where an order IS meaningful (`orderBy`, and `DISTINCT ON`,
100
+ * whose list is re-emitted as an ORDER BY prefix), the statement is sent
101
+ * unnamed instead. See `MAX_NAMED_ORDER_KEYS` in filters.ts and the `distinct`
102
+ * mark in builder.ts.
103
+ *
104
+ * Duplicates collapse, which is the other half of "canonical": two spellings of
105
+ * one column (`userId` and `user_id`, both legal on an introspected schema)
106
+ * used to emit that column twice in the SELECT list.
107
+ *
108
+ * Cost is one Set of the projected columns plus one pass over `allColumns`, on
109
+ * a path that already walks the caller's keys.
110
+ *
111
+ * @param meta the table the columns belong to; without `allColumns` there is no
112
+ * canonical order to appeal to and the input is returned untouched.
113
+ * @param columns already-resolved snake_case column names.
114
+ */
115
+ export declare function canonicalColumnOrder(meta: ColumnNameSource, columns: string[]): string[];
116
+ /**
117
+ * {@link canonicalColumnOrder} for a write's `data` entries: same order, same
118
+ * reason, two deliberate differences.
119
+ *
120
+ * DUPLICATES ARE KEPT. Two keys resolving to one column (`{ userId, user_id }`)
121
+ * is a caller error that PostgreSQL reports precisely ("multiple assignments to
122
+ * same column", 42701, and the INSERT equivalent). Collapsing them here would
123
+ * turn that error into a silent write of whichever value survived, so the entry
124
+ * list is REORDERED and never shortened. A stable sort keeps such a pair
125
+ * adjacent and in the caller's relative order, so the engine still sees, and
126
+ * still rejects, both assignments.
127
+ *
128
+ * THE KEY SPELLING IS PRESERVED. Everything downstream (`coerceWriteValue`,
129
+ * `buildSetClause`, `toSqlColumn`) re-resolves the caller's key itself, so only
130
+ * the ORDER of the entries changes here, never their content.
131
+ *
132
+ * A key that resolves to no column sorts LAST, in the caller's relative order.
133
+ * It is about to raise E003 from the SQL builder either way, and the builder is
134
+ * where that error belongs; ordering must not pre-empt it with a worse one.
135
+ */
136
+ export declare function canonicalWriteEntries(meta: ColumnNameSource, entries: [string, unknown][]): [string, unknown][];
137
+ /**
138
+ * Brands a WHERE object whose `AND` / `OR` array Turbine ITSELF synthesized,
139
+ * rather than one the caller wrote.
140
+ *
141
+ * The distinction matters for exactly one rule: a caller-written combinator
142
+ * array is treated as a VARIABLE-ARITY shape and its statement is sent unnamed
143
+ * (see `markVariableArity` in where.ts, and `acquireSql` in builder.ts).
144
+ * Turbine's own wrappers, the `{ AND: [userWhere, globalFilter] }` the
145
+ * global-filter merge produces and the `{ AND: [where, correlation] }` the
146
+ * batched loader produces, have a FIXED arity of two, decided by Turbine, not
147
+ * reachable from a request body. Counting them would have taken every query on
148
+ * a table with a configured global filter off named prepared statements, i.e.
149
+ * penalised precisely the multi-tenant setups the rule exists to protect.
150
+ *
151
+ * A `Symbol.for` key for the same cross-copy-identity reason the warn registry
152
+ * uses one (an ESM and a CJS copy of this module in one process must agree),
153
+ * and a SYMBOL rather than a string key so `Object.keys` (which is what every
154
+ * where walker enumerates) never sees it and no emitted SQL can change.
155
+ */
156
+ export declare const INTERNAL_COMBINATOR: unique symbol;
157
+ /** Tag `where` as carrying a Turbine-synthesized combinator, and return it. */
158
+ export declare function markInternalCombinator<T extends object>(where: T): T;
159
+ /** Was this WHERE object's combinator synthesized by Turbine? */
160
+ export declare function isInternalCombinator(where: unknown): boolean;
56
161
  /**
57
162
  * Escape single quotes for use as string keys in json_build_object().
58
163
  * Doubles single quotes per SQL quoting rules.
@@ -65,8 +170,15 @@ export declare function escSingleQuote(s: string): string;
65
170
  export declare function escapeLike(value: string): string;
66
171
  /**
67
172
  * Simple LRU (Least Recently Used) cache with a fixed maximum size.
68
- * When the cache exceeds maxSize, the oldest (least recently used) entry is evicted.
69
- * Uses Map insertion order for O(1) eviction.
173
+ * When the cache exceeds maxSize, the oldest (least recently used) entry is
174
+ * evicted. Uses Map insertion order, so EVICTION is O(1).
175
+ *
176
+ * The access-order reorder in {@link get} is not: `Map.delete` + `Map.set`
177
+ * leaves a tombstone, and V8 rehashes the whole table once live plus deleted
178
+ * entries reach capacity, so the reorder amortizes to O(capacity) per hit.
179
+ * At the 1,000-entry default that measured 1,356 ns against 2.6 ns for a plain
180
+ * `Map.get`, on the hottest lookup in the SQL build. See {@link get} for why
181
+ * skipping it below capacity is not merely an optimization but exact.
70
182
  */
71
183
  export declare class LRUCache<K, V> {
72
184
  private maxSize;
@@ -79,6 +191,16 @@ export declare class LRUCache<K, V> {
79
191
  /** Cached SQL template paired with its prepared-statement name. */
80
192
  export interface SqlCacheEntry {
81
193
  sql: string;
194
+ /**
195
+ * The name this statement executes under, or `''` for "send it UNNAMED".
196
+ *
197
+ * The empty string is a real value here, not a missing one: a shape whose SQL
198
+ * text is a function of a caller-chosen ARITY (an `OR`/`AND` array) must never
199
+ * take a server-side prepared statement, because those are never DEALLOCATEd
200
+ * and the client's LRU bounds only the client. Storing the verdict on the
201
+ * ENTRY is what makes it survive the cache: a later HIT reuses this name, so
202
+ * a warmed template cannot regain one. See `buildCacheEntry` in builder.ts.
203
+ */
82
204
  name: string;
83
205
  }
84
206
  /**
@@ -467,6 +589,28 @@ export declare function unknownFieldMessage(table: string, field: string, meta:
467
589
  * it. Naming the fix costs one sentence and saves a search.
468
590
  */
469
591
  export declare function relationInProjectionMessage(table: string, field: string, clause: 'select' | 'omit'): string;
592
+ /**
593
+ * Dev-only advisory for a sort/grouping term Turbine dropped as redundant
594
+ * (`filters.ts` `dedupeOrderEntries` / `dedupeColumnList`).
595
+ *
596
+ * Deliberately a WARNING and not an error, and deliberately not silent either.
597
+ * The dropped term provably cannot change a result, so failing the query would
598
+ * be a false alarm on a shape correct code produces (a caller-chosen sort key
599
+ * plus an unconditional primary-key tiebreak, which collide exactly when the
600
+ * caller sorts by the primary key). But a caller who wrote the duplicate by
601
+ * hand, or who believes a second key is doing something, should hear about it
602
+ * once. Dev-only for the usual reason: it describes the QUERY the application
603
+ * sends, which does not vary with the environment, so a production process
604
+ * learns nothing from repeating it.
605
+ *
606
+ * Keyed per dropped key, so a sort assembled per request from the same UI says
607
+ * it once rather than once per request.
608
+ */
609
+ export declare function warnRedundantSortTerm(table: string, clause: string, dropped: readonly {
610
+ key: string;
611
+ first: string;
612
+ resolved: string;
613
+ }[]): void;
470
614
  /**
471
615
  * The two projection SHAPE refusals (0.65), shared by the SQL engines' single
472
616
  * resolver, the batched loader's raw-arg check, and PowDB, so every path that
@@ -8,10 +8,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
8
8
  return (mod && mod.__esModule) ? mod : { "default": mod };
9
9
  };
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.JSON_WIRE_COERCION_OIDS = exports.OPERATOR_KEYS = exports.LRUCache = void 0;
11
+ exports.JSON_WIRE_COERCION_OIDS = exports.OPERATOR_KEYS = exports.LRUCache = exports.INTERNAL_COMBINATOR = void 0;
12
12
  exports.quoteIdent = quoteIdent;
13
13
  exports.ownLookup = ownLookup;
14
14
  exports.resolveColumnName = resolveColumnName;
15
+ exports.canonicalColumnOrder = canonicalColumnOrder;
16
+ exports.canonicalWriteEntries = canonicalWriteEntries;
17
+ exports.markInternalCombinator = markInternalCombinator;
18
+ exports.isInternalCombinator = isInternalCombinator;
15
19
  exports.escSingleQuote = escSingleQuote;
16
20
  exports.escapeLike = escapeLike;
17
21
  exports.fnv1a64Hex = fnv1a64Hex;
@@ -37,6 +41,7 @@ exports.closestName = closestName;
37
41
  exports.suggestKey = suggestKey;
38
42
  exports.unknownFieldMessage = unknownFieldMessage;
39
43
  exports.relationInProjectionMessage = relationInProjectionMessage;
44
+ exports.warnRedundantSortTerm = warnRedundantSortTerm;
40
45
  exports.selectNamesNothingMessage = selectNamesNothingMessage;
41
46
  exports.selectOmitExclusiveMessage = selectOmitExclusiveMessage;
42
47
  const pg_1 = __importDefault(require("pg"));
@@ -103,6 +108,157 @@ function resolveColumnName(meta, key) {
103
108
  return snake;
104
109
  return undefined;
105
110
  }
111
+ // ---------------------------------------------------------------------------
112
+ // Caller-controlled key ORDER, canonicalized
113
+ // ---------------------------------------------------------------------------
114
+ /**
115
+ * THE canonical order for a caller-supplied set of columns: the table's own
116
+ * `allColumns` order, which is the order `omit` and the default projection
117
+ * already produce.
118
+ *
119
+ * ## The failure mode this exists to close, and it is NOT arity
120
+ *
121
+ * Every distinct SQL text Turbine emits is parsed on the server as a NAMED
122
+ * prepared statement and is never DEALLOCATEd. The arity rule
123
+ * (`markVariableArity`) bounds the shapes whose LENGTH the caller picks. It
124
+ * does nothing about the shapes whose length is fixed and whose ORDER the
125
+ * caller picks, and those grow the SQL text just as freely: `select: { a, b }`
126
+ * and `select: { b, a }` are the same query and two different statements.
127
+ *
128
+ * Measured against PostgreSQL 16 on a SEVEN-column table, one connection, with
129
+ * the arity fix already landed:
130
+ *
131
+ * baseline prepared= 0 CachedPlanSource= 0.0 MB
132
+ * after 5040 `select` permutations prepared= 5040 CachedPlanSource=39.4 MB
133
+ * after 300 varying-arity ORs prepared= 5040 CachedPlanSource=39.4 MB
134
+ * after 5040 `distinct` permutations prepared=10080 CachedPlanSource=59.1 MB
135
+ * 720 reordered PATCH bodies (update) prepared= 720 CachedPlanSource= 2.8 MB
136
+ *
137
+ * The write row is the realistic one: `JSON.parse` preserves insertion order,
138
+ * so `update({ where, data: JSON.parse(reqBody) })` hands a request body the
139
+ * SET-clause column order for free. No arrays, no unusual input, no opt-in.
140
+ *
141
+ * The reachable space is ORDERED subsets, sum over k of k! * C(n,k): 9.9e6 for
142
+ * 10 columns, 6.6e18 for 20. Canonicalizing collapses each permutation class to
143
+ * one statement, leaving the UNORDERED subsets (2^n) that `omit` has always
144
+ * had.
145
+ *
146
+ * ## Why ordering is the right remedy here rather than withholding the name
147
+ *
148
+ * A projection's SELECT-list order is not semantically meaningful to Turbine:
149
+ * rows are assembled by NAME (`parseRow`, `jsonScalarPairs` and
150
+ * `buildRelationShape` all key off the same resolved list), so reordering it
151
+ * changes no value. Where an order IS meaningful (`orderBy`, and `DISTINCT ON`,
152
+ * whose list is re-emitted as an ORDER BY prefix), the statement is sent
153
+ * unnamed instead. See `MAX_NAMED_ORDER_KEYS` in filters.ts and the `distinct`
154
+ * mark in builder.ts.
155
+ *
156
+ * Duplicates collapse, which is the other half of "canonical": two spellings of
157
+ * one column (`userId` and `user_id`, both legal on an introspected schema)
158
+ * used to emit that column twice in the SELECT list.
159
+ *
160
+ * Cost is one Set of the projected columns plus one pass over `allColumns`, on
161
+ * a path that already walks the caller's keys.
162
+ *
163
+ * @param meta the table the columns belong to; without `allColumns` there is no
164
+ * canonical order to appeal to and the input is returned untouched.
165
+ * @param columns already-resolved snake_case column names.
166
+ */
167
+ function canonicalColumnOrder(meta, columns) {
168
+ const all = meta.allColumns;
169
+ if (all === undefined || columns.length < 2)
170
+ return columns;
171
+ const wanted = new Set(columns);
172
+ const ordered = [];
173
+ for (const col of all) {
174
+ if (wanted.delete(col))
175
+ ordered.push(col);
176
+ }
177
+ // A resolved column absent from `allColumns` is only reachable from
178
+ // hand-built metadata whose maps disagree with each other. Keep it (in the
179
+ // caller's relative order) rather than silently dropping it from the
180
+ // projection: this function reorders, it never decides what is projected.
181
+ if (wanted.size > 0) {
182
+ for (const col of columns) {
183
+ if (wanted.delete(col))
184
+ ordered.push(col);
185
+ }
186
+ }
187
+ return ordered;
188
+ }
189
+ /**
190
+ * {@link canonicalColumnOrder} for a write's `data` entries: same order, same
191
+ * reason, two deliberate differences.
192
+ *
193
+ * DUPLICATES ARE KEPT. Two keys resolving to one column (`{ userId, user_id }`)
194
+ * is a caller error that PostgreSQL reports precisely ("multiple assignments to
195
+ * same column", 42701, and the INSERT equivalent). Collapsing them here would
196
+ * turn that error into a silent write of whichever value survived, so the entry
197
+ * list is REORDERED and never shortened. A stable sort keeps such a pair
198
+ * adjacent and in the caller's relative order, so the engine still sees, and
199
+ * still rejects, both assignments.
200
+ *
201
+ * THE KEY SPELLING IS PRESERVED. Everything downstream (`coerceWriteValue`,
202
+ * `buildSetClause`, `toSqlColumn`) re-resolves the caller's key itself, so only
203
+ * the ORDER of the entries changes here, never their content.
204
+ *
205
+ * A key that resolves to no column sorts LAST, in the caller's relative order.
206
+ * It is about to raise E003 from the SQL builder either way, and the builder is
207
+ * where that error belongs; ordering must not pre-empt it with a worse one.
208
+ */
209
+ function canonicalWriteEntries(meta, entries) {
210
+ const all = meta.allColumns;
211
+ if (all === undefined || entries.length < 2)
212
+ return entries;
213
+ const index = new Map();
214
+ for (let i = 0; i < all.length; i++)
215
+ index.set(all[i], i);
216
+ const rank = (key) => {
217
+ const column = resolveColumnName(meta, key);
218
+ const at = column === undefined ? undefined : index.get(column);
219
+ return at ?? Number.MAX_SAFE_INTEGER;
220
+ };
221
+ // Ranks are computed once per entry rather than inside the comparator, which
222
+ // would re-resolve every key O(k log k) times.
223
+ const ranked = entries.map((entry, at) => ({ entry, rank: rank(entry[0]), at }));
224
+ // `at` breaks ties explicitly rather than relying on sort stability, so two
225
+ // spellings of one column, and every unresolvable key, keep the order the
226
+ // caller wrote them in.
227
+ ranked.sort((a, b) => a.rank - b.rank || a.at - b.at);
228
+ return ranked.map((r) => r.entry);
229
+ }
230
+ // ---------------------------------------------------------------------------
231
+ // Internally-synthesized combinator brand
232
+ // ---------------------------------------------------------------------------
233
+ /**
234
+ * Brands a WHERE object whose `AND` / `OR` array Turbine ITSELF synthesized,
235
+ * rather than one the caller wrote.
236
+ *
237
+ * The distinction matters for exactly one rule: a caller-written combinator
238
+ * array is treated as a VARIABLE-ARITY shape and its statement is sent unnamed
239
+ * (see `markVariableArity` in where.ts, and `acquireSql` in builder.ts).
240
+ * Turbine's own wrappers, the `{ AND: [userWhere, globalFilter] }` the
241
+ * global-filter merge produces and the `{ AND: [where, correlation] }` the
242
+ * batched loader produces, have a FIXED arity of two, decided by Turbine, not
243
+ * reachable from a request body. Counting them would have taken every query on
244
+ * a table with a configured global filter off named prepared statements, i.e.
245
+ * penalised precisely the multi-tenant setups the rule exists to protect.
246
+ *
247
+ * A `Symbol.for` key for the same cross-copy-identity reason the warn registry
248
+ * uses one (an ESM and a CJS copy of this module in one process must agree),
249
+ * and a SYMBOL rather than a string key so `Object.keys` (which is what every
250
+ * where walker enumerates) never sees it and no emitted SQL can change.
251
+ */
252
+ exports.INTERNAL_COMBINATOR = Symbol.for('turbine.internalCombinator');
253
+ /** Tag `where` as carrying a Turbine-synthesized combinator, and return it. */
254
+ function markInternalCombinator(where) {
255
+ Object.defineProperty(where, exports.INTERNAL_COMBINATOR, { value: true, enumerable: false, configurable: true });
256
+ return where;
257
+ }
258
+ /** Was this WHERE object's combinator synthesized by Turbine? */
259
+ function isInternalCombinator(where) {
260
+ return (typeof where === 'object' && where !== null && where[exports.INTERNAL_COMBINATOR] === true);
261
+ }
106
262
  /**
107
263
  * Escape single quotes for use as string keys in json_build_object().
108
264
  * Doubles single quotes per SQL quoting rules.
@@ -122,8 +278,15 @@ function escapeLike(value) {
122
278
  // ---------------------------------------------------------------------------
123
279
  /**
124
280
  * Simple LRU (Least Recently Used) cache with a fixed maximum size.
125
- * When the cache exceeds maxSize, the oldest (least recently used) entry is evicted.
126
- * Uses Map insertion order for O(1) eviction.
281
+ * When the cache exceeds maxSize, the oldest (least recently used) entry is
282
+ * evicted. Uses Map insertion order, so EVICTION is O(1).
283
+ *
284
+ * The access-order reorder in {@link get} is not: `Map.delete` + `Map.set`
285
+ * leaves a tombstone, and V8 rehashes the whole table once live plus deleted
286
+ * entries reach capacity, so the reorder amortizes to O(capacity) per hit.
287
+ * At the 1,000-entry default that measured 1,356 ns against 2.6 ns for a plain
288
+ * `Map.get`, on the hottest lookup in the SQL build. See {@link get} for why
289
+ * skipping it below capacity is not merely an optimization but exact.
127
290
  */
128
291
  class LRUCache {
129
292
  maxSize;
@@ -133,7 +296,19 @@ class LRUCache {
133
296
  }
134
297
  get(key) {
135
298
  const value = this.cache.get(key);
136
- if (value !== undefined) {
299
+ // Access order is only ever CONSUMED by eviction (`set` drops the first
300
+ // key once the cache is full), so while it is still filling, maintaining
301
+ // that order cannot change WHICH entries are present: nothing is evicted,
302
+ // and the reorder is pure cost. So it starts the moment the cache is full,
303
+ // and from then on every hit reorders exactly as before.
304
+ //
305
+ // What is traded, stated plainly: reads that happened while the cache was
306
+ // filling are not reflected in the order, so the first evictions after it
307
+ // fills can drop an entry that was hot early rather than the true
308
+ // least-recently-used one. Reads after that point are ordered normally, so
309
+ // the effect does not accumulate, and the worst case is one extra SQL
310
+ // rebuild on the next miss. This cache decides speed, never results.
311
+ if (value !== undefined && this.cache.size >= this.maxSize) {
137
312
  // Move to end (most recently used)
138
313
  this.cache.delete(key);
139
314
  this.cache.set(key, value);
@@ -913,6 +1088,37 @@ function relationInProjectionMessage(table, field, clause) {
913
1088
  : `${head} A relation is only present when you ask for it in \`with\`, so leave it out of \`with\` to leave it` +
914
1089
  ' out of the result.';
915
1090
  }
1091
+ /**
1092
+ * Dev-only advisory for a sort/grouping term Turbine dropped as redundant
1093
+ * (`filters.ts` `dedupeOrderEntries` / `dedupeColumnList`).
1094
+ *
1095
+ * Deliberately a WARNING and not an error, and deliberately not silent either.
1096
+ * The dropped term provably cannot change a result, so failing the query would
1097
+ * be a false alarm on a shape correct code produces (a caller-chosen sort key
1098
+ * plus an unconditional primary-key tiebreak, which collide exactly when the
1099
+ * caller sorts by the primary key). But a caller who wrote the duplicate by
1100
+ * hand, or who believes a second key is doing something, should hear about it
1101
+ * once. Dev-only for the usual reason: it describes the QUERY the application
1102
+ * sends, which does not vary with the environment, so a production process
1103
+ * learns nothing from repeating it.
1104
+ *
1105
+ * Keyed per dropped key, so a sort assembled per request from the same UI says
1106
+ * it once rather than once per request.
1107
+ */
1108
+ function warnRedundantSortTerm(table, clause, dropped) {
1109
+ if (process.env.NODE_ENV === 'production')
1110
+ return;
1111
+ for (const d of dropped) {
1112
+ if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.redundantSortTerm, `${table}|${clause}|${d.key}`))
1113
+ continue;
1114
+ const how = d.first === d.key
1115
+ ? `"${d.key}" is named twice`
1116
+ : `"${d.key}" and the earlier "${d.first}" both target ${d.resolved}`;
1117
+ console.warn(`[turbine] ${clause} on table "${table}": ${how}, so the later one was dropped. It could not have ` +
1118
+ 'changed the result (the earlier term already orders those rows), and dropping it keeps the query on ' +
1119
+ 'a shared prepared statement. Remove it to silence this.');
1120
+ }
1121
+ }
916
1122
  /**
917
1123
  * The two projection SHAPE refusals (0.65), shared by the SQL engines' single
918
1124
  * resolver, the batched loader's raw-arg check, and PowDB, so every path that
@@ -65,6 +65,16 @@ export declare const WARN_NS: {
65
65
  * config says it once.
66
66
  */
67
67
  readonly unknownConfigKey: "unknownConfigKey";
68
+ /**
69
+ * A caller-supplied `orderBy` (or `distinct` / `distinctOn.columns`) named one
70
+ * sort expression twice and Turbine dropped the redundant term
71
+ * (filters.ts `dedupeOrderEntries` / `dedupeColumnList`). Not an error: the
72
+ * dropped term provably cannot move a row, and the shape is produced by
73
+ * correct defensive code that appends a fixed tiebreak to a caller-chosen sort
74
+ * key. Keyed on `table|context|key`, so a per-request sort that always
75
+ * collides says it once rather than once per request.
76
+ */
77
+ readonly redundantSortTerm: "redundantSortTerm";
68
78
  /**
69
79
  * `relationLoadStrategy: 'flatten'` was asked for but a relation stayed on the
70
80
  * correlated-subquery path (relations.ts `planFlattenWith`, builder.ts
@@ -106,6 +106,16 @@ exports.WARN_NS = {
106
106
  * config says it once.
107
107
  */
108
108
  unknownConfigKey: 'unknownConfigKey',
109
+ /**
110
+ * A caller-supplied `orderBy` (or `distinct` / `distinctOn.columns`) named one
111
+ * sort expression twice and Turbine dropped the redundant term
112
+ * (filters.ts `dedupeOrderEntries` / `dedupeColumnList`). Not an error: the
113
+ * dropped term provably cannot move a row, and the shape is produced by
114
+ * correct defensive code that appends a fixed tiebreak to a caller-chosen sort
115
+ * key. Keyed on `table|context|key`, so a per-request sort that always
116
+ * collides says it once rather than once per request.
117
+ */
118
+ redundantSortTerm: 'redundantSortTerm',
109
119
  /**
110
120
  * `relationLoadStrategy: 'flatten'` was asked for but a relation stayed on the
111
121
  * correlated-subquery path (relations.ts `planFlattenWith`, builder.ts
@@ -36,6 +36,36 @@
36
36
  import type { RelationDef, TableMetadata } from '../schema.js';
37
37
  /** A table-scoped WHERE object (or an `OR`/`AND`/`NOT` branch of one). */
38
38
  export type WhereRecord = Record<string, unknown>;
39
+ /**
40
+ * Maximum nesting of `OR` / `AND` / `NOT` combinators and relation-filter
41
+ * descents in one WHERE clause (and in one groupBy `HAVING`).
42
+ *
43
+ * Every walker over a where clause, the fingerprint, the SQL build and the
44
+ * cache-hit param collect, and every one of them for the table-SCOPED sub-where
45
+ * too, is directly recursive on these three keys. Nothing bounded that
46
+ * recursion, so a request body could choose the JavaScript stack depth:
47
+ * measured with wire-realistic JSON, a 4,000-deep `NOT` chain built fine and an
48
+ * 8,000-deep one (a 64 KB body, comfortably under `express.json()`'s 100 KB
49
+ * default) threw `RangeError: Maximum call stack size exceeded`. A `RangeError`
50
+ * is not a {@link TurbineError}, so it walks straight past the typed-error
51
+ * surface callers catch on and lands as an unhandled rejection.
52
+ *
53
+ * 32 matches the depth cap Studio's fail-closed PII guard already uses, and
54
+ * sits an order of magnitude above anything a hand-written or
55
+ * query-builder-generated predicate reaches (`with` is capped at 10 for the
56
+ * same class of reason). Exceeding it is a {@link ValidationError} (E003), the
57
+ * same typed refusal every other malformed where shape gets.
58
+ */
59
+ export declare const MAX_WHERE_DEPTH = 32;
60
+ /**
61
+ * Refuse a where/having walk that has nested past {@link MAX_WHERE_DEPTH}.
62
+ *
63
+ * Called at the TOP of every recursive walker rather than at the recursion
64
+ * site, so a cap breach is caught on whichever walker runs first (on a cache
65
+ * HIT the SQL build never runs, so a check placed only there would be skipped
66
+ * exactly when the request is being served fastest).
67
+ */
68
+ export declare function assertWhereDepth(depth: number, clause?: 'where' | 'having'): void;
39
69
  /**
40
70
  * Everything the shared walk needs from the owning {@link QueryInterface}. Bound
41
71
  * once per instance (see `QueryInterface`'s `whereHost` field) so the walk stays
@@ -35,11 +35,52 @@
35
35
  * step.
36
36
  */
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.MAX_WHERE_DEPTH = void 0;
39
+ exports.assertWhereDepth = assertWhereDepth;
38
40
  exports.walkWhere = walkWhere;
39
41
  exports.classifyScalarForSql = classifyScalarForSql;
40
42
  exports.fingerprintScalarToken = fingerprintScalarToken;
43
+ const errors_js_1 = require("../errors.js");
41
44
  const filters_js_1 = require("./filters.js");
42
45
  const utils_js_1 = require("./utils.js");
46
+ /**
47
+ * Maximum nesting of `OR` / `AND` / `NOT` combinators and relation-filter
48
+ * descents in one WHERE clause (and in one groupBy `HAVING`).
49
+ *
50
+ * Every walker over a where clause, the fingerprint, the SQL build and the
51
+ * cache-hit param collect, and every one of them for the table-SCOPED sub-where
52
+ * too, is directly recursive on these three keys. Nothing bounded that
53
+ * recursion, so a request body could choose the JavaScript stack depth:
54
+ * measured with wire-realistic JSON, a 4,000-deep `NOT` chain built fine and an
55
+ * 8,000-deep one (a 64 KB body, comfortably under `express.json()`'s 100 KB
56
+ * default) threw `RangeError: Maximum call stack size exceeded`. A `RangeError`
57
+ * is not a {@link TurbineError}, so it walks straight past the typed-error
58
+ * surface callers catch on and lands as an unhandled rejection.
59
+ *
60
+ * 32 matches the depth cap Studio's fail-closed PII guard already uses, and
61
+ * sits an order of magnitude above anything a hand-written or
62
+ * query-builder-generated predicate reaches (`with` is capped at 10 for the
63
+ * same class of reason). Exceeding it is a {@link ValidationError} (E003), the
64
+ * same typed refusal every other malformed where shape gets.
65
+ */
66
+ exports.MAX_WHERE_DEPTH = 32;
67
+ /**
68
+ * Refuse a where/having walk that has nested past {@link MAX_WHERE_DEPTH}.
69
+ *
70
+ * Called at the TOP of every recursive walker rather than at the recursion
71
+ * site, so a cap breach is caught on whichever walker runs first (on a cache
72
+ * HIT the SQL build never runs, so a check placed only there would be skipped
73
+ * exactly when the request is being served fastest).
74
+ */
75
+ function assertWhereDepth(depth, clause = 'where') {
76
+ if (depth <= exports.MAX_WHERE_DEPTH)
77
+ return;
78
+ throw new errors_js_1.ValidationError(`[turbine] \`${clause}\` clause nests more than ${exports.MAX_WHERE_DEPTH} levels of AND / OR / NOT ` +
79
+ `(or relation filters) deep. That is far past anything a real predicate needs, and an unbounded ` +
80
+ `walk over caller-supplied nesting is a stack-overflow surface, so it is refused. If this is a ` +
81
+ `generated predicate, flatten it: a single \`AND\` / \`OR\` array of N conditions is one level, ` +
82
+ `not N.`);
83
+ }
43
84
  /** True when a normalized relation filter carries at least one cardinality key. */
44
85
  function isRelationFilterObj(filterObj) {
45
86
  return ('some' in filterObj || 'every' in filterObj || 'none' in filterObj || 'is' in filterObj || 'isNot' in filterObj);