turbine-orm 0.81.0 → 0.90.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 (295) hide show
  1. package/README.md +26 -25
  2. package/dist/checkout.d.ts +27 -2
  3. package/dist/checkout.js +75 -4
  4. package/dist/cjs/checkout.d.ts +27 -2
  5. package/dist/cjs/checkout.js +76 -3
  6. package/dist/cjs/cli/code-first-tags.d.ts +62 -0
  7. package/dist/cjs/cli/code-first-tags.js +124 -0
  8. package/dist/cjs/cli/compile-query.js +9 -0
  9. package/dist/cjs/cli/config.d.ts +35 -6
  10. package/dist/cjs/cli/config.js +17 -1
  11. package/dist/cjs/cli/destructive.d.ts +7 -4
  12. package/dist/cjs/cli/destructive.js +224 -20
  13. package/dist/cjs/cli/error-catalog.js +36 -0
  14. package/dist/cjs/cli/error-text.d.ts +15 -0
  15. package/dist/cjs/cli/error-text.js +40 -0
  16. package/dist/cjs/cli/index.d.ts +62 -4
  17. package/dist/cjs/cli/index.js +568 -108
  18. package/dist/cjs/cli/local-server-guard.d.ts +53 -0
  19. package/dist/cjs/cli/local-server-guard.js +119 -0
  20. package/dist/cjs/cli/local-server-output.d.ts +41 -0
  21. package/dist/cjs/cli/local-server-output.js +103 -0
  22. package/dist/cjs/cli/mcp.d.ts +8 -0
  23. package/dist/cjs/cli/mcp.js +121 -32
  24. package/dist/cjs/cli/migrate.d.ts +113 -5
  25. package/dist/cjs/cli/migrate.js +270 -21
  26. package/dist/cjs/cli/observe.d.ts +6 -1
  27. package/dist/cjs/cli/observe.js +85 -55
  28. package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
  29. package/dist/cjs/cli/pii-predicate-guard.js +98 -37
  30. package/dist/cjs/cli/prisma-resolve.js +17 -13
  31. package/dist/cjs/cli/prisma-schema.js +12 -4
  32. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  33. package/dist/cjs/cli/studio.d.ts +14 -2
  34. package/dist/cjs/cli/studio.js +244 -69
  35. package/dist/cjs/cli/ui.d.ts +1 -1
  36. package/dist/cjs/client.d.ts +558 -85
  37. package/dist/cjs/client.js +1270 -395
  38. package/dist/cjs/connection-guard.d.ts +13 -0
  39. package/dist/cjs/connection-guard.js +28 -0
  40. package/dist/cjs/connection-url.d.ts +51 -16
  41. package/dist/cjs/connection-url.js +67 -34
  42. package/dist/cjs/diagnostics.d.ts +81 -0
  43. package/dist/cjs/diagnostics.js +147 -0
  44. package/dist/cjs/dialect.d.ts +449 -20
  45. package/dist/cjs/dialect.js +117 -76
  46. package/dist/cjs/engine-config.d.ts +7 -6
  47. package/dist/cjs/errors.d.ts +126 -6
  48. package/dist/cjs/errors.js +256 -13
  49. package/dist/cjs/generate.d.ts +57 -7
  50. package/dist/cjs/generate.js +635 -279
  51. package/dist/cjs/global-filters-config.d.ts +59 -0
  52. package/dist/cjs/global-filters-config.js +367 -0
  53. package/dist/cjs/index.d.ts +9 -4
  54. package/dist/cjs/index.js +10 -2
  55. package/dist/cjs/introspect.d.ts +31 -6
  56. package/dist/cjs/introspect.js +108 -50
  57. package/dist/cjs/log-codes.d.ts +12 -0
  58. package/dist/cjs/log-codes.js +77 -0
  59. package/dist/cjs/logger.d.ts +87 -0
  60. package/dist/cjs/logger.js +98 -0
  61. package/dist/cjs/mssql.d.ts +8 -5
  62. package/dist/cjs/mssql.js +352 -65
  63. package/dist/cjs/mysql.d.ts +3 -5
  64. package/dist/cjs/mysql.js +420 -70
  65. package/dist/cjs/nested-write.d.ts +3 -0
  66. package/dist/cjs/nested-write.js +179 -114
  67. package/dist/cjs/pipeline-submittable.d.ts +12 -0
  68. package/dist/cjs/pipeline-submittable.js +118 -44
  69. package/dist/cjs/pipeline.js +21 -26
  70. package/dist/cjs/powdb-introspect.d.ts +5 -2
  71. package/dist/cjs/powdb-introspect.js +20 -15
  72. package/dist/cjs/powdb-shared.d.ts +36 -5
  73. package/dist/cjs/powdb-shared.js +67 -8
  74. package/dist/cjs/powdb.d.ts +4 -0
  75. package/dist/cjs/powdb.js +183 -51
  76. package/dist/cjs/powql.d.ts +394 -51
  77. package/dist/cjs/powql.js +1829 -284
  78. package/dist/cjs/prisma-compat.d.ts +23 -15
  79. package/dist/cjs/prisma-compat.js +507 -372
  80. package/dist/cjs/query/aggregates.d.ts +17 -1
  81. package/dist/cjs/query/aggregates.js +261 -29
  82. package/dist/cjs/query/batched-loader.d.ts +25 -6
  83. package/dist/cjs/query/batched-loader.js +103 -20
  84. package/dist/cjs/query/builder.d.ts +325 -184
  85. package/dist/cjs/query/builder.js +1452 -490
  86. package/dist/cjs/query/column-ref.d.ts +61 -0
  87. package/dist/cjs/query/column-ref.js +73 -0
  88. package/dist/cjs/query/compound-unique.d.ts +21 -0
  89. package/dist/cjs/query/compound-unique.js +98 -18
  90. package/dist/cjs/query/deferred.d.ts +53 -16
  91. package/dist/cjs/query/expr.d.ts +217 -0
  92. package/dist/cjs/query/expr.js +859 -0
  93. package/dist/cjs/query/filters.d.ts +155 -53
  94. package/dist/cjs/query/filters.js +613 -76
  95. package/dist/cjs/query/index.d.ts +2 -0
  96. package/dist/cjs/query/index.js +7 -1
  97. package/dist/cjs/query/json-value.d.ts +43 -0
  98. package/dist/cjs/query/json-value.js +59 -0
  99. package/dist/cjs/query/normalize-args.d.ts +87 -0
  100. package/dist/cjs/query/normalize-args.js +527 -0
  101. package/dist/cjs/query/option-surface.d.ts +16 -4
  102. package/dist/cjs/query/option-surface.js +57 -14
  103. package/dist/cjs/query/relation-names.d.ts +14 -0
  104. package/dist/cjs/query/relation-names.js +77 -18
  105. package/dist/cjs/query/relations.d.ts +30 -3
  106. package/dist/cjs/query/relations.js +219 -63
  107. package/dist/cjs/query/row-lock.d.ts +49 -0
  108. package/dist/cjs/query/row-lock.js +138 -0
  109. package/dist/cjs/query/sql-comment.d.ts +161 -0
  110. package/dist/cjs/query/sql-comment.js +381 -0
  111. package/dist/cjs/query/stream-idle.d.ts +132 -0
  112. package/dist/cjs/query/stream-idle.js +212 -0
  113. package/dist/cjs/query/table-client.d.ts +101 -0
  114. package/dist/cjs/query/table-client.js +74 -0
  115. package/dist/cjs/query/types.d.ts +677 -298
  116. package/dist/cjs/query/utils.d.ts +105 -6
  117. package/dist/cjs/query/utils.js +426 -19
  118. package/dist/cjs/query/warn-registry.d.ts +18 -0
  119. package/dist/cjs/query/warn-registry.js +18 -0
  120. package/dist/cjs/query/where-compile.d.ts +138 -14
  121. package/dist/cjs/query/where-compile.js +513 -29
  122. package/dist/cjs/query/where.d.ts +159 -59
  123. package/dist/cjs/query/where.js +716 -246
  124. package/dist/cjs/query/writes.d.ts +112 -50
  125. package/dist/cjs/query/writes.js +1498 -200
  126. package/dist/cjs/query-events.d.ts +4 -2
  127. package/dist/cjs/query-events.js +3 -1
  128. package/dist/cjs/realtime.d.ts +7 -3
  129. package/dist/cjs/realtime.js +13 -2
  130. package/dist/cjs/schema-builder.d.ts +55 -4
  131. package/dist/cjs/schema-builder.js +382 -18
  132. package/dist/cjs/schema-metadata.d.ts +21 -1
  133. package/dist/cjs/schema-metadata.js +29 -7
  134. package/dist/cjs/schema-sql.d.ts +75 -5
  135. package/dist/cjs/schema-sql.js +497 -45
  136. package/dist/cjs/schema.d.ts +26 -5
  137. package/dist/cjs/schema.js +110 -13
  138. package/dist/cjs/sql-placeholders.d.ts +56 -0
  139. package/dist/cjs/sql-placeholders.js +204 -0
  140. package/dist/cjs/sqlite.d.ts +75 -20
  141. package/dist/cjs/sqlite.js +593 -95
  142. package/dist/cjs/supplied-pool-types.d.ts +48 -0
  143. package/dist/cjs/supplied-pool-types.js +256 -0
  144. package/dist/cjs/transaction-control.d.ts +51 -0
  145. package/dist/cjs/transaction-control.js +316 -0
  146. package/dist/cjs/transaction-state.d.ts +342 -0
  147. package/dist/cjs/transaction-state.js +707 -0
  148. package/dist/cjs/typed-sql.d.ts +37 -3
  149. package/dist/cjs/typed-sql.js +102 -8
  150. package/dist/cli/code-first-tags.d.ts +62 -0
  151. package/dist/cli/code-first-tags.js +120 -0
  152. package/dist/cli/compile-query.js +9 -0
  153. package/dist/cli/config.d.ts +35 -6
  154. package/dist/cli/config.js +16 -1
  155. package/dist/cli/destructive.d.ts +7 -4
  156. package/dist/cli/destructive.js +224 -20
  157. package/dist/cli/error-catalog.js +36 -0
  158. package/dist/cli/error-text.d.ts +15 -0
  159. package/dist/cli/error-text.js +37 -0
  160. package/dist/cli/index.d.ts +62 -4
  161. package/dist/cli/index.js +569 -112
  162. package/dist/cli/local-server-guard.d.ts +53 -0
  163. package/dist/cli/local-server-guard.js +113 -0
  164. package/dist/cli/local-server-output.d.ts +41 -0
  165. package/dist/cli/local-server-output.js +98 -0
  166. package/dist/cli/mcp.d.ts +8 -0
  167. package/dist/cli/mcp.js +121 -34
  168. package/dist/cli/migrate.d.ts +113 -5
  169. package/dist/cli/migrate.js +261 -21
  170. package/dist/cli/observe.d.ts +6 -1
  171. package/dist/cli/observe.js +84 -54
  172. package/dist/cli/pii-predicate-guard.d.ts +43 -23
  173. package/dist/cli/pii-predicate-guard.js +100 -35
  174. package/dist/cli/prisma-resolve.js +15 -11
  175. package/dist/cli/prisma-schema.js +12 -4
  176. package/dist/cli/studio-ui.generated.js +1 -1
  177. package/dist/cli/studio.d.ts +14 -2
  178. package/dist/cli/studio.js +246 -71
  179. package/dist/cli/ui.d.ts +1 -1
  180. package/dist/client.d.ts +558 -85
  181. package/dist/client.js +1275 -400
  182. package/dist/connection-guard.d.ts +13 -0
  183. package/dist/connection-guard.js +27 -0
  184. package/dist/connection-url.d.ts +51 -16
  185. package/dist/connection-url.js +63 -34
  186. package/dist/diagnostics.d.ts +81 -0
  187. package/dist/diagnostics.js +144 -0
  188. package/dist/dialect.d.ts +449 -20
  189. package/dist/dialect.js +115 -43
  190. package/dist/engine-config.d.ts +7 -6
  191. package/dist/errors.d.ts +126 -6
  192. package/dist/errors.js +252 -12
  193. package/dist/generate.d.ts +57 -7
  194. package/dist/generate.js +635 -281
  195. package/dist/global-filters-config.d.ts +59 -0
  196. package/dist/global-filters-config.js +361 -0
  197. package/dist/index.d.ts +9 -4
  198. package/dist/index.js +4 -2
  199. package/dist/introspect.d.ts +31 -6
  200. package/dist/introspect.js +106 -50
  201. package/dist/log-codes.d.ts +12 -0
  202. package/dist/log-codes.js +74 -0
  203. package/dist/logger.d.ts +87 -0
  204. package/dist/logger.js +93 -0
  205. package/dist/mssql.d.ts +8 -5
  206. package/dist/mssql.js +354 -67
  207. package/dist/mysql.d.ts +3 -5
  208. package/dist/mysql.js +423 -73
  209. package/dist/nested-write.d.ts +3 -0
  210. package/dist/nested-write.js +177 -112
  211. package/dist/pipeline-submittable.d.ts +12 -0
  212. package/dist/pipeline-submittable.js +117 -44
  213. package/dist/pipeline.js +21 -26
  214. package/dist/powdb-introspect.d.ts +5 -2
  215. package/dist/powdb-introspect.js +21 -16
  216. package/dist/powdb-shared.d.ts +36 -5
  217. package/dist/powdb-shared.js +65 -8
  218. package/dist/powdb.d.ts +4 -0
  219. package/dist/powdb.js +183 -51
  220. package/dist/powql.d.ts +394 -51
  221. package/dist/powql.js +1836 -291
  222. package/dist/prisma-compat.d.ts +23 -15
  223. package/dist/prisma-compat.js +508 -373
  224. package/dist/query/aggregates.d.ts +17 -1
  225. package/dist/query/aggregates.js +263 -32
  226. package/dist/query/batched-loader.d.ts +25 -6
  227. package/dist/query/batched-loader.js +101 -21
  228. package/dist/query/builder.d.ts +325 -184
  229. package/dist/query/builder.js +1458 -495
  230. package/dist/query/column-ref.d.ts +61 -0
  231. package/dist/query/column-ref.js +66 -0
  232. package/dist/query/compound-unique.d.ts +21 -0
  233. package/dist/query/compound-unique.js +97 -19
  234. package/dist/query/deferred.d.ts +53 -16
  235. package/dist/query/expr.d.ts +217 -0
  236. package/dist/query/expr.js +848 -0
  237. package/dist/query/filters.d.ts +155 -53
  238. package/dist/query/filters.js +597 -72
  239. package/dist/query/index.d.ts +2 -0
  240. package/dist/query/index.js +4 -0
  241. package/dist/query/json-value.d.ts +43 -0
  242. package/dist/query/json-value.js +54 -0
  243. package/dist/query/normalize-args.d.ts +87 -0
  244. package/dist/query/normalize-args.js +517 -0
  245. package/dist/query/option-surface.d.ts +16 -4
  246. package/dist/query/option-surface.js +56 -13
  247. package/dist/query/relation-names.d.ts +14 -0
  248. package/dist/query/relation-names.js +77 -19
  249. package/dist/query/relations.d.ts +30 -3
  250. package/dist/query/relations.js +220 -67
  251. package/dist/query/row-lock.d.ts +49 -0
  252. package/dist/query/row-lock.js +131 -0
  253. package/dist/query/sql-comment.d.ts +161 -0
  254. package/dist/query/sql-comment.js +374 -0
  255. package/dist/query/stream-idle.d.ts +132 -0
  256. package/dist/query/stream-idle.js +203 -0
  257. package/dist/query/table-client.d.ts +101 -0
  258. package/dist/query/table-client.js +70 -0
  259. package/dist/query/types.d.ts +677 -298
  260. package/dist/query/utils.d.ts +105 -6
  261. package/dist/query/utils.js +414 -18
  262. package/dist/query/warn-registry.d.ts +18 -0
  263. package/dist/query/warn-registry.js +18 -0
  264. package/dist/query/where-compile.d.ts +138 -14
  265. package/dist/query/where-compile.js +508 -31
  266. package/dist/query/where.d.ts +159 -59
  267. package/dist/query/where.js +711 -248
  268. package/dist/query/writes.d.ts +112 -50
  269. package/dist/query/writes.js +1496 -202
  270. package/dist/query-events.d.ts +4 -2
  271. package/dist/query-events.js +3 -1
  272. package/dist/realtime.d.ts +7 -3
  273. package/dist/realtime.js +13 -2
  274. package/dist/schema-builder.d.ts +55 -4
  275. package/dist/schema-builder.js +381 -18
  276. package/dist/schema-metadata.d.ts +21 -1
  277. package/dist/schema-metadata.js +28 -7
  278. package/dist/schema-sql.d.ts +75 -5
  279. package/dist/schema-sql.js +494 -44
  280. package/dist/schema.d.ts +26 -5
  281. package/dist/schema.js +109 -13
  282. package/dist/sql-placeholders.d.ts +56 -0
  283. package/dist/sql-placeholders.js +200 -0
  284. package/dist/sqlite.d.ts +75 -20
  285. package/dist/sqlite.js +597 -99
  286. package/dist/supplied-pool-types.d.ts +48 -0
  287. package/dist/supplied-pool-types.js +247 -0
  288. package/dist/transaction-control.d.ts +51 -0
  289. package/dist/transaction-control.js +311 -0
  290. package/dist/transaction-state.d.ts +342 -0
  291. package/dist/transaction-state.js +696 -0
  292. package/dist/typed-sql.d.ts +37 -3
  293. package/dist/typed-sql.js +100 -8
  294. package/package.json +7 -4
  295. package/skills/turbine-orm/SKILL.md +56 -6
@@ -29,6 +29,16 @@ import type { BuilderCtx } from './where.js';
29
29
  * one policy.
30
30
  */
31
31
  export declare function assertAggregatePiiOptIn(table: string, meta: TableMetadata | undefined, field: string, column: string, usage: string, includePii: boolean | undefined): void;
32
+ /**
33
+ * `by` is required, and must be a non-empty array of field names and
34
+ * `{ field, path }` JSON keys. Checked before anything reads it: a missing
35
+ * `by` was a TypeError ("args.by is not iterable"), a string was iterated
36
+ * character by character ("Unknown field \"n\""), an empty array emitted
37
+ * `GROUP BY ` with nothing after it for the server to reject, and a `null`
38
+ * entry was a TypeError. Exported for PowqlInterface, whose groupBy takes
39
+ * the same `by`.
40
+ */
41
+ export declare function assertGroupByKeys(table: string, by: unknown): void;
32
42
  export declare function buildGroupBy<T extends object>(qi: BuilderCtx, args: GroupByArgs<T>): DeferredQuery<Record<string, unknown>[]>;
33
43
  export declare function buildGroupByOrderBy(qi: BuilderCtx, orderBy: GroupByOrderBy | GroupByOrderBy[], byOrderExprs: Map<string, string>, aggOrderExprs: Map<string, string>): string;
34
44
  /**
@@ -98,6 +108,12 @@ export declare function buildHavingClauses<T extends object>(qi: BuilderCtx, hav
98
108
  * shorthand for equality. Operands are not numeric-only: `_min` / `_max`
99
109
  * return a stored cell, so `MIN("title") > 'm'` is as valid as
100
110
  * `SUM("views") > 10`. Unknown operator keys throw {@link ValidationError}.
111
+ *
112
+ * `operandType` is the column type a `_min` / `_max` operand is compared with.
113
+ * When given, each operand (and each `in` / `notIn` element) is bound through
114
+ * the where compiler's {@link whereMod.textComparedOperand}, so on MySQL a
115
+ * number compared with the MIN of a text column is sent as text rather than
116
+ * compared numerically (`'0zz' = 0` is true there).
101
117
  */
102
- export declare function buildHavingNumericClauses(qi: BuilderCtx, expr: string, filter: unknown, params: unknown[]): string[];
118
+ export declare function buildHavingNumericClauses(qi: BuilderCtx, expr: string, filter: unknown, params: unknown[], operandType?: string): string[];
103
119
  export declare function buildAggregate<T extends object>(qi: BuilderCtx, args: AggregateArgs<T>): DeferredQuery<AggregateResult<T>>;
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.assertAggregatePiiOptIn = assertAggregatePiiOptIn;
47
+ exports.assertGroupByKeys = assertGroupByKeys;
47
48
  exports.buildGroupBy = buildGroupBy;
48
49
  exports.buildGroupByOrderBy = buildGroupByOrderBy;
49
50
  exports.resolveJsonPathTarget = resolveJsonPathTarget;
@@ -87,7 +88,39 @@ function assertAggregatePiiOptIn(table, meta, field, column, usage, includePii)
87
88
  '`_count` over a PII column (a count, not a value) and `where` / `orderBy` / `having` on PII ' +
88
89
  'columns need no opt-in.');
89
90
  }
91
+ /**
92
+ * `by` is required, and must be a non-empty array of field names and
93
+ * `{ field, path }` JSON keys. Checked before anything reads it: a missing
94
+ * `by` was a TypeError ("args.by is not iterable"), a string was iterated
95
+ * character by character ("Unknown field \"n\""), an empty array emitted
96
+ * `GROUP BY ` with nothing after it for the server to reject, and a `null`
97
+ * entry was a TypeError. Exported for PowqlInterface, whose groupBy takes
98
+ * the same `by`.
99
+ */
100
+ function assertGroupByKeys(table, by) {
101
+ if (!Array.isArray(by)) {
102
+ const example = typeof by === 'string' ? `, for example \`by: [${JSON.stringify(by)}]\`` : '';
103
+ throw new errors_js_1.ValidationError(`groupBy on table "${table}" requires \`by\`, an array of field names${example}; got ${describeByValue(by)}.`);
104
+ }
105
+ if (by.length === 0) {
106
+ throw new errors_js_1.ValidationError(`groupBy on table "${table}" requires at least one \`by\` key. To aggregate every row as one group, use aggregate().`);
107
+ }
108
+ for (const entry of by) {
109
+ if (typeof entry !== 'string' && (typeof entry !== 'object' || entry === null || Array.isArray(entry))) {
110
+ throw new errors_js_1.ValidationError(`groupBy \`by\` on table "${table}" takes field names and { field, path } JSON keys; got ${describeByValue(entry)}.`);
111
+ }
112
+ }
113
+ }
114
+ /** The kind of a bad `by` value, never the value itself. */
115
+ function describeByValue(value) {
116
+ if (value === null || value === undefined)
117
+ return String(value);
118
+ if (Array.isArray(value))
119
+ return 'an array';
120
+ return typeof value === 'object' ? 'an object' : `a ${typeof value}`;
121
+ }
90
122
  function buildGroupBy(qi, args) {
123
+ assertGroupByKeys(qi.table, args.by);
91
124
  const meta = qi.schema.tables[qi.table];
92
125
  // Up-front, so a bad `by` key is reported before anything else in the call,
93
126
  // and through `qi.toColumn` (the ONE `resolveColumnName` rule) rather than
@@ -286,6 +319,8 @@ function buildGroupBy(qi, args) {
286
319
  buildAggregates('_avg', 'AVG', args._avg);
287
320
  buildAggregates('_min', 'MIN', args._min);
288
321
  buildAggregates('_max', 'MAX', args._max);
322
+ const percentiles = planPercentiles(qi, args._percentile, params, includePii, 'groupBy', claimResultKey);
323
+ selectExprs.push(...percentiles.map((plan) => plan.expr));
289
324
  let sql = `SELECT ${selectExprs.join(', ')} FROM ${fromSql} GROUP BY ${groupExprs.join(', ')}`;
290
325
  // HAVING, filter whole groups by their aggregate values.
291
326
  // Appends to the same `params` array, so placeholders continue from the
@@ -399,6 +434,8 @@ function buildGroupBy(qi, args) {
399
434
  restructured._min = minObj;
400
435
  if (hasMaxs)
401
436
  restructured._max = maxObj;
437
+ if (percentiles.length > 0)
438
+ restructured._percentile = readPercentiles(qi, percentiles, row);
402
439
  return restructured;
403
440
  }),
404
441
  tag: `${qi.table}.groupBy`,
@@ -478,7 +515,9 @@ function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
478
515
  const pushOrderTerm = (expr, label, value) => {
479
516
  const first = seen.get(expr);
480
517
  if (first !== undefined) {
481
- (0, utils_js_1.warnRedundantSortTerm)(qi.table, 'groupBy orderBy', [{ key: label, first, resolved: expr }]);
518
+ (0, utils_js_1.warnRedundantSortTerm)(whereMod.adviceLogger(qi), qi.table, 'groupBy orderBy', [
519
+ { key: label, first, resolved: expr },
520
+ ]);
482
521
  return;
483
522
  }
484
523
  seen.set(expr, label);
@@ -488,6 +527,9 @@ function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
488
527
  for (const [key, value] of (0, filters_js_1.orderByEntries)(orderBy)) {
489
528
  if (value === undefined)
490
529
  continue;
530
+ if (key === '_percentile') {
531
+ throw new errors_js_1.ValidationError(`groupBy orderBy on table "${qi.table}": ${PERCENTILE_NOT_ORDERABLE}`);
532
+ }
491
533
  // Aggregate ordering blocks.
492
534
  if (aggBlocks.has(key)) {
493
535
  if (key === '_count') {
@@ -572,7 +614,7 @@ function resolveJsonPathTarget(qi, context, field, path) {
572
614
  * JSON paths push their text[] param here, after the WHERE params.
573
615
  */
574
616
  function buildDistinctOnSource(qi, distinctOn, whereSql, params) {
575
- if (qi.dialect.name !== 'postgresql') {
617
+ if (!qi.dialect.capabilities.distinctOn) {
576
618
  throw new errors_js_1.UnsupportedFeatureError('DISTINCT ON row source (groupBy distinctOn)', qi.dialect.name, 'groupBy({ distinctOn }) requires PostgreSQL: SELECT DISTINCT ON is not portable.');
577
619
  }
578
620
  if (!Array.isArray(distinctOn.columns) || distinctOn.columns.length === 0) {
@@ -587,9 +629,9 @@ function buildDistinctOnSource(qi, distinctOn, whereSql, params) {
587
629
  // twice (see `dedupeColumnList`). The orderBy below needs no equivalent: its
588
630
  // keys come from an object, and the DISTINCT ON columns lead the ORDER BY, so
589
631
  // a key repeating one of them is already merged by `orderParts`' construction.
590
- const dedupedCols = (0, filters_js_1.dedupeColumnList)(qi.tableMeta, distinctOn.columns);
632
+ const dedupedCols = (0, filters_js_1.dedupeColumnList)(qi.tableMeta, distinctOn.columns, 'distinctOn.columns');
591
633
  if (dedupedCols)
592
- (0, utils_js_1.warnRedundantSortTerm)(qi.table, 'groupBy distinctOn.columns', dedupedCols.dropped);
634
+ (0, utils_js_1.warnRedundantSortTerm)(whereMod.adviceLogger(qi), qi.table, 'groupBy distinctOn.columns', dedupedCols.dropped);
593
635
  const distinctCols = (dedupedCols?.columns ?? distinctOn.columns).map((c) => qi.q(qi.toColumn(c)));
594
636
  // DISTINCT ON expressions must lead the ORDER BY; the user's orderBy then
595
637
  // decides which row survives per combination.
@@ -653,6 +695,8 @@ function buildHavingClauses(qi, having, params, jsonAggExprs, groupKeys, depth =
653
695
  (0, where_compile_js_1.assertWhereDepth)(depth, 'having');
654
696
  const clauses = [];
655
697
  for (const [key, value] of Object.entries(having)) {
698
+ // Same `undefined` rule as the where walk (`walkWhere`): the args seam
699
+ // removes it, and a key still holding it here is skipped.
656
700
  if (value === undefined)
657
701
  continue;
658
702
  // Top-level `_count` (no field) → COUNT(*) for the whole group.
@@ -684,9 +728,16 @@ function buildHavingClauses(qi, having, params, jsonAggExprs, groupKeys, depth =
684
728
  quotedCol ??= qi.q(qi.toColumn(key));
685
729
  return quotedCol;
686
730
  };
731
+ const plainAggregate = (agg) => agg.fn === 'AVG' ? avgCall(qi, qi.toColumn(key)) : `${agg.fn}(${columnExpr()})`;
687
732
  for (const agg of aggEntries) {
688
- const expr = jsonAggExprs?.get(`${key}:${agg.key}`) ?? `${agg.fn}(${columnExpr()})`;
689
- clauses.push(...buildHavingNumericClauses(qi, expr, agg.filter, params));
733
+ const jsonExpr = jsonAggExprs?.get(`${key}:${agg.key}`);
734
+ const expr = jsonExpr ?? plainAggregate(agg);
735
+ // `_min` / `_max` return the column's own stored value, so their operands
736
+ // bind the way a where operand against that column does.
737
+ const operandType = jsonExpr === undefined && (agg.fn === 'MIN' || agg.fn === 'MAX')
738
+ ? qi.columnPgTypeMap.get(qi.toColumn(key))
739
+ : undefined;
740
+ clauses.push(...buildHavingNumericClauses(qi, expr, agg.filter, params, operandType));
690
741
  }
691
742
  }
692
743
  return clauses;
@@ -713,12 +764,15 @@ function splitHavingField(qi, field, value) {
713
764
  if (fn) {
714
765
  aggEntries.push({ key: k, fn, filter: v });
715
766
  }
767
+ else if (k === '_percentile') {
768
+ throw new errors_js_1.ValidationError(`groupBy having on "${qi.table}.${field}": ${PERCENTILE_NOT_ORDERABLE}`);
769
+ }
716
770
  else if (k.startsWith('_')) {
717
771
  throw new errors_js_1.ValidationError(`Unknown aggregate "${k}" in having for field "${field}" on table "${qi.table}". ` +
718
772
  `Supported: ${Object.keys(HAVING_AGGREGATE_FNS).join(', ')}.`);
719
773
  }
720
774
  else {
721
- scalarKeys[k] = v;
775
+ (0, filters_js_1.setOwn)(scalarKeys, k, v);
722
776
  }
723
777
  }
724
778
  if (aggEntries.length === 0)
@@ -728,12 +782,18 @@ function splitHavingField(qi, field, value) {
728
782
  /**
729
783
  * Compile a `having` AND / OR / NOT branch. Each condition is a nested
730
784
  * {@link HavingClause}; its own clauses are ANDed (parenthesized when there is
731
- * more than one) before being combined. `AND` contributes its parts directly
732
- * (the caller ANDs them), mirroring {@link buildWhereClause}'s combinator
733
- * shapes so HAVING and WHERE read the same way.
785
+ * more than one) before being combined. The branch shapes are the WHERE
786
+ * combinator rule ({@link combinatorBranches}), so HAVING and WHERE accept and
787
+ * refuse the same values and mean the same thing by them: `AND` contributes its
788
+ * parts directly (the caller ANDs them), `OR: []` matches no group, a `NOT`
789
+ * object negates the object as a whole, and a `NOT` array negates each branch.
734
790
  */
735
791
  function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys, depth = 0) {
736
- const conditions = Array.isArray(value) ? value : [value];
792
+ const branches = (0, where_compile_js_1.combinatorBranches)(key, value, qi.table, 'having');
793
+ if (branches === null)
794
+ return [];
795
+ if (branches.length === 0)
796
+ return key === 'OR' ? ['1 = 0'] : [];
737
797
  // Same variable-arity shape as the WHERE combinators: an ARRAY branch writes
738
798
  // one parenthesized condition per element into the SQL text, so a
739
799
  // caller-sized `having.OR` is a new statement per length.
@@ -745,15 +805,11 @@ function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys,
745
805
  // them through the cache later cannot silently reopen the hole, and so the
746
806
  // rule reads the same in both clause compilers. The DEPTH cap above is the
747
807
  // half of this that bites here and now.
748
- if (Array.isArray(value) && key !== 'NOT')
808
+ if (Array.isArray(value))
749
809
  qi.markVariableArity();
750
810
  const parts = [];
751
- for (const condition of conditions) {
752
- if (!(0, filters_js_1.isUnmatchedPlainObject)(condition)) {
753
- throw new errors_js_1.ValidationError(`Invalid having "${key}" on table "${qi.table}": expected ` +
754
- `${key === 'OR' ? 'an array of having objects' : 'a having object (or an array of them)'}.`);
755
- }
756
- const sub = buildHavingClauses(qi, condition, params, jsonAggExprs, groupKeys, depth + 1);
811
+ for (const branch of branches) {
812
+ const sub = buildHavingClauses(qi, branch, params, jsonAggExprs, groupKeys, depth + 1);
757
813
  if (sub.length === 0)
758
814
  continue;
759
815
  parts.push(sub.length === 1 ? sub[0] : `(${sub.join(' AND ')})`);
@@ -764,6 +820,8 @@ function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys,
764
820
  return parts;
765
821
  if (key === 'OR')
766
822
  return [`(${parts.join(' OR ')})`];
823
+ if (Array.isArray(value))
824
+ return parts.map((part) => `NOT (${part})`);
767
825
  return [`NOT (${parts.join(' AND ')})`];
768
826
  }
769
827
  /**
@@ -817,6 +875,7 @@ function buildHavingScalarClauses(qi, field, value, params, groupKeys) {
817
875
  .join(', ')} on ${ref.label} in having for table "${qi.table}".`);
818
876
  }
819
877
  else {
878
+ (0, filters_js_1.assertEqualityOperand)(value, { column: ref.label, table: qi.table, kind: 'scalar' });
820
879
  params.push(value);
821
880
  clauses.push(`${ref.expr} = ${qi.p(params.length)}`);
822
881
  }
@@ -828,14 +887,22 @@ function buildHavingScalarClauses(qi, field, value, params, groupKeys) {
828
887
  * shorthand for equality. Operands are not numeric-only: `_min` / `_max`
829
888
  * return a stored cell, so `MIN("title") > 'm'` is as valid as
830
889
  * `SUM("views") > 10`. Unknown operator keys throw {@link ValidationError}.
890
+ *
891
+ * `operandType` is the column type a `_min` / `_max` operand is compared with.
892
+ * When given, each operand (and each `in` / `notIn` element) is bound through
893
+ * the where compiler's {@link whereMod.textComparedOperand}, so on MySQL a
894
+ * number compared with the MIN of a text column is sent as text rather than
895
+ * compared numerically (`'0zz' = 0` is true there).
831
896
  */
832
- function buildHavingNumericClauses(qi, expr, filter, params) {
897
+ function buildHavingNumericClauses(qi, expr, filter, params, operandType) {
898
+ const bind = (value) => operandType === undefined ? value : whereMod.textComparedOperand(qi, operandType, value);
899
+ const bindList = (value) => operandType !== undefined && Array.isArray(value) ? value.map(bind) : value;
833
900
  if (filter === null) {
834
901
  throw new errors_js_1.ValidationError(`Invalid having filter on "${expr}" for table "${qi.table}": expected a value or operator object.`);
835
902
  }
836
903
  // Bare value (number, string, boolean, Date, …) → equality.
837
904
  if (typeof filter !== 'object' || filter instanceof Date) {
838
- params.push(filter);
905
+ params.push(bind(filter));
839
906
  return [`${expr} = ${qi.p(params.length)}`];
840
907
  }
841
908
  const op = filter;
@@ -848,35 +915,35 @@ function buildHavingNumericClauses(qi, expr, filter, params) {
848
915
  }
849
916
  const clauses = [];
850
917
  if (op.equals !== undefined) {
851
- params.push(op.equals);
918
+ params.push(bind(op.equals));
852
919
  clauses.push(`${expr} = ${qi.p(params.length)}`);
853
920
  }
854
921
  if (op.not !== undefined) {
855
- params.push(op.not);
922
+ params.push(bind(op.not));
856
923
  clauses.push(`${expr} != ${qi.p(params.length)}`);
857
924
  }
858
925
  if (op.gt !== undefined) {
859
- params.push(op.gt);
926
+ params.push(bind(op.gt));
860
927
  clauses.push(`${expr} > ${qi.p(params.length)}`);
861
928
  }
862
929
  if (op.gte !== undefined) {
863
- params.push(op.gte);
930
+ params.push(bind(op.gte));
864
931
  clauses.push(`${expr} >= ${qi.p(params.length)}`);
865
932
  }
866
933
  if (op.lt !== undefined) {
867
- params.push(op.lt);
934
+ params.push(bind(op.lt));
868
935
  clauses.push(`${expr} < ${qi.p(params.length)}`);
869
936
  }
870
937
  if (op.lte !== undefined) {
871
- params.push(op.lte);
938
+ params.push(bind(op.lte));
872
939
  clauses.push(`${expr} <= ${qi.p(params.length)}`);
873
940
  }
874
941
  if (op.in !== undefined) {
875
- params.push(qi.inParam(op.in));
942
+ params.push(qi.inParam(bindList(op.in)));
876
943
  clauses.push(qi.inClause(expr, qi.p(params.length), false));
877
944
  }
878
945
  if (op.notIn !== undefined) {
879
- params.push(qi.inParam(op.notIn));
946
+ params.push(qi.inParam(bindList(op.notIn)));
880
947
  clauses.push(qi.inClause(expr, qi.p(params.length), true));
881
948
  }
882
949
  return clauses;
@@ -961,9 +1028,19 @@ function isExactNumericType(pgType) {
961
1028
  }
962
1029
  /** `AVG(col)`, float-cast unless the source column is exact (see {@link isExactNumericType}). */
963
1030
  function plainAvgExpr(qi, col) {
964
- const inner = `AVG(${qi.q(col)})`;
1031
+ const inner = avgCall(qi, col);
965
1032
  return isExactNumericType(whereMod.getColumnPgType(qi, col)) ? inner : qi.castAgg(inner, 'float');
966
1033
  }
1034
+ /**
1035
+ * `AVG(col)` over a plain column, with the input widened where the engine
1036
+ * would otherwise average an integer column as an integer (see
1037
+ * {@link Dialect.averageArgument}). Shared by SELECT and HAVING, so a filter
1038
+ * on `_avg` compares the same value the result reports.
1039
+ */
1040
+ function avgCall(qi, col) {
1041
+ const ref = qi.q(col);
1042
+ return `AVG(${qi.dialect.averageArgument?.(ref, whereMod.getColumnPgType(qi, col)) ?? ref})`;
1043
+ }
967
1044
  /**
968
1045
  * A `_sum` / `_avg` value over a plain column: the driver's value verbatim
969
1046
  * for an exact source type (see {@link isExactNumericType}), a JS number for
@@ -976,6 +1053,156 @@ function sumAvgValue(qi, col, value) {
976
1053
  return value;
977
1054
  return Number(value);
978
1055
  }
1056
+ // ---------------------------------------------------------------------------
1057
+ // _percentile (PostgreSQL: percentile_cont / percentile_disc)
1058
+ // ---------------------------------------------------------------------------
1059
+ /** Why an engine refuses `_percentile`, keyed by dialect name. */
1060
+ const PERCENTILE_HINT = {
1061
+ mysql: 'Percentile aggregates need PostgreSQL; MySQL has no percentile_cont/percentile_disc.',
1062
+ sqlite: 'Percentile aggregates need PostgreSQL; SQLite has percentile functions only in builds compiled with ' +
1063
+ 'SQLITE_ENABLE_PERCENTILE, which node:sqlite cannot be assumed to have.',
1064
+ mssql: 'Percentile aggregates are not available on SQL Server in this release: PERCENTILE_CONT is a window ' +
1065
+ 'function there, not an aggregate.',
1066
+ };
1067
+ const PERCENTILE_SPEC_KEYS = ['p', 'method'];
1068
+ const PERCENTILE_METHODS = ['cont', 'disc'];
1069
+ const PERCENTILE_NOT_ORDERABLE = 'ordering or filtering groups by _percentile is not supported yet.';
1070
+ /**
1071
+ * Compile `_percentile` into SELECT expressions, binding each fraction (or
1072
+ * fraction array) as a `float8` / `float8[]` parameter so the SQL text does not
1073
+ * depend on the values or on how many there are.
1074
+ *
1075
+ * Refusals, all before any SQL runs: E017 on a dialect without
1076
+ * `supportsPercentileAggregates`; E003 for a spec that is not `{ p, method? }`,
1077
+ * a fraction outside [0, 1] or not a finite number, an empty array, an unknown
1078
+ * method, `cont` on a date/time column (`percentile_cont` has no temporal
1079
+ * form), a JSON-path target, and two keys naming one column. Both methods need
1080
+ * `includePii: UNSAFE` on a PII column: `disc` returns a stored cell, and so
1081
+ * does `cont` at p = 0 or 1 or over a one-row group.
1082
+ */
1083
+ function planPercentiles(qi, spec, params, includePii, operation, claimResultKey) {
1084
+ if (spec === undefined)
1085
+ return [];
1086
+ const where = `${operation} _percentile on table "${qi.table}"`;
1087
+ if (typeof spec !== 'object' || spec === null || Array.isArray(spec)) {
1088
+ throw new errors_js_1.ValidationError(`${where} must be an object of field -> { p, method? }, e.g. { latencyMs: { p: 0.95 } }.`);
1089
+ }
1090
+ const entries = Object.entries(spec).filter(([, v]) => v !== undefined && v !== null);
1091
+ if (entries.length === 0)
1092
+ return [];
1093
+ if (qi.dialect.supportsPercentileAggregates !== true) {
1094
+ throw new errors_js_1.UnsupportedFeatureError('_percentile', qi.dialect.name, PERCENTILE_HINT[qi.dialect.name] ?? 'Percentile aggregates need PostgreSQL.');
1095
+ }
1096
+ const meta = qi.schema.tables[qi.table];
1097
+ const plans = [];
1098
+ const aliases = new Set();
1099
+ for (const [key, entry] of entries) {
1100
+ const label = `${where}, field "${key}"`;
1101
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
1102
+ throw new errors_js_1.ValidationError(`${label}: expected { p, method? }, received ${describeValue(entry)}.`);
1103
+ }
1104
+ if ('path' in entry) {
1105
+ throw new errors_js_1.ValidationError(`${label}: a JSON-path target is not supported for _percentile yet.`);
1106
+ }
1107
+ for (const k of Object.keys(entry)) {
1108
+ if (!PERCENTILE_SPEC_KEYS.includes(k)) {
1109
+ const hint = (0, utils_js_1.suggestKey)(k, PERCENTILE_SPEC_KEYS);
1110
+ throw new errors_js_1.ValidationError(`${label}: unknown key "${k}".${hint ? ` Did you mean "${hint}"?` : ''} Expected { p, method? }.`);
1111
+ }
1112
+ }
1113
+ const { p, method = 'cont' } = entry;
1114
+ if (!PERCENTILE_METHODS.includes(method)) {
1115
+ throw new errors_js_1.ValidationError(`${label}: method must be 'cont' or 'disc', received ${describeValue(method)}.`);
1116
+ }
1117
+ if (Array.isArray(p)) {
1118
+ if (p.length === 0)
1119
+ throw new errors_js_1.ValidationError(`${label}: p is an empty array. Give at least one fraction.`);
1120
+ for (const fraction of p)
1121
+ assertFraction(fraction, label);
1122
+ }
1123
+ else {
1124
+ assertFraction(p, label);
1125
+ }
1126
+ const col = qi.toColumn(key);
1127
+ if (method === 'cont' && qi.tableMeta.dateColumns.has(col)) {
1128
+ throw new errors_js_1.ValidationError(`${label}: method 'cont' interpolates and needs a numeric or interval column, and "${col}" is a ` +
1129
+ "date/time column. Use method: 'disc' for an actual stored value.");
1130
+ }
1131
+ assertAggregatePiiOptIn(qi.table, meta, key, col, `${operation} _percentile`, includePii);
1132
+ const alias = `_percentile_${col}`;
1133
+ if (aliases.has(alias)) {
1134
+ throw new errors_js_1.ValidationError(`${label}: column "${col}" is already named by another _percentile key.`);
1135
+ }
1136
+ aliases.add(alias);
1137
+ claimResultKey?.(alias, `_percentile of column "${col}"`);
1138
+ params.push(Array.isArray(p) ? [...p] : p);
1139
+ const fractions = `${qi.p(params.length)}::float8${Array.isArray(p) ? '[]' : ''}`;
1140
+ let expr = `percentile_${method}(${fractions}) WITHIN GROUP (ORDER BY ${qi.q(col)})`;
1141
+ // numeric[] has a driver parser that returns JS numbers, which rounds an
1142
+ // exact value the scalar form returns as text. Read the array as text too.
1143
+ if (Array.isArray(p) && method === 'disc' && isDecimalType(whereMod.getColumnPgType(qi, col))) {
1144
+ expr = `(${expr})::text[]`;
1145
+ }
1146
+ plans.push({
1147
+ expr: `${expr} AS ${qi.q(alias)}`,
1148
+ alias,
1149
+ field: qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col),
1150
+ col,
1151
+ });
1152
+ }
1153
+ return plans;
1154
+ }
1155
+ function assertFraction(value, label) {
1156
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1)
1157
+ return;
1158
+ throw new errors_js_1.ValidationError(`${label}: p must be a fraction from 0 to 1 (0.95 for the 95th percentile), or an array of them, ` +
1159
+ `received ${describeValue(value)}.`);
1160
+ }
1161
+ function describeValue(value) {
1162
+ if (typeof value === 'string')
1163
+ return `'${value}'`;
1164
+ if (Array.isArray(value))
1165
+ return 'an array';
1166
+ if (value !== null && typeof value === 'object')
1167
+ return 'an object';
1168
+ return String(value);
1169
+ }
1170
+ function isDecimalType(pgType) {
1171
+ const paren = pgType.indexOf('(');
1172
+ const base = (paren === -1 ? pgType : pgType.slice(0, paren)).trim().toLowerCase();
1173
+ return base === 'numeric' || base === 'decimal';
1174
+ }
1175
+ /** The `_percentile` block of one result row, keyed by canonical field. */
1176
+ function readPercentiles(qi, plans, row) {
1177
+ const out = {};
1178
+ for (const plan of plans) {
1179
+ const value = row[plan.alias];
1180
+ (0, filters_js_1.setOwn)(out, plan.field, Array.isArray(value)
1181
+ ? value.map((element) => percentileElement(qi, plan.col, element))
1182
+ : percentileElement(qi, plan.col, value));
1183
+ }
1184
+ return out;
1185
+ }
1186
+ /**
1187
+ * One percentile value, read the way the column itself reads: a date/time
1188
+ * value through {@link temporalAggValue} (element by element in the array
1189
+ * form, where the driver may hand back text or a bare infinity), and an int8
1190
+ * that the array form delivers as text through the same exact-number rule the
1191
+ * scalar int8 parser applies. `null` over zero rows.
1192
+ */
1193
+ function percentileElement(qi, col, value) {
1194
+ if (value === null || value === undefined)
1195
+ return null;
1196
+ if (typeof value === 'string' && isInt8Type(whereMod.getColumnPgType(qi, col))) {
1197
+ const n = Number(value);
1198
+ return Number.isSafeInteger(n) ? n : value;
1199
+ }
1200
+ return temporalAggValue(qi, col, value);
1201
+ }
1202
+ function isInt8Type(pgType) {
1203
+ const base = pgType.trim().toLowerCase();
1204
+ return base === 'int8' || base === 'bigint';
1205
+ }
979
1206
  function buildAggregate(qi, args) {
980
1207
  qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
981
1208
  // Resolved once, up front: see buildGroupBy.
@@ -1062,6 +1289,8 @@ function buildAggregate(qi, args) {
1062
1289
  }
1063
1290
  }
1064
1291
  }
1292
+ const percentiles = planPercentiles(qi, args._percentile, params, includePii, 'aggregate');
1293
+ selectExprs.push(...percentiles.map((plan) => plan.expr));
1065
1294
  if (selectExprs.length === 0) {
1066
1295
  selectExprs.push(`${qi.castAgg('COUNT(*)', 'int')} AS _count`);
1067
1296
  }
@@ -1137,6 +1366,9 @@ function buildAggregate(qi, args) {
1137
1366
  aggResult._min = minObj;
1138
1367
  if (hasMaxs)
1139
1368
  aggResult._max = maxObj;
1369
+ if (percentiles.length > 0) {
1370
+ aggResult._percentile = readPercentiles(qi, percentiles, row);
1371
+ }
1140
1372
  return aggResult;
1141
1373
  },
1142
1374
  tag: `${qi.table}.aggregate`,
@@ -149,14 +149,23 @@ export declare function defaultProjectionFields(meta: TableMetadata, includePii:
149
149
  visible: string[];
150
150
  } | undefined;
151
151
  /**
152
- * Adjust a `select`/`omit` pair so that `fields` are guaranteed present in the
153
- * query result, returning the adjusted projection plus the list of fields that
154
- * were added ONLY for stitching and must be stripped from the final entities.
152
+ * Key under which {@link includeKeysForBatching} records the fields IT added to
153
+ * a `select`, as a `ReadonlySet<string>` of field names.
155
154
  *
156
- * Used both for the base query (parent keys) and each follow-up query (child
157
- * keys) so a caller's `select: { title: true }` on a relation still stitches even
158
- * though the FK was not requested, and the FK never appears in the output.
155
+ * Why it exists: naming a PII-tagged column in `select` is refused unless the
156
+ * query passes `includePii: UNSAFE` (see `resolveProjection`), and the loader
157
+ * has to project a correlation or ordering column that may itself be tagged.
158
+ * That column is stripped from every entity before the caller sees it, so it is
159
+ * the one legitimate way a PII name reaches a `select` without the caller
160
+ * writing it. The marker is a symbol key: `Object.keys`, `Object.entries`,
161
+ * spreads of a parsed body and `JSON.stringify` never see one, and
162
+ * `JSON.parse` cannot produce one, so request data cannot forge the exemption.
159
163
  */
164
+ export declare const STITCH_ONLY_FIELDS: unique symbol;
165
+ /** The fields the batched loader (not the caller) added to this `select`, if any. */
166
+ export declare function stitchOnlyFields(select: object | undefined): ReadonlySet<string> | undefined;
167
+ /** Record `fields` on `select` as added by an engine loader, not by the caller. Returns `select`. */
168
+ export declare function markStitchOnly(select: Record<string, boolean>, fields: readonly string[]): Record<string, boolean>;
160
169
  /**
161
170
  * The two projection SHAPE rules from `resolveProjection`, checked on the RAW
162
171
  * caller args BEFORE `includeKeysForBatching` adjusts them. The adjustment
@@ -167,6 +176,16 @@ export declare function defaultProjectionFields(meta: TableMetadata, includePii:
167
176
  * resolver so the two strategies refuse identically, word for word.
168
177
  */
169
178
  export declare function assertProjectionShape(table: string, select: Record<string, boolean> | undefined, omit: Record<string, boolean> | undefined): void;
179
+ /**
180
+ * Adjust a `select`/`omit` pair so that `fields` are guaranteed present in the
181
+ * query result, returning the adjusted projection plus the list of fields that
182
+ * were added ONLY for stitching and must be stripped from the final entities.
183
+ *
184
+ * Used both for the base query (parent keys) and each follow-up query (child
185
+ * keys) so a caller's `select: { title: true }` on a relation still stitches even
186
+ * though the FK was not requested, and the FK never appears in the output. A
187
+ * returned `select` carries {@link STITCH_ONLY_FIELDS} for the fields added here.
188
+ */
170
189
  export declare function includeKeysForBatching(
171
190
  /**
172
191
  * The table the projection is compiled against. `select` / `omit` keys are