@hypequery/datasets 0.23.1 → 0.25.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 +14 -0
- package/dist/aggregations.d.ts +3 -0
- package/dist/aggregations.d.ts.map +1 -1
- package/dist/aggregations.js +6 -0
- package/dist/cache/definition-identity.d.ts +9 -6
- package/dist/cache/definition-identity.d.ts.map +1 -1
- package/dist/cache/definition-identity.js +14 -8
- package/dist/cache/protocol-cache-keys.d.ts.map +1 -1
- package/dist/cache/protocol-cache-keys.js +29 -6
- package/dist/cache/query-signature.d.ts.map +1 -1
- package/dist/cache/query-signature.js +12 -1
- package/dist/catalog.d.ts +7 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +5 -2
- package/dist/client-contract/query-schema.d.ts +19 -0
- package/dist/client-contract/query-schema.d.ts.map +1 -0
- package/dist/client-contract/query-schema.js +85 -0
- package/dist/constants.d.ts +5 -4
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +7 -6
- package/dist/contract.d.ts +5 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +6 -3
- package/dist/dataset-compilation.d.ts +2 -0
- package/dist/dataset-compilation.d.ts.map +1 -1
- package/dist/dataset-sql-dialect.d.ts +119 -0
- package/dist/dataset-sql-dialect.d.ts.map +1 -1
- package/dist/dataset.d.ts +4 -2
- package/dist/dataset.d.ts.map +1 -1
- package/dist/dataset.js +23 -31
- package/dist/in-memory-backend.d.ts.map +1 -1
- package/dist/in-memory-backend.js +2 -0
- package/dist/index.d.ts +49 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -6
- package/dist/internal.d.ts +5 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +5 -1
- package/dist/measure.d.ts +9 -1
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +9 -1
- package/dist/metric-query-engine.d.ts +0 -1
- package/dist/metric-query-engine.d.ts.map +1 -1
- package/dist/metric-query-engine.js +11 -3
- package/dist/protocol-adapter.d.ts.map +1 -1
- package/dist/protocol-adapter.js +2 -3
- package/dist/protocol-deployment-adapter.d.ts.map +1 -1
- package/dist/protocol-deployment-adapter.js +7 -1
- package/dist/protocol-rehydrate.d.ts.map +1 -1
- package/dist/protocol-rehydrate.js +1 -0
- package/dist/protocol-schema-adapter.d.ts +7 -7
- package/dist/protocol-schema-adapter.d.ts.map +1 -1
- package/dist/protocol-schema-adapter.js +12 -245
- package/dist/query-builder-protocol.d.ts +34 -1
- package/dist/query-builder-protocol.d.ts.map +1 -1
- package/dist/query-planner.d.ts +14 -5
- package/dist/query-planner.d.ts.map +1 -1
- package/dist/query-planner.js +53 -34
- package/dist/relationship-check.d.ts.map +1 -1
- package/dist/relationship-check.js +8 -8
- package/dist/schemas.d.ts +17 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +16 -0
- package/dist/semantic-planner.d.ts.map +1 -1
- package/dist/semantic-planner.js +49 -13
- package/dist/semantic-query-schema.d.ts +8 -1
- package/dist/semantic-query-schema.d.ts.map +1 -1
- package/dist/semantic-query-schema.js +18 -12
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +4 -3
- package/dist/types.d.ts +105 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/aggregate-state-functions.d.ts +24 -0
- package/dist/utils/aggregate-state-functions.d.ts.map +1 -0
- package/dist/utils/aggregate-state-functions.js +60 -0
- package/dist/utils/approximate-measures.d.ts +3 -1
- package/dist/utils/approximate-measures.d.ts.map +1 -1
- package/dist/utils/approximate-measures.js +8 -2
- package/dist/utils/base-measure-definition.d.ts +16 -0
- package/dist/utils/base-measure-definition.d.ts.map +1 -1
- package/dist/utils/base-measure-definition.js +31 -0
- package/dist/utils/build-dataset-query-builder.d.ts +1 -1
- package/dist/utils/build-dataset-query-builder.d.ts.map +1 -1
- package/dist/utils/build-dataset-query-builder.js +26 -21
- package/dist/utils/by-name.d.ts +8 -0
- package/dist/utils/by-name.d.ts.map +1 -0
- package/dist/utils/by-name.js +44 -0
- package/dist/utils/calendar-shift-sql.d.ts +5 -4
- package/dist/utils/calendar-shift-sql.d.ts.map +1 -1
- package/dist/utils/calendar-shift-sql.js +8 -8
- package/dist/utils/clickhouse-dataset-sql-dialect.d.ts +2 -2
- package/dist/utils/clickhouse-dataset-sql-dialect.d.ts.map +1 -1
- package/dist/utils/clickhouse-dataset-sql-dialect.js +143 -1
- package/dist/utils/compile-dataset-query.d.ts +1 -1
- package/dist/utils/compile-dataset-query.d.ts.map +1 -1
- package/dist/utils/compile-dataset-query.js +22 -8
- package/dist/utils/composite-time-measure-sql.d.ts +3 -5
- package/dist/utils/composite-time-measure-sql.d.ts.map +1 -1
- package/dist/utils/composite-time-measure-sql.js +15 -16
- package/dist/utils/count-aggregations.d.ts +4 -0
- package/dist/utils/count-aggregations.d.ts.map +1 -0
- package/dist/utils/count-aggregations.js +6 -0
- package/dist/utils/dataset-definition-validation.d.ts +1 -1
- package/dist/utils/dataset-definition-validation.d.ts.map +1 -1
- package/dist/utils/dataset-definition-validation.js +55 -26
- package/dist/utils/dataset-derived-query.d.ts +2 -4
- package/dist/utils/dataset-derived-query.d.ts.map +1 -1
- package/dist/utils/dataset-derived-query.js +11 -14
- package/dist/utils/dataset-having.d.ts +5 -7
- package/dist/utils/dataset-having.d.ts.map +1 -1
- package/dist/utils/dataset-having.js +9 -12
- package/dist/utils/dataset-normalization.d.ts.map +1 -1
- package/dist/utils/dataset-normalization.js +1 -0
- package/dist/utils/dataset-query-validation.d.ts +1 -1
- package/dist/utils/dataset-query-validation.d.ts.map +1 -1
- package/dist/utils/dataset-query-validation.js +19 -2
- package/dist/utils/dataset-source.d.ts +42 -0
- package/dist/utils/dataset-source.d.ts.map +1 -0
- package/dist/utils/dataset-source.js +59 -0
- package/dist/utils/dataset-sql-dialect.d.ts +13 -2
- package/dist/utils/dataset-sql-dialect.d.ts.map +1 -1
- package/dist/utils/dataset-sql-dialect.js +74 -1
- package/dist/utils/dataset-time-keys.d.ts +32 -0
- package/dist/utils/dataset-time-keys.d.ts.map +1 -0
- package/dist/utils/dataset-time-keys.js +55 -0
- package/dist/utils/dataset-validation.d.ts.map +1 -1
- package/dist/utils/dataset-validation.js +7 -0
- package/dist/utils/deployable-formulas.d.ts +33 -0
- package/dist/utils/deployable-formulas.d.ts.map +1 -0
- package/dist/utils/deployable-formulas.js +102 -0
- package/dist/utils/derived-cte-validation.d.ts +5 -0
- package/dist/utils/derived-cte-validation.d.ts.map +1 -1
- package/dist/utils/derived-cte-validation.js +77 -8
- package/dist/utils/derived-measure-sql.d.ts +10 -2
- package/dist/utils/derived-measure-sql.d.ts.map +1 -1
- package/dist/utils/derived-measure-sql.js +23 -43
- package/dist/utils/enforced-constraints.d.ts +14 -0
- package/dist/utils/enforced-constraints.d.ts.map +1 -0
- package/dist/utils/enforced-constraints.js +57 -0
- package/dist/utils/filter-field-type.d.ts +14 -0
- package/dist/utils/filter-field-type.d.ts.map +1 -0
- package/dist/utils/filter-field-type.js +21 -0
- package/dist/utils/filtered-aggregation-sql.d.ts +2 -1
- package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -1
- package/dist/utils/filtered-aggregation-sql.js +18 -35
- package/dist/utils/groupable.d.ts +7 -0
- package/dist/utils/groupable.d.ts.map +1 -0
- package/dist/utils/groupable.js +8 -0
- package/dist/utils/metric-derived-query.d.ts +2 -4
- package/dist/utils/metric-derived-query.d.ts.map +1 -1
- package/dist/utils/metric-derived-query.js +58 -51
- package/dist/utils/metric-query-builder.d.ts +1 -1
- package/dist/utils/metric-query-builder.d.ts.map +1 -1
- package/dist/utils/metric-query-builder.js +22 -20
- package/dist/utils/metric-query-validation.d.ts.map +1 -1
- package/dist/utils/metric-query-validation.js +14 -1
- package/dist/utils/portable-aggregations.d.ts +1 -0
- package/dist/utils/portable-aggregations.d.ts.map +1 -1
- package/dist/utils/portable-aggregations.js +5 -0
- package/dist/utils/protocol-schema-conversion.d.ts +12 -0
- package/dist/utils/protocol-schema-conversion.d.ts.map +1 -0
- package/dist/utils/protocol-schema-conversion.js +61 -0
- package/dist/utils/query-field-references.d.ts +10 -0
- package/dist/utils/query-field-references.d.ts.map +1 -0
- package/dist/utils/query-field-references.js +38 -0
- package/dist/utils/query-timezone.d.ts +41 -3
- package/dist/utils/query-timezone.d.ts.map +1 -1
- package/dist/utils/query-timezone.js +76 -9
- package/dist/utils/raw-sql-dialect-check.d.ts +12 -0
- package/dist/utils/raw-sql-dialect-check.d.ts.map +1 -0
- package/dist/utils/raw-sql-dialect-check.js +37 -0
- package/dist/utils/relationship-builder-plan.d.ts +37 -18
- package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
- package/dist/utils/relationship-builder-plan.js +43 -28
- package/dist/utils/relationship-measure-sql.d.ts +14 -4
- package/dist/utils/relationship-measure-sql.d.ts.map +1 -1
- package/dist/utils/relationship-measure-sql.js +36 -10
- package/dist/utils/relationship-measures.d.ts.map +1 -1
- package/dist/utils/relationship-measures.js +3 -0
- package/dist/utils/relationship-validation.d.ts.map +1 -1
- package/dist/utils/relationship-validation.js +2 -0
- package/dist/utils/row-scope.d.ts +23 -0
- package/dist/utils/row-scope.d.ts.map +1 -0
- package/dist/utils/row-scope.js +26 -0
- package/dist/utils/scoped-dataset-source.d.ts +23 -0
- package/dist/utils/scoped-dataset-source.d.ts.map +1 -0
- package/dist/utils/scoped-dataset-source.js +34 -0
- package/dist/utils/semantic-tool.d.ts +2 -2
- package/dist/utils/semantic-tool.d.ts.map +1 -1
- package/dist/utils/semantic-tool.js +1 -1
- package/dist/utils/shift-measure-sql.d.ts +5 -4
- package/dist/utils/shift-measure-sql.d.ts.map +1 -1
- package/dist/utils/shift-measure-sql.js +25 -26
- package/dist/utils/shift-wall-time-sql.d.ts +2 -1
- package/dist/utils/shift-wall-time-sql.d.ts.map +1 -1
- package/dist/utils/shift-wall-time-sql.js +7 -7
- package/dist/utils/snapshot-execution-context.d.ts.map +1 -1
- package/dist/utils/snapshot-execution-context.js +3 -0
- package/dist/utils/statement-parameters.d.ts +58 -0
- package/dist/utils/statement-parameters.d.ts.map +1 -0
- package/dist/utils/statement-parameters.js +101 -0
- package/dist/utils/time-axis-calendar-guard.d.ts +2 -1
- package/dist/utils/time-axis-calendar-guard.d.ts.map +1 -1
- package/dist/utils/time-axis-calendar-guard.js +5 -7
- package/dist/utils/time-measure-axis-sql.d.ts +3 -2
- package/dist/utils/time-measure-axis-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-axis-sql.js +20 -22
- package/dist/utils/time-measure-context-sql.d.ts +3 -2
- package/dist/utils/time-measure-context-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-context-sql.js +11 -11
- package/dist/utils/time-measure-dataset-sql.d.ts +7 -5
- package/dist/utils/time-measure-dataset-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-dataset-sql.js +17 -12
- package/dist/utils/time-measure-leaf-sql.d.ts +3 -2
- package/dist/utils/time-measure-leaf-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-leaf-sql.js +19 -18
- package/dist/utils/time-measure-result-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-result-sql.js +10 -11
- package/dist/utils/time-measure-source-sql.d.ts +5 -2
- package/dist/utils/time-measure-source-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-source-sql.js +33 -23
- package/dist/utils/time-measure-values-sql.d.ts +4 -3
- package/dist/utils/time-measure-values-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-values-sql.js +26 -29
- package/dist/utils/timestamp-instants.d.ts +17 -0
- package/dist/utils/timestamp-instants.d.ts.map +1 -0
- package/dist/utils/timestamp-instants.js +34 -0
- package/dist/utils/timestamp-text.d.ts +12 -0
- package/dist/utils/timestamp-text.d.ts.map +1 -0
- package/dist/utils/timestamp-text.js +18 -0
- package/dist/utils/zod-json-schema.d.ts +8 -0
- package/dist/utils/zod-json-schema.d.ts.map +1 -0
- package/dist/utils/zod-json-schema.js +18 -0
- package/dist/utils/zod-schema-version.d.ts +19 -0
- package/dist/utils/zod-schema-version.d.ts.map +1 -0
- package/dist/utils/zod-schema-version.js +6 -0
- package/dist/utils/zod-v3-protocol-schema.d.ts +10 -0
- package/dist/utils/zod-v3-protocol-schema.d.ts.map +1 -0
- package/dist/utils/zod-v3-protocol-schema.js +164 -0
- package/dist/utils/zod-v4-protocol-schema.d.ts +12 -0
- package/dist/utils/zod-v4-protocol-schema.d.ts.map +1 -0
- package/dist/utils/zod-v4-protocol-schema.js +171 -0
- package/dist/validation.d.ts +7 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +42 -8
- package/package.json +21 -12
- package/dist/api.type-test.d.ts +0 -2
- package/dist/api.type-test.d.ts.map +0 -1
- package/dist/api.type-test.js +0 -251
- package/dist/utils/time-arithmetic-sql.d.ts +0 -4
- package/dist/utils/time-arithmetic-sql.d.ts.map +0 -1
- package/dist/utils/time-arithmetic-sql.js +0 -9
- package/dist/utils/window-aggregation-sql.d.ts +0 -5
- package/dist/utils/window-aggregation-sql.d.ts.map +0 -1
- package/dist/utils/window-aggregation-sql.js +0 -17
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multiple time axes per dataset (HQ-80).
|
|
3
|
+
*
|
|
4
|
+
* `timeKey` is a dataset's default time axis; `timeKeys` names the other axes
|
|
5
|
+
* a query may select with `query.timeKey`, such as `shipped_at` beside
|
|
6
|
+
* `created_at`. A query on another axis compiles against a view of the dataset
|
|
7
|
+
* whose `timeKey` is that axis, so grains, windows, shifts and time-range
|
|
8
|
+
* checks follow the selection without each of them knowing about it.
|
|
9
|
+
*/
|
|
10
|
+
/** Every time axis a query may select, the default first; empty without a `timeKey`. */
|
|
11
|
+
export function datasetTimeKeys(dataset) {
|
|
12
|
+
if (!dataset.timeKey)
|
|
13
|
+
return [];
|
|
14
|
+
return [dataset.timeKey, ...(dataset.timeKeys ?? []).filter(key => key !== dataset.timeKey)];
|
|
15
|
+
}
|
|
16
|
+
/** The validation message for a query's `timeKey` selection, or undefined. */
|
|
17
|
+
export function selectedTimeKeyError(dataset, timeKey) {
|
|
18
|
+
if (timeKey === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (typeof timeKey !== 'string')
|
|
21
|
+
return 'Invalid timeKey: expected a string.';
|
|
22
|
+
const keys = datasetTimeKeys(dataset);
|
|
23
|
+
if (keys.length === 0) {
|
|
24
|
+
return `Cannot select timeKey "${timeKey}" — dataset "${dataset.name}" has no timeKey.`;
|
|
25
|
+
}
|
|
26
|
+
if (!keys.includes(timeKey)) {
|
|
27
|
+
return `Unknown timeKey "${timeKey}" on dataset "${dataset.name}". Available: ${keys.join(', ')}`;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* True when a query selects an axis other than the dataset's default. Naming
|
|
33
|
+
* the default explicitly is the same query as omitting it.
|
|
34
|
+
*/
|
|
35
|
+
export function selectsAlternateTimeKey(dataset, timeKey) {
|
|
36
|
+
return timeKey !== undefined && timeKey !== dataset.timeKey;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The dataset as seen by a query on `timeKey`: the selected axis becomes
|
|
40
|
+
* `timeKey` and the rest stay declared, so the view is itself a valid dataset
|
|
41
|
+
* and rebasing it again is a no-op. Throws for an axis the dataset does not
|
|
42
|
+
* declare; validate first to report it as a query error.
|
|
43
|
+
*/
|
|
44
|
+
export function datasetOnTimeKey(dataset, timeKey) {
|
|
45
|
+
if (!selectsAlternateTimeKey(dataset, timeKey))
|
|
46
|
+
return dataset;
|
|
47
|
+
const error = selectedTimeKeyError(dataset, timeKey);
|
|
48
|
+
if (error)
|
|
49
|
+
throw new Error(error);
|
|
50
|
+
return {
|
|
51
|
+
...dataset,
|
|
52
|
+
timeKey,
|
|
53
|
+
timeKeys: Object.freeze(datasetTimeKeys(dataset).filter(key => key !== timeKey)),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"dataset-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAarB,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,eAAe,EACrB,OAAO,CAAC,EAAE;IAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAE,GACvC,IAAI,CAiEN;AAED,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,mBAAmB,CAAC,MAAM,CAAC,GAClC,IAAI,CAmBN"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateFilterValue } from '../validation.js';
|
|
2
2
|
import { validatePercentileLevel } from '../measure.js';
|
|
3
|
+
import { assertAggregateStateFunction } from './aggregate-state-functions.js';
|
|
3
4
|
const NUMERIC_FIELD_TYPES = new Set(['number']);
|
|
4
5
|
// Aggregations whose output tracks the input field's type, so a metric
|
|
5
6
|
// (contract valueType: 'number') needs a numeric input dimension.
|
|
@@ -15,6 +16,12 @@ export function validateBaseMetric(ds, metricName, spec, options) {
|
|
|
15
16
|
throw new Error(`Invalid metric "${metricName}": measure filters are not supported on ${spec.aggregation} aggregations.`);
|
|
16
17
|
}
|
|
17
18
|
}
|
|
19
|
+
if (spec.aggregation === 'merge') {
|
|
20
|
+
assertAggregateStateFunction(spec.stateFunction, `Invalid metric "${metricName}":`);
|
|
21
|
+
if (spec.filters?.length) {
|
|
22
|
+
throw new Error(`Invalid metric "${metricName}": measure filters are not supported on merge aggregations.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
18
25
|
if (spec.aggregation === 'percentile') {
|
|
19
26
|
if (spec.level == null) {
|
|
20
27
|
throw new Error(`Invalid metric "${metricName}": percentile() requires a level.`);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subset of derived-measure formulas a deployment contract accepts.
|
|
3
|
+
*
|
|
4
|
+
* Local planning accepts every formula the helpers can build. The RFC 0006
|
|
5
|
+
* deployment grammar is narrower: a derived measure may reference only base
|
|
6
|
+
* measures, and a literal may appear only as `round` decimals or as a
|
|
7
|
+
* `coalesce` fallback. Checking that subset against the definition, rather
|
|
8
|
+
* than the emitted contract, lets the failure name the dataset and measure
|
|
9
|
+
* instead of a JSON path such as `$.datasets[2].measures[19].expression.right`.
|
|
10
|
+
*/
|
|
11
|
+
import type { AnyDatasetInstance, DatasetMeasureDefinition, DerivedMeasureDefinition } from '../types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Why `definition` cannot be deployed, or `undefined` when it can.
|
|
14
|
+
*
|
|
15
|
+
* `measures` are the measures of the dataset that owns the definition. The
|
|
16
|
+
* dataset has already validated references, aliases and cycles, so only the
|
|
17
|
+
* deployment-specific restrictions are reported here.
|
|
18
|
+
*/
|
|
19
|
+
export declare function deployableFormulaIssue(definition: DerivedMeasureDefinition, measures: Readonly<Record<string, DatasetMeasureDefinition>>): string | undefined;
|
|
20
|
+
/** Every `[measure name, issue]` pair among a dataset's derived measures, in definition order. */
|
|
21
|
+
export declare function deployableMeasureIssues(measures: Readonly<Record<string, DatasetMeasureDefinition>> | undefined): Array<[string, string]>;
|
|
22
|
+
/**
|
|
23
|
+
* Refuse a dataset whose derived measures the deployment contract cannot carry.
|
|
24
|
+
*
|
|
25
|
+
* Local queries accept every formula the helpers build. A deployment contract
|
|
26
|
+
* accepts a narrower subset: derived measures may reference only base measures,
|
|
27
|
+
* and a literal may appear only as `round` decimals or as a `coalesce`
|
|
28
|
+
* fallback. `buildProtocolDeploymentContract` (and so `hypequery deploy`) runs
|
|
29
|
+
* this check first, so the error names the dataset and measure
|
|
30
|
+
* (`orders.doubled: ...`) instead of a contract path.
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertDeployable(dataset: AnyDatasetInstance): void;
|
|
33
|
+
//# sourceMappingURL=deployable-formulas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deployable-formulas.d.ts","sourceRoot":"","sources":["../../src/utils/deployable-formulas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AA6C1G;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,wBAAwB,EACpC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC,GAC3D,MAAM,GAAG,SAAS,CAYpB;AAED,kGAAkG;AAClG,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC,GAAG,SAAS,GACvE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAOzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAMlE"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subset of derived-measure formulas a deployment contract accepts.
|
|
3
|
+
*
|
|
4
|
+
* Local planning accepts every formula the helpers can build. The RFC 0006
|
|
5
|
+
* deployment grammar is narrower: a derived measure may reference only base
|
|
6
|
+
* measures, and a literal may appear only as `round` decimals or as a
|
|
7
|
+
* `coalesce` fallback. Checking that subset against the definition, rather
|
|
8
|
+
* than the emitted contract, lets the failure name the dataset and measure
|
|
9
|
+
* instead of a JSON path such as `$.datasets[2].measures[19].expression.right`.
|
|
10
|
+
*/
|
|
11
|
+
import { isDerivedMeasure, splitDatasetMeasures } from './dataset-measures.js';
|
|
12
|
+
const LITERAL_POSITIONS = 'literals are only accepted as round decimals or a coalesce fallback';
|
|
13
|
+
function literalIssue(expression, where) {
|
|
14
|
+
if (expression.kind !== 'literal')
|
|
15
|
+
return undefined;
|
|
16
|
+
return `a numeric literal cannot be ${where} in a deployable formula; ${LITERAL_POSITIONS}`;
|
|
17
|
+
}
|
|
18
|
+
/** Why `expression` cannot be deployed, or `undefined` when it can. */
|
|
19
|
+
function expressionIssue(expression) {
|
|
20
|
+
switch (expression.kind) {
|
|
21
|
+
case 'ref':
|
|
22
|
+
return undefined;
|
|
23
|
+
case 'literal':
|
|
24
|
+
return `a literal cannot be a deployable formula on its own; ${LITERAL_POSITIONS}`;
|
|
25
|
+
case 'binary':
|
|
26
|
+
for (const operand of [expression.left, expression.right]) {
|
|
27
|
+
const issue = literalIssue(operand, 'an arithmetic operand') ?? expressionIssue(operand);
|
|
28
|
+
if (issue !== undefined)
|
|
29
|
+
return issue;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
case 'function': {
|
|
33
|
+
const [first, second] = expression.args;
|
|
34
|
+
if (first === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
const issue = literalIssue(first, `the value passed to ${expression.name}`) ?? expressionIssue(first);
|
|
37
|
+
if (issue !== undefined || second === undefined)
|
|
38
|
+
return issue;
|
|
39
|
+
if (expression.name === 'round') {
|
|
40
|
+
return second.kind === 'literal' && typeof second.value === 'number'
|
|
41
|
+
? undefined
|
|
42
|
+
: 'round decimals must be a numeric literal in a deployable formula';
|
|
43
|
+
}
|
|
44
|
+
if (expression.name === 'coalesce' && second.kind === 'literal') {
|
|
45
|
+
return typeof second.value === 'number'
|
|
46
|
+
? undefined
|
|
47
|
+
: 'a coalesce fallback must be a number in a deployable formula; null, booleans and strings are not accepted';
|
|
48
|
+
}
|
|
49
|
+
return expressionIssue(second);
|
|
50
|
+
}
|
|
51
|
+
default:
|
|
52
|
+
return 'formula contains an unsupported expression kind';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Why `definition` cannot be deployed, or `undefined` when it can.
|
|
57
|
+
*
|
|
58
|
+
* `measures` are the measures of the dataset that owns the definition. The
|
|
59
|
+
* dataset has already validated references, aliases and cycles, so only the
|
|
60
|
+
* deployment-specific restrictions are reported here.
|
|
61
|
+
*/
|
|
62
|
+
export function deployableFormulaIssue(definition, measures) {
|
|
63
|
+
// Window and shift inputs are refused by the contract adapter itself, with
|
|
64
|
+
// the contract version as the reason; only a derived input is reported here.
|
|
65
|
+
for (const [alias, measureName] of Object.entries(definition.uses)) {
|
|
66
|
+
const input = Object.hasOwn(measures, measureName) ? measures[measureName] : undefined;
|
|
67
|
+
if (!isDerivedMeasure(input))
|
|
68
|
+
continue;
|
|
69
|
+
const spelled = alias === measureName ? `"${measureName}"` : `"${measureName}" (as "${alias}")`;
|
|
70
|
+
return `references derived measure ${spelled}; a deployable formula may only reference base measures, `
|
|
71
|
+
+ 'so inline that formula or reference the base measures directly';
|
|
72
|
+
}
|
|
73
|
+
const aliases = Object.fromEntries(Object.keys(definition.uses).map(alias => [alias, alias]));
|
|
74
|
+
return expressionIssue(definition.formula(aliases).expression);
|
|
75
|
+
}
|
|
76
|
+
/** Every `[measure name, issue]` pair among a dataset's derived measures, in definition order. */
|
|
77
|
+
export function deployableMeasureIssues(measures) {
|
|
78
|
+
const issues = [];
|
|
79
|
+
for (const [name, definition] of Object.entries(splitDatasetMeasures(measures).derived)) {
|
|
80
|
+
const issue = deployableFormulaIssue(definition, measures ?? {});
|
|
81
|
+
if (issue !== undefined)
|
|
82
|
+
issues.push([name, issue]);
|
|
83
|
+
}
|
|
84
|
+
return issues;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Refuse a dataset whose derived measures the deployment contract cannot carry.
|
|
88
|
+
*
|
|
89
|
+
* Local queries accept every formula the helpers build. A deployment contract
|
|
90
|
+
* accepts a narrower subset: derived measures may reference only base measures,
|
|
91
|
+
* and a literal may appear only as `round` decimals or as a `coalesce`
|
|
92
|
+
* fallback. `buildProtocolDeploymentContract` (and so `hypequery deploy`) runs
|
|
93
|
+
* this check first, so the error names the dataset and measure
|
|
94
|
+
* (`orders.doubled: ...`) instead of a contract path.
|
|
95
|
+
*/
|
|
96
|
+
export function assertDeployable(dataset) {
|
|
97
|
+
const [first] = deployableMeasureIssues(dataset.measures);
|
|
98
|
+
if (first !== undefined) {
|
|
99
|
+
const [name, issue] = first;
|
|
100
|
+
throw new Error(`${dataset.name}.${name}: ${issue}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The top-level GROUP BY terms of a statement. Scans rather than matching a
|
|
3
|
+
* regex, so quoted names (`limit`), string literals and nested calls with
|
|
4
|
+
* commas neither end the clause nor split a term.
|
|
5
|
+
*/
|
|
1
6
|
export declare function extractGroupByExpressions(sql: string): string[];
|
|
2
7
|
export declare function validateDerivedCteGrouping(sql: string, aggregateAliases: string[], intendedGroupBy: string[]): string[];
|
|
3
8
|
//# sourceMappingURL=derived-cte-validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"derived-cte-validation.d.ts","sourceRoot":"","sources":["../../src/utils/derived-cte-validation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"derived-cte-validation.d.ts","sourceRoot":"","sources":["../../src/utils/derived-cte-validation.ts"],"names":[],"mappings":"AA6BA;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CA4B/D;AAED,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,MAAM,EAAE,EAC1B,eAAe,EAAE,MAAM,EAAE,GACxB,MAAM,EAAE,CAkCV"}
|
|
@@ -1,16 +1,85 @@
|
|
|
1
|
+
/** Compare identifiers by name, whether or not the planner or builder quoted them. */
|
|
2
|
+
function identifierName(expression) {
|
|
3
|
+
const quote = expression[0];
|
|
4
|
+
if ((quote === '`' || quote === '"') && expression.length > 1 && expression.endsWith(quote)) {
|
|
5
|
+
return expression.slice(1, -1).split(quote + quote).join(quote);
|
|
6
|
+
}
|
|
7
|
+
return expression;
|
|
8
|
+
}
|
|
9
|
+
const GROUP_BY_START = /^GROUP\s+BY\b/i;
|
|
10
|
+
const GROUP_BY_END = /^(?:HAVING|ORDER\s+BY|LIMIT|OFFSET)\b/i;
|
|
11
|
+
/** Index just past the quoted identifier or string at `index`, or `index` when none starts there. */
|
|
12
|
+
function skipQuoted(sql, index) {
|
|
13
|
+
const quote = sql[index];
|
|
14
|
+
if (quote !== '`' && quote !== '"' && quote !== "'")
|
|
15
|
+
return index;
|
|
16
|
+
let i = index + 1;
|
|
17
|
+
while (i < sql.length) {
|
|
18
|
+
if (sql[i] === '\\') {
|
|
19
|
+
i += 2;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (sql[i] === quote) {
|
|
23
|
+
if (sql[i + 1] !== quote)
|
|
24
|
+
return i + 1;
|
|
25
|
+
i += 2;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
i++;
|
|
29
|
+
}
|
|
30
|
+
return i;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The top-level GROUP BY terms of a statement. Scans rather than matching a
|
|
34
|
+
* regex, so quoted names (`limit`), string literals and nested calls with
|
|
35
|
+
* commas neither end the clause nor split a term.
|
|
36
|
+
*/
|
|
1
37
|
export function extractGroupByExpressions(sql) {
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
38
|
+
const terms = [];
|
|
39
|
+
let depth = 0;
|
|
40
|
+
let termStart = -1;
|
|
41
|
+
let i = 0;
|
|
42
|
+
while (i < sql.length) {
|
|
43
|
+
const quotedEnd = skipQuoted(sql, i);
|
|
44
|
+
if (quotedEnd > i) {
|
|
45
|
+
i = quotedEnd;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const char = sql[i];
|
|
49
|
+
if (char === '(')
|
|
50
|
+
depth++;
|
|
51
|
+
else if (char === ')')
|
|
52
|
+
depth--;
|
|
53
|
+
else if (depth === 0) {
|
|
54
|
+
const atWord = /[A-Za-z]/.test(char) && (i === 0 || !/[\w$]/.test(sql[i - 1]));
|
|
55
|
+
if (termStart < 0 && atWord) {
|
|
56
|
+
const start = sql.slice(i).match(GROUP_BY_START);
|
|
57
|
+
if (start) {
|
|
58
|
+
i += start[0].length;
|
|
59
|
+
termStart = i;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (termStart >= 0 && atWord && GROUP_BY_END.test(sql.slice(i))) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
else if (termStart >= 0 && char === ',') {
|
|
67
|
+
terms.push(sql.slice(termStart, i));
|
|
68
|
+
termStart = i + 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
i++;
|
|
5
72
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
73
|
+
if (termStart < 0)
|
|
74
|
+
return [];
|
|
75
|
+
terms.push(sql.slice(termStart, i));
|
|
76
|
+
return terms.map(term => term.trim()).filter(Boolean);
|
|
10
77
|
}
|
|
11
78
|
export function validateDerivedCteGrouping(sql, aggregateAliases, intendedGroupBy) {
|
|
12
79
|
const errors = [];
|
|
13
|
-
const actualGroupBy = extractGroupByExpressions(sql);
|
|
80
|
+
const actualGroupBy = extractGroupByExpressions(sql).map(identifierName);
|
|
81
|
+
aggregateAliases = aggregateAliases.map(identifierName);
|
|
82
|
+
intendedGroupBy = intendedGroupBy.map(identifierName);
|
|
14
83
|
if (intendedGroupBy.length === 0 && actualGroupBy.length > 0) {
|
|
15
84
|
errors.push('Derived metric planner emitted GROUP BY for an ungrouped query.');
|
|
16
85
|
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import type { DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
|
|
2
|
+
import type { SemanticExpression } from '../semantic-plan.js';
|
|
1
3
|
import type { DerivedMeasureDefinition } from '../types.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Render a formula through the dialect. `resolveRef` maps each formula input
|
|
6
|
+
* to the SQL that reads it, and rejects inputs the formula did not declare.
|
|
7
|
+
* The one formula renderer for derived measures and derived metrics.
|
|
8
|
+
*/
|
|
9
|
+
export declare function formulaExpressionSql(dialect: DatasetSqlDialectRules, expression: SemanticExpression, resolveRef: (name: string) => string): string;
|
|
10
|
+
export declare function derivedExpressionSql(dialect: DatasetSqlDialectRules, definition: DerivedMeasureDefinition, resolve?: (name: string) => string): string;
|
|
11
|
+
export declare function derivedProjection(dialect: DatasetSqlDialectRules, name: string, definition: DerivedMeasureDefinition): string;
|
|
4
12
|
//# sourceMappingURL=derived-measure-sql.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"derived-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/derived-measure-sql.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"derived-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/derived-measure-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAE5D;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,sBAAsB,EAC/B,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GACnC,MAAM,CAeR;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,sBAAsB,EAC/B,UAAU,EAAE,wBAAwB,EACpC,OAAO,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAgC,GAC1D,MAAM,CAQR;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,sBAAsB,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,wBAAwB,GAAG,MAAM,CAE7H"}
|
|
@@ -1,49 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Render a formula through the dialect. `resolveRef` maps each formula input
|
|
3
|
+
* to the SQL that reads it, and rejects inputs the formula did not declare.
|
|
4
|
+
* The one formula renderer for derived measures and derived metrics.
|
|
5
|
+
*/
|
|
6
|
+
export function formulaExpressionSql(dialect, expression, resolveRef) {
|
|
3
7
|
switch (expression.kind) {
|
|
4
|
-
case 'ref':
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (expression.value === null)
|
|
13
|
-
return 'NULL';
|
|
14
|
-
if (typeof expression.value === 'number') {
|
|
15
|
-
if (!Number.isFinite(expression.value))
|
|
16
|
-
throw new Error('Formula contains a non-finite number.');
|
|
17
|
-
return String(expression.value);
|
|
18
|
-
}
|
|
19
|
-
if (typeof expression.value === 'boolean')
|
|
20
|
-
return expression.value ? 'true' : 'false';
|
|
21
|
-
return `'${expression.value.replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
|
|
22
|
-
}
|
|
23
|
-
case 'binary': {
|
|
24
|
-
const operators = { add: '+', subtract: '-', multiply: '*', divide: '/' };
|
|
25
|
-
const operator = operators[expression.operator];
|
|
26
|
-
if (!operator)
|
|
27
|
-
throw new Error('Formula contains an unsupported binary operator.');
|
|
28
|
-
return `(${expressionSql(expression.left, uses, resolve)} ${operator} ${expressionSql(expression.right, uses, resolve)})`;
|
|
29
|
-
}
|
|
30
|
-
case 'function': {
|
|
31
|
-
const args = expression.args.map(arg => expressionSql(arg, uses, resolve));
|
|
32
|
-
switch (expression.name) {
|
|
33
|
-
case 'nullIfZero': return `NULLIF(${args[0]}, 0)`;
|
|
34
|
-
case 'coalesce': return `COALESCE(${args.join(', ')})`;
|
|
35
|
-
case 'round': return `ROUND(${args.join(', ')})`;
|
|
36
|
-
case 'floor': return `FLOOR(${args[0]})`;
|
|
37
|
-
case 'ceil': return `CEIL(${args[0]})`;
|
|
38
|
-
default: throw new Error('Formula contains an unsupported function.');
|
|
39
|
-
}
|
|
40
|
-
}
|
|
8
|
+
case 'ref':
|
|
9
|
+
return resolveRef(expression.name);
|
|
10
|
+
case 'literal':
|
|
11
|
+
return dialect.formulaLiteral(expression.value);
|
|
12
|
+
case 'binary':
|
|
13
|
+
return dialect.formulaBinary(expression.operator, formulaExpressionSql(dialect, expression.left, resolveRef), formulaExpressionSql(dialect, expression.right, resolveRef));
|
|
14
|
+
case 'function':
|
|
15
|
+
return dialect.formulaFunction(expression.name, expression.args.map(arg => formulaExpressionSql(dialect, arg, resolveRef)));
|
|
41
16
|
}
|
|
42
17
|
}
|
|
43
|
-
export function derivedExpressionSql(definition, resolve =
|
|
18
|
+
export function derivedExpressionSql(dialect, definition, resolve = dialect.quoteIdentifier) {
|
|
44
19
|
const aliases = Object.fromEntries(Object.keys(definition.uses).map(alias => [alias, alias]));
|
|
45
|
-
return
|
|
20
|
+
return formulaExpressionSql(dialect, definition.formula(aliases).expression, name => {
|
|
21
|
+
if (!Object.hasOwn(definition.uses, name)) {
|
|
22
|
+
throw new Error(`Formula references undeclared input "${name}".`);
|
|
23
|
+
}
|
|
24
|
+
return resolve(definition.uses[name]);
|
|
25
|
+
});
|
|
46
26
|
}
|
|
47
|
-
export function derivedProjection(name, definition) {
|
|
48
|
-
return `${derivedExpressionSql(definition)} AS ${
|
|
27
|
+
export function derivedProjection(dialect, name, definition) {
|
|
28
|
+
return `${derivedExpressionSql(dialect, definition)} AS ${dialect.quoteIdentifier(name)}`;
|
|
49
29
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, ExecutionContext } from '../types.js';
|
|
2
|
+
/** Constraints are policy, not queries; a handful per call is plenty (RFC 0019). */
|
|
3
|
+
export declare const MAX_ENFORCED_CONSTRAINTS = 32;
|
|
4
|
+
/**
|
|
5
|
+
* Errors for the call's RFC 0019 enforced constraints on `ds`. A constraint
|
|
6
|
+
* the dataset cannot enforce refuses the call, with a message that does not
|
|
7
|
+
* describe the constraint: it is the provider's policy, not the caller's
|
|
8
|
+
* input. `callerFilters` is the number of filters the caller supplied, which
|
|
9
|
+
* share the dataset's filter ceiling with the constraints.
|
|
10
|
+
*/
|
|
11
|
+
export declare function enforcedConstraintErrors(ds: AnyDatasetInstance, context: ExecutionContext | undefined, callerFilters: number): string[];
|
|
12
|
+
/** Throws when the call's constraints cannot all be enforced on `ds`. */
|
|
13
|
+
export declare function assertEnforcedConstraints(ds: AnyDatasetInstance, context?: ExecutionContext): void;
|
|
14
|
+
//# sourceMappingURL=enforced-constraints.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enforced-constraints.d.ts","sourceRoot":"","sources":["../../src/utils/enforced-constraints.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAgB,MAAM,aAAa,CAAC;AAKtF,oFAAoF;AACpF,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAiB3C;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,kBAAkB,EACtB,OAAO,EAAE,gBAAgB,GAAG,SAAS,EACrC,aAAa,EAAE,MAAM,GACpB,MAAM,EAAE,CAaV;AAED,yEAAyE;AACzE,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAGlG"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { isSemanticFilterOperator } from '../constants.js';
|
|
2
|
+
import { validateFilterValue } from '../validation.js';
|
|
3
|
+
import { isQualifiedField, resolveQualifiedField } from './relationship-fields.js';
|
|
4
|
+
import { validateQualifiedFilter } from './relationship-validation.js';
|
|
5
|
+
/** Constraints are policy, not queries; a handful per call is plenty (RFC 0019). */
|
|
6
|
+
export const MAX_ENFORCED_CONSTRAINTS = 32;
|
|
7
|
+
function constraintError(ds, constraint, context) {
|
|
8
|
+
if (typeof constraint !== 'object' || constraint === null)
|
|
9
|
+
return true;
|
|
10
|
+
const filter = constraint;
|
|
11
|
+
if (typeof filter.field !== 'string' || !isSemanticFilterOperator(filter.operator))
|
|
12
|
+
return true;
|
|
13
|
+
if (isQualifiedField(filter.field)) {
|
|
14
|
+
if (!resolveQualifiedField(ds, filter.field)?.resolved)
|
|
15
|
+
return true;
|
|
16
|
+
return validateQualifiedFilter(ds, filter, context) !== null;
|
|
17
|
+
}
|
|
18
|
+
if (!Object.hasOwn(ds.filters, filter.field))
|
|
19
|
+
return true;
|
|
20
|
+
const definition = ds.filters[filter.field];
|
|
21
|
+
if (definition.operators && !definition.operators.includes(filter.operator))
|
|
22
|
+
return true;
|
|
23
|
+
const fieldType = ds.dimensions[definition.field ?? filter.field]?.fieldType;
|
|
24
|
+
return !!fieldType && validateFilterValue(filter, fieldType) !== null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Errors for the call's RFC 0019 enforced constraints on `ds`. A constraint
|
|
28
|
+
* the dataset cannot enforce refuses the call, with a message that does not
|
|
29
|
+
* describe the constraint: it is the provider's policy, not the caller's
|
|
30
|
+
* input. `callerFilters` is the number of filters the caller supplied, which
|
|
31
|
+
* share the dataset's filter ceiling with the constraints.
|
|
32
|
+
*/
|
|
33
|
+
export function enforcedConstraintErrors(ds, context, callerFilters) {
|
|
34
|
+
const constraints = context?.runtime?.constraints;
|
|
35
|
+
if (constraints === undefined)
|
|
36
|
+
return [];
|
|
37
|
+
const unenforceable = [`Dataset "${ds.name}" cannot enforce a runtime constraint.`];
|
|
38
|
+
if (!Array.isArray(constraints) || constraints.length > MAX_ENFORCED_CONSTRAINTS)
|
|
39
|
+
return unenforceable;
|
|
40
|
+
if (constraints.length === 0)
|
|
41
|
+
return [];
|
|
42
|
+
const maxFilters = ds.limits?.maxFilters;
|
|
43
|
+
if (maxFilters && constraints.length > maxFilters)
|
|
44
|
+
return unenforceable;
|
|
45
|
+
if (constraints.some(constraint => constraintError(ds, constraint, context)))
|
|
46
|
+
return unenforceable;
|
|
47
|
+
if (maxFilters && callerFilters > maxFilters - constraints.length) {
|
|
48
|
+
return [`Too many filters: ${callerFilters} (max ${maxFilters - constraints.length} under the enforced constraints)`];
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
/** Throws when the call's constraints cannot all be enforced on `ds`. */
|
|
53
|
+
export function assertEnforcedConstraints(ds, context) {
|
|
54
|
+
const [error] = enforcedConstraintErrors(ds, context, 0);
|
|
55
|
+
if (error)
|
|
56
|
+
throw new Error(error);
|
|
57
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, FieldType } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The semantic field type behind a caller filter: a filter alias, a dimension
|
|
4
|
+
* name, or a relationship-qualified target dimension. Undefined when the
|
|
5
|
+
* field does not resolve to a declared dimension.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveFilterFieldType(ds: AnyDatasetInstance, filterField: string): FieldType | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* The field type of a dimension named directly, as segment filters name them.
|
|
10
|
+
* Filter aliases are not consulted: an alias may share a dimension's name
|
|
11
|
+
* while pointing at a different dimension.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveDimensionFieldType(ds: AnyDatasetInstance, dimensionName: string): FieldType | undefined;
|
|
14
|
+
//# sourceMappingURL=filter-field-type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-field-type.d.ts","sourceRoot":"","sources":["../../src/utils/filter-field-type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGjE;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAMzG;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE9G"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isQualifiedField, resolveQualifiedField } from './relationship-fields.js';
|
|
2
|
+
/**
|
|
3
|
+
* The semantic field type behind a caller filter: a filter alias, a dimension
|
|
4
|
+
* name, or a relationship-qualified target dimension. Undefined when the
|
|
5
|
+
* field does not resolve to a declared dimension.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveFilterFieldType(ds, filterField) {
|
|
8
|
+
if (isQualifiedField(filterField)) {
|
|
9
|
+
return resolveQualifiedField(ds, filterField)?.resolved?.targetDimension.fieldType;
|
|
10
|
+
}
|
|
11
|
+
const resolvedField = ds.filters[filterField]?.field ?? filterField;
|
|
12
|
+
return ds.dimensions[resolvedField]?.fieldType;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The field type of a dimension named directly, as segment filters name them.
|
|
16
|
+
* Filter aliases are not consulted: an alias may share a dimension's name
|
|
17
|
+
* while pointing at a different dimension.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveDimensionFieldType(ds, dimensionName) {
|
|
20
|
+
return ds.dimensions[dimensionName]?.fieldType;
|
|
21
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AggregationSpec, AnyDatasetInstance } from '../types.js';
|
|
2
2
|
import type { RelationshipBuilderContext } from './relationship-builder-plan.js';
|
|
3
|
+
import type { DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
|
|
3
4
|
type DatasetShape = AnyDatasetInstance;
|
|
4
|
-
export declare function applyFilteredAggregationExpression(ds: DatasetShape, spec: AggregationSpec, fieldOrExpr: string, joinCtx
|
|
5
|
+
export declare function applyFilteredAggregationExpression(ds: DatasetShape, spec: AggregationSpec, fieldOrExpr: string, joinCtx: RelationshipBuilderContext | undefined, dialect: DatasetSqlDialectRules): string;
|
|
5
6
|
export {};
|
|
6
7
|
//# sourceMappingURL=filtered-aggregation-sql.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filtered-aggregation-sql.d.ts","sourceRoot":"","sources":["../../src/utils/filtered-aggregation-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAgB,MAAM,aAAa,CAAC;AAErF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"filtered-aggregation-sql.d.ts","sourceRoot":"","sources":["../../src/utils/filtered-aggregation-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAgB,MAAM,aAAa,CAAC;AAErF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AACjF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAExE,KAAK,YAAY,GAAG,kBAAkB,CAAC;AA6CvC,wBAAgB,kCAAkC,CAChD,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,eAAe,EACrB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,0BAA0B,GAAG,SAAS,EAC/C,OAAO,EAAE,sBAAsB,GAC9B,MAAM,CAmCR"}
|