Skip to content

Commit 3d45e42

Browse files
authored
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.
1 parent 2a19282 commit 3d45e42

2 files changed

Lines changed: 160 additions & 0 deletions

File tree

datafusion/expr/src/expr.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2186,6 +2186,32 @@ impl Expr {
21862186
rewrite_placeholder(item, expr.as_ref(), schema)?;
21872187
}
21882188
}
2189+
Expr::InSubquery(InSubquery {
2190+
expr,
2191+
subquery,
2192+
negated: _,
2193+
}) => {
2194+
let subquery_schema = subquery.subquery.schema();
2195+
match &subquery_schema.fields()[..] {
2196+
[subquery_field] => {
2197+
let column = Expr::Column(Column::new_unqualified(
2198+
subquery_field.name().clone(),
2199+
));
2200+
rewrite_placeholder(
2201+
expr.as_mut(),
2202+
&column,
2203+
subquery_schema,
2204+
)?;
2205+
}
2206+
_ => {
2207+
return plan_err!(
2208+
"InSubquery should only return one column, but found {}: {}",
2209+
subquery_schema.fields().len(),
2210+
subquery_schema.field_names().join(", ")
2211+
);
2212+
}
2213+
}
2214+
}
21892215
Expr::Like(Like { expr, pattern, .. })
21902216
| Expr::SimilarTo(Like { expr, pattern, .. }) => {
21912217
rewrite_placeholder(pattern.as_mut(), expr.as_ref(), schema)?;
@@ -3817,6 +3843,108 @@ mod test {
38173843
}
38183844
}
38193845

3846+
#[test]
3847+
fn infer_placeholder_in_subquery() {
3848+
// WHERE $1 IN (SELECT a FROM t)
3849+
let subquery_field = Field::new("a", DataType::Int32, false);
3850+
let subquery_schema = Arc::new(
3851+
DFSchema::from_unqualified_fields(
3852+
vec![subquery_field].into(),
3853+
Default::default(),
3854+
)
3855+
.unwrap(),
3856+
);
3857+
let subquery = Subquery {
3858+
subquery: Arc::new(LogicalPlan::EmptyRelation(EmptyRelation {
3859+
produce_one_row: false,
3860+
schema: subquery_schema,
3861+
})),
3862+
outer_ref_columns: vec![],
3863+
spans: Spans::new(),
3864+
};
3865+
3866+
let in_subquery = Expr::InSubquery(InSubquery {
3867+
expr: Box::new(Expr::Placeholder(Placeholder {
3868+
id: "$1".to_string(),
3869+
field: None,
3870+
})),
3871+
subquery,
3872+
negated: false,
3873+
});
3874+
3875+
let outer_schema = DFSchema::empty();
3876+
let (inferred_expr, contains_placeholder) =
3877+
in_subquery.infer_placeholder_types(&outer_schema).unwrap();
3878+
3879+
assert!(contains_placeholder);
3880+
3881+
match inferred_expr {
3882+
Expr::InSubquery(in_subquery) => match *in_subquery.expr {
3883+
Expr::Placeholder(placeholder) => {
3884+
let inferred = placeholder.field.expect("placeholder field");
3885+
assert_eq!(inferred.data_type(), &DataType::Int32);
3886+
assert!(inferred.is_nullable());
3887+
}
3888+
_ => panic!("Expected Placeholder expression in InSubquery"),
3889+
},
3890+
_ => panic!("Expected InSubquery expression"),
3891+
}
3892+
}
3893+
3894+
#[test]
3895+
fn infer_placeholder_not_in_subquery() {
3896+
// WHERE $1 NOT IN (SELECT a FROM t)
3897+
let subquery_field = Field::new("a", DataType::Int32, false);
3898+
let subquery_schema = Arc::new(
3899+
DFSchema::from_unqualified_fields(
3900+
vec![subquery_field].into(),
3901+
Default::default(),
3902+
)
3903+
.unwrap(),
3904+
);
3905+
let subquery = Subquery {
3906+
subquery: Arc::new(LogicalPlan::EmptyRelation(EmptyRelation {
3907+
produce_one_row: false,
3908+
schema: subquery_schema,
3909+
})),
3910+
outer_ref_columns: vec![],
3911+
spans: Spans::new(),
3912+
};
3913+
3914+
let not_in_subquery = Expr::InSubquery(InSubquery {
3915+
expr: Box::new(Expr::Placeholder(Placeholder {
3916+
id: "$1".to_string(),
3917+
field: None,
3918+
})),
3919+
subquery,
3920+
negated: true,
3921+
});
3922+
3923+
let outer_schema = DFSchema::empty();
3924+
let (inferred_expr, contains_placeholder) = not_in_subquery
3925+
.infer_placeholder_types(&outer_schema)
3926+
.unwrap();
3927+
3928+
assert!(contains_placeholder);
3929+
3930+
match inferred_expr {
3931+
Expr::InSubquery(in_subquery) => {
3932+
assert!(in_subquery.negated, "negated flag must be preserved");
3933+
match *in_subquery.expr {
3934+
Expr::Placeholder(placeholder) => {
3935+
let inferred = placeholder.field.expect("placeholder field");
3936+
assert_eq!(inferred.data_type(), &DataType::Int32);
3937+
assert!(inferred.is_nullable());
3938+
}
3939+
_ => {
3940+
panic!("Expected Placeholder expression in InSubquery")
3941+
}
3942+
}
3943+
}
3944+
_ => panic!("Expected InSubquery expression"),
3945+
}
3946+
}
3947+
38203948
#[test]
38213949
fn infer_placeholder_like_and_similar_to() {
38223950
// name LIKE $1

datafusion/sqllogictest/test_files/prepare.slt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,38 @@ EXECUTE my_plan('j%');
107107
statement ok
108108
DEALLOCATE my_plan
109109

110+
# Allow prepare $1 IN (subquery)
111+
statement ok
112+
PREPARE my_plan AS SELECT id FROM person WHERE $1 IN (SELECT age FROM person);
113+
114+
query I rowsort
115+
EXECUTE my_plan(20);
116+
----
117+
1
118+
119+
query I rowsort
120+
EXECUTE my_plan(99);
121+
----
122+
123+
statement ok
124+
DEALLOCATE my_plan
125+
126+
# Allow prepare $1 NOT IN (subquery)
127+
statement ok
128+
PREPARE my_plan AS SELECT id FROM person WHERE $1 NOT IN (SELECT age FROM person);
129+
130+
query I rowsort
131+
EXECUTE my_plan(99);
132+
----
133+
1
134+
135+
query I rowsort
136+
EXECUTE my_plan(20);
137+
----
138+
139+
statement ok
140+
DEALLOCATE my_plan
141+
110142
# Check for missing parameters
111143
statement ok
112144
PREPARE my_plan AS SELECT * FROM person WHERE id < $1;

0 commit comments

Comments
 (0)