@hypequery/datasets 0.8.0 → 0.10.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 (65) hide show
  1. package/dist/aggregations.d.ts +12 -0
  2. package/dist/aggregations.d.ts.map +1 -1
  3. package/dist/aggregations.js +32 -0
  4. package/dist/api.type-test.js +6 -0
  5. package/dist/catalog.d.ts +8 -1
  6. package/dist/catalog.d.ts.map +1 -1
  7. package/dist/catalog.js +18 -6
  8. package/dist/contract.d.ts +7 -1
  9. package/dist/contract.d.ts.map +1 -1
  10. package/dist/contract.js +5 -1
  11. package/dist/dataset-query.d.ts.map +1 -1
  12. package/dist/dataset-query.js +8 -5
  13. package/dist/dataset.d.ts +4 -3
  14. package/dist/dataset.d.ts.map +1 -1
  15. package/dist/dataset.js +5 -4
  16. package/dist/executor.d.ts +7 -1
  17. package/dist/executor.d.ts.map +1 -1
  18. package/dist/executor.js +39 -10
  19. package/dist/field.d.ts +4 -4
  20. package/dist/field.d.ts.map +1 -1
  21. package/dist/in-memory-backend.d.ts +5 -0
  22. package/dist/in-memory-backend.d.ts.map +1 -1
  23. package/dist/in-memory-backend.js +118 -2
  24. package/dist/index.d.ts +4 -3
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +6 -2
  27. package/dist/measure.d.ts +20 -0
  28. package/dist/measure.d.ts.map +1 -1
  29. package/dist/measure.js +51 -0
  30. package/dist/query-builder-protocol.d.ts +22 -0
  31. package/dist/query-builder-protocol.d.ts.map +1 -1
  32. package/dist/query-planner.d.ts +7 -6
  33. package/dist/query-planner.d.ts.map +1 -1
  34. package/dist/query-planner.js +98 -37
  35. package/dist/relationships.d.ts +9 -9
  36. package/dist/relationships.d.ts.map +1 -1
  37. package/dist/semantic-plan.d.ts +54 -9
  38. package/dist/semantic-plan.d.ts.map +1 -1
  39. package/dist/semantic-plan.js +9 -0
  40. package/dist/semantic-planner.d.ts.map +1 -1
  41. package/dist/semantic-planner.js +73 -5
  42. package/dist/tools.d.ts.map +1 -1
  43. package/dist/tools.js +29 -18
  44. package/dist/types.d.ts +38 -13
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/utils/dataset-normalization.d.ts +1 -1
  47. package/dist/utils/dataset-normalization.d.ts.map +1 -1
  48. package/dist/utils/dataset-normalization.js +13 -1
  49. package/dist/utils/dataset-query-validation.d.ts.map +1 -1
  50. package/dist/utils/dataset-query-validation.js +50 -12
  51. package/dist/utils/dataset-validation.d.ts.map +1 -1
  52. package/dist/utils/dataset-validation.js +21 -1
  53. package/dist/utils/filtered-aggregation-sql.d.ts +2 -1
  54. package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -1
  55. package/dist/utils/filtered-aggregation-sql.js +13 -4
  56. package/dist/utils/relationship-builder-plan.d.ts +56 -0
  57. package/dist/utils/relationship-builder-plan.d.ts.map +1 -0
  58. package/dist/utils/relationship-builder-plan.js +92 -0
  59. package/dist/utils/relationship-fields.d.ts +62 -0
  60. package/dist/utils/relationship-fields.d.ts.map +1 -0
  61. package/dist/utils/relationship-fields.js +98 -0
  62. package/dist/utils/relationship-validation.d.ts +15 -0
  63. package/dist/utils/relationship-validation.d.ts.map +1 -0
  64. package/dist/utils/relationship-validation.js +29 -0
  65. package/package.json +1 -1
@@ -44,6 +44,40 @@ function applyTenant(rows, tenant) {
44
44
  }
45
45
  return rows.filter((row) => row[tenant.field] === tenant.value);
46
46
  }
47
+ /**
48
+ * Enriches base rows with columns from to-one joined targets, keyed by the
49
+ * qualified name `<relationship>.<column>`. Mirrors a query-time LEFT JOIN:
50
+ * base rows without a matching target keep the joined columns undefined.
51
+ */
52
+ function applyJoins(rows, joins, tables) {
53
+ if (!joins || joins.length === 0) {
54
+ return rows;
55
+ }
56
+ const indexes = joins.map((join) => {
57
+ const targetRows = applyTenant(tables[join.source] ?? [], join.tenant);
58
+ const index = new Map();
59
+ for (const row of targetRows) {
60
+ const key = String(row[join.to]);
61
+ // To-one: first matching target wins; a mis-declared to-many still won't fan out.
62
+ if (!index.has(key)) {
63
+ index.set(key, row);
64
+ }
65
+ }
66
+ return { join, index };
67
+ });
68
+ return rows.map((row) => {
69
+ const enriched = { ...row };
70
+ for (const { join, index } of indexes) {
71
+ const match = index.get(String(row[join.from]));
72
+ if (match) {
73
+ for (const [column, value] of Object.entries(match)) {
74
+ enriched[`${join.relationship}.${column}`] = value;
75
+ }
76
+ }
77
+ }
78
+ return enriched;
79
+ });
80
+ }
47
81
  function periodForValue(value, grain) {
48
82
  const date = new Date(String(value));
49
83
  if (Number.isNaN(date.getTime())) {
@@ -75,9 +109,63 @@ function groupKey(row, plan) {
75
109
  }
76
110
  return JSON.stringify(parts);
77
111
  }
112
+ /**
113
+ * Percentile by linear interpolation over sorted values. Approximate parity
114
+ * with ClickHouse's sampling-based `quantile`; exact only for clean inputs.
115
+ */
116
+ function percentileOf(values, level) {
117
+ if (values.length === 0) {
118
+ // ClickHouse `quantile` over an empty set yields nan.
119
+ return Number.NaN;
120
+ }
121
+ const sorted = [...values].sort((a, b) => a - b);
122
+ const position = (sorted.length - 1) * level;
123
+ const lower = Math.floor(position);
124
+ const upper = Math.ceil(position);
125
+ if (lower === upper) {
126
+ return sorted[lower];
127
+ }
128
+ return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
129
+ }
130
+ /**
131
+ * Sample variance. Fewer than two values yields NaN, as in ClickHouse:
132
+ * `varSamp`/`stddevSamp` divide by n - 1, so a single row gives nan.
133
+ */
134
+ function sampleVarianceOf(values) {
135
+ if (values.length < 2) {
136
+ return Number.NaN;
137
+ }
138
+ const mean = values.reduce((total, value) => total + value, 0) / values.length;
139
+ const squaredDeviations = values.reduce((total, value) => total + (value - mean) ** 2, 0);
140
+ return squaredDeviations / (values.length - 1);
141
+ }
142
+ /** Value of `field` on the row where `argField` is most extreme; empty → null. */
143
+ function argExtremeOf(rows, field, argField, extreme) {
144
+ let best;
145
+ for (const row of rows) {
146
+ // ClickHouse argMax/argMin skip rows where either the value being
147
+ // returned or the ordering value is NULL ("both `arg` and `val` behave
148
+ // as aggregate functions, they both skip NULL"). Verified against
149
+ // ClickHouse 25.1: argMax(amount, created_at) over
150
+ // [(NULL, '2024-01-05'), (10, '2024-01-01')] returns 10, not NULL.
151
+ if (row[argField] == null || row[field] == null) {
152
+ continue;
153
+ }
154
+ if (best === undefined
155
+ || (extreme === 'max' ? row[argField] > best[argField] : row[argField] < best[argField])) {
156
+ best = row;
157
+ }
158
+ }
159
+ return best?.[field] ?? null;
160
+ }
78
161
  function aggregateRows(rows, aggregation) {
162
+ if ((aggregation.aggregation === 'argMax' || aggregation.aggregation === 'argMin')
163
+ && aggregation.filters?.length) {
164
+ throw new Error(`Measure filters are not supported on ${aggregation.aggregation} aggregations.`);
165
+ }
79
166
  const filteredRows = applyFilters(rows, aggregation.filters ?? []);
80
167
  const values = filteredRows.map((row) => row[aggregation.field]);
168
+ const numericValues = () => values.filter((value) => value != null).map((value) => Number(value));
81
169
  switch (aggregation.aggregation) {
82
170
  case 'sum':
83
171
  return values.reduce((total, value) => total + Number(value ?? 0), 0);
@@ -93,6 +181,23 @@ function aggregateRows(rows, aggregation) {
93
181
  return Math.min(...values.map((value) => Number(value)));
94
182
  case 'max':
95
183
  return Math.max(...values.map((value) => Number(value)));
184
+ case 'argMax':
185
+ case 'argMin': {
186
+ if (!aggregation.argField) {
187
+ throw new Error(`Aggregation "${aggregation.aggregation}" for "${aggregation.name}" requires an argField.`);
188
+ }
189
+ return argExtremeOf(filteredRows, aggregation.field, aggregation.argField, aggregation.aggregation === 'argMax' ? 'max' : 'min');
190
+ }
191
+ case 'percentile': {
192
+ if (aggregation.level == null) {
193
+ throw new Error(`Aggregation "percentile" for "${aggregation.name}" requires a level.`);
194
+ }
195
+ return percentileOf(numericValues(), aggregation.level);
196
+ }
197
+ case 'stddev':
198
+ return Math.sqrt(sampleVarianceOf(numericValues()));
199
+ case 'variance':
200
+ return sampleVarianceOf(numericValues());
96
201
  default:
97
202
  return 0;
98
203
  }
@@ -175,17 +280,28 @@ function serializeMeasures(rows, measures) {
175
280
  return rows.map((row) => {
176
281
  const next = { ...row };
177
282
  for (const name of measures) {
178
- if (next[name] != null) {
283
+ if (typeof next[name] === 'number' && !Number.isFinite(next[name])) {
284
+ // ClickHouse JSON output serializes nan/inf as null
285
+ // (output_format_json_quote_denormals defaults to 0).
286
+ next[name] = null;
287
+ }
288
+ else if (next[name] != null) {
179
289
  next[name] = String(next[name]);
180
290
  }
181
291
  }
182
292
  return next;
183
293
  });
184
294
  }
295
+ /**
296
+ * @deprecated The plan/backend execution path is frozen (bug fixes only); this
297
+ * backend remains as the test harness for that path. New code should use
298
+ * `createDatasetClient({ queryBuilder })`.
299
+ */
185
300
  export function createInMemoryBackend(tables) {
186
301
  function executeAggregate(plan) {
187
302
  const table = tables[plan.source] ?? [];
188
- const filteredRows = applyFilters(applyTenant(table, plan.tenant), plan.filters);
303
+ const joinedRows = applyJoins(table, plan.joins, tables);
304
+ const filteredRows = applyFilters(applyTenant(joinedRows, plan.tenant), plan.filters);
189
305
  const groups = new Map();
190
306
  for (const row of filteredRows) {
191
307
  const key = groupKey(row, plan);
package/dist/index.d.ts CHANGED
@@ -2,11 +2,12 @@ export { dataset } from './dataset.js';
2
2
  export { dimension } from './field.js';
3
3
  export { measure } from './measure.js';
4
4
  export { belongsTo, hasMany, hasOne } from './relationships.js';
5
- export { sum, count, countDistinct, avg, min, max } from './aggregations.js';
5
+ export { sum, count, countDistinct, avg, min, max, percentile, median, argMax, argMin, stddev, variance } from './aggregations.js';
6
6
  export { divide, multiply, subtract, add, nullIfZero, coalesce, round, floor, ceil, } from './formulas.js';
7
7
  export { eq, neq, gt, gte, lt, lte, inList, notInList, between, like, asc, desc, filter, order, } from './query-helpers.js';
8
8
  export { createDatasetRegistry } from './registry.js';
9
- export { getDatasetCatalog, getDatasetCatalogs } from './catalog.js';
9
+ export { getDatasetCatalog, getDatasetCatalogs, getQueryableRelationshipFields } from './catalog.js';
10
+ export { listQueryableRelationshipFields } from './utils/relationship-fields.js';
10
11
  export type { DatasetCatalog, DatasetCatalogMap, DatasetCatalogSource, DimensionCatalogEntry, MeasureCatalogEntry, MetricCatalogEntry, FilterCatalogEntry, RelationshipCatalogEntry, } from './catalog.js';
11
12
  export { serializeSemanticContract, contractToStableJson, hashContract, SEMANTIC_CONTRACT_VERSION, } from './contract.js';
12
13
  export type { SemanticContract, SerializeSemanticContractOptions, ContractDataset, ContractDimension, ContractMeasure, ContractMetric, ContractFilter, ContractRelationship, } from './contract.js';
@@ -26,5 +27,5 @@ export type { QueryBuilderLike, QueryBuilderFactoryLike, QueryBuilderFactoryComp
26
27
  export { toQueryBuilderFactory } from './query-builder-protocol.js';
27
28
  export { validateSQLIdentifier, isSafeSQLIdentifier, quoteSQLIdentifier } from './sql-utils.js';
28
29
  export { GRAIN_FUNCTIONS, SEMANTIC_FILTER_OPERATORS, SUPPORTED_TIME_GRAINS } from './constants.js';
29
- export type { FieldType, DimensionType, DimensionOptions, DimensionDefinition, MeasureOptions, MeasureDefinition, InferDimensionType, RelationshipKind, RelationshipDefinition, AggregationType, MeasureAggregation, AggregationSpec, FormulaExpr, DerivedMetricSpec, TimeGrain, MetricRef, BaseMetricRef, DerivedMetricRef, GrainedMetricRef, MetricContract, MetricFilter, MetricOrderBy, MetricQuery, DatasetQuery, MetricResultMeta, MetricResult, DatasetQueryResult, MetricHandle, ExecutionContext, SemanticExecutionRuntime, SemanticTenantRuntime, SemanticFilterDefinition, SemanticFiltersDefinition, DatasetConfig, DatasetLimits, DatasetInstance, AnyDatasetInstance, BaseMetricConfig, DerivedMetricConfig, DatasetRegistryInstance, DatasetFieldNames, DatasetDimensionNames, DatasetMeasureNames, DatasetOrderableNames, DatasetQueryFor, DatasetRow, DatasetRowFor, DatasetQueryResultFor, MetricQueryFor, MetricRow, MetricRowFor, MetricResultFor, KnownStringKeys, } from './types.js';
30
+ export type { FieldType, DimensionType, DimensionOptions, DimensionDefinition, MeasureOptions, MeasureDefinition, InferDimensionType, RelationshipKind, RelationshipDefinition, AggregationType, MeasureAggregation, AggregationSpec, FormulaExpr, DerivedMetricSpec, TimeGrain, MetricRef, BaseMetricRef, DerivedMetricRef, GrainedMetricRef, MetricContract, MetricFilter, MetricOrderBy, MetricQuery, DatasetQuery, MetricResultMeta, MetricResult, DatasetQueryResult, MetricHandle, ExecutionContext, SemanticExecutionRuntime, SemanticTenantRuntime, SemanticFilterDefinition, SemanticFiltersDefinition, DatasetConfig, DatasetLimits, DatasetInstance, AnyDatasetInstance, BaseMetricConfig, DerivedMetricConfig, DatasetRegistryInstance, DatasetFieldNames, DatasetDimensionNames, DatasetQueryableDimensions, DatasetMeasureNames, DatasetOrderableNames, DatasetQueryFor, DatasetRow, DatasetRowFor, DatasetQueryResultFor, MetricQueryFor, MetricRow, MetricRowFor, MetricResultFor, KnownStringKeys, } from './types.js';
30
31
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ export { measure } from './measure.js';
7
7
  // Relationship helpers
8
8
  export { belongsTo, hasMany, hasOne } from './relationships.js';
9
9
  // Aggregation helpers
10
- export { sum, count, countDistinct, avg, min, max } from './aggregations.js';
10
+ export { sum, count, countDistinct, avg, min, max, percentile, median, argMax, argMin, stddev, variance } from './aggregations.js';
11
11
  // Formula helpers
12
12
  export { divide, multiply, subtract, add, nullIfZero, coalesce, round, floor, ceil, } from './formulas.js';
13
13
  // Semantic query helpers
@@ -15,7 +15,8 @@ export { eq, neq, gt, gte, lt, lte, inList, notInList, between, like, asc, desc,
15
15
  // Registry
16
16
  export { createDatasetRegistry } from './registry.js';
17
17
  // Catalog
18
- export { getDatasetCatalog, getDatasetCatalogs } from './catalog.js';
18
+ export { getDatasetCatalog, getDatasetCatalogs, getQueryableRelationshipFields } from './catalog.js';
19
+ export { listQueryableRelationshipFields } from './utils/relationship-fields.js';
19
20
  // Semantic contract (stable, hashable export for snapshots/diffs/validation)
20
21
  export { serializeSemanticContract, contractToStableJson, hashContract, SEMANTIC_CONTRACT_VERSION, } from './contract.js';
21
22
  // Agent/tool metadata
@@ -25,6 +26,9 @@ export { createDatasetClient } from './executor.js';
25
26
  // Semantic query result cache
26
27
  export { createMemoryCacheStore } from './cache/semantic-query-cache.js';
27
28
  export { buildDatasetQuerySignature, buildMetricQuerySignature, } from './cache/query-signature.js';
29
+ // Plan/backend protocol — FROZEN. Deprecated in favor of the query-builder
30
+ // path (`createDatasetClient({ queryBuilder })`); bug fixes only, no new
31
+ // features. See the deprecation notes on each declaration.
28
32
  export { createInMemoryBackend } from './in-memory-backend.js';
29
33
  export { validateFilterValue, matchesFieldType } from './validation.js';
30
34
  export { toQueryBuilderFactory } from './query-builder-protocol.js';
package/dist/measure.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { MeasureDefinition, MeasureOptions } from './types.js';
2
+ export declare function validatePercentileLevel(level: number): void;
3
+ declare function createPercentileMeasure(field: string, level: number, opts?: MeasureOptions): MeasureDefinition;
2
4
  export declare const measure: {
3
5
  readonly sum: (field: string, opts?: MeasureOptions) => MeasureDefinition;
4
6
  readonly count: (field: string, opts?: MeasureOptions) => MeasureDefinition;
@@ -6,5 +8,23 @@ export declare const measure: {
6
8
  readonly avg: (field: string, opts?: MeasureOptions) => MeasureDefinition;
7
9
  readonly min: (field: string, opts?: MeasureOptions) => MeasureDefinition;
8
10
  readonly max: (field: string, opts?: MeasureOptions) => MeasureDefinition;
11
+ /** Approximate percentile of a numeric field (ClickHouse `quantile(level)`). */
12
+ readonly percentile: typeof createPercentileMeasure;
13
+ /** Median — sugar for `percentile(field, 0.5)`. */
14
+ readonly median: (field: string, opts?: MeasureOptions) => MeasureDefinition;
15
+ /**
16
+ * Value of `field` on the row where `by` is greatest (ClickHouse `argMax`).
17
+ * The runtime value follows `field`'s type; aggregate result columns are
18
+ * exposed as strings to match the dataset result contract.
19
+ * Measure filters are not supported on argMax/argMin.
20
+ */
21
+ readonly argMax: (field: string, by: string, opts?: Omit<MeasureOptions, "filters">) => MeasureDefinition;
22
+ /** Value of `field` on the row where `by` is smallest (ClickHouse `argMin`). */
23
+ readonly argMin: (field: string, by: string, opts?: Omit<MeasureOptions, "filters">) => MeasureDefinition;
24
+ /** Sample standard deviation (ClickHouse `stddevSamp`). */
25
+ readonly stddev: (field: string, opts?: MeasureOptions) => MeasureDefinition;
26
+ /** Sample variance (ClickHouse `varSamp`). */
27
+ readonly variance: (field: string, opts?: MeasureOptions) => MeasureDefinition;
9
28
  };
29
+ export {};
10
30
  //# sourceMappingURL=measure.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"measure.d.ts","sourceRoot":"","sources":["../src/measure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAsB,MAAM,YAAY,CAAC;AAcxF,eAAO,MAAM,OAAO;0BAXH,MAAM,SAAS,cAAc,KAAG,iBAAiB;4BAAjD,MAAM,SAAS,cAAc,KAAG,iBAAiB;oCAAjD,MAAM,SAAS,cAAc,KAAG,iBAAiB;0BAAjD,MAAM,SAAS,cAAc,KAAG,iBAAiB;0BAAjD,MAAM,SAAS,cAAc,KAAG,iBAAiB;0BAAjD,MAAM,SAAS,cAAc,KAAG,iBAAiB;CAkBxD,CAAC"}
1
+ {"version":3,"file":"measure.d.ts","sourceRoot":"","sources":["../src/measure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAsB,MAAM,YAAY,CAAC;AAcxF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAI3D;AAmBD,iBAAS,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,cAAc,GAAG,iBAAiB,CAYvG;AAED,eAAO,MAAM,OAAO;0BAhDH,MAAM,SAAS,cAAc,KAAG,iBAAiB;4BAAjD,MAAM,SAAS,cAAc,KAAG,iBAAiB;oCAAjD,MAAM,SAAS,cAAc,KAAG,iBAAiB;0BAAjD,MAAM,SAAS,cAAc,KAAG,iBAAiB;0BAAjD,MAAM,SAAS,cAAc,KAAG,iBAAiB;0BAAjD,MAAM,SAAS,cAAc,KAAG,iBAAiB;IAuDhE,gFAAgF;;IAEhF,mDAAmD;6BACnC,MAAM,SAAS,cAAc,KAAG,iBAAiB;IAEjE;;;;;OAKG;6BA/CY,MAAM,MAAM,MAAM,SAAS,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,KAAG,iBAAiB;IAiD7F,gFAAgF;6BAjDjE,MAAM,MAAM,MAAM,SAAS,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC,KAAG,iBAAiB;IAmD7F,2DAA2D;6BArE5C,MAAM,SAAS,cAAc,KAAG,iBAAiB;IAuEhE,8CAA8C;+BAvE/B,MAAM,SAAS,cAAc,KAAG,iBAAiB;CAyExD,CAAC"}
package/dist/measure.js CHANGED
@@ -9,6 +9,40 @@ function createMeasureHelper(aggregation) {
9
9
  filters: opts?.filters,
10
10
  });
11
11
  }
12
+ export function validatePercentileLevel(level) {
13
+ if (typeof level !== 'number' || !Number.isFinite(level) || level < 0 || level > 1) {
14
+ throw new Error(`Invalid percentile level ${level}: expected a number between 0 and 1.`);
15
+ }
16
+ }
17
+ function createArgMeasureHelper(aggregation) {
18
+ return (field, by, opts) => {
19
+ if (typeof by !== 'string' || by.trim().length === 0) {
20
+ throw new Error(`measure.${aggregation}("${field}", by) requires a "by" field.`);
21
+ }
22
+ return {
23
+ __type: 'measure_definition',
24
+ aggregation,
25
+ field,
26
+ argField: by,
27
+ sql: opts?.sql,
28
+ label: opts?.label,
29
+ description: opts?.description,
30
+ };
31
+ };
32
+ }
33
+ function createPercentileMeasure(field, level, opts) {
34
+ validatePercentileLevel(level);
35
+ return {
36
+ __type: 'measure_definition',
37
+ aggregation: 'percentile',
38
+ field,
39
+ level,
40
+ sql: opts?.sql,
41
+ label: opts?.label,
42
+ description: opts?.description,
43
+ filters: opts?.filters,
44
+ };
45
+ }
12
46
  export const measure = {
13
47
  sum: createMeasureHelper('sum'),
14
48
  count: createMeasureHelper('count'),
@@ -16,4 +50,21 @@ export const measure = {
16
50
  avg: createMeasureHelper('avg'),
17
51
  min: createMeasureHelper('min'),
18
52
  max: createMeasureHelper('max'),
53
+ /** Approximate percentile of a numeric field (ClickHouse `quantile(level)`). */
54
+ percentile: createPercentileMeasure,
55
+ /** Median — sugar for `percentile(field, 0.5)`. */
56
+ median: (field, opts) => createPercentileMeasure(field, 0.5, opts),
57
+ /**
58
+ * Value of `field` on the row where `by` is greatest (ClickHouse `argMax`).
59
+ * The runtime value follows `field`'s type; aggregate result columns are
60
+ * exposed as strings to match the dataset result contract.
61
+ * Measure filters are not supported on argMax/argMin.
62
+ */
63
+ argMax: createArgMeasureHelper('argMax'),
64
+ /** Value of `field` on the row where `by` is smallest (ClickHouse `argMin`). */
65
+ argMin: createArgMeasureHelper('argMin'),
66
+ /** Sample standard deviation (ClickHouse `stddevSamp`). */
67
+ stddev: createMeasureHelper('stddev'),
68
+ /** Sample variance (ClickHouse `varSamp`). */
69
+ variance: createMeasureHelper('variance'),
19
70
  };
@@ -17,7 +17,24 @@ export interface QueryBuilderLike {
17
17
  avg(column: string, alias?: string): QueryBuilderLike;
18
18
  min(column: string, alias?: string): QueryBuilderLike;
19
19
  max(column: string, alias?: string): QueryBuilderLike;
20
+ /** Value of `column` on the row where `argColumn` is greatest. */
21
+ argMax?(column: string, argColumn: string, alias?: string): QueryBuilderLike;
22
+ /** Value of `column` on the row where `argColumn` is smallest. */
23
+ argMin?(column: string, argColumn: string, alias?: string): QueryBuilderLike;
24
+ /** Approximate percentile of `column` at `level` in [0, 1]. */
25
+ quantile?(column: string, level: number, alias?: string): QueryBuilderLike;
26
+ /** Sample standard deviation of `column`. */
27
+ stddev?(column: string, alias?: string): QueryBuilderLike;
28
+ /** Sample variance of `column`. */
29
+ variance?(column: string, alias?: string): QueryBuilderLike;
20
30
  where(column: string, operator: string, value: unknown): QueryBuilderLike;
31
+ leftJoin(table: string, leftColumn: string, rightColumn: string, alias?: string, on?: QueryBuilderJoinCondition | QueryBuilderJoinCondition[]): QueryBuilderLike;
32
+ /**
33
+ * Optional single-match LEFT JOIN (ClickHouse `LEFT ANY JOIN`). When a
34
+ * builder provides it, relationship joins use it so duplicate target join
35
+ * keys cannot fan out aggregates; otherwise `leftJoin` is used.
36
+ */
37
+ leftAnyJoin?(table: string, leftColumn: string, rightColumn: string, alias?: string, on?: QueryBuilderJoinCondition | QueryBuilderJoinCondition[]): QueryBuilderLike;
21
38
  groupBy(columns: string | string[]): QueryBuilderLike;
22
39
  orderBy(column: string, direction?: 'ASC' | 'DESC'): QueryBuilderLike;
23
40
  limit(count: number): QueryBuilderLike;
@@ -28,6 +45,11 @@ export interface QueryBuilderLike {
28
45
  };
29
46
  execute<T = Record<string, unknown>>(): Promise<T[]>;
30
47
  }
48
+ export interface QueryBuilderJoinCondition {
49
+ column: string;
50
+ operator: string;
51
+ value: unknown;
52
+ }
31
53
  /** A query builder factory (what `createQueryBuilder(config)` returns). */
32
54
  export interface QueryBuilderFactoryLike {
33
55
  table(name: string): QueryBuilderLike;
@@ -1 +1 @@
1
- {"version":3,"file":"query-builder-protocol.d.ts","sourceRoot":"","sources":["../src/query-builder-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,gBAAgB,CAAC;IAGrD,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACtD,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACxD,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IAChE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACtD,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACtD,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAAC;IAGtD,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAAC;IAG1E,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,gBAAgB,CAAC;IAGtD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,gBAAgB,CAAC;IACtE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACvC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAAC;IAGxC,eAAe,IAAI;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC;IAC1D,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;CACtD;AAED,2EAA2E;AAC3E,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACtC,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;CACtF;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,6BAA6B;IAC5C,KAAK,CAAC,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACzD;AAED,iEAAiE;AACjE,MAAM,MAAM,wBAAwB,GAChC,uBAAuB,GACvB,6BAA6B,CAAC;AAElC;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,wBAAwB,GAChC,uBAAuB,CAEzB"}
1
+ {"version":3,"file":"query-builder-protocol.d.ts","sourceRoot":"","sources":["../src/query-builder-protocol.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,16 @@
1
1
  import type { AggregationSpec, AnyDatasetInstance, ExecutionContext, MeasureDefinition, MetricOrderBy, TimeGrain } from "./types.js";
2
2
  import type { QueryBuilderLike } from "./query-builder-protocol.js";
3
+ import { type RelationshipBuilderContext } from './utils/relationship-builder-plan.js';
3
4
  type DatasetShape = AnyDatasetInstance;
4
- export declare function resolveDimensionExpression(ds: DatasetShape, dimensionName: string): string;
5
- export declare function resolveFilterField(ds: DatasetShape, filterField: string): string;
6
- export declare function buildDimensionSelectionPlan(ds: DatasetShape, dimensions: string[], grain: TimeGrain | undefined): {
5
+ export declare function resolveDimensionExpression(ds: DatasetShape, dimensionName: string, joinCtx?: RelationshipBuilderContext): string;
6
+ export declare function resolveFilterField(ds: DatasetShape, filterField: string, joinCtx?: RelationshipBuilderContext): string;
7
+ export declare function buildDimensionSelectionPlan(ds: DatasetShape, dimensions: string[], grain: TimeGrain | undefined, joinCtx?: RelationshipBuilderContext): {
7
8
  selectParts: string[];
8
9
  groupByParts: string[];
9
10
  };
10
- export declare function applyAggregationSpec(qb: QueryBuilderLike, ds: DatasetShape, spec: AggregationSpec, alias: string): QueryBuilderLike;
11
- export declare function applyMeasureDefinition(qb: QueryBuilderLike, ds: DatasetShape, name: string, definition: MeasureDefinition): QueryBuilderLike;
12
- export declare function appendOrderLimitOffset(qb: QueryBuilderLike, orderBy: MetricOrderBy[] | undefined, grain: TimeGrain | undefined, limit?: number, offset?: number): QueryBuilderLike;
11
+ export declare function applyAggregationSpec(qb: QueryBuilderLike, ds: DatasetShape, spec: AggregationSpec, alias: string, joinCtx?: RelationshipBuilderContext): QueryBuilderLike;
12
+ export declare function applyMeasureDefinition(qb: QueryBuilderLike, ds: DatasetShape, name: string, definition: MeasureDefinition, joinCtx?: RelationshipBuilderContext): QueryBuilderLike;
13
+ export declare function appendOrderLimitOffset(qb: QueryBuilderLike, orderBy: MetricOrderBy[] | undefined, grain: TimeGrain | undefined, limit?: number, offset?: number, joinCtx?: RelationshipBuilderContext): QueryBuilderLike;
13
14
  export declare function resolveTenantFilterColumn(ds: DatasetShape, context?: ExecutionContext): string | undefined;
14
15
  export {};
15
16
  //# sourceMappingURL=query-planner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"query-planner.d.ts","sourceRoot":"","sources":["../src/query-planner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,SAAS,EACV,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAKpE,KAAK,YAAY,GAAG,kBAAkB,CAAC;AAMvC,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,YAAY,EAChB,aAAa,EAAE,MAAM,GACpB,MAAM,CAGR;AAED,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,YAAY,EAChB,WAAW,EAAE,MAAM,GAClB,MAAM,CAGR;AAED,wBAAgB,2BAA2B,CACzC,EAAE,EAAE,YAAY,EAChB,UAAU,EAAE,MAAM,EAAE,EACpB,KAAK,EAAE,SAAS,GAAG,SAAS,GAC3B;IAAE,WAAW,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,CAwBnD;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,gBAAgB,EACpB,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,MAAM,GACZ,gBAAgB,CAuBlB;AAED,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,iBAAiB,GAC5B,gBAAgB,CA6BlB;AAED,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,OAAO,EAAE,aAAa,EAAE,GAAG,SAAS,EACpC,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,GACd,gBAAgB,CAiBlB;AAED,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,YAAY,EAChB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAMpB"}
1
+ {"version":3,"file":"query-planner.d.ts","sourceRoot":"","sources":["../src/query-planner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,SAAS,EACV,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAMpE,OAAO,EAGL,KAAK,0BAA0B,EAChC,MAAM,sCAAsC,CAAC;AAI9C,KAAK,YAAY,GAAG,kBAAkB,CAAC;AA6BvC,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,YAAY,EAChB,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,0BAA0B,GACnC,MAAM,CAUR;AAED,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,YAAY,EAChB,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,0BAA0B,GACnC,MAAM,CAMR;AAED,wBAAgB,2BAA2B,CACzC,EAAE,EAAE,YAAY,EAChB,UAAU,EAAE,MAAM,EAAE,EACpB,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,OAAO,CAAC,EAAE,0BAA0B,GACnC;IAAE,WAAW,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,CAkCnD;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,gBAAgB,EACpB,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,0BAA0B,GACnC,gBAAgB,CAgElB;AAED,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,iBAAiB,EAC7B,OAAO,CAAC,EAAE,0BAA0B,GACnC,gBAAgB,CAElB;AAED,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,OAAO,EAAE,aAAa,EAAE,GAAG,SAAS,EACpC,KAAK,EAAE,SAAS,GAAG,SAAS,EAC5B,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,0BAA0B,GACnC,gBAAgB,CAoBlB;AAED,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,YAAY,EAChB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAMpB"}
@@ -1,18 +1,48 @@
1
1
  import { GRAIN_FUNCTIONS } from "./constants.js";
2
2
  import { applyFilteredAggregationExpression } from './utils/filtered-aggregation-sql.js';
3
3
  import { getRuntimeTenantPredicate } from './utils/tenant-runtime.js';
4
+ import { quoteSQLIdentifier } from './sql-utils.js';
5
+ import { isQualifiedField } from './utils/relationship-fields.js';
6
+ import { qualifyBaseColumn, resolveQualifiedColumn, } from './utils/relationship-builder-plan.js';
7
+ import { measureToAggregationSpec } from './utils/dataset-normalization.js';
8
+ import { validatePercentileLevel } from './measure.js';
4
9
  function toOrderDirection(direction) {
5
10
  return direction === 'asc' ? 'ASC' : 'DESC';
6
11
  }
7
- export function resolveDimensionExpression(ds, dimensionName) {
12
+ /**
13
+ * Raw SQL expressions on the base dataset are emitted verbatim, so their column
14
+ * references are not table-qualified. When relationship joins are active a bare
15
+ * `price` in such an expression is ambiguous if the joined table also has a
16
+ * `price` column. Until the builder rewrites identifiers inside expressions,
17
+ * reject the combination rather than emit ambiguous SQL.
18
+ */
19
+ function assertNoRawSqlUnderJoins(kind, name, sql, joinCtx) {
20
+ if (!joinCtx) {
21
+ return;
22
+ }
23
+ throw new Error(`SQL-backed ${kind} "${name}" cannot be combined with relationship joins: its expression ` +
24
+ `("${sql}") is not table-qualified and may collide with joined columns. Query it without ` +
25
+ `relationship-qualified fields, or redeclare it as a plain column.`);
26
+ }
27
+ export function resolveDimensionExpression(ds, dimensionName, joinCtx) {
28
+ if (joinCtx && isQualifiedField(dimensionName)) {
29
+ return resolveQualifiedColumn(ds, dimensionName);
30
+ }
8
31
  const definition = ds.dimensions[dimensionName];
9
- return definition?.sql ?? definition?.column ?? dimensionName;
32
+ if (definition?.sql) {
33
+ assertNoRawSqlUnderJoins('dimension', dimensionName, definition.sql, joinCtx);
34
+ return definition.sql;
35
+ }
36
+ return qualifyBaseColumn(joinCtx, definition?.column ?? dimensionName);
10
37
  }
11
- export function resolveFilterField(ds, filterField) {
38
+ export function resolveFilterField(ds, filterField, joinCtx) {
39
+ if (joinCtx && isQualifiedField(filterField)) {
40
+ return resolveQualifiedColumn(ds, filterField);
41
+ }
12
42
  const resolvedField = ds.filters[filterField]?.field ?? filterField;
13
- return resolveDimensionExpression(ds, resolvedField);
43
+ return resolveDimensionExpression(ds, resolvedField, joinCtx);
14
44
  }
15
- export function buildDimensionSelectionPlan(ds, dimensions, grain) {
45
+ export function buildDimensionSelectionPlan(ds, dimensions, grain, joinCtx) {
16
46
  const selectParts = [];
17
47
  const groupByParts = new Set();
18
48
  if (grain) {
@@ -20,23 +50,36 @@ export function buildDimensionSelectionPlan(ds, dimensions, grain) {
20
50
  if (!fn) {
21
51
  throw new Error(`Unsupported time grain "${grain}".`);
22
52
  }
23
- selectParts.push(`${fn}(${ds.timeKey}) AS period`);
53
+ selectParts.push(`${fn}(${qualifyBaseColumn(joinCtx, String(ds.timeKey))}) AS period`);
24
54
  groupByParts.add("period");
25
55
  }
26
56
  for (const dimensionName of dimensions) {
27
- const expression = resolveDimensionExpression(ds, dimensionName);
28
- if (expression === dimensionName) {
57
+ const expression = resolveDimensionExpression(ds, dimensionName, joinCtx);
58
+ if (joinCtx) {
59
+ // With joins in scope, every selection is table-qualified and aliased so
60
+ // grouping/ordering can reference the alias unambiguously.
61
+ const alias = isQualifiedField(dimensionName)
62
+ ? quoteSQLIdentifier(dimensionName)
63
+ : dimensionName;
64
+ selectParts.push(`${expression} AS ${alias}`);
65
+ groupByParts.add(alias);
66
+ }
67
+ else if (expression === dimensionName) {
29
68
  selectParts.push(dimensionName);
69
+ groupByParts.add(dimensionName);
30
70
  }
31
71
  else {
32
72
  selectParts.push(`${expression} AS ${dimensionName}`);
73
+ groupByParts.add(dimensionName);
33
74
  }
34
- groupByParts.add(dimensionName);
35
75
  }
36
76
  return { selectParts, groupByParts: Array.from(groupByParts) };
37
77
  }
38
- export function applyAggregationSpec(qb, ds, spec, alias) {
39
- const fieldOrExpr = applyFilteredAggregationExpression(ds, spec, spec.sql ?? resolveDimensionExpression(ds, spec.field));
78
+ export function applyAggregationSpec(qb, ds, spec, alias, joinCtx) {
79
+ if (spec.sql) {
80
+ assertNoRawSqlUnderJoins('measure', alias, spec.sql, joinCtx);
81
+ }
82
+ const fieldOrExpr = applyFilteredAggregationExpression(ds, spec, spec.sql ?? resolveDimensionExpression(ds, spec.field, joinCtx), joinCtx);
40
83
  switch (spec.aggregation) {
41
84
  case "sum":
42
85
  return qb.sum(fieldOrExpr, alias);
@@ -50,39 +93,57 @@ export function applyAggregationSpec(qb, ds, spec, alias) {
50
93
  return qb.min(fieldOrExpr, alias);
51
94
  case "max":
52
95
  return qb.max(fieldOrExpr, alias);
96
+ case "argMax":
97
+ case "argMin": {
98
+ if (!spec.argField) {
99
+ throw new Error(`Aggregation "${spec.aggregation}" for "${alias}" requires an argField ("by" column).`);
100
+ }
101
+ const argExpr = resolveDimensionExpression(ds, spec.argField, joinCtx);
102
+ if (spec.aggregation === "argMax") {
103
+ if (!qb.argMax) {
104
+ throw new Error('Query builder does not support argMax aggregations.');
105
+ }
106
+ return qb.argMax(fieldOrExpr, argExpr, alias);
107
+ }
108
+ if (!qb.argMin) {
109
+ throw new Error('Query builder does not support argMin aggregations.');
110
+ }
111
+ return qb.argMin(fieldOrExpr, argExpr, alias);
112
+ }
113
+ case "percentile": {
114
+ if (spec.level == null) {
115
+ throw new Error(`Aggregation "percentile" for "${alias}" requires a level.`);
116
+ }
117
+ validatePercentileLevel(spec.level);
118
+ if (!qb.quantile) {
119
+ throw new Error('Query builder does not support percentile aggregations.');
120
+ }
121
+ return qb.quantile(fieldOrExpr, spec.level, alias);
122
+ }
123
+ case "stddev":
124
+ if (!qb.stddev) {
125
+ throw new Error('Query builder does not support stddev aggregations.');
126
+ }
127
+ return qb.stddev(fieldOrExpr, alias);
128
+ case "variance":
129
+ if (!qb.variance) {
130
+ throw new Error('Query builder does not support variance aggregations.');
131
+ }
132
+ return qb.variance(fieldOrExpr, alias);
53
133
  default:
54
134
  throw new Error(`Unknown aggregation type: ${spec.aggregation}`);
55
135
  }
56
136
  }
57
- export function applyMeasureDefinition(qb, ds, name, definition) {
58
- const baseFieldOrExpr = definition.sql ?? resolveDimensionExpression(ds, definition.field);
59
- const fieldOrExpr = applyFilteredAggregationExpression(ds, {
60
- __type: 'aggregation_spec',
61
- aggregation: definition.aggregation,
62
- field: definition.field,
63
- filters: definition.filters,
64
- }, baseFieldOrExpr);
65
- switch (definition.aggregation) {
66
- case "sum":
67
- return qb.sum(fieldOrExpr, name);
68
- case "count":
69
- return qb.count(fieldOrExpr, name);
70
- case "countDistinct":
71
- return qb.countDistinct(fieldOrExpr, name);
72
- case "avg":
73
- return qb.avg(fieldOrExpr, name);
74
- case "min":
75
- return qb.min(fieldOrExpr, name);
76
- case "max":
77
- return qb.max(fieldOrExpr, name);
78
- default:
79
- throw new Error(`Unsupported measure aggregation: ${definition.aggregation}`);
80
- }
137
+ export function applyMeasureDefinition(qb, ds, name, definition, joinCtx) {
138
+ return applyAggregationSpec(qb, ds, measureToAggregationSpec(name, definition), name, joinCtx);
81
139
  }
82
- export function appendOrderLimitOffset(qb, orderBy, grain, limit, offset) {
140
+ export function appendOrderLimitOffset(qb, orderBy, grain, limit, offset, joinCtx) {
83
141
  if (orderBy && orderBy.length > 0) {
84
142
  for (const order of orderBy) {
85
- qb = qb.orderBy(order.field, toOrderDirection(order.direction));
143
+ const column = joinCtx && isQualifiedField(order.field)
144
+ ? quoteSQLIdentifier(order.field)
145
+ : order.field;
146
+ qb = qb.orderBy(column, toOrderDirection(order.direction));
86
147
  }
87
148
  }
88
149
  else if (grain) {
@@ -18,27 +18,27 @@
18
18
  */
19
19
  import type { RelationshipDefinition } from './types.js';
20
20
  /** Many-to-one relationship (FK on this table). */
21
- export declare function belongsTo(target: () => {
21
+ export declare function belongsTo<TTarget extends {
22
22
  __type: 'dataset';
23
23
  name: string;
24
- }, join: {
24
+ }>(target: () => TTarget, join: {
25
25
  from: string;
26
26
  to: string;
27
- }): RelationshipDefinition;
27
+ }): RelationshipDefinition<TTarget, 'belongsTo'>;
28
28
  /** One-to-many relationship (FK on target table). */
29
- export declare function hasMany(target: () => {
29
+ export declare function hasMany<TTarget extends {
30
30
  __type: 'dataset';
31
31
  name: string;
32
- }, join: {
32
+ }>(target: () => TTarget, join: {
33
33
  from: string;
34
34
  to: string;
35
- }): RelationshipDefinition;
35
+ }): RelationshipDefinition<TTarget, 'hasMany'>;
36
36
  /** One-to-one relationship (FK on target table). */
37
- export declare function hasOne(target: () => {
37
+ export declare function hasOne<TTarget extends {
38
38
  __type: 'dataset';
39
39
  name: string;
40
- }, join: {
40
+ }>(target: () => TTarget, join: {
41
41
  from: string;
42
42
  to: string;
43
- }): RelationshipDefinition;
43
+ }): RelationshipDefinition<TTarget, 'hasOne'>;
44
44
  //# sourceMappingURL=relationships.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"relationships.d.ts","sourceRoot":"","sources":["../src/relationships.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAoB,MAAM,YAAY,CAAC;AAgB3E,mDAAmD;AACnD,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACjD,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAExB;AAED,qDAAqD;AACrD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACjD,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAExB;AAED,oDAAoD;AACpD,wBAAgB,MAAM,CACpB,MAAM,EAAE,MAAM;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACjD,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAExB"}
1
+ {"version":3,"file":"relationships.d.ts","sourceRoot":"","sources":["../src/relationships.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAoB,MAAM,YAAY,CAAC;AAmB3E,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,OAAO,SAAS;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC3E,MAAM,EAAE,MAAM,OAAO,EACrB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAAC,OAAO,EAAE,WAAW,CAAC,CAE9C;AAED,qDAAqD;AACrD,wBAAgB,OAAO,CAAC,OAAO,SAAS;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACzE,MAAM,EAAE,MAAM,OAAO,EACrB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAAC,OAAO,EAAE,SAAS,CAAC,CAE5C;AAED,oDAAoD;AACpD,wBAAgB,MAAM,CAAC,OAAO,SAAS;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACxE,MAAM,EAAE,MAAM,OAAO,EACrB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAE3C"}