@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.
Files changed (39) hide show
  1. package/dist/index.d.ts +4 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -1
  4. package/dist/portable-execution-errors.d.ts +49 -0
  5. package/dist/portable-execution-errors.d.ts.map +1 -0
  6. package/dist/portable-execution-errors.js +52 -0
  7. package/dist/portable-executor.d.ts +61 -0
  8. package/dist/portable-executor.d.ts.map +1 -0
  9. package/dist/portable-executor.js +90 -0
  10. package/dist/protocol-adapter.d.ts.map +1 -1
  11. package/dist/protocol-adapter.js +5 -97
  12. package/dist/protocol-rehydrate.d.ts +10 -0
  13. package/dist/protocol-rehydrate.d.ts.map +1 -1
  14. package/dist/protocol-rehydrate.js +62 -16
  15. package/dist/protocol-schema-adapter.d.ts +9 -0
  16. package/dist/protocol-schema-adapter.d.ts.map +1 -0
  17. package/dist/protocol-schema-adapter.js +249 -0
  18. package/dist/relationships.d.ts +25 -4
  19. package/dist/relationships.d.ts.map +1 -1
  20. package/dist/relationships.js +25 -4
  21. package/dist/semantic-query-schema.d.ts +31 -0
  22. package/dist/semantic-query-schema.d.ts.map +1 -1
  23. package/dist/semantic-query-schema.js +29 -1
  24. package/dist/utils/portable-execution-deadline.d.ts +4 -0
  25. package/dist/utils/portable-execution-deadline.d.ts.map +1 -0
  26. package/dist/utils/portable-execution-deadline.js +27 -0
  27. package/dist/utils/portable-result-budget.d.ts +12 -0
  28. package/dist/utils/portable-result-budget.d.ts.map +1 -0
  29. package/dist/utils/portable-result-budget.js +21 -0
  30. package/dist/utils/portable-semantic-query.d.ts +5 -0
  31. package/dist/utils/portable-semantic-query.d.ts.map +1 -0
  32. package/dist/utils/portable-semantic-query.js +36 -0
  33. package/dist/utils/protocol-metric-expressions.d.ts +17 -0
  34. package/dist/utils/protocol-metric-expressions.d.ts.map +1 -0
  35. package/dist/utils/protocol-metric-expressions.js +118 -0
  36. package/dist/utils/protocol-rehydrate-derivation.d.ts +27 -0
  37. package/dist/utils/protocol-rehydrate-derivation.d.ts.map +1 -0
  38. package/dist/utils/protocol-rehydrate-derivation.js +104 -0
  39. 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.14.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.12.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.0"
63
+ "@hypequery/protocol-conformance": "^0.11.1"
64
64
  },
65
65
  "repository": {
66
66
  "type": "git",