@hypequery/datasets 0.23.0 → 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.
Files changed (223) hide show
  1. package/README.md +14 -0
  2. package/dist/aggregations.d.ts +3 -0
  3. package/dist/aggregations.d.ts.map +1 -1
  4. package/dist/aggregations.js +6 -0
  5. package/dist/cache/definition-identity.js +2 -2
  6. package/dist/cache/query-signature.d.ts.map +1 -1
  7. package/dist/cache/query-signature.js +3 -0
  8. package/dist/catalog.d.ts +3 -0
  9. package/dist/catalog.d.ts.map +1 -1
  10. package/dist/catalog.js +3 -2
  11. package/dist/client-contract/query-schema.d.ts +19 -0
  12. package/dist/client-contract/query-schema.d.ts.map +1 -0
  13. package/dist/client-contract/query-schema.js +85 -0
  14. package/dist/constants.d.ts +5 -4
  15. package/dist/constants.d.ts.map +1 -1
  16. package/dist/constants.js +7 -6
  17. package/dist/contract.d.ts +2 -0
  18. package/dist/contract.d.ts.map +1 -1
  19. package/dist/contract.js +4 -3
  20. package/dist/dataset-sql-dialect.d.ts +117 -0
  21. package/dist/dataset-sql-dialect.d.ts.map +1 -1
  22. package/dist/dataset.d.ts +1 -1
  23. package/dist/dataset.d.ts.map +1 -1
  24. package/dist/dataset.js +2 -1
  25. package/dist/in-memory-backend.d.ts.map +1 -1
  26. package/dist/in-memory-backend.js +2 -0
  27. package/dist/index.d.ts +48 -5
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +34 -6
  30. package/dist/internal.d.ts +5 -1
  31. package/dist/internal.d.ts.map +1 -1
  32. package/dist/internal.js +5 -1
  33. package/dist/measure.d.ts +9 -1
  34. package/dist/measure.d.ts.map +1 -1
  35. package/dist/measure.js +9 -1
  36. package/dist/metric-query-engine.d.ts.map +1 -1
  37. package/dist/metric-query-engine.js +4 -3
  38. package/dist/protocol-adapter.d.ts.map +1 -1
  39. package/dist/protocol-adapter.js +5 -3
  40. package/dist/protocol-deployment-adapter.d.ts.map +1 -1
  41. package/dist/protocol-deployment-adapter.js +7 -1
  42. package/dist/protocol-schema-adapter.d.ts +7 -7
  43. package/dist/protocol-schema-adapter.d.ts.map +1 -1
  44. package/dist/protocol-schema-adapter.js +12 -245
  45. package/dist/query-builder-protocol.d.ts +32 -1
  46. package/dist/query-builder-protocol.d.ts.map +1 -1
  47. package/dist/query-planner.d.ts +14 -5
  48. package/dist/query-planner.d.ts.map +1 -1
  49. package/dist/query-planner.js +53 -34
  50. package/dist/relationship-check.d.ts.map +1 -1
  51. package/dist/relationship-check.js +5 -3
  52. package/dist/schemas.d.ts +17 -0
  53. package/dist/schemas.d.ts.map +1 -0
  54. package/dist/schemas.js +16 -0
  55. package/dist/semantic-planner.d.ts.map +1 -1
  56. package/dist/semantic-planner.js +16 -0
  57. package/dist/semantic-query-schema.d.ts +2 -1
  58. package/dist/semantic-query-schema.d.ts.map +1 -1
  59. package/dist/semantic-query-schema.js +12 -12
  60. package/dist/types.d.ts +22 -1
  61. package/dist/types.d.ts.map +1 -1
  62. package/dist/utils/aggregate-state-functions.d.ts +24 -0
  63. package/dist/utils/aggregate-state-functions.d.ts.map +1 -0
  64. package/dist/utils/aggregate-state-functions.js +60 -0
  65. package/dist/utils/approximate-measures.d.ts +3 -1
  66. package/dist/utils/approximate-measures.d.ts.map +1 -1
  67. package/dist/utils/approximate-measures.js +8 -2
  68. package/dist/utils/base-measure-definition.d.ts +16 -0
  69. package/dist/utils/base-measure-definition.d.ts.map +1 -1
  70. package/dist/utils/base-measure-definition.js +31 -0
  71. package/dist/utils/build-dataset-query-builder.d.ts.map +1 -1
  72. package/dist/utils/build-dataset-query-builder.js +16 -8
  73. package/dist/utils/by-name.d.ts +8 -0
  74. package/dist/utils/by-name.d.ts.map +1 -0
  75. package/dist/utils/by-name.js +44 -0
  76. package/dist/utils/calendar-shift-sql.d.ts +5 -4
  77. package/dist/utils/calendar-shift-sql.d.ts.map +1 -1
  78. package/dist/utils/calendar-shift-sql.js +8 -8
  79. package/dist/utils/clickhouse-dataset-sql-dialect.d.ts +2 -2
  80. package/dist/utils/clickhouse-dataset-sql-dialect.d.ts.map +1 -1
  81. package/dist/utils/clickhouse-dataset-sql-dialect.js +140 -1
  82. package/dist/utils/compile-dataset-query.d.ts.map +1 -1
  83. package/dist/utils/compile-dataset-query.js +9 -6
  84. package/dist/utils/composite-time-measure-sql.d.ts +3 -5
  85. package/dist/utils/composite-time-measure-sql.d.ts.map +1 -1
  86. package/dist/utils/composite-time-measure-sql.js +15 -16
  87. package/dist/utils/count-aggregations.d.ts +4 -0
  88. package/dist/utils/count-aggregations.d.ts.map +1 -0
  89. package/dist/utils/count-aggregations.js +6 -0
  90. package/dist/utils/dataset-definition-validation.d.ts.map +1 -1
  91. package/dist/utils/dataset-definition-validation.js +36 -26
  92. package/dist/utils/dataset-derived-query.d.ts +2 -4
  93. package/dist/utils/dataset-derived-query.d.ts.map +1 -1
  94. package/dist/utils/dataset-derived-query.js +11 -14
  95. package/dist/utils/dataset-having.d.ts +5 -7
  96. package/dist/utils/dataset-having.d.ts.map +1 -1
  97. package/dist/utils/dataset-having.js +9 -12
  98. package/dist/utils/dataset-normalization.d.ts.map +1 -1
  99. package/dist/utils/dataset-normalization.js +1 -0
  100. package/dist/utils/dataset-query-validation.d.ts.map +1 -1
  101. package/dist/utils/dataset-query-validation.js +7 -0
  102. package/dist/utils/dataset-source.d.ts +9 -0
  103. package/dist/utils/dataset-source.d.ts.map +1 -0
  104. package/dist/utils/dataset-source.js +16 -0
  105. package/dist/utils/dataset-sql-dialect.d.ts +13 -2
  106. package/dist/utils/dataset-sql-dialect.d.ts.map +1 -1
  107. package/dist/utils/dataset-sql-dialect.js +74 -1
  108. package/dist/utils/dataset-validation.d.ts.map +1 -1
  109. package/dist/utils/dataset-validation.js +7 -0
  110. package/dist/utils/deployable-formulas.d.ts +33 -0
  111. package/dist/utils/deployable-formulas.d.ts.map +1 -0
  112. package/dist/utils/deployable-formulas.js +102 -0
  113. package/dist/utils/derived-cte-validation.d.ts +5 -0
  114. package/dist/utils/derived-cte-validation.d.ts.map +1 -1
  115. package/dist/utils/derived-cte-validation.js +77 -8
  116. package/dist/utils/derived-measure-sql.d.ts +10 -2
  117. package/dist/utils/derived-measure-sql.d.ts.map +1 -1
  118. package/dist/utils/derived-measure-sql.js +23 -43
  119. package/dist/utils/filter-field-type.d.ts +14 -0
  120. package/dist/utils/filter-field-type.d.ts.map +1 -0
  121. package/dist/utils/filter-field-type.js +21 -0
  122. package/dist/utils/filtered-aggregation-sql.d.ts +2 -1
  123. package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -1
  124. package/dist/utils/filtered-aggregation-sql.js +18 -35
  125. package/dist/utils/groupable.d.ts +7 -0
  126. package/dist/utils/groupable.d.ts.map +1 -0
  127. package/dist/utils/groupable.js +8 -0
  128. package/dist/utils/metric-derived-query.d.ts +2 -4
  129. package/dist/utils/metric-derived-query.d.ts.map +1 -1
  130. package/dist/utils/metric-derived-query.js +54 -41
  131. package/dist/utils/metric-query-builder.d.ts.map +1 -1
  132. package/dist/utils/metric-query-builder.js +17 -9
  133. package/dist/utils/metric-query-validation.d.ts.map +1 -1
  134. package/dist/utils/metric-query-validation.js +7 -0
  135. package/dist/utils/portable-aggregations.d.ts +1 -0
  136. package/dist/utils/portable-aggregations.d.ts.map +1 -1
  137. package/dist/utils/portable-aggregations.js +5 -0
  138. package/dist/utils/protocol-schema-conversion.d.ts +12 -0
  139. package/dist/utils/protocol-schema-conversion.d.ts.map +1 -0
  140. package/dist/utils/protocol-schema-conversion.js +61 -0
  141. package/dist/utils/query-field-references.d.ts +10 -0
  142. package/dist/utils/query-field-references.d.ts.map +1 -0
  143. package/dist/utils/query-field-references.js +38 -0
  144. package/dist/utils/query-timezone.d.ts +41 -3
  145. package/dist/utils/query-timezone.d.ts.map +1 -1
  146. package/dist/utils/query-timezone.js +75 -8
  147. package/dist/utils/raw-sql-dialect-check.d.ts +12 -0
  148. package/dist/utils/raw-sql-dialect-check.d.ts.map +1 -0
  149. package/dist/utils/raw-sql-dialect-check.js +37 -0
  150. package/dist/utils/relationship-builder-plan.d.ts +25 -8
  151. package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
  152. package/dist/utils/relationship-builder-plan.js +23 -9
  153. package/dist/utils/relationship-measure-sql.d.ts +14 -4
  154. package/dist/utils/relationship-measure-sql.d.ts.map +1 -1
  155. package/dist/utils/relationship-measure-sql.js +33 -10
  156. package/dist/utils/relationship-measures.d.ts.map +1 -1
  157. package/dist/utils/relationship-measures.js +3 -0
  158. package/dist/utils/semantic-tool.d.ts +2 -2
  159. package/dist/utils/semantic-tool.d.ts.map +1 -1
  160. package/dist/utils/semantic-tool.js +1 -1
  161. package/dist/utils/shift-measure-sql.d.ts +5 -4
  162. package/dist/utils/shift-measure-sql.d.ts.map +1 -1
  163. package/dist/utils/shift-measure-sql.js +25 -26
  164. package/dist/utils/shift-wall-time-sql.d.ts +2 -1
  165. package/dist/utils/shift-wall-time-sql.d.ts.map +1 -1
  166. package/dist/utils/shift-wall-time-sql.js +7 -7
  167. package/dist/utils/statement-parameters.d.ts +58 -0
  168. package/dist/utils/statement-parameters.d.ts.map +1 -0
  169. package/dist/utils/statement-parameters.js +101 -0
  170. package/dist/utils/time-axis-calendar-guard.d.ts +2 -1
  171. package/dist/utils/time-axis-calendar-guard.d.ts.map +1 -1
  172. package/dist/utils/time-axis-calendar-guard.js +5 -7
  173. package/dist/utils/time-measure-axis-sql.d.ts +2 -1
  174. package/dist/utils/time-measure-axis-sql.d.ts.map +1 -1
  175. package/dist/utils/time-measure-axis-sql.js +19 -22
  176. package/dist/utils/time-measure-context-sql.d.ts +3 -2
  177. package/dist/utils/time-measure-context-sql.d.ts.map +1 -1
  178. package/dist/utils/time-measure-context-sql.js +11 -11
  179. package/dist/utils/time-measure-dataset-sql.d.ts +7 -5
  180. package/dist/utils/time-measure-dataset-sql.d.ts.map +1 -1
  181. package/dist/utils/time-measure-dataset-sql.js +17 -12
  182. package/dist/utils/time-measure-leaf-sql.d.ts +2 -1
  183. package/dist/utils/time-measure-leaf-sql.d.ts.map +1 -1
  184. package/dist/utils/time-measure-leaf-sql.js +17 -18
  185. package/dist/utils/time-measure-result-sql.d.ts.map +1 -1
  186. package/dist/utils/time-measure-result-sql.js +10 -11
  187. package/dist/utils/time-measure-source-sql.d.ts +2 -2
  188. package/dist/utils/time-measure-source-sql.d.ts.map +1 -1
  189. package/dist/utils/time-measure-source-sql.js +25 -16
  190. package/dist/utils/time-measure-values-sql.d.ts +4 -3
  191. package/dist/utils/time-measure-values-sql.d.ts.map +1 -1
  192. package/dist/utils/time-measure-values-sql.js +26 -29
  193. package/dist/utils/timestamp-instants.d.ts +17 -0
  194. package/dist/utils/timestamp-instants.d.ts.map +1 -0
  195. package/dist/utils/timestamp-instants.js +34 -0
  196. package/dist/utils/timestamp-text.d.ts +12 -0
  197. package/dist/utils/timestamp-text.d.ts.map +1 -0
  198. package/dist/utils/timestamp-text.js +18 -0
  199. package/dist/utils/zod-json-schema.d.ts +8 -0
  200. package/dist/utils/zod-json-schema.d.ts.map +1 -0
  201. package/dist/utils/zod-json-schema.js +18 -0
  202. package/dist/utils/zod-schema-version.d.ts +19 -0
  203. package/dist/utils/zod-schema-version.d.ts.map +1 -0
  204. package/dist/utils/zod-schema-version.js +6 -0
  205. package/dist/utils/zod-v3-protocol-schema.d.ts +10 -0
  206. package/dist/utils/zod-v3-protocol-schema.d.ts.map +1 -0
  207. package/dist/utils/zod-v3-protocol-schema.js +164 -0
  208. package/dist/utils/zod-v4-protocol-schema.d.ts +12 -0
  209. package/dist/utils/zod-v4-protocol-schema.d.ts.map +1 -0
  210. package/dist/utils/zod-v4-protocol-schema.js +171 -0
  211. package/dist/validation.d.ts +7 -0
  212. package/dist/validation.d.ts.map +1 -1
  213. package/dist/validation.js +42 -8
  214. package/package.json +21 -12
  215. package/dist/api.type-test.d.ts +0 -2
  216. package/dist/api.type-test.d.ts.map +0 -1
  217. package/dist/api.type-test.js +0 -251
  218. package/dist/utils/time-arithmetic-sql.d.ts +0 -4
  219. package/dist/utils/time-arithmetic-sql.d.ts.map +0 -1
  220. package/dist/utils/time-arithmetic-sql.js +0 -9
  221. package/dist/utils/window-aggregation-sql.d.ts +0 -5
  222. package/dist/utils/window-aggregation-sql.d.ts.map +0 -1
  223. package/dist/utils/window-aggregation-sql.js +0 -17
@@ -1,9 +1,47 @@
1
- import type { AnyDatasetInstance } from '../types.js';
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;AAEtD,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAS/D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAE,CAG/D;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMrF;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GACtF,MAAM,CAMR"}
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
- /** Conversion preserves timestamp instants and interprets Date values as local dates. */
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
- const name = new Intl.DateTimeFormat('en', { timeZone: timezone }).resolvedOptions().timeZone;
25
- return `toDateTime64(${expression}, 9, '${name}')`;
26
+ return new Intl.DateTimeFormat('en', { timeZone: timezone }).resolvedOptions().timeZone;
26
27
  }
27
- /** Apply the query zone to time-key predicates, including semantic filter aliases. */
28
- export function queryTimeFilterSql(ds, field, expression, timezone) {
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 expression;
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 ? queryTimeSql(expression, timezone) : expression;
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 { DatasetSqlDialect } from '../dataset-sql-dialect.js';
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
- /** Physical target source table. */
26
+ /** Joined source: the target projected with its match marker. */
20
27
  source: string;
21
- matchMarker?: string;
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?: DatasetSqlDialect): RelationshipBuilderContext | undefined;
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 its SQL column reference
48
- * (`<relationship>.<targetColumn>`). Throws if the name cannot be resolved,
49
- * which should be unreachable after validation.
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 resolveQualifiedColumn(ds: AnyDatasetInstance, name: string): string;
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,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAGnE;;;;;;;;;GASG;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,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,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;CACtD;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,iBAA+C,GACvD,0BAA0B,GAAG,SAAS,CA+CxC;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,0BAA0B,GAAG,SAAS,EAC3C,MAAM,EAAE,MAAM,GACb,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,MAAM,GACX,MAAM,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,0BAA0B,GAAG,SAAS,GAC1C,gBAAgB,CAoClB"}
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 { relationshipMeasureSource } from './relationship-measure-sql.js';
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 = measureNames.length ? relationshipMeasureSource(target, keys.map(key => key.to), measureNames, dialect) : undefined;
37
+ const projected = relationshipJoinSource(target, keys.map(key => key.to), measureNames, dialect);
38
38
  joinByRelationship.set(relationshipName, {
39
39
  relationship: relationshipName,
40
- source: projected?.source ?? target.source,
41
- matchMarker: projected?.marker,
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 its SQL column reference
62
- * (`<relationship>.<targetColumn>`). Throws if the name cannot be resolved,
63
- * which should be unreachable after validation.
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 resolveQualifiedColumn(ds, name) {
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
- return `${relationshipName}.${targetColumn}`;
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 { DatasetSqlDialect } from '../dataset-sql-dialect.js';
4
- /** Explicit projection avoids marker collisions and makes absent targets detectable under join_use_nulls=0. */
5
- export declare function relationshipMeasureSource(target: AnyDatasetInstance, to: readonly string[], names: readonly string[], dialect: DatasetSqlDialect): {
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,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEnE,+GAA+G;AAC/G,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,iBAAiB;;;EAgBhJ;AAED,wBAAgB,8BAA8B,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,0BAA0B;;;;EAevH"}
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
- /** Explicit projection avoids marker collisions and makes absent targets detectable under join_use_nulls=0. */
6
- export function relationshipMeasureSource(target, to, names, dialect) {
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.source.split('.').map(quote).join('.');
25
- const projection = [...columns].map(quote);
26
- return { source: `(SELECT ${[...projection, `toNullable(1) AS ${quote(marker)}`].join(', ')} FROM ${source})`, marker };
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
- export function relationshipMeasureExpressions(ds, name, joinCtx) {
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 = `isNotNull(${relationshipName}.${marker})`;
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: `if(${matched}, ${filtered}, NULL)`,
42
- arg: definition.argField ? `if(${matched}, ${resolveDimensionExpression(target, definition.argField, ownerContext)}, NULL)` : undefined,
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,CAkB5G;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"}
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 { ZodTypeAny } from 'zod';
1
+ import type { ZodType } from 'zod';
2
2
  export declare function semanticToolNamePart(name: string): string;
3
- export declare function parseCanonicalToolInput(schema: ZodTypeAny, input: Record<string, unknown>, label: string): Record<string, unknown>;
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,UAAU,EAAE,MAAM,KAAK,CAAC;AAEtC,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,UAAU,EAClB,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"}
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;AAI1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAK9D,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,MAAM;;;;;EAoBlG;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,GAAG,MAAM,CAGpG;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAOrH;AAED,+FAA+F;AAC/F,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,sBAAsB,EAAE,KAAK,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,MAAM,CAMlH;AAED,2FAA2F;AAC3F,wBAAgB,eAAe,CAAC,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAsBhK"}
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"}