@hypequery/datasets 0.22.0 → 0.23.1
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/protocol-cache-keys.d.ts.map +1 -1
- package/dist/cache/protocol-cache-keys.js +5 -0
- package/dist/cache/query-signature.d.ts.map +1 -1
- package/dist/cache/query-signature.js +9 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +4 -0
- package/dist/constants.d.ts +12 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +24 -0
- package/dist/contract.d.ts +4 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +4 -0
- package/dist/dataset-compilation.d.ts +5 -0
- package/dist/dataset-compilation.d.ts.map +1 -1
- package/dist/execution-capabilities.json +1 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +3 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/protocol-adapter.d.ts.map +1 -1
- package/dist/protocol-adapter.js +1 -0
- package/dist/protocol-rehydrate.d.ts.map +1 -1
- package/dist/protocol-rehydrate.js +9 -2
- package/dist/query-builder-protocol.d.ts +7 -0
- package/dist/query-builder-protocol.d.ts.map +1 -1
- package/dist/relationship-check.d.ts +3 -2
- package/dist/relationship-check.d.ts.map +1 -1
- package/dist/relationship-check.js +8 -3
- package/dist/relationships.d.ts +6 -16
- package/dist/relationships.d.ts.map +1 -1
- package/dist/relationships.js +3 -12
- package/dist/semantic-planner.d.ts.map +1 -1
- package/dist/semantic-planner.js +2 -0
- 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 +29 -2
- package/dist/types.d.ts +49 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/build-dataset-query-builder.d.ts.map +1 -1
- package/dist/utils/build-dataset-query-builder.js +3 -0
- package/dist/utils/calendar-shift-sql.d.ts +18 -0
- package/dist/utils/calendar-shift-sql.d.ts.map +1 -0
- package/dist/utils/calendar-shift-sql.js +21 -0
- package/dist/utils/compile-dataset-query.d.ts.map +1 -1
- package/dist/utils/compile-dataset-query.js +4 -3
- package/dist/utils/dataset-definition-validation.d.ts.map +1 -1
- package/dist/utils/dataset-definition-validation.js +52 -2
- package/dist/utils/dataset-derived-query.d.ts +5 -0
- package/dist/utils/dataset-derived-query.d.ts.map +1 -1
- package/dist/utils/dataset-derived-query.js +16 -0
- package/dist/utils/dataset-having.d.ts +21 -0
- package/dist/utils/dataset-having.d.ts.map +1 -0
- package/dist/utils/dataset-having.js +106 -0
- package/dist/utils/dataset-normalization.d.ts.map +1 -1
- package/dist/utils/dataset-normalization.js +5 -1
- package/dist/utils/dataset-query-validation.d.ts.map +1 -1
- package/dist/utils/dataset-query-validation.js +2 -0
- package/dist/utils/protocol-metric-expressions.d.ts +8 -0
- package/dist/utils/protocol-metric-expressions.d.ts.map +1 -1
- package/dist/utils/protocol-metric-expressions.js +23 -11
- package/dist/utils/relationship-builder-plan.d.ts +2 -0
- package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
- package/dist/utils/relationship-builder-plan.js +16 -4
- package/dist/utils/relationship-definition.d.ts +6 -0
- package/dist/utils/relationship-definition.d.ts.map +1 -0
- package/dist/utils/relationship-definition.js +9 -0
- package/dist/utils/relationship-key-check.d.ts +4 -0
- package/dist/utils/relationship-key-check.d.ts.map +1 -1
- package/dist/utils/relationship-key-check.js +19 -7
- package/dist/utils/relationship-keys.d.ts +17 -0
- package/dist/utils/relationship-keys.d.ts.map +1 -0
- package/dist/utils/relationship-keys.js +63 -0
- package/dist/utils/relationship-measure-sql.d.ts +1 -1
- package/dist/utils/relationship-measure-sql.d.ts.map +1 -1
- package/dist/utils/relationship-measure-sql.js +1 -1
- package/dist/utils/semantic-query-syntax.d.ts.map +1 -1
- package/dist/utils/semantic-query-syntax.js +2 -3
- package/dist/utils/shift-measure-sql.d.ts.map +1 -1
- package/dist/utils/shift-measure-sql.js +10 -3
- package/dist/utils/time-measure-context-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-context-sql.js +8 -2
- package/package.json +9 -5
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { SEMANTIC_HAVING_OPERATORS } from '../constants.js';
|
|
2
|
+
import { selectedTimeMeasures } from './time-query-measures.js';
|
|
3
|
+
const HAVING_OPERATORS = new Set(SEMANTIC_HAVING_OPERATORS);
|
|
4
|
+
const COMPARISONS = {
|
|
5
|
+
eq: '=',
|
|
6
|
+
neq: '!=',
|
|
7
|
+
gt: '>',
|
|
8
|
+
gte: '>=',
|
|
9
|
+
lt: '<',
|
|
10
|
+
lte: '<=',
|
|
11
|
+
};
|
|
12
|
+
function isFiniteNumber(value) {
|
|
13
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
14
|
+
}
|
|
15
|
+
function conditionValueError(condition) {
|
|
16
|
+
const { measure, operator, value } = condition;
|
|
17
|
+
if (operator === 'between') {
|
|
18
|
+
return Array.isArray(value) && value.length === 2 && value.every(isFiniteNumber)
|
|
19
|
+
? undefined
|
|
20
|
+
: `Having "between" on "${measure}" expects a two-item array of finite numbers.`;
|
|
21
|
+
}
|
|
22
|
+
if (operator === 'in' || operator === 'notIn') {
|
|
23
|
+
return Array.isArray(value) && value.length > 0 && value.every(isFiniteNumber)
|
|
24
|
+
? undefined
|
|
25
|
+
: `Having "${operator}" on "${measure}" expects a non-empty array of finite numbers.`;
|
|
26
|
+
}
|
|
27
|
+
return isFiniteNumber(value)
|
|
28
|
+
? undefined
|
|
29
|
+
: `Having "${operator}" on "${measure}" expects a finite number.`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validates `having` conditions against the query they belong to.
|
|
33
|
+
*
|
|
34
|
+
* A condition may only reference a measure the query selects: that is the
|
|
35
|
+
* value the condition reads, and it keeps every rendered reference an output
|
|
36
|
+
* alias rather than an expression taken from input.
|
|
37
|
+
*/
|
|
38
|
+
export function datasetHavingErrors(ds, query, selectedMeasures) {
|
|
39
|
+
if (query.having === undefined)
|
|
40
|
+
return [];
|
|
41
|
+
if (!Array.isArray(query.having))
|
|
42
|
+
return ['Having must be an array of conditions.'];
|
|
43
|
+
if (query.having.length === 0)
|
|
44
|
+
return [];
|
|
45
|
+
const errors = [];
|
|
46
|
+
if (selectedTimeMeasures(ds, query).size > 0) {
|
|
47
|
+
errors.push('Having is not supported on queries that select window or shift measures.');
|
|
48
|
+
}
|
|
49
|
+
// `maxFilters` bounds every predicate the query adds, before and after
|
|
50
|
+
// aggregation alike. Filters alone over the limit are reported separately.
|
|
51
|
+
const maxFilters = ds.limits?.maxFilters;
|
|
52
|
+
const filterCount = Array.isArray(query.filters) ? query.filters.length : 0;
|
|
53
|
+
const conditionCount = filterCount + query.having.length;
|
|
54
|
+
if (maxFilters && conditionCount > maxFilters && filterCount <= maxFilters) {
|
|
55
|
+
errors.push(`Too many filters and having conditions: ${conditionCount} (max ${maxFilters})`);
|
|
56
|
+
}
|
|
57
|
+
const selected = new Set(selectedMeasures);
|
|
58
|
+
for (const condition of query.having) {
|
|
59
|
+
if (typeof condition !== 'object' || condition === null) {
|
|
60
|
+
errors.push('Each having condition must be an object with measure, operator and value.');
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const { measure, operator } = condition;
|
|
64
|
+
if (typeof measure !== 'string' || !selected.has(measure)) {
|
|
65
|
+
errors.push(`Having measure "${String(measure)}" must be one of the selected measures: ${[...selected].join(', ')}`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (typeof operator !== 'string' || !HAVING_OPERATORS.has(operator)) {
|
|
69
|
+
errors.push(`Unsupported having operator "${String(operator)}" on "${measure}". ` +
|
|
70
|
+
`Supported: ${SEMANTIC_HAVING_OPERATORS.join(', ')}`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const valueError = conditionValueError(condition);
|
|
74
|
+
if (valueError)
|
|
75
|
+
errors.push(valueError);
|
|
76
|
+
}
|
|
77
|
+
return errors;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Renders validated `having` conditions as one parameterized predicate.
|
|
81
|
+
*
|
|
82
|
+
* `measureSql` maps a selected measure to the expression that reads its
|
|
83
|
+
* aggregated value in the outer query. Values are only ever bound as `?`
|
|
84
|
+
* parameters, appended in placeholder order.
|
|
85
|
+
*/
|
|
86
|
+
export function datasetHavingSql(conditions, measureSql) {
|
|
87
|
+
const parameters = [];
|
|
88
|
+
const predicates = conditions.map((condition) => {
|
|
89
|
+
const column = measureSql(condition.measure);
|
|
90
|
+
if (condition.operator === 'between') {
|
|
91
|
+
parameters.push(condition.value[0], condition.value[1]);
|
|
92
|
+
return `${column} BETWEEN ? AND ?`;
|
|
93
|
+
}
|
|
94
|
+
if (condition.operator === 'in' || condition.operator === 'notIn') {
|
|
95
|
+
parameters.push(...condition.value);
|
|
96
|
+
const placeholders = condition.value.map(() => '?').join(', ');
|
|
97
|
+
return `${column} ${condition.operator === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;
|
|
98
|
+
}
|
|
99
|
+
const comparison = COMPARISONS[condition.operator];
|
|
100
|
+
if (!comparison)
|
|
101
|
+
throw new Error(`Unsupported having operator "${String(condition.operator)}".`);
|
|
102
|
+
parameters.push(condition.value);
|
|
103
|
+
return `${column} ${comparison} ?`;
|
|
104
|
+
});
|
|
105
|
+
return { sql: predicates.join(' AND '), parameters };
|
|
106
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-normalization.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-normalization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,wBAAwB,EACxB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"dataset-normalization.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-normalization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,wBAAwB,EACxB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAGrB,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;AAC5D,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AAE/D,wBAAgB,mBAAmB,CAAC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EACzF,MAAM,EAAE,aAAa,CAAC,WAAW,EAAE,WAAW,EAAE,gBAAgB,CAAC,GAChE,WAAW,CAEb;AAED,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC/C,OAAO,CAAC,EAAE,yBAAyB,GAClC,yBAAyB,CAgB3B;AAED,wBAAgB,iBAAiB,CAAC,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,EAC1F,QAAQ,EAAE,SAAS,GAAG,SAAS,GAC9B,SAAS,CAEX;AAED,wBAAgB,sBAAsB,CAAC,cAAc,SAAS,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAClG,aAAa,EAAE,cAAc,GAAG,SAAS,EACzC,MAAM,EAAE,MAAM,GACb,cAAc,CAkBhB;AAED,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,GAC5B,eAAe,CAcjB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { relationshipKeys } from './relationship-keys.js';
|
|
1
2
|
export function normalizeDimensions(config) {
|
|
2
3
|
return config.dimensions;
|
|
3
4
|
}
|
|
@@ -19,7 +20,10 @@ export function normalizeMeasures(measures) {
|
|
|
19
20
|
return (measures ?? {});
|
|
20
21
|
}
|
|
21
22
|
export function normalizeRelationships(relationships, source) {
|
|
22
|
-
for (const name of Object.
|
|
23
|
+
for (const [name, relationship] of Object.entries(relationships ?? {})) {
|
|
24
|
+
// Hand-built definitions skip the helpers, so check composite keys here.
|
|
25
|
+
if (relationship.keys)
|
|
26
|
+
relationshipKeys(relationship);
|
|
23
27
|
if (name === source) {
|
|
24
28
|
throw new Error(`Invalid relationship "${name}": the name matches the dataset source table, so the join ` +
|
|
25
29
|
'alias would shadow the base table and produce ambiguous SQL. Rename the relationship.');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-query-validation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"dataset-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-query-validation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAkB9E,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAyKlB"}
|
|
@@ -9,6 +9,7 @@ import { isQualifiedField, resolveQualifiedField, } from './relationship-fields.
|
|
|
9
9
|
import { validateQualifiedFilter, validateRelationshipTenantRuntime, } from './relationship-validation.js';
|
|
10
10
|
import { segmentSelectionErrors } from './segments.js';
|
|
11
11
|
import { analyzeTimeMeasureAxis } from './time-measure-axis.js';
|
|
12
|
+
import { datasetHavingErrors } from './dataset-having.js';
|
|
12
13
|
export function validateDatasetQueryInput(ds, query, context) {
|
|
13
14
|
const errors = [...semanticQuerySyntaxErrors(query), ...queryTimezoneErrors(query.timezone)];
|
|
14
15
|
const dimensionNames = Object.keys(ds.dimensions);
|
|
@@ -147,6 +148,7 @@ export function validateDatasetQueryInput(ds, query, context) {
|
|
|
147
148
|
if (ds.limits?.maxMeasures && query.measures && query.measures.length > ds.limits.maxMeasures) {
|
|
148
149
|
errors.push(`Too many measures: ${query.measures.length} (max ${ds.limits.maxMeasures})`);
|
|
149
150
|
}
|
|
151
|
+
errors.push(...datasetHavingErrors(ds, query, selectedMeasures));
|
|
150
152
|
if (ds.limits?.maxFilters && query.filters && query.filters.length > ds.limits.maxFilters) {
|
|
151
153
|
errors.push(`Too many filters: ${query.filters.length} (max ${ds.limits.maxFilters})`);
|
|
152
154
|
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { type ProtocolExpression, type ProtocolMetricDerivation } from '@hypequery/protocol';
|
|
2
2
|
import type { AggregationSpec, DerivedMeasureDefinition, DerivedMetricSpec, MetricFilter } from '../types.js';
|
|
3
3
|
export declare function filterExpression(filter: MetricFilter): ProtocolExpression;
|
|
4
|
+
/** The published form; aliases must satisfy the protocol identifier grammar. */
|
|
4
5
|
export declare function derivedMeasureExpression(definition: DerivedMeasureDefinition): ProtocolExpression;
|
|
6
|
+
/**
|
|
7
|
+
* The catalog form. Catalogs describe local datasets that may never be published,
|
|
8
|
+
* so aliases keep their locally valid names (for example `__hypequery_value`, or
|
|
9
|
+
* names past the protocol's length limit) instead of failing catalog creation.
|
|
10
|
+
* Publishing still validates them through `derivedMeasureExpression`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function derivedMeasureCatalogExpression(definition: DerivedMeasureDefinition): ProtocolExpression;
|
|
5
13
|
export declare function metricExpression(spec: AggregationSpec | DerivedMetricSpec): ProtocolExpression;
|
|
6
14
|
/**
|
|
7
15
|
* The formula in the shape it was authored in, beside the inlined form.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol-metric-expressions.d.ts","sourceRoot":"","sources":["../../src/utils/protocol-metric-expressions.ts"],"names":[],"mappings":"AAAA,OAAO,EAEgB,KAAK,kBAAkB,EAAE,KAAK,wBAAwB,EAC5E,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,eAAe,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAuC9G,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,kBAAkB,CAgBzE;
|
|
1
|
+
{"version":3,"file":"protocol-metric-expressions.d.ts","sourceRoot":"","sources":["../../src/utils/protocol-metric-expressions.ts"],"names":[],"mappings":"AAAA,OAAO,EAEgB,KAAK,kBAAkB,EAAE,KAAK,wBAAwB,EAC5E,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,eAAe,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAuC9G,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,kBAAkB,CAgBzE;AAsDD,gFAAgF;AAChF,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,wBAAwB,GAAG,kBAAkB,CAEjG;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAAC,UAAU,EAAE,wBAAwB,GAAG,kBAAkB,CAExG;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,iBAAiB,GAAG,kBAAkB,CAQ9F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,wBAAwB,CASlF"}
|
|
@@ -61,34 +61,46 @@ function aggregationExpression(spec) {
|
|
|
61
61
|
};
|
|
62
62
|
return result;
|
|
63
63
|
}
|
|
64
|
-
function semanticExpression(expression, references = {}) {
|
|
64
|
+
function semanticExpression(expression, references = {}, referenceName = parseProtocolQualifiedIdentifier) {
|
|
65
65
|
switch (expression.kind) {
|
|
66
66
|
case 'ref':
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
67
|
+
// Own properties only: an alias such as `constructor` must stay a reference.
|
|
68
|
+
return Object.hasOwn(references, expression.name)
|
|
69
|
+
? references[expression.name]
|
|
70
|
+
: { kind: 'reference', name: referenceName(expression.name) };
|
|
72
71
|
case 'literal':
|
|
73
72
|
return { kind: 'literal', value: canonicalValue(expression.value) };
|
|
74
73
|
case 'binary':
|
|
75
74
|
return {
|
|
76
75
|
kind: 'binary',
|
|
77
76
|
operator: expression.operator,
|
|
78
|
-
left: semanticExpression(expression.left, references),
|
|
79
|
-
right: semanticExpression(expression.right, references),
|
|
77
|
+
left: semanticExpression(expression.left, references, referenceName),
|
|
78
|
+
right: semanticExpression(expression.right, references, referenceName),
|
|
80
79
|
};
|
|
81
80
|
case 'function':
|
|
82
81
|
return {
|
|
83
82
|
kind: 'call',
|
|
84
83
|
function: expression.name,
|
|
85
|
-
args: expression.args.map(argument => semanticExpression(argument, references)),
|
|
84
|
+
args: expression.args.map(argument => semanticExpression(argument, references, referenceName)),
|
|
86
85
|
};
|
|
87
86
|
}
|
|
88
87
|
}
|
|
89
|
-
|
|
88
|
+
function derivedFormula(definition) {
|
|
90
89
|
const aliases = Object.fromEntries(Object.keys(definition.uses).map(alias => [alias, alias]));
|
|
91
|
-
return
|
|
90
|
+
return definition.formula(aliases).expression;
|
|
91
|
+
}
|
|
92
|
+
/** The published form; aliases must satisfy the protocol identifier grammar. */
|
|
93
|
+
export function derivedMeasureExpression(definition) {
|
|
94
|
+
return semanticExpression(derivedFormula(definition));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The catalog form. Catalogs describe local datasets that may never be published,
|
|
98
|
+
* so aliases keep their locally valid names (for example `__hypequery_value`, or
|
|
99
|
+
* names past the protocol's length limit) instead of failing catalog creation.
|
|
100
|
+
* Publishing still validates them through `derivedMeasureExpression`.
|
|
101
|
+
*/
|
|
102
|
+
export function derivedMeasureCatalogExpression(definition) {
|
|
103
|
+
return semanticExpression(derivedFormula(definition), {}, name => name);
|
|
92
104
|
}
|
|
93
105
|
export function metricExpression(spec) {
|
|
94
106
|
if (spec.__type === 'aggregation_spec')
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RelationshipKey } from '../types.js';
|
|
1
2
|
import type { DatasetSqlDialect } from '../dataset-sql-dialect.js';
|
|
2
3
|
/**
|
|
3
4
|
* Query-builder-path planning for to-one relationship joins.
|
|
@@ -18,6 +19,7 @@ export interface ResolvedBuilderJoin {
|
|
|
18
19
|
/** Physical target source table. */
|
|
19
20
|
source: string;
|
|
20
21
|
matchMarker?: string;
|
|
22
|
+
keys?: readonly RelationshipKey[];
|
|
21
23
|
/** Base join column (unqualified). */
|
|
22
24
|
from: string;
|
|
23
25
|
/** Target join column (unqualified). */
|
|
@@ -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,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAGnE;;;;;;;;;GASG;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,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,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;CACtD;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,iBAA+C,GACvD,0BAA0B,GAAG,SAAS,
|
|
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,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAGnE;;;;;;;;;GASG;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,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,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;CACtD;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,iBAA+C,GACvD,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;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,MAAM,GACX,MAAM,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,0BAA0B,GAAG,SAAS,GAC1C,gBAAgB,CAoClB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { relationshipKeys } from './relationship-keys.js';
|
|
1
2
|
import { relationshipMeasureSource } from './relationship-measure-sql.js';
|
|
2
3
|
import { clickhouseDatasetSqlDialect } from './clickhouse-dataset-sql-dialect.js';
|
|
3
4
|
import { resolveRelationshipReference } from './relationship-measures.js';
|
|
@@ -29,14 +30,18 @@ export function buildRelationshipBuilderContext(ds, query, context, dialect = cl
|
|
|
29
30
|
if (joinByRelationship.has(relationshipName)) {
|
|
30
31
|
continue;
|
|
31
32
|
}
|
|
32
|
-
const measureNames = (query.measures ?? [])
|
|
33
|
-
|
|
33
|
+
const measureNames = (query.measures ?? [])
|
|
34
|
+
.filter(name => name.startsWith(`${relationshipName}.`))
|
|
35
|
+
.map(name => name.slice(relationshipName.length + 1));
|
|
36
|
+
const keys = relationshipKeys(relationship);
|
|
37
|
+
const projected = measureNames.length ? relationshipMeasureSource(target, keys.map(key => key.to), measureNames, dialect) : undefined;
|
|
34
38
|
joinByRelationship.set(relationshipName, {
|
|
35
39
|
relationship: relationshipName,
|
|
36
40
|
source: projected?.source ?? target.source,
|
|
37
41
|
matchMarker: projected?.marker,
|
|
38
42
|
from: relationship.from,
|
|
39
43
|
to: relationship.to,
|
|
44
|
+
keys,
|
|
40
45
|
tenant: tenantPredicate && target.tenantKey
|
|
41
46
|
? { field: target.tenantKey, ...tenantPredicate }
|
|
42
47
|
: undefined,
|
|
@@ -85,13 +90,20 @@ export function applyRelationshipJoins(qb, ctx) {
|
|
|
85
90
|
'would fan out duplicate target keys and inflate aggregates, so relationship-qualified fields ' +
|
|
86
91
|
'are unavailable with this builder.');
|
|
87
92
|
}
|
|
88
|
-
|
|
93
|
+
const tenant = join.tenant
|
|
89
94
|
? {
|
|
90
95
|
column: `${join.relationship}.${join.tenant.field}`,
|
|
91
96
|
operator: join.tenant.operator,
|
|
92
97
|
value: join.tenant.value,
|
|
93
98
|
}
|
|
94
|
-
: undefined
|
|
99
|
+
: undefined;
|
|
100
|
+
if (join.keys && join.keys.length > 1) {
|
|
101
|
+
// Key-pair form; @hypequery/clickhouse >= 2.13.0.
|
|
102
|
+
const pairs = join.keys.map(key => [`${ctx.baseSource}.${key.from}`, `${join.relationship}.${key.to}`]);
|
|
103
|
+
qb = qb.leftAnyJoin(join.source, [pairs[0], ...pairs.slice(1)], join.relationship, tenant);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
qb = qb.leftAnyJoin(join.source, `${ctx.baseSource}.${join.from}`, `${join.relationship}.${join.to}`, join.relationship, tenant);
|
|
95
107
|
}
|
|
96
108
|
return qb;
|
|
97
109
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RelationshipDefinition, RelationshipKind, RelationshipJoin } from '../types.js';
|
|
2
|
+
export declare function createRelationship<TTarget extends {
|
|
3
|
+
__type: 'dataset';
|
|
4
|
+
name: string;
|
|
5
|
+
}, TKind extends RelationshipKind>(kind: TKind, target: () => TTarget, join: RelationshipJoin): RelationshipDefinition<TTarget, TKind>;
|
|
6
|
+
//# sourceMappingURL=relationship-definition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-definition.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG9F,wBAAgB,kBAAkB,CAChC,OAAO,SAAS;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACnD,KAAK,SAAS,gBAAgB,EAE9B,IAAI,EAAE,KAAK,EACX,MAAM,EAAE,MAAM,OAAO,EACrB,IAAI,EAAE,gBAAgB,GACrB,sBAAsB,CAAC,OAAO,EAAE,KAAK,CAAC,CAOxC"}
|
|
@@ -11,6 +11,8 @@ export interface ToOneRelationshipTarget {
|
|
|
11
11
|
target: AnyDatasetInstance;
|
|
12
12
|
/** Target join column, which a to-one declaration requires to be unique. */
|
|
13
13
|
column: string;
|
|
14
|
+
/** Full target key; absent on legacy single-key findings. */
|
|
15
|
+
columns?: readonly string[];
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Lists the to-one relationships to check. `hasMany` is excluded because it
|
|
@@ -27,6 +29,8 @@ export interface RelationshipKeyIssue {
|
|
|
27
29
|
source: string;
|
|
28
30
|
/** Target join column. */
|
|
29
31
|
column: string;
|
|
32
|
+
/** Full target key; absent on legacy single-key findings. */
|
|
33
|
+
columns?: readonly string[];
|
|
30
34
|
/** Target rows with a non-NULL key, within the checked tenant scope. Large counts are decimal strings. */
|
|
31
35
|
rows: number | string;
|
|
32
36
|
/** Distinct non-NULL keys, within the checked tenant scope. Large counts are decimal strings. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationship-key-check.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-key-check.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAA0B,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"relationship-key-check.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-key-check.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAA0B,MAAM,aAAa,CAAC;AAG9E,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE3D,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,sBAAsB;IACtB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,kBAAkB,EACtB,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GACvB,uBAAuB,EAAE,CA2B3B;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,0GAA0G;IAC1G,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,iGAAiG;IACjG,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAchD;AAMD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,uBAAuB,EAC9B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACnB,oBAAoB,GAAG,SAAS,CAmBlC"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Pure helpers for `checkRelationships`: which relationships a check covers,
|
|
3
3
|
* and how a key-count row becomes a finding.
|
|
4
4
|
*/
|
|
5
|
+
import { relationshipKeys } from './relationship-keys.js';
|
|
5
6
|
/**
|
|
6
7
|
* Lists the to-one relationships to check. `hasMany` is excluded because it
|
|
7
8
|
* never joins, so its key has no uniqueness requirement. An explicit
|
|
@@ -22,12 +23,16 @@ export function listToOneRelationships(ds, only) {
|
|
|
22
23
|
}
|
|
23
24
|
return declared
|
|
24
25
|
.filter(([name, relationship]) => relationship.kind !== 'hasMany' && (!only || only.includes(name)))
|
|
25
|
-
.map(([name, relationship]) =>
|
|
26
|
-
relationship
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
.map(([name, relationship]) => {
|
|
27
|
+
const columns = relationshipKeys(relationship).map(key => key.to);
|
|
28
|
+
return {
|
|
29
|
+
relationship: name,
|
|
30
|
+
kind: relationship.kind,
|
|
31
|
+
target: relationship.target(),
|
|
32
|
+
column: relationship.to,
|
|
33
|
+
...(columns.length > 1 ? { columns } : {}),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
31
36
|
}
|
|
32
37
|
/** Reads a count without losing precision when a driver returns UInt64 as a string. */
|
|
33
38
|
export function readCount(value) {
|
|
@@ -63,10 +68,17 @@ export function relationshipKeyIssue(entry, rows, distinctKeys) {
|
|
|
63
68
|
target: entry.target.name,
|
|
64
69
|
source,
|
|
65
70
|
column: entry.column,
|
|
71
|
+
...(entry.columns ? { columns: [...entry.columns] } : {}),
|
|
66
72
|
rows: displayCount(rows),
|
|
67
73
|
distinctKeys: displayCount(distinctKeys),
|
|
68
|
-
message: `Relationship "${entry.relationship}" is declared ${entry.kind}, but
|
|
74
|
+
message: `Relationship "${entry.relationship}" is declared ${entry.kind}, but ${describeKey(source, entry)} ` +
|
|
69
75
|
`has ${rows} rows for ${distinctKeys} distinct keys. Joins pick an arbitrary matching row; ` +
|
|
70
76
|
'make the key unique or declare the relationship as hasMany.',
|
|
71
77
|
};
|
|
72
78
|
}
|
|
79
|
+
/** `"customers.id"`, or `("customers.id", "customers.region")` for a composite key. */
|
|
80
|
+
function describeKey(source, entry) {
|
|
81
|
+
if (!entry.columns)
|
|
82
|
+
return `"${source}.${entry.column}"`;
|
|
83
|
+
return `(${entry.columns.map(column => `"${source}.${column}"`).join(', ')})`;
|
|
84
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RelationshipDefinition, RelationshipJoin, RelationshipKey } from '../types.js';
|
|
2
|
+
type NormalizedRelationshipJoin = Pick<RelationshipDefinition, 'from' | 'to' | 'keys'>;
|
|
3
|
+
/**
|
|
4
|
+
* Snapshot an authored join. `{ from, to }` passes through unchanged, as it
|
|
5
|
+
* always has; `keys` pairs are validated before they are interpolated into SQL.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeRelationshipJoin(join: RelationshipJoin): NormalizedRelationshipJoin;
|
|
8
|
+
/**
|
|
9
|
+
* Rebuild a serialized composite key. Portable records carry `keys` only for
|
|
10
|
+
* composite relationships, and `from`/`to` must mirror the first pair.
|
|
11
|
+
*/
|
|
12
|
+
export declare function rehydrateCompositeRelationshipJoin(from: string, to: string, keys: readonly RelationshipKey[]): NormalizedRelationshipJoin;
|
|
13
|
+
export declare function validateRelationshipKeys(keys: readonly RelationshipKey[]): asserts keys is readonly [RelationshipKey, ...RelationshipKey[]];
|
|
14
|
+
/** Return the whole equality key; legacy relationships remain single-key. */
|
|
15
|
+
export declare function relationshipKeys(relationship: Pick<RelationshipDefinition, 'from' | 'to' | 'keys'>): readonly RelationshipKey[];
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=relationship-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-keys.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-keys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAI7F,KAAK,0BAA0B,GAAG,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,CAAC;AAEvF;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,gBAAgB,GAAG,0BAA0B,CAgB5F;AAED;;;GAGG;AACH,wBAAgB,kCAAkC,CAChD,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,SAAS,eAAe,EAAE,GAC/B,0BAA0B,CAU5B;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,SAAS,eAAe,EAAE,GAC/B,OAAO,CAAC,IAAI,IAAI,SAAS,CAAC,eAAe,EAAE,GAAG,eAAe,EAAE,CAAC,CAYlE;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,GAAG,SAAS,eAAe,EAAE,CAQ/H"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const COLUMN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot an authored join. `{ from, to }` passes through unchanged, as it
|
|
4
|
+
* always has; `keys` pairs are validated before they are interpolated into SQL.
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeRelationshipJoin(join) {
|
|
7
|
+
if (join.keys === undefined) {
|
|
8
|
+
return { from: join.from, to: join.to };
|
|
9
|
+
}
|
|
10
|
+
if (join.from !== undefined || join.to !== undefined) {
|
|
11
|
+
throw new Error('Relationship keys cannot be combined with from/to.');
|
|
12
|
+
}
|
|
13
|
+
const keys = join.keys;
|
|
14
|
+
validateRelationshipKeys(keys);
|
|
15
|
+
const copy = [
|
|
16
|
+
Object.freeze({ from: keys[0].from, to: keys[0].to }),
|
|
17
|
+
...keys.slice(1).map(key => Object.freeze({ from: key.from, to: key.to })),
|
|
18
|
+
];
|
|
19
|
+
// A single pair is the legacy relationship: contracts and catalogs carry
|
|
20
|
+
// `keys` only for composite relationships, however the pair was authored.
|
|
21
|
+
return { from: copy[0].from, to: copy[0].to, ...(copy.length > 1 ? { keys: Object.freeze(copy) } : {}) };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Rebuild a serialized composite key. Portable records carry `keys` only for
|
|
25
|
+
* composite relationships, and `from`/`to` must mirror the first pair.
|
|
26
|
+
*/
|
|
27
|
+
export function rehydrateCompositeRelationshipJoin(from, to, keys) {
|
|
28
|
+
validateRelationshipKeys(keys);
|
|
29
|
+
if (keys.length < 2) {
|
|
30
|
+
throw new Error('Serialized relationship keys must contain at least two pairs; single-key relationships use from/to.');
|
|
31
|
+
}
|
|
32
|
+
const join = normalizeRelationshipJoin({ keys });
|
|
33
|
+
if (join.from !== from || join.to !== to) {
|
|
34
|
+
throw new Error('Relationship from/to must match the first keys pair.');
|
|
35
|
+
}
|
|
36
|
+
return join;
|
|
37
|
+
}
|
|
38
|
+
export function validateRelationshipKeys(keys) {
|
|
39
|
+
if (!Array.isArray(keys) || keys.length === 0)
|
|
40
|
+
throw new Error('Relationship keys must be a non-empty array.');
|
|
41
|
+
const from = new Set();
|
|
42
|
+
const to = new Set();
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
if (!key || typeof key.from !== 'string' || typeof key.to !== 'string' || !COLUMN.test(key.from) || !COLUMN.test(key.to)) {
|
|
45
|
+
throw new Error('Relationship keys must contain safe physical from/to column identifiers.');
|
|
46
|
+
}
|
|
47
|
+
if (from.has(key.from) || to.has(key.to))
|
|
48
|
+
throw new Error('Relationship keys must not repeat a source or target column.');
|
|
49
|
+
from.add(key.from);
|
|
50
|
+
to.add(key.to);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Return the whole equality key; legacy relationships remain single-key. */
|
|
54
|
+
export function relationshipKeys(relationship) {
|
|
55
|
+
const keys = relationship.keys ?? [{ from: relationship.from, to: relationship.to }];
|
|
56
|
+
if (!relationship.keys)
|
|
57
|
+
return keys;
|
|
58
|
+
validateRelationshipKeys(keys);
|
|
59
|
+
if (keys[0].from !== relationship.from || keys[0].to !== relationship.to) {
|
|
60
|
+
throw new Error('Relationship from/to must match the first keys pair.');
|
|
61
|
+
}
|
|
62
|
+
return keys;
|
|
63
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { AnyDatasetInstance } from '../types.js';
|
|
|
2
2
|
import type { RelationshipBuilderContext } from './relationship-builder-plan.js';
|
|
3
3
|
import type { DatasetSqlDialect } from '../dataset-sql-dialect.js';
|
|
4
4
|
/** Explicit projection avoids marker collisions and makes absent targets detectable under join_use_nulls=0. */
|
|
5
|
-
export declare function relationshipMeasureSource(target: AnyDatasetInstance, to: string, names: readonly string[], dialect: DatasetSqlDialect): {
|
|
5
|
+
export declare function relationshipMeasureSource(target: AnyDatasetInstance, to: readonly string[], names: readonly string[], dialect: DatasetSqlDialect): {
|
|
6
6
|
source: string;
|
|
7
7
|
marker: string;
|
|
8
8
|
};
|
|
@@ -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,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEnE,+GAA+G;AAC/G,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,iBAAiB;;;
|
|
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,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEnE,+GAA+G;AAC/G,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,iBAAiB;;;EAgBhJ;AAED,wBAAgB,8BAA8B,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,0BAA0B;;;;EAevH"}
|
|
@@ -4,7 +4,7 @@ import { measureToAggregationSpec } from './dataset-normalization.js';
|
|
|
4
4
|
import { applyFilteredAggregationExpression } from './filtered-aggregation-sql.js';
|
|
5
5
|
/** Explicit projection avoids marker collisions and makes absent targets detectable under join_use_nulls=0. */
|
|
6
6
|
export function relationshipMeasureSource(target, to, names, dialect) {
|
|
7
|
-
const columns = new Set([to, ...(target.tenantKey ? [target.tenantKey] : [])]);
|
|
7
|
+
const columns = new Set([...to, ...(target.tenantKey ? [target.tenantKey] : [])]);
|
|
8
8
|
for (const [name, dimension] of Object.entries(target.dimensions))
|
|
9
9
|
if (!dimension.sql)
|
|
10
10
|
columns.add(dimension.column ?? name);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-query-syntax.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-query-syntax.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"semantic-query-syntax.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-query-syntax.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7D,oFAAoF;AACpF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,YAAY,GAAG,WAAW,GAAG,MAAM,EAAE,CAarF;AAED,kEAAkE;AAClE,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAIzE"}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const FILTER_OPERATORS = new Set(SEMANTIC_FILTER_OPERATORS);
|
|
1
|
+
import { isSemanticFilterOperator } from '../constants.js';
|
|
3
2
|
/** Reject SQL structure outside the semantic contract before reaching a builder. */
|
|
4
3
|
export function semanticQuerySyntaxErrors(query) {
|
|
5
4
|
const errors = [];
|
|
6
5
|
for (const filter of query.filters ?? []) {
|
|
7
|
-
if (!
|
|
6
|
+
if (!isSemanticFilterOperator(filter.operator)) {
|
|
8
7
|
errors.push(`Unsupported semantic filter operator "${String(filter.operator)}".`);
|
|
9
8
|
}
|
|
10
9
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shift-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/shift-measure-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAI1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"shift-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/shift-measure-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAI1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAK9D,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,MAAM;;;;;EAoBlG;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,GAAG,MAAM,CAGpG;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAOrH;AAED,+FAA+F;AAC/F,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,sBAAsB,EAAE,KAAK,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,MAAM,CAMlH;AAED,2FAA2F;AAC3F,wBAAgB,eAAe,CAAC,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAsBhK"}
|
|
@@ -3,18 +3,25 @@ import { addTimeSql, subtractTimeSql } from './time-arithmetic-sql.js';
|
|
|
3
3
|
import { calendarBucketGuardSql } from './time-axis-calendar-guard.js';
|
|
4
4
|
import { calendarShiftAtFineGrain } from './shift-measure-grains.js';
|
|
5
5
|
import { shiftWallTimeGuardSql } from './shift-wall-time-sql.js';
|
|
6
|
+
import { anchoredCalendarShiftSql } from './calendar-shift-sql.js';
|
|
6
7
|
/** Preserve the selected portion of a bucket and both authored endpoint operators. */
|
|
7
8
|
export function shiftRangeSql(shift, axis, period) {
|
|
8
9
|
const first = period ?? '_hq_first';
|
|
9
10
|
const last = period ?? '_hq_last';
|
|
10
11
|
const end = addTimeSql(last, 1, axis.grain);
|
|
11
|
-
const
|
|
12
|
+
const calendar = calendarShiftAtFineGrain(shift.interval, axis.grain);
|
|
12
13
|
// Clamp the bucket anchor, then retain its width. Independently clamping both
|
|
13
14
|
// dates would turn March 30–31 into February 28–28, dropping an entire day.
|
|
15
|
+
// A partial bucket keeps the matching slice of that shifted range, so both
|
|
16
|
+
// bounds are placed relative to the anchor rather than shifted on their own.
|
|
17
|
+
const shiftBound = (point, anchor) => calendar
|
|
18
|
+
? anchoredCalendarShiftSql(point, anchor, shift.interval)
|
|
19
|
+
: subtractTimeSql(point, shift.interval.amount, shift.interval.unit);
|
|
20
|
+
const upper = shiftBound(`least(${end}, _hq_upper)`, last);
|
|
14
21
|
const shiftedEnd = addTimeSql(subtractTimeSql(last, shift.interval.amount, shift.interval.unit), 1, axis.grain);
|
|
15
22
|
return {
|
|
16
|
-
lower:
|
|
17
|
-
upper:
|
|
23
|
+
lower: shiftBound(`greatest(${first}, _hq_lower)`, first),
|
|
24
|
+
upper: calendar ? `if(_hq_upper >= ${end}, ${shiftedEnd}, ${upper})` : upper,
|
|
18
25
|
lowerInclusive: period ? `(${period} > _hq_lower OR ${Number(axis.lowerInclusive)})` : String(Number(axis.lowerInclusive)),
|
|
19
26
|
upperInclusive: `(_hq_upper < ${end} AND ${Number(axis.upperInclusive)})`,
|
|
20
27
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-measure-context-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-context-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"time-measure-context-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-context-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAO9D,8EAA8E;AAC9E,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,SAAS,wBAAwB,EAAE,EAAE,IAAI,EAAE,eAAe,GAAG,MAAM,EAAE,CAerH;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,EAAE,IAAI,EAAE,eAAe,GAAG,MAAM,CAOxG"}
|
|
@@ -2,16 +2,22 @@ import { addTimeSql as add, subtractTimeSql as subtract } from './time-arithmeti
|
|
|
2
2
|
import { calendarShiftAtFineGrain } from './shift-measure-grains.js';
|
|
3
3
|
import { calendarBucketGuardSql } from './time-axis-calendar-guard.js';
|
|
4
4
|
import { shiftWallTimeGuardSql } from './shift-wall-time-sql.js';
|
|
5
|
+
import { anchoredCalendarShiftSql } from './calendar-shift-sql.js';
|
|
5
6
|
/** Keep output coordinates separate from each shifted evaluation interval. */
|
|
6
7
|
export function timeMeasureContextCtes(contexts, axis) {
|
|
7
8
|
const end = add('_hq_period', 1, axis.grain);
|
|
8
9
|
const root = `_hq_context0 AS (SELECT _hq_period, _hq_period AS _hq_eval_period, ${end} AS _hq_eval_end, greatest(_hq_period, _hq_lower) AS _hq_eval_lower, least(${end}, _hq_upper) AS _hq_eval_upper, (_hq_period > _hq_lower OR ${Number(axis.lowerInclusive)}) AS _hq_lower_inclusive, (_hq_upper < ${end} AND ${Number(axis.upperInclusive)}) AS _hq_upper_inclusive FROM _hq_series CROSS JOIN _hq_bounds)`;
|
|
9
10
|
return [root, ...contexts.slice(1).map(context => {
|
|
10
11
|
const interval = context.interval;
|
|
12
|
+
const calendar = calendarShiftAtFineGrain(interval, axis.grain);
|
|
11
13
|
const shift = (value) => subtract(`p.${value}`, interval.amount, interval.unit);
|
|
14
|
+
// Partial bounds stay inside the shifted bucket; see anchoredCalendarShiftSql.
|
|
15
|
+
const shiftBound = (value) => calendar
|
|
16
|
+
? anchoredCalendarShiftSql(`p.${value}`, 'p._hq_eval_period', interval)
|
|
17
|
+
: shift(value);
|
|
12
18
|
const period = shift('_hq_eval_period');
|
|
13
|
-
const shiftedEnd =
|
|
14
|
-
return `_hq_context${context.id} AS (SELECT p._hq_period, ${period} AS _hq_eval_period, ${shiftedEnd} AS _hq_eval_end, ${
|
|
19
|
+
const shiftedEnd = calendar ? add(period, 1, axis.grain) : shift('_hq_eval_end');
|
|
20
|
+
return `_hq_context${context.id} AS (SELECT p._hq_period, ${period} AS _hq_eval_period, ${shiftedEnd} AS _hq_eval_end, ${shiftBound('_hq_eval_lower')} AS _hq_eval_lower, if(p._hq_eval_upper = p._hq_eval_end, ${shiftedEnd}, ${shiftBound('_hq_eval_upper')}) AS _hq_eval_upper, p._hq_lower_inclusive, p._hq_upper_inclusive FROM _hq_context${context.parent} AS p)`;
|
|
15
21
|
})];
|
|
16
22
|
}
|
|
17
23
|
export function timeMeasureContextGuard(context, axis) {
|