Skip to content

fix(tesseract): keep pre-aggregations for RBAC access-denied 1=0 segment#11123

Merged
waralexrom merged 3 commits into
masterfrom
tesseract-pre-agg-access-policy
Jun 19, 2026
Merged

fix(tesseract): keep pre-aggregations for RBAC access-denied 1=0 segment#11123
waralexrom merged 3 commits into
masterfrom
tesseract-pre-agg-access-policy

Conversation

@waralexrom

Copy link
Copy Markdown
Member

Summary

When a cube's access policy denies the queried members, RBAC (CompilerApi.applyRowLevelSecurity) appends a constant member-expression segment { expression: () => '1 = 0', name: 'rlsAccessDenied' }. Under the Tesseract native planner this segment was disqualifying pre-aggregation matching, so denied queries (typically against a view) were sent to the raw data source instead of the rollup. The legacy planner doesn't have this problem.

Changes

  • dimension_matcher.rs: an ad-hoc member-expression segment with no member dependencies (a constant filter pushable on top of any rollup) is treated as a full match instead of NotMatched. Named cube segments still require pre-aggregation coverage.
  • base_segment.rs: track is_member_expression to distinguish ad-hoc query-level expressions from registered named segments (both compile to a MemberExpression, only their construction differs).
  • Tests: inline DimensionMatcher test, a crate-level pre-aggregation SQL test (segment injected by hand, since the crate has no RBAC), and a schema-compiler integration test. Small test-harness helpers to inject member-expression segments.

Testing

  • cargo test in cubesqlplanner: 1016 passed / 0 failed (the new tests verified red without the fix, green with it).
  • schema-compiler pre-aggregation integration suites: 70 passed under CUBEJS_TESSERACT_* flags, 67 passed on the legacy planner, 0 failed.
  • Reproduced end to end on a real group-gated access policy before/after, confirming the rollup is selected with WHERE (1 = 0) applied on top.

A member-expression segment with no member dependencies (the rlsAccessDenied
'1 = 0' segment RBAC injects on access denial) must not disqualify pre-aggregation
matching. Adds a crate-level cubesqlplanner test (segment injected by hand) and a
schema-compiler integration test (run with and without the Tesseract flags in CI).
Both currently fail under the native planner and pass on the legacy planner.
…egment

An ad-hoc member-expression segment that references no members (the rlsAccessDenied
'1 = 0' segment RBAC injects on access denial) was disqualifying pre-aggregation
matching in the native planner, sending denied queries to the raw source instead of
the rollup. Such a constant is a filter pushable on top of any rollup, so the segment
matcher now treats it as a full match. Named cube segments still require coverage.

This matches the legacy planner, which ignores member-expression segments when
selecting pre-aggregations. Covered by an inline DimensionMatcher test.
@waralexrom waralexrom requested a review from a team as a code owner June 19, 2026 05:58
@github-actions github-actions Bot added rust Pull requests that update Rust code javascript Pull requests that update Javascript code labels Jun 19, 2026
@claude

claude Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

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


Code Review

Verdict: LGTM — 0 high, 0 medium, 2 low (nits). Click to expand.

Tasks

  • Read all changed files
  • Verify fix correctness vs. the RBAC 1 = 0 injection
  • Check test coverage
  • Look for edge cases / regressions

Correctness

The fix is well-targeted and the reasoning is sound:

  • BaseSegment::try_new already distinguishes named segments (caller passes Some(full_name)) from ad-hoc member-expression segments (caller passes None), so deriving is_member_expression = full_name.is_none() accurately reflects the two construction paths (compile_named_segment vs compile_member_expression_segment in query_properties_compiler.rs:326–378).
  • The matcher guard segment.is_member_expression() && symbol.get_dependencies().is_empty() is intentionally narrow: it only short-circuits constant ad-hoc expressions. Named segments — even trivially constant ones declared in segments: — still flow through the regular coverage check, which matches user expectation that anything they explicitly added to a cube must be represented in the rollup.
  • Behavioural symmetry with the legacy planner is preserved (the PR body notes the legacy path didn't have this issue), and the fix doesn't change SQL generation — the 1 = 0 is still applied on top of the rollup, just no longer blocking selection.

Test coverage

Three layers — unit DimensionMatcher test, crate-level pre-aggregation SQL integration test, and a schema-compiler integration test that uses the exact byte-for-byte segment shape from CompilerApi.applyRowLevelSecurity. The schema-compiler test is the most valuable: it pins the regression at the public-API surface so a future RBAC refactor that changes the segment shape will be caught here.

Nits (low priority, not blocking)

  1. base_segment.rs:44 — inferring is_member_expression from full_name.is_none() couples two concerns (display name vs. construction kind). If a future refactor ever supplies a full_name to an ad-hoc expression for nicer error messages, the flag flips silently. Consider passing the flag explicitly as a try_new parameter, or naming the field after what it actually means (has_explicit_full_name). Not blocking — both callers are in the same file and the comment makes the intent clear.

  2. dimension_matcher.rs:57–59 — small whitespace nit: the is_member_expression() accessor and full_name() lack a blank line between them while member_evaluator() and with_member_evaluator() do. cargo fmt will probably want one.

Security / performance

No concerns. The fast-path is is_empty() on dependencies — a no-op compared to the symbol walk it replaces. No new allocations on the hot path.


· tesseract-pre-agg-access-policy

@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.14%. Comparing base (cb35d10) to head (80a6253).

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11123      +/-   ##
==========================================
- Coverage   83.63%   79.14%   -4.49%     
==========================================
  Files         256      473     +217     
  Lines       79019    96411   +17392     
  Branches        0     3524    +3524     
==========================================
+ Hits        66086    76303   +10217     
- Misses      12933    19594    +6661     
- Partials        0      514     +514     
Flag Coverage Δ
cube-backend 58.75% <ø> (?)
cubesql 83.63% <ø> (-0.01%) ⬇️

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

☔ View full report in Codecov by Harness.
📢 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 merged commit a67fa3c into master Jun 19, 2026
140 of 141 checks passed
@waralexrom waralexrom deleted the tesseract-pre-agg-access-policy branch June 19, 2026 07:09
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