Skip to content

Commit 8b34114

Browse files
committed
feat(tesseract): Segments and MemberExpressions segments support
1 parent b5e943f commit 8b34114

File tree

17 files changed

+295
-17
lines changed

17 files changed

+295
-17
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,

packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ cubes:
1313
- name: customers
1414
sql: >
1515
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
1620
1721
dimensions:
1822
- name: id
@@ -100,14 +104,24 @@ views:
100104
cubeName: 'customers_view',
101105
},
102106
],
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+
],
103122
allowUngroupedWithoutPrimaryKey: true,
104123
ungrouped: true,
105-
}, [
106-
107-
{ orders__date_year: '2023-01-01T00:00:00.000Z',
108-
orders__revenue: '15',
109-
orders__revenue_1_y_ago: '5',
110-
orders__cagr_1_y: '2.0000000000000000' },
111-
{ orders__date_year: '2024-01-01T00:00:00.000Z', orders__revenue: '30', orders__revenue_1_y_ago: '15', orders__cagr_1_y: '1.0000000000000000' },
112-
{ orders__date_year: '2025-01-01T00:00:00.000Z', orders__revenue: '5', orders__revenue_1_y_ago: '30', orders__cagr_1_y: '-0.83333333333333333333' }]));
124+
},
125+
126+
[{ count: 1, city: 'New York', cubejoinfield: 'NULL' }, { count: 1, city: 'New York', cubejoinfield: 'NULL' }]));
113127
});

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)