@hypequery/datasets 0.8.0 → 0.9.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/api.type-test.js +1 -0
- package/dist/catalog.d.ts +4 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +16 -6
- package/dist/contract.d.ts +3 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +3 -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 +41 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/query-builder-protocol.d.ts +12 -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 +63 -16
- package/dist/relationships.d.ts +9 -9
- package/dist/relationships.d.ts.map +1 -1
- package/dist/semantic-plan.d.ts +50 -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 +67 -5
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +29 -18
- package/dist/types.d.ts +29 -12
- 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 +11 -1
- package/dist/utils/dataset-query-validation.d.ts.map +1 -1
- package/dist/utils/dataset-query-validation.js +50 -12
- 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 +4 -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
|
@@ -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;AAE9C,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,CA2BlB;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,CAiClB;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,46 @@
|
|
|
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';
|
|
4
7
|
function toOrderDirection(direction) {
|
|
5
8
|
return direction === 'asc' ? 'ASC' : 'DESC';
|
|
6
9
|
}
|
|
7
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Raw SQL expressions on the base dataset are emitted verbatim, so their column
|
|
12
|
+
* references are not table-qualified. When relationship joins are active a bare
|
|
13
|
+
* `price` in such an expression is ambiguous if the joined table also has a
|
|
14
|
+
* `price` column. Until the builder rewrites identifiers inside expressions,
|
|
15
|
+
* reject the combination rather than emit ambiguous SQL.
|
|
16
|
+
*/
|
|
17
|
+
function assertNoRawSqlUnderJoins(kind, name, sql, joinCtx) {
|
|
18
|
+
if (!joinCtx) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`SQL-backed ${kind} "${name}" cannot be combined with relationship joins: its expression ` +
|
|
22
|
+
`("${sql}") is not table-qualified and may collide with joined columns. Query it without ` +
|
|
23
|
+
`relationship-qualified fields, or redeclare it as a plain column.`);
|
|
24
|
+
}
|
|
25
|
+
export function resolveDimensionExpression(ds, dimensionName, joinCtx) {
|
|
26
|
+
if (joinCtx && isQualifiedField(dimensionName)) {
|
|
27
|
+
return resolveQualifiedColumn(ds, dimensionName);
|
|
28
|
+
}
|
|
8
29
|
const definition = ds.dimensions[dimensionName];
|
|
9
|
-
|
|
30
|
+
if (definition?.sql) {
|
|
31
|
+
assertNoRawSqlUnderJoins('dimension', dimensionName, definition.sql, joinCtx);
|
|
32
|
+
return definition.sql;
|
|
33
|
+
}
|
|
34
|
+
return qualifyBaseColumn(joinCtx, definition?.column ?? dimensionName);
|
|
10
35
|
}
|
|
11
|
-
export function resolveFilterField(ds, filterField) {
|
|
36
|
+
export function resolveFilterField(ds, filterField, joinCtx) {
|
|
37
|
+
if (joinCtx && isQualifiedField(filterField)) {
|
|
38
|
+
return resolveQualifiedColumn(ds, filterField);
|
|
39
|
+
}
|
|
12
40
|
const resolvedField = ds.filters[filterField]?.field ?? filterField;
|
|
13
|
-
return resolveDimensionExpression(ds, resolvedField);
|
|
41
|
+
return resolveDimensionExpression(ds, resolvedField, joinCtx);
|
|
14
42
|
}
|
|
15
|
-
export function buildDimensionSelectionPlan(ds, dimensions, grain) {
|
|
43
|
+
export function buildDimensionSelectionPlan(ds, dimensions, grain, joinCtx) {
|
|
16
44
|
const selectParts = [];
|
|
17
45
|
const groupByParts = new Set();
|
|
18
46
|
if (grain) {
|
|
@@ -20,23 +48,36 @@ export function buildDimensionSelectionPlan(ds, dimensions, grain) {
|
|
|
20
48
|
if (!fn) {
|
|
21
49
|
throw new Error(`Unsupported time grain "${grain}".`);
|
|
22
50
|
}
|
|
23
|
-
selectParts.push(`${fn}(${ds.timeKey}) AS period`);
|
|
51
|
+
selectParts.push(`${fn}(${qualifyBaseColumn(joinCtx, String(ds.timeKey))}) AS period`);
|
|
24
52
|
groupByParts.add("period");
|
|
25
53
|
}
|
|
26
54
|
for (const dimensionName of dimensions) {
|
|
27
|
-
const expression = resolveDimensionExpression(ds, dimensionName);
|
|
28
|
-
if (
|
|
55
|
+
const expression = resolveDimensionExpression(ds, dimensionName, joinCtx);
|
|
56
|
+
if (joinCtx) {
|
|
57
|
+
// With joins in scope, every selection is table-qualified and aliased so
|
|
58
|
+
// grouping/ordering can reference the alias unambiguously.
|
|
59
|
+
const alias = isQualifiedField(dimensionName)
|
|
60
|
+
? quoteSQLIdentifier(dimensionName)
|
|
61
|
+
: dimensionName;
|
|
62
|
+
selectParts.push(`${expression} AS ${alias}`);
|
|
63
|
+
groupByParts.add(alias);
|
|
64
|
+
}
|
|
65
|
+
else if (expression === dimensionName) {
|
|
29
66
|
selectParts.push(dimensionName);
|
|
67
|
+
groupByParts.add(dimensionName);
|
|
30
68
|
}
|
|
31
69
|
else {
|
|
32
70
|
selectParts.push(`${expression} AS ${dimensionName}`);
|
|
71
|
+
groupByParts.add(dimensionName);
|
|
33
72
|
}
|
|
34
|
-
groupByParts.add(dimensionName);
|
|
35
73
|
}
|
|
36
74
|
return { selectParts, groupByParts: Array.from(groupByParts) };
|
|
37
75
|
}
|
|
38
|
-
export function applyAggregationSpec(qb, ds, spec, alias) {
|
|
39
|
-
|
|
76
|
+
export function applyAggregationSpec(qb, ds, spec, alias, joinCtx) {
|
|
77
|
+
if (spec.sql) {
|
|
78
|
+
assertNoRawSqlUnderJoins('measure', alias, spec.sql, joinCtx);
|
|
79
|
+
}
|
|
80
|
+
const fieldOrExpr = applyFilteredAggregationExpression(ds, spec, spec.sql ?? resolveDimensionExpression(ds, spec.field, joinCtx), joinCtx);
|
|
40
81
|
switch (spec.aggregation) {
|
|
41
82
|
case "sum":
|
|
42
83
|
return qb.sum(fieldOrExpr, alias);
|
|
@@ -54,14 +95,17 @@ export function applyAggregationSpec(qb, ds, spec, alias) {
|
|
|
54
95
|
throw new Error(`Unknown aggregation type: ${spec.aggregation}`);
|
|
55
96
|
}
|
|
56
97
|
}
|
|
57
|
-
export function applyMeasureDefinition(qb, ds, name, definition) {
|
|
58
|
-
|
|
98
|
+
export function applyMeasureDefinition(qb, ds, name, definition, joinCtx) {
|
|
99
|
+
if (definition.sql) {
|
|
100
|
+
assertNoRawSqlUnderJoins('measure', name, definition.sql, joinCtx);
|
|
101
|
+
}
|
|
102
|
+
const baseFieldOrExpr = definition.sql ?? resolveDimensionExpression(ds, definition.field, joinCtx);
|
|
59
103
|
const fieldOrExpr = applyFilteredAggregationExpression(ds, {
|
|
60
104
|
__type: 'aggregation_spec',
|
|
61
105
|
aggregation: definition.aggregation,
|
|
62
106
|
field: definition.field,
|
|
63
107
|
filters: definition.filters,
|
|
64
|
-
}, baseFieldOrExpr);
|
|
108
|
+
}, baseFieldOrExpr, joinCtx);
|
|
65
109
|
switch (definition.aggregation) {
|
|
66
110
|
case "sum":
|
|
67
111
|
return qb.sum(fieldOrExpr, name);
|
|
@@ -79,10 +123,13 @@ export function applyMeasureDefinition(qb, ds, name, definition) {
|
|
|
79
123
|
throw new Error(`Unsupported measure aggregation: ${definition.aggregation}`);
|
|
80
124
|
}
|
|
81
125
|
}
|
|
82
|
-
export function appendOrderLimitOffset(qb, orderBy, grain, limit, offset) {
|
|
126
|
+
export function appendOrderLimitOffset(qb, orderBy, grain, limit, offset, joinCtx) {
|
|
83
127
|
if (orderBy && orderBy.length > 0) {
|
|
84
128
|
for (const order of orderBy) {
|
|
85
|
-
|
|
129
|
+
const column = joinCtx && isQualifiedField(order.field)
|
|
130
|
+
? quoteSQLIdentifier(order.field)
|
|
131
|
+
: order.field;
|
|
132
|
+
qb = qb.orderBy(column, toOrderDirection(order.direction));
|
|
86
133
|
}
|
|
87
134
|
}
|
|
88
135
|
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"}
|
package/dist/semantic-plan.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database-neutral semantic plan protocol (`PlanNode` / `SemanticBackend`).
|
|
3
|
+
*
|
|
4
|
+
* FROZEN: this execution path is superseded by the query-builder path —
|
|
5
|
+
* `createDatasetClient({ queryBuilder })` is the canonical way to execute
|
|
6
|
+
* semantic queries. The plan/backend protocol receives bug fixes only; new
|
|
7
|
+
* semantic features land on the query-builder path and are not guaranteed to
|
|
8
|
+
* be mirrored here.
|
|
9
|
+
*/
|
|
1
10
|
import type { AggregationType, FieldType, MetricFilter, MetricOrderBy, TimeGrain } from './types.js';
|
|
2
11
|
export type SemanticBinaryOperator = 'add' | 'subtract' | 'multiply' | 'divide';
|
|
3
12
|
export type SemanticFunctionName = 'nullIfZero' | 'coalesce' | 'round' | 'floor' | 'ceil';
|
|
13
|
+
/** @deprecated Part of the frozen plan/backend protocol; use `createDatasetClient({ queryBuilder })` instead. */
|
|
4
14
|
export type SemanticExpression = {
|
|
5
15
|
kind: 'ref';
|
|
6
16
|
name: string;
|
|
@@ -17,17 +27,43 @@ export type SemanticExpression = {
|
|
|
17
27
|
name: SemanticFunctionName;
|
|
18
28
|
args: SemanticExpression[];
|
|
19
29
|
};
|
|
30
|
+
/** @deprecated Part of the frozen plan/backend protocol; use `createDatasetClient({ queryBuilder })` instead. */
|
|
20
31
|
export interface SemanticDimensionPlan {
|
|
21
32
|
name: string;
|
|
22
33
|
field: string;
|
|
23
34
|
fieldType?: FieldType;
|
|
24
35
|
}
|
|
36
|
+
/** @deprecated Part of the frozen plan/backend protocol; use `createDatasetClient({ queryBuilder })` instead. */
|
|
25
37
|
export interface SemanticAggregationPlan {
|
|
26
38
|
name: string;
|
|
27
39
|
aggregation: AggregationType;
|
|
28
40
|
field: string;
|
|
29
41
|
filters?: MetricFilter[];
|
|
30
42
|
}
|
|
43
|
+
export type SemanticTenantPredicate = {
|
|
44
|
+
field: string;
|
|
45
|
+
operator: 'eq';
|
|
46
|
+
value: string;
|
|
47
|
+
} | {
|
|
48
|
+
field: string;
|
|
49
|
+
operator: 'in';
|
|
50
|
+
value: string[];
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* A query-time LEFT JOIN for a to-one relationship. `relationship` is the alias
|
|
54
|
+
* used to qualify joined columns (`<relationship>.<column>`); `from`/`to` are
|
|
55
|
+
* the unqualified base and target join columns. An optional `tenant` predicate
|
|
56
|
+
* scopes the joined target when runtime tenancy is active on the target.
|
|
57
|
+
*/
|
|
58
|
+
export interface SemanticJoinPlan {
|
|
59
|
+
relationship: string;
|
|
60
|
+
source: string;
|
|
61
|
+
from: string;
|
|
62
|
+
to: string;
|
|
63
|
+
type: 'left';
|
|
64
|
+
tenant?: SemanticTenantPredicate;
|
|
65
|
+
}
|
|
66
|
+
/** @deprecated Part of the frozen plan/backend protocol; use `createDatasetClient({ queryBuilder })` instead. */
|
|
31
67
|
export interface SemanticGrainPlan {
|
|
32
68
|
field: string;
|
|
33
69
|
unit: TimeGrain;
|
|
@@ -35,6 +71,7 @@ export interface SemanticGrainPlan {
|
|
|
35
71
|
timezone?: string;
|
|
36
72
|
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
37
73
|
}
|
|
74
|
+
/** @deprecated Part of the frozen plan/backend protocol; use `createDatasetClient({ queryBuilder })` instead. */
|
|
38
75
|
export type PlanNode = {
|
|
39
76
|
kind: 'aggregate';
|
|
40
77
|
source: string;
|
|
@@ -45,15 +82,13 @@ export type PlanNode = {
|
|
|
45
82
|
orderBy?: MetricOrderBy[];
|
|
46
83
|
limit?: number;
|
|
47
84
|
offset?: number;
|
|
48
|
-
tenant?:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
value: string[];
|
|
56
|
-
};
|
|
85
|
+
tenant?: SemanticTenantPredicate;
|
|
86
|
+
/**
|
|
87
|
+
* To-one relationship LEFT JOINs. When present, joined columns referenced by
|
|
88
|
+
* dimensions/filters are qualified as `<relationship>.<column>`; base columns
|
|
89
|
+
* stay unqualified (SQL backends qualify them with `source` as needed).
|
|
90
|
+
*/
|
|
91
|
+
joins?: SemanticJoinPlan[];
|
|
57
92
|
} | {
|
|
58
93
|
kind: 'derive';
|
|
59
94
|
input: PlanNode;
|
|
@@ -65,6 +100,7 @@ export type PlanNode = {
|
|
|
65
100
|
limit?: number;
|
|
66
101
|
offset?: number;
|
|
67
102
|
};
|
|
103
|
+
/** @deprecated Part of the frozen plan/backend protocol; use `createDatasetClient({ queryBuilder })` instead. */
|
|
68
104
|
export interface SemanticBackendResult<T = Record<string, unknown>> {
|
|
69
105
|
data: T[];
|
|
70
106
|
meta?: {
|
|
@@ -73,6 +109,11 @@ export interface SemanticBackendResult<T = Record<string, unknown>> {
|
|
|
73
109
|
tenant?: string;
|
|
74
110
|
};
|
|
75
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* @deprecated The plan/backend execution path is frozen (bug fixes only).
|
|
114
|
+
* Pass a query builder to `createDatasetClient({ queryBuilder })` instead of
|
|
115
|
+
* implementing a backend.
|
|
116
|
+
*/
|
|
76
117
|
export interface SemanticBackend {
|
|
77
118
|
execute<T = Record<string, unknown>>(plan: PlanNode): Promise<SemanticBackendResult<T>>;
|
|
78
119
|
explain?(plan: PlanNode): Promise<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-plan.d.ts","sourceRoot":"","sources":["../src/semantic-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,SAAS,EACT,YAAY,EACZ,aAAa,EACb,SAAS,EACV,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,sBAAsB,GAAG,KAAK,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;AAChF,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1F,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;CAAE,GAC5D;IACA,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,sBAAsB,CAAC;IACjC,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,kBAAkB,CAAC;CAC3B,GACC;IACA,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,oBAAoB,CAAC;IAC3B,IAAI,EAAE,kBAAkB,EAAE,CAAC;CAC5B,CAAC;AAEJ,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,eAAe,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;CACvC;AAED,MAAM,MAAM,QAAQ,GAChB;IACA,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,qBAAqB,EAAE,CAAC;IACpC,YAAY,EAAE,uBAAuB,EAAE,CAAC;IACxC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE
|
|
1
|
+
{"version":3,"file":"semantic-plan.d.ts","sourceRoot":"","sources":["../src/semantic-plan.ts"],"names":[],"mappings":"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"}
|
package/dist/semantic-plan.js
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database-neutral semantic plan protocol (`PlanNode` / `SemanticBackend`).
|
|
3
|
+
*
|
|
4
|
+
* FROZEN: this execution path is superseded by the query-builder path —
|
|
5
|
+
* `createDatasetClient({ queryBuilder })` is the canonical way to execute
|
|
6
|
+
* semantic queries. The plan/backend protocol receives bug fixes only; new
|
|
7
|
+
* semantic features land on the query-builder path and are not guaranteed to
|
|
8
|
+
* be mirrored here.
|
|
9
|
+
*/
|
|
1
10
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"semantic-planner.d.ts","sourceRoot":"","sources":["../src/semantic-planner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,
|
|
1
|
+
{"version":3,"file":"semantic-planner.d.ts","sourceRoot":"","sources":["../src/semantic-planner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAGhB,WAAW,EACX,SAAS,EACT,gBAAgB,EACjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EACV,QAAQ,EAIT,MAAM,oBAAoB,CAAC;AAuL5B,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,kBAAkB,EACtB,KAAK,GAAE,YAAiB,EACxB,OAAO,CAAC,EAAE,gBAAgB,GACzB,QAAQ,CAaV;AAuFD,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,GAAG,gBAAgB,EACpC,KAAK,GAAE,WAAgB,EACvB,OAAO,CAAC,EAAE,gBAAgB,GACzB,QAAQ,CAUV"}
|
package/dist/semantic-planner.js
CHANGED
|
@@ -2,6 +2,7 @@ import { getMetricGrain, getMetricRef, } from './utils/metric-handle.js';
|
|
|
2
2
|
import { validateDatasetQuery } from './dataset-query.js';
|
|
3
3
|
import { isSupportedTimeGrain } from './constants.js';
|
|
4
4
|
import { getRuntimeTenantPredicate } from './utils/tenant-runtime.js';
|
|
5
|
+
import { isQualifiedField, resolveQualifiedField } from './utils/relationship-fields.js';
|
|
5
6
|
function resolveField(ds, field) {
|
|
6
7
|
const dimension = ds.dimensions[field];
|
|
7
8
|
if (dimension?.sql) {
|
|
@@ -10,15 +11,41 @@ function resolveField(ds, field) {
|
|
|
10
11
|
}
|
|
11
12
|
return dimension?.column ?? field;
|
|
12
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolves a relationship-qualified field to its plan field name
|
|
16
|
+
* (`<relationship>.<targetColumn>`). Validation runs before planning, so an
|
|
17
|
+
* unresolved qualified name here is a programming error.
|
|
18
|
+
*/
|
|
19
|
+
function resolveQualifiedPlanField(ds, name) {
|
|
20
|
+
const resolution = resolveQualifiedField(ds, name);
|
|
21
|
+
if (!resolution || !resolution.resolved) {
|
|
22
|
+
throw new Error(resolution?.error ?? `Cannot resolve relationship field "${name}".`);
|
|
23
|
+
}
|
|
24
|
+
return resolution.resolved;
|
|
25
|
+
}
|
|
13
26
|
function dimensionsForQuery(ds, dimensions = []) {
|
|
14
|
-
return dimensions.map((name) =>
|
|
15
|
-
name
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
return dimensions.map((name) => {
|
|
28
|
+
if (isQualifiedField(name)) {
|
|
29
|
+
const { relationshipName, targetColumn, targetDimension } = resolveQualifiedPlanField(ds, name);
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
field: `${relationshipName}.${targetColumn}`,
|
|
33
|
+
fieldType: targetDimension.fieldType,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
field: resolveField(ds, name),
|
|
39
|
+
fieldType: ds.dimensions[name]?.fieldType,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
19
42
|
}
|
|
20
43
|
function normalizeFilters(ds, filters = []) {
|
|
21
44
|
return filters.map((filter) => {
|
|
45
|
+
if (isQualifiedField(filter.field)) {
|
|
46
|
+
const { relationshipName, targetColumn } = resolveQualifiedPlanField(ds, filter.field);
|
|
47
|
+
return { ...filter, field: `${relationshipName}.${targetColumn}` };
|
|
48
|
+
}
|
|
22
49
|
const resolvedField = ds.filters[filter.field]?.field ?? filter.field;
|
|
23
50
|
return {
|
|
24
51
|
...filter,
|
|
@@ -26,6 +53,40 @@ function normalizeFilters(ds, filters = []) {
|
|
|
26
53
|
};
|
|
27
54
|
});
|
|
28
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Collects deduped to-one LEFT JOINs for every relationship referenced by the
|
|
58
|
+
* query's dimensions, filters, or orderBy, scoping each joined target with the
|
|
59
|
+
* runtime tenant predicate when the target declares a `tenantKey`.
|
|
60
|
+
*/
|
|
61
|
+
function collectJoins(ds, dimensions = [], filters = [], orderBy = [], context) {
|
|
62
|
+
const referenced = [
|
|
63
|
+
...dimensions,
|
|
64
|
+
...filters.map((filter) => filter.field),
|
|
65
|
+
...orderBy.map((order) => order.field),
|
|
66
|
+
].filter(isQualifiedField);
|
|
67
|
+
if (referenced.length === 0) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const tenantPredicate = getRuntimeTenantPredicate(context);
|
|
71
|
+
const joins = new Map();
|
|
72
|
+
for (const name of referenced) {
|
|
73
|
+
const { relationshipName, relationship, target } = resolveQualifiedPlanField(ds, name);
|
|
74
|
+
if (joins.has(relationshipName)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
joins.set(relationshipName, {
|
|
78
|
+
relationship: relationshipName,
|
|
79
|
+
source: target.source,
|
|
80
|
+
from: relationship.from,
|
|
81
|
+
to: relationship.to,
|
|
82
|
+
type: 'left',
|
|
83
|
+
tenant: tenantPredicate && target.tenantKey
|
|
84
|
+
? { field: target.tenantKey, ...tenantPredicate }
|
|
85
|
+
: undefined,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return Array.from(joins.values());
|
|
89
|
+
}
|
|
29
90
|
function aggregationForMeasure(ds, name) {
|
|
30
91
|
const measure = ds.measures[name];
|
|
31
92
|
if (!measure) {
|
|
@@ -77,6 +138,7 @@ function aggregatePlan(ds, query, aggregations, context) {
|
|
|
77
138
|
limit: query.limit,
|
|
78
139
|
offset: query.offset,
|
|
79
140
|
tenant: tenantForContext(ds, context),
|
|
141
|
+
joins: collectJoins(ds, query.dimensions, query.filters, query.orderBy, context),
|
|
80
142
|
};
|
|
81
143
|
}
|
|
82
144
|
export function buildDatasetPlan(ds, query = {}, context) {
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,WAAW,EAGZ,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,iBAAiB,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,WAAW,EAGZ,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,iBAAiB,EAKjB,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAC;AAGtB,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxC,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,MAAM,WAAW,sBAAsB,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO;IACzF,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtE;AAED,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,aAAa,GAAG,YAAY,CAAC;AAEvE,MAAM,WAAW,oBAAoB;IACnC,OAAO,CACL,MAAM,EAAE,kBAAkB,GAAG,YAAY,EACzC,KAAK,CAAC,EAAE,YAAY,GAAG,WAAW,EAClC,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;IAC/C,SAAS,EAAE,oBAAoB,CAAC;IAChC,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,UAAU,CAAC;KACxB,CAAC;CACH;AAED,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;CACzB;AAoXD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,sBAAsB,EAAE,CAenG;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,sBAAsB,EAAE,GAAG,oBAAoB,EAAE,CASrF;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,sBAAsB,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAWjG;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,sBAAsB,EAAE,GAAG,iBAAiB,EAAE,CAM/E;AAED,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|
package/dist/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getDatasetCatalog, getDatasetCatalogs, } from './catalog.js';
|
|
1
|
+
import { getDatasetCatalog, getDatasetCatalogs, getQueryableRelationshipFields, } from './catalog.js';
|
|
2
2
|
import { SEMANTIC_FILTER_OPERATORS } from './constants.js';
|
|
3
3
|
function enumSchema(values, description) {
|
|
4
4
|
const unique = Array.from(new Set(values));
|
|
@@ -29,11 +29,19 @@ function limitSchema(catalogs) {
|
|
|
29
29
|
...(finiteLimits.length > 0 ? { maximum: Math.max(...finiteLimits) } : {}),
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
+
function catalogFieldLists(catalog) {
|
|
33
|
+
const relationshipFields = getQueryableRelationshipFields(catalog);
|
|
34
|
+
return {
|
|
35
|
+
dimensions: [...Object.keys(catalog.dimensions), ...relationshipFields],
|
|
36
|
+
filters: [...Object.keys(catalog.filters), ...relationshipFields],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
32
39
|
function querySchema(catalogs, includeDataset) {
|
|
40
|
+
const fieldLists = catalogs.map(catalogFieldLists);
|
|
33
41
|
const datasetNames = catalogs.map(catalog => catalog.name);
|
|
34
|
-
const dimensionNames = Array.from(new Set(
|
|
42
|
+
const dimensionNames = Array.from(new Set(fieldLists.flatMap(fields => fields.dimensions)));
|
|
35
43
|
const measureNames = Array.from(new Set(catalogs.flatMap(catalog => Object.keys(catalog.measures))));
|
|
36
|
-
const filterNames = Array.from(new Set(
|
|
44
|
+
const filterNames = Array.from(new Set(fieldLists.flatMap(fields => fields.filters)));
|
|
37
45
|
const grainNames = Array.from(new Set(catalogs.flatMap(catalog => catalog.supportedGrains)));
|
|
38
46
|
const orderFields = Array.from(new Set(catalogs.flatMap(catalog => catalog.orderableFields)));
|
|
39
47
|
const properties = {
|
|
@@ -150,7 +158,7 @@ function assertAllowedValues(values, allowed, label) {
|
|
|
150
158
|
throw new Error(`Invalid ${label}: ${invalid.join(', ')}. Available: ${allowed.join(', ') || '(none)'}.`);
|
|
151
159
|
}
|
|
152
160
|
}
|
|
153
|
-
function normalizeDatasetQuery(input, catalog, options = {}) {
|
|
161
|
+
function normalizeDatasetQuery(input, catalog, fields, options = {}) {
|
|
154
162
|
const dimensions = assertStringArray(input.dimensions, 'dimensions');
|
|
155
163
|
const measures = assertStringArray(input.measures, 'measures');
|
|
156
164
|
const filters = assertFilters(input.filters);
|
|
@@ -158,12 +166,12 @@ function normalizeDatasetQuery(input, catalog, options = {}) {
|
|
|
158
166
|
const limit = assertNonNegativeInteger(input.limit, 'limit');
|
|
159
167
|
const offset = assertNonNegativeInteger(input.offset, 'offset');
|
|
160
168
|
let by;
|
|
161
|
-
assertAllowedValues(dimensions,
|
|
169
|
+
assertAllowedValues(dimensions, fields.dimensions, 'dimensions');
|
|
162
170
|
assertAllowedValues(measures, Object.keys(catalog.measures), 'measures');
|
|
163
|
-
assertAllowedValues(filters.map(filter => filter.field),
|
|
171
|
+
assertAllowedValues(filters.map(filter => filter.field), fields.filters, 'filter fields');
|
|
164
172
|
assertAllowedValues(orderBy.map(order => order.field), options.orderableFields ?? catalog.orderableFields, 'orderBy fields');
|
|
165
173
|
for (const filter of filters) {
|
|
166
|
-
const allowedOperators = catalog.filters[filter.field]?.operators ?? [];
|
|
174
|
+
const allowedOperators = catalog.filters[filter.field]?.operators ?? [...SEMANTIC_FILTER_OPERATORS];
|
|
167
175
|
if (!allowedOperators.includes(filter.operator)) {
|
|
168
176
|
throw new Error(`Invalid filter operator for "${filter.field}": ${filter.operator}. Allowed: ${allowedOperators.join(', ')}.`);
|
|
169
177
|
}
|
|
@@ -204,6 +212,7 @@ function redactSql(result, includeSql) {
|
|
|
204
212
|
}
|
|
205
213
|
function buildCatalogTool(datasets, catalogs, analytics, includeSql) {
|
|
206
214
|
const catalogList = Object.values(catalogs);
|
|
215
|
+
const fieldLists = Object.fromEntries(Object.entries(catalogs).map(([name, catalog]) => [name, catalogFieldLists(catalog)]));
|
|
207
216
|
return {
|
|
208
217
|
name: 'query_dataset',
|
|
209
218
|
description: 'Query governed analytics datasets by selecting dimensions, measures, filters, and time grains.',
|
|
@@ -213,7 +222,7 @@ function buildCatalogTool(datasets, catalogs, analytics, includeSql) {
|
|
|
213
222
|
throw new Error(`Invalid dataset: ${String(input.dataset)}. Available: ${Object.keys(datasets).join(', ')}.`);
|
|
214
223
|
}
|
|
215
224
|
const catalog = catalogs[input.dataset];
|
|
216
|
-
const query = normalizeDatasetQuery(input, catalog);
|
|
225
|
+
const query = normalizeDatasetQuery(input, catalog, fieldLists[input.dataset]);
|
|
217
226
|
const result = await analytics.execute(datasets[input.dataset], query, context);
|
|
218
227
|
return redactSql(result, includeSql);
|
|
219
228
|
},
|
|
@@ -222,19 +231,20 @@ function buildCatalogTool(datasets, catalogs, analytics, includeSql) {
|
|
|
222
231
|
function buildDatasetTools(datasets, catalogs, analytics, includeSql) {
|
|
223
232
|
return Object.entries(datasets).map(([datasetName, dataset]) => {
|
|
224
233
|
const catalog = catalogs[datasetName];
|
|
234
|
+
const fields = catalogFieldLists(catalog);
|
|
225
235
|
return {
|
|
226
236
|
name: `query_${toolNamePart(datasetName)}`,
|
|
227
237
|
description: `Query the ${datasetName} analytics dataset.`,
|
|
228
238
|
parameters: querySchema([catalog], false),
|
|
229
239
|
async execute(input, context) {
|
|
230
|
-
const query = normalizeDatasetQuery(input, catalog);
|
|
240
|
+
const query = normalizeDatasetQuery(input, catalog, fields);
|
|
231
241
|
const result = await analytics.execute(dataset, query, context);
|
|
232
242
|
return redactSql(result, includeSql);
|
|
233
243
|
},
|
|
234
244
|
};
|
|
235
245
|
});
|
|
236
246
|
}
|
|
237
|
-
function metricQuerySchema(catalog,
|
|
247
|
+
function metricQuerySchema(catalog, orderableFields) {
|
|
238
248
|
const schema = querySchema([catalog], false);
|
|
239
249
|
const properties = schema.properties ?? {};
|
|
240
250
|
delete properties.measures;
|
|
@@ -243,7 +253,7 @@ function metricQuerySchema(catalog, metricName) {
|
|
|
243
253
|
items: {
|
|
244
254
|
type: 'object',
|
|
245
255
|
properties: {
|
|
246
|
-
field: enumSchema(
|
|
256
|
+
field: enumSchema(orderableFields),
|
|
247
257
|
direction: enumSchema(['asc', 'desc']),
|
|
248
258
|
},
|
|
249
259
|
required: ['field', 'direction'],
|
|
@@ -259,18 +269,19 @@ function buildMetricTools(datasets, catalogs, analytics, includeSql) {
|
|
|
259
269
|
const tools = [];
|
|
260
270
|
for (const [datasetName, dataset] of Object.entries(datasets)) {
|
|
261
271
|
const catalog = catalogs[datasetName];
|
|
272
|
+
const fields = catalogFieldLists(catalog);
|
|
262
273
|
for (const [metricName, metric] of Object.entries(dataset.metrics ?? {})) {
|
|
274
|
+
const orderableFields = [
|
|
275
|
+
...fields.dimensions,
|
|
276
|
+
metricName,
|
|
277
|
+
...(catalog.timeKey ? ['period'] : []),
|
|
278
|
+
];
|
|
263
279
|
tools.push({
|
|
264
280
|
name: `query_${toolNamePart(metricName)}`,
|
|
265
281
|
description: `Query the ${metricName} metric from the ${datasetName} dataset.`,
|
|
266
|
-
parameters: metricQuerySchema(catalog,
|
|
282
|
+
parameters: metricQuerySchema(catalog, orderableFields),
|
|
267
283
|
async execute(input, context) {
|
|
268
|
-
const
|
|
269
|
-
...Object.keys(catalog.dimensions),
|
|
270
|
-
metricName,
|
|
271
|
-
...(catalog.timeKey ? ['period'] : []),
|
|
272
|
-
];
|
|
273
|
-
const query = normalizeDatasetQuery({ ...input, measures: [] }, catalog, { orderableFields });
|
|
284
|
+
const query = normalizeDatasetQuery({ ...input, measures: [] }, catalog, fields, { orderableFields });
|
|
274
285
|
const result = await analytics.execute(metric, query, context);
|
|
275
286
|
return redactSql(result, includeSql);
|
|
276
287
|
},
|