@hypequery/datasets 0.26.0 → 0.28.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 (78) hide show
  1. package/dist/cache/protocol-cache-keys.d.ts.map +1 -1
  2. package/dist/cache/protocol-cache-keys.js +6 -1
  3. package/dist/client-contract/build-from-deployment.d.ts +26 -0
  4. package/dist/client-contract/build-from-deployment.d.ts.map +1 -0
  5. package/dist/client-contract/build-from-deployment.js +76 -0
  6. package/dist/client-contract/deployment-annotations.d.ts +6 -0
  7. package/dist/client-contract/deployment-annotations.d.ts.map +1 -0
  8. package/dist/client-contract/deployment-annotations.js +32 -0
  9. package/dist/client-contract/deployment-fields.d.ts +19 -0
  10. package/dist/client-contract/deployment-fields.d.ts.map +1 -0
  11. package/dist/client-contract/deployment-fields.js +103 -0
  12. package/dist/client-contract/deployment-profile.d.ts +22 -0
  13. package/dist/client-contract/deployment-profile.d.ts.map +1 -0
  14. package/dist/client-contract/deployment-profile.js +29 -0
  15. package/dist/client-contract/query-schema.d.ts.map +1 -1
  16. package/dist/client-contract/query-schema.js +9 -3
  17. package/dist/constants.d.ts +13 -2
  18. package/dist/constants.d.ts.map +1 -1
  19. package/dist/constants.js +18 -2
  20. package/dist/in-memory-backend.d.ts.map +1 -1
  21. package/dist/in-memory-backend.js +4 -0
  22. package/dist/index.d.ts +6 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +3 -2
  25. package/dist/query-helpers.d.ts +6 -0
  26. package/dist/query-helpers.d.ts.map +1 -1
  27. package/dist/query-helpers.js +10 -0
  28. package/dist/query-planner.js +1 -1
  29. package/dist/relationship-check.d.ts.map +1 -1
  30. package/dist/relationship-check.js +11 -6
  31. package/dist/semantic-query-schema.d.ts.map +1 -1
  32. package/dist/semantic-query-schema.js +17 -8
  33. package/dist/sql-portability.d.ts.map +1 -1
  34. package/dist/sql-portability.js +3 -1
  35. package/dist/types.d.ts +19 -5
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/utils/clickhouse-dataset-sql-dialect.d.ts.map +1 -1
  38. package/dist/utils/clickhouse-dataset-sql-dialect.js +5 -0
  39. package/dist/utils/dataset-definition-validation.js +2 -2
  40. package/dist/utils/dataset-filter-types.d.ts +7 -3
  41. package/dist/utils/dataset-filter-types.d.ts.map +1 -1
  42. package/dist/utils/dataset-query-validation.d.ts.map +1 -1
  43. package/dist/utils/dataset-query-validation.js +4 -2
  44. package/dist/utils/enforced-constraints.d.ts.map +1 -1
  45. package/dist/utils/enforced-constraints.js +2 -1
  46. package/dist/utils/filter-definition.d.ts +2 -2
  47. package/dist/utils/filter-definition.d.ts.map +1 -1
  48. package/dist/utils/filter-operator-policy.d.ts +11 -0
  49. package/dist/utils/filter-operator-policy.d.ts.map +1 -0
  50. package/dist/utils/filter-operator-policy.js +13 -0
  51. package/dist/utils/filtered-aggregation-sql.d.ts.map +1 -1
  52. package/dist/utils/filtered-aggregation-sql.js +4 -0
  53. package/dist/utils/json-pointer-segments.d.ts +7 -0
  54. package/dist/utils/json-pointer-segments.d.ts.map +1 -0
  55. package/dist/utils/json-pointer-segments.js +9 -0
  56. package/dist/utils/metric-query-validation.d.ts.map +1 -1
  57. package/dist/utils/metric-query-validation.js +4 -2
  58. package/dist/utils/protocol-metric-expressions.d.ts.map +1 -1
  59. package/dist/utils/protocol-metric-expressions.js +4 -0
  60. package/dist/utils/protocol-rehydrate-filters.d.ts +1 -1
  61. package/dist/utils/protocol-rehydrate-filters.d.ts.map +1 -1
  62. package/dist/utils/protocol-rehydrate-filters.js +7 -1
  63. package/dist/utils/query-timezone.d.ts.map +1 -1
  64. package/dist/utils/query-timezone.js +3 -1
  65. package/dist/utils/relationship-builder-plan.d.ts +7 -2
  66. package/dist/utils/relationship-builder-plan.d.ts.map +1 -1
  67. package/dist/utils/relationship-builder-plan.js +14 -4
  68. package/dist/utils/relationship-validation.d.ts.map +1 -1
  69. package/dist/utils/relationship-validation.js +2 -1
  70. package/dist/utils/scoped-dataset-source.js +1 -1
  71. package/dist/utils/segments.d.ts.map +1 -1
  72. package/dist/utils/segments.js +6 -4
  73. package/dist/utils/tenant-runtime.d.ts +7 -0
  74. package/dist/utils/tenant-runtime.d.ts.map +1 -1
  75. package/dist/utils/tenant-runtime.js +16 -0
  76. package/dist/validation.d.ts.map +1 -1
  77. package/dist/validation.js +9 -0
  78. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"protocol-rehydrate-filters.d.ts","sourceRoot":"","sources":["../../src/utils/protocol-rehydrate-filters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,kBAAkB,EAC9B,WAAW,EAAE,MAAM,KAAK,GACvB,YAAY,CASd"}
1
+ {"version":3,"file":"protocol-rehydrate-filters.d.ts","sourceRoot":"","sources":["../../src/utils/protocol-rehydrate-filters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,kBAAkB,EAC9B,WAAW,EAAE,MAAM,KAAK,GACvB,YAAY,CAcd"}
@@ -1,12 +1,18 @@
1
1
  /**
2
2
  * A measure's fixed filters cross the contract boundary as RFC 0003
3
3
  * expressions. `protocol-adapter.ts` only ever emits `reference <op> literal`
4
- * for them, so this inverts exactly that shape and refuses anything else rather
4
+ * (or `reference isNull` / `isNotNull`) for them, so this inverts exactly that shape and refuses anything else rather
5
5
  * than partially interpreting an expression it cannot faithfully rebuild.
6
6
  */
7
7
  export function rehydrateMeasureFilter(expression, unsupported) {
8
8
  if (expression.kind !== 'comparison')
9
9
  throw unsupported();
10
+ if (!('right' in expression)) {
11
+ // A null check (RFC 0023): `reference isNull`, with no value to unwrap.
12
+ if (expression.left.kind !== 'reference')
13
+ throw unsupported();
14
+ return { field: String(expression.left.name), operator: expression.operator };
15
+ }
10
16
  const { left, right, operator } = expression;
11
17
  if (left.kind !== 'reference' || right.kind !== 'literal')
12
18
  throw unsupported();
@@ -1 +1 @@
1
- {"version":3,"file":"query-timezone.d.ts","sourceRoot":"","sources":["../../src/utils/query-timezone.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAM1F,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;AASD,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEtH;AAED,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAK9E;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,sBAAsB,EAC/B,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GACtF,MAAM,CAER;AAED,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IACjC,sFAAsF;IACtF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;IACxC,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;CACzC;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,EAC/B,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,GAAE,sBAA2B,GACnC,kBAAkB,EAAE,CAwCtB"}
1
+ {"version":3,"file":"query-timezone.d.ts","sourceRoot":"","sources":["../../src/utils/query-timezone.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAM1F,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;AASD,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEtH;AAED,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAK9E;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,sBAAsB,EAC/B,EAAE,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GACtF,MAAM,CAER;AAED,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IACjC,sFAAsF;IACtF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,CAAC;IACxC,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;CACzC;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,EAC/B,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,GAAE,sBAA2B,GACnC,kBAAkB,EAAE,CA0CtB"}
@@ -69,7 +69,9 @@ export function queryTimeFilter(dialect, ds, filter, expression, timezone, optio
69
69
  ? queryTimeSql(dialect, expression, timezone)
70
70
  : expression;
71
71
  const local = { expression: localExpression, operator: filter.operator, value: filter.value };
72
- if (fieldType !== 'timestamp' || filter.operator === 'like')
72
+ // A null check has no value to read in any zone.
73
+ if (fieldType !== 'timestamp' || filter.operator === 'like'
74
+ || filter.operator === 'isNull' || filter.operator === 'isNotNull')
73
75
  return [local];
74
76
  const values = Array.isArray(filter.value) ? filter.value : [filter.value];
75
77
  const instants = values.map(parseTimestampInstant);
@@ -52,8 +52,13 @@ type QueryLike = Pick<DatasetQuery & MetricQuery, 'dimensions' | 'filters' | 'or
52
52
  * here is skipped defensively.
53
53
  */
54
54
  export declare function buildRelationshipBuilderContext(ds: AnyDatasetInstance, query: QueryLike, context?: ExecutionContext, dialect?: DatasetSqlDialectRules): RelationshipBuilderContext | undefined;
55
- /** Qualifies a base physical column with the base source when joins are active. */
56
- export declare function qualifyBaseColumn(ctx: RelationshipBuilderContext | undefined, column: string): string;
55
+ /**
56
+ * Qualifies a base physical column with the base source. Always qualified:
57
+ * ClickHouse resolves an output alias before a column of the same name, so a
58
+ * bare `status` could read a dimension aliased `status`, and a bare `amount`
59
+ * inside another aggregate could read a measure aliased `amount`.
60
+ */
61
+ export declare function qualifyBaseColumn(ctx: RelationshipBuilderContext | undefined, column: string, source: string): string;
57
62
  /**
58
63
  * Resolves a relationship-qualified field to a null-guarded SQL expression:
59
64
  * the target column when the join matched, NULL otherwise
@@ -1 +1 @@
1
- {"version":3,"file":"relationship-builder-plan.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-builder-plan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAGxE;;;;;;;;;;;;;;;;GAgBG;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,EAAuC,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAG7F,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IAClC,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,uFAAuF;IACvF,KAAK,EAAE,OAAO,CAAC;CAChB;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;IACrD,oEAAoE;IACpE,OAAO,EAAE,sBAAsB,CAAC;CACjC;AAED,KAAK,SAAS,GAAG,IAAI,CAAC,YAAY,GAAG,WAAW,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;AAEzH;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,SAAS,EAChB,OAAO,CAAC,EAAE,gBAAgB,EAC1B,OAAO,GAAE,sBAAoD,GAC5D,0BAA0B,GAAG,SAAS,CA+CxC;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,0BAA0B,GAAG,SAAS,EAC3C,MAAM,EAAE,MAAM,GACb,MAAM,CAER;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,0BAA0B,GAC9B,MAAM,CAaR;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,0BAA0B,GAAG,SAAS,GAC1C,gBAAgB,CAoClB"}
1
+ {"version":3,"file":"relationship-builder-plan.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-builder-plan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAIxE;;;;;;;;;;;;;;;;GAgBG;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,EAAuC,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAG7F,MAAM,WAAW,mBAAmB;IAClC,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IAClC,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,4EAA4E;IAC5E,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,uFAAuF;IACvF,KAAK,EAAE,OAAO,CAAC;CAChB;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;IACrD,oEAAoE;IACpE,OAAO,EAAE,sBAAsB,CAAC;CACjC;AAED,KAAK,SAAS,GAAG,IAAI,CAAC,YAAY,GAAG,WAAW,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;AAEzH;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,SAAS,EAChB,OAAO,CAAC,EAAE,gBAAgB,EAC1B,OAAO,GAAE,sBAAoD,GAC5D,0BAA0B,GAAG,SAAS,CAmDxC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,0BAA0B,GAAG,SAAS,EAC3C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,MAAM,CAER;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,0BAA0B,GAC9B,MAAM,CAaR;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,gBAAgB,EACpB,GAAG,EAAE,0BAA0B,GAAG,SAAS,GAC1C,gBAAgB,CAoClB"}
@@ -2,6 +2,7 @@ import { relationshipKeys } from './relationship-keys.js';
2
2
  import { relationshipJoinSource } from './relationship-measure-sql.js';
3
3
  import { clickhouseDatasetSqlDialect } from './clickhouse-dataset-sql-dialect.js';
4
4
  import { resolveRelationshipReference } from './relationship-measures.js';
5
+ import { joinsWithinTenant } from './tenant-runtime.js';
5
6
  import { datasetRowScope, runtimeConstraints } from './row-scope.js';
6
7
  import { isQualifiedField, resolveQualifiedField } from './relationship-fields.js';
7
8
  /**
@@ -34,7 +35,11 @@ export function buildRelationshipBuilderContext(ds, query, context, dialect = cl
34
35
  const measureNames = (query.measures ?? [])
35
36
  .filter(name => name.startsWith(`${relationshipName}.`))
36
37
  .map(name => name.slice(relationshipName.length + 1));
37
- const keys = relationshipKeys(relationship);
38
+ const authoredKeys = relationshipKeys(relationship);
39
+ // Under a scope of several tenants, a key reused by another tenant must not match.
40
+ const keys = joinsWithinTenant(ds, target, context)
41
+ ? [...authoredKeys, { from: ds.tenantKey, to: target.tenantKey }]
42
+ : authoredKeys;
38
43
  const projected = relationshipJoinSource(target, keys.map(key => key.to), measureNames, dialect);
39
44
  joinByRelationship.set(relationshipName, {
40
45
  relationship: relationshipName,
@@ -54,9 +59,14 @@ export function buildRelationshipBuilderContext(ds, query, context, dialect = cl
54
59
  dialect,
55
60
  };
56
61
  }
57
- /** Qualifies a base physical column with the base source when joins are active. */
58
- export function qualifyBaseColumn(ctx, column) {
59
- return ctx ? `${ctx.baseSource}.${column}` : column;
62
+ /**
63
+ * Qualifies a base physical column with the base source. Always qualified:
64
+ * ClickHouse resolves an output alias before a column of the same name, so a
65
+ * bare `status` could read a dimension aliased `status`, and a bare `amount`
66
+ * inside another aggregate could read a measure aliased `amount`.
67
+ */
68
+ export function qualifyBaseColumn(ctx, column, source) {
69
+ return `${ctx ? ctx.baseSource : source}.${column}`;
60
70
  }
61
71
  /**
62
72
  * Resolves a relationship-qualified field to a null-guarded SQL expression:
@@ -1 +1 @@
1
- {"version":3,"file":"relationship-validation.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-validation.ts"],"names":[],"mappings":"AACA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACZ,MAAM,aAAa,CAAC;AAYrB,KAAK,iBAAiB,GAAG,IAAI,CAC3B,YAAY,GAAG,WAAW,EAC1B,YAAY,GAAG,SAAS,GAAG,SAAS,CACrC,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;AAEnC;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,iBAAiB,EACxB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAqBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,IAAI,CA4Bf"}
1
+ {"version":3,"file":"relationship-validation.d.ts","sourceRoot":"","sources":["../../src/utils/relationship-validation.ts"],"names":[],"mappings":"AAEA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACZ,MAAM,aAAa,CAAC;AAYrB,KAAK,iBAAiB,GAAG,IAAI,CAC3B,YAAY,GAAG,WAAW,EAC1B,YAAY,GAAG,SAAS,GAAG,SAAS,CACrC,GAAG,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;AAEnC;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,iBAAiB,EACxB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAqBpB;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,kBAAkB,EACtB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,IAAI,CA4Bf"}
@@ -1,4 +1,5 @@
1
1
  import { resolveRelationshipReference } from './relationship-measures.js';
2
+ import { filterAllowsOperator } from './filter-operator-policy.js';
2
3
  import { validateFilterValue } from '../validation.js';
3
4
  import { getRuntimeTenantPredicate, hasTenantRuntime, } from './tenant-runtime.js';
4
5
  import { runtimeConstraints } from './row-scope.js';
@@ -55,7 +56,7 @@ export function validateQualifiedFilter(ds, filter, context) {
55
56
  || definition.field !== targetDimensionName) {
56
57
  return `Filter "${filter.field}" is not exposed by target dataset "${target.name}".`;
57
58
  }
58
- if (definition.operators && !definition.operators.includes(filter.operator)) {
59
+ if (!filterAllowsOperator(definition, filter.operator)) {
59
60
  return `Filter "${filter.field}" does not allow operator "${filter.operator}".`;
60
61
  }
61
62
  return validateFilterValue(filter, targetDimension.fieldType);
@@ -21,7 +21,7 @@ export function scopedDatasetSource(options) {
21
21
  // A resolved join target is read with FINAL even when this dataset isn't.
22
22
  if (queryReadsFinal(ds, joinCtx) && qb.settings)
23
23
  qb = qb.settings(FINAL_SAFETY_SETTINGS);
24
- qb = applyRowScope(qb, datasetRowScope(ds, context), column => qualifyBaseColumn(joinCtx, column));
24
+ qb = applyRowScope(qb, datasetRowScope(ds, context), column => qualifyBaseColumn(joinCtx, column, ds.source));
25
25
  // Constraints are filters in the dataset's vocabulary, applied exactly as a
26
26
  // caller filter on the same field would be.
27
27
  for (const constraint of runtimeConstraints(context)) {
@@ -1 +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"}
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;AAK5G,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,CAqCnC;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"}
@@ -8,10 +8,9 @@
8
8
  * traverse a relationship or constrain the tenant column, which only trusted
9
9
  * runtime context scopes.
10
10
  */
11
- import { SEMANTIC_FILTER_OPERATORS } from '../constants.js';
11
+ import { isSemanticFilterOperator } from '../constants.js';
12
12
  import { validateFilterValue } from '../validation.js';
13
13
  const SEGMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
14
- const OPERATORS = new Set(SEMANTIC_FILTER_OPERATORS);
15
14
  /** Throws when a declared segment is malformed; returns a frozen copy otherwise. */
16
15
  export function normalizeSegments(datasetName, owner, segments) {
17
16
  const fail = (name, message) => {
@@ -33,7 +32,7 @@ export function normalizeSegments(datasetName, owner, segments) {
33
32
  if (owner.tenantKey !== undefined && (column === owner.tenantKey || filter.field === owner.tenantKey)) {
34
33
  fail(name, `"${filter.field}" is the tenant column, which only runtime tenant scoping may constrain.`);
35
34
  }
36
- if (!OPERATORS.has(filter.operator))
35
+ if (!isSemanticFilterOperator(filter.operator))
37
36
  fail(name, `unsupported operator "${String(filter.operator)}".`);
38
37
  const valueError = validateFilterValue(filter, dimension.fieldType);
39
38
  if (valueError)
@@ -42,7 +41,10 @@ export function normalizeSegments(datasetName, owner, segments) {
42
41
  result.push([name, Object.freeze({
43
42
  filters: Object.freeze(segment.filters.map(filter => Object.freeze({
44
43
  ...filter,
45
- value: Array.isArray(filter.value) ? Object.freeze([...filter.value]) : filter.value,
44
+ // A null check (RFC 0023) has no value to copy.
45
+ ...(filter.value === undefined ? {} : {
46
+ value: Array.isArray(filter.value) ? Object.freeze([...filter.value]) : filter.value,
47
+ }),
46
48
  }))),
47
49
  ...(segment.label !== undefined ? { label: segment.label } : {}),
48
50
  ...(segment.description !== undefined ? { description: segment.description } : {}),
@@ -9,6 +9,13 @@ export type TenantPredicate = {
9
9
  export declare function getRuntimeTenantPredicate(context?: ExecutionContext): TenantPredicate | undefined;
10
10
  export declare function getRuntimeTenantId(context?: ExecutionContext): string | undefined;
11
11
  export declare function isCrossTenantRuntime(context?: ExecutionContext): boolean;
12
+ /**
13
+ * Whether a join from `base` to `target` must also match their tenant keys.
14
+ * One tenant pins both sides to the same id already. A scope over several
15
+ * tenants, or all of them, admits every scoped tenant's target rows, so a key
16
+ * reused across tenants would otherwise match another tenant's row.
17
+ */
18
+ export declare function joinsWithinTenant(base: Pick<AnyDatasetInstance, 'tenantKey'>, target: Pick<AnyDatasetInstance, 'tenantKey'>, context?: ExecutionContext): boolean;
12
19
  export declare function hasTenantRuntime(context?: ExecutionContext): boolean;
13
20
  export declare function validateTenantRuntime(ds: AnyDatasetInstance, context?: ExecutionContext): string | undefined;
14
21
  //# sourceMappingURL=tenant-runtime.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tenant-runtime.d.ts","sourceRoot":"","sources":["../../src/utils/tenant-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAExC,wBAAgB,yBAAyB,CACvC,OAAO,CAAC,EAAE,gBAAgB,GACzB,eAAe,GAAG,SAAS,CAe7B;AAED,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAGjF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAGxE;AAED,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAEpE;AAED,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,kBAAkB,EACtB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAQpB"}
1
+ {"version":3,"file":"tenant-runtime.d.ts","sourceRoot":"","sources":["../../src/utils/tenant-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAExC,wBAAgB,yBAAyB,CACvC,OAAO,CAAC,EAAE,gBAAgB,GACzB,eAAe,GAAG,SAAS,CAe7B;AAED,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAGjF;AAED,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAGxE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,IAAI,CAAC,kBAAkB,EAAE,WAAW,CAAC,EAC3C,MAAM,EAAE,IAAI,CAAC,kBAAkB,EAAE,WAAW,CAAC,EAC7C,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAST;AAED,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAEpE;AAED,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,kBAAkB,EACtB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAQpB"}
@@ -22,6 +22,22 @@ export function isCrossTenantRuntime(context) {
22
22
  const tenant = context?.runtime?.tenant;
23
23
  return !!tenant && typeof tenant === 'object' && 'scope' in tenant && tenant.scope === 'all';
24
24
  }
25
+ /**
26
+ * Whether a join from `base` to `target` must also match their tenant keys.
27
+ * One tenant pins both sides to the same id already. A scope over several
28
+ * tenants, or all of them, admits every scoped tenant's target rows, so a key
29
+ * reused across tenants would otherwise match another tenant's row.
30
+ */
31
+ export function joinsWithinTenant(base, target, context) {
32
+ if (!base.tenantKey || !target.tenantKey) {
33
+ return false;
34
+ }
35
+ const predicate = getRuntimeTenantPredicate(context);
36
+ if (predicate) {
37
+ return predicate.operator === 'in' && predicate.value.length > 1;
38
+ }
39
+ return isCrossTenantRuntime(context);
40
+ }
25
41
  export function hasTenantRuntime(context) {
26
42
  return !!getRuntimeTenantPredicate(context) || isCrossTenantRuntime(context);
27
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI1D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAgB9E;AAED,0FAA0F;AAC1F,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAG9E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3D;AAgBD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CA6C7F"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI1D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAgB9E;AAED,0FAA0F;AAC1F,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAG9E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3D;AAgBD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAsD7F"}
@@ -101,6 +101,15 @@ export function validateFilterValue(filter, fieldType) {
101
101
  return filter.value.every(value => matchesFieldType(fieldType, value))
102
102
  ? null
103
103
  : typeError(filter, fieldType, true);
104
+ case 'isNull':
105
+ case 'isNotNull': {
106
+ // Any field type may be NULL; the check itself takes no value (RFC 0023).
107
+ // Runtime input is untyped, so read the value the types say is absent.
108
+ const value = filter.value;
109
+ return value === undefined
110
+ ? null
111
+ : `"${filter.operator}" takes no value for field "${filter.field}"; omit "value".`;
112
+ }
104
113
  default:
105
114
  return `Unsupported semantic filter operator "${String(filter.operator)}".`;
106
115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypequery/datasets",
3
- "version": "0.26.0",
3
+ "version": "0.28.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",
@@ -55,7 +55,7 @@
55
55
  "!dist/**/type-tests/**"
56
56
  ],
57
57
  "dependencies": {
58
- "@hypequery/protocol": "^0.23.0",
58
+ "@hypequery/protocol": "^0.24.2",
59
59
  "@noble/hashes": "^2.4.0",
60
60
  "zod": "^4.6.6",
61
61
  "zod-to-json-schema": "^3.25.2"
@@ -73,7 +73,7 @@
73
73
  "typescript": "^6.0.3",
74
74
  "@vitest/coverage-v8": "^5.0.5",
75
75
  "vitest": "^5.0.5",
76
- "@hypequery/protocol-conformance": "^0.16.2"
76
+ "@hypequery/protocol-conformance": "^0.17.0"
77
77
  },
78
78
  "repository": {
79
79
  "type": "git",