michaelsembwever commented on code in PR #24657:
URL: https://github.com/apache/datafusion/pull/24657#discussion_r3945262636


##########
datafusion/core/src/physical_planner.rs:
##########
@@ -2203,6 +2220,132 @@ 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,
+}
+
+/// 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
+/// - `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, and a "not implemented" error when part 
of
+/// the `WHERE` clause cannot reach the provider.
+fn classify_dml_input(
+    input: &Arc<LogicalPlan>,
+    target: &TableReference,
+    op: &str,
+) -> Result<DmlInput> {
+    let mut allowed_refs = vec![target.clone()];
+    input.apply(|node| {
+        if let LogicalPlan::SubqueryAlias(alias) = node
+            && 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)
+    })?;
+
+    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(_)
+            | LogicalPlan::Projection(_)
+            | LogicalPlan::SubqueryAlias(_)
+            | LogicalPlan::Sort(_)
+            | LogicalPlan::Repartition(_)
+            // A `Limit` reaches the provider as no filter at all, so a DELETE
+            // ignores it. That is a separate gap, kept as it is here.

Review Comment:
   The code comment at datafusion/core/src/physical_planner.rs:2334 now states 
that effect precisely instead of calling it "a separate gap".
   
   Note:
   - `DELETE FROM t LIMIT 1` on rows (1),(2),(3) reports 3 and empties the 
table; `DELETE FROM u WHERE column1 > 1 LIMIT 1` reports 2 and deletes both 
matching rows;
   - the gap is `DELETE` only. `UPDATE ... LIMIT` is already rejected at 
planning (datafusion/sql/src/statement.rs:1168), and `DELETE ... ORDER BY` too, 
so `DELETE ... LIMIT n` names no row order. 
   
   Created issue https://github.com/apache/datafusion/issues/24998



-- 
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]

Reply via email to