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>>;
@@ -10,11 +10,11 @@
10
10
  */
11
11
  import { UnsupportedFeatureError, ValidationError } from '../errors.js';
12
12
  import { snakeToCamel } from '../schema.js';
13
- import { dedupeColumnList, isJsonPathOrderBy, isUnmatchedPlainObject, isVectorOrderBy, isWhereOperator, normalizeOrderBy, orderByEntries, } from './filters.js';
13
+ import { assertEqualityOperand, dedupeColumnList, isJsonPathOrderBy, isUnmatchedPlainObject, isVectorOrderBy, isWhereOperator, normalizeOrderBy, orderByEntries, setOwn, } from './filters.js';
14
14
  import { assertOrderDirection, resolveSkipGlobalFilters, resolveUnsafeFlag } from './types.js';
15
- import { isTemporalInfinity, ownLookup, parseDbDate, resolveColumnName, warnRedundantSortTerm } from './utils.js';
15
+ import { isTemporalInfinity, ownLookup, parseDbDate, resolveColumnName, suggestKey, warnRedundantSortTerm, } from './utils.js';
16
16
  import * as whereMod from './where.js';
17
- import { assertWhereDepth } from './where-compile.js';
17
+ import { assertWhereDepth, combinatorBranches } from './where-compile.js';
18
18
  /**
19
19
  * Enforce the PII contract on the aggregate surface. A PII-tagged
20
20
  * (`defineSchema` `pii: true`) column is excluded from every default
@@ -44,7 +44,39 @@ export function assertAggregatePiiOptIn(table, meta, field, column, usage, inclu
44
44
  '`_count` over a PII column (a count, not a value) and `where` / `orderBy` / `having` on PII ' +
45
45
  'columns need no opt-in.');
46
46
  }
47
+ /**
48
+ * `by` is required, and must be a non-empty array of field names and
49
+ * `{ field, path }` JSON keys. Checked before anything reads it: a missing
50
+ * `by` was a TypeError ("args.by is not iterable"), a string was iterated
51
+ * character by character ("Unknown field \"n\""), an empty array emitted
52
+ * `GROUP BY ` with nothing after it for the server to reject, and a `null`
53
+ * entry was a TypeError. Exported for PowqlInterface, whose groupBy takes
54
+ * the same `by`.
55
+ */
56
+ export function assertGroupByKeys(table, by) {
57
+ if (!Array.isArray(by)) {
58
+ const example = typeof by === 'string' ? `, for example \`by: [${JSON.stringify(by)}]\`` : '';
59
+ throw new ValidationError(`groupBy on table "${table}" requires \`by\`, an array of field names${example}; got ${describeByValue(by)}.`);
60
+ }
61
+ if (by.length === 0) {
62
+ throw new ValidationError(`groupBy on table "${table}" requires at least one \`by\` key. To aggregate every row as one group, use aggregate().`);
63
+ }
64
+ for (const entry of by) {
65
+ if (typeof entry !== 'string' && (typeof entry !== 'object' || entry === null || Array.isArray(entry))) {
66
+ throw new ValidationError(`groupBy \`by\` on table "${table}" takes field names and { field, path } JSON keys; got ${describeByValue(entry)}.`);
67
+ }
68
+ }
69
+ }
70
+ /** The kind of a bad `by` value, never the value itself. */
71
+ function describeByValue(value) {
72
+ if (value === null || value === undefined)
73
+ return String(value);
74
+ if (Array.isArray(value))
75
+ return 'an array';
76
+ return typeof value === 'object' ? 'an object' : `a ${typeof value}`;
77
+ }
47
78
  export function buildGroupBy(qi, args) {
79
+ assertGroupByKeys(qi.table, args.by);
48
80
  const meta = qi.schema.tables[qi.table];
49
81
  // Up-front, so a bad `by` key is reported before anything else in the call,
50
82
  // and through `qi.toColumn` (the ONE `resolveColumnName` rule) rather than
@@ -243,6 +275,8 @@ export function buildGroupBy(qi, args) {
243
275
  buildAggregates('_avg', 'AVG', args._avg);
244
276
  buildAggregates('_min', 'MIN', args._min);
245
277
  buildAggregates('_max', 'MAX', args._max);
278
+ const percentiles = planPercentiles(qi, args._percentile, params, includePii, 'groupBy', claimResultKey);
279
+ selectExprs.push(...percentiles.map((plan) => plan.expr));
246
280
  let sql = `SELECT ${selectExprs.join(', ')} FROM ${fromSql} GROUP BY ${groupExprs.join(', ')}`;
247
281
  // HAVING, filter whole groups by their aggregate values.
248
282
  // Appends to the same `params` array, so placeholders continue from the
@@ -356,6 +390,8 @@ export function buildGroupBy(qi, args) {
356
390
  restructured._min = minObj;
357
391
  if (hasMaxs)
358
392
  restructured._max = maxObj;
393
+ if (percentiles.length > 0)
394
+ restructured._percentile = readPercentiles(qi, percentiles, row);
359
395
  return restructured;
360
396
  }),
361
397
  tag: `${qi.table}.groupBy`,
@@ -435,7 +471,9 @@ export function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
435
471
  const pushOrderTerm = (expr, label, value) => {
436
472
  const first = seen.get(expr);
437
473
  if (first !== undefined) {
438
- warnRedundantSortTerm(qi.table, 'groupBy orderBy', [{ key: label, first, resolved: expr }]);
474
+ warnRedundantSortTerm(whereMod.adviceLogger(qi), qi.table, 'groupBy orderBy', [
475
+ { key: label, first, resolved: expr },
476
+ ]);
439
477
  return;
440
478
  }
441
479
  seen.set(expr, label);
@@ -445,6 +483,9 @@ export function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
445
483
  for (const [key, value] of orderByEntries(orderBy)) {
446
484
  if (value === undefined)
447
485
  continue;
486
+ if (key === '_percentile') {
487
+ throw new ValidationError(`groupBy orderBy on table "${qi.table}": ${PERCENTILE_NOT_ORDERABLE}`);
488
+ }
448
489
  // Aggregate ordering blocks.
449
490
  if (aggBlocks.has(key)) {
450
491
  if (key === '_count') {
@@ -529,7 +570,7 @@ export function resolveJsonPathTarget(qi, context, field, path) {
529
570
  * JSON paths push their text[] param here, after the WHERE params.
530
571
  */
531
572
  export function buildDistinctOnSource(qi, distinctOn, whereSql, params) {
532
- if (qi.dialect.name !== 'postgresql') {
573
+ if (!qi.dialect.capabilities.distinctOn) {
533
574
  throw new UnsupportedFeatureError('DISTINCT ON row source (groupBy distinctOn)', qi.dialect.name, 'groupBy({ distinctOn }) requires PostgreSQL: SELECT DISTINCT ON is not portable.');
534
575
  }
535
576
  if (!Array.isArray(distinctOn.columns) || distinctOn.columns.length === 0) {
@@ -544,9 +585,9 @@ export function buildDistinctOnSource(qi, distinctOn, whereSql, params) {
544
585
  // twice (see `dedupeColumnList`). The orderBy below needs no equivalent: its
545
586
  // keys come from an object, and the DISTINCT ON columns lead the ORDER BY, so
546
587
  // a key repeating one of them is already merged by `orderParts`' construction.
547
- const dedupedCols = dedupeColumnList(qi.tableMeta, distinctOn.columns);
588
+ const dedupedCols = dedupeColumnList(qi.tableMeta, distinctOn.columns, 'distinctOn.columns');
548
589
  if (dedupedCols)
549
- warnRedundantSortTerm(qi.table, 'groupBy distinctOn.columns', dedupedCols.dropped);
590
+ warnRedundantSortTerm(whereMod.adviceLogger(qi), qi.table, 'groupBy distinctOn.columns', dedupedCols.dropped);
550
591
  const distinctCols = (dedupedCols?.columns ?? distinctOn.columns).map((c) => qi.q(qi.toColumn(c)));
551
592
  // DISTINCT ON expressions must lead the ORDER BY; the user's orderBy then
552
593
  // decides which row survives per combination.
@@ -610,6 +651,8 @@ export function buildHavingClauses(qi, having, params, jsonAggExprs, groupKeys,
610
651
  assertWhereDepth(depth, 'having');
611
652
  const clauses = [];
612
653
  for (const [key, value] of Object.entries(having)) {
654
+ // Same `undefined` rule as the where walk (`walkWhere`): the args seam
655
+ // removes it, and a key still holding it here is skipped.
613
656
  if (value === undefined)
614
657
  continue;
615
658
  // Top-level `_count` (no field) → COUNT(*) for the whole group.
@@ -641,9 +684,16 @@ export function buildHavingClauses(qi, having, params, jsonAggExprs, groupKeys,
641
684
  quotedCol ??= qi.q(qi.toColumn(key));
642
685
  return quotedCol;
643
686
  };
687
+ const plainAggregate = (agg) => agg.fn === 'AVG' ? avgCall(qi, qi.toColumn(key)) : `${agg.fn}(${columnExpr()})`;
644
688
  for (const agg of aggEntries) {
645
- const expr = jsonAggExprs?.get(`${key}:${agg.key}`) ?? `${agg.fn}(${columnExpr()})`;
646
- clauses.push(...buildHavingNumericClauses(qi, expr, agg.filter, params));
689
+ const jsonExpr = jsonAggExprs?.get(`${key}:${agg.key}`);
690
+ const expr = jsonExpr ?? plainAggregate(agg);
691
+ // `_min` / `_max` return the column's own stored value, so their operands
692
+ // bind the way a where operand against that column does.
693
+ const operandType = jsonExpr === undefined && (agg.fn === 'MIN' || agg.fn === 'MAX')
694
+ ? qi.columnPgTypeMap.get(qi.toColumn(key))
695
+ : undefined;
696
+ clauses.push(...buildHavingNumericClauses(qi, expr, agg.filter, params, operandType));
647
697
  }
648
698
  }
649
699
  return clauses;
@@ -670,12 +720,15 @@ function splitHavingField(qi, field, value) {
670
720
  if (fn) {
671
721
  aggEntries.push({ key: k, fn, filter: v });
672
722
  }
723
+ else if (k === '_percentile') {
724
+ throw new ValidationError(`groupBy having on "${qi.table}.${field}": ${PERCENTILE_NOT_ORDERABLE}`);
725
+ }
673
726
  else if (k.startsWith('_')) {
674
727
  throw new ValidationError(`Unknown aggregate "${k}" in having for field "${field}" on table "${qi.table}". ` +
675
728
  `Supported: ${Object.keys(HAVING_AGGREGATE_FNS).join(', ')}.`);
676
729
  }
677
730
  else {
678
- scalarKeys[k] = v;
731
+ setOwn(scalarKeys, k, v);
679
732
  }
680
733
  }
681
734
  if (aggEntries.length === 0)
@@ -685,12 +738,18 @@ function splitHavingField(qi, field, value) {
685
738
  /**
686
739
  * Compile a `having` AND / OR / NOT branch. Each condition is a nested
687
740
  * {@link HavingClause}; its own clauses are ANDed (parenthesized when there is
688
- * more than one) before being combined. `AND` contributes its parts directly
689
- * (the caller ANDs them), mirroring {@link buildWhereClause}'s combinator
690
- * shapes so HAVING and WHERE read the same way.
741
+ * more than one) before being combined. The branch shapes are the WHERE
742
+ * combinator rule ({@link combinatorBranches}), so HAVING and WHERE accept and
743
+ * refuse the same values and mean the same thing by them: `AND` contributes its
744
+ * parts directly (the caller ANDs them), `OR: []` matches no group, a `NOT`
745
+ * object negates the object as a whole, and a `NOT` array negates each branch.
691
746
  */
692
747
  function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys, depth = 0) {
693
- const conditions = Array.isArray(value) ? value : [value];
748
+ const branches = combinatorBranches(key, value, qi.table, 'having');
749
+ if (branches === null)
750
+ return [];
751
+ if (branches.length === 0)
752
+ return key === 'OR' ? ['1 = 0'] : [];
694
753
  // Same variable-arity shape as the WHERE combinators: an ARRAY branch writes
695
754
  // one parenthesized condition per element into the SQL text, so a
696
755
  // caller-sized `having.OR` is a new statement per length.
@@ -702,15 +761,11 @@ function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys,
702
761
  // them through the cache later cannot silently reopen the hole, and so the
703
762
  // rule reads the same in both clause compilers. The DEPTH cap above is the
704
763
  // half of this that bites here and now.
705
- if (Array.isArray(value) && key !== 'NOT')
764
+ if (Array.isArray(value))
706
765
  qi.markVariableArity();
707
766
  const parts = [];
708
- for (const condition of conditions) {
709
- if (!isUnmatchedPlainObject(condition)) {
710
- throw new ValidationError(`Invalid having "${key}" on table "${qi.table}": expected ` +
711
- `${key === 'OR' ? 'an array of having objects' : 'a having object (or an array of them)'}.`);
712
- }
713
- const sub = buildHavingClauses(qi, condition, params, jsonAggExprs, groupKeys, depth + 1);
767
+ for (const branch of branches) {
768
+ const sub = buildHavingClauses(qi, branch, params, jsonAggExprs, groupKeys, depth + 1);
714
769
  if (sub.length === 0)
715
770
  continue;
716
771
  parts.push(sub.length === 1 ? sub[0] : `(${sub.join(' AND ')})`);
@@ -721,6 +776,8 @@ function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys,
721
776
  return parts;
722
777
  if (key === 'OR')
723
778
  return [`(${parts.join(' OR ')})`];
779
+ if (Array.isArray(value))
780
+ return parts.map((part) => `NOT (${part})`);
724
781
  return [`NOT (${parts.join(' AND ')})`];
725
782
  }
726
783
  /**
@@ -774,6 +831,7 @@ function buildHavingScalarClauses(qi, field, value, params, groupKeys) {
774
831
  .join(', ')} on ${ref.label} in having for table "${qi.table}".`);
775
832
  }
776
833
  else {
834
+ assertEqualityOperand(value, { column: ref.label, table: qi.table, kind: 'scalar' });
777
835
  params.push(value);
778
836
  clauses.push(`${ref.expr} = ${qi.p(params.length)}`);
779
837
  }
@@ -785,14 +843,22 @@ function buildHavingScalarClauses(qi, field, value, params, groupKeys) {
785
843
  * shorthand for equality. Operands are not numeric-only: `_min` / `_max`
786
844
  * return a stored cell, so `MIN("title") > 'm'` is as valid as
787
845
  * `SUM("views") > 10`. Unknown operator keys throw {@link ValidationError}.
846
+ *
847
+ * `operandType` is the column type a `_min` / `_max` operand is compared with.
848
+ * When given, each operand (and each `in` / `notIn` element) is bound through
849
+ * the where compiler's {@link whereMod.textComparedOperand}, so on MySQL a
850
+ * number compared with the MIN of a text column is sent as text rather than
851
+ * compared numerically (`'0zz' = 0` is true there).
788
852
  */
789
- export function buildHavingNumericClauses(qi, expr, filter, params) {
853
+ export function buildHavingNumericClauses(qi, expr, filter, params, operandType) {
854
+ const bind = (value) => operandType === undefined ? value : whereMod.textComparedOperand(qi, operandType, value);
855
+ const bindList = (value) => operandType !== undefined && Array.isArray(value) ? value.map(bind) : value;
790
856
  if (filter === null) {
791
857
  throw new ValidationError(`Invalid having filter on "${expr}" for table "${qi.table}": expected a value or operator object.`);
792
858
  }
793
859
  // Bare value (number, string, boolean, Date, …) → equality.
794
860
  if (typeof filter !== 'object' || filter instanceof Date) {
795
- params.push(filter);
861
+ params.push(bind(filter));
796
862
  return [`${expr} = ${qi.p(params.length)}`];
797
863
  }
798
864
  const op = filter;
@@ -805,35 +871,35 @@ export function buildHavingNumericClauses(qi, expr, filter, params) {
805
871
  }
806
872
  const clauses = [];
807
873
  if (op.equals !== undefined) {
808
- params.push(op.equals);
874
+ params.push(bind(op.equals));
809
875
  clauses.push(`${expr} = ${qi.p(params.length)}`);
810
876
  }
811
877
  if (op.not !== undefined) {
812
- params.push(op.not);
878
+ params.push(bind(op.not));
813
879
  clauses.push(`${expr} != ${qi.p(params.length)}`);
814
880
  }
815
881
  if (op.gt !== undefined) {
816
- params.push(op.gt);
882
+ params.push(bind(op.gt));
817
883
  clauses.push(`${expr} > ${qi.p(params.length)}`);
818
884
  }
819
885
  if (op.gte !== undefined) {
820
- params.push(op.gte);
886
+ params.push(bind(op.gte));
821
887
  clauses.push(`${expr} >= ${qi.p(params.length)}`);
822
888
  }
823
889
  if (op.lt !== undefined) {
824
- params.push(op.lt);
890
+ params.push(bind(op.lt));
825
891
  clauses.push(`${expr} < ${qi.p(params.length)}`);
826
892
  }
827
893
  if (op.lte !== undefined) {
828
- params.push(op.lte);
894
+ params.push(bind(op.lte));
829
895
  clauses.push(`${expr} <= ${qi.p(params.length)}`);
830
896
  }
831
897
  if (op.in !== undefined) {
832
- params.push(qi.inParam(op.in));
898
+ params.push(qi.inParam(bindList(op.in)));
833
899
  clauses.push(qi.inClause(expr, qi.p(params.length), false));
834
900
  }
835
901
  if (op.notIn !== undefined) {
836
- params.push(qi.inParam(op.notIn));
902
+ params.push(qi.inParam(bindList(op.notIn)));
837
903
  clauses.push(qi.inClause(expr, qi.p(params.length), true));
838
904
  }
839
905
  return clauses;
@@ -918,9 +984,19 @@ function isExactNumericType(pgType) {
918
984
  }
919
985
  /** `AVG(col)`, float-cast unless the source column is exact (see {@link isExactNumericType}). */
920
986
  function plainAvgExpr(qi, col) {
921
- const inner = `AVG(${qi.q(col)})`;
987
+ const inner = avgCall(qi, col);
922
988
  return isExactNumericType(whereMod.getColumnPgType(qi, col)) ? inner : qi.castAgg(inner, 'float');
923
989
  }
990
+ /**
991
+ * `AVG(col)` over a plain column, with the input widened where the engine
992
+ * would otherwise average an integer column as an integer (see
993
+ * {@link Dialect.averageArgument}). Shared by SELECT and HAVING, so a filter
994
+ * on `_avg` compares the same value the result reports.
995
+ */
996
+ function avgCall(qi, col) {
997
+ const ref = qi.q(col);
998
+ return `AVG(${qi.dialect.averageArgument?.(ref, whereMod.getColumnPgType(qi, col)) ?? ref})`;
999
+ }
924
1000
  /**
925
1001
  * A `_sum` / `_avg` value over a plain column: the driver's value verbatim
926
1002
  * for an exact source type (see {@link isExactNumericType}), a JS number for
@@ -933,6 +1009,156 @@ function sumAvgValue(qi, col, value) {
933
1009
  return value;
934
1010
  return Number(value);
935
1011
  }
1012
+ // ---------------------------------------------------------------------------
1013
+ // _percentile (PostgreSQL: percentile_cont / percentile_disc)
1014
+ // ---------------------------------------------------------------------------
1015
+ /** Why an engine refuses `_percentile`, keyed by dialect name. */
1016
+ const PERCENTILE_HINT = {
1017
+ mysql: 'Percentile aggregates need PostgreSQL; MySQL has no percentile_cont/percentile_disc.',
1018
+ sqlite: 'Percentile aggregates need PostgreSQL; SQLite has percentile functions only in builds compiled with ' +
1019
+ 'SQLITE_ENABLE_PERCENTILE, which node:sqlite cannot be assumed to have.',
1020
+ mssql: 'Percentile aggregates are not available on SQL Server in this release: PERCENTILE_CONT is a window ' +
1021
+ 'function there, not an aggregate.',
1022
+ };
1023
+ const PERCENTILE_SPEC_KEYS = ['p', 'method'];
1024
+ const PERCENTILE_METHODS = ['cont', 'disc'];
1025
+ const PERCENTILE_NOT_ORDERABLE = 'ordering or filtering groups by _percentile is not supported yet.';
1026
+ /**
1027
+ * Compile `_percentile` into SELECT expressions, binding each fraction (or
1028
+ * fraction array) as a `float8` / `float8[]` parameter so the SQL text does not
1029
+ * depend on the values or on how many there are.
1030
+ *
1031
+ * Refusals, all before any SQL runs: E017 on a dialect without
1032
+ * `supportsPercentileAggregates`; E003 for a spec that is not `{ p, method? }`,
1033
+ * a fraction outside [0, 1] or not a finite number, an empty array, an unknown
1034
+ * method, `cont` on a date/time column (`percentile_cont` has no temporal
1035
+ * form), a JSON-path target, and two keys naming one column. Both methods need
1036
+ * `includePii: UNSAFE` on a PII column: `disc` returns a stored cell, and so
1037
+ * does `cont` at p = 0 or 1 or over a one-row group.
1038
+ */
1039
+ function planPercentiles(qi, spec, params, includePii, operation, claimResultKey) {
1040
+ if (spec === undefined)
1041
+ return [];
1042
+ const where = `${operation} _percentile on table "${qi.table}"`;
1043
+ if (typeof spec !== 'object' || spec === null || Array.isArray(spec)) {
1044
+ throw new ValidationError(`${where} must be an object of field -> { p, method? }, e.g. { latencyMs: { p: 0.95 } }.`);
1045
+ }
1046
+ const entries = Object.entries(spec).filter(([, v]) => v !== undefined && v !== null);
1047
+ if (entries.length === 0)
1048
+ return [];
1049
+ if (qi.dialect.supportsPercentileAggregates !== true) {
1050
+ throw new UnsupportedFeatureError('_percentile', qi.dialect.name, PERCENTILE_HINT[qi.dialect.name] ?? 'Percentile aggregates need PostgreSQL.');
1051
+ }
1052
+ const meta = qi.schema.tables[qi.table];
1053
+ const plans = [];
1054
+ const aliases = new Set();
1055
+ for (const [key, entry] of entries) {
1056
+ const label = `${where}, field "${key}"`;
1057
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
1058
+ throw new ValidationError(`${label}: expected { p, method? }, received ${describeValue(entry)}.`);
1059
+ }
1060
+ if ('path' in entry) {
1061
+ throw new ValidationError(`${label}: a JSON-path target is not supported for _percentile yet.`);
1062
+ }
1063
+ for (const k of Object.keys(entry)) {
1064
+ if (!PERCENTILE_SPEC_KEYS.includes(k)) {
1065
+ const hint = suggestKey(k, PERCENTILE_SPEC_KEYS);
1066
+ throw new ValidationError(`${label}: unknown key "${k}".${hint ? ` Did you mean "${hint}"?` : ''} Expected { p, method? }.`);
1067
+ }
1068
+ }
1069
+ const { p, method = 'cont' } = entry;
1070
+ if (!PERCENTILE_METHODS.includes(method)) {
1071
+ throw new ValidationError(`${label}: method must be 'cont' or 'disc', received ${describeValue(method)}.`);
1072
+ }
1073
+ if (Array.isArray(p)) {
1074
+ if (p.length === 0)
1075
+ throw new ValidationError(`${label}: p is an empty array. Give at least one fraction.`);
1076
+ for (const fraction of p)
1077
+ assertFraction(fraction, label);
1078
+ }
1079
+ else {
1080
+ assertFraction(p, label);
1081
+ }
1082
+ const col = qi.toColumn(key);
1083
+ if (method === 'cont' && qi.tableMeta.dateColumns.has(col)) {
1084
+ throw new ValidationError(`${label}: method 'cont' interpolates and needs a numeric or interval column, and "${col}" is a ` +
1085
+ "date/time column. Use method: 'disc' for an actual stored value.");
1086
+ }
1087
+ assertAggregatePiiOptIn(qi.table, meta, key, col, `${operation} _percentile`, includePii);
1088
+ const alias = `_percentile_${col}`;
1089
+ if (aliases.has(alias)) {
1090
+ throw new ValidationError(`${label}: column "${col}" is already named by another _percentile key.`);
1091
+ }
1092
+ aliases.add(alias);
1093
+ claimResultKey?.(alias, `_percentile of column "${col}"`);
1094
+ params.push(Array.isArray(p) ? [...p] : p);
1095
+ const fractions = `${qi.p(params.length)}::float8${Array.isArray(p) ? '[]' : ''}`;
1096
+ let expr = `percentile_${method}(${fractions}) WITHIN GROUP (ORDER BY ${qi.q(col)})`;
1097
+ // numeric[] has a driver parser that returns JS numbers, which rounds an
1098
+ // exact value the scalar form returns as text. Read the array as text too.
1099
+ if (Array.isArray(p) && method === 'disc' && isDecimalType(whereMod.getColumnPgType(qi, col))) {
1100
+ expr = `(${expr})::text[]`;
1101
+ }
1102
+ plans.push({
1103
+ expr: `${expr} AS ${qi.q(alias)}`,
1104
+ alias,
1105
+ field: qi.tableMeta.reverseColumnMap[col] ?? snakeToCamel(col),
1106
+ col,
1107
+ });
1108
+ }
1109
+ return plans;
1110
+ }
1111
+ function assertFraction(value, label) {
1112
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1)
1113
+ return;
1114
+ throw new ValidationError(`${label}: p must be a fraction from 0 to 1 (0.95 for the 95th percentile), or an array of them, ` +
1115
+ `received ${describeValue(value)}.`);
1116
+ }
1117
+ function describeValue(value) {
1118
+ if (typeof value === 'string')
1119
+ return `'${value}'`;
1120
+ if (Array.isArray(value))
1121
+ return 'an array';
1122
+ if (value !== null && typeof value === 'object')
1123
+ return 'an object';
1124
+ return String(value);
1125
+ }
1126
+ function isDecimalType(pgType) {
1127
+ const paren = pgType.indexOf('(');
1128
+ const base = (paren === -1 ? pgType : pgType.slice(0, paren)).trim().toLowerCase();
1129
+ return base === 'numeric' || base === 'decimal';
1130
+ }
1131
+ /** The `_percentile` block of one result row, keyed by canonical field. */
1132
+ function readPercentiles(qi, plans, row) {
1133
+ const out = {};
1134
+ for (const plan of plans) {
1135
+ const value = row[plan.alias];
1136
+ setOwn(out, plan.field, Array.isArray(value)
1137
+ ? value.map((element) => percentileElement(qi, plan.col, element))
1138
+ : percentileElement(qi, plan.col, value));
1139
+ }
1140
+ return out;
1141
+ }
1142
+ /**
1143
+ * One percentile value, read the way the column itself reads: a date/time
1144
+ * value through {@link temporalAggValue} (element by element in the array
1145
+ * form, where the driver may hand back text or a bare infinity), and an int8
1146
+ * that the array form delivers as text through the same exact-number rule the
1147
+ * scalar int8 parser applies. `null` over zero rows.
1148
+ */
1149
+ function percentileElement(qi, col, value) {
1150
+ if (value === null || value === undefined)
1151
+ return null;
1152
+ if (typeof value === 'string' && isInt8Type(whereMod.getColumnPgType(qi, col))) {
1153
+ const n = Number(value);
1154
+ return Number.isSafeInteger(n) ? n : value;
1155
+ }
1156
+ return temporalAggValue(qi, col, value);
1157
+ }
1158
+ function isInt8Type(pgType) {
1159
+ const base = pgType.trim().toLowerCase();
1160
+ return base === 'int8' || base === 'bigint';
1161
+ }
936
1162
  export function buildAggregate(qi, args) {
937
1163
  qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
938
1164
  // Resolved once, up front: see buildGroupBy.
@@ -1019,6 +1245,8 @@ export function buildAggregate(qi, args) {
1019
1245
  }
1020
1246
  }
1021
1247
  }
1248
+ const percentiles = planPercentiles(qi, args._percentile, params, includePii, 'aggregate');
1249
+ selectExprs.push(...percentiles.map((plan) => plan.expr));
1022
1250
  if (selectExprs.length === 0) {
1023
1251
  selectExprs.push(`${qi.castAgg('COUNT(*)', 'int')} AS _count`);
1024
1252
  }
@@ -1094,6 +1322,9 @@ export function buildAggregate(qi, args) {
1094
1322
  aggResult._min = minObj;
1095
1323
  if (hasMaxs)
1096
1324
  aggResult._max = maxObj;
1325
+ if (percentiles.length > 0) {
1326
+ aggResult._percentile = readPercentiles(qi, percentiles, row);
1327
+ }
1097
1328
  return aggResult;
1098
1329
  },
1099
1330
  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