Skip to content

Commit e1cbfbb

Browse files
committed
fix(cubesql): Avoid panics during filter rewrites
1 parent 28c1e3b commit e1cbfbb

File tree

2 files changed

+115
-20
lines changed

2 files changed

+115
-20
lines changed

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

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2807,7 +2807,7 @@ impl FilterRules {
28072807
ScalarValue::TimestampNanosecond(_, _)
28082808
| ScalarValue::Date32(_)
28092809
| ScalarValue::Date64(_) => {
2810-
if let Some(timestamp) =
2810+
if let Ok(Some(timestamp)) =
28112811
Self::scalar_to_native_datetime(&literal)
28122812
{
28132813
let value = format_iso_timestamp(timestamp);
@@ -2842,7 +2842,10 @@ impl FilterRules {
28422842
continue;
28432843
}
28442844
}
2845-
x => panic!("Unsupported filter scalar: {:?}", x),
2845+
x => {
2846+
log::trace!("Unsupported filter scalar: {x:?}");
2847+
continue;
2848+
}
28462849
};
28472850

28482851
subst.insert(
@@ -3442,6 +3445,7 @@ impl FilterRules {
34423445
}
34433446

34443447
// Transform ?expr IN (?literal) to ?expr = ?literal
3448+
// TODO it's incorrect: inner expr can be null, or can be non-literal (and domain in not clear)
34453449
fn transform_filter_in_to_equal(
34463450
&self,
34473451
negated_var: &'static str,
@@ -3501,7 +3505,10 @@ impl FilterRules {
35013505
let values = list
35023506
.into_iter()
35033507
.map(|literal| FilterRules::scalar_to_value(literal))
3504-
.collect::<Vec<_>>();
3508+
.collect::<Result<Vec<_>, _>>();
3509+
let Ok(values) = values else {
3510+
return false;
3511+
};
35053512

35063513
if let Some((member_name, cube)) = Self::filter_member_name(
35073514
egraph,
@@ -3552,8 +3559,8 @@ impl FilterRules {
35523559
}
35533560
}
35543561

3555-
fn scalar_to_value(literal: &ScalarValue) -> String {
3556-
match literal {
3562+
fn scalar_to_value(literal: &ScalarValue) -> Result<String, &'static str> {
3563+
Ok(match literal {
35573564
ScalarValue::Utf8(Some(value)) => value.to_string(),
35583565
ScalarValue::Int64(Some(value)) => value.to_string(),
35593566
ScalarValue::Boolean(Some(value)) => value.to_string(),
@@ -3564,18 +3571,24 @@ impl FilterRules {
35643571
ScalarValue::TimestampNanosecond(_, _)
35653572
| ScalarValue::Date32(_)
35663573
| ScalarValue::Date64(_) => {
3567-
if let Some(timestamp) = Self::scalar_to_native_datetime(literal) {
3568-
return format_iso_timestamp(timestamp);
3574+
if let Some(timestamp) = Self::scalar_to_native_datetime(literal)? {
3575+
format_iso_timestamp(timestamp)
3576+
} else {
3577+
log::trace!("Unsupported filter scalar: {literal:?}");
3578+
return Err("Unsupported filter scalar");
35693579
}
3570-
3571-
panic!("Unsupported filter scalar: {:?}", literal);
35723580
}
3573-
x => panic!("Unsupported filter scalar: {:?}", x),
3574-
}
3581+
x => {
3582+
log::trace!("Unsupported filter scalar: {x:?}");
3583+
return Err("Unsupported filter scalar");
3584+
}
3585+
})
35753586
}
35763587

3577-
fn scalar_to_native_datetime(literal: &ScalarValue) -> Option<NaiveDateTime> {
3578-
match literal {
3588+
fn scalar_to_native_datetime(
3589+
literal: &ScalarValue,
3590+
) -> Result<Option<NaiveDateTime>, &'static str> {
3591+
Ok(match literal {
35793592
ScalarValue::TimestampNanosecond(_, _)
35803593
| ScalarValue::Date32(_)
35813594
| ScalarValue::Date64(_) => {
@@ -3589,13 +3602,17 @@ impl FilterRules {
35893602
} else if let Some(array) = array.as_any().downcast_ref::<Date64Array>() {
35903603
array.value_as_datetime(0)
35913604
} else {
3592-
panic!("Unexpected array type: {:?}", array.data_type())
3605+
log::trace!("Unexpected array type: {:?}", array.data_type());
3606+
return Err("Unexpected array type");
35933607
};
35943608

35953609
timestamp
35963610
}
3597-
_ => panic!("Unsupported filter scalar: {:?}", literal),
3598-
}
3611+
x => {
3612+
log::trace!("Unsupported filter scalar: {x:?}");
3613+
return Err("Unsupported filter scalar");
3614+
}
3615+
})
35993616
}
36003617

36013618
fn transform_is_null(
@@ -3865,10 +3882,15 @@ impl FilterRules {
38653882
Some(MemberType::Time) => (),
38663883
_ => continue,
38673884
}
3868-
let values = vec![
3869-
FilterRules::scalar_to_value(&low),
3870-
FilterRules::scalar_to_value(&high),
3871-
];
3885+
3886+
let Ok(low) = FilterRules::scalar_to_value(&low) else {
3887+
return false;
3888+
};
3889+
let Ok(high) = FilterRules::scalar_to_value(&high) else {
3890+
return false;
3891+
};
3892+
3893+
let values = vec![low, high];
38723894

38733895
subst.insert(
38743896
filter_member_var,

rust/cubesql/cubesql/src/compile/test/test_filters.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use cubeclient::models::{V1LoadRequestQuery, V1LoadRequestQueryFilterItem};
2+
use datafusion::physical_plan::displayable;
23
use pretty_assertions::assert_eq;
34

45
use crate::compile::{
@@ -60,3 +61,75 @@ GROUP BY
6061
}
6162
);
6263
}
64+
65+
#[tokio::test]
66+
async fn test_filter_dim_in_null() {
67+
if !Rewriter::sql_push_down_enabled() {
68+
return;
69+
}
70+
init_testing_logger();
71+
72+
let query_plan = convert_select_to_query_plan(
73+
// language=PostgreSQL
74+
r#"
75+
SELECT
76+
dim_str0
77+
FROM
78+
MultiTypeCube
79+
WHERE dim_str1 IN (NULL)
80+
"#
81+
.to_string(),
82+
DatabaseProtocol::PostgreSQL,
83+
)
84+
.await;
85+
86+
let physical_plan = query_plan.as_physical_plan().await.unwrap();
87+
println!(
88+
"Physical plan: {}",
89+
displayable(physical_plan.as_ref()).indent()
90+
);
91+
92+
// For now this tests only that query is rewritable
93+
// TODO support this as "notSet" filter
94+
95+
assert!(query_plan
96+
.as_logical_plan()
97+
.find_cube_scan_wrapped_sql()
98+
.wrapped_sql
99+
.sql
100+
.contains(r#"\"expr\":\"${MultiTypeCube.dim_str1} IN (NULL)\""#));
101+
}
102+
103+
#[tokio::test]
104+
async fn test_filter_superset_is_null() {
105+
if !Rewriter::sql_push_down_enabled() {
106+
return;
107+
}
108+
init_testing_logger();
109+
110+
let query_plan = convert_select_to_query_plan(
111+
// language=PostgreSQL
112+
r#"
113+
SELECT dim_str0 FROM MultiTypeCube WHERE (dim_str1 IS NULL OR dim_str1 IN (NULL) AND (1<>1))
114+
"#
115+
.to_string(),
116+
DatabaseProtocol::PostgreSQL,
117+
)
118+
.await;
119+
120+
let physical_plan = query_plan.as_physical_plan().await.unwrap();
121+
println!(
122+
"Physical plan: {}",
123+
displayable(physical_plan.as_ref()).indent()
124+
);
125+
126+
// For now this tests only that query is rewritable
127+
// TODO support this as "notSet" filter
128+
129+
assert!(query_plan
130+
.as_logical_plan()
131+
.find_cube_scan_wrapped_sql()
132+
.wrapped_sql
133+
.sql
134+
.contains(r#"\"expr\":\"(${MultiTypeCube.dim_str1} IS NULL OR (${MultiTypeCube.dim_str1} IN (NULL) AND FALSE))\""#));
135+
}

0 commit comments

Comments
 (0)