@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
@@ -0,0 +1,14 @@
1
+ import type { AnyDatasetInstance, FieldType } from '../types.js';
2
+ /**
3
+ * The semantic field type behind a caller filter: a filter alias, a dimension
4
+ * name, or a relationship-qualified target dimension. Undefined when the
5
+ * field does not resolve to a declared dimension.
6
+ */
7
+ export declare function resolveFilterFieldType(ds: AnyDatasetInstance, filterField: string): FieldType | undefined;
8
+ /**
9
+ * The field type of a dimension named directly, as segment filters name them.
10
+ * Filter aliases are not consulted: an alias may share a dimension's name
11
+ * while pointing at a different dimension.
12
+ */
13
+ export declare function resolveDimensionFieldType(ds: AnyDatasetInstance, dimensionName: string): FieldType | undefined;
14
+ //# sourceMappingURL=filter-field-type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-field-type.d.ts","sourceRoot":"","sources":["../../src/utils/filter-field-type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGjE;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAMzG;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE9G"}
@@ -0,0 +1,21 @@
1
+ import { isQualifiedField, resolveQualifiedField } from './relationship-fields.js';
2
+ /**
3
+ * The semantic field type behind a caller filter: a filter alias, a dimension
4
+ * name, or a relationship-qualified target dimension. Undefined when the
5
+ * field does not resolve to a declared dimension.
6
+ */
7
+ export function resolveFilterFieldType(ds, filterField) {
8
+ if (isQualifiedField(filterField)) {
9
+ return resolveQualifiedField(ds, filterField)?.resolved?.targetDimension.fieldType;
10
+ }
11
+ const resolvedField = ds.filters[filterField]?.field ?? filterField;
12
+ return ds.dimensions[resolvedField]?.fieldType;
13
+ }
14
+ /**
15
+ * The field type of a dimension named directly, as segment filters name them.
16
+ * Filter aliases are not consulted: an alias may share a dimension's name
17
+ * while pointing at a different dimension.
18
+ */
19
+ export function resolveDimensionFieldType(ds, dimensionName) {
20
+ return ds.dimensions[dimensionName]?.fieldType;
21
+ }
@@ -1,6 +1,7 @@
1
1
  import type { AggregationSpec, AnyDatasetInstance } from '../types.js';
2
2
  import type { RelationshipBuilderContext } from './relationship-builder-plan.js';
3
+ import type { DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
3
4
  type DatasetShape = AnyDatasetInstance;
4
- export declare function applyFilteredAggregationExpression(ds: DatasetShape, spec: AggregationSpec, fieldOrExpr: string, joinCtx?: RelationshipBuilderContext): string;
5
+ export declare function applyFilteredAggregationExpression(ds: DatasetShape, spec: AggregationSpec, fieldOrExpr: string, joinCtx: RelationshipBuilderContext | undefined, dialect: DatasetSqlDialectRules): string;
5
6
  export {};
6
7
  //# sourceMappingURL=filtered-aggregation-sql.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"filtered-aggregation-sql.d.ts","sourceRoot":"","sources":["../../src/utils/filtered-aggregation-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAgB,MAAM,aAAa,CAAC;AAErF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAEjF,KAAK,YAAY,GAAG,kBAAkB,CAAC;AAqEvC,wBAAgB,kCAAkC,CAChD,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,eAAe,EACrB,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,0BAA0B,GACnC,MAAM,CAgCR"}
1
+ {"version":3,"file":"filtered-aggregation-sql.d.ts","sourceRoot":"","sources":["../../src/utils/filtered-aggregation-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAgB,MAAM,aAAa,CAAC;AAErF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AACjF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAExE,KAAK,YAAY,GAAG,kBAAkB,CAAC;AA6CvC,wBAAgB,kCAAkC,CAChD,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,eAAe,EACrB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,0BAA0B,GAAG,SAAS,EAC/C,OAAO,EAAE,sBAAsB,GAC9B,MAAM,CAmCR"}
@@ -1,69 +1,49 @@
1
1
  import { resolveFilterField } from '../query-planner.js';
2
- function renderMeasureFilterLiteral(value) {
3
- if (value === null) {
4
- return 'NULL';
5
- }
6
- if (typeof value === 'string') {
7
- // ClickHouse also treats backslash as a string escape. Doubling quotes
8
- // without escaping backslashes lets a value ending in \\ before a quote
9
- // break out of the literal and change the predicate.
10
- return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "''")}'`;
11
- }
12
- if (typeof value === 'number') {
13
- if (!Number.isFinite(value)) {
14
- throw new Error(`Invalid non-finite numeric literal in measure filter: ${value}`);
15
- }
16
- return String(value);
17
- }
18
- if (typeof value === 'boolean') {
19
- return value ? '1' : '0';
20
- }
21
- throw new Error(`Unsupported literal type in measure filter: ${typeof value}`);
22
- }
23
- function renderMeasureFilterCondition(ds, filter, joinCtx) {
2
+ function renderMeasureFilterCondition(ds, filter, joinCtx, dialect) {
24
3
  const field = resolveFilterField(ds, filter.field, joinCtx);
4
+ const literal = (value) => dialect.measureFilterLiteral(value);
25
5
  switch (filter.operator) {
26
6
  case 'eq':
27
- return `${field} = ${renderMeasureFilterLiteral(filter.value)}`;
7
+ return `${field} = ${literal(filter.value)}`;
28
8
  case 'neq':
29
- return `${field} != ${renderMeasureFilterLiteral(filter.value)}`;
9
+ return `${field} != ${literal(filter.value)}`;
30
10
  case 'gt':
31
- return `${field} > ${renderMeasureFilterLiteral(filter.value)}`;
11
+ return `${field} > ${literal(filter.value)}`;
32
12
  case 'gte':
33
- return `${field} >= ${renderMeasureFilterLiteral(filter.value)}`;
13
+ return `${field} >= ${literal(filter.value)}`;
34
14
  case 'lt':
35
- return `${field} < ${renderMeasureFilterLiteral(filter.value)}`;
15
+ return `${field} < ${literal(filter.value)}`;
36
16
  case 'lte':
37
- return `${field} <= ${renderMeasureFilterLiteral(filter.value)}`;
17
+ return `${field} <= ${literal(filter.value)}`;
38
18
  case 'like':
39
- return `${field} LIKE ${renderMeasureFilterLiteral(filter.value)}`;
19
+ return `${field} LIKE ${literal(filter.value)}`;
40
20
  case 'in':
41
21
  case 'notIn': {
42
22
  if (!Array.isArray(filter.value) || filter.value.length === 0) {
43
23
  throw new Error(`"${filter.operator}" measure filters require a non-empty array.`);
44
24
  }
45
- const values = filter.value.map(renderMeasureFilterLiteral).join(', ');
25
+ const values = filter.value.map(literal).join(', ');
46
26
  return `${field} ${filter.operator === 'in' ? 'IN' : 'NOT IN'} (${values})`;
47
27
  }
48
28
  case 'between': {
49
29
  if (!Array.isArray(filter.value) || filter.value.length !== 2) {
50
30
  throw new Error('"between" measure filters require a two-item array.');
51
31
  }
52
- return `${field} BETWEEN ${renderMeasureFilterLiteral(filter.value[0])} AND ${renderMeasureFilterLiteral(filter.value[1])}`;
32
+ return `${field} BETWEEN ${literal(filter.value[0])} AND ${literal(filter.value[1])}`;
53
33
  }
54
34
  }
55
35
  }
56
- export function applyFilteredAggregationExpression(ds, spec, fieldOrExpr, joinCtx) {
36
+ export function applyFilteredAggregationExpression(ds, spec, fieldOrExpr, joinCtx, dialect) {
57
37
  if (!spec.filters?.length) {
58
38
  return fieldOrExpr;
59
39
  }
60
40
  const combinedCondition = spec.filters
61
- .map(filter => renderMeasureFilterCondition(ds, filter, joinCtx))
41
+ .map(filter => renderMeasureFilterCondition(ds, filter, joinCtx, dialect))
62
42
  .map(condition => `(${condition})`)
63
43
  .join(' AND ');
64
44
  switch (spec.aggregation) {
65
45
  case 'sum':
66
- return `if(${combinedCondition}, ${fieldOrExpr}, 0)`;
46
+ return dialect.filteredAggregateInput(combinedCondition, fieldOrExpr, '0');
67
47
  // Aggregate functions skip NULLs, so the NULL fallback excludes
68
48
  // non-matching rows from percentile/stddev/variance too.
69
49
  case 'count':
@@ -75,7 +55,10 @@ export function applyFilteredAggregationExpression(ds, spec, fieldOrExpr, joinCt
75
55
  case 'stddev':
76
56
  case 'variance':
77
57
  case 'approxCountDistinct':
78
- return `if(${combinedCondition}, ${fieldOrExpr}, NULL)`;
58
+ return dialect.filteredAggregateInput(combinedCondition, fieldOrExpr, 'NULL');
59
+ case 'merge':
60
+ // A filtered merge needs -MergeIf; wrapping a state column in if() is a type error.
61
+ throw new Error('Measure filters are not supported on merge aggregations.');
79
62
  case 'argMax':
80
63
  case 'argMin':
81
64
  // NULL handling under argMax/argMin varies across ClickHouse versions.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `groupable: false` declares a dimension that backs filters or measures but
3
+ * must not become a grouping key. The query schemas leave it out; the client
4
+ * refuses it too, as the Python planner does.
5
+ */
6
+ export declare function notGroupableError(name: string): string;
7
+ //# sourceMappingURL=groupable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"groupable.d.ts","sourceRoot":"","sources":["../../src/utils/groupable.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `groupable: false` declares a dimension that backs filters or measures but
3
+ * must not become a grouping key. The query schemas leave it out; the client
4
+ * refuses it too, as the Python planner does.
5
+ */
6
+ export function notGroupableError(name) {
7
+ return `Dimension "${name}" is not groupable.`;
8
+ }
@@ -1,8 +1,6 @@
1
1
  import type { MetricRef, MetricQuery, ExecutionContext, TimeGrain } from '../types.js';
2
2
  import type { QueryBuilderFactoryLike } from '../query-builder-protocol.js';
3
+ import { type PlannedStatement } from './statement-parameters.js';
3
4
  import type { DerivedMetricSpec } from '../types.js';
4
- export declare function buildDerivedMetricSql(ref: MetricRef, spec: DerivedMetricSpec, query: MetricQuery, grain: TimeGrain | undefined, builderFactory: QueryBuilderFactoryLike, context?: ExecutionContext): {
5
- sql: string;
6
- params: unknown[];
7
- };
5
+ export declare function buildDerivedMetricSql(ref: MetricRef, spec: DerivedMetricSpec, query: MetricQuery, grain: TimeGrain | undefined, builderFactory: QueryBuilderFactoryLike, context?: ExecutionContext): PlannedStatement;
8
6
  //# sourceMappingURL=metric-derived-query.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"metric-derived-query.d.ts","sourceRoot":"","sources":["../../src/utils/metric-derived-query.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAoB,MAAM,8BAA8B,CAAC;AAiB9F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAKrD,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,iBAAiB,EACvB,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,cAAc,EAAE,uBAAuB,EACvC,OAAO,CAAC,EAAE,gBAAgB,GACzB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAiHpC"}
1
+ {"version":3,"file":"metric-derived-query.d.ts","sourceRoot":"","sources":["../../src/utils/metric-derived-query.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAoB,MAAM,8BAA8B,CAAC;AAS9F,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAUvF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAKrD,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,iBAAiB,EACvB,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,cAAc,EAAE,uBAAuB,EACvC,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAyHlB"}
@@ -1,8 +1,12 @@
1
+ import { datasetSourceBuilder } from './dataset-source.js';
1
2
  import { semanticQuerySyntaxErrors, semanticOrderDirection } from './semantic-query-syntax.js';
2
3
  /** Metric SQL compilation; factory selection and execution belong to the client. */
3
4
  import { resolveDatasetSqlDialect } from './dataset-sql-dialect.js';
5
+ import { assertRawSqlForDialect } from './raw-sql-dialect-check.js';
4
6
  import { applyAggregationSpec, buildDimensionSelectionPlan, resolveDimensionExpression, resolveFilterField, resolveTenantFilterColumn, } from '../query-planner.js';
5
- import { queryTimeFilterSql } from './query-timezone.js';
7
+ import { queryTimeFilter } from './query-timezone.js';
8
+ import { StatementParameters } from './statement-parameters.js';
9
+ import { formulaExpressionSql } from './derived-measure-sql.js';
6
10
  import { isTenantScopedFilter } from './metric-handle.js';
7
11
  import { getRuntimeTenantPredicate } from './tenant-runtime.js';
8
12
  import { applyRelationshipJoins, buildRelationshipBuilderContext, qualifyBaseColumn, } from './relationship-builder-plan.js';
@@ -15,29 +19,35 @@ export function buildDerivedMetricSql(ref, spec, query, grain, builderFactory, c
15
19
  if (syntaxErrors.length)
16
20
  throw new Error(syntaxErrors.join('; '));
17
21
  const dialect = resolveDatasetSqlDialect(builderFactory);
22
+ assertRawSqlForDialect(ref.dataset, dialect);
18
23
  const ds = ref.dataset;
19
- const joinCtx = buildRelationshipBuilderContext(ds, query, context);
24
+ const joinCtx = buildRelationshipBuilderContext(ds, query, context, dialect);
20
25
  // Build the CTE inner query using the builder
21
- let cteBuilder = builderFactory.table(ds.source);
26
+ let cteBuilder = datasetSourceBuilder(builderFactory, ds);
22
27
  cteBuilder = applyRelationshipJoins(cteBuilder, joinCtx);
23
28
  const { selectParts, groupByParts } = buildDimensionSelectionPlan(ds, query.dimensions ?? [], grain, joinCtx, query.timezone, dialect);
24
29
  if (selectParts.length > 0) {
25
30
  cteBuilder = cteBuilder.select(selectParts);
26
31
  }
32
+ // Group before aggregating: the builder only infers GROUP BY when none is
33
+ // set, and cannot infer it from quoted aliases.
34
+ if (groupByParts.length > 0) {
35
+ cteBuilder = cteBuilder.groupBy(groupByParts);
36
+ }
27
37
  // Base aggregations
28
38
  const refAliases = {};
39
+ const inputNames = {};
29
40
  const aggregateAliases = [];
30
41
  for (const [alias, baseMetric] of Object.entries(spec.uses)) {
31
42
  const baseSpec = baseMetric.spec;
32
43
  if (baseSpec.__type !== 'aggregation_spec') {
33
44
  throw new Error(`Derived metric "${ref.name}" references non-base metric "${alias}".`);
34
45
  }
35
- cteBuilder = applyAggregationSpec(cteBuilder, ds, baseSpec, alias, joinCtx);
36
- refAliases[alias] = alias;
37
- aggregateAliases.push(alias);
38
- }
39
- if (groupByParts.length > 0) {
40
- cteBuilder = cteBuilder.groupBy(groupByParts);
46
+ const quoted = dialect.quoteIdentifier(alias);
47
+ cteBuilder = applyAggregationSpec(cteBuilder, ds, baseSpec, alias, joinCtx, dialect);
48
+ refAliases[alias] = quoted;
49
+ inputNames[alias] = alias;
50
+ aggregateAliases.push(quoted);
41
51
  }
42
52
  // Filters on CTE
43
53
  const tenantColumn = resolveTenantFilterColumn(ds, context);
@@ -50,13 +60,20 @@ export function buildDerivedMetricSql(ref, spec, query, grain, builderFactory, c
50
60
  throw new Error(`Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`);
51
61
  }
52
62
  const resolvedField = resolveFilterField(ds, filter.field, joinCtx);
53
- cteBuilder = cteBuilder.where(queryTimeFilterSql(ds, filter.field, resolvedField, query.timezone), filter.operator, filter.value);
63
+ for (const predicate of queryTimeFilter(dialect, ds, filter, resolvedField, query.timezone)) {
64
+ cteBuilder = cteBuilder.where(predicate.expression, predicate.operator, predicate.value);
65
+ }
54
66
  }
55
67
  for (const filter of segmentFilters(ds, query.segments)) {
56
- cteBuilder = cteBuilder.where(resolveDimensionExpression(ds, filter.field, joinCtx), filter.operator, filter.value);
68
+ const predicates = queryTimeFilter(dialect, ds, filter, resolveDimensionExpression(ds, filter.field, joinCtx), query.timezone, { field: 'dimension' });
69
+ for (const predicate of predicates) {
70
+ cteBuilder = cteBuilder.where(predicate.expression, predicate.operator, predicate.value);
71
+ }
57
72
  }
58
- const { sql: cteSql, parameters: cteParams } = cteBuilder.toSQLWithParams();
59
- const groupingErrors = validateDerivedCteGrouping(cteSql, aggregateAliases, groupByParts);
73
+ // Check the builder's own SQL: embedded text may be an opaque block marker.
74
+ // Replaced by a plan-level check in HQ-197.
75
+ const cteStatement = cteBuilder.toSQLWithParams();
76
+ const groupingErrors = validateDerivedCteGrouping(cteStatement.sql, aggregateAliases, groupByParts);
60
77
  if (groupingErrors.length > 0) {
61
78
  throw new Error(groupingErrors.join('; '));
62
79
  }
@@ -64,43 +81,39 @@ export function buildDerivedMetricSql(ref, spec, query, grain, builderFactory, c
64
81
  // because table('base') would fail schema typing
65
82
  const outerSelectParts = [];
66
83
  if (grain)
67
- outerSelectParts.push('period');
84
+ outerSelectParts.push(dialect.quoteIdentifier('period'));
68
85
  for (const dim of query.dimensions ?? []) {
69
86
  // Joined dimensions surface from the CTE under their quoted qualified alias.
70
- if (joinCtx && isQualifiedField(dim)) {
71
- outerSelectParts.push(dialect.quoteIdentifier(dim));
72
- continue;
73
- }
74
- validateSQLIdentifier(dim, 'dimension name');
75
- outerSelectParts.push(dim);
87
+ if (!(joinCtx && isQualifiedField(dim)))
88
+ validateSQLIdentifier(dim, 'dimension name');
89
+ outerSelectParts.push(dialect.quoteIdentifier(dim));
76
90
  }
77
- const formulaExpr = spec.formula(refAliases);
91
+ // Render through the dialect, like derived measures. FormulaExpr.toSQL()
92
+ // is kept for definition identity, not for planning.
78
93
  validateSQLIdentifier(ref.name, 'metric name');
79
- outerSelectParts.push(`${formulaExpr.toSQL()} AS ${ref.name}`);
80
- let sql = `WITH base AS (${cteSql}) SELECT ${outerSelectParts.join(', ')} FROM base`;
94
+ const formulaSql = formulaExpressionSql(dialect, spec.formula(inputNames).expression, name => {
95
+ const quoted = refAliases[name];
96
+ if (quoted === undefined || !Object.hasOwn(refAliases, name)) {
97
+ throw new Error(`Derived metric "${ref.name}" formula references undeclared input "${name}".`);
98
+ }
99
+ return quoted;
100
+ });
101
+ outerSelectParts.push(`${formulaSql} AS ${dialect.quoteIdentifier(ref.name)}`);
102
+ const parameters = new StatementParameters(dialect);
103
+ let text = `WITH base AS (${parameters.embedBuilder(cteBuilder, cteStatement)}) SELECT ${outerSelectParts.join(', ')} FROM base`;
81
104
  // ORDER BY
82
105
  if (query.orderBy && query.orderBy.length > 0) {
83
106
  const orderParts = query.orderBy.map(o => {
84
- if (joinCtx && isQualifiedField(o.field)) {
85
- return `${dialect.quoteIdentifier(o.field)} ${semanticOrderDirection(o.direction)}`;
86
- }
87
- validateSQLIdentifier(o.field, 'order by field');
88
- return `${o.field} ${semanticOrderDirection(o.direction)}`;
107
+ if (!(joinCtx && isQualifiedField(o.field)))
108
+ validateSQLIdentifier(o.field, 'order by field');
109
+ return dialect.orderTerm(dialect.quoteIdentifier(o.field), semanticOrderDirection(o.direction));
89
110
  });
90
- sql += ` ORDER BY ${orderParts.join(', ')}`;
111
+ text += ` ORDER BY ${orderParts.join(', ')}`;
91
112
  }
92
113
  else if (grain) {
93
- sql += ' ORDER BY period';
94
- }
95
- if (query.limit != null) {
96
- // Ensure limit is a safe integer
97
- const limit = Math.floor(Math.abs(query.limit));
98
- sql += ` LIMIT ${limit}`;
99
- }
100
- if (query.offset != null) {
101
- // Ensure offset is a safe integer
102
- const offset = Math.floor(Math.abs(query.offset));
103
- sql += ` OFFSET ${offset}`;
114
+ text += ` ORDER BY ${dialect.orderTerm(dialect.quoteIdentifier('period'))}`;
104
115
  }
105
- return { sql, params: cteParams };
116
+ // Ensure limit and offset are safe integers.
117
+ text += dialect.paginate(query.limit != null ? Math.floor(Math.abs(query.limit)) : undefined, query.offset != null ? Math.floor(Math.abs(query.offset)) : undefined);
118
+ return { text, parameters };
106
119
  }
@@ -1 +1 @@
1
- {"version":3,"file":"metric-query-builder.d.ts","sourceRoot":"","sources":["../../src/utils/metric-query-builder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAkB9F,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEvE,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,eAAe,EACrB,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,cAAc,EAAE,uBAAuB,EACvC,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAwDlB"}
1
+ {"version":3,"file":"metric-query-builder.d.ts","sourceRoot":"","sources":["../../src/utils/metric-query-builder.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAkB9F,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEvE,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,eAAe,EACrB,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,cAAc,EAAE,uBAAuB,EACvC,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CA6DlB"}
@@ -1,8 +1,10 @@
1
+ import { datasetSourceBuilder } from './dataset-source.js';
1
2
  import { semanticQuerySyntaxErrors } from './semantic-query-syntax.js';
2
3
  /** Metric SQL compilation; factory selection and execution belong to the client. */
3
4
  import { resolveDatasetSqlDialect } from './dataset-sql-dialect.js';
5
+ import { assertRawSqlForDialect } from './raw-sql-dialect-check.js';
4
6
  import { applyAggregationSpec, appendOrderLimitOffset, buildDimensionSelectionPlan, resolveDimensionExpression, resolveFilterField, resolveTenantFilterColumn, } from '../query-planner.js';
5
- import { queryTimeFilterSql } from './query-timezone.js';
7
+ import { queryTimeFilter } from './query-timezone.js';
6
8
  import { isTenantScopedFilter } from './metric-handle.js';
7
9
  import { getRuntimeTenantPredicate } from './tenant-runtime.js';
8
10
  import { applyRelationshipJoins, buildRelationshipBuilderContext, qualifyBaseColumn, } from './relationship-builder-plan.js';
@@ -12,19 +14,20 @@ export function buildMetricQueryBuilder(ref, spec, ds, query, grain, builderFact
12
14
  if (syntaxErrors.length)
13
15
  throw new Error(syntaxErrors.join('; '));
14
16
  const dialect = resolveDatasetSqlDialect(builderFactory);
15
- const joinCtx = buildRelationshipBuilderContext(ds, query, context);
16
- let qb = builderFactory.table(ds.source);
17
+ assertRawSqlForDialect(ds, dialect);
18
+ const joinCtx = buildRelationshipBuilderContext(ds, query, context, dialect);
19
+ let qb = datasetSourceBuilder(builderFactory, ds);
17
20
  qb = applyRelationshipJoins(qb, joinCtx);
18
21
  const { selectParts, groupByParts } = buildDimensionSelectionPlan(ds, query.dimensions ?? [], grain, joinCtx, query.timezone, dialect);
19
22
  if (selectParts.length > 0) {
20
23
  qb = qb.select(selectParts);
21
24
  }
22
- // Aggregation (appends to select, auto-sets groupBy on non-agg columns)
23
- qb = applyAggregationSpec(qb, ds, spec, ref.name, joinCtx);
24
- // Explicit groupBy (ensures period + dims are grouped even if aggregation auto-groupBy misses them)
25
+ // Group before aggregating: the builder only infers GROUP BY when none is
26
+ // set, and cannot infer it from quoted aliases.
25
27
  if (groupByParts.length > 0) {
26
28
  qb = qb.groupBy(groupByParts);
27
29
  }
30
+ qb = applyAggregationSpec(qb, ds, spec, ref.name, joinCtx, dialect);
28
31
  // Tenant auto-injection
29
32
  const tenantColumn = resolveTenantFilterColumn(ds, context);
30
33
  const tenantPredicate = getRuntimeTenantPredicate(context);
@@ -37,14 +40,19 @@ export function buildMetricQueryBuilder(ref, spec, ds, query, grain, builderFact
37
40
  throw new Error(`Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`);
38
41
  }
39
42
  const resolvedField = resolveFilterField(ds, filter.field, joinCtx);
40
- qb = qb.where(queryTimeFilterSql(ds, filter.field, resolvedField, query.timezone), filter.operator, filter.value);
43
+ for (const predicate of queryTimeFilter(dialect, ds, filter, resolvedField, query.timezone)) {
44
+ qb = qb.where(predicate.expression, predicate.operator, predicate.value);
45
+ }
41
46
  }
42
47
  // Segments: author-defined, so they resolve dimensions directly rather
43
48
  // than through the caller-facing filter allow-list.
44
49
  for (const filter of segmentFilters(ds, query.segments)) {
45
- qb = qb.where(resolveDimensionExpression(ds, filter.field, joinCtx), filter.operator, filter.value);
50
+ const predicates = queryTimeFilter(dialect, ds, filter, resolveDimensionExpression(ds, filter.field, joinCtx), query.timezone, { field: 'dimension' });
51
+ for (const predicate of predicates) {
52
+ qb = qb.where(predicate.expression, predicate.operator, predicate.value);
53
+ }
46
54
  }
47
55
  // Order, limit, offset
48
- qb = appendOrderLimitOffset(qb, query.orderBy, grain, query.limit, query.offset, joinCtx, dialect);
56
+ qb = appendOrderLimitOffset(qb, query.orderBy, grain, query.limit, query.offset, joinCtx, dialect, { ds, dimensions: query.dimensions ?? [] });
49
57
  return qb;
50
58
  }
@@ -1 +1 @@
1
- {"version":3,"file":"metric-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/metric-query-validation.ts"],"names":[],"mappings":"AACA,gFAAgF;AAChF,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAI9E,OAAO,EAAsD,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAM3G,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAgIlB"}
1
+ {"version":3,"file":"metric-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/metric-query-validation.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAI9E,OAAO,EAAsD,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAM3G,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,YAAY,EACpB,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAoIlB"}
@@ -1,3 +1,4 @@
1
+ import { notGroupableError } from './groupable.js';
1
2
  import { semanticQuerySyntaxErrors } from './semantic-query-syntax.js';
2
3
  import { validateFilterValue } from '../validation.js';
3
4
  import { queryTimezoneErrors } from './query-timezone.js';
@@ -38,11 +39,17 @@ export function validateMetricQueryInput(metric, query, context) {
38
39
  if (resolution?.error) {
39
40
  errors.push(resolution.error);
40
41
  }
42
+ else if (resolution?.resolved?.targetDimension.groupable === false) {
43
+ errors.push(notGroupableError(dim));
44
+ }
41
45
  continue;
42
46
  }
43
47
  if (!dimensionNames.includes(dim)) {
44
48
  errors.push(`Unknown dimension "${dim}". Available: ${dimensionNames.join(', ')}`);
45
49
  }
50
+ else if (ds.dimensions[dim]?.groupable === false) {
51
+ errors.push(notGroupableError(dim));
52
+ }
46
53
  }
47
54
  // Validate filters
48
55
  for (const filter of query.filters ?? []) {
@@ -4,6 +4,7 @@
4
4
  * `approxCountDistinct` runs locally, but it is an expression extension 2 and
5
5
  * deployment contract 3 feature (RFC 0015). Publishing emits contract 2 until
6
6
  * it can emit contract 3, so it refuses the measure with an error naming it.
7
+ * `merge` reads ClickHouse aggregate-state columns and has no contract form.
7
8
  */
8
9
  import type { ProtocolAggregation } from '@hypequery/protocol';
9
10
  import type { AggregationType } from '../types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"portable-aggregations.d.ts","sourceRoot":"","sources":["../../src/utils/portable-aggregations.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,KAAK,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,EAAE,qBAAqB,CAAC,CAAC;AAE/E,2FAA2F;AAC3F,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,eAAe,EAC5B,KAAK,EAAE,MAAM,GACZ,mBAAmB,CAQrB"}
1
+ {"version":3,"file":"portable-aggregations.d.ts","sourceRoot":"","sources":["../../src/utils/portable-aggregations.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,KAAK,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,EAAE,qBAAqB,CAAC,CAAC;AAE/E,2FAA2F;AAC3F,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,eAAe,EAC5B,KAAK,EAAE,MAAM,GACZ,mBAAmB,CAcrB"}
@@ -4,9 +4,14 @@
4
4
  * `approxCountDistinct` runs locally, but it is an expression extension 2 and
5
5
  * deployment contract 3 feature (RFC 0015). Publishing emits contract 2 until
6
6
  * it can emit contract 3, so it refuses the measure with an error naming it.
7
+ * `merge` reads ClickHouse aggregate-state columns and has no contract form.
7
8
  */
8
9
  /** Returns `aggregation` when publishing can carry it; otherwise throws naming `owner`. */
9
10
  export function requirePortableAggregation(aggregation, owner) {
11
+ if (aggregation === 'merge') {
12
+ throw new Error(`${owner} uses merge, which cannot be published: the deployment contract has no aggregate-state ` +
13
+ 'measures. Use it for local queries only.');
14
+ }
10
15
  if (aggregation === 'approxCountDistinct') {
11
16
  throw new Error(`${owner} uses approxCountDistinct, which cannot be published yet: approximate aggregations ` +
12
17
  'need deployment contract 3 (RFC 0015). Use it for local queries, or publish countDistinct instead.');
@@ -0,0 +1,12 @@
1
+ import { type CanonicalValue } from '@hypequery/protocol';
2
+ export declare class ProtocolSchemaAdapterError extends TypeError {
3
+ readonly path: string;
4
+ constructor(message: string, path?: string);
5
+ }
6
+ export type ProtocolSchemaDraft = Record<string, unknown>;
7
+ export declare function canonicalValue(input: unknown, path: string): CanonicalValue;
8
+ /** Values of a TypeScript enum object, without the reverse mappings of numeric members. */
9
+ export declare function nativeEnumValues(input: Record<string, string | number>): readonly (string | number)[];
10
+ export declare function annotate(description: unknown, result: ProtocolSchemaDraft): ProtocolSchemaDraft;
11
+ export declare function withDefault(inner: ProtocolSchemaDraft, value: unknown, path: string): ProtocolSchemaDraft;
12
+ //# sourceMappingURL=protocol-schema-conversion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol-schema-conversion.d.ts","sourceRoot":"","sources":["../../src/utils/protocol-schema-conversion.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAElF,qBAAa,0BAA2B,SAAQ,SAAS;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,OAAO,EAAE,MAAM,EAAE,IAAI,SAAM;CAKxC;AAED,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE1D,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,CAgC3E;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAKrG;AAED,wBAAgB,QAAQ,CAAC,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,GAAG,mBAAmB,CAE/F;AAED,wBAAgB,WAAW,CACzB,KAAK,EAAE,mBAAmB,EAC1B,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,GACX,mBAAmB,CAKrB"}
@@ -0,0 +1,61 @@
1
+ import { validateCanonicalValue } from '@hypequery/protocol';
2
+ export class ProtocolSchemaAdapterError extends TypeError {
3
+ path;
4
+ constructor(message, path = '$') {
5
+ super(`${message} at ${path}`);
6
+ this.name = 'ProtocolSchemaAdapterError';
7
+ this.path = path;
8
+ }
9
+ }
10
+ export function canonicalValue(input, path) {
11
+ try {
12
+ if (Array.isArray(input)) {
13
+ return validateCanonicalValue({
14
+ $hypequery: {
15
+ type: 'array',
16
+ version: 1,
17
+ values: input.map((item, index) => canonicalValue(item, `${path}[${index}]`)),
18
+ },
19
+ });
20
+ }
21
+ if (typeof input === 'object' && input !== null) {
22
+ if ('$hypequery' in input)
23
+ return validateCanonicalValue(input);
24
+ const prototype = Object.getPrototypeOf(input);
25
+ if (prototype !== Object.prototype && prototype !== null) {
26
+ throw new ProtocolSchemaAdapterError('Default is not portable plain data', path);
27
+ }
28
+ return validateCanonicalValue({
29
+ $hypequery: {
30
+ type: 'map',
31
+ version: 1,
32
+ entries: Object.entries(input)
33
+ .sort(([left], [right]) => left.localeCompare(right))
34
+ .map(([key, value]) => [key, canonicalValue(value, `${path}.${key}`)]),
35
+ },
36
+ });
37
+ }
38
+ return validateCanonicalValue(input);
39
+ }
40
+ catch (error) {
41
+ if (error instanceof ProtocolSchemaAdapterError)
42
+ throw error;
43
+ throw new ProtocolSchemaAdapterError('Default is not a canonical protocol value', path);
44
+ }
45
+ }
46
+ /** Values of a TypeScript enum object, without the reverse mappings of numeric members. */
47
+ export function nativeEnumValues(input) {
48
+ const values = Object.keys(input)
49
+ .filter(key => typeof input[String(input[key])] !== 'number')
50
+ .map(key => input[key]);
51
+ return [...new Set(values)];
52
+ }
53
+ export function annotate(description, result) {
54
+ return typeof description === 'string' ? { ...result, description } : result;
55
+ }
56
+ export function withDefault(inner, value, path) {
57
+ if (inner.kind === 'void') {
58
+ throw new ProtocolSchemaAdapterError('Void schemas cannot carry defaults', path);
59
+ }
60
+ return { ...inner, default: canonicalValue(value, `${path}.default`) };
61
+ }
@@ -0,0 +1,10 @@
1
+ import type { AnyDatasetInstance, DatasetQuery } from '../types.js';
2
+ /**
3
+ * Every field a query reads, as the planner will: requested names, the fields
4
+ * named filters and segments stand for, and, for each selected measure (every
5
+ * base measure when `measures` is omitted), its dependencies and the fields
6
+ * its fixed filters read. A measure filter can name `<relationship>.<field>`,
7
+ * so selecting a measure can join a relationship the request never names.
8
+ */
9
+ export declare function queryFieldReferences(dataset: AnyDatasetInstance, query: DatasetQuery): string[];
10
+ //# sourceMappingURL=query-field-references.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-field-references.d.ts","sourceRoot":"","sources":["../../src/utils/query-field-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAgB,MAAM,aAAa,CAAC;AASlF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,EAAE,CAsB/F"}
@@ -0,0 +1,38 @@
1
+ import { baseMeasureNames, isBaseMeasure } from './dataset-measures.js';
2
+ import { measureDependencies } from './measure-dependencies.js';
3
+ import { segmentFilters } from './segments.js';
4
+ function filterField(dataset, filter) {
5
+ return dataset.filters?.[filter.field]?.field ?? filter.field;
6
+ }
7
+ /**
8
+ * Every field a query reads, as the planner will: requested names, the fields
9
+ * named filters and segments stand for, and, for each selected measure (every
10
+ * base measure when `measures` is omitted), its dependencies and the fields
11
+ * its fixed filters read. A measure filter can name `<relationship>.<field>`,
12
+ * so selecting a measure can join a relationship the request never names.
13
+ */
14
+ export function queryFieldReferences(dataset, query) {
15
+ const names = [
16
+ ...(query.dimensions ?? []),
17
+ ...(query.filters ?? []).map(filter => filterField(dataset, filter)),
18
+ ...segmentFilters(dataset, query.segments).map(filter => filterField(dataset, filter)),
19
+ ...(query.orderBy ?? []).map(order => order.field),
20
+ ...(query.having ?? []).map(condition => condition.measure),
21
+ ];
22
+ const visited = new Set();
23
+ const visit = (name) => {
24
+ if (visited.has(name))
25
+ return;
26
+ visited.add(name);
27
+ names.push(name);
28
+ if (!Object.hasOwn(dataset.measures, name))
29
+ return;
30
+ const definition = dataset.measures[name];
31
+ measureDependencies(definition).forEach(visit);
32
+ if (isBaseMeasure(definition)) {
33
+ names.push(...(definition.filters ?? []).map(filter => filterField(dataset, filter)));
34
+ }
35
+ };
36
+ (query.measures ?? baseMeasureNames(dataset.measures)).forEach(visit);
37
+ return names;
38
+ }