@hypequery/datasets 0.21.0 → 0.23.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 (174) hide show
  1. package/README.md +6 -0
  2. package/dist/api.type-test.js +7 -0
  3. package/dist/cache/protocol-cache-keys.d.ts.map +1 -1
  4. package/dist/cache/protocol-cache-keys.js +5 -0
  5. package/dist/cache/query-signature.d.ts.map +1 -1
  6. package/dist/cache/query-signature.js +9 -0
  7. package/dist/catalog.d.ts +12 -0
  8. package/dist/catalog.d.ts.map +1 -1
  9. package/dist/catalog.js +14 -1
  10. package/dist/constants.d.ts +12 -1
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +24 -0
  13. package/dist/contract.d.ts +4 -1
  14. package/dist/contract.d.ts.map +1 -1
  15. package/dist/contract.js +4 -0
  16. package/dist/dataset-compilation.d.ts +50 -0
  17. package/dist/dataset-compilation.d.ts.map +1 -0
  18. package/dist/dataset-compilation.js +1 -0
  19. package/dist/dataset-query.d.ts +3 -3
  20. package/dist/dataset-query.d.ts.map +1 -1
  21. package/dist/dataset-query.js +7 -71
  22. package/dist/dataset-sql-dialect.d.ts +11 -0
  23. package/dist/dataset-sql-dialect.d.ts.map +1 -0
  24. package/dist/dataset-sql-dialect.js +1 -0
  25. package/dist/dataset.d.ts +3 -2
  26. package/dist/dataset.d.ts.map +1 -1
  27. package/dist/execution-capabilities.d.ts +28 -0
  28. package/dist/execution-capabilities.d.ts.map +1 -0
  29. package/dist/execution-capabilities.js +17 -0
  30. package/dist/execution-capabilities.json +13 -0
  31. package/dist/executor.d.ts +17 -38
  32. package/dist/executor.d.ts.map +1 -1
  33. package/dist/executor.js +40 -400
  34. package/dist/index.d.ts +10 -5
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -3
  37. package/dist/internal.d.ts +3 -0
  38. package/dist/internal.d.ts.map +1 -1
  39. package/dist/internal.js +2 -0
  40. package/dist/measure.d.ts +54 -17
  41. package/dist/measure.d.ts.map +1 -1
  42. package/dist/measure.js +3 -63
  43. package/dist/metric-query-engine.d.ts +29 -0
  44. package/dist/metric-query-engine.d.ts.map +1 -0
  45. package/dist/metric-query-engine.js +106 -0
  46. package/dist/portable-executor.d.ts +20 -6
  47. package/dist/portable-executor.d.ts.map +1 -1
  48. package/dist/portable-executor.js +29 -9
  49. package/dist/protocol-adapter.d.ts.map +1 -1
  50. package/dist/protocol-adapter.js +1 -0
  51. package/dist/protocol-rehydrate.d.ts.map +1 -1
  52. package/dist/protocol-rehydrate.js +9 -2
  53. package/dist/query-builder-protocol.d.ts +11 -0
  54. package/dist/query-builder-protocol.d.ts.map +1 -1
  55. package/dist/query-helpers.d.ts +10 -10
  56. package/dist/query-helpers.d.ts.map +1 -1
  57. package/dist/query-helpers.js +1 -3
  58. package/dist/query-planner.d.ts +7 -3
  59. package/dist/query-planner.d.ts.map +1 -1
  60. package/dist/query-planner.js +10 -12
  61. package/dist/relationship-check.d.ts +3 -2
  62. package/dist/relationship-check.d.ts.map +1 -1
  63. package/dist/relationship-check.js +8 -3
  64. package/dist/relationships.d.ts +6 -16
  65. package/dist/relationships.d.ts.map +1 -1
  66. package/dist/relationships.js +3 -12
  67. package/dist/semantic-planner.d.ts.map +1 -1
  68. package/dist/semantic-planner.js +4 -0
  69. package/dist/semantic-query-schema.d.ts +6 -0
  70. package/dist/semantic-query-schema.d.ts.map +1 -1
  71. package/dist/semantic-query-schema.js +31 -4
  72. package/dist/types.d.ts +67 -11
  73. package/dist/types.d.ts.map +1 -1
  74. package/dist/utils/agent-catalog-projection.d.ts +1 -0
  75. package/dist/utils/agent-catalog-projection.d.ts.map +1 -1
  76. package/dist/utils/agent-catalog-projection.js +2 -1
  77. package/dist/utils/base-measure-definition.d.ts +23 -0
  78. package/dist/utils/base-measure-definition.d.ts.map +1 -0
  79. package/dist/utils/base-measure-definition.js +65 -0
  80. package/dist/utils/build-dataset-query-builder.d.ts +5 -0
  81. package/dist/utils/build-dataset-query-builder.d.ts.map +1 -0
  82. package/dist/utils/build-dataset-query-builder.js +65 -0
  83. package/dist/utils/calendar-shift-sql.d.ts +18 -0
  84. package/dist/utils/calendar-shift-sql.d.ts.map +1 -0
  85. package/dist/utils/calendar-shift-sql.js +21 -0
  86. package/dist/utils/clickhouse-dataset-sql-dialect.d.ts +4 -0
  87. package/dist/utils/clickhouse-dataset-sql-dialect.d.ts.map +1 -0
  88. package/dist/utils/clickhouse-dataset-sql-dialect.js +6 -0
  89. package/dist/utils/compile-dataset-query.d.ts +10 -0
  90. package/dist/utils/compile-dataset-query.d.ts.map +1 -0
  91. package/dist/utils/compile-dataset-query.js +72 -0
  92. package/dist/utils/composite-time-measure-sql.d.ts.map +1 -1
  93. package/dist/utils/composite-time-measure-sql.js +2 -1
  94. package/dist/utils/dataset-definition-validation.d.ts.map +1 -1
  95. package/dist/utils/dataset-definition-validation.js +52 -2
  96. package/dist/utils/dataset-derived-query.d.ts +6 -2
  97. package/dist/utils/dataset-derived-query.d.ts.map +1 -1
  98. package/dist/utils/dataset-derived-query.js +28 -26
  99. package/dist/utils/dataset-filter-types.d.ts +28 -0
  100. package/dist/utils/dataset-filter-types.d.ts.map +1 -0
  101. package/dist/utils/dataset-filter-types.js +1 -0
  102. package/dist/utils/dataset-having.d.ts +21 -0
  103. package/dist/utils/dataset-having.d.ts.map +1 -0
  104. package/dist/utils/dataset-having.js +106 -0
  105. package/dist/utils/dataset-normalization.d.ts.map +1 -1
  106. package/dist/utils/dataset-normalization.js +5 -1
  107. package/dist/utils/dataset-query-validation.d.ts.map +1 -1
  108. package/dist/utils/dataset-query-validation.js +17 -3
  109. package/dist/utils/dataset-sql-dialect.d.ts +5 -0
  110. package/dist/utils/dataset-sql-dialect.d.ts.map +1 -0
  111. package/dist/utils/dataset-sql-dialect.js +5 -0
  112. package/dist/utils/dataset-target.d.ts +3 -0
  113. package/dist/utils/dataset-target.d.ts.map +1 -0
  114. package/dist/utils/dataset-target.js +3 -0
  115. package/dist/utils/filter-definition.d.ts +3 -0
  116. package/dist/utils/filter-definition.d.ts.map +1 -0
  117. package/dist/utils/filter-definition.js +3 -0
  118. package/dist/utils/metric-derived-query.d.ts +8 -0
  119. package/dist/utils/metric-derived-query.d.ts.map +1 -0
  120. package/dist/utils/metric-derived-query.js +106 -0
  121. package/dist/utils/metric-query-builder.d.ts +5 -0
  122. package/dist/utils/metric-query-builder.d.ts.map +1 -0
  123. package/dist/utils/metric-query-builder.js +50 -0
  124. package/dist/utils/metric-query-result.d.ts +10 -0
  125. package/dist/utils/metric-query-result.d.ts.map +1 -0
  126. package/dist/utils/metric-query-result.js +18 -0
  127. package/dist/utils/metric-query-validation.d.ts +6 -0
  128. package/dist/utils/metric-query-validation.d.ts.map +1 -0
  129. package/dist/utils/metric-query-validation.js +116 -0
  130. package/dist/utils/protocol-metric-expressions.d.ts +8 -0
  131. package/dist/utils/protocol-metric-expressions.d.ts.map +1 -1
  132. package/dist/utils/protocol-metric-expressions.js +23 -11
  133. package/dist/utils/relationship-builder-plan.d.ts +6 -2
  134. package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
  135. package/dist/utils/relationship-builder-plan.js +24 -15
  136. package/dist/utils/relationship-definition.d.ts +6 -0
  137. package/dist/utils/relationship-definition.d.ts.map +1 -0
  138. package/dist/utils/relationship-definition.js +9 -0
  139. package/dist/utils/relationship-key-check.d.ts +4 -0
  140. package/dist/utils/relationship-key-check.d.ts.map +1 -1
  141. package/dist/utils/relationship-key-check.js +19 -7
  142. package/dist/utils/relationship-keys.d.ts +17 -0
  143. package/dist/utils/relationship-keys.d.ts.map +1 -0
  144. package/dist/utils/relationship-keys.js +63 -0
  145. package/dist/utils/relationship-measure-sql.d.ts +14 -0
  146. package/dist/utils/relationship-measure-sql.d.ts.map +1 -0
  147. package/dist/utils/relationship-measure-sql.js +44 -0
  148. package/dist/utils/relationship-measure-types.d.ts +36 -0
  149. package/dist/utils/relationship-measure-types.d.ts.map +1 -0
  150. package/dist/utils/relationship-measure-types.js +1 -0
  151. package/dist/utils/relationship-measures.d.ts +23 -0
  152. package/dist/utils/relationship-measures.d.ts.map +1 -0
  153. package/dist/utils/relationship-measures.js +62 -0
  154. package/dist/utils/relationship-validation.d.ts +1 -1
  155. package/dist/utils/relationship-validation.d.ts.map +1 -1
  156. package/dist/utils/relationship-validation.js +3 -5
  157. package/dist/utils/semantic-query-syntax.d.ts +6 -0
  158. package/dist/utils/semantic-query-syntax.d.ts.map +1 -0
  159. package/dist/utils/semantic-query-syntax.js +24 -0
  160. package/dist/utils/shift-measure-sql.d.ts.map +1 -1
  161. package/dist/utils/shift-measure-sql.js +10 -3
  162. package/dist/utils/snapshot-execution-context.d.ts +4 -0
  163. package/dist/utils/snapshot-execution-context.d.ts.map +1 -0
  164. package/dist/utils/snapshot-execution-context.js +16 -0
  165. package/dist/utils/time-measure-context-sql.d.ts.map +1 -1
  166. package/dist/utils/time-measure-context-sql.js +8 -2
  167. package/dist/utils/time-measure-dataset-sql.d.ts.map +1 -1
  168. package/dist/utils/time-measure-dataset-sql.js +3 -2
  169. package/dist/utils/time-measure-result-sql.d.ts.map +1 -1
  170. package/dist/utils/time-measure-result-sql.js +2 -1
  171. package/dist/utils/time-measure-source-sql.d.ts.map +1 -1
  172. package/dist/utils/time-measure-source-sql.js +8 -4
  173. package/dist/validation.js +1 -1
  174. package/package.json +10 -5
@@ -0,0 +1,72 @@
1
+ import { queryMeasureDefinitions } from './relationship-measures.js';
2
+ import { buildDatasetQueryBuilder } from './build-dataset-query-builder.js';
3
+ import { baseMeasureNames } from './dataset-measures.js';
4
+ import { buildDerivedDatasetSql, hasSelectedDerivedMeasure, needsOuterDatasetQuery } from './dataset-derived-query.js';
5
+ import { measureDependencyNames } from './measure-dependencies.js';
6
+ import { overfetchLimit } from './pagination.js';
7
+ import { resolveResultLimit } from './result-limits.js';
8
+ import { buildTimeMeasureDatasetSql } from './time-measure-dataset-sql.js';
9
+ import { selectedTimeMeasures } from './time-query-measures.js';
10
+ /** The one dispatch point for dataset preview and query-builder execution. */
11
+ export function prepareDatasetQuery(dataset, input, options, mode = 'execution') {
12
+ const query = structuredClone({
13
+ ...input,
14
+ limit: mode === 'execution' ? resolveResultLimit(input.limit, dataset.limits).limit : input.limit,
15
+ });
16
+ // Preserve omission while validating/planning: maxMeasures constrains an
17
+ // explicit selection, not the dataset's default set of base measures.
18
+ const measures = query.measures ?? baseMeasureNames(dataset.measures);
19
+ const executionLimit = mode === 'execution' ? overfetchLimit(query.limit) : query.limit;
20
+ const executionOptions = { ...options, executionLimit };
21
+ const preflightStatements = [];
22
+ let statement;
23
+ let execute;
24
+ let plan;
25
+ const abortSignal = options.context?.abortSignal;
26
+ if (selectedTimeMeasures(dataset, query).size) {
27
+ plan = 'time';
28
+ const compiled = buildTimeMeasureDatasetSql(dataset, query, executionOptions);
29
+ statement = compiled;
30
+ preflightStatements.push({ sql: compiled.timeAxisSql, parameters: compiled.parameters });
31
+ execute = async () => {
32
+ await options.builderFactory.rawQuery(compiled.timeAxisSql, compiled.parameters, { abortSignal });
33
+ return options.builderFactory.rawQuery(compiled.sql, compiled.parameters, { abortSignal });
34
+ };
35
+ }
36
+ else if (needsOuterDatasetQuery(dataset, query)) {
37
+ plan = hasSelectedDerivedMeasure(dataset, query) ? 'derived' : 'aggregate';
38
+ statement = buildDerivedDatasetSql(dataset, query, executionOptions, buildDatasetQueryBuilder);
39
+ execute = () => options.builderFactory.rawQuery(statement.sql, statement.parameters, { abortSignal });
40
+ }
41
+ else {
42
+ plan = 'aggregate';
43
+ const builder = buildDatasetQueryBuilder(dataset, query, executionOptions);
44
+ statement = builder.toSQLWithParams();
45
+ // Retain builder execution: custom builders may carry adapter settings.
46
+ execute = () => builder.execute({ abortSignal });
47
+ }
48
+ const description = Object.freeze({
49
+ kind: 'dataset-compilation', version: 1, dataset: dataset.name, plan,
50
+ dimensions: Object.freeze([...(query.dimensions ?? [])]),
51
+ measures: Object.freeze([...measures]),
52
+ measureDependencies: Object.freeze(measureDependencyNames(queryMeasureDefinitions(dataset, measures), measures)),
53
+ filters: Object.freeze((query.filters ?? []).map(({ field, operator }) => Object.freeze({ field, operator }))),
54
+ having: Object.freeze((query.having ?? []).map(({ measure, operator }) => Object.freeze({ measure, operator }))),
55
+ segments: Object.freeze([...(query.segments ?? [])]),
56
+ by: query.by, timezone: query.timezone,
57
+ effectiveLimit: query.limit, executionLimit, offset: query.offset ?? 0,
58
+ parameterCount: statement.parameters.length,
59
+ preflightStatementCount: preflightStatements.length,
60
+ });
61
+ const compilation = Object.freeze({
62
+ sql: statement.sql,
63
+ parameters: Object.freeze(structuredClone(statement.parameters)),
64
+ query: Object.freeze(structuredClone({ ...query, measures })),
65
+ preflightStatements: Object.freeze(preflightStatements.map(preflight => Object.freeze({
66
+ sql: preflight.sql, parameters: Object.freeze(structuredClone(preflight.parameters)),
67
+ }))),
68
+ describe: () => description,
69
+ toJSON: () => description,
70
+ });
71
+ return { compilation, execute };
72
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"composite-time-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/composite-time-measure-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAW9D,mFAAmF;AACnF,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,4BAA4B,EAAE,IAAI,EAAE,eAAe;;;;EAgDrJ"}
1
+ {"version":3,"file":"composite-time-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/composite-time-measure-sql.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAW9D,mFAAmF;AACnF,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,4BAA4B,EAAE,IAAI,EAAE,eAAe;;;;EAgDrJ"}
@@ -1,3 +1,4 @@
1
+ import { queryMeasureDefinitions } from './relationship-measures.js';
1
2
  import { quoteSQLIdentifier } from '../sql-utils.js';
2
3
  import { isDerivedMeasure, isWindowMeasure } from './dataset-measures.js';
3
4
  import { buildMeasureEvaluationGraph, evaluationFormula } from './measure-evaluation-graph.js';
@@ -9,7 +10,7 @@ import { derivedProjection } from './derived-measure-sql.js';
9
10
  import { buildTimeMeasureResultSql } from './time-measure-result-sql.js';
10
11
  /** Aggregate each dependency in its evaluation context, then evaluate formulas. */
11
12
  export function buildCompositeTimeMeasureSql(ds, query, options, axis) {
12
- const graph = buildMeasureEvaluationGraph(ds.measures, query.measures ?? []);
13
+ const graph = buildMeasureEvaluationGraph(queryMeasureDefinitions(ds, query.measures ?? []), query.measures ?? []);
13
14
  const leaves = graph.nodes.filter(node => !isDerivedMeasure(node.definition));
14
15
  const baseNames = new Set(leaves.map(node => isWindowMeasure(node.definition) ? node.definition.measure : node.name));
15
16
  const source = buildTimeMeasureSourceSql(ds, query, baseNames, axis.filters, options);
@@ -1 +1 @@
1
- {"version":3,"file":"dataset-definition-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-definition-validation.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AAOrB,KAAK,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AACzD,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;AAC5D,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AA+M/D;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,aAAa,EAAE,WAAW,EAAE,gBAAgB,CAAC,GAClE,IAAI,CA0DN"}
1
+ {"version":3,"file":"dataset-definition-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-definition-validation.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,wBAAwB,EACxB,mBAAmB,EAEnB,sBAAsB,EAEvB,MAAM,aAAa,CAAC;AAQrB,KAAK,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AACzD,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;AAC5D,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AAwR/D;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,aAAa,EAAE,WAAW,EAAE,gBAAgB,CAAC,GAClE,IAAI,CA2DN"}
@@ -1,5 +1,6 @@
1
1
  import { validateShiftMeasures } from './shift-measure-validation.js';
2
2
  import { escapeRegExp, isSafeSQLIdentifier, stripSqlLiterals } from '../sql-utils.js';
3
+ import { SEMANTIC_FILTER_OPERATORS, isSemanticFilterOperator } from '../constants.js';
3
4
  import { validateDerivedMeasures } from './derived-measure-validation.js';
4
5
  import { isDerivedMeasure, isWindowMeasure, isShiftMeasure } from './dataset-measures.js';
5
6
  import { validateWindowMeasures } from './window-measure-validation.js';
@@ -113,7 +114,54 @@ function validateDimensions(datasetName, dimensions) {
113
114
  assertSafeColumn(datasetName, definition.column ?? name, `dimension "${name}" column`);
114
115
  }
115
116
  }
116
- function validateMeasures(datasetName, measures, dimensions) {
117
+ function assertSemanticOperator(datasetName, operator, context) {
118
+ if (!isSemanticFilterOperator(operator)) {
119
+ fail(datasetName, `${context} uses unsupported operator "${String(operator)}". ` +
120
+ `Supported: ${SEMANTIC_FILTER_OPERATORS.join(', ')}.`);
121
+ }
122
+ }
123
+ /**
124
+ * Validates the declared filter map. A filter's `field` names the dimension it
125
+ * filters on; when it is not a declared dimension it falls through to SQL as a
126
+ * bare column, so it must then be a safe identifier.
127
+ */
128
+ function validateFilters(datasetName, filters, dimensions) {
129
+ for (const [name, definition] of Object.entries(filters ?? {})) {
130
+ // JavaScript callers can omit `field`; never let it stringify to "undefined".
131
+ if (typeof definition.field !== 'string') {
132
+ fail(datasetName, `filter "${name}" must name a field.`);
133
+ }
134
+ if (!Object.hasOwn(dimensions, definition.field)) {
135
+ assertSafeColumn(datasetName, definition.field, `filter "${name}" field`);
136
+ }
137
+ for (const operator of definition.operators ?? []) {
138
+ assertSemanticOperator(datasetName, operator, `filter "${name}"`);
139
+ }
140
+ }
141
+ }
142
+ /**
143
+ * Validates a measure's fixed filters. These are rendered straight into the
144
+ * aggregate's `if(...)` condition, so the field must resolve to a declared
145
+ * dimension or filter, or else be a safe (optionally relationship-qualified)
146
+ * identifier, and the operator must be one the renderer knows.
147
+ */
148
+ function validateMeasureFilters(datasetName, measureName, filters, dimensions, declaredFilters) {
149
+ for (const filter of filters) {
150
+ const context = `measure "${measureName}" filter on "${String(filter.field)}"`;
151
+ if (typeof filter.field !== 'string') {
152
+ fail(datasetName, `${context} must name a field.`);
153
+ }
154
+ const declared = Object.hasOwn(dimensions, filter.field)
155
+ || (declaredFilters !== undefined && Object.hasOwn(declaredFilters, filter.field));
156
+ if (!declared && !isSafeQualifiedName(filter.field, 2)) {
157
+ fail(datasetName, `${context} is not a declared dimension or a safe column identifier. Measure filter ` +
158
+ 'fields are interpolated into SQL, so they must contain only letters, numbers and ' +
159
+ 'underscores (optionally "<relationship>.<field>").');
160
+ }
161
+ assertSemanticOperator(datasetName, filter.operator, context);
162
+ }
163
+ }
164
+ function validateMeasures(datasetName, measures, dimensions, declaredFilters) {
117
165
  for (const [name, definition] of Object.entries(measures)) {
118
166
  assertSafeName(datasetName, 'measure', name);
119
167
  if (typeof definition !== 'object' || definition === null) {
@@ -127,6 +175,7 @@ function validateMeasures(datasetName, measures, dimensions) {
127
175
  if (definition.sql !== undefined) {
128
176
  validateRawSql(datasetName, 'measure', name, definition.sql, definition.dependencies);
129
177
  }
178
+ validateMeasureFilters(datasetName, name, definition.filters ?? [], dimensions, declaredFilters);
130
179
  // `field` and `argField` name either a declared dimension or a physical
131
180
  // column that the model deliberately does not expose (the `allowHiddenField`
132
181
  // case in metric validation). Either way the value reaches SQL, so a name
@@ -199,11 +248,12 @@ export function validateDatasetDefinition(name, config) {
199
248
  // measure-only model is a legitimate thing to define.
200
249
  const dimensions = config.dimensions ?? {};
201
250
  validateDimensions(name, dimensions);
251
+ validateFilters(name, config.filters, dimensions);
202
252
  const measures = config.measures ?? {};
203
253
  validateWindowMeasures(name, config.timeKey, measures);
204
254
  validateShiftMeasures(name, config.timeKey, measures);
205
255
  validateDerivedMeasures(name, measures);
206
- validateMeasures(name, measures, dimensions);
256
+ validateMeasures(name, measures, dimensions, config.filters);
207
257
  validateLimits(name, config.limits);
208
258
  validateDatasetAgentMetadata(name, config);
209
259
  }
@@ -1,12 +1,16 @@
1
- import type { AnyDatasetInstance, DatasetQuery, DatasetQueryResult } from '../types.js';
1
+ import type { AnyDatasetInstance, DatasetQuery } from '../types.js';
2
2
  import type { QueryBuilderLike } from '../query-builder-protocol.js';
3
3
  import type { DatasetQueryExecutionOptions } from '../dataset-query.js';
4
4
  type BuildBaseQuery = (dataset: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions) => QueryBuilderLike;
5
5
  export declare function hasSelectedDerivedMeasure(ds: AnyDatasetInstance, query: DatasetQuery): boolean;
6
+ /**
7
+ * True when the query needs an outer query over the grouped result: derived
8
+ * measures are computed there, and `having` conditions filter there.
9
+ */
10
+ export declare function needsOuterDatasetQuery(ds: AnyDatasetInstance, query: DatasetQuery): boolean;
6
11
  export declare function buildDerivedDatasetSql(ds: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions, buildBaseQuery: BuildBaseQuery): {
7
12
  sql: string;
8
13
  parameters: unknown[];
9
14
  };
10
- export declare function runDerivedDatasetQuery(ds: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions, buildBaseQuery: BuildBaseQuery): Promise<DatasetQueryResult>;
11
15
  export {};
12
16
  //# sourceMappingURL=dataset-derived-query.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dataset-derived-query.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-derived-query.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAKrE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAExE,KAAK,cAAc,GAAG,CACpB,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,KAClC,gBAAgB,CAAC;AAEtB,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAE9F;AAED,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,EACrC,cAAc,EAAE,cAAc,GAC7B;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,EAAE,CAAA;CAAE,CA2CxC;AAED,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,EACrC,cAAc,EAAE,cAAc,GAC7B,OAAO,CAAC,kBAAkB,CAAC,CAgB7B"}
1
+ {"version":3,"file":"dataset-derived-query.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-derived-query.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAGxE,KAAK,cAAc,GAAG,CACpB,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,KAClC,gBAAgB,CAAC;AAEtB,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAE9F;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAE3F;AAED,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,EACrC,cAAc,EAAE,cAAc,GAC7B;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,EAAE,CAAA;CAAE,CAqDxC"}
@@ -1,27 +1,37 @@
1
+ import { queryMeasureDefinitions } from './relationship-measures.js';
2
+ import { semanticOrderDirection } from './semantic-query-syntax.js';
1
3
  import { measureDependencyNames } from './measure-dependencies.js';
2
4
  import { derivedExpressionSql } from './derived-measure-sql.js';
3
5
  import { baseMeasureNames, getBaseMeasure, getDerivedMeasure } from './dataset-measures.js';
4
- import { quoteSQLIdentifier } from '../sql-utils.js';
6
+ import { resolveDatasetSqlDialect } from './dataset-sql-dialect.js';
5
7
  import { validateDatasetQueryInput } from './dataset-query-validation.js';
6
- import { overfetchLimit } from './pagination.js';
7
- import { toDatasetQueryResult } from './dataset-query-result.js';
8
+ import { datasetHavingSql } from './dataset-having.js';
8
9
  export function hasSelectedDerivedMeasure(ds, query) {
9
10
  return (query.measures ?? []).some(name => getDerivedMeasure(ds.measures, name) !== undefined);
10
11
  }
12
+ /**
13
+ * True when the query needs an outer query over the grouped result: derived
14
+ * measures are computed there, and `having` conditions filter there.
15
+ */
16
+ export function needsOuterDatasetQuery(ds, query) {
17
+ return hasSelectedDerivedMeasure(ds, query) || (query.having?.length ?? 0) > 0;
18
+ }
11
19
  export function buildDerivedDatasetSql(ds, query, options, buildBaseQuery) {
12
20
  const validation = validateDatasetQueryInput(ds, query, options.context);
13
21
  if (!validation.valid) {
14
22
  throw new Error(`Invalid dataset query: ${validation.errors.join('; ')}`);
15
23
  }
24
+ const dialect = resolveDatasetSqlDialect(options.builderFactory);
16
25
  const selected = query.measures ?? baseMeasureNames(ds.measures);
17
- const baseMeasures = measureDependencyNames(ds.measures, selected).filter(name => getBaseMeasure(ds.measures, name));
26
+ const baseMeasures = measureDependencyNames(queryMeasureDefinitions(ds, selected), selected).filter(name => name.includes('.') || getBaseMeasure(ds.measures, name));
18
27
  const resolve = (name) => {
19
28
  const derived = getDerivedMeasure(ds.measures, name);
20
- return derived ? `(${derivedExpressionSql(derived, resolve)})` : quoteSQLIdentifier(name);
29
+ return derived ? `(${derivedExpressionSql(derived, resolve)})` : dialect.quoteIdentifier(name);
21
30
  };
22
31
  const inner = buildBaseQuery(ds, {
23
32
  ...query,
24
33
  measures: [...baseMeasures],
34
+ having: undefined,
25
35
  orderBy: undefined,
26
36
  limit: undefined,
27
37
  offset: undefined,
@@ -29,21 +39,28 @@ export function buildDerivedDatasetSql(ds, query, options, buildBaseQuery) {
29
39
  const { sql: innerSql, parameters } = inner.toSQLWithParams();
30
40
  const projections = [];
31
41
  if (query.by)
32
- projections.push(quoteSQLIdentifier('period'));
42
+ projections.push(dialect.quoteIdentifier('period'));
33
43
  for (const dimension of query.dimensions ?? [])
34
- projections.push(quoteSQLIdentifier(dimension));
44
+ projections.push(dialect.quoteIdentifier(dimension));
35
45
  for (const name of selected) {
36
46
  const derived = getDerivedMeasure(ds.measures, name);
37
47
  projections.push(derived
38
- ? `${derivedExpressionSql(derived, resolve)} AS ${quoteSQLIdentifier(name)}`
39
- : quoteSQLIdentifier(name));
48
+ ? `${derivedExpressionSql(derived, resolve)} AS ${dialect.quoteIdentifier(name)}`
49
+ : dialect.quoteIdentifier(name));
40
50
  }
41
51
  let sql = `WITH base AS (${innerSql}) SELECT ${projections.join(', ')} FROM base`;
52
+ // Conditions read the same expression the projection does, never the output
53
+ // alias, so the predicate stays valid on dialects without alias resolution.
54
+ if (query.having?.length) {
55
+ const having = datasetHavingSql(query.having, resolve);
56
+ sql += ` WHERE ${having.sql}`;
57
+ parameters.push(...having.parameters);
58
+ }
42
59
  if (query.orderBy?.length) {
43
- sql += ` ORDER BY ${query.orderBy.map(order => (`${quoteSQLIdentifier(order.field)} ${order.direction === 'asc' ? 'ASC' : 'DESC'}`)).join(', ')}`;
60
+ sql += ` ORDER BY ${query.orderBy.map(order => (`${dialect.quoteIdentifier(order.field)} ${semanticOrderDirection(order.direction)}`)).join(', ')}`;
44
61
  }
45
62
  else if (query.by) {
46
- sql += ` ORDER BY ${quoteSQLIdentifier('period')} ASC`;
63
+ sql += ` ORDER BY ${dialect.quoteIdentifier('period')} ASC`;
47
64
  }
48
65
  const limit = options.executionLimit ?? query.limit;
49
66
  if (limit !== undefined)
@@ -52,18 +69,3 @@ export function buildDerivedDatasetSql(ds, query, options, buildBaseQuery) {
52
69
  sql += ` OFFSET ${query.offset}`;
53
70
  return { sql, parameters };
54
71
  }
55
- export async function runDerivedDatasetQuery(ds, query, options, buildBaseQuery) {
56
- const start = Date.now();
57
- const { sql, parameters } = buildDerivedDatasetSql(ds, query, {
58
- ...options,
59
- executionLimit: overfetchLimit(query.limit),
60
- }, buildBaseQuery);
61
- const rows = await options.builderFactory.rawQuery(sql, parameters, { abortSignal: options.context?.abortSignal });
62
- return toDatasetQueryResult(rows, {
63
- dataset: ds,
64
- query,
65
- sql,
66
- timingMs: Date.now() - start,
67
- context: options.context,
68
- });
69
- }
@@ -0,0 +1,28 @@
1
+ import type { DatasetInstance, DimensionDefinition, MetricFilter, MetricFilterOperator, RelationshipDefinition, SemanticFilterDefinition, SemanticFiltersDefinition } from '../types.js';
2
+ /** The generated allowlist mirrors normalizeFilters; explicit maps replace it. */
3
+ export type NormalizedDatasetFilters<TDimensions extends Record<string, DimensionDefinition>, TFilters extends SemanticFiltersDefinition | undefined> = TFilters extends SemanticFiltersDefinition ? TFilters : {
4
+ [K in keyof TDimensions as TDimensions[K] extends {
5
+ filterable: false;
6
+ } ? never : K]: SemanticFilterDefinition & {
7
+ field: K & string;
8
+ };
9
+ };
10
+ type FilterOperators<TDefinition> = TDefinition extends {
11
+ operators: readonly (infer TOperator extends MetricFilterOperator)[];
12
+ } ? TOperator : MetricFilterOperator;
13
+ type LocalFilterFor<TFilters extends SemanticFiltersDefinition> = {
14
+ [K in keyof TFilters & string]: K extends `${string}.${string}` ? never : MetricFilter<K, unknown, FilterOperators<TFilters[K]>>;
15
+ }[keyof TFilters & string];
16
+ type RelationshipFilterFor<TRelationships extends Record<string, RelationshipDefinition>> = {
17
+ [R in keyof TRelationships & string]: TRelationships[R] extends RelationshipDefinition<infer TTarget, infer TKind> ? TKind extends 'hasMany' ? never : TTarget extends DatasetInstance<any, any, any, any> ? {
18
+ [K in keyof TTarget['filters'] & keyof TTarget['dimensions'] & string]: TTarget['dimensions'][K] extends {
19
+ sql: string;
20
+ } ? never : K extends TTarget['filters'][K]['field'] ? MetricFilter<`${R}.${K}`, unknown, FilterOperators<TTarget['filters'][K]>> : never;
21
+ }[keyof TTarget['filters'] & keyof TTarget['dimensions'] & string] : never : never;
22
+ }[keyof TRelationships & string];
23
+ /** Filter names and operator policies exposed by a dataset and its to-one targets. */
24
+ export type DatasetFilterFor<TDataset extends DatasetInstance<any, any, any, any>> = LocalFilterFor<TDataset['filters']> | RelationshipFilterFor<TDataset['relationships']>;
25
+ /** Names in the normalized filter allowlist, including one-hop relationship filters. */
26
+ export type DatasetFilterNames<TDataset extends DatasetInstance<any, any, any, any>> = DatasetFilterFor<TDataset>['field'];
27
+ export {};
28
+ //# sourceMappingURL=dataset-filter-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset-filter-types.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-filter-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACpB,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB,kFAAkF;AAClF,MAAM,MAAM,wBAAwB,CAClC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EACvD,QAAQ,SAAS,yBAAyB,GAAG,SAAS,IACpD,QAAQ,SAAS,yBAAyB,GAAG,QAAQ,GAAG;KACzD,CAAC,IAAI,MAAM,WAAW,IAAI,WAAW,CAAC,CAAC,CAAC,SAAS;QAAE,UAAU,EAAE,KAAK,CAAA;KAAE,GAAG,KAAK,GAAG,CAAC,GAAG,wBAAwB,GAAG;QAC/G,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC;KACnB;CACF,CAAC;AAEF,KAAK,eAAe,CAAC,WAAW,IAAI,WAAW,SAAS;IACtD,SAAS,EAAE,SAAS,CAAC,MAAM,SAAS,SAAS,oBAAoB,CAAC,EAAE,CAAC;CACtE,GAAG,SAAS,GAAG,oBAAoB,CAAC;AAErC,KAAK,cAAc,CAAC,QAAQ,SAAS,yBAAyB,IAAI;KAC/D,CAAC,IAAI,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,SAAS,GAAG,MAAM,IAAI,MAAM,EAAE,GAC3D,KAAK,GACL,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;CAC3D,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC;AAE3B,KAAK,qBAAqB,CAAC,cAAc,SAAS,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,IAAI;KACzF,CAAC,IAAI,MAAM,cAAc,GAAG,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,SAAS,sBAAsB,CAAC,MAAM,OAAO,EAAE,MAAM,KAAK,CAAC,GAC9G,KAAK,SAAS,SAAS,GAAG,KAAK,GAC7B,OAAO,SAAS,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GACjD;SACC,CAAC,IAAI,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,MAAM,GACnE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,GAAG,KAAK,GACpD,CAAC,SAAS,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GACtC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAC1E,KAAK;KACd,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,MAAM,CAAC,GAChE,KAAK,GACT,KAAK;CACV,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,CAAC;AAEjC,sFAAsF;AACtF,MAAM,MAAM,gBAAgB,CAAC,QAAQ,SAAS,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAC7E,cAAc,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,GACnC,qBAAqB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,CAAC;AAErD,wFAAwF;AACxF,MAAM,MAAM,kBAAkB,CAAC,QAAQ,SAAS,eAAe,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IACjF,gBAAgB,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import type { AnyDatasetInstance, DatasetHavingCondition, DatasetQuery } from '../types.js';
2
+ /**
3
+ * Validates `having` conditions against the query they belong to.
4
+ *
5
+ * A condition may only reference a measure the query selects: that is the
6
+ * value the condition reads, and it keeps every rendered reference an output
7
+ * alias rather than an expression taken from input.
8
+ */
9
+ export declare function datasetHavingErrors(ds: AnyDatasetInstance, query: DatasetQuery, selectedMeasures: readonly string[]): string[];
10
+ /**
11
+ * Renders validated `having` conditions as one parameterized predicate.
12
+ *
13
+ * `measureSql` maps a selected measure to the expression that reads its
14
+ * aggregated value in the outer query. Values are only ever bound as `?`
15
+ * parameters, appended in placeholder order.
16
+ */
17
+ export declare function datasetHavingSql(conditions: readonly DatasetHavingCondition[], measureSql: (measure: string) => string): {
18
+ sql: string;
19
+ parameters: unknown[];
20
+ };
21
+ //# sourceMappingURL=dataset-having.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset-having.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-having.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAmC5F;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,gBAAgB,EAAE,SAAS,MAAM,EAAE,GAClC,MAAM,EAAE,CA0CV;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,SAAS,sBAAsB,EAAE,EAC7C,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GACtC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,EAAE,CAAA;CAAE,CAmBxC"}
@@ -0,0 +1,106 @@
1
+ import { SEMANTIC_HAVING_OPERATORS } from '../constants.js';
2
+ import { selectedTimeMeasures } from './time-query-measures.js';
3
+ const HAVING_OPERATORS = new Set(SEMANTIC_HAVING_OPERATORS);
4
+ const COMPARISONS = {
5
+ eq: '=',
6
+ neq: '!=',
7
+ gt: '>',
8
+ gte: '>=',
9
+ lt: '<',
10
+ lte: '<=',
11
+ };
12
+ function isFiniteNumber(value) {
13
+ return typeof value === 'number' && Number.isFinite(value);
14
+ }
15
+ function conditionValueError(condition) {
16
+ const { measure, operator, value } = condition;
17
+ if (operator === 'between') {
18
+ return Array.isArray(value) && value.length === 2 && value.every(isFiniteNumber)
19
+ ? undefined
20
+ : `Having "between" on "${measure}" expects a two-item array of finite numbers.`;
21
+ }
22
+ if (operator === 'in' || operator === 'notIn') {
23
+ return Array.isArray(value) && value.length > 0 && value.every(isFiniteNumber)
24
+ ? undefined
25
+ : `Having "${operator}" on "${measure}" expects a non-empty array of finite numbers.`;
26
+ }
27
+ return isFiniteNumber(value)
28
+ ? undefined
29
+ : `Having "${operator}" on "${measure}" expects a finite number.`;
30
+ }
31
+ /**
32
+ * Validates `having` conditions against the query they belong to.
33
+ *
34
+ * A condition may only reference a measure the query selects: that is the
35
+ * value the condition reads, and it keeps every rendered reference an output
36
+ * alias rather than an expression taken from input.
37
+ */
38
+ export function datasetHavingErrors(ds, query, selectedMeasures) {
39
+ if (query.having === undefined)
40
+ return [];
41
+ if (!Array.isArray(query.having))
42
+ return ['Having must be an array of conditions.'];
43
+ if (query.having.length === 0)
44
+ return [];
45
+ const errors = [];
46
+ if (selectedTimeMeasures(ds, query).size > 0) {
47
+ errors.push('Having is not supported on queries that select window or shift measures.');
48
+ }
49
+ // `maxFilters` bounds every predicate the query adds, before and after
50
+ // aggregation alike. Filters alone over the limit are reported separately.
51
+ const maxFilters = ds.limits?.maxFilters;
52
+ const filterCount = Array.isArray(query.filters) ? query.filters.length : 0;
53
+ const conditionCount = filterCount + query.having.length;
54
+ if (maxFilters && conditionCount > maxFilters && filterCount <= maxFilters) {
55
+ errors.push(`Too many filters and having conditions: ${conditionCount} (max ${maxFilters})`);
56
+ }
57
+ const selected = new Set(selectedMeasures);
58
+ for (const condition of query.having) {
59
+ if (typeof condition !== 'object' || condition === null) {
60
+ errors.push('Each having condition must be an object with measure, operator and value.');
61
+ continue;
62
+ }
63
+ const { measure, operator } = condition;
64
+ if (typeof measure !== 'string' || !selected.has(measure)) {
65
+ errors.push(`Having measure "${String(measure)}" must be one of the selected measures: ${[...selected].join(', ')}`);
66
+ continue;
67
+ }
68
+ if (typeof operator !== 'string' || !HAVING_OPERATORS.has(operator)) {
69
+ errors.push(`Unsupported having operator "${String(operator)}" on "${measure}". ` +
70
+ `Supported: ${SEMANTIC_HAVING_OPERATORS.join(', ')}`);
71
+ continue;
72
+ }
73
+ const valueError = conditionValueError(condition);
74
+ if (valueError)
75
+ errors.push(valueError);
76
+ }
77
+ return errors;
78
+ }
79
+ /**
80
+ * Renders validated `having` conditions as one parameterized predicate.
81
+ *
82
+ * `measureSql` maps a selected measure to the expression that reads its
83
+ * aggregated value in the outer query. Values are only ever bound as `?`
84
+ * parameters, appended in placeholder order.
85
+ */
86
+ export function datasetHavingSql(conditions, measureSql) {
87
+ const parameters = [];
88
+ const predicates = conditions.map((condition) => {
89
+ const column = measureSql(condition.measure);
90
+ if (condition.operator === 'between') {
91
+ parameters.push(condition.value[0], condition.value[1]);
92
+ return `${column} BETWEEN ? AND ?`;
93
+ }
94
+ if (condition.operator === 'in' || condition.operator === 'notIn') {
95
+ parameters.push(...condition.value);
96
+ const placeholders = condition.value.map(() => '?').join(', ');
97
+ return `${column} ${condition.operator === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;
98
+ }
99
+ const comparison = COMPARISONS[condition.operator];
100
+ if (!comparison)
101
+ throw new Error(`Unsupported having operator "${String(condition.operator)}".`);
102
+ parameters.push(condition.value);
103
+ return `${column} ${comparison} ?`;
104
+ });
105
+ return { sql: predicates.join(' AND '), parameters };
106
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"dataset-normalization.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-normalization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,wBAAwB,EACxB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAErB,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;AAC5D,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AAE/D,wBAAgB,mBAAmB,CAAC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EACzF,MAAM,EAAE,aAAa,CAAC,WAAW,EAAE,WAAW,EAAE,gBAAgB,CAAC,GAChE,WAAW,CAEb;AAED,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC/C,OAAO,CAAC,EAAE,yBAAyB,GAClC,yBAAyB,CAgB3B;AAED,wBAAgB,iBAAiB,CAAC,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,EAC1F,QAAQ,EAAE,SAAS,GAAG,SAAS,GAC9B,SAAS,CAEX;AAED,wBAAgB,sBAAsB,CAAC,cAAc,SAAS,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAClG,aAAa,EAAE,cAAc,GAAG,SAAS,EACzC,MAAM,EAAE,MAAM,GACb,cAAc,CAgBhB;AAED,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,GAC5B,eAAe,CAcjB"}
1
+ {"version":3,"file":"dataset-normalization.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-normalization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,wBAAwB,EACxB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,yBAAyB,EAC1B,MAAM,aAAa,CAAC;AAGrB,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;AAC5D,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AAE/D,wBAAgB,mBAAmB,CAAC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EACzF,MAAM,EAAE,aAAa,CAAC,WAAW,EAAE,WAAW,EAAE,gBAAgB,CAAC,GAChE,WAAW,CAEb;AAED,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EAC/C,OAAO,CAAC,EAAE,yBAAyB,GAClC,yBAAyB,CAgB3B;AAED,wBAAgB,iBAAiB,CAAC,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,EAC1F,QAAQ,EAAE,SAAS,GAAG,SAAS,GAC9B,SAAS,CAEX;AAED,wBAAgB,sBAAsB,CAAC,cAAc,SAAS,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAClG,aAAa,EAAE,cAAc,GAAG,SAAS,EACzC,MAAM,EAAE,MAAM,GACb,cAAc,CAkBhB;AAED,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,iBAAiB,GAC5B,eAAe,CAcjB"}
@@ -1,3 +1,4 @@
1
+ import { relationshipKeys } from './relationship-keys.js';
1
2
  export function normalizeDimensions(config) {
2
3
  return config.dimensions;
3
4
  }
@@ -19,7 +20,10 @@ export function normalizeMeasures(measures) {
19
20
  return (measures ?? {});
20
21
  }
21
22
  export function normalizeRelationships(relationships, source) {
22
- for (const name of Object.keys(relationships ?? {})) {
23
+ for (const [name, relationship] of Object.entries(relationships ?? {})) {
24
+ // Hand-built definitions skip the helpers, so check composite keys here.
25
+ if (relationship.keys)
26
+ relationshipKeys(relationship);
23
27
  if (name === source) {
24
28
  throw new Error(`Invalid relationship "${name}": the name matches the dataset source table, so the join ` +
25
29
  'alias would shadow the base table and produce ambiguous SQL. Rename the relationship.');
@@ -1 +1 @@
1
- {"version":3,"file":"dataset-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-query-validation.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAiB9E,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAmKlB"}
1
+ {"version":3,"file":"dataset-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-query-validation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAkB9E,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAyKlB"}
@@ -1,3 +1,5 @@
1
+ import { resolveRelationshipMeasure } from './relationship-measures.js';
2
+ import { semanticQuerySyntaxErrors } from './semantic-query-syntax.js';
1
3
  import { queryTimezoneErrors } from './query-timezone.js';
2
4
  import { baseMeasureNames } from './dataset-measures.js';
3
5
  import { validateFilterValue } from '../validation.js';
@@ -7,12 +9,18 @@ import { isQualifiedField, resolveQualifiedField, } from './relationship-fields.
7
9
  import { validateQualifiedFilter, validateRelationshipTenantRuntime, } from './relationship-validation.js';
8
10
  import { segmentSelectionErrors } from './segments.js';
9
11
  import { analyzeTimeMeasureAxis } from './time-measure-axis.js';
12
+ import { datasetHavingErrors } from './dataset-having.js';
10
13
  export function validateDatasetQueryInput(ds, query, context) {
11
- const errors = queryTimezoneErrors(query.timezone);
14
+ const errors = [...semanticQuerySyntaxErrors(query), ...queryTimezoneErrors(query.timezone)];
12
15
  const dimensionNames = Object.keys(ds.dimensions);
13
16
  const measureNames = Object.keys(ds.measures);
14
17
  const selectedDimensions = query.dimensions ?? [];
15
18
  const selectedMeasures = query.measures ?? baseMeasureNames(ds.measures);
19
+ for (const name of selectedDimensions) {
20
+ if (selectedMeasures.includes(name)) {
21
+ errors.push(`Output "${name}" cannot be selected as both a dimension and a measure. Select one or give the definitions distinct names.`);
22
+ }
23
+ }
16
24
  const filterNames = Object.keys(ds.filters);
17
25
  const orderableFields = new Set([
18
26
  ...selectedDimensions,
@@ -47,7 +55,12 @@ export function validateDatasetQueryInput(ds, query, context) {
47
55
  if (query.measures) {
48
56
  for (const measure of query.measures) {
49
57
  if (isQualifiedField(measure)) {
50
- errors.push(`Measure "${measure}" is relationship-qualified. Measures can only be defined on the base dataset "${ds.name}", not traversed through relationships.`);
58
+ try {
59
+ resolveRelationshipMeasure(ds, measure);
60
+ }
61
+ catch (error) {
62
+ errors.push(error.message);
63
+ }
51
64
  continue;
52
65
  }
53
66
  if (!measureNames.includes(measure)) {
@@ -95,7 +108,7 @@ export function validateDatasetQueryInput(ds, query, context) {
95
108
  if (query.orderBy) {
96
109
  const invalid = [];
97
110
  for (const order of query.orderBy) {
98
- if (isQualifiedField(order.field)) {
111
+ if (isQualifiedField(order.field) && !selectedMeasures.includes(order.field)) {
99
112
  const resolution = resolveQualifiedField(ds, order.field);
100
113
  if (resolution?.error) {
101
114
  errors.push(resolution.error);
@@ -135,6 +148,7 @@ export function validateDatasetQueryInput(ds, query, context) {
135
148
  if (ds.limits?.maxMeasures && query.measures && query.measures.length > ds.limits.maxMeasures) {
136
149
  errors.push(`Too many measures: ${query.measures.length} (max ${ds.limits.maxMeasures})`);
137
150
  }
151
+ errors.push(...datasetHavingErrors(ds, query, selectedMeasures));
138
152
  if (ds.limits?.maxFilters && query.filters && query.filters.length > ds.limits.maxFilters) {
139
153
  errors.push(`Too many filters: ${query.filters.length} (max ${ds.limits.maxFilters})`);
140
154
  }
@@ -0,0 +1,5 @@
1
+ import type { DatasetSqlDialect } from '../dataset-sql-dialect.js';
2
+ import type { QueryBuilderFactoryInput } from '../query-builder-protocol.js';
3
+ /** Resolve from the active factory, so a runtime builder override carries its dialect. */
4
+ export declare function resolveDatasetSqlDialect(factory: QueryBuilderFactoryInput): DatasetSqlDialect;
5
+ //# sourceMappingURL=dataset-sql-dialect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset-sql-dialect.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-sql-dialect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAG7E,0FAA0F;AAC1F,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CAE7F"}
@@ -0,0 +1,5 @@
1
+ import { clickhouseDatasetSqlDialect } from './clickhouse-dataset-sql-dialect.js';
2
+ /** Resolve from the active factory, so a runtime builder override carries its dialect. */
3
+ export function resolveDatasetSqlDialect(factory) {
4
+ return factory.datasetSqlDialect ?? clickhouseDatasetSqlDialect;
5
+ }
@@ -0,0 +1,3 @@
1
+ import type { AnyDatasetInstance } from '../types.js';
2
+ export declare function isDatasetInstance(target: unknown): target is AnyDatasetInstance;
3
+ //# sourceMappingURL=dataset-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset-target.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-target.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,IAAI,kBAAkB,CAE/E"}
@@ -0,0 +1,3 @@
1
+ export function isDatasetInstance(target) {
2
+ return !!target && typeof target === 'object' && '__type' in target && target.__type === 'dataset';
3
+ }
@@ -0,0 +1,3 @@
1
+ import type { MetricFilter, MetricFilterOperator } from '../types.js';
2
+ export declare function createFilter<const TField extends string, const TValue, TOperator extends MetricFilterOperator>(field: TField, operator: TOperator, value: TValue): MetricFilter<TField, TValue, TOperator>;
3
+ //# sourceMappingURL=filter-definition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter-definition.d.ts","sourceRoot":"","sources":["../../src/utils/filter-definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEtE,wBAAgB,YAAY,CAC1B,KAAK,CAAC,MAAM,SAAS,MAAM,EAC3B,KAAK,CAAC,MAAM,EACZ,SAAS,SAAS,oBAAoB,EACtC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAE5F"}
@@ -0,0 +1,3 @@
1
+ export function createFilter(field, operator, value) {
2
+ return { field, operator, value };
3
+ }
@@ -0,0 +1,8 @@
1
+ import type { MetricRef, MetricQuery, ExecutionContext, TimeGrain } from '../types.js';
2
+ import type { QueryBuilderFactoryLike } from '../query-builder-protocol.js';
3
+ 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
+ };
8
+ //# sourceMappingURL=metric-derived-query.d.ts.map
@@ -0,0 +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"}