@hypequery/datasets 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/api.type-test.js +7 -0
- 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 +12 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +14 -1
- 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 +50 -0
- package/dist/dataset-compilation.d.ts.map +1 -0
- package/dist/dataset-compilation.js +1 -0
- package/dist/dataset-query.d.ts +3 -3
- package/dist/dataset-query.d.ts.map +1 -1
- package/dist/dataset-query.js +7 -71
- package/dist/dataset-sql-dialect.d.ts +11 -0
- package/dist/dataset-sql-dialect.d.ts.map +1 -0
- package/dist/dataset-sql-dialect.js +1 -0
- package/dist/dataset.d.ts +3 -2
- package/dist/dataset.d.ts.map +1 -1
- package/dist/execution-capabilities.d.ts +28 -0
- package/dist/execution-capabilities.d.ts.map +1 -0
- package/dist/execution-capabilities.js +17 -0
- package/dist/execution-capabilities.json +13 -0
- package/dist/executor.d.ts +17 -38
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +40 -400
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/internal.d.ts +3 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -0
- package/dist/measure.d.ts +54 -17
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +3 -63
- package/dist/metric-query-engine.d.ts +29 -0
- package/dist/metric-query-engine.d.ts.map +1 -0
- package/dist/metric-query-engine.js +106 -0
- package/dist/portable-executor.d.ts +20 -6
- package/dist/portable-executor.d.ts.map +1 -1
- package/dist/portable-executor.js +29 -9
- 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 +11 -0
- package/dist/query-builder-protocol.d.ts.map +1 -1
- package/dist/query-helpers.d.ts +10 -10
- package/dist/query-helpers.d.ts.map +1 -1
- package/dist/query-helpers.js +1 -3
- package/dist/query-planner.d.ts +7 -3
- package/dist/query-planner.d.ts.map +1 -1
- package/dist/query-planner.js +10 -12
- 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 +4 -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 +31 -4
- package/dist/types.d.ts +67 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/agent-catalog-projection.d.ts +1 -0
- package/dist/utils/agent-catalog-projection.d.ts.map +1 -1
- package/dist/utils/agent-catalog-projection.js +2 -1
- package/dist/utils/base-measure-definition.d.ts +23 -0
- package/dist/utils/base-measure-definition.d.ts.map +1 -0
- package/dist/utils/base-measure-definition.js +65 -0
- package/dist/utils/build-dataset-query-builder.d.ts +5 -0
- package/dist/utils/build-dataset-query-builder.d.ts.map +1 -0
- package/dist/utils/build-dataset-query-builder.js +65 -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/clickhouse-dataset-sql-dialect.d.ts +4 -0
- package/dist/utils/clickhouse-dataset-sql-dialect.d.ts.map +1 -0
- package/dist/utils/clickhouse-dataset-sql-dialect.js +6 -0
- package/dist/utils/compile-dataset-query.d.ts +10 -0
- package/dist/utils/compile-dataset-query.d.ts.map +1 -0
- package/dist/utils/compile-dataset-query.js +72 -0
- package/dist/utils/composite-time-measure-sql.d.ts.map +1 -1
- package/dist/utils/composite-time-measure-sql.js +2 -1
- 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 +6 -2
- package/dist/utils/dataset-derived-query.d.ts.map +1 -1
- package/dist/utils/dataset-derived-query.js +28 -26
- package/dist/utils/dataset-filter-types.d.ts +28 -0
- package/dist/utils/dataset-filter-types.d.ts.map +1 -0
- package/dist/utils/dataset-filter-types.js +1 -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 +17 -3
- package/dist/utils/dataset-sql-dialect.d.ts +5 -0
- package/dist/utils/dataset-sql-dialect.d.ts.map +1 -0
- package/dist/utils/dataset-sql-dialect.js +5 -0
- package/dist/utils/dataset-target.d.ts +3 -0
- package/dist/utils/dataset-target.d.ts.map +1 -0
- package/dist/utils/dataset-target.js +3 -0
- package/dist/utils/filter-definition.d.ts +3 -0
- package/dist/utils/filter-definition.d.ts.map +1 -0
- package/dist/utils/filter-definition.js +3 -0
- package/dist/utils/metric-derived-query.d.ts +8 -0
- package/dist/utils/metric-derived-query.d.ts.map +1 -0
- package/dist/utils/metric-derived-query.js +106 -0
- package/dist/utils/metric-query-builder.d.ts +5 -0
- package/dist/utils/metric-query-builder.d.ts.map +1 -0
- package/dist/utils/metric-query-builder.js +50 -0
- package/dist/utils/metric-query-result.d.ts +10 -0
- package/dist/utils/metric-query-result.d.ts.map +1 -0
- package/dist/utils/metric-query-result.js +18 -0
- package/dist/utils/metric-query-validation.d.ts +6 -0
- package/dist/utils/metric-query-validation.d.ts.map +1 -0
- package/dist/utils/metric-query-validation.js +116 -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 +6 -2
- package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
- package/dist/utils/relationship-builder-plan.js +24 -15
- 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 +14 -0
- package/dist/utils/relationship-measure-sql.d.ts.map +1 -0
- package/dist/utils/relationship-measure-sql.js +44 -0
- package/dist/utils/relationship-measure-types.d.ts +36 -0
- package/dist/utils/relationship-measure-types.d.ts.map +1 -0
- package/dist/utils/relationship-measure-types.js +1 -0
- package/dist/utils/relationship-measures.d.ts +23 -0
- package/dist/utils/relationship-measures.d.ts.map +1 -0
- package/dist/utils/relationship-measures.js +62 -0
- package/dist/utils/relationship-validation.d.ts +1 -1
- package/dist/utils/relationship-validation.d.ts.map +1 -1
- package/dist/utils/relationship-validation.js +3 -5
- package/dist/utils/semantic-query-syntax.d.ts +6 -0
- package/dist/utils/semantic-query-syntax.d.ts.map +1 -0
- package/dist/utils/semantic-query-syntax.js +24 -0
- package/dist/utils/shift-measure-sql.d.ts.map +1 -1
- package/dist/utils/shift-measure-sql.js +10 -3
- package/dist/utils/snapshot-execution-context.d.ts +4 -0
- package/dist/utils/snapshot-execution-context.d.ts.map +1 -0
- package/dist/utils/snapshot-execution-context.js +16 -0
- package/dist/utils/time-measure-context-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-context-sql.js +8 -2
- package/dist/utils/time-measure-dataset-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-dataset-sql.js +3 -2
- package/dist/utils/time-measure-result-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-result-sql.js +2 -1
- package/dist/utils/time-measure-source-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-source-sql.js +8 -4
- package/dist/validation.js +1 -1
- package/package.json +10 -5
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { queryMeasureDefinitions } from './relationship-measures.js';
|
|
2
|
+
import { buildDatasetQueryBuilder } from './build-dataset-query-builder.js';
|
|
3
|
+
import { baseMeasureNames } from './dataset-measures.js';
|
|
4
|
+
import { buildDerivedDatasetSql, hasSelectedDerivedMeasure, needsOuterDatasetQuery } from './dataset-derived-query.js';
|
|
5
|
+
import { measureDependencyNames } from './measure-dependencies.js';
|
|
6
|
+
import { overfetchLimit } from './pagination.js';
|
|
7
|
+
import { resolveResultLimit } from './result-limits.js';
|
|
8
|
+
import { buildTimeMeasureDatasetSql } from './time-measure-dataset-sql.js';
|
|
9
|
+
import { selectedTimeMeasures } from './time-query-measures.js';
|
|
10
|
+
/** The one dispatch point for dataset preview and query-builder execution. */
|
|
11
|
+
export function prepareDatasetQuery(dataset, input, options, mode = 'execution') {
|
|
12
|
+
const query = structuredClone({
|
|
13
|
+
...input,
|
|
14
|
+
limit: mode === 'execution' ? resolveResultLimit(input.limit, dataset.limits).limit : input.limit,
|
|
15
|
+
});
|
|
16
|
+
// Preserve omission while validating/planning: maxMeasures constrains an
|
|
17
|
+
// explicit selection, not the dataset's default set of base measures.
|
|
18
|
+
const measures = query.measures ?? baseMeasureNames(dataset.measures);
|
|
19
|
+
const executionLimit = mode === 'execution' ? overfetchLimit(query.limit) : query.limit;
|
|
20
|
+
const executionOptions = { ...options, executionLimit };
|
|
21
|
+
const preflightStatements = [];
|
|
22
|
+
let statement;
|
|
23
|
+
let execute;
|
|
24
|
+
let plan;
|
|
25
|
+
const abortSignal = options.context?.abortSignal;
|
|
26
|
+
if (selectedTimeMeasures(dataset, query).size) {
|
|
27
|
+
plan = 'time';
|
|
28
|
+
const compiled = buildTimeMeasureDatasetSql(dataset, query, executionOptions);
|
|
29
|
+
statement = compiled;
|
|
30
|
+
preflightStatements.push({ sql: compiled.timeAxisSql, parameters: compiled.parameters });
|
|
31
|
+
execute = async () => {
|
|
32
|
+
await options.builderFactory.rawQuery(compiled.timeAxisSql, compiled.parameters, { abortSignal });
|
|
33
|
+
return options.builderFactory.rawQuery(compiled.sql, compiled.parameters, { abortSignal });
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
else if (needsOuterDatasetQuery(dataset, query)) {
|
|
37
|
+
plan = hasSelectedDerivedMeasure(dataset, query) ? 'derived' : 'aggregate';
|
|
38
|
+
statement = buildDerivedDatasetSql(dataset, query, executionOptions, buildDatasetQueryBuilder);
|
|
39
|
+
execute = () => options.builderFactory.rawQuery(statement.sql, statement.parameters, { abortSignal });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
plan = 'aggregate';
|
|
43
|
+
const builder = buildDatasetQueryBuilder(dataset, query, executionOptions);
|
|
44
|
+
statement = builder.toSQLWithParams();
|
|
45
|
+
// Retain builder execution: custom builders may carry adapter settings.
|
|
46
|
+
execute = () => builder.execute({ abortSignal });
|
|
47
|
+
}
|
|
48
|
+
const description = Object.freeze({
|
|
49
|
+
kind: 'dataset-compilation', version: 1, dataset: dataset.name, plan,
|
|
50
|
+
dimensions: Object.freeze([...(query.dimensions ?? [])]),
|
|
51
|
+
measures: Object.freeze([...measures]),
|
|
52
|
+
measureDependencies: Object.freeze(measureDependencyNames(queryMeasureDefinitions(dataset, measures), measures)),
|
|
53
|
+
filters: Object.freeze((query.filters ?? []).map(({ field, operator }) => Object.freeze({ field, operator }))),
|
|
54
|
+
having: Object.freeze((query.having ?? []).map(({ measure, operator }) => Object.freeze({ measure, operator }))),
|
|
55
|
+
segments: Object.freeze([...(query.segments ?? [])]),
|
|
56
|
+
by: query.by, timezone: query.timezone,
|
|
57
|
+
effectiveLimit: query.limit, executionLimit, offset: query.offset ?? 0,
|
|
58
|
+
parameterCount: statement.parameters.length,
|
|
59
|
+
preflightStatementCount: preflightStatements.length,
|
|
60
|
+
});
|
|
61
|
+
const compilation = Object.freeze({
|
|
62
|
+
sql: statement.sql,
|
|
63
|
+
parameters: Object.freeze(structuredClone(statement.parameters)),
|
|
64
|
+
query: Object.freeze(structuredClone({ ...query, measures })),
|
|
65
|
+
preflightStatements: Object.freeze(preflightStatements.map(preflight => Object.freeze({
|
|
66
|
+
sql: preflight.sql, parameters: Object.freeze(structuredClone(preflight.parameters)),
|
|
67
|
+
}))),
|
|
68
|
+
describe: () => description,
|
|
69
|
+
toJSON: () => description,
|
|
70
|
+
});
|
|
71
|
+
return { compilation, execute };
|
|
72
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composite-time-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/composite-time-measure-sql.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"composite-time-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/composite-time-measure-sql.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAW9D,mFAAmF;AACnF,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,4BAA4B,EAAE,IAAI,EAAE,eAAe;;;;EAgDrJ"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { queryMeasureDefinitions } from './relationship-measures.js';
|
|
1
2
|
import { quoteSQLIdentifier } from '../sql-utils.js';
|
|
2
3
|
import { isDerivedMeasure, isWindowMeasure } from './dataset-measures.js';
|
|
3
4
|
import { buildMeasureEvaluationGraph, evaluationFormula } from './measure-evaluation-graph.js';
|
|
@@ -9,7 +10,7 @@ import { derivedProjection } from './derived-measure-sql.js';
|
|
|
9
10
|
import { buildTimeMeasureResultSql } from './time-measure-result-sql.js';
|
|
10
11
|
/** Aggregate each dependency in its evaluation context, then evaluate formulas. */
|
|
11
12
|
export function buildCompositeTimeMeasureSql(ds, query, options, axis) {
|
|
12
|
-
const graph = buildMeasureEvaluationGraph(ds.measures, query.measures ?? []);
|
|
13
|
+
const graph = buildMeasureEvaluationGraph(queryMeasureDefinitions(ds, query.measures ?? []), query.measures ?? []);
|
|
13
14
|
const leaves = graph.nodes.filter(node => !isDerivedMeasure(node.definition));
|
|
14
15
|
const baseNames = new Set(leaves.map(node => isWindowMeasure(node.definition) ? node.definition.measure : node.name));
|
|
15
16
|
const source = buildTimeMeasureSourceSql(ds, query, baseNames, axis.filters, options);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-definition-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-definition-validation.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,wBAAwB,EACxB,mBAAmB,
|
|
1
|
+
{"version":3,"file":"dataset-definition-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-definition-validation.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,wBAAwB,EACxB,mBAAmB,EAEnB,sBAAsB,EAEvB,MAAM,aAAa,CAAC;AAQrB,KAAK,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AACzD,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;AAC5D,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AAwR/D;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,aAAa,EAAE,WAAW,EAAE,gBAAgB,CAAC,GAClE,IAAI,CA2DN"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateShiftMeasures } from './shift-measure-validation.js';
|
|
2
2
|
import { escapeRegExp, isSafeSQLIdentifier, stripSqlLiterals } from '../sql-utils.js';
|
|
3
|
+
import { SEMANTIC_FILTER_OPERATORS, isSemanticFilterOperator } from '../constants.js';
|
|
3
4
|
import { validateDerivedMeasures } from './derived-measure-validation.js';
|
|
4
5
|
import { isDerivedMeasure, isWindowMeasure, isShiftMeasure } from './dataset-measures.js';
|
|
5
6
|
import { validateWindowMeasures } from './window-measure-validation.js';
|
|
@@ -113,7 +114,54 @@ function validateDimensions(datasetName, dimensions) {
|
|
|
113
114
|
assertSafeColumn(datasetName, definition.column ?? name, `dimension "${name}" column`);
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
|
-
function
|
|
117
|
+
function assertSemanticOperator(datasetName, operator, context) {
|
|
118
|
+
if (!isSemanticFilterOperator(operator)) {
|
|
119
|
+
fail(datasetName, `${context} uses unsupported operator "${String(operator)}". ` +
|
|
120
|
+
`Supported: ${SEMANTIC_FILTER_OPERATORS.join(', ')}.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Validates the declared filter map. A filter's `field` names the dimension it
|
|
125
|
+
* filters on; when it is not a declared dimension it falls through to SQL as a
|
|
126
|
+
* bare column, so it must then be a safe identifier.
|
|
127
|
+
*/
|
|
128
|
+
function validateFilters(datasetName, filters, dimensions) {
|
|
129
|
+
for (const [name, definition] of Object.entries(filters ?? {})) {
|
|
130
|
+
// JavaScript callers can omit `field`; never let it stringify to "undefined".
|
|
131
|
+
if (typeof definition.field !== 'string') {
|
|
132
|
+
fail(datasetName, `filter "${name}" must name a field.`);
|
|
133
|
+
}
|
|
134
|
+
if (!Object.hasOwn(dimensions, definition.field)) {
|
|
135
|
+
assertSafeColumn(datasetName, definition.field, `filter "${name}" field`);
|
|
136
|
+
}
|
|
137
|
+
for (const operator of definition.operators ?? []) {
|
|
138
|
+
assertSemanticOperator(datasetName, operator, `filter "${name}"`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Validates a measure's fixed filters. These are rendered straight into the
|
|
144
|
+
* aggregate's `if(...)` condition, so the field must resolve to a declared
|
|
145
|
+
* dimension or filter, or else be a safe (optionally relationship-qualified)
|
|
146
|
+
* identifier, and the operator must be one the renderer knows.
|
|
147
|
+
*/
|
|
148
|
+
function validateMeasureFilters(datasetName, measureName, filters, dimensions, declaredFilters) {
|
|
149
|
+
for (const filter of filters) {
|
|
150
|
+
const context = `measure "${measureName}" filter on "${String(filter.field)}"`;
|
|
151
|
+
if (typeof filter.field !== 'string') {
|
|
152
|
+
fail(datasetName, `${context} must name a field.`);
|
|
153
|
+
}
|
|
154
|
+
const declared = Object.hasOwn(dimensions, filter.field)
|
|
155
|
+
|| (declaredFilters !== undefined && Object.hasOwn(declaredFilters, filter.field));
|
|
156
|
+
if (!declared && !isSafeQualifiedName(filter.field, 2)) {
|
|
157
|
+
fail(datasetName, `${context} is not a declared dimension or a safe column identifier. Measure filter ` +
|
|
158
|
+
'fields are interpolated into SQL, so they must contain only letters, numbers and ' +
|
|
159
|
+
'underscores (optionally "<relationship>.<field>").');
|
|
160
|
+
}
|
|
161
|
+
assertSemanticOperator(datasetName, filter.operator, context);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function validateMeasures(datasetName, measures, dimensions, declaredFilters) {
|
|
117
165
|
for (const [name, definition] of Object.entries(measures)) {
|
|
118
166
|
assertSafeName(datasetName, 'measure', name);
|
|
119
167
|
if (typeof definition !== 'object' || definition === null) {
|
|
@@ -127,6 +175,7 @@ function validateMeasures(datasetName, measures, dimensions) {
|
|
|
127
175
|
if (definition.sql !== undefined) {
|
|
128
176
|
validateRawSql(datasetName, 'measure', name, definition.sql, definition.dependencies);
|
|
129
177
|
}
|
|
178
|
+
validateMeasureFilters(datasetName, name, definition.filters ?? [], dimensions, declaredFilters);
|
|
130
179
|
// `field` and `argField` name either a declared dimension or a physical
|
|
131
180
|
// column that the model deliberately does not expose (the `allowHiddenField`
|
|
132
181
|
// case in metric validation). Either way the value reaches SQL, so a name
|
|
@@ -199,11 +248,12 @@ export function validateDatasetDefinition(name, config) {
|
|
|
199
248
|
// measure-only model is a legitimate thing to define.
|
|
200
249
|
const dimensions = config.dimensions ?? {};
|
|
201
250
|
validateDimensions(name, dimensions);
|
|
251
|
+
validateFilters(name, config.filters, dimensions);
|
|
202
252
|
const measures = config.measures ?? {};
|
|
203
253
|
validateWindowMeasures(name, config.timeKey, measures);
|
|
204
254
|
validateShiftMeasures(name, config.timeKey, measures);
|
|
205
255
|
validateDerivedMeasures(name, measures);
|
|
206
|
-
validateMeasures(name, measures, dimensions);
|
|
256
|
+
validateMeasures(name, measures, dimensions, config.filters);
|
|
207
257
|
validateLimits(name, config.limits);
|
|
208
258
|
validateDatasetAgentMetadata(name, config);
|
|
209
259
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import type { AnyDatasetInstance, DatasetQuery
|
|
1
|
+
import type { AnyDatasetInstance, DatasetQuery } from '../types.js';
|
|
2
2
|
import type { QueryBuilderLike } from '../query-builder-protocol.js';
|
|
3
3
|
import type { DatasetQueryExecutionOptions } from '../dataset-query.js';
|
|
4
4
|
type BuildBaseQuery = (dataset: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions) => QueryBuilderLike;
|
|
5
5
|
export declare function hasSelectedDerivedMeasure(ds: AnyDatasetInstance, query: DatasetQuery): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* True when the query needs an outer query over the grouped result: derived
|
|
8
|
+
* measures are computed there, and `having` conditions filter there.
|
|
9
|
+
*/
|
|
10
|
+
export declare function needsOuterDatasetQuery(ds: AnyDatasetInstance, query: DatasetQuery): boolean;
|
|
6
11
|
export declare function buildDerivedDatasetSql(ds: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions, buildBaseQuery: BuildBaseQuery): {
|
|
7
12
|
sql: string;
|
|
8
13
|
parameters: unknown[];
|
|
9
14
|
};
|
|
10
|
-
export declare function runDerivedDatasetQuery(ds: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions, buildBaseQuery: BuildBaseQuery): Promise<DatasetQueryResult>;
|
|
11
15
|
export {};
|
|
12
16
|
//# sourceMappingURL=dataset-derived-query.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset-derived-query.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-derived-query.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dataset-derived-query.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-derived-query.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAGxE,KAAK,cAAc,GAAG,CACpB,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,KAClC,gBAAgB,CAAC;AAEtB,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAE9F;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAE3F;AAED,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,EACrC,cAAc,EAAE,cAAc,GAC7B;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,EAAE,CAAA;CAAE,CAqDxC"}
|
|
@@ -1,27 +1,37 @@
|
|
|
1
|
+
import { queryMeasureDefinitions } from './relationship-measures.js';
|
|
2
|
+
import { semanticOrderDirection } from './semantic-query-syntax.js';
|
|
1
3
|
import { measureDependencyNames } from './measure-dependencies.js';
|
|
2
4
|
import { derivedExpressionSql } from './derived-measure-sql.js';
|
|
3
5
|
import { baseMeasureNames, getBaseMeasure, getDerivedMeasure } from './dataset-measures.js';
|
|
4
|
-
import {
|
|
6
|
+
import { resolveDatasetSqlDialect } from './dataset-sql-dialect.js';
|
|
5
7
|
import { validateDatasetQueryInput } from './dataset-query-validation.js';
|
|
6
|
-
import {
|
|
7
|
-
import { toDatasetQueryResult } from './dataset-query-result.js';
|
|
8
|
+
import { datasetHavingSql } from './dataset-having.js';
|
|
8
9
|
export function hasSelectedDerivedMeasure(ds, query) {
|
|
9
10
|
return (query.measures ?? []).some(name => getDerivedMeasure(ds.measures, name) !== undefined);
|
|
10
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* True when the query needs an outer query over the grouped result: derived
|
|
14
|
+
* measures are computed there, and `having` conditions filter there.
|
|
15
|
+
*/
|
|
16
|
+
export function needsOuterDatasetQuery(ds, query) {
|
|
17
|
+
return hasSelectedDerivedMeasure(ds, query) || (query.having?.length ?? 0) > 0;
|
|
18
|
+
}
|
|
11
19
|
export function buildDerivedDatasetSql(ds, query, options, buildBaseQuery) {
|
|
12
20
|
const validation = validateDatasetQueryInput(ds, query, options.context);
|
|
13
21
|
if (!validation.valid) {
|
|
14
22
|
throw new Error(`Invalid dataset query: ${validation.errors.join('; ')}`);
|
|
15
23
|
}
|
|
24
|
+
const dialect = resolveDatasetSqlDialect(options.builderFactory);
|
|
16
25
|
const selected = query.measures ?? baseMeasureNames(ds.measures);
|
|
17
|
-
const baseMeasures = measureDependencyNames(ds
|
|
26
|
+
const baseMeasures = measureDependencyNames(queryMeasureDefinitions(ds, selected), selected).filter(name => name.includes('.') || getBaseMeasure(ds.measures, name));
|
|
18
27
|
const resolve = (name) => {
|
|
19
28
|
const derived = getDerivedMeasure(ds.measures, name);
|
|
20
|
-
return derived ? `(${derivedExpressionSql(derived, resolve)})` :
|
|
29
|
+
return derived ? `(${derivedExpressionSql(derived, resolve)})` : dialect.quoteIdentifier(name);
|
|
21
30
|
};
|
|
22
31
|
const inner = buildBaseQuery(ds, {
|
|
23
32
|
...query,
|
|
24
33
|
measures: [...baseMeasures],
|
|
34
|
+
having: undefined,
|
|
25
35
|
orderBy: undefined,
|
|
26
36
|
limit: undefined,
|
|
27
37
|
offset: undefined,
|
|
@@ -29,21 +39,28 @@ export function buildDerivedDatasetSql(ds, query, options, buildBaseQuery) {
|
|
|
29
39
|
const { sql: innerSql, parameters } = inner.toSQLWithParams();
|
|
30
40
|
const projections = [];
|
|
31
41
|
if (query.by)
|
|
32
|
-
projections.push(
|
|
42
|
+
projections.push(dialect.quoteIdentifier('period'));
|
|
33
43
|
for (const dimension of query.dimensions ?? [])
|
|
34
|
-
projections.push(
|
|
44
|
+
projections.push(dialect.quoteIdentifier(dimension));
|
|
35
45
|
for (const name of selected) {
|
|
36
46
|
const derived = getDerivedMeasure(ds.measures, name);
|
|
37
47
|
projections.push(derived
|
|
38
|
-
? `${derivedExpressionSql(derived, resolve)} AS ${
|
|
39
|
-
:
|
|
48
|
+
? `${derivedExpressionSql(derived, resolve)} AS ${dialect.quoteIdentifier(name)}`
|
|
49
|
+
: dialect.quoteIdentifier(name));
|
|
40
50
|
}
|
|
41
51
|
let sql = `WITH base AS (${innerSql}) SELECT ${projections.join(', ')} FROM base`;
|
|
52
|
+
// Conditions read the same expression the projection does, never the output
|
|
53
|
+
// alias, so the predicate stays valid on dialects without alias resolution.
|
|
54
|
+
if (query.having?.length) {
|
|
55
|
+
const having = datasetHavingSql(query.having, resolve);
|
|
56
|
+
sql += ` WHERE ${having.sql}`;
|
|
57
|
+
parameters.push(...having.parameters);
|
|
58
|
+
}
|
|
42
59
|
if (query.orderBy?.length) {
|
|
43
|
-
sql += ` ORDER BY ${query.orderBy.map(order => (`${
|
|
60
|
+
sql += ` ORDER BY ${query.orderBy.map(order => (`${dialect.quoteIdentifier(order.field)} ${semanticOrderDirection(order.direction)}`)).join(', ')}`;
|
|
44
61
|
}
|
|
45
62
|
else if (query.by) {
|
|
46
|
-
sql += ` ORDER BY ${
|
|
63
|
+
sql += ` ORDER BY ${dialect.quoteIdentifier('period')} ASC`;
|
|
47
64
|
}
|
|
48
65
|
const limit = options.executionLimit ?? query.limit;
|
|
49
66
|
if (limit !== undefined)
|
|
@@ -52,18 +69,3 @@ export function buildDerivedDatasetSql(ds, query, options, buildBaseQuery) {
|
|
|
52
69
|
sql += ` OFFSET ${query.offset}`;
|
|
53
70
|
return { sql, parameters };
|
|
54
71
|
}
|
|
55
|
-
export async function runDerivedDatasetQuery(ds, query, options, buildBaseQuery) {
|
|
56
|
-
const start = Date.now();
|
|
57
|
-
const { sql, parameters } = buildDerivedDatasetSql(ds, query, {
|
|
58
|
-
...options,
|
|
59
|
-
executionLimit: overfetchLimit(query.limit),
|
|
60
|
-
}, buildBaseQuery);
|
|
61
|
-
const rows = await options.builderFactory.rawQuery(sql, parameters, { abortSignal: options.context?.abortSignal });
|
|
62
|
-
return toDatasetQueryResult(rows, {
|
|
63
|
-
dataset: ds,
|
|
64
|
-
query,
|
|
65
|
-
sql,
|
|
66
|
-
timingMs: Date.now() - start,
|
|
67
|
-
context: options.context,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DatasetInstance, DimensionDefinition, MetricFilter, MetricFilterOperator, RelationshipDefinition, SemanticFilterDefinition, SemanticFiltersDefinition } from '../types.js';
|
|
2
|
+
/** The generated allowlist mirrors normalizeFilters; explicit maps replace it. */
|
|
3
|
+
export type NormalizedDatasetFilters<TDimensions extends Record<string, DimensionDefinition>, TFilters extends SemanticFiltersDefinition | undefined> = TFilters extends SemanticFiltersDefinition ? TFilters : {
|
|
4
|
+
[K in keyof TDimensions as TDimensions[K] extends {
|
|
5
|
+
filterable: false;
|
|
6
|
+
} ? never : K]: SemanticFilterDefinition & {
|
|
7
|
+
field: K & string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
type FilterOperators<TDefinition> = TDefinition extends {
|
|
11
|
+
operators: readonly (infer TOperator extends MetricFilterOperator)[];
|
|
12
|
+
} ? TOperator : MetricFilterOperator;
|
|
13
|
+
type LocalFilterFor<TFilters extends SemanticFiltersDefinition> = {
|
|
14
|
+
[K in keyof TFilters & string]: K extends `${string}.${string}` ? never : MetricFilter<K, unknown, FilterOperators<TFilters[K]>>;
|
|
15
|
+
}[keyof TFilters & string];
|
|
16
|
+
type RelationshipFilterFor<TRelationships extends Record<string, RelationshipDefinition>> = {
|
|
17
|
+
[R in keyof TRelationships & string]: TRelationships[R] extends RelationshipDefinition<infer TTarget, infer TKind> ? TKind extends 'hasMany' ? never : TTarget extends DatasetInstance<any, any, any, any> ? {
|
|
18
|
+
[K in keyof TTarget['filters'] & keyof TTarget['dimensions'] & string]: TTarget['dimensions'][K] extends {
|
|
19
|
+
sql: string;
|
|
20
|
+
} ? never : K extends TTarget['filters'][K]['field'] ? MetricFilter<`${R}.${K}`, unknown, FilterOperators<TTarget['filters'][K]>> : never;
|
|
21
|
+
}[keyof TTarget['filters'] & keyof TTarget['dimensions'] & string] : never : never;
|
|
22
|
+
}[keyof TRelationships & string];
|
|
23
|
+
/** Filter names and operator policies exposed by a dataset and its to-one targets. */
|
|
24
|
+
export type DatasetFilterFor<TDataset extends DatasetInstance<any, any, any, any>> = LocalFilterFor<TDataset['filters']> | RelationshipFilterFor<TDataset['relationships']>;
|
|
25
|
+
/** Names in the normalized filter allowlist, including one-hop relationship filters. */
|
|
26
|
+
export type DatasetFilterNames<TDataset extends DatasetInstance<any, any, any, any>> = DatasetFilterFor<TDataset>['field'];
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=dataset-filter-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-filter-types.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-filter-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACpB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB,kFAAkF;AAClF,MAAM,MAAM,wBAAwB,CAClC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EACvD,QAAQ,SAAS,yBAAyB,GAAG,SAAS,IACpD,QAAQ,SAAS,yBAAyB,GAAG,QAAQ,GAAG;KACzD,CAAC,IAAI,MAAM,WAAW,IAAI,WAAW,CAAC,CAAC,CAAC,SAAS;QAAE,UAAU,EAAE,KAAK,CAAA;KAAE,GAAG,KAAK,GAAG,CAAC,GAAG,wBAAwB,GAAG;QAC/G,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC;KACnB;CACF,CAAC;AAEF,KAAK,eAAe,CAAC,WAAW,IAAI,WAAW,SAAS;IACtD,SAAS,EAAE,SAAS,CAAC,MAAM,SAAS,SAAS,oBAAoB,CAAC,EAAE,CAAC;CACtE,GAAG,SAAS,GAAG,oBAAoB,CAAC;AAErC,KAAK,cAAc,CAAC,QAAQ,SAAS,yBAAyB,IAAI;KAC/D,CAAC,IAAI,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,SAAS,GAAG,MAAM,IAAI,MAAM,EAAE,GAC3D,KAAK,GACL,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;CAC3D,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC;AAE3B,KAAK,qBAAqB,CAAC,cAAc,SAAS,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,IAAI;KACzF,CAAC,IAAI,MAAM,cAAc,GAAG,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,SAAS,sBAAsB,CAAC,MAAM,OAAO,EAAE,MAAM,KAAK,CAAC,GAC9G,KAAK,SAAS,SAAS,GAAG,KAAK,GAC7B,OAAO,SAAS,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GACjD;SACC,CAAC,IAAI,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,MAAM,GACnE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,GAAG,KAAK,GACpD,CAAC,SAAS,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GACtC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAC1E,KAAK;KACd,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,MAAM,CAAC,GAChE,KAAK,GACT,KAAK;CACV,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,CAAC;AAEjC,sFAAsF;AACtF,MAAM,MAAM,gBAAgB,CAAC,QAAQ,SAAS,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAC7E,cAAc,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,GACnC,qBAAqB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC;AAErD,wFAAwF;AACxF,MAAM,MAAM,kBAAkB,CAAC,QAAQ,SAAS,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IACjF,gBAAgB,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, DatasetHavingCondition, DatasetQuery } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates `having` conditions against the query they belong to.
|
|
4
|
+
*
|
|
5
|
+
* A condition may only reference a measure the query selects: that is the
|
|
6
|
+
* value the condition reads, and it keeps every rendered reference an output
|
|
7
|
+
* alias rather than an expression taken from input.
|
|
8
|
+
*/
|
|
9
|
+
export declare function datasetHavingErrors(ds: AnyDatasetInstance, query: DatasetQuery, selectedMeasures: readonly string[]): string[];
|
|
10
|
+
/**
|
|
11
|
+
* Renders validated `having` conditions as one parameterized predicate.
|
|
12
|
+
*
|
|
13
|
+
* `measureSql` maps a selected measure to the expression that reads its
|
|
14
|
+
* aggregated value in the outer query. Values are only ever bound as `?`
|
|
15
|
+
* parameters, appended in placeholder order.
|
|
16
|
+
*/
|
|
17
|
+
export declare function datasetHavingSql(conditions: readonly DatasetHavingCondition[], measureSql: (measure: string) => string): {
|
|
18
|
+
sql: string;
|
|
19
|
+
parameters: unknown[];
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=dataset-having.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-having.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-having.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAmC5F;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,gBAAgB,EAAE,SAAS,MAAM,EAAE,GAClC,MAAM,EAAE,CA0CV;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,SAAS,sBAAsB,EAAE,EAC7C,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GACtC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,EAAE,CAAA;CAAE,CAmBxC"}
|
|
@@ -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":"
|
|
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"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveRelationshipMeasure } from './relationship-measures.js';
|
|
2
|
+
import { semanticQuerySyntaxErrors } from './semantic-query-syntax.js';
|
|
1
3
|
import { queryTimezoneErrors } from './query-timezone.js';
|
|
2
4
|
import { baseMeasureNames } from './dataset-measures.js';
|
|
3
5
|
import { validateFilterValue } from '../validation.js';
|
|
@@ -7,12 +9,18 @@ import { isQualifiedField, resolveQualifiedField, } from './relationship-fields.
|
|
|
7
9
|
import { validateQualifiedFilter, validateRelationshipTenantRuntime, } from './relationship-validation.js';
|
|
8
10
|
import { segmentSelectionErrors } from './segments.js';
|
|
9
11
|
import { analyzeTimeMeasureAxis } from './time-measure-axis.js';
|
|
12
|
+
import { datasetHavingErrors } from './dataset-having.js';
|
|
10
13
|
export function validateDatasetQueryInput(ds, query, context) {
|
|
11
|
-
const errors = queryTimezoneErrors(query.timezone);
|
|
14
|
+
const errors = [...semanticQuerySyntaxErrors(query), ...queryTimezoneErrors(query.timezone)];
|
|
12
15
|
const dimensionNames = Object.keys(ds.dimensions);
|
|
13
16
|
const measureNames = Object.keys(ds.measures);
|
|
14
17
|
const selectedDimensions = query.dimensions ?? [];
|
|
15
18
|
const selectedMeasures = query.measures ?? baseMeasureNames(ds.measures);
|
|
19
|
+
for (const name of selectedDimensions) {
|
|
20
|
+
if (selectedMeasures.includes(name)) {
|
|
21
|
+
errors.push(`Output "${name}" cannot be selected as both a dimension and a measure. Select one or give the definitions distinct names.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
16
24
|
const filterNames = Object.keys(ds.filters);
|
|
17
25
|
const orderableFields = new Set([
|
|
18
26
|
...selectedDimensions,
|
|
@@ -47,7 +55,12 @@ export function validateDatasetQueryInput(ds, query, context) {
|
|
|
47
55
|
if (query.measures) {
|
|
48
56
|
for (const measure of query.measures) {
|
|
49
57
|
if (isQualifiedField(measure)) {
|
|
50
|
-
|
|
58
|
+
try {
|
|
59
|
+
resolveRelationshipMeasure(ds, measure);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
errors.push(error.message);
|
|
63
|
+
}
|
|
51
64
|
continue;
|
|
52
65
|
}
|
|
53
66
|
if (!measureNames.includes(measure)) {
|
|
@@ -95,7 +108,7 @@ export function validateDatasetQueryInput(ds, query, context) {
|
|
|
95
108
|
if (query.orderBy) {
|
|
96
109
|
const invalid = [];
|
|
97
110
|
for (const order of query.orderBy) {
|
|
98
|
-
if (isQualifiedField(order.field)) {
|
|
111
|
+
if (isQualifiedField(order.field) && !selectedMeasures.includes(order.field)) {
|
|
99
112
|
const resolution = resolveQualifiedField(ds, order.field);
|
|
100
113
|
if (resolution?.error) {
|
|
101
114
|
errors.push(resolution.error);
|
|
@@ -135,6 +148,7 @@ export function validateDatasetQueryInput(ds, query, context) {
|
|
|
135
148
|
if (ds.limits?.maxMeasures && query.measures && query.measures.length > ds.limits.maxMeasures) {
|
|
136
149
|
errors.push(`Too many measures: ${query.measures.length} (max ${ds.limits.maxMeasures})`);
|
|
137
150
|
}
|
|
151
|
+
errors.push(...datasetHavingErrors(ds, query, selectedMeasures));
|
|
138
152
|
if (ds.limits?.maxFilters && query.filters && query.filters.length > ds.limits.maxFilters) {
|
|
139
153
|
errors.push(`Too many filters: ${query.filters.length} (max ${ds.limits.maxFilters})`);
|
|
140
154
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DatasetSqlDialect } from '../dataset-sql-dialect.js';
|
|
2
|
+
import type { QueryBuilderFactoryInput } from '../query-builder-protocol.js';
|
|
3
|
+
/** Resolve from the active factory, so a runtime builder override carries its dialect. */
|
|
4
|
+
export declare function resolveDatasetSqlDialect(factory: QueryBuilderFactoryInput): DatasetSqlDialect;
|
|
5
|
+
//# sourceMappingURL=dataset-sql-dialect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-sql-dialect.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-sql-dialect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAG7E,0FAA0F;AAC1F,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CAE7F"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { clickhouseDatasetSqlDialect } from './clickhouse-dataset-sql-dialect.js';
|
|
2
|
+
/** Resolve from the active factory, so a runtime builder override carries its dialect. */
|
|
3
|
+
export function resolveDatasetSqlDialect(factory) {
|
|
4
|
+
return factory.datasetSqlDialect ?? clickhouseDatasetSqlDialect;
|
|
5
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-target.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-target.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,IAAI,kBAAkB,CAE/E"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MetricFilter, MetricFilterOperator } from '../types.js';
|
|
2
|
+
export declare function createFilter<const TField extends string, const TValue, TOperator extends MetricFilterOperator>(field: TField, operator: TOperator, value: TValue): MetricFilter<TField, TValue, TOperator>;
|
|
3
|
+
//# sourceMappingURL=filter-definition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-definition.d.ts","sourceRoot":"","sources":["../../src/utils/filter-definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEtE,wBAAgB,YAAY,CAC1B,KAAK,CAAC,MAAM,SAAS,MAAM,EAC3B,KAAK,CAAC,MAAM,EACZ,SAAS,SAAS,oBAAoB,EACtC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAE5F"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MetricRef, MetricQuery, ExecutionContext, TimeGrain } from '../types.js';
|
|
2
|
+
import type { QueryBuilderFactoryLike } from '../query-builder-protocol.js';
|
|
3
|
+
import type { DerivedMetricSpec } from '../types.js';
|
|
4
|
+
export declare function buildDerivedMetricSql(ref: MetricRef, spec: DerivedMetricSpec, query: MetricQuery, grain: TimeGrain | undefined, builderFactory: QueryBuilderFactoryLike, context?: ExecutionContext): {
|
|
5
|
+
sql: string;
|
|
6
|
+
params: unknown[];
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=metric-derived-query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric-derived-query.d.ts","sourceRoot":"","sources":["../../src/utils/metric-derived-query.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAoB,MAAM,8BAA8B,CAAC;AAiB9F,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;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAiHpC"}
|