alamb commented on code in PR #16362:
URL: https://github.com/apache/datafusion/pull/16362#discussion_r2160308484


##########
datafusion/optimizer/src/simplify_predicates.rs:
##########
@@ -0,0 +1,194 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use datafusion_common::{Column, Result, ScalarValue};
+use datafusion_expr::{BinaryExpr, Cast, Expr, Operator};
+use std::collections::BTreeMap;
+
+pub(crate) fn simplify_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {
+    // Early return for simple cases
+    if predicates.len() <= 1 {
+        return Ok(predicates);
+    }
+
+    // Group predicates by their column reference
+    let mut column_predicates: BTreeMap<Column, Vec<Expr>> = BTreeMap::new();
+    let mut other_predicates = Vec::new();
+
+    for pred in predicates {
+        match &pred {
+            Expr::BinaryExpr(BinaryExpr {
+                left,
+                op:
+                    Operator::Gt
+                    | Operator::GtEq
+                    | Operator::Lt
+                    | Operator::LtEq
+                    | Operator::Eq,
+                right,
+            }) => {
+                let left_col = extract_column_from_expr(left);
+                let right_col = extract_column_from_expr(right);
+                let left_lit = left.is_literal();
+                let right_lit = right.is_literal();
+                if let (Some(col), true) = (&left_col, right_lit) {
+                    
column_predicates.entry(col.clone()).or_default().push(pred);
+                } else if let (true, Some(col)) = (left_lit, &right_col) {
+                    
column_predicates.entry(col.clone()).or_default().push(pred);
+                } else {
+                    other_predicates.push(pred);
+                }
+            }
+            _ => other_predicates.push(pred),
+        }
+    }
+
+    // Process each column's predicates to remove redundancies
+    let mut result = other_predicates;
+    for (_, preds) in column_predicates {
+        let simplified = simplify_column_predicates(preds)?;
+        result.extend(simplified);
+    }
+
+    Ok(result)
+}
+
+fn simplify_column_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {
+    if predicates.len() <= 1 {
+        return Ok(predicates);
+    }
+
+    // Group by operator type, but combining similar operators
+    let mut greater_predicates = Vec::new(); // Combines > and >=
+    let mut less_predicates = Vec::new(); // Combines < and <=
+    let mut eq_predicates = Vec::new();
+
+    for pred in predicates {
+        match &pred {
+            Expr::BinaryExpr(BinaryExpr { left: _, op, right }) => {
+                let right_is_literal = right.is_literal();
+                match (op, right_is_literal) {
+                    (Operator::Gt, true)
+                    | (Operator::Lt, false)
+                    | (Operator::GtEq, true)
+                    | (Operator::LtEq, false) => greater_predicates.push(pred),
+                    (Operator::Lt, true)
+                    | (Operator::Gt, false)
+                    | (Operator::LtEq, true)
+                    | (Operator::GtEq, false) => less_predicates.push(pred),
+                    (Operator::Eq, _) => eq_predicates.push(pred),
+                    _ => unreachable!("Unexpected operator: {}", op),
+                }
+            }
+            _ => unreachable!("Unexpected predicate {}", pred.to_string()),
+        }
+    }
+
+    let mut result = Vec::new();
+
+    // If we have equality predicates, they're the most restrictive
+    if !eq_predicates.is_empty() {
+        if eq_predicates.len() > 1 {

Review Comment:
   I don't think this is true if there are multiple predicates with the same 
constant, such as `a = 5 AND a = 5`
   
   In actual queries I think the same predicates should perhaps have already 
been deduplicated by the expr simplifier, but that isn't stated anywhere nor 
checked 🤔 
   
   



##########
datafusion/optimizer/src/simplify_predicates.rs:
##########
@@ -0,0 +1,194 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use datafusion_common::{Column, Result, ScalarValue};
+use datafusion_expr::{BinaryExpr, Cast, Expr, Operator};
+use std::collections::BTreeMap;
+
+pub(crate) fn simplify_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {
+    // Early return for simple cases
+    if predicates.len() <= 1 {
+        return Ok(predicates);
+    }
+
+    // Group predicates by their column reference
+    let mut column_predicates: BTreeMap<Column, Vec<Expr>> = BTreeMap::new();
+    let mut other_predicates = Vec::new();
+
+    for pred in predicates {
+        match &pred {
+            Expr::BinaryExpr(BinaryExpr {
+                left,
+                op:
+                    Operator::Gt
+                    | Operator::GtEq
+                    | Operator::Lt
+                    | Operator::LtEq
+                    | Operator::Eq,
+                right,
+            }) => {
+                let left_col = extract_column_from_expr(left);
+                let right_col = extract_column_from_expr(right);
+                let left_lit = left.is_literal();
+                let right_lit = right.is_literal();
+                if let (Some(col), true) = (&left_col, right_lit) {
+                    
column_predicates.entry(col.clone()).or_default().push(pred);
+                } else if let (true, Some(col)) = (left_lit, &right_col) {
+                    
column_predicates.entry(col.clone()).or_default().push(pred);
+                } else {
+                    other_predicates.push(pred);
+                }
+            }
+            _ => other_predicates.push(pred),
+        }
+    }
+
+    // Process each column's predicates to remove redundancies
+    let mut result = other_predicates;
+    for (_, preds) in column_predicates {
+        let simplified = simplify_column_predicates(preds)?;
+        result.extend(simplified);
+    }
+
+    Ok(result)
+}
+
+fn simplify_column_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {
+    if predicates.len() <= 1 {
+        return Ok(predicates);
+    }
+
+    // Group by operator type, but combining similar operators
+    let mut greater_predicates = Vec::new(); // Combines > and >=
+    let mut less_predicates = Vec::new(); // Combines < and <=
+    let mut eq_predicates = Vec::new();
+
+    for pred in predicates {
+        match &pred {
+            Expr::BinaryExpr(BinaryExpr { left: _, op, right }) => {
+                let right_is_literal = right.is_literal();
+                match (op, right_is_literal) {
+                    (Operator::Gt, true)
+                    | (Operator::Lt, false)
+                    | (Operator::GtEq, true)
+                    | (Operator::LtEq, false) => greater_predicates.push(pred),
+                    (Operator::Lt, true)
+                    | (Operator::Gt, false)
+                    | (Operator::LtEq, true)
+                    | (Operator::GtEq, false) => less_predicates.push(pred),
+                    (Operator::Eq, _) => eq_predicates.push(pred),
+                    _ => unreachable!("Unexpected operator: {}", op),
+                }
+            }
+            _ => unreachable!("Unexpected predicate {}", pred.to_string()),
+        }
+    }
+
+    let mut result = Vec::new();
+
+    // If we have equality predicates, they're the most restrictive
+    if !eq_predicates.is_empty() {
+        if eq_predicates.len() > 1 {
+            result.push(Expr::Literal(ScalarValue::Boolean(Some(false)), 
None));
+        } else {
+            result.push(eq_predicates[0].clone());
+        }
+    } else {
+        // Handle all greater-than-style predicates (keep the most restrictive 
- highest value)
+        if !greater_predicates.is_empty() {
+            if let Some(most_restrictive) =
+                find_most_restrictive_predicate(&greater_predicates, true)?
+            {
+                result.push(most_restrictive);
+            } else {
+                result.extend(greater_predicates);
+            }
+        }
+
+        // Handle all less-than-style predicates (keep the most restrictive - 
lowest value)
+        if !less_predicates.is_empty() {
+            if let Some(most_restrictive) =
+                find_most_restrictive_predicate(&less_predicates, false)?
+            {
+                result.push(most_restrictive);
+            } else {
+                result.extend(less_predicates);
+            }
+        }
+    }
+
+    Ok(result)
+}
+
+fn find_most_restrictive_predicate(
+    predicates: &[Expr],
+    find_greater: bool,
+) -> Result<Option<Expr>> {
+    if predicates.is_empty() {
+        return Ok(None);
+    }
+
+    let mut most_restrictive = predicates[0].clone();
+    let mut best_value: Option<ScalarValue> = None;
+
+    for pred in predicates {
+        if let Expr::BinaryExpr(BinaryExpr { left, op: _, right }) = pred {
+            // Extract the literal value based on which side has it
+            let mut scalar_value = None;
+            if right.is_literal() {
+                if let Expr::Literal(scalar, None) = right.as_ref() {
+                    scalar_value = Some(scalar.clone());
+                }

Review Comment:
   I think you could avoid clones here with something like
   
   ```suggestion
               if let Expr::Literal(scalar, None) = right {
                       scalar_value = Some(scalar);
                }
   ```
   
   (and similarly for the other match statements)



##########
datafusion/sqllogictest/test_files/simplify_predicates.slt:
##########
@@ -0,0 +1,212 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+
+#   http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Test cases for predicate simplification feature
+# Basic redundant comparison simplification
+
+statement ok
+set datafusion.explain.logical_plan_only=true;
+
+statement ok
+CREATE TABLE test_data (
+    int_col INT,
+    float_col FLOAT,
+    str_col VARCHAR,
+    date_col DATE,
+    bool_col BOOLEAN
+);
+
+# x > 5 AND x > 6 should simplify to x > 6
+query TT
+EXPLAIN SELECT * FROM test_data WHERE int_col > 5 AND int_col > 6;
+----
+logical_plan
+01)Filter: test_data.int_col > Int32(6)
+02)--TableScan: test_data projection=[int_col, float_col, str_col, date_col, 
bool_col]
+
+# x > 5 AND x >= 6 should simplify to x >= 6
+query TT
+EXPLAIN SELECT * FROM test_data WHERE int_col > 5 AND int_col >= 6;
+----
+logical_plan
+01)Filter: test_data.int_col >= Int32(6)
+02)--TableScan: test_data projection=[int_col, float_col, str_col, date_col, 
bool_col]
+
+# x < 10 AND x <= 8 should simplify to x <= 8
+query TT
+EXPLAIN SELECT * FROM test_data WHERE int_col < 10 AND int_col <= 8;
+----
+logical_plan
+01)Filter: test_data.int_col <= Int32(8)
+02)--TableScan: test_data projection=[int_col, float_col, str_col, date_col, 
bool_col]
+
+# x > 5 AND x > 6 AND x > 7 should simplify to x > 7
+query TT
+EXPLAIN SELECT * FROM test_data WHERE int_col > 5 AND int_col > 6 AND int_col 
> 7;
+----
+logical_plan
+01)Filter: test_data.int_col > Int32(7)
+02)--TableScan: test_data projection=[int_col, float_col, str_col, date_col, 
bool_col]
+
+# x > 5 AND y < 10 AND x > 6 AND y < 8 should simplify to x > 6 AND y < 8
+query TT
+EXPLAIN SELECT * FROM test_data WHERE int_col > 5 AND float_col < 10 AND 
int_col > 6 AND float_col < 8;
+----
+logical_plan
+01)Filter: test_data.float_col < Float32(8) AND test_data.int_col > Int32(6)
+02)--TableScan: test_data projection=[int_col, float_col, str_col, date_col, 
bool_col]
+
+
+# x = 7 AND x > 5 should simplify to x = 7

Review Comment:
   Could we also add a test for a repeated predicate like `x = 7 AND x = 7` ?



##########
datafusion/expr/src/expr.rs:
##########
@@ -2069,6 +2069,11 @@ impl Expr {
             _ => None,
         }
     }
+
+    /// Check if the Expr is literal

Review Comment:
   As a minor nit -- I wonder if you changed this to `as_literal` the code 
below could get simpler
   
   ```rust
   pub fn as_literal(&self) -> Option<&Literal> {
    if let Expr::Literal(lit, _) = self { Some(lit) } else { None } 
   }
   ```
   
   Then some of the matching might be simpler below



##########
datafusion/optimizer/src/simplify_predicates.rs:
##########
@@ -0,0 +1,194 @@
+// Licensed to the Apache Software Foundation (ASF) under one

Review Comment:
   A minor code reorganization comment: I recommend we put this module into 
`simplify_expressions` to make it easier to find 
(`datafusion/optimizer/src/simplify_expressions/predicates.rs`) for example
   
   I view this as a very nice first step for more sophisticated predicate 
simplification



##########
datafusion/optimizer/src/simplify_predicates.rs:
##########
@@ -0,0 +1,194 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use datafusion_common::{Column, Result, ScalarValue};
+use datafusion_expr::{BinaryExpr, Cast, Expr, Operator};
+use std::collections::BTreeMap;
+
+pub(crate) fn simplify_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {
+    // Early return for simple cases
+    if predicates.len() <= 1 {
+        return Ok(predicates);
+    }
+
+    // Group predicates by their column reference
+    let mut column_predicates: BTreeMap<Column, Vec<Expr>> = BTreeMap::new();
+    let mut other_predicates = Vec::new();
+
+    for pred in predicates {
+        match &pred {
+            Expr::BinaryExpr(BinaryExpr {
+                left,
+                op:
+                    Operator::Gt
+                    | Operator::GtEq
+                    | Operator::Lt
+                    | Operator::LtEq
+                    | Operator::Eq,
+                right,
+            }) => {
+                let left_col = extract_column_from_expr(left);
+                let right_col = extract_column_from_expr(right);
+                let left_lit = left.is_literal();
+                let right_lit = right.is_literal();
+                if let (Some(col), true) = (&left_col, right_lit) {
+                    
column_predicates.entry(col.clone()).or_default().push(pred);
+                } else if let (true, Some(col)) = (left_lit, &right_col) {
+                    
column_predicates.entry(col.clone()).or_default().push(pred);
+                } else {
+                    other_predicates.push(pred);
+                }
+            }
+            _ => other_predicates.push(pred),
+        }
+    }
+
+    // Process each column's predicates to remove redundancies
+    let mut result = other_predicates;
+    for (_, preds) in column_predicates {
+        let simplified = simplify_column_predicates(preds)?;
+        result.extend(simplified);
+    }
+
+    Ok(result)
+}
+
+fn simplify_column_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {

Review Comment:
   As a follow on PR, perhaps we can explore the possibility here of using the 
boundary analysis, basically
   
   
https://docs.rs/datafusion/latest/datafusion/physical_expr/intervals/cp_solver/index.html
   
   That could potentially be a more general form of analysis, rather than these 
particular rules



##########
datafusion/optimizer/src/push_down_filter.rs:
##########
@@ -778,6 +779,16 @@ impl OptimizerRule for PushDownFilter {
             return Ok(Transformed::no(plan));
         };
 
+        let predicate = split_conjunction_owned(filter.predicate.clone());
+        let old_predicate_len = predicate.len();
+        let new_predicates = simplify_predicates(predicate)?;

Review Comment:
   I think we could use the cp_solver approach to handle expressions more 
generally and I also agree this could / should be done in a follow on PR



##########
datafusion/optimizer/src/simplify_predicates.rs:
##########
@@ -0,0 +1,194 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use datafusion_common::{Column, Result, ScalarValue};
+use datafusion_expr::{BinaryExpr, Cast, Expr, Operator};
+use std::collections::BTreeMap;
+
+pub(crate) fn simplify_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {

Review Comment:
   It would be great to explain in comments how this is different than the 
existing simplifier in 
https://github.com/apache/datafusion/blob/30bb5480890cc9f05cfe09bf449555a12581f8a7/datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs#L18-L17



##########
datafusion/optimizer/src/simplify_predicates.rs:
##########
@@ -0,0 +1,194 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use datafusion_common::{Column, Result, ScalarValue};
+use datafusion_expr::{BinaryExpr, Cast, Expr, Operator};
+use std::collections::BTreeMap;
+
+pub(crate) fn simplify_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {
+    // Early return for simple cases
+    if predicates.len() <= 1 {
+        return Ok(predicates);
+    }
+
+    // Group predicates by their column reference
+    let mut column_predicates: BTreeMap<Column, Vec<Expr>> = BTreeMap::new();
+    let mut other_predicates = Vec::new();
+
+    for pred in predicates {
+        match &pred {
+            Expr::BinaryExpr(BinaryExpr {
+                left,
+                op:
+                    Operator::Gt
+                    | Operator::GtEq
+                    | Operator::Lt
+                    | Operator::LtEq
+                    | Operator::Eq,
+                right,
+            }) => {
+                let left_col = extract_column_from_expr(left);
+                let right_col = extract_column_from_expr(right);
+                let left_lit = left.is_literal();
+                let right_lit = right.is_literal();
+                if let (Some(col), true) = (&left_col, right_lit) {
+                    
column_predicates.entry(col.clone()).or_default().push(pred);
+                } else if let (true, Some(col)) = (left_lit, &right_col) {
+                    
column_predicates.entry(col.clone()).or_default().push(pred);
+                } else {
+                    other_predicates.push(pred);
+                }
+            }
+            _ => other_predicates.push(pred),
+        }
+    }
+
+    // Process each column's predicates to remove redundancies
+    let mut result = other_predicates;
+    for (_, preds) in column_predicates {
+        let simplified = simplify_column_predicates(preds)?;
+        result.extend(simplified);
+    }
+
+    Ok(result)
+}
+
+fn simplify_column_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {
+    if predicates.len() <= 1 {
+        return Ok(predicates);
+    }
+
+    // Group by operator type, but combining similar operators
+    let mut greater_predicates = Vec::new(); // Combines > and >=
+    let mut less_predicates = Vec::new(); // Combines < and <=
+    let mut eq_predicates = Vec::new();
+
+    for pred in predicates {
+        match &pred {
+            Expr::BinaryExpr(BinaryExpr { left: _, op, right }) => {
+                let right_is_literal = right.is_literal();
+                match (op, right_is_literal) {
+                    (Operator::Gt, true)
+                    | (Operator::Lt, false)
+                    | (Operator::GtEq, true)
+                    | (Operator::LtEq, false) => greater_predicates.push(pred),
+                    (Operator::Lt, true)
+                    | (Operator::Gt, false)
+                    | (Operator::LtEq, true)
+                    | (Operator::GtEq, false) => less_predicates.push(pred),
+                    (Operator::Eq, _) => eq_predicates.push(pred),
+                    _ => unreachable!("Unexpected operator: {}", op),
+                }
+            }
+            _ => unreachable!("Unexpected predicate {}", pred.to_string()),
+        }
+    }
+
+    let mut result = Vec::new();
+
+    // If we have equality predicates, they're the most restrictive
+    if !eq_predicates.is_empty() {
+        if eq_predicates.len() > 1 {
+            result.push(Expr::Literal(ScalarValue::Boolean(Some(false)), 
None));
+        } else {
+            result.push(eq_predicates[0].clone());

Review Comment:
   You could avoid a clone here I think via 
   ```suggestion
               result.push(eq_predicates.pop().unwrap())
   ```



##########
datafusion/optimizer/src/simplify_predicates.rs:
##########
@@ -0,0 +1,194 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use datafusion_common::{Column, Result, ScalarValue};
+use datafusion_expr::{BinaryExpr, Cast, Expr, Operator};
+use std::collections::BTreeMap;
+
+pub(crate) fn simplify_predicates(predicates: Vec<Expr>) -> Result<Vec<Expr>> {

Review Comment:
   Is one major difference that it explicitly uses the filtering logic (for 
example `x < 5`will never be true even when x could be nullable, but `x < 5` 
can only be simplified to `false` when we know `x is not null`)?



-- 
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: github-unsubscr...@datafusion.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: github-unsubscr...@datafusion.apache.org
For additional commands, e-mail: github-h...@datafusion.apache.org

Reply via email to