You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by gi...@apache.org on 2026/05/21 19:14:05 UTC
(datafusion) 01/01: Fix: Infer placeholder type from subquery (#22436)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-22436-50d74a704d7f35a6cc184251a8f1236cd94de684
in repository https://gitbox.apache.org/repos/asf/datafusion.git
commit 44027bc40eb479ac81d97cc07f656734cefb7139
Author: H <25...@users.noreply.github.com>
AuthorDate: Thu May 21 15:13:41 2026 -0400
Fix: Infer placeholder type from subquery (#22436)
## Which issue does this PR close?
Closes #15979.
## Rationale for this change
`$1 IN (SELECT ...)` left the placeholder untyped because
`infer_placeholder_types` had no arm for `InSubquery` and made
`get_parameter_types()` return `None` for these placeholders.
## What changes are included in this PR?
Adds the `InSubquery` arm to `infer_placeholder_types`, reading the type
from the subquery's projected column. Covers both `IN` and `NOT IN`.
## How are these changes tested?
Unit tests for `IN` and `NOT IN` placeholder inference, plus end-to-end
sqllogictests with `PREPARE`/`EXECUTE`.
## Are there any user-facing changes?
No.
---
datafusion/expr/src/expr.rs | 113 +++++++++++++++++++++++++
datafusion/sqllogictest/test_files/prepare.slt | 32 +++++++
2 files changed, 145 insertions(+)
diff --git a/datafusion/expr/src/expr.rs b/datafusion/expr/src/expr.rs
index d6276b944c..244d459c9b 100644
--- a/datafusion/expr/src/expr.rs
+++ b/datafusion/expr/src/expr.rs
@@ -2186,6 +2186,17 @@ impl Expr {
rewrite_placeholder(item, expr.as_ref(), schema)?;
}
}
+ Expr::InSubquery(InSubquery {
+ expr,
+ subquery,
+ negated: _,
+ }) => {
+ let subquery_schema = subquery.subquery.schema();
+ let column = Expr::Column(Column::new_unqualified(
+ subquery_schema.fields()[0].name().clone(),
+ ));
+ rewrite_placeholder(expr.as_mut(), &column, subquery_schema)?;
+ }
Expr::Like(Like { expr, pattern, .. })
| Expr::SimilarTo(Like { expr, pattern, .. }) => {
rewrite_placeholder(pattern.as_mut(), expr.as_ref(), schema)?;
@@ -3817,6 +3828,108 @@ mod test {
}
}
+ #[test]
+ fn infer_placeholder_in_subquery() {
+ // WHERE $1 IN (SELECT a FROM t)
+ let subquery_field = Field::new("a", DataType::Int32, false);
+ let subquery_schema = Arc::new(
+ DFSchema::from_unqualified_fields(
+ vec![subquery_field].into(),
+ Default::default(),
+ )
+ .unwrap(),
+ );
+ let subquery = Subquery {
+ subquery: Arc::new(LogicalPlan::EmptyRelation(EmptyRelation {
+ produce_one_row: false,
+ schema: subquery_schema,
+ })),
+ outer_ref_columns: vec![],
+ spans: Spans::new(),
+ };
+
+ let in_subquery = Expr::InSubquery(InSubquery {
+ expr: Box::new(Expr::Placeholder(Placeholder {
+ id: "$1".to_string(),
+ field: None,
+ })),
+ subquery,
+ negated: false,
+ });
+
+ let outer_schema = DFSchema::empty();
+ let (inferred_expr, contains_placeholder) =
+ in_subquery.infer_placeholder_types(&outer_schema).unwrap();
+
+ assert!(contains_placeholder);
+
+ match inferred_expr {
+ Expr::InSubquery(in_subquery) => match *in_subquery.expr {
+ Expr::Placeholder(placeholder) => {
+ let inferred = placeholder.field.expect("placeholder field");
+ assert_eq!(inferred.data_type(), &DataType::Int32);
+ assert!(inferred.is_nullable());
+ }
+ _ => panic!("Expected Placeholder expression in InSubquery"),
+ },
+ _ => panic!("Expected InSubquery expression"),
+ }
+ }
+
+ #[test]
+ fn infer_placeholder_not_in_subquery() {
+ // WHERE $1 NOT IN (SELECT a FROM t)
+ let subquery_field = Field::new("a", DataType::Int32, false);
+ let subquery_schema = Arc::new(
+ DFSchema::from_unqualified_fields(
+ vec![subquery_field].into(),
+ Default::default(),
+ )
+ .unwrap(),
+ );
+ let subquery = Subquery {
+ subquery: Arc::new(LogicalPlan::EmptyRelation(EmptyRelation {
+ produce_one_row: false,
+ schema: subquery_schema,
+ })),
+ outer_ref_columns: vec![],
+ spans: Spans::new(),
+ };
+
+ let not_in_subquery = Expr::InSubquery(InSubquery {
+ expr: Box::new(Expr::Placeholder(Placeholder {
+ id: "$1".to_string(),
+ field: None,
+ })),
+ subquery,
+ negated: true,
+ });
+
+ let outer_schema = DFSchema::empty();
+ let (inferred_expr, contains_placeholder) = not_in_subquery
+ .infer_placeholder_types(&outer_schema)
+ .unwrap();
+
+ assert!(contains_placeholder);
+
+ match inferred_expr {
+ Expr::InSubquery(in_subquery) => {
+ assert!(in_subquery.negated, "negated flag must be preserved");
+ match *in_subquery.expr {
+ Expr::Placeholder(placeholder) => {
+ let inferred = placeholder.field.expect("placeholder field");
+ assert_eq!(inferred.data_type(), &DataType::Int32);
+ assert!(inferred.is_nullable());
+ }
+ _ => {
+ panic!("Expected Placeholder expression in InSubquery")
+ }
+ }
+ }
+ _ => panic!("Expected InSubquery expression"),
+ }
+ }
+
#[test]
fn infer_placeholder_like_and_similar_to() {
// name LIKE $1
diff --git a/datafusion/sqllogictest/test_files/prepare.slt b/datafusion/sqllogictest/test_files/prepare.slt
index 16e41834a3..bf91d95d5d 100644
--- a/datafusion/sqllogictest/test_files/prepare.slt
+++ b/datafusion/sqllogictest/test_files/prepare.slt
@@ -107,6 +107,38 @@ EXECUTE my_plan('j%');
statement ok
DEALLOCATE my_plan
+# Allow prepare $1 IN (subquery)
+statement ok
+PREPARE my_plan AS SELECT id FROM person WHERE $1 IN (SELECT age FROM person);
+
+query I rowsort
+EXECUTE my_plan(20);
+----
+1
+
+query I rowsort
+EXECUTE my_plan(99);
+----
+
+statement ok
+DEALLOCATE my_plan
+
+# Allow prepare $1 NOT IN (subquery)
+statement ok
+PREPARE my_plan AS SELECT id FROM person WHERE $1 NOT IN (SELECT age FROM person);
+
+query I rowsort
+EXECUTE my_plan(99);
+----
+1
+
+query I rowsort
+EXECUTE my_plan(20);
+----
+
+statement ok
+DEALLOCATE my_plan
+
# Check for missing parameters
statement ok
PREPARE my_plan AS SELECT * FROM person WHERE id < $1;
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org