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>>;
|
|
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
44
44
|
})();
|
|
45
45
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
46
|
exports.assertAggregatePiiOptIn = assertAggregatePiiOptIn;
|
|
47
|
+
exports.assertGroupByKeys = assertGroupByKeys;
|
|
47
48
|
exports.buildGroupBy = buildGroupBy;
|
|
48
49
|
exports.buildGroupByOrderBy = buildGroupByOrderBy;
|
|
49
50
|
exports.resolveJsonPathTarget = resolveJsonPathTarget;
|
|
@@ -87,7 +88,39 @@ function assertAggregatePiiOptIn(table, meta, field, column, usage, includePii)
|
|
|
87
88
|
'`_count` over a PII column (a count, not a value) and `where` / `orderBy` / `having` on PII ' +
|
|
88
89
|
'columns need no opt-in.');
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* `by` is required, and must be a non-empty array of field names and
|
|
93
|
+
* `{ field, path }` JSON keys. Checked before anything reads it: a missing
|
|
94
|
+
* `by` was a TypeError ("args.by is not iterable"), a string was iterated
|
|
95
|
+
* character by character ("Unknown field \"n\""), an empty array emitted
|
|
96
|
+
* `GROUP BY ` with nothing after it for the server to reject, and a `null`
|
|
97
|
+
* entry was a TypeError. Exported for PowqlInterface, whose groupBy takes
|
|
98
|
+
* the same `by`.
|
|
99
|
+
*/
|
|
100
|
+
function assertGroupByKeys(table, by) {
|
|
101
|
+
if (!Array.isArray(by)) {
|
|
102
|
+
const example = typeof by === 'string' ? `, for example \`by: [${JSON.stringify(by)}]\`` : '';
|
|
103
|
+
throw new errors_js_1.ValidationError(`groupBy on table "${table}" requires \`by\`, an array of field names${example}; got ${describeByValue(by)}.`);
|
|
104
|
+
}
|
|
105
|
+
if (by.length === 0) {
|
|
106
|
+
throw new errors_js_1.ValidationError(`groupBy on table "${table}" requires at least one \`by\` key. To aggregate every row as one group, use aggregate().`);
|
|
107
|
+
}
|
|
108
|
+
for (const entry of by) {
|
|
109
|
+
if (typeof entry !== 'string' && (typeof entry !== 'object' || entry === null || Array.isArray(entry))) {
|
|
110
|
+
throw new errors_js_1.ValidationError(`groupBy \`by\` on table "${table}" takes field names and { field, path } JSON keys; got ${describeByValue(entry)}.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** The kind of a bad `by` value, never the value itself. */
|
|
115
|
+
function describeByValue(value) {
|
|
116
|
+
if (value === null || value === undefined)
|
|
117
|
+
return String(value);
|
|
118
|
+
if (Array.isArray(value))
|
|
119
|
+
return 'an array';
|
|
120
|
+
return typeof value === 'object' ? 'an object' : `a ${typeof value}`;
|
|
121
|
+
}
|
|
90
122
|
function buildGroupBy(qi, args) {
|
|
123
|
+
assertGroupByKeys(qi.table, args.by);
|
|
91
124
|
const meta = qi.schema.tables[qi.table];
|
|
92
125
|
// Up-front, so a bad `by` key is reported before anything else in the call,
|
|
93
126
|
// and through `qi.toColumn` (the ONE `resolveColumnName` rule) rather than
|
|
@@ -286,6 +319,8 @@ function buildGroupBy(qi, args) {
|
|
|
286
319
|
buildAggregates('_avg', 'AVG', args._avg);
|
|
287
320
|
buildAggregates('_min', 'MIN', args._min);
|
|
288
321
|
buildAggregates('_max', 'MAX', args._max);
|
|
322
|
+
const percentiles = planPercentiles(qi, args._percentile, params, includePii, 'groupBy', claimResultKey);
|
|
323
|
+
selectExprs.push(...percentiles.map((plan) => plan.expr));
|
|
289
324
|
let sql = `SELECT ${selectExprs.join(', ')} FROM ${fromSql} GROUP BY ${groupExprs.join(', ')}`;
|
|
290
325
|
// HAVING, filter whole groups by their aggregate values.
|
|
291
326
|
// Appends to the same `params` array, so placeholders continue from the
|
|
@@ -399,6 +434,8 @@ function buildGroupBy(qi, args) {
|
|
|
399
434
|
restructured._min = minObj;
|
|
400
435
|
if (hasMaxs)
|
|
401
436
|
restructured._max = maxObj;
|
|
437
|
+
if (percentiles.length > 0)
|
|
438
|
+
restructured._percentile = readPercentiles(qi, percentiles, row);
|
|
402
439
|
return restructured;
|
|
403
440
|
}),
|
|
404
441
|
tag: `${qi.table}.groupBy`,
|
|
@@ -478,7 +515,9 @@ function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
|
|
|
478
515
|
const pushOrderTerm = (expr, label, value) => {
|
|
479
516
|
const first = seen.get(expr);
|
|
480
517
|
if (first !== undefined) {
|
|
481
|
-
(0, utils_js_1.warnRedundantSortTerm)(qi.table, 'groupBy orderBy', [
|
|
518
|
+
(0, utils_js_1.warnRedundantSortTerm)(whereMod.adviceLogger(qi), qi.table, 'groupBy orderBy', [
|
|
519
|
+
{ key: label, first, resolved: expr },
|
|
520
|
+
]);
|
|
482
521
|
return;
|
|
483
522
|
}
|
|
484
523
|
seen.set(expr, label);
|
|
@@ -488,6 +527,9 @@ function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
|
|
|
488
527
|
for (const [key, value] of (0, filters_js_1.orderByEntries)(orderBy)) {
|
|
489
528
|
if (value === undefined)
|
|
490
529
|
continue;
|
|
530
|
+
if (key === '_percentile') {
|
|
531
|
+
throw new errors_js_1.ValidationError(`groupBy orderBy on table "${qi.table}": ${PERCENTILE_NOT_ORDERABLE}`);
|
|
532
|
+
}
|
|
491
533
|
// Aggregate ordering blocks.
|
|
492
534
|
if (aggBlocks.has(key)) {
|
|
493
535
|
if (key === '_count') {
|
|
@@ -572,7 +614,7 @@ function resolveJsonPathTarget(qi, context, field, path) {
|
|
|
572
614
|
* JSON paths push their text[] param here, after the WHERE params.
|
|
573
615
|
*/
|
|
574
616
|
function buildDistinctOnSource(qi, distinctOn, whereSql, params) {
|
|
575
|
-
if (qi.dialect.
|
|
617
|
+
if (!qi.dialect.capabilities.distinctOn) {
|
|
576
618
|
throw new errors_js_1.UnsupportedFeatureError('DISTINCT ON row source (groupBy distinctOn)', qi.dialect.name, 'groupBy({ distinctOn }) requires PostgreSQL: SELECT DISTINCT ON is not portable.');
|
|
577
619
|
}
|
|
578
620
|
if (!Array.isArray(distinctOn.columns) || distinctOn.columns.length === 0) {
|
|
@@ -587,9 +629,9 @@ function buildDistinctOnSource(qi, distinctOn, whereSql, params) {
|
|
|
587
629
|
// twice (see `dedupeColumnList`). The orderBy below needs no equivalent: its
|
|
588
630
|
// keys come from an object, and the DISTINCT ON columns lead the ORDER BY, so
|
|
589
631
|
// a key repeating one of them is already merged by `orderParts`' construction.
|
|
590
|
-
const dedupedCols = (0, filters_js_1.dedupeColumnList)(qi.tableMeta, distinctOn.columns);
|
|
632
|
+
const dedupedCols = (0, filters_js_1.dedupeColumnList)(qi.tableMeta, distinctOn.columns, 'distinctOn.columns');
|
|
591
633
|
if (dedupedCols)
|
|
592
|
-
(0, utils_js_1.warnRedundantSortTerm)(qi.table, 'groupBy distinctOn.columns', dedupedCols.dropped);
|
|
634
|
+
(0, utils_js_1.warnRedundantSortTerm)(whereMod.adviceLogger(qi), qi.table, 'groupBy distinctOn.columns', dedupedCols.dropped);
|
|
593
635
|
const distinctCols = (dedupedCols?.columns ?? distinctOn.columns).map((c) => qi.q(qi.toColumn(c)));
|
|
594
636
|
// DISTINCT ON expressions must lead the ORDER BY; the user's orderBy then
|
|
595
637
|
// decides which row survives per combination.
|
|
@@ -653,6 +695,8 @@ function buildHavingClauses(qi, having, params, jsonAggExprs, groupKeys, depth =
|
|
|
653
695
|
(0, where_compile_js_1.assertWhereDepth)(depth, 'having');
|
|
654
696
|
const clauses = [];
|
|
655
697
|
for (const [key, value] of Object.entries(having)) {
|
|
698
|
+
// Same `undefined` rule as the where walk (`walkWhere`): the args seam
|
|
699
|
+
// removes it, and a key still holding it here is skipped.
|
|
656
700
|
if (value === undefined)
|
|
657
701
|
continue;
|
|
658
702
|
// Top-level `_count` (no field) → COUNT(*) for the whole group.
|
|
@@ -684,9 +728,16 @@ function buildHavingClauses(qi, having, params, jsonAggExprs, groupKeys, depth =
|
|
|
684
728
|
quotedCol ??= qi.q(qi.toColumn(key));
|
|
685
729
|
return quotedCol;
|
|
686
730
|
};
|
|
731
|
+
const plainAggregate = (agg) => agg.fn === 'AVG' ? avgCall(qi, qi.toColumn(key)) : `${agg.fn}(${columnExpr()})`;
|
|
687
732
|
for (const agg of aggEntries) {
|
|
688
|
-
const
|
|
689
|
-
|
|
733
|
+
const jsonExpr = jsonAggExprs?.get(`${key}:${agg.key}`);
|
|
734
|
+
const expr = jsonExpr ?? plainAggregate(agg);
|
|
735
|
+
// `_min` / `_max` return the column's own stored value, so their operands
|
|
736
|
+
// bind the way a where operand against that column does.
|
|
737
|
+
const operandType = jsonExpr === undefined && (agg.fn === 'MIN' || agg.fn === 'MAX')
|
|
738
|
+
? qi.columnPgTypeMap.get(qi.toColumn(key))
|
|
739
|
+
: undefined;
|
|
740
|
+
clauses.push(...buildHavingNumericClauses(qi, expr, agg.filter, params, operandType));
|
|
690
741
|
}
|
|
691
742
|
}
|
|
692
743
|
return clauses;
|
|
@@ -713,12 +764,15 @@ function splitHavingField(qi, field, value) {
|
|
|
713
764
|
if (fn) {
|
|
714
765
|
aggEntries.push({ key: k, fn, filter: v });
|
|
715
766
|
}
|
|
767
|
+
else if (k === '_percentile') {
|
|
768
|
+
throw new errors_js_1.ValidationError(`groupBy having on "${qi.table}.${field}": ${PERCENTILE_NOT_ORDERABLE}`);
|
|
769
|
+
}
|
|
716
770
|
else if (k.startsWith('_')) {
|
|
717
771
|
throw new errors_js_1.ValidationError(`Unknown aggregate "${k}" in having for field "${field}" on table "${qi.table}". ` +
|
|
718
772
|
`Supported: ${Object.keys(HAVING_AGGREGATE_FNS).join(', ')}.`);
|
|
719
773
|
}
|
|
720
774
|
else {
|
|
721
|
-
scalarKeys
|
|
775
|
+
(0, filters_js_1.setOwn)(scalarKeys, k, v);
|
|
722
776
|
}
|
|
723
777
|
}
|
|
724
778
|
if (aggEntries.length === 0)
|
|
@@ -728,12 +782,18 @@ function splitHavingField(qi, field, value) {
|
|
|
728
782
|
/**
|
|
729
783
|
* Compile a `having` AND / OR / NOT branch. Each condition is a nested
|
|
730
784
|
* {@link HavingClause}; its own clauses are ANDed (parenthesized when there is
|
|
731
|
-
* more than one) before being combined.
|
|
732
|
-
*
|
|
733
|
-
*
|
|
785
|
+
* more than one) before being combined. The branch shapes are the WHERE
|
|
786
|
+
* combinator rule ({@link combinatorBranches}), so HAVING and WHERE accept and
|
|
787
|
+
* refuse the same values and mean the same thing by them: `AND` contributes its
|
|
788
|
+
* parts directly (the caller ANDs them), `OR: []` matches no group, a `NOT`
|
|
789
|
+
* object negates the object as a whole, and a `NOT` array negates each branch.
|
|
734
790
|
*/
|
|
735
791
|
function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys, depth = 0) {
|
|
736
|
-
const
|
|
792
|
+
const branches = (0, where_compile_js_1.combinatorBranches)(key, value, qi.table, 'having');
|
|
793
|
+
if (branches === null)
|
|
794
|
+
return [];
|
|
795
|
+
if (branches.length === 0)
|
|
796
|
+
return key === 'OR' ? ['1 = 0'] : [];
|
|
737
797
|
// Same variable-arity shape as the WHERE combinators: an ARRAY branch writes
|
|
738
798
|
// one parenthesized condition per element into the SQL text, so a
|
|
739
799
|
// caller-sized `having.OR` is a new statement per length.
|
|
@@ -745,15 +805,11 @@ function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys,
|
|
|
745
805
|
// them through the cache later cannot silently reopen the hole, and so the
|
|
746
806
|
// rule reads the same in both clause compilers. The DEPTH cap above is the
|
|
747
807
|
// half of this that bites here and now.
|
|
748
|
-
if (Array.isArray(value)
|
|
808
|
+
if (Array.isArray(value))
|
|
749
809
|
qi.markVariableArity();
|
|
750
810
|
const parts = [];
|
|
751
|
-
for (const
|
|
752
|
-
|
|
753
|
-
throw new errors_js_1.ValidationError(`Invalid having "${key}" on table "${qi.table}": expected ` +
|
|
754
|
-
`${key === 'OR' ? 'an array of having objects' : 'a having object (or an array of them)'}.`);
|
|
755
|
-
}
|
|
756
|
-
const sub = buildHavingClauses(qi, condition, params, jsonAggExprs, groupKeys, depth + 1);
|
|
811
|
+
for (const branch of branches) {
|
|
812
|
+
const sub = buildHavingClauses(qi, branch, params, jsonAggExprs, groupKeys, depth + 1);
|
|
757
813
|
if (sub.length === 0)
|
|
758
814
|
continue;
|
|
759
815
|
parts.push(sub.length === 1 ? sub[0] : `(${sub.join(' AND ')})`);
|
|
@@ -764,6 +820,8 @@ function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys,
|
|
|
764
820
|
return parts;
|
|
765
821
|
if (key === 'OR')
|
|
766
822
|
return [`(${parts.join(' OR ')})`];
|
|
823
|
+
if (Array.isArray(value))
|
|
824
|
+
return parts.map((part) => `NOT (${part})`);
|
|
767
825
|
return [`NOT (${parts.join(' AND ')})`];
|
|
768
826
|
}
|
|
769
827
|
/**
|
|
@@ -817,6 +875,7 @@ function buildHavingScalarClauses(qi, field, value, params, groupKeys) {
|
|
|
817
875
|
.join(', ')} on ${ref.label} in having for table "${qi.table}".`);
|
|
818
876
|
}
|
|
819
877
|
else {
|
|
878
|
+
(0, filters_js_1.assertEqualityOperand)(value, { column: ref.label, table: qi.table, kind: 'scalar' });
|
|
820
879
|
params.push(value);
|
|
821
880
|
clauses.push(`${ref.expr} = ${qi.p(params.length)}`);
|
|
822
881
|
}
|
|
@@ -828,14 +887,22 @@ function buildHavingScalarClauses(qi, field, value, params, groupKeys) {
|
|
|
828
887
|
* shorthand for equality. Operands are not numeric-only: `_min` / `_max`
|
|
829
888
|
* return a stored cell, so `MIN("title") > 'm'` is as valid as
|
|
830
889
|
* `SUM("views") > 10`. Unknown operator keys throw {@link ValidationError}.
|
|
890
|
+
*
|
|
891
|
+
* `operandType` is the column type a `_min` / `_max` operand is compared with.
|
|
892
|
+
* When given, each operand (and each `in` / `notIn` element) is bound through
|
|
893
|
+
* the where compiler's {@link whereMod.textComparedOperand}, so on MySQL a
|
|
894
|
+
* number compared with the MIN of a text column is sent as text rather than
|
|
895
|
+
* compared numerically (`'0zz' = 0` is true there).
|
|
831
896
|
*/
|
|
832
|
-
function buildHavingNumericClauses(qi, expr, filter, params) {
|
|
897
|
+
function buildHavingNumericClauses(qi, expr, filter, params, operandType) {
|
|
898
|
+
const bind = (value) => operandType === undefined ? value : whereMod.textComparedOperand(qi, operandType, value);
|
|
899
|
+
const bindList = (value) => operandType !== undefined && Array.isArray(value) ? value.map(bind) : value;
|
|
833
900
|
if (filter === null) {
|
|
834
901
|
throw new errors_js_1.ValidationError(`Invalid having filter on "${expr}" for table "${qi.table}": expected a value or operator object.`);
|
|
835
902
|
}
|
|
836
903
|
// Bare value (number, string, boolean, Date, …) → equality.
|
|
837
904
|
if (typeof filter !== 'object' || filter instanceof Date) {
|
|
838
|
-
params.push(filter);
|
|
905
|
+
params.push(bind(filter));
|
|
839
906
|
return [`${expr} = ${qi.p(params.length)}`];
|
|
840
907
|
}
|
|
841
908
|
const op = filter;
|
|
@@ -848,35 +915,35 @@ function buildHavingNumericClauses(qi, expr, filter, params) {
|
|
|
848
915
|
}
|
|
849
916
|
const clauses = [];
|
|
850
917
|
if (op.equals !== undefined) {
|
|
851
|
-
params.push(op.equals);
|
|
918
|
+
params.push(bind(op.equals));
|
|
852
919
|
clauses.push(`${expr} = ${qi.p(params.length)}`);
|
|
853
920
|
}
|
|
854
921
|
if (op.not !== undefined) {
|
|
855
|
-
params.push(op.not);
|
|
922
|
+
params.push(bind(op.not));
|
|
856
923
|
clauses.push(`${expr} != ${qi.p(params.length)}`);
|
|
857
924
|
}
|
|
858
925
|
if (op.gt !== undefined) {
|
|
859
|
-
params.push(op.gt);
|
|
926
|
+
params.push(bind(op.gt));
|
|
860
927
|
clauses.push(`${expr} > ${qi.p(params.length)}`);
|
|
861
928
|
}
|
|
862
929
|
if (op.gte !== undefined) {
|
|
863
|
-
params.push(op.gte);
|
|
930
|
+
params.push(bind(op.gte));
|
|
864
931
|
clauses.push(`${expr} >= ${qi.p(params.length)}`);
|
|
865
932
|
}
|
|
866
933
|
if (op.lt !== undefined) {
|
|
867
|
-
params.push(op.lt);
|
|
934
|
+
params.push(bind(op.lt));
|
|
868
935
|
clauses.push(`${expr} < ${qi.p(params.length)}`);
|
|
869
936
|
}
|
|
870
937
|
if (op.lte !== undefined) {
|
|
871
|
-
params.push(op.lte);
|
|
938
|
+
params.push(bind(op.lte));
|
|
872
939
|
clauses.push(`${expr} <= ${qi.p(params.length)}`);
|
|
873
940
|
}
|
|
874
941
|
if (op.in !== undefined) {
|
|
875
|
-
params.push(qi.inParam(op.in));
|
|
942
|
+
params.push(qi.inParam(bindList(op.in)));
|
|
876
943
|
clauses.push(qi.inClause(expr, qi.p(params.length), false));
|
|
877
944
|
}
|
|
878
945
|
if (op.notIn !== undefined) {
|
|
879
|
-
params.push(qi.inParam(op.notIn));
|
|
946
|
+
params.push(qi.inParam(bindList(op.notIn)));
|
|
880
947
|
clauses.push(qi.inClause(expr, qi.p(params.length), true));
|
|
881
948
|
}
|
|
882
949
|
return clauses;
|
|
@@ -961,9 +1028,19 @@ function isExactNumericType(pgType) {
|
|
|
961
1028
|
}
|
|
962
1029
|
/** `AVG(col)`, float-cast unless the source column is exact (see {@link isExactNumericType}). */
|
|
963
1030
|
function plainAvgExpr(qi, col) {
|
|
964
|
-
const inner =
|
|
1031
|
+
const inner = avgCall(qi, col);
|
|
965
1032
|
return isExactNumericType(whereMod.getColumnPgType(qi, col)) ? inner : qi.castAgg(inner, 'float');
|
|
966
1033
|
}
|
|
1034
|
+
/**
|
|
1035
|
+
* `AVG(col)` over a plain column, with the input widened where the engine
|
|
1036
|
+
* would otherwise average an integer column as an integer (see
|
|
1037
|
+
* {@link Dialect.averageArgument}). Shared by SELECT and HAVING, so a filter
|
|
1038
|
+
* on `_avg` compares the same value the result reports.
|
|
1039
|
+
*/
|
|
1040
|
+
function avgCall(qi, col) {
|
|
1041
|
+
const ref = qi.q(col);
|
|
1042
|
+
return `AVG(${qi.dialect.averageArgument?.(ref, whereMod.getColumnPgType(qi, col)) ?? ref})`;
|
|
1043
|
+
}
|
|
967
1044
|
/**
|
|
968
1045
|
* A `_sum` / `_avg` value over a plain column: the driver's value verbatim
|
|
969
1046
|
* for an exact source type (see {@link isExactNumericType}), a JS number for
|
|
@@ -976,6 +1053,156 @@ function sumAvgValue(qi, col, value) {
|
|
|
976
1053
|
return value;
|
|
977
1054
|
return Number(value);
|
|
978
1055
|
}
|
|
1056
|
+
// ---------------------------------------------------------------------------
|
|
1057
|
+
// _percentile (PostgreSQL: percentile_cont / percentile_disc)
|
|
1058
|
+
// ---------------------------------------------------------------------------
|
|
1059
|
+
/** Why an engine refuses `_percentile`, keyed by dialect name. */
|
|
1060
|
+
const PERCENTILE_HINT = {
|
|
1061
|
+
mysql: 'Percentile aggregates need PostgreSQL; MySQL has no percentile_cont/percentile_disc.',
|
|
1062
|
+
sqlite: 'Percentile aggregates need PostgreSQL; SQLite has percentile functions only in builds compiled with ' +
|
|
1063
|
+
'SQLITE_ENABLE_PERCENTILE, which node:sqlite cannot be assumed to have.',
|
|
1064
|
+
mssql: 'Percentile aggregates are not available on SQL Server in this release: PERCENTILE_CONT is a window ' +
|
|
1065
|
+
'function there, not an aggregate.',
|
|
1066
|
+
};
|
|
1067
|
+
const PERCENTILE_SPEC_KEYS = ['p', 'method'];
|
|
1068
|
+
const PERCENTILE_METHODS = ['cont', 'disc'];
|
|
1069
|
+
const PERCENTILE_NOT_ORDERABLE = 'ordering or filtering groups by _percentile is not supported yet.';
|
|
1070
|
+
/**
|
|
1071
|
+
* Compile `_percentile` into SELECT expressions, binding each fraction (or
|
|
1072
|
+
* fraction array) as a `float8` / `float8[]` parameter so the SQL text does not
|
|
1073
|
+
* depend on the values or on how many there are.
|
|
1074
|
+
*
|
|
1075
|
+
* Refusals, all before any SQL runs: E017 on a dialect without
|
|
1076
|
+
* `supportsPercentileAggregates`; E003 for a spec that is not `{ p, method? }`,
|
|
1077
|
+
* a fraction outside [0, 1] or not a finite number, an empty array, an unknown
|
|
1078
|
+
* method, `cont` on a date/time column (`percentile_cont` has no temporal
|
|
1079
|
+
* form), a JSON-path target, and two keys naming one column. Both methods need
|
|
1080
|
+
* `includePii: UNSAFE` on a PII column: `disc` returns a stored cell, and so
|
|
1081
|
+
* does `cont` at p = 0 or 1 or over a one-row group.
|
|
1082
|
+
*/
|
|
1083
|
+
function planPercentiles(qi, spec, params, includePii, operation, claimResultKey) {
|
|
1084
|
+
if (spec === undefined)
|
|
1085
|
+
return [];
|
|
1086
|
+
const where = `${operation} _percentile on table "${qi.table}"`;
|
|
1087
|
+
if (typeof spec !== 'object' || spec === null || Array.isArray(spec)) {
|
|
1088
|
+
throw new errors_js_1.ValidationError(`${where} must be an object of field -> { p, method? }, e.g. { latencyMs: { p: 0.95 } }.`);
|
|
1089
|
+
}
|
|
1090
|
+
const entries = Object.entries(spec).filter(([, v]) => v !== undefined && v !== null);
|
|
1091
|
+
if (entries.length === 0)
|
|
1092
|
+
return [];
|
|
1093
|
+
if (qi.dialect.supportsPercentileAggregates !== true) {
|
|
1094
|
+
throw new errors_js_1.UnsupportedFeatureError('_percentile', qi.dialect.name, PERCENTILE_HINT[qi.dialect.name] ?? 'Percentile aggregates need PostgreSQL.');
|
|
1095
|
+
}
|
|
1096
|
+
const meta = qi.schema.tables[qi.table];
|
|
1097
|
+
const plans = [];
|
|
1098
|
+
const aliases = new Set();
|
|
1099
|
+
for (const [key, entry] of entries) {
|
|
1100
|
+
const label = `${where}, field "${key}"`;
|
|
1101
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
1102
|
+
throw new errors_js_1.ValidationError(`${label}: expected { p, method? }, received ${describeValue(entry)}.`);
|
|
1103
|
+
}
|
|
1104
|
+
if ('path' in entry) {
|
|
1105
|
+
throw new errors_js_1.ValidationError(`${label}: a JSON-path target is not supported for _percentile yet.`);
|
|
1106
|
+
}
|
|
1107
|
+
for (const k of Object.keys(entry)) {
|
|
1108
|
+
if (!PERCENTILE_SPEC_KEYS.includes(k)) {
|
|
1109
|
+
const hint = (0, utils_js_1.suggestKey)(k, PERCENTILE_SPEC_KEYS);
|
|
1110
|
+
throw new errors_js_1.ValidationError(`${label}: unknown key "${k}".${hint ? ` Did you mean "${hint}"?` : ''} Expected { p, method? }.`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
const { p, method = 'cont' } = entry;
|
|
1114
|
+
if (!PERCENTILE_METHODS.includes(method)) {
|
|
1115
|
+
throw new errors_js_1.ValidationError(`${label}: method must be 'cont' or 'disc', received ${describeValue(method)}.`);
|
|
1116
|
+
}
|
|
1117
|
+
if (Array.isArray(p)) {
|
|
1118
|
+
if (p.length === 0)
|
|
1119
|
+
throw new errors_js_1.ValidationError(`${label}: p is an empty array. Give at least one fraction.`);
|
|
1120
|
+
for (const fraction of p)
|
|
1121
|
+
assertFraction(fraction, label);
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
assertFraction(p, label);
|
|
1125
|
+
}
|
|
1126
|
+
const col = qi.toColumn(key);
|
|
1127
|
+
if (method === 'cont' && qi.tableMeta.dateColumns.has(col)) {
|
|
1128
|
+
throw new errors_js_1.ValidationError(`${label}: method 'cont' interpolates and needs a numeric or interval column, and "${col}" is a ` +
|
|
1129
|
+
"date/time column. Use method: 'disc' for an actual stored value.");
|
|
1130
|
+
}
|
|
1131
|
+
assertAggregatePiiOptIn(qi.table, meta, key, col, `${operation} _percentile`, includePii);
|
|
1132
|
+
const alias = `_percentile_${col}`;
|
|
1133
|
+
if (aliases.has(alias)) {
|
|
1134
|
+
throw new errors_js_1.ValidationError(`${label}: column "${col}" is already named by another _percentile key.`);
|
|
1135
|
+
}
|
|
1136
|
+
aliases.add(alias);
|
|
1137
|
+
claimResultKey?.(alias, `_percentile of column "${col}"`);
|
|
1138
|
+
params.push(Array.isArray(p) ? [...p] : p);
|
|
1139
|
+
const fractions = `${qi.p(params.length)}::float8${Array.isArray(p) ? '[]' : ''}`;
|
|
1140
|
+
let expr = `percentile_${method}(${fractions}) WITHIN GROUP (ORDER BY ${qi.q(col)})`;
|
|
1141
|
+
// numeric[] has a driver parser that returns JS numbers, which rounds an
|
|
1142
|
+
// exact value the scalar form returns as text. Read the array as text too.
|
|
1143
|
+
if (Array.isArray(p) && method === 'disc' && isDecimalType(whereMod.getColumnPgType(qi, col))) {
|
|
1144
|
+
expr = `(${expr})::text[]`;
|
|
1145
|
+
}
|
|
1146
|
+
plans.push({
|
|
1147
|
+
expr: `${expr} AS ${qi.q(alias)}`,
|
|
1148
|
+
alias,
|
|
1149
|
+
field: qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col),
|
|
1150
|
+
col,
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
return plans;
|
|
1154
|
+
}
|
|
1155
|
+
function assertFraction(value, label) {
|
|
1156
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1)
|
|
1157
|
+
return;
|
|
1158
|
+
throw new errors_js_1.ValidationError(`${label}: p must be a fraction from 0 to 1 (0.95 for the 95th percentile), or an array of them, ` +
|
|
1159
|
+
`received ${describeValue(value)}.`);
|
|
1160
|
+
}
|
|
1161
|
+
function describeValue(value) {
|
|
1162
|
+
if (typeof value === 'string')
|
|
1163
|
+
return `'${value}'`;
|
|
1164
|
+
if (Array.isArray(value))
|
|
1165
|
+
return 'an array';
|
|
1166
|
+
if (value !== null && typeof value === 'object')
|
|
1167
|
+
return 'an object';
|
|
1168
|
+
return String(value);
|
|
1169
|
+
}
|
|
1170
|
+
function isDecimalType(pgType) {
|
|
1171
|
+
const paren = pgType.indexOf('(');
|
|
1172
|
+
const base = (paren === -1 ? pgType : pgType.slice(0, paren)).trim().toLowerCase();
|
|
1173
|
+
return base === 'numeric' || base === 'decimal';
|
|
1174
|
+
}
|
|
1175
|
+
/** The `_percentile` block of one result row, keyed by canonical field. */
|
|
1176
|
+
function readPercentiles(qi, plans, row) {
|
|
1177
|
+
const out = {};
|
|
1178
|
+
for (const plan of plans) {
|
|
1179
|
+
const value = row[plan.alias];
|
|
1180
|
+
(0, filters_js_1.setOwn)(out, plan.field, Array.isArray(value)
|
|
1181
|
+
? value.map((element) => percentileElement(qi, plan.col, element))
|
|
1182
|
+
: percentileElement(qi, plan.col, value));
|
|
1183
|
+
}
|
|
1184
|
+
return out;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* One percentile value, read the way the column itself reads: a date/time
|
|
1188
|
+
* value through {@link temporalAggValue} (element by element in the array
|
|
1189
|
+
* form, where the driver may hand back text or a bare infinity), and an int8
|
|
1190
|
+
* that the array form delivers as text through the same exact-number rule the
|
|
1191
|
+
* scalar int8 parser applies. `null` over zero rows.
|
|
1192
|
+
*/
|
|
1193
|
+
function percentileElement(qi, col, value) {
|
|
1194
|
+
if (value === null || value === undefined)
|
|
1195
|
+
return null;
|
|
1196
|
+
if (typeof value === 'string' && isInt8Type(whereMod.getColumnPgType(qi, col))) {
|
|
1197
|
+
const n = Number(value);
|
|
1198
|
+
return Number.isSafeInteger(n) ? n : value;
|
|
1199
|
+
}
|
|
1200
|
+
return temporalAggValue(qi, col, value);
|
|
1201
|
+
}
|
|
1202
|
+
function isInt8Type(pgType) {
|
|
1203
|
+
const base = pgType.trim().toLowerCase();
|
|
1204
|
+
return base === 'int8' || base === 'bigint';
|
|
1205
|
+
}
|
|
979
1206
|
function buildAggregate(qi, args) {
|
|
980
1207
|
qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
981
1208
|
// Resolved once, up front: see buildGroupBy.
|
|
@@ -1062,6 +1289,8 @@ function buildAggregate(qi, args) {
|
|
|
1062
1289
|
}
|
|
1063
1290
|
}
|
|
1064
1291
|
}
|
|
1292
|
+
const percentiles = planPercentiles(qi, args._percentile, params, includePii, 'aggregate');
|
|
1293
|
+
selectExprs.push(...percentiles.map((plan) => plan.expr));
|
|
1065
1294
|
if (selectExprs.length === 0) {
|
|
1066
1295
|
selectExprs.push(`${qi.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
1067
1296
|
}
|
|
@@ -1137,6 +1366,9 @@ function buildAggregate(qi, args) {
|
|
|
1137
1366
|
aggResult._min = minObj;
|
|
1138
1367
|
if (hasMaxs)
|
|
1139
1368
|
aggResult._max = maxObj;
|
|
1369
|
+
if (percentiles.length > 0) {
|
|
1370
|
+
aggResult._percentile = readPercentiles(qi, percentiles, row);
|
|
1371
|
+
}
|
|
1140
1372
|
return aggResult;
|
|
1141
1373
|
},
|
|
1142
1374
|
tag: `${qi.table}.aggregate`,
|
|
@@ -149,14 +149,23 @@ export declare function defaultProjectionFields(meta: TableMetadata, includePii:
|
|
|
149
149
|
visible: string[];
|
|
150
150
|
} | undefined;
|
|
151
151
|
/**
|
|
152
|
-
*
|
|
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
|