feat: basic support for executing prepared statements#13242
feat: basic support for executing prepared statements#13242jonahgao merged 11 commits intoapache:mainfrom
Conversation
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn prepared_statement_invalid_types() -> Result<()> { |
There was a problem hiding this comment.
Remove this test because now parameters will be cast to the target type.
This is also the behavior of PostgreSQL.
psql=> prepare p(int) as select 100+$1;
PREPARE
psql=> execute p('100');
?column?
----------
200
(1 row)
psql=> execute p(20.12);
?column?
----------
120
(1 row)| // sql to statement then to prepare logical plan with parameters | ||
| // c1 defined as UINT32, c2 defined as UInt64 but the params are Int32 and Float64 | ||
| let dataframe = | ||
| ctx.sql("PREPARE my_plan(INT, DOUBLE) AS SELECT c1, c2 FROM test WHERE c1 > $2 AND c1 < $1").await?; |
There was a problem hiding this comment.
PREPARE now returns an empty DataFrame and can't call with_param_values on it. I think this change is reasonable because the result of PREPARE is not a real relation, as it doesn't contain valid rows. Its result is more like DDL.
There was a problem hiding this comment.
I agree -- PREPARE is a statement in my mind (and has no results)
alamb
left a comment
There was a problem hiding this comment.
Thank you so much @jonahgao -- this PR looks basically perfect. My only concern is the security implications of executing prepared statements when the SQL is defined to be "read only"
It would also be neat to have some way to deallocate prepared statements, but that would be a natural follow on ticket (perhaps adding support for DEALLOCATE for example)
| self.set_variable(stmt).await | ||
| } | ||
|
|
||
| LogicalPlan::Prepare(Prepare { |
There was a problem hiding this comment.
I am worried that LogicalPlan::Prepare seems to be runnable even if we run sql_with_options and disable statements.
datafusion/datafusion/core/src/execution/context/mod.rs
Lines 605 to 614 in aad7744
I realize that this PR does not change if Prepare is a statement or not, but it is the first that actually makes Prepare do something
Could you please add a test like this (but for PREPARE statements) and make sure the statement can't be executed if statements are disabled?
datafusion/datafusion/core/tests/sql/sql_api.rs
Lines 99 to 114 in 6692382
Perhaps (as a follow on PR) we can make LogicalPlan::Prepare a statement https://docs.rs/datafusion/latest/datafusion/logical_expr/enum.Statement.html
There was a problem hiding this comment.
Temporarily added PREPARE to BadPlanVisitor and added a test for this in 4729766
I will make LogicalPlan::Prepare a statement in a follow-up PR.
| .collect::<Result<_>>()?; | ||
| } | ||
|
|
||
| let params = ParamValues::List(params); |
| Ok(()) | ||
| } | ||
| Entry::Occupied(e) => { | ||
| exec_err!("Prepared statement '{}' already exists", e.key()) |
There was a problem hiding this comment.
Since this errors if an existing prepared statement should we add some way to erase / clear prepared plans? Now there is no way to avoid accumulating them over time
There was a problem hiding this comment.
I plan to implement DEALLOCATE in next PR.
| // sql to statement then to prepare logical plan with parameters | ||
| // c1 defined as UINT32, c2 defined as UInt64 but the params are Int32 and Float64 | ||
| let dataframe = | ||
| ctx.sql("PREPARE my_plan(INT, DOUBLE) AS SELECT c1, c2 FROM test WHERE c1 > $2 AND c1 < $1").await?; |
There was a problem hiding this comment.
I agree -- PREPARE is a statement in my mind (and has no results)
|
|
||
| # Syntax error: no name specified after the keyword prepare | ||
| statement error | ||
| statement error DataFusion error: SQL error: ParserError |
| 2 Bar | ||
|
|
||
|
|
||
| # Test issue: https://github.com/apache/datafusion/issues/12294 |
| plan_err!("Statement not supported: {}", stmt.name()) | ||
| } | ||
| // TODO: Implement PREPARE as a LogicalPlan::Statement | ||
| LogicalPlan::Prepare(_) if !self.options.allow_statements => { |
|
Thanks @alamb for the review. |
Which issue does this PR close?
Closes #4549.
Rationale for this change
Store the logical plan of the prepared statement in the session state. During
EXECUTE, retrieve the prepared logical plan, replace the placeholders with execution parameters, and then execute.What changes are included in this PR?
Support executing prepared statements.
Are these changes tested?
Yes
Are there any user-facing changes?
Yes.
ctx.sql("Prepare p(int) as query")now returns an empty DataFrame, similar to Create View and other DDL statements. Previously, it would return a DataFrame containing the Prepare logical plan.