jayzhan211 commented on code in PR #26640:
URL: https://github.com/apache/datafusion/pull/26640#discussion_r4400542838


##########
datafusion/optimizer/src/decorrelate.rs:
##########
@@ -910,6 +936,34 @@ impl PullUpCorrelatedExpr {
     }
 }
 
+/// Whether the emptiness of `plan`'s output follows from the emptiness of its
+/// input alone, so that `plan` cannot tell apart two non-empty inputs. 
Removing
+/// a `Limit` below such a node changes which rows it is given, never whether 
it
+/// is given any, and so never whether the subquery is empty.
+///
+/// A `Projection`, a `SubqueryAlias`, a `Sort`, a `Window` and a `Repartition`
+/// each output one row per input row. A `DISTINCT` and a grouped `Aggregate` 
output rows
+/// exactly when their input has rows, and an `Aggregate` without a grouping
+/// always outputs its one row, so no `Aggregate` can be emptied by the rows a
+/// `Limit` held back.
+///
+/// A `Filter` is what this leaves out: it reads values, so the rows the 
`Limit`
+/// kept decide whether anything survives it. A `Join`, a `Union` and an
+/// `Unnest` can empty a non-empty input too. Every other variant is treated 
the
+/// same way, so that a new one does not silently become droppable.

Review Comment:
   Optional cleanup: a `Union` can't empty a non-empty input, because its 
output is non-empty whenever any input is. It's left off the list because the 
`Union` arm already stops the pull-up when it holds an outer reference. Maybe:
   ```diff
   -/// kept decide whether anything survives it. A `Join`, a `Union` and an
   -/// `Unnest` can empty a non-empty input too. Every other variant is 
treated the
   -/// same way, so that a new one does not silently become droppable.
   +/// kept decide whether anything survives it. A `Join` and an `Unnest` can 
empty
   +/// a non-empty input too. Every other variant is treated the same way, so 
that
   +/// a new one does not silently become droppable.
   ```



##########
datafusion/optimizer/src/decorrelate.rs:
##########
@@ -910,6 +936,34 @@ impl PullUpCorrelatedExpr {
     }
 }
 
+/// Whether the emptiness of `plan`'s output follows from the emptiness of its
+/// input alone, so that `plan` cannot tell apart two non-empty inputs. 
Removing
+/// a `Limit` below such a node changes which rows it is given, never whether 
it
+/// is given any, and so never whether the subquery is empty.
+///
+/// A `Projection`, a `SubqueryAlias`, a `Sort`, a `Window` and a `Repartition`
+/// each output one row per input row. A `DISTINCT` and a grouped `Aggregate` 
output rows
+/// exactly when their input has rows, and an `Aggregate` without a grouping
+/// always outputs its one row, so no `Aggregate` can be emptied by the rows a
+/// `Limit` held back.
+///
+/// A `Filter` is what this leaves out: it reads values, so the rows the 
`Limit`
+/// kept decide whether anything survives it. A `Join`, a `Union` and an
+/// `Unnest` can empty a non-empty input too. Every other variant is treated 
the
+/// same way, so that a new one does not silently become droppable.
+fn emptiness_follows_input(plan: &LogicalPlan) -> bool {
+    matches!(
+        plan,
+        LogicalPlan::Projection(_)
+            | LogicalPlan::SubqueryAlias(_)
+            | LogicalPlan::Sort(_)
+            | LogicalPlan::Window(_)
+            | LogicalPlan::Repartition(_)
+            | LogicalPlan::Distinct(Distinct::All(_))
+            | LogicalPlan::Aggregate(_)

Review Comment:
   @raghavyadav01 thanks for the fix! One regression I hit: an `EXISTS` with a 
`MATERIALIZED` CTE whose `LIMIT` sits at the root of the continuation now fails 
to plan, but on `main` it returns the right rows. The `Cte` arm pushes `false` 
for itself and then calls `continuation.rewrite(self)`, so the `Limit` inside 
sees a blocking node above it. A `Cte` outputs exactly its continuation's rows, 
and only the continuation is rewritten, so its emptiness follows its input.
   
   Repro (`main`: `1, 2`; this PR: `Physical plan does not support logical 
expression Exists`):
   ```sql
   SET datafusion.sql_parser.dialect = 'PostgreSQL';
   CREATE TABLE a(k INT, x INT) AS VALUES (1, 10), (2, 20), (3, 30);
   CREATE TABLE b(k INT, v INT) AS VALUES (1, 100), (1, 200), (2, 300);
   SELECT a.k FROM a WHERE EXISTS (
     WITH c AS MATERIALIZED (SELECT * FROM b)
     SELECT 1 FROM c WHERE c.k = a.k LIMIT 1);
   ```
   
   Fix:
   ```diff
                | LogicalPlan::Distinct(Distinct::All(_))
                | LogicalPlan::Aggregate(_)
   +            | LogicalPlan::MaterializedCte(MaterializedCtePlan::Cte(_))
        )
   ```
   
   Could you add these two cases to `cte_materialized.slt` (just before "A body 
that refers to the outer query…")? The first must decorrelate, and the second 
must stay refused (on `main` it wrongly returns `1`):
   ```sql
   # A LIMIT at the root of the continuation does not change whether it is 
empty.
   query I rowsort
   SELECT k FROM t WHERE EXISTS (
     WITH r AS MATERIALIZED (SELECT k AS rk FROM t)
     SELECT 1 FROM r WHERE r.rk = t.k + 1 LIMIT 1)
   ----
   1
   2
   3
   
   # A filter above the LIMIT reads the row the LIMIT kept, so it stays 
correlated.
   query error Physical plan does not support logical expression Exists
   SELECT k FROM t WHERE EXISTS (
     WITH r AS MATERIALIZED (SELECT k AS rk, v AS rv FROM t)
     SELECT 1 FROM (SELECT * FROM r WHERE r.rk = t.k LIMIT 1) s WHERE s.rv > 0)
   ```



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