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
130 changes: 130 additions & 0 deletions rust/cubesql/cubesql/src/compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9541,6 +9541,136 @@ ORDER BY "source"."str0" ASC
)
}

#[tokio::test]
async fn test_filter_extract_by_year_and_week() {
init_testing_logger();

async fn assert_week_result(week: i32, start_date: &str, end_date: &str) {
let query_plan = convert_select_to_query_plan(
format!(r#"
SELECT COUNT(*) AS "count",
EXTRACT(YEAR FROM "KibanaSampleDataEcommerce"."order_date") AS "yr:completedAt:ok"
FROM "public"."KibanaSampleDataEcommerce" "KibanaSampleDataEcommerce"
WHERE EXTRACT(YEAR FROM "KibanaSampleDataEcommerce"."order_date") = 2019
AND EXTRACT(WEEK FROM "KibanaSampleDataEcommerce"."order_date") = {}
GROUP BY 2
"#, week),
DatabaseProtocol::PostgreSQL,
).await;

assert_eq!(
query_plan.as_logical_plan().find_cube_scan().request,
V1LoadRequestQuery {
measures: Some(vec!["KibanaSampleDataEcommerce.count".to_string()]),
dimensions: Some(vec![]),
segments: Some(vec![]),
time_dimensions: Some(vec![V1LoadRequestQueryTimeDimension {
dimension: "KibanaSampleDataEcommerce.order_date".to_string(),
granularity: Some("year".to_string()),
date_range: Some(json!(vec![start_date, end_date])),
},]),
order: Some(vec![]),
..Default::default()
}
)
}

// Test week 1 (first week of 2019)
// In 2019, January 1 is a Tuesday, so ISO week 1 starts on Monday, December 31, 2018
// But since our range is constrained to 2019, it should be Jan 1-6
assert_week_result(1, "2019-01-01", "2019-01-06").await;

// Test week 15 (mid-April)
// Week 15 of 2019 is April 8-14
assert_week_result(15, "2019-04-08", "2019-04-14").await;

// Test week 52 (end of year)
// Week 52 of 2019 is December 23-29
assert_week_result(52, "2019-12-23", "2019-12-29").await;
}

#[tokio::test]
async fn test_filter_extract_by_year_and_week_with_trunc() {
init_testing_logger();

let logical_plan = convert_select_to_query_plan(
r#"
SELECT
COUNT(*) AS "count",
EXTRACT(YEAR FROM "KibanaSampleDataEcommerce"."order_date") AS "yr:completedAt:ok"
FROM "public"."KibanaSampleDataEcommerce" "KibanaSampleDataEcommerce"
WHERE EXTRACT(YEAR FROM "KibanaSampleDataEcommerce"."order_date") = 2019
AND CAST(TRUNC(EXTRACT(WEEK FROM "KibanaSampleDataEcommerce"."order_date")) AS INTEGER) = 15
GROUP BY 2
"#
.to_string(),
DatabaseProtocol::PostgreSQL,
)
.await
.as_logical_plan();

assert_eq!(
logical_plan.find_cube_scan().request,
V1LoadRequestQuery {
measures: Some(vec!["KibanaSampleDataEcommerce.count".to_string()]),
dimensions: Some(vec![]),
segments: Some(vec![]),
time_dimensions: Some(vec![V1LoadRequestQueryTimeDimension {
dimension: "KibanaSampleDataEcommerce.order_date".to_string(),
granularity: Some("year".to_string()),
date_range: Some(json!(vec![
"2019-04-08".to_string(),
"2019-04-14".to_string(),
])),
},]),
order: Some(vec![]),
..Default::default()
}
)
}

#[tokio::test]
async fn test_filter_date_part_by_year_quarter_month_week() {
init_testing_logger();

let logical_plan = convert_select_to_query_plan(
r#"
SELECT
COUNT(*) AS "count",
DATE_PART('year', "KibanaSampleDataEcommerce"."order_date") AS "yr:completedAt:ok"
FROM "public"."KibanaSampleDataEcommerce" "KibanaSampleDataEcommerce"
WHERE DATE_PART('year', "KibanaSampleDataEcommerce"."order_date") = 2019
AND DATE_PART('quarter', "KibanaSampleDataEcommerce"."order_date") = 2
AND DATE_PART('month', "KibanaSampleDataEcommerce"."order_date") = 4
AND DATE_PART('week', "KibanaSampleDataEcommerce"."order_date") = 15
GROUP BY 2
"#
.to_string(),
DatabaseProtocol::PostgreSQL,
)
.await
.as_logical_plan();

assert_eq!(
logical_plan.find_cube_scan().request,
V1LoadRequestQuery {
measures: Some(vec!["KibanaSampleDataEcommerce.count".to_string()]),
dimensions: Some(vec![]),
segments: Some(vec![]),
time_dimensions: Some(vec![V1LoadRequestQueryTimeDimension {
dimension: "KibanaSampleDataEcommerce.order_date".to_string(),
granularity: Some("year".to_string()),
date_range: Some(json!(vec![
"2019-04-08".to_string(),
"2019-04-14".to_string(),
])),
},]),
order: Some(vec![]),
..Default::default()
}
)
}

#[tokio::test]
async fn test_tableau_filter_extract_by_year() {
init_testing_logger();
Expand Down
103 changes: 11 additions & 92 deletions rust/cubesql/cubesql/src/compile/rewrite/rules/filters.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::utils;
use super::utils::{self, try_merge_range_with_date_part};
use crate::compile::date_parser::parse_date_str;
use crate::{
compile::rewrite::{
Expand Down Expand Up @@ -50,7 +50,6 @@ use datafusion::{
};
use egg::{Subst, Var};
use std::{
cmp::{max, min},
collections::HashSet,
fmt::Display,
ops::{Index, IndexMut},
Expand Down Expand Up @@ -4025,98 +4024,18 @@ impl FilterRules {
return false;
};

let new_values = match granularity.as_str() {
"month" => {
// Check that the range only covers one year
let start_date_year = start_date.year();
if start_date_year != end_date.year() {
return false;
}

// Month value must be valid
if !(1..=12).contains(&value) {
return false;
}

// Obtain the new range
let Some(new_start_date) =
NaiveDate::from_ymd_opt(start_date_year, value as u32, 1)
else {
return false;
};
let Some(new_end_date) = new_start_date
.checked_add_months(Months::new(1))
.and_then(|date| date.checked_sub_days(Days::new(1)))
else {
return false;
};

// If the resulting range is outside of the original range, we can't merge
// the filters
if new_start_date > end_date || new_end_date < start_date {
return false;
}

// Preserves existing constraints, for example:
// inDataRange: order_date >= '2019-02-15' AND order_date < '2019-03-10'
// Month filter: EXTRACT(MONTH FROM order_date) = 2 (February)
let new_start_date = max(new_start_date, start_date);
let new_end_date = min(new_end_date, end_date);

vec![
new_start_date.format("%Y-%m-%d").to_string(),
new_end_date.format("%Y-%m-%d").to_string(),
]
}
"quarter" | "qtr" => {
// Check that the range only covers one year
let start_date_year = start_date.year();
if start_date_year != end_date.year() {
return false;
}

// Quarter value must be valid (1-4)
if !(1..=4).contains(&value) {
return false;
}

let quarter_start_month = (value - 1) * 3 + 1;

// Obtain the new range
let Some(new_start_date) =
NaiveDate::from_ymd_opt(start_date_year, quarter_start_month as u32, 1)
else {
return false;
};

let Some(new_end_date) = new_start_date
.checked_add_months(Months::new(3))
.and_then(|date| date.checked_sub_days(Days::new(1)))
else {
return false;
};

// Paranoid check, If the resulting range is outside of the original range, we can't merge
// the filters
if new_start_date > end_date || new_end_date < start_date {
return false;
}

// Preserves existing constraints, for example:
// inDataRange: order_date >= '2019-04-15' AND order_date < '2019-12-31'
// Month filter: EXTRACT(QUARTER FROM order_date) = 2
let new_start_date = max(new_start_date, start_date);
let new_end_date = min(new_end_date, end_date);

vec![
new_start_date.format("%Y-%m-%d").to_string(),
new_end_date.format("%Y-%m-%d").to_string(),
]
}
// TODO: handle more granularities
_ => return false,
// Use the utility function to calculate the date range for the given granularity
let Some((new_start_date, new_end_date)) =
try_merge_range_with_date_part(start_date, end_date, granularity.as_str(), value)
else {
return false;
};

let new_values = vec![
new_start_date.format("%Y-%m-%d").to_string(),
new_end_date.format("%Y-%m-%d").to_string(),
];

subst.insert(
new_values_var,
egraph.add(LogicalPlanLanguage::FilterMemberValues(FilterMemberValues(
Expand Down
Loading
Loading