@hypequery/datasets 0.13.6 → 0.15.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 (128) hide show
  1. package/README.md +10 -0
  2. package/dist/agent-catalog.d.ts +97 -0
  3. package/dist/agent-catalog.d.ts.map +1 -0
  4. package/dist/agent-catalog.js +79 -0
  5. package/dist/api.type-test.js +19 -1
  6. package/dist/catalog.d.ts +17 -6
  7. package/dist/catalog.d.ts.map +1 -1
  8. package/dist/catalog.js +18 -1
  9. package/dist/contract.d.ts +11 -7
  10. package/dist/contract.d.ts.map +1 -1
  11. package/dist/contract.js +22 -1
  12. package/dist/dataset-query.js +1 -1
  13. package/dist/dataset.d.ts.map +1 -1
  14. package/dist/dataset.js +19 -2
  15. package/dist/executor.d.ts +6 -0
  16. package/dist/executor.d.ts.map +1 -1
  17. package/dist/executor.js +55 -23
  18. package/dist/field.d.ts.map +1 -1
  19. package/dist/field.js +2 -0
  20. package/dist/index.d.ts +12 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +9 -0
  23. package/dist/measure.d.ts.map +1 -1
  24. package/dist/measure.js +4 -0
  25. package/dist/portable-execution-errors.d.ts +49 -0
  26. package/dist/portable-execution-errors.d.ts.map +1 -0
  27. package/dist/portable-execution-errors.js +52 -0
  28. package/dist/portable-executor.d.ts +61 -0
  29. package/dist/portable-executor.d.ts.map +1 -0
  30. package/dist/portable-executor.js +90 -0
  31. package/dist/protocol-adapter.d.ts.map +1 -1
  32. package/dist/protocol-adapter.js +26 -97
  33. package/dist/protocol-rehydrate.d.ts +56 -0
  34. package/dist/protocol-rehydrate.d.ts.map +1 -0
  35. package/dist/protocol-rehydrate.js +264 -0
  36. package/dist/publishing/features/dataset-publication.d.ts +10 -0
  37. package/dist/publishing/features/dataset-publication.d.ts.map +1 -0
  38. package/dist/publishing/features/dataset-publication.js +34 -0
  39. package/dist/publishing/node.d.ts +16 -0
  40. package/dist/publishing/node.d.ts.map +1 -0
  41. package/dist/publishing/node.js +35 -0
  42. package/dist/publishing/publisher.d.ts +17 -0
  43. package/dist/publishing/publisher.d.ts.map +1 -0
  44. package/dist/publishing/publisher.js +32 -0
  45. package/dist/publishing/types.d.ts +31 -0
  46. package/dist/publishing/types.d.ts.map +1 -0
  47. package/dist/publishing/types.js +1 -0
  48. package/dist/query-builder-protocol.d.ts +6 -2
  49. package/dist/query-builder-protocol.d.ts.map +1 -1
  50. package/dist/semantic-plan.d.ts +3 -1
  51. package/dist/semantic-plan.d.ts.map +1 -1
  52. package/dist/semantic-planner.d.ts.map +1 -1
  53. package/dist/semantic-planner.js +4 -0
  54. package/dist/semantic-query-schema.d.ts +60 -0
  55. package/dist/semantic-query-schema.d.ts.map +1 -0
  56. package/dist/semantic-query-schema.js +223 -0
  57. package/dist/sql-utils.d.ts +36 -0
  58. package/dist/sql-utils.d.ts.map +1 -1
  59. package/dist/sql-utils.js +76 -0
  60. package/dist/tools.d.ts +5 -0
  61. package/dist/tools.d.ts.map +1 -1
  62. package/dist/tools.js +25 -267
  63. package/dist/types.d.ts +98 -8
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/utils/agent-catalog-projection.d.ts +78 -0
  66. package/dist/utils/agent-catalog-projection.d.ts.map +1 -0
  67. package/dist/utils/agent-catalog-projection.js +179 -0
  68. package/dist/utils/dataset-cache-policy.d.ts +34 -0
  69. package/dist/utils/dataset-cache-policy.d.ts.map +1 -0
  70. package/dist/utils/dataset-cache-policy.js +50 -0
  71. package/dist/utils/dataset-contract.d.ts +2 -2
  72. package/dist/utils/dataset-contract.d.ts.map +1 -1
  73. package/dist/utils/dataset-contract.js +3 -1
  74. package/dist/utils/dataset-definition-validation.d.ts +35 -0
  75. package/dist/utils/dataset-definition-validation.d.ts.map +1 -0
  76. package/dist/utils/dataset-definition-validation.js +215 -0
  77. package/dist/utils/dataset-metric-ref.d.ts +2 -2
  78. package/dist/utils/dataset-metric-ref.d.ts.map +1 -1
  79. package/dist/utils/dataset-metric-ref.js +5 -3
  80. package/dist/utils/metric-alias.d.ts +4 -0
  81. package/dist/utils/metric-alias.d.ts.map +1 -0
  82. package/dist/utils/metric-alias.js +28 -0
  83. package/dist/utils/portable-execution-deadline.d.ts +4 -0
  84. package/dist/utils/portable-execution-deadline.d.ts.map +1 -0
  85. package/dist/utils/portable-execution-deadline.js +27 -0
  86. package/dist/utils/portable-result-budget.d.ts +12 -0
  87. package/dist/utils/portable-result-budget.d.ts.map +1 -0
  88. package/dist/utils/portable-result-budget.js +21 -0
  89. package/dist/utils/portable-semantic-query.d.ts +5 -0
  90. package/dist/utils/portable-semantic-query.d.ts.map +1 -0
  91. package/dist/utils/portable-semantic-query.js +36 -0
  92. package/dist/utils/protocol-metric-capabilities.d.ts +7 -0
  93. package/dist/utils/protocol-metric-capabilities.d.ts.map +1 -0
  94. package/dist/utils/protocol-metric-capabilities.js +57 -0
  95. package/dist/utils/protocol-metric-expressions.d.ts +17 -0
  96. package/dist/utils/protocol-metric-expressions.d.ts.map +1 -0
  97. package/dist/utils/protocol-metric-expressions.js +118 -0
  98. package/dist/utils/protocol-rehydrate-derivation.d.ts +27 -0
  99. package/dist/utils/protocol-rehydrate-derivation.d.ts.map +1 -0
  100. package/dist/utils/protocol-rehydrate-derivation.js +104 -0
  101. package/dist/utils/protocol-rehydrate-filters.d.ts +10 -0
  102. package/dist/utils/protocol-rehydrate-filters.d.ts.map +1 -0
  103. package/dist/utils/protocol-rehydrate-filters.js +36 -0
  104. package/dist/utils/protocol-semantic-metadata.d.ts +5 -0
  105. package/dist/utils/protocol-semantic-metadata.d.ts.map +1 -0
  106. package/dist/utils/protocol-semantic-metadata.js +5 -0
  107. package/dist/utils/published-name.d.ts +3 -0
  108. package/dist/utils/published-name.d.ts.map +1 -0
  109. package/dist/utils/published-name.js +7 -0
  110. package/dist/utils/published-relationships.d.ts +34 -0
  111. package/dist/utils/published-relationships.d.ts.map +1 -0
  112. package/dist/utils/published-relationships.js +35 -0
  113. package/dist/utils/relationship-fields.d.ts +7 -0
  114. package/dist/utils/relationship-fields.d.ts.map +1 -1
  115. package/dist/utils/relationship-fields.js +13 -1
  116. package/dist/utils/result-limits.d.ts +34 -0
  117. package/dist/utils/result-limits.d.ts.map +1 -0
  118. package/dist/utils/result-limits.js +40 -0
  119. package/dist/utils/semantic-metadata-validation.d.ts +4 -0
  120. package/dist/utils/semantic-metadata-validation.d.ts.map +1 -0
  121. package/dist/utils/semantic-metadata-validation.js +103 -0
  122. package/dist/utils/semantic-metadata.d.ts +4 -0
  123. package/dist/utils/semantic-metadata.d.ts.map +1 -0
  124. package/dist/utils/semantic-metadata.js +13 -0
  125. package/dist/utils/semantic-tool.d.ts +5 -0
  126. package/dist/utils/semantic-tool.d.ts.map +1 -0
  127. package/dist/utils/semantic-tool.js +29 -0
  128. package/package.json +5 -4
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Per-source projections behind `projectAgentSafeCatalog()`.
3
+ *
4
+ * Each function maps one dataset representation onto the logical shape an agent
5
+ * may see. They all apply the same rules — drop dimensions that are neither
6
+ * filterable nor groupable, keep only filters whose field survived that drop,
7
+ * keep only metric references that resolve, snapshot semantic metadata, and
8
+ * sort every collection by name — so the projection is deterministic regardless
9
+ * of which source produced it.
10
+ */
11
+ import type { ProtocolDatasetContract } from '@hypequery/protocol';
12
+ import type { AgentCatalogDataset } from '../agent-catalog.js';
13
+ import type { DatasetDefaults, DatasetFreshness, DatasetLimits, FieldType, SemanticMetadata } from '../types.js';
14
+ /**
15
+ * The structural shape shared by `DatasetCatalog` and `ContractDataset`.
16
+ *
17
+ * Both carry their collections as name-keyed records with the same entry
18
+ * fields, so one projection covers the local-catalog and semantic-contract
19
+ * sources. Only what the projection reads is declared here.
20
+ */
21
+ export interface RecordShapedDataset extends SemanticMetadata {
22
+ readonly name: string;
23
+ readonly description?: string;
24
+ readonly freshness?: DatasetFreshness;
25
+ readonly owner?: string;
26
+ readonly defaults?: DatasetDefaults;
27
+ readonly timeKey?: string;
28
+ readonly dimensions: Readonly<Record<string, SemanticMetadata & {
29
+ readonly type: FieldType;
30
+ readonly label?: string;
31
+ readonly description?: string;
32
+ readonly filterable: boolean;
33
+ readonly groupable: boolean;
34
+ }>>;
35
+ readonly measures: Readonly<Record<string, SemanticMetadata & {
36
+ readonly label?: string;
37
+ readonly description?: string;
38
+ }>>;
39
+ readonly metrics: Readonly<Record<string, SemanticMetadata & {
40
+ readonly label?: string;
41
+ readonly description?: string;
42
+ readonly dimensions: readonly string[];
43
+ readonly filters: readonly string[];
44
+ readonly grains: readonly string[];
45
+ readonly grain?: string;
46
+ }>>;
47
+ readonly filters: Readonly<Record<string, SemanticMetadata & {
48
+ readonly field: string;
49
+ readonly label?: string;
50
+ readonly description?: string;
51
+ readonly operators?: readonly string[];
52
+ readonly valueType?: FieldType;
53
+ }>>;
54
+ readonly relationships: Readonly<Record<string, {
55
+ readonly target: string;
56
+ readonly queryable: boolean;
57
+ readonly fields: readonly string[];
58
+ }>>;
59
+ readonly limits?: DatasetLimits;
60
+ }
61
+ export declare function optionalText<T extends {
62
+ label?: string;
63
+ description?: string;
64
+ }>(value: T): Pick<T, 'label' | 'description'>;
65
+ /** Reduces a limits object to the four keys the agent-safe catalog publishes. */
66
+ export declare function normalizedLimits(limits: DatasetLimits | undefined): DatasetLimits;
67
+ export declare function datasetDescription(name: string, description?: string): string;
68
+ /** Projects a `DatasetCatalog` or `ContractDataset` — they share one shape. */
69
+ export declare function recordDatasetToAgentDataset(dataset: RecordShapedDataset): AgentCatalogDataset;
70
+ /**
71
+ * Projects a portable deployment contract dataset.
72
+ *
73
+ * Kept separate from `recordDatasetToAgentDataset`: the contract carries its
74
+ * collections as arrays of named entries, and relationship fields are not
75
+ * stored — they are derived from the target dataset's publishable dimensions.
76
+ */
77
+ export declare function protocolDatasetToAgentDataset(dataset: ProtocolDatasetContract, datasets: ReadonlyMap<string, ProtocolDatasetContract>): AgentCatalogDataset;
78
+ //# sourceMappingURL=agent-catalog-projection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-catalog-projection.d.ts","sourceRoot":"","sources":["../../src/utils/agent-catalog-projection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Per-source projections behind `projectAgentSafeCatalog()`.
3
+ *
4
+ * Each function maps one dataset representation onto the logical shape an agent
5
+ * may see. They all apply the same rules — drop dimensions that are neither
6
+ * filterable nor groupable, keep only filters whose field survived that drop,
7
+ * keep only metric references that resolve, snapshot semantic metadata, and
8
+ * sort every collection by name — so the projection is deterministic regardless
9
+ * of which source produced it.
10
+ */
11
+ import { compareStrings, uniqueSorted } from './canonical-json.js';
12
+ import { snapshotSemanticMetadata } from './semantic-metadata.js';
13
+ export function optionalText(value) {
14
+ return {
15
+ ...(value.label !== undefined ? { label: value.label } : {}),
16
+ ...(value.description !== undefined ? { description: value.description } : {}),
17
+ };
18
+ }
19
+ /** Reduces a limits object to the four keys the agent-safe catalog publishes. */
20
+ export function normalizedLimits(limits) {
21
+ return {
22
+ ...(limits?.maxDimensions !== undefined ? { maxDimensions: limits.maxDimensions } : {}),
23
+ ...(limits?.maxMeasures !== undefined ? { maxMeasures: limits.maxMeasures } : {}),
24
+ ...(limits?.maxFilters !== undefined ? { maxFilters: limits.maxFilters } : {}),
25
+ ...(limits?.maxResultSize !== undefined ? { maxResultSize: limits.maxResultSize } : {}),
26
+ };
27
+ }
28
+ export function datasetDescription(name, description) {
29
+ return description ?? `${name} analytics dataset.`;
30
+ }
31
+ function snapshotDefaults(defaults) {
32
+ return {
33
+ ...(defaults.dimensions !== undefined
34
+ ? { dimensions: uniqueSorted(defaults.dimensions) }
35
+ : {}),
36
+ ...(defaults.timeGrain !== undefined ? { timeGrain: defaults.timeGrain } : {}),
37
+ };
38
+ }
39
+ /** The dataset-level metadata every source projects the same way. */
40
+ function datasetMetadata(dataset) {
41
+ return {
42
+ ...snapshotSemanticMetadata(dataset),
43
+ ...(dataset.freshness !== undefined ? { freshness: { ...dataset.freshness } } : {}),
44
+ ...(dataset.owner !== undefined ? { owner: dataset.owner } : {}),
45
+ ...(dataset.defaults !== undefined ? { defaults: snapshotDefaults(dataset.defaults) } : {}),
46
+ };
47
+ }
48
+ function sortedByName(items) {
49
+ return items.sort((left, right) => compareStrings(left.name, right.name));
50
+ }
51
+ /** Projects a `DatasetCatalog` or `ContractDataset` — they share one shape. */
52
+ export function recordDatasetToAgentDataset(dataset) {
53
+ const dimensions = sortedByName(Object.entries(dataset.dimensions)
54
+ .filter(([, dimension]) => dimension.filterable || dimension.groupable)
55
+ .map(([name, dimension]) => ({
56
+ name,
57
+ type: dimension.type,
58
+ ...optionalText(dimension),
59
+ ...snapshotSemanticMetadata(dimension),
60
+ filterable: dimension.filterable,
61
+ groupable: dimension.groupable,
62
+ })));
63
+ const dimensionNames = new Set(dimensions.map(dimension => dimension.name));
64
+ // A filter is only publishable when its backing dimension survived above;
65
+ // otherwise it would name a field the agent cannot see or select.
66
+ const filterNames = new Set(Object.entries(dataset.filters)
67
+ .filter(([, filter]) => filter.valueType !== undefined && dimensionNames.has(filter.field))
68
+ .map(([name]) => name));
69
+ return {
70
+ name: dataset.name,
71
+ description: datasetDescription(dataset.name, dataset.description),
72
+ ...datasetMetadata(dataset),
73
+ timeDimension: dataset.timeKey !== undefined && dimensionNames.has(dataset.timeKey)
74
+ ? dataset.timeKey
75
+ : null,
76
+ dimensions,
77
+ measures: sortedByName(Object.entries(dataset.measures).map(([name, measure]) => ({
78
+ name,
79
+ ...optionalText(measure),
80
+ ...snapshotSemanticMetadata(measure),
81
+ }))),
82
+ metrics: sortedByName(Object.entries(dataset.metrics).map(([name, metric]) => ({
83
+ name,
84
+ ...optionalText(metric),
85
+ ...snapshotSemanticMetadata(metric),
86
+ dimensions: uniqueSorted(metric.dimensions.filter(item => dimensionNames.has(item))),
87
+ filters: uniqueSorted(metric.filters.filter(item => filterNames.has(item))),
88
+ grains: uniqueSorted(metric.grains),
89
+ ...(metric.grain !== undefined ? { grain: metric.grain } : {}),
90
+ }))),
91
+ filters: sortedByName(Object.entries(dataset.filters)
92
+ .filter(([name]) => filterNames.has(name))
93
+ .map(([name, filter]) => ({
94
+ name,
95
+ type: filter.valueType,
96
+ ...optionalText(filter),
97
+ ...snapshotSemanticMetadata(filter),
98
+ operators: uniqueSorted(filter.operators ?? []),
99
+ }))),
100
+ relationships: sortedByName(Object.entries(dataset.relationships)
101
+ .filter(([, relationship]) => relationship.queryable)
102
+ .map(([name, relationship]) => ({
103
+ name,
104
+ target: relationship.target,
105
+ fields: uniqueSorted(relationship.fields),
106
+ }))),
107
+ limits: normalizedLimits(dataset.limits),
108
+ };
109
+ }
110
+ /**
111
+ * Projects a portable deployment contract dataset.
112
+ *
113
+ * Kept separate from `recordDatasetToAgentDataset`: the contract carries its
114
+ * collections as arrays of named entries, and relationship fields are not
115
+ * stored — they are derived from the target dataset's publishable dimensions.
116
+ */
117
+ export function protocolDatasetToAgentDataset(dataset, datasets) {
118
+ const dimensions = sortedByName(dataset.dimensions
119
+ .filter(dimension => dimension.filterable || dimension.groupable)
120
+ .map(dimension => ({
121
+ name: dimension.name,
122
+ type: dimension.type,
123
+ ...optionalText(dimension),
124
+ ...snapshotSemanticMetadata(dimension),
125
+ filterable: dimension.filterable,
126
+ groupable: dimension.groupable,
127
+ })));
128
+ const dimensionNames = new Set(dimensions.map(dimension => dimension.name));
129
+ const dimensionTypes = new Map(dataset.dimensions.map(dimension => [dimension.name, dimension.type]));
130
+ const filterNames = new Set(dataset.filters
131
+ .filter(filter => dimensionNames.has(String(filter.field)))
132
+ .map(filter => String(filter.name)));
133
+ return {
134
+ name: dataset.name,
135
+ description: datasetDescription(dataset.name, dataset.description),
136
+ ...datasetMetadata(dataset),
137
+ timeDimension: dataset.timeField !== undefined && dimensionNames.has(String(dataset.timeField))
138
+ ? String(dataset.timeField)
139
+ : null,
140
+ dimensions,
141
+ measures: sortedByName(dataset.measures.map(measure => ({
142
+ name: measure.name,
143
+ ...optionalText(measure),
144
+ ...snapshotSemanticMetadata(measure),
145
+ }))),
146
+ metrics: sortedByName(dataset.metrics.map(metric => ({
147
+ name: metric.name,
148
+ ...optionalText(metric),
149
+ ...snapshotSemanticMetadata(metric),
150
+ dimensions: uniqueSorted(metric.dimensions.filter(name => dimensionNames.has(String(name)))),
151
+ filters: uniqueSorted(metric.filters.filter(name => filterNames.has(String(name)))),
152
+ grains: uniqueSorted(metric.grains),
153
+ ...(metric.grain !== undefined ? { grain: metric.grain } : {}),
154
+ }))),
155
+ filters: sortedByName(dataset.filters
156
+ .filter(filter => filterNames.has(String(filter.name)))
157
+ .map((filter) => {
158
+ const type = dimensionTypes.get(String(filter.field));
159
+ return type === undefined ? undefined : {
160
+ name: filter.name,
161
+ type,
162
+ ...optionalText(filter),
163
+ ...snapshotSemanticMetadata(filter),
164
+ operators: uniqueSorted(filter.operators),
165
+ };
166
+ })
167
+ .filter((filter) => filter !== undefined)),
168
+ relationships: sortedByName(dataset.relationships
169
+ .filter(relationship => relationship.queryable)
170
+ .map(relationship => ({
171
+ name: relationship.name,
172
+ target: relationship.target,
173
+ fields: uniqueSorted((datasets.get(relationship.target)?.dimensions ?? [])
174
+ .filter(dimension => dimension.filterable || dimension.groupable)
175
+ .map(dimension => `${relationship.name}.${dimension.name}`)),
176
+ }))),
177
+ limits: normalizedLimits(dataset.limits),
178
+ };
179
+ }
@@ -0,0 +1,34 @@
1
+ import type { DatasetCachePolicy, ExecutionContext } from '../types.js';
2
+ import type { SemanticCacheOptions } from '../cache/semantic-query-cache.js';
3
+ /**
4
+ * Client-level cache defaults, as the resolver needs to see them.
5
+ *
6
+ * They have to be resolved here rather than left to the cache: the cache fills a
7
+ * missing `ttlMs` from its own defaults *after* this runs, so a value left
8
+ * undefined here would be filled in unclamped and escape the dataset's ceiling
9
+ * entirely.
10
+ */
11
+ export type ClientCacheDefaults = Pick<SemanticCacheOptions, 'ttlMs' | 'staleWhileRevalidateMs'>;
12
+ /**
13
+ * Folds a dataset's declared cache policy into one call's cache runtime.
14
+ *
15
+ * Precedence mirrors `resolveCompiledDeadline` in `@hypequery/clickhouse`: the
16
+ * call site may shorten the window but never extend it. A declared `ttlMs`
17
+ * supplies the default when neither the call nor the client does; a declared
18
+ * `maxTtlMs` clamps whatever any of the three asked for.
19
+ *
20
+ * **`maxTtlMs` is a ceiling, not a default.** When no layer supplies a TTL, the
21
+ * result stays uncached — a maximum lifetime is not a reason to start caching
22
+ * something nobody asked to cache.
23
+ *
24
+ * **The ceiling bounds total age, not each window separately.** A cached entry
25
+ * is servable for `ttlMs + staleWhileRevalidateMs`, so clamping the two
26
+ * independently would allow twice the declared maximum. The stale window gets
27
+ * whatever the TTL leaves of the budget.
28
+ *
29
+ * A caller that opted out (`cache: false`, `mode: 'bypass'`) stays opted out.
30
+ * Opting out is a shortening, and a dataset policy is not a way to force a
31
+ * result into a cache the caller declined.
32
+ */
33
+ export declare function resolveDatasetCacheRuntime(policy: DatasetCachePolicy | undefined, callerCache: ExecutionContext['cache'], clientDefaults?: ClientCacheDefaults): ExecutionContext['cache'];
34
+ //# sourceMappingURL=dataset-cache-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset-cache-policy.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-cache-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,oBAAoB,EAAwB,MAAM,kCAAkC,CAAC;AAEnG;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,oBAAoB,EACpB,OAAO,GAAG,wBAAwB,CACnC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,kBAAkB,GAAG,SAAS,EACtC,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,EACtC,cAAc,CAAC,EAAE,mBAAmB,GACnC,gBAAgB,CAAC,OAAO,CAAC,CAsC3B"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Folds a dataset's declared cache policy into one call's cache runtime.
3
+ *
4
+ * Precedence mirrors `resolveCompiledDeadline` in `@hypequery/clickhouse`: the
5
+ * call site may shorten the window but never extend it. A declared `ttlMs`
6
+ * supplies the default when neither the call nor the client does; a declared
7
+ * `maxTtlMs` clamps whatever any of the three asked for.
8
+ *
9
+ * **`maxTtlMs` is a ceiling, not a default.** When no layer supplies a TTL, the
10
+ * result stays uncached — a maximum lifetime is not a reason to start caching
11
+ * something nobody asked to cache.
12
+ *
13
+ * **The ceiling bounds total age, not each window separately.** A cached entry
14
+ * is servable for `ttlMs + staleWhileRevalidateMs`, so clamping the two
15
+ * independently would allow twice the declared maximum. The stale window gets
16
+ * whatever the TTL leaves of the budget.
17
+ *
18
+ * A caller that opted out (`cache: false`, `mode: 'bypass'`) stays opted out.
19
+ * Opting out is a shortening, and a dataset policy is not a way to force a
20
+ * result into a cache the caller declined.
21
+ */
22
+ export function resolveDatasetCacheRuntime(policy, callerCache, clientDefaults) {
23
+ if (policy === undefined) {
24
+ return callerCache;
25
+ }
26
+ if (callerCache === false || callerCache?.mode === 'bypass') {
27
+ return callerCache;
28
+ }
29
+ // The value each layer would have produced, most specific first. The client
30
+ // default is included so the ceiling below applies to it too.
31
+ const requestedTtl = callerCache?.ttlMs ?? policy.ttlMs ?? clientDefaults?.ttlMs;
32
+ const requestedStale = callerCache?.staleWhileRevalidateMs ?? clientDefaults?.staleWhileRevalidateMs;
33
+ const resolved = { ...callerCache };
34
+ if (requestedTtl === undefined) {
35
+ // No layer asked for caching. The ceiling does not create it, and there is
36
+ // no default left downstream for it to fall back to.
37
+ return resolved;
38
+ }
39
+ // Set unconditionally once any layer supplied a value, including an explicit
40
+ // `0`. Returning without it would leave the cache to re-derive a TTL from the
41
+ // client default, turning "do not cache this dataset" into the client's TTL.
42
+ resolved.ttlMs =
43
+ policy.maxTtlMs === undefined ? requestedTtl : Math.min(requestedTtl, policy.maxTtlMs);
44
+ if (policy.maxTtlMs !== undefined && resolved.ttlMs > 0) {
45
+ // Set explicitly for the same reason: an undefined stale window would be
46
+ // filled from the client default and layered on an already-clamped TTL.
47
+ resolved.staleWhileRevalidateMs = Math.min(requestedStale ?? 0, Math.max(0, policy.maxTtlMs - resolved.ttlMs));
48
+ }
49
+ return resolved;
50
+ }
@@ -1,3 +1,3 @@
1
- import type { AggregationSpec, AnyDatasetInstance, DerivedMetricSpec, MetricContract, TimeGrain } from '../types.js';
2
- export declare function buildMetricContract(metricName: string, ds: AnyDatasetInstance, spec: AggregationSpec | DerivedMetricSpec, label?: string, description?: string, grain?: TimeGrain): MetricContract;
1
+ import type { AggregationSpec, AnyDatasetInstance, DerivedMetricSpec, MetricContract, SemanticMetadata, TimeGrain } from '../types.js';
2
+ export declare function buildMetricContract(metricName: string, ds: AnyDatasetInstance, spec: AggregationSpec | DerivedMetricSpec, label?: string, description?: string, metadata?: SemanticMetadata, grain?: TimeGrain): MetricContract;
3
3
  //# sourceMappingURL=dataset-contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dataset-contract.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,SAAS,EACV,MAAM,aAAa,CAAC;AAIrB,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,eAAe,GAAG,iBAAiB,EACzC,KAAK,CAAC,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,MAAM,EACpB,KAAK,CAAC,EAAE,SAAS,GAChB,cAAc,CA6BhB"}
1
+ {"version":3,"file":"dataset-contract.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-contract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,SAAS,EACV,MAAM,aAAa,CAAC;AAKrB,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,eAAe,GAAG,iBAAiB,EACzC,KAAK,CAAC,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,MAAM,EACpB,QAAQ,GAAE,gBAAqB,EAC/B,KAAK,CAAC,EAAE,SAAS,GAChB,cAAc,CA8BhB"}
@@ -1,5 +1,6 @@
1
+ import { snapshotSemanticMetadata } from './semantic-metadata.js';
1
2
  const ALL_GRAINS = ['day', 'week', 'month', 'quarter', 'year'];
2
- export function buildMetricContract(metricName, ds, spec, label, description, grain) {
3
+ export function buildMetricContract(metricName, ds, spec, label, description, metadata = {}, grain) {
3
4
  const dimensionNames = Object.keys(ds.dimensions);
4
5
  const measureNames = Object.keys(ds.measures);
5
6
  const filterNames = Object.keys(ds.filters).length > 0
@@ -17,6 +18,7 @@ export function buildMetricContract(metricName, ds, spec, label, description, gr
17
18
  valueType: 'number',
18
19
  label,
19
20
  description,
21
+ ...snapshotSemanticMetadata(metadata),
20
22
  dimensions: dimensionNames,
21
23
  measures: measureNames,
22
24
  filters: filterNames,
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Definition-time structural validation for `dataset()`.
3
+ *
4
+ * Every check here runs when the dataset is *constructed*, not when it is
5
+ * queried. A dataset is normally defined at module scope, so a malformed model
6
+ * fails at import — in the build, in CI, and in the first test that touches the
7
+ * module — instead of on the first request that happens to exercise the broken
8
+ * part. The alternative is a typo in `tenantKey` that constructs cleanly, passes
9
+ * review, and is discovered by a query that quietly returns another tenant's
10
+ * rows.
11
+ *
12
+ * Two categories of check live here:
13
+ *
14
+ * - **Identifier safety.** `source`, `tenantKey`, `timeKey`, dimension columns
15
+ * and measure fields are all interpolated into SQL as identifiers. They are
16
+ * validated once here rather than trusted at every call site that builds a
17
+ * predicate from them.
18
+ * - **Raw SQL shape.** The `sql` escape hatch on dimensions and measures is an
19
+ * expression, never a statement. A value carrying a statement terminator or a
20
+ * comment is rejected structurally, so it cannot become a second statement or
21
+ * comment out the rest of a clause.
22
+ */
23
+ import type { DatasetConfig, DimensionDefinition, MeasureDefinition, RelationshipDefinition } from '../types.js';
24
+ type AnyDimensions = Record<string, DimensionDefinition>;
25
+ type AnyMeasures = Record<string, MeasureDefinition>;
26
+ type AnyRelationships = Record<string, RelationshipDefinition>;
27
+ /**
28
+ * Validates a dataset definition. Throws on the first problem found.
29
+ *
30
+ * Called by `dataset()` before the instance is built, so an invalid model never
31
+ * becomes a queryable object.
32
+ */
33
+ export declare function validateDatasetDefinition(name: string, config: DatasetConfig<AnyDimensions, AnyMeasures, AnyRelationships>): void;
34
+ export {};
35
+ //# sourceMappingURL=dataset-definition-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataset-definition-validation.d.ts","sourceRoot":"","sources":["../../src/utils/dataset-definition-validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EACV,aAAa,EACb,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AAIrB,KAAK,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AACzD,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AACrD,KAAK,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AAuM/D;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,CAAC,aAAa,EAAE,WAAW,EAAE,gBAAgB,CAAC,GAClE,IAAI,CAsDN"}
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Definition-time structural validation for `dataset()`.
3
+ *
4
+ * Every check here runs when the dataset is *constructed*, not when it is
5
+ * queried. A dataset is normally defined at module scope, so a malformed model
6
+ * fails at import — in the build, in CI, and in the first test that touches the
7
+ * module — instead of on the first request that happens to exercise the broken
8
+ * part. The alternative is a typo in `tenantKey` that constructs cleanly, passes
9
+ * review, and is discovered by a query that quietly returns another tenant's
10
+ * rows.
11
+ *
12
+ * Two categories of check live here:
13
+ *
14
+ * - **Identifier safety.** `source`, `tenantKey`, `timeKey`, dimension columns
15
+ * and measure fields are all interpolated into SQL as identifiers. They are
16
+ * validated once here rather than trusted at every call site that builds a
17
+ * predicate from them.
18
+ * - **Raw SQL shape.** The `sql` escape hatch on dimensions and measures is an
19
+ * expression, never a statement. A value carrying a statement terminator or a
20
+ * comment is rejected structurally, so it cannot become a second statement or
21
+ * comment out the rest of a clause.
22
+ */
23
+ import { escapeRegExp, isSafeSQLIdentifier, stripSqlLiterals } from '../sql-utils.js';
24
+ import { validateDatasetAgentMetadata } from './semantic-metadata-validation.js';
25
+ /**
26
+ * Statement terminators and comment openers.
27
+ *
28
+ * A raw `sql` value is spliced into a larger expression, so any of these turns
29
+ * one expression into something else: `;` starts a second statement, `--` and
30
+ * `/*` comment out whatever the builder appends after it.
31
+ */
32
+ const STATEMENT_BREAKERS = /;|--|\/\*/;
33
+ /** Dataset, dimension and measure names address fields as `<relationship>.<dimension>`. */
34
+ const QUALIFIED_SEPARATOR = '.';
35
+ function fail(datasetName, message) {
36
+ throw new Error(`Invalid dataset "${datasetName}": ${message}`);
37
+ }
38
+ /**
39
+ * Validates a possibly-qualified physical name such as `orders` or
40
+ * `analytics.orders`. Each segment must stand alone as a safe identifier, so a
41
+ * qualified name cannot smuggle anything past the per-segment check.
42
+ */
43
+ function isSafeQualifiedName(value, maxSegments) {
44
+ const segments = value.split(QUALIFIED_SEPARATOR);
45
+ if (segments.length > maxSegments) {
46
+ return false;
47
+ }
48
+ return segments.every(isSafeSQLIdentifier);
49
+ }
50
+ function assertSafeColumn(datasetName, value, context) {
51
+ if (!isSafeSQLIdentifier(value)) {
52
+ fail(datasetName, `${context} "${value}" is not a safe column identifier. It is interpolated into SQL, so it ` +
53
+ 'must contain only letters, numbers and underscores, and start with a letter or underscore.');
54
+ }
55
+ }
56
+ /**
57
+ * Validates a semantic name — a dimension or measure key.
58
+ *
59
+ * These are not physical columns, but they are identifiers: they appear in query
60
+ * inputs, generated tool schemas, and protocol artifacts, where the strict
61
+ * identifier grammar applies. Rejecting here beats failing later during artifact
62
+ * production, which is far from the definition that caused it.
63
+ */
64
+ function assertSafeName(datasetName, kind, name) {
65
+ if (!isSafeSQLIdentifier(name)) {
66
+ fail(datasetName, `${kind} name "${name}" must contain only letters, numbers and underscores, and start ` +
67
+ 'with a letter or underscore, so it stays a valid identifier in generated artifacts.');
68
+ }
69
+ }
70
+ /**
71
+ * Checks a raw `sql` expression and the dependencies declared alongside it.
72
+ *
73
+ * The dependency check runs in the direction that fails silently: a declared
74
+ * dependency the expression never references means the definition believes it
75
+ * reads a column it does not read. The opposite direction — an identifier used
76
+ * but not declared — would need a SQL parser, and is caught by the protocol
77
+ * adapter when the artifact is produced.
78
+ */
79
+ function validateRawSql(datasetName, kind, name, sql, dependencies) {
80
+ if (sql.trim().length === 0) {
81
+ fail(datasetName, `${kind} "${name}" declares an empty sql expression.`);
82
+ }
83
+ // Quoted spans are blanked first, so a terminator or comment opener *inside* a
84
+ // string literal is data rather than syntax. An unterminated quote is itself a
85
+ // rejection: left as data, an open quote would hide everything after it.
86
+ let code;
87
+ let referenceable;
88
+ try {
89
+ code = stripSqlLiterals(sql);
90
+ // A quoted identifier is a column reference, so the dependency check below
91
+ // needs its text even though the terminator check above must not see it.
92
+ referenceable = stripSqlLiterals(sql, { keepQuotedIdentifiers: true });
93
+ }
94
+ catch {
95
+ fail(datasetName, `${kind} "${name}" sql has an unterminated quoted literal.`);
96
+ }
97
+ if (STATEMENT_BREAKERS.test(code)) {
98
+ fail(datasetName, `${kind} "${name}" sql must be a single expression without statement terminators or ` +
99
+ 'comments (";", "--", "/*") outside a quoted literal.');
100
+ }
101
+ for (const dependency of dependencies ?? []) {
102
+ // Dependencies are qualified identifiers (`analytics.orders.amount`); the
103
+ // expression references the column itself, so the final segment is what has
104
+ // to appear in it. Escaped before it becomes a pattern: a raw value carrying
105
+ // regex syntax would either throw at construction or match something else.
106
+ const segments = dependency.split(QUALIFIED_SEPARATOR);
107
+ const column = segments[segments.length - 1] ?? '';
108
+ if (column.length === 0 || !new RegExp(`\\b${escapeRegExp(column)}\\b`).test(referenceable)) {
109
+ fail(datasetName, `${kind} "${name}" declares dependency "${dependency}", but its sql expression never ` +
110
+ 'references it. A dependency the expression does not read makes the definition claim ' +
111
+ 'a column it never touches.');
112
+ }
113
+ }
114
+ }
115
+ function validateDimensions(datasetName, dimensions) {
116
+ for (const [name, definition] of Object.entries(dimensions)) {
117
+ if (name.includes(QUALIFIED_SEPARATOR)) {
118
+ fail(datasetName, `dimension "${name}" cannot contain "." because qualified fields are addressed as ` +
119
+ '"<relationship>.<dimension>".');
120
+ }
121
+ // The name is a semantic identifier in its own right — it appears in
122
+ // protocol artifacts and query inputs — so it is checked whether or not an
123
+ // explicit column stands in for it below. Without this, the check silently
124
+ // depended on the column being omitted.
125
+ assertSafeName(datasetName, 'dimension', name);
126
+ if (definition.sql !== undefined) {
127
+ validateRawSql(datasetName, 'dimension', name, definition.sql, definition.dependencies);
128
+ continue;
129
+ }
130
+ // A dimension with no explicit column is backed by a column of the same name.
131
+ assertSafeColumn(datasetName, definition.column ?? name, `dimension "${name}" column`);
132
+ }
133
+ }
134
+ function validateMeasures(datasetName, measures, dimensions) {
135
+ for (const [name, definition] of Object.entries(measures)) {
136
+ assertSafeName(datasetName, 'measure', name);
137
+ if (definition.sql !== undefined) {
138
+ validateRawSql(datasetName, 'measure', name, definition.sql, definition.dependencies);
139
+ }
140
+ // `field` and `argField` name either a declared dimension or a physical
141
+ // column that the model deliberately does not expose (the `allowHiddenField`
142
+ // case in metric validation). Either way the value reaches SQL, so a name
143
+ // that is not a declared dimension must at least be a safe identifier.
144
+ for (const [label, value] of [
145
+ ['field', definition.field],
146
+ ['argField', definition.argField],
147
+ ]) {
148
+ if (value === undefined || value in dimensions) {
149
+ continue;
150
+ }
151
+ assertSafeColumn(datasetName, value, `measure "${name}" ${label}`);
152
+ }
153
+ }
154
+ }
155
+ function validateLimits(datasetName, limits) {
156
+ if (!limits) {
157
+ return;
158
+ }
159
+ for (const [name, value] of Object.entries(limits)) {
160
+ if (value === undefined) {
161
+ continue;
162
+ }
163
+ if (!Number.isInteger(value) || value <= 0) {
164
+ fail(datasetName, `limits.${name} must be a positive integer, received ${String(value)}.`);
165
+ }
166
+ }
167
+ }
168
+ /**
169
+ * Validates a dataset definition. Throws on the first problem found.
170
+ *
171
+ * Called by `dataset()` before the instance is built, so an invalid model never
172
+ * becomes a queryable object.
173
+ */
174
+ export function validateDatasetDefinition(name, config) {
175
+ if (typeof name !== 'string' || name.trim().length === 0) {
176
+ throw new Error('Invalid dataset: a non-empty dataset name is required.');
177
+ }
178
+ if (!isSafeSQLIdentifier(name)) {
179
+ fail(name, 'dataset names must contain only letters, numbers and underscores, and start with a letter ' +
180
+ 'or underscore, so they remain valid identifiers in generated artifacts.');
181
+ }
182
+ if (typeof config.source !== 'string' || config.source.trim().length === 0) {
183
+ fail(name, 'a non-empty source table is required.');
184
+ }
185
+ // `database.table` is the deepest qualification ClickHouse addresses.
186
+ if (!isSafeQualifiedName(config.source, 2)) {
187
+ fail(name, `source "${config.source}" is not a safe table identifier. Expected "table" or ` +
188
+ '"database.table", each segment containing only letters, numbers and underscores.');
189
+ }
190
+ // The tenant key is the isolation boundary: it becomes a WHERE predicate on
191
+ // every query against a tenant-scoped dataset, so it is checked here rather
192
+ // than trusted wherever that predicate is assembled.
193
+ if (config.tenantKey !== undefined) {
194
+ if (config.tenantKey.trim().length === 0) {
195
+ fail(name, 'tenantKey cannot be empty. Omit it entirely for a dataset without tenancy.');
196
+ }
197
+ assertSafeColumn(name, config.tenantKey, 'tenantKey');
198
+ }
199
+ if (config.timeKey !== undefined) {
200
+ if (config.timeKey.trim().length === 0) {
201
+ fail(name, 'timeKey cannot be empty. Omit it entirely for a dataset without a time key.');
202
+ }
203
+ if (!(config.timeKey in (config.dimensions ?? {}))) {
204
+ assertSafeColumn(name, config.timeKey, 'timeKey');
205
+ }
206
+ }
207
+ // A dataset with no dimensions is not rejected here: the query layer already
208
+ // refuses an empty dataset query, which is the honest place for it — a
209
+ // measure-only model is a legitimate thing to define.
210
+ const dimensions = config.dimensions ?? {};
211
+ validateDimensions(name, dimensions);
212
+ validateMeasures(name, config.measures ?? {}, dimensions);
213
+ validateLimits(name, config.limits);
214
+ validateDatasetAgentMetadata(name, config);
215
+ }
@@ -1,9 +1,9 @@
1
- import type { AggregationSpec, BaseMetricConfig, BaseMetricRef, DatasetInstance, DerivedMetricConfig, DerivedMetricRef, DerivedMetricSpec, DimensionDefinition, MeasureDefinition, MetricRef, RelationshipDefinition } from '../types.js';
1
+ import type { AggregationSpec, BaseMetricConfig, BaseMetricRef, DatasetInstance, DerivedMetricConfig, DerivedMetricRef, DerivedMetricSpec, DimensionDefinition, MeasureDefinition, MetricRef, RelationshipDefinition, SemanticMetadata } from '../types.js';
2
2
  type AnyDimensions = Record<string, DimensionDefinition>;
3
3
  type AnyMeasures = Record<string, MeasureDefinition>;
4
4
  type AnyRelationships = Record<string, RelationshipDefinition>;
5
5
  export declare function isDerivedMetricConfig<TMeasures extends Record<string, MeasureDefinition>, TDatasetName extends string>(config: BaseMetricConfig<TMeasures> | DerivedMetricConfig<TDatasetName>): config is DerivedMetricConfig<TDatasetName>;
6
- export declare function createMetricRef<TDatasetName extends string, TMetricName extends string, TSpec extends AggregationSpec | DerivedMetricSpec<TDatasetName>, TDataset extends DatasetInstance<AnyDimensions, AnyMeasures, AnyRelationships, TDatasetName>>(ds: TDataset, name: TMetricName, spec: TSpec, label?: string, description?: string): MetricRef<TDatasetName, TMetricName, TSpec, TDataset>;
6
+ export declare function createMetricRef<TDatasetName extends string, TMetricName extends string, TSpec extends AggregationSpec | DerivedMetricSpec<TDatasetName>, TDataset extends DatasetInstance<AnyDimensions, AnyMeasures, AnyRelationships, TDatasetName>>(ds: TDataset, name: TMetricName, spec: TSpec, label?: string, description?: string, metadata?: SemanticMetadata): MetricRef<TDatasetName, TMetricName, TSpec, TDataset>;
7
7
  export declare function createDerivedMetricSpec<TDatasetName extends string>(config: DerivedMetricConfig<TDatasetName>): DerivedMetricSpec<TDatasetName>;
8
8
  export type { BaseMetricRef, DerivedMetricRef };
9
9
  //# sourceMappingURL=dataset-metric-ref.d.ts.map