Skip to content

Commit b2c03e7

Browse files
authored
feat(tesseract): Segments and MemberExpressions segments support (#9336)
* fix(tesseract): Fix error on memeber expressions working on views * feat(tesseract): Segments and MemberExpressions segments support
1 parent f2183c1 commit b2c03e7

File tree

17 files changed

+400
-9
lines changed

17 files changed

+400
-9
lines changed

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ export class BaseQuery {
683683
const queryParams = {
684684
measures: this.options.measures,
685685
dimensions: this.options.dimensions,
686+
segments: this.options.segments,
686687
timeDimensions: this.options.timeDimensions,
687688
timezone: this.options.timezone,
688689
joinGraph: this.joinGraph,
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import {
2+
getEnv,
3+
} from '@cubejs-backend/shared';
4+
import { PostgresQuery } from '../../../src/adapter/PostgresQuery';
5+
import { prepareYamlCompiler } from '../../unit/PrepareCompiler';
6+
import { dbRunner } from './PostgresDBRunner';
7+
8+
describe('Member Expression', () => {
9+
jest.setTimeout(200000);
10+
11+
const { compiler, joinGraph, cubeEvaluator } = prepareYamlCompiler(`
12+
cubes:
13+
- name: customers
14+
sql: >
15+
SELECT 9 as ID, 'state1' as STATE, 'New York' as CITY
16+
UNION ALL
17+
SELECT 10 as ID, 'state2' as STATE, 'New York' as CITY
18+
UNION ALL
19+
SELECT 11 as ID, 'state3' as STATE, 'LA' as CITY
20+
21+
dimensions:
22+
- name: id
23+
sql: ID
24+
type: number
25+
primary_key: true
26+
27+
- name: state
28+
sql: STATE
29+
type: string
30+
31+
- name: city
32+
sql: CITY
33+
type: string
34+
35+
36+
measures:
37+
- name: count
38+
type: count
39+
40+
views:
41+
- name: customers_view
42+
43+
cubes:
44+
- join_path: customers
45+
includes:
46+
- count
47+
48+
- city
49+
50+
`);
51+
52+
async function runQueryTest(q, expectedResult) {
53+
/* if (!getEnv('nativeSqlPlanner')) {
54+
return;
55+
} */
56+
await compiler.compile();
57+
const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, q);
58+
59+
console.log(query.buildSqlAndParams());
60+
61+
const res = await dbRunner.testQuery(query.buildSqlAndParams());
62+
console.log(JSON.stringify(res));
63+
64+
expect(res).toEqual(
65+
expectedResult
66+
);
67+
}
68+
69+
it('member expression over views', async () => runQueryTest({
70+
measures: [
71+
{
72+
// eslint-disable-next-line no-new-func
73+
expression: new Function(
74+
'customers_view',
75+
// eslint-disable-next-line no-template-curly-in-string
76+
'return `${customers_view.count}`'
77+
),
78+
// eslint-disable-next-line no-template-curly-in-string
79+
definition: '${customers_view.count}',
80+
expressionName: 'count',
81+
cubeName: 'customers_view',
82+
},
83+
{
84+
// eslint-disable-next-line no-new-func
85+
expression: new Function(
86+
'customers_view',
87+
// eslint-disable-next-line no-template-curly-in-string
88+
'return `${customers_view.city}`'
89+
),
90+
// eslint-disable-next-line no-template-curly-in-string
91+
definition: '${customers_view.city}',
92+
expressionName: 'city',
93+
cubeName: 'customers_view',
94+
},
95+
{
96+
// eslint-disable-next-line no-new-func
97+
expression: new Function(
98+
// eslint-disable-next-line no-template-curly-in-string
99+
'return `\'NULL\'`'
100+
),
101+
// eslint-disable-next-line no-template-curly-in-string
102+
definition: 'CAST(NULL AS STRING)',
103+
expressionName: 'cubejoinfield',
104+
cubeName: 'customers_view',
105+
},
106+
],
107+
segments: [
108+
{
109+
// eslint-disable-next-line no-new-func
110+
expression: new Function(
111+
'customers_view',
112+
// eslint-disable-next-line no-template-curly-in-string
113+
'return `(${customers_view.city} = \'New York\')`'
114+
),
115+
// eslint-disable-next-line no-template-curly-in-string
116+
definition: '(${customers_view.city} = \'New York\')',
117+
expressionName: 'castomers_view_c',
118+
cubeName: 'customers_view',
119+
},
120+
121+
],
122+
allowUngroupedWithoutPrimaryKey: true,
123+
ungrouped: true,
124+
},
125+
126+
[{ count: 1, city: 'New York', cubejoinfield: 'NULL' }, { count: 1, city: 'New York', cubejoinfield: 'NULL' }]));
127+
});

packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ describe('SQL Generation', () => {
3535
}
3636
},
3737
38+
segments: {
39+
some_source: {
40+
sql: \`\${CUBE}.source = 'some'\`
41+
}
42+
},
43+
3844
measures: {
3945
visitor_count: {
4046
type: 'number',
@@ -3276,6 +3282,43 @@ SELECT 1 AS revenue, cast('2024-01-01' AS timestamp) as time UNION ALL
32763282
}]
32773283
));
32783284

3285+
it('simple join with segment', async () => runQueryTest(
3286+
{
3287+
measures: [
3288+
'visitors.visitor_revenue',
3289+
'visitors.visitor_count',
3290+
'visitor_checkins.visitor_checkins_count',
3291+
'visitors.per_visitor_revenue'
3292+
],
3293+
timeDimensions: [{
3294+
dimension: 'visitors.created_at',
3295+
granularity: 'day',
3296+
dateRange: ['2017-01-01', '2017-01-30']
3297+
}],
3298+
segments: ['visitors.some_source'],
3299+
timezone: 'America/Los_Angeles',
3300+
order: [{
3301+
id: 'visitors.created_at'
3302+
}]
3303+
},
3304+
[
3305+
{
3306+
visitors__created_at_day: '2017-01-02T00:00:00.000Z',
3307+
visitors__visitor_revenue: '100',
3308+
visitors__visitor_count: '1',
3309+
vc__visitor_checkins_count: '3',
3310+
visitors__per_visitor_revenue: '100'
3311+
},
3312+
{
3313+
visitors__created_at_day: '2017-01-04T00:00:00.000Z',
3314+
visitors__visitor_revenue: '200',
3315+
visitors__visitor_count: '1',
3316+
vc__visitor_checkins_count: '2',
3317+
visitors__per_visitor_revenue: '200'
3318+
},
3319+
]
3320+
));
3321+
32793322
// Subquery aggregation for multiplied measure (and any `keysSelect` for that matter)
32803323
// should pick up all dimensions, even through member expressions
32813324
it('multiplied sum with dimension member expressions', async () => runQueryTest(

rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_query_options.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ pub trait BaseQueryOptions {
6767
fn measures(&self) -> Result<Option<Vec<OptionsMember>>, CubeError>;
6868
#[nbridge(field, optional, vec)]
6969
fn dimensions(&self) -> Result<Option<Vec<OptionsMember>>, CubeError>;
70+
#[nbridge(field, optional, vec)]
71+
fn segments(&self) -> Result<Option<Vec<OptionsMember>>, CubeError>;
7072
#[nbridge(field)]
7173
fn cube_evaluator(&self) -> Result<Rc<dyn CubeEvaluator>, CubeError>;
7274
#[nbridge(field)]

rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/evaluator.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::cube_definition::{CubeDefinition, NativeCubeDefinition};
22
use super::dimension_definition::{DimensionDefinition, NativeDimensionDefinition};
33
use super::measure_definition::{MeasureDefinition, NativeMeasureDefinition};
44
use super::member_sql::{MemberSql, NativeMemberSql};
5+
use super::segment_definition::{NativeSegmentDefinition, SegmentDefinition};
56
use cubenativeutils::wrappers::serializer::{
67
NativeDeserialize, NativeDeserializer, NativeSerialize,
78
};
@@ -34,8 +35,10 @@ pub trait CubeEvaluator {
3435
-> Result<Rc<dyn MeasureDefinition>, CubeError>;
3536
fn dimension_by_path(
3637
&self,
37-
measure_path: String,
38+
dimension_path: String,
3839
) -> Result<Rc<dyn DimensionDefinition>, CubeError>;
40+
fn segment_by_path(&self, segment_path: String)
41+
-> Result<Rc<dyn SegmentDefinition>, CubeError>;
3942
fn cube_from_path(&self, cube_path: String) -> Result<Rc<dyn CubeDefinition>, CubeError>;
4043
fn is_measure(&self, path: Vec<String>) -> Result<bool, CubeError>;
4144
fn is_dimension(&self, path: Vec<String>) -> Result<bool, CubeError>;

rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub mod member_order_by;
2222
pub mod member_sql;
2323
pub mod options_member;
2424
pub mod security_context;
25+
pub mod segment_definition;
2526
pub mod sql_templates_render;
2627
pub mod sql_utils;
2728
pub mod struct_with_sql_member;

rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/options_member.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use cubenativeutils::wrappers::inner_types::InnerTypes;
33
use cubenativeutils::wrappers::serializer::NativeDeserialize;
44
use cubenativeutils::wrappers::NativeObjectHandle;
55
use cubenativeutils::CubeError;
6+
use std::fmt::Debug;
67
use std::rc::Rc;
78

89
pub enum OptionsMember {
@@ -23,3 +24,15 @@ impl<IT: InnerTypes> NativeDeserialize<IT> for OptionsMember {
2324
}
2425
}
2526
}
27+
28+
impl Debug for OptionsMember {
29+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30+
match self {
31+
Self::MemberName(name) => f.debug_tuple("MemberName").field(name).finish(),
32+
Self::MemberExpression(member_expression) => f
33+
.debug_tuple("MemberExpression")
34+
.field(member_expression.static_data())
35+
.finish(),
36+
}
37+
}
38+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use super::member_sql::{MemberSql, NativeMemberSql};
2+
use cubenativeutils::wrappers::serializer::{
3+
NativeDeserialize, NativeDeserializer, NativeSerialize,
4+
};
5+
use cubenativeutils::wrappers::NativeContextHolder;
6+
use cubenativeutils::wrappers::NativeObjectHandle;
7+
use cubenativeutils::CubeError;
8+
use serde::{Deserialize, Serialize};
9+
use std::any::Any;
10+
use std::rc::Rc;
11+
12+
#[derive(Serialize, Deserialize, Debug)]
13+
pub struct SegmentDefinitionStatic {
14+
#[serde(rename = "type")]
15+
pub segment_type: Option<String>,
16+
#[serde(rename = "ownedByCube")]
17+
pub owned_by_cube: Option<bool>,
18+
}
19+
20+
#[nativebridge::native_bridge(SegmentDefinitionStatic)]
21+
pub trait SegmentDefinition {
22+
#[nbridge(field)]
23+
fn sql(&self) -> Result<Rc<dyn MemberSql>, CubeError>;
24+
}

rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::planner::filter::BaseFilter;
1+
use crate::planner::filter::{BaseFilter, BaseSegment};
22
use crate::planner::sql_evaluator::MemberSymbol;
33
use crate::planner::sql_templates::PlanSqlTemplates;
44
use crate::planner::VisitorContext;
@@ -34,6 +34,7 @@ impl FilterGroup {
3434
pub enum FilterItem {
3535
Group(Rc<FilterGroup>),
3636
Item(Rc<BaseFilter>),
37+
Segment(Rc<BaseSegment>),
3738
}
3839

3940
#[derive(Clone)]
@@ -78,6 +79,10 @@ impl FilterItem {
7879
let sql = item.to_sql(context.clone(), templates)?;
7980
format!("({})", sql)
8081
}
82+
FilterItem::Segment(item) => {
83+
let sql = item.to_sql(context.clone(), templates)?;
84+
format!("({})", sql)
85+
}
8186
};
8287
Ok(res)
8388
}
@@ -96,6 +101,7 @@ impl FilterItem {
96101
}
97102
}
98103
FilterItem::Item(item) => result.push(item.member_evaluator().clone()),
104+
FilterItem::Segment(item) => result.push(item.member_evaluator().clone()),
99105
}
100106
}
101107
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use crate::cube_bridge::dimension_definition::DimensionDefinition;
2+
use crate::planner::query_tools::QueryTools;
3+
use crate::planner::sql_evaluator::{MemberExpressionSymbol, MemberSymbol, SqlCall};
4+
use crate::planner::sql_templates::PlanSqlTemplates;
5+
use crate::planner::{evaluate_with_context, BaseMember, BaseMemberHelper, VisitorContext};
6+
use cubenativeutils::CubeError;
7+
use std::rc::Rc;
8+
9+
pub struct BaseSegment {
10+
full_name: String,
11+
query_tools: Rc<QueryTools>,
12+
member_evaluator: Rc<MemberSymbol>,
13+
cube_name: String,
14+
name: String,
15+
}
16+
17+
impl PartialEq for BaseSegment {
18+
fn eq(&self, other: &Self) -> bool {
19+
self.full_name == other.full_name
20+
}
21+
}
22+
23+
impl BaseSegment {
24+
pub fn try_new(
25+
expression: Rc<SqlCall>,
26+
cube_name: String,
27+
name: String,
28+
full_name: Option<String>,
29+
query_tools: Rc<QueryTools>,
30+
) -> Result<Rc<Self>, CubeError> {
31+
let member_expression_symbol =
32+
MemberExpressionSymbol::new(cube_name.clone(), name.clone(), expression, None);
33+
let full_name = full_name.unwrap_or(member_expression_symbol.full_name());
34+
let member_evaluator = Rc::new(MemberSymbol::MemberExpression(member_expression_symbol));
35+
36+
Ok(Rc::new(Self {
37+
full_name,
38+
query_tools,
39+
member_evaluator,
40+
cube_name,
41+
name,
42+
}))
43+
}
44+
pub fn to_sql(
45+
&self,
46+
context: Rc<VisitorContext>,
47+
templates: &PlanSqlTemplates,
48+
) -> Result<String, CubeError> {
49+
evaluate_with_context(
50+
&self.member_evaluator,
51+
self.query_tools.clone(),
52+
context,
53+
templates,
54+
)
55+
}
56+
57+
pub fn full_name(&self) -> String {
58+
self.full_name.clone()
59+
}
60+
61+
pub fn member_evaluator(&self) -> Rc<MemberSymbol> {
62+
self.member_evaluator.clone()
63+
}
64+
65+
pub fn cube_name(&self) -> &String {
66+
&self.cube_name
67+
}
68+
69+
pub fn name(&self) -> &String {
70+
&self.name
71+
}
72+
}

0 commit comments

Comments
 (0)