@hypequery/datasets 0.21.0 → 0.23.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/README.md +6 -0
- package/dist/api.type-test.js +7 -0
- package/dist/cache/protocol-cache-keys.d.ts.map +1 -1
- package/dist/cache/protocol-cache-keys.js +5 -0
- package/dist/cache/query-signature.d.ts.map +1 -1
- package/dist/cache/query-signature.js +9 -0
- package/dist/catalog.d.ts +12 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +14 -1
- package/dist/constants.d.ts +12 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +24 -0
- package/dist/contract.d.ts +4 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +4 -0
- package/dist/dataset-compilation.d.ts +50 -0
- package/dist/dataset-compilation.d.ts.map +1 -0
- package/dist/dataset-compilation.js +1 -0
- package/dist/dataset-query.d.ts +3 -3
- package/dist/dataset-query.d.ts.map +1 -1
- package/dist/dataset-query.js +7 -71
- package/dist/dataset-sql-dialect.d.ts +11 -0
- package/dist/dataset-sql-dialect.d.ts.map +1 -0
- package/dist/dataset-sql-dialect.js +1 -0
- package/dist/dataset.d.ts +3 -2
- package/dist/dataset.d.ts.map +1 -1
- package/dist/execution-capabilities.d.ts +28 -0
- package/dist/execution-capabilities.d.ts.map +1 -0
- package/dist/execution-capabilities.js +17 -0
- package/dist/execution-capabilities.json +13 -0
- package/dist/executor.d.ts +17 -38
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +40 -400
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/internal.d.ts +3 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -0
- package/dist/measure.d.ts +54 -17
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +3 -63
- package/dist/metric-query-engine.d.ts +29 -0
- package/dist/metric-query-engine.d.ts.map +1 -0
- package/dist/metric-query-engine.js +106 -0
- package/dist/portable-executor.d.ts +20 -6
- package/dist/portable-executor.d.ts.map +1 -1
- package/dist/portable-executor.js +29 -9
- package/dist/protocol-adapter.d.ts.map +1 -1
- package/dist/protocol-adapter.js +1 -0
- package/dist/protocol-rehydrate.d.ts.map +1 -1
- package/dist/protocol-rehydrate.js +9 -2
- package/dist/query-builder-protocol.d.ts +11 -0
- package/dist/query-builder-protocol.d.ts.map +1 -1
- package/dist/query-helpers.d.ts +10 -10
- package/dist/query-helpers.d.ts.map +1 -1
- package/dist/query-helpers.js +1 -3
- package/dist/query-planner.d.ts +7 -3
- package/dist/query-planner.d.ts.map +1 -1
- package/dist/query-planner.js +10 -12
- package/dist/relationship-check.d.ts +3 -2
- package/dist/relationship-check.d.ts.map +1 -1
- package/dist/relationship-check.js +8 -3
- package/dist/relationships.d.ts +6 -16
- package/dist/relationships.d.ts.map +1 -1
- package/dist/relationships.js +3 -12
- package/dist/semantic-planner.d.ts.map +1 -1
- package/dist/semantic-planner.js +4 -0
- package/dist/semantic-query-schema.d.ts +6 -0
- package/dist/semantic-query-schema.d.ts.map +1 -1
- package/dist/semantic-query-schema.js +31 -4
- package/dist/types.d.ts +67 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/agent-catalog-projection.d.ts +1 -0
- package/dist/utils/agent-catalog-projection.d.ts.map +1 -1
- package/dist/utils/agent-catalog-projection.js +2 -1
- package/dist/utils/base-measure-definition.d.ts +23 -0
- package/dist/utils/base-measure-definition.d.ts.map +1 -0
- package/dist/utils/base-measure-definition.js +65 -0
- package/dist/utils/build-dataset-query-builder.d.ts +5 -0
- package/dist/utils/build-dataset-query-builder.d.ts.map +1 -0
- package/dist/utils/build-dataset-query-builder.js +65 -0
- package/dist/utils/calendar-shift-sql.d.ts +18 -0
- package/dist/utils/calendar-shift-sql.d.ts.map +1 -0
- package/dist/utils/calendar-shift-sql.js +21 -0
- package/dist/utils/clickhouse-dataset-sql-dialect.d.ts +4 -0
- package/dist/utils/clickhouse-dataset-sql-dialect.d.ts.map +1 -0
- package/dist/utils/clickhouse-dataset-sql-dialect.js +6 -0
- package/dist/utils/compile-dataset-query.d.ts +10 -0
- package/dist/utils/compile-dataset-query.d.ts.map +1 -0
- package/dist/utils/compile-dataset-query.js +72 -0
- package/dist/utils/composite-time-measure-sql.d.ts.map +1 -1
- package/dist/utils/composite-time-measure-sql.js +2 -1
- package/dist/utils/dataset-definition-validation.d.ts.map +1 -1
- package/dist/utils/dataset-definition-validation.js +52 -2
- package/dist/utils/dataset-derived-query.d.ts +6 -2
- package/dist/utils/dataset-derived-query.d.ts.map +1 -1
- package/dist/utils/dataset-derived-query.js +28 -26
- package/dist/utils/dataset-filter-types.d.ts +28 -0
- package/dist/utils/dataset-filter-types.d.ts.map +1 -0
- package/dist/utils/dataset-filter-types.js +1 -0
- package/dist/utils/dataset-having.d.ts +21 -0
- package/dist/utils/dataset-having.d.ts.map +1 -0
- package/dist/utils/dataset-having.js +106 -0
- package/dist/utils/dataset-normalization.d.ts.map +1 -1
- package/dist/utils/dataset-normalization.js +5 -1
- package/dist/utils/dataset-query-validation.d.ts.map +1 -1
- package/dist/utils/dataset-query-validation.js +17 -3
- package/dist/utils/dataset-sql-dialect.d.ts +5 -0
- package/dist/utils/dataset-sql-dialect.d.ts.map +1 -0
- package/dist/utils/dataset-sql-dialect.js +5 -0
- package/dist/utils/dataset-target.d.ts +3 -0
- package/dist/utils/dataset-target.d.ts.map +1 -0
- package/dist/utils/dataset-target.js +3 -0
- package/dist/utils/filter-definition.d.ts +3 -0
- package/dist/utils/filter-definition.d.ts.map +1 -0
- package/dist/utils/filter-definition.js +3 -0
- package/dist/utils/metric-derived-query.d.ts +8 -0
- package/dist/utils/metric-derived-query.d.ts.map +1 -0
- package/dist/utils/metric-derived-query.js +106 -0
- package/dist/utils/metric-query-builder.d.ts +5 -0
- package/dist/utils/metric-query-builder.d.ts.map +1 -0
- package/dist/utils/metric-query-builder.js +50 -0
- package/dist/utils/metric-query-result.d.ts +10 -0
- package/dist/utils/metric-query-result.d.ts.map +1 -0
- package/dist/utils/metric-query-result.js +18 -0
- package/dist/utils/metric-query-validation.d.ts +6 -0
- package/dist/utils/metric-query-validation.d.ts.map +1 -0
- package/dist/utils/metric-query-validation.js +116 -0
- package/dist/utils/protocol-metric-expressions.d.ts +8 -0
- package/dist/utils/protocol-metric-expressions.d.ts.map +1 -1
- package/dist/utils/protocol-metric-expressions.js +23 -11
- package/dist/utils/relationship-builder-plan.d.ts +6 -2
- package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
- package/dist/utils/relationship-builder-plan.js +24 -15
- package/dist/utils/relationship-definition.d.ts +6 -0
- package/dist/utils/relationship-definition.d.ts.map +1 -0
- package/dist/utils/relationship-definition.js +9 -0
- package/dist/utils/relationship-key-check.d.ts +4 -0
- package/dist/utils/relationship-key-check.d.ts.map +1 -1
- package/dist/utils/relationship-key-check.js +19 -7
- package/dist/utils/relationship-keys.d.ts +17 -0
- package/dist/utils/relationship-keys.d.ts.map +1 -0
- package/dist/utils/relationship-keys.js +63 -0
- package/dist/utils/relationship-measure-sql.d.ts +14 -0
- package/dist/utils/relationship-measure-sql.d.ts.map +1 -0
- package/dist/utils/relationship-measure-sql.js +44 -0
- package/dist/utils/relationship-measure-types.d.ts +36 -0
- package/dist/utils/relationship-measure-types.d.ts.map +1 -0
- package/dist/utils/relationship-measure-types.js +1 -0
- package/dist/utils/relationship-measures.d.ts +23 -0
- package/dist/utils/relationship-measures.d.ts.map +1 -0
- package/dist/utils/relationship-measures.js +62 -0
- package/dist/utils/relationship-validation.d.ts +1 -1
- package/dist/utils/relationship-validation.d.ts.map +1 -1
- package/dist/utils/relationship-validation.js +3 -5
- package/dist/utils/semantic-query-syntax.d.ts +6 -0
- package/dist/utils/semantic-query-syntax.d.ts.map +1 -0
- package/dist/utils/semantic-query-syntax.js +24 -0
- package/dist/utils/shift-measure-sql.d.ts.map +1 -1
- package/dist/utils/shift-measure-sql.js +10 -3
- package/dist/utils/snapshot-execution-context.d.ts +4 -0
- package/dist/utils/snapshot-execution-context.d.ts.map +1 -0
- package/dist/utils/snapshot-execution-context.js +16 -0
- package/dist/utils/time-measure-context-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-context-sql.js +8 -2
- package/dist/utils/time-measure-dataset-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-dataset-sql.js +3 -2
- package/dist/utils/time-measure-result-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-result-sql.js +2 -1
- package/dist/utils/time-measure-source-sql.d.ts.map +1 -1
- package/dist/utils/time-measure-source-sql.js +8 -4
- package/dist/validation.js +1 -1
- package/package.json +10 -5
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RelationshipDefinition, RelationshipJoin, RelationshipKey } from '../types.js';
|
|
2
|
+
type NormalizedRelationshipJoin = Pick<RelationshipDefinition, 'from' | 'to' | 'keys'>;
|
|
3
|
+
/**
|
|
4
|
+
* Snapshot an authored join. `{ from, to }` passes through unchanged, as it
|
|
5
|
+
* always has; `keys` pairs are validated before they are interpolated into SQL.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeRelationshipJoin(join: RelationshipJoin): NormalizedRelationshipJoin;
|
|
8
|
+
/**
|
|
9
|
+
* Rebuild a serialized composite key. Portable records carry `keys` only for
|
|
10
|
+
* composite relationships, and `from`/`to` must mirror the first pair.
|
|
11
|
+
*/
|
|
12
|
+
export declare function rehydrateCompositeRelationshipJoin(from: string, to: string, keys: readonly RelationshipKey[]): NormalizedRelationshipJoin;
|
|
13
|
+
export declare function validateRelationshipKeys(keys: readonly RelationshipKey[]): asserts keys is readonly [RelationshipKey, ...RelationshipKey[]];
|
|
14
|
+
/** Return the whole equality key; legacy relationships remain single-key. */
|
|
15
|
+
export declare function relationshipKeys(relationship: Pick<RelationshipDefinition, 'from' | 'to' | 'keys'>): readonly RelationshipKey[];
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=relationship-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-keys.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-keys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAI7F,KAAK,0BAA0B,GAAG,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,CAAC;AAEvF;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,gBAAgB,GAAG,0BAA0B,CAgB5F;AAED;;;GAGG;AACH,wBAAgB,kCAAkC,CAChD,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,SAAS,eAAe,EAAE,GAC/B,0BAA0B,CAU5B;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,SAAS,eAAe,EAAE,GAC/B,OAAO,CAAC,IAAI,IAAI,SAAS,CAAC,eAAe,EAAE,GAAG,eAAe,EAAE,CAAC,CAYlE;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC,GAAG,SAAS,eAAe,EAAE,CAQ/H"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const COLUMN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot an authored join. `{ from, to }` passes through unchanged, as it
|
|
4
|
+
* always has; `keys` pairs are validated before they are interpolated into SQL.
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeRelationshipJoin(join) {
|
|
7
|
+
if (join.keys === undefined) {
|
|
8
|
+
return { from: join.from, to: join.to };
|
|
9
|
+
}
|
|
10
|
+
if (join.from !== undefined || join.to !== undefined) {
|
|
11
|
+
throw new Error('Relationship keys cannot be combined with from/to.');
|
|
12
|
+
}
|
|
13
|
+
const keys = join.keys;
|
|
14
|
+
validateRelationshipKeys(keys);
|
|
15
|
+
const copy = [
|
|
16
|
+
Object.freeze({ from: keys[0].from, to: keys[0].to }),
|
|
17
|
+
...keys.slice(1).map(key => Object.freeze({ from: key.from, to: key.to })),
|
|
18
|
+
];
|
|
19
|
+
// A single pair is the legacy relationship: contracts and catalogs carry
|
|
20
|
+
// `keys` only for composite relationships, however the pair was authored.
|
|
21
|
+
return { from: copy[0].from, to: copy[0].to, ...(copy.length > 1 ? { keys: Object.freeze(copy) } : {}) };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Rebuild a serialized composite key. Portable records carry `keys` only for
|
|
25
|
+
* composite relationships, and `from`/`to` must mirror the first pair.
|
|
26
|
+
*/
|
|
27
|
+
export function rehydrateCompositeRelationshipJoin(from, to, keys) {
|
|
28
|
+
validateRelationshipKeys(keys);
|
|
29
|
+
if (keys.length < 2) {
|
|
30
|
+
throw new Error('Serialized relationship keys must contain at least two pairs; single-key relationships use from/to.');
|
|
31
|
+
}
|
|
32
|
+
const join = normalizeRelationshipJoin({ keys });
|
|
33
|
+
if (join.from !== from || join.to !== to) {
|
|
34
|
+
throw new Error('Relationship from/to must match the first keys pair.');
|
|
35
|
+
}
|
|
36
|
+
return join;
|
|
37
|
+
}
|
|
38
|
+
export function validateRelationshipKeys(keys) {
|
|
39
|
+
if (!Array.isArray(keys) || keys.length === 0)
|
|
40
|
+
throw new Error('Relationship keys must be a non-empty array.');
|
|
41
|
+
const from = new Set();
|
|
42
|
+
const to = new Set();
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
if (!key || typeof key.from !== 'string' || typeof key.to !== 'string' || !COLUMN.test(key.from) || !COLUMN.test(key.to)) {
|
|
45
|
+
throw new Error('Relationship keys must contain safe physical from/to column identifiers.');
|
|
46
|
+
}
|
|
47
|
+
if (from.has(key.from) || to.has(key.to))
|
|
48
|
+
throw new Error('Relationship keys must not repeat a source or target column.');
|
|
49
|
+
from.add(key.from);
|
|
50
|
+
to.add(key.to);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Return the whole equality key; legacy relationships remain single-key. */
|
|
54
|
+
export function relationshipKeys(relationship) {
|
|
55
|
+
const keys = relationship.keys ?? [{ from: relationship.from, to: relationship.to }];
|
|
56
|
+
if (!relationship.keys)
|
|
57
|
+
return keys;
|
|
58
|
+
validateRelationshipKeys(keys);
|
|
59
|
+
if (keys[0].from !== relationship.from || keys[0].to !== relationship.to) {
|
|
60
|
+
throw new Error('Relationship from/to must match the first keys pair.');
|
|
61
|
+
}
|
|
62
|
+
return keys;
|
|
63
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AnyDatasetInstance } from '../types.js';
|
|
2
|
+
import type { RelationshipBuilderContext } from './relationship-builder-plan.js';
|
|
3
|
+
import type { DatasetSqlDialect } from '../dataset-sql-dialect.js';
|
|
4
|
+
/** Explicit projection avoids marker collisions and makes absent targets detectable under join_use_nulls=0. */
|
|
5
|
+
export declare function relationshipMeasureSource(target: AnyDatasetInstance, to: readonly string[], names: readonly string[], dialect: DatasetSqlDialect): {
|
|
6
|
+
source: string;
|
|
7
|
+
marker: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function relationshipMeasureExpressions(ds: AnyDatasetInstance, name: string, joinCtx: RelationshipBuilderContext): {
|
|
10
|
+
definition: import("../types.js").MeasureDefinition;
|
|
11
|
+
field: string;
|
|
12
|
+
arg: string | undefined;
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=relationship-measure-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-measure-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAmB,MAAM,aAAa,CAAC;AAEvE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAIjF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEnE,+GAA+G;AAC/G,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,EAAE,EAAE,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,iBAAiB;;;EAgBhJ;AAED,wBAAgB,8BAA8B,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,0BAA0B;;;;EAevH"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { resolveRelationshipMeasure } from './relationship-measures.js';
|
|
2
|
+
import { resolveDimensionExpression } from '../query-planner.js';
|
|
3
|
+
import { measureToAggregationSpec } from './dataset-normalization.js';
|
|
4
|
+
import { applyFilteredAggregationExpression } from './filtered-aggregation-sql.js';
|
|
5
|
+
/** Explicit projection avoids marker collisions and makes absent targets detectable under join_use_nulls=0. */
|
|
6
|
+
export function relationshipMeasureSource(target, to, names, dialect) {
|
|
7
|
+
const columns = new Set([...to, ...(target.tenantKey ? [target.tenantKey] : [])]);
|
|
8
|
+
for (const [name, dimension] of Object.entries(target.dimensions))
|
|
9
|
+
if (!dimension.sql)
|
|
10
|
+
columns.add(dimension.column ?? name);
|
|
11
|
+
for (const name of names) {
|
|
12
|
+
const measure = target.measures[name];
|
|
13
|
+
if (measure.__type !== 'measure_definition')
|
|
14
|
+
continue;
|
|
15
|
+
for (const field of [measure.field, measure.argField, ...(measure.filters ?? []).map(filter => target.filters[filter.field]?.field ?? filter.field)]) {
|
|
16
|
+
if (field && field !== '*')
|
|
17
|
+
columns.add(target.dimensions[field]?.column ?? field);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
let marker = '_hq_match';
|
|
21
|
+
while (columns.has(marker))
|
|
22
|
+
marker += '_';
|
|
23
|
+
const quote = dialect.quoteIdentifier;
|
|
24
|
+
const source = target.source.split('.').map(quote).join('.');
|
|
25
|
+
const projection = [...columns].map(quote);
|
|
26
|
+
return { source: `(SELECT ${[...projection, `toNullable(1) AS ${quote(marker)}`].join(', ')} FROM ${source})`, marker };
|
|
27
|
+
}
|
|
28
|
+
export function relationshipMeasureExpressions(ds, name, joinCtx) {
|
|
29
|
+
const resolved = resolveRelationshipMeasure(ds, name);
|
|
30
|
+
const { relationshipName, target, definition } = resolved;
|
|
31
|
+
const marker = joinCtx.joinByRelationship.get(relationshipName)?.matchMarker;
|
|
32
|
+
if (!marker)
|
|
33
|
+
throw new Error(`Missing match marker for relationship measure "${name}".`);
|
|
34
|
+
const ownerContext = { ...joinCtx, baseSource: relationshipName };
|
|
35
|
+
const spec = measureToAggregationSpec(name, definition);
|
|
36
|
+
const matched = `isNotNull(${relationshipName}.${marker})`;
|
|
37
|
+
const field = definition.field === '*' ? `${relationshipName}.${marker}` : resolveDimensionExpression(target, definition.field, ownerContext);
|
|
38
|
+
const filtered = applyFilteredAggregationExpression(target, spec, field, ownerContext);
|
|
39
|
+
return {
|
|
40
|
+
definition,
|
|
41
|
+
field: `if(${matched}, ${filtered}, NULL)`,
|
|
42
|
+
arg: definition.argField ? `if(${matched}, ${resolveDimensionExpression(target, definition.argField, ownerContext)}, NULL)` : undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DatasetInstance, MeasureDefinition, RelationshipDefinition } from '../types.js';
|
|
2
|
+
type SafeAggregation = 'countDistinct' | 'approxCountDistinct' | 'min' | 'max' | 'argMax' | 'argMin';
|
|
3
|
+
type SqlDimensions<TDimensions> = {
|
|
4
|
+
[K in keyof TDimensions & string]: TDimensions[K] extends {
|
|
5
|
+
sql: string;
|
|
6
|
+
} ? K : never;
|
|
7
|
+
}[keyof TDimensions & string];
|
|
8
|
+
/** Fixed filters use their owner's allowlist aliases before dimension resolution. */
|
|
9
|
+
type FixedFilterInputs<TMeasure, TFilters> = TMeasure extends {
|
|
10
|
+
filters: readonly (infer TFilter)[];
|
|
11
|
+
} ? TFilter extends {
|
|
12
|
+
field: infer TName extends string;
|
|
13
|
+
} ? TName extends keyof TFilters ? TFilters[TName] extends {
|
|
14
|
+
field: infer TField extends string;
|
|
15
|
+
} ? TField : TName : TName : never : never;
|
|
16
|
+
type SafeMeasureNames<TMeasures, TKind, TDimensions, TFilters> = {
|
|
17
|
+
[K in keyof TMeasures & string]: TMeasures[K] extends MeasureDefinition ? TMeasures[K] extends {
|
|
18
|
+
sql: string;
|
|
19
|
+
} ? never : TMeasures[K] extends {
|
|
20
|
+
field: infer F;
|
|
21
|
+
argField?: infer A;
|
|
22
|
+
} ? (Extract<F, SqlDimensions<TDimensions>> | Extract<A, SqlDimensions<TDimensions>> | Extract<FixedFilterInputs<TMeasures[K], TFilters>, SqlDimensions<TDimensions>>) extends never ? TKind extends 'hasOne' ? K : TMeasures[K] extends {
|
|
23
|
+
aggregation: SafeAggregation;
|
|
24
|
+
} ? K : never : never : never : never;
|
|
25
|
+
}[keyof TMeasures & string];
|
|
26
|
+
/** One-hop base aggregates whose cardinality is safe for the declared relationship. */
|
|
27
|
+
export type QueryableRelationshipMeasureNames<TRelationships> = {
|
|
28
|
+
[K in keyof TRelationships & string]: TRelationships[K] extends RelationshipDefinition<infer TTarget, infer TKind> ? TKind extends 'hasMany' ? never : TTarget extends DatasetInstance<infer TDimensions, infer TMeasures, any, any> ? `${K}.${SafeMeasureNames<TMeasures, TKind, TDimensions, TTarget['filters']>}` : never : never;
|
|
29
|
+
}[keyof TRelationships & string];
|
|
30
|
+
export type RelationshipMeasureDefinition<TDataset, TName extends string> = TDataset extends {
|
|
31
|
+
relationships: infer TRelationships;
|
|
32
|
+
} ? TName extends `${infer R}.${infer M}` ? R extends keyof TRelationships ? TRelationships[R] extends RelationshipDefinition<infer TTarget> ? TTarget extends {
|
|
33
|
+
measures: infer TMeasures;
|
|
34
|
+
} ? M extends keyof TMeasures ? TMeasures[M] : never : never : never : never : never : never;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=relationship-measure-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-measure-types.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-measure-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AnyDatasetInstance, MeasureDefinition, RelationshipDefinition } from '../types.js';
|
|
2
|
+
export interface ResolvedRelationshipMeasure {
|
|
3
|
+
relationshipName: string;
|
|
4
|
+
relationship: RelationshipDefinition;
|
|
5
|
+
target: AnyDatasetInstance;
|
|
6
|
+
definition: MeasureDefinition;
|
|
7
|
+
}
|
|
8
|
+
/** Resolve only the one-hop, duplicate-safe aggregate vocabulary of RFC 0015. */
|
|
9
|
+
export declare function resolveRelationshipMeasure(ds: AnyDatasetInstance, name: string): ResolvedRelationshipMeasure;
|
|
10
|
+
export declare function listRelationshipMeasures(name: string, relationship: RelationshipDefinition): Record<string, MeasureDefinition>;
|
|
11
|
+
/** Expand selected relationship aggregates for dependency graphs without changing author definitions. */
|
|
12
|
+
export declare function queryMeasureDefinitions(ds: AnyDatasetInstance, selected: readonly string[]): Record<string, import("../types.js").DatasetMeasureDefinition>;
|
|
13
|
+
/** Resolve a selected measure or a dimension reference while collecting joins. */
|
|
14
|
+
export declare function resolveRelationshipReference(ds: AnyDatasetInstance, name: string, measures?: readonly string[]): {
|
|
15
|
+
resolved: import("./relationship-fields.js").ResolvedQualifiedField;
|
|
16
|
+
error?: undefined;
|
|
17
|
+
} | {
|
|
18
|
+
resolved?: undefined;
|
|
19
|
+
error: string;
|
|
20
|
+
} | {
|
|
21
|
+
resolved: ResolvedRelationshipMeasure;
|
|
22
|
+
} | null;
|
|
23
|
+
//# sourceMappingURL=relationship-measures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relationship-measures.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-measures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAMjG,MAAM,WAAW,2BAA2B;IAC1C,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,sBAAsB,CAAC;IACrC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,iBAAiB,CAAC;CAC/B;AAED,iFAAiF;AACjF,wBAAgB,0BAA0B,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,GAAG,2BAA2B,CAkB5G;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,sBAAsB,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAS9H;AAED,yGAAyG;AACzG,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,kBAAkB,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,kEAM1F;AAED,kFAAkF;AAClF,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,SAAS,MAAM,EAAO;;;;;;;;SAIlH"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isBaseMeasure } from './dataset-measures.js';
|
|
2
|
+
import { parseQualifiedField, resolveQualifiedField } from './relationship-fields.js';
|
|
3
|
+
const DUPLICATE_INSENSITIVE = new Set(['countDistinct', 'approxCountDistinct', 'min', 'max', 'argMax', 'argMin']);
|
|
4
|
+
/** Resolve only the one-hop, duplicate-safe aggregate vocabulary of RFC 0015. */
|
|
5
|
+
export function resolveRelationshipMeasure(ds, name) {
|
|
6
|
+
const parsed = parseQualifiedField(name);
|
|
7
|
+
if (!parsed || parsed.field.includes('.'))
|
|
8
|
+
throw new Error(`Measure "${name}" must use a one-hop relationship path.`);
|
|
9
|
+
const relationship = Object.hasOwn(ds.relationships, parsed.relationship) ? ds.relationships[parsed.relationship] : undefined;
|
|
10
|
+
if (!relationship)
|
|
11
|
+
throw new Error(`Unknown relationship "${parsed.relationship}" in measure "${name}".`);
|
|
12
|
+
if (relationship.kind === 'hasMany')
|
|
13
|
+
throw new Error(`Measure "${name}" cannot traverse hasMany: it would fan out aggregates.`);
|
|
14
|
+
const target = relationship.target();
|
|
15
|
+
const definition = Object.hasOwn(target.measures, parsed.field) ? target.measures[parsed.field] : undefined;
|
|
16
|
+
if (!definition)
|
|
17
|
+
throw new Error(`Unknown measure "${parsed.field}" on relationship target "${target.name}".`);
|
|
18
|
+
if (!isBaseMeasure(definition))
|
|
19
|
+
throw new Error(`Measure "${name}" must reference a base aggregate; derived, window, and shift measures are not selectable through relationships.`);
|
|
20
|
+
if (relationship.kind === 'belongsTo' && !DUPLICATE_INSENSITIVE.has(definition.aggregation)) {
|
|
21
|
+
throw new Error(`Measure "${name}" cannot use ${definition.aggregation} through belongsTo: repeated target rows would inflate the aggregation. Use a duplicate-insensitive measure or query the target dataset.`);
|
|
22
|
+
}
|
|
23
|
+
const referenced = [definition.field, definition.argField, ...(definition.filters ?? []).map(filter => target.filters[filter.field]?.field ?? filter.field)];
|
|
24
|
+
if (definition.sql || referenced.some(field => field && target.dimensions[field]?.sql)) {
|
|
25
|
+
throw new Error(`SQL-backed measure "${name}" or its inputs cannot be traversed through relationships.`);
|
|
26
|
+
}
|
|
27
|
+
return { relationshipName: parsed.relationship, relationship, target, definition };
|
|
28
|
+
}
|
|
29
|
+
export function listRelationshipMeasures(name, relationship) {
|
|
30
|
+
if (relationship.kind === 'hasMany')
|
|
31
|
+
return {};
|
|
32
|
+
const ds = { relationships: { [name]: relationship } };
|
|
33
|
+
const target = relationship.target();
|
|
34
|
+
return Object.fromEntries(Object.keys(target.measures).flatMap(measure => {
|
|
35
|
+
const qualified = `${name}.${measure}`;
|
|
36
|
+
try {
|
|
37
|
+
return [[qualified, resolveRelationshipMeasure(ds, qualified).definition]];
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
/** Expand selected relationship aggregates for dependency graphs without changing author definitions. */
|
|
45
|
+
export function queryMeasureDefinitions(ds, selected) {
|
|
46
|
+
const qualified = selected.filter(name => name.includes('.'));
|
|
47
|
+
return qualified.length ? {
|
|
48
|
+
...ds.measures,
|
|
49
|
+
...Object.fromEntries(qualified.map(name => [name, resolveRelationshipMeasure(ds, name).definition])),
|
|
50
|
+
} : ds.measures;
|
|
51
|
+
}
|
|
52
|
+
/** Resolve a selected measure or a dimension reference while collecting joins. */
|
|
53
|
+
export function resolveRelationshipReference(ds, name, measures = []) {
|
|
54
|
+
if (!measures.includes(name))
|
|
55
|
+
return resolveQualifiedField(ds, name);
|
|
56
|
+
try {
|
|
57
|
+
return { resolved: resolveRelationshipMeasure(ds, name) };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return { error: error.message };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* dataset-query and metric-query validators.
|
|
4
4
|
*/
|
|
5
5
|
import type { AnyDatasetInstance, DatasetQuery, ExecutionContext, MetricFilter, MetricQuery } from '../types.js';
|
|
6
|
-
type RelationshipQuery = Pick<DatasetQuery & MetricQuery, 'dimensions' | 'filters' | 'orderBy'>;
|
|
6
|
+
type RelationshipQuery = Pick<DatasetQuery & MetricQuery, 'dimensions' | 'filters' | 'orderBy'> & Pick<DatasetQuery, 'measures'>;
|
|
7
7
|
/**
|
|
8
8
|
* Tenant-less base datasets may still reach tenant-scoped data through a
|
|
9
9
|
* relationship. Require either a concrete runtime tenant or the trusted
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relationship-validation.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-validation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"relationship-validation.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-validation.ts"],"names":[],"mappings":"AACA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACZ,MAAM,aAAa,CAAC;AAWrB,KAAK,iBAAiB,GAAG,IAAI,CAC3B,YAAY,GAAG,WAAW,EAC1B,YAAY,GAAG,SAAS,GAAG,SAAS,CACrC,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;AAEnC;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,iBAAiB,EACxB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAoBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,IAAI,CA4Bf"}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Shared validation for relationship-qualified filters, used by both the
|
|
3
|
-
* dataset-query and metric-query validators.
|
|
4
|
-
*/
|
|
1
|
+
import { resolveRelationshipReference } from './relationship-measures.js';
|
|
5
2
|
import { validateFilterValue } from '../validation.js';
|
|
6
3
|
import { getRuntimeTenantPredicate, hasTenantRuntime, } from './tenant-runtime.js';
|
|
7
4
|
import { isQualifiedField, resolveQualifiedField, } from './relationship-fields.js';
|
|
@@ -16,11 +13,12 @@ export function validateRelationshipTenantRuntime(ds, query, context) {
|
|
|
16
13
|
}
|
|
17
14
|
const referenced = [
|
|
18
15
|
...(query.dimensions ?? []),
|
|
16
|
+
...(query.measures ?? []),
|
|
19
17
|
...(query.filters ?? []).map((filter) => filter.field),
|
|
20
18
|
...(query.orderBy ?? []).map((order) => order.field),
|
|
21
19
|
].filter(isQualifiedField);
|
|
22
20
|
for (const name of referenced) {
|
|
23
|
-
const resolution =
|
|
21
|
+
const resolution = resolveRelationshipReference(ds, name, query.measures);
|
|
24
22
|
if (resolution?.resolved?.target.tenantKey) {
|
|
25
23
|
return `Dataset "${ds.name}" requires runtime tenant scoping because relationship "${resolution.resolved.relationshipName}" targets a tenant-scoped dataset.`;
|
|
26
24
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DatasetQuery, MetricQuery } from '../types.js';
|
|
2
|
+
/** Reject SQL structure outside the semantic contract before reaching a builder. */
|
|
3
|
+
export declare function semanticQuerySyntaxErrors(query: DatasetQuery | MetricQuery): string[];
|
|
4
|
+
/** Never interpolate a caller-provided direction into raw SQL. */
|
|
5
|
+
export declare function semanticOrderDirection(direction: unknown): 'ASC' | 'DESC';
|
|
6
|
+
//# sourceMappingURL=semantic-query-syntax.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-query-syntax.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-query-syntax.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7D,oFAAoF;AACpF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,YAAY,GAAG,WAAW,GAAG,MAAM,EAAE,CAarF;AAED,kEAAkE;AAClE,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAIzE"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isSemanticFilterOperator } from '../constants.js';
|
|
2
|
+
/** Reject SQL structure outside the semantic contract before reaching a builder. */
|
|
3
|
+
export function semanticQuerySyntaxErrors(query) {
|
|
4
|
+
const errors = [];
|
|
5
|
+
for (const filter of query.filters ?? []) {
|
|
6
|
+
if (!isSemanticFilterOperator(filter.operator)) {
|
|
7
|
+
errors.push(`Unsupported semantic filter operator "${String(filter.operator)}".`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
for (const order of query.orderBy ?? []) {
|
|
11
|
+
if (order.direction !== 'asc' && order.direction !== 'desc') {
|
|
12
|
+
errors.push('Invalid order direction: expected "asc" or "desc".');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return errors;
|
|
16
|
+
}
|
|
17
|
+
/** Never interpolate a caller-provided direction into raw SQL. */
|
|
18
|
+
export function semanticOrderDirection(direction) {
|
|
19
|
+
if (direction === 'asc')
|
|
20
|
+
return 'ASC';
|
|
21
|
+
if (direction === 'desc')
|
|
22
|
+
return 'DESC';
|
|
23
|
+
throw new Error('Invalid order direction: expected "asc" or "desc".');
|
|
24
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shift-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/shift-measure-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAI1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"shift-measure-sql.d.ts","sourceRoot":"","sources":["../../src/utils/shift-measure-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAI1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAK9D,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,MAAM;;;;;EAoBlG;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,GAAG,MAAM,CAGpG;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAOrH;AAED,+FAA+F;AAC/F,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,sBAAsB,EAAE,KAAK,EAAE,eAAe,CAAC,OAAO,CAAC,GAAG,MAAM,CAMlH;AAED,2FAA2F;AAC3F,wBAAgB,eAAe,CAAC,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAsBhK"}
|
|
@@ -3,18 +3,25 @@ import { addTimeSql, subtractTimeSql } from './time-arithmetic-sql.js';
|
|
|
3
3
|
import { calendarBucketGuardSql } from './time-axis-calendar-guard.js';
|
|
4
4
|
import { calendarShiftAtFineGrain } from './shift-measure-grains.js';
|
|
5
5
|
import { shiftWallTimeGuardSql } from './shift-wall-time-sql.js';
|
|
6
|
+
import { anchoredCalendarShiftSql } from './calendar-shift-sql.js';
|
|
6
7
|
/** Preserve the selected portion of a bucket and both authored endpoint operators. */
|
|
7
8
|
export function shiftRangeSql(shift, axis, period) {
|
|
8
9
|
const first = period ?? '_hq_first';
|
|
9
10
|
const last = period ?? '_hq_last';
|
|
10
11
|
const end = addTimeSql(last, 1, axis.grain);
|
|
11
|
-
const
|
|
12
|
+
const calendar = calendarShiftAtFineGrain(shift.interval, axis.grain);
|
|
12
13
|
// Clamp the bucket anchor, then retain its width. Independently clamping both
|
|
13
14
|
// dates would turn March 30–31 into February 28–28, dropping an entire day.
|
|
15
|
+
// A partial bucket keeps the matching slice of that shifted range, so both
|
|
16
|
+
// bounds are placed relative to the anchor rather than shifted on their own.
|
|
17
|
+
const shiftBound = (point, anchor) => calendar
|
|
18
|
+
? anchoredCalendarShiftSql(point, anchor, shift.interval)
|
|
19
|
+
: subtractTimeSql(point, shift.interval.amount, shift.interval.unit);
|
|
20
|
+
const upper = shiftBound(`least(${end}, _hq_upper)`, last);
|
|
14
21
|
const shiftedEnd = addTimeSql(subtractTimeSql(last, shift.interval.amount, shift.interval.unit), 1, axis.grain);
|
|
15
22
|
return {
|
|
16
|
-
lower:
|
|
17
|
-
upper:
|
|
23
|
+
lower: shiftBound(`greatest(${first}, _hq_lower)`, first),
|
|
24
|
+
upper: calendar ? `if(_hq_upper >= ${end}, ${shiftedEnd}, ${upper})` : upper,
|
|
18
25
|
lowerInclusive: period ? `(${period} > _hq_lower OR ${Number(axis.lowerInclusive)})` : String(Number(axis.lowerInclusive)),
|
|
19
26
|
upperInclusive: `(_hq_upper < ${end} AND ${Number(axis.upperInclusive)})`,
|
|
20
27
|
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ExecutionContext } from '../types.js';
|
|
2
|
+
/** Bind cache identity and deferred execution to the same request-owned scope. */
|
|
3
|
+
export declare function snapshotExecutionContext(context?: ExecutionContext): ExecutionContext | undefined;
|
|
4
|
+
//# sourceMappingURL=snapshot-execution-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-execution-context.d.ts","sourceRoot":"","sources":["../../src/utils/snapshot-execution-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,kFAAkF;AAClF,wBAAgB,wBAAwB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,SAAS,CAajG"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Bind cache identity and deferred execution to the same request-owned scope. */
|
|
2
|
+
export function snapshotExecutionContext(context) {
|
|
3
|
+
if (context === undefined)
|
|
4
|
+
return undefined;
|
|
5
|
+
return {
|
|
6
|
+
...context,
|
|
7
|
+
...(context.runtime === undefined ? {} : {
|
|
8
|
+
runtime: {
|
|
9
|
+
...context.runtime,
|
|
10
|
+
// Copy tenant objects and ID lists; retain adapter and AbortSignal identity.
|
|
11
|
+
tenant: structuredClone(context.runtime.tenant),
|
|
12
|
+
},
|
|
13
|
+
}),
|
|
14
|
+
...(context.cache && { cache: { ...context.cache } }),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-measure-context-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-context-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"time-measure-context-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-context-sql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAO9D,8EAA8E;AAC9E,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,SAAS,wBAAwB,EAAE,EAAE,IAAI,EAAE,eAAe,GAAG,MAAM,EAAE,CAerH;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,EAAE,IAAI,EAAE,eAAe,GAAG,MAAM,CAOxG"}
|
|
@@ -2,16 +2,22 @@ import { addTimeSql as add, subtractTimeSql as subtract } from './time-arithmeti
|
|
|
2
2
|
import { calendarShiftAtFineGrain } from './shift-measure-grains.js';
|
|
3
3
|
import { calendarBucketGuardSql } from './time-axis-calendar-guard.js';
|
|
4
4
|
import { shiftWallTimeGuardSql } from './shift-wall-time-sql.js';
|
|
5
|
+
import { anchoredCalendarShiftSql } from './calendar-shift-sql.js';
|
|
5
6
|
/** Keep output coordinates separate from each shifted evaluation interval. */
|
|
6
7
|
export function timeMeasureContextCtes(contexts, axis) {
|
|
7
8
|
const end = add('_hq_period', 1, axis.grain);
|
|
8
9
|
const root = `_hq_context0 AS (SELECT _hq_period, _hq_period AS _hq_eval_period, ${end} AS _hq_eval_end, greatest(_hq_period, _hq_lower) AS _hq_eval_lower, least(${end}, _hq_upper) AS _hq_eval_upper, (_hq_period > _hq_lower OR ${Number(axis.lowerInclusive)}) AS _hq_lower_inclusive, (_hq_upper < ${end} AND ${Number(axis.upperInclusive)}) AS _hq_upper_inclusive FROM _hq_series CROSS JOIN _hq_bounds)`;
|
|
9
10
|
return [root, ...contexts.slice(1).map(context => {
|
|
10
11
|
const interval = context.interval;
|
|
12
|
+
const calendar = calendarShiftAtFineGrain(interval, axis.grain);
|
|
11
13
|
const shift = (value) => subtract(`p.${value}`, interval.amount, interval.unit);
|
|
14
|
+
// Partial bounds stay inside the shifted bucket; see anchoredCalendarShiftSql.
|
|
15
|
+
const shiftBound = (value) => calendar
|
|
16
|
+
? anchoredCalendarShiftSql(`p.${value}`, 'p._hq_eval_period', interval)
|
|
17
|
+
: shift(value);
|
|
12
18
|
const period = shift('_hq_eval_period');
|
|
13
|
-
const shiftedEnd =
|
|
14
|
-
return `_hq_context${context.id} AS (SELECT p._hq_period, ${period} AS _hq_eval_period, ${shiftedEnd} AS _hq_eval_end, ${
|
|
19
|
+
const shiftedEnd = calendar ? add(period, 1, axis.grain) : shift('_hq_eval_end');
|
|
20
|
+
return `_hq_context${context.id} AS (SELECT p._hq_period, ${period} AS _hq_eval_period, ${shiftedEnd} AS _hq_eval_end, ${shiftBound('_hq_eval_lower')} AS _hq_eval_lower, if(p._hq_eval_upper = p._hq_eval_end, ${shiftedEnd}, ${shiftBound('_hq_eval_upper')}) AS _hq_eval_upper, p._hq_lower_inclusive, p._hq_upper_inclusive FROM _hq_context${context.parent} AS p)`;
|
|
15
21
|
})];
|
|
16
22
|
}
|
|
17
23
|
export function timeMeasureContextGuard(context, axis) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-measure-dataset-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-dataset-sql.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"time-measure-dataset-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-dataset-sql.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAYxE,+EAA+E;AAC/E,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,GACpC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAmE7D"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { queryMeasureDefinitions } from './relationship-measures.js';
|
|
1
2
|
import { buildCompositeTimeMeasureSql } from './composite-time-measure-sql.js';
|
|
2
3
|
import { measureDependencyNames } from './measure-dependencies.js';
|
|
3
4
|
import { getBaseMeasure, getDerivedMeasure, isShiftMeasure, isWindowMeasure } from './dataset-measures.js';
|
|
@@ -18,7 +19,7 @@ export function buildTimeMeasureDatasetSql(ds, query, options) {
|
|
|
18
19
|
const { axis } = analyzeTimeMeasureAxis(ds, query);
|
|
19
20
|
if (!axis || !ds.timeKey)
|
|
20
21
|
throw new Error('Window measures require a bounded time axis.');
|
|
21
|
-
const dependencies = measureDependencyNames(ds.measures, query.measures ?? []);
|
|
22
|
+
const dependencies = measureDependencyNames(queryMeasureDefinitions(ds, query.measures ?? []), query.measures ?? []);
|
|
22
23
|
const composite = dependencies.some(name => {
|
|
23
24
|
const definition = ds.measures[name];
|
|
24
25
|
if (isShiftMeasure(definition))
|
|
@@ -49,7 +50,7 @@ export function buildTimeMeasureDatasetSql(ds, query, options) {
|
|
|
49
50
|
// Scan only requested populations: intervening rows must not introduce dimensions.
|
|
50
51
|
const end = add('_hq_last', 1, axis.grain);
|
|
51
52
|
const populations = [];
|
|
52
|
-
if ([...needed].some(name => getBaseMeasure(ds.measures, name))) {
|
|
53
|
+
if ([...needed].some(name => name.includes('.') || getBaseMeasure(ds.measures, name))) {
|
|
53
54
|
populations.push(`(_hq_time >= _hq_first AND _hq_time < ${end})`);
|
|
54
55
|
}
|
|
55
56
|
if (windows.length) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-measure-result-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-result-sql.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"time-measure-result-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-result-sql.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAIxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAE5E,sFAAsF;AACtF,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,4BAA4B,EACrC,IAAI,EAAE,SAAS,uBAAuB,EAAE,EACxC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,eAAe,EAAE,MAAM,EACvB,iBAAiB,UAAQ,GACxB,MAAM,CAuBR"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { semanticOrderDirection } from './semantic-query-syntax.js';
|
|
1
2
|
import { quoteSQLIdentifier } from '../sql-utils.js';
|
|
2
3
|
import { getDerivedMeasure } from './dataset-measures.js';
|
|
3
4
|
import { derivedProjection } from './derived-measure-sql.js';
|
|
@@ -20,7 +21,7 @@ export function buildTimeMeasureResultSql(ds, query, options, dims, ctes, valida
|
|
|
20
21
|
+ ` UNION ALL SELECT ${nullProjections} FROM _hq_bounds WHERE ${validationGuard} = 1)`;
|
|
21
22
|
let sql = `WITH ${[...ctes, resultCte].join(',\n')} SELECT ${outputNames.map(quoteSQLIdentifier).join(', ')} FROM _hq_result`;
|
|
22
23
|
const order = query.orderBy?.length ? query.orderBy : [{ field: 'period', direction: 'asc' }];
|
|
23
|
-
sql += ` ORDER BY ${order.map(item => `${quoteSQLIdentifier(item.field)} ${item.direction
|
|
24
|
+
sql += ` ORDER BY ${order.map(item => `${quoteSQLIdentifier(item.field)} ${semanticOrderDirection(item.direction)}`).join(', ')}`;
|
|
24
25
|
const limit = options.executionLimit ?? query.limit;
|
|
25
26
|
if (limit !== undefined)
|
|
26
27
|
sql += ` LIMIT ${limit}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-measure-source-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-source-sql.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"time-measure-source-sql.d.ts","sourceRoot":"","sources":["../../src/utils/time-measure-source-sql.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAUxE,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,iBAAiB,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,OAAO,EAAE,CAAC;IACtB,UAAU,EAAE,uBAAuB,EAAE,CAAC;IACtC,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,0FAA0F;AAC1F,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC3B,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,OAAO,EAAE,4BAA4B,GACpC,oBAAoB,CA6DtB"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveRelationshipMeasure } from './relationship-measures.js';
|
|
2
|
+
import { relationshipMeasureExpressions } from './relationship-measure-sql.js';
|
|
1
3
|
import { queryTimeSql } from './query-timezone.js';
|
|
2
4
|
import { resolveDimensionExpression, resolveFilterField, resolveTenantFilterColumn } from '../query-planner.js';
|
|
3
5
|
import { applyRelationshipJoins, buildRelationshipBuilderContext, qualifyBaseColumn } from './relationship-builder-plan.js';
|
|
@@ -19,7 +21,8 @@ export function buildTimeMeasureSourceSql(ds, query, baseNames, filters, options
|
|
|
19
21
|
for (const dimension of dims)
|
|
20
22
|
sourceParts.push(resolveDimensionExpression(ds, dimension.name, joinCtx));
|
|
21
23
|
for (const name of baseNames) {
|
|
22
|
-
const
|
|
24
|
+
const related = name.includes('.') ? resolveRelationshipMeasure(ds, name) : undefined;
|
|
25
|
+
const definition = related?.definition ?? getBaseMeasure(ds.measures, name);
|
|
23
26
|
if (!definition)
|
|
24
27
|
throw new Error(`Window input "${name}" is not a base measure.`);
|
|
25
28
|
const spec = measureToAggregationSpec(name, definition);
|
|
@@ -28,9 +31,10 @@ export function buildTimeMeasureSourceSql(ds, query, baseNames, filters, options
|
|
|
28
31
|
const value = `_hq_v${bases.length}`;
|
|
29
32
|
const arg = `_hq_a${bases.length}`;
|
|
30
33
|
bases.push({ name, definition, value, arg });
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
sourceParts.push(
|
|
34
|
+
const expressions = related ? relationshipMeasureExpressions(ds, name, joinCtx) : undefined;
|
|
35
|
+
const expression = expressions?.field ?? spec.sql ?? resolveDimensionExpression(ds, spec.field, joinCtx);
|
|
36
|
+
sourceParts.push(expressions ? expression : applyFilteredAggregationExpression(ds, spec, expression, joinCtx));
|
|
37
|
+
sourceParts.push(expressions?.arg ?? (spec.argField ? resolveDimensionExpression(ds, spec.argField, joinCtx) : 'NULL'));
|
|
34
38
|
}
|
|
35
39
|
// A positional tuple isolates internal names from the source table's names.
|
|
36
40
|
const sourceIdentifiers = [
|
package/dist/validation.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypequery/datasets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Code-first TypeScript semantic layer for ClickHouse datasets, metrics, multi-tenancy, and AI agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"semantic-layer",
|
|
@@ -42,25 +42,29 @@
|
|
|
42
42
|
"dist"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@hypequery/protocol": "^0.
|
|
45
|
+
"@hypequery/protocol": "^0.19.0",
|
|
46
46
|
"@noble/hashes": "^1.8.0",
|
|
47
47
|
"zod": "^3.22.4",
|
|
48
48
|
"zod-to-json-schema": "^3.25.2"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"zod": "^3.22.4"
|
|
51
|
+
"zod": "^3.22.4",
|
|
52
|
+
"@hypequery/clickhouse": ">=2.13.0"
|
|
52
53
|
},
|
|
53
54
|
"peerDependenciesMeta": {
|
|
54
55
|
"zod": {
|
|
55
56
|
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@hypequery/clickhouse": {
|
|
59
|
+
"optional": true
|
|
56
60
|
}
|
|
57
61
|
},
|
|
58
62
|
"devDependencies": {
|
|
59
63
|
"@types/node": "^18.19.80",
|
|
60
|
-
"typescript": "^
|
|
64
|
+
"typescript": "^6.0.3",
|
|
61
65
|
"@vitest/coverage-v8": "^5.0.2",
|
|
62
66
|
"vitest": "^5.0.2",
|
|
63
|
-
"@hypequery/protocol-conformance": "^0.15.
|
|
67
|
+
"@hypequery/protocol-conformance": "^0.15.1"
|
|
64
68
|
},
|
|
65
69
|
"repository": {
|
|
66
70
|
"type": "git",
|
|
@@ -83,6 +87,7 @@
|
|
|
83
87
|
"test:types": "tsc --project tsconfig.type-tests.json",
|
|
84
88
|
"test:watch": "vitest watch",
|
|
85
89
|
"test:coverage": "vitest run --coverage",
|
|
90
|
+
"test:coverage:full": "node scripts/run-integration-tests.js --include-unit-tests --coverage",
|
|
86
91
|
"lint": "eslint \"src/**/*.ts\""
|
|
87
92
|
}
|
|
88
93
|
}
|