@hypequery/datasets 0.7.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 +87 -22
- 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 +33 -16
- 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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution helpers for relationship-qualified field names (`relationship.field`).
|
|
3
|
+
*
|
|
4
|
+
* v1 supports one hop over to-one relationships (`belongsTo`, `hasOne`) only,
|
|
5
|
+
* addressing dimensions declared on the target dataset. `hasMany` stays
|
|
6
|
+
* metadata-only to avoid fan-out corruption of aggregates. See
|
|
7
|
+
* `plans/relationship-aware-semantics-design.md`.
|
|
8
|
+
*/
|
|
9
|
+
import type { AnyDatasetInstance, DimensionDefinition, RelationshipDefinition } from '../types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Enumerates the queryable qualified field names (`<name>.<dimension>`) a
|
|
12
|
+
* relationship contributes, applying the same rules `resolveQualifiedField`
|
|
13
|
+
* enforces at query time: `hasMany` contributes nothing and SQL-backed target
|
|
14
|
+
* dimensions are excluded. Keep the two in sync — the catalog, contract, and
|
|
15
|
+
* generated input schemas all advertise exactly this list.
|
|
16
|
+
*/
|
|
17
|
+
export declare function listQueryableRelationshipFields(name: string, relationship: RelationshipDefinition): string[];
|
|
18
|
+
export interface ParsedQualifiedField {
|
|
19
|
+
/** The relationship name (prefix before the first dot). */
|
|
20
|
+
relationship: string;
|
|
21
|
+
/** Everything after the first dot — the target field, possibly multi-hop. */
|
|
22
|
+
field: string;
|
|
23
|
+
}
|
|
24
|
+
/** True when a field name is relationship-qualified (contains a dot). */
|
|
25
|
+
export declare function isQualifiedField(name: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Splits a qualified name into its relationship prefix and remaining field.
|
|
28
|
+
* Returns null when `name` is not qualified (no dot), so callers can treat it
|
|
29
|
+
* as a local field.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseQualifiedField(name: string): ParsedQualifiedField | null;
|
|
32
|
+
export interface ResolvedQualifiedField {
|
|
33
|
+
/** Relationship name, also used as the SQL table alias / qualified prefix. */
|
|
34
|
+
relationshipName: string;
|
|
35
|
+
relationship: RelationshipDefinition;
|
|
36
|
+
/** The resolved target dataset instance. */
|
|
37
|
+
target: AnyDatasetInstance;
|
|
38
|
+
/** Target dimension name (the allowlist key on the target). */
|
|
39
|
+
targetDimensionName: string;
|
|
40
|
+
targetDimension: DimensionDefinition;
|
|
41
|
+
/** Physical column backing the target dimension. */
|
|
42
|
+
targetColumn: string;
|
|
43
|
+
/** The qualified name exactly as referenced, e.g. `customer.country`. */
|
|
44
|
+
qualifiedName: string;
|
|
45
|
+
}
|
|
46
|
+
export type QualifiedFieldResolution = {
|
|
47
|
+
resolved: ResolvedQualifiedField;
|
|
48
|
+
error?: undefined;
|
|
49
|
+
} | {
|
|
50
|
+
resolved?: undefined;
|
|
51
|
+
error: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Resolves a relationship-qualified field name against a base dataset.
|
|
55
|
+
*
|
|
56
|
+
* Returns null when `name` is not qualified (caller handles it as a local
|
|
57
|
+
* field). Otherwise returns either the resolved to-one target dimension or an
|
|
58
|
+
* actionable validation error (unknown relationship, multi-hop, `hasMany`,
|
|
59
|
+
* unknown target dimension, or SQL-backed target dimension).
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveQualifiedField(ds: AnyDatasetInstance, name: string): QualifiedFieldResolution | null;
|
|
62
|
+
//# sourceMappingURL=relationship-fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-fields.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-fields.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AAErB;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,sBAAsB,GACnC,MAAM,EAAE,CAQV;AAED,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;CACf;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAM7E;AAED,MAAM,WAAW,sBAAsB;IACrC,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,sBAAsB,CAAC;IACrC,4CAA4C;IAC5C,MAAM,EAAE,kBAAkB,CAAC;IAC3B,+DAA+D;IAC/D,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,mBAAmB,CAAC;IACrC,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,wBAAwB,GAChC;IAAE,QAAQ,EAAE,sBAAsB,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,MAAM,GACX,wBAAwB,GAAG,IAAI,CAuDjC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution helpers for relationship-qualified field names (`relationship.field`).
|
|
3
|
+
*
|
|
4
|
+
* v1 supports one hop over to-one relationships (`belongsTo`, `hasOne`) only,
|
|
5
|
+
* addressing dimensions declared on the target dataset. `hasMany` stays
|
|
6
|
+
* metadata-only to avoid fan-out corruption of aggregates. See
|
|
7
|
+
* `plans/relationship-aware-semantics-design.md`.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Enumerates the queryable qualified field names (`<name>.<dimension>`) a
|
|
11
|
+
* relationship contributes, applying the same rules `resolveQualifiedField`
|
|
12
|
+
* enforces at query time: `hasMany` contributes nothing and SQL-backed target
|
|
13
|
+
* dimensions are excluded. Keep the two in sync — the catalog, contract, and
|
|
14
|
+
* generated input schemas all advertise exactly this list.
|
|
15
|
+
*/
|
|
16
|
+
export function listQueryableRelationshipFields(name, relationship) {
|
|
17
|
+
if (relationship.kind === 'hasMany') {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const target = relationship.target();
|
|
21
|
+
return Object.entries(target?.dimensions ?? {})
|
|
22
|
+
.filter(([, dimension]) => !dimension.sql)
|
|
23
|
+
.map(([field]) => `${name}.${field}`);
|
|
24
|
+
}
|
|
25
|
+
/** True when a field name is relationship-qualified (contains a dot). */
|
|
26
|
+
export function isQualifiedField(name) {
|
|
27
|
+
return name.includes('.');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Splits a qualified name into its relationship prefix and remaining field.
|
|
31
|
+
* Returns null when `name` is not qualified (no dot), so callers can treat it
|
|
32
|
+
* as a local field.
|
|
33
|
+
*/
|
|
34
|
+
export function parseQualifiedField(name) {
|
|
35
|
+
const dot = name.indexOf('.');
|
|
36
|
+
if (dot === -1) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return { relationship: name.slice(0, dot), field: name.slice(dot + 1) };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolves a relationship-qualified field name against a base dataset.
|
|
43
|
+
*
|
|
44
|
+
* Returns null when `name` is not qualified (caller handles it as a local
|
|
45
|
+
* field). Otherwise returns either the resolved to-one target dimension or an
|
|
46
|
+
* actionable validation error (unknown relationship, multi-hop, `hasMany`,
|
|
47
|
+
* unknown target dimension, or SQL-backed target dimension).
|
|
48
|
+
*/
|
|
49
|
+
export function resolveQualifiedField(ds, name) {
|
|
50
|
+
const parsed = parseQualifiedField(name);
|
|
51
|
+
if (!parsed) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const { relationship: relationshipName, field } = parsed;
|
|
55
|
+
const relationship = ds.relationships[relationshipName];
|
|
56
|
+
if (!relationship) {
|
|
57
|
+
const available = Object.keys(ds.relationships);
|
|
58
|
+
return {
|
|
59
|
+
error: available.length > 0
|
|
60
|
+
? `Unknown relationship "${relationshipName}" in field "${name}" on dataset "${ds.name}". Available relationships: ${available.join(', ')}.`
|
|
61
|
+
: `Unknown field "${name}": dataset "${ds.name}" declares no relationships.`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (field.includes('.')) {
|
|
65
|
+
return {
|
|
66
|
+
error: `Multi-hop relationship path "${name}" is not supported. Only a single hop is allowed (e.g. "${relationshipName}.<field>").`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (relationship.kind === 'hasMany') {
|
|
70
|
+
return {
|
|
71
|
+
error: `Relationship "${relationshipName}" on dataset "${ds.name}" is a hasMany (to-many) relationship and is not queryable, because joining it would fan out and corrupt aggregates. It remains available as metadata only.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const target = relationship.target();
|
|
75
|
+
const targetDimension = target.dimensions[field];
|
|
76
|
+
if (!targetDimension) {
|
|
77
|
+
const available = Object.keys(target.dimensions);
|
|
78
|
+
return {
|
|
79
|
+
error: `Unknown dimension "${field}" on relationship "${relationshipName}" (target dataset "${target.name}"). Available: ${available.join(', ')}.`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (targetDimension.sql) {
|
|
83
|
+
return {
|
|
84
|
+
error: `Dimension "${name}" is SQL-backed on target dataset "${target.name}"; SQL-backed dimensions are not yet queryable through relationships.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
resolved: {
|
|
89
|
+
relationshipName,
|
|
90
|
+
relationship,
|
|
91
|
+
target,
|
|
92
|
+
targetDimensionName: field,
|
|
93
|
+
targetDimension,
|
|
94
|
+
targetColumn: targetDimension.column ?? field,
|
|
95
|
+
qualifiedName: name,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared validation for relationship-qualified filters, used by both the
|
|
3
|
+
* dataset-query and metric-query validators.
|
|
4
|
+
*/
|
|
5
|
+
import type { AnyDatasetInstance, ExecutionContext, MetricFilter } from '../types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Validates a relationship-qualified filter (`relationship.field`). Returns an
|
|
8
|
+
* error message string, or null when the filter is valid.
|
|
9
|
+
*
|
|
10
|
+
* Enforces the same tenant rule the base dataset uses: when runtime tenancy is
|
|
11
|
+
* active and the joined target declares a `tenantKey`, an explicit filter on
|
|
12
|
+
* that column is rejected (the planner injects the tenant predicate instead).
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateQualifiedFilter(ds: AnyDatasetInstance, filter: MetricFilter, context?: ExecutionContext): string | null;
|
|
15
|
+
//# sourceMappingURL=relationship-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-validation.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-validation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACb,MAAM,aAAa,CAAC;AAKrB;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,IAAI,CAgBf"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared validation for relationship-qualified filters, used by both the
|
|
3
|
+
* dataset-query and metric-query validators.
|
|
4
|
+
*/
|
|
5
|
+
import { validateFilterValue } from '../validation.js';
|
|
6
|
+
import { getRuntimeTenantPredicate } from './tenant-runtime.js';
|
|
7
|
+
import { resolveQualifiedField } from './relationship-fields.js';
|
|
8
|
+
/**
|
|
9
|
+
* Validates a relationship-qualified filter (`relationship.field`). Returns an
|
|
10
|
+
* error message string, or null when the filter is valid.
|
|
11
|
+
*
|
|
12
|
+
* Enforces the same tenant rule the base dataset uses: when runtime tenancy is
|
|
13
|
+
* active and the joined target declares a `tenantKey`, an explicit filter on
|
|
14
|
+
* that column is rejected (the planner injects the tenant predicate instead).
|
|
15
|
+
*/
|
|
16
|
+
export function validateQualifiedFilter(ds, filter, context) {
|
|
17
|
+
const resolution = resolveQualifiedField(ds, filter.field);
|
|
18
|
+
if (!resolution) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (!resolution.resolved) {
|
|
22
|
+
return resolution.error;
|
|
23
|
+
}
|
|
24
|
+
const { target, targetColumn, targetDimension } = resolution.resolved;
|
|
25
|
+
if (getRuntimeTenantPredicate(context) && target.tenantKey && targetColumn === target.tenantKey) {
|
|
26
|
+
return `Cannot filter on tenant field "${filter.field}" when runtime tenancy enforcement is active.`;
|
|
27
|
+
}
|
|
28
|
+
return validateFilterValue(filter, targetDimension.fieldType);
|
|
29
|
+
}
|