@hypequery/datasets 0.23.1 → 0.25.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 (255) 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.d.ts +9 -6
  6. package/dist/cache/definition-identity.d.ts.map +1 -1
  7. package/dist/cache/definition-identity.js +14 -8
  8. package/dist/cache/protocol-cache-keys.d.ts.map +1 -1
  9. package/dist/cache/protocol-cache-keys.js +29 -6
  10. package/dist/cache/query-signature.d.ts.map +1 -1
  11. package/dist/cache/query-signature.js +12 -1
  12. package/dist/catalog.d.ts +7 -0
  13. package/dist/catalog.d.ts.map +1 -1
  14. package/dist/catalog.js +5 -2
  15. package/dist/client-contract/query-schema.d.ts +19 -0
  16. package/dist/client-contract/query-schema.d.ts.map +1 -0
  17. package/dist/client-contract/query-schema.js +85 -0
  18. package/dist/constants.d.ts +5 -4
  19. package/dist/constants.d.ts.map +1 -1
  20. package/dist/constants.js +7 -6
  21. package/dist/contract.d.ts +5 -0
  22. package/dist/contract.d.ts.map +1 -1
  23. package/dist/contract.js +6 -3
  24. package/dist/dataset-compilation.d.ts +2 -0
  25. package/dist/dataset-compilation.d.ts.map +1 -1
  26. package/dist/dataset-sql-dialect.d.ts +119 -0
  27. package/dist/dataset-sql-dialect.d.ts.map +1 -1
  28. package/dist/dataset.d.ts +4 -2
  29. package/dist/dataset.d.ts.map +1 -1
  30. package/dist/dataset.js +23 -31
  31. package/dist/in-memory-backend.d.ts.map +1 -1
  32. package/dist/in-memory-backend.js +2 -0
  33. package/dist/index.d.ts +49 -6
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +34 -6
  36. package/dist/internal.d.ts +5 -1
  37. package/dist/internal.d.ts.map +1 -1
  38. package/dist/internal.js +5 -1
  39. package/dist/measure.d.ts +9 -1
  40. package/dist/measure.d.ts.map +1 -1
  41. package/dist/measure.js +9 -1
  42. package/dist/metric-query-engine.d.ts +0 -1
  43. package/dist/metric-query-engine.d.ts.map +1 -1
  44. package/dist/metric-query-engine.js +11 -3
  45. package/dist/protocol-adapter.d.ts.map +1 -1
  46. package/dist/protocol-adapter.js +2 -3
  47. package/dist/protocol-deployment-adapter.d.ts.map +1 -1
  48. package/dist/protocol-deployment-adapter.js +7 -1
  49. package/dist/protocol-rehydrate.d.ts.map +1 -1
  50. package/dist/protocol-rehydrate.js +1 -0
  51. package/dist/protocol-schema-adapter.d.ts +7 -7
  52. package/dist/protocol-schema-adapter.d.ts.map +1 -1
  53. package/dist/protocol-schema-adapter.js +12 -245
  54. package/dist/query-builder-protocol.d.ts +34 -1
  55. package/dist/query-builder-protocol.d.ts.map +1 -1
  56. package/dist/query-planner.d.ts +14 -5
  57. package/dist/query-planner.d.ts.map +1 -1
  58. package/dist/query-planner.js +53 -34
  59. package/dist/relationship-check.d.ts.map +1 -1
  60. package/dist/relationship-check.js +8 -8
  61. package/dist/schemas.d.ts +17 -0
  62. package/dist/schemas.d.ts.map +1 -0
  63. package/dist/schemas.js +16 -0
  64. package/dist/semantic-planner.d.ts.map +1 -1
  65. package/dist/semantic-planner.js +49 -13
  66. package/dist/semantic-query-schema.d.ts +8 -1
  67. package/dist/semantic-query-schema.d.ts.map +1 -1
  68. package/dist/semantic-query-schema.js +18 -12
  69. package/dist/tools.d.ts.map +1 -1
  70. package/dist/tools.js +4 -3
  71. package/dist/types.d.ts +105 -7
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/utils/aggregate-state-functions.d.ts +24 -0
  74. package/dist/utils/aggregate-state-functions.d.ts.map +1 -0
  75. package/dist/utils/aggregate-state-functions.js +60 -0
  76. package/dist/utils/approximate-measures.d.ts +3 -1
  77. package/dist/utils/approximate-measures.d.ts.map +1 -1
  78. package/dist/utils/approximate-measures.js +8 -2
  79. package/dist/utils/base-measure-definition.d.ts +16 -0
  80. package/dist/utils/base-measure-definition.d.ts.map +1 -1
  81. package/dist/utils/base-measure-definition.js +31 -0
  82. package/dist/utils/build-dataset-query-builder.d.ts +1 -1
  83. package/dist/utils/build-dataset-query-builder.d.ts.map +1 -1
  84. package/dist/utils/build-dataset-query-builder.js +26 -21
  85. package/dist/utils/by-name.d.ts +8 -0
  86. package/dist/utils/by-name.d.ts.map +1 -0
  87. package/dist/utils/by-name.js +44 -0
  88. package/dist/utils/calendar-shift-sql.d.ts +5 -4
  89. package/dist/utils/calendar-shift-sql.d.ts.map +1 -1
  90. package/dist/utils/calendar-shift-sql.js +8 -8
  91. package/dist/utils/clickhouse-dataset-sql-dialect.d.ts +2 -2
  92. package/dist/utils/clickhouse-dataset-sql-dialect.d.ts.map +1 -1
  93. package/dist/utils/clickhouse-dataset-sql-dialect.js +143 -1
  94. package/dist/utils/compile-dataset-query.d.ts +1 -1
  95. package/dist/utils/compile-dataset-query.d.ts.map +1 -1
  96. package/dist/utils/compile-dataset-query.js +22 -8
  97. package/dist/utils/composite-time-measure-sql.d.ts +3 -5
  98. package/dist/utils/composite-time-measure-sql.d.ts.map +1 -1
  99. package/dist/utils/composite-time-measure-sql.js +15 -16
  100. package/dist/utils/count-aggregations.d.ts +4 -0
  101. package/dist/utils/count-aggregations.d.ts.map +1 -0
  102. package/dist/utils/count-aggregations.js +6 -0
  103. package/dist/utils/dataset-definition-validation.d.ts +1 -1
  104. package/dist/utils/dataset-definition-validation.d.ts.map +1 -1
  105. package/dist/utils/dataset-definition-validation.js +55 -26
  106. package/dist/utils/dataset-derived-query.d.ts +2 -4
  107. package/dist/utils/dataset-derived-query.d.ts.map +1 -1
  108. package/dist/utils/dataset-derived-query.js +11 -14
  109. package/dist/utils/dataset-having.d.ts +5 -7
  110. package/dist/utils/dataset-having.d.ts.map +1 -1
  111. package/dist/utils/dataset-having.js +9 -12
  112. package/dist/utils/dataset-normalization.d.ts.map +1 -1
  113. package/dist/utils/dataset-normalization.js +1 -0
  114. package/dist/utils/dataset-query-validation.d.ts +1 -1
  115. package/dist/utils/dataset-query-validation.d.ts.map +1 -1
  116. package/dist/utils/dataset-query-validation.js +19 -2
  117. package/dist/utils/dataset-source.d.ts +42 -0
  118. package/dist/utils/dataset-source.d.ts.map +1 -0
  119. package/dist/utils/dataset-source.js +59 -0
  120. package/dist/utils/dataset-sql-dialect.d.ts +13 -2
  121. package/dist/utils/dataset-sql-dialect.d.ts.map +1 -1
  122. package/dist/utils/dataset-sql-dialect.js +74 -1
  123. package/dist/utils/dataset-time-keys.d.ts +32 -0
  124. package/dist/utils/dataset-time-keys.d.ts.map +1 -0
  125. package/dist/utils/dataset-time-keys.js +55 -0
  126. package/dist/utils/dataset-validation.d.ts.map +1 -1
  127. package/dist/utils/dataset-validation.js +7 -0
  128. package/dist/utils/deployable-formulas.d.ts +33 -0
  129. package/dist/utils/deployable-formulas.d.ts.map +1 -0
  130. package/dist/utils/deployable-formulas.js +102 -0
  131. package/dist/utils/derived-cte-validation.d.ts +5 -0
  132. package/dist/utils/derived-cte-validation.d.ts.map +1 -1
  133. package/dist/utils/derived-cte-validation.js +77 -8
  134. package/dist/utils/derived-measure-sql.d.ts +10 -2
  135. package/dist/utils/derived-measure-sql.d.ts.map +1 -1
  136. package/dist/utils/derived-measure-sql.js +23 -43
  137. package/dist/utils/enforced-constraints.d.ts +14 -0
  138. package/dist/utils/enforced-constraints.d.ts.map +1 -0
  139. package/dist/utils/enforced-constraints.js +57 -0
  140. package/dist/utils/filter-field-type.d.ts +14 -0
  141. package/dist/utils/filter-field-type.d.ts.map +1 -0
  142. package/dist/utils/filter-field-type.js +21 -0
  143. package/dist/utils/filtered-aggregation-sql.d.ts +2 -1
  144. package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -1
  145. package/dist/utils/filtered-aggregation-sql.js +18 -35
  146. package/dist/utils/groupable.d.ts +7 -0
  147. package/dist/utils/groupable.d.ts.map +1 -0
  148. package/dist/utils/groupable.js +8 -0
  149. package/dist/utils/metric-derived-query.d.ts +2 -4
  150. package/dist/utils/metric-derived-query.d.ts.map +1 -1
  151. package/dist/utils/metric-derived-query.js +58 -51
  152. package/dist/utils/metric-query-builder.d.ts +1 -1
  153. package/dist/utils/metric-query-builder.d.ts.map +1 -1
  154. package/dist/utils/metric-query-builder.js +22 -20
  155. package/dist/utils/metric-query-validation.d.ts.map +1 -1
  156. package/dist/utils/metric-query-validation.js +14 -1
  157. package/dist/utils/portable-aggregations.d.ts +1 -0
  158. package/dist/utils/portable-aggregations.d.ts.map +1 -1
  159. package/dist/utils/portable-aggregations.js +5 -0
  160. package/dist/utils/protocol-schema-conversion.d.ts +12 -0
  161. package/dist/utils/protocol-schema-conversion.d.ts.map +1 -0
  162. package/dist/utils/protocol-schema-conversion.js +61 -0
  163. package/dist/utils/query-field-references.d.ts +10 -0
  164. package/dist/utils/query-field-references.d.ts.map +1 -0
  165. package/dist/utils/query-field-references.js +38 -0
  166. package/dist/utils/query-timezone.d.ts +41 -3
  167. package/dist/utils/query-timezone.d.ts.map +1 -1
  168. package/dist/utils/query-timezone.js +76 -9
  169. package/dist/utils/raw-sql-dialect-check.d.ts +12 -0
  170. package/dist/utils/raw-sql-dialect-check.d.ts.map +1 -0
  171. package/dist/utils/raw-sql-dialect-check.js +37 -0
  172. package/dist/utils/relationship-builder-plan.d.ts +37 -18
  173. package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
  174. package/dist/utils/relationship-builder-plan.js +43 -28
  175. package/dist/utils/relationship-measure-sql.d.ts +14 -4
  176. package/dist/utils/relationship-measure-sql.d.ts.map +1 -1
  177. package/dist/utils/relationship-measure-sql.js +36 -10
  178. package/dist/utils/relationship-measures.d.ts.map +1 -1
  179. package/dist/utils/relationship-measures.js +3 -0
  180. package/dist/utils/relationship-validation.d.ts.map +1 -1
  181. package/dist/utils/relationship-validation.js +2 -0
  182. package/dist/utils/row-scope.d.ts +23 -0
  183. package/dist/utils/row-scope.d.ts.map +1 -0
  184. package/dist/utils/row-scope.js +26 -0
  185. package/dist/utils/scoped-dataset-source.d.ts +23 -0
  186. package/dist/utils/scoped-dataset-source.d.ts.map +1 -0
  187. package/dist/utils/scoped-dataset-source.js +34 -0
  188. package/dist/utils/semantic-tool.d.ts +2 -2
  189. package/dist/utils/semantic-tool.d.ts.map +1 -1
  190. package/dist/utils/semantic-tool.js +1 -1
  191. package/dist/utils/shift-measure-sql.d.ts +5 -4
  192. package/dist/utils/shift-measure-sql.d.ts.map +1 -1
  193. package/dist/utils/shift-measure-sql.js +25 -26
  194. package/dist/utils/shift-wall-time-sql.d.ts +2 -1
  195. package/dist/utils/shift-wall-time-sql.d.ts.map +1 -1
  196. package/dist/utils/shift-wall-time-sql.js +7 -7
  197. package/dist/utils/snapshot-execution-context.d.ts.map +1 -1
  198. package/dist/utils/snapshot-execution-context.js +3 -0
  199. package/dist/utils/statement-parameters.d.ts +58 -0
  200. package/dist/utils/statement-parameters.d.ts.map +1 -0
  201. package/dist/utils/statement-parameters.js +101 -0
  202. package/dist/utils/time-axis-calendar-guard.d.ts +2 -1
  203. package/dist/utils/time-axis-calendar-guard.d.ts.map +1 -1
  204. package/dist/utils/time-axis-calendar-guard.js +5 -7
  205. package/dist/utils/time-measure-axis-sql.d.ts +3 -2
  206. package/dist/utils/time-measure-axis-sql.d.ts.map +1 -1
  207. package/dist/utils/time-measure-axis-sql.js +20 -22
  208. package/dist/utils/time-measure-context-sql.d.ts +3 -2
  209. package/dist/utils/time-measure-context-sql.d.ts.map +1 -1
  210. package/dist/utils/time-measure-context-sql.js +11 -11
  211. package/dist/utils/time-measure-dataset-sql.d.ts +7 -5
  212. package/dist/utils/time-measure-dataset-sql.d.ts.map +1 -1
  213. package/dist/utils/time-measure-dataset-sql.js +17 -12
  214. package/dist/utils/time-measure-leaf-sql.d.ts +3 -2
  215. package/dist/utils/time-measure-leaf-sql.d.ts.map +1 -1
  216. package/dist/utils/time-measure-leaf-sql.js +19 -18
  217. package/dist/utils/time-measure-result-sql.d.ts.map +1 -1
  218. package/dist/utils/time-measure-result-sql.js +10 -11
  219. package/dist/utils/time-measure-source-sql.d.ts +5 -2
  220. package/dist/utils/time-measure-source-sql.d.ts.map +1 -1
  221. package/dist/utils/time-measure-source-sql.js +33 -23
  222. package/dist/utils/time-measure-values-sql.d.ts +4 -3
  223. package/dist/utils/time-measure-values-sql.d.ts.map +1 -1
  224. package/dist/utils/time-measure-values-sql.js +26 -29
  225. package/dist/utils/timestamp-instants.d.ts +17 -0
  226. package/dist/utils/timestamp-instants.d.ts.map +1 -0
  227. package/dist/utils/timestamp-instants.js +34 -0
  228. package/dist/utils/timestamp-text.d.ts +12 -0
  229. package/dist/utils/timestamp-text.d.ts.map +1 -0
  230. package/dist/utils/timestamp-text.js +18 -0
  231. package/dist/utils/zod-json-schema.d.ts +8 -0
  232. package/dist/utils/zod-json-schema.d.ts.map +1 -0
  233. package/dist/utils/zod-json-schema.js +18 -0
  234. package/dist/utils/zod-schema-version.d.ts +19 -0
  235. package/dist/utils/zod-schema-version.d.ts.map +1 -0
  236. package/dist/utils/zod-schema-version.js +6 -0
  237. package/dist/utils/zod-v3-protocol-schema.d.ts +10 -0
  238. package/dist/utils/zod-v3-protocol-schema.d.ts.map +1 -0
  239. package/dist/utils/zod-v3-protocol-schema.js +164 -0
  240. package/dist/utils/zod-v4-protocol-schema.d.ts +12 -0
  241. package/dist/utils/zod-v4-protocol-schema.d.ts.map +1 -0
  242. package/dist/utils/zod-v4-protocol-schema.js +171 -0
  243. package/dist/validation.d.ts +7 -0
  244. package/dist/validation.d.ts.map +1 -1
  245. package/dist/validation.js +42 -8
  246. package/package.json +21 -12
  247. package/dist/api.type-test.d.ts +0 -2
  248. package/dist/api.type-test.d.ts.map +0 -1
  249. package/dist/api.type-test.js +0 -251
  250. package/dist/utils/time-arithmetic-sql.d.ts +0 -4
  251. package/dist/utils/time-arithmetic-sql.d.ts.map +0 -1
  252. package/dist/utils/time-arithmetic-sql.js +0 -9
  253. package/dist/utils/window-aggregation-sql.d.ts +0 -5
  254. package/dist/utils/window-aggregation-sql.d.ts.map +0 -1
  255. package/dist/utils/window-aggregation-sql.js +0 -17
@@ -1,18 +1,13 @@
1
1
  import { validateShiftMeasures } from './shift-measure-validation.js';
2
2
  import { escapeRegExp, isSafeSQLIdentifier, stripSqlLiterals } from '../sql-utils.js';
3
+ import { clickhouseDatasetSqlDialect } from './clickhouse-dataset-sql-dialect.js';
4
+ import { rawSqlErrorMessage } from './raw-sql-dialect-check.js';
3
5
  import { SEMANTIC_FILTER_OPERATORS, isSemanticFilterOperator } from '../constants.js';
4
6
  import { validateDerivedMeasures } from './derived-measure-validation.js';
5
7
  import { isDerivedMeasure, isWindowMeasure, isShiftMeasure } from './dataset-measures.js';
6
8
  import { validateWindowMeasures } from './window-measure-validation.js';
7
9
  import { validateDatasetAgentMetadata } from './semantic-metadata-validation.js';
8
- /**
9
- * Statement terminators and comment openers.
10
- *
11
- * A raw `sql` value is spliced into a larger expression, so any of these turns
12
- * one expression into something else: `;` starts a second statement, `--` and
13
- * `/*` comment out whatever the builder appends after it.
14
- */
15
- const STATEMENT_BREAKERS = /;|--|\/\*/;
10
+ import { AGGREGATE_STATE_FUNCTIONS, isAggregateStateFunction } from './aggregate-state-functions.js';
16
11
  /** Dataset, dimension and measure names address fields as `<relationship>.<dimension>`. */
17
12
  const QUALIFIED_SEPARATOR = '.';
18
13
  function fail(datasetName, message) {
@@ -63,24 +58,16 @@ function validateRawSql(datasetName, kind, name, sql, dependencies) {
63
58
  if (sql.trim().length === 0) {
64
59
  fail(datasetName, `${kind} "${name}" declares an empty sql expression.`);
65
60
  }
66
- // Quoted spans are blanked first, so a terminator or comment opener *inside* a
67
- // string literal is data rather than syntax. An unterminated quote is itself a
68
- // rejection: left as data, an open quote would hide everything after it.
69
- let code;
70
- let referenceable;
71
- try {
72
- code = stripSqlLiterals(sql);
73
- // A quoted identifier is a column reference, so the dependency check below
74
- // needs its text even though the terminator check above must not see it.
75
- referenceable = stripSqlLiterals(sql, { keepQuotedIdentifiers: true });
76
- }
77
- catch {
78
- fail(datasetName, `${kind} "${name}" sql has an unterminated quoted literal.`);
79
- }
80
- if (STATEMENT_BREAKERS.test(code)) {
81
- fail(datasetName, `${kind} "${name}" sql must be a single expression without statement terminators or ` +
82
- 'comments (";", "--", "/*") outside a quoted literal.');
83
- }
61
+ // Definitions are engine-neutral, so this checks ClickHouse lexical rules;
62
+ // compilation re-checks under any other engine's rules (raw-sql-dialect-check).
63
+ // An unterminated quote is itself a rejection: left as data, an open quote
64
+ // would hide everything after it.
65
+ const lexicalError = clickhouseDatasetSqlDialect.rawSqlExpressionError(sql);
66
+ if (lexicalError)
67
+ fail(datasetName, rawSqlErrorMessage(kind, name, lexicalError));
68
+ // A quoted identifier is a column reference, so the dependency check below
69
+ // needs its text even though the terminator check above must not see it.
70
+ const referenceable = stripSqlLiterals(sql, { keepQuotedIdentifiers: true });
84
71
  for (const dependency of dependencies ?? []) {
85
72
  // Dependencies are qualified identifiers (`analytics.orders.amount`); the
86
73
  // expression references the column itself, so the final segment is what has
@@ -176,6 +163,9 @@ function validateMeasures(datasetName, measures, dimensions, declaredFilters) {
176
163
  validateRawSql(datasetName, 'measure', name, definition.sql, definition.dependencies);
177
164
  }
178
165
  validateMeasureFilters(datasetName, name, definition.filters ?? [], dimensions, declaredFilters);
166
+ if (definition.aggregation === 'merge') {
167
+ validateMergeMeasure(datasetName, name, definition);
168
+ }
179
169
  // `field` and `argField` name either a declared dimension or a physical
180
170
  // column that the model deliberately does not expose (the `allowHiddenField`
181
171
  // case in metric validation). Either way the value reaches SQL, so a name
@@ -191,6 +181,23 @@ function validateMeasures(datasetName, measures, dimensions, declaredFilters) {
191
181
  }
192
182
  }
193
183
  }
184
+ /**
185
+ * A merge measure names its state function (interpolated into SQL as
186
+ * `<fn>Merge`) and reads a plain column: raw SQL or filters would wrap the
187
+ * state column in an expression the `-Merge` combinator rejects.
188
+ */
189
+ function validateMergeMeasure(datasetName, name, definition) {
190
+ if (!isAggregateStateFunction(definition.stateFunction)) {
191
+ fail(datasetName, `measure "${name}" merges unsupported aggregate state "${String(definition.stateFunction)}". ` +
192
+ `Supported state functions: ${AGGREGATE_STATE_FUNCTIONS.join(', ')}.`);
193
+ }
194
+ if (definition.filters?.length) {
195
+ fail(datasetName, `measure "${name}" cannot have filters: merge measures read stored states.`);
196
+ }
197
+ if (definition.sql !== undefined) {
198
+ fail(datasetName, `measure "${name}" cannot use sql: merge measures read a stored state column.`);
199
+ }
200
+ }
194
201
  function validateLimits(datasetName, limits) {
195
202
  if (!limits) {
196
203
  return;
@@ -226,6 +233,9 @@ export function validateDatasetDefinition(name, config) {
226
233
  fail(name, `source "${config.source}" is not a safe table identifier. Expected "table" or ` +
227
234
  '"database.table", each segment containing only letters, numbers and underscores.');
228
235
  }
236
+ if (config.final !== undefined && typeof config.final !== 'boolean') {
237
+ fail(name, 'final must be true or false.');
238
+ }
229
239
  // The tenant key is the isolation boundary: it becomes a WHERE predicate on
230
240
  // every query against a tenant-scoped dataset, so it is checked here rather
231
241
  // than trusted wherever that predicate is assembled.
@@ -243,6 +253,25 @@ export function validateDatasetDefinition(name, config) {
243
253
  assertSafeColumn(name, config.timeKey, 'timeKey');
244
254
  }
245
255
  }
256
+ if (config.timeKeys !== undefined) {
257
+ if (!Array.isArray(config.timeKeys)) {
258
+ fail(name, 'timeKeys must be an array of dimension names or columns.');
259
+ }
260
+ if (config.timeKey === undefined) {
261
+ fail(name, 'timeKeys requires timeKey, the default time axis.');
262
+ }
263
+ if (new Set(config.timeKeys).size !== config.timeKeys.length) {
264
+ fail(name, 'timeKeys must not contain duplicates.');
265
+ }
266
+ for (const timeKey of config.timeKeys) {
267
+ if (typeof timeKey !== 'string' || timeKey.trim().length === 0) {
268
+ fail(name, 'timeKeys entries must be non-empty strings.');
269
+ }
270
+ if (!(timeKey in (config.dimensions ?? {}))) {
271
+ assertSafeColumn(name, timeKey, 'timeKeys');
272
+ }
273
+ }
274
+ }
246
275
  // A dataset with no dimensions is not rejected here: the query layer already
247
276
  // refuses an empty dataset query, which is the honest place for it — a
248
277
  // measure-only model is a legitimate thing to define.
@@ -1,6 +1,7 @@
1
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
+ import { type PlannedStatement } from './statement-parameters.js';
4
5
  type BuildBaseQuery = (dataset: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions) => QueryBuilderLike;
5
6
  export declare function hasSelectedDerivedMeasure(ds: AnyDatasetInstance, query: DatasetQuery): boolean;
6
7
  /**
@@ -8,9 +9,6 @@ export declare function hasSelectedDerivedMeasure(ds: AnyDatasetInstance, query:
8
9
  * measures are computed there, and `having` conditions filter there.
9
10
  */
10
11
  export declare function needsOuterDatasetQuery(ds: AnyDatasetInstance, query: DatasetQuery): boolean;
11
- export declare function buildDerivedDatasetSql(ds: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions, buildBaseQuery: BuildBaseQuery): {
12
- sql: string;
13
- parameters: unknown[];
14
- };
12
+ export declare function buildDerivedDatasetSql(ds: AnyDatasetInstance, query: DatasetQuery, options: DatasetQueryExecutionOptions, buildBaseQuery: BuildBaseQuery): PlannedStatement;
15
13
  export {};
16
14
  //# 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":"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
+ {"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;AAExE,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAEvF,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,gBAAgB,CAmDlB"}
@@ -6,6 +6,7 @@ import { baseMeasureNames, getBaseMeasure, getDerivedMeasure } from './dataset-m
6
6
  import { resolveDatasetSqlDialect } from './dataset-sql-dialect.js';
7
7
  import { validateDatasetQueryInput } from './dataset-query-validation.js';
8
8
  import { datasetHavingSql } from './dataset-having.js';
9
+ import { StatementParameters } from './statement-parameters.js';
9
10
  export function hasSelectedDerivedMeasure(ds, query) {
10
11
  return (query.measures ?? []).some(name => getDerivedMeasure(ds.measures, name) !== undefined);
11
12
  }
@@ -26,7 +27,7 @@ export function buildDerivedDatasetSql(ds, query, options, buildBaseQuery) {
26
27
  const baseMeasures = measureDependencyNames(queryMeasureDefinitions(ds, selected), selected).filter(name => name.includes('.') || getBaseMeasure(ds.measures, name));
27
28
  const resolve = (name) => {
28
29
  const derived = getDerivedMeasure(ds.measures, name);
29
- return derived ? `(${derivedExpressionSql(derived, resolve)})` : dialect.quoteIdentifier(name);
30
+ return derived ? `(${derivedExpressionSql(dialect, derived, resolve)})` : dialect.quoteIdentifier(name);
30
31
  };
31
32
  const inner = buildBaseQuery(ds, {
32
33
  ...query,
@@ -36,7 +37,8 @@ export function buildDerivedDatasetSql(ds, query, options, buildBaseQuery) {
36
37
  limit: undefined,
37
38
  offset: undefined,
38
39
  }, { ...options, executionLimit: undefined, skipDefaultOrderBy: true });
39
- const { sql: innerSql, parameters } = inner.toSQLWithParams();
40
+ const parameters = new StatementParameters(dialect);
41
+ const base = parameters.embedBuilder(inner);
40
42
  const projections = [];
41
43
  if (query.by)
42
44
  projections.push(dialect.quoteIdentifier('period'));
@@ -45,27 +47,22 @@ export function buildDerivedDatasetSql(ds, query, options, buildBaseQuery) {
45
47
  for (const name of selected) {
46
48
  const derived = getDerivedMeasure(ds.measures, name);
47
49
  projections.push(derived
48
- ? `${derivedExpressionSql(derived, resolve)} AS ${dialect.quoteIdentifier(name)}`
50
+ ? `${derivedExpressionSql(dialect, derived, resolve)} AS ${dialect.quoteIdentifier(name)}`
49
51
  : dialect.quoteIdentifier(name));
50
52
  }
51
- let sql = `WITH base AS (${innerSql}) SELECT ${projections.join(', ')} FROM base`;
53
+ let text = `WITH base AS (${base}) SELECT ${projections.join(', ')} FROM base`;
52
54
  // Conditions read the same expression the projection does, never the output
53
55
  // alias, so the predicate stays valid on dialects without alias resolution.
54
56
  if (query.having?.length) {
55
- const having = datasetHavingSql(query.having, resolve);
56
- sql += ` WHERE ${having.sql}`;
57
- parameters.push(...having.parameters);
57
+ text += ` WHERE ${datasetHavingSql(query.having, resolve, parameters)}`;
58
58
  }
59
59
  if (query.orderBy?.length) {
60
- sql += ` ORDER BY ${query.orderBy.map(order => (`${dialect.quoteIdentifier(order.field)} ${semanticOrderDirection(order.direction)}`)).join(', ')}`;
60
+ text += ` ORDER BY ${query.orderBy.map(order => (dialect.orderTerm(dialect.quoteIdentifier(order.field), semanticOrderDirection(order.direction)))).join(', ')}`;
61
61
  }
62
62
  else if (query.by) {
63
- sql += ` ORDER BY ${dialect.quoteIdentifier('period')} ASC`;
63
+ text += ` ORDER BY ${dialect.orderTerm(dialect.quoteIdentifier('period'), 'ASC')}`;
64
64
  }
65
65
  const limit = options.executionLimit ?? query.limit;
66
- if (limit !== undefined)
67
- sql += ` LIMIT ${limit}`;
68
- if (query.offset !== undefined)
69
- sql += ` OFFSET ${query.offset}`;
70
- return { sql, parameters };
66
+ text += dialect.paginate(limit, query.offset);
67
+ return { text, parameters };
71
68
  }
@@ -1,4 +1,5 @@
1
1
  import type { AnyDatasetInstance, DatasetHavingCondition, DatasetQuery } from '../types.js';
2
+ import type { StatementParameters } from './statement-parameters.js';
2
3
  /**
3
4
  * Validates `having` conditions against the query they belong to.
4
5
  *
@@ -8,14 +9,11 @@ import type { AnyDatasetInstance, DatasetHavingCondition, DatasetQuery } from '.
8
9
  */
9
10
  export declare function datasetHavingErrors(ds: AnyDatasetInstance, query: DatasetQuery, selectedMeasures: readonly string[]): string[];
10
11
  /**
11
- * Renders validated `having` conditions as one parameterized predicate.
12
+ * Renders validated `having` conditions as one predicate.
12
13
  *
13
14
  * `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.
15
+ * aggregated value in the outer query. Values are only ever bound through
16
+ * `statement`, never written into the SQL text.
16
17
  */
17
- export declare function datasetHavingSql(conditions: readonly DatasetHavingCondition[], measureSql: (measure: string) => string): {
18
- sql: string;
19
- parameters: unknown[];
20
- };
18
+ export declare function datasetHavingSql(conditions: readonly DatasetHavingCondition[], measureSql: (measure: string) => string, statement: StatementParameters): string;
21
19
  //# sourceMappingURL=dataset-having.d.ts.map
@@ -1 +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"}
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;AAE5F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAkCrE;;;;;;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,EACvC,SAAS,EAAE,mBAAmB,GAC7B,MAAM,CAgBR"}
@@ -77,30 +77,27 @@ export function datasetHavingErrors(ds, query, selectedMeasures) {
77
77
  return errors;
78
78
  }
79
79
  /**
80
- * Renders validated `having` conditions as one parameterized predicate.
80
+ * Renders validated `having` conditions as one predicate.
81
81
  *
82
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.
83
+ * aggregated value in the outer query. Values are only ever bound through
84
+ * `statement`, never written into the SQL text.
85
85
  */
86
- export function datasetHavingSql(conditions, measureSql) {
87
- const parameters = [];
86
+ export function datasetHavingSql(conditions, measureSql, statement) {
87
+ const bind = (value) => statement.bind(value, 'number');
88
88
  const predicates = conditions.map((condition) => {
89
89
  const column = measureSql(condition.measure);
90
90
  if (condition.operator === 'between') {
91
- parameters.push(condition.value[0], condition.value[1]);
92
- return `${column} BETWEEN ? AND ?`;
91
+ return `${column} BETWEEN ${bind(condition.value[0])} AND ${bind(condition.value[1])}`;
93
92
  }
94
93
  if (condition.operator === 'in' || condition.operator === 'notIn') {
95
- parameters.push(...condition.value);
96
- const placeholders = condition.value.map(() => '?').join(', ');
94
+ const placeholders = condition.value.map(bind).join(', ');
97
95
  return `${column} ${condition.operator === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;
98
96
  }
99
97
  const comparison = COMPARISONS[condition.operator];
100
98
  if (!comparison)
101
99
  throw new Error(`Unsupported having operator "${String(condition.operator)}".`);
102
- parameters.push(condition.value);
103
- return `${column} ${comparison} ?`;
100
+ return `${column} ${comparison} ${bind(condition.value)}`;
104
101
  });
105
- return { sql: predicates.join(' AND '), parameters };
102
+ return predicates.join(' AND ');
106
103
  }
@@ -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;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
+ {"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,CAejB"}
@@ -45,6 +45,7 @@ export function measureToAggregationSpec(measureName, definition) {
45
45
  field: definition.field,
46
46
  argField: definition.argField,
47
47
  level: definition.level,
48
+ ...(definition.stateFunction !== undefined ? { stateFunction: definition.stateFunction } : {}),
48
49
  sql: definition.sql,
49
50
  filters: definition.filters,
50
51
  };
@@ -1,4 +1,4 @@
1
1
  import type { AnyDatasetInstance, DatasetQuery, ExecutionContext } from '../types.js';
2
2
  import { type ValidationResult } from '../validation.js';
3
- export declare function validateDatasetQueryInput(ds: AnyDatasetInstance, query: DatasetQuery, context?: ExecutionContext): ValidationResult;
3
+ export declare function validateDatasetQueryInput(dataset: AnyDatasetInstance, query: DatasetQuery, context?: ExecutionContext): ValidationResult;
4
4
  //# sourceMappingURL=dataset-query-validation.d.ts.map
@@ -1 +1 @@
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
+ {"version":3,"file":"dataset-query-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-query-validation.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAoB9E,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,kBAAkB,EAC3B,KAAK,EAAE,YAAY,EACnB,OAAO,CAAC,EAAE,gBAAgB,GACzB,gBAAgB,CAqLlB"}
@@ -1,3 +1,4 @@
1
+ import { notGroupableError } from './groupable.js';
1
2
  import { resolveRelationshipMeasure } from './relationship-measures.js';
2
3
  import { semanticQuerySyntaxErrors } from './semantic-query-syntax.js';
3
4
  import { queryTimezoneErrors } from './query-timezone.js';
@@ -8,10 +9,19 @@ import { getRuntimeTenantPredicate, validateTenantRuntime, } from './tenant-runt
8
9
  import { isQualifiedField, resolveQualifiedField, } from './relationship-fields.js';
9
10
  import { validateQualifiedFilter, validateRelationshipTenantRuntime, } from './relationship-validation.js';
10
11
  import { segmentSelectionErrors } from './segments.js';
12
+ import { enforcedConstraintErrors } from './enforced-constraints.js';
11
13
  import { analyzeTimeMeasureAxis } from './time-measure-axis.js';
12
14
  import { datasetHavingErrors } from './dataset-having.js';
13
- export function validateDatasetQueryInput(ds, query, context) {
14
- const errors = [...semanticQuerySyntaxErrors(query), ...queryTimezoneErrors(query.timezone)];
15
+ import { datasetOnTimeKey, selectedTimeKeyError } from './dataset-time-keys.js';
16
+ export function validateDatasetQueryInput(dataset, query, context) {
17
+ // Validate against the selected time axis, so grain and window checks see it.
18
+ const timeKeyError = selectedTimeKeyError(dataset, query.timeKey);
19
+ const ds = timeKeyError ? dataset : datasetOnTimeKey(dataset, query.timeKey);
20
+ const errors = [
21
+ ...semanticQuerySyntaxErrors(query),
22
+ ...queryTimezoneErrors(query.timezone),
23
+ ...(timeKeyError ? [timeKeyError] : []),
24
+ ];
15
25
  const dimensionNames = Object.keys(ds.dimensions);
16
26
  const measureNames = Object.keys(ds.measures);
17
27
  const selectedDimensions = query.dimensions ?? [];
@@ -35,6 +45,7 @@ export function validateDatasetQueryInput(ds, query, context) {
35
45
  if (relationshipTenantError) {
36
46
  errors.push(relationshipTenantError);
37
47
  }
48
+ errors.push(...enforcedConstraintErrors(ds, context, query.filters?.length ?? 0));
38
49
  if (selectedDimensions.length === 0 && selectedMeasures.length === 0) {
39
50
  errors.push(`Dataset "${ds.name}" query must select at least one dimension or measure.`);
40
51
  }
@@ -45,11 +56,17 @@ export function validateDatasetQueryInput(ds, query, context) {
45
56
  if (resolution?.error) {
46
57
  errors.push(resolution.error);
47
58
  }
59
+ else if (resolution?.resolved?.targetDimension.groupable === false) {
60
+ errors.push(notGroupableError(dimension));
61
+ }
48
62
  continue;
49
63
  }
50
64
  if (!dimensionNames.includes(dimension)) {
51
65
  errors.push(`Unknown dimensions: ${dimension}. Available: ${dimensionNames.join(', ')}`);
52
66
  }
67
+ else if (ds.dimensions[dimension]?.groupable === false) {
68
+ errors.push(notGroupableError(dimension));
69
+ }
53
70
  }
54
71
  }
55
72
  if (query.measures) {
@@ -0,0 +1,42 @@
1
+ import type { AnyDatasetInstance } from '../types.js';
2
+ import type { QueryBuilderFactoryLike, QueryBuilderLike } from '../query-builder-protocol.js';
3
+ import type { RelationshipBuilderContext } from './relationship-builder-plan.js';
4
+ /**
5
+ * The dataset's source table as a builder, read with `FINAL` when the dataset
6
+ * declares `rowResolution`. Every builder-path read of a dataset source goes
7
+ * through here so no query can skip row-version resolution.
8
+ */
9
+ export declare function datasetSourceBuilder(builderFactory: Pick<QueryBuilderFactoryLike, 'table'>, ds: Pick<AnyDatasetInstance, 'name' | 'source' | 'rowResolution'>): QueryBuilderLike;
10
+ /**
11
+ * A filter moved to PREWHERE runs before `FINAL` and can match an older version
12
+ * of a row. The server default forbids that; pinning it also covers profiles
13
+ * that enable it. `readonly = 1` accepts a setting equal to the profile's and
14
+ * refuses it exactly where the profile is unsafe, so the query fails closed.
15
+ */
16
+ export declare const FINAL_SAFETY_SETTINGS: {
17
+ readonly optimize_move_to_prewhere_if_final: 0;
18
+ };
19
+ /**
20
+ * Whether a query reads a source with `FINAL`: the dataset's own, or a
21
+ * relationship target it actually joins. Unused relationships don't count, so
22
+ * a plain query never carries the pin.
23
+ */
24
+ export declare function queryReadsFinal(ds: Pick<AnyDatasetInstance, 'rowResolution'>, joinCtx: Pick<RelationshipBuilderContext, 'joins'> | undefined): boolean;
25
+ /**
26
+ * The statement with `FINAL_SAFETY_SETTINGS` in its own SQL, for paths that run
27
+ * rendered SQL (`rawQuery`), where builder settings don't travel.
28
+ */
29
+ export declare function withFinalSafety<T extends {
30
+ sql: string;
31
+ }>(readsFinal: boolean, statement: T): T;
32
+ /**
33
+ * After `FINAL`, the rows that are current state: not deleted in a
34
+ * ReplacingMergeTree, positive in a collapsing engine. `FINAL` alone removes
35
+ * engine-declared deletions on current ClickHouse; the filter covers deletion
36
+ * columns the engine doesn't declare, older servers and unmatched cancel rows.
37
+ */
38
+ export declare function currentRowPredicate(ds: Pick<AnyDatasetInstance, 'rowResolution'>): {
39
+ column: string;
40
+ value: 0 | 1;
41
+ } | undefined;
42
+ //# sourceMappingURL=dataset-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset-source.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAC9F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAEjF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,cAAc,EAAE,IAAI,CAAC,uBAAuB,EAAE,OAAO,CAAC,EACtD,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,MAAM,GAAG,QAAQ,GAAG,eAAe,CAAC,GAChE,gBAAgB,CAgBlB;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;CAAqD,CAAC;AAExF;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,eAAe,CAAC,EAC7C,OAAO,EAAE,IAAI,CAAC,0BAA0B,EAAE,OAAO,CAAC,GAAG,SAAS,GAC7D,OAAO,CAET;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,GAAG,CAAC,CAI/F;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,eAAe,CAAC,GAC5C;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAA;CAAE,GAAG,SAAS,CAM9C"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The dataset's source table as a builder, read with `FINAL` when the dataset
3
+ * declares `rowResolution`. Every builder-path read of a dataset source goes
4
+ * through here so no query can skip row-version resolution.
5
+ */
6
+ export function datasetSourceBuilder(builderFactory, ds) {
7
+ const qb = builderFactory.table(ds.source);
8
+ if (!ds.rowResolution) {
9
+ return qb;
10
+ }
11
+ if (typeof qb.final !== 'function') {
12
+ throw new Error(`Dataset "${ds.name}" reads its source with FINAL, but the query builder does not implement final(). ` +
13
+ 'Reading it without FINAL would count every unmerged row version, so the query is refused.');
14
+ }
15
+ const resolved = qb.final();
16
+ const safe = resolved.settings ? resolved.settings(FINAL_SAFETY_SETTINGS) : resolved;
17
+ const current = currentRowPredicate(ds);
18
+ // Qualified like every base column, so it stays unambiguous once joins are added.
19
+ return current ? safe.where(`${ds.source}.${current.column}`, 'eq', current.value) : safe;
20
+ }
21
+ /**
22
+ * A filter moved to PREWHERE runs before `FINAL` and can match an older version
23
+ * of a row. The server default forbids that; pinning it also covers profiles
24
+ * that enable it. `readonly = 1` accepts a setting equal to the profile's and
25
+ * refuses it exactly where the profile is unsafe, so the query fails closed.
26
+ */
27
+ export const FINAL_SAFETY_SETTINGS = { optimize_move_to_prewhere_if_final: 0 };
28
+ /**
29
+ * Whether a query reads a source with `FINAL`: the dataset's own, or a
30
+ * relationship target it actually joins. Unused relationships don't count, so
31
+ * a plain query never carries the pin.
32
+ */
33
+ export function queryReadsFinal(ds, joinCtx) {
34
+ return Boolean(ds.rowResolution) || Boolean(joinCtx?.joins.some(join => join.final));
35
+ }
36
+ /**
37
+ * The statement with `FINAL_SAFETY_SETTINGS` in its own SQL, for paths that run
38
+ * rendered SQL (`rawQuery`), where builder settings don't travel.
39
+ */
40
+ export function withFinalSafety(readsFinal, statement) {
41
+ if (!readsFinal)
42
+ return statement;
43
+ const pins = Object.entries(FINAL_SAFETY_SETTINGS).map(([name, value]) => `${name} = ${value}`);
44
+ return { ...statement, sql: `${statement.sql} SETTINGS ${pins.join(', ')}` };
45
+ }
46
+ /**
47
+ * After `FINAL`, the rows that are current state: not deleted in a
48
+ * ReplacingMergeTree, positive in a collapsing engine. `FINAL` alone removes
49
+ * engine-declared deletions on current ClickHouse; the filter covers deletion
50
+ * columns the engine doesn't declare, older servers and unmatched cancel rows.
51
+ */
52
+ export function currentRowPredicate(ds) {
53
+ const policy = ds.rowResolution;
54
+ if (!policy || !('engine' in policy))
55
+ return undefined;
56
+ if (policy.engine === 'replacing')
57
+ return policy.deletedColumn === undefined ? undefined : { column: policy.deletedColumn, value: 0 };
58
+ return { column: policy.signColumn, value: 1 };
59
+ }
@@ -1,5 +1,16 @@
1
- import type { DatasetSqlDialect } from '../dataset-sql-dialect.js';
1
+ import type { DatasetSqlDialect, DatasetSqlDialectRules } from '../dataset-sql-dialect.js';
2
2
  import type { QueryBuilderFactoryInput } from '../query-builder-protocol.js';
3
+ /**
4
+ * Register a complete engine rule set, so a factory can carry it as its
5
+ * `datasetSqlDialect` and have it used as-is rather than completed with the
6
+ * ClickHouse rules. Internal: for engine packages and tests.
7
+ */
8
+ export declare function defineDatasetSqlDialectRules<TRules extends DatasetSqlDialectRules>(rules: TRules): Readonly<TRules>;
9
+ /**
10
+ * Complete a public dialect hook with the ClickHouse rules it does not
11
+ * override. Memoized, so one hook always resolves to one rule set.
12
+ */
13
+ export declare function completeDatasetSqlDialect(dialect: DatasetSqlDialect): DatasetSqlDialectRules;
3
14
  /** Resolve from the active factory, so a runtime builder override carries its dialect. */
4
- export declare function resolveDatasetSqlDialect(factory: QueryBuilderFactoryInput): DatasetSqlDialect;
15
+ export declare function resolveDatasetSqlDialect(factory: QueryBuilderFactoryInput): DatasetSqlDialectRules;
5
16
  //# sourceMappingURL=dataset-sql-dialect.d.ts.map
@@ -1 +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"}
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,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAC3F,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAM7E;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,SAAS,sBAAsB,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAInH;AAsCD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,iBAAiB,GAAG,sBAAsB,CAgB5F;AAED,0FAA0F;AAC1F,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,wBAAwB,GAAG,sBAAsB,CAIlG"}
@@ -1,5 +1,78 @@
1
1
  import { clickhouseDatasetSqlDialect } from './clickhouse-dataset-sql-dialect.js';
2
+ const resolved = new WeakMap();
3
+ const complete = new WeakSet();
4
+ /**
5
+ * Register a complete engine rule set, so a factory can carry it as its
6
+ * `datasetSqlDialect` and have it used as-is rather than completed with the
7
+ * ClickHouse rules. Internal: for engine packages and tests.
8
+ */
9
+ export function defineDatasetSqlDialectRules(rules) {
10
+ const frozen = Object.isFrozen(rules) ? rules : receiverBoundSnapshot(rules);
11
+ complete.add(frozen);
12
+ return frozen;
13
+ }
14
+ /**
15
+ * A frozen snapshot of a rule set that leaves the caller's object unfrozen.
16
+ *
17
+ * Own members are copied as they are, so a rule shared with another rule set
18
+ * (spread from the ClickHouse rules, say) keeps its identity. Members a class
19
+ * declares are read through the original instance, methods bound to it and
20
+ * getters evaluated on it: its `#private` fields exist only on the object its
21
+ * constructor ran on, so a copy must not become their receiver. Members
22
+ * inherited from a plain object stay inherited.
23
+ */
24
+ function receiverBoundSnapshot(rules) {
25
+ const snapshot = Object.create(Object.getPrototypeOf(rules), Object.getOwnPropertyDescriptors(rules));
26
+ for (let owner = Object.getPrototypeOf(rules); owner && isClassPrototype(owner); owner = Object.getPrototypeOf(owner)) {
27
+ for (const key of Reflect.ownKeys(owner)) {
28
+ if (key === 'constructor' || Object.hasOwn(snapshot, key))
29
+ continue;
30
+ // Read through the original, so a getter sees its own private fields;
31
+ // a method is bound to it for the same reason.
32
+ const value = Reflect.get(rules, key);
33
+ // Define rather than assign: assignment cannot shadow a read-only
34
+ // member inherited from a frozen prototype such as the ClickHouse rules.
35
+ Object.defineProperty(snapshot, key, {
36
+ value: typeof value === 'function' ? value.bind(rules) : value,
37
+ enumerable: true,
38
+ });
39
+ }
40
+ }
41
+ return Object.freeze(snapshot);
42
+ }
43
+ /** A prototype a class declaration created: its constructor points back at it. */
44
+ function isClassPrototype(owner) {
45
+ if (owner === Object.prototype || !Object.hasOwn(owner, 'constructor'))
46
+ return false;
47
+ const constructor = owner.constructor;
48
+ return typeof constructor === 'function' && constructor.prototype === owner;
49
+ }
50
+ /**
51
+ * Complete a public dialect hook with the ClickHouse rules it does not
52
+ * override. Memoized, so one hook always resolves to one rule set.
53
+ */
54
+ export function completeDatasetSqlDialect(dialect) {
55
+ if (dialect === clickhouseDatasetSqlDialect)
56
+ return clickhouseDatasetSqlDialect;
57
+ if (complete.has(dialect))
58
+ return dialect;
59
+ let rules = resolved.get(dialect);
60
+ if (!rules) {
61
+ // Read the public members explicitly: a class-based hook keeps its methods
62
+ // on the prototype, which spreading would silently drop.
63
+ rules = Object.freeze({
64
+ ...clickhouseDatasetSqlDialect,
65
+ name: dialect.name,
66
+ quoteIdentifier: (identifier) => dialect.quoteIdentifier(identifier),
67
+ quoteQualifiedName: (name) => name.split('.').map(part => dialect.quoteIdentifier(part)).join('.'),
68
+ });
69
+ resolved.set(dialect, rules);
70
+ }
71
+ return rules;
72
+ }
2
73
  /** Resolve from the active factory, so a runtime builder override carries its dialect. */
3
74
  export function resolveDatasetSqlDialect(factory) {
4
- return factory.datasetSqlDialect ?? clickhouseDatasetSqlDialect;
75
+ return factory.datasetSqlDialect
76
+ ? completeDatasetSqlDialect(factory.datasetSqlDialect)
77
+ : clickhouseDatasetSqlDialect;
5
78
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Multiple time axes per dataset (HQ-80).
3
+ *
4
+ * `timeKey` is a dataset's default time axis; `timeKeys` names the other axes
5
+ * a query may select with `query.timeKey`, such as `shipped_at` beside
6
+ * `created_at`. A query on another axis compiles against a view of the dataset
7
+ * whose `timeKey` is that axis, so grains, windows, shifts and time-range
8
+ * checks follow the selection without each of them knowing about it.
9
+ */
10
+ interface TimeAxes {
11
+ readonly name?: string;
12
+ readonly timeKey?: string;
13
+ readonly timeKeys?: readonly string[];
14
+ }
15
+ /** Every time axis a query may select, the default first; empty without a `timeKey`. */
16
+ export declare function datasetTimeKeys(dataset: TimeAxes): string[];
17
+ /** The validation message for a query's `timeKey` selection, or undefined. */
18
+ export declare function selectedTimeKeyError(dataset: TimeAxes, timeKey: unknown): string | undefined;
19
+ /**
20
+ * True when a query selects an axis other than the dataset's default. Naming
21
+ * the default explicitly is the same query as omitting it.
22
+ */
23
+ export declare function selectsAlternateTimeKey(dataset: TimeAxes, timeKey: string | undefined): timeKey is string;
24
+ /**
25
+ * The dataset as seen by a query on `timeKey`: the selected axis becomes
26
+ * `timeKey` and the rest stay declared, so the view is itself a valid dataset
27
+ * and rebasing it again is a no-op. Throws for an axis the dataset does not
28
+ * declare; validate first to report it as a query error.
29
+ */
30
+ export declare function datasetOnTimeKey<TDataset extends TimeAxes>(dataset: TDataset, timeKey: string | undefined): TDataset;
31
+ export {};
32
+ //# sourceMappingURL=dataset-time-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset-time-keys.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-time-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,UAAU,QAAQ;IAChB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,OAAO,EAAE,QAAQ,GAAG,MAAM,EAAE,CAG3D;AAED,8EAA8E;AAC9E,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAW5F;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,IAAI,MAAM,CAEzG;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,SAAS,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CASpH"}