Skip to content

Commit b30c200

Browse files
authored
Allow place holders like $1 in more types of queries. (#13632)
* Allow place holders in the column list Previously, a query like `SELECT $1;` would fail to generate a LogicalPlan. With this change these queries are now workable. This required creating a new LogicalPlan::validate_parametere_types that is called from Optimizer::optimize to assert that all parameter types have been inferred correctly. * Remove redundant asserts * Fix typo in comments * Add comment explaining DataType::Null * 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. * Fix expected error message
1 parent 589bfd4 commit b30c200

File tree

6 files changed

+293
-32
lines changed

6 files changed

+293
-32
lines changed

datafusion/core/tests/dataframe/mod.rs

+156-16
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 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,152 @@ 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+
];
2049+
2050+
assert_batches_eq!(expected, &df.collect().await.unwrap());
2051+
2052+
Ok(())
2053+
}
2054+
2055+
#[tokio::test]
2056+
async fn test_dataframe_placeholder_column_parameter() -> Result<()> {
2057+
let ctx = SessionContext::new();
2058+
2059+
// Creating LogicalPlans with placeholders should work
2060+
let df = ctx.read_empty().unwrap().select_exprs(&["$1"]).unwrap();
2061+
let logical_plan = df.logical_plan();
2062+
let formatted = logical_plan.display_indent_schema().to_string();
2063+
let actual: Vec<&str> = formatted.trim().lines().collect();
2064+
2065+
#[rustfmt::skip]
2066+
let expected = vec![
2067+
"Projection: $1 [$1:Null;N]",
2068+
" EmptyRelation []"
2069+
];
2070+
2071+
assert_eq!(
2072+
expected, actual,
2073+
"\n\nexpected:\n\n{expected:#?}\nactual:\n\n{actual:#?}\n\n"
2074+
);
2075+
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+
);
2084+
2085+
// Providing a parameter value should resolve the error
2086+
let df = ctx
2087+
.read_empty()
2088+
.unwrap()
2089+
.select_exprs(&["$1"])
2090+
.unwrap()
2091+
.with_param_values(vec![("1", ScalarValue::from(3i32))])
2092+
.unwrap();
2093+
2094+
let logical_plan = df.logical_plan();
2095+
let formatted = logical_plan.display_indent_schema().to_string();
2096+
let actual: Vec<&str> = formatted.trim().lines().collect();
2097+
let expected = vec![
2098+
"Projection: Int32(3) AS $1 [$1:Null;N]",
2099+
" EmptyRelation []",
2100+
];
2101+
assert_eq!(
2102+
expected, actual,
2103+
"\n\nexpected:\n\n{expected:#?}\nactual:\n\n{actual:#?}\n\n"
2104+
);
2105+
2106+
#[rustfmt::skip]
2107+
let expected = [
2108+
"+----+",
2109+
"| $1 |",
2110+
"+----+",
2111+
"| 3 |",
2112+
"+----+"
2113+
];
2114+
2115+
assert_batches_eq!(expected, &df.collect().await.unwrap());
2116+
2117+
Ok(())
2118+
}
2119+
2120+
#[tokio::test]
2121+
async fn test_dataframe_placeholder_like_expression() -> Result<()> {
2122+
let ctx = SessionContext::new();
2123+
2124+
// Creating LogicalPlans with placeholders should work
2125+
let df = ctx
2126+
.read_empty()
2127+
.unwrap()
2128+
.with_column("a", lit("foo"))
2129+
.unwrap()
2130+
.filter(col("a").like(placeholder("$1")))
2131+
.unwrap();
2132+
2133+
let logical_plan = df.logical_plan();
2134+
let formatted = logical_plan.display_indent_schema().to_string();
2135+
let actual: Vec<&str> = formatted.trim().lines().collect();
2136+
let expected = vec![
2137+
"Filter: a LIKE $1 [a:Utf8]",
2138+
" Projection: Utf8(\"foo\") AS a [a:Utf8]",
2139+
" EmptyRelation []",
2140+
];
2141+
assert_eq!(
2142+
expected, actual,
2143+
"\n\nexpected:\n\n{expected:#?}\nactual:\n\n{actual:#?}\n\n"
2144+
);
2145+
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+
);
2154+
2155+
// Providing a parameter value should resolve the error
2156+
let df = ctx
2157+
.read_empty()
2158+
.unwrap()
2159+
.with_column("a", lit("foo"))
2160+
.unwrap()
2161+
.filter(col("a").like(placeholder("$1")))
2162+
.unwrap()
2163+
.with_param_values(vec![("1", ScalarValue::from("f%"))])
2164+
.unwrap();
2165+
2166+
let logical_plan = df.logical_plan();
2167+
let formatted = logical_plan.display_indent_schema().to_string();
2168+
let actual: Vec<&str> = formatted.trim().lines().collect();
2169+
let expected = vec![
2170+
"Filter: a LIKE Utf8(\"f%\") [a:Utf8]",
2171+
" Projection: Utf8(\"foo\") AS a [a:Utf8]",
2172+
" EmptyRelation []",
2173+
];
2174+
assert_eq!(
2175+
expected, actual,
2176+
"\n\nexpected:\n\n{expected:#?}\nactual:\n\n{actual:#?}\n\n"
2177+
);
2178+
2179+
#[rustfmt::skip]
2180+
let expected = [
2181+
"+-----+",
2182+
"| a |",
2183+
"+-----+",
2184+
"| foo |",
2185+
"+-----+"
2186+
];
20452187

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

20502190
Ok(())
20512191
}

datafusion/core/tests/sql/select.rs

+92
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/expr_schema.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,13 @@ impl ExprSchemable for Expr {
215215
}) => get_result_type(&left.get_type(schema)?, op, &right.get_type(schema)?),
216216
Expr::Like { .. } | Expr::SimilarTo { .. } => Ok(DataType::Boolean),
217217
Expr::Placeholder(Placeholder { data_type, .. }) => {
218-
data_type.clone().ok_or_else(|| {
219-
plan_datafusion_err!(
220-
"Placeholder type could not be resolved. Make sure that the \
221-
placeholder is bound to a concrete type, e.g. by providing \
222-
parameter values."
223-
)
224-
})
218+
if let Some(dtype) = data_type {
219+
Ok(dtype.clone())
220+
} else {
221+
// If the placeholder's type hasn't been specified, treat it as
222+
// null (unspecified placeholders generate an error during planning)
223+
Ok(DataType::Null)
224+
}
225225
}
226226
Expr::Wildcard { .. } => Ok(DataType::Null),
227227
Expr::GroupingSet(_) => {

datafusion/physical-expr/src/planner.rs

+4-1
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/sql/tests/sql_integration.rs

-4
Original file line numberDiff line numberDiff line change
@@ -571,10 +571,6 @@ Dml: op=[Insert Into] table=[test_decimal]
571571
"INSERT INTO person (id, first_name, last_name) VALUES ($1, $2, $3, $4)",
572572
"Error during planning: Placeholder $4 refers to a non existent column"
573573
)]
574-
#[case::placeholder_type_unresolved(
575-
"INSERT INTO person (id, first_name, last_name) VALUES ($2, $4, $6)",
576-
"Error during planning: Placeholder type could not be resolved. Make sure that the placeholder is bound to a concrete type, e.g. by providing parameter values."
577-
)]
578574
#[case::placeholder_type_unresolved(
579575
"INSERT INTO person (id, first_name, last_name) VALUES ($id, $first_name, $last_name)",
580576
"Error during planning: Can't parse placeholder: $id"

datafusion/sqllogictest/test_files/prepare.slt

+34-4
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@ PREPARE my_plan(INT, INT) AS SELECT 1 + $1;
5353
statement error SQL error: ParserError
5454
PREPARE my_plan(INT) AS SELECT id, age FROM person WHERE age is $1;
5555

56-
# TODO: allow prepare without specifying data types
57-
statement error Placeholder type could not be resolved
58-
PREPARE my_plan AS SELECT $1;
59-
6056
# #######################
6157
# Test prepare and execute statements
6258

@@ -68,6 +64,40 @@ EXECUTE my_plan('Foo', 'Bar');
6864
statement error Prepared statement \'my_plan\' does not exist
6965
DEALLOCATE my_plan;
7066

67+
# Allow prepare without specifying data types
68+
statement ok
69+
PREPARE my_plan AS SELECT $1;
70+
71+
query T
72+
EXECUTE my_plan('Foo');
73+
----
74+
Foo
75+
76+
statement ok
77+
DEALLOCATE my_plan
78+
79+
# Allow prepare col LIKE $1
80+
statement ok
81+
PREPARE my_plan AS SELECT * FROM person WHERE first_name LIKE $1;
82+
83+
query ITTITRPI rowsort
84+
EXECUTE my_plan('j%');
85+
----
86+
1 jane smith 20 MA 100000.45 2000-11-12T00:00:00 99
87+
88+
statement ok
89+
DEALLOCATE my_plan
90+
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+
71101
statement ok
72102
PREPARE my_plan(STRING, STRING) AS SELECT * FROM (VALUES(1, $1), (2, $2)) AS t (num, letter);
73103

0 commit comments

Comments
 (0)