@hypequery/datasets 0.1.0 → 0.2.1
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/README.md +498 -0
- package/dist/api.type-test.d.ts +2 -0
- package/dist/api.type-test.d.ts.map +1 -0
- package/dist/api.type-test.js +103 -0
- package/dist/catalog.d.ts +68 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +105 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +26 -0
- package/dist/dataset-query.d.ts +16 -0
- package/dist/dataset-query.d.ts.map +1 -0
- package/dist/dataset-query.js +56 -0
- package/dist/dataset.d.ts +1 -1
- package/dist/dataset.d.ts.map +1 -1
- package/dist/dataset.js +22 -157
- package/dist/executor.d.ts +42 -14
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +188 -36
- package/dist/formulas.d.ts +1 -1
- package/dist/formulas.d.ts.map +1 -1
- package/dist/formulas.js +27 -12
- package/dist/in-memory-backend.d.ts +5 -0
- package/dist/in-memory-backend.d.ts.map +1 -0
- package/dist/in-memory-backend.js +221 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/dist/internal.d.ts +23 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +19 -0
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +1 -0
- package/dist/query-builder-protocol.d.ts +2 -2
- package/dist/query-builder-protocol.d.ts.map +1 -1
- package/dist/query-builder-protocol.js +1 -1
- package/dist/query-helpers.d.ts +12 -12
- package/dist/query-helpers.d.ts.map +1 -1
- package/dist/query-planner.d.ts +9 -7
- package/dist/query-planner.d.ts.map +1 -1
- package/dist/query-planner.js +26 -9
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +1 -1
- package/dist/relationships.d.ts +1 -1
- package/dist/relationships.js +1 -1
- package/dist/semantic-plan.d.ts +82 -0
- package/dist/semantic-plan.d.ts.map +1 -0
- package/dist/semantic-plan.js +1 -0
- package/dist/semantic-planner.d.ts +5 -0
- package/dist/semantic-planner.d.ts.map +1 -0
- package/dist/semantic-planner.js +155 -0
- package/dist/sql-utils.d.ts +1 -1
- package/dist/sql-utils.js +4 -4
- package/dist/tools.d.ts +53 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +322 -0
- package/dist/types.d.ts +130 -52
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/dataset-contract.d.ts +3 -0
- package/dist/utils/dataset-contract.d.ts.map +1 -0
- package/dist/utils/dataset-contract.js +30 -0
- package/dist/utils/dataset-metric-ref.d.ts +9 -0
- package/dist/utils/dataset-metric-ref.d.ts.map +1 -0
- package/dist/utils/dataset-metric-ref.js +39 -0
- package/dist/utils/dataset-normalization.d.ts +10 -0
- package/dist/utils/dataset-normalization.d.ts.map +1 -0
- package/dist/utils/dataset-normalization.js +35 -0
- package/dist/utils/dataset-query-validation.d.ts +4 -0
- package/dist/utils/dataset-query-validation.d.ts.map +1 -0
- package/dist/utils/dataset-query-validation.js +96 -0
- package/dist/utils/dataset-validation.d.ts +6 -0
- package/dist/utils/dataset-validation.d.ts.map +1 -0
- package/dist/utils/dataset-validation.js +42 -0
- package/dist/utils/derived-cte-validation.d.ts +3 -0
- package/dist/utils/derived-cte-validation.d.ts.map +1 -0
- package/dist/utils/derived-cte-validation.js +32 -0
- package/dist/utils/filtered-aggregation-sql.d.ts +5 -0
- package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -0
- package/dist/utils/filtered-aggregation-sql.js +73 -0
- package/dist/utils/metric-handle.d.ts +11 -0
- package/dist/utils/metric-handle.d.ts.map +1 -0
- package/dist/utils/metric-handle.js +36 -0
- package/dist/utils/pagination.d.ts +17 -0
- package/dist/utils/pagination.d.ts.map +1 -0
- package/dist/utils/pagination.js +23 -0
- package/dist/utils/tenant-runtime.d.ts +14 -0
- package/dist/utils/tenant-runtime.d.ts.map +1 -0
- package/dist/utils/tenant-runtime.js +36 -0
- package/package.json +14 -2
package/dist/executor.js
CHANGED
|
@@ -1,34 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Semantic dataset client internals.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* executor builds queries via the builder's fluent API, then calls `.execute()`.
|
|
7
|
-
* 2. **Raw adapter** (deprecated) — pass a `MetricAdapter` with a `rawQuery` function
|
|
8
|
-
* and the executor generates SQL strings manually.
|
|
9
|
-
*
|
|
10
|
-
* The executor stays DB-agnostic via duck-typed protocol interfaces.
|
|
4
|
+
* Public callers use `createDatasetClient(...).execute(...)`. The implementation
|
|
5
|
+
* stays database-agnostic via query-builder and backend protocol interfaces.
|
|
11
6
|
*/
|
|
12
7
|
import { validateSQLIdentifier } from './sql-utils.js';
|
|
8
|
+
import { SUPPORTED_TIME_GRAINS, isSupportedTimeGrain } from './constants.js';
|
|
13
9
|
import { validateFilterValue } from './validation.js';
|
|
14
10
|
import { applyAggregationSpec, appendOrderLimitOffset, buildDimensionSelectionPlan, resolveFilterField, resolveTenantFilterColumn, } from './query-planner.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
import { assertMetricHandle, getMetricGrain, getMetricRef, isTenantScopedFilter, } from './utils/metric-handle.js';
|
|
12
|
+
import { validateDerivedCteGrouping } from './utils/derived-cte-validation.js';
|
|
13
|
+
import { buildDatasetQueryBuilder, runDatasetQuery, validateDatasetQuery, } from './dataset-query.js';
|
|
14
|
+
import { buildDatasetPlan, buildMetricPlan, } from './semantic-planner.js';
|
|
15
|
+
import { getRuntimeTenantId, getRuntimeTenantPredicate, validateTenantRuntime, } from './utils/tenant-runtime.js';
|
|
16
|
+
import { applyPagination, overfetchLimit } from './utils/pagination.js';
|
|
17
|
+
function validateQuery(metric, query, context) {
|
|
19
18
|
const errors = [];
|
|
20
|
-
const ref = metric
|
|
19
|
+
const ref = getMetricRef(metric);
|
|
21
20
|
const ds = ref.dataset;
|
|
22
21
|
const dimensionNames = Object.keys(ds.dimensions);
|
|
23
22
|
const filterNames = Object.keys(ds.filters).length > 0
|
|
24
23
|
? Object.keys(ds.filters)
|
|
25
24
|
: dimensionNames;
|
|
26
|
-
const grain = metric
|
|
25
|
+
const grain = getMetricGrain(metric, query);
|
|
27
26
|
const orderableFields = new Set([
|
|
28
27
|
...(query.dimensions ?? []),
|
|
29
28
|
ref.name,
|
|
30
29
|
...(grain ? ['period'] : []),
|
|
31
30
|
]);
|
|
31
|
+
const tenantRuntimeError = validateTenantRuntime(ds, context);
|
|
32
|
+
if (tenantRuntimeError) {
|
|
33
|
+
errors.push(tenantRuntimeError);
|
|
34
|
+
}
|
|
32
35
|
if (metric.__type === 'grained_metric_ref' && query.by && query.by !== metric.grain) {
|
|
33
36
|
errors.push(`Metric "${ref.name}" is already grained by "${metric.grain}" and cannot be queried with by="${query.by}".`);
|
|
34
37
|
}
|
|
@@ -50,6 +53,10 @@ function validateQuery(metric, query) {
|
|
|
50
53
|
continue;
|
|
51
54
|
}
|
|
52
55
|
const resolvedField = ds.filters[filter.field]?.field ?? filter.field;
|
|
56
|
+
if (isTenantScopedFilter(ds, filter, context)) {
|
|
57
|
+
errors.push(`Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
53
60
|
const fieldType = ds.dimensions[resolvedField]?.fieldType;
|
|
54
61
|
if (fieldType) {
|
|
55
62
|
const filterError = validateFilterValue(filter, fieldType);
|
|
@@ -68,6 +75,16 @@ function validateQuery(metric, query) {
|
|
|
68
75
|
if (query.by && !ds.timeKey) {
|
|
69
76
|
errors.push(`Cannot use "by" grain — dataset "${ds.name}" has no timeKey.`);
|
|
70
77
|
}
|
|
78
|
+
// Validate grain is one the planner can bucket on
|
|
79
|
+
if (query.by && !isSupportedTimeGrain(query.by)) {
|
|
80
|
+
errors.push(`Unsupported time grain "${query.by}". Supported: ${SUPPORTED_TIME_GRAINS.join(', ')}`);
|
|
81
|
+
}
|
|
82
|
+
if (query.limit != null && (!Number.isInteger(query.limit) || query.limit < 0)) {
|
|
83
|
+
errors.push(`Invalid limit: expected a non-negative integer.`);
|
|
84
|
+
}
|
|
85
|
+
if (query.offset != null && (!Number.isInteger(query.offset) || query.offset < 0)) {
|
|
86
|
+
errors.push(`Invalid offset: expected a non-negative integer.`);
|
|
87
|
+
}
|
|
71
88
|
// Validate limits
|
|
72
89
|
if (ds.limits) {
|
|
73
90
|
if (ds.limits.maxDimensions && (query.dimensions?.length ?? 0) > ds.limits.maxDimensions) {
|
|
@@ -79,10 +96,16 @@ function validateQuery(metric, query) {
|
|
|
79
96
|
if (ds.limits.maxFilters && (query.filters?.length ?? 0) > ds.limits.maxFilters) {
|
|
80
97
|
errors.push(`Too many filters (${query.filters?.length}). Max: ${ds.limits.maxFilters}`);
|
|
81
98
|
}
|
|
99
|
+
if (ds.limits.maxResultSize && query.limit != null && query.limit > ds.limits.maxResultSize) {
|
|
100
|
+
errors.push(`Too many results requested (${query.limit}). Max: ${ds.limits.maxResultSize}`);
|
|
101
|
+
}
|
|
82
102
|
}
|
|
83
103
|
return { valid: errors.length === 0, errors };
|
|
84
104
|
}
|
|
85
|
-
|
|
105
|
+
function isDatasetInstance(target) {
|
|
106
|
+
return !!target && typeof target === 'object' && '__type' in target && target.__type === 'dataset';
|
|
107
|
+
}
|
|
108
|
+
export class MetricQueryEngine {
|
|
86
109
|
builderFactory;
|
|
87
110
|
constructor(options) {
|
|
88
111
|
this.builderFactory = options.builderFactory;
|
|
@@ -94,7 +117,8 @@ export class MetricExecutor {
|
|
|
94
117
|
* Execute a metric query. Generates SQL, applies tenant/filter context, executes.
|
|
95
118
|
*/
|
|
96
119
|
async run(metric, query = {}, context) {
|
|
97
|
-
|
|
120
|
+
assertMetricHandle(metric);
|
|
121
|
+
const validation = this.validate(metric, query, context);
|
|
98
122
|
if (!validation.valid) {
|
|
99
123
|
throw new Error(`Invalid metric query: ${validation.errors.join('; ')}`);
|
|
100
124
|
}
|
|
@@ -105,8 +129,13 @@ export class MetricExecutor {
|
|
|
105
129
|
* Generate SQL without executing.
|
|
106
130
|
*/
|
|
107
131
|
toSQL(metric, query = {}, context) {
|
|
108
|
-
|
|
109
|
-
const
|
|
132
|
+
assertMetricHandle(metric);
|
|
133
|
+
const validation = this.validate(metric, query, context);
|
|
134
|
+
if (!validation.valid) {
|
|
135
|
+
throw new Error(`Invalid metric query: ${validation.errors.join('; ')}`);
|
|
136
|
+
}
|
|
137
|
+
const ref = getMetricRef(metric);
|
|
138
|
+
const grain = getMetricGrain(metric, query);
|
|
110
139
|
const spec = ref.spec;
|
|
111
140
|
if (spec.__type === 'derived_metric_spec') {
|
|
112
141
|
return this.buildDerivedSQLViaBuilder(ref, spec, query, grain, context).sql;
|
|
@@ -117,30 +146,55 @@ export class MetricExecutor {
|
|
|
117
146
|
/**
|
|
118
147
|
* Validate a metric query against the metric's contract.
|
|
119
148
|
*/
|
|
120
|
-
validate(metric, query) {
|
|
121
|
-
|
|
149
|
+
validate(metric, query, context) {
|
|
150
|
+
assertMetricHandle(metric);
|
|
151
|
+
const queryValidation = validateQuery(metric, query, context);
|
|
152
|
+
if (!queryValidation.valid) {
|
|
153
|
+
return queryValidation;
|
|
154
|
+
}
|
|
155
|
+
const ref = getMetricRef(metric);
|
|
156
|
+
const grain = getMetricGrain(metric, query);
|
|
157
|
+
try {
|
|
158
|
+
if (ref.spec.__type === 'derived_metric_spec') {
|
|
159
|
+
this.buildDerivedSQLViaBuilder(ref, ref.spec, query, grain, context);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
this.buildBaseQuery(ref, ref.spec, ref.dataset, query, grain, context).toSQLWithParams();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
return {
|
|
167
|
+
valid: false,
|
|
168
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return queryValidation;
|
|
122
172
|
}
|
|
123
173
|
// ---------------------------------------------------------------------------
|
|
124
174
|
// Query builder path
|
|
125
175
|
// ---------------------------------------------------------------------------
|
|
126
176
|
async runViaBuilder(metric, query, context, start) {
|
|
127
|
-
const ref = metric
|
|
128
|
-
const grain = metric
|
|
177
|
+
const ref = getMetricRef(metric);
|
|
178
|
+
const grain = getMetricGrain(metric, query);
|
|
129
179
|
const spec = ref.spec;
|
|
130
180
|
const activeBuilderFactory = context?.runtime?.builderFactory ?? this.builderFactory;
|
|
181
|
+
// Over-fetch one row so we can report `hasMore` without a count query.
|
|
182
|
+
const buildQuery = { ...query, limit: overfetchLimit(query.limit) };
|
|
131
183
|
if (spec.__type === 'derived_metric_spec') {
|
|
132
184
|
// Derived metrics: build CTE via builder, outer query via string, execute via rawQuery
|
|
133
|
-
const { sql, params } = this.buildDerivedSQLViaBuilder(ref, spec,
|
|
134
|
-
const
|
|
185
|
+
const { sql, params } = this.buildDerivedSQLViaBuilder(ref, spec, buildQuery, grain, context);
|
|
186
|
+
const rows = await activeBuilderFactory.rawQuery(sql, params);
|
|
135
187
|
const timingMs = Date.now() - start;
|
|
136
|
-
|
|
188
|
+
const { data, pagination } = applyPagination(rows, query.limit, query.offset);
|
|
189
|
+
return { data, meta: { sql, timingMs, tenant: getRuntimeTenantId(context), rowCount: data.length, pagination } };
|
|
137
190
|
}
|
|
138
191
|
// Base metrics: fully use the builder's execute()
|
|
139
|
-
const builder = this.buildBaseQuery(ref, spec, ref.dataset,
|
|
192
|
+
const builder = this.buildBaseQuery(ref, spec, ref.dataset, buildQuery, grain, context);
|
|
140
193
|
const { sql } = builder.toSQLWithParams();
|
|
141
|
-
const
|
|
194
|
+
const rows = await builder.execute();
|
|
142
195
|
const timingMs = Date.now() - start;
|
|
143
|
-
|
|
196
|
+
const { data, pagination } = applyPagination(rows, query.limit, query.offset);
|
|
197
|
+
return { data, meta: { sql, timingMs, tenant: getRuntimeTenantId(context), rowCount: data.length, pagination } };
|
|
144
198
|
}
|
|
145
199
|
buildBaseQuery(ref, spec, ds, query, grain, context) {
|
|
146
200
|
const activeBuilderFactory = context?.runtime?.builderFactory ?? this.builderFactory;
|
|
@@ -157,13 +211,15 @@ export class MetricExecutor {
|
|
|
157
211
|
}
|
|
158
212
|
// Tenant auto-injection
|
|
159
213
|
const tenantColumn = resolveTenantFilterColumn(ds, context);
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
qb = qb.where(tenantColumn, 'eq', tenantId);
|
|
214
|
+
const tenantPredicate = getRuntimeTenantPredicate(context);
|
|
215
|
+
if (tenantPredicate && tenantColumn) {
|
|
216
|
+
qb = qb.where(tenantColumn, tenantPredicate.operator, tenantPredicate.value);
|
|
164
217
|
}
|
|
165
218
|
// User filters
|
|
166
219
|
for (const filter of query.filters ?? []) {
|
|
220
|
+
if (isTenantScopedFilter(ds, filter, context)) {
|
|
221
|
+
throw new Error(`Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`);
|
|
222
|
+
}
|
|
167
223
|
const resolvedField = resolveFilterField(ds, filter.field);
|
|
168
224
|
qb = qb.where(resolvedField, filter.operator, filter.value);
|
|
169
225
|
}
|
|
@@ -182,6 +238,7 @@ export class MetricExecutor {
|
|
|
182
238
|
}
|
|
183
239
|
// Base aggregations
|
|
184
240
|
const refAliases = {};
|
|
241
|
+
const aggregateAliases = [];
|
|
185
242
|
for (const [alias, baseMetric] of Object.entries(spec.uses)) {
|
|
186
243
|
const baseSpec = baseMetric.spec;
|
|
187
244
|
if (baseSpec.__type !== 'aggregation_spec') {
|
|
@@ -189,22 +246,29 @@ export class MetricExecutor {
|
|
|
189
246
|
}
|
|
190
247
|
cteBuilder = applyAggregationSpec(cteBuilder, ds, baseSpec, alias);
|
|
191
248
|
refAliases[alias] = alias;
|
|
249
|
+
aggregateAliases.push(alias);
|
|
192
250
|
}
|
|
193
251
|
if (groupByParts.length > 0) {
|
|
194
252
|
cteBuilder = cteBuilder.groupBy(groupByParts);
|
|
195
253
|
}
|
|
196
254
|
// Filters on CTE
|
|
197
255
|
const tenantColumn = resolveTenantFilterColumn(ds, context);
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
cteBuilder = cteBuilder.where(tenantColumn, 'eq', tenantId);
|
|
256
|
+
const tenantPredicate = getRuntimeTenantPredicate(context);
|
|
257
|
+
if (tenantPredicate && tenantColumn) {
|
|
258
|
+
cteBuilder = cteBuilder.where(tenantColumn, tenantPredicate.operator, tenantPredicate.value);
|
|
202
259
|
}
|
|
203
260
|
for (const filter of query.filters ?? []) {
|
|
261
|
+
if (isTenantScopedFilter(ds, filter, context)) {
|
|
262
|
+
throw new Error(`Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`);
|
|
263
|
+
}
|
|
204
264
|
const resolvedField = resolveFilterField(ds, filter.field);
|
|
205
265
|
cteBuilder = cteBuilder.where(resolvedField, filter.operator, filter.value);
|
|
206
266
|
}
|
|
207
267
|
const { sql: cteSql, parameters: cteParams } = cteBuilder.toSQLWithParams();
|
|
268
|
+
const groupingErrors = validateDerivedCteGrouping(cteSql, aggregateAliases, groupByParts);
|
|
269
|
+
if (groupingErrors.length > 0) {
|
|
270
|
+
throw new Error(groupingErrors.join('; '));
|
|
271
|
+
}
|
|
208
272
|
// Outer query: trivial SELECT from the CTE — stays as string concat
|
|
209
273
|
// because table('base') would fail schema typing
|
|
210
274
|
const outerSelectParts = [];
|
|
@@ -242,3 +306,91 @@ export class MetricExecutor {
|
|
|
242
306
|
return { sql, params: cteParams };
|
|
243
307
|
}
|
|
244
308
|
}
|
|
309
|
+
export class DatasetClientImpl extends MetricQueryEngine {
|
|
310
|
+
backend;
|
|
311
|
+
constructor(options) {
|
|
312
|
+
if (!options.queryBuilder && !options.backend) {
|
|
313
|
+
throw new Error('createDatasetClient requires either queryBuilder or backend.');
|
|
314
|
+
}
|
|
315
|
+
super({
|
|
316
|
+
builderFactory: options.queryBuilder ?? {
|
|
317
|
+
table() {
|
|
318
|
+
throw new Error('This dataset client was created with a semantic backend, not a query builder.');
|
|
319
|
+
},
|
|
320
|
+
async rawQuery() {
|
|
321
|
+
throw new Error('This dataset client was created with a semantic backend, not a query builder.');
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
this.backend = options.backend;
|
|
326
|
+
}
|
|
327
|
+
planMetric(metric, query = {}, context) {
|
|
328
|
+
assertMetricHandle(metric);
|
|
329
|
+
const validation = validateQuery(metric, query, context);
|
|
330
|
+
if (!validation.valid) {
|
|
331
|
+
throw new Error(`Invalid metric query: ${validation.errors.join('; ')}`);
|
|
332
|
+
}
|
|
333
|
+
return buildMetricPlan(metric, query, context);
|
|
334
|
+
}
|
|
335
|
+
planDataset(ds, query = {}, context) {
|
|
336
|
+
return buildDatasetPlan(ds, query, context);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Execute a semantic target.
|
|
340
|
+
*/
|
|
341
|
+
execute(target, query = {}, context) {
|
|
342
|
+
if (isDatasetInstance(target)) {
|
|
343
|
+
return this.executeDataset(target, query, context);
|
|
344
|
+
}
|
|
345
|
+
return this.executeMetric(target, query, context);
|
|
346
|
+
}
|
|
347
|
+
toSQL(target, query = {}, context) {
|
|
348
|
+
if (isDatasetInstance(target)) {
|
|
349
|
+
return this.toDatasetSQL(target, query, context);
|
|
350
|
+
}
|
|
351
|
+
return super.toSQL(target, query, context);
|
|
352
|
+
}
|
|
353
|
+
validate(target, query = {}, context) {
|
|
354
|
+
if (isDatasetInstance(target)) {
|
|
355
|
+
return validateDatasetQuery(target, query, context);
|
|
356
|
+
}
|
|
357
|
+
return super.validate(target, query, context);
|
|
358
|
+
}
|
|
359
|
+
executeMetric(metric, query, context) {
|
|
360
|
+
if (this.backend) {
|
|
361
|
+
const validation = validateQuery(metric, query, context);
|
|
362
|
+
if (!validation.valid) {
|
|
363
|
+
throw new Error(`Invalid metric query: ${validation.errors.join('; ')}`);
|
|
364
|
+
}
|
|
365
|
+
return this.backend.execute(this.planMetric(metric, query, context));
|
|
366
|
+
}
|
|
367
|
+
return this.run(metric, query, context);
|
|
368
|
+
}
|
|
369
|
+
executeDataset(ds, query, context) {
|
|
370
|
+
if (this.backend) {
|
|
371
|
+
const validation = this.validate(ds, query, context);
|
|
372
|
+
if (!validation.valid) {
|
|
373
|
+
throw new Error(`Invalid dataset query: ${validation.errors.join('; ')}`);
|
|
374
|
+
}
|
|
375
|
+
return this.backend.execute(this.planDataset(ds, query, context));
|
|
376
|
+
}
|
|
377
|
+
const validation = this.validate(ds, query, context);
|
|
378
|
+
if (!validation.valid) {
|
|
379
|
+
throw new Error(`Invalid dataset query: ${validation.errors.join('; ')}`);
|
|
380
|
+
}
|
|
381
|
+
return runDatasetQuery(ds, query, {
|
|
382
|
+
builderFactory: context?.runtime?.builderFactory ?? this.getBuilderFactory(),
|
|
383
|
+
context,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
toDatasetSQL(ds, query, context) {
|
|
387
|
+
const builder = buildDatasetQueryBuilder(ds, query, {
|
|
388
|
+
builderFactory: context?.runtime?.builderFactory ?? this.getBuilderFactory(),
|
|
389
|
+
context,
|
|
390
|
+
});
|
|
391
|
+
return builder.toSQLWithParams().sql;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
export function createDatasetClient(options) {
|
|
395
|
+
return new DatasetClientImpl(options);
|
|
396
|
+
}
|
package/dist/formulas.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Formula helpers for derived metrics.
|
|
3
3
|
*
|
|
4
4
|
* These are symbolic — they build FormulaExpr objects that get compiled to SQL
|
|
5
|
-
* by the
|
|
5
|
+
* by the semantic query engine. They do not produce raw SQL strings directly.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
package/dist/formulas.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formulas.d.ts","sourceRoot":"","sources":["../src/formulas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"formulas.d.ts","sourceRoot":"","sources":["../src/formulas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAkC9C,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAKpF;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAKtF;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAKtF;AAED,wBAAgB,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAKjF;AAMD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAK/D;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,CAMtG;AAMD,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE,QAAQ,GAAE,MAAU,GAAG,WAAW,CAKhF;AAED,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAK1D;AAED,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAKzD"}
|
package/dist/formulas.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Formula helpers for derived metrics.
|
|
3
3
|
*
|
|
4
4
|
* These are symbolic — they build FormulaExpr objects that get compiled to SQL
|
|
5
|
-
* by the
|
|
5
|
+
* by the semantic query engine. They do not produce raw SQL strings directly.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* });
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
|
-
function expr(sqlFn) {
|
|
16
|
-
return { __type: 'formula_expr', toSQL: sqlFn };
|
|
15
|
+
function expr(expression, sqlFn) {
|
|
16
|
+
return { __type: 'formula_expr', expression, toSQL: sqlFn };
|
|
17
17
|
}
|
|
18
18
|
/** Check if a value is a FormulaExpr. */
|
|
19
19
|
function resolveArg(a) {
|
|
@@ -21,40 +21,55 @@ function resolveArg(a) {
|
|
|
21
21
|
return a;
|
|
22
22
|
return a.toSQL();
|
|
23
23
|
}
|
|
24
|
+
function isFormulaExpr(value) {
|
|
25
|
+
return typeof value === 'object'
|
|
26
|
+
&& value !== null
|
|
27
|
+
&& '__type' in value
|
|
28
|
+
&& value.__type === 'formula_expr';
|
|
29
|
+
}
|
|
30
|
+
function resolveExpression(a) {
|
|
31
|
+
if (isFormulaExpr(a)) {
|
|
32
|
+
return a.expression;
|
|
33
|
+
}
|
|
34
|
+
if (typeof a === 'string') {
|
|
35
|
+
return { kind: 'ref', name: a };
|
|
36
|
+
}
|
|
37
|
+
return { kind: 'literal', value: a };
|
|
38
|
+
}
|
|
24
39
|
// ---------------------------------------------------------------------------
|
|
25
40
|
// Arithmetic
|
|
26
41
|
// ---------------------------------------------------------------------------
|
|
27
42
|
export function divide(a, b) {
|
|
28
|
-
return expr(() => `(${resolveArg(a)}) / (${resolveArg(b)})`);
|
|
43
|
+
return expr({ kind: 'binary', operator: 'divide', left: resolveExpression(a), right: resolveExpression(b) }, () => `(${resolveArg(a)}) / (${resolveArg(b)})`);
|
|
29
44
|
}
|
|
30
45
|
export function multiply(a, b) {
|
|
31
|
-
return expr(() => `(${resolveArg(a)}) * (${resolveArg(b)})`);
|
|
46
|
+
return expr({ kind: 'binary', operator: 'multiply', left: resolveExpression(a), right: resolveExpression(b) }, () => `(${resolveArg(a)}) * (${resolveArg(b)})`);
|
|
32
47
|
}
|
|
33
48
|
export function subtract(a, b) {
|
|
34
|
-
return expr(() => `(${resolveArg(a)}) - (${resolveArg(b)})`);
|
|
49
|
+
return expr({ kind: 'binary', operator: 'subtract', left: resolveExpression(a), right: resolveExpression(b) }, () => `(${resolveArg(a)}) - (${resolveArg(b)})`);
|
|
35
50
|
}
|
|
36
51
|
export function add(a, b) {
|
|
37
|
-
return expr(() => `(${resolveArg(a)}) + (${resolveArg(b)})`);
|
|
52
|
+
return expr({ kind: 'binary', operator: 'add', left: resolveExpression(a), right: resolveExpression(b) }, () => `(${resolveArg(a)}) + (${resolveArg(b)})`);
|
|
38
53
|
}
|
|
39
54
|
// ---------------------------------------------------------------------------
|
|
40
55
|
// Null handling
|
|
41
56
|
// ---------------------------------------------------------------------------
|
|
42
57
|
export function nullIfZero(a) {
|
|
43
|
-
return expr(() => `NULLIF(${resolveArg(a)}, 0)`);
|
|
58
|
+
return expr({ kind: 'function', name: 'nullIfZero', args: [resolveExpression(a)] }, () => `NULLIF(${resolveArg(a)}, 0)`);
|
|
44
59
|
}
|
|
45
60
|
export function coalesce(a, fallback) {
|
|
46
61
|
const fb = typeof fallback === 'number' ? String(fallback) : resolveArg(fallback);
|
|
47
|
-
return expr(() => `COALESCE(${resolveArg(a)}, ${fb})`);
|
|
62
|
+
return expr({ kind: 'function', name: 'coalesce', args: [resolveExpression(a), resolveExpression(fallback)] }, () => `COALESCE(${resolveArg(a)}, ${fb})`);
|
|
48
63
|
}
|
|
49
64
|
// ---------------------------------------------------------------------------
|
|
50
65
|
// Rounding
|
|
51
66
|
// ---------------------------------------------------------------------------
|
|
52
67
|
export function round(a, decimals = 0) {
|
|
53
|
-
return expr(() => `ROUND(${resolveArg(a)}, ${decimals})`);
|
|
68
|
+
return expr({ kind: 'function', name: 'round', args: [resolveExpression(a), resolveExpression(decimals)] }, () => `ROUND(${resolveArg(a)}, ${decimals})`);
|
|
54
69
|
}
|
|
55
70
|
export function floor(a) {
|
|
56
|
-
return expr(() => `FLOOR(${resolveArg(a)})`);
|
|
71
|
+
return expr({ kind: 'function', name: 'floor', args: [resolveExpression(a)] }, () => `FLOOR(${resolveArg(a)})`);
|
|
57
72
|
}
|
|
58
73
|
export function ceil(a) {
|
|
59
|
-
return expr(() => `CEIL(${resolveArg(a)})`);
|
|
74
|
+
return expr({ kind: 'function', name: 'ceil', args: [resolveExpression(a)] }, () => `CEIL(${resolveArg(a)})`);
|
|
60
75
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { SemanticBackend } from './semantic-plan.js';
|
|
2
|
+
export type InMemoryTable = Array<Record<string, unknown>>;
|
|
3
|
+
export type InMemoryTables = Record<string, InMemoryTable>;
|
|
4
|
+
export declare function createInMemoryBackend(tables: InMemoryTables): SemanticBackend;
|
|
5
|
+
//# sourceMappingURL=in-memory-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-memory-backend.d.ts","sourceRoot":"","sources":["../src/in-memory-backend.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,eAAe,EAIhB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,MAAM,aAAa,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAC3D,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AA2K3D,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,GAAG,eAAe,CAmE7E"}
|