Skip to content

Commit 4ce2797

Browse files
waralexrommarianore-muttdata
authored andcommitted
feat(tesseract): Custom granularities support (cube-js#9400)
1 parent aea0d1e commit 4ce2797

File tree

26 files changed

+1369
-103
lines changed

26 files changed

+1369
-103
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
QueryAlias,
2121
getEnv,
2222
localTimestampToUtc,
23-
timeSeries as timeSeriesBase
23+
timeSeries as timeSeriesBase,
24+
timeSeriesFromCustomInterval
2425
} from '@cubejs-backend/shared';
2526

2627
import { UserError } from '../compiler/UserError';
@@ -720,11 +721,16 @@ export class BaseQuery {
720721
return this.paramAllocator.getParams();
721722
}
722723

723-
// FIXME helper for native generator, maybe should be moved entire to rust
724+
// FIXME helper for native generator, maybe should be moved entirely to rust
724725
generateTimeSeries(granularity, dateRange) {
725726
return timeSeriesBase(granularity, dateRange);
726727
}
727728

729+
// FIXME helper for native generator, maybe should be moved entirely to rust
730+
generateCustomTimeSeries(granularityInterval, dateRange, origin) {
731+
return timeSeriesFromCustomInterval(granularityInterval, dateRange, moment(origin), { timestampPrecision: 3 });
732+
}
733+
728734
get shouldReuseParams() {
729735
return false;
730736
}

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

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -640,37 +640,32 @@ export class CubeSymbols {
640640

641641
protected resolveSymbolsCallDeps(cubeName, sql) {
642642
try {
643-
return this.resolveSymbolsCallDeps2(cubeName, sql);
643+
const deps: any[] = [];
644+
this.resolveSymbolsCall(sql, (name) => {
645+
deps.push({ name });
646+
const resolvedSymbol = this.resolveSymbol(
647+
cubeName,
648+
name
649+
);
650+
if (resolvedSymbol._objectWithResolvedProperties) {
651+
return resolvedSymbol;
652+
}
653+
return '';
654+
}, {
655+
depsResolveFn: (name, parent) => {
656+
deps.push({ name, parent });
657+
return deps.length - 1;
658+
},
659+
currResolveIndexFn: () => deps.length - 1,
660+
contextSymbols: this.depsContextSymbols(),
661+
662+
});
663+
return deps;
644664
} catch (e) {
645-
console.log(e);
646665
return [];
647666
}
648667
}
649668

650-
protected resolveSymbolsCallDeps2(cubeName, sql) {
651-
const deps: any[] = [];
652-
this.resolveSymbolsCall(sql, (name) => {
653-
deps.push({ name, undefined });
654-
const resolvedSymbol = this.resolveSymbol(
655-
cubeName,
656-
name
657-
);
658-
if (resolvedSymbol._objectWithResolvedProperties) {
659-
return resolvedSymbol;
660-
}
661-
return '';
662-
}, {
663-
depsResolveFn: (name, parent) => {
664-
deps.push({ name, parent });
665-
return deps.length - 1;
666-
},
667-
currResolveIndexFn: () => deps.length - 1,
668-
contextSymbols: this.depsContextSymbols(),
669-
670-
});
671-
return deps;
672-
}
673-
674669
protected depsContextSymbols() {
675670
return Object.assign({
676671
filterParams: this.filtersProxyDep(),
@@ -719,7 +714,6 @@ export class CubeSymbols {
719714

720715
public resolveSymbol(cubeName, name) {
721716
const { sqlResolveFn, contextSymbols, collectJoinHints, depsResolveFn, currResolveIndexFn } = this.resolveSymbolsCallContext || {};
722-
723717
if (name === 'USER_CONTEXT') {
724718
throw new Error('Support for USER_CONTEXT was removed, please migrate to SECURITY_CONTEXT.');
725719
}
@@ -758,6 +752,9 @@ export class CubeSymbols {
758752
const parentIndex = currResolveIndexFn();
759753
cube = this.cubeDependenciesProxy(parentIndex, newCubeName);
760754
return cube;
755+
} else if (this.symbols[cubeName] && this.symbols[cubeName][name] && this.symbols[cubeName][name].type === 'time') {
756+
const parentIndex = currResolveIndexFn();
757+
return this.timeDimDependenciesProxy(parentIndex);
761758
}
762759
}
763760
return cube || (this.symbols[cubeName] && this.symbols[cubeName][name]);
@@ -877,6 +874,10 @@ export class CubeSymbols {
877874
}
878875
if (cube[propertyName]) {
879876
depsResolveFn(propertyName, parentIndex);
877+
if (cube[propertyName].type === 'time') {
878+
return this.timeDimDependenciesProxy(parentIndex);
879+
}
880+
880881
return '';
881882
}
882883
if (self.symbols[propertyName]) {
@@ -891,6 +892,25 @@ export class CubeSymbols {
891892
});
892893
}
893894

895+
protected timeDimDependenciesProxy(parentIndex) {
896+
const self = this;
897+
const { depsResolveFn } = self.resolveSymbolsCallContext || {};
898+
return new Proxy({}, {
899+
get: (v, propertyName) => {
900+
if (propertyName === '_objectWithResolvedProperties') {
901+
return true;
902+
}
903+
if (propertyName === 'toString') {
904+
return () => '';
905+
}
906+
if (typeof propertyName === 'string') {
907+
depsResolveFn(propertyName, parentIndex);
908+
}
909+
return undefined;
910+
}
911+
});
912+
}
913+
894914
public isCurrentCube(name) {
895915
return CURRENT_CUBE_CONSTANTS.indexOf(name) >= 0;
896916
}

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,80 @@ SELECT 1 AS revenue, cast('2024-01-01' AS timestamp) as time UNION ALL
968968
}
969969
]));
970970

971+
it('rolling window with two time dimension granularities one custom one regular', async () => runQueryTest({
972+
973+
measures: [
974+
'visitors.countRollingWeekToDate'
975+
],
976+
timeDimensions: [
977+
{
978+
dimension: 'visitors.created_at',
979+
granularity: 'three_days',
980+
dateRange: ['2017-01-01', '2017-01-10']
981+
},
982+
{
983+
dimension: 'visitors.created_at',
984+
granularity: 'day',
985+
dateRange: ['2017-01-01', '2017-01-10']
986+
}
987+
],
988+
order: [{
989+
id: 'visitors.created_at'
990+
}],
991+
timezone: 'America/Los_Angeles'
992+
}, [
993+
{
994+
visitors__count_rolling_week_to_date: null,
995+
visitors__created_at_day: '2017-01-01T00:00:00.000Z',
996+
visitors__created_at_three_days: '2017-01-01T00:00:00.000Z',
997+
},
998+
{
999+
visitors__count_rolling_week_to_date: '1',
1000+
visitors__created_at_day: '2017-01-02T00:00:00.000Z',
1001+
visitors__created_at_three_days: '2017-01-01T00:00:00.000Z',
1002+
},
1003+
{
1004+
visitors__count_rolling_week_to_date: '1',
1005+
visitors__created_at_day: '2017-01-03T00:00:00.000Z',
1006+
visitors__created_at_three_days: '2017-01-01T00:00:00.000Z',
1007+
},
1008+
{
1009+
visitors__count_rolling_week_to_date: '2',
1010+
visitors__created_at_day: '2017-01-04T00:00:00.000Z',
1011+
visitors__created_at_three_days: '2017-01-04T00:00:00.000Z',
1012+
},
1013+
{
1014+
visitors__count_rolling_week_to_date: '3',
1015+
visitors__created_at_day: '2017-01-05T00:00:00.000Z',
1016+
visitors__created_at_three_days: '2017-01-04T00:00:00.000Z',
1017+
},
1018+
{
1019+
visitors__count_rolling_week_to_date: '5',
1020+
visitors__created_at_day: '2017-01-06T00:00:00.000Z',
1021+
visitors__created_at_three_days: '2017-01-04T00:00:00.000Z',
1022+
},
1023+
{
1024+
visitors__count_rolling_week_to_date: '5',
1025+
visitors__created_at_day: '2017-01-07T00:00:00.000Z',
1026+
visitors__created_at_three_days: '2017-01-07T00:00:00.000Z',
1027+
},
1028+
{
1029+
visitors__count_rolling_week_to_date: '5',
1030+
visitors__created_at_day: '2017-01-08T00:00:00.000Z',
1031+
visitors__created_at_three_days: '2017-01-07T00:00:00.000Z',
1032+
},
1033+
{
1034+
visitors__count_rolling_week_to_date: null,
1035+
visitors__created_at_day: '2017-01-09T00:00:00.000Z',
1036+
visitors__created_at_three_days: '2017-01-07T00:00:00.000Z',
1037+
},
1038+
{
1039+
visitors__count_rolling_week_to_date: null,
1040+
visitors__created_at_day: '2017-01-10T00:00:00.000Z',
1041+
visitors__created_at_three_days: '2017-01-10T00:00:00.000Z',
1042+
}
1043+
]));
1044+
9711045
it('two rolling windows with two time dimension granularities', async () => runQueryTest({
9721046
measures: [
9731047
'visitors.countRollingUnbounded',

rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_tools.rs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use super::base_query_options::FilterItem;
22
use super::filter_group::{FilterGroup, NativeFilterGroup};
33
use super::filter_params::{FilterParams, NativeFilterParams};
4-
use super::member_sql::{MemberSql, NativeMemberSql};
54
use super::security_context::{NativeSecurityContext, SecurityContext};
65
use super::sql_templates_render::{NativeSqlTemplatesRender, SqlTemplatesRender};
76
use super::sql_utils::{NativeSqlUtils, SqlUtils};
@@ -11,16 +10,9 @@ use cubenativeutils::wrappers::serializer::{
1110
use cubenativeutils::wrappers::NativeContextHolder;
1211
use cubenativeutils::wrappers::NativeObjectHandle;
1312
use cubenativeutils::CubeError;
14-
use serde::Deserialize;
1513
use std::any::Any;
1614
use std::rc::Rc;
1715

18-
#[derive(Deserialize, Debug)]
19-
pub struct CallDep {
20-
pub name: String,
21-
pub parent: Option<usize>,
22-
}
23-
2416
#[nativebridge::native_bridge]
2517
pub trait BaseTools {
2618
fn convert_tz(&self, field: String) -> Result<String, CubeError>;
@@ -30,11 +22,6 @@ pub trait BaseTools {
3022
dimension: String,
3123
) -> Result<String, CubeError>;
3224
fn sql_templates(&self) -> Result<Rc<dyn SqlTemplatesRender>, CubeError>;
33-
fn resolve_symbols_call_deps(
34-
&self,
35-
cube_name: String,
36-
sql: Rc<dyn MemberSql>,
37-
) -> Result<Vec<CallDep>, CubeError>;
3825
fn security_context_for_rust(&self) -> Result<Rc<dyn SecurityContext>, CubeError>;
3926
fn sql_utils_for_rust(&self) -> Result<Rc<dyn SqlUtils>, CubeError>;
4027
fn filters_proxy_for_rust(
@@ -52,11 +39,23 @@ pub trait BaseTools {
5239
granularity: String,
5340
date_range: Vec<String>,
5441
) -> Result<Vec<Vec<String>>, CubeError>;
42+
fn generate_custom_time_series(
43+
&self,
44+
granularity: String,
45+
date_range: Vec<String>,
46+
origin: String,
47+
) -> Result<Vec<Vec<String>>, CubeError>;
5548
fn get_allocated_params(&self) -> Result<Vec<String>, CubeError>;
5649
fn all_cube_members(&self, path: String) -> Result<Vec<String>, CubeError>;
5750
//===== TODO Move to templates
5851
fn hll_init(&self, sql: String) -> Result<String, CubeError>;
5952
fn hll_merge(&self, sql: String) -> Result<String, CubeError>;
6053
fn hll_cardinality_merge(&self, sql: String) -> Result<String, CubeError>;
6154
fn count_distinct_approx(&self, sql: String) -> Result<String, CubeError>;
55+
fn date_bin(
56+
&self,
57+
interval: String,
58+
source: String,
59+
origin: String,
60+
) -> Result<String, CubeError>;
6261
}

rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ use serde::{Deserialize, Serialize};
1111
use std::any::Any;
1212
use std::rc::Rc;
1313

14+
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Hash)]
15+
pub struct GranularityDefinition {
16+
pub interval: String,
17+
pub origin: Option<String>,
18+
pub offset: Option<String>,
19+
}
1420
#[derive(Serialize, Deserialize, Debug)]
1521
pub struct DimenstionDefinitionStatic {
1622
#[serde(rename = "type")]

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use super::cube_definition::{CubeDefinition, NativeCubeDefinition};
2-
use super::dimension_definition::{DimensionDefinition, NativeDimensionDefinition};
2+
use super::dimension_definition::{
3+
DimensionDefinition, GranularityDefinition, NativeDimensionDefinition,
4+
};
35
use super::measure_definition::{MeasureDefinition, NativeMeasureDefinition};
46
use super::member_sql::{MemberSql, NativeMemberSql};
57
use super::segment_definition::{NativeSegmentDefinition, SegmentDefinition};
@@ -48,4 +50,5 @@ pub trait CubeEvaluator {
4850
cube_name: String,
4951
sql: Rc<dyn MemberSql>,
5052
) -> Result<Vec<CallDep>, CubeError>;
53+
fn resolve_granularity(&self, path: Vec<String>) -> Result<GranularityDefinition, CubeError>;
5154
}

rust/cubesqlplanner/cubesqlplanner/src/plan/time_series.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::{Schema, SchemaColumn};
22
use crate::planner::{
33
query_tools::QueryTools,
44
sql_templates::{PlanSqlTemplates, TemplateProjectionColumn},
5+
Granularity,
56
};
67
use cubenativeutils::CubeError;
78
use std::rc::Rc;
@@ -11,7 +12,7 @@ pub struct TimeSeries {
1112
#[allow(dead_code)]
1213
time_dimension_name: String,
1314
date_range: TimeSeriesDateRange,
14-
granularity: String,
15+
granularity: Granularity,
1516
schema: Rc<Schema>,
1617
}
1718

@@ -25,7 +26,7 @@ impl TimeSeries {
2526
query_tools: Rc<QueryTools>,
2627
time_dimension_name: String,
2728
date_range: TimeSeriesDateRange,
28-
granularity: String,
29+
granularity: Granularity,
2930
) -> Self {
3031
let column = SchemaColumn::new(format!("date_from"), Some(time_dimension_name.clone()));
3132
let schema = Rc::new(Schema::new(vec![column]));
@@ -88,7 +89,11 @@ impl TimeSeries {
8889
(format!("({})", from), format!("({})", to))
8990
}
9091
};
91-
templates.generated_time_series_select(&from_date, &to_date, &self.granularity)
92+
templates.generated_time_series_select(
93+
&from_date,
94+
&to_date,
95+
&self.granularity.granularity_interval(),
96+
)
9297
} else {
9398
let (from_date, to_date) = match &self.date_range {
9499
TimeSeriesDateRange::Filter(from_date, to_date) => {
@@ -100,10 +105,18 @@ impl TimeSeries {
100105
));
101106
}
102107
};
103-
let series = self.query_tools.base_tools().generate_time_series(
104-
self.granularity.clone(),
105-
vec![from_date.clone(), to_date.clone()],
106-
)?;
108+
let series = if self.granularity.is_predefined_granularity() {
109+
self.query_tools.base_tools().generate_time_series(
110+
self.granularity.granularity().clone(),
111+
vec![from_date.clone(), to_date.clone()],
112+
)?
113+
} else {
114+
self.query_tools.base_tools().generate_custom_time_series(
115+
self.granularity.granularity_interval().clone(),
116+
vec![from_date.clone(), to_date.clone()],
117+
self.granularity.origin_local_formatted(),
118+
)?
119+
};
107120
templates.time_series_select(from_date.clone(), to_date.clone(), series)
108121
}
109122
}

rust/cubesqlplanner/cubesqlplanner/src/planner/base_dimension.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ impl BaseDimension {
132132
self.member_evaluator.clone()
133133
}
134134

135+
pub fn definition(&self) -> Option<Rc<dyn DimensionDefinition>> {
136+
self.definition.clone()
137+
}
138+
135139
pub fn sql_call(&self) -> Result<Rc<SqlCall>, CubeError> {
136140
match self.member_evaluator.as_ref() {
137141
MemberSymbol::Dimension(d) => {

0 commit comments

Comments
 (0)