@hypequery/datasets 0.18.0 → 0.20.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 +11 -1
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/aggregations.d.ts +2 -0
- package/dist/aggregations.d.ts.map +1 -1
- package/dist/aggregations.js +4 -0
- package/dist/api.type-test.js +57 -0
- 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 +27 -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 +14 -2
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +37 -10
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -0
- package/dist/contract.d.ts +7 -2
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +9 -0
- package/dist/dataset-query.d.ts.map +1 -1
- package/dist/dataset-query.js +27 -5
- package/dist/dataset.d.ts +2 -2
- package/dist/dataset.d.ts.map +1 -1
- package/dist/dataset.js +12 -4
- package/dist/executor.d.ts +7 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +75 -22
- package/dist/in-memory-backend.d.ts.map +1 -1
- package/dist/in-memory-backend.js +34 -7
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/measure.d.ts +17 -2
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +16 -1
- package/dist/portable-execution-errors.d.ts +4 -0
- package/dist/portable-execution-errors.d.ts.map +1 -1
- package/dist/portable-execution-errors.js +4 -1
- package/dist/portable-executor.d.ts.map +1 -1
- package/dist/portable-executor.js +6 -3
- package/dist/protocol-adapter.d.ts.map +1 -1
- package/dist/protocol-adapter.js +25 -8
- package/dist/protocol-deployment-adapter.d.ts.map +1 -1
- package/dist/protocol-deployment-adapter.js +2 -1
- package/dist/protocol-rehydrate.d.ts +5 -1
- package/dist/protocol-rehydrate.d.ts.map +1 -1
- package/dist/protocol-rehydrate.js +20 -10
- package/dist/query-builder-protocol.d.ts +7 -3
- package/dist/query-builder-protocol.d.ts.map +1 -1
- package/dist/query-planner.d.ts +1 -1
- package/dist/query-planner.d.ts.map +1 -1
- package/dist/query-planner.js +10 -17
- package/dist/relationship-check.d.ts +47 -0
- package/dist/relationship-check.d.ts.map +1 -0
- package/dist/relationship-check.js +59 -0
- package/dist/relationships.d.ts +10 -10
- package/dist/relationships.js +10 -10
- 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 +15 -4
- package/dist/semantic-query-schema.d.ts.map +1 -1
- package/dist/semantic-query-schema.js +7 -0
- package/dist/types.d.ts +99 -18
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/agent-catalog-projection.d.ts +9 -2
- package/dist/utils/agent-catalog-projection.d.ts.map +1 -1
- package/dist/utils/agent-catalog-projection.js +6 -10
- package/dist/utils/approximate-measures.d.ts +10 -0
- package/dist/utils/approximate-measures.d.ts.map +1 -0
- package/dist/utils/approximate-measures.js +12 -0
- package/dist/utils/dataset-contract.d.ts.map +1 -1
- package/dist/utils/dataset-contract.js +4 -3
- 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 +7 -50
- 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-metric-ref.js +5 -0
- 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 +13 -6
- package/dist/utils/dataset-time-grains.d.ts +15 -0
- package/dist/utils/dataset-time-grains.d.ts.map +1 -0
- package/dist/utils/dataset-time-grains.js +17 -0
- package/dist/utils/derived-measure-sql.d.ts +3 -0
- package/dist/utils/derived-measure-sql.d.ts.map +1 -0
- package/dist/utils/derived-measure-sql.js +46 -0
- package/dist/utils/derived-measure-validation.d.ts.map +1 -1
- package/dist/utils/derived-measure-validation.js +6 -5
- package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -1
- package/dist/utils/filtered-aggregation-sql.js +1 -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 +16 -0
- package/dist/utils/portable-aggregations.d.ts +14 -0
- package/dist/utils/portable-aggregations.d.ts.map +1 -0
- package/dist/utils/portable-aggregations.js +15 -0
- package/dist/utils/portable-grains.d.ts +22 -0
- package/dist/utils/portable-grains.d.ts.map +1 -0
- package/dist/utils/portable-grains.js +35 -0
- package/dist/utils/portable-semantic-query.d.ts.map +1 -1
- package/dist/utils/portable-semantic-query.js +2 -1
- package/dist/utils/protocol-metric-expressions.d.ts.map +1 -1
- package/dist/utils/protocol-metric-expressions.js +2 -1
- 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/relationship-builder-plan.d.ts +6 -3
- package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
- package/dist/utils/relationship-builder-plan.js +13 -5
- package/dist/utils/relationship-key-check.d.ts +43 -0
- package/dist/utils/relationship-key-check.d.ts.map +1 -0
- package/dist/utils/relationship-key-check.js +72 -0
- package/dist/utils/segments.d.ts +31 -0
- package/dist/utils/segments.d.ts.map +1 -0
- package/dist/utils/segments.js +91 -0
- package/dist/utils/semantic-metadata-validation.d.ts.map +1 -1
- package/dist/utils/semantic-metadata-validation.js +23 -2
- 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-sql.d.ts +18 -0
- package/dist/utils/shift-measure-sql.d.ts.map +1 -0
- package/dist/utils/shift-measure-sql.js +46 -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/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 +12 -0
- package/dist/utils/time-measure-axis-sql.d.ts.map +1 -0
- package/dist/utils/time-measure-axis-sql.js +47 -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 +45 -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 +63 -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 +28 -0
- package/dist/utils/unsupported-contract-reasons.d.ts +32 -0
- package/dist/utils/unsupported-contract-reasons.d.ts.map +1 -0
- package/dist/utils/unsupported-contract-reasons.js +30 -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 +25 -0
- package/package.json +3 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TimeGrain, WindowMeasureDefinition } from '../types.js';
|
|
2
|
+
import type { ParsedTimeBound } from './time-axis-bounds.js';
|
|
3
|
+
/** Exact conversions from RFC 0015; calendar months never become days. */
|
|
4
|
+
export declare function intervalBuckets(amount: number, unit: TimeGrain, grain: TimeGrain): number | undefined;
|
|
5
|
+
/** UTC estimate; SQL uses the time expression's timezone for exact buckets. */
|
|
6
|
+
export declare function utcBucketStart(timestamp: number, grain: TimeGrain): number;
|
|
7
|
+
/** Check the authored window's compatibility and maximum per-row contribution. */
|
|
8
|
+
export declare function windowGrainErrors(name: string, window: WindowMeasureDefinition, grain: TimeGrain): string[];
|
|
9
|
+
/** Estimate only: SQL determines exact bucket counts in the query timezone. */
|
|
10
|
+
export declare function estimateTimeAxisBuckets(start: ParsedTimeBound, end: ParsedTimeBound, upperInclusive: boolean, grain: TimeGrain): number;
|
|
11
|
+
/** Leave mixed bounds and local sub-day ranges to SQL's timezone-aware check. */
|
|
12
|
+
export declare function exceedsEstimatedTimeAxisLimit(start: ParsedTimeBound, end: ParsedTimeBound, grain: TimeGrain, bucketCount: number, resultLimit: number | undefined): boolean;
|
|
13
|
+
//# sourceMappingURL=time-axis-intervals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-axis-intervals.d.ts","sourceRoot":"","sources":["../../src/utils/time-axis-intervals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAa7D,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAQrG;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAgB1E;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,uBAAuB,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,EAAE,CAU3G;AAED,+EAA+E;AAC/E,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,eAAe,EACtB,GAAG,EAAE,eAAe,EACpB,cAAc,EAAE,OAAO,EACvB,KAAK,EAAE,SAAS,GACf,MAAM,CAcR;AAED,iFAAiF;AACjF,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,eAAe,EACtB,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAMT"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const FIXED_MINUTES = { minute: 1, hour: 60, day: 1440, week: 10080 };
|
|
2
|
+
const CALENDAR_MONTHS = { month: 1, quarter: 3, year: 12 };
|
|
3
|
+
const MAX_TO_DATE_BUCKETS = {
|
|
4
|
+
hour: { minute: 60 },
|
|
5
|
+
day: { minute: 1440, hour: 24 },
|
|
6
|
+
week: { minute: 10080, hour: 168, day: 7 },
|
|
7
|
+
month: { minute: 44640, hour: 744, day: 31 },
|
|
8
|
+
quarter: { minute: 132480, hour: 2208, day: 92, month: 3 },
|
|
9
|
+
year: { minute: 527040, hour: 8784, day: 366, month: 12, quarter: 4 },
|
|
10
|
+
};
|
|
11
|
+
/** Exact conversions from RFC 0015; calendar months never become days. */
|
|
12
|
+
export function intervalBuckets(amount, unit, grain) {
|
|
13
|
+
const units = FIXED_MINUTES[unit] ?? CALENDAR_MONTHS[unit];
|
|
14
|
+
const buckets = FIXED_MINUTES[grain] ?? CALENDAR_MONTHS[grain];
|
|
15
|
+
const bothFixed = FIXED_MINUTES[unit] !== undefined && FIXED_MINUTES[grain] !== undefined;
|
|
16
|
+
const bothCalendar = CALENDAR_MONTHS[unit] !== undefined && CALENDAR_MONTHS[grain] !== undefined;
|
|
17
|
+
if (units === undefined || buckets === undefined || (!bothFixed && !bothCalendar))
|
|
18
|
+
return undefined;
|
|
19
|
+
const count = amount * units / buckets;
|
|
20
|
+
return Number.isSafeInteger(count) && count > 0 ? count : undefined;
|
|
21
|
+
}
|
|
22
|
+
/** UTC estimate; SQL uses the time expression's timezone for exact buckets. */
|
|
23
|
+
export function utcBucketStart(timestamp, grain) {
|
|
24
|
+
const date = new Date(timestamp);
|
|
25
|
+
if (grain === 'minute') {
|
|
26
|
+
date.setUTCSeconds(0, 0);
|
|
27
|
+
return date.getTime();
|
|
28
|
+
}
|
|
29
|
+
if (grain === 'hour') {
|
|
30
|
+
date.setUTCMinutes(0, 0, 0);
|
|
31
|
+
return date.getTime();
|
|
32
|
+
}
|
|
33
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
34
|
+
if (grain === 'week')
|
|
35
|
+
date.setUTCDate(date.getUTCDate() - date.getUTCDay());
|
|
36
|
+
if (grain === 'month' || grain === 'quarter' || grain === 'year')
|
|
37
|
+
date.setUTCDate(1);
|
|
38
|
+
if (grain === 'quarter')
|
|
39
|
+
date.setUTCMonth(Math.floor(date.getUTCMonth() / 3) * 3);
|
|
40
|
+
if (grain === 'year')
|
|
41
|
+
date.setUTCMonth(0);
|
|
42
|
+
return date.getTime();
|
|
43
|
+
}
|
|
44
|
+
/** Check the authored window's compatibility and maximum per-row contribution. */
|
|
45
|
+
export function windowGrainErrors(name, window, grain) {
|
|
46
|
+
const count = window.trailing
|
|
47
|
+
? intervalBuckets(window.trailing.amount, window.trailing.unit, grain)
|
|
48
|
+
: window.toDate ? MAX_TO_DATE_BUCKETS[window.toDate]?.[grain] : 1;
|
|
49
|
+
if (count === undefined) {
|
|
50
|
+
const toDateRequirement = window.toDate ? ' at a strictly coarser grain' : '';
|
|
51
|
+
return [`Window measure "${name}" must span whole "${grain}" buckets${toDateRequirement}.`];
|
|
52
|
+
}
|
|
53
|
+
if (count > 1000)
|
|
54
|
+
return [`Window measure "${name}" would make a row contribute to more than 1,000 buckets.`];
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
/** Estimate only: SQL determines exact bucket counts in the query timezone. */
|
|
58
|
+
export function estimateTimeAxisBuckets(start, end, upperInclusive, grain) {
|
|
59
|
+
const first = utcBucketStart(start.milliseconds, grain);
|
|
60
|
+
const upperBucket = utcBucketStart(end.milliseconds, grain);
|
|
61
|
+
const excludesUpperBucket = !upperInclusive && !end.subMillisecond && end.milliseconds === upperBucket;
|
|
62
|
+
const last = utcBucketStart(end.milliseconds - (excludesUpperBucket ? 1 : 0), grain);
|
|
63
|
+
const calendar = CALENDAR_MONTHS[grain];
|
|
64
|
+
if (calendar !== undefined) {
|
|
65
|
+
const firstDate = new Date(first);
|
|
66
|
+
const lastDate = new Date(last);
|
|
67
|
+
const months = (lastDate.getUTCFullYear() - firstDate.getUTCFullYear()) * 12
|
|
68
|
+
+ lastDate.getUTCMonth() - firstDate.getUTCMonth();
|
|
69
|
+
return months / calendar + 1;
|
|
70
|
+
}
|
|
71
|
+
return (last - first) / ((FIXED_MINUTES[grain] ?? 1) * 60000) + 1;
|
|
72
|
+
}
|
|
73
|
+
/** Leave mixed bounds and local sub-day ranges to SQL's timezone-aware check. */
|
|
74
|
+
export function exceedsEstimatedTimeAxisLimit(start, end, grain, bucketCount, resultLimit) {
|
|
75
|
+
if (resultLimit === undefined)
|
|
76
|
+
return false;
|
|
77
|
+
const canEstimate = start.hasOffset === end.hasOffset
|
|
78
|
+
&& (start.hasOffset || (grain !== 'minute' && grain !== 'hour'));
|
|
79
|
+
// Allow two boundary buckets because the query timezone is unknown.
|
|
80
|
+
return resultLimit === 0 || (canEstimate && bucketCount - 2 > resultLimit);
|
|
81
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { TimeMeasureAxis } from './time-measure-axis.js';
|
|
2
|
+
import type { TimeMeasureSqlSource } from './time-measure-source-sql.js';
|
|
3
|
+
import type { ShiftMeasureDefinition } from '../types.js';
|
|
4
|
+
export interface TimeMeasureSqlAxis {
|
|
5
|
+
ctes: string[];
|
|
6
|
+
timeAxisSql: string;
|
|
7
|
+
guard: string;
|
|
8
|
+
rangeGuard: string;
|
|
9
|
+
}
|
|
10
|
+
/** Build bounded output buckets using the query timezone. */
|
|
11
|
+
export declare function buildTimeMeasureAxisSql(source: TimeMeasureSqlSource, axis: TimeMeasureAxis, shifts?: readonly ShiftMeasureDefinition[]): TimeMeasureSqlAxis;
|
|
12
|
+
//# sourceMappingURL=time-measure-axis-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-measure-axis-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-axis-sql.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAGzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAG1D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAMD,6DAA6D;AAC7D,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,oBAAoB,EAC5B,IAAI,EAAE,eAAe,EACrB,MAAM,GAAE,SAAS,sBAAsB,EAAO,GAC7C,kBAAkB,CAwCpB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { GRAIN_FUNCTIONS } from '../constants.js';
|
|
2
|
+
import { addTimeSql as add, subtractTimeSql as subtract } from './time-arithmetic-sql.js';
|
|
3
|
+
import { calendarBucketGuardSql } from './time-axis-calendar-guard.js';
|
|
4
|
+
import { shiftCalendarGuardSql } from './shift-measure-sql.js';
|
|
5
|
+
function literal(value) {
|
|
6
|
+
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
|
|
7
|
+
}
|
|
8
|
+
/** Build bounded output buckets using the query timezone. */
|
|
9
|
+
export function buildTimeMeasureAxisSql(source, axis, shifts = []) {
|
|
10
|
+
const first = `${GRAIN_FUNCTIONS[axis.grain]}(_hq_lower)`;
|
|
11
|
+
const upperBucket = `${GRAIN_FUNCTIONS[axis.grain]}(_hq_upper)`;
|
|
12
|
+
const last = axis.upperInclusive
|
|
13
|
+
? upperBucket
|
|
14
|
+
: `if(_hq_upper = ${upperBucket}, ${subtract(upperBucket, 1, axis.grain)}, ${upperBucket})`;
|
|
15
|
+
const count = `dateDiff('${axis.grain}', _hq_first, _hq_last) + 1`;
|
|
16
|
+
const emptyRange = axis.lowerInclusive && axis.upperInclusive ? '' : ' OR _hq_lower = _hq_upper';
|
|
17
|
+
const rangeGuard = `throwIf(_hq_lower > _hq_upper${emptyRange}, 'Window measure time range must be non-empty and ordered.')`;
|
|
18
|
+
const limitGuard = axis.resultLimit === undefined
|
|
19
|
+
? '0'
|
|
20
|
+
: `throwIf(${count} > ${axis.resultLimit}, 'Window series exceeds the effective result limit of ${axis.resultLimit} buckets.')`;
|
|
21
|
+
const calendarGuard = calendarBucketGuardSql('_hq_first', `${count} + ${limitGuard} + ${rangeGuard}`, axis.grain);
|
|
22
|
+
const physicalTimeType = `toTypeName(tupleElement((SELECT ${source.rowAlias} FROM _hq_raw LIMIT 0), 1))`;
|
|
23
|
+
const physicalGuard = axis.grain === 'minute' || axis.grain === 'hour'
|
|
24
|
+
? `throwIf(position(${physicalTimeType}, 'Date') > 0 AND position(${physicalTimeType}, 'DateTime') = 0, 'Sub-day time measures require a time-of-day column; Date and Date32 are not supported.')`
|
|
25
|
+
: '0';
|
|
26
|
+
const shiftGuards = shifts.map(shift => shiftCalendarGuardSql(shift, axis.grain, `${limitGuard} + ${rangeGuard}`));
|
|
27
|
+
const guard = [limitGuard, physicalGuard, calendarGuard, ...shiftGuards].join(' + ');
|
|
28
|
+
// A zero-row scalar retains the converted timestamp type and query timezone.
|
|
29
|
+
// Reading any(time) here would scan the entire population just for its type.
|
|
30
|
+
const timezone = 'timezoneOf(assumeNotNull((SELECT _hq_time FROM _hq_source LIMIT 0)))';
|
|
31
|
+
const bounds = [
|
|
32
|
+
`parseDateTime64BestEffort(${literal(axis.lower)}, 9, ${timezone}) AS _hq_lower`,
|
|
33
|
+
`parseDateTime64BestEffort(${literal(axis.upper)}, 9, ${timezone}) AS _hq_upper`,
|
|
34
|
+
`${first} AS _hq_first`,
|
|
35
|
+
`${last} AS _hq_last`,
|
|
36
|
+
];
|
|
37
|
+
const bucketIndices = `arrayJoin(range(toUInt64(${count} + ${guard} + ${rangeGuard})))`;
|
|
38
|
+
const period = add('_hq_first', bucketIndices, axis.grain);
|
|
39
|
+
const ctes = [
|
|
40
|
+
`_hq_bounds AS (SELECT ${bounds.join(', ')})`,
|
|
41
|
+
`_hq_series AS (SELECT ${period} AS _hq_period FROM _hq_bounds)`,
|
|
42
|
+
];
|
|
43
|
+
// Always check the singleton axis before aggregating. ClickHouse can skip
|
|
44
|
+
// a series guard when a dimensional CROSS JOIN has an empty population.
|
|
45
|
+
const timeAxisSql = `WITH ${[...source.ctes, ctes[0]].join(',\n')} SELECT ${guard} + ${rangeGuard} AS _hq_validated FROM _hq_bounds`;
|
|
46
|
+
return { ctes, timeAxisSql, guard, rangeGuard };
|
|
47
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, DatasetQuery, MetricFilter, TimeGrain } from '../types.js';
|
|
2
|
+
export { intervalBuckets, utcBucketStart } from './time-axis-intervals.js';
|
|
3
|
+
export interface TimeMeasureAxis {
|
|
4
|
+
grain: TimeGrain;
|
|
5
|
+
lower: string;
|
|
6
|
+
upper: string;
|
|
7
|
+
lowerInclusive: boolean;
|
|
8
|
+
upperInclusive: boolean;
|
|
9
|
+
filters: MetricFilter[];
|
|
10
|
+
/** UTC estimate; execution enforces the exact timezone-aware count. */
|
|
11
|
+
bucketCount: number;
|
|
12
|
+
resultLimit?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Validate time-measure grains, resolve bounds, then estimate the output series. */
|
|
15
|
+
export declare function analyzeTimeMeasureAxis(dataset: AnyDatasetInstance, query: DatasetQuery): {
|
|
16
|
+
axis?: TimeMeasureAxis;
|
|
17
|
+
errors: string[];
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=time-measure-axis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-measure-axis.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-axis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAQ7F,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE3E,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,OAAO,CAAC;IACxB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,qFAAqF;AACrF,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,YAAY,GAClB;IAAE,IAAI,CAAC,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAmC9C"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isSupportedTimeGrain } from '../constants.js';
|
|
2
|
+
import { selectedTimeMeasures } from './time-query-measures.js';
|
|
3
|
+
import { isShiftMeasure } from './dataset-measures.js';
|
|
4
|
+
import { resolveTimeAxisRange, isDefinitelyEmptyTimeRange } from './time-axis-bounds.js';
|
|
5
|
+
import { intervalBuckets, windowGrainErrors, estimateTimeAxisBuckets, exceedsEstimatedTimeAxisLimit } from './time-axis-intervals.js';
|
|
6
|
+
export { intervalBuckets, utcBucketStart } from './time-axis-intervals.js';
|
|
7
|
+
/** Validate time-measure grains, resolve bounds, then estimate the output series. */
|
|
8
|
+
export function analyzeTimeMeasureAxis(dataset, query) {
|
|
9
|
+
const timeMeasures = selectedTimeMeasures(dataset, query);
|
|
10
|
+
if (timeMeasures.size === 0)
|
|
11
|
+
return { errors: [] };
|
|
12
|
+
const errors = [];
|
|
13
|
+
const subjects = [...timeMeasures.values()].some(isShiftMeasure) ? 'Window and shift measures' : 'Window measures';
|
|
14
|
+
if (!isSupportedTimeGrain(query.by))
|
|
15
|
+
return { errors: [`${subjects} require a supported "by" grain.`] };
|
|
16
|
+
const grain = query.by;
|
|
17
|
+
for (const [name, window] of timeMeasures) {
|
|
18
|
+
if (isShiftMeasure(window)) {
|
|
19
|
+
if (intervalBuckets(window.interval.amount, window.interval.unit, grain) === undefined)
|
|
20
|
+
errors.push(`Shift measure "${name}" must span whole "${grain}" buckets.`);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
errors.push(...windowGrainErrors(name, window, grain));
|
|
24
|
+
}
|
|
25
|
+
const range = resolveTimeAxisRange(dataset, query.filters ?? []);
|
|
26
|
+
if (!range) {
|
|
27
|
+
return { errors: [...errors, `${subjects} require exactly one bounded time range: between, or gt/gte with lt/lte, using ISO timestamps.`] };
|
|
28
|
+
}
|
|
29
|
+
if (isDefinitelyEmptyTimeRange(range)) {
|
|
30
|
+
return { errors: [...errors, 'Window measure time range must be non-empty and ordered.'] };
|
|
31
|
+
}
|
|
32
|
+
const { start, end, lowerInclusive, upperInclusive, filters } = range;
|
|
33
|
+
const bucketCount = estimateTimeAxisBuckets(start, end, upperInclusive, grain);
|
|
34
|
+
const resultLimit = query.limit ?? dataset.limits?.maxResultSize;
|
|
35
|
+
if (exceedsEstimatedTimeAxisLimit(start, end, grain, bucketCount, resultLimit)) {
|
|
36
|
+
errors.push(`Window series exceeds the effective result limit of ${resultLimit} buckets.`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
errors,
|
|
40
|
+
axis: {
|
|
41
|
+
grain, lower: start.text, upper: end.text,
|
|
42
|
+
lowerInclusive, upperInclusive, filters, bucketCount, resultLimit,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, DatasetQuery } from '../types.js';
|
|
2
|
+
import type { DatasetQueryExecutionOptions } from '../dataset-query.js';
|
|
3
|
+
/** Plan source rows → output axis → window/shift values → final projection. */
|
|
4
|
+
export declare function buildTimeMeasureDatasetSql(ds: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions): {
|
|
5
|
+
sql: string;
|
|
6
|
+
parameters: unknown[];
|
|
7
|
+
timeAxisSql: string;
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=time-measure-dataset-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-measure-dataset-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-dataset-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAYxE,+EAA+E;AAC/E,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,GACpC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAuD7D"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getBaseMeasure, getDerivedMeasure, isShiftMeasure, isWindowMeasure } from './dataset-measures.js';
|
|
2
|
+
import { selectedTimeMeasures } from './time-query-measures.js';
|
|
3
|
+
import { analyzeTimeMeasureAxis } from './time-measure-axis.js';
|
|
4
|
+
import { validateDatasetQueryInput } from './dataset-query-validation.js';
|
|
5
|
+
import { addTimeSql as add } from './time-arithmetic-sql.js';
|
|
6
|
+
import { shiftBucketCtes, shiftRangeSql, shiftRangePredicateSql } from './shift-measure-sql.js';
|
|
7
|
+
import { buildTimeMeasureSourceSql } from './time-measure-source-sql.js';
|
|
8
|
+
import { buildTimeMeasureAxisSql } from './time-measure-axis-sql.js';
|
|
9
|
+
import { buildTimeMeasureValuesSql, windowMeasureRowsSql, windowScanStartSql } from './time-measure-values-sql.js';
|
|
10
|
+
import { buildTimeMeasureResultSql } from './time-measure-result-sql.js';
|
|
11
|
+
/** Plan source rows → output axis → window/shift values → final projection. */
|
|
12
|
+
export function buildTimeMeasureDatasetSql(ds, query, options) {
|
|
13
|
+
const validation = validateDatasetQueryInput(ds, query, options.context);
|
|
14
|
+
if (!validation.valid)
|
|
15
|
+
throw new Error(`Invalid dataset query: ${validation.errors.join('; ')}`);
|
|
16
|
+
const { axis } = analyzeTimeMeasureAxis(ds, query);
|
|
17
|
+
if (!axis || !ds.timeKey)
|
|
18
|
+
throw new Error('Window measures require a bounded time axis.');
|
|
19
|
+
const timeMeasures = selectedTimeMeasures(ds, query);
|
|
20
|
+
const windows = [...timeMeasures.values()].filter(isWindowMeasure);
|
|
21
|
+
const shifts = [...timeMeasures.values()].filter(isShiftMeasure);
|
|
22
|
+
const needed = new Set((query.measures ?? []).filter(name => !getDerivedMeasure(ds.measures, name)));
|
|
23
|
+
for (const name of query.measures ?? []) {
|
|
24
|
+
const derived = getDerivedMeasure(ds.measures, name);
|
|
25
|
+
if (derived)
|
|
26
|
+
Object.values(derived.uses).forEach(input => needed.add(input));
|
|
27
|
+
}
|
|
28
|
+
const baseNames = new Set([...needed].map(name => timeMeasures.get(name)?.measure ?? name));
|
|
29
|
+
const source = buildTimeMeasureSourceSql(ds, query, baseNames, axis.filters, options);
|
|
30
|
+
const timeAxis = buildTimeMeasureAxisSql(source, axis, shifts);
|
|
31
|
+
// Scan only requested populations: intervening rows must not introduce dimensions.
|
|
32
|
+
const end = add('_hq_last', 1, axis.grain);
|
|
33
|
+
const populations = [];
|
|
34
|
+
if ([...needed].some(name => getBaseMeasure(ds.measures, name))) {
|
|
35
|
+
populations.push(`(_hq_time >= _hq_first AND _hq_time < ${end})`);
|
|
36
|
+
}
|
|
37
|
+
if (windows.length) {
|
|
38
|
+
const starts = windows.filter(window => !window.cumulative)
|
|
39
|
+
.map(window => windowScanStartSql(window, axis.grain));
|
|
40
|
+
const lower = windows.some(window => window.cumulative)
|
|
41
|
+
? '' : ` AND _hq_time >= least(${starts.join(', ')})`;
|
|
42
|
+
populations.push(`(_hq_time < ${end}${lower})`);
|
|
43
|
+
}
|
|
44
|
+
for (const shift of shifts) {
|
|
45
|
+
populations.push(`(${shiftRangePredicateSql('_hq_time', shiftRangeSql(shift, axis))})`);
|
|
46
|
+
}
|
|
47
|
+
const scanCte = `_hq_scanned AS (SELECT * FROM _hq_source CROSS JOIN _hq_bounds WHERE ${populations.join(' OR ')})`;
|
|
48
|
+
const inputs = [...needed].map((name, index) => {
|
|
49
|
+
const timeMeasure = timeMeasures.get(name);
|
|
50
|
+
const window = isWindowMeasure(timeMeasure) ? timeMeasure : undefined;
|
|
51
|
+
const base = source.bases.find(base => base.name === (timeMeasure?.measure ?? name));
|
|
52
|
+
if (!base)
|
|
53
|
+
throw new Error(`Missing base input for measure "${name}".`);
|
|
54
|
+
if (isShiftMeasure(timeMeasure)) {
|
|
55
|
+
const shifted = shiftBucketCtes(timeMeasure, axis, index);
|
|
56
|
+
return { name, base, rowsSql: shifted.rowsSql, rowCtes: shifted.ctes, restrictToAxis: false };
|
|
57
|
+
}
|
|
58
|
+
return { name, base, window, rowsSql: windowMeasureRowsSql(window, axis) };
|
|
59
|
+
});
|
|
60
|
+
const values = buildTimeMeasureValuesSql(source.dimensions, inputs, axis);
|
|
61
|
+
const sql = buildTimeMeasureResultSql(ds, query, options, source.dimensions, [...source.ctes, ...timeAxis.ctes, scanCte, ...values], `${timeAxis.guard} + ${timeAxis.rangeGuard}`);
|
|
62
|
+
return { sql, parameters: source.parameters, timeAxisSql: timeAxis.timeAxisSql };
|
|
63
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, DatasetQuery } from '../types.js';
|
|
2
|
+
import type { DatasetQueryExecutionOptions } from '../dataset-query.js';
|
|
3
|
+
import type { TimeMeasureSqlDimension } from './time-measure-source-sql.js';
|
|
4
|
+
/** Evaluate formulas after aggregation, then apply output ordering and pagination. */
|
|
5
|
+
export declare function buildTimeMeasureResultSql(ds: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions, dims: readonly TimeMeasureSqlDimension[], ctes: readonly string[], validationGuard: string): string;
|
|
6
|
+
//# sourceMappingURL=time-measure-result-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-measure-result-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-result-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAIxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAE5E,sFAAsF;AACtF,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,EACrC,IAAI,EAAE,SAAS,uBAAuB,EAAE,EACxC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,eAAe,EAAE,MAAM,GACtB,MAAM,CAuBR"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { quoteSQLIdentifier } from '../sql-utils.js';
|
|
2
|
+
import { getDerivedMeasure } from './dataset-measures.js';
|
|
3
|
+
import { derivedProjection } from './derived-measure-sql.js';
|
|
4
|
+
/** Evaluate formulas after aggregation, then apply output ordering and pagination. */
|
|
5
|
+
export function buildTimeMeasureResultSql(ds, query, options, dims, ctes, validationGuard) {
|
|
6
|
+
const selected = query.measures ?? [];
|
|
7
|
+
const projections = [
|
|
8
|
+
quoteSQLIdentifier('period'),
|
|
9
|
+
...dims.map(dimension => quoteSQLIdentifier(dimension.name)),
|
|
10
|
+
...selected.map(name => {
|
|
11
|
+
const derived = getDerivedMeasure(ds.measures, name);
|
|
12
|
+
return derived ? derivedProjection(name, derived) : quoteSQLIdentifier(name);
|
|
13
|
+
}),
|
|
14
|
+
];
|
|
15
|
+
// Keep toSQL independently guarded too. This branch produces no valid
|
|
16
|
+
// result rows, and forces axis validation even when _hq_values is empty.
|
|
17
|
+
const outputNames = ['period', ...dims.map(dimension => dimension.name), ...selected];
|
|
18
|
+
const nullProjections = outputNames.map(name => `NULL AS ${quoteSQLIdentifier(name)}`).join(', ');
|
|
19
|
+
const resultCte = `_hq_result AS (SELECT ${projections.join(', ')} FROM _hq_values`
|
|
20
|
+
+ ` UNION ALL SELECT ${nullProjections} FROM _hq_bounds WHERE ${validationGuard} = 1)`;
|
|
21
|
+
let sql = `WITH ${[...ctes, resultCte].join(',\n')} SELECT ${outputNames.map(quoteSQLIdentifier).join(', ')} FROM _hq_result`;
|
|
22
|
+
const order = query.orderBy?.length ? query.orderBy : [{ field: 'period', direction: 'asc' }];
|
|
23
|
+
sql += ` ORDER BY ${order.map(item => `${quoteSQLIdentifier(item.field)} ${item.direction === 'asc' ? 'ASC' : 'DESC'}`).join(', ')}`;
|
|
24
|
+
const limit = options.executionLimit ?? query.limit;
|
|
25
|
+
if (limit !== undefined)
|
|
26
|
+
sql += ` LIMIT ${limit}`;
|
|
27
|
+
if (query.offset !== undefined)
|
|
28
|
+
sql += ` OFFSET ${query.offset}`;
|
|
29
|
+
return sql;
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, DatasetQuery, MeasureDefinition, MetricFilter } from '../types.js';
|
|
2
|
+
import type { DatasetQueryExecutionOptions } from '../dataset-query.js';
|
|
3
|
+
export interface TimeMeasureSqlDimension {
|
|
4
|
+
name: string;
|
|
5
|
+
alias: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TimeMeasureSqlBase {
|
|
8
|
+
name: string;
|
|
9
|
+
definition: MeasureDefinition;
|
|
10
|
+
value: string;
|
|
11
|
+
arg: string;
|
|
12
|
+
}
|
|
13
|
+
export interface TimeMeasureSqlSource {
|
|
14
|
+
ctes: string[];
|
|
15
|
+
parameters: unknown[];
|
|
16
|
+
dimensions: TimeMeasureSqlDimension[];
|
|
17
|
+
bases: TimeMeasureSqlBase[];
|
|
18
|
+
rowAlias: string;
|
|
19
|
+
}
|
|
20
|
+
/** Isolate authored expressions, joins, tenancy and filters from internal SQL aliases. */
|
|
21
|
+
export declare function buildTimeMeasureSourceSql(ds: AnyDatasetInstance, query: DatasetQuery, baseNames: Iterable<string>, filters: readonly MetricFilter[], options: DatasetQueryExecutionOptions): TimeMeasureSqlSource;
|
|
22
|
+
//# sourceMappingURL=time-measure-source-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-measure-source-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-source-sql.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAUxE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,iBAAiB,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,OAAO,EAAE,CAAC;IACtB,UAAU,EAAE,uBAAuB,EAAE,CAAC;IACtC,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,0FAA0F;AAC1F,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC3B,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,OAAO,EAAE,4BAA4B,GACpC,oBAAoB,CA2DtB"}
|