@hypequery/datasets 0.18.0 → 0.20.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 (202) hide show
  1. package/dist/agent-catalog.d.ts +11 -1
  2. package/dist/agent-catalog.d.ts.map +1 -1
  3. package/dist/aggregations.d.ts +2 -0
  4. package/dist/aggregations.d.ts.map +1 -1
  5. package/dist/aggregations.js +4 -0
  6. package/dist/api.type-test.js +57 -0
  7. package/dist/cache/definition-identity.d.ts +26 -0
  8. package/dist/cache/definition-identity.d.ts.map +1 -0
  9. package/dist/cache/definition-identity.js +164 -0
  10. package/dist/cache/protocol-cache-keys.d.ts +38 -0
  11. package/dist/cache/protocol-cache-keys.d.ts.map +1 -0
  12. package/dist/cache/protocol-cache-keys.js +191 -0
  13. package/dist/cache/query-signature.d.ts +6 -0
  14. package/dist/cache/query-signature.d.ts.map +1 -1
  15. package/dist/cache/query-signature.js +27 -13
  16. package/dist/cache/semantic-query-cache.d.ts +23 -0
  17. package/dist/cache/semantic-query-cache.d.ts.map +1 -1
  18. package/dist/catalog.d.ts +14 -2
  19. package/dist/catalog.d.ts.map +1 -1
  20. package/dist/catalog.js +37 -10
  21. package/dist/constants.d.ts.map +1 -1
  22. package/dist/constants.js +2 -0
  23. package/dist/contract.d.ts +7 -2
  24. package/dist/contract.d.ts.map +1 -1
  25. package/dist/contract.js +9 -0
  26. package/dist/dataset-query.d.ts.map +1 -1
  27. package/dist/dataset-query.js +27 -5
  28. package/dist/dataset.d.ts +2 -2
  29. package/dist/dataset.d.ts.map +1 -1
  30. package/dist/dataset.js +12 -4
  31. package/dist/executor.d.ts +7 -1
  32. package/dist/executor.d.ts.map +1 -1
  33. package/dist/executor.js +75 -22
  34. package/dist/in-memory-backend.d.ts.map +1 -1
  35. package/dist/in-memory-backend.js +34 -7
  36. package/dist/index.d.ts +8 -6
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +3 -2
  39. package/dist/measure.d.ts +17 -2
  40. package/dist/measure.d.ts.map +1 -1
  41. package/dist/measure.js +16 -1
  42. package/dist/portable-execution-errors.d.ts +4 -0
  43. package/dist/portable-execution-errors.d.ts.map +1 -1
  44. package/dist/portable-execution-errors.js +4 -1
  45. package/dist/portable-executor.d.ts.map +1 -1
  46. package/dist/portable-executor.js +6 -3
  47. package/dist/protocol-adapter.d.ts.map +1 -1
  48. package/dist/protocol-adapter.js +25 -8
  49. package/dist/protocol-deployment-adapter.d.ts.map +1 -1
  50. package/dist/protocol-deployment-adapter.js +2 -1
  51. package/dist/protocol-rehydrate.d.ts +5 -1
  52. package/dist/protocol-rehydrate.d.ts.map +1 -1
  53. package/dist/protocol-rehydrate.js +20 -10
  54. package/dist/query-builder-protocol.d.ts +7 -3
  55. package/dist/query-builder-protocol.d.ts.map +1 -1
  56. package/dist/query-planner.d.ts +1 -1
  57. package/dist/query-planner.d.ts.map +1 -1
  58. package/dist/query-planner.js +10 -17
  59. package/dist/relationship-check.d.ts +47 -0
  60. package/dist/relationship-check.d.ts.map +1 -0
  61. package/dist/relationship-check.js +59 -0
  62. package/dist/relationships.d.ts +10 -10
  63. package/dist/relationships.js +10 -10
  64. package/dist/semantic-plan.d.ts +0 -1
  65. package/dist/semantic-plan.d.ts.map +1 -1
  66. package/dist/semantic-planner.d.ts.map +1 -1
  67. package/dist/semantic-planner.js +15 -4
  68. package/dist/semantic-query-schema.d.ts.map +1 -1
  69. package/dist/semantic-query-schema.js +7 -0
  70. package/dist/types.d.ts +99 -18
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/utils/agent-catalog-projection.d.ts +9 -2
  73. package/dist/utils/agent-catalog-projection.d.ts.map +1 -1
  74. package/dist/utils/agent-catalog-projection.js +6 -10
  75. package/dist/utils/approximate-measures.d.ts +10 -0
  76. package/dist/utils/approximate-measures.d.ts.map +1 -0
  77. package/dist/utils/approximate-measures.js +12 -0
  78. package/dist/utils/dataset-contract.d.ts.map +1 -1
  79. package/dist/utils/dataset-contract.js +4 -3
  80. package/dist/utils/dataset-definition-validation.d.ts.map +1 -1
  81. package/dist/utils/dataset-definition-validation.js +6 -24
  82. package/dist/utils/dataset-derived-query.d.ts.map +1 -1
  83. package/dist/utils/dataset-derived-query.js +7 -50
  84. package/dist/utils/dataset-measures.d.ts +13 -6
  85. package/dist/utils/dataset-measures.d.ts.map +1 -1
  86. package/dist/utils/dataset-measures.js +25 -4
  87. package/dist/utils/dataset-metric-ref.d.ts +2 -2
  88. package/dist/utils/dataset-metric-ref.d.ts.map +1 -1
  89. package/dist/utils/dataset-metric-ref.js +5 -0
  90. package/dist/utils/dataset-query-result.d.ts.map +1 -1
  91. package/dist/utils/dataset-query-result.js +2 -1
  92. package/dist/utils/dataset-query-validation.d.ts.map +1 -1
  93. package/dist/utils/dataset-query-validation.js +13 -6
  94. package/dist/utils/dataset-time-grains.d.ts +15 -0
  95. package/dist/utils/dataset-time-grains.d.ts.map +1 -0
  96. package/dist/utils/dataset-time-grains.js +17 -0
  97. package/dist/utils/derived-measure-sql.d.ts +3 -0
  98. package/dist/utils/derived-measure-sql.d.ts.map +1 -0
  99. package/dist/utils/derived-measure-sql.js +46 -0
  100. package/dist/utils/derived-measure-validation.d.ts.map +1 -1
  101. package/dist/utils/derived-measure-validation.js +6 -5
  102. package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -1
  103. package/dist/utils/filtered-aggregation-sql.js +1 -0
  104. package/dist/utils/measure-time-grains.d.ts +4 -0
  105. package/dist/utils/measure-time-grains.d.ts.map +1 -0
  106. package/dist/utils/measure-time-grains.js +16 -0
  107. package/dist/utils/portable-aggregations.d.ts +14 -0
  108. package/dist/utils/portable-aggregations.d.ts.map +1 -0
  109. package/dist/utils/portable-aggregations.js +15 -0
  110. package/dist/utils/portable-grains.d.ts +22 -0
  111. package/dist/utils/portable-grains.d.ts.map +1 -0
  112. package/dist/utils/portable-grains.js +35 -0
  113. package/dist/utils/portable-semantic-query.d.ts.map +1 -1
  114. package/dist/utils/portable-semantic-query.js +2 -1
  115. package/dist/utils/protocol-metric-expressions.d.ts.map +1 -1
  116. package/dist/utils/protocol-metric-expressions.js +2 -1
  117. package/dist/utils/query-timezone.d.ts +9 -0
  118. package/dist/utils/query-timezone.d.ts.map +1 -0
  119. package/dist/utils/query-timezone.js +35 -0
  120. package/dist/utils/relationship-builder-plan.d.ts +6 -3
  121. package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
  122. package/dist/utils/relationship-builder-plan.js +13 -5
  123. package/dist/utils/relationship-key-check.d.ts +43 -0
  124. package/dist/utils/relationship-key-check.d.ts.map +1 -0
  125. package/dist/utils/relationship-key-check.js +72 -0
  126. package/dist/utils/segments.d.ts +31 -0
  127. package/dist/utils/segments.d.ts.map +1 -0
  128. package/dist/utils/segments.js +91 -0
  129. package/dist/utils/semantic-metadata-validation.d.ts.map +1 -1
  130. package/dist/utils/semantic-metadata-validation.js +23 -2
  131. package/dist/utils/semantic-metadata.d.ts.map +1 -1
  132. package/dist/utils/semantic-metadata.js +3 -1
  133. package/dist/utils/shift-measure-definition.d.ts +4 -0
  134. package/dist/utils/shift-measure-definition.d.ts.map +1 -0
  135. package/dist/utils/shift-measure-definition.js +9 -0
  136. package/dist/utils/shift-measure-sql.d.ts +18 -0
  137. package/dist/utils/shift-measure-sql.d.ts.map +1 -0
  138. package/dist/utils/shift-measure-sql.js +46 -0
  139. package/dist/utils/shift-measure-validation.d.ts +3 -0
  140. package/dist/utils/shift-measure-validation.d.ts.map +1 -0
  141. package/dist/utils/shift-measure-validation.js +20 -0
  142. package/dist/utils/sql-under-joins.d.ts +10 -0
  143. package/dist/utils/sql-under-joins.d.ts.map +1 -0
  144. package/dist/utils/sql-under-joins.js +15 -0
  145. package/dist/utils/time-arithmetic-sql.d.ts +4 -0
  146. package/dist/utils/time-arithmetic-sql.d.ts.map +1 -0
  147. package/dist/utils/time-arithmetic-sql.js +9 -0
  148. package/dist/utils/time-axis-bounds.d.ts +24 -0
  149. package/dist/utils/time-axis-bounds.d.ts.map +1 -0
  150. package/dist/utils/time-axis-bounds.js +71 -0
  151. package/dist/utils/time-axis-calendar-guard.d.ts +9 -0
  152. package/dist/utils/time-axis-calendar-guard.d.ts.map +1 -0
  153. package/dist/utils/time-axis-calendar-guard.js +17 -0
  154. package/dist/utils/time-axis-intervals.d.ts +13 -0
  155. package/dist/utils/time-axis-intervals.d.ts.map +1 -0
  156. package/dist/utils/time-axis-intervals.js +81 -0
  157. package/dist/utils/time-measure-axis-sql.d.ts +12 -0
  158. package/dist/utils/time-measure-axis-sql.d.ts.map +1 -0
  159. package/dist/utils/time-measure-axis-sql.js +47 -0
  160. package/dist/utils/time-measure-axis.d.ts +19 -0
  161. package/dist/utils/time-measure-axis.d.ts.map +1 -0
  162. package/dist/utils/time-measure-axis.js +45 -0
  163. package/dist/utils/time-measure-dataset-sql.d.ts +9 -0
  164. package/dist/utils/time-measure-dataset-sql.d.ts.map +1 -0
  165. package/dist/utils/time-measure-dataset-sql.js +63 -0
  166. package/dist/utils/time-measure-result-sql.d.ts +6 -0
  167. package/dist/utils/time-measure-result-sql.d.ts.map +1 -0
  168. package/dist/utils/time-measure-result-sql.js +30 -0
  169. package/dist/utils/time-measure-source-sql.d.ts +22 -0
  170. package/dist/utils/time-measure-source-sql.d.ts.map +1 -0
  171. package/dist/utils/time-measure-source-sql.js +75 -0
  172. package/dist/utils/time-measure-values-sql.d.ts +20 -0
  173. package/dist/utils/time-measure-values-sql.d.ts.map +1 -0
  174. package/dist/utils/time-measure-values-sql.js +87 -0
  175. package/dist/utils/time-query-measures.d.ts +6 -0
  176. package/dist/utils/time-query-measures.d.ts.map +1 -0
  177. package/dist/utils/time-query-measures.js +28 -0
  178. package/dist/utils/unsupported-contract-reasons.d.ts +32 -0
  179. package/dist/utils/unsupported-contract-reasons.d.ts.map +1 -0
  180. package/dist/utils/unsupported-contract-reasons.js +30 -0
  181. package/dist/utils/window-aggregation-sql.d.ts +5 -0
  182. package/dist/utils/window-aggregation-sql.d.ts.map +1 -0
  183. package/dist/utils/window-aggregation-sql.js +17 -0
  184. package/dist/utils/window-catalog-metadata.d.ts +14 -0
  185. package/dist/utils/window-catalog-metadata.d.ts.map +1 -0
  186. package/dist/utils/window-catalog-metadata.js +17 -0
  187. package/dist/utils/window-measure-definition.d.ts +4 -0
  188. package/dist/utils/window-measure-definition.d.ts.map +1 -0
  189. package/dist/utils/window-measure-definition.js +11 -0
  190. package/dist/utils/window-measure-dependencies.d.ts +4 -0
  191. package/dist/utils/window-measure-dependencies.d.ts.map +1 -0
  192. package/dist/utils/window-measure-dependencies.js +5 -0
  193. package/dist/utils/window-measure-validation.d.ts +3 -0
  194. package/dist/utils/window-measure-validation.d.ts.map +1 -0
  195. package/dist/utils/window-measure-validation.js +49 -0
  196. package/dist/utils/window-query-measures.d.ts +4 -0
  197. package/dist/utils/window-query-measures.d.ts.map +1 -0
  198. package/dist/utils/window-query-measures.js +17 -0
  199. package/dist/utils/window-query-signature.d.ts +4 -0
  200. package/dist/utils/window-query-signature.d.ts.map +1 -0
  201. package/dist/utils/window-query-signature.js +25 -0
  202. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"portable-semantic-query.d.ts","sourceRoot":"","sources":["../../src/utils/portable-semantic-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAA+B,qBAAqB,EAAa,MAAM,aAAa,CAAC;AAIjG,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,qBAAqB,GAAG,SAAS,CAKhF;AAWD,wBAAgB,aAAa,CAAC,SAAS,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqBxG"}
1
+ {"version":3,"file":"portable-semantic-query.d.ts","sourceRoot":"","sources":["../../src/utils/portable-semantic-query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAA+B,qBAAqB,EAAa,MAAM,aAAa,CAAC;AAKjG,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,qBAAqB,GAAG,SAAS,CAKhF;AAYD,wBAAgB,aAAa,CAAC,SAAS,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqBxG"}
@@ -1,5 +1,6 @@
1
1
  import { PortableExecutionUnsupportedError } from '../portable-execution-errors.js';
2
2
  import { rehydrateMeasureFilter } from './protocol-rehydrate-filters.js';
3
+ import { UNSUPPORTED_CONTRACT_REASONS } from './unsupported-contract-reasons.js';
3
4
  export function tenantRuntime(tenant) {
4
5
  if (tenant === undefined || tenant === null)
5
6
  return undefined;
@@ -10,7 +11,7 @@ export function tenantRuntime(tenant) {
10
11
  return tenant;
11
12
  }
12
13
  function operationFilters(operation) {
13
- return (operation.filters ?? []).map((expression, index) => rehydrateMeasureFilter(expression, () => new PortableExecutionUnsupportedError(`Filter ${index} is not a field/operator/value comparison, so it cannot be planned.`)));
14
+ return (operation.filters ?? []).map((expression, index) => rehydrateMeasureFilter(expression, () => new PortableExecutionUnsupportedError(`Filter ${index} is not a field/operator/value comparison, so it cannot be planned.`, { reason: UNSUPPORTED_CONTRACT_REASONS.queryFilterNotComparison })));
14
15
  }
15
16
  export function semanticQuery(operation, maxRows) {
16
17
  const filters = operationFilters(operation);
@@ -1 +1 @@
1
- {"version":3,"file":"protocol-metric-expressions.d.ts","sourceRoot":"","sources":["../../src/utils/protocol-metric-expressions.ts"],"names":[],"mappings":"AAAA,OAAO,EAEgB,KAAK,kBAAkB,EAAE,KAAK,wBAAwB,EAC5E,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,eAAe,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAsC9G,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,kBAAkB,CAgBzE;AA+CD,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,wBAAwB,GAAG,kBAAkB,CAGjG;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,iBAAiB,GAAG,kBAAkB,CAQ9F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,wBAAwB,CASlF"}
1
+ {"version":3,"file":"protocol-metric-expressions.d.ts","sourceRoot":"","sources":["../../src/utils/protocol-metric-expressions.ts"],"names":[],"mappings":"AAAA,OAAO,EAEgB,KAAK,kBAAkB,EAAE,KAAK,wBAAwB,EAC5E,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,eAAe,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAuC9G,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,GAAG,kBAAkB,CAgBzE;AA+CD,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,wBAAwB,GAAG,kBAAkB,CAGjG;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,iBAAiB,GAAG,kBAAkB,CAQ9F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,wBAAwB,CASlF"}
@@ -1,4 +1,5 @@
1
1
  import { parseProtocolIdentifier, parseProtocolQualifiedIdentifier, validateCanonicalValue, } from '@hypequery/protocol';
2
+ import { requirePortableAggregation } from './portable-aggregations.js';
2
3
  function canonicalValue(input) {
3
4
  if (Array.isArray(input)) {
4
5
  return validateCanonicalValue({
@@ -48,7 +49,7 @@ export function filterExpression(filter) {
48
49
  function aggregationExpression(spec) {
49
50
  const result = {
50
51
  kind: 'aggregate',
51
- aggregation: spec.aggregation,
52
+ aggregation: requirePortableAggregation(spec.aggregation, `An aggregate over "${spec.field}"`),
52
53
  field: parseProtocolQualifiedIdentifier(spec.field),
53
54
  ...(spec.argField !== undefined
54
55
  ? { argField: parseProtocolQualifiedIdentifier(spec.argField) }
@@ -0,0 +1,9 @@
1
+ import type { AnyDatasetInstance } from '../types.js';
2
+ /** Named zones only: reject fixed offsets, invalid zones, and SQL fragments. */
3
+ export declare function isQueryTimezone(value: unknown): value is string;
4
+ export declare function queryTimezoneErrors(timezone: unknown): string[];
5
+ /** Conversion preserves timestamp instants and interprets Date values as local dates. */
6
+ export declare function queryTimeSql(expression: string, timezone: string | undefined): string;
7
+ /** Apply the query zone to time-key predicates, including semantic filter aliases. */
8
+ export declare function queryTimeFilterSql(ds: AnyDatasetInstance, field: string, expression: string, timezone: string | undefined): string;
9
+ //# sourceMappingURL=query-timezone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-timezone.d.ts","sourceRoot":"","sources":["../../src/utils/query-timezone.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAS/D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAE,CAG/D;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMrF;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GACtF,MAAM,CAMR"}
@@ -0,0 +1,35 @@
1
+ /** Named zones only: reject fixed offsets, invalid zones, and SQL fragments. */
2
+ export function isQueryTimezone(value) {
3
+ if (typeof value !== 'string' || value.length > 100
4
+ || !/^[A-Za-z][A-Za-z0-9._+-]*(?:\/[A-Za-z0-9._+-]+)*$/.test(value))
5
+ return false;
6
+ try {
7
+ new Intl.DateTimeFormat('en', { timeZone: value });
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ export function queryTimezoneErrors(timezone) {
15
+ return timezone === undefined || isQueryTimezone(timezone)
16
+ ? [] : ['Invalid timezone: use an IANA name such as UTC or America/New_York.'];
17
+ }
18
+ /** Conversion preserves timestamp instants and interprets Date values as local dates. */
19
+ export function queryTimeSql(expression, timezone) {
20
+ timezone ??= 'UTC';
21
+ const errors = queryTimezoneErrors(timezone);
22
+ if (errors.length)
23
+ throw new Error(errors[0]);
24
+ const name = new Intl.DateTimeFormat('en', { timeZone: timezone }).resolvedOptions().timeZone;
25
+ return `toDateTime64(${expression}, 9, '${name}')`;
26
+ }
27
+ /** Apply the query zone to time-key predicates, including semantic filter aliases. */
28
+ export function queryTimeFilterSql(ds, field, expression, timezone) {
29
+ if (!ds.timeKey || field.includes('.'))
30
+ return expression;
31
+ const resolved = ds.filters[field]?.field ?? field;
32
+ const column = ds.dimensions[resolved]?.column ?? resolved;
33
+ const timeColumn = ds.dimensions[ds.timeKey]?.column ?? ds.timeKey;
34
+ return column === timeColumn ? queryTimeSql(expression, timezone) : expression;
35
+ }
@@ -46,10 +46,13 @@ export declare function qualifyBaseColumn(ctx: RelationshipBuilderContext | unde
46
46
  */
47
47
  export declare function resolveQualifiedColumn(ds: AnyDatasetInstance, name: string): string;
48
48
  /**
49
- * Applies each relationship LEFT JOIN to the builder and, when runtime tenancy
49
+ * Applies each relationship join to the builder as a single-match LEFT JOIN
50
+ * (`leftAnyJoin`, e.g. ClickHouse `LEFT ANY JOIN`) and, when runtime tenancy
50
51
  * is active on a target, scopes the joined rows with the tenant predicate.
51
- * Builders that support single-match joins (`leftAnyJoin`, e.g. ClickHouse
52
- * `LEFT ANY JOIN`) get them so duplicate target keys cannot fan out aggregates.
52
+ *
53
+ * A builder without `leftAnyJoin` is refused rather than downgraded to a plain
54
+ * `leftJoin`: duplicate target keys would fan out base rows and silently
55
+ * inflate every aggregate in the query.
53
56
  */
54
57
  export declare function applyRelationshipJoins(qb: QueryBuilderLike, ctx: RelationshipBuilderContext | undefined): QueryBuilderLike;
55
58
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"relationship-builder-plan.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-builder-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtF,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,kEAAkE;IAClE,MAAM,CAAC,EAAE,eAAe,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CACtD;AAED,KAAK,SAAS,GAAG,IAAI,CAAC,YAAY,GAAG,WAAW,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC,CAAC;AAExF;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,SAAS,EAChB,OAAO,CAAC,EAAE,gBAAgB,GACzB,0BAA0B,GAAG,SAAS,CAuCxC;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,0BAA0B,GAAG,SAAS,EAC3C,MAAM,EAAE,MAAM,GACb,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,MAAM,GACX,MAAM,CAOR;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,0BAA0B,GAAG,SAAS,GAC1C,gBAAgB,CAqBlB"}
1
+ {"version":3,"file":"relationship-builder-plan.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-builder-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACZ,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtF,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,kEAAkE;IAClE,MAAM,CAAC,EAAE,eAAe,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CACtD;AAED,KAAK,SAAS,GAAG,IAAI,CAAC,YAAY,GAAG,WAAW,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC,CAAC;AAExF;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,SAAS,EAChB,OAAO,CAAC,EAAE,gBAAgB,GACzB,0BAA0B,GAAG,SAAS,CAuCxC;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,0BAA0B,GAAG,SAAS,EAC3C,MAAM,EAAE,MAAM,GACb,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,MAAM,GACX,MAAM,CAOR;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,0BAA0B,GAAG,SAAS,GAC1C,gBAAgB,CA4BlB"}
@@ -69,18 +69,26 @@ export function resolveQualifiedColumn(ds, name) {
69
69
  return `${relationshipName}.${targetColumn}`;
70
70
  }
71
71
  /**
72
- * Applies each relationship LEFT JOIN to the builder and, when runtime tenancy
72
+ * Applies each relationship join to the builder as a single-match LEFT JOIN
73
+ * (`leftAnyJoin`, e.g. ClickHouse `LEFT ANY JOIN`) and, when runtime tenancy
73
74
  * is active on a target, scopes the joined rows with the tenant predicate.
74
- * Builders that support single-match joins (`leftAnyJoin`, e.g. ClickHouse
75
- * `LEFT ANY JOIN`) get them so duplicate target keys cannot fan out aggregates.
75
+ *
76
+ * A builder without `leftAnyJoin` is refused rather than downgraded to a plain
77
+ * `leftJoin`: duplicate target keys would fan out base rows and silently
78
+ * inflate every aggregate in the query.
76
79
  */
77
80
  export function applyRelationshipJoins(qb, ctx) {
78
81
  if (!ctx) {
79
82
  return qb;
80
83
  }
81
84
  for (const join of ctx.joins) {
82
- const applyJoin = qb.leftAnyJoin?.bind(qb) ?? qb.leftJoin.bind(qb);
83
- qb = applyJoin(join.source, `${ctx.baseSource}.${join.from}`, `${join.relationship}.${join.to}`, join.relationship, join.tenant
85
+ if (!qb.leftAnyJoin) {
86
+ throw new Error(`Relationship "${join.relationship}" cannot be joined: the query builder does not implement ` +
87
+ 'leftAnyJoin (a single-match LEFT JOIN such as ClickHouse "LEFT ANY JOIN"). A plain leftJoin ' +
88
+ 'would fan out duplicate target keys and inflate aggregates, so relationship-qualified fields ' +
89
+ 'are unavailable with this builder.');
90
+ }
91
+ qb = qb.leftAnyJoin(join.source, `${ctx.baseSource}.${join.from}`, `${join.relationship}.${join.to}`, join.relationship, join.tenant
84
92
  ? {
85
93
  column: `${join.relationship}.${join.tenant.field}`,
86
94
  operator: join.tenant.operator,
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Pure helpers for `checkRelationships`: which relationships a check covers,
3
+ * and how a key-count row becomes a finding.
4
+ */
5
+ import type { AnyDatasetInstance } from '../types.js';
6
+ export type ToOneRelationshipKind = 'belongsTo' | 'hasOne';
7
+ export interface ToOneRelationshipTarget {
8
+ relationship: string;
9
+ kind: ToOneRelationshipKind;
10
+ /** Target dataset. */
11
+ target: AnyDatasetInstance;
12
+ /** Target join column, which a to-one declaration requires to be unique. */
13
+ column: string;
14
+ }
15
+ /**
16
+ * Lists the to-one relationships to check. `hasMany` is excluded because it
17
+ * never joins, so its key has no uniqueness requirement. An explicit
18
+ * `only` list must name declared to-one relationships.
19
+ */
20
+ export declare function listToOneRelationships(ds: AnyDatasetInstance, only?: readonly string[]): ToOneRelationshipTarget[];
21
+ export interface RelationshipKeyIssue {
22
+ relationship: string;
23
+ kind: ToOneRelationshipKind;
24
+ /** Target dataset name. */
25
+ target: string;
26
+ /** Physical target table. */
27
+ source: string;
28
+ /** Target join column. */
29
+ column: string;
30
+ /** Target rows with a non-NULL key, within the checked tenant scope. Large counts are decimal strings. */
31
+ rows: number | string;
32
+ /** Distinct non-NULL keys, within the checked tenant scope. Large counts are decimal strings. */
33
+ distinctKeys: number | string;
34
+ message: string;
35
+ }
36
+ /** Reads a count without losing precision when a driver returns UInt64 as a string. */
37
+ export declare function readCount(value: unknown): bigint;
38
+ /**
39
+ * Returns the finding for a to-one relationship whose target key repeats, or
40
+ * undefined when every non-NULL key is unique.
41
+ */
42
+ export declare function relationshipKeyIssue(entry: ToOneRelationshipTarget, rows: bigint, distinctKeys: bigint): RelationshipKeyIssue | undefined;
43
+ //# sourceMappingURL=relationship-key-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relationship-key-check.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-key-check.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAA0B,MAAM,aAAa,CAAC;AAE9E,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE3D,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,sBAAsB;IACtB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,kBAAkB,EACtB,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GACvB,uBAAuB,EAAE,CAuB3B;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,0GAA0G;IAC1G,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,iGAAiG;IACjG,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAchD;AAMD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,uBAAuB,EAC9B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACnB,oBAAoB,GAAG,SAAS,CAkBlC"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Pure helpers for `checkRelationships`: which relationships a check covers,
3
+ * and how a key-count row becomes a finding.
4
+ */
5
+ /**
6
+ * Lists the to-one relationships to check. `hasMany` is excluded because it
7
+ * never joins, so its key has no uniqueness requirement. An explicit
8
+ * `only` list must name declared to-one relationships.
9
+ */
10
+ export function listToOneRelationships(ds, only) {
11
+ const declared = Object.entries(ds.relationships);
12
+ if (only) {
13
+ for (const name of only) {
14
+ const relationship = Object.hasOwn(ds.relationships, name) ? ds.relationships[name] : undefined;
15
+ if (!relationship) {
16
+ throw new Error(`Unknown relationship "${name}" on dataset "${ds.name}".`);
17
+ }
18
+ if (relationship.kind === 'hasMany') {
19
+ throw new Error(`Relationship "${name}" on dataset "${ds.name}" is hasMany; only to-one relationships have a key to check.`);
20
+ }
21
+ }
22
+ }
23
+ return declared
24
+ .filter(([name, relationship]) => relationship.kind !== 'hasMany' && (!only || only.includes(name)))
25
+ .map(([name, relationship]) => ({
26
+ relationship: name,
27
+ kind: relationship.kind,
28
+ target: relationship.target(),
29
+ column: relationship.to,
30
+ }));
31
+ }
32
+ /** Reads a count without losing precision when a driver returns UInt64 as a string. */
33
+ export function readCount(value) {
34
+ if (value == null) {
35
+ return 0n;
36
+ }
37
+ if (typeof value === 'bigint' && value >= 0n) {
38
+ return value;
39
+ }
40
+ if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
41
+ return BigInt(value);
42
+ }
43
+ if (typeof value === 'string' && /^\d+$/.test(value)) {
44
+ return BigInt(value);
45
+ }
46
+ throw new Error(`Expected a non-negative integer row count, received ${JSON.stringify(String(value))}.`);
47
+ }
48
+ function displayCount(count) {
49
+ return count <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(count) : count.toString();
50
+ }
51
+ /**
52
+ * Returns the finding for a to-one relationship whose target key repeats, or
53
+ * undefined when every non-NULL key is unique.
54
+ */
55
+ export function relationshipKeyIssue(entry, rows, distinctKeys) {
56
+ if (rows <= distinctKeys) {
57
+ return undefined;
58
+ }
59
+ const source = entry.target.source;
60
+ return {
61
+ relationship: entry.relationship,
62
+ kind: entry.kind,
63
+ target: entry.target.name,
64
+ source,
65
+ column: entry.column,
66
+ rows: displayCount(rows),
67
+ distinctKeys: displayCount(distinctKeys),
68
+ message: `Relationship "${entry.relationship}" is declared ${entry.kind}, but "${source}.${entry.column}" ` +
69
+ `has ${rows} rows for ${distinctKeys} distinct keys. Joins pick an arbitrary matching row; ` +
70
+ 'make the key unique or declare the relationship as hasMany.',
71
+ };
72
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Dataset segments (RFC 0015): named, author-defined row filters that a query
3
+ * selects by name.
4
+ *
5
+ * A segment compares the dataset's own dimensions with literals. It may use a
6
+ * dimension that is not exposed as a filter: the filter allow-list governs
7
+ * what a caller may construct, not what an author may declare. It may not
8
+ * traverse a relationship or constrain the tenant column, which only trusted
9
+ * runtime context scopes.
10
+ */
11
+ import type { AnyDatasetInstance, DimensionDefinition, MetricFilter, SegmentDefinition } from '../types.js';
12
+ interface SegmentOwner {
13
+ readonly tenantKey?: string;
14
+ readonly dimensions: Readonly<Record<string, DimensionDefinition>>;
15
+ }
16
+ /** Throws when a declared segment is malformed; returns a frozen copy otherwise. */
17
+ export declare function normalizeSegments(datasetName: string, owner: SegmentOwner, segments: Record<string, SegmentDefinition> | undefined): Record<string, SegmentDefinition>;
18
+ /**
19
+ * Publishing emits deployment contract 2, which cannot carry segments
20
+ * (RFC 0015), so a dataset that declares them is refused by name.
21
+ */
22
+ export declare function assertNoPublishedSegments(ds: Pick<AnyDatasetInstance, 'name' | 'segments'>): void;
23
+ /** Validation errors for a query's `segments` selection. */
24
+ export declare function segmentSelectionErrors(ds: AnyDatasetInstance, names: unknown): string[];
25
+ /**
26
+ * The conditions a selection applies, as filters over dimension names. Order
27
+ * follows the selection, then each segment's authored filters.
28
+ */
29
+ export declare function segmentFilters(ds: AnyDatasetInstance, names: readonly string[] | undefined): MetricFilter[];
30
+ export {};
31
+ //# sourceMappingURL=segments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segments.d.ts","sourceRoot":"","sources":["../../src/utils/segments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAM5G,UAAU,YAAY;IACpB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC;CACpE;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,SAAS,GACtD,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAkCnC;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,MAAM,GAAG,UAAU,CAAC,GAAG,IAAI,CAQjG;AAED,4DAA4D;AAC5D,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,EAAE,CAiBvF;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,YAAY,EAAE,CAE3G"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Dataset segments (RFC 0015): named, author-defined row filters that a query
3
+ * selects by name.
4
+ *
5
+ * A segment compares the dataset's own dimensions with literals. It may use a
6
+ * dimension that is not exposed as a filter: the filter allow-list governs
7
+ * what a caller may construct, not what an author may declare. It may not
8
+ * traverse a relationship or constrain the tenant column, which only trusted
9
+ * runtime context scopes.
10
+ */
11
+ import { SEMANTIC_FILTER_OPERATORS } from '../constants.js';
12
+ import { validateFilterValue } from '../validation.js';
13
+ const SEGMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
14
+ const OPERATORS = new Set(SEMANTIC_FILTER_OPERATORS);
15
+ /** Throws when a declared segment is malformed; returns a frozen copy otherwise. */
16
+ export function normalizeSegments(datasetName, owner, segments) {
17
+ const fail = (name, message) => {
18
+ throw new Error(`Invalid segment "${name}" on dataset "${datasetName}": ${message}`);
19
+ };
20
+ const result = [];
21
+ for (const [name, segment] of Object.entries(segments ?? {})) {
22
+ if (!SEGMENT_NAME.test(name))
23
+ fail(name, 'names must be identifiers (letters, digits, underscores).');
24
+ if (!Array.isArray(segment?.filters) || segment.filters.length === 0) {
25
+ fail(name, 'filters must be a non-empty array.');
26
+ }
27
+ for (const filter of segment.filters) {
28
+ const dimension = Object.hasOwn(owner.dimensions, filter.field) ? owner.dimensions[filter.field] : undefined;
29
+ if (!dimension) {
30
+ fail(name, `"${filter.field}" is not a dimension of this dataset; segments cannot use relationships.`);
31
+ }
32
+ const column = dimension.column ?? filter.field;
33
+ if (owner.tenantKey !== undefined && (column === owner.tenantKey || filter.field === owner.tenantKey)) {
34
+ fail(name, `"${filter.field}" is the tenant column, which only runtime tenant scoping may constrain.`);
35
+ }
36
+ if (!OPERATORS.has(filter.operator))
37
+ fail(name, `unsupported operator "${String(filter.operator)}".`);
38
+ const valueError = validateFilterValue(filter, dimension.fieldType);
39
+ if (valueError)
40
+ fail(name, valueError);
41
+ }
42
+ result.push([name, Object.freeze({
43
+ filters: Object.freeze(segment.filters.map(filter => Object.freeze({
44
+ ...filter,
45
+ value: Array.isArray(filter.value) ? Object.freeze([...filter.value]) : filter.value,
46
+ }))),
47
+ ...(segment.label !== undefined ? { label: segment.label } : {}),
48
+ ...(segment.description !== undefined ? { description: segment.description } : {}),
49
+ })]);
50
+ }
51
+ // Object.fromEntries preserves names such as "__proto__" as own properties.
52
+ return Object.freeze(Object.fromEntries(result));
53
+ }
54
+ /**
55
+ * Publishing emits deployment contract 2, which cannot carry segments
56
+ * (RFC 0015), so a dataset that declares them is refused by name.
57
+ */
58
+ export function assertNoPublishedSegments(ds) {
59
+ const names = Object.keys(ds.segments ?? {});
60
+ if (names.length > 0) {
61
+ throw new Error(`Dataset "${ds.name}" declares segments (${names.join(', ')}), which cannot be published yet: ` +
62
+ 'segments need deployment contract 3 (RFC 0015). Use them for local queries until publishing emits contract 3.');
63
+ }
64
+ }
65
+ /** Validation errors for a query's `segments` selection. */
66
+ export function segmentSelectionErrors(ds, names) {
67
+ if (names === undefined)
68
+ return [];
69
+ if (!Array.isArray(names))
70
+ return ['segments must be an array of segment names.'];
71
+ const errors = [];
72
+ const seen = new Set();
73
+ const available = Object.keys(ds.segments ?? {});
74
+ for (const name of names) {
75
+ if (typeof name !== 'string' || !Object.hasOwn(ds.segments ?? {}, name)) {
76
+ errors.push(`Unknown segment "${String(name)}" on dataset "${ds.name}". Available: ${available.join(', ') || '(none)'}`);
77
+ }
78
+ else if (seen.has(name)) {
79
+ errors.push(`Segment "${name}" is selected more than once.`);
80
+ }
81
+ seen.add(String(name));
82
+ }
83
+ return errors;
84
+ }
85
+ /**
86
+ * The conditions a selection applies, as filters over dimension names. Order
87
+ * follows the selection, then each segment's authored filters.
88
+ */
89
+ export function segmentFilters(ds, names) {
90
+ return (names ?? []).flatMap(name => ds.segments?.[name]?.filters ?? []);
91
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"semantic-metadata-validation.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-metadata-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACtB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AAoBrB,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,gBAAgB,GACzB,IAAI,CA8BN;AAED,wBAAgB,4BAA4B,CAC1C,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EACvD,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,EAC1D,cAAc,SAAS,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAE7D,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,CAAC,WAAW,EAAE,SAAS,EAAE,cAAc,CAAC,GAC5D,IAAI,CAuDN"}
1
+ {"version":3,"file":"semantic-metadata-validation.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-metadata-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACtB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AAsBrB,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,gBAAgB,GACzB,IAAI,CA8BN;AAED,wBAAgB,4BAA4B,CAC1C,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,EACvD,SAAS,SAAS,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,EAC1D,cAAc,SAAS,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAE7D,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,aAAa,CAAC,WAAW,EAAE,SAAS,EAAE,cAAc,CAAC,GAC5D,IAAI,CA4EN"}
@@ -1,7 +1,8 @@
1
+ import { SUPPORTED_TIME_GRAINS } from '../constants.js';
1
2
  const MAX_METADATA_ITEMS = 100;
2
3
  const MAX_METADATA_TEXT_BYTES = 4_096;
3
4
  const SENSITIVITIES = new Set(['public', 'internal', 'confidential', 'restricted']);
4
- const TIME_GRAINS = new Set(['day', 'week', 'month', 'quarter', 'year']);
5
+ const TIME_GRAINS = new Set(SUPPORTED_TIME_GRAINS);
5
6
  const textEncoder = new TextEncoder();
6
7
  function fail(datasetName, location, message) {
7
8
  throw new Error(`Invalid dataset "${datasetName}": ${location} ${message}`);
@@ -33,7 +34,7 @@ export function validateSemanticMetadata(datasetName, location, metadata) {
33
34
  validateText(datasetName, `${location}.${key}[${index}]`, value);
34
35
  }
35
36
  }
36
- for (const key of ['format', 'unit', 'timezone']) {
37
+ for (const key of ['format', 'unit']) {
37
38
  const value = metadata[key];
38
39
  if (value !== undefined)
39
40
  validateText(datasetName, `${location}.${key}`, value);
@@ -90,6 +91,26 @@ export function validateDatasetAgentMetadata(datasetName, config) {
90
91
  if (dimensions.length === 0 && config.defaults.timeGrain === undefined) {
91
92
  fail(datasetName, 'defaults', 'must define dimensions or timeGrain.');
92
93
  }
94
+ if (config.defaults.timeGrain !== undefined && config.timeGrains !== undefined
95
+ && !config.timeGrains.includes(config.defaults.timeGrain)) {
96
+ fail(datasetName, 'defaults.timeGrain', 'is not one of the dataset timeGrains.');
97
+ }
98
+ }
99
+ if (config.timeGrains !== undefined) {
100
+ if (!Array.isArray(config.timeGrains) || config.timeGrains.length === 0) {
101
+ fail(datasetName, 'timeGrains', 'must be a non-empty array.');
102
+ }
103
+ if (config.timeKey === undefined) {
104
+ fail(datasetName, 'timeGrains', 'requires the dataset to define timeKey.');
105
+ }
106
+ if (new Set(config.timeGrains).size !== config.timeGrains.length) {
107
+ fail(datasetName, 'timeGrains', 'must not contain duplicates.');
108
+ }
109
+ for (const grain of config.timeGrains) {
110
+ if (!TIME_GRAINS.has(grain)) {
111
+ fail(datasetName, 'timeGrains', `contains unsupported time grain "${String(grain)}".`);
112
+ }
113
+ }
93
114
  }
94
115
  for (const [name, dimension] of Object.entries(config.dimensions)) {
95
116
  validateSemanticMetadata(datasetName, `dimensions.${name}`, dimension);
@@ -1 +1 @@
1
- {"version":3,"file":"semantic-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,0FAA0F;AAC1F,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,CAUrF"}
1
+ {"version":3,"file":"semantic-metadata.d.ts","sourceRoot":"","sources":["../../src/utils/semantic-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,0FAA0F;AAC1F,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,CAYrF"}
@@ -1,13 +1,15 @@
1
1
  import { uniqueSorted } from './canonical-json.js';
2
2
  /** Copy agent metadata without carrying unrelated config fields into public contracts. */
3
3
  export function snapshotSemanticMetadata(metadata) {
4
+ if ('timezone' in metadata) {
5
+ throw new Error('Set timezone on createDatasetClient or the query, not semantic metadata.');
6
+ }
4
7
  return {
5
8
  ...(metadata.examples !== undefined ? { examples: uniqueSorted(metadata.examples) } : {}),
6
9
  ...(metadata.synonyms !== undefined ? { synonyms: uniqueSorted(metadata.synonyms) } : {}),
7
10
  ...(metadata.format !== undefined ? { format: metadata.format } : {}),
8
11
  ...(metadata.unit !== undefined ? { unit: metadata.unit } : {}),
9
12
  ...(metadata.currency !== undefined ? { currency: metadata.currency } : {}),
10
- ...(metadata.timezone !== undefined ? { timezone: metadata.timezone } : {}),
11
13
  ...(metadata.sensitivity !== undefined ? { sensitivity: metadata.sensitivity } : {}),
12
14
  };
13
15
  }
@@ -0,0 +1,4 @@
1
+ import type { MeasureTimeInterval, ShiftMeasureDefinition } from '../types.js';
2
+ export type ShiftMeasureOptions = Pick<ShiftMeasureDefinition, 'label' | 'description' | 'examples' | 'synonyms' | 'format' | 'unit' | 'currency' | 'sensitivity'>;
3
+ export declare function createShiftMeasure<const TMeasureName extends string>(baseMeasure: TMeasureName, interval: MeasureTimeInterval, options?: ShiftMeasureOptions): ShiftMeasureDefinition<TMeasureName>;
4
+ //# sourceMappingURL=shift-measure-definition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shift-measure-definition.d.ts","sourceRoot":"","sources":["../../src/utils/shift-measure-definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAG/E,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,sBAAsB,EAC3D,OAAO,GAAG,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,aAAa,CAAC,CAAC;AAEtG,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,YAAY,SAAS,MAAM,EAClE,WAAW,EAAE,YAAY,EACzB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,CAAC,EAAE,mBAAmB,GAC5B,sBAAsB,CAAC,YAAY,CAAC,CAOtC"}
@@ -0,0 +1,9 @@
1
+ import { snapshotSemanticMetadata } from './semantic-metadata.js';
2
+ export function createShiftMeasure(baseMeasure, interval, options) {
3
+ return {
4
+ __type: 'shift_measure_definition', measure: baseMeasure, interval: { ...interval },
5
+ ...snapshotSemanticMetadata(options ?? {}),
6
+ ...(options?.label === undefined ? {} : { label: options.label }),
7
+ ...(options?.description === undefined ? {} : { description: options.description }),
8
+ };
9
+ }
@@ -0,0 +1,18 @@
1
+ import type { ShiftMeasureDefinition, TimeGrain } from '../types.js';
2
+ import type { TimeMeasureAxis } from './time-measure-axis.js';
3
+ /** Preserve the selected portion of a bucket and both authored endpoint operators. */
4
+ export declare function shiftRangeSql(shift: ShiftMeasureDefinition, axis: TimeMeasureAxis, period?: string): {
5
+ lower: string;
6
+ upper: string;
7
+ lowerInclusive: string;
8
+ upperInclusive: string;
9
+ };
10
+ export declare function shiftRangePredicateSql(time: string, range: ReturnType<typeof shiftRangeSql>): string;
11
+ /** Check the shifted scan independently of whether it contains source rows. */
12
+ export declare function shiftCalendarGuardSql(shift: ShiftMeasureDefinition, grain: TimeGrain, axisGuard: string): string;
13
+ /** Map output buckets to their earlier half-open ranges, retaining calendar arithmetic. */
14
+ export declare function shiftBucketCtes(shift: ShiftMeasureDefinition, axis: TimeMeasureAxis, index: number): {
15
+ ctes: string[];
16
+ rowsSql: string;
17
+ };
18
+ //# sourceMappingURL=shift-measure-sql.d.ts.map
@@ -0,0 +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,SAAS,EAAE,MAAM,aAAa,CAAC;AAIrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,MAAM;;;;;EAQlG;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,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAOhH;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,CAAA;CAAE,CAkBxI"}
@@ -0,0 +1,46 @@
1
+ import { GRAIN_FUNCTIONS } from '../constants.js';
2
+ import { addTimeSql, subtractTimeSql } from './time-arithmetic-sql.js';
3
+ import { calendarBucketGuardSql } from './time-axis-calendar-guard.js';
4
+ /** Preserve the selected portion of a bucket and both authored endpoint operators. */
5
+ export function shiftRangeSql(shift, axis, period) {
6
+ const end = period ? addTimeSql(period, 1, axis.grain) : undefined;
7
+ return {
8
+ lower: subtractTimeSql(period ? `greatest(${period}, _hq_lower)` : '_hq_lower', shift.interval.amount, shift.interval.unit),
9
+ upper: subtractTimeSql(end ? `least(${end}, _hq_upper)` : '_hq_upper', shift.interval.amount, shift.interval.unit),
10
+ lowerInclusive: period ? `(${period} > _hq_lower OR ${Number(axis.lowerInclusive)})` : String(Number(axis.lowerInclusive)),
11
+ upperInclusive: end ? `(_hq_upper < ${end} AND ${Number(axis.upperInclusive)})` : String(Number(axis.upperInclusive)),
12
+ };
13
+ }
14
+ export function shiftRangePredicateSql(time, range) {
15
+ return `(${time} > ${range.lower} OR (${range.lowerInclusive} AND ${time} = ${range.lower}))`
16
+ + ` AND (${time} < ${range.upper} OR (${range.upperInclusive} AND ${time} = ${range.upper}))`;
17
+ }
18
+ /** Check the shifted scan independently of whether it contains source rows. */
19
+ export function shiftCalendarGuardSql(shift, grain, axisGuard) {
20
+ const lower = subtractTimeSql('_hq_first', shift.interval.amount, shift.interval.unit);
21
+ const upper = subtractTimeSql(addTimeSql('_hq_last', 1, grain), shift.interval.amount, shift.interval.unit);
22
+ const first = `${GRAIN_FUNCTIONS[grain]}(${lower})`;
23
+ const last = `${GRAIN_FUNCTIONS[grain]}(${upper})`;
24
+ const count = `dateDiff('${grain}', ${first}, ${last}) + ${axisGuard}`;
25
+ return calendarBucketGuardSql(first, count, grain);
26
+ }
27
+ /** Map output buckets to their earlier half-open ranges, retaining calendar arithmetic. */
28
+ export function shiftBucketCtes(shift, axis, index) {
29
+ const { grain } = axis;
30
+ const ranges = `_hq_shift_ranges${index}`;
31
+ const keys = `_hq_shift_keys${index}`;
32
+ const range = shiftRangeSql(shift, axis, '_hq_period');
33
+ const bucket = `${GRAIN_FUNCTIONS[grain]}(_hq_shift_lower)`;
34
+ const upperBucket = `${GRAIN_FUNCTIONS[grain]}(_hq_shift_upper)`;
35
+ const count = `greatest(0, dateDiff('${grain}', ${bucket}, ${upperBucket}) + if(_hq_shift_upper > ${upperBucket} OR _hq_shift_upper_inclusive, 1, 0))`;
36
+ return {
37
+ ctes: [
38
+ `${ranges} AS (SELECT _hq_period, ${range.lower} AS _hq_shift_lower, ${range.upper} AS _hq_shift_upper, ${range.lowerInclusive} AS _hq_shift_lower_inclusive, ${range.upperInclusive} AS _hq_shift_upper_inclusive FROM _hq_series CROSS JOIN _hq_bounds)`,
39
+ // A DST transition can map a one-hour output bucket to zero or two
40
+ // source hours. Enumerate source bucket keys and keep exact endpoints.
41
+ // The equality join avoids a source-rows × output-series Cartesian product.
42
+ `${keys} AS (SELECT *, ${addTimeSql(bucket, `arrayJoin(range(toUInt64(${count})))`, grain)} AS _hq_source_period FROM ${ranges})`,
43
+ ],
44
+ rowsSql: `SELECT r.*, b._hq_period FROM _hq_scanned AS r INNER JOIN ${keys} AS b ON ${GRAIN_FUNCTIONS[grain]}(r._hq_time) = b._hq_source_period WHERE ${shiftRangePredicateSql('r._hq_time', { lower: 'b._hq_shift_lower', upper: 'b._hq_shift_upper', lowerInclusive: 'b._hq_shift_lower_inclusive', upperInclusive: 'b._hq_shift_upper_inclusive' })}`,
45
+ };
46
+ }
@@ -0,0 +1,3 @@
1
+ import type { DatasetMeasureDefinition } from '../types.js';
2
+ export declare function validateShiftMeasures(datasetName: string, timeKey: string | undefined, measures: Record<string, DatasetMeasureDefinition>): void;
3
+ //# sourceMappingURL=shift-measure-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shift-measure-validation.d.ts","sourceRoot":"","sources":["../../src/utils/shift-measure-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAK5D,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,GAAG,IAAI,CAYhJ"}
@@ -0,0 +1,20 @@
1
+ import { isSafeSQLIdentifier } from '../sql-utils.js';
2
+ import { isSupportedTimeGrain } from '../constants.js';
3
+ import { getBaseMeasure, isShiftMeasure } from './dataset-measures.js';
4
+ export function validateShiftMeasures(datasetName, timeKey, measures) {
5
+ for (const [name, definition] of Object.entries(measures)) {
6
+ if (!isShiftMeasure(definition))
7
+ continue;
8
+ const prefix = `Invalid dataset "${datasetName}": shift measure "${name}"`;
9
+ if (!isSafeSQLIdentifier(name))
10
+ throw new Error(`${prefix} name is not a safe identifier.`);
11
+ if (!timeKey)
12
+ throw new Error(`${prefix} requires a timeKey.`);
13
+ if (!getBaseMeasure(measures, definition.measure))
14
+ throw new Error(`${prefix} must wrap a base measure; "${String(definition.measure)}" is not one.`);
15
+ const interval = definition.interval;
16
+ if (!interval || !Number.isSafeInteger(interval.amount) || interval.amount <= 0 || !isSupportedTimeGrain(interval.unit)) {
17
+ throw new Error(`${prefix} interval needs a positive safe integer and supported unit.`);
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,10 @@
1
+ import type { RelationshipBuilderContext } from './relationship-builder-plan.js';
2
+ /**
3
+ * Raw SQL expressions on the base dataset are emitted verbatim, so their column
4
+ * references are not table-qualified. When relationship joins are active a bare
5
+ * `price` in such an expression is ambiguous if the joined table also has a
6
+ * `price` column. Until the builder rewrites identifiers inside expressions,
7
+ * reject the combination rather than emit ambiguous SQL.
8
+ */
9
+ export declare function assertNoRawSqlUnderJoins(kind: 'dimension' | 'measure', name: string, sql: string, joinCtx?: RelationshipBuilderContext): void;
10
+ //# sourceMappingURL=sql-under-joins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql-under-joins.d.ts","sourceRoot":"","sources":["../../src/utils/sql-under-joins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAEjF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,0BAA0B,GACnC,IAAI,CASN"}