@hypequery/datasets 0.23.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/aggregations.d.ts +3 -0
- package/dist/aggregations.d.ts.map +1 -1
- package/dist/aggregations.js +6 -0
- package/dist/cache/definition-identity.js +2 -2
- package/dist/cache/query-signature.d.ts.map +1 -1
- package/dist/cache/query-signature.js +3 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +3 -2
- package/dist/client-contract/query-schema.d.ts +19 -0
- package/dist/client-contract/query-schema.d.ts.map +1 -0
- package/dist/client-contract/query-schema.js +85 -0
- package/dist/constants.d.ts +5 -4
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +7 -6
- package/dist/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +4 -3
- package/dist/dataset-sql-dialect.d.ts +117 -0
- package/dist/dataset-sql-dialect.d.ts.map +1 -1
- package/dist/dataset.d.ts +1 -1
- package/dist/dataset.d.ts.map +1 -1
- package/dist/dataset.js +2 -1
- package/dist/in-memory-backend.d.ts.map +1 -1
- package/dist/in-memory-backend.js +2 -0
- package/dist/index.d.ts +48 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -6
- package/dist/internal.d.ts +5 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +5 -1
- package/dist/measure.d.ts +9 -1
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +9 -1
- package/dist/metric-query-engine.d.ts.map +1 -1
- package/dist/metric-query-engine.js +4 -3
- package/dist/protocol-adapter.d.ts.map +1 -1
- package/dist/protocol-adapter.js +5 -3
- package/dist/protocol-deployment-adapter.d.ts.map +1 -1
- package/dist/protocol-deployment-adapter.js +7 -1
- package/dist/protocol-schema-adapter.d.ts +7 -7
- package/dist/protocol-schema-adapter.d.ts.map +1 -1
- package/dist/protocol-schema-adapter.js +12 -245
- package/dist/query-builder-protocol.d.ts +32 -1
- package/dist/query-builder-protocol.d.ts.map +1 -1
- package/dist/query-planner.d.ts +14 -5
- package/dist/query-planner.d.ts.map +1 -1
- package/dist/query-planner.js +53 -34
- package/dist/relationship-check.d.ts.map +1 -1
- package/dist/relationship-check.js +5 -3
- package/dist/schemas.d.ts +17 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +16 -0
- package/dist/semantic-planner.d.ts.map +1 -1
- package/dist/semantic-planner.js +16 -0
- package/dist/semantic-query-schema.d.ts +2 -1
- package/dist/semantic-query-schema.d.ts.map +1 -1
- package/dist/semantic-query-schema.js +12 -12
- package/dist/types.d.ts +22 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/aggregate-state-functions.d.ts +24 -0
- package/dist/utils/aggregate-state-functions.d.ts.map +1 -0
- package/dist/utils/aggregate-state-functions.js +60 -0
- package/dist/utils/approximate-measures.d.ts +3 -1
- package/dist/utils/approximate-measures.d.ts.map +1 -1
- package/dist/utils/approximate-measures.js +8 -2
- package/dist/utils/base-measure-definition.d.ts +16 -0
- package/dist/utils/base-measure-definition.d.ts.map +1 -1
- package/dist/utils/base-measure-definition.js +31 -0
- package/dist/utils/build-dataset-query-builder.d.ts.map +1 -1
- package/dist/utils/build-dataset-query-builder.js +16 -8
- package/dist/utils/by-name.d.ts +8 -0
- package/dist/utils/by-name.d.ts.map +1 -0
- package/dist/utils/by-name.js +44 -0
- package/dist/utils/calendar-shift-sql.d.ts +5 -4
- package/dist/utils/calendar-shift-sql.d.ts.map +1 -1
- package/dist/utils/calendar-shift-sql.js +8 -8
- package/dist/utils/clickhouse-dataset-sql-dialect.d.ts +2 -2
- package/dist/utils/clickhouse-dataset-sql-dialect.d.ts.map +1 -1
- package/dist/utils/clickhouse-dataset-sql-dialect.js +140 -1
- package/dist/utils/compile-dataset-query.d.ts.map +1 -1
- package/dist/utils/compile-dataset-query.js +9 -6
- package/dist/utils/composite-time-measure-sql.d.ts +3 -5
- package/dist/utils/composite-time-measure-sql.d.ts.map +1 -1
- package/dist/utils/composite-time-measure-sql.js +15 -16
- package/dist/utils/count-aggregations.d.ts +4 -0
- package/dist/utils/count-aggregations.d.ts.map +1 -0
- package/dist/utils/count-aggregations.js +6 -0
- package/dist/utils/dataset-definition-validation.d.ts.map +1 -1
- package/dist/utils/dataset-definition-validation.js +36 -26
- package/dist/utils/dataset-derived-query.d.ts +2 -4
- package/dist/utils/dataset-derived-query.d.ts.map +1 -1
- package/dist/utils/dataset-derived-query.js +11 -14
- package/dist/utils/dataset-having.d.ts +5 -7
- package/dist/utils/dataset-having.d.ts.map +1 -1
- package/dist/utils/dataset-having.js +9 -12
- package/dist/utils/dataset-normalization.d.ts.map +1 -1
- package/dist/utils/dataset-normalization.js +1 -0
- package/dist/utils/dataset-query-validation.d.ts.map +1 -1
- package/dist/utils/dataset-query-validation.js +7 -0
- package/dist/utils/dataset-source.d.ts +9 -0
- package/dist/utils/dataset-source.d.ts.map +1 -0
- package/dist/utils/dataset-source.js +16 -0
- package/dist/utils/dataset-sql-dialect.d.ts +13 -2
- package/dist/utils/dataset-sql-dialect.d.ts.map +1 -1
- package/dist/utils/dataset-sql-dialect.js +74 -1
- package/dist/utils/dataset-validation.d.ts.map +1 -1
- package/dist/utils/dataset-validation.js +7 -0
- package/dist/utils/deployable-formulas.d.ts +33 -0
- package/dist/utils/deployable-formulas.d.ts.map +1 -0
- package/dist/utils/deployable-formulas.js +102 -0
- package/dist/utils/derived-cte-validation.d.ts +5 -0
- package/dist/utils/derived-cte-validation.d.ts.map +1 -1
- package/dist/utils/derived-cte-validation.js +77 -8
- package/dist/utils/derived-measure-sql.d.ts +10 -2
- package/dist/utils/derived-measure-sql.d.ts.map +1 -1
- package/dist/utils/derived-measure-sql.js +23 -43
- package/dist/utils/filter-field-type.d.ts +14 -0
- package/dist/utils/filter-field-type.d.ts.map +1 -0
- package/dist/utils/filter-field-type.js +21 -0
- package/dist/utils/filtered-aggregation-sql.d.ts +2 -1
- package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -1
- package/dist/utils/filtered-aggregation-sql.js +18 -35
- package/dist/utils/groupable.d.ts +7 -0
- package/dist/utils/groupable.d.ts.map +1 -0
- package/dist/utils/groupable.js +8 -0
- package/dist/utils/metric-derived-query.d.ts +2 -4
- package/dist/utils/metric-derived-query.d.ts.map +1 -1
- package/dist/utils/metric-derived-query.js +54 -41
- package/dist/utils/metric-query-builder.d.ts.map +1 -1
- package/dist/utils/metric-query-builder.js +17 -9
- package/dist/utils/metric-query-validation.d.ts.map +1 -1
- package/dist/utils/metric-query-validation.js +7 -0
- package/dist/utils/portable-aggregations.d.ts +1 -0
- package/dist/utils/portable-aggregations.d.ts.map +1 -1
- package/dist/utils/portable-aggregations.js +5 -0
- package/dist/utils/protocol-schema-conversion.d.ts +12 -0
- package/dist/utils/protocol-schema-conversion.d.ts.map +1 -0
- package/dist/utils/protocol-schema-conversion.js +61 -0
- package/dist/utils/query-field-references.d.ts +10 -0
- package/dist/utils/query-field-references.d.ts.map +1 -0
- package/dist/utils/query-field-references.js +38 -0
- package/dist/utils/query-timezone.d.ts +41 -3
- package/dist/utils/query-timezone.d.ts.map +1 -1
- package/dist/utils/query-timezone.js +75 -8
- package/dist/utils/raw-sql-dialect-check.d.ts +12 -0
- package/dist/utils/raw-sql-dialect-check.d.ts.map +1 -0
- package/dist/utils/raw-sql-dialect-check.js +37 -0
- package/dist/utils/relationship-builder-plan.d.ts +25 -8
- package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
- package/dist/utils/relationship-builder-plan.js +23 -9
- package/dist/utils/relationship-measure-sql.d.ts +14 -4
- package/dist/utils/relationship-measure-sql.d.ts.map +1 -1
- package/dist/utils/relationship-measure-sql.js +33 -10
- package/dist/utils/relationship-measures.d.ts.map +1 -1
- package/dist/utils/relationship-measures.js +3 -0
- package/dist/utils/semantic-tool.d.ts +2 -2
- package/dist/utils/semantic-tool.d.ts.map +1 -1
- package/dist/utils/semantic-tool.js +1 -1
- package/dist/utils/shift-measure-sql.d.ts +5 -4
- package/dist/utils/shift-measure-sql.d.ts.map +1 -1
- package/dist/utils/shift-measure-sql.js +25 -26
- package/dist/utils/shift-wall-time-sql.d.ts +2 -1
- package/dist/utils/shift-wall-time-sql.d.ts.map +1 -1
- package/dist/utils/shift-wall-time-sql.js +7 -7
- package/dist/utils/statement-parameters.d.ts +58 -0
- package/dist/utils/statement-parameters.d.ts.map +1 -0
- package/dist/utils/statement-parameters.js +101 -0
- package/dist/utils/time-axis-calendar-guard.d.ts +2 -1
- package/dist/utils/time-axis-calendar-guard.d.ts.map +1 -1
- package/dist/utils/time-axis-calendar-guard.js +5 -7
- package/dist/utils/time-measure-axis-sql.d.ts +2 -1
- package/dist/utils/time-measure-axis-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-axis-sql.js +19 -22
- package/dist/utils/time-measure-context-sql.d.ts +3 -2
- package/dist/utils/time-measure-context-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-context-sql.js +11 -11
- package/dist/utils/time-measure-dataset-sql.d.ts +7 -5
- package/dist/utils/time-measure-dataset-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-dataset-sql.js +17 -12
- package/dist/utils/time-measure-leaf-sql.d.ts +2 -1
- package/dist/utils/time-measure-leaf-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-leaf-sql.js +17 -18
- package/dist/utils/time-measure-result-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-result-sql.js +10 -11
- package/dist/utils/time-measure-source-sql.d.ts +2 -2
- package/dist/utils/time-measure-source-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-source-sql.js +25 -16
- package/dist/utils/time-measure-values-sql.d.ts +4 -3
- package/dist/utils/time-measure-values-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-values-sql.js +26 -29
- package/dist/utils/timestamp-instants.d.ts +17 -0
- package/dist/utils/timestamp-instants.d.ts.map +1 -0
- package/dist/utils/timestamp-instants.js +34 -0
- package/dist/utils/timestamp-text.d.ts +12 -0
- package/dist/utils/timestamp-text.d.ts.map +1 -0
- package/dist/utils/timestamp-text.js +18 -0
- package/dist/utils/zod-json-schema.d.ts +8 -0
- package/dist/utils/zod-json-schema.d.ts.map +1 -0
- package/dist/utils/zod-json-schema.js +18 -0
- package/dist/utils/zod-schema-version.d.ts +19 -0
- package/dist/utils/zod-schema-version.d.ts.map +1 -0
- package/dist/utils/zod-schema-version.js +6 -0
- package/dist/utils/zod-v3-protocol-schema.d.ts +10 -0
- package/dist/utils/zod-v3-protocol-schema.d.ts.map +1 -0
- package/dist/utils/zod-v3-protocol-schema.js +164 -0
- package/dist/utils/zod-v4-protocol-schema.d.ts +12 -0
- package/dist/utils/zod-v4-protocol-schema.d.ts.map +1 -0
- package/dist/utils/zod-v4-protocol-schema.js +171 -0
- package/dist/validation.d.ts +7 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +42 -8
- package/package.json +21 -12
- package/dist/api.type-test.d.ts +0 -2
- package/dist/api.type-test.d.ts.map +0 -1
- package/dist/api.type-test.js +0 -251
- package/dist/utils/time-arithmetic-sql.d.ts +0 -4
- package/dist/utils/time-arithmetic-sql.d.ts.map +0 -1
- package/dist/utils/time-arithmetic-sql.js +0 -9
- package/dist/utils/window-aggregation-sql.d.ts +0 -5
- package/dist/utils/window-aggregation-sql.d.ts.map +0 -1
- package/dist/utils/window-aggregation-sql.js +0 -17
|
@@ -1,9 +1,47 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
|
|
2
|
+
import type { AnyDatasetInstance, MetricFilter, MetricFilterOperator } from '../types.js';
|
|
2
3
|
/** Named zones only: reject fixed offsets, invalid zones, and SQL fragments. */
|
|
3
4
|
export declare function isQueryTimezone(value: unknown): value is string;
|
|
4
5
|
export declare function queryTimezoneErrors(timezone: unknown): string[];
|
|
5
6
|
/** Conversion preserves timestamp instants and interprets Date values as local dates. */
|
|
6
|
-
export declare function queryTimeSql(expression: string, timezone: string | undefined): string;
|
|
7
|
+
export declare function queryTimeSql(dialect: DatasetSqlDialectRules, expression: string, timezone: string | undefined): string;
|
|
8
|
+
/** True when a local filter field reads the dataset's time-key column. */
|
|
9
|
+
export declare function isTimeKeyFilter(ds: AnyDatasetInstance, field: string): boolean;
|
|
7
10
|
/** Apply the query zone to time-key predicates, including semantic filter aliases. */
|
|
8
|
-
export declare function queryTimeFilterSql(ds: AnyDatasetInstance, field: string, expression: string, timezone: string | undefined): string;
|
|
11
|
+
export declare function queryTimeFilterSql(dialect: DatasetSqlDialectRules, ds: AnyDatasetInstance, field: string, expression: string, timezone: string | undefined): string;
|
|
12
|
+
/** One `where` call: the column side, its operator, and the value to bind. */
|
|
13
|
+
export interface QueryTimePredicate {
|
|
14
|
+
/** The column side of the predicate, converted to a zone when the value needs one. */
|
|
15
|
+
readonly expression: string;
|
|
16
|
+
readonly operator: MetricFilterOperator;
|
|
17
|
+
/** The value to bind, with instants rewritten to the wall clock of that zone. */
|
|
18
|
+
readonly value: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface QueryTimeFilterOptions {
|
|
21
|
+
/**
|
|
22
|
+
* How `filter.field` names its dimension. Caller filters (`'filter'`, the
|
|
23
|
+
* default) go through the dataset's filter aliases and read the time key in
|
|
24
|
+
* the query zone. Segment filters (`'dimension'`) name a dimension directly
|
|
25
|
+
* and keep the column's own zone for offset-free values.
|
|
26
|
+
*/
|
|
27
|
+
readonly field?: 'filter' | 'dimension';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The `where` predicates for one filter under the query zone.
|
|
31
|
+
*
|
|
32
|
+
* A timestamp value carrying `Z` or an offset is an instant, which ClickHouse
|
|
33
|
+
* cannot read as a DateTime64 literal. Such a value is rewritten to its UTC
|
|
34
|
+
* wall clock and compared against the column converted to UTC, where no wall
|
|
35
|
+
* clock is ambiguous, so the comparison stays exact in every query zone.
|
|
36
|
+
* Offset-free values keep the predicate they had: the query zone for a
|
|
37
|
+
* caller filter on the time key, the column's own zone otherwise.
|
|
38
|
+
*
|
|
39
|
+
* A range or exclusion list mixing both kinds splits into one predicate per
|
|
40
|
+
* kind, so an instant bound never loses its offset to a local one. A mixed
|
|
41
|
+
* `in` list cannot split (its members are alternatives, and predicates only
|
|
42
|
+
* combine with AND), so it is rejected rather than read in one zone that
|
|
43
|
+
* changes a member's meaning. Non-timestamp fields and `like` patterns are
|
|
44
|
+
* bound unchanged.
|
|
45
|
+
*/
|
|
46
|
+
export declare function queryTimeFilter(dialect: DatasetSqlDialectRules, ds: AnyDatasetInstance, filter: MetricFilter, expression: string, timezone: string | undefined, options?: QueryTimeFilterOptions): QueryTimePredicate[];
|
|
9
47
|
//# sourceMappingURL=query-timezone.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-timezone.d.ts","sourceRoot":"","sources":["../../src/utils/query-timezone.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"query-timezone.d.ts","sourceRoot":"","sources":["../../src/utils/query-timezone.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAK1F,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAS/D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAE,CAG/D;AASD,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEtH;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAM9E;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,sBAAsB,EAC/B,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GACtF,MAAM,CAER;AAED,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IACjC,sFAAsF;IACtF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;IACxC,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;CACzC;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,EAC/B,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,GAAE,sBAA2B,GACnC,kBAAkB,EAAE,CAwCtB"}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { resolveDimensionFieldType, resolveFilterFieldType } from './filter-field-type.js';
|
|
2
|
+
import { instantWallClock, parseTimestampInstant } from './timestamp-instants.js';
|
|
3
|
+
import { mixedInstantListError } from '../validation.js';
|
|
1
4
|
/** Named zones only: reject fixed offsets, invalid zones, and SQL fragments. */
|
|
2
5
|
export function isQueryTimezone(value) {
|
|
3
6
|
if (typeof value !== 'string' || value.length > 100
|
|
@@ -15,21 +18,85 @@ export function queryTimezoneErrors(timezone) {
|
|
|
15
18
|
return timezone === undefined || isQueryTimezone(timezone)
|
|
16
19
|
? [] : ['Invalid timezone: use an IANA name such as UTC or America/New_York.'];
|
|
17
20
|
}
|
|
18
|
-
|
|
19
|
-
export function queryTimeSql(expression, timezone) {
|
|
21
|
+
function canonicalTimezone(timezone) {
|
|
20
22
|
timezone ??= 'UTC';
|
|
21
23
|
const errors = queryTimezoneErrors(timezone);
|
|
22
24
|
if (errors.length)
|
|
23
25
|
throw new Error(errors[0]);
|
|
24
|
-
|
|
25
|
-
return `toDateTime64(${expression}, 9, '${name}')`;
|
|
26
|
+
return new Intl.DateTimeFormat('en', { timeZone: timezone }).resolvedOptions().timeZone;
|
|
26
27
|
}
|
|
27
|
-
/**
|
|
28
|
-
export function
|
|
28
|
+
/** Conversion preserves timestamp instants and interprets Date values as local dates. */
|
|
29
|
+
export function queryTimeSql(dialect, expression, timezone) {
|
|
30
|
+
return dialect.toQueryTimezone(expression, canonicalTimezone(timezone));
|
|
31
|
+
}
|
|
32
|
+
/** True when a local filter field reads the dataset's time-key column. */
|
|
33
|
+
export function isTimeKeyFilter(ds, field) {
|
|
29
34
|
if (!ds.timeKey || field.includes('.'))
|
|
30
|
-
return
|
|
35
|
+
return false;
|
|
31
36
|
const resolved = ds.filters[field]?.field ?? field;
|
|
32
37
|
const column = ds.dimensions[resolved]?.column ?? resolved;
|
|
33
38
|
const timeColumn = ds.dimensions[ds.timeKey]?.column ?? ds.timeKey;
|
|
34
|
-
return column === timeColumn
|
|
39
|
+
return column === timeColumn;
|
|
40
|
+
}
|
|
41
|
+
/** Apply the query zone to time-key predicates, including semantic filter aliases. */
|
|
42
|
+
export function queryTimeFilterSql(dialect, ds, field, expression, timezone) {
|
|
43
|
+
return isTimeKeyFilter(ds, field) ? queryTimeSql(dialect, expression, timezone) : expression;
|
|
44
|
+
}
|
|
45
|
+
const INSTANT_ZONE = 'UTC';
|
|
46
|
+
/**
|
|
47
|
+
* The `where` predicates for one filter under the query zone.
|
|
48
|
+
*
|
|
49
|
+
* A timestamp value carrying `Z` or an offset is an instant, which ClickHouse
|
|
50
|
+
* cannot read as a DateTime64 literal. Such a value is rewritten to its UTC
|
|
51
|
+
* wall clock and compared against the column converted to UTC, where no wall
|
|
52
|
+
* clock is ambiguous, so the comparison stays exact in every query zone.
|
|
53
|
+
* Offset-free values keep the predicate they had: the query zone for a
|
|
54
|
+
* caller filter on the time key, the column's own zone otherwise.
|
|
55
|
+
*
|
|
56
|
+
* A range or exclusion list mixing both kinds splits into one predicate per
|
|
57
|
+
* kind, so an instant bound never loses its offset to a local one. A mixed
|
|
58
|
+
* `in` list cannot split (its members are alternatives, and predicates only
|
|
59
|
+
* combine with AND), so it is rejected rather than read in one zone that
|
|
60
|
+
* changes a member's meaning. Non-timestamp fields and `like` patterns are
|
|
61
|
+
* bound unchanged.
|
|
62
|
+
*/
|
|
63
|
+
export function queryTimeFilter(dialect, ds, filter, expression, timezone, options = {}) {
|
|
64
|
+
const callerFilter = options.field !== 'dimension';
|
|
65
|
+
const fieldType = callerFilter
|
|
66
|
+
? resolveFilterFieldType(ds, filter.field)
|
|
67
|
+
: resolveDimensionFieldType(ds, filter.field);
|
|
68
|
+
const localExpression = callerFilter && isTimeKeyFilter(ds, filter.field)
|
|
69
|
+
? queryTimeSql(dialect, expression, timezone)
|
|
70
|
+
: expression;
|
|
71
|
+
const local = { expression: localExpression, operator: filter.operator, value: filter.value };
|
|
72
|
+
if (fieldType !== 'timestamp' || filter.operator === 'like')
|
|
73
|
+
return [local];
|
|
74
|
+
const values = Array.isArray(filter.value) ? filter.value : [filter.value];
|
|
75
|
+
const instants = values.map(parseTimestampInstant);
|
|
76
|
+
if (!instants.some(Boolean))
|
|
77
|
+
return [local];
|
|
78
|
+
const instantExpression = dialect.toQueryTimezone(expression, INSTANT_ZONE);
|
|
79
|
+
const rewrite = (zone) => values.map((value, index) => {
|
|
80
|
+
const instant = instants[index];
|
|
81
|
+
return instant ? instantWallClock(instant, zone) : value;
|
|
82
|
+
});
|
|
83
|
+
if (instants.every(Boolean)) {
|
|
84
|
+
const rewritten = rewrite(INSTANT_ZONE);
|
|
85
|
+
return [{ expression: instantExpression, operator: filter.operator, value: Array.isArray(filter.value) ? rewritten : rewritten[0] }];
|
|
86
|
+
}
|
|
87
|
+
switch (filter.operator) {
|
|
88
|
+
case 'between': {
|
|
89
|
+
const bound = (index, operator) => instants[index]
|
|
90
|
+
? { expression: instantExpression, operator, value: instantWallClock(instants[index], INSTANT_ZONE) }
|
|
91
|
+
: { expression: localExpression, operator, value: values[index] };
|
|
92
|
+
return [bound(0, 'gte'), bound(1, 'lte')];
|
|
93
|
+
}
|
|
94
|
+
case 'notIn':
|
|
95
|
+
return [
|
|
96
|
+
{ expression: localExpression, operator: 'notIn', value: values.filter((_, index) => !instants[index]) },
|
|
97
|
+
{ expression: instantExpression, operator: 'notIn', value: rewrite(INSTANT_ZONE).filter((_, index) => instants[index]) },
|
|
98
|
+
];
|
|
99
|
+
default:
|
|
100
|
+
throw new Error(mixedInstantListError(filter.field));
|
|
101
|
+
}
|
|
35
102
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DatasetRawSqlExpressionError, DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
|
|
2
|
+
import type { AnyDatasetInstance } from '../types.js';
|
|
3
|
+
export declare function rawSqlErrorMessage(kind: 'dimension' | 'measure', name: string, error: DatasetRawSqlExpressionError): string;
|
|
4
|
+
/**
|
|
5
|
+
* Re-check a dataset's raw `sql` fields under the active engine's lexical
|
|
6
|
+
* rules before planning. Definitions are validated with ClickHouse rules, and
|
|
7
|
+
* engines disagree on what hides a terminator: with standard-conforming
|
|
8
|
+
* strings, `'\'; DROP TABLE x; --'` ends its literal early on Postgres. A
|
|
9
|
+
* ClickHouse-rule dialect is skipped; it was checked at definition time.
|
|
10
|
+
*/
|
|
11
|
+
export declare function assertRawSqlForDialect(ds: AnyDatasetInstance, dialect: DatasetSqlDialectRules): void;
|
|
12
|
+
//# sourceMappingURL=raw-sql-dialect-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"raw-sql-dialect-check.d.ts","sourceRoot":"","sources":["../../src/utils/raw-sql-dialect-check.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,4BAA4B,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACtG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGtD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,4BAA4B,GAAG,MAAM,CAK3H;AAID;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,kBAAkB,EAAE,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAiBpG"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { clickhouseDatasetSqlDialect } from './clickhouse-dataset-sql-dialect.js';
|
|
2
|
+
export function rawSqlErrorMessage(kind, name, error) {
|
|
3
|
+
return error === 'unterminated-literal'
|
|
4
|
+
? `${kind} "${name}" sql has an unterminated quoted literal.`
|
|
5
|
+
: `${kind} "${name}" sql must be a single expression without statement terminators or `
|
|
6
|
+
+ 'comments (";", "--", "/*") outside a quoted literal.';
|
|
7
|
+
}
|
|
8
|
+
const checked = new WeakMap();
|
|
9
|
+
/**
|
|
10
|
+
* Re-check a dataset's raw `sql` fields under the active engine's lexical
|
|
11
|
+
* rules before planning. Definitions are validated with ClickHouse rules, and
|
|
12
|
+
* engines disagree on what hides a terminator: with standard-conforming
|
|
13
|
+
* strings, `'\'; DROP TABLE x; --'` ends its literal early on Postgres. A
|
|
14
|
+
* ClickHouse-rule dialect is skipped; it was checked at definition time.
|
|
15
|
+
*/
|
|
16
|
+
export function assertRawSqlForDialect(ds, dialect) {
|
|
17
|
+
if (dialect.rawSqlExpressionError === clickhouseDatasetSqlDialect.rawSqlExpressionError)
|
|
18
|
+
return;
|
|
19
|
+
let dialects = checked.get(ds);
|
|
20
|
+
if (dialects?.has(dialect))
|
|
21
|
+
return;
|
|
22
|
+
const fields = [
|
|
23
|
+
...Object.entries(ds.dimensions).map(([name, definition]) => ['dimension', name, definition.sql]),
|
|
24
|
+
...Object.entries(ds.measures).map(([name, definition]) => ['measure', name, 'sql' in definition ? definition.sql : undefined]),
|
|
25
|
+
];
|
|
26
|
+
for (const [kind, name, sql] of fields) {
|
|
27
|
+
if (sql === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
const error = dialect.rawSqlExpressionError(sql);
|
|
30
|
+
if (error) {
|
|
31
|
+
throw new Error(`Invalid dataset "${ds.name}" for the ${dialect.name} datasets dialect: ${rawSqlErrorMessage(kind, name, error)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!dialects)
|
|
35
|
+
checked.set(ds, dialects = new WeakSet());
|
|
36
|
+
dialects.add(dialect);
|
|
37
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RelationshipKey } from '../types.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
|
|
3
3
|
/**
|
|
4
4
|
* Query-builder-path planning for to-one relationship joins.
|
|
5
5
|
*
|
|
@@ -9,6 +9,13 @@ import type { DatasetSqlDialect } from '../dataset-sql-dialect.js';
|
|
|
9
9
|
* name, and aliases joined selections back to their quoted qualified name. When
|
|
10
10
|
* no qualified field is referenced, no context is produced and the builder path
|
|
11
11
|
* behaves exactly as before.
|
|
12
|
+
*
|
|
13
|
+
* Every joined target is a projection carrying a match marker (see
|
|
14
|
+
* `relationshipJoinSource`), and every joined dimension or filter reads through
|
|
15
|
+
* it: a base row without a match yields NULL for the target's columns
|
|
16
|
+
* regardless of the engine's `join_use_nulls` setting, so a missing or
|
|
17
|
+
* other-tenant target never surfaces as `''`/`0` and never passes a `neq`
|
|
18
|
+
* filter. This mirrors the Python planner.
|
|
12
19
|
*/
|
|
13
20
|
import type { AnyDatasetInstance, DatasetQuery, ExecutionContext, MetricQuery } from '../types.js';
|
|
14
21
|
import type { QueryBuilderLike } from '../query-builder-protocol.js';
|
|
@@ -16,9 +23,10 @@ import { type TenantPredicate } from './tenant-runtime.js';
|
|
|
16
23
|
export interface ResolvedBuilderJoin {
|
|
17
24
|
/** Relationship name, used as the joined table's SQL alias. */
|
|
18
25
|
relationship: string;
|
|
19
|
-
/**
|
|
26
|
+
/** Joined source: the target projected with its match marker. */
|
|
20
27
|
source: string;
|
|
21
|
-
|
|
28
|
+
/** Column of `source` that is non-null only when the target matched. */
|
|
29
|
+
matchMarker: string;
|
|
22
30
|
keys?: readonly RelationshipKey[];
|
|
23
31
|
/** Base join column (unqualified). */
|
|
24
32
|
from: string;
|
|
@@ -33,6 +41,8 @@ export interface RelationshipBuilderContext {
|
|
|
33
41
|
baseSource: string;
|
|
34
42
|
joins: ResolvedBuilderJoin[];
|
|
35
43
|
joinByRelationship: Map<string, ResolvedBuilderJoin>;
|
|
44
|
+
/** Dialect the join sources and match guards were rendered with. */
|
|
45
|
+
dialect: DatasetSqlDialectRules;
|
|
36
46
|
}
|
|
37
47
|
type QueryLike = Pick<DatasetQuery & MetricQuery, 'dimensions' | 'filters' | 'orderBy'> & Pick<DatasetQuery, 'measures'>;
|
|
38
48
|
/**
|
|
@@ -40,15 +50,22 @@ type QueryLike = Pick<DatasetQuery & MetricQuery, 'dimensions' | 'filters' | 'or
|
|
|
40
50
|
* references no relationship-qualified fields. Validation runs first, so any
|
|
41
51
|
* qualified name that fails to resolve here is skipped defensively.
|
|
42
52
|
*/
|
|
43
|
-
export declare function buildRelationshipBuilderContext(ds: AnyDatasetInstance, query: QueryLike, context?: ExecutionContext, dialect?:
|
|
53
|
+
export declare function buildRelationshipBuilderContext(ds: AnyDatasetInstance, query: QueryLike, context?: ExecutionContext, dialect?: DatasetSqlDialectRules): RelationshipBuilderContext | undefined;
|
|
44
54
|
/** Qualifies a base physical column with the base source when joins are active. */
|
|
45
55
|
export declare function qualifyBaseColumn(ctx: RelationshipBuilderContext | undefined, column: string): string;
|
|
46
56
|
/**
|
|
47
|
-
* Resolves a relationship-qualified field to
|
|
48
|
-
*
|
|
49
|
-
*
|
|
57
|
+
* Resolves a relationship-qualified field to a null-guarded SQL expression:
|
|
58
|
+
* the target column when the join matched, NULL otherwise
|
|
59
|
+
* (`if(isNotNull(<relationship>.<marker>), <relationship>.<column>, NULL)`).
|
|
60
|
+
* Throws if the name cannot be resolved, which should be unreachable after
|
|
61
|
+
* validation.
|
|
62
|
+
*
|
|
63
|
+
* Without the guard, ClickHouse fills unmatched LEFT JOIN columns with type
|
|
64
|
+
* defaults unless `join_use_nulls` is on, so an order without a customer would
|
|
65
|
+
* group under `''` and pass a `neq` filter. The guard makes the planner's
|
|
66
|
+
* documented NULL semantics hold whatever the server setting.
|
|
50
67
|
*/
|
|
51
|
-
export declare function
|
|
68
|
+
export declare function resolveQualifiedExpression(ds: AnyDatasetInstance, name: string, ctx: RelationshipBuilderContext): string;
|
|
52
69
|
/**
|
|
53
70
|
* Applies each relationship join to the builder as a single-match LEFT JOIN
|
|
54
71
|
* (`leftAnyJoin`, e.g. ClickHouse `LEFT ANY JOIN`) and, when runtime tenancy
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationship-builder-plan.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-builder-plan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"relationship-builder-plan.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-builder-plan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAGxE;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtF,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IAClC,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,kEAAkE;IAClE,MAAM,CAAC,EAAE,eAAe,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACrD,oEAAoE;IACpE,OAAO,EAAE,sBAAsB,CAAC;CACjC;AAED,KAAK,SAAS,GAAG,IAAI,CAAC,YAAY,GAAG,WAAW,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;AAEzH;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,SAAS,EAChB,OAAO,CAAC,EAAE,gBAAgB,EAC1B,OAAO,GAAE,sBAAoD,GAC5D,0BAA0B,GAAG,SAAS,CAgDxC;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,0BAA0B,GAAG,SAAS,EAC3C,MAAM,EAAE,MAAM,GACb,MAAM,CAER;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,0BAA0B,GAC9B,MAAM,CAaR;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,0BAA0B,GAAG,SAAS,GAC1C,gBAAgB,CAoClB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { relationshipKeys } from './relationship-keys.js';
|
|
2
|
-
import {
|
|
2
|
+
import { relationshipJoinSource } from './relationship-measure-sql.js';
|
|
3
3
|
import { clickhouseDatasetSqlDialect } from './clickhouse-dataset-sql-dialect.js';
|
|
4
4
|
import { resolveRelationshipReference } from './relationship-measures.js';
|
|
5
5
|
import { getRuntimeTenantPredicate } from './tenant-runtime.js';
|
|
@@ -34,11 +34,11 @@ export function buildRelationshipBuilderContext(ds, query, context, dialect = cl
|
|
|
34
34
|
.filter(name => name.startsWith(`${relationshipName}.`))
|
|
35
35
|
.map(name => name.slice(relationshipName.length + 1));
|
|
36
36
|
const keys = relationshipKeys(relationship);
|
|
37
|
-
const projected =
|
|
37
|
+
const projected = relationshipJoinSource(target, keys.map(key => key.to), measureNames, dialect);
|
|
38
38
|
joinByRelationship.set(relationshipName, {
|
|
39
39
|
relationship: relationshipName,
|
|
40
|
-
source: projected
|
|
41
|
-
matchMarker: projected
|
|
40
|
+
source: projected.source,
|
|
41
|
+
matchMarker: projected.marker,
|
|
42
42
|
from: relationship.from,
|
|
43
43
|
to: relationship.to,
|
|
44
44
|
keys,
|
|
@@ -51,6 +51,7 @@ export function buildRelationshipBuilderContext(ds, query, context, dialect = cl
|
|
|
51
51
|
baseSource: ds.source,
|
|
52
52
|
joins: Array.from(joinByRelationship.values()),
|
|
53
53
|
joinByRelationship,
|
|
54
|
+
dialect,
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
/** Qualifies a base physical column with the base source when joins are active. */
|
|
@@ -58,17 +59,30 @@ export function qualifyBaseColumn(ctx, column) {
|
|
|
58
59
|
return ctx ? `${ctx.baseSource}.${column}` : column;
|
|
59
60
|
}
|
|
60
61
|
/**
|
|
61
|
-
* Resolves a relationship-qualified field to
|
|
62
|
-
*
|
|
63
|
-
*
|
|
62
|
+
* Resolves a relationship-qualified field to a null-guarded SQL expression:
|
|
63
|
+
* the target column when the join matched, NULL otherwise
|
|
64
|
+
* (`if(isNotNull(<relationship>.<marker>), <relationship>.<column>, NULL)`).
|
|
65
|
+
* Throws if the name cannot be resolved, which should be unreachable after
|
|
66
|
+
* validation.
|
|
67
|
+
*
|
|
68
|
+
* Without the guard, ClickHouse fills unmatched LEFT JOIN columns with type
|
|
69
|
+
* defaults unless `join_use_nulls` is on, so an order without a customer would
|
|
70
|
+
* group under `''` and pass a `neq` filter. The guard makes the planner's
|
|
71
|
+
* documented NULL semantics hold whatever the server setting.
|
|
64
72
|
*/
|
|
65
|
-
export function
|
|
73
|
+
export function resolveQualifiedExpression(ds, name, ctx) {
|
|
66
74
|
const resolution = resolveQualifiedField(ds, name);
|
|
67
75
|
if (!resolution || !resolution.resolved) {
|
|
68
76
|
throw new Error(resolution?.error ?? `Cannot resolve relationship field "${name}".`);
|
|
69
77
|
}
|
|
70
78
|
const { relationshipName, targetColumn } = resolution.resolved;
|
|
71
|
-
|
|
79
|
+
const join = ctx.joinByRelationship.get(relationshipName);
|
|
80
|
+
if (!join) {
|
|
81
|
+
throw new Error(`Relationship "${relationshipName}" referenced by "${name}" was not planned as a join.`);
|
|
82
|
+
}
|
|
83
|
+
const { dialect } = ctx;
|
|
84
|
+
const matched = dialect.isNotNull(`${relationshipName}.${join.matchMarker}`);
|
|
85
|
+
return dialect.ifElse(matched, `${relationshipName}.${targetColumn}`, 'NULL');
|
|
72
86
|
}
|
|
73
87
|
/**
|
|
74
88
|
* Applies each relationship join to the builder as a single-match LEFT JOIN
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import type { AnyDatasetInstance } from '../types.js';
|
|
2
2
|
import type { RelationshipBuilderContext } from './relationship-builder-plan.js';
|
|
3
|
-
import type {
|
|
4
|
-
/**
|
|
5
|
-
|
|
3
|
+
import type { DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
|
|
4
|
+
/**
|
|
5
|
+
* The joined target as a projection whose marker is non-null only on a match.
|
|
6
|
+
*
|
|
7
|
+
* Without `join_use_nulls`, an unmatched LEFT JOIN row carries column defaults
|
|
8
|
+
* such as `0` and `''`, which `min` or `countDistinct` would count and which
|
|
9
|
+
* joined dimensions would expose as data. The marker is the one value that
|
|
10
|
+
* tells a match from a default, and an explicit projection keeps it from
|
|
11
|
+
* colliding with a target column. `names` are the related measures selected
|
|
12
|
+
* from the target (possibly none); the projection always carries the join
|
|
13
|
+
* keys, the tenant key and every column-backed dimension.
|
|
14
|
+
*/
|
|
15
|
+
export declare function relationshipJoinSource(target: AnyDatasetInstance, to: readonly string[], names: readonly string[], dialect: DatasetSqlDialectRules): {
|
|
6
16
|
source: string;
|
|
7
17
|
marker: string;
|
|
8
18
|
};
|
|
9
|
-
export declare function relationshipMeasureExpressions(ds: AnyDatasetInstance, name: string, joinCtx: RelationshipBuilderContext): {
|
|
19
|
+
export declare function relationshipMeasureExpressions(ds: AnyDatasetInstance, name: string, joinCtx: RelationshipBuilderContext, dialect: DatasetSqlDialectRules): {
|
|
10
20
|
definition: import("../types.js").MeasureDefinition;
|
|
11
21
|
field: string;
|
|
12
22
|
arg: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationship-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-measure-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAmB,MAAM,aAAa,CAAC;AAEvE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAIjF,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"relationship-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-measure-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAmB,MAAM,aAAa,CAAC;AAEvE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAIjF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAExE;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,sBAAsB;;;EAkBlJ;AAeD,wBAAgB,8BAA8B,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,0BAA0B,EAAE,OAAO,EAAE,sBAAsB;;;;EAexJ"}
|
|
@@ -2,8 +2,18 @@ import { resolveRelationshipMeasure } from './relationship-measures.js';
|
|
|
2
2
|
import { resolveDimensionExpression } from '../query-planner.js';
|
|
3
3
|
import { measureToAggregationSpec } from './dataset-normalization.js';
|
|
4
4
|
import { applyFilteredAggregationExpression } from './filtered-aggregation-sql.js';
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The joined target as a projection whose marker is non-null only on a match.
|
|
7
|
+
*
|
|
8
|
+
* Without `join_use_nulls`, an unmatched LEFT JOIN row carries column defaults
|
|
9
|
+
* such as `0` and `''`, which `min` or `countDistinct` would count and which
|
|
10
|
+
* joined dimensions would expose as data. The marker is the one value that
|
|
11
|
+
* tells a match from a default, and an explicit projection keeps it from
|
|
12
|
+
* colliding with a target column. `names` are the related measures selected
|
|
13
|
+
* from the target (possibly none); the projection always carries the join
|
|
14
|
+
* keys, the tenant key and every column-backed dimension.
|
|
15
|
+
*/
|
|
16
|
+
export function relationshipJoinSource(target, to, names, dialect) {
|
|
7
17
|
const columns = new Set([...to, ...(target.tenantKey ? [target.tenantKey] : [])]);
|
|
8
18
|
for (const [name, dimension] of Object.entries(target.dimensions))
|
|
9
19
|
if (!dimension.sql)
|
|
@@ -21,11 +31,24 @@ export function relationshipMeasureSource(target, to, names, dialect) {
|
|
|
21
31
|
while (columns.has(marker))
|
|
22
32
|
marker += '_';
|
|
23
33
|
const quote = dialect.quoteIdentifier;
|
|
24
|
-
const source = target
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
const source = relationshipTargetTable(target, dialect);
|
|
35
|
+
// Sorted so the SQL is byte-identical however the target's dimensions are
|
|
36
|
+
// ordered (authored vs. rehydrated catalogs declare them differently).
|
|
37
|
+
const projection = [...columns].sort().map(quote);
|
|
38
|
+
return { source: `(SELECT ${[...projection, `${dialect.toNullable('1')} AS ${quote(marker)}`].join(', ')} FROM ${source})`, marker };
|
|
27
39
|
}
|
|
28
|
-
|
|
40
|
+
/** The target's quoted table, read with `FINAL` when the target dataset requires it. */
|
|
41
|
+
function relationshipTargetTable(target, dialect) {
|
|
42
|
+
const quoted = dialect.quoteQualifiedName(target.source);
|
|
43
|
+
if (!target.final)
|
|
44
|
+
return quoted;
|
|
45
|
+
if (!dialect.finalTable) {
|
|
46
|
+
throw new Error(`Dataset "${target.name}" is declared final: true, but the "${dialect.name}" SQL dialect has no FINAL ` +
|
|
47
|
+
'modifier, so it cannot be joined without counting unmerged row versions.');
|
|
48
|
+
}
|
|
49
|
+
return dialect.finalTable(quoted);
|
|
50
|
+
}
|
|
51
|
+
export function relationshipMeasureExpressions(ds, name, joinCtx, dialect) {
|
|
29
52
|
const resolved = resolveRelationshipMeasure(ds, name);
|
|
30
53
|
const { relationshipName, target, definition } = resolved;
|
|
31
54
|
const marker = joinCtx.joinByRelationship.get(relationshipName)?.matchMarker;
|
|
@@ -33,12 +56,12 @@ export function relationshipMeasureExpressions(ds, name, joinCtx) {
|
|
|
33
56
|
throw new Error(`Missing match marker for relationship measure "${name}".`);
|
|
34
57
|
const ownerContext = { ...joinCtx, baseSource: relationshipName };
|
|
35
58
|
const spec = measureToAggregationSpec(name, definition);
|
|
36
|
-
const matched =
|
|
59
|
+
const matched = dialect.isNotNull(`${relationshipName}.${marker}`);
|
|
37
60
|
const field = definition.field === '*' ? `${relationshipName}.${marker}` : resolveDimensionExpression(target, definition.field, ownerContext);
|
|
38
|
-
const filtered = applyFilteredAggregationExpression(target, spec, field, ownerContext);
|
|
61
|
+
const filtered = applyFilteredAggregationExpression(target, spec, field, ownerContext, dialect);
|
|
39
62
|
return {
|
|
40
63
|
definition,
|
|
41
|
-
field:
|
|
42
|
-
arg: definition.argField ?
|
|
64
|
+
field: dialect.ifElse(matched, filtered, 'NULL'),
|
|
65
|
+
arg: definition.argField ? dialect.ifElse(matched, resolveDimensionExpression(target, definition.argField, ownerContext), 'NULL') : undefined,
|
|
43
66
|
};
|
|
44
67
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationship-measures.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-measures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAMjG,MAAM,WAAW,2BAA2B;IAC1C,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,sBAAsB,CAAC;IACrC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,iBAAiB,CAAC;CAC/B;AAED,iFAAiF;AACjF,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,GAAG,2BAA2B,
|
|
1
|
+
{"version":3,"file":"relationship-measures.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-measures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAMjG,MAAM,WAAW,2BAA2B;IAC1C,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,sBAAsB,CAAC;IACrC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,iBAAiB,CAAC;CAC/B;AAED,iFAAiF;AACjF,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,GAAG,2BAA2B,CAqB5G;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,sBAAsB,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAS9H;AAED,yGAAyG;AACzG,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,kBAAkB,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,kEAM1F;AAED,kFAAkF;AAClF,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,SAAS,MAAM,EAAO;;;;;;;;SAIlH"}
|
|
@@ -17,6 +17,9 @@ export function resolveRelationshipMeasure(ds, name) {
|
|
|
17
17
|
throw new Error(`Unknown measure "${parsed.field}" on relationship target "${target.name}".`);
|
|
18
18
|
if (!isBaseMeasure(definition))
|
|
19
19
|
throw new Error(`Measure "${name}" must reference a base aggregate; derived, window, and shift measures are not selectable through relationships.`);
|
|
20
|
+
if (definition.aggregation === 'merge') {
|
|
21
|
+
throw new Error(`Measure "${name}" cannot merge aggregate states through a relationship: joined rows would repeat or drop states. Query the target dataset directly.`);
|
|
22
|
+
}
|
|
20
23
|
if (relationship.kind === 'belongsTo' && !DUPLICATE_INSENSITIVE.has(definition.aggregation)) {
|
|
21
24
|
throw new Error(`Measure "${name}" cannot use ${definition.aggregation} through belongsTo: repeated target rows would inflate the aggregation. Use a duplicate-insensitive measure or query the target dataset.`);
|
|
22
25
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ZodType } from 'zod';
|
|
2
2
|
export declare function semanticToolNamePart(name: string): string;
|
|
3
|
-
export declare function parseCanonicalToolInput(schema:
|
|
3
|
+
export declare function parseCanonicalToolInput(schema: ZodType, input: Record<string, unknown>, label: string): Record<string, unknown>;
|
|
4
4
|
export declare function redactSemanticToolSql(result: unknown, includeSql: boolean): unknown;
|
|
5
5
|
//# sourceMappingURL=semantic-tool.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-tool.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-tool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"semantic-tool.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-tool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAEnC,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,KAAK,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,GAAG,OAAO,CAenF"}
|
|
@@ -6,7 +6,7 @@ export function parseCanonicalToolInput(schema, input, label) {
|
|
|
6
6
|
const result = schema.safeParse(input);
|
|
7
7
|
if (!result.success) {
|
|
8
8
|
const issues = result.error.issues.map((issue) => {
|
|
9
|
-
const path = issue.path.length > 0 ? issue.path.join('.') : 'input';
|
|
9
|
+
const path = issue.path.length > 0 ? issue.path.map(String).join('.') : 'input';
|
|
10
10
|
return `${path}: ${issue.message}`;
|
|
11
11
|
}).join('; ');
|
|
12
12
|
throw new Error(`Invalid ${label}: ${issues}`);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
|
|
1
2
|
import type { ShiftMeasureDefinition } from '../types.js';
|
|
2
3
|
import type { TimeMeasureAxis } from './time-measure-axis.js';
|
|
3
4
|
/** Preserve the selected portion of a bucket and both authored endpoint operators. */
|
|
4
|
-
export declare function shiftRangeSql(shift: ShiftMeasureDefinition, axis: TimeMeasureAxis, period?: string): {
|
|
5
|
+
export declare function shiftRangeSql(dialect: DatasetSqlDialectRules, shift: ShiftMeasureDefinition, axis: TimeMeasureAxis, period?: string): {
|
|
5
6
|
lower: string;
|
|
6
7
|
upper: string;
|
|
7
8
|
lowerInclusive: string;
|
|
@@ -9,11 +10,11 @@ export declare function shiftRangeSql(shift: ShiftMeasureDefinition, axis: TimeM
|
|
|
9
10
|
};
|
|
10
11
|
export declare function shiftRangePredicateSql(time: string, range: ReturnType<typeof shiftRangeSql>): string;
|
|
11
12
|
/** Check the shifted scan independently of whether it contains source rows. */
|
|
12
|
-
export declare function shiftCalendarGuardSql(shift: ShiftMeasureDefinition, axis: TimeMeasureAxis, axisGuard: string): string;
|
|
13
|
+
export declare function shiftCalendarGuardSql(dialect: DatasetSqlDialectRules, shift: ShiftMeasureDefinition, axis: TimeMeasureAxis, axisGuard: string): string;
|
|
13
14
|
/** Validate each output bucket after the series exists, including empty source populations. */
|
|
14
|
-
export declare function shiftWallTimeSeriesGuardSql(shift: ShiftMeasureDefinition, grain: TimeMeasureAxis['grain']): string;
|
|
15
|
+
export declare function shiftWallTimeSeriesGuardSql(dialect: DatasetSqlDialectRules, shift: ShiftMeasureDefinition, grain: TimeMeasureAxis['grain']): string;
|
|
15
16
|
/** Map output buckets to their earlier half-open ranges, retaining calendar arithmetic. */
|
|
16
|
-
export declare function shiftBucketCtes(shift: ShiftMeasureDefinition, axis: TimeMeasureAxis, index: number): {
|
|
17
|
+
export declare function shiftBucketCtes(dialect: DatasetSqlDialectRules, shift: ShiftMeasureDefinition, axis: TimeMeasureAxis, index: number): {
|
|
17
18
|
ctes: string[];
|
|
18
19
|
rowsSql: string;
|
|
19
20
|
sourceTimesSql: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shift-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/shift-measure-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"shift-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/shift-measure-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAK9D,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,OAAO,EAAE,sBAAsB,EAAE,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,MAAM;;;;;EAoBnI;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,GAAG,MAAM,CAGpG;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,EAAE,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAOtJ;AAED,+FAA+F;AAC/F,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,sBAAsB,EAAE,KAAK,EAAE,sBAAsB,EAAE,KAAK,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,MAAM,CAMnJ;AAED,2FAA2F;AAC3F,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,EAAE,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAuBjM"}
|