Skip to content

Commit 367b24a

Browse files
committed
feat(tesseract): expose view default value filters to Tesseract
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.
1 parent a83f812 commit 367b24a

13 files changed

Lines changed: 560 additions & 3 deletions

File tree

packages/cubejs-backend-native/src/bridge_test_exports.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ use cubesqlplanner::cube_bridge::{
9191
timeshift_definition::{
9292
time_shift_definition_bridge_fields_meta, NativeTimeShiftDefinition, TimeShiftDefinition,
9393
},
94+
view_filter_definition::{
95+
view_filter_definition_bridge_fields_meta, NativeViewFilterDefinition,
96+
},
9497
};
9598
use neon::prelude::*;
9699
use std::any::Any;
@@ -451,6 +454,7 @@ bridge_registry! {
451454
"sqlUtils" => NativeSqlUtils, sql_utils_bridge_fields_meta, invoke_sql_utils;
452455
"structWithSqlMember" => NativeStructWithSqlMember, struct_with_sql_member_bridge_fields_meta, invoke_struct_with_sql_member;
453456
"timeShiftDefinition" => NativeTimeShiftDefinition, time_shift_definition_bridge_fields_meta, invoke_time_shift_definition;
457+
"viewFilterDefinition" => NativeViewFilterDefinition, view_filter_definition_bridge_fields_meta, invoke_view_filter_definition;
454458
}
455459

456460
fn list_bridge_fields_inner<IT: InnerTypes>(
@@ -708,10 +712,17 @@ fn invoke_time_shift_definition<IT: InnerTypes>(b: &NativeTimeShiftDefinition<IT
708712
r
709713
}
710714

715+
fn invoke_view_filter_definition<IT: InnerTypes>(
716+
_b: &NativeViewFilterDefinition<IT>,
717+
) -> InvokeResult {
718+
InvokeResult::new()
719+
}
720+
711721
fn invoke_cube_definition<IT: InnerTypes>(b: &NativeCubeDefinition<IT>) -> InvokeResult {
712722
let mut r = InvokeResult::new();
713723
r.record("sql_table", b.sql_table());
714724
r.record("sql", b.sql());
725+
r.record("filters", b.filters());
715726
r
716727
}
717728

packages/cubejs-backend-native/test/bridge/bridge-fixtures.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,20 @@ export const preAggregationDescriptionFixture = (): unknown => ({
131131
// measure_references, dimension_references, etc — all optional getters
132132
});
133133

134+
export const viewFilterDefinitionFixture = (): unknown => ({
135+
operator: 'equals',
136+
memberReference: 'orders.currency',
137+
// Values are stringified by CubeEvaluator.prepareViewFilters before reaching
138+
// Tesseract; nulls are kept to exercise the Option<Vec<Option<String>>> shape.
139+
valuesReferences: ['USD', null],
140+
unlessReferences: ['orders.currency'],
141+
});
142+
134143
export const cubeDefinitionFixture = (): unknown => ({
135144
name: 'Orders',
136145
// sqlAlias, isView, isCalendar, joinMap optional
137146
// sql_table, sql optional getters
147+
filters: [viewFilterDefinitionFixture()],
138148
});
139149

140150
export const dimensionDefinitionFixture = (): unknown => ({
@@ -270,4 +280,5 @@ export const FIXTURES: Record<string, BridgeFixtureFactory> = {
270280
sqlUtils: sqlUtilsFixture,
271281
structWithSqlMember: structWithSqlMemberFixture,
272282
timeShiftDefinition: timeShiftDefinitionFixture,
283+
viewFilterDefinition: viewFilterDefinitionFixture,
273284
};

packages/cubejs-backend-native/test/bridge/object-bridges-coverage.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,16 @@ const BRIDGES: BridgeSpec[] = [
8787
{ name: 'caseSwitchItem', expected: ['sql', 'value'] },
8888
{
8989
name: 'cubeDefinition',
90-
expected: ['is_calendar', 'is_view', 'join_map', 'name', 'sql', 'sql_alias', 'sql_table'],
90+
expected: [
91+
'filters',
92+
'is_calendar',
93+
'is_view',
94+
'join_map',
95+
'name',
96+
'sql',
97+
'sql_alias',
98+
'sql_table',
99+
],
91100
},
92101
{
93102
name: 'cubeEvaluator',
@@ -215,6 +224,10 @@ const BRIDGES: BridgeSpec[] = [
215224
{ name: 'sqlUtils', expected: [] },
216225
{ name: 'structWithSqlMember', expected: ['sql'] },
217226
{ name: 'timeShiftDefinition', expected: ['interval', 'name', 'sql', 'timeshift_type'] },
227+
{
228+
name: 'viewFilterDefinition',
229+
expected: ['member_reference', 'operator', 'unless_references', 'values_references'],
230+
},
218231
];
219232

220233
const describeBridge = bridgeHarnessAvailable ? describe : describe.skip;

packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
PreAggregationDefinition,
1414
PreAggregationDefinitionRollup,
1515
type ToString,
16+
ViewDefaultValueFilter,
1617
ViewIncludedMember
1718
} from './CubeSymbols';
1819
import { UserError } from './UserError';
@@ -147,6 +148,7 @@ export type EvaluatedCube = {
147148
accessPolicy?: AccessPolicyDefinition[];
148149
isView?: boolean;
149150
includedMembers?: ViewIncludedMember[];
151+
filters?: ViewDefaultValueFilter[];
150152
};
151153

152154
export class CubeEvaluator extends CubeSymbols {
@@ -207,10 +209,86 @@ export class CubeEvaluator extends CubeSymbols {
207209
this.prepareFolders(cube, errorReporter);
208210

209211
this.prepareAccessPolicy(cube, errorReporter);
212+
this.prepareViewFilters(cube, errorReporter);
210213

211214
return cube;
212215
}
213216

217+
private prepareViewFilters(cube: any, errorReporter: ErrorReporter) {
218+
if (!cube.filters) {
219+
return;
220+
}
221+
222+
const included = (cube.includedMembers as ViewIncludedMember[] | undefined) || [];
223+
224+
const resolveViewMember = (memberType: string, reference: string): string | null => {
225+
let lookupName = reference;
226+
let lookupPath: string | null = null;
227+
228+
if (reference.indexOf('.') !== -1) {
229+
const parts = reference.split('.');
230+
if (parts[0] === cube.name) {
231+
// Identifier form resolved via view's own namespace, e.g. 'orders_view.currency'
232+
lookupName = parts.slice(1).join('.');
233+
} else {
234+
// Fully-qualified member path, e.g. 'orders.currency'
235+
lookupPath = reference;
236+
}
237+
}
238+
239+
const match = lookupPath
240+
? included.find((m) => m.memberPath === lookupPath)
241+
: included.find((m) => m.name === lookupName);
242+
243+
if (!match) {
244+
errorReporter.error(
245+
`Member '${reference}' used as ${memberType} in default value filter is not included in view '${cube.name}'`
246+
);
247+
return null;
248+
}
249+
return match.memberPath;
250+
};
251+
252+
for (const filter of cube.filters as ViewDefaultValueFilter[]) {
253+
const rawMember = this.evaluateReferences(cube.name, filter.member);
254+
const resolved = resolveViewMember('member', rawMember);
255+
if (resolved !== null) {
256+
filter.memberReference = resolved;
257+
}
258+
259+
if (filter.values) {
260+
const evaluated = filter.values();
261+
if (!Array.isArray(evaluated)) {
262+
errorReporter.error(
263+
`'values' in default value filter for view '${cube.name}' must evaluate to an array, got: ${typeof evaluated}`
264+
);
265+
} else {
266+
// Coerce to strings to match the FilterItem.values contract used by
267+
// regular query filters (Option<Vec<Option<String>>> on the Rust side).
268+
filter.valuesReferences = evaluated.map(
269+
(v) => (v === null || v === undefined ? null : String(v))
270+
);
271+
}
272+
}
273+
274+
if (filter.unless) {
275+
const rawUnless = this.evaluateReferences(
276+
cube.name,
277+
filter.unless,
278+
{ originalSorting: true }
279+
);
280+
const resolvedUnless: string[] = [];
281+
for (const ref of rawUnless) {
282+
const r = resolveViewMember('unless', ref);
283+
if (r !== null) {
284+
resolvedUnless.push(r);
285+
}
286+
}
287+
filter.unlessReferences = resolvedUnless;
288+
}
289+
}
290+
}
291+
214292
private allMembersOrList(cube: any, specifier: string | string[]): string[] {
215293
const types = ['measures', 'dimensions', 'segments'];
216294
if (specifier === '*') {

packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@ export type AccessPolicyDefinition = {
148148
}[]
149149
};
150150

151+
export type ViewDefaultValueFilter = {
152+
member: (...args: Array<unknown>) => ToString;
153+
memberReference?: string;
154+
operator: string;
155+
values?: (...args: Array<unknown>) => Array<unknown>;
156+
valuesReferences?: Array<unknown>;
157+
unless?: (...args: Array<unknown>) => Array<ToString>;
158+
unlessReferences?: string[];
159+
};
160+
151161
export type ViewIncludedMember = {
152162
type: string;
153163
memberPath: string;
@@ -216,6 +226,7 @@ export interface CubeDefinition {
216226
isView?: boolean;
217227
viewGroup?: string | ((...args: any[]) => any);
218228
viewGroups?: string[] | ((...args: any[]) => any);
229+
filters?: ViewDefaultValueFilter[];
219230
calendar?: boolean;
220231
isSplitView?: boolean;
221232
includedMembers?: ViewIncludedMember[];

0 commit comments

Comments
 (0)