@hypequery/datasets 0.14.0 → 0.16.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/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/portable-execution-errors.d.ts +49 -0
- package/dist/portable-execution-errors.d.ts.map +1 -0
- package/dist/portable-execution-errors.js +52 -0
- package/dist/portable-executor.d.ts +61 -0
- package/dist/portable-executor.d.ts.map +1 -0
- package/dist/portable-executor.js +90 -0
- package/dist/protocol-adapter.d.ts.map +1 -1
- package/dist/protocol-adapter.js +5 -97
- package/dist/protocol-rehydrate.d.ts +10 -0
- package/dist/protocol-rehydrate.d.ts.map +1 -1
- package/dist/protocol-rehydrate.js +62 -16
- package/dist/protocol-schema-adapter.d.ts +9 -0
- package/dist/protocol-schema-adapter.d.ts.map +1 -0
- package/dist/protocol-schema-adapter.js +249 -0
- package/dist/relationships.d.ts +25 -4
- package/dist/relationships.d.ts.map +1 -1
- package/dist/relationships.js +25 -4
- package/dist/semantic-query-schema.d.ts +31 -0
- package/dist/semantic-query-schema.d.ts.map +1 -1
- package/dist/semantic-query-schema.js +29 -1
- package/dist/utils/portable-execution-deadline.d.ts +4 -0
- package/dist/utils/portable-execution-deadline.d.ts.map +1 -0
- package/dist/utils/portable-execution-deadline.js +27 -0
- package/dist/utils/portable-result-budget.d.ts +12 -0
- package/dist/utils/portable-result-budget.d.ts.map +1 -0
- package/dist/utils/portable-result-budget.js +21 -0
- package/dist/utils/portable-semantic-query.d.ts +5 -0
- package/dist/utils/portable-semantic-query.d.ts.map +1 -0
- package/dist/utils/portable-semantic-query.js +36 -0
- package/dist/utils/protocol-metric-expressions.d.ts +17 -0
- package/dist/utils/protocol-metric-expressions.d.ts.map +1 -0
- package/dist/utils/protocol-metric-expressions.js +118 -0
- package/dist/utils/protocol-rehydrate-derivation.d.ts +27 -0
- package/dist/utils/protocol-rehydrate-derivation.d.ts.map +1 -0
- package/dist/utils/protocol-rehydrate-derivation.js +104 -0
- package/package.json +3 -3
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { parseProtocolIdentifier, parseProtocolQualifiedIdentifier, validateCanonicalValue, } from '@hypequery/protocol';
|
|
2
|
+
function canonicalValue(input) {
|
|
3
|
+
if (Array.isArray(input)) {
|
|
4
|
+
return validateCanonicalValue({
|
|
5
|
+
$hypequery: {
|
|
6
|
+
type: 'array',
|
|
7
|
+
version: 1,
|
|
8
|
+
values: input.map(canonicalValue),
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
if (typeof input === 'object' && input !== null) {
|
|
13
|
+
if ('$hypequery' in input)
|
|
14
|
+
return validateCanonicalValue(input);
|
|
15
|
+
const prototype = Object.getPrototypeOf(input);
|
|
16
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
17
|
+
throw new TypeError('Dataset protocol adapter only accepts plain filter values.');
|
|
18
|
+
}
|
|
19
|
+
return validateCanonicalValue({
|
|
20
|
+
$hypequery: {
|
|
21
|
+
type: 'map',
|
|
22
|
+
version: 1,
|
|
23
|
+
entries: Object.entries(input)
|
|
24
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
25
|
+
.map(([key, value]) => [key, canonicalValue(value)]),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return validateCanonicalValue(input);
|
|
30
|
+
}
|
|
31
|
+
export function filterExpression(filter) {
|
|
32
|
+
const left = {
|
|
33
|
+
kind: 'reference',
|
|
34
|
+
name: parseProtocolQualifiedIdentifier(filter.field),
|
|
35
|
+
};
|
|
36
|
+
const right = {
|
|
37
|
+
kind: 'literal',
|
|
38
|
+
value: canonicalValue(filter.value),
|
|
39
|
+
};
|
|
40
|
+
const result = {
|
|
41
|
+
kind: 'comparison',
|
|
42
|
+
operator: filter.operator,
|
|
43
|
+
left,
|
|
44
|
+
right,
|
|
45
|
+
};
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function aggregationExpression(spec) {
|
|
49
|
+
const result = {
|
|
50
|
+
kind: 'aggregate',
|
|
51
|
+
aggregation: spec.aggregation,
|
|
52
|
+
field: parseProtocolQualifiedIdentifier(spec.field),
|
|
53
|
+
...(spec.argField !== undefined
|
|
54
|
+
? { argField: parseProtocolQualifiedIdentifier(spec.argField) }
|
|
55
|
+
: {}),
|
|
56
|
+
...(spec.level !== undefined ? { level: spec.level } : {}),
|
|
57
|
+
...(spec.filters?.length
|
|
58
|
+
? { filters: spec.filters.map(filterExpression) }
|
|
59
|
+
: {}),
|
|
60
|
+
};
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function semanticExpression(expression, references = {}) {
|
|
64
|
+
switch (expression.kind) {
|
|
65
|
+
case 'ref':
|
|
66
|
+
return references[expression.name]
|
|
67
|
+
?? {
|
|
68
|
+
kind: 'reference',
|
|
69
|
+
name: parseProtocolQualifiedIdentifier(expression.name),
|
|
70
|
+
};
|
|
71
|
+
case 'literal':
|
|
72
|
+
return { kind: 'literal', value: canonicalValue(expression.value) };
|
|
73
|
+
case 'binary':
|
|
74
|
+
return {
|
|
75
|
+
kind: 'binary',
|
|
76
|
+
operator: expression.operator,
|
|
77
|
+
left: semanticExpression(expression.left, references),
|
|
78
|
+
right: semanticExpression(expression.right, references),
|
|
79
|
+
};
|
|
80
|
+
case 'function':
|
|
81
|
+
return {
|
|
82
|
+
kind: 'call',
|
|
83
|
+
function: expression.name,
|
|
84
|
+
args: expression.args.map(argument => semanticExpression(argument, references)),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function metricExpression(spec) {
|
|
89
|
+
if (spec.__type === 'aggregation_spec')
|
|
90
|
+
return aggregationExpression(spec);
|
|
91
|
+
const aliases = Object.fromEntries(Object.keys(spec.uses).map(alias => [alias, alias]));
|
|
92
|
+
const references = Object.fromEntries(Object.entries(spec.uses).map(([alias, metric]) => [
|
|
93
|
+
alias,
|
|
94
|
+
aggregationExpression(metric.spec),
|
|
95
|
+
]));
|
|
96
|
+
return semanticExpression(spec.formula(aliases).expression, references);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The formula in the shape it was authored in, beside the inlined form.
|
|
100
|
+
*
|
|
101
|
+
* `metricExpression` above substitutes each input's aggregate where the formula
|
|
102
|
+
* named it, which states what the metric means but drops the aliases. Those
|
|
103
|
+
* aliases are the column names of the intermediate aggregate, so a catalog
|
|
104
|
+
* rebuilt without them computes the same number through different SQL.
|
|
105
|
+
*
|
|
106
|
+
* Input order follows `uses` and is not sorted: each entry becomes a column of
|
|
107
|
+
* that intermediate result in this order.
|
|
108
|
+
*/
|
|
109
|
+
export function metricDerivation(spec) {
|
|
110
|
+
const aliases = Object.fromEntries(Object.keys(spec.uses).map(alias => [alias, alias]));
|
|
111
|
+
return {
|
|
112
|
+
inputs: Object.entries(spec.uses).map(([alias, metric]) => ({
|
|
113
|
+
alias: parseProtocolIdentifier(alias),
|
|
114
|
+
expression: aggregationExpression(metric.spec),
|
|
115
|
+
})),
|
|
116
|
+
expression: semanticExpression(spec.formula(aliases).expression),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebuilds a derived metric's formula from the contract's authored form.
|
|
3
|
+
*
|
|
4
|
+
* The formula is reconstructed by calling the same helpers in `formulas.ts`
|
|
5
|
+
* that authored it, rather than by compiling the expression to SQL here. That
|
|
6
|
+
* is deliberate: those helpers carry the `toSQL` closures that decide spacing,
|
|
7
|
+
* parenthesisation, and function spelling, so reusing them makes byte-identity
|
|
8
|
+
* structural. A second compiler would be a second place for that spelling to
|
|
9
|
+
* live, and the two would drift the first time either changed.
|
|
10
|
+
*/
|
|
11
|
+
import type { ProtocolMetricDerivation } from '@hypequery/protocol';
|
|
12
|
+
import type { FormulaExpr } from '../types.js';
|
|
13
|
+
type Unsupported = (reason: string) => Error;
|
|
14
|
+
/**
|
|
15
|
+
* The formula function a derived metric spec expects.
|
|
16
|
+
*
|
|
17
|
+
* The returned function maps each declared alias through the caller-supplied
|
|
18
|
+
* `inputs` record, exactly as an authored formula does — both the plan builder
|
|
19
|
+
* and the query-builder path pass alias-to-alias, so the emitted column names
|
|
20
|
+
* are the ones the contract declared.
|
|
21
|
+
*
|
|
22
|
+
* The formula is built once here as well, so a malformed one is refused when
|
|
23
|
+
* the metric is rebuilt rather than on the first query that reaches it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function rehydrateDerivedFormula(derivation: ProtocolMetricDerivation, unsupported: Unsupported): (inputs: Record<string, string>) => FormulaExpr;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=protocol-rehydrate-derivation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol-rehydrate-derivation.d.ts","sourceRoot":"","sources":["../../src/utils/protocol-rehydrate-derivation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAsB,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAYxF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAQ/C,KAAK,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,KAAK,CAAC;AA8G7C;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,wBAAwB,EACpC,WAAW,EAAE,WAAW,GACvB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,WAAW,CAGjD"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebuilds a derived metric's formula from the contract's authored form.
|
|
3
|
+
*
|
|
4
|
+
* The formula is reconstructed by calling the same helpers in `formulas.ts`
|
|
5
|
+
* that authored it, rather than by compiling the expression to SQL here. That
|
|
6
|
+
* is deliberate: those helpers carry the `toSQL` closures that decide spacing,
|
|
7
|
+
* parenthesisation, and function spelling, so reusing them makes byte-identity
|
|
8
|
+
* structural. A second compiler would be a second place for that spelling to
|
|
9
|
+
* live, and the two would drift the first time either changed.
|
|
10
|
+
*/
|
|
11
|
+
import { add, ceil, coalesce, divide, floor, multiply, nullIfZero, round, subtract, } from '../formulas.js';
|
|
12
|
+
const BINARY = {
|
|
13
|
+
add, subtract, multiply, divide,
|
|
14
|
+
};
|
|
15
|
+
function arity(args, expected, name, unsupported) {
|
|
16
|
+
if (args.length !== expected) {
|
|
17
|
+
throw unsupported(`"${name}" takes ${expected} argument(s), received ${args.length}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function literalNumber(expression, unsupported) {
|
|
21
|
+
if (expression.kind !== 'literal' || typeof expression.value !== 'number') {
|
|
22
|
+
throw unsupported('expected a numeric literal argument');
|
|
23
|
+
}
|
|
24
|
+
return expression.value;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A `coalesce` fallback, the one position that accepts a bare value.
|
|
28
|
+
*
|
|
29
|
+
* `formulas.ts` renders a number through `String(...)` and anything else
|
|
30
|
+
* through the operand path, so both round-trip to the same SQL.
|
|
31
|
+
*/
|
|
32
|
+
function fallback(expression, resolve, unsupported) {
|
|
33
|
+
if (expression.kind !== 'literal')
|
|
34
|
+
return operand(expression, resolve, unsupported);
|
|
35
|
+
if (typeof expression.value !== 'number') {
|
|
36
|
+
throw unsupported('a "coalesce" fallback literal must be numeric');
|
|
37
|
+
}
|
|
38
|
+
return expression.value;
|
|
39
|
+
}
|
|
40
|
+
function operand(expression, resolve, unsupported) {
|
|
41
|
+
switch (expression.kind) {
|
|
42
|
+
case 'reference':
|
|
43
|
+
// An alias, or a column the formula named directly. `resolveArg` in
|
|
44
|
+
// `formulas.ts` passes a string through unchanged, which is what the
|
|
45
|
+
// authored form did with the same name.
|
|
46
|
+
return resolve(String(expression.name));
|
|
47
|
+
case 'binary': {
|
|
48
|
+
const build = BINARY[expression.operator];
|
|
49
|
+
if (build === undefined)
|
|
50
|
+
throw unsupported(`unsupported operator "${expression.operator}"`);
|
|
51
|
+
return build(operand(expression.left, resolve, unsupported), operand(expression.right, resolve, unsupported));
|
|
52
|
+
}
|
|
53
|
+
case 'call':
|
|
54
|
+
switch (expression.function) {
|
|
55
|
+
case 'nullIfZero':
|
|
56
|
+
arity(expression.args, 1, 'nullIfZero', unsupported);
|
|
57
|
+
return nullIfZero(operand(expression.args[0], resolve, unsupported));
|
|
58
|
+
case 'floor':
|
|
59
|
+
arity(expression.args, 1, 'floor', unsupported);
|
|
60
|
+
return floor(operand(expression.args[0], resolve, unsupported));
|
|
61
|
+
case 'ceil':
|
|
62
|
+
arity(expression.args, 1, 'ceil', unsupported);
|
|
63
|
+
return ceil(operand(expression.args[0], resolve, unsupported));
|
|
64
|
+
case 'round':
|
|
65
|
+
arity(expression.args, 2, 'round', unsupported);
|
|
66
|
+
return round(operand(expression.args[0], resolve, unsupported), literalNumber(expression.args[1], unsupported));
|
|
67
|
+
case 'coalesce':
|
|
68
|
+
arity(expression.args, 2, 'coalesce', unsupported);
|
|
69
|
+
return coalesce(operand(expression.args[0], resolve, unsupported), fallback(expression.args[1], resolve, unsupported));
|
|
70
|
+
default:
|
|
71
|
+
throw unsupported(`unsupported function "${String(expression.function)}"`);
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
// A literal outside a `round` precision or `coalesce` fallback, or an
|
|
75
|
+
// aggregate that should have been carried as a named input. Either way
|
|
76
|
+
// the authoring API cannot express it, so rebuilding would be a guess.
|
|
77
|
+
throw unsupported(`a formula cannot contain a "${expression.kind}" expression here`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function buildFormula(derivation, resolve, unsupported) {
|
|
81
|
+
const built = operand(derivation.expression, resolve, unsupported);
|
|
82
|
+
if (typeof built === 'string') {
|
|
83
|
+
// `spec.formula(...)` must return a `FormulaExpr`. A formula that is a bare
|
|
84
|
+
// reference names one of its inputs instead of combining them, which the
|
|
85
|
+
// authoring API cannot produce.
|
|
86
|
+
throw unsupported('a formula must combine its inputs rather than name one');
|
|
87
|
+
}
|
|
88
|
+
return built;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The formula function a derived metric spec expects.
|
|
92
|
+
*
|
|
93
|
+
* The returned function maps each declared alias through the caller-supplied
|
|
94
|
+
* `inputs` record, exactly as an authored formula does — both the plan builder
|
|
95
|
+
* and the query-builder path pass alias-to-alias, so the emitted column names
|
|
96
|
+
* are the ones the contract declared.
|
|
97
|
+
*
|
|
98
|
+
* The formula is built once here as well, so a malformed one is refused when
|
|
99
|
+
* the metric is rebuilt rather than on the first query that reaches it.
|
|
100
|
+
*/
|
|
101
|
+
export function rehydrateDerivedFormula(derivation, unsupported) {
|
|
102
|
+
buildFormula(derivation, name => name, unsupported);
|
|
103
|
+
return inputs => buildFormula(derivation, name => inputs[name] ?? name, unsupported);
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/datasets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Code-first TypeScript semantic layer for ClickHouse datasets, metrics, multi-tenancy, and AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"semantic-layer",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"dist"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@hypequery/protocol": "^0.
|
|
45
|
+
"@hypequery/protocol": "^0.13.0",
|
|
46
46
|
"@noble/hashes": "^1.8.0",
|
|
47
47
|
"zod": "^3.22.4",
|
|
48
48
|
"zod-to-json-schema": "^3.23.5"
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"typescript": "^5.7.3",
|
|
61
61
|
"@vitest/coverage-v8": "^3.2.6",
|
|
62
62
|
"vitest": "^3.2.6",
|
|
63
|
-
"@hypequery/protocol-conformance": "^0.11.
|
|
63
|
+
"@hypequery/protocol-conformance": "^0.11.1"
|
|
64
64
|
},
|
|
65
65
|
"repository": {
|
|
66
66
|
"type": "git",
|