feat(tesseract): default value filters for views (CORE-357)#10892
feat(tesseract): default value filters for views (CORE-357)#10892waralexrom wants to merge 4 commits into
Conversation
Adds a view-only top-level `filters` field accepting entries with `member`, `operator`, `values`, and optional `unless`. Wires the new member/values/unless paths into the JS and YAML transpilers, and rejects `filters` on regular cubes via Joi's unknown-key validation.
Wires the view-only `filters` field (added to the Joi schema in a703f81) end-to-end: - CubeEvaluator.prepareViewFilters resolves `member`/`unless` against the view's includedMembers (short, real-cube and view-prefixed paths all normalize to the real cube path) and stringifies `values` to match the existing FilterItem.values contract. - New ViewFilterDefinition bridge in cubesqlplanner (static-only, no trait fields) plus an optional `filters()` getter on CubeDefinition. - MockViewFilterDefinition and `filters` field on MockCubeDefinition for Rust-side test fixtures. - backend-native bridge_registry registration with fixture, expected field set and round-trip coverage for the new struct.
…ompiler Default-value filters declared on a view (CORE-357) are now materialized into the query during `QueryPropertiesCompiler::compile_filters`: - Active views are detected from compiled MemberSymbols (`compiled_path().cube_name()` for every dimension / time-dim / measure / filter-member). Each view's `filters()` becomes a candidate. - A candidate is dispatched through the existing `FilterCompiler::add_item` so default filters share the same operator/typing path as explicit ones, and get routed into the right bucket (dimension / measure / time-dim). - `unless` is filter-only: a member referenced only by projection does not release the default, since changing what columns the user selects must not silently change the row set. Only an explicit filter on the same member overrides the default. JS evaluator (CubeEvaluator.prepareViewFilters) resolves `member` / `unless` to view-scoped paths (`<view>.<member>`) so they line up with `MemberSymbol::full_name` on the Rust side. Test coverage: - Unit (cube_evaluator-level): default filter applies when view is active; projection alone does not trigger `unless`; explicit filter on the unless-member releases the default; filter applies when unless-member is absent; filter applies even when member is in dimensions if `unless` is not declared. - Postgres integration: virtual `type: switch` currency dim plus a `case`-switch measure on `country`; covers union-collapse via default filter, projection-not-triggering-unless, explicit-filter-overrides, default-filter-with-switch-measure, default-filter-when-unless-member- is-absent. The Postgres seed (`view_default_filters_tables.sql`) deliberately ships without a `currency` column — currency is purely virtual; `country` drives the switch-case measure.
New file scaffolded on calendars.test.ts: a single inline YAML model with both a real string dimension (`country`) and a virtual `type: switch` dimension (`currency`), each exposed through two views — `_unconditional` and `_with_unless`. Tests run through the full JS pipeline (Joi schema, YAML transpiler, CubeEvaluator.prepareViewFilters, BaseQuery, Tesseract planner) against testcontainers Postgres, gated on `nativeSqlPlanner`. Six cases — three per filter flavour: default applies without `unless`, projection alone does not release the default, an explicit filter on the unless-member overrides the default. YamlCompiler fix in the same commit: `filters.[N].member` (string) and `filters.[N].unless` (array) are now wrapped as f-string literals, like `values`. They are member references in the view's own namespace, not Python expressions — and the view's `includedMembers` are not resolvable at transpile time, so the previous default path treated names like `country` as undefined identifiers and blew up at runtime with `country is not defined`.
|
Claude finished @waralexrom's task in 3m 32s —— View job Code Review: feat(tesseract): default value filters for views (CORE-357)
Overall AssessmentThis is a well-structured feature addition with clean layer separation — schema validation (Joi), reference resolution (CubeEvaluator), bridge definition (Rust What Looks Good
Issues Found1. Segments not included in view activation (filed as inline comment)
2. Missing The method relies on Minor Observations
Test Coverage Summary
Solid work overall. The segments gap is the one functional concern worth addressing before merge. |
|
Claude finished @waralexrom's task in 2m 59s —— View job Code Review: feat(tesseract): default value filters for views (CORE-357)
This is a well-structured PR that adds view-level default value filters across the full stack (schema validation → JS evaluator → Rust bridge → Tesseract planner). The implementation is clean, the design decisions are well-reasoned, and the test coverage is thorough. Below are my findings. Architecture & DesignThe overall design is sound:
The Issues & Suggestions1.
|
| Layer | Tests | Verdict |
|---|---|---|
| Schema validation (Joi) | 6 tests: valid filter, valid with unless, set operator, missing values, missing member, invalid operator, non-view cube rejection | Thorough |
| JS evaluator (CubeEvaluator) | 5 tests: reference resolution (short/real-path/view-path), member-not-included error, unless-not-included error, fully-qualified non-included path error, mixed-type value coercion | Thorough |
| Rust unit tests | 5 tests: default applies, unless + projection keeps filter, unless + explicit filter releases, unless member absent, no-unless + projection | Thorough |
| Rust integration (snapshot) | 5 tests with Postgres execution: switch union collapse, unless projection, explicit override, unless member untouched, switch-case measure | Thorough |
| E2E Postgres | 6 tests: real dimension + switch dimension, each with unconditional / unless+projection / unless+explicit filter | Thorough |
| Bridge coverage | Fixture + fields meta for viewFilterDefinition added |
Complete |
Edge cases covered:
- Null values in filter values array (coerced correctly)
- Mixed types (numbers, booleans, strings, null) in values
set/notSetoperators without values- Members referenced by short name, cube-qualified path, and view-qualified path
- Members not included in the view (error)
- Unless references not included in the view (error)
One scenario not explicitly tested: Multiple default filters from the same view where one filter's unless member is also another filter's member. The current code handles this correctly (unless only checks explicit user filters, not other defaults), but an explicit test would document this behavior.
Security
No security concerns. The feature:
- Only operates on server-side schema definitions, not user input
- Values are coerced to strings matching the existing
FilterItem.valuescontract - Member references are validated against
includedMembersbefore reaching the planner - No SQL injection risk — filters go through the same
FilterCompilerpath as regular filters
Summary
This is a high-quality PR. The implementation is well-layered, the design decisions (especially filter-only unless) are clearly documented with inline comments explaining the why, and the test coverage is comprehensive across all layers. The code is clean and follows existing patterns in both the TypeScript and Rust codebases.
Verdict: Looks good to merge. The issues noted above are low-severity observations rather than blockers.
| for sym in dimensions | ||
| .iter() | ||
| .chain(time_dimensions) | ||
| .chain(measures) | ||
| .chain(filter_members.iter()) |
There was a problem hiding this comment.
Bug (minor): Segments are excluded from the view activation scan. If a query touches a view only through a segment (e.g., segments: [orders_view.some_segment] with no dimensions/measures from that view), the view's default filters won't fire.
segments is compiled at line 50 but never passed into compile_filters → apply_view_default_filters. Consider whether segments should also participate in view activation. The fix would be to thread segments through compile_filters and add it to this chain — segment FilterItems contain MemberSymbols accessible via all_member_evaluators() just like the other buckets.
This may be an intentional omission if segments-only queries are rare, but it's worth documenting either way.
| private prepareViewFilters(cube: any, errorReporter: ErrorReporter) { | ||
| if (!cube.filters) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Nit (defensive): prepareViewFilters relies on !cube.filters to skip regular cubes. The Joi schema already prevents filters from appearing on non-view cubes, so this works in practice. But since prepareViewFilters accesses cube.includedMembers (which is undefined on regular cubes), an explicit isView guard here would make the contract clearer and more robust against future schema changes:
| private prepareViewFilters(cube: any, errorReporter: ErrorReporter) { | |
| if (!cube.filters) { | |
| return; | |
| } | |
| private prepareViewFilters(cube: any, errorReporter: ErrorReporter) { | |
| if (!cube.isView || !cube.filters) { | |
| return; | |
| } |
| const isViewFilterMember = /^filters\.\d+\.member$/.test(fullPath); | ||
| const isViewFilterUnless = /^filters\.\d+\.unless$/.test(fullPath); | ||
| if (typeof obj === 'string' && ['sql', 'sqlTable'].includes(propertyPath[propertyPath.length - 1])) { | ||
| return this.parsePythonIntoArrowFunction(`f"${this.escapeDoubleQuotes(obj)}"`, cubeName, obj, errorsReport); | ||
| } else if (typeof obj === 'string' && isViewFilterMember) { | ||
| return this.parsePythonIntoArrowFunction(`f"${this.escapeDoubleQuotes(obj)}"`, cubeName, obj, errorsReport); |
There was a problem hiding this comment.
Nit: The isViewFilterMember branch (line 199–200) duplicates the sql/sqlTable branch above it — both produce parsePythonIntoArrowFunction(f"...", ...). Consider merging them:
} else if (typeof obj === 'string' && (
['sql', 'sqlTable'].includes(propertyPath[propertyPath.length - 1]) ||
isViewFilterMember
)) {
return this.parsePythonIntoArrowFunction(`f"${this.escapeDoubleQuotes(obj)}"`, cubeName, obj, errorsReport);
}Minor readability win — not blocking.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #10892 +/- ##
========================================
Coverage 78.86% 78.87%
========================================
Files 470 470
Lines 92289 92478 +189
Branches 3435 3448 +13
========================================
+ Hits 72788 72944 +156
- Misses 18999 19030 +31
- Partials 502 504 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Summary
Adds support for view-level default value filters (CORE-357). A view can now declare:
Whenever the view is touched by a query, the default filter is added; an explicit user filter on the same member can release it via
unless. The motivating customer case is aswitchdimension where the planner otherwise rolls up across all switch values.