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.
- package/README.md +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- 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>>;
|
package/dist/query/aggregates.js
CHANGED
|
@@ -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', [
|
|
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.
|
|
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
|
|
646
|
-
|
|
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
|
|
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.
|
|
689
|
-
*
|
|
690
|
-
*
|
|
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
|
|
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)
|
|
764
|
+
if (Array.isArray(value))
|
|
706
765
|
qi.markVariableArity();
|
|
707
766
|
const parts = [];
|
|
708
|
-
for (const
|
|
709
|
-
|
|
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 =
|
|
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
|
-
*
|
|
153
|
-
*
|
|
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
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
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
|