Skip to content

Commit d96ef83

Browse files
committed
feat(cubesql): Filter push down for date_part('year') = ?y & date_part('quarter') = ?q
1 parent 644fbd4 commit d96ef83

File tree

2 files changed

+162
-0
lines changed

2 files changed

+162
-0
lines changed

rust/cubesql/cubesql/src/compile/mod.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9462,6 +9462,84 @@ ORDER BY "source"."str0" ASC
94629462
)
94639463
}
94649464

9465+
#[tokio::test]
9466+
async fn test_filter_extract_by_year_and_quarter() {
9467+
init_testing_logger();
9468+
9469+
let logical_plan = convert_select_to_query_plan(
9470+
r#"
9471+
SELECT
9472+
COUNT(*) AS "count",
9473+
EXTRACT(YEAR FROM "KibanaSampleDataEcommerce"."order_date") AS "yr:completedAt:ok"
9474+
FROM "public"."KibanaSampleDataEcommerce" "KibanaSampleDataEcommerce"
9475+
WHERE EXTRACT(YEAR FROM "KibanaSampleDataEcommerce"."order_date") = 2019 AND EXTRACT(QUARTER FROM "KibanaSampleDataEcommerce"."order_date") = 2
9476+
GROUP BY 2
9477+
;"#
9478+
.to_string(),
9479+
DatabaseProtocol::PostgreSQL,
9480+
)
9481+
.await
9482+
.as_logical_plan();
9483+
9484+
assert_eq!(
9485+
logical_plan.find_cube_scan().request,
9486+
V1LoadRequestQuery {
9487+
measures: Some(vec!["KibanaSampleDataEcommerce.count".to_string()]),
9488+
dimensions: Some(vec![]),
9489+
segments: Some(vec![]),
9490+
time_dimensions: Some(vec![V1LoadRequestQueryTimeDimension {
9491+
dimension: "KibanaSampleDataEcommerce.order_date".to_string(),
9492+
granularity: Some("year".to_string()),
9493+
date_range: Some(json!(vec![
9494+
"2019-04-01".to_string(),
9495+
"2019-06-31".to_string(),
9496+
])),
9497+
},]),
9498+
order: Some(vec![]),
9499+
..Default::default()
9500+
}
9501+
)
9502+
}
9503+
9504+
#[tokio::test]
9505+
async fn test_filter_extract_by_year_and_month() {
9506+
init_testing_logger();
9507+
9508+
let logical_plan = convert_select_to_query_plan(
9509+
r#"
9510+
SELECT
9511+
COUNT(*) AS "count",
9512+
EXTRACT(YEAR FROM "KibanaSampleDataEcommerce"."order_date") AS "yr:completedAt:ok"
9513+
FROM "public"."KibanaSampleDataEcommerce" "KibanaSampleDataEcommerce"
9514+
WHERE EXTRACT(YEAR FROM "KibanaSampleDataEcommerce"."order_date") = 2019 AND EXTRACT(MONTH FROM "KibanaSampleDataEcommerce"."order_date") = 2
9515+
GROUP BY 2
9516+
;"#
9517+
.to_string(),
9518+
DatabaseProtocol::PostgreSQL,
9519+
)
9520+
.await
9521+
.as_logical_plan();
9522+
9523+
assert_eq!(
9524+
logical_plan.find_cube_scan().request,
9525+
V1LoadRequestQuery {
9526+
measures: Some(vec!["KibanaSampleDataEcommerce.count".to_string()]),
9527+
dimensions: Some(vec![]),
9528+
segments: Some(vec![]),
9529+
time_dimensions: Some(vec![V1LoadRequestQueryTimeDimension {
9530+
dimension: "KibanaSampleDataEcommerce.order_date".to_string(),
9531+
granularity: Some("year".to_string()),
9532+
date_range: Some(json!(vec![
9533+
"2019-02-01".to_string(),
9534+
"2019-02-28".to_string(),
9535+
])),
9536+
},]),
9537+
order: Some(vec![]),
9538+
..Default::default()
9539+
}
9540+
)
9541+
}
9542+
94659543
#[tokio::test]
94669544
async fn test_tableau_filter_extract_by_year() {
94679545
init_testing_logger();

rust/cubesql/cubesql/src/compile/rewrite/rules/filters.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,6 +1726,44 @@ impl RewriteRules for FilterRules {
17261726
"?filter_aliases",
17271727
),
17281728
),
1729+
// EXTRACT(YEAR FROM "KibanaSampleDataEcommerce"."order_date") = 2019
1730+
// AND EXTRACT(MONTH FROM "KibanaSampleDataEcommerce"."order_date") = 3
1731+
transforming_rewrite(
1732+
"extract-date-range-and-gran-equals",
1733+
filter_op(
1734+
filter_op_filters(
1735+
filter_member("?member", "FilterMemberOp:inDateRange", "?values"),
1736+
filter_replacer(
1737+
binary_expr(
1738+
self.fun_expr(
1739+
"DatePart",
1740+
vec![literal_expr("?granularity"), column_expr("?column")],
1741+
),
1742+
"=",
1743+
literal_expr("?value"),
1744+
),
1745+
"?alias_to_cube",
1746+
"?members",
1747+
"?filter_aliases",
1748+
),
1749+
),
1750+
"FilterOpOp:and",
1751+
),
1752+
filter_member("?member", "FilterMemberOp:inDateRange", "?new_values"),
1753+
self.transform_filter_extract_date_range_and_trunc_gran_equals(
1754+
"?member",
1755+
"?values",
1756+
"?granularity",
1757+
"?column",
1758+
"?value",
1759+
"?alias_to_cube",
1760+
"?members",
1761+
"?filter_aliases",
1762+
"?new_values",
1763+
),
1764+
),
1765+
// TODO: Introduce rule to unwrap TRUNC(EXTRACT(?granularity FROM ?column_expr))
1766+
//
17291767
// TRUNC(EXTRACT(YEAR FROM "KibanaSampleDataEcommerce"."order_date")) = 2019
17301768
// AND TRUNC(EXTRACT(MONTH FROM "KibanaSampleDataEcommerce"."order_date")) = 3
17311769
transforming_rewrite(
@@ -1765,6 +1803,7 @@ impl RewriteRules for FilterRules {
17651803
"?new_values",
17661804
),
17671805
),
1806+
// TODO: Introduce rule to unwrap TRUNC(EXTRACT(?granularity FROM ?column_expr))
17681807
// When the filter set above is paired with other filters, it needs to be
17691808
// regrouped for the above rewrite rule to match
17701809
rewrite(
@@ -1829,6 +1868,7 @@ impl RewriteRules for FilterRules {
18291868
"FilterOpOp:and",
18301869
),
18311870
),
1871+
// TODO: Introduce rule to unwrap TRUNC(EXTRACT(?granularity FROM ?column_expr))
18321872
// The filter set above may be inverted, let's account for that as well
18331873
rewrite(
18341874
"extract-date-range-and-trunc-reverse",
@@ -1877,6 +1917,7 @@ impl RewriteRules for FilterRules {
18771917
"FilterOpOp:and",
18781918
),
18791919
),
1920+
// TODO: Introduce rule to unwrap TRUNC(EXTRACT(?granularity FROM ?column_expr))
18801921
rewrite(
18811922
"extract-date-range-and-trunc-reverse-nested",
18821923
filter_op(
@@ -3991,6 +4032,7 @@ impl FilterRules {
39914032
if start_date_year != end_date.year() {
39924033
return false;
39934034
}
4035+
39944036
// Month value must be valid
39954037
if !(1..=12).contains(&value) {
39964038
return false;
@@ -4022,6 +4064,48 @@ impl FilterRules {
40224064
new_end_date.format("%Y-%m-%d").to_string(),
40234065
]
40244066
}
4067+
"quarter" | "qtr" => {
4068+
// Check that the range only covers one year
4069+
let start_date_year = start_date.year();
4070+
if start_date_year != end_date.year() {
4071+
return false;
4072+
}
4073+
4074+
// Quarter value must be valid (1-4)
4075+
if !(1..=4).contains(&value) {
4076+
return false;
4077+
}
4078+
4079+
let quarter_start_month = (value - 1) * 3 + 1;
4080+
4081+
// Obtain the new range
4082+
let Some(new_start_date) =
4083+
NaiveDate::from_ymd_opt(start_date_year, quarter_start_month as u32, 1)
4084+
else {
4085+
return false;
4086+
};
4087+
4088+
let Some(new_end_date) = new_start_date
4089+
.checked_add_months(Months::new(3))
4090+
.and_then(|date| date.checked_sub_days(Days::new(1)))
4091+
else {
4092+
return false;
4093+
};
4094+
4095+
// Paranoid check, If the resulting range is outside of the original range, we can't merge
4096+
// the filters
4097+
if new_start_date > end_date || new_end_date < start_date {
4098+
return false;
4099+
}
4100+
4101+
let new_start_date = max(new_start_date, start_date);
4102+
let new_end_date = min(new_end_date, end_date);
4103+
4104+
vec![
4105+
new_start_date.format("%Y-%m-%d").to_string(),
4106+
new_end_date.format("%Y-%m-%d").to_string(),
4107+
]
4108+
}
40254109
// TODO: handle more granularities
40264110
_ => return false,
40274111
};

0 commit comments

Comments
 (0)