@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.
- package/dist/aggregations.d.ts +12 -0
- package/dist/aggregations.d.ts.map +1 -1
- package/dist/aggregations.js +32 -0
- package/dist/api.type-test.js +6 -0
- package/dist/catalog.d.ts +8 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +18 -6
- package/dist/contract.d.ts +7 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +5 -1
- package/dist/dataset-query.d.ts.map +1 -1
- package/dist/dataset-query.js +8 -5
- package/dist/dataset.d.ts +4 -3
- package/dist/dataset.d.ts.map +1 -1
- package/dist/dataset.js +5 -4
- package/dist/executor.d.ts +7 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +39 -10
- package/dist/field.d.ts +4 -4
- package/dist/field.d.ts.map +1 -1
- package/dist/in-memory-backend.d.ts +5 -0
- package/dist/in-memory-backend.d.ts.map +1 -1
- package/dist/in-memory-backend.js +118 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/measure.d.ts +20 -0
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +51 -0
- package/dist/query-builder-protocol.d.ts +22 -0
- package/dist/query-builder-protocol.d.ts.map +1 -1
- package/dist/query-planner.d.ts +7 -6
- package/dist/query-planner.d.ts.map +1 -1
- package/dist/query-planner.js +98 -37
- package/dist/relationships.d.ts +9 -9
- package/dist/relationships.d.ts.map +1 -1
- package/dist/semantic-plan.d.ts +54 -9
- package/dist/semantic-plan.d.ts.map +1 -1
- package/dist/semantic-plan.js +9 -0
- package/dist/semantic-planner.d.ts.map +1 -1
- package/dist/semantic-planner.js +73 -5
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +29 -18
- package/dist/types.d.ts +38 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/dataset-normalization.d.ts +1 -1
- package/dist/utils/dataset-normalization.d.ts.map +1 -1
- package/dist/utils/dataset-normalization.js +13 -1
- package/dist/utils/dataset-query-validation.d.ts.map +1 -1
- package/dist/utils/dataset-query-validation.js +50 -12
- package/dist/utils/dataset-validation.d.ts.map +1 -1
- package/dist/utils/dataset-validation.js +21 -1
- package/dist/utils/filtered-aggregation-sql.d.ts +2 -1
- package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -1
- package/dist/utils/filtered-aggregation-sql.js +13 -4
- package/dist/utils/relationship-builder-plan.d.ts +56 -0
- package/dist/utils/relationship-builder-plan.d.ts.map +1 -0
- package/dist/utils/relationship-builder-plan.js +92 -0
- package/dist/utils/relationship-fields.d.ts +62 -0
- package/dist/utils/relationship-fields.d.ts.map +1 -0
- package/dist/utils/relationship-fields.js +98 -0
- package/dist/utils/relationship-validation.d.ts +15 -0
- package/dist/utils/relationship-validation.d.ts.map +1 -0
- package/dist/utils/relationship-validation.js +29 -0
- 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]
|
|
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
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAGhE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAGhE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAGnI,OAAO,EACL,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAC/B,UAAU,EAAE,QAAQ,EACpB,KAAK,EAAE,KAAK,EAAE,IAAI,GACnB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EACzB,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAChC,GAAG,EAAE,IAAI,EACT,MAAM,EAAE,KAAK,GACd,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAGtD,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,8BAA8B,EAAE,MAAM,cAAc,CAAC;AACrG,OAAO,EAAE,+BAA+B,EAAE,MAAM,gCAAgC,CAAC;AACjF,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,GACzB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,yBAAyB,EACzB,oBAAoB,EACpB,YAAY,EACZ,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,gBAAgB,EAChB,gCAAgC,EAChC,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,cAAc,EACd,oBAAoB,GACrB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,YAAY,EACZ,UAAU,GACX,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,mBAAmB,EACnB,oBAAoB,EACpB,eAAe,EACf,2BAA2B,EAC3B,UAAU,EACV,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,YAAY,EAAE,aAAa,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAG/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,YAAY,EACV,kBAAkB,EAClB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,4BAA4B,CAAC;AAIpC,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC5E,YAAY,EACV,QAAQ,EACR,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAG5B,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGxE,YAAY,EACV,gBAAgB,EAChB,uBAAuB,EACvB,6BAA6B,EAC7B,wBAAwB,GACzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAGpE,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGhG,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAGnG,YAAY,EACV,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,wBAAwB,EACxB,qBAAqB,EACrB,wBAAwB,EACxB,yBAAyB,EACzB,aAAa,EACb,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EACjB,qBAAqB,EACrB,0BAA0B,EAC1B,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,UAAU,EACV,aAAa,EACb,qBAAqB,EACrB,cAAc,EACd,SAAS,EACT,YAAY,EACZ,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAC"}
|
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
|
package/dist/measure.d.ts.map
CHANGED
|
@@ -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;
|
|
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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"}
|
package/dist/query-planner.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/query-planner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/relationships.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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;
|
|
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"}
|