This is an automated email from the ASF dual-hosted git repository.

agrove pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-python.git


The following commit(s) were added to refs/heads/main by this push:
     new 3eb198b  Add PyExpr to_variant conversions (#793)
3eb198b is described below

commit 3eb198b5171ebdd41a0279d1da059544429945ca
Author: Michael J Ward <[email protected]>
AuthorDate: Sun Aug 4 18:07:34 2024 -0500

    Add PyExpr to_variant conversions (#793)
    
    * make PyExpr::to_variant arms explicit
    
    * update PyInList to wrap expr::InList
    
    * update PyExists to wrap expr::Exists
    
    * update PyInSubquery to wrap expr::InSubquery
    
    * update Placeholder to wrap expr::Placeholder
    
    * make PyLogicalPlan::to_variant match arms explicit
    
    * add PySortExpr wrapper
    
    * add PyUnnestExpr wrapper
    
    * update PyAlias to wrap upstream Alias
    
    * return not implemented error for unimplemnted variants in 
PyExpr::to_variant
    
    * added to_variant python test from the GH issue
    
    * remove unused import
    
    * return unsupported_variants for unimplemented variants in  
PyLogicalPlan::to_variant
---
 python/datafusion/tests/test_expr.py  | 28 +++++++++++++++++
 src/expr.rs                           | 51 ++++++++++++++++++++++++++----
 src/expr/alias.rs                     | 32 ++++++++++---------
 src/expr/exists.rs                    | 15 +++++----
 src/expr/in_list.rs                   | 22 +++++--------
 src/expr/in_subquery.rs               | 22 +++++--------
 src/expr/placeholder.rs               | 21 ++++++-------
 src/expr/{alias.rs => sort_expr.rs}   | 56 +++++++++++++++++----------------
 src/expr/{alias.rs => unnest_expr.rs} | 58 +++++++++++++++++------------------
 src/sql/logical.rs                    | 15 +++++++--
 10 files changed, 194 insertions(+), 126 deletions(-)

diff --git a/python/datafusion/tests/test_expr.py 
b/python/datafusion/tests/test_expr.py
index c9f0e98..1a41120 100644
--- a/python/datafusion/tests/test_expr.py
+++ b/python/datafusion/tests/test_expr.py
@@ -139,3 +139,31 @@ def test_relational_expr(test_ctx):
     assert df.filter(col("b") != "beta").count() == 2
 
     assert df.filter(col("a") == "beta").count() == 0
+
+
+def test_expr_to_variant():
+    # Taken from https://github.com/apache/datafusion-python/issues/781
+    from datafusion import SessionContext
+    from datafusion.expr import Filter
+
+
+    def traverse_logical_plan(plan):
+        cur_node = plan.to_variant()
+        if isinstance(cur_node, Filter):
+            return cur_node.predicate().to_variant()
+        if hasattr(plan, 'inputs'):
+            for input_plan in plan.inputs():
+                res = traverse_logical_plan(input_plan)
+                if res is not None:
+                    return res
+
+    ctx = SessionContext()
+    data = {'id': [1, 2, 3], 'name': ['Alice', 'Bob', 'Charlie']}
+    ctx.from_pydict(data, name='table1')
+    query = "SELECT * FROM table1 t1 WHERE t1.name IN ('dfa', 'ad', 'dfre', 
'vsa')"
+    logical_plan = ctx.sql(query).optimized_logical_plan()
+    variant = traverse_logical_plan(logical_plan)
+    assert variant is not None
+    assert variant.expr().to_variant().qualified_name() == 'table1.name'
+    assert str(variant.list()) == '[Expr(Utf8("dfa")), Expr(Utf8("ad")), 
Expr(Utf8("dfre")), Expr(Utf8("vsa"))]'
+    assert not variant.negated()
diff --git a/src/expr.rs b/src/expr.rs
index aab0daa..04bfc85 100644
--- a/src/expr.rs
+++ b/src/expr.rs
@@ -33,7 +33,7 @@ use datafusion_expr::{
 };
 
 use crate::common::data_type::{DataTypeMap, RexType};
-use crate::errors::{py_runtime_err, py_type_err, DataFusionError};
+use crate::errors::{py_runtime_err, py_type_err, py_unsupported_variant_err, 
DataFusionError};
 use crate::expr::aggregate_expr::PyAggregateFunction;
 use crate::expr::binary_expr::PyBinaryExpr;
 use crate::expr::column::PyColumn;
@@ -84,11 +84,13 @@ pub mod scalar_subquery;
 pub mod scalar_variable;
 pub mod signature;
 pub mod sort;
+pub mod sort_expr;
 pub mod subquery;
 pub mod subquery_alias;
 pub mod table_scan;
 pub mod union;
 pub mod unnest;
+pub mod unnest_expr;
 pub mod window;
 
 /// A PyExpr that can be used on a DataFrame
@@ -119,8 +121,9 @@ pub fn py_expr_list(expr: &[Expr]) -> PyResult<Vec<PyExpr>> 
{
 impl PyExpr {
     /// Return the specific expression
     fn to_variant(&self, py: Python) -> PyResult<PyObject> {
-        Python::with_gil(|_| match &self.expr {
-            Expr::Alias(alias) => Ok(PyAlias::new(&alias.expr, 
&alias.name).into_py(py)),
+        Python::with_gil(|_| {
+            match &self.expr {
+            Expr::Alias(alias) => Ok(PyAlias::from(alias.clone()).into_py(py)),
             Expr::Column(col) => Ok(PyColumn::from(col.clone()).into_py(py)),
             Expr::ScalarVariable(data_type, variables) => {
                 Ok(PyScalarVariable::new(data_type, variables).into_py(py))
@@ -141,10 +144,44 @@ impl PyExpr {
             Expr::AggregateFunction(expr) => {
                 Ok(PyAggregateFunction::from(expr.clone()).into_py(py))
             }
-            other => Err(py_runtime_err(format!(
-                "Cannot convert this Expr to a Python object: {:?}",
-                other
+            Expr::SimilarTo(value) => 
Ok(PySimilarTo::from(value.clone()).into_py(py)),
+            Expr::Between(value) => 
Ok(between::PyBetween::from(value.clone()).into_py(py)),
+            Expr::Case(value) => 
Ok(case::PyCase::from(value.clone()).into_py(py)),
+            Expr::Cast(value) => 
Ok(cast::PyCast::from(value.clone()).into_py(py)),
+            Expr::TryCast(value) => 
Ok(cast::PyTryCast::from(value.clone()).into_py(py)),
+            Expr::Sort(value) => 
Ok(sort_expr::PySortExpr::from(value.clone()).into_py(py)),
+            Expr::ScalarFunction(value) => 
Err(py_unsupported_variant_err(format!(
+                "Converting Expr::ScalarFunction to a Python object is not 
implemented: {:?}",
+                value
             ))),
+            Expr::WindowFunction(value) => 
Err(py_unsupported_variant_err(format!(
+                "Converting Expr::WindowFunction to a Python object is not 
implemented: {:?}",
+                value
+            ))),
+            Expr::InList(value) => 
Ok(in_list::PyInList::from(value.clone()).into_py(py)),
+            Expr::Exists(value) => 
Ok(exists::PyExists::from(value.clone()).into_py(py)),
+            Expr::InSubquery(value) => {
+                Ok(in_subquery::PyInSubquery::from(value.clone()).into_py(py))
+            }
+            Expr::ScalarSubquery(value) => {
+                
Ok(scalar_subquery::PyScalarSubquery::from(value.clone()).into_py(py))
+            }
+            Expr::Wildcard { qualifier } => 
Err(py_unsupported_variant_err(format!(
+                "Converting Expr::Wildcard to a Python object is not 
implemented : {:?}",
+                qualifier
+            ))),
+            Expr::GroupingSet(value) => {
+                
Ok(grouping_set::PyGroupingSet::from(value.clone()).into_py(py))
+            }
+            Expr::Placeholder(value) => {
+                Ok(placeholder::PyPlaceholder::from(value.clone()).into_py(py))
+            }
+            Expr::OuterReferenceColumn(data_type, column) => 
Err(py_unsupported_variant_err(format!(
+                "Converting Expr::OuterReferenceColumn to a Python object is 
not implemented: {:?} - {:?}",
+                data_type, column
+            ))),
+            Expr::Unnest(value) => 
Ok(unnest_expr::PyUnnestExpr::from(value.clone()).into_py(py)),
+        }
         })
     }
 
@@ -599,6 +636,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> 
PyResult<()> {
     m.add_class::<cross_join::PyCrossJoin>()?;
     m.add_class::<union::PyUnion>()?;
     m.add_class::<unnest::PyUnnest>()?;
+    m.add_class::<unnest_expr::PyUnnestExpr>()?;
     m.add_class::<extension::PyExtension>()?;
     m.add_class::<filter::PyFilter>()?;
     m.add_class::<projection::PyProjection>()?;
@@ -606,6 +644,7 @@ pub(crate) fn init_module(m: &Bound<'_, PyModule>) -> 
PyResult<()> {
     m.add_class::<create_memory_table::PyCreateMemoryTable>()?;
     m.add_class::<create_view::PyCreateView>()?;
     m.add_class::<distinct::PyDistinct>()?;
+    m.add_class::<sort_expr::PySortExpr>()?;
     m.add_class::<subquery_alias::PySubqueryAlias>()?;
     m.add_class::<drop_table::PyDropTable>()?;
     m.add_class::<repartition::PyPartitioning>()?;
diff --git a/src/expr/alias.rs b/src/expr/alias.rs
index 2ce6563..3208800 100644
--- a/src/expr/alias.rs
+++ b/src/expr/alias.rs
@@ -19,13 +19,24 @@ use crate::expr::PyExpr;
 use pyo3::prelude::*;
 use std::fmt::{self, Display, Formatter};
 
-use datafusion_expr::Expr;
+use datafusion_expr::expr::Alias;
 
 #[pyclass(name = "Alias", module = "datafusion.expr", subclass)]
 #[derive(Clone)]
 pub struct PyAlias {
-    expr: PyExpr,
-    alias_name: String,
+    alias: Alias,
+}
+
+impl From<Alias> for PyAlias {
+    fn from(alias: Alias) -> Self {
+        Self { alias }
+    }
+}
+
+impl From<PyAlias> for Alias {
+    fn from(py_alias: PyAlias) -> Self {
+        py_alias.alias
+    }
 }
 
 impl Display for PyAlias {
@@ -35,29 +46,20 @@ impl Display for PyAlias {
             "Alias
             \nExpr: `{:?}`
             \nAlias Name: `{}`",
-            &self.expr, &self.alias_name
+            &self.alias.expr, &self.alias.name
         )
     }
 }
 
-impl PyAlias {
-    pub fn new(expr: &Expr, alias_name: &String) -> Self {
-        Self {
-            expr: expr.clone().into(),
-            alias_name: alias_name.to_owned(),
-        }
-    }
-}
-
 #[pymethods]
 impl PyAlias {
     /// Retrieve the "name" of the alias
     fn alias(&self) -> PyResult<String> {
-        Ok(self.alias_name.clone())
+        Ok(self.alias.name.clone())
     }
 
     fn expr(&self) -> PyResult<PyExpr> {
-        Ok(self.expr.clone())
+        Ok((*self.alias.expr.clone()).into())
     }
 
     /// Get a String representation of this column
diff --git a/src/expr/exists.rs b/src/expr/exists.rs
index 7df9a6e..fd2aa8c 100644
--- a/src/expr/exists.rs
+++ b/src/expr/exists.rs
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use datafusion_expr::Subquery;
+use datafusion_expr::expr::Exists;
 use pyo3::prelude::*;
 
 use super::subquery::PySubquery;
@@ -23,23 +23,22 @@ use super::subquery::PySubquery;
 #[pyclass(name = "Exists", module = "datafusion.expr", subclass)]
 #[derive(Clone)]
 pub struct PyExists {
-    subquery: Subquery,
-    negated: bool,
+    exists: Exists,
 }
 
-impl PyExists {
-    pub fn new(subquery: Subquery, negated: bool) -> Self {
-        Self { subquery, negated }
+impl From<Exists> for PyExists {
+    fn from(exists: Exists) -> Self {
+        PyExists { exists }
     }
 }
 
 #[pymethods]
 impl PyExists {
     fn subquery(&self) -> PySubquery {
-        self.subquery.clone().into()
+        self.exists.subquery.clone().into()
     }
 
     fn negated(&self) -> bool {
-        self.negated
+        self.exists.negated
     }
 }
diff --git a/src/expr/in_list.rs b/src/expr/in_list.rs
index 840eee2..c1a99a3 100644
--- a/src/expr/in_list.rs
+++ b/src/expr/in_list.rs
@@ -16,38 +16,32 @@
 // under the License.
 
 use crate::expr::PyExpr;
-use datafusion_expr::Expr;
+use datafusion_expr::expr::InList;
 use pyo3::prelude::*;
 
 #[pyclass(name = "InList", module = "datafusion.expr", subclass)]
 #[derive(Clone)]
 pub struct PyInList {
-    expr: Box<Expr>,
-    list: Vec<Expr>,
-    negated: bool,
+    in_list: InList,
 }
 
-impl PyInList {
-    pub fn new(expr: Box<Expr>, list: Vec<Expr>, negated: bool) -> Self {
-        Self {
-            expr,
-            list,
-            negated,
-        }
+impl From<InList> for PyInList {
+    fn from(in_list: InList) -> Self {
+        PyInList { in_list }
     }
 }
 
 #[pymethods]
 impl PyInList {
     fn expr(&self) -> PyExpr {
-        (*self.expr).clone().into()
+        (*self.in_list.expr).clone().into()
     }
 
     fn list(&self) -> Vec<PyExpr> {
-        self.list.iter().map(|e| e.clone().into()).collect()
+        self.in_list.list.iter().map(|e| e.clone().into()).collect()
     }
 
     fn negated(&self) -> bool {
-        self.negated
+        self.in_list.negated
     }
 }
diff --git a/src/expr/in_subquery.rs b/src/expr/in_subquery.rs
index 6cee4a1..7dfafdb 100644
--- a/src/expr/in_subquery.rs
+++ b/src/expr/in_subquery.rs
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use datafusion_expr::{Expr, Subquery};
+use datafusion_expr::expr::InSubquery;
 use pyo3::prelude::*;
 
 use super::{subquery::PySubquery, PyExpr};
@@ -23,32 +23,26 @@ use super::{subquery::PySubquery, PyExpr};
 #[pyclass(name = "InSubquery", module = "datafusion.expr", subclass)]
 #[derive(Clone)]
 pub struct PyInSubquery {
-    expr: Box<Expr>,
-    subquery: Subquery,
-    negated: bool,
+    in_subquery: InSubquery,
 }
 
-impl PyInSubquery {
-    pub fn new(expr: Box<Expr>, subquery: Subquery, negated: bool) -> Self {
-        Self {
-            expr,
-            subquery,
-            negated,
-        }
+impl From<InSubquery> for PyInSubquery {
+    fn from(in_subquery: InSubquery) -> Self {
+        PyInSubquery { in_subquery }
     }
 }
 
 #[pymethods]
 impl PyInSubquery {
     fn expr(&self) -> PyExpr {
-        (*self.expr).clone().into()
+        (*self.in_subquery.expr).clone().into()
     }
 
     fn subquery(&self) -> PySubquery {
-        self.subquery.clone().into()
+        self.in_subquery.subquery.clone().into()
     }
 
     fn negated(&self) -> bool {
-        self.negated
+        self.in_subquery.negated
     }
 }
diff --git a/src/expr/placeholder.rs b/src/expr/placeholder.rs
index e37c8b5..ca75ce3 100644
--- a/src/expr/placeholder.rs
+++ b/src/expr/placeholder.rs
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use datafusion::arrow::datatypes::DataType;
+use datafusion_expr::expr::Placeholder;
 use pyo3::prelude::*;
 
 use crate::common::data_type::PyDataType;
@@ -23,26 +23,25 @@ use crate::common::data_type::PyDataType;
 #[pyclass(name = "Placeholder", module = "datafusion.expr", subclass)]
 #[derive(Clone)]
 pub struct PyPlaceholder {
-    id: String,
-    data_type: Option<DataType>,
+    placeholder: Placeholder,
 }
 
-impl PyPlaceholder {
-    pub fn new(id: String, data_type: DataType) -> Self {
-        Self {
-            id,
-            data_type: Some(data_type),
-        }
+impl From<Placeholder> for PyPlaceholder {
+    fn from(placeholder: Placeholder) -> Self {
+        PyPlaceholder { placeholder }
     }
 }
 
 #[pymethods]
 impl PyPlaceholder {
     fn id(&self) -> String {
-        self.id.clone()
+        self.placeholder.id.clone()
     }
 
     fn data_type(&self) -> Option<PyDataType> {
-        self.data_type.as_ref().map(|e| e.clone().into())
+        self.placeholder
+            .data_type
+            .as_ref()
+            .map(|e| e.clone().into())
     }
 }
diff --git a/src/expr/alias.rs b/src/expr/sort_expr.rs
similarity index 60%
copy from src/expr/alias.rs
copy to src/expr/sort_expr.rs
index 2ce6563..6a8a0cf 100644
--- a/src/expr/alias.rs
+++ b/src/expr/sort_expr.rs
@@ -16,51 +16,55 @@
 // under the License.
 
 use crate::expr::PyExpr;
+use datafusion_expr::SortExpr;
 use pyo3::prelude::*;
 use std::fmt::{self, Display, Formatter};
 
-use datafusion_expr::Expr;
-
-#[pyclass(name = "Alias", module = "datafusion.expr", subclass)]
+#[pyclass(name = "SortExpr", module = "datafusion.expr", subclass)]
 #[derive(Clone)]
-pub struct PyAlias {
-    expr: PyExpr,
-    alias_name: String,
+pub struct PySortExpr {
+    sort: SortExpr,
+}
+
+impl From<PySortExpr> for SortExpr {
+    fn from(sort: PySortExpr) -> Self {
+        sort.sort
+    }
+}
+
+impl From<SortExpr> for PySortExpr {
+    fn from(sort: SortExpr) -> PySortExpr {
+        PySortExpr { sort }
+    }
 }
 
-impl Display for PyAlias {
+impl Display for PySortExpr {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
         write!(
             f,
-            "Alias
-            \nExpr: `{:?}`
-            \nAlias Name: `{}`",
-            &self.expr, &self.alias_name
+            "Sort
+            Expr: {:?}
+            Asc: {:?}
+            NullsFirst: {:?}",
+            &self.sort.expr, &self.sort.asc, &self.sort.nulls_first
         )
     }
 }
 
-impl PyAlias {
-    pub fn new(expr: &Expr, alias_name: &String) -> Self {
-        Self {
-            expr: expr.clone().into(),
-            alias_name: alias_name.to_owned(),
-        }
+#[pymethods]
+impl PySortExpr {
+    fn expr(&self) -> PyResult<PyExpr> {
+        Ok((*self.sort.expr).clone().into())
     }
-}
 
-#[pymethods]
-impl PyAlias {
-    /// Retrieve the "name" of the alias
-    fn alias(&self) -> PyResult<String> {
-        Ok(self.alias_name.clone())
+    fn ascending(&self) -> PyResult<bool> {
+        Ok(self.sort.asc)
     }
 
-    fn expr(&self) -> PyResult<PyExpr> {
-        Ok(self.expr.clone())
+    fn nulls_first(&self) -> PyResult<bool> {
+        Ok(self.sort.nulls_first)
     }
 
-    /// Get a String representation of this column
     fn __repr__(&self) -> String {
         format!("{}", self)
     }
diff --git a/src/expr/alias.rs b/src/expr/unnest_expr.rs
similarity index 57%
copy from src/expr/alias.rs
copy to src/expr/unnest_expr.rs
index 2ce6563..a2f8230 100644
--- a/src/expr/alias.rs
+++ b/src/expr/unnest_expr.rs
@@ -15,53 +15,53 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::expr::PyExpr;
+use datafusion_expr::expr::Unnest;
 use pyo3::prelude::*;
 use std::fmt::{self, Display, Formatter};
 
-use datafusion_expr::Expr;
+use super::PyExpr;
 
-#[pyclass(name = "Alias", module = "datafusion.expr", subclass)]
+#[pyclass(name = "UnnestExpr", module = "datafusion.expr", subclass)]
 #[derive(Clone)]
-pub struct PyAlias {
-    expr: PyExpr,
-    alias_name: String,
+pub struct PyUnnestExpr {
+    unnest: Unnest,
 }
 
-impl Display for PyAlias {
+impl From<Unnest> for PyUnnestExpr {
+    fn from(unnest: Unnest) -> PyUnnestExpr {
+        PyUnnestExpr { unnest }
+    }
+}
+
+impl From<PyUnnestExpr> for Unnest {
+    fn from(unnest: PyUnnestExpr) -> Self {
+        unnest.unnest
+    }
+}
+
+impl Display for PyUnnestExpr {
     fn fmt(&self, f: &mut Formatter) -> fmt::Result {
         write!(
             f,
-            "Alias
-            \nExpr: `{:?}`
-            \nAlias Name: `{}`",
-            &self.expr, &self.alias_name
+            "Unnest
+            Expr: {:?}",
+            &self.unnest.expr,
         )
     }
 }
 
-impl PyAlias {
-    pub fn new(expr: &Expr, alias_name: &String) -> Self {
-        Self {
-            expr: expr.clone().into(),
-            alias_name: alias_name.to_owned(),
-        }
-    }
-}
-
 #[pymethods]
-impl PyAlias {
-    /// Retrieve the "name" of the alias
-    fn alias(&self) -> PyResult<String> {
-        Ok(self.alias_name.clone())
+impl PyUnnestExpr {
+    /// Retrieves the expression that is being unnested
+    fn expr(&self) -> PyResult<PyExpr> {
+        Ok((*self.unnest.expr).clone().into())
     }
 
-    fn expr(&self) -> PyResult<PyExpr> {
-        Ok(self.expr.clone())
+    fn __repr__(&self) -> PyResult<String> {
+        Ok(format!("UnnestExpr({})", self))
     }
 
-    /// Get a String representation of this column
-    fn __repr__(&self) -> String {
-        format!("{}", self)
+    fn __name__(&self) -> PyResult<String> {
+        Ok("UnnestExpr".to_string())
     }
 }
diff --git a/src/sql/logical.rs b/src/sql/logical.rs
index b1446b9..c4471f5 100644
--- a/src/sql/logical.rs
+++ b/src/sql/logical.rs
@@ -81,9 +81,18 @@ impl PyLogicalPlan {
             LogicalPlan::SubqueryAlias(plan) => 
PySubqueryAlias::from(plan.clone()).to_variant(py),
             LogicalPlan::Unnest(plan) => 
PyUnnest::from(plan.clone()).to_variant(py),
             LogicalPlan::Window(plan) => 
PyWindow::from(plan.clone()).to_variant(py),
-            other => Err(py_unsupported_variant_err(format!(
-                "Cannot convert this plan to a LogicalNode: {:?}",
-                other
+            LogicalPlan::Repartition(_)
+            | LogicalPlan::Union(_)
+            | LogicalPlan::Statement(_)
+            | LogicalPlan::Values(_)
+            | LogicalPlan::Prepare(_)
+            | LogicalPlan::Dml(_)
+            | LogicalPlan::Ddl(_)
+            | LogicalPlan::Copy(_)
+            | LogicalPlan::DescribeTable(_)
+            | LogicalPlan::RecursiveQuery(_) => 
Err(py_unsupported_variant_err(format!(
+                "Conversion of variant not implemented: {:?}",
+                self.plan
             ))),
         }
     }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to