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
23 changes: 0 additions & 23 deletions packages/cubejs-backend-native/src/bridge_test_exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,21 +189,6 @@ impl BaseTools for StubBaseTools {
fn sql_utils_for_rust(&self) -> Result<Rc<dyn SqlUtils>, CubeError> {
Err(stub_err("sql_utils_for_rust"))
}
fn generate_time_series(
&self,
_granularity: String,
_date_range: Vec<String>,
) -> Result<Vec<Vec<String>>, CubeError> {
Err(stub_err("generate_time_series"))
}
fn generate_custom_time_series(
&self,
_granularity: String,
_date_range: Vec<String>,
_origin: String,
) -> Result<Vec<Vec<String>>, CubeError> {
Err(stub_err("generate_custom_time_series"))
}
fn get_allocated_params(&self) -> Result<Vec<String>, CubeError> {
Err(stub_err("get_allocated_params"))
}
Expand Down Expand Up @@ -821,14 +806,6 @@ fn invoke_base_tools<IT: InnerTypes>(b: &NativeBaseTools<IT>) -> InvokeResult {
r.record("driver_tools", b.driver_tools(false));
r.record("sql_templates", b.sql_templates());
r.record("sql_utils_for_rust", b.sql_utils_for_rust());
r.record(
"generate_time_series",
b.generate_time_series("day".to_string(), vec![]),
);
r.record(
"generate_custom_time_series",
b.generate_custom_time_series("day".to_string(), vec![], "2024-01-01".to_string()),
);
r.record("get_allocated_params", b.get_allocated_params());
r.record("all_cube_members", b.all_cube_members("Orders".to_string()));
r.record(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,6 @@ export const baseToolsFixture = (): unknown => ({
driverTools: () => driverToolsFixture(),
sqlTemplates: () => ({}),
sqlUtilsForRust: () => sqlUtilsFixture(),
generateTimeSeries: () => [],
generateCustomTimeSeries: () => [],
getAllocatedParams: () => [],
allCubeMembers: () => [],
intervalAndMinimalTimeUnit: () => ['1', 'day'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ const BRIDGES: BridgeSpec[] = [
expected: [
'all_cube_members',
'driver_tools',
'generate_custom_time_series',
'generate_time_series',
'get_allocated_params',
'get_pre_aggregation_by_name',
'interval_and_minimal_time_unit',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,14 @@ use std::any::Any;
use std::rc::Rc;

/// Dialect-independent callbacks to the JavaScript side, used
/// during compilation and planning: SQL templates, time-series
/// generation, allocated params, pre-aggregation lookup, join-tree
/// resolution. Dialect-specific helpers live behind `DriverTools`,
/// reachable via `driver_tools()`.
/// during compilation and planning: SQL templates, allocated params,
/// pre-aggregation lookup, join-tree resolution. Dialect-specific
/// helpers live behind `DriverTools`, reachable via `driver_tools()`.
#[nativebridge::native_bridge]
pub trait BaseTools {
fn driver_tools(&self, external: bool) -> Result<Rc<dyn DriverTools>, CubeError>;
fn sql_templates(&self) -> Result<Rc<dyn SqlTemplatesRender>, CubeError>;
fn sql_utils_for_rust(&self) -> Result<Rc<dyn SqlUtils>, CubeError>;
fn generate_time_series(
&self,
granularity: String,
date_range: Vec<String>,
) -> Result<Vec<Vec<String>>, CubeError>;
fn generate_custom_time_series(
&self,
granularity: String,
date_range: Vec<String>,
origin: String,
) -> Result<Vec<Vec<String>>, CubeError>;
fn get_allocated_params(&self) -> Result<Vec<String>, CubeError>;
fn all_cube_members(&self, path: String) -> Result<Vec<String>, CubeError>;
fn interval_and_minimal_time_unit(&self, interval: String) -> Result<Vec<String>, CubeError>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
use super::{Schema, SchemaColumn};
use crate::planner::{
query_tools::QueryTools, sql_templates::PlanSqlTemplates, Granularity, MemberSymbol,
};
use crate::planner::{sql_templates::PlanSqlTemplates, Granularity, MemberSymbol, QueryTimeSeries};
use cubenativeutils::CubeError;
use std::rc::Rc;

pub struct TimeSeries {
query_tools: Rc<QueryTools>,
#[allow(dead_code)]
time_dimension_name: String,
date_range: TimeSeriesDateRange,
Expand All @@ -21,15 +18,13 @@ pub enum TimeSeriesDateRange {

impl TimeSeries {
pub fn new(
query_tools: Rc<QueryTools>,
time_dimension: &Rc<MemberSymbol>,
date_range: TimeSeriesDateRange,
granularity: Granularity,
) -> Self {
let column = SchemaColumn::new(format!("date_from"), Some(time_dimension.clone()));
let schema = Rc::new(Schema::new(vec![column]));
Self {
query_tools,
time_dimension_name: time_dimension.full_name(),
granularity,
date_range,
Expand Down Expand Up @@ -101,16 +96,20 @@ impl TimeSeries {
));
}
};
let precision = templates.timestamp_precision()?;
let range = [raw_from_date.clone(), raw_to_date.clone()];
let series = if self.granularity.is_predefined_granularity() {
self.query_tools.base_tools().generate_time_series(
self.granularity.granularity().clone(),
vec![raw_from_date.clone(), raw_to_date.clone()],
QueryTimeSeries::generate_predefined(
self.granularity.granularity(),
&range,
precision,
)?
} else {
self.query_tools.base_tools().generate_custom_time_series(
self.granularity.granularity_interval().to_sql(),
vec![raw_from_date.clone(), raw_to_date.clone()],
self.granularity.origin_local_formatted(),
QueryTimeSeries::generate_custom(
&self.granularity.granularity_interval().to_sql(),
&range,
&self.granularity.origin_local_formatted(),
precision,
)?
};
templates.time_series_select(from_date.clone(), to_date.clone(), series)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,7 @@ impl<'a> LogicalNodeProcessor<'a, MultiStageTimeSeries> for MultiStageTimeSeries
}
};

let time_series = TimeSeries::new(
query_tools.clone(),
&time_dimension,
ts_date_range,
granularity_obj,
);
let time_series = TimeSeries::new(&time_dimension, ts_date_range, granularity_obj);
let query_plan = QueryPlan::TimeSeries(Rc::new(time_series));
Ok(query_plan)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ impl QueryDateTime {
)
}

pub fn start_of_iso_week(&self) -> Self {
let tz = self.date_time.timezone();
let date = self.date_time.date_naive();
let from_monday = date.weekday().num_days_from_monday() as i64;
let monday = date - Duration::days(from_monday);
Self::new(
tz.with_ymd_and_hms(monday.year(), monday.month(), monday.day(), 0, 0, 0)
.unwrap(),
)
}

pub fn date_time(&self) -> DateTime<Tz> {
self.date_time
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,18 @@ impl Granularity {
let origin = if let Some(origin) = origin {
QueryDateTime::from_date_str(timezone, &origin)?
} else if let Some(offset) = &granularity_offset {
let origin = Self::default_origin(timezone)?;
// Week-based intervals expect the offset relative to the start of a week.
let origin = Self::fix_origin_for_weeks_if_needed(
Self::default_origin(timezone)?,
&granularity_interval,
);
let interval = SqlInterval::from_str(offset)?;
origin.add_interval(&interval)?
} else {
Self::default_origin(timezone)?
Self::fix_origin_for_weeks_if_needed(
Self::default_origin(timezone)?,
&granularity_interval,
)
};

let is_natural_aligned = granularity_interval.is_trivial();
Expand Down Expand Up @@ -161,6 +168,17 @@ impl Granularity {
Ok(QueryDateTime::now(timezone)?.start_of_year())
}

fn fix_origin_for_weeks_if_needed(
origin: QueryDateTime,
interval: &SqlInterval,
) -> QueryDateTime {
if interval.is_week_only() {
origin.start_of_iso_week()
} else {
origin
}
}

pub fn apply_to_input_sql(
&self,
templates: &PlanSqlTemplates,
Expand Down Expand Up @@ -220,3 +238,57 @@ impl Granularity {
Ok(true)
}
}

#[cfg(test)]
mod tests {
use super::*;
use chrono::{Datelike, NaiveDate, Weekday};

fn origin_date(g: &Granularity) -> NaiveDate {
NaiveDate::parse_from_str(&g.origin_local_formatted()[..10], "%Y-%m-%d").unwrap()
}

fn custom(interval: &str, origin: Option<&str>, offset: Option<&str>) -> Granularity {
Granularity::try_new_custom(
"UTC".parse::<Tz>().unwrap(),
"test_granularity".to_string(),
origin.map(str::to_string),
interval.to_string(),
offset.map(str::to_string),
None,
)
.unwrap()
}

#[test]
fn week_only_default_origin_snaps_to_iso_monday() {
assert_eq!(
origin_date(&custom("2 weeks", None, None)).weekday(),
Weekday::Mon
);
}

#[test]
fn non_week_default_origin_stays_at_year_start() {
let d = origin_date(&custom("2 days", None, None));
assert_eq!((d.month(), d.day()), (1, 1));
}

#[test]
fn week_with_offset_aligns_to_monday_then_offsets() {
// Monday-of-year-start + 2 days => Wednesday.
assert_eq!(
origin_date(&custom("2 weeks", None, Some("2 days"))).weekday(),
Weekday::Wed
);
}

#[test]
fn explicit_origin_is_not_snapped_for_week_interval() {
// 2024-01-03 is a Wednesday; an explicit origin must be preserved verbatim.
assert_eq!(
origin_date(&custom("2 weeks", Some("2024-01-03"), None)),
NaiveDate::from_ymd_opt(2024, 1, 3).unwrap()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ mod date_time_helper;
mod granularity;
mod granularity_helper;
mod sql_interval;
mod time_series;

pub use date_time::*;
pub use date_time_helper::*;
pub use granularity::*;
pub use granularity_helper::*;
pub use sql_interval::*;
pub use time_series::*;
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ impl SqlInterval {
Ok(res.to_string())
}

pub fn is_week_only(&self) -> bool {
self.week != 0
&& self.year == 0
&& self.quarter == 0
&& self.month == 0
&& self.day == 0
&& self.hour == 0
&& self.minute == 0
&& self.second == 0
}

pub fn is_trivial(&self) -> bool {
let fields = [
self.year,
Expand Down
Loading
Loading