@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.
Files changed (57) hide show
  1. package/dist/api.type-test.js +1 -0
  2. package/dist/catalog.d.ts +4 -1
  3. package/dist/catalog.d.ts.map +1 -1
  4. package/dist/catalog.js +16 -6
  5. package/dist/contract.d.ts +3 -1
  6. package/dist/contract.d.ts.map +1 -1
  7. package/dist/contract.js +3 -1
  8. package/dist/dataset-query.d.ts.map +1 -1
  9. package/dist/dataset-query.js +8 -5
  10. package/dist/dataset.d.ts +4 -3
  11. package/dist/dataset.d.ts.map +1 -1
  12. package/dist/dataset.js +5 -4
  13. package/dist/executor.d.ts +7 -1
  14. package/dist/executor.d.ts.map +1 -1
  15. package/dist/executor.js +39 -10
  16. package/dist/field.d.ts +4 -4
  17. package/dist/field.d.ts.map +1 -1
  18. package/dist/in-memory-backend.d.ts +5 -0
  19. package/dist/in-memory-backend.d.ts.map +1 -1
  20. package/dist/in-memory-backend.js +41 -1
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +5 -1
  24. package/dist/query-builder-protocol.d.ts +12 -0
  25. package/dist/query-builder-protocol.d.ts.map +1 -1
  26. package/dist/query-planner.d.ts +7 -6
  27. package/dist/query-planner.d.ts.map +1 -1
  28. package/dist/query-planner.js +63 -16
  29. package/dist/relationships.d.ts +9 -9
  30. package/dist/relationships.d.ts.map +1 -1
  31. package/dist/semantic-plan.d.ts +50 -9
  32. package/dist/semantic-plan.d.ts.map +1 -1
  33. package/dist/semantic-plan.js +9 -0
  34. package/dist/semantic-planner.d.ts.map +1 -1
  35. package/dist/semantic-planner.js +67 -5
  36. package/dist/tools.d.ts.map +1 -1
  37. package/dist/tools.js +29 -18
  38. package/dist/types.d.ts +29 -12
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/utils/dataset-normalization.d.ts +1 -1
  41. package/dist/utils/dataset-normalization.d.ts.map +1 -1
  42. package/dist/utils/dataset-normalization.js +11 -1
  43. package/dist/utils/dataset-query-validation.d.ts.map +1 -1
  44. package/dist/utils/dataset-query-validation.js +50 -12
  45. package/dist/utils/filtered-aggregation-sql.d.ts +2 -1
  46. package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -1
  47. package/dist/utils/filtered-aggregation-sql.js +4 -4
  48. package/dist/utils/relationship-builder-plan.d.ts +56 -0
  49. package/dist/utils/relationship-builder-plan.d.ts.map +1 -0
  50. package/dist/utils/relationship-builder-plan.js +92 -0
  51. package/dist/utils/relationship-fields.d.ts +62 -0
  52. package/dist/utils/relationship-fields.d.ts.map +1 -0
  53. package/dist/utils/relationship-fields.js +98 -0
  54. package/dist/utils/relationship-validation.d.ts +15 -0
  55. package/dist/utils/relationship-validation.d.ts.map +1 -0
  56. package/dist/utils/relationship-validation.js +29 -0
  57. 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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"}
@@ -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;AAKpE,KAAK,YAAY,GAAG,kBAAkB,CAAC;AAMvC,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,YAAY,EAChB,aAAa,EAAE,MAAM,GACpB,MAAM,CAGR;AAED,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,YAAY,EAChB,WAAW,EAAE,MAAM,GAClB,MAAM,CAGR;AAED,wBAAgB,2BAA2B,CACzC,EAAE,EAAE,YAAY,EAChB,UAAU,EAAE,MAAM,EAAE,EACpB,KAAK,EAAE,SAAS,GAAG,SAAS,GAC3B;IAAE,WAAW,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,CAwBnD;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,gBAAgB,EACpB,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,MAAM,GACZ,gBAAgB,CAuBlB;AAED,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,iBAAiB,GAC5B,gBAAgB,CA6BlB;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,GACd,gBAAgB,CAiBlB;AAED,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,YAAY,EAChB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAMpB"}
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"}
@@ -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
- export function resolveDimensionExpression(ds, dimensionName) {
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
- return definition?.sql ?? definition?.column ?? dimensionName;
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 (expression === dimensionName) {
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
- const fieldOrExpr = applyFilteredAggregationExpression(ds, spec, spec.sql ?? resolveDimensionExpression(ds, spec.field));
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
- const baseFieldOrExpr = definition.sql ?? resolveDimensionExpression(ds, definition.field);
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
- qb = qb.orderBy(order.field, toOrderDirection(order.direction));
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) {
@@ -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(target: () => {
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(target: () => {
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(target: () => {
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;AAgB3E,mDAAmD;AACnD,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACjD,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAExB;AAED,qDAAqD;AACrD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACjD,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAExB;AAED,oDAAoD;AACpD,wBAAgB,MAAM,CACpB,MAAM,EAAE,MAAM;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACjD,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACjC,sBAAsB,CAExB"}
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"}
@@ -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
- field: string;
50
- operator: 'eq';
51
- value: string;
52
- } | {
53
- field: string;
54
- operator: 'in';
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":"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"}
1
+ {"version":3,"file":"semantic-plan.d.ts","sourceRoot":"","sources":["../src/semantic-plan.ts"],"names":[],"mappings":"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"}
@@ -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,EAEhB,WAAW,EACX,SAAS,EACT,gBAAgB,EACjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EACV,QAAQ,EAGT,MAAM,oBAAoB,CAAC;AAgH5B,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"}
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"}
@@ -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
- field: resolveField(ds, name),
17
- fieldType: ds.dimensions[name]?.fieldType,
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) {
@@ -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,EAIjB,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;AA8VD,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"}
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(catalogs.flatMap(catalog => Object.keys(catalog.dimensions))));
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(catalogs.flatMap(catalog => Object.keys(catalog.filters))));
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, Object.keys(catalog.dimensions), 'dimensions');
169
+ assertAllowedValues(dimensions, fields.dimensions, 'dimensions');
162
170
  assertAllowedValues(measures, Object.keys(catalog.measures), 'measures');
163
- assertAllowedValues(filters.map(filter => filter.field), Object.keys(catalog.filters), 'filter fields');
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, metricName) {
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([...Object.keys(catalog.dimensions), metricName, ...(catalog.timeKey ? ['period'] : [])]),
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, metricName),
282
+ parameters: metricQuerySchema(catalog, orderableFields),
267
283
  async execute(input, context) {
268
- const orderableFields = [
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
  },