@hypequery/datasets 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-catalog.d.ts +2 -1
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/api.type-test.js +22 -1
- package/dist/cache/definition-identity.d.ts +26 -0
- package/dist/cache/definition-identity.d.ts.map +1 -0
- package/dist/cache/definition-identity.js +164 -0
- package/dist/cache/protocol-cache-keys.d.ts +38 -0
- package/dist/cache/protocol-cache-keys.d.ts.map +1 -0
- package/dist/cache/protocol-cache-keys.js +191 -0
- package/dist/cache/query-signature.d.ts +6 -0
- package/dist/cache/query-signature.d.ts.map +1 -1
- package/dist/cache/query-signature.js +10 -13
- package/dist/cache/semantic-query-cache.d.ts +23 -0
- package/dist/cache/semantic-query-cache.d.ts.map +1 -1
- package/dist/catalog.d.ts +6 -5
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +29 -9
- package/dist/contract.d.ts +3 -2
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- package/dist/dataset-query.d.ts.map +1 -1
- package/dist/dataset-query.js +21 -4
- package/dist/dataset.d.ts +2 -2
- package/dist/dataset.d.ts.map +1 -1
- package/dist/dataset.js +9 -4
- package/dist/executor.d.ts +7 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +60 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/measure.d.ts +12 -2
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +11 -1
- package/dist/protocol-adapter.d.ts.map +1 -1
- package/dist/protocol-adapter.js +10 -1
- package/dist/protocol-deployment-adapter.d.ts.map +1 -1
- package/dist/protocol-deployment-adapter.js +2 -1
- package/dist/query-planner.d.ts +1 -1
- package/dist/query-planner.d.ts.map +1 -1
- package/dist/query-planner.js +5 -17
- package/dist/semantic-plan.d.ts +0 -1
- package/dist/semantic-plan.d.ts.map +1 -1
- package/dist/semantic-planner.d.ts.map +1 -1
- package/dist/semantic-planner.js +10 -3
- package/dist/semantic-query-schema.d.ts.map +1 -1
- package/dist/semantic-query-schema.js +2 -0
- package/dist/types.d.ts +61 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/agent-catalog-projection.d.ts +3 -2
- package/dist/utils/agent-catalog-projection.d.ts.map +1 -1
- package/dist/utils/agent-catalog-projection.js +2 -10
- package/dist/utils/approximate-measures.d.ts +3 -2
- package/dist/utils/approximate-measures.d.ts.map +1 -1
- package/dist/utils/approximate-measures.js +11 -8
- package/dist/utils/composite-time-measure-sql.d.ts +10 -0
- package/dist/utils/composite-time-measure-sql.d.ts.map +1 -0
- package/dist/utils/composite-time-measure-sql.js +60 -0
- package/dist/utils/dataset-contract.d.ts.map +1 -1
- package/dist/utils/dataset-contract.js +2 -1
- package/dist/utils/dataset-definition-validation.d.ts.map +1 -1
- package/dist/utils/dataset-definition-validation.js +6 -24
- package/dist/utils/dataset-derived-query.d.ts.map +1 -1
- package/dist/utils/dataset-derived-query.js +12 -55
- package/dist/utils/dataset-measures.d.ts +13 -6
- package/dist/utils/dataset-measures.d.ts.map +1 -1
- package/dist/utils/dataset-measures.js +25 -4
- package/dist/utils/dataset-metric-ref.d.ts +2 -2
- package/dist/utils/dataset-metric-ref.d.ts.map +1 -1
- package/dist/utils/dataset-query-result.d.ts.map +1 -1
- package/dist/utils/dataset-query-result.js +2 -1
- package/dist/utils/dataset-query-validation.d.ts.map +1 -1
- package/dist/utils/dataset-query-validation.js +7 -3
- package/dist/utils/derived-measure-sql.d.ts +4 -0
- package/dist/utils/derived-measure-sql.d.ts.map +1 -0
- package/dist/utils/derived-measure-sql.js +49 -0
- package/dist/utils/derived-measure-validation.d.ts.map +1 -1
- package/dist/utils/derived-measure-validation.js +7 -27
- package/dist/utils/measure-dependencies.d.ts +8 -0
- package/dist/utils/measure-dependencies.d.ts.map +1 -0
- package/dist/utils/measure-dependencies.js +44 -0
- package/dist/utils/measure-evaluation-graph.d.ts +23 -0
- package/dist/utils/measure-evaluation-graph.d.ts.map +1 -0
- package/dist/utils/measure-evaluation-graph.js +38 -0
- package/dist/utils/measure-time-grains.d.ts +4 -0
- package/dist/utils/measure-time-grains.d.ts.map +1 -0
- package/dist/utils/measure-time-grains.js +18 -0
- package/dist/utils/query-timezone.d.ts +9 -0
- package/dist/utils/query-timezone.d.ts.map +1 -0
- package/dist/utils/query-timezone.js +35 -0
- package/dist/utils/semantic-metadata-validation.js +1 -1
- package/dist/utils/semantic-metadata.d.ts.map +1 -1
- package/dist/utils/semantic-metadata.js +3 -1
- package/dist/utils/shift-measure-definition.d.ts +4 -0
- package/dist/utils/shift-measure-definition.d.ts.map +1 -0
- package/dist/utils/shift-measure-definition.js +9 -0
- package/dist/utils/shift-measure-grains.d.ts +5 -0
- package/dist/utils/shift-measure-grains.d.ts.map +1 -0
- package/dist/utils/shift-measure-grains.js +10 -0
- package/dist/utils/shift-measure-sql.d.ts +21 -0
- package/dist/utils/shift-measure-sql.d.ts.map +1 -0
- package/dist/utils/shift-measure-sql.js +63 -0
- package/dist/utils/shift-measure-validation.d.ts +3 -0
- package/dist/utils/shift-measure-validation.d.ts.map +1 -0
- package/dist/utils/shift-measure-validation.js +20 -0
- package/dist/utils/shift-wall-time-sql.d.ts +4 -0
- package/dist/utils/shift-wall-time-sql.d.ts.map +1 -0
- package/dist/utils/shift-wall-time-sql.js +17 -0
- package/dist/utils/sql-under-joins.d.ts +10 -0
- package/dist/utils/sql-under-joins.d.ts.map +1 -0
- package/dist/utils/sql-under-joins.js +15 -0
- package/dist/utils/time-arithmetic-sql.d.ts +4 -0
- package/dist/utils/time-arithmetic-sql.d.ts.map +1 -0
- package/dist/utils/time-arithmetic-sql.js +9 -0
- package/dist/utils/time-axis-bounds.d.ts +24 -0
- package/dist/utils/time-axis-bounds.d.ts.map +1 -0
- package/dist/utils/time-axis-bounds.js +71 -0
- package/dist/utils/time-axis-calendar-guard.d.ts +9 -0
- package/dist/utils/time-axis-calendar-guard.d.ts.map +1 -0
- package/dist/utils/time-axis-calendar-guard.js +17 -0
- package/dist/utils/time-axis-intervals.d.ts +13 -0
- package/dist/utils/time-axis-intervals.d.ts.map +1 -0
- package/dist/utils/time-axis-intervals.js +81 -0
- package/dist/utils/time-measure-axis-sql.d.ts +13 -0
- package/dist/utils/time-measure-axis-sql.d.ts.map +1 -0
- package/dist/utils/time-measure-axis-sql.js +48 -0
- package/dist/utils/time-measure-axis.d.ts +19 -0
- package/dist/utils/time-measure-axis.d.ts.map +1 -0
- package/dist/utils/time-measure-axis.js +46 -0
- package/dist/utils/time-measure-context-sql.d.ts +6 -0
- package/dist/utils/time-measure-context-sql.d.ts.map +1 -0
- package/dist/utils/time-measure-context-sql.js +21 -0
- package/dist/utils/time-measure-dataset-sql.d.ts +9 -0
- package/dist/utils/time-measure-dataset-sql.d.ts.map +1 -0
- package/dist/utils/time-measure-dataset-sql.js +80 -0
- package/dist/utils/time-measure-leaf-sql.d.ts +14 -0
- package/dist/utils/time-measure-leaf-sql.d.ts.map +1 -0
- package/dist/utils/time-measure-leaf-sql.js +52 -0
- package/dist/utils/time-measure-result-sql.d.ts +6 -0
- package/dist/utils/time-measure-result-sql.d.ts.map +1 -0
- package/dist/utils/time-measure-result-sql.js +30 -0
- package/dist/utils/time-measure-source-sql.d.ts +22 -0
- package/dist/utils/time-measure-source-sql.d.ts.map +1 -0
- package/dist/utils/time-measure-source-sql.js +75 -0
- package/dist/utils/time-measure-values-sql.d.ts +20 -0
- package/dist/utils/time-measure-values-sql.d.ts.map +1 -0
- package/dist/utils/time-measure-values-sql.js +87 -0
- package/dist/utils/time-query-measures.d.ts +6 -0
- package/dist/utils/time-query-measures.d.ts.map +1 -0
- package/dist/utils/time-query-measures.js +24 -0
- package/dist/utils/window-aggregation-sql.d.ts +5 -0
- package/dist/utils/window-aggregation-sql.d.ts.map +1 -0
- package/dist/utils/window-aggregation-sql.js +17 -0
- package/dist/utils/window-catalog-metadata.d.ts +14 -0
- package/dist/utils/window-catalog-metadata.d.ts.map +1 -0
- package/dist/utils/window-catalog-metadata.js +17 -0
- package/dist/utils/window-measure-definition.d.ts +4 -0
- package/dist/utils/window-measure-definition.d.ts.map +1 -0
- package/dist/utils/window-measure-definition.js +11 -0
- package/dist/utils/window-measure-dependencies.d.ts +4 -0
- package/dist/utils/window-measure-dependencies.d.ts.map +1 -0
- package/dist/utils/window-measure-dependencies.js +5 -0
- package/dist/utils/window-measure-validation.d.ts +3 -0
- package/dist/utils/window-measure-validation.d.ts.map +1 -0
- package/dist/utils/window-measure-validation.js +49 -0
- package/dist/utils/window-query-measures.d.ts +4 -0
- package/dist/utils/window-query-measures.d.ts.map +1 -0
- package/dist/utils/window-query-measures.js +17 -0
- package/dist/utils/window-query-signature.d.ts +4 -0
- package/dist/utils/window-query-signature.d.ts.map +1 -0
- package/dist/utils/window-query-signature.js +35 -0
- package/package.json +7 -7
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isBaseMeasure, isDerivedMeasure, isShiftMeasure, isWindowMeasure } from './dataset-measures.js';
|
|
2
|
+
export function measureDependencies(definition) {
|
|
3
|
+
if (isDerivedMeasure(definition))
|
|
4
|
+
return Object.values(definition.uses);
|
|
5
|
+
if (isShiftMeasure(definition) || isWindowMeasure(definition))
|
|
6
|
+
return [definition.measure];
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
/** Dependency order, with each shared input visited once. */
|
|
10
|
+
export function measureDependencyNames(measures, selected) {
|
|
11
|
+
const visited = new Set();
|
|
12
|
+
const visiting = new Set();
|
|
13
|
+
const names = [];
|
|
14
|
+
const visit = (name) => {
|
|
15
|
+
if (visiting.has(name))
|
|
16
|
+
throw new Error(`Measure "${name}" contains a dependency cycle.`);
|
|
17
|
+
if (visited.has(name) || !Object.hasOwn(measures, name))
|
|
18
|
+
return;
|
|
19
|
+
visiting.add(name);
|
|
20
|
+
measureDependencies(measures[name]).forEach(visit);
|
|
21
|
+
visiting.delete(name);
|
|
22
|
+
visited.add(name);
|
|
23
|
+
names.push(name);
|
|
24
|
+
};
|
|
25
|
+
selected.forEach(visit);
|
|
26
|
+
return names;
|
|
27
|
+
}
|
|
28
|
+
export function assertMeasureDependenciesAcyclic(dataset, measures) {
|
|
29
|
+
try {
|
|
30
|
+
measureDependencyNames(measures, Object.keys(measures));
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new Error(`Invalid dataset "${dataset}": ${String(error)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Wrappers inherit aggregation metadata only when their input has one. */
|
|
37
|
+
export function inheritedBaseMeasure(measures, name) {
|
|
38
|
+
const definition = measures[name];
|
|
39
|
+
if (isBaseMeasure(definition))
|
|
40
|
+
return definition;
|
|
41
|
+
if (isWindowMeasure(definition) || isShiftMeasure(definition))
|
|
42
|
+
return inheritedBaseMeasure(measures, definition.measure);
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DatasetMeasureDefinition, DerivedMeasureDefinition, MeasureTimeInterval } from '../types.js';
|
|
2
|
+
export interface MeasureEvaluationContext {
|
|
3
|
+
id: number;
|
|
4
|
+
parent?: number;
|
|
5
|
+
interval?: MeasureTimeInterval;
|
|
6
|
+
}
|
|
7
|
+
export interface MeasureEvaluationNode {
|
|
8
|
+
id: number;
|
|
9
|
+
name: string;
|
|
10
|
+
context: number;
|
|
11
|
+
definition: DatasetMeasureDefinition;
|
|
12
|
+
inputs?: Record<string, number>;
|
|
13
|
+
}
|
|
14
|
+
/** A shift changes evaluation context; formulas combine values in that context. */
|
|
15
|
+
export declare function buildMeasureEvaluationGraph(measures: Readonly<Record<string, DatasetMeasureDefinition>>, selected: readonly string[]): {
|
|
16
|
+
contexts: MeasureEvaluationContext[];
|
|
17
|
+
nodes: MeasureEvaluationNode[];
|
|
18
|
+
outputs: {
|
|
19
|
+
[k: string]: number;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export declare function evaluationFormula(node: MeasureEvaluationNode): DerivedMeasureDefinition;
|
|
23
|
+
//# sourceMappingURL=measure-evaluation-graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measure-evaluation-graph.d.ts","sourceRoot":"","sources":["../../src/utils/measure-evaluation-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAG3G,MAAM,WAAW,wBAAwB;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAA;CAAE;AACzG,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,wBAAwB,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,mFAAmF;AACnF,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE;;;;;;EA+BpI;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,wBAAwB,CAIvF"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isDerivedMeasure, isShiftMeasure } from './dataset-measures.js';
|
|
2
|
+
/** A shift changes evaluation context; formulas combine values in that context. */
|
|
3
|
+
export function buildMeasureEvaluationGraph(measures, selected) {
|
|
4
|
+
const contexts = [{ id: 0 }];
|
|
5
|
+
const nodes = [];
|
|
6
|
+
const memo = new Map();
|
|
7
|
+
const contextMemo = new Map();
|
|
8
|
+
const visit = (name, context) => {
|
|
9
|
+
const key = `${context}:${name}`;
|
|
10
|
+
const existing = memo.get(key);
|
|
11
|
+
if (existing !== undefined)
|
|
12
|
+
return existing;
|
|
13
|
+
const definition = measures[name];
|
|
14
|
+
if (isShiftMeasure(definition)) {
|
|
15
|
+
const contextKey = `${context}:${definition.interval.amount}:${definition.interval.unit}`;
|
|
16
|
+
let shifted = contextMemo.get(contextKey);
|
|
17
|
+
if (shifted === undefined) {
|
|
18
|
+
shifted = contexts.length;
|
|
19
|
+
contexts.push({ id: shifted, parent: context, interval: definition.interval });
|
|
20
|
+
contextMemo.set(contextKey, shifted);
|
|
21
|
+
}
|
|
22
|
+
const input = visit(definition.measure, shifted);
|
|
23
|
+
memo.set(key, input);
|
|
24
|
+
return input;
|
|
25
|
+
}
|
|
26
|
+
const inputs = isDerivedMeasure(definition)
|
|
27
|
+
? Object.fromEntries(Object.entries(definition.uses).map(([alias, input]) => [alias, visit(input, context)])) : undefined;
|
|
28
|
+
const id = nodes.length;
|
|
29
|
+
nodes.push({ id, name, context, definition, inputs });
|
|
30
|
+
memo.set(key, id);
|
|
31
|
+
return id;
|
|
32
|
+
};
|
|
33
|
+
const outputs = Object.fromEntries(selected.map(name => [name, visit(name, 0)]));
|
|
34
|
+
return { contexts, nodes, outputs };
|
|
35
|
+
}
|
|
36
|
+
export function evaluationFormula(node) {
|
|
37
|
+
return { ...node.definition, uses: Object.fromEntries(Object.entries(node.inputs ?? {}).map(([alias, id]) => [alias, `_hq_value${id}`])) };
|
|
38
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DatasetMeasureDefinition, TimeGrain } from '../types.js';
|
|
2
|
+
/** Intersect time-measure requirements with the dataset's declared grains. */
|
|
3
|
+
export declare function measureSupportedGrains(measures: Readonly<Record<string, DatasetMeasureDefinition>>, name: string, grains: readonly TimeGrain[]): TimeGrain[];
|
|
4
|
+
//# sourceMappingURL=measure-time-grains.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measure-time-grains.d.ts","sourceRoot":"","sources":["../../src/utils/measure-time-grains.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAKvE,8EAA8E;AAC9E,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,SAAS,EAAE,GACvG,SAAS,EAAE,CAWb"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getDerivedMeasure, isShiftMeasure, isWindowMeasure } from './dataset-measures.js';
|
|
2
|
+
import { windowGrainErrors } from './time-axis-intervals.js';
|
|
3
|
+
import { supportsShiftGrain } from './shift-measure-grains.js';
|
|
4
|
+
/** Intersect time-measure requirements with the dataset's declared grains. */
|
|
5
|
+
export function measureSupportedGrains(measures, name, grains) {
|
|
6
|
+
const definition = measures[name];
|
|
7
|
+
if (isShiftMeasure(definition)) {
|
|
8
|
+
return measureSupportedGrains(measures, definition.measure, grains)
|
|
9
|
+
.filter(grain => supportsShiftGrain(definition.interval, grain));
|
|
10
|
+
}
|
|
11
|
+
if (isWindowMeasure(definition))
|
|
12
|
+
return grains.filter(grain => !windowGrainErrors(name, definition, grain).length);
|
|
13
|
+
const derived = getDerivedMeasure(measures, name);
|
|
14
|
+
if (!derived)
|
|
15
|
+
return [...grains];
|
|
16
|
+
const inputs = Object.values(derived.uses).map(input => measureSupportedGrains(measures, input, grains));
|
|
17
|
+
return grains.filter(grain => inputs.every(supported => supported.includes(grain)));
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnyDatasetInstance } from '../types.js';
|
|
2
|
+
/** Named zones only: reject fixed offsets, invalid zones, and SQL fragments. */
|
|
3
|
+
export declare function isQueryTimezone(value: unknown): value is string;
|
|
4
|
+
export declare function queryTimezoneErrors(timezone: unknown): string[];
|
|
5
|
+
/** Conversion preserves timestamp instants and interprets Date values as local dates. */
|
|
6
|
+
export declare function queryTimeSql(expression: string, timezone: string | undefined): string;
|
|
7
|
+
/** Apply the query zone to time-key predicates, including semantic filter aliases. */
|
|
8
|
+
export declare function queryTimeFilterSql(ds: AnyDatasetInstance, field: string, expression: string, timezone: string | undefined): string;
|
|
9
|
+
//# sourceMappingURL=query-timezone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query-timezone.d.ts","sourceRoot":"","sources":["../../src/utils/query-timezone.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAS/D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAE,CAG/D;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMrF;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GACtF,MAAM,CAMR"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Named zones only: reject fixed offsets, invalid zones, and SQL fragments. */
|
|
2
|
+
export function isQueryTimezone(value) {
|
|
3
|
+
if (typeof value !== 'string' || value.length > 100
|
|
4
|
+
|| !/^[A-Za-z][A-Za-z0-9._+-]*(?:\/[A-Za-z0-9._+-]+)*$/.test(value))
|
|
5
|
+
return false;
|
|
6
|
+
try {
|
|
7
|
+
new Intl.DateTimeFormat('en', { timeZone: value });
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function queryTimezoneErrors(timezone) {
|
|
15
|
+
return timezone === undefined || isQueryTimezone(timezone)
|
|
16
|
+
? [] : ['Invalid timezone: use an IANA name such as UTC or America/New_York.'];
|
|
17
|
+
}
|
|
18
|
+
/** Conversion preserves timestamp instants and interprets Date values as local dates. */
|
|
19
|
+
export function queryTimeSql(expression, timezone) {
|
|
20
|
+
timezone ??= 'UTC';
|
|
21
|
+
const errors = queryTimezoneErrors(timezone);
|
|
22
|
+
if (errors.length)
|
|
23
|
+
throw new Error(errors[0]);
|
|
24
|
+
const name = new Intl.DateTimeFormat('en', { timeZone: timezone }).resolvedOptions().timeZone;
|
|
25
|
+
return `toDateTime64(${expression}, 9, '${name}')`;
|
|
26
|
+
}
|
|
27
|
+
/** Apply the query zone to time-key predicates, including semantic filter aliases. */
|
|
28
|
+
export function queryTimeFilterSql(ds, field, expression, timezone) {
|
|
29
|
+
if (!ds.timeKey || field.includes('.'))
|
|
30
|
+
return expression;
|
|
31
|
+
const resolved = ds.filters[field]?.field ?? field;
|
|
32
|
+
const column = ds.dimensions[resolved]?.column ?? resolved;
|
|
33
|
+
const timeColumn = ds.dimensions[ds.timeKey]?.column ?? ds.timeKey;
|
|
34
|
+
return column === timeColumn ? queryTimeSql(expression, timezone) : expression;
|
|
35
|
+
}
|
|
@@ -34,7 +34,7 @@ export function validateSemanticMetadata(datasetName, location, metadata) {
|
|
|
34
34
|
validateText(datasetName, `${location}.${key}[${index}]`, value);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
for (const key of ['format', 'unit'
|
|
37
|
+
for (const key of ['format', 'unit']) {
|
|
38
38
|
const value = metadata[key];
|
|
39
39
|
if (value !== undefined)
|
|
40
40
|
validateText(datasetName, `${location}.${key}`, value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,0FAA0F;AAC1F,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,
|
|
1
|
+
{"version":3,"file":"semantic-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,0FAA0F;AAC1F,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,CAYrF"}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { uniqueSorted } from './canonical-json.js';
|
|
2
2
|
/** Copy agent metadata without carrying unrelated config fields into public contracts. */
|
|
3
3
|
export function snapshotSemanticMetadata(metadata) {
|
|
4
|
+
if ('timezone' in metadata) {
|
|
5
|
+
throw new Error('Set timezone on createDatasetClient or the query, not semantic metadata.');
|
|
6
|
+
}
|
|
4
7
|
return {
|
|
5
8
|
...(metadata.examples !== undefined ? { examples: uniqueSorted(metadata.examples) } : {}),
|
|
6
9
|
...(metadata.synonyms !== undefined ? { synonyms: uniqueSorted(metadata.synonyms) } : {}),
|
|
7
10
|
...(metadata.format !== undefined ? { format: metadata.format } : {}),
|
|
8
11
|
...(metadata.unit !== undefined ? { unit: metadata.unit } : {}),
|
|
9
12
|
...(metadata.currency !== undefined ? { currency: metadata.currency } : {}),
|
|
10
|
-
...(metadata.timezone !== undefined ? { timezone: metadata.timezone } : {}),
|
|
11
13
|
...(metadata.sensitivity !== undefined ? { sensitivity: metadata.sensitivity } : {}),
|
|
12
14
|
};
|
|
13
15
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MeasureTimeInterval, ShiftMeasureDefinition } from '../types.js';
|
|
2
|
+
export type ShiftMeasureOptions = Pick<ShiftMeasureDefinition, 'label' | 'description' | 'examples' | 'synonyms' | 'format' | 'unit' | 'currency' | 'sensitivity'>;
|
|
3
|
+
export declare function createShiftMeasure<const TMeasureName extends string>(baseMeasure: TMeasureName, interval: MeasureTimeInterval, options?: ShiftMeasureOptions): ShiftMeasureDefinition<TMeasureName>;
|
|
4
|
+
//# sourceMappingURL=shift-measure-definition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shift-measure-definition.d.ts","sourceRoot":"","sources":["../../src/utils/shift-measure-definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAG/E,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,sBAAsB,EAC3D,OAAO,GAAG,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,aAAa,CAAC,CAAC;AAEtG,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,YAAY,SAAS,MAAM,EAClE,WAAW,EAAE,YAAY,EACzB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,CAAC,EAAE,mBAAmB,GAC5B,sBAAsB,CAAC,YAAY,CAAC,CAOtC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { snapshotSemanticMetadata } from './semantic-metadata.js';
|
|
2
|
+
export function createShiftMeasure(baseMeasure, interval, options) {
|
|
3
|
+
return {
|
|
4
|
+
__type: 'shift_measure_definition', measure: baseMeasure, interval: { ...interval },
|
|
5
|
+
...snapshotSemanticMetadata(options ?? {}),
|
|
6
|
+
...(options?.label === undefined ? {} : { label: options.label }),
|
|
7
|
+
...(options?.description === undefined ? {} : { description: options.description }),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MeasureTimeInterval, TimeGrain } from '../types.js';
|
|
2
|
+
/** Calendar shifts preserve dates; they do not convert months into fixed durations. */
|
|
3
|
+
export declare function calendarShiftAtFineGrain(interval: MeasureTimeInterval, grain: TimeGrain): boolean;
|
|
4
|
+
export declare function supportsShiftGrain(interval: MeasureTimeInterval, grain: TimeGrain): boolean;
|
|
5
|
+
//# sourceMappingURL=shift-measure-grains.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shift-measure-grains.d.ts","sourceRoot":"","sources":["../../src/utils/shift-measure-grains.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGlE,uFAAuF;AACvF,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,mBAAmB,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAGjG;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,mBAAmB,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAG3F"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { intervalBuckets } from './time-axis-intervals.js';
|
|
2
|
+
/** Calendar shifts preserve dates; they do not convert months into fixed durations. */
|
|
3
|
+
export function calendarShiftAtFineGrain(interval, grain) {
|
|
4
|
+
return ['month', 'quarter', 'year'].includes(interval.unit)
|
|
5
|
+
&& ['minute', 'hour', 'day', 'week'].includes(grain);
|
|
6
|
+
}
|
|
7
|
+
export function supportsShiftGrain(interval, grain) {
|
|
8
|
+
return calendarShiftAtFineGrain(interval, grain)
|
|
9
|
+
|| intervalBuckets(interval.amount, interval.unit, grain) !== undefined;
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ShiftMeasureDefinition } from '../types.js';
|
|
2
|
+
import type { TimeMeasureAxis } from './time-measure-axis.js';
|
|
3
|
+
/** Preserve the selected portion of a bucket and both authored endpoint operators. */
|
|
4
|
+
export declare function shiftRangeSql(shift: ShiftMeasureDefinition, axis: TimeMeasureAxis, period?: string): {
|
|
5
|
+
lower: string;
|
|
6
|
+
upper: string;
|
|
7
|
+
lowerInclusive: string;
|
|
8
|
+
upperInclusive: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function shiftRangePredicateSql(time: string, range: ReturnType<typeof shiftRangeSql>): string;
|
|
11
|
+
/** Check the shifted scan independently of whether it contains source rows. */
|
|
12
|
+
export declare function shiftCalendarGuardSql(shift: ShiftMeasureDefinition, axis: TimeMeasureAxis, axisGuard: string): string;
|
|
13
|
+
/** Validate each output bucket after the series exists, including empty source populations. */
|
|
14
|
+
export declare function shiftWallTimeSeriesGuardSql(shift: ShiftMeasureDefinition, grain: TimeMeasureAxis['grain']): string;
|
|
15
|
+
/** Map output buckets to their earlier half-open ranges, retaining calendar arithmetic. */
|
|
16
|
+
export declare function shiftBucketCtes(shift: ShiftMeasureDefinition, axis: TimeMeasureAxis, index: number): {
|
|
17
|
+
ctes: string[];
|
|
18
|
+
rowsSql: string;
|
|
19
|
+
sourceTimesSql: string;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=shift-measure-sql.d.ts.map
|
|
@@ -0,0 +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;AAI9D,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,MAAM;;;;;EAclG;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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { GRAIN_FUNCTIONS } from '../constants.js';
|
|
2
|
+
import { addTimeSql, subtractTimeSql } from './time-arithmetic-sql.js';
|
|
3
|
+
import { calendarBucketGuardSql } from './time-axis-calendar-guard.js';
|
|
4
|
+
import { calendarShiftAtFineGrain } from './shift-measure-grains.js';
|
|
5
|
+
import { shiftWallTimeGuardSql } from './shift-wall-time-sql.js';
|
|
6
|
+
/** Preserve the selected portion of a bucket and both authored endpoint operators. */
|
|
7
|
+
export function shiftRangeSql(shift, axis, period) {
|
|
8
|
+
const first = period ?? '_hq_first';
|
|
9
|
+
const last = period ?? '_hq_last';
|
|
10
|
+
const end = addTimeSql(last, 1, axis.grain);
|
|
11
|
+
const upper = subtractTimeSql(`least(${end}, _hq_upper)`, shift.interval.amount, shift.interval.unit);
|
|
12
|
+
// Clamp the bucket anchor, then retain its width. Independently clamping both
|
|
13
|
+
// dates would turn March 30–31 into February 28–28, dropping an entire day.
|
|
14
|
+
const shiftedEnd = addTimeSql(subtractTimeSql(last, shift.interval.amount, shift.interval.unit), 1, axis.grain);
|
|
15
|
+
return {
|
|
16
|
+
lower: subtractTimeSql(`greatest(${first}, _hq_lower)`, shift.interval.amount, shift.interval.unit),
|
|
17
|
+
upper: calendarShiftAtFineGrain(shift.interval, axis.grain) ? `if(_hq_upper >= ${end}, ${shiftedEnd}, ${upper})` : upper,
|
|
18
|
+
lowerInclusive: period ? `(${period} > _hq_lower OR ${Number(axis.lowerInclusive)})` : String(Number(axis.lowerInclusive)),
|
|
19
|
+
upperInclusive: `(_hq_upper < ${end} AND ${Number(axis.upperInclusive)})`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function shiftRangePredicateSql(time, range) {
|
|
23
|
+
return `(${time} > ${range.lower} OR (${range.lowerInclusive} AND ${time} = ${range.lower}))`
|
|
24
|
+
+ ` AND (${time} < ${range.upper} OR (${range.upperInclusive} AND ${time} = ${range.upper}))`;
|
|
25
|
+
}
|
|
26
|
+
/** Check the shifted scan independently of whether it contains source rows. */
|
|
27
|
+
export function shiftCalendarGuardSql(shift, axis, axisGuard) {
|
|
28
|
+
const { grain } = axis;
|
|
29
|
+
const { lower, upper } = shiftRangeSql(shift, axis);
|
|
30
|
+
const first = `${GRAIN_FUNCTIONS[grain]}(${lower})`;
|
|
31
|
+
const last = `${GRAIN_FUNCTIONS[grain]}(${upper})`;
|
|
32
|
+
const count = `dateDiff('${grain}', ${first}, ${last}) + ${axisGuard}`;
|
|
33
|
+
return calendarBucketGuardSql(first, count, grain);
|
|
34
|
+
}
|
|
35
|
+
/** Validate each output bucket after the series exists, including empty source populations. */
|
|
36
|
+
export function shiftWallTimeSeriesGuardSql(shift, grain) {
|
|
37
|
+
const end = addTimeSql('_hq_period', 1, grain);
|
|
38
|
+
return shiftWallTimeGuardSql(shift.interval, grain, '_hq_series CROSS JOIN _hq_bounds', '_hq_period', 'greatest(_hq_period, _hq_lower)', `least(${end}, _hq_upper)`, end);
|
|
39
|
+
}
|
|
40
|
+
/** Map output buckets to their earlier half-open ranges, retaining calendar arithmetic. */
|
|
41
|
+
export function shiftBucketCtes(shift, axis, index) {
|
|
42
|
+
const { grain } = axis;
|
|
43
|
+
const ranges = `_hq_shift_ranges${index}`;
|
|
44
|
+
const keys = `_hq_shift_keys${index}`;
|
|
45
|
+
const range = shiftRangeSql(shift, axis, '_hq_period');
|
|
46
|
+
const bucket = `${GRAIN_FUNCTIONS[grain]}(_hq_shift_lower)`;
|
|
47
|
+
const upperBucket = `${GRAIN_FUNCTIONS[grain]}(_hq_shift_upper)`;
|
|
48
|
+
const count = `greatest(0, dateDiff('${grain}', ${bucket}, ${upperBucket}) + if(_hq_shift_upper > ${upperBucket} OR _hq_shift_upper_inclusive, 1, 0))`;
|
|
49
|
+
const join = ` AS r INNER JOIN ${keys} AS b ON ${GRAIN_FUNCTIONS[grain]}(r._hq_time) = b._hq_source_period WHERE ${shiftRangePredicateSql('r._hq_time', { lower: 'b._hq_shift_lower', upper: 'b._hq_shift_upper', lowerInclusive: 'b._hq_shift_lower_inclusive', upperInclusive: 'b._hq_shift_upper_inclusive' })}`;
|
|
50
|
+
return {
|
|
51
|
+
// Membership preserves duplicate source rows while excluding dates and
|
|
52
|
+
// partial ranges that no output bucket maps to, including clamped overlaps.
|
|
53
|
+
sourceTimesSql: `SELECT r._hq_time FROM _hq_source${join}`,
|
|
54
|
+
ctes: [
|
|
55
|
+
`${ranges} AS (SELECT _hq_period, ${range.lower} AS _hq_shift_lower, ${range.upper} AS _hq_shift_upper, ${range.lowerInclusive} AS _hq_shift_lower_inclusive, ${range.upperInclusive} AS _hq_shift_upper_inclusive FROM _hq_series CROSS JOIN _hq_bounds)`,
|
|
56
|
+
// A DST transition can map a one-hour output bucket to zero or two
|
|
57
|
+
// source hours. Enumerate source bucket keys and keep exact endpoints.
|
|
58
|
+
// The equality join avoids a source-rows × output-series Cartesian product.
|
|
59
|
+
`${keys} AS (SELECT *, ${addTimeSql(bucket, `arrayJoin(range(toUInt64(${count})))`, grain)} AS _hq_source_period FROM ${ranges})`,
|
|
60
|
+
],
|
|
61
|
+
rowsSql: `SELECT r.*, b._hq_period FROM _hq_scanned${join}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shift-measure-validation.d.ts","sourceRoot":"","sources":["../../src/utils/shift-measure-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAK5D,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,GAAG,IAAI,CAYhJ"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isSafeSQLIdentifier } from '../sql-utils.js';
|
|
2
|
+
import { isSupportedTimeGrain } from '../constants.js';
|
|
3
|
+
import { isShiftMeasure } from './dataset-measures.js';
|
|
4
|
+
export function validateShiftMeasures(datasetName, timeKey, measures) {
|
|
5
|
+
for (const [name, definition] of Object.entries(measures)) {
|
|
6
|
+
if (!isShiftMeasure(definition))
|
|
7
|
+
continue;
|
|
8
|
+
const prefix = `Invalid dataset "${datasetName}": shift measure "${name}"`;
|
|
9
|
+
if (!isSafeSQLIdentifier(name))
|
|
10
|
+
throw new Error(`${prefix} name is not a safe identifier.`);
|
|
11
|
+
if (!timeKey)
|
|
12
|
+
throw new Error(`${prefix} requires a timeKey.`);
|
|
13
|
+
if (!Object.hasOwn(measures, definition.measure))
|
|
14
|
+
throw new Error(`${prefix} references missing measure "${String(definition.measure)}".`);
|
|
15
|
+
const interval = definition.interval;
|
|
16
|
+
if (!interval || !Number.isSafeInteger(interval.amount) || interval.amount <= 0 || !isSupportedTimeGrain(interval.unit)) {
|
|
17
|
+
throw new Error(`${prefix} interval needs a positive safe integer and supported unit.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MeasureTimeInterval, TimeGrain } from '../types.js';
|
|
2
|
+
/** Reject DST normalization that moves a shifted sub-day bucket to another local time. */
|
|
3
|
+
export declare function shiftWallTimeGuardSql(interval: MeasureTimeInterval, grain: TimeGrain, source: string, period: string, lower: string, upper: string, end: string): string;
|
|
4
|
+
//# sourceMappingURL=shift-wall-time-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shift-wall-time-sql.d.ts","sourceRoot":"","sources":["../../src/utils/shift-wall-time-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAIlE,0FAA0F;AAC1F,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,MAAM,CAYR"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { subtractTimeSql } from './time-arithmetic-sql.js';
|
|
2
|
+
import { calendarShiftAtFineGrain } from './shift-measure-grains.js';
|
|
3
|
+
/** Reject DST normalization that moves a shifted sub-day bucket to another local time. */
|
|
4
|
+
export function shiftWallTimeGuardSql(interval, grain, source, period, lower, upper, end) {
|
|
5
|
+
if (!calendarShiftAtFineGrain(interval, grain) || (grain !== 'minute' && grain !== 'hour'))
|
|
6
|
+
return '0';
|
|
7
|
+
const changesWallTime = (value) => {
|
|
8
|
+
const shifted = subtractTimeSql(value, interval.amount, interval.unit);
|
|
9
|
+
return ['Hour', 'Minute', 'Second']
|
|
10
|
+
.map(part => `to${part}(${value}) != to${part}(${shifted})`)
|
|
11
|
+
.join(' OR ');
|
|
12
|
+
};
|
|
13
|
+
const changed = `(${changesWallTime(period)}) OR (${changesWallTime(lower)})`
|
|
14
|
+
+ ` OR (${upper} != ${end} AND (${changesWallTime(upper)}))`;
|
|
15
|
+
return `(SELECT throwIf(countIf(${changed}) > 0, `
|
|
16
|
+
+ `'Calendar shift lands on a nonexistent local time; use a UTC query timezone.') FROM ${source})`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RelationshipBuilderContext } from './relationship-builder-plan.js';
|
|
2
|
+
/**
|
|
3
|
+
* Raw SQL expressions on the base dataset are emitted verbatim, so their column
|
|
4
|
+
* references are not table-qualified. When relationship joins are active a bare
|
|
5
|
+
* `price` in such an expression is ambiguous if the joined table also has a
|
|
6
|
+
* `price` column. Until the builder rewrites identifiers inside expressions,
|
|
7
|
+
* reject the combination rather than emit ambiguous SQL.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertNoRawSqlUnderJoins(kind: 'dimension' | 'measure', name: string, sql: string, joinCtx?: RelationshipBuilderContext): void;
|
|
10
|
+
//# sourceMappingURL=sql-under-joins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-under-joins.d.ts","sourceRoot":"","sources":["../../src/utils/sql-under-joins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAEjF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,0BAA0B,GACnC,IAAI,CASN"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw SQL expressions on the base dataset are emitted verbatim, so their column
|
|
3
|
+
* references are not table-qualified. When relationship joins are active a bare
|
|
4
|
+
* `price` in such an expression is ambiguous if the joined table also has a
|
|
5
|
+
* `price` column. Until the builder rewrites identifiers inside expressions,
|
|
6
|
+
* reject the combination rather than emit ambiguous SQL.
|
|
7
|
+
*/
|
|
8
|
+
export function assertNoRawSqlUnderJoins(kind, name, sql, joinCtx) {
|
|
9
|
+
if (!joinCtx) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
throw new Error(`SQL-backed ${kind} "${name}" cannot be combined with relationship joins: its expression ` +
|
|
13
|
+
`("${sql}") is not table-qualified and may collide with joined columns. Query it without ` +
|
|
14
|
+
`relationship-qualified fields, or redeclare it as a plain column.`);
|
|
15
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TimeGrain } from '../types.js';
|
|
2
|
+
export declare function addTimeSql(time: string, amount: string | number, grain: TimeGrain): string;
|
|
3
|
+
export declare function subtractTimeSql(time: string, amount: number, grain: TimeGrain): string;
|
|
4
|
+
//# sourceMappingURL=time-arithmetic-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-arithmetic-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-arithmetic-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM7C,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAE1F;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAEtF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const INTERVAL_FUNCTIONS = {
|
|
2
|
+
minute: 'Minutes', hour: 'Hours', day: 'Days', week: 'Weeks', month: 'Months', quarter: 'Quarters', year: 'Years',
|
|
3
|
+
};
|
|
4
|
+
export function addTimeSql(time, amount, grain) {
|
|
5
|
+
return `add${INTERVAL_FUNCTIONS[grain]}(${time}, ${amount})`;
|
|
6
|
+
}
|
|
7
|
+
export function subtractTimeSql(time, amount, grain) {
|
|
8
|
+
return `subtract${INTERVAL_FUNCTIONS[grain]}(${time}, ${amount})`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, MetricFilter } from '../types.js';
|
|
2
|
+
export interface ParsedTimeBound {
|
|
3
|
+
/** Preserve the authored text: SQL interprets local bounds in the query timezone. */
|
|
4
|
+
text: string;
|
|
5
|
+
/** UTC instant for offset bounds; a wall-clock estimate for local bounds. */
|
|
6
|
+
milliseconds: number;
|
|
7
|
+
subMillisecond: boolean;
|
|
8
|
+
nanoseconds: bigint;
|
|
9
|
+
hasOffset: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface TimeAxisRange {
|
|
12
|
+
start: ParsedTimeBound;
|
|
13
|
+
end: ParsedTimeBound;
|
|
14
|
+
lowerInclusive: boolean;
|
|
15
|
+
upperInclusive: boolean;
|
|
16
|
+
filters: MetricFilter[];
|
|
17
|
+
}
|
|
18
|
+
/** Parse without applying the process timezone, preserving nanosecond precision. */
|
|
19
|
+
export declare function parseTimeBound(value: unknown): ParsedTimeBound | undefined;
|
|
20
|
+
/** Extract exactly one bounded time range, retaining all non-time predicates. */
|
|
21
|
+
export declare function resolveTimeAxisRange(dataset: AnyDatasetInstance, filters: MetricFilter[]): TimeAxisRange | undefined;
|
|
22
|
+
/** Mixed offset/local bounds cannot be ordered until the query timezone is known. */
|
|
23
|
+
export declare function isDefinitelyEmptyTimeRange(range: TimeAxisRange): boolean;
|
|
24
|
+
//# sourceMappingURL=time-axis-bounds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-axis-bounds.d.ts","sourceRoot":"","sources":["../../src/utils/time-axis-bounds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEpE,MAAM,WAAW,eAAe;IAC9B,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,eAAe,CAAC;IACvB,GAAG,EAAE,eAAe,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,OAAO,CAAC;IACxB,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AAID,oFAAoF;AACpF,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,GAAG,SAAS,CAuB1E;AAMD,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,aAAa,GAAG,SAAS,CA2BpH;AAED,qFAAqF;AACrF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAKxE"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const ISO_TIME_BOUND = /^(\d{4}-\d{2}-\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(Z|[+-]\d{2}:\d{2})?)?$/;
|
|
2
|
+
/** Parse without applying the process timezone, preserving nanosecond precision. */
|
|
3
|
+
export function parseTimeBound(value) {
|
|
4
|
+
if (typeof value !== 'string')
|
|
5
|
+
return undefined;
|
|
6
|
+
const match = ISO_TIME_BOUND.exec(value);
|
|
7
|
+
if (!match)
|
|
8
|
+
return undefined;
|
|
9
|
+
const [, day, hour, minute, second, fraction = '', offset] = match;
|
|
10
|
+
// Date.parse normalizes invalid calendar dates, so verify the authored day first.
|
|
11
|
+
const date = new Date(`${day}T00:00:00Z`);
|
|
12
|
+
if (!Number.isFinite(date.getTime()) || date.toISOString().slice(0, 10) !== day)
|
|
13
|
+
return undefined;
|
|
14
|
+
if (Number(hour ?? 0) > 23 || Number(minute ?? 0) > 59 || Number(second ?? 0) > 59)
|
|
15
|
+
return undefined;
|
|
16
|
+
const clock = hour === undefined ? `${day}T00:00:00` : value.replace(' ', 'T');
|
|
17
|
+
const hasOffset = offset !== undefined;
|
|
18
|
+
const milliseconds = Date.parse(hasOffset ? clock : `${clock}Z`);
|
|
19
|
+
if (!Number.isFinite(milliseconds))
|
|
20
|
+
return undefined;
|
|
21
|
+
const nanosecondRemainder = BigInt(fraction.padEnd(9, '0').slice(3));
|
|
22
|
+
return {
|
|
23
|
+
text: value,
|
|
24
|
+
milliseconds,
|
|
25
|
+
subMillisecond: nanosecondRemainder !== 0n,
|
|
26
|
+
nanoseconds: BigInt(milliseconds) * 1000000n + nanosecondRemainder,
|
|
27
|
+
hasOffset,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function timeColumn(dataset, name) {
|
|
31
|
+
return dataset.dimensions[name]?.column ?? name;
|
|
32
|
+
}
|
|
33
|
+
/** Extract exactly one bounded time range, retaining all non-time predicates. */
|
|
34
|
+
export function resolveTimeAxisRange(dataset, filters) {
|
|
35
|
+
const timeFilters = filters.filter(filter => {
|
|
36
|
+
const field = dataset.filters[filter.field]?.field ?? filter.field;
|
|
37
|
+
return !field.includes('.') && timeColumn(dataset, field) === timeColumn(dataset, dataset.timeKey ?? '');
|
|
38
|
+
});
|
|
39
|
+
let lower;
|
|
40
|
+
let upper;
|
|
41
|
+
let lowerInclusive = true;
|
|
42
|
+
let upperInclusive = true;
|
|
43
|
+
const between = timeFilters.length === 1 ? timeFilters[0] : undefined;
|
|
44
|
+
if (between?.operator === 'between' && Array.isArray(between.value) && between.value.length === 2) {
|
|
45
|
+
[lower, upper] = between.value;
|
|
46
|
+
}
|
|
47
|
+
else if (timeFilters.length === 2) {
|
|
48
|
+
const start = timeFilters.find(filter => filter.operator === 'gt' || filter.operator === 'gte');
|
|
49
|
+
const end = timeFilters.find(filter => filter.operator === 'lt' || filter.operator === 'lte');
|
|
50
|
+
lower = start?.value;
|
|
51
|
+
upper = end?.value;
|
|
52
|
+
lowerInclusive = start?.operator === 'gte';
|
|
53
|
+
upperInclusive = end?.operator === 'lte';
|
|
54
|
+
}
|
|
55
|
+
const start = parseTimeBound(lower);
|
|
56
|
+
const end = parseTimeBound(upper);
|
|
57
|
+
if (!start || !end)
|
|
58
|
+
return undefined;
|
|
59
|
+
return {
|
|
60
|
+
start, end, lowerInclusive, upperInclusive,
|
|
61
|
+
filters: filters.filter(filter => !timeFilters.includes(filter)),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Mixed offset/local bounds cannot be ordered until the query timezone is known. */
|
|
65
|
+
export function isDefinitelyEmptyTimeRange(range) {
|
|
66
|
+
const { start, end, lowerInclusive, upperInclusive } = range;
|
|
67
|
+
if (start.hasOffset !== end.hasOffset)
|
|
68
|
+
return false;
|
|
69
|
+
return start.nanoseconds > end.nanoseconds
|
|
70
|
+
|| (start.nanoseconds === end.nanoseconds && !(lowerInclusive && upperInclusive));
|
|
71
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TimeGrain } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Calendar arithmetic can clamp a nonexistent date onto the previous bucket
|
|
4
|
+
* (for example Pacific/Apia, 2011-12-30). Reject non-increasing bucket starts:
|
|
5
|
+
* repeating them would duplicate aggregates and cumulative contributions.
|
|
6
|
+
* `count` must already include the series/fanout guards, bounding enumeration.
|
|
7
|
+
*/
|
|
8
|
+
export declare function calendarBucketGuardSql(start: string, count: string, grain: TimeGrain): string;
|
|
9
|
+
//# sourceMappingURL=time-axis-calendar-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-axis-calendar-guard.d.ts","sourceRoot":"","sources":["../../src/utils/time-axis-calendar-guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAQ7F"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { addTimeSql } from './time-arithmetic-sql.js';
|
|
2
|
+
/**
|
|
3
|
+
* Calendar arithmetic can clamp a nonexistent date onto the previous bucket
|
|
4
|
+
* (for example Pacific/Apia, 2011-12-30). Reject non-increasing bucket starts:
|
|
5
|
+
* repeating them would duplicate aggregates and cumulative contributions.
|
|
6
|
+
* `count` must already include the series/fanout guards, bounding enumeration.
|
|
7
|
+
*/
|
|
8
|
+
export function calendarBucketGuardSql(start, count, grain) {
|
|
9
|
+
if (grain === 'minute' || grain === 'hour')
|
|
10
|
+
return '0';
|
|
11
|
+
const current = addTimeSql(start, '_hq_calendar_index', grain);
|
|
12
|
+
const next = addTimeSql(start, '_hq_calendar_index + 1', grain);
|
|
13
|
+
// Include the end boundary, which also bounds the source-row scan.
|
|
14
|
+
const indices = `range(toUInt64(greatest(1, ${count})))`;
|
|
15
|
+
return `throwIf(arrayExists(_hq_calendar_index -> ${next} <= ${current}, ${indices}), `
|
|
16
|
+
+ "'Time range crosses a skipped local calendar bucket; use a UTC time key for this range.')";
|
|
17
|
+
}
|