Skip to content

Commit e5325c3

Browse files
committed
Move unbound placeholder error to physical-expr
Previously, these errors would occurr during optimization. Now that we allow unbound placeholders through the optimizer they fail when creating the physical plan instead.
1 parent 4be985a commit e5325c3

6 files changed

Lines changed: 169 additions & 76 deletions

File tree

datafusion/core/tests/dataframe/mod.rs

Lines changed: 63 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1985,6 +1985,7 @@ async fn test_array_agg() -> Result<()> {
19851985
async fn test_dataframe_placeholder_missing_param_values() -> Result<()> {
19861986
let ctx = SessionContext::new();
19871987

1988+
// Creating LogicalPlans with placeholders should work.
19881989
let df = ctx
19891990
.read_empty()
19901991
.unwrap()
@@ -2006,17 +2007,16 @@ async fn test_dataframe_placeholder_missing_param_values() -> Result<()> {
20062007
"\n\nexpected:\n\n{expected:#?}\nactual:\n\n{actual:#?}\n\n"
20072008
);
20082009

2009-
// The placeholder is not replaced with a value,
2010-
// so the filter data type is not know, i.e. a = $0.
2011-
// Therefore, the optimization fails.
2012-
let optimized_plan = ctx.state().optimize(logical_plan);
2013-
assert!(optimized_plan.is_err());
2014-
assert!(optimized_plan
2015-
.unwrap_err()
2016-
.to_string()
2017-
.contains("Placeholder type for '$0' could not be resolved. Make sure that the placeholder is bound to a concrete type, e.g. by providing parameter values."));
2018-
2019-
// Prodiving a parameter value should resolve the error
2010+
// Executing LogicalPlans with placeholders that don't have bound values
2011+
// should fail.
2012+
let results = df.collect().await;
2013+
let err_mesg = results.unwrap_err().strip_backtrace();
2014+
assert_eq!(
2015+
err_mesg,
2016+
"Execution error: Placeholder '$0' was not provided a value for execution."
2017+
);
2018+
2019+
// Providing a parameter value should resolve the error
20202020
let df = ctx
20212021
.read_empty()
20222022
.unwrap()
@@ -2040,12 +2040,14 @@ async fn test_dataframe_placeholder_missing_param_values() -> Result<()> {
20402040
"\n\nexpected:\n\n{expected:#?}\nactual:\n\n{actual:#?}\n\n"
20412041
);
20422042

2043-
let optimized_plan = ctx.state().optimize(logical_plan);
2044-
assert!(optimized_plan.is_ok());
2043+
// N.B., the test is basically `SELECT 1 as a WHERE a = 3;` which returns no results.
2044+
#[rustfmt::skip]
2045+
let expected = [
2046+
"++",
2047+
"++"
2048+
];
20452049

2046-
let actual = optimized_plan.unwrap().display_indent_schema().to_string();
2047-
let expected = "EmptyRelation [a:Int32]";
2048-
assert_eq!(expected, actual);
2050+
assert_batches_eq!(expected, &df.collect().await.unwrap());
20492051

20502052
Ok(())
20512053
}
@@ -2054,27 +2056,33 @@ async fn test_dataframe_placeholder_missing_param_values() -> Result<()> {
20542056
async fn test_dataframe_placeholder_column_parameter() -> Result<()> {
20552057
let ctx = SessionContext::new();
20562058

2059+
// Creating LogicalPlans with placeholders should work
20572060
let df = ctx.read_empty().unwrap().select_exprs(&["$1"]).unwrap();
2058-
20592061
let logical_plan = df.logical_plan();
20602062
let formatted = logical_plan.display_indent_schema().to_string();
20612063
let actual: Vec<&str> = formatted.trim().lines().collect();
2062-
let expected = vec!["Projection: $1 [$1:Null;N]", " EmptyRelation []"];
2064+
2065+
#[rustfmt::skip]
2066+
let expected = vec![
2067+
"Projection: $1 [$1:Null;N]",
2068+
" EmptyRelation []"
2069+
];
2070+
20632071
assert_eq!(
20642072
expected, actual,
20652073
"\n\nexpected:\n\n{expected:#?}\nactual:\n\n{actual:#?}\n\n"
20662074
);
20672075

2068-
// The placeholder is not replaced with a value,
2069-
// so the filter data type is not known, i.e. a = $0.
2070-
// Therefore, the optimization fails.
2071-
let optimized_plan = ctx.state().optimize(logical_plan);
2072-
assert!(optimized_plan
2073-
.unwrap_err()
2074-
.to_string()
2075-
.contains("Placeholder type for '$1' could not be resolved. Make sure that the placeholder is bound to a concrete type, e.g. by providing parameter values."));
2076+
// Executing LogicalPlans with placeholders that don't have bound values
2077+
// should fail.
2078+
let results = df.collect().await;
2079+
let err_mesg = results.unwrap_err().strip_backtrace();
2080+
assert_eq!(
2081+
err_mesg,
2082+
"Execution error: Placeholder '$1' was not provided a value for execution."
2083+
);
20762084

2077-
// Prodiving a parameter value should resolve the error
2085+
// Providing a parameter value should resolve the error
20782086
let df = ctx
20792087
.read_empty()
20802088
.unwrap()
@@ -2095,16 +2103,16 @@ async fn test_dataframe_placeholder_column_parameter() -> Result<()> {
20952103
"\n\nexpected:\n\n{expected:#?}\nactual:\n\n{actual:#?}\n\n"
20962104
);
20972105

2098-
let optimized_plan = ctx.state().optimize(logical_plan);
2099-
assert!(optimized_plan.is_ok());
2100-
2101-
let formatted = optimized_plan.unwrap().display_indent_schema().to_string();
2102-
let actual: Vec<&str> = formatted.trim().lines().collect();
2103-
let expected = vec![
2104-
"Projection: Int32(3) AS $1 [$1:Int32]",
2105-
" EmptyRelation []",
2106+
#[rustfmt::skip]
2107+
let expected = [
2108+
"+----+",
2109+
"| $1 |",
2110+
"+----+",
2111+
"| 3 |",
2112+
"+----+"
21062113
];
2107-
assert_eq!(expected, actual);
2114+
2115+
assert_batches_eq!(expected, &df.collect().await.unwrap());
21082116

21092117
Ok(())
21102118
}
@@ -2113,6 +2121,7 @@ async fn test_dataframe_placeholder_column_parameter() -> Result<()> {
21132121
async fn test_dataframe_placeholder_like_expression() -> Result<()> {
21142122
let ctx = SessionContext::new();
21152123

2124+
// Creating LogicalPlans with placeholders should work
21162125
let df = ctx
21172126
.read_empty()
21182127
.unwrap()
@@ -2134,16 +2143,16 @@ async fn test_dataframe_placeholder_like_expression() -> Result<()> {
21342143
"\n\nexpected:\n\n{expected:#?}\nactual:\n\n{actual:#?}\n\n"
21352144
);
21362145

2137-
// The placeholder is not replaced with a value,
2138-
// so the filter data type is not known, i.e. a = $0.
2139-
// Therefore, the optimization fails.
2140-
let optimized_plan = ctx.state().optimize(logical_plan);
2141-
assert!(optimized_plan
2142-
.unwrap_err()
2143-
.to_string()
2144-
.contains("Placeholder type for '$1' could not be resolved. Make sure that the placeholder is bound to a concrete type, e.g. by providing parameter values."));
2146+
// Executing LogicalPlans with placeholders that don't have bound values
2147+
// should fail.
2148+
let results = df.collect().await;
2149+
let err_mesg = results.unwrap_err().strip_backtrace();
2150+
assert_eq!(
2151+
err_mesg,
2152+
"Execution error: Placeholder '$1' was not provided a value for execution."
2153+
);
21452154

2146-
// Prodiving a parameter value should resolve the error
2155+
// Providing a parameter value should resolve the error
21472156
let df = ctx
21482157
.read_empty()
21492158
.unwrap()
@@ -2167,16 +2176,16 @@ async fn test_dataframe_placeholder_like_expression() -> Result<()> {
21672176
"\n\nexpected:\n\n{expected:#?}\nactual:\n\n{actual:#?}\n\n"
21682177
);
21692178

2170-
let optimized_plan = ctx.state().optimize(logical_plan);
2171-
assert!(optimized_plan.is_ok());
2172-
2173-
let formatted = optimized_plan.unwrap().display_indent_schema().to_string();
2174-
let actual: Vec<&str> = formatted.trim().lines().collect();
2175-
let expected = vec![
2176-
"Projection: Utf8(\"foo\") AS a [a:Utf8]",
2177-
" EmptyRelation []",
2179+
#[rustfmt::skip]
2180+
let expected = [
2181+
"+-----+",
2182+
"| a |",
2183+
"+-----+",
2184+
"| foo |",
2185+
"+-----+"
21782186
];
2179-
assert_eq!(expected, actual);
2187+
2188+
assert_batches_eq!(expected, &df.collect().await.unwrap());
21802189

21812190
Ok(())
21822191
}

datafusion/core/tests/sql/select.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,98 @@ async fn test_parameter_invalid_types() -> Result<()> {
229229
Ok(())
230230
}
231231

232+
#[tokio::test]
233+
async fn test_positional_parameter_not_bound() -> Result<()> {
234+
let ctx = SessionContext::new();
235+
let signed_ints: Int32Array = vec![-1, 0, 1].into();
236+
let unsigned_ints: UInt64Array = vec![1, 2, 3].into();
237+
let batch = RecordBatch::try_from_iter(vec![
238+
("signed", Arc::new(signed_ints) as ArrayRef),
239+
("unsigned", Arc::new(unsigned_ints) as ArrayRef),
240+
])?;
241+
ctx.register_batch("test", batch)?;
242+
243+
let query = "SELECT signed, unsigned FROM test \
244+
WHERE $1 >= signed AND signed <= $2 \
245+
AND unsigned <= $3 AND unsigned = $4";
246+
247+
let results = ctx.sql(query).await?.collect().await;
248+
249+
assert_eq!(
250+
results.unwrap_err().strip_backtrace(),
251+
"Execution error: Placeholder '$1' was not provided a value for execution."
252+
);
253+
254+
let results = ctx
255+
.sql(query)
256+
.await?
257+
.with_param_values(vec![
258+
ScalarValue::from(4_i32),
259+
ScalarValue::from(-1_i64),
260+
ScalarValue::from(2_i32),
261+
ScalarValue::from("1"),
262+
])?
263+
.collect()
264+
.await?;
265+
266+
let expected = [
267+
"+--------+----------+",
268+
"| signed | unsigned |",
269+
"+--------+----------+",
270+
"| -1 | 1 |",
271+
"+--------+----------+",
272+
];
273+
assert_batches_sorted_eq!(expected, &results);
274+
275+
Ok(())
276+
}
277+
278+
#[tokio::test]
279+
async fn test_named_parameter_not_bound() -> Result<()> {
280+
let ctx = SessionContext::new();
281+
let signed_ints: Int32Array = vec![-1, 0, 1].into();
282+
let unsigned_ints: UInt64Array = vec![1, 2, 3].into();
283+
let batch = RecordBatch::try_from_iter(vec![
284+
("signed", Arc::new(signed_ints) as ArrayRef),
285+
("unsigned", Arc::new(unsigned_ints) as ArrayRef),
286+
])?;
287+
ctx.register_batch("test", batch)?;
288+
289+
let query = "SELECT signed, unsigned FROM test \
290+
WHERE $foo >= signed AND signed <= $bar \
291+
AND unsigned <= $baz AND unsigned = $str";
292+
293+
let results = ctx.sql(query).await?.collect().await;
294+
295+
assert_eq!(
296+
results.unwrap_err().strip_backtrace(),
297+
"Execution error: Placeholder '$foo' was not provided a value for execution."
298+
);
299+
300+
let results = ctx
301+
.sql(query)
302+
.await?
303+
.with_param_values(vec![
304+
("foo", ScalarValue::from(4_i32)),
305+
("bar", ScalarValue::from(-1_i64)),
306+
("baz", ScalarValue::from(2_i32)),
307+
("str", ScalarValue::from("1")),
308+
])?
309+
.collect()
310+
.await?;
311+
312+
let expected = [
313+
"+--------+----------+",
314+
"| signed | unsigned |",
315+
"+--------+----------+",
316+
"| -1 | 1 |",
317+
"+--------+----------+",
318+
];
319+
assert_batches_sorted_eq!(expected, &results);
320+
321+
Ok(())
322+
}
323+
232324
#[tokio::test]
233325
async fn test_version_function() {
234326
let expected_version = format!(

datafusion/expr/src/logical_plan/plan.rs

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,26 +1501,6 @@ impl LogicalPlan {
15011501
.map(|_| param_types)
15021502
}
15031503

1504-
/// Walk the logical plan, error out if any `Placeholder` tokens with DataType::Null
1505-
pub fn validate_parameter_types(&self) -> Result<(), DataFusionError> {
1506-
self.apply_with_subqueries(|plan| {
1507-
plan.apply_expressions(|expr| {
1508-
expr.apply(|expr| {
1509-
if let Expr::Placeholder(Placeholder { id, data_type }) = expr {
1510-
if data_type.is_none() {
1511-
plan_err!(
1512-
"Placeholder type for '{id}' could not be resolved. Make sure that the \
1513-
placeholder is bound to a concrete type, e.g. by providing \
1514-
parameter values.")?;
1515-
}
1516-
}
1517-
Ok(TreeNodeRecursion::Continue)
1518-
})
1519-
})
1520-
})
1521-
.map(|_| ())
1522-
}
1523-
15241504
// ------------
15251505
// Various implementations for printing out LogicalPlans
15261506
// ------------

datafusion/optimizer/src/optimizer.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,6 @@ impl Optimizer {
357357
{
358358
let start_time = Instant::now();
359359
let options = config.options();
360-
plan.validate_parameter_types()?;
361360
let mut new_plan = plan;
362361

363362
let mut previous_plans = HashSet::with_capacity(16);

datafusion/physical-expr/src/planner.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use datafusion_common::{
2828
exec_err, not_impl_err, plan_err, DFSchema, Result, ScalarValue, ToDFSchema,
2929
};
3030
use datafusion_expr::execution_props::ExecutionProps;
31-
use datafusion_expr::expr::{Alias, Cast, InList, ScalarFunction};
31+
use datafusion_expr::expr::{Alias, Cast, InList, Placeholder, ScalarFunction};
3232
use datafusion_expr::var_provider::is_system_variables;
3333
use datafusion_expr::var_provider::VarType;
3434
use datafusion_expr::{
@@ -361,6 +361,9 @@ pub fn create_physical_expr(
361361
expressions::in_list(value_expr, list_exprs, negated, input_schema)
362362
}
363363
},
364+
Expr::Placeholder(Placeholder { id, .. }) => {
365+
exec_err!("Placeholder '{id}' was not provided a value for execution.")
366+
}
364367
other => {
365368
not_impl_err!("Physical plan does not support logical expression {other:?}")
366369
}

datafusion/sqllogictest/test_files/prepare.slt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ EXECUTE my_plan('j%');
8888
statement ok
8989
DEALLOCATE my_plan
9090

91+
# Check for missing parameters
92+
statement ok
93+
PREPARE my_plan AS SELECT * FROM person WHERE id < $1;
94+
95+
statement error No value found for placeholder with id $1
96+
EXECUTE my_plan
97+
98+
statement ok
99+
DEALLOCATE my_plan
100+
91101
statement ok
92102
PREPARE my_plan(STRING, STRING) AS SELECT * FROM (VALUES(1, $1), (2, $2)) AS t (num, letter);
93103

0 commit comments

Comments
 (0)