martin-g commented on code in PR #24657:
URL: https://github.com/apache/datafusion/pull/24657#discussion_r4060511160
##########
datafusion/core/src/physical_planner.rs:
##########
@@ -2246,6 +2226,149 @@ fn get_physical_expr_pair(
Ok((physical_expr, physical_name))
}
+/// How a DELETE or an UPDATE reaches its target table.
+///
+/// The `filters` argument of [`TableProvider::delete_from`] and
+/// [`TableProvider::update`] is the only channel that carries the `WHERE`
clause
+/// to the provider, and an empty vector means "no `WHERE` clause, so every
row".
+/// A plan whose row restriction cannot travel through that channel must
+/// therefore never reach the provider.
+///
+/// [`TableProvider::delete_from`]:
datafusion_catalog::TableProvider::delete_from
+/// [`TableProvider::update`]: datafusion_catalog::TableProvider::update
+enum DmlInput {
+ /// Every row restriction of the statement reaches the provider as a
filter.
+ Filters,
+ /// No row matches, so the statement affects no rows and the provider is
not
+ /// called at all.
+ NoRows,
+}
+
+/// Collect the table references that a predicate of a DELETE or an UPDATE may
+/// name: the target table itself, and the alias of every scan of the target
+/// table in the input plan.
+///
+/// Both [`classify_dml_input`] and [`extract_dml_filters`] need this set, so
the
+/// caller collects it once and passes it to each of them.
+fn collect_dml_target_refs(
+ input: &Arc<LogicalPlan>,
+ target: &TableReference,
+) -> Result<Vec<TableReference>> {
+ let mut allowed_refs = vec![target.clone()];
+ input.apply(|node| {
+ if let LogicalPlan::SubqueryAlias(alias) = node
+ // Check if this alias points to the target table
+ && let LogicalPlan::TableScan(scan) = alias.input.as_ref()
+ && scan.table_name.resolved_eq(target)
+ {
+ allowed_refs.push(TableReference::bare(alias.alias.to_string()));
+ }
+ Ok(TreeNodeRecursion::Continue)
+ })?;
+ Ok(allowed_refs)
+}
+
+/// Check that the input plan of a DELETE or an UPDATE can reach the table
+/// provider without losing part of its `WHERE` clause.
+///
+/// The optimizer rewrites an `IN` or an `EXISTS` subquery into a semi join,
and
+/// it folds an always-false predicate into an empty relation. In both cases
the
+/// condition leaves the `Filter` nodes that [`extract_dml_filters`] reads, and
+/// the provider would see an empty filter list and change every row.
+///
+/// # Parameters
+/// - `input`: the input plan of the DELETE or the UPDATE
+/// - `target`: the target table of the statement
+/// - `allowed_refs`: the target table and its aliases, from
[`collect_dml_target_refs`]
+/// - `op`: `"DELETE"` or `"UPDATE"`, used in the error message
+///
+/// # Returns
+/// * [`DmlInput::Filters`] when the provider may be called
+/// * [`DmlInput::NoRows`] when the statement matches no row
+/// * a "not implemented" error when part of the `WHERE` clause cannot reach
the provider.
+fn classify_dml_input(
+ input: &Arc<LogicalPlan>,
+ target: &TableReference,
+ allowed_refs: &[TableReference],
+ op: &str,
+) -> Result<DmlInput> {
+ let mut result = DmlInput::Filters;
+ input.apply(|node| {
+ match node {
+ // An empty relation means the optimizer proved that no row
matches,
+ // so the statement affects no rows.
+ LogicalPlan::EmptyRelation(empty) if !empty.produce_one_row => {
+ result = DmlInput::NoRows;
+ return Ok(TreeNodeRecursion::Stop);
+ }
+ // A join carries the condition in its `on` clause, where
+ // `extract_dml_filters` cannot read it. The optimizer builds one
for
+ // an `IN` or an `EXISTS` subquery.
+ LogicalPlan::Join(join) => {
+ return not_impl_err!(
+ "{op} on table '{target}' with an IN or an EXISTS subquery
in its \
+ WHERE clause is not supported: the optimizer rewrites the
subquery \
+ into a {} join, and the condition does not reach the
table provider",
+ join.join_type
+ );
+ }
+ LogicalPlan::Filter(filter) => {
+ // A predicate on another table restricts the rows of the
target
+ // table, and the provider cannot evaluate it.
+ for predicate in split_conjunction(&filter.predicate) {
+ if !predicate_is_on_target_multi(predicate, allowed_refs)?
{
+ return not_impl_err!(
+ "{op} on table '{target}' with a WHERE clause that
\
+ references another table is not supported"
+ );
+ }
+ }
+ }
+ // Plans that pass every row of the target table through, or that
+ // hold no row restriction of their own.
+ LogicalPlan::TableScan(_)
Review Comment:
This should probably check that the TableScan's `table_name` is the same as
the passed `target`.
```rust
LogicalPlan::TableScan(scan) => {
if !scan.table_name.resolved_eq(target) {
return not_impl_err!(
"{op} on table '{target}' with a scan of another table is not
supported"
);
}
```
##########
datafusion/core/src/physical_planner.rs:
##########
@@ -2246,6 +2226,149 @@ fn get_physical_expr_pair(
Ok((physical_expr, physical_name))
}
+/// How a DELETE or an UPDATE reaches its target table.
+///
+/// The `filters` argument of [`TableProvider::delete_from`] and
+/// [`TableProvider::update`] is the only channel that carries the `WHERE`
clause
+/// to the provider, and an empty vector means "no `WHERE` clause, so every
row".
+/// A plan whose row restriction cannot travel through that channel must
+/// therefore never reach the provider.
+///
+/// [`TableProvider::delete_from`]:
datafusion_catalog::TableProvider::delete_from
+/// [`TableProvider::update`]: datafusion_catalog::TableProvider::update
+enum DmlInput {
+ /// Every row restriction of the statement reaches the provider as a
filter.
+ Filters,
+ /// No row matches, so the statement affects no rows and the provider is
not
+ /// called at all.
+ NoRows,
+}
+
+/// Collect the table references that a predicate of a DELETE or an UPDATE may
+/// name: the target table itself, and the alias of every scan of the target
+/// table in the input plan.
+///
+/// Both [`classify_dml_input`] and [`extract_dml_filters`] need this set, so
the
+/// caller collects it once and passes it to each of them.
+fn collect_dml_target_refs(
+ input: &Arc<LogicalPlan>,
+ target: &TableReference,
+) -> Result<Vec<TableReference>> {
+ let mut allowed_refs = vec![target.clone()];
+ input.apply(|node| {
+ if let LogicalPlan::SubqueryAlias(alias) = node
+ // Check if this alias points to the target table
+ && let LogicalPlan::TableScan(scan) = alias.input.as_ref()
+ && scan.table_name.resolved_eq(target)
+ {
+ allowed_refs.push(TableReference::bare(alias.alias.to_string()));
+ }
+ Ok(TreeNodeRecursion::Continue)
+ })?;
+ Ok(allowed_refs)
+}
+
+/// Check that the input plan of a DELETE or an UPDATE can reach the table
+/// provider without losing part of its `WHERE` clause.
+///
+/// The optimizer rewrites an `IN` or an `EXISTS` subquery into a semi join,
and
+/// it folds an always-false predicate into an empty relation. In both cases
the
+/// condition leaves the `Filter` nodes that [`extract_dml_filters`] reads, and
+/// the provider would see an empty filter list and change every row.
+///
+/// # Parameters
+/// - `input`: the input plan of the DELETE or the UPDATE
+/// - `target`: the target table of the statement
+/// - `allowed_refs`: the target table and its aliases, from
[`collect_dml_target_refs`]
+/// - `op`: `"DELETE"` or `"UPDATE"`, used in the error message
+///
+/// # Returns
+/// * [`DmlInput::Filters`] when the provider may be called
+/// * [`DmlInput::NoRows`] when the statement matches no row
+/// * a "not implemented" error when part of the `WHERE` clause cannot reach
the provider.
+fn classify_dml_input(
+ input: &Arc<LogicalPlan>,
+ target: &TableReference,
+ allowed_refs: &[TableReference],
+ op: &str,
+) -> Result<DmlInput> {
+ let mut result = DmlInput::Filters;
+ input.apply(|node| {
+ match node {
+ // An empty relation means the optimizer proved that no row
matches,
+ // so the statement affects no rows.
+ LogicalPlan::EmptyRelation(empty) if !empty.produce_one_row => {
+ result = DmlInput::NoRows;
+ return Ok(TreeNodeRecursion::Stop);
+ }
+ // A join carries the condition in its `on` clause, where
+ // `extract_dml_filters` cannot read it. The optimizer builds one
for
+ // an `IN` or an `EXISTS` subquery.
+ LogicalPlan::Join(join) => {
Review Comment:
This will lead to an error for any JOIN even if there is no `IN or EXISTS`
optimized to a `join`. The error message will be wrong in this case.
##########
datafusion/core/src/physical_planner.rs:
##########
@@ -2246,6 +2226,149 @@ fn get_physical_expr_pair(
Ok((physical_expr, physical_name))
}
+/// How a DELETE or an UPDATE reaches its target table.
+///
+/// The `filters` argument of [`TableProvider::delete_from`] and
+/// [`TableProvider::update`] is the only channel that carries the `WHERE`
clause
+/// to the provider, and an empty vector means "no `WHERE` clause, so every
row".
+/// A plan whose row restriction cannot travel through that channel must
+/// therefore never reach the provider.
+///
+/// [`TableProvider::delete_from`]:
datafusion_catalog::TableProvider::delete_from
+/// [`TableProvider::update`]: datafusion_catalog::TableProvider::update
+enum DmlInput {
+ /// Every row restriction of the statement reaches the provider as a
filter.
+ Filters,
+ /// No row matches, so the statement affects no rows and the provider is
not
+ /// called at all.
+ NoRows,
+}
+
+/// Collect the table references that a predicate of a DELETE or an UPDATE may
+/// name: the target table itself, and the alias of every scan of the target
+/// table in the input plan.
+///
+/// Both [`classify_dml_input`] and [`extract_dml_filters`] need this set, so
the
+/// caller collects it once and passes it to each of them.
+fn collect_dml_target_refs(
+ input: &Arc<LogicalPlan>,
+ target: &TableReference,
+) -> Result<Vec<TableReference>> {
+ let mut allowed_refs = vec![target.clone()];
+ input.apply(|node| {
+ if let LogicalPlan::SubqueryAlias(alias) = node
+ // Check if this alias points to the target table
+ && let LogicalPlan::TableScan(scan) = alias.input.as_ref()
+ && scan.table_name.resolved_eq(target)
+ {
+ allowed_refs.push(TableReference::bare(alias.alias.to_string()));
+ }
+ Ok(TreeNodeRecursion::Continue)
+ })?;
+ Ok(allowed_refs)
+}
+
+/// Check that the input plan of a DELETE or an UPDATE can reach the table
+/// provider without losing part of its `WHERE` clause.
+///
+/// The optimizer rewrites an `IN` or an `EXISTS` subquery into a semi join,
and
+/// it folds an always-false predicate into an empty relation. In both cases
the
+/// condition leaves the `Filter` nodes that [`extract_dml_filters`] reads, and
+/// the provider would see an empty filter list and change every row.
+///
+/// # Parameters
+/// - `input`: the input plan of the DELETE or the UPDATE
+/// - `target`: the target table of the statement
+/// - `allowed_refs`: the target table and its aliases, from
[`collect_dml_target_refs`]
+/// - `op`: `"DELETE"` or `"UPDATE"`, used in the error message
+///
+/// # Returns
+/// * [`DmlInput::Filters`] when the provider may be called
+/// * [`DmlInput::NoRows`] when the statement matches no row
+/// * a "not implemented" error when part of the `WHERE` clause cannot reach
the provider.
+fn classify_dml_input(
+ input: &Arc<LogicalPlan>,
+ target: &TableReference,
+ allowed_refs: &[TableReference],
+ op: &str,
+) -> Result<DmlInput> {
+ let mut result = DmlInput::Filters;
+ input.apply(|node| {
+ match node {
+ // An empty relation means the optimizer proved that no row
matches,
+ // so the statement affects no rows.
+ LogicalPlan::EmptyRelation(empty) if !empty.produce_one_row => {
+ result = DmlInput::NoRows;
+ return Ok(TreeNodeRecursion::Stop);
+ }
+ // A join carries the condition in its `on` clause, where
+ // `extract_dml_filters` cannot read it. The optimizer builds one
for
+ // an `IN` or an `EXISTS` subquery.
+ LogicalPlan::Join(join) => {
+ return not_impl_err!(
+ "{op} on table '{target}' with an IN or an EXISTS subquery
in its \
+ WHERE clause is not supported: the optimizer rewrites the
subquery \
+ into a {} join, and the condition does not reach the
table provider",
+ join.join_type
+ );
+ }
+ LogicalPlan::Filter(filter) => {
+ // A predicate on another table restricts the rows of the
target
+ // table, and the provider cannot evaluate it.
+ for predicate in split_conjunction(&filter.predicate) {
+ if !predicate_is_on_target_multi(predicate, allowed_refs)?
{
+ return not_impl_err!(
+ "{op} on table '{target}' with a WHERE clause that
\
+ references another table is not supported"
+ );
+ }
+ }
+ }
+ // Plans that pass every row of the target table through, or that
+ // hold no row restriction of their own.
+ LogicalPlan::TableScan(_)
Review Comment:
Also the TableScan's `filters` should be in the allowed refs.
```rust
for filter in &scan.filters {
if !predicate_is_on_target_multi(filter, allowed_refs)? {
return not_impl_err!(
"{op} on table '{target}' with a scan filter \
that references another table is not supported"
);
}
}
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]