Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/cubejs-backend-native/src/bridge_test_exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ use cubesqlplanner::cube_bridge::{
member_sql::{
FilterGroupItem, FilterParamsItem, MemberSql, NativeMemberSql, SqlTemplate, SqlTemplateArgs,
},
multi_stage_filter::{
multi_stage_filter_references_bridge_fields_meta, NativeMultiStageFilterReferences,
},
pre_aggregation_description::{
pre_aggregation_description_bridge_fields_meta, NativePreAggregationDescription,
PreAggregationDescription,
Expand Down Expand Up @@ -446,6 +449,7 @@ bridge_registry! {
"memberDefinition" => NativeMemberDefinition, member_definition_bridge_fields_meta, invoke_member_definition;
"memberExpressionDefinition" => NativeMemberExpressionDefinition, member_expression_definition_bridge_fields_meta, invoke_member_expression_definition;
"memberOrderBy" => NativeMemberOrderBy, member_order_by_bridge_fields_meta, invoke_member_order_by;
"multiStageFilter" => NativeMultiStageFilterReferences, multi_stage_filter_references_bridge_fields_meta, invoke_multi_stage_filter;
"preAggregationDescription" => NativePreAggregationDescription, pre_aggregation_description_bridge_fields_meta, invoke_pre_aggregation_description;
"preAggregationObj" => NativePreAggregationObj, pre_aggregation_obj_bridge_fields_meta, invoke_pre_aggregation_obj;
"preAggregationTimeDimension" => NativePreAggregationTimeDimension, pre_aggregation_time_dimension_bridge_fields_meta, invoke_pre_aggregation_time_dimension;
Expand Down Expand Up @@ -733,6 +737,7 @@ fn invoke_dimension_definition<IT: InnerTypes>(b: &NativeDimensionDefinition<IT>
r.record("latitude", b.latitude());
r.record("longitude", b.longitude());
r.record("time_shift", b.time_shift());
r.record("filter", b.filter());
r.record("mask_sql", b.mask_sql());
r
}
Expand All @@ -742,12 +747,23 @@ fn invoke_measure_definition<IT: InnerTypes>(b: &NativeMeasureDefinition<IT>) ->
r.record("sql", b.sql());
r.record("case", b.case());
r.record("filters", b.filters());
r.record("filter", b.filter());
r.record("drill_filters", b.drill_filters());
r.record("order_by", b.order_by());
r.record("mask_sql", b.mask_sql());
r
}

fn invoke_multi_stage_filter<IT: InnerTypes>(
_b: &NativeMultiStageFilterReferences<IT>,
) -> InvokeResult {
// MultiStageFilterReferences exposes only serde-static fields (no trait
// methods), so there is nothing to round-trip here beyond what `try_new`
// already validates. Returning an empty `InvokeResult` matches the
// pattern used by other static-only bridges (e.g. filterGroup).
InvokeResult::new()
}

fn invoke_expression_struct<IT: InnerTypes>(b: &NativeExpressionStruct<IT>) -> InvokeResult {
let mut r = InvokeResult::new();
r.record("add_filters", b.add_filters());
Expand Down
12 changes: 12 additions & 0 deletions packages/cubejs-backend-native/test/bridge/bridge-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ export const memberOrderByFixture = (): unknown => ({
dir: 'asc',
});

// MultiStageFilterReferences has no trait methods — every field is a
// serde-static on `MultiStageFilterReferencesStatic`. `exclude` and
// `keep_only` are mutually exclusive at planner level, so the fixture only
// populates one of them. All fields are optional, so a `{}` literal would
// also parse — populating real values exercises serde more usefully.
export const multiStageFilterFixture = (): unknown => ({
mode: 'relative',
excludeReferences: ['orders.status'],
include: [{ member: 'orders.amount', operator: 'gt', values: ['0'] }],
});

export const memberDefinitionFixture = (): unknown => ({
type: 'dimension',
// sql is optional
Expand Down Expand Up @@ -272,6 +283,7 @@ export const FIXTURES: Record<string, BridgeFixtureFactory> = {
memberDefinition: memberDefinitionFixture,
memberExpressionDefinition: memberExpressionDefinitionFixture,
memberOrderBy: memberOrderByFixture,
multiStageFilter: multiStageFilterFixture,
preAggregationDescription: preAggregationDescriptionFixture,
preAggregationObj: preAggregationObjFixture,
preAggregationTimeDimension: preAggregationTimeDimensionFixture,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const BRIDGES: BridgeSpec[] = [
'add_group_by_references',
'case',
'dimension_type',
'filter',
'latitude',
'longitude',
'mask_sql',
Expand Down Expand Up @@ -178,6 +179,7 @@ const BRIDGES: BridgeSpec[] = [
'add_group_by_references',
'case',
'drill_filters',
'filter',
'filters',
'group_by_references',
'mask_sql',
Expand All @@ -197,6 +199,10 @@ const BRIDGES: BridgeSpec[] = [
expected: ['cube_name', 'definition', 'expression', 'expression_name', 'name'],
},
{ name: 'memberOrderBy', expected: ['dir', 'sql'] },
{
name: 'multiStageFilter',
expected: ['exclude', 'include', 'keep_only', 'mode'],
},
{
name: 'preAggregationDescription',
expected: [
Expand Down
34 changes: 34 additions & 0 deletions packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ export type SegmentDefinition = {
multiStage?: boolean;
};

export type MultiStageFilterIncludeMember = {
member: string;
operator: string;
values?: string[];
};

export type MultiStageFilterIncludeItem =
| MultiStageFilterIncludeMember
| { and: MultiStageFilterIncludeItem[] }
| { or: MultiStageFilterIncludeItem[] };

export type MultiStageFilterDirective = {
mode?: 'relative' | 'fixed';
exclude?: (...args: Array<unknown>) => Array<ToString>;
keepOnly?: (...args: Array<unknown>) => Array<ToString>;
include?: MultiStageFilterIncludeItem[];
// Resolved sibling fields populated by `evaluateMultiStageReferences`.
// The native bridge reads these (not the function forms above).
excludeReferences?: string[];
keepOnlyReferences?: string[];
};

export type DimensionDefinition = {
type: string;
sql(): string;
Expand All @@ -43,6 +65,9 @@ export type DimensionDefinition = {
order?: 'asc' | 'desc';
key?: (...args: any[]) => ToString;
keyReference?: string;
addGroupBy?: (...args: Array<unknown>) => Array<ToString>;
addGroupByReferences?: string[];
filter?: MultiStageFilterDirective;
};

export type TimeShiftDefinition = {
Expand All @@ -66,6 +91,7 @@ export type MeasureDefinition = {
ownedByCube: boolean;
rollingWindow?: any
filters?: any
filter?: MultiStageFilterDirective;
primaryKey?: true;
drillFilters?: any;
multiStage?: boolean;
Expand Down Expand Up @@ -592,6 +618,14 @@ export class CubeEvaluator extends CubeSymbols {
: {}),
}));
}
if (member.filter) {
if (typeof member.filter.exclude === 'function') {
member.filter.excludeReferences = this.evaluateReferences(cubeName, member.filter.exclude);
}
if (typeof member.filter.keepOnly === 'function') {
member.filter.keepOnlyReferences = this.evaluateReferences(cubeName, member.filter.keepOnly);
}
}
}
}
}
Expand Down
59 changes: 59 additions & 0 deletions packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,63 @@ const timeShiftItemOptional = Joi.object({
.xor('name', 'interval')
.and('interval', 'type');

// Top-level predicate inside `filter.include`: same shape as a query-time
// filter. `member` and `values` are plain string/array only — neither
// `filter.include[*].member` nor `filter.include[*].values` is covered by
// `CubePropContextTranspiler.transpiledFieldsPatterns`, so a function form
// here would never receive the `CUBE`/`SECURITY_CONTEXT` arguments and would
// fail at runtime. Use the existing `accessPolicy.rowLevel.filters` if you
// need dynamic predicates resolved against the security context.
const MultiStageIncludeMemberFilterSchema = Joi.object().keys({
member: Joi.string().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.

'notEquals',
'contains',
'notContains',
'startsWith',
'notStartsWith',
'endsWith',
'notEndsWith',
'in',
'notIn',
'gt',
'gte',
'lt',
'lte',
'set',
'notSet',
'inDateRange',
'notInDateRange',
'onTheDate',
'beforeDate',
'beforeOrOnDate',
'afterDate',
'afterOrOnDate',
'measureFilter',
).required(),
values: Joi.when('operator', {
is: Joi.valid('set', 'notSet'),
then: Joi.array().optional(),
otherwise: Joi.array().required()
})
});

const MultiStageIncludeConditionSchema = Joi.object().keys({
or: Joi.array().items(MultiStageIncludeMemberFilterSchema, Joi.link('...').description('Multi-stage include condition')),
and: Joi.array().items(MultiStageIncludeMemberFilterSchema, Joi.link('...').description('Multi-stage include condition')),
}).xor('or', 'and');

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');

const CaseSchema = Joi.object().keys({
when: Joi.array().items(Joi.object().keys({
sql: Joi.func().required(),
Expand Down Expand Up @@ -858,6 +915,7 @@ const MeasuresSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().
groupBy: Joi.func(),
reduceBy: Joi.func(),
addGroupBy: Joi.func(),
filter: MultiStageFilter,
timeShift: Joi.alternatives().conditional(Joi.array().length(1), {
then: Joi.array().items(timeShiftItemOptional),
otherwise: Joi.array().items(timeShiftItemRequired)
Expand Down Expand Up @@ -940,6 +998,7 @@ const DimensionsSchema = Joi.object().pattern(identifierRegex, Joi.alternatives(
multiStage: Joi.boolean().valid(true),
sql: Joi.func().required(),
addGroupBy: Joi.func(),
filter: MultiStageFilter,
}),
// TODO should be valid only for calendar cubes, but this requires significant refactoring
// of all schemas. Left for the future when we'll switch to zod.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const transpiledFieldsPatterns: Array<RegExp> = [
/^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(orderBy|order_by)\.[0-9]+\.sql$/,
/^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeShift|time_shift)\.[0-9]+\.(timeDimension|time_dimension)$/,
/^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by)$/,
/^(measures|dimensions)\.[_a-zA-Z][_a-zA-Z0-9]*\.filter\.(exclude|keepOnly|keep_only)$/,
/^(measures|dimensions)\.[_a-zA-Z][_a-zA-Z0-9]*\.case\.switch$/,
/^dimensions\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by|key)$/,
/^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.indexes\.[_a-zA-Z][_a-zA-Z0-9]*\.columns$/,
Expand Down
Loading
Loading