@hypequery/datasets 0.24.0 → 0.26.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 +4 -6
- package/dist/protocol-rehydrate.d.ts.map +1 -1
- package/dist/protocol-rehydrate.js +2 -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/aggregate-state-functions.d.ts +2 -1
- package/dist/utils/aggregate-state-functions.d.ts.map +1 -1
- package/dist/utils/aggregate-state-functions.js +5 -19
- 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/portable-aggregations.d.ts +6 -3
- package/dist/utils/portable-aggregations.d.ts.map +1 -1
- package/dist/utils/portable-aggregations.js +16 -7
- 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
|
@@ -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"}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { scopedDatasetSource } from './scoped-dataset-source.js';
|
|
2
2
|
import { semanticQuerySyntaxErrors } 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, appendOrderLimitOffset, buildDimensionSelectionPlan, resolveDimensionExpression, resolveFilterField,
|
|
6
|
+
import { applyAggregationSpec, appendOrderLimitOffset, buildDimensionSelectionPlan, resolveDimensionExpression, resolveFilterField, } from '../query-planner.js';
|
|
7
7
|
import { queryTimeFilter } from './query-timezone.js';
|
|
8
8
|
import { isTenantScopedFilter } from './metric-handle.js';
|
|
9
|
-
import {
|
|
10
|
-
import { applyRelationshipJoins, buildRelationshipBuilderContext, qualifyBaseColumn, } from './relationship-builder-plan.js';
|
|
9
|
+
import { buildRelationshipBuilderContext } from './relationship-builder-plan.js';
|
|
11
10
|
import { segmentFilters } from './segments.js';
|
|
12
|
-
|
|
11
|
+
import { datasetOnTimeKey } from './dataset-time-keys.js';
|
|
12
|
+
export function buildMetricQueryBuilder(ref, spec, dataset, query, grain, builderFactory, context) {
|
|
13
13
|
const syntaxErrors = semanticQuerySyntaxErrors(query);
|
|
14
14
|
if (syntaxErrors.length)
|
|
15
15
|
throw new Error(syntaxErrors.join('; '));
|
|
16
|
+
const ds = datasetOnTimeKey(dataset, query.timeKey);
|
|
16
17
|
const dialect = resolveDatasetSqlDialect(builderFactory);
|
|
17
18
|
assertRawSqlForDialect(ds, dialect);
|
|
18
19
|
const joinCtx = buildRelationshipBuilderContext(ds, query, context, dialect);
|
|
19
|
-
let qb =
|
|
20
|
-
qb = applyRelationshipJoins(qb, joinCtx);
|
|
20
|
+
let qb = scopedDatasetSource({ builderFactory, ds, context, joinCtx, dialect, timezone: query.timezone });
|
|
21
21
|
const { selectParts, groupByParts } = buildDimensionSelectionPlan(ds, query.dimensions ?? [], grain, joinCtx, query.timezone, dialect);
|
|
22
22
|
if (selectParts.length > 0) {
|
|
23
23
|
qb = qb.select(selectParts);
|
|
@@ -28,12 +28,6 @@ export function buildMetricQueryBuilder(ref, spec, ds, query, grain, builderFact
|
|
|
28
28
|
qb = qb.groupBy(groupByParts);
|
|
29
29
|
}
|
|
30
30
|
qb = applyAggregationSpec(qb, ds, spec, ref.name, joinCtx, dialect);
|
|
31
|
-
// Tenant auto-injection
|
|
32
|
-
const tenantColumn = resolveTenantFilterColumn(ds, context);
|
|
33
|
-
const tenantPredicate = getRuntimeTenantPredicate(context);
|
|
34
|
-
if (tenantPredicate && tenantColumn) {
|
|
35
|
-
qb = qb.where(qualifyBaseColumn(joinCtx, tenantColumn), tenantPredicate.operator, tenantPredicate.value);
|
|
36
|
-
}
|
|
37
31
|
// User filters
|
|
38
32
|
for (const filter of query.filters ?? []) {
|
|
39
33
|
if (isTenantScopedFilter(ds, filter, context)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metric-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/metric-query-validation.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAI9E,OAAO,EAAsD,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"metric-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/metric-query-validation.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAI9E,OAAO,EAAsD,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAQ3G,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAuIlB"}
|
|
@@ -9,10 +9,15 @@ import { validateTenantRuntime } from './tenant-runtime.js';
|
|
|
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 { datasetOnTimeKey, selectedTimeKeyError } from './dataset-time-keys.js';
|
|
13
|
+
import { enforcedConstraintErrors } from './enforced-constraints.js';
|
|
12
14
|
export function validateMetricQueryInput(metric, query, context) {
|
|
13
15
|
const errors = [...semanticQuerySyntaxErrors(query), ...protocolMetricCapabilityErrors(metric, query), ...queryTimezoneErrors(query.timezone)];
|
|
14
16
|
const ref = getMetricRef(metric);
|
|
15
|
-
const
|
|
17
|
+
const timeKeyError = selectedTimeKeyError(ref.dataset, query.timeKey);
|
|
18
|
+
if (timeKeyError)
|
|
19
|
+
errors.push(timeKeyError);
|
|
20
|
+
const ds = timeKeyError ? ref.dataset : datasetOnTimeKey(ref.dataset, query.timeKey);
|
|
16
21
|
const dimensionNames = Object.keys(ds.dimensions);
|
|
17
22
|
const filterNames = Object.keys(ds.filters);
|
|
18
23
|
const grain = getMetricGrain(metric, query);
|
|
@@ -29,6 +34,7 @@ export function validateMetricQueryInput(metric, query, context) {
|
|
|
29
34
|
if (relationshipTenantError) {
|
|
30
35
|
errors.push(relationshipTenantError);
|
|
31
36
|
}
|
|
37
|
+
errors.push(...enforcedConstraintErrors(ds, context, query.filters?.length ?? 0));
|
|
32
38
|
if (metric.__type === 'grained_metric_ref' && query.by && query.by !== metric.grain) {
|
|
33
39
|
errors.push(`Metric "${ref.name}" is already grained by "${metric.grain}" and cannot be queried with by="${query.by}".`);
|
|
34
40
|
}
|
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
* `approxCountDistinct` runs locally, but it is an expression extension 2 and
|
|
5
5
|
* deployment contract 3 feature (RFC 0015). Publishing emits contract 2 until
|
|
6
6
|
* it can emit contract 3, so it refuses the measure with an error naming it.
|
|
7
|
-
* `merge`
|
|
7
|
+
* `merge` is a deployment measure with its state function (RFC 0022), but the
|
|
8
|
+
* expression grammar has no merge aggregate, so a metric over one is refused.
|
|
8
9
|
*/
|
|
9
|
-
import type { ProtocolAggregation } from '@hypequery/protocol';
|
|
10
|
+
import type { ProtocolAggregation, ProtocolDatasetMeasure } from '@hypequery/protocol';
|
|
10
11
|
import type { AggregationType } from '../types.js';
|
|
11
12
|
type PortableAggregation = Exclude<ProtocolAggregation, 'approxCountDistinct'>;
|
|
12
|
-
/**
|
|
13
|
+
/** A dataset measure's aggregation as the contract carries it; otherwise throws naming `owner`. */
|
|
14
|
+
export declare function requirePublishableMeasureAggregation(aggregation: AggregationType, owner: string): ProtocolDatasetMeasure['aggregation'];
|
|
15
|
+
/** Returns `aggregation` when an expression can carry it; otherwise throws naming `owner`. */
|
|
13
16
|
export declare function requirePortableAggregation(aggregation: AggregationType, owner: string): PortableAggregation;
|
|
14
17
|
export {};
|
|
15
18
|
//# sourceMappingURL=portable-aggregations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"portable-aggregations.d.ts","sourceRoot":"","sources":["../../src/utils/portable-aggregations.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"portable-aggregations.d.ts","sourceRoot":"","sources":["../../src/utils/portable-aggregations.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACvF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,KAAK,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,EAAE,qBAAqB,CAAC,CAAC;AAc/E,mGAAmG;AACnG,wBAAgB,oCAAoC,CAClD,WAAW,EAAE,eAAe,EAC5B,KAAK,EAAE,MAAM,GACZ,sBAAsB,CAAC,aAAa,CAAC,CAGvC;AAED,8FAA8F;AAC9F,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,eAAe,EAC5B,KAAK,EAAE,MAAM,GACZ,mBAAmB,CASrB"}
|
|
@@ -4,17 +4,26 @@
|
|
|
4
4
|
* `approxCountDistinct` runs locally, but it is an expression extension 2 and
|
|
5
5
|
* deployment contract 3 feature (RFC 0015). Publishing emits contract 2 until
|
|
6
6
|
* it can emit contract 3, so it refuses the measure with an error naming it.
|
|
7
|
-
* `merge`
|
|
7
|
+
* `merge` is a deployment measure with its state function (RFC 0022), but the
|
|
8
|
+
* expression grammar has no merge aggregate, so a metric over one is refused.
|
|
8
9
|
*/
|
|
9
|
-
|
|
10
|
-
export function requirePortableAggregation(aggregation, owner) {
|
|
11
|
-
if (aggregation === 'merge') {
|
|
12
|
-
throw new Error(`${owner} uses merge, which cannot be published: the deployment contract has no aggregate-state ` +
|
|
13
|
-
'measures. Use it for local queries only.');
|
|
14
|
-
}
|
|
10
|
+
function refuseApproximate(aggregation, owner) {
|
|
15
11
|
if (aggregation === 'approxCountDistinct') {
|
|
16
12
|
throw new Error(`${owner} uses approxCountDistinct, which cannot be published yet: approximate aggregations ` +
|
|
17
13
|
'need deployment contract 3 (RFC 0015). Use it for local queries, or publish countDistinct instead.');
|
|
18
14
|
}
|
|
15
|
+
}
|
|
16
|
+
/** A dataset measure's aggregation as the contract carries it; otherwise throws naming `owner`. */
|
|
17
|
+
export function requirePublishableMeasureAggregation(aggregation, owner) {
|
|
18
|
+
refuseApproximate(aggregation, owner);
|
|
19
|
+
return aggregation;
|
|
20
|
+
}
|
|
21
|
+
/** Returns `aggregation` when an expression can carry it; otherwise throws naming `owner`. */
|
|
22
|
+
export function requirePortableAggregation(aggregation, owner) {
|
|
23
|
+
if (aggregation === 'merge') {
|
|
24
|
+
throw new Error(`${owner} uses merge, which a published metric cannot express: the expression grammar has no ` +
|
|
25
|
+
'aggregate-state merge. Publish the merge measure itself and query it, or use it for local queries only.');
|
|
26
|
+
}
|
|
27
|
+
refuseApproximate(aggregation, owner);
|
|
19
28
|
return aggregation;
|
|
20
29
|
}
|
|
@@ -5,7 +5,7 @@ export declare function isQueryTimezone(value: unknown): value is string;
|
|
|
5
5
|
export declare function queryTimezoneErrors(timezone: unknown): string[];
|
|
6
6
|
/** Conversion preserves timestamp instants and interprets Date values as local dates. */
|
|
7
7
|
export declare function queryTimeSql(dialect: DatasetSqlDialectRules, expression: string, timezone: string | undefined): string;
|
|
8
|
-
/** True when a local filter field reads the dataset's time-
|
|
8
|
+
/** True when a local filter field reads one of the dataset's time-axis columns. */
|
|
9
9
|
export declare function isTimeKeyFilter(ds: AnyDatasetInstance, field: string): boolean;
|
|
10
10
|
/** Apply the query zone to time-key predicates, including semantic filter aliases. */
|
|
11
11
|
export declare function queryTimeFilterSql(dialect: DatasetSqlDialectRules, ds: AnyDatasetInstance, field: string, expression: string, timezone: string | undefined): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-timezone.d.ts","sourceRoot":"","sources":["../../src/utils/query-timezone.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"query-timezone.d.ts","sourceRoot":"","sources":["../../src/utils/query-timezone.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAM1F,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAS/D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAE,CAG/D;AASD,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEtH;AAED,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAK9E;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,sBAAsB,EAC/B,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GACtF,MAAM,CAER;AAED,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IACjC,sFAAsF;IACtF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;IACxC,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;CACzC;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,EAC/B,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,GAAE,sBAA2B,GACnC,kBAAkB,EAAE,CAwCtB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveDimensionFieldType, resolveFilterFieldType } from './filter-field-type.js';
|
|
2
2
|
import { instantWallClock, parseTimestampInstant } from './timestamp-instants.js';
|
|
3
3
|
import { mixedInstantListError } from '../validation.js';
|
|
4
|
+
import { datasetTimeKeys } from './dataset-time-keys.js';
|
|
4
5
|
/** Named zones only: reject fixed offsets, invalid zones, and SQL fragments. */
|
|
5
6
|
export function isQueryTimezone(value) {
|
|
6
7
|
if (typeof value !== 'string' || value.length > 100
|
|
@@ -29,14 +30,13 @@ function canonicalTimezone(timezone) {
|
|
|
29
30
|
export function queryTimeSql(dialect, expression, timezone) {
|
|
30
31
|
return dialect.toQueryTimezone(expression, canonicalTimezone(timezone));
|
|
31
32
|
}
|
|
32
|
-
/** True when a local filter field reads the dataset's time-
|
|
33
|
+
/** True when a local filter field reads one of the dataset's time-axis columns. */
|
|
33
34
|
export function isTimeKeyFilter(ds, field) {
|
|
34
35
|
if (!ds.timeKey || field.includes('.'))
|
|
35
36
|
return false;
|
|
36
37
|
const resolved = ds.filters[field]?.field ?? field;
|
|
37
38
|
const column = ds.dimensions[resolved]?.column ?? resolved;
|
|
38
|
-
|
|
39
|
-
return column === timeColumn;
|
|
39
|
+
return datasetTimeKeys(ds).some(timeKey => (ds.dimensions[timeKey]?.column ?? timeKey) === column);
|
|
40
40
|
}
|
|
41
41
|
/** Apply the query zone to time-key predicates, including semantic filter aliases. */
|
|
42
42
|
export function queryTimeFilterSql(dialect, ds, field, expression, timezone) {
|
|
@@ -19,7 +19,7 @@ import type { DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
|
|
|
19
19
|
*/
|
|
20
20
|
import type { AnyDatasetInstance, DatasetQuery, ExecutionContext, MetricQuery } from '../types.js';
|
|
21
21
|
import type { QueryBuilderLike } from '../query-builder-protocol.js';
|
|
22
|
-
import { type
|
|
22
|
+
import { type RowScopePredicate } from './row-scope.js';
|
|
23
23
|
export interface ResolvedBuilderJoin {
|
|
24
24
|
/** Relationship name, used as the joined table's SQL alias. */
|
|
25
25
|
relationship: string;
|
|
@@ -32,10 +32,10 @@ export interface ResolvedBuilderJoin {
|
|
|
32
32
|
from: string;
|
|
33
33
|
/** Target join column (unqualified). */
|
|
34
34
|
to: string;
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
/** The target's own row scope (RFC 0020), applied in the join condition. */
|
|
36
|
+
scope: RowScopePredicate[];
|
|
37
|
+
/** The target is read with `FINAL` (RFC 0021), so the query needs the PREWHERE pin. */
|
|
38
|
+
final: boolean;
|
|
39
39
|
}
|
|
40
40
|
export interface RelationshipBuilderContext {
|
|
41
41
|
baseSource: string;
|
|
@@ -46,9 +46,10 @@ export interface RelationshipBuilderContext {
|
|
|
46
46
|
}
|
|
47
47
|
type QueryLike = Pick<DatasetQuery & MetricQuery, 'dimensions' | 'filters' | 'orderBy'> & Pick<DatasetQuery, 'measures'>;
|
|
48
48
|
/**
|
|
49
|
-
* Builds the join context for a query, or returns undefined when the
|
|
50
|
-
*
|
|
51
|
-
* qualified name that fails to resolve
|
|
49
|
+
* Builds the join context for a query, or returns undefined when neither the
|
|
50
|
+
* query nor the call's enforced constraints reference a relationship-qualified
|
|
51
|
+
* field. Validation runs first, so any qualified name that fails to resolve
|
|
52
|
+
* here is skipped defensively.
|
|
52
53
|
*/
|
|
53
54
|
export declare function buildRelationshipBuilderContext(ds: AnyDatasetInstance, query: QueryLike, context?: ExecutionContext, dialect?: DatasetSqlDialectRules): RelationshipBuilderContext | undefined;
|
|
54
55
|
/** Qualifies a base physical column with the base source when joins are active. */
|
|
@@ -68,8 +69,9 @@ export declare function qualifyBaseColumn(ctx: RelationshipBuilderContext | unde
|
|
|
68
69
|
export declare function resolveQualifiedExpression(ds: AnyDatasetInstance, name: string, ctx: RelationshipBuilderContext): string;
|
|
69
70
|
/**
|
|
70
71
|
* Applies each relationship join to the builder as a single-match LEFT JOIN
|
|
71
|
-
* (`leftAnyJoin`, e.g. ClickHouse `LEFT ANY JOIN`)
|
|
72
|
-
*
|
|
72
|
+
* (`leftAnyJoin`, e.g. ClickHouse `LEFT ANY JOIN`), scoping the joined rows
|
|
73
|
+
* with the target's own row scope: an out-of-scope target row behaves exactly
|
|
74
|
+
* like a missing one.
|
|
73
75
|
*
|
|
74
76
|
* A builder without `leftAnyJoin` is refused rather than downgraded to a plain
|
|
75
77
|
* `leftJoin`: duplicate target keys would fan out base rows and silently
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationship-builder-plan.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-builder-plan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAGxE;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,
|
|
1
|
+
{"version":3,"file":"relationship-builder-plan.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-builder-plan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAGxE;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAuC,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAG7F,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IAClC,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,uFAAuF;IACvF,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACrD,oEAAoE;IACpE,OAAO,EAAE,sBAAsB,CAAC;CACjC;AAED,KAAK,SAAS,GAAG,IAAI,CAAC,YAAY,GAAG,WAAW,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;AAEzH;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,SAAS,EAChB,OAAO,CAAC,EAAE,gBAAgB,EAC1B,OAAO,GAAE,sBAAoD,GAC5D,0BAA0B,GAAG,SAAS,CA+CxC;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,0BAA0B,GAAG,SAAS,EAC3C,MAAM,EAAE,MAAM,GACb,MAAM,CAER;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,0BAA0B,GAC9B,MAAM,CAaR;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,0BAA0B,GAAG,SAAS,GAC1C,gBAAgB,CAoClB"}
|
|
@@ -2,12 +2,13 @@ import { relationshipKeys } from './relationship-keys.js';
|
|
|
2
2
|
import { relationshipJoinSource } from './relationship-measure-sql.js';
|
|
3
3
|
import { clickhouseDatasetSqlDialect } from './clickhouse-dataset-sql-dialect.js';
|
|
4
4
|
import { resolveRelationshipReference } from './relationship-measures.js';
|
|
5
|
-
import {
|
|
5
|
+
import { datasetRowScope, runtimeConstraints } from './row-scope.js';
|
|
6
6
|
import { isQualifiedField, resolveQualifiedField } from './relationship-fields.js';
|
|
7
7
|
/**
|
|
8
|
-
* Builds the join context for a query, or returns undefined when the
|
|
9
|
-
*
|
|
10
|
-
* qualified name that fails to resolve
|
|
8
|
+
* Builds the join context for a query, or returns undefined when neither the
|
|
9
|
+
* query nor the call's enforced constraints reference a relationship-qualified
|
|
10
|
+
* field. Validation runs first, so any qualified name that fails to resolve
|
|
11
|
+
* here is skipped defensively.
|
|
11
12
|
*/
|
|
12
13
|
export function buildRelationshipBuilderContext(ds, query, context, dialect = clickhouseDatasetSqlDialect) {
|
|
13
14
|
const referenced = [
|
|
@@ -15,11 +16,11 @@ export function buildRelationshipBuilderContext(ds, query, context, dialect = cl
|
|
|
15
16
|
...(query.measures ?? []),
|
|
16
17
|
...(query.filters ?? []).map((filter) => filter.field),
|
|
17
18
|
...(query.orderBy ?? []).map((order) => order.field),
|
|
19
|
+
...runtimeConstraints(context).map((constraint) => constraint.field),
|
|
18
20
|
].filter(isQualifiedField);
|
|
19
21
|
if (referenced.length === 0) {
|
|
20
22
|
return undefined;
|
|
21
23
|
}
|
|
22
|
-
const tenantPredicate = getRuntimeTenantPredicate(context);
|
|
23
24
|
const joinByRelationship = new Map();
|
|
24
25
|
for (const name of referenced) {
|
|
25
26
|
const resolution = resolveRelationshipReference(ds, name, query.measures);
|
|
@@ -42,9 +43,8 @@ export function buildRelationshipBuilderContext(ds, query, context, dialect = cl
|
|
|
42
43
|
from: relationship.from,
|
|
43
44
|
to: relationship.to,
|
|
44
45
|
keys,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
: undefined,
|
|
46
|
+
scope: datasetRowScope(target, context),
|
|
47
|
+
final: Boolean(target.rowResolution),
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
return {
|
|
@@ -86,8 +86,9 @@ export function resolveQualifiedExpression(ds, name, ctx) {
|
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
88
|
* Applies each relationship join to the builder as a single-match LEFT JOIN
|
|
89
|
-
* (`leftAnyJoin`, e.g. ClickHouse `LEFT ANY JOIN`)
|
|
90
|
-
*
|
|
89
|
+
* (`leftAnyJoin`, e.g. ClickHouse `LEFT ANY JOIN`), scoping the joined rows
|
|
90
|
+
* with the target's own row scope: an out-of-scope target row behaves exactly
|
|
91
|
+
* like a missing one.
|
|
91
92
|
*
|
|
92
93
|
* A builder without `leftAnyJoin` is refused rather than downgraded to a plain
|
|
93
94
|
* `leftJoin`: duplicate target keys would fan out base rows and silently
|
|
@@ -104,20 +105,20 @@ export function applyRelationshipJoins(qb, ctx) {
|
|
|
104
105
|
'would fan out duplicate target keys and inflate aggregates, so relationship-qualified fields ' +
|
|
105
106
|
'are unavailable with this builder.');
|
|
106
107
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
const conditions = join.scope.map(predicate => ({
|
|
109
|
+
column: `${join.relationship}.${predicate.column}`,
|
|
110
|
+
operator: predicate.operator,
|
|
111
|
+
value: predicate.value,
|
|
112
|
+
}));
|
|
113
|
+
// A single condition keeps the object form older builders accept.
|
|
114
|
+
const scope = conditions.length > 1 ? conditions : conditions[0];
|
|
114
115
|
if (join.keys && join.keys.length > 1) {
|
|
115
116
|
// Key-pair form; @hypequery/clickhouse >= 2.13.0.
|
|
116
117
|
const pairs = join.keys.map(key => [`${ctx.baseSource}.${key.from}`, `${join.relationship}.${key.to}`]);
|
|
117
|
-
qb = qb.leftAnyJoin(join.source, [pairs[0], ...pairs.slice(1)], join.relationship,
|
|
118
|
+
qb = qb.leftAnyJoin(join.source, [pairs[0], ...pairs.slice(1)], join.relationship, scope);
|
|
118
119
|
continue;
|
|
119
120
|
}
|
|
120
|
-
qb = qb.leftAnyJoin(join.source, `${ctx.baseSource}.${join.from}`, `${join.relationship}.${join.to}`, join.relationship,
|
|
121
|
+
qb = qb.leftAnyJoin(join.source, `${ctx.baseSource}.${join.from}`, `${join.relationship}.${join.to}`, join.relationship, scope);
|
|
121
122
|
}
|
|
122
123
|
return qb;
|
|
123
124
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationship-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-measure-sql.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"relationship-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-measure-sql.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAmB,MAAM,aAAa,CAAC;AAEvE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAIjF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAExE;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,sBAAsB;;;EAoBlJ;AAeD,wBAAgB,8BAA8B,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,0BAA0B,EAAE,OAAO,EAAE,sBAAsB;;;;EAexJ"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currentRowPredicate } from './dataset-source.js';
|
|
1
2
|
import { resolveRelationshipMeasure } from './relationship-measures.js';
|
|
2
3
|
import { resolveDimensionExpression } from '../query-planner.js';
|
|
3
4
|
import { measureToAggregationSpec } from './dataset-normalization.js';
|
|
@@ -35,15 +36,17 @@ export function relationshipJoinSource(target, to, names, dialect) {
|
|
|
35
36
|
// Sorted so the SQL is byte-identical however the target's dimensions are
|
|
36
37
|
// ordered (authored vs. rehydrated catalogs declare them differently).
|
|
37
38
|
const projection = [...columns].sort().map(quote);
|
|
38
|
-
|
|
39
|
+
const current = currentRowPredicate(target);
|
|
40
|
+
const where = current ? ` WHERE ${quote(current.column)} = ${current.value}` : '';
|
|
41
|
+
return { source: `(SELECT ${[...projection, `${dialect.toNullable('1')} AS ${quote(marker)}`].join(', ')} FROM ${source}${where})`, marker };
|
|
39
42
|
}
|
|
40
43
|
/** The target's quoted table, read with `FINAL` when the target dataset requires it. */
|
|
41
44
|
function relationshipTargetTable(target, dialect) {
|
|
42
45
|
const quoted = dialect.quoteQualifiedName(target.source);
|
|
43
|
-
if (!target.
|
|
46
|
+
if (!target.rowResolution)
|
|
44
47
|
return quoted;
|
|
45
48
|
if (!dialect.finalTable) {
|
|
46
|
-
throw new Error(`Dataset "${target.name}"
|
|
49
|
+
throw new Error(`Dataset "${target.name}" reads its source with FINAL, but the "${dialect.name}" SQL dialect has no FINAL ` +
|
|
47
50
|
'modifier, so it cannot be joined without counting unmerged row versions.');
|
|
48
51
|
}
|
|
49
52
|
return dialect.finalTable(quoted);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationship-validation.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-validation.ts"],"names":[],"mappings":"AACA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACZ,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"relationship-validation.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-validation.ts"],"names":[],"mappings":"AACA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACZ,MAAM,aAAa,CAAC;AAYrB,KAAK,iBAAiB,GAAG,IAAI,CAC3B,YAAY,GAAG,WAAW,EAC1B,YAAY,GAAG,SAAS,GAAG,SAAS,CACrC,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;AAEnC;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,iBAAiB,EACxB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAqBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,IAAI,CA4Bf"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveRelationshipReference } from './relationship-measures.js';
|
|
2
2
|
import { validateFilterValue } from '../validation.js';
|
|
3
3
|
import { getRuntimeTenantPredicate, hasTenantRuntime, } from './tenant-runtime.js';
|
|
4
|
+
import { runtimeConstraints } from './row-scope.js';
|
|
4
5
|
import { isQualifiedField, resolveQualifiedField, } from './relationship-fields.js';
|
|
5
6
|
/**
|
|
6
7
|
* Tenant-less base datasets may still reach tenant-scoped data through a
|
|
@@ -16,6 +17,7 @@ export function validateRelationshipTenantRuntime(ds, query, context) {
|
|
|
16
17
|
...(query.measures ?? []),
|
|
17
18
|
...(query.filters ?? []).map((filter) => filter.field),
|
|
18
19
|
...(query.orderBy ?? []).map((order) => order.field),
|
|
20
|
+
...runtimeConstraints(context).map((constraint) => constraint.field),
|
|
19
21
|
].filter(isQualifiedField);
|
|
20
22
|
for (const name of referenced) {
|
|
21
23
|
const resolution = resolveRelationshipReference(ds, name, query.measures);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, ExecutionContext, MetricFilter } from '../types.js';
|
|
2
|
+
import type { QueryBuilderLike } from '../query-builder-protocol.js';
|
|
3
|
+
import { type TenantPredicate } from './tenant-runtime.js';
|
|
4
|
+
/** A trusted predicate on one of a dataset's own physical columns. */
|
|
5
|
+
export type RowScopePredicate = TenantPredicate & {
|
|
6
|
+
readonly column: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The trusted predicates restricting which source rows of `ds` any query may
|
|
10
|
+
* read (RFC 0020). They apply to the queried dataset and to every joined
|
|
11
|
+
* relationship target alike. `tenantKey` with a runtime tenant lowers to one
|
|
12
|
+
* predicate; the trusted cross-tenant scope (`scope: 'all'`) lowers to none.
|
|
13
|
+
*/
|
|
14
|
+
export declare function datasetRowScope(ds: Pick<AnyDatasetInstance, 'tenantKey'>, context?: ExecutionContext): RowScopePredicate[];
|
|
15
|
+
/**
|
|
16
|
+
* The call's RFC 0019 enforced constraints, from trusted context only. They
|
|
17
|
+
* apply to the queried dataset, in its filter vocabulary, never to joined
|
|
18
|
+
* targets.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runtimeConstraints(context?: ExecutionContext): readonly MetricFilter[];
|
|
21
|
+
/** ANDs each row-scope predicate onto `qb`, with columns qualified by `qualify`. */
|
|
22
|
+
export declare function applyRowScope(qb: QueryBuilderLike, scope: readonly RowScopePredicate[], qualify?: (column: string) => string): QueryBuilderLike;
|
|
23
|
+
//# sourceMappingURL=row-scope.d.ts.map
|