@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.
Files changed (106) hide show
  1. package/dist/cache/definition-identity.d.ts +9 -6
  2. package/dist/cache/definition-identity.d.ts.map +1 -1
  3. package/dist/cache/definition-identity.js +12 -6
  4. package/dist/cache/protocol-cache-keys.d.ts.map +1 -1
  5. package/dist/cache/protocol-cache-keys.js +29 -6
  6. package/dist/cache/query-signature.d.ts.map +1 -1
  7. package/dist/cache/query-signature.js +11 -3
  8. package/dist/catalog.d.ts +4 -0
  9. package/dist/catalog.d.ts.map +1 -1
  10. package/dist/catalog.js +2 -0
  11. package/dist/contract.d.ts +3 -0
  12. package/dist/contract.d.ts.map +1 -1
  13. package/dist/contract.js +2 -0
  14. package/dist/dataset-compilation.d.ts +2 -0
  15. package/dist/dataset-compilation.d.ts.map +1 -1
  16. package/dist/dataset-sql-dialect.d.ts +3 -1
  17. package/dist/dataset-sql-dialect.d.ts.map +1 -1
  18. package/dist/dataset.d.ts +3 -1
  19. package/dist/dataset.d.ts.map +1 -1
  20. package/dist/dataset.js +23 -32
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/metric-query-engine.d.ts +0 -1
  24. package/dist/metric-query-engine.d.ts.map +1 -1
  25. package/dist/metric-query-engine.js +8 -1
  26. package/dist/protocol-adapter.d.ts.map +1 -1
  27. package/dist/protocol-adapter.js +4 -6
  28. package/dist/protocol-rehydrate.d.ts.map +1 -1
  29. package/dist/protocol-rehydrate.js +2 -0
  30. package/dist/query-builder-protocol.d.ts +3 -1
  31. package/dist/query-builder-protocol.d.ts.map +1 -1
  32. package/dist/relationship-check.d.ts.map +1 -1
  33. package/dist/relationship-check.js +3 -5
  34. package/dist/semantic-planner.d.ts.map +1 -1
  35. package/dist/semantic-planner.js +35 -15
  36. package/dist/semantic-query-schema.d.ts +6 -0
  37. package/dist/semantic-query-schema.d.ts.map +1 -1
  38. package/dist/semantic-query-schema.js +6 -0
  39. package/dist/tools.d.ts.map +1 -1
  40. package/dist/tools.js +4 -3
  41. package/dist/types.d.ts +84 -7
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/utils/aggregate-state-functions.d.ts +2 -1
  44. package/dist/utils/aggregate-state-functions.d.ts.map +1 -1
  45. package/dist/utils/aggregate-state-functions.js +5 -19
  46. package/dist/utils/build-dataset-query-builder.d.ts +1 -1
  47. package/dist/utils/build-dataset-query-builder.d.ts.map +1 -1
  48. package/dist/utils/build-dataset-query-builder.js +12 -15
  49. package/dist/utils/clickhouse-dataset-sql-dialect.d.ts.map +1 -1
  50. package/dist/utils/clickhouse-dataset-sql-dialect.js +3 -0
  51. package/dist/utils/compile-dataset-query.d.ts +1 -1
  52. package/dist/utils/compile-dataset-query.d.ts.map +1 -1
  53. package/dist/utils/compile-dataset-query.js +16 -5
  54. package/dist/utils/dataset-definition-validation.d.ts +1 -1
  55. package/dist/utils/dataset-definition-validation.d.ts.map +1 -1
  56. package/dist/utils/dataset-definition-validation.js +19 -0
  57. package/dist/utils/dataset-query-validation.d.ts +1 -1
  58. package/dist/utils/dataset-query-validation.d.ts.map +1 -1
  59. package/dist/utils/dataset-query-validation.js +12 -2
  60. package/dist/utils/dataset-source.d.ts +35 -2
  61. package/dist/utils/dataset-source.d.ts.map +1 -1
  62. package/dist/utils/dataset-source.js +47 -4
  63. package/dist/utils/dataset-time-keys.d.ts +32 -0
  64. package/dist/utils/dataset-time-keys.d.ts.map +1 -0
  65. package/dist/utils/dataset-time-keys.js +55 -0
  66. package/dist/utils/enforced-constraints.d.ts +14 -0
  67. package/dist/utils/enforced-constraints.d.ts.map +1 -0
  68. package/dist/utils/enforced-constraints.js +57 -0
  69. package/dist/utils/metric-derived-query.d.ts.map +1 -1
  70. package/dist/utils/metric-derived-query.js +6 -12
  71. package/dist/utils/metric-query-builder.d.ts +1 -1
  72. package/dist/utils/metric-query-builder.d.ts.map +1 -1
  73. package/dist/utils/metric-query-builder.js +7 -13
  74. package/dist/utils/metric-query-validation.d.ts.map +1 -1
  75. package/dist/utils/metric-query-validation.js +7 -1
  76. package/dist/utils/portable-aggregations.d.ts +6 -3
  77. package/dist/utils/portable-aggregations.d.ts.map +1 -1
  78. package/dist/utils/portable-aggregations.js +16 -7
  79. package/dist/utils/query-timezone.d.ts +1 -1
  80. package/dist/utils/query-timezone.d.ts.map +1 -1
  81. package/dist/utils/query-timezone.js +3 -3
  82. package/dist/utils/relationship-builder-plan.d.ts +12 -10
  83. package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
  84. package/dist/utils/relationship-builder-plan.js +20 -19
  85. package/dist/utils/relationship-measure-sql.d.ts.map +1 -1
  86. package/dist/utils/relationship-measure-sql.js +6 -3
  87. package/dist/utils/relationship-validation.d.ts.map +1 -1
  88. package/dist/utils/relationship-validation.js +2 -0
  89. package/dist/utils/row-scope.d.ts +23 -0
  90. package/dist/utils/row-scope.d.ts.map +1 -0
  91. package/dist/utils/row-scope.js +26 -0
  92. package/dist/utils/scoped-dataset-source.d.ts +23 -0
  93. package/dist/utils/scoped-dataset-source.d.ts.map +1 -0
  94. package/dist/utils/scoped-dataset-source.js +34 -0
  95. package/dist/utils/snapshot-execution-context.d.ts.map +1 -1
  96. package/dist/utils/snapshot-execution-context.js +3 -0
  97. package/dist/utils/time-measure-axis-sql.d.ts +1 -1
  98. package/dist/utils/time-measure-axis-sql.d.ts.map +1 -1
  99. package/dist/utils/time-measure-axis-sql.js +2 -1
  100. package/dist/utils/time-measure-leaf-sql.d.ts +1 -1
  101. package/dist/utils/time-measure-leaf-sql.d.ts.map +1 -1
  102. package/dist/utils/time-measure-leaf-sql.js +3 -1
  103. package/dist/utils/time-measure-source-sql.d.ts +3 -0
  104. package/dist/utils/time-measure-source-sql.d.ts.map +1 -1
  105. package/dist/utils/time-measure-source-sql.js +10 -9
  106. 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;AAS9F,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAUvF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAKrD,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,CAyHlB"}
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 { datasetSourceBuilder } from './dataset-source.js';
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, resolveTenantFilterColumn, } from '../query-planner.js';
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 { getRuntimeTenantPredicate } from './tenant-runtime.js';
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 = datasetSourceBuilder(builderFactory, ds);
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, ds: AnyDatasetInstance, query: MetricQuery, grain: TimeGrain | undefined, builderFactory: QueryBuilderFactoryLike, context?: ExecutionContext): QueryBuilderLike;
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;AAkB9F,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEvE,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,eAAe,EACrB,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,cAAc,EAAE,uBAAuB,EACvC,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CA6DlB"}
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 { datasetSourceBuilder } from './dataset-source.js';
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, resolveTenantFilterColumn, } from '../query-planner.js';
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 { getRuntimeTenantPredicate } from './tenant-runtime.js';
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
- export function buildMetricQueryBuilder(ref, spec, ds, query, grain, builderFactory, context) {
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 = datasetSourceBuilder(builderFactory, ds);
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;AAM3G,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAoIlB"}
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 ds = ref.dataset;
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` reads ClickHouse aggregate-state columns and has no contract form.
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
- /** Returns `aggregation` when publishing can carry it; otherwise throws naming `owner`. */
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;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,KAAK,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,EAAE,qBAAqB,CAAC,CAAC;AAE/E,2FAA2F;AAC3F,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,eAAe,EAC5B,KAAK,EAAE,MAAM,GACZ,mBAAmB,CAcrB"}
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` reads ClickHouse aggregate-state columns and has no contract form.
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
- /** Returns `aggregation` when publishing can carry it; otherwise throws naming `owner`. */
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-key column. */
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;AAK1F,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,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAM9E;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
+ {"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-key column. */
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
- const timeColumn = ds.dimensions[ds.timeKey]?.column ?? ds.timeKey;
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 TenantPredicate } from './tenant-runtime.js';
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
- /** Tenant predicate applied to the joined target, when active. */
36
- tenant?: TenantPredicate & {
37
- field: string;
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 query
50
- * references no relationship-qualified fields. Validation runs first, so any
51
- * qualified name that fails to resolve here is skipped defensively.
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`) and, when runtime tenancy
72
- * is active on a target, scopes the joined rows with the tenant predicate.
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,EAA6B,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtF,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,kEAAkE;IAClE,MAAM,CAAC,EAAE,eAAe,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;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;;;;GAIG;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,CAgDxC;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;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,0BAA0B,GAAG,SAAS,GAC1C,gBAAgB,CAoClB"}
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 { getRuntimeTenantPredicate } from './tenant-runtime.js';
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 query
9
- * references no relationship-qualified fields. Validation runs first, so any
10
- * qualified name that fails to resolve here is skipped defensively.
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
- tenant: tenantPredicate && target.tenantKey
46
- ? { field: target.tenantKey, ...tenantPredicate }
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`) and, when runtime tenancy
90
- * is active on a target, scopes the joined rows with the tenant predicate.
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 tenant = join.tenant
108
- ? {
109
- column: `${join.relationship}.${join.tenant.field}`,
110
- operator: join.tenant.operator,
111
- value: join.tenant.value,
112
- }
113
- : undefined;
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, tenant);
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, tenant);
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":"AAAA,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;;;EAkBlJ;AAeD,wBAAgB,8BAA8B,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,0BAA0B,EAAE,OAAO,EAAE,sBAAsB;;;;EAexJ"}
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
- return { source: `(SELECT ${[...projection, `${dialect.toNullable('1')} AS ${quote(marker)}`].join(', ')} FROM ${source})`, marker };
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.final)
46
+ if (!target.rowResolution)
44
47
  return quoted;
45
48
  if (!dialect.finalTable) {
46
- throw new Error(`Dataset "${target.name}" is declared final: true, but the "${dialect.name}" SQL dialect has no FINAL ` +
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;AAWrB,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,CAoBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,IAAI,CA4Bf"}
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