@hypequery/datasets 0.19.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.
Files changed (150) hide show
  1. package/dist/agent-catalog.d.ts +2 -1
  2. package/dist/agent-catalog.d.ts.map +1 -1
  3. package/dist/api.type-test.js +22 -0
  4. package/dist/cache/definition-identity.d.ts +26 -0
  5. package/dist/cache/definition-identity.d.ts.map +1 -0
  6. package/dist/cache/definition-identity.js +164 -0
  7. package/dist/cache/protocol-cache-keys.d.ts +38 -0
  8. package/dist/cache/protocol-cache-keys.d.ts.map +1 -0
  9. package/dist/cache/protocol-cache-keys.js +191 -0
  10. package/dist/cache/query-signature.d.ts +6 -0
  11. package/dist/cache/query-signature.d.ts.map +1 -1
  12. package/dist/cache/query-signature.js +10 -13
  13. package/dist/cache/semantic-query-cache.d.ts +23 -0
  14. package/dist/cache/semantic-query-cache.d.ts.map +1 -1
  15. package/dist/catalog.d.ts +3 -2
  16. package/dist/catalog.d.ts.map +1 -1
  17. package/dist/catalog.js +25 -8
  18. package/dist/contract.d.ts +2 -1
  19. package/dist/contract.d.ts.map +1 -1
  20. package/dist/contract.js +2 -0
  21. package/dist/dataset-query.d.ts.map +1 -1
  22. package/dist/dataset-query.js +21 -4
  23. package/dist/dataset.d.ts +2 -2
  24. package/dist/dataset.d.ts.map +1 -1
  25. package/dist/dataset.js +9 -4
  26. package/dist/executor.d.ts +7 -1
  27. package/dist/executor.d.ts.map +1 -1
  28. package/dist/executor.js +60 -18
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/measure.d.ts +12 -2
  32. package/dist/measure.d.ts.map +1 -1
  33. package/dist/measure.js +11 -1
  34. package/dist/protocol-adapter.d.ts.map +1 -1
  35. package/dist/protocol-adapter.js +10 -1
  36. package/dist/protocol-deployment-adapter.d.ts.map +1 -1
  37. package/dist/protocol-deployment-adapter.js +2 -1
  38. package/dist/query-planner.d.ts +1 -1
  39. package/dist/query-planner.d.ts.map +1 -1
  40. package/dist/query-planner.js +5 -17
  41. package/dist/semantic-plan.d.ts +0 -1
  42. package/dist/semantic-plan.d.ts.map +1 -1
  43. package/dist/semantic-planner.d.ts.map +1 -1
  44. package/dist/semantic-planner.js +10 -3
  45. package/dist/semantic-query-schema.d.ts.map +1 -1
  46. package/dist/semantic-query-schema.js +2 -0
  47. package/dist/types.d.ts +62 -14
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/utils/agent-catalog-projection.d.ts +3 -2
  50. package/dist/utils/agent-catalog-projection.d.ts.map +1 -1
  51. package/dist/utils/agent-catalog-projection.js +2 -10
  52. package/dist/utils/approximate-measures.d.ts +2 -2
  53. package/dist/utils/approximate-measures.d.ts.map +1 -1
  54. package/dist/utils/approximate-measures.js +3 -6
  55. package/dist/utils/dataset-contract.d.ts.map +1 -1
  56. package/dist/utils/dataset-contract.js +2 -1
  57. package/dist/utils/dataset-definition-validation.d.ts.map +1 -1
  58. package/dist/utils/dataset-definition-validation.js +6 -24
  59. package/dist/utils/dataset-derived-query.d.ts.map +1 -1
  60. package/dist/utils/dataset-derived-query.js +7 -50
  61. package/dist/utils/dataset-measures.d.ts +13 -6
  62. package/dist/utils/dataset-measures.d.ts.map +1 -1
  63. package/dist/utils/dataset-measures.js +25 -4
  64. package/dist/utils/dataset-metric-ref.d.ts +2 -2
  65. package/dist/utils/dataset-metric-ref.d.ts.map +1 -1
  66. package/dist/utils/dataset-query-result.d.ts.map +1 -1
  67. package/dist/utils/dataset-query-result.js +2 -1
  68. package/dist/utils/dataset-query-validation.d.ts.map +1 -1
  69. package/dist/utils/dataset-query-validation.js +7 -3
  70. package/dist/utils/derived-measure-sql.d.ts +3 -0
  71. package/dist/utils/derived-measure-sql.d.ts.map +1 -0
  72. package/dist/utils/derived-measure-sql.js +46 -0
  73. package/dist/utils/derived-measure-validation.d.ts.map +1 -1
  74. package/dist/utils/derived-measure-validation.js +6 -5
  75. package/dist/utils/measure-time-grains.d.ts +4 -0
  76. package/dist/utils/measure-time-grains.d.ts.map +1 -0
  77. package/dist/utils/measure-time-grains.js +16 -0
  78. package/dist/utils/query-timezone.d.ts +9 -0
  79. package/dist/utils/query-timezone.d.ts.map +1 -0
  80. package/dist/utils/query-timezone.js +35 -0
  81. package/dist/utils/semantic-metadata-validation.js +1 -1
  82. package/dist/utils/semantic-metadata.d.ts.map +1 -1
  83. package/dist/utils/semantic-metadata.js +3 -1
  84. package/dist/utils/shift-measure-definition.d.ts +4 -0
  85. package/dist/utils/shift-measure-definition.d.ts.map +1 -0
  86. package/dist/utils/shift-measure-definition.js +9 -0
  87. package/dist/utils/shift-measure-sql.d.ts +18 -0
  88. package/dist/utils/shift-measure-sql.d.ts.map +1 -0
  89. package/dist/utils/shift-measure-sql.js +46 -0
  90. package/dist/utils/shift-measure-validation.d.ts +3 -0
  91. package/dist/utils/shift-measure-validation.d.ts.map +1 -0
  92. package/dist/utils/shift-measure-validation.js +20 -0
  93. package/dist/utils/sql-under-joins.d.ts +10 -0
  94. package/dist/utils/sql-under-joins.d.ts.map +1 -0
  95. package/dist/utils/sql-under-joins.js +15 -0
  96. package/dist/utils/time-arithmetic-sql.d.ts +4 -0
  97. package/dist/utils/time-arithmetic-sql.d.ts.map +1 -0
  98. package/dist/utils/time-arithmetic-sql.js +9 -0
  99. package/dist/utils/time-axis-bounds.d.ts +24 -0
  100. package/dist/utils/time-axis-bounds.d.ts.map +1 -0
  101. package/dist/utils/time-axis-bounds.js +71 -0
  102. package/dist/utils/time-axis-calendar-guard.d.ts +9 -0
  103. package/dist/utils/time-axis-calendar-guard.d.ts.map +1 -0
  104. package/dist/utils/time-axis-calendar-guard.js +17 -0
  105. package/dist/utils/time-axis-intervals.d.ts +13 -0
  106. package/dist/utils/time-axis-intervals.d.ts.map +1 -0
  107. package/dist/utils/time-axis-intervals.js +81 -0
  108. package/dist/utils/time-measure-axis-sql.d.ts +12 -0
  109. package/dist/utils/time-measure-axis-sql.d.ts.map +1 -0
  110. package/dist/utils/time-measure-axis-sql.js +47 -0
  111. package/dist/utils/time-measure-axis.d.ts +19 -0
  112. package/dist/utils/time-measure-axis.d.ts.map +1 -0
  113. package/dist/utils/time-measure-axis.js +45 -0
  114. package/dist/utils/time-measure-dataset-sql.d.ts +9 -0
  115. package/dist/utils/time-measure-dataset-sql.d.ts.map +1 -0
  116. package/dist/utils/time-measure-dataset-sql.js +63 -0
  117. package/dist/utils/time-measure-result-sql.d.ts +6 -0
  118. package/dist/utils/time-measure-result-sql.d.ts.map +1 -0
  119. package/dist/utils/time-measure-result-sql.js +30 -0
  120. package/dist/utils/time-measure-source-sql.d.ts +22 -0
  121. package/dist/utils/time-measure-source-sql.d.ts.map +1 -0
  122. package/dist/utils/time-measure-source-sql.js +75 -0
  123. package/dist/utils/time-measure-values-sql.d.ts +20 -0
  124. package/dist/utils/time-measure-values-sql.d.ts.map +1 -0
  125. package/dist/utils/time-measure-values-sql.js +87 -0
  126. package/dist/utils/time-query-measures.d.ts +6 -0
  127. package/dist/utils/time-query-measures.d.ts.map +1 -0
  128. package/dist/utils/time-query-measures.js +28 -0
  129. package/dist/utils/window-aggregation-sql.d.ts +5 -0
  130. package/dist/utils/window-aggregation-sql.d.ts.map +1 -0
  131. package/dist/utils/window-aggregation-sql.js +17 -0
  132. package/dist/utils/window-catalog-metadata.d.ts +14 -0
  133. package/dist/utils/window-catalog-metadata.d.ts.map +1 -0
  134. package/dist/utils/window-catalog-metadata.js +17 -0
  135. package/dist/utils/window-measure-definition.d.ts +4 -0
  136. package/dist/utils/window-measure-definition.d.ts.map +1 -0
  137. package/dist/utils/window-measure-definition.js +11 -0
  138. package/dist/utils/window-measure-dependencies.d.ts +4 -0
  139. package/dist/utils/window-measure-dependencies.d.ts.map +1 -0
  140. package/dist/utils/window-measure-dependencies.js +5 -0
  141. package/dist/utils/window-measure-validation.d.ts +3 -0
  142. package/dist/utils/window-measure-validation.d.ts.map +1 -0
  143. package/dist/utils/window-measure-validation.js +49 -0
  144. package/dist/utils/window-query-measures.d.ts +4 -0
  145. package/dist/utils/window-query-measures.d.ts.map +1 -0
  146. package/dist/utils/window-query-measures.js +17 -0
  147. package/dist/utils/window-query-signature.d.ts +4 -0
  148. package/dist/utils/window-query-signature.d.ts.map +1 -0
  149. package/dist/utils/window-query-signature.js +25 -0
  150. package/package.json +3 -3
@@ -0,0 +1,46 @@
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
+ /** Preserve the selected portion of a bucket and both authored endpoint operators. */
5
+ export function shiftRangeSql(shift, axis, period) {
6
+ const end = period ? addTimeSql(period, 1, axis.grain) : undefined;
7
+ return {
8
+ lower: subtractTimeSql(period ? `greatest(${period}, _hq_lower)` : '_hq_lower', shift.interval.amount, shift.interval.unit),
9
+ upper: subtractTimeSql(end ? `least(${end}, _hq_upper)` : '_hq_upper', shift.interval.amount, shift.interval.unit),
10
+ lowerInclusive: period ? `(${period} > _hq_lower OR ${Number(axis.lowerInclusive)})` : String(Number(axis.lowerInclusive)),
11
+ upperInclusive: end ? `(_hq_upper < ${end} AND ${Number(axis.upperInclusive)})` : String(Number(axis.upperInclusive)),
12
+ };
13
+ }
14
+ export function shiftRangePredicateSql(time, range) {
15
+ return `(${time} > ${range.lower} OR (${range.lowerInclusive} AND ${time} = ${range.lower}))`
16
+ + ` AND (${time} < ${range.upper} OR (${range.upperInclusive} AND ${time} = ${range.upper}))`;
17
+ }
18
+ /** Check the shifted scan independently of whether it contains source rows. */
19
+ export function shiftCalendarGuardSql(shift, grain, axisGuard) {
20
+ const lower = subtractTimeSql('_hq_first', shift.interval.amount, shift.interval.unit);
21
+ const upper = subtractTimeSql(addTimeSql('_hq_last', 1, grain), shift.interval.amount, shift.interval.unit);
22
+ const first = `${GRAIN_FUNCTIONS[grain]}(${lower})`;
23
+ const last = `${GRAIN_FUNCTIONS[grain]}(${upper})`;
24
+ const count = `dateDiff('${grain}', ${first}, ${last}) + ${axisGuard}`;
25
+ return calendarBucketGuardSql(first, count, grain);
26
+ }
27
+ /** Map output buckets to their earlier half-open ranges, retaining calendar arithmetic. */
28
+ export function shiftBucketCtes(shift, axis, index) {
29
+ const { grain } = axis;
30
+ const ranges = `_hq_shift_ranges${index}`;
31
+ const keys = `_hq_shift_keys${index}`;
32
+ const range = shiftRangeSql(shift, axis, '_hq_period');
33
+ const bucket = `${GRAIN_FUNCTIONS[grain]}(_hq_shift_lower)`;
34
+ const upperBucket = `${GRAIN_FUNCTIONS[grain]}(_hq_shift_upper)`;
35
+ const count = `greatest(0, dateDiff('${grain}', ${bucket}, ${upperBucket}) + if(_hq_shift_upper > ${upperBucket} OR _hq_shift_upper_inclusive, 1, 0))`;
36
+ return {
37
+ ctes: [
38
+ `${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)`,
39
+ // A DST transition can map a one-hour output bucket to zero or two
40
+ // source hours. Enumerate source bucket keys and keep exact endpoints.
41
+ // The equality join avoids a source-rows × output-series Cartesian product.
42
+ `${keys} AS (SELECT *, ${addTimeSql(bucket, `arrayJoin(range(toUInt64(${count})))`, grain)} AS _hq_source_period FROM ${ranges})`,
43
+ ],
44
+ rowsSql: `SELECT r.*, b._hq_period FROM _hq_scanned 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' })}`,
45
+ };
46
+ }
@@ -0,0 +1,3 @@
1
+ import type { DatasetMeasureDefinition } from '../types.js';
2
+ export declare function validateShiftMeasures(datasetName: string, timeKey: string | undefined, measures: Record<string, DatasetMeasureDefinition>): void;
3
+ //# sourceMappingURL=shift-measure-validation.d.ts.map
@@ -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 { getBaseMeasure, 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 (!getBaseMeasure(measures, definition.measure))
14
+ throw new Error(`${prefix} must wrap a base measure; "${String(definition.measure)}" is not one.`);
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,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
+ }
@@ -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
+ }