Skip to content

feat(tesseract): Multi-stage filter directives#10827

Open
waralexrom wants to merge 5 commits into
masterfrom
tesseract-set-of-multi-stage-directives
Open

feat(tesseract): Multi-stage filter directives#10827
waralexrom wants to merge 5 commits into
masterfrom
tesseract-set-of-multi-stage-directives

Conversation

@waralexrom
Copy link
Copy Markdown
Member

Summary

Adds a filter: directive on multi-stage measures and dimensions that lets schema authors override the inherited filter context at the leaf state of a CTE. Supports mode: relative|fixed, exclude, keepOnly, and include (with arbitrary AND/OR groups of query-style predicates).

@waralexrom waralexrom requested a review from a team as a code owner May 6, 2026 12:34
@github-actions github-actions Bot added rust Pull requests that update Rust code javascript Pull requests that update Javascript code labels May 6, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 6, 2026

Claude finished @waralexrom's task in 3m 49s —— View job


Review of PR #10827: Multi-stage filter directives

  • Gather context and understand the full diff
  • Review schema compiler changes (JS/TS)
  • Review Rust bridge and planner changes
  • Review test coverage
  • Post comprehensive review

Overall Assessment

This is a well-structured feature addition that introduces filter: directives on multi-stage measures and dimensions. The architecture is clean — the JS schema compiler handles validation and reference resolution, the Rust bridge carries the data across, and the planner applies the filter logic at CTE leaf state. The consolidation of scattered multi-stage fields (reduce_by, add_group_by, group_by, time_shift, and now filter) into a single MultiStageProperties struct is a nice refactoring win.

Code Quality

Strengths:

  • The MultiStageProperties consolidation in Rust reduces field sprawl in MeasureSymbol (4 fields → 1) and DimensionSymbol (2 → 1), making both symbols easier to reason about
  • Idiomatic Rust API improvements: return types changed from &Option<Vec<...>> to Option<&Vec<...>>, enabling .cloned() instead of .clone() at call sites
  • The Weak<QueryTools> back-reference in Compiler correctly avoids an Rc cycle, with a clear comment explaining the invariant
  • Filter tree operations (tree_ops.rs) are properly extracted as reusable functions, replacing inline methods on MultiStageAppliedState

Issues flagged (see inline comments):

  1. exclude/keepOnly mutual exclusivity missing in Joi schema — The Rust planner enforces this, but the JS validator doesn't, leading to late confusing errors. Fix: add .nand('exclude', 'keepOnly') to MultiStageFilter
  2. MultiStageFilterIncludeItem = any — Should have a structural type matching the Joi schema shape
  3. member: Joi.func() in include schema unreachable — No transpiler pattern exists for filter.include[n].member, so the function alternative can never be satisfied

Security

No security concerns. The include predicates flow through the existing FilterCompiler which already handles member path resolution and SQL generation safely. No user input reaches SQL without going through the established filter pipeline.

Performance

No performance concerns. The filter directive is applied once per multi-stage node during planning (not per-row). The tree_ops functions do a single recursive pass over the filter tree. The Weak reference upgrade in Compiler::query_tools() is O(1).

Testing

Test coverage is thorough across all layers:

Layer Tests Coverage
JS Joi validation 7 cases (valid + invalid shapes) cube-validator.test.ts
JS YAML compilation 1 end-to-end YAML → evaluator test yaml-schema.test.ts
JS integration (Postgres) 3 tests (include, exclude, chain) multi-stage-filter.test.ts
Rust tree_ops unit 10 tests (exclude, keep_only, has_filter) tests/filter/tree_ops.rs
Rust symbol construction 6 tests (measure + dimension filter resolution) tests/measure_symbol.rs
Rust integration 8 snapshot tests covering all directive variants tests/integration/multi_stage/filter_directive.rs
Rust mock/YAML bridge 4 tests (serialization, qualification) mock_*_definition.rs

Good coverage of edge cases: empty groups bubbling up, nested empty groups, keepOnly with empty list, mutual exclusivity enforcement, and the mode: fixed vs relative chain divergence.

Documentation

The TODO comments documenting known mode: fixed interaction gaps (rolling window, switch-case pruning) are excellent — they explain the mechanism, what breaks, and why. The MultiStageProperties and MultiStageFilter struct comments clearly describe the data flow.

No user-facing documentation changes are included, which makes sense if this is behind the Tesseract flag (CUBESQL_SQL_PUSH_DOWN=true).

Summary

Clean feature with good test coverage and thoughtful architecture. The main actionable item is adding .nand('exclude', 'keepOnly') to the Joi schema so users get validation errors early instead of cryptic Rust planner errors. The other inline comments are minor improvements.

Comment on lines +859 to +867
const MultiStageFilter = Joi.object().keys({
mode: Joi.string().valid('relative', 'fixed'),
exclude: Joi.func(),
keepOnly: Joi.func(),
include: Joi.array().items(
MultiStageIncludeMemberFilterSchema,
MultiStageIncludeConditionSchema
),
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue (medium): exclude and keepOnly mutual exclusivity not enforced at validation time.

The Rust side (build_filter in multi_stage.rs:170) correctly rejects schemas that specify both exclude and keepOnly, but this Joi schema allows both through. This means users will get a late, less-helpful error from the Rust planner instead of a clear schema validation error.

Consider adding .nand('exclude', 'keepOnly') here:

Suggested change
const MultiStageFilter = Joi.object().keys({
mode: Joi.string().valid('relative', 'fixed'),
exclude: Joi.func(),
keepOnly: Joi.func(),
include: Joi.array().items(
MultiStageIncludeMemberFilterSchema,
MultiStageIncludeConditionSchema
),
});
const MultiStageFilter = Joi.object().keys({
mode: Joi.string().valid('relative', 'fixed'),
exclude: Joi.func(),
keepOnly: Joi.func(),
include: Joi.array().items(
MultiStageIncludeMemberFilterSchema,
MultiStageIncludeConditionSchema
),
}).nand('exclude', 'keepOnly');

interval?: string;
type?: 'next' | 'prior';
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: any type defeats type safety.

MultiStageFilterIncludeItem is typed as any, but you have a well-defined shape for it in the Joi schema (member, operator, values, and nested and/or groups). Consider giving it a structural type:

export type MultiStageFilterIncludeItem = {
  member?: string;
  operator?: string;
  values?: string[];
  and?: MultiStageFilterIncludeItem[];
  or?: MultiStageFilterIncludeItem[];
};

This would catch shape mismatches at compile time rather than at runtime in the Rust bridge.

const MultiStageIncludeMemberFilterSchema = Joi.object().keys({
member: Joi.alternatives(Joi.string(), Joi.func()).required(),
operator: Joi.any().valid(
'equals',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Is member: Joi.func() reachable?

The transpiler pattern (CubePropContextTranspiler.ts) only covers filter.exclude and filter.keepOnly — there's no pattern for filter.include[n].member. This means CUBE.dim references inside include items won't be transpiled, so the Joi.func() alternative for member can never be satisfied in practice for JavaScript models.

If the function form isn't intended to work here, removing it would avoid confusing schema authors who try member: CUBE.dim and get a runtime error. If it is intended, a transpiler pattern would need to be added.

Comment on lines +46 to +47
FilterItem::Segment(_) => {}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Segment items silently dropped in keep_only_members.

Both exclude_members and keep_only_members drop FilterItem::Segment entries. The old extract_filters_exclude_member also dropped segments, so this is pre-existing behavior. However, keep_only_members is new — if a user writes keepOnly: [orders.status] and the filter tree contains segments, they'll be silently removed even though the user didn't ask for that.

Not necessarily a bug (segments may not make sense in the keepOnly context), but worth a comment explaining the intent, or alternatively preserving segments unconditionally.

Comment on lines +31 to 37
/// Back-reference to the owning `QueryTools`. Set by `set_query_tools`
/// at the end of `QueryTools::try_new`, after the `Rc<QueryTools>` is
/// available. Held as `Weak` to avoid an `Rc` cycle: `QueryTools` owns
/// `Rc<RefCell<Compiler>>` strongly, so Compiler cannot also hold a
/// strong handle back without leaking.
query_tools: Weak<QueryTools>,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Positive: Good use of Weak to avoid Rc cycle.

The Weak<QueryTools> back-reference is the right pattern here — QueryTools owns Rc<RefCell<Compiler>>, so a strong Rc back would leak. The comment explaining why it's Weak is helpful for future maintainers.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

❌ Patch coverage is 75.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.86%. Comparing base (22031fb) to head (ff460c1).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
...bejs-schema-compiler/src/compiler/CubeEvaluator.ts 60.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #10827      +/-   ##
==========================================
- Coverage   78.86%   78.86%   -0.01%     
==========================================
  Files         470      470              
  Lines       92289    92297       +8     
  Branches     3435     3438       +3     
==========================================
+ Hits        72788    72794       +6     
- Misses      18999    19001       +2     
  Partials      502      502              
Flag Coverage Δ
cube-backend 58.37% <75.00%> (+<0.01%) ⬆️
cubesql 83.47% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@waralexrom waralexrom closed this May 6, 2026
@waralexrom waralexrom reopened this May 6, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 6, 2026

Claude finished @waralexrom's task in 4m 17s —— View job


Claude finished reviewing this PR —— View job


Review of PR #10827: Multi-stage filter directives (re-review after new commits)

  • Gather context and review the full diff (including new commits since last review)
  • Review schema compiler changes (JS/TS) — check if previous feedback was addressed
  • Review Rust bridge and planner changes
  • Review test coverage
  • Post comprehensive review

Previous Review Feedback Resolution

The two new commits since the initial review (4016da0 and 3a76dac) address all three actionable items from the prior review:

Issue Status Commit
.nand('exclude', 'keepOnly') missing in Joi Fixed 4016da0
MultiStageFilterIncludeItem = any Fixed — now a proper structural union type 4016da0
Joi.func() on member unreachable Fixed — removed, with clear comment explaining why 4016da0
Segments silently dropped by tree_ops Fixed — segments now handled symmetrically 3a76dac

All four were done well. The filter_directive_match_names helper that bridges expr:-prefixed segment names to bare BaseSegment::full_name() is the right solution for the segment matching problem.


Overall Assessment

This is a clean, well-architected feature addition. The code quality is high across all layers. The MultiStageProperties consolidation and idiomatic Rust API improvements are good refactoring wins that came naturally with the feature.

Code Quality

Strengths:

  • The MultiStageProperties struct consolidates 4 scattered fields on MeasureSymbol and 2 on DimensionSymbol into a single field, improving readability
  • Return type changes from &Option<Vec<...>> to Option<&Vec<...>> with .cloned() at call sites — idiomatic Rust
  • The Weak<QueryTools> back-reference in Compiler correctly avoids an Rc cycle, with clear documentation
  • tree_ops.rs extracts filter tree operations as reusable functions with clean recursive implementations
  • filter_directive_match_names at multi_stage_query_planner.rs:851 elegantly handles the expr: prefix mismatch between segment representations
  • TypeScript types now mirror the Joi schema shape precisely (MultiStageFilterIncludeItem union at CubeEvaluator.ts:70-73)

One minor observation:

  • In tree_ops.rs:60, has_filter_for_member skips FilterItem::Segment entries — this is correct because self-filter removal targets dimensions/measures, not segments. But this is easy to misread as a bug. The match arm is implicit (segments fall through to the next iteration). A brief comment would help, though the test at line 317 (has_filter_for_member_ignores_segments) documents the intent well.

Security

No concerns. The include predicates flow through FilterCompiler, which handles member path resolution and SQL generation through the established safe pipeline. No user input reaches SQL without proper escaping.

Performance

No concerns. Filter directive application happens once per multi-stage node during planning (not per-row). tree_ops functions do a single recursive pass. The Weak reference upgrade is O(1).

Testing

Test coverage is excellent across all layers:

Layer Tests File
JS Joi validation 8 cases (valid + invalid shapes + mutual exclusivity) cube-validator.test.ts
JS YAML compilation 1 end-to-end YAML → evaluator test yaml-schema.test.ts
JS integration (Postgres) 3 tests (include, exclude, chain divergence) multi-stage-filter.test.ts
Rust tree_ops unit 14 tests (exclude, keep_only, has_filter + segment handling) tests/filter/tree_ops.rs
Rust symbol construction 6 tests (measure + dimension filter resolution, mutual exclusivity) tests/measure_symbol.rs
Rust integration 10 snapshot tests covering all directive variants + segments tests/integration/multi_stage/filter_directive.rs

Edge cases covered: empty groups bubbling up, nested empty groups, keepOnly with empty list, mutual exclusivity enforcement, mode: fixed vs relative chain divergence, segment exclude/keep_only, expr: prefix bridging.

One area that could benefit from additional testing (non-blocking):

  • include with nested and/or groups — the Joi schema supports Joi.link('...') for recursive conditions, and the Rust FilterCompiler handles them, but there's no integration test for deeply nested and(or(...)) shapes. The include_or_group test covers one level of nesting.

Documentation

  • The TODO comments at multi_stage_query_planner.rs:828-845 documenting known mode: fixed interaction gaps (rolling window, switch-case pruning) are excellent
  • The MultiStageFilter struct doc comment at multi_stage.rs:30-38 clearly describes the data flow and why include_* entries are pre-split by member type
  • The transpiler comment at CubeValidator.ts:818-820 explaining why member is string-only is helpful for schema authors

Summary

This PR is in good shape. The previous review's actionable items have all been addressed, and the new segment handling is well-implemented with proper test coverage. No blocking issues remain.


@waralexrom waralexrom closed this May 6, 2026
@waralexrom waralexrom reopened this May 6, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 6, 2026

Claude finished @waralexrom's task in 3m 33s —— View job


Review of PR #10827: Multi-stage filter directives (re-review on reopen)

  • Gather context and review full diff (all 13 commits)
  • Review schema compiler changes (JS/TS)
  • Review Rust bridge and planner changes
  • Review test coverage
  • Post comprehensive review

Previous Review Feedback

All four actionable items from earlier reviews have been resolved:

Issue Status Commit
.nand('exclude', 'keepOnly') missing in Joi Fixed 4016da0
MultiStageFilterIncludeItem = any Fixed — proper union type 4016da0
Joi.func() on member unreachable Fixed — removed, with clear comment 474c3c2
Segments silently dropped by tree_ops Fixed — handled symmetrically 3a76dac

Overall Assessment

This is a clean, well-architected feature. The MultiStageProperties consolidation, idiomatic Rust API changes, and reusable tree_ops module are solid refactoring wins. No blocking issues remain.

Code Quality

Strengths:

  • MultiStageProperties consolidationMeasureSymbol drops 4 scattered fields to 1, DimensionSymbol drops 2 to 1. This is the right call; the struct is cleanly placed in symbols/common/multi_stage.rs with thorough doc comments describing the data flow (multi_stage.rs:30-38)
  • Idiomatic return typesmulti_stage() returns Option<&MultiStageProperties> instead of &Option<...>, enabling .and_then() at call sites instead of awkward .as_ref().and_then() chains
  • Weak<QueryTools> back-reference in compiler.rs:36 correctly avoids an Rc cycle with clear documentation of the invariant
  • filter_directive_match_names at multi_stage_query_planner.rs:851 elegantly bridges the expr: prefix mismatch between MemberExpression::full_name() and BaseSegment::full_name()
  • TypeScript types now mirror Joi preciselyMultiStageFilterIncludeItem (CubeEvaluator.ts:40-43) is a proper discriminated union instead of any
  • Transpiler pattern at CubePropContextTranspiler.ts:22 covers both camelCase and snake_case variants for filter.exclude / filter.keepOnly
  • Joi validation comment at CubeValidator.ts:814-820 clearly explains why member is string-only (no transpiler pattern for filter.include[*].member)

Minor observations (non-blocking):

  1. has_filter_for_member skips segments (tree_ops.rs:73) — This is correct because self-filter removal targets dimensions/measures, not segments. The test at line 317 documents the intent. The empty match arm is easy to misread; a one-line comment like // segments are never self-filters would help future readers.

  2. apply_to_deps clones include_* rather than transforming (multi_stage.rs:123-126) — The comment explains this is correct because FilterItems hold resolved member references that aren't affected by dependency chain transforms. Just noting the design decision is well-justified.

  3. _cube_name unused in build_filter (multi_stage.rs:160) — The leading underscore suppresses the lint, which is fine. If it's intended for future use, a brief TODO would signal that; otherwise it could simply be removed from the signature.

Security

No concerns. The include predicates flow through FilterCompiler, which handles member path resolution and SQL generation through the established pipeline. No user input reaches SQL without proper member name resolution and operator validation. The Joi schema validates operator names against a known allowlist (CubeValidator.ts:823-848).

Performance

No concerns. Filter directive application happens once per multi-stage node during planning. tree_ops functions do a single recursive pass over the filter tree. The Weak reference upgrade is O(1).

Testing

Excellent coverage across all layers:

Layer Tests File
JS Joi validation 8 cases (valid + invalid + mutual exclusivity) cube-validator.test.ts:389-622
JS YAML compilation 1 end-to-end YAML → evaluator test yaml-schema.test.ts:2060-2109
JS integration (Postgres) 3 tests (include, exclude, chain divergence) multi-stage-filter.test.ts
Rust tree_ops unit 14 tests (exclude, keep_only, has_filter, segments) tests/filter/tree_ops.rs
Rust symbol construction 6 tests (filter resolution, default mode, mutual exclusivity) tests/measure_symbol.rs:475-587
Rust integration 10 snapshot tests covering all directive variants tests/integration/multi_stage/filter_directive.rs

Edge cases covered: empty groups bubbling up, nested empty groups, keepOnly with empty list, mutual exclusivity enforcement, mode: fixed vs relative chain divergence, segment exclude/keep_only, expr: prefix bridging.

One area that could benefit from additional testing (non-blocking): The include with deeply nested and(or(and(...))) groups — the Joi schema supports Joi.link('...') for recursive conditions, but there's no integration test for multi-level nesting beyond one or group.

Documentation

  • TODO comments at multi_stage_query_planner.rs:827-845 documenting mode: fixed interaction gaps with rolling windows and switch-case pruning are excellent — they explain the mechanism, what breaks, and why
  • MultiStageFilter struct doc comment at multi_stage.rs:30-38 clearly describes the data flow
  • Comment at CubeValidator.ts:814-820 explains why member is string-only for schema authors

Summary

This PR is well-executed with no blocking issues. All prior review feedback has been addressed. The code is clean, well-tested, and the architecture makes the right trade-offs. The MultiStageProperties consolidation and tree_ops extraction improve the codebase beyond just the new feature.


waralexrom and others added 4 commits May 13, 2026 12:00
- Forbid `exclude` and `keepOnly` together at validation time (`.nand`),
  matching the Rust planner's mutual-exclusivity check — schema authors
  now get an early Joi error instead of a late planner error.
- Drop `Joi.func()` from `filter.include[*].member`: the transpiler does
  not cover that path, so the function alternative is unreachable.
- Replace `MultiStageFilterIncludeItem = any` with a structural union of
  member predicate / and-group / or-group, mirroring the Joi shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… values

CubePropContextTranspiler does not cover filter.include[*].values, so a
function form there would never receive CUBE/SECURITY_CONTEXT arguments and
would fail at runtime — same reason Joi.func() was already removed from
filter.include[*].member. Restrict the Joi schema and the TS type to plain
arrays.

Also reorder MultiStage* type declarations above DimensionDefinition so they
are defined before use (no-use-before-define), and drop a stray blank line
left over from the rebase in yaml-schema.test.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…yPlanner::try_new

Move root_state construction out of plan_queries into a fallible
constructor backed by a private build_root_state static. Drops the
RefCell<Option<…>> + lazy-init + expect pattern in favor of an
Rc<QueryProperties> field set once at construction time. plan_queries
reuses self.root_state instead of rebuilding the same value locally.

Also annotate the MultiStageFilter::include_time_dimension bucket and
FilterCompiler::add_item with the pending time-dimension classification
work: the bucket is structurally symmetric to QueryProperties but stays
empty until FilterCompiler routes time-dim filters separately from
plain dimensions.

Drive-by: cargo fmt --all picks up a stale import order in
tests/filter/tree_ops.rs left over from the feature commit; bundled in
to keep the pre-commit hook green.
@waralexrom waralexrom force-pushed the tesseract-set-of-multi-stage-directives branch from 474c3c2 to 3244476 Compare May 13, 2026 15:26
@waralexrom waralexrom closed this May 13, 2026
@waralexrom waralexrom reopened this May 13, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 13, 2026

Claude finished @waralexrom's task in 2m 53s —— View job


Review of PR #10827: Multi-stage filter directives (re-review on reopen)

  • Gather context and review full diff (all 4 commits, 47 files)
  • Review schema compiler changes (JS/TS)
  • Review Rust bridge and planner changes
  • Review test coverage
  • Post comprehensive review

Previous Review Feedback

All actionable items from earlier reviews have been addressed:

Issue Status Commit
.nand('exclude', 'keepOnly') missing in Joi Fixed 281f696
MultiStageFilterIncludeItem = any Fixed — proper discriminated union type 281f696
Joi.func() on member unreachable Fixed — removed, with clear comment explaining why d129c3d
Segments silently dropped by tree_ops Fixed — handled symmetrically in both exclude_members and keep_only_members c09397f

Overall Assessment

This is a clean, well-architected feature addition. The MultiStageProperties consolidation, idiomatic Rust API improvements, and reusable tree_ops module are solid refactoring wins. No blocking issues.

Code Quality

Strengths:

  • MultiStageProperties consolidation (symbols/common/multi_stage.rs) — MeasureSymbol drops 4 scattered fields (is_multi_stage, time_shift, reduce_by, add_group_by, group_by) to one multi_stage: Option<MultiStageProperties>. DimensionSymbol drops 2 fields similarly. Clean single-responsibility struct with good doc comments (lines 30-38).

  • is_reference detection simplified correctly (measure_symbol.rs:703-711) — The old condition checked time_shifts.is_none() && reduce_by.is_none() && add_group_by.is_none() && group_by.is_none() in addition to !is_multi_stage. Since those fields now only exist inside MultiStageProperties, the !is_multi_stage check is sufficient and the simplification is sound.

  • Idiomatic return types — Accessors like time_shift(), reduce_by(), add_group_by(), group_by() now return Option<&T> instead of &Option<T>, enabling .cloned() at call sites instead of .clone(). This is a nice API improvement.

  • Weak<QueryTools> back-reference (compiler.rs:28-37) correctly avoids an Rc cycle with clear documentation of the invariant. The set_query_tools call in QueryTools::try_new after Rc materialization is the right pattern.

  • filter_directive_match_names (multi_stage_query_planner.rs:873-880) elegantly bridges the expr: prefix mismatch between MemberExpression::full_name() and BaseSegment::full_name().

  • TypeScript types now mirror Joi preciselyMultiStageFilterIncludeItem (CubeEvaluator.ts:40-43) is a proper discriminated union.

  • Transpiler patterns (CubePropContextTranspiler.ts:22) correctly cover both camelCase and snake_case variants for filter.exclude / filter.keepOnly, with a clear comment at CubeValidator.ts:814-820 explaining why member in filter.include is string-only.

Minor observations (non-blocking):

  1. has_filter_for_member skips segments (tree_ops.rs:73) — Correct because self-filter removal targets dimensions/measures, not segments. The test at tree_ops.rs:317 (has_filter_for_member_ignores_segments) documents the intent well. The empty FilterItem::Segment(_) => {} match arm is easy to misread as accidental.

  2. _cube_name unused in build_filter (multi_stage.rs:164) — Leading underscore suppresses the lint. If intended for future use (e.g., qualifying short member names in include), a brief TODO would signal that; otherwise the parameter could be removed.

  3. evaluateMultiStageReferences parameter type (CubeEvaluator.ts:515) — The method signature uses MeasureDefinition but is also called with dimensions. It works because the types are structurally compatible, but MeasureDefinition | DimensionDefinition would be more precise.

Security

No concerns. The include predicates flow through FilterCompiler, which handles member path resolution and SQL generation through the established safe pipeline. The Joi schema validates operator names against a known allowlist (CubeValidator.ts:823-848). No user input reaches SQL without proper member name resolution and operator validation.

Performance

No concerns. Filter directive application happens once per multi-stage node during planning (not per-row). tree_ops functions do a single recursive pass over the filter tree. The Weak reference upgrade is O(1). The root_state is built once in MultiStageQueryPlanner::try_new and shared via Rc.

Testing

Excellent coverage across all layers:

Layer Tests File
JS Joi validation 8 cases (valid + invalid + mutual exclusivity) cube-validator.test.ts:389-622
JS YAML compilation 1 end-to-end YAML → evaluator test yaml-schema.test.ts
JS integration (Postgres) 3 tests (include, exclude, chain divergence) multi-stage-filter.test.ts
Rust tree_ops unit 14 tests (exclude, keep_only, has_filter, segments) tests/filter/tree_ops.rs
Rust symbol construction 6 tests (filter resolution, default mode, mutual exclusivity) tests/measure_symbol.rs:475-587
Rust integration 10 snapshot tests covering all directive variants tests/integration/multi_stage/filter_directive.rs

Edge cases covered: empty groups bubbling up, nested empty groups, keepOnly with empty list, mutual exclusivity enforcement, mode: fixed vs relative chain divergence, segment exclude/keep_only, expr: prefix bridging.

One area that could benefit from additional testing (non-blocking): The include with deeply nested and(or(and(...))) groups — the Joi schema supports Joi.link('...') for recursive conditions, but there's no integration test for multi-level nesting beyond one or group.

Documentation

  • TODO comments at multi_stage_query_planner.rs:849-870 documenting known mode: fixed interaction gaps with rolling windows and switch-case pruning are excellent — they explain the mechanism, what breaks, and why
  • MultiStageFilter struct doc comment at multi_stage.rs:30-38 clearly describes the data flow
  • FilterCompiler TODO at compiler.rs:30-33 documents the known gap where time-dimension filters aren't classified separately
  • Comment at CubeValidator.ts:814-820 explains why member is string-only for schema authors

Summary

This PR is well-executed with no blocking issues. All prior review feedback has been addressed. The code is clean, well-tested, and the architecture makes the right trade-offs. The MultiStageProperties consolidation and tree_ops extraction improve the codebase beyond just the new feature.


…erage

Adding `filter()` to MeasureDefinition / DimensionDefinition surfaces in
the bridge harness as `invoke dispatcher out of sync` — extend both
invoke fns to record the new field and add `filter` to the expected
field lists for these two bridges.

Register `multiStageFilter` itself as a first-class bridge in the
harness: enable static-meta on `MultiStageFilterReferencesStatic`
(derive NativeBridgeStatic + `with_static_meta` on the trait attribute),
add the registry entry in bridge_test_exports.rs with an empty
invoke fn (no trait methods — all data is serde-static, matching the
filterGroup pattern), pin the expected field set
['exclude', 'include', 'keep_only', 'mode'], and ship a fully-populated
fixture so serde is exercised end-to-end rather than just `try_new`.
@waralexrom waralexrom requested a review from a team as a code owner May 13, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update Javascript code rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants