@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,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
|
}
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"row-scope.d.ts","sourceRoot":"","sources":["../../src/utils/row-scope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACb,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtF,sEAAsE;AACtE,MAAM,MAAM,iBAAiB,GAAG,eAAe,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9E;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,WAAW,CAAC,EACzC,OAAO,CAAC,EAAE,gBAAgB,GACzB,iBAAiB,EAAE,CAGrB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,YAAY,EAAE,CAEtF;AAED,oFAAoF;AACpF,wBAAgB,aAAa,CAC3B,EAAE,EAAE,gBAAgB,EACpB,KAAK,EAAE,SAAS,iBAAiB,EAAE,EACnC,OAAO,GAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAyB,GACrD,gBAAgB,CAKlB"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getRuntimeTenantPredicate } from './tenant-runtime.js';
|
|
2
|
+
/**
|
|
3
|
+
* The trusted predicates restricting which source rows of `ds` any query may
|
|
4
|
+
* read (RFC 0020). They apply to the queried dataset and to every joined
|
|
5
|
+
* relationship target alike. `tenantKey` with a runtime tenant lowers to one
|
|
6
|
+
* predicate; the trusted cross-tenant scope (`scope: 'all'`) lowers to none.
|
|
7
|
+
*/
|
|
8
|
+
export function datasetRowScope(ds, context) {
|
|
9
|
+
const tenant = getRuntimeTenantPredicate(context);
|
|
10
|
+
return tenant && ds.tenantKey ? [{ column: ds.tenantKey, ...tenant }] : [];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The call's RFC 0019 enforced constraints, from trusted context only. They
|
|
14
|
+
* apply to the queried dataset, in its filter vocabulary, never to joined
|
|
15
|
+
* targets.
|
|
16
|
+
*/
|
|
17
|
+
export function runtimeConstraints(context) {
|
|
18
|
+
return context?.runtime?.constraints ?? [];
|
|
19
|
+
}
|
|
20
|
+
/** ANDs each row-scope predicate onto `qb`, with columns qualified by `qualify`. */
|
|
21
|
+
export function applyRowScope(qb, scope, qualify = column => column) {
|
|
22
|
+
for (const predicate of scope) {
|
|
23
|
+
qb = qb.where(qualify(predicate.column), predicate.operator, predicate.value);
|
|
24
|
+
}
|
|
25
|
+
return qb;
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, ExecutionContext } from '../types.js';
|
|
2
|
+
import type { QueryBuilderFactoryLike, QueryBuilderLike } from '../query-builder-protocol.js';
|
|
3
|
+
import type { DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
|
|
4
|
+
import { type RelationshipBuilderContext } from './relationship-builder-plan.js';
|
|
5
|
+
export interface ScopedDatasetSourceOptions {
|
|
6
|
+
builderFactory: Pick<QueryBuilderFactoryLike, 'table'>;
|
|
7
|
+
ds: AnyDatasetInstance;
|
|
8
|
+
context?: ExecutionContext;
|
|
9
|
+
joinCtx: RelationshipBuilderContext | undefined;
|
|
10
|
+
dialect: DatasetSqlDialectRules;
|
|
11
|
+
timezone: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The dataset's source as a builder with every trusted row restriction
|
|
15
|
+
* applied (RFC 0020): `FINAL`, relationship joins scoped by each target's own
|
|
16
|
+
* row scope, the dataset's row scope, and the call's enforced constraints.
|
|
17
|
+
*
|
|
18
|
+
* Every builder-path query shape reads its source through here, so none can
|
|
19
|
+
* read rows the context does not allow. Callers add their own selections,
|
|
20
|
+
* filters and aggregations on top; every predicate here is ANDed with them.
|
|
21
|
+
*/
|
|
22
|
+
export declare function scopedDatasetSource(options: ScopedDatasetSourceOptions): QueryBuilderLike;
|
|
23
|
+
//# sourceMappingURL=scoped-dataset-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scoped-dataset-source.d.ts","sourceRoot":"","sources":["../../src/utils/scoped-dataset-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAC9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAIxE,OAAO,EAGL,KAAK,0BAA0B,EAChC,MAAM,gCAAgC,CAAC;AAIxC,MAAM,WAAW,0BAA0B;IACzC,cAAc,EAAE,IAAI,CAAC,uBAAuB,EAAE,OAAO,CAAC,CAAC;IACvD,EAAE,EAAE,kBAAkB,CAAC;IACvB,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,OAAO,EAAE,0BAA0B,GAAG,SAAS,CAAC;IAChD,OAAO,EAAE,sBAAsB,CAAC;IAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,gBAAgB,CAiBzF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { resolveFilterField } from '../query-planner.js';
|
|
2
|
+
import { FINAL_SAFETY_SETTINGS, datasetSourceBuilder, queryReadsFinal } from './dataset-source.js';
|
|
3
|
+
import { queryTimeFilter } from './query-timezone.js';
|
|
4
|
+
import { applyRelationshipJoins, qualifyBaseColumn, } from './relationship-builder-plan.js';
|
|
5
|
+
import { assertEnforcedConstraints } from './enforced-constraints.js';
|
|
6
|
+
import { applyRowScope, datasetRowScope, runtimeConstraints } from './row-scope.js';
|
|
7
|
+
/**
|
|
8
|
+
* The dataset's source as a builder with every trusted row restriction
|
|
9
|
+
* applied (RFC 0020): `FINAL`, relationship joins scoped by each target's own
|
|
10
|
+
* row scope, the dataset's row scope, and the call's enforced constraints.
|
|
11
|
+
*
|
|
12
|
+
* Every builder-path query shape reads its source through here, so none can
|
|
13
|
+
* read rows the context does not allow. Callers add their own selections,
|
|
14
|
+
* filters and aggregations on top; every predicate here is ANDed with them.
|
|
15
|
+
*/
|
|
16
|
+
export function scopedDatasetSource(options) {
|
|
17
|
+
const { ds, context, joinCtx, dialect, timezone } = options;
|
|
18
|
+
// Validators report this too; checked here so no path can skip it.
|
|
19
|
+
assertEnforcedConstraints(ds, context);
|
|
20
|
+
let qb = applyRelationshipJoins(datasetSourceBuilder(options.builderFactory, ds), joinCtx);
|
|
21
|
+
// A resolved join target is read with FINAL even when this dataset isn't.
|
|
22
|
+
if (queryReadsFinal(ds, joinCtx) && qb.settings)
|
|
23
|
+
qb = qb.settings(FINAL_SAFETY_SETTINGS);
|
|
24
|
+
qb = applyRowScope(qb, datasetRowScope(ds, context), column => qualifyBaseColumn(joinCtx, column));
|
|
25
|
+
// Constraints are filters in the dataset's vocabulary, applied exactly as a
|
|
26
|
+
// caller filter on the same field would be.
|
|
27
|
+
for (const constraint of runtimeConstraints(context)) {
|
|
28
|
+
const expression = resolveFilterField(ds, constraint.field, joinCtx);
|
|
29
|
+
for (const predicate of queryTimeFilter(dialect, ds, constraint, expression, timezone)) {
|
|
30
|
+
qb = qb.where(predicate.expression, predicate.operator, predicate.value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return qb;
|
|
34
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot-execution-context.d.ts","sourceRoot":"","sources":["../../src/utils/snapshot-execution-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,kFAAkF;AAClF,wBAAgB,wBAAwB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"snapshot-execution-context.d.ts","sourceRoot":"","sources":["../../src/utils/snapshot-execution-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,kFAAkF;AAClF,wBAAgB,wBAAwB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,SAAS,CAgBjG"}
|
|
@@ -9,6 +9,9 @@ export function snapshotExecutionContext(context) {
|
|
|
9
9
|
...context.runtime,
|
|
10
10
|
// Copy tenant objects and ID lists; retain adapter and AbortSignal identity.
|
|
11
11
|
tenant: structuredClone(context.runtime.tenant),
|
|
12
|
+
...(context.runtime.constraints === undefined
|
|
13
|
+
? {}
|
|
14
|
+
: { constraints: structuredClone(context.runtime.constraints) }),
|
|
12
15
|
},
|
|
13
16
|
}),
|
|
14
17
|
...(context.cache && { cache: { ...context.cache } }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { StatementParameters } from './statement-parameters.js';
|
|
2
2
|
import type { TimeMeasureAxis } from './time-measure-axis.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type TimeMeasureSqlSource } from './time-measure-source-sql.js';
|
|
4
4
|
import type { ShiftMeasureDefinition } from '../types.js';
|
|
5
5
|
export interface TimeMeasureSqlAxis {
|
|
6
6
|
ctes: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-measure-axis-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-axis-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"time-measure-axis-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-axis-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAqB,KAAK,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAE5F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAG1D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,6DAA6D;AAC7D,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,mBAAmB,EAC9B,MAAM,EAAE,oBAAoB,EAC5B,IAAI,EAAE,eAAe,EACrB,MAAM,GAAE,SAAS,sBAAsB,EAAO,GAC7C,kBAAkB,CA2CpB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sourceTimezoneSql } from './time-measure-source-sql.js';
|
|
1
2
|
import { calendarBucketGuardSql } from './time-axis-calendar-guard.js';
|
|
2
3
|
import { shiftCalendarGuardSql, shiftWallTimeSeriesGuardSql } from './shift-measure-sql.js';
|
|
3
4
|
/** Build bounded output buckets using the query timezone. */
|
|
@@ -24,7 +25,7 @@ export function buildTimeMeasureAxisSql(statement, source, axis, shifts = []) {
|
|
|
24
25
|
const postSeriesGuard = shifts.map(shift => shiftWallTimeSeriesGuardSql(dialect, shift, axis.grain)).join(' + ') || '0';
|
|
25
26
|
// A zero-row scalar retains the converted timestamp type and query timezone.
|
|
26
27
|
// Reading any(time) here would scan the entire population just for its type.
|
|
27
|
-
const timezone =
|
|
28
|
+
const timezone = sourceTimezoneSql(dialect);
|
|
28
29
|
const bounds = [
|
|
29
30
|
// The bounds come from the caller's time filter: bind them, never inline them.
|
|
30
31
|
`${dialect.parseTimestamp(statement.bind(axis.lower, 'string'), timezone)} AS _hq_lower`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
|
|
2
2
|
import type { WindowMeasureDefinition } from '../types.js';
|
|
3
3
|
import type { TimeMeasureAxis } from './time-measure-axis.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type TimeMeasureSqlBase, type TimeMeasureSqlDimension } from './time-measure-source-sql.js';
|
|
5
5
|
/** Keyed source membership retains distinct/average semantics and row multiplicity. */
|
|
6
6
|
export declare function timeMeasureLeafSql(dialect: DatasetSqlDialectRules, id: number, context: number, base: TimeMeasureSqlBase, window: WindowMeasureDefinition | undefined, axis: TimeMeasureAxis, dims: readonly TimeMeasureSqlDimension[]): {
|
|
7
7
|
ctes: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-measure-leaf-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-leaf-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"time-measure-leaf-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-leaf-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAqB,KAAK,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAKxH,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,sBAAsB,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,uBAAuB,GAAG,SAAS,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,uBAAuB,EAAE;;;;;;;;EA+CtO"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sourceTimezoneSql } from './time-measure-source-sql.js';
|
|
1
2
|
import { calendarBucketGuardSql } from './time-axis-calendar-guard.js';
|
|
2
3
|
import { shiftRangePredicateSql } from './shift-measure-sql.js';
|
|
3
4
|
import { isCountAggregation } from './count-aggregations.js';
|
|
@@ -27,7 +28,8 @@ export function timeMeasureLeafSql(dialect, id, context, base, window, axis, dim
|
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
const lower = window?.trailing ? dialect.subtractTime('_hq_eval_end', window.trailing.amount, window.trailing.unit)
|
|
30
|
-
|
|
31
|
+
// Keep the to-date start zoned: a calendar Date would bucket in server time.
|
|
32
|
+
: window?.toDate ? dialect.truncateTimeInZone('_hq_eval_period', window.toDate, sourceTimezoneSql(dialect)) : '_hq_eval_lower';
|
|
31
33
|
const upper = window ? '_hq_eval_end' : '_hq_eval_upper';
|
|
32
34
|
const lowerInclusive = window ? '1' : '_hq_lower_inclusive';
|
|
33
35
|
const upperInclusive = window ? '0' : '_hq_upper_inclusive';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AnyDatasetInstance, DatasetQuery, MeasureDefinition, MetricFilter } from '../types.js';
|
|
2
2
|
import type { DatasetQueryExecutionOptions } from '../dataset-query.js';
|
|
3
3
|
import type { StatementParameters } from './statement-parameters.js';
|
|
4
|
+
import type { DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
|
|
4
5
|
export interface TimeMeasureSqlDimension {
|
|
5
6
|
name: string;
|
|
6
7
|
alias: string;
|
|
@@ -17,6 +18,8 @@ export interface TimeMeasureSqlSource {
|
|
|
17
18
|
bases: TimeMeasureSqlBase[];
|
|
18
19
|
rowAlias: string;
|
|
19
20
|
}
|
|
21
|
+
/** The query timezone carried by `_hq_source` time, read from a zero-row scalar without scanning. */
|
|
22
|
+
export declare function sourceTimezoneSql(dialect: DatasetSqlDialectRules): string;
|
|
20
23
|
/** Isolate authored expressions, joins, tenancy and filters from internal SQL aliases. */
|
|
21
24
|
export declare function buildTimeMeasureSourceSql(ds: AnyDatasetInstance, query: DatasetQuery, baseNames: Iterable<string>, filters: readonly MetricFilter[], options: DatasetQueryExecutionOptions, statement: StatementParameters): TimeMeasureSqlSource;
|
|
22
25
|
//# sourceMappingURL=time-measure-source-sql.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-measure-source-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-source-sql.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"time-measure-source-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-source-sql.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAQxE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAExE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,iBAAiB,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,uBAAuB,EAAE,CAAC;IACtC,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qGAAqG;AACrG,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAEzE;AAED,0FAA0F;AAC1F,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC3B,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,OAAO,EAAE,4BAA4B,EACrC,SAAS,EAAE,mBAAmB,GAC7B,oBAAoB,CAoEtB"}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { scopedDatasetSource } from './scoped-dataset-source.js';
|
|
2
2
|
import { resolveRelationshipMeasure } from './relationship-measures.js';
|
|
3
3
|
import { relationshipMeasureExpressions } from './relationship-measure-sql.js';
|
|
4
4
|
import { queryTimeFilter, queryTimeSql } from './query-timezone.js';
|
|
5
|
-
import { resolveDimensionExpression, resolveFilterField
|
|
6
|
-
import {
|
|
7
|
-
import { getRuntimeTenantPredicate } from './tenant-runtime.js';
|
|
5
|
+
import { resolveDimensionExpression, resolveFilterField } from '../query-planner.js';
|
|
6
|
+
import { buildRelationshipBuilderContext } from './relationship-builder-plan.js';
|
|
8
7
|
import { segmentFilters } from './segments.js';
|
|
9
8
|
import { getBaseMeasure } from './dataset-measures.js';
|
|
10
9
|
import { measureToAggregationSpec } from './dataset-normalization.js';
|
|
11
10
|
import { applyFilteredAggregationExpression } from './filtered-aggregation-sql.js';
|
|
12
11
|
import { assertNoRawSqlUnderJoins } from './sql-under-joins.js';
|
|
12
|
+
/** The query timezone carried by `_hq_source` time, read from a zero-row scalar without scanning. */
|
|
13
|
+
export function sourceTimezoneSql(dialect) {
|
|
14
|
+
return dialect.timezoneOf(dialect.assumeNotNull('(SELECT _hq_time FROM _hq_source LIMIT 0)'));
|
|
15
|
+
}
|
|
13
16
|
/** Isolate authored expressions, joins, tenancy and filters from internal SQL aliases. */
|
|
14
17
|
export function buildTimeMeasureSourceSql(ds, query, baseNames, filters, options, statement) {
|
|
15
18
|
const { dialect } = statement;
|
|
@@ -17,7 +20,9 @@ export function buildTimeMeasureSourceSql(ds, query, baseNames, filters, options
|
|
|
17
20
|
const joinCtx = buildRelationshipBuilderContext(ds, query, options.context, dialect);
|
|
18
21
|
// Keep source expressions in their own SELECT scope: internal aliases cannot
|
|
19
22
|
// capture authored SQL identifiers, even if a physical column shares a name.
|
|
20
|
-
let raw =
|
|
23
|
+
let raw = scopedDatasetSource({
|
|
24
|
+
builderFactory: options.builderFactory, ds, context: options.context, joinCtx, dialect, timezone: query.timezone,
|
|
25
|
+
});
|
|
21
26
|
const dims = (query.dimensions ?? []).map((name, i) => ({ name, alias: `_hq_d${i}` }));
|
|
22
27
|
const sourceParts = [resolveDimensionExpression(ds, ds.timeKey, joinCtx)];
|
|
23
28
|
for (const dimension of dims)
|
|
@@ -50,10 +55,6 @@ export function buildTimeMeasureSourceSql(ds, query, baseNames, filters, options
|
|
|
50
55
|
while (sourceIdentifiers.some(text => text.includes(rowAlias)))
|
|
51
56
|
rowAlias += '_';
|
|
52
57
|
raw = raw.select([`${dialect.packRow(sourceParts)} AS ${rowAlias}`]);
|
|
53
|
-
const tenant = getRuntimeTenantPredicate(options.context);
|
|
54
|
-
const tenantColumn = resolveTenantFilterColumn(ds, options.context);
|
|
55
|
-
if (tenant && tenantColumn)
|
|
56
|
-
raw = raw.where(qualifyBaseColumn(joinCtx, tenantColumn), tenant.operator, tenant.value);
|
|
57
58
|
// The time axis already consumed every time-key bound, so these are the
|
|
58
59
|
// remaining caller filters.
|
|
59
60
|
for (const filter of filters) {
|