You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by gi...@apache.org on 2026/05/29 13:06:18 UTC
(datafusion) 01/01: refactor: Port IsNullExpr proto serialization hooks (#22509)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-22509-32a1fe5498c00e1a5327ff796c9f8335691bdb32
in repository https://gitbox.apache.org/repos/asf/datafusion.git
commit 22f4bc20ef388f89c1cf6a5aa85f06f46225dfc1
Author: chakkk309 <ch...@gmail.com>
AuthorDate: Fri May 29 21:05:53 2026 +0800
refactor: Port IsNullExpr proto serialization hooks (#22509)
## Which issue does this PR close?
- Closes #22423.
## Rationale for this change
This is part of #22418, which migrates built-in `PhysicalExpr`
implementations away from the central protobuf serialization /
deserialization chains.
`IsNullExpr` can now own its protobuf serialization through
`PhysicalExpr::try_to_proto` and its deserialization through
`IsNullExpr::try_from_proto`, matching the pattern introduced for
`Column` and `BinaryExpr`.
## What changes are included in this PR?
- Adds `PhysicalExpr::try_to_proto` support for `IsNullExpr`.
- Adds `IsNullExpr::try_from_proto`.
- Wires `IsNullExpr` deserialization in `from_proto.rs` through the new
hook.
- Removes the old `IsNullExpr` serialization branch from the central
`to_proto.rs` downcast chain.
- Adds direct proto hook tests for:
- successful `IsNullExpr` roundtrip
- wrong protobuf expression variant
- missing required child expression
## Are these changes tested?
Yes.
I ran:
```bash
cargo test -p datafusion-physical-expr --features proto is_null_
cargo clippy -p datafusion-physical-expr --features proto --tests -- -D warnings
cargo test -p datafusion-proto --test proto_integration
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all
Co-authored-by: chakkk309 <ch...@users.noreply.github.com>
---
.../physical-expr/src/expressions/is_null.rs | 148 ++++++++++++++++++++-
datafusion/proto/src/physical_plan/from_proto.rs | 10 +-
datafusion/proto/src/physical_plan/to_proto.rs | 13 +-
3 files changed, 148 insertions(+), 23 deletions(-)
diff --git a/datafusion/physical-expr/src/expressions/is_null.rs b/datafusion/physical-expr/src/expressions/is_null.rs
index 8534ddb8d1..da008a1cfb 100644
--- a/datafusion/physical-expr/src/expressions/is_null.rs
+++ b/datafusion/physical-expr/src/expressions/is_null.rs
@@ -22,8 +22,7 @@ use arrow::{
datatypes::{DataType, Schema},
record_batch::RecordBatch,
};
-use datafusion_common::Result;
-use datafusion_common::ScalarValue;
+use datafusion_common::{Result, ScalarValue};
use datafusion_expr::ColumnarValue;
use std::hash::Hash;
use std::sync::Arc;
@@ -102,6 +101,45 @@ impl PhysicalExpr for IsNullExpr {
self.arg.fmt_sql(f)?;
write!(f, " IS NULL")
}
+
+ #[cfg(feature = "proto")]
+ fn try_to_proto(
+ &self,
+ ctx: &datafusion_physical_expr_common::physical_expr::proto_encode::PhysicalExprEncodeCtx<'_>,
+ ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalExprNode>> {
+ use datafusion_proto_models::protobuf;
+
+ Ok(Some(protobuf::PhysicalExprNode {
+ expr_id: None,
+ expr_type: Some(protobuf::physical_expr_node::ExprType::IsNullExpr(
+ Box::new(protobuf::PhysicalIsNull {
+ expr: Some(Box::new(ctx.encode_child(&self.arg)?)),
+ }),
+ )),
+ }))
+ }
+}
+
+#[cfg(feature = "proto")]
+impl IsNullExpr {
+ /// Reconstruct an [`IsNullExpr`] from its protobuf representation.
+ pub fn try_from_proto(
+ node: &datafusion_proto_models::protobuf::PhysicalExprNode,
+ ctx: &datafusion_physical_expr_common::physical_expr::proto_decode::PhysicalExprDecodeCtx<'_>,
+ ) -> Result<Arc<dyn PhysicalExpr>> {
+ use datafusion_physical_expr_common::expect_expr_variant;
+ use datafusion_proto_models::protobuf;
+
+ let node = expect_expr_variant!(
+ node,
+ protobuf::physical_expr_node::ExprType::IsNullExpr,
+ "IsNullExpr",
+ );
+ let expr =
+ ctx.decode_required_expression(node.expr.as_deref(), "IsNullExpr", "expr")?;
+
+ Ok(Arc::new(IsNullExpr::new(expr)))
+ }
}
/// Create an IS NULL expression
@@ -224,3 +262,109 @@ mod tests {
Ok(())
}
}
+
+#[cfg(all(test, feature = "proto"))]
+mod proto_tests {
+ use super::*;
+ use crate::expressions::{Column, col};
+ use crate::proto_test_util::{
+ StubDecoder, StubEncoder, UnreachableDecoder, column_node,
+ };
+ use arrow::datatypes::Field;
+ use datafusion_common::DataFusionError;
+ use datafusion_physical_expr_common::physical_expr::proto_decode::PhysicalExprDecodeCtx;
+ use datafusion_physical_expr_common::physical_expr::proto_encode::PhysicalExprEncodeCtx;
+ use datafusion_proto_models::protobuf::{
+ PhysicalExprNode, PhysicalIsNull, physical_expr_node,
+ };
+
+ fn is_null_node(expr: Option<Box<PhysicalExprNode>>) -> PhysicalExprNode {
+ PhysicalExprNode {
+ expr_id: None,
+ expr_type: Some(physical_expr_node::ExprType::IsNullExpr(Box::new(
+ PhysicalIsNull { expr },
+ ))),
+ }
+ }
+
+ fn is_null_fixture() -> IsNullExpr {
+ let schema = Schema::new(vec![Field::new("a", DataType::Int64, true)]);
+ IsNullExpr::new(col("a", &schema).unwrap())
+ }
+
+ #[test]
+ fn try_to_proto_encodes_is_null_expr() {
+ let is_null = is_null_fixture();
+ let encoder = StubEncoder::ok();
+ let ctx = PhysicalExprEncodeCtx::new(&encoder);
+
+ let node = is_null
+ .try_to_proto(&ctx)
+ .unwrap()
+ .expect("IsNullExpr should encode to Some(node)");
+
+ assert!(node.expr_id.is_none());
+ let is_null_node = match node.expr_type {
+ Some(physical_expr_node::ExprType::IsNullExpr(boxed)) => *boxed,
+ other => panic!("expected an IsNullExpr node, got {other:?}"),
+ };
+ assert!(is_null_node.expr.is_some());
+ }
+
+ #[test]
+ fn try_to_proto_propagates_expr_encode_error() {
+ let is_null = is_null_fixture();
+ let encoder = StubEncoder::failing_on(1);
+ let ctx = PhysicalExprEncodeCtx::new(&encoder);
+ let err = is_null.try_to_proto(&ctx).unwrap_err();
+ assert!(matches!(err, DataFusionError::Internal(msg) if msg.contains("call 1")));
+ }
+
+ #[test]
+ fn try_from_proto_decodes_is_null_expr() {
+ let node = is_null_node(Some(Box::new(column_node("a"))));
+ let schema = Schema::empty();
+ let decoder = StubDecoder::ok();
+ let ctx = PhysicalExprDecodeCtx::new(&schema, &decoder);
+
+ let decoded = IsNullExpr::try_from_proto(&node, &ctx).unwrap();
+ let is_null = decoded
+ .downcast_ref::<IsNullExpr>()
+ .expect("decoded expr should be an IsNullExpr");
+ assert!(is_null.arg().downcast_ref::<Column>().is_some());
+ }
+
+ #[test]
+ fn try_from_proto_rejects_non_is_null_node() {
+ let node = column_node("a");
+ let schema = Schema::empty();
+ let decoder = UnreachableDecoder;
+ let ctx = PhysicalExprDecodeCtx::new(&schema, &decoder);
+ let err = IsNullExpr::try_from_proto(&node, &ctx).unwrap_err();
+ assert!(
+ matches!(err, DataFusionError::Internal(msg) if msg.contains("PhysicalExprNode is not a IsNullExpr"))
+ );
+ }
+
+ #[test]
+ fn try_from_proto_rejects_missing_expr() {
+ let node = is_null_node(None);
+ let schema = Schema::empty();
+ let decoder = UnreachableDecoder;
+ let ctx = PhysicalExprDecodeCtx::new(&schema, &decoder);
+ let err = IsNullExpr::try_from_proto(&node, &ctx).unwrap_err();
+ assert!(
+ matches!(err, DataFusionError::Internal(msg) if msg.contains("IsNullExpr is missing required field 'expr'"))
+ );
+ }
+
+ #[test]
+ fn try_from_proto_propagates_expr_decode_error() {
+ let node = is_null_node(Some(Box::new(column_node("a"))));
+ let schema = Schema::empty();
+ let decoder = StubDecoder::failing_on(1);
+ let ctx = PhysicalExprDecodeCtx::new(&schema, &decoder);
+ let err = IsNullExpr::try_from_proto(&node, &ctx).unwrap_err();
+ assert!(matches!(err, DataFusionError::Internal(msg) if msg.contains("call 1")));
+ }
+}
diff --git a/datafusion/proto/src/physical_plan/from_proto.rs b/datafusion/proto/src/physical_plan/from_proto.rs
index 8d7c11fb6a..75311e2440 100644
--- a/datafusion/proto/src/physical_plan/from_proto.rs
+++ b/datafusion/proto/src/physical_plan/from_proto.rs
@@ -298,15 +298,7 @@ pub fn parse_physical_expr_with_converter(
ExprType::Sort(_) => {
return not_impl_err!("Cannot convert sort expr node to physical expression");
}
- ExprType::IsNullExpr(e) => {
- Arc::new(IsNullExpr::new(parse_required_physical_expr(
- e.expr.as_deref(),
- ctx,
- "expr",
- input_schema,
- proto_converter,
- )?))
- }
+ ExprType::IsNullExpr(_) => IsNullExpr::try_from_proto(proto, &decode_ctx)?,
ExprType::IsNotNullExpr(_) => IsNotNullExpr::try_from_proto(proto, &decode_ctx)?,
ExprType::NotExpr(_) => NotExpr::try_from_proto(proto, &decode_ctx)?,
ExprType::Negative(_) => NegativeExpr::try_from_proto(proto, &decode_ctx)?,
diff --git a/datafusion/proto/src/physical_plan/to_proto.rs b/datafusion/proto/src/physical_plan/to_proto.rs
index 17d363fa06..cb7580269b 100644
--- a/datafusion/proto/src/physical_plan/to_proto.rs
+++ b/datafusion/proto/src/physical_plan/to_proto.rs
@@ -36,7 +36,7 @@ use datafusion_physical_expr::scalar_subquery::ScalarSubqueryExpr;
use datafusion_physical_expr::window::{SlidingAggregateWindowExpr, StandardWindowExpr};
use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
use datafusion_physical_plan::expressions::{
- CaseExpr, DynamicFilterPhysicalExpr, IsNullExpr, Literal, TryCastExpr,
+ CaseExpr, DynamicFilterPhysicalExpr, Literal, TryCastExpr,
};
use datafusion_physical_plan::udaf::AggregateFunctionExpr;
use datafusion_physical_plan::windows::{PlainAggregateWindowExpr, WindowUDFExpr};
@@ -345,17 +345,6 @@ pub fn serialize_physical_expr_with_converter(
),
),
})
- } else if let Some(expr) = expr.downcast_ref::<IsNullExpr>() {
- Ok(protobuf::PhysicalExprNode {
- expr_id,
- expr_type: Some(protobuf::physical_expr_node::ExprType::IsNullExpr(
- Box::new(protobuf::PhysicalIsNull {
- expr: Some(Box::new(
- proto_converter.physical_expr_to_proto(expr.arg(), codec)?,
- )),
- }),
- )),
- })
} else if let Some(lit) = expr.downcast_ref::<Literal>() {
Ok(protobuf::PhysicalExprNode {
expr_id,
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org