@hypequery/datasets 0.18.0 → 0.19.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 (98) hide show
  1. package/dist/agent-catalog.d.ts +9 -0
  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 +35 -0
  7. package/dist/cache/query-signature.d.ts.map +1 -1
  8. package/dist/cache/query-signature.js +17 -0
  9. package/dist/catalog.d.ts +11 -0
  10. package/dist/catalog.d.ts.map +1 -1
  11. package/dist/catalog.js +12 -2
  12. package/dist/constants.d.ts.map +1 -1
  13. package/dist/constants.js +2 -0
  14. package/dist/contract.d.ts +5 -1
  15. package/dist/contract.d.ts.map +1 -1
  16. package/dist/contract.js +7 -0
  17. package/dist/dataset-query.d.ts.map +1 -1
  18. package/dist/dataset-query.js +6 -1
  19. package/dist/dataset.d.ts +2 -2
  20. package/dist/dataset.d.ts.map +1 -1
  21. package/dist/dataset.js +3 -0
  22. package/dist/executor.d.ts.map +1 -1
  23. package/dist/executor.js +15 -4
  24. package/dist/in-memory-backend.d.ts.map +1 -1
  25. package/dist/in-memory-backend.js +34 -7
  26. package/dist/index.d.ts +8 -6
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -2
  29. package/dist/measure.d.ts +5 -0
  30. package/dist/measure.d.ts.map +1 -1
  31. package/dist/measure.js +5 -0
  32. package/dist/portable-execution-errors.d.ts +4 -0
  33. package/dist/portable-execution-errors.d.ts.map +1 -1
  34. package/dist/portable-execution-errors.js +4 -1
  35. package/dist/portable-executor.d.ts.map +1 -1
  36. package/dist/portable-executor.js +6 -3
  37. package/dist/protocol-adapter.d.ts.map +1 -1
  38. package/dist/protocol-adapter.js +15 -7
  39. package/dist/protocol-rehydrate.d.ts +5 -1
  40. package/dist/protocol-rehydrate.d.ts.map +1 -1
  41. package/dist/protocol-rehydrate.js +20 -10
  42. package/dist/query-builder-protocol.d.ts +7 -3
  43. package/dist/query-builder-protocol.d.ts.map +1 -1
  44. package/dist/query-planner.d.ts.map +1 -1
  45. package/dist/query-planner.js +5 -0
  46. package/dist/relationship-check.d.ts +47 -0
  47. package/dist/relationship-check.d.ts.map +1 -0
  48. package/dist/relationship-check.js +59 -0
  49. package/dist/relationships.d.ts +10 -10
  50. package/dist/relationships.js +10 -10
  51. package/dist/semantic-planner.d.ts.map +1 -1
  52. package/dist/semantic-planner.js +5 -1
  53. package/dist/semantic-query-schema.d.ts.map +1 -1
  54. package/dist/semantic-query-schema.js +5 -0
  55. package/dist/types.d.ts +39 -6
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/utils/agent-catalog-projection.d.ts +6 -0
  58. package/dist/utils/agent-catalog-projection.d.ts.map +1 -1
  59. package/dist/utils/agent-catalog-projection.js +4 -0
  60. package/dist/utils/approximate-measures.d.ts +10 -0
  61. package/dist/utils/approximate-measures.d.ts.map +1 -0
  62. package/dist/utils/approximate-measures.js +15 -0
  63. package/dist/utils/dataset-contract.d.ts.map +1 -1
  64. package/dist/utils/dataset-contract.js +2 -2
  65. package/dist/utils/dataset-metric-ref.d.ts.map +1 -1
  66. package/dist/utils/dataset-metric-ref.js +5 -0
  67. package/dist/utils/dataset-query-validation.d.ts.map +1 -1
  68. package/dist/utils/dataset-query-validation.js +6 -3
  69. package/dist/utils/dataset-time-grains.d.ts +15 -0
  70. package/dist/utils/dataset-time-grains.d.ts.map +1 -0
  71. package/dist/utils/dataset-time-grains.js +17 -0
  72. package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -1
  73. package/dist/utils/filtered-aggregation-sql.js +1 -0
  74. package/dist/utils/portable-aggregations.d.ts +14 -0
  75. package/dist/utils/portable-aggregations.d.ts.map +1 -0
  76. package/dist/utils/portable-aggregations.js +15 -0
  77. package/dist/utils/portable-grains.d.ts +22 -0
  78. package/dist/utils/portable-grains.d.ts.map +1 -0
  79. package/dist/utils/portable-grains.js +35 -0
  80. package/dist/utils/portable-semantic-query.d.ts.map +1 -1
  81. package/dist/utils/portable-semantic-query.js +2 -1
  82. package/dist/utils/protocol-metric-expressions.d.ts.map +1 -1
  83. package/dist/utils/protocol-metric-expressions.js +2 -1
  84. package/dist/utils/relationship-builder-plan.d.ts +6 -3
  85. package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
  86. package/dist/utils/relationship-builder-plan.js +13 -5
  87. package/dist/utils/relationship-key-check.d.ts +43 -0
  88. package/dist/utils/relationship-key-check.d.ts.map +1 -0
  89. package/dist/utils/relationship-key-check.js +72 -0
  90. package/dist/utils/segments.d.ts +31 -0
  91. package/dist/utils/segments.d.ts.map +1 -0
  92. package/dist/utils/segments.js +91 -0
  93. package/dist/utils/semantic-metadata-validation.d.ts.map +1 -1
  94. package/dist/utils/semantic-metadata-validation.js +22 -1
  95. package/dist/utils/unsupported-contract-reasons.d.ts +32 -0
  96. package/dist/utils/unsupported-contract-reasons.d.ts.map +1 -0
  97. package/dist/utils/unsupported-contract-reasons.js +30 -0
  98. package/package.json +3 -3
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Aggregations that the published deployment contract can carry.
3
+ *
4
+ * `approxCountDistinct` runs locally, but it is an expression extension 2 and
5
+ * deployment contract 3 feature (RFC 0015). Publishing emits contract 2 until
6
+ * it can emit contract 3, so it refuses the measure with an error naming it.
7
+ */
8
+ /** Returns `aggregation` when publishing can carry it; otherwise throws naming `owner`. */
9
+ export function requirePortableAggregation(aggregation, owner) {
10
+ if (aggregation === 'approxCountDistinct') {
11
+ throw new Error(`${owner} uses approxCountDistinct, which cannot be published yet: approximate aggregations ` +
12
+ 'need deployment contract 3 (RFC 0015). Use it for local queries, or publish countDistinct instead.');
13
+ }
14
+ return aggregation;
15
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Time grains that the published deployment contract can carry.
3
+ *
4
+ * `minute` and `hour` run locally, but they are expression extension 2 and
5
+ * deployment contract 3 features (RFC 0015). Publishing emits contract 2 until
6
+ * it can emit contract 3, so a dataset that pins a sub-day grain is refused
7
+ * with an error naming what needs it, rather than failing later as an opaque
8
+ * contract validation error.
9
+ */
10
+ import type { ProtocolTimeGrain } from '@hypequery/protocol';
11
+ import type { TimeGrain } from '../types.js';
12
+ /** Grains deployment contract 2 and invocation 1 carry, in ascending order. */
13
+ export declare const PORTABLE_TIME_GRAINS: readonly ProtocolTimeGrain[];
14
+ export declare function isPortableTimeGrain(grain: TimeGrain): grain is ProtocolTimeGrain;
15
+ /** Returns `grain` when publishing can carry it; otherwise throws naming `owner`. */
16
+ export declare function requirePortableTimeGrain(grain: TimeGrain, owner: string): ProtocolTimeGrain;
17
+ /** Contract 2 cannot represent a dataset-level restriction on portable grains. */
18
+ export declare function assertPublishableTimeGrains(dataset: {
19
+ readonly name: string;
20
+ readonly timeGrains?: readonly TimeGrain[];
21
+ }): void;
22
+ //# sourceMappingURL=portable-grains.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portable-grains.d.ts","sourceRoot":"","sources":["../../src/utils/portable-grains.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,EAAE,SAAS,iBAAiB,EAE3D,CAAC;AAIH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,IAAI,iBAAiB,CAEhF;AAED,qFAAqF;AACrF,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAQ3F;AAED,kFAAkF;AAClF,wBAAgB,2BAA2B,CACzC,OAAO,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,SAAS,EAAE,CAAA;CAAE,GAC7E,IAAI,CASN"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Time grains that the published deployment contract can carry.
3
+ *
4
+ * `minute` and `hour` run locally, but they are expression extension 2 and
5
+ * deployment contract 3 features (RFC 0015). Publishing emits contract 2 until
6
+ * it can emit contract 3, so a dataset that pins a sub-day grain is refused
7
+ * with an error naming what needs it, rather than failing later as an opaque
8
+ * contract validation error.
9
+ */
10
+ /** Grains deployment contract 2 and invocation 1 carry, in ascending order. */
11
+ export const PORTABLE_TIME_GRAINS = Object.freeze([
12
+ 'day', 'week', 'month', 'quarter', 'year',
13
+ ]);
14
+ const CONTRACT_2_GRAINS = new Set(PORTABLE_TIME_GRAINS);
15
+ export function isPortableTimeGrain(grain) {
16
+ return CONTRACT_2_GRAINS.has(grain);
17
+ }
18
+ /** Returns `grain` when publishing can carry it; otherwise throws naming `owner`. */
19
+ export function requirePortableTimeGrain(grain, owner) {
20
+ if (!isPortableTimeGrain(grain)) {
21
+ throw new Error(`${owner} uses the "${grain}" time grain, which cannot be published yet: sub-day grains ` +
22
+ 'need deployment contract 3 (RFC 0015). Use it for local queries, or pin a grain of a day or longer.');
23
+ }
24
+ return grain;
25
+ }
26
+ /** Contract 2 cannot represent a dataset-level restriction on portable grains. */
27
+ export function assertPublishableTimeGrains(dataset) {
28
+ if (dataset.timeGrains === undefined)
29
+ return;
30
+ const missing = PORTABLE_TIME_GRAINS.filter(grain => !dataset.timeGrains?.includes(grain));
31
+ if (missing.length > 0) {
32
+ throw new Error(`Dataset "${dataset.name}" timeGrains excludes ${missing.join(', ')}, but deployment contract 2 ` +
33
+ 'cannot preserve dataset-level grain restrictions. Use all day-through-year grains or keep this dataset local.');
34
+ }
35
+ }
@@ -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) }
@@ -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}`);
@@ -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);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Why portable execution excluded a contract surface.
3
+ *
4
+ * Decision 0005 excludes any surface a rebuilt catalog cannot reproduce
5
+ * byte-for-byte rather than approximating it. Every exclusion names one of
6
+ * these reasons, so an operator can tell which surface a deployment tripped on
7
+ * without parsing the message, and the corpus can assert that nothing is
8
+ * refused for an unnamed reason. The wire failure stays
9
+ * `HQ_SEMANTIC_UNSUPPORTED_CAPABILITY`; the reason travels beside it.
10
+ */
11
+ export declare const UNSUPPORTED_CONTRACT_REASONS: {
12
+ /** A measure's fixed filter is not a field/operator/literal comparison. */
13
+ readonly measureFilterNotComparison: "HQ_PORTABLE_MEASURE_FILTER_NOT_COMPARISON";
14
+ /** A metric input or base metric is not an aggregate expression. */
15
+ readonly expressionNotAggregate: "HQ_PORTABLE_EXPRESSION_NOT_AGGREGATE";
16
+ /** No declared measure has the aggregation a metric expression names. */
17
+ readonly noMatchingMeasure: "HQ_PORTABLE_NO_MATCHING_MEASURE";
18
+ /** Two measures share an aggregation but emit different SQL. */
19
+ readonly ambiguousMeasureSql: "HQ_PORTABLE_AMBIGUOUS_MEASURE_SQL";
20
+ /** A derived metric arrived without the authored formula and aliases. */
21
+ readonly derivedMetricWithoutFormula: "HQ_PORTABLE_DERIVED_METRIC_WITHOUT_FORMULA";
22
+ /** A derived formula uses an operator, function, or shape with no builder. */
23
+ readonly unsupportedFormula: "HQ_PORTABLE_UNSUPPORTED_FORMULA";
24
+ /** A relationship names a dataset outside the supplied contract. */
25
+ readonly relationshipTargetMissing: "HQ_PORTABLE_RELATIONSHIP_TARGET_MISSING";
26
+ /** An invocation filter is not a field/operator/literal comparison. */
27
+ readonly queryFilterNotComparison: "HQ_PORTABLE_QUERY_FILTER_NOT_COMPARISON";
28
+ /** The invoked dataset is not part of the activated contract. */
29
+ readonly datasetNotActivated: "HQ_PORTABLE_DATASET_NOT_ACTIVATED";
30
+ };
31
+ export type UnsupportedContractReason = typeof UNSUPPORTED_CONTRACT_REASONS[keyof typeof UNSUPPORTED_CONTRACT_REASONS];
32
+ //# sourceMappingURL=unsupported-contract-reasons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsupported-contract-reasons.d.ts","sourceRoot":"","sources":["../../src/utils/unsupported-contract-reasons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,eAAO,MAAM,4BAA4B;IACvC,2EAA2E;;IAE3E,oEAAoE;;IAEpE,yEAAyE;;IAEzE,gEAAgE;;IAEhE,yEAAyE;;IAEzE,8EAA8E;;IAE9E,oEAAoE;;IAEpE,uEAAuE;;IAEvE,iEAAiE;;CAEzD,CAAC;AAEX,MAAM,MAAM,yBAAyB,GACnC,OAAO,4BAA4B,CAAC,MAAM,OAAO,4BAA4B,CAAC,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Why portable execution excluded a contract surface.
3
+ *
4
+ * Decision 0005 excludes any surface a rebuilt catalog cannot reproduce
5
+ * byte-for-byte rather than approximating it. Every exclusion names one of
6
+ * these reasons, so an operator can tell which surface a deployment tripped on
7
+ * without parsing the message, and the corpus can assert that nothing is
8
+ * refused for an unnamed reason. The wire failure stays
9
+ * `HQ_SEMANTIC_UNSUPPORTED_CAPABILITY`; the reason travels beside it.
10
+ */
11
+ export const UNSUPPORTED_CONTRACT_REASONS = {
12
+ /** A measure's fixed filter is not a field/operator/literal comparison. */
13
+ measureFilterNotComparison: 'HQ_PORTABLE_MEASURE_FILTER_NOT_COMPARISON',
14
+ /** A metric input or base metric is not an aggregate expression. */
15
+ expressionNotAggregate: 'HQ_PORTABLE_EXPRESSION_NOT_AGGREGATE',
16
+ /** No declared measure has the aggregation a metric expression names. */
17
+ noMatchingMeasure: 'HQ_PORTABLE_NO_MATCHING_MEASURE',
18
+ /** Two measures share an aggregation but emit different SQL. */
19
+ ambiguousMeasureSql: 'HQ_PORTABLE_AMBIGUOUS_MEASURE_SQL',
20
+ /** A derived metric arrived without the authored formula and aliases. */
21
+ derivedMetricWithoutFormula: 'HQ_PORTABLE_DERIVED_METRIC_WITHOUT_FORMULA',
22
+ /** A derived formula uses an operator, function, or shape with no builder. */
23
+ unsupportedFormula: 'HQ_PORTABLE_UNSUPPORTED_FORMULA',
24
+ /** A relationship names a dataset outside the supplied contract. */
25
+ relationshipTargetMissing: 'HQ_PORTABLE_RELATIONSHIP_TARGET_MISSING',
26
+ /** An invocation filter is not a field/operator/literal comparison. */
27
+ queryFilterNotComparison: 'HQ_PORTABLE_QUERY_FILTER_NOT_COMPARISON',
28
+ /** The invoked dataset is not part of the activated contract. */
29
+ datasetNotActivated: 'HQ_PORTABLE_DATASET_NOT_ACTIVATED',
30
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/datasets",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Code-first TypeScript semantic layer for ClickHouse datasets, metrics, multi-tenancy, and AI agents",
5
5
  "keywords": [
6
6
  "semantic-layer",
@@ -42,7 +42,7 @@
42
42
  "dist"
43
43
  ],
44
44
  "dependencies": {
45
- "@hypequery/protocol": "^0.15.0",
45
+ "@hypequery/protocol": "^0.16.0",
46
46
  "@noble/hashes": "^1.8.0",
47
47
  "zod": "^3.22.4",
48
48
  "zod-to-json-schema": "^3.23.5"
@@ -60,7 +60,7 @@
60
60
  "typescript": "^5.7.3",
61
61
  "@vitest/coverage-v8": "^3.2.6",
62
62
  "vitest": "^3.2.6",
63
- "@hypequery/protocol-conformance": "^0.12.0"
63
+ "@hypequery/protocol-conformance": "^0.13.0"
64
64
  },
65
65
  "repository": {
66
66
  "type": "git",