@hypequery/datasets 0.24.0 → 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/dist/cache/definition-identity.d.ts +9 -6
- package/dist/cache/definition-identity.d.ts.map +1 -1
- package/dist/cache/definition-identity.js +12 -6
- 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 +11 -3
- package/dist/catalog.d.ts +4 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +2 -0
- package/dist/contract.d.ts +3 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- package/dist/dataset-compilation.d.ts +2 -0
- package/dist/dataset-compilation.d.ts.map +1 -1
- package/dist/dataset-sql-dialect.d.ts +3 -1
- package/dist/dataset-sql-dialect.d.ts.map +1 -1
- package/dist/dataset.d.ts +3 -1
- package/dist/dataset.d.ts.map +1 -1
- package/dist/dataset.js +23 -32
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -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 +8 -1
- package/dist/protocol-adapter.d.ts.map +1 -1
- package/dist/protocol-adapter.js +1 -4
- package/dist/protocol-rehydrate.d.ts.map +1 -1
- package/dist/protocol-rehydrate.js +1 -0
- package/dist/query-builder-protocol.d.ts +3 -1
- package/dist/query-builder-protocol.d.ts.map +1 -1
- package/dist/relationship-check.d.ts.map +1 -1
- package/dist/relationship-check.js +3 -5
- package/dist/semantic-planner.d.ts.map +1 -1
- package/dist/semantic-planner.js +35 -15
- package/dist/semantic-query-schema.d.ts +6 -0
- package/dist/semantic-query-schema.d.ts.map +1 -1
- package/dist/semantic-query-schema.js +6 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +4 -3
- package/dist/types.d.ts +84 -7
- package/dist/types.d.ts.map +1 -1
- 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 +12 -15
- package/dist/utils/clickhouse-dataset-sql-dialect.d.ts.map +1 -1
- package/dist/utils/clickhouse-dataset-sql-dialect.js +3 -0
- 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 +16 -5
- 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 +19 -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 +12 -2
- package/dist/utils/dataset-source.d.ts +35 -2
- package/dist/utils/dataset-source.d.ts.map +1 -1
- package/dist/utils/dataset-source.js +47 -4
- 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/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/metric-derived-query.d.ts.map +1 -1
- package/dist/utils/metric-derived-query.js +6 -12
- 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 +7 -13
- package/dist/utils/metric-query-validation.d.ts.map +1 -1
- package/dist/utils/metric-query-validation.js +7 -1
- package/dist/utils/query-timezone.d.ts +1 -1
- package/dist/utils/query-timezone.d.ts.map +1 -1
- package/dist/utils/query-timezone.js +3 -3
- package/dist/utils/relationship-builder-plan.d.ts +12 -10
- package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
- package/dist/utils/relationship-builder-plan.js +20 -19
- package/dist/utils/relationship-measure-sql.d.ts.map +1 -1
- package/dist/utils/relationship-measure-sql.js +6 -3
- 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/snapshot-execution-context.d.ts.map +1 -1
- package/dist/utils/snapshot-execution-context.js +3 -0
- package/dist/utils/time-measure-axis-sql.d.ts +1 -1
- package/dist/utils/time-measure-axis-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-axis-sql.js +2 -1
- package/dist/utils/time-measure-leaf-sql.d.ts +1 -1
- package/dist/utils/time-measure-leaf-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-leaf-sql.js +3 -1
- package/dist/utils/time-measure-source-sql.d.ts +3 -0
- package/dist/utils/time-measure-source-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-source-sql.js +10 -9
- package/package.json +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AnyDatasetInstance, DatasetQuery } from '../types.js';
|
|
2
2
|
import type { QueryBuilderLike } from '../query-builder-protocol.js';
|
|
3
3
|
import type { DatasetQueryExecutionOptions } from '../dataset-query.js';
|
|
4
|
-
export declare function buildDatasetQueryBuilder(
|
|
4
|
+
export declare function buildDatasetQueryBuilder(dataset: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions): QueryBuilderLike;
|
|
5
5
|
//# sourceMappingURL=build-dataset-query-builder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-dataset-query-builder.d.ts","sourceRoot":"","sources":["../../src/utils/build-dataset-query-builder.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACb,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"build-dataset-query-builder.d.ts","sourceRoot":"","sources":["../../src/utils/build-dataset-query-builder.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACb,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAgBrE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAExE,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,GACpC,gBAAgB,CAoElB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { scopedDatasetSource } from './scoped-dataset-source.js';
|
|
2
2
|
import { relationshipMeasureExpressions } from './relationship-measure-sql.js';
|
|
3
3
|
import { measureToAggregationSpec } from './dataset-normalization.js';
|
|
4
4
|
import { applyAggregationSpec } from '../query-planner.js';
|
|
@@ -7,31 +7,33 @@ import { assertRawSqlForDialect } from './raw-sql-dialect-check.js';
|
|
|
7
7
|
import { queryTimeFilter } from './query-timezone.js';
|
|
8
8
|
import { selectedTimeMeasures } from './time-query-measures.js';
|
|
9
9
|
import { baseMeasureNames, getBaseMeasure } from './dataset-measures.js';
|
|
10
|
-
import { appendOrderLimitOffset, applyMeasureDefinition, buildDimensionSelectionPlan, resolveDimensionExpression, resolveFilterField,
|
|
10
|
+
import { appendOrderLimitOffset, applyMeasureDefinition, buildDimensionSelectionPlan, resolveDimensionExpression, resolveFilterField, } from '../query-planner.js';
|
|
11
11
|
import { validateDatasetQueryInput } from './dataset-query-validation.js';
|
|
12
|
-
import {
|
|
13
|
-
import { applyRelationshipJoins, buildRelationshipBuilderContext, qualifyBaseColumn, } from './relationship-builder-plan.js';
|
|
12
|
+
import { buildRelationshipBuilderContext } from './relationship-builder-plan.js';
|
|
14
13
|
import { hasSelectedDerivedMeasure, } from './dataset-derived-query.js';
|
|
15
14
|
import { segmentFilters } from './segments.js';
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
import { datasetOnTimeKey } from './dataset-time-keys.js';
|
|
16
|
+
export function buildDatasetQueryBuilder(dataset, query, options) {
|
|
17
|
+
if (selectedTimeMeasures(dataset, query).size) {
|
|
18
18
|
throw new Error('A time-based dataset query needs the time-measure SQL planner; use createDatasetClient().toSQL().');
|
|
19
19
|
}
|
|
20
|
-
if (hasSelectedDerivedMeasure(
|
|
20
|
+
if (hasSelectedDerivedMeasure(dataset, query)) {
|
|
21
21
|
throw new Error('A derived dataset query needs the outer SQL projection; use createDatasetClient().toSQL().');
|
|
22
22
|
}
|
|
23
23
|
if (query.having?.length) {
|
|
24
24
|
throw new Error('A dataset query with having needs the outer SQL projection; use createDatasetClient().toSQL().');
|
|
25
25
|
}
|
|
26
|
-
const validation = validateDatasetQueryInput(
|
|
26
|
+
const validation = validateDatasetQueryInput(dataset, query, options.context);
|
|
27
27
|
if (!validation.valid) {
|
|
28
28
|
throw new Error(`Invalid dataset query: ${validation.errors.join('; ')}`);
|
|
29
29
|
}
|
|
30
|
+
const ds = datasetOnTimeKey(dataset, query.timeKey);
|
|
30
31
|
const dialect = resolveDatasetSqlDialect(options.builderFactory);
|
|
31
32
|
assertRawSqlForDialect(ds, dialect);
|
|
32
33
|
const joinCtx = buildRelationshipBuilderContext(ds, query, options.context, dialect);
|
|
33
|
-
let qb =
|
|
34
|
-
|
|
34
|
+
let qb = scopedDatasetSource({
|
|
35
|
+
builderFactory: options.builderFactory, ds, context: options.context, joinCtx, dialect, timezone: query.timezone,
|
|
36
|
+
});
|
|
35
37
|
const { selectParts, groupByParts } = buildDimensionSelectionPlan(ds, query.dimensions ?? [], query.by, joinCtx, query.timezone, dialect);
|
|
36
38
|
const measureNames = query.measures ?? baseMeasureNames(ds.measures);
|
|
37
39
|
if (selectParts.length > 0) {
|
|
@@ -51,11 +53,6 @@ export function buildDatasetQueryBuilder(ds, query, options) {
|
|
|
51
53
|
throw new Error(`Measure "${measureName}" is not a base measure.`);
|
|
52
54
|
qb = applyMeasureDefinition(qb, ds, measureName, definition, joinCtx, dialect);
|
|
53
55
|
}
|
|
54
|
-
const tenantColumn = resolveTenantFilterColumn(ds, options.context);
|
|
55
|
-
const tenantPredicate = getRuntimeTenantPredicate(options.context);
|
|
56
|
-
if (tenantPredicate && tenantColumn) {
|
|
57
|
-
qb = qb.where(qualifyBaseColumn(joinCtx, tenantColumn), tenantPredicate.operator, tenantPredicate.value);
|
|
58
|
-
}
|
|
59
56
|
for (const filter of query.filters ?? []) {
|
|
60
57
|
const resolvedField = resolveFilterField(ds, filter.field, joinCtx);
|
|
61
58
|
for (const predicate of queryTimeFilter(dialect, ds, filter, resolvedField, query.timezone)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clickhouse-dataset-sql-dialect.d.ts","sourceRoot":"","sources":["../../src/utils/clickhouse-dataset-sql-dialect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA4B,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAwClG,4EAA4E;AAC5E,eAAO,MAAM,2BAA2B,EAAE,
|
|
1
|
+
{"version":3,"file":"clickhouse-dataset-sql-dialect.d.ts","sourceRoot":"","sources":["../../src/utils/clickhouse-dataset-sql-dialect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA4B,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAwClG,4EAA4E;AAC5E,eAAO,MAAM,2BAA2B,EAAE,sBAiHP,CAAC"}
|
|
@@ -69,6 +69,9 @@ export const clickhouseDatasetSqlDialect = Object.freeze({
|
|
|
69
69
|
throw new Error(`Unsupported literal type in measure filter: ${typeof value}`);
|
|
70
70
|
},
|
|
71
71
|
truncateTime: (time, grain) => `${TRUNCATE_FUNCTIONS[grain]}(${time})`,
|
|
72
|
+
// Calendar truncation returns a zone-less Date; later day arithmetic on it
|
|
73
|
+
// would run in the server zone, not the query zone.
|
|
74
|
+
truncateTimeInZone: (time, grain, timezone) => `toDateTime64(${TRUNCATE_FUNCTIONS[grain]}(${time}), 9, ${timezone})`,
|
|
72
75
|
addTime: (time, amount, grain) => `add${INTERVAL_UNITS[grain]}(${time}, ${amount})`,
|
|
73
76
|
subtractTime: (time, amount, grain) => `subtract${INTERVAL_UNITS[grain]}(${time}, ${amount})`,
|
|
74
77
|
timeDiff: (grain, start, end) => `dateDiff('${grain}', ${start}, ${end})`,
|
|
@@ -6,5 +6,5 @@ export interface PreparedDatasetQuery {
|
|
|
6
6
|
execute(): Promise<Record<string, unknown>[]>;
|
|
7
7
|
}
|
|
8
8
|
/** The one dispatch point for dataset preview and query-builder execution. */
|
|
9
|
-
export declare function prepareDatasetQuery(
|
|
9
|
+
export declare function prepareDatasetQuery(source: AnyDatasetInstance, input: DatasetQuery, options: DatasetQueryExecutionOptions, mode?: 'execution' | 'logical'): PreparedDatasetQuery;
|
|
10
10
|
//# sourceMappingURL=compile-dataset-query.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compile-dataset-query.d.ts","sourceRoot":"","sources":["../../src/utils/compile-dataset-query.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"compile-dataset-query.d.ts","sourceRoot":"","sources":["../../src/utils/compile-dataset-query.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAA2D,MAAM,2BAA2B,CAAC;AAC7H,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAExE,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAWpE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,WAAW,EAAE,kBAAkB,CAAC;IACzC,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;CAC/C;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,EACrC,IAAI,GAAE,WAAW,GAAG,SAAuB,GAC1C,oBAAoB,CAyEtB"}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { queryReadsFinal, withFinalSafety } from './dataset-source.js';
|
|
2
|
+
import { buildRelationshipBuilderContext } from './relationship-builder-plan.js';
|
|
3
|
+
import { resolveDatasetSqlDialect } from './dataset-sql-dialect.js';
|
|
1
4
|
import { queryMeasureDefinitions } from './relationship-measures.js';
|
|
2
5
|
import { buildDatasetQueryBuilder } from './build-dataset-query-builder.js';
|
|
3
6
|
import { baseMeasureNames } from './dataset-measures.js';
|
|
@@ -8,8 +11,12 @@ import { resolveResultLimit } from './result-limits.js';
|
|
|
8
11
|
import { buildTimeMeasureDatasetSql } from './time-measure-dataset-sql.js';
|
|
9
12
|
import { selectedTimeMeasures } from './time-query-measures.js';
|
|
10
13
|
import { renderStatement } from './statement-parameters.js';
|
|
14
|
+
import { datasetOnTimeKey, selectedTimeKeyError } from './dataset-time-keys.js';
|
|
11
15
|
/** The one dispatch point for dataset preview and query-builder execution. */
|
|
12
|
-
export function prepareDatasetQuery(
|
|
16
|
+
export function prepareDatasetQuery(source, input, options, mode = 'execution') {
|
|
17
|
+
// Every planner below compiles against the selected time axis. An unknown
|
|
18
|
+
// axis is left for each planner's validation to report as a query error.
|
|
19
|
+
const dataset = selectedTimeKeyError(source, input.timeKey) ? source : datasetOnTimeKey(source, input.timeKey);
|
|
13
20
|
const query = structuredClone({
|
|
14
21
|
...input,
|
|
15
22
|
limit: mode === 'execution' ? resolveResultLimit(input.limit, dataset.limits).limit : input.limit,
|
|
@@ -24,11 +31,14 @@ export function prepareDatasetQuery(dataset, input, options, mode = 'execution')
|
|
|
24
31
|
let execute;
|
|
25
32
|
let plan;
|
|
26
33
|
const abortSignal = options.context?.abortSignal;
|
|
34
|
+
// Raw-SQL plans lose builder settings, so they carry the FINAL pin in their SQL.
|
|
35
|
+
// Called after planning, which validates the query and its enforced constraints.
|
|
36
|
+
const readsFinal = () => queryReadsFinal(dataset, buildRelationshipBuilderContext(dataset, query, options.context, resolveDatasetSqlDialect(options.builderFactory)));
|
|
27
37
|
if (selectedTimeMeasures(dataset, query).size) {
|
|
28
38
|
plan = 'time';
|
|
29
39
|
const planned = buildTimeMeasureDatasetSql(dataset, query, executionOptions);
|
|
30
|
-
const main = renderStatement(planned.statement);
|
|
31
|
-
const preflight = renderStatement(planned.preflight);
|
|
40
|
+
const main = withFinalSafety(readsFinal(), renderStatement(planned.statement));
|
|
41
|
+
const preflight = withFinalSafety(readsFinal(), renderStatement(planned.preflight));
|
|
32
42
|
statement = main;
|
|
33
43
|
preflightStatements.push(preflight);
|
|
34
44
|
execute = async () => {
|
|
@@ -38,7 +48,8 @@ export function prepareDatasetQuery(dataset, input, options, mode = 'execution')
|
|
|
38
48
|
}
|
|
39
49
|
else if (needsOuterDatasetQuery(dataset, query)) {
|
|
40
50
|
plan = hasSelectedDerivedMeasure(dataset, query) ? 'derived' : 'aggregate';
|
|
41
|
-
|
|
51
|
+
const derived = renderStatement(buildDerivedDatasetSql(dataset, query, executionOptions, buildDatasetQueryBuilder));
|
|
52
|
+
statement = withFinalSafety(readsFinal(), derived);
|
|
42
53
|
execute = () => options.builderFactory.rawQuery(statement.sql, statement.parameters, { abortSignal });
|
|
43
54
|
}
|
|
44
55
|
else {
|
|
@@ -56,7 +67,7 @@ export function prepareDatasetQuery(dataset, input, options, mode = 'execution')
|
|
|
56
67
|
filters: Object.freeze((query.filters ?? []).map(({ field, operator }) => Object.freeze({ field, operator }))),
|
|
57
68
|
having: Object.freeze((query.having ?? []).map(({ measure, operator }) => Object.freeze({ measure, operator }))),
|
|
58
69
|
segments: Object.freeze([...(query.segments ?? [])]),
|
|
59
|
-
by: query.by, timezone: query.timezone,
|
|
70
|
+
by: query.by, timeKey: query.timeKey, timezone: query.timezone,
|
|
60
71
|
effectiveLimit: query.limit, executionLimit, offset: query.offset ?? 0,
|
|
61
72
|
parameterCount: statement.parameters.length,
|
|
62
73
|
preflightStatementCount: preflightStatements.length,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Two categories of check live here:
|
|
13
13
|
*
|
|
14
|
-
* - **Identifier safety.** `source`, `tenantKey`, `timeKey`, dimension columns
|
|
14
|
+
* - **Identifier safety.** `source`, `tenantKey`, `timeKey`, `timeKeys`, dimension columns
|
|
15
15
|
* and measure fields are all interpolated into SQL as identifiers. They are
|
|
16
16
|
* validated once here rather than trusted at every call site that builds a
|
|
17
17
|
* predicate from them.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-definition-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-definition-validation.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,wBAAwB,EACxB,mBAAmB,EAGnB,sBAAsB,EAEvB,MAAM,aAAa,CAAC;AAWrB,KAAK,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AACzD,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;AAC5D,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AA2R/D;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,aAAa,EAAE,WAAW,EAAE,gBAAgB,CAAC,GAClE,IAAI,
|
|
1
|
+
{"version":3,"file":"dataset-definition-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-definition-validation.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,wBAAwB,EACxB,mBAAmB,EAGnB,sBAAsB,EAEvB,MAAM,aAAa,CAAC;AAWrB,KAAK,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AACzD,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;AAC5D,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AA2R/D;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,aAAa,EAAE,WAAW,EAAE,gBAAgB,CAAC,GAClE,IAAI,CAmFN"}
|
|
@@ -253,6 +253,25 @@ export function validateDatasetDefinition(name, config) {
|
|
|
253
253
|
assertSafeColumn(name, config.timeKey, 'timeKey');
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
+
if (config.timeKeys !== undefined) {
|
|
257
|
+
if (!Array.isArray(config.timeKeys)) {
|
|
258
|
+
fail(name, 'timeKeys must be an array of dimension names or columns.');
|
|
259
|
+
}
|
|
260
|
+
if (config.timeKey === undefined) {
|
|
261
|
+
fail(name, 'timeKeys requires timeKey, the default time axis.');
|
|
262
|
+
}
|
|
263
|
+
if (new Set(config.timeKeys).size !== config.timeKeys.length) {
|
|
264
|
+
fail(name, 'timeKeys must not contain duplicates.');
|
|
265
|
+
}
|
|
266
|
+
for (const timeKey of config.timeKeys) {
|
|
267
|
+
if (typeof timeKey !== 'string' || timeKey.trim().length === 0) {
|
|
268
|
+
fail(name, 'timeKeys entries must be non-empty strings.');
|
|
269
|
+
}
|
|
270
|
+
if (!(timeKey in (config.dimensions ?? {}))) {
|
|
271
|
+
assertSafeColumn(name, timeKey, 'timeKeys');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
256
275
|
// A dataset with no dimensions is not rejected here: the query layer already
|
|
257
276
|
// refuses an empty dataset query, which is the honest place for it — a
|
|
258
277
|
// measure-only model is a legitimate thing to define.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { AnyDatasetInstance, DatasetQuery, ExecutionContext } from '../types.js';
|
|
2
2
|
import { type ValidationResult } from '../validation.js';
|
|
3
|
-
export declare function validateDatasetQueryInput(
|
|
3
|
+
export declare function validateDatasetQueryInput(dataset: AnyDatasetInstance, query: DatasetQuery, context?: ExecutionContext): ValidationResult;
|
|
4
4
|
//# sourceMappingURL=dataset-query-validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-query-validation.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"dataset-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-query-validation.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAoB9E,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAqLlB"}
|
|
@@ -9,10 +9,19 @@ import { getRuntimeTenantPredicate, validateTenantRuntime, } from './tenant-runt
|
|
|
9
9
|
import { isQualifiedField, resolveQualifiedField, } from './relationship-fields.js';
|
|
10
10
|
import { validateQualifiedFilter, validateRelationshipTenantRuntime, } from './relationship-validation.js';
|
|
11
11
|
import { segmentSelectionErrors } from './segments.js';
|
|
12
|
+
import { enforcedConstraintErrors } from './enforced-constraints.js';
|
|
12
13
|
import { analyzeTimeMeasureAxis } from './time-measure-axis.js';
|
|
13
14
|
import { datasetHavingErrors } from './dataset-having.js';
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
import { datasetOnTimeKey, selectedTimeKeyError } from './dataset-time-keys.js';
|
|
16
|
+
export function validateDatasetQueryInput(dataset, query, context) {
|
|
17
|
+
// Validate against the selected time axis, so grain and window checks see it.
|
|
18
|
+
const timeKeyError = selectedTimeKeyError(dataset, query.timeKey);
|
|
19
|
+
const ds = timeKeyError ? dataset : datasetOnTimeKey(dataset, query.timeKey);
|
|
20
|
+
const errors = [
|
|
21
|
+
...semanticQuerySyntaxErrors(query),
|
|
22
|
+
...queryTimezoneErrors(query.timezone),
|
|
23
|
+
...(timeKeyError ? [timeKeyError] : []),
|
|
24
|
+
];
|
|
16
25
|
const dimensionNames = Object.keys(ds.dimensions);
|
|
17
26
|
const measureNames = Object.keys(ds.measures);
|
|
18
27
|
const selectedDimensions = query.dimensions ?? [];
|
|
@@ -36,6 +45,7 @@ export function validateDatasetQueryInput(ds, query, context) {
|
|
|
36
45
|
if (relationshipTenantError) {
|
|
37
46
|
errors.push(relationshipTenantError);
|
|
38
47
|
}
|
|
48
|
+
errors.push(...enforcedConstraintErrors(ds, context, query.filters?.length ?? 0));
|
|
39
49
|
if (selectedDimensions.length === 0 && selectedMeasures.length === 0) {
|
|
40
50
|
errors.push(`Dataset "${ds.name}" query must select at least one dimension or measure.`);
|
|
41
51
|
}
|
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
import type { AnyDatasetInstance } from '../types.js';
|
|
2
2
|
import type { QueryBuilderFactoryLike, QueryBuilderLike } from '../query-builder-protocol.js';
|
|
3
|
+
import type { RelationshipBuilderContext } from './relationship-builder-plan.js';
|
|
3
4
|
/**
|
|
4
5
|
* The dataset's source table as a builder, read with `FINAL` when the dataset
|
|
5
|
-
*
|
|
6
|
+
* declares `rowResolution`. Every builder-path read of a dataset source goes
|
|
6
7
|
* through here so no query can skip row-version resolution.
|
|
7
8
|
*/
|
|
8
|
-
export declare function datasetSourceBuilder(builderFactory: Pick<QueryBuilderFactoryLike, 'table'>, ds: Pick<AnyDatasetInstance, 'name' | 'source' | '
|
|
9
|
+
export declare function datasetSourceBuilder(builderFactory: Pick<QueryBuilderFactoryLike, 'table'>, ds: Pick<AnyDatasetInstance, 'name' | 'source' | 'rowResolution'>): QueryBuilderLike;
|
|
10
|
+
/**
|
|
11
|
+
* A filter moved to PREWHERE runs before `FINAL` and can match an older version
|
|
12
|
+
* of a row. The server default forbids that; pinning it also covers profiles
|
|
13
|
+
* that enable it. `readonly = 1` accepts a setting equal to the profile's and
|
|
14
|
+
* refuses it exactly where the profile is unsafe, so the query fails closed.
|
|
15
|
+
*/
|
|
16
|
+
export declare const FINAL_SAFETY_SETTINGS: {
|
|
17
|
+
readonly optimize_move_to_prewhere_if_final: 0;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Whether a query reads a source with `FINAL`: the dataset's own, or a
|
|
21
|
+
* relationship target it actually joins. Unused relationships don't count, so
|
|
22
|
+
* a plain query never carries the pin.
|
|
23
|
+
*/
|
|
24
|
+
export declare function queryReadsFinal(ds: Pick<AnyDatasetInstance, 'rowResolution'>, joinCtx: Pick<RelationshipBuilderContext, 'joins'> | undefined): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The statement with `FINAL_SAFETY_SETTINGS` in its own SQL, for paths that run
|
|
27
|
+
* rendered SQL (`rawQuery`), where builder settings don't travel.
|
|
28
|
+
*/
|
|
29
|
+
export declare function withFinalSafety<T extends {
|
|
30
|
+
sql: string;
|
|
31
|
+
}>(readsFinal: boolean, statement: T): T;
|
|
32
|
+
/**
|
|
33
|
+
* After `FINAL`, the rows that are current state: not deleted in a
|
|
34
|
+
* ReplacingMergeTree, positive in a collapsing engine. `FINAL` alone removes
|
|
35
|
+
* engine-declared deletions on current ClickHouse; the filter covers deletion
|
|
36
|
+
* columns the engine doesn't declare, older servers and unmatched cancel rows.
|
|
37
|
+
*/
|
|
38
|
+
export declare function currentRowPredicate(ds: Pick<AnyDatasetInstance, 'rowResolution'>): {
|
|
39
|
+
column: string;
|
|
40
|
+
value: 0 | 1;
|
|
41
|
+
} | undefined;
|
|
9
42
|
//# sourceMappingURL=dataset-source.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-source.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"dataset-source.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAC9F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAEjF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,cAAc,EAAE,IAAI,CAAC,uBAAuB,EAAE,OAAO,CAAC,EACtD,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,MAAM,GAAG,QAAQ,GAAG,eAAe,CAAC,GAChE,gBAAgB,CAgBlB;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;CAAqD,CAAC;AAExF;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,eAAe,CAAC,EAC7C,OAAO,EAAE,IAAI,CAAC,0BAA0B,EAAE,OAAO,CAAC,GAAG,SAAS,GAC7D,OAAO,CAET;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,GAAG,CAAC,CAI/F;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,eAAe,CAAC,GAC5C;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAA;CAAE,GAAG,SAAS,CAM9C"}
|
|
@@ -1,16 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The dataset's source table as a builder, read with `FINAL` when the dataset
|
|
3
|
-
*
|
|
3
|
+
* declares `rowResolution`. Every builder-path read of a dataset source goes
|
|
4
4
|
* through here so no query can skip row-version resolution.
|
|
5
5
|
*/
|
|
6
6
|
export function datasetSourceBuilder(builderFactory, ds) {
|
|
7
7
|
const qb = builderFactory.table(ds.source);
|
|
8
|
-
if (!ds.
|
|
8
|
+
if (!ds.rowResolution) {
|
|
9
9
|
return qb;
|
|
10
10
|
}
|
|
11
11
|
if (typeof qb.final !== 'function') {
|
|
12
|
-
throw new Error(`Dataset "${ds.name}"
|
|
12
|
+
throw new Error(`Dataset "${ds.name}" reads its source with FINAL, but the query builder does not implement final(). ` +
|
|
13
13
|
'Reading it without FINAL would count every unmerged row version, so the query is refused.');
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
const resolved = qb.final();
|
|
16
|
+
const safe = resolved.settings ? resolved.settings(FINAL_SAFETY_SETTINGS) : resolved;
|
|
17
|
+
const current = currentRowPredicate(ds);
|
|
18
|
+
// Qualified like every base column, so it stays unambiguous once joins are added.
|
|
19
|
+
return current ? safe.where(`${ds.source}.${current.column}`, 'eq', current.value) : safe;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A filter moved to PREWHERE runs before `FINAL` and can match an older version
|
|
23
|
+
* of a row. The server default forbids that; pinning it also covers profiles
|
|
24
|
+
* that enable it. `readonly = 1` accepts a setting equal to the profile's and
|
|
25
|
+
* refuses it exactly where the profile is unsafe, so the query fails closed.
|
|
26
|
+
*/
|
|
27
|
+
export const FINAL_SAFETY_SETTINGS = { optimize_move_to_prewhere_if_final: 0 };
|
|
28
|
+
/**
|
|
29
|
+
* Whether a query reads a source with `FINAL`: the dataset's own, or a
|
|
30
|
+
* relationship target it actually joins. Unused relationships don't count, so
|
|
31
|
+
* a plain query never carries the pin.
|
|
32
|
+
*/
|
|
33
|
+
export function queryReadsFinal(ds, joinCtx) {
|
|
34
|
+
return Boolean(ds.rowResolution) || Boolean(joinCtx?.joins.some(join => join.final));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The statement with `FINAL_SAFETY_SETTINGS` in its own SQL, for paths that run
|
|
38
|
+
* rendered SQL (`rawQuery`), where builder settings don't travel.
|
|
39
|
+
*/
|
|
40
|
+
export function withFinalSafety(readsFinal, statement) {
|
|
41
|
+
if (!readsFinal)
|
|
42
|
+
return statement;
|
|
43
|
+
const pins = Object.entries(FINAL_SAFETY_SETTINGS).map(([name, value]) => `${name} = ${value}`);
|
|
44
|
+
return { ...statement, sql: `${statement.sql} SETTINGS ${pins.join(', ')}` };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* After `FINAL`, the rows that are current state: not deleted in a
|
|
48
|
+
* ReplacingMergeTree, positive in a collapsing engine. `FINAL` alone removes
|
|
49
|
+
* engine-declared deletions on current ClickHouse; the filter covers deletion
|
|
50
|
+
* columns the engine doesn't declare, older servers and unmatched cancel rows.
|
|
51
|
+
*/
|
|
52
|
+
export function currentRowPredicate(ds) {
|
|
53
|
+
const policy = ds.rowResolution;
|
|
54
|
+
if (!policy || !('engine' in policy))
|
|
55
|
+
return undefined;
|
|
56
|
+
if (policy.engine === 'replacing')
|
|
57
|
+
return policy.deletedColumn === undefined ? undefined : { column: policy.deletedColumn, value: 0 };
|
|
58
|
+
return { column: policy.signColumn, value: 1 };
|
|
16
59
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
interface TimeAxes {
|
|
11
|
+
readonly name?: string;
|
|
12
|
+
readonly timeKey?: string;
|
|
13
|
+
readonly timeKeys?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/** Every time axis a query may select, the default first; empty without a `timeKey`. */
|
|
16
|
+
export declare function datasetTimeKeys(dataset: TimeAxes): string[];
|
|
17
|
+
/** The validation message for a query's `timeKey` selection, or undefined. */
|
|
18
|
+
export declare function selectedTimeKeyError(dataset: TimeAxes, timeKey: unknown): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* True when a query selects an axis other than the dataset's default. Naming
|
|
21
|
+
* the default explicitly is the same query as omitting it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function selectsAlternateTimeKey(dataset: TimeAxes, timeKey: string | undefined): timeKey is string;
|
|
24
|
+
/**
|
|
25
|
+
* The dataset as seen by a query on `timeKey`: the selected axis becomes
|
|
26
|
+
* `timeKey` and the rest stay declared, so the view is itself a valid dataset
|
|
27
|
+
* and rebasing it again is a no-op. Throws for an axis the dataset does not
|
|
28
|
+
* declare; validate first to report it as a query error.
|
|
29
|
+
*/
|
|
30
|
+
export declare function datasetOnTimeKey<TDataset extends TimeAxes>(dataset: TDataset, timeKey: string | undefined): TDataset;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=dataset-time-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-time-keys.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-time-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,UAAU,QAAQ;IAChB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,OAAO,EAAE,QAAQ,GAAG,MAAM,EAAE,CAG3D;AAED,8EAA8E;AAC9E,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAW5F;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,IAAI,MAAM,CAEzG;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,SAAS,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CASpH"}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metric-derived-query.d.ts","sourceRoot":"","sources":["../../src/utils/metric-derived-query.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAoB,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"metric-derived-query.d.ts","sourceRoot":"","sources":["../../src/utils/metric-derived-query.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAoB,MAAM,8BAA8B,CAAC;AAQ9F,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAKvF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAMrD,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,iBAAiB,EACvB,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,cAAc,EAAE,uBAAuB,EACvC,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAmHlB"}
|
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { scopedDatasetSource } from './scoped-dataset-source.js';
|
|
2
2
|
import { semanticQuerySyntaxErrors, semanticOrderDirection } from './semantic-query-syntax.js';
|
|
3
3
|
/** Metric SQL compilation; factory selection and execution belong to the client. */
|
|
4
4
|
import { resolveDatasetSqlDialect } from './dataset-sql-dialect.js';
|
|
5
5
|
import { assertRawSqlForDialect } from './raw-sql-dialect-check.js';
|
|
6
|
-
import { applyAggregationSpec, buildDimensionSelectionPlan, resolveDimensionExpression, resolveFilterField,
|
|
6
|
+
import { applyAggregationSpec, buildDimensionSelectionPlan, resolveDimensionExpression, resolveFilterField, } from '../query-planner.js';
|
|
7
7
|
import { queryTimeFilter } from './query-timezone.js';
|
|
8
8
|
import { StatementParameters } from './statement-parameters.js';
|
|
9
9
|
import { formulaExpressionSql } from './derived-measure-sql.js';
|
|
10
10
|
import { isTenantScopedFilter } from './metric-handle.js';
|
|
11
|
-
import {
|
|
12
|
-
import { applyRelationshipJoins, buildRelationshipBuilderContext, qualifyBaseColumn, } from './relationship-builder-plan.js';
|
|
11
|
+
import { buildRelationshipBuilderContext } from './relationship-builder-plan.js';
|
|
13
12
|
import { segmentFilters } from './segments.js';
|
|
14
13
|
import { validateSQLIdentifier } from '../sql-utils.js';
|
|
15
14
|
import { isQualifiedField } from './relationship-fields.js';
|
|
16
15
|
import { validateDerivedCteGrouping } from './derived-cte-validation.js';
|
|
16
|
+
import { datasetOnTimeKey } from './dataset-time-keys.js';
|
|
17
17
|
export function buildDerivedMetricSql(ref, spec, query, grain, builderFactory, context) {
|
|
18
18
|
const syntaxErrors = semanticQuerySyntaxErrors(query);
|
|
19
19
|
if (syntaxErrors.length)
|
|
20
20
|
throw new Error(syntaxErrors.join('; '));
|
|
21
21
|
const dialect = resolveDatasetSqlDialect(builderFactory);
|
|
22
22
|
assertRawSqlForDialect(ref.dataset, dialect);
|
|
23
|
-
const ds = ref.dataset;
|
|
23
|
+
const ds = datasetOnTimeKey(ref.dataset, query.timeKey);
|
|
24
24
|
const joinCtx = buildRelationshipBuilderContext(ds, query, context, dialect);
|
|
25
25
|
// Build the CTE inner query using the builder
|
|
26
|
-
let cteBuilder =
|
|
27
|
-
cteBuilder = applyRelationshipJoins(cteBuilder, joinCtx);
|
|
26
|
+
let cteBuilder = scopedDatasetSource({ builderFactory, ds, context, joinCtx, dialect, timezone: query.timezone });
|
|
28
27
|
const { selectParts, groupByParts } = buildDimensionSelectionPlan(ds, query.dimensions ?? [], grain, joinCtx, query.timezone, dialect);
|
|
29
28
|
if (selectParts.length > 0) {
|
|
30
29
|
cteBuilder = cteBuilder.select(selectParts);
|
|
@@ -50,11 +49,6 @@ export function buildDerivedMetricSql(ref, spec, query, grain, builderFactory, c
|
|
|
50
49
|
aggregateAliases.push(quoted);
|
|
51
50
|
}
|
|
52
51
|
// Filters on CTE
|
|
53
|
-
const tenantColumn = resolveTenantFilterColumn(ds, context);
|
|
54
|
-
const tenantPredicate = getRuntimeTenantPredicate(context);
|
|
55
|
-
if (tenantPredicate && tenantColumn) {
|
|
56
|
-
cteBuilder = cteBuilder.where(qualifyBaseColumn(joinCtx, tenantColumn), tenantPredicate.operator, tenantPredicate.value);
|
|
57
|
-
}
|
|
58
52
|
for (const filter of query.filters ?? []) {
|
|
59
53
|
if (isTenantScopedFilter(ds, filter, context)) {
|
|
60
54
|
throw new Error(`Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MetricRef, MetricQuery, ExecutionContext, TimeGrain } from '../types.js';
|
|
2
2
|
import type { QueryBuilderFactoryLike, QueryBuilderLike } from '../query-builder-protocol.js';
|
|
3
3
|
import type { AggregationSpec, AnyDatasetInstance } from '../types.js';
|
|
4
|
-
export declare function buildMetricQueryBuilder(ref: MetricRef, spec: AggregationSpec,
|
|
4
|
+
export declare function buildMetricQueryBuilder(ref: MetricRef, spec: AggregationSpec, dataset: AnyDatasetInstance, query: MetricQuery, grain: TimeGrain | undefined, builderFactory: QueryBuilderFactoryLike, context?: ExecutionContext): QueryBuilderLike;
|
|
5
5
|
//# sourceMappingURL=metric-query-builder.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metric-query-builder.d.ts","sourceRoot":"","sources":["../../src/utils/metric-query-builder.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"metric-query-builder.d.ts","sourceRoot":"","sources":["../../src/utils/metric-query-builder.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAa9F,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEvE,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,cAAc,EAAE,uBAAuB,EACvC,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAsDlB"}
|