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

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new eec8b947c7 refactor(proto): migrate JsonSource serde (#24178)
eec8b947c7 is described below

commit eec8b947c71a916385a28a3723753f4ffc7cd8d0
Author: Burak Şen <[email protected]>
AuthorDate: Sat Aug 8 18:37:37 2026 +0300

    refactor(proto): migrate JsonSource serde (#24178)
    
    ## Which issue does this PR close?
    
    - Part of #23516.
    
    ## Rationale for this change
    
    Part of epic #23494. Moves `JsonSource` protobuf serialization from the
    central dispatch into the source implementation.
    
    ## What changes are included in this PR?
    
    Add protobuf serialization and deserialization to `JsonSource`.
    
    Repoint the live decode arm to `JsonSource::try_from_proto` and remove
    the old central encode arm. Keep `try_into_json_scan_physical_plan` as a
    deprecated compatibility wrapper that delegates to the new
    implementation.
    
    The protobuf wire format remains unchanged.
    
    ## Are these changes tested?
    
    Yes. Added `roundtrip_json_scan`.
    
    ## Are there any user-facing changes?
    
    The existing `PhysicalPlanNodeExt` method remains available as a
    deprecated compatibility wrapper. There is no immediate API removal or
    wire-format change.
---
 datafusion/datasource-json/Cargo.toml              |  1 +
 datafusion/datasource-json/src/source.rs           | 54 ++++++++++++++++++++++
 datafusion/proto/src/physical_plan/mod.rs          | 39 ++++++----------
 .../proto/tests/cases/roundtrip_physical_plan.rs   | 20 ++++++--
 4 files changed, 85 insertions(+), 29 deletions(-)

diff --git a/datafusion/datasource-json/Cargo.toml 
b/datafusion/datasource-json/Cargo.toml
index 7aefbb42c1..04192083f5 100644
--- a/datafusion/datasource-json/Cargo.toml
+++ b/datafusion/datasource-json/Cargo.toml
@@ -31,6 +31,7 @@ version.workspace = true
 all-features = true
 
 [features]
+# Enables protobuf serialization hooks for JSON sources and sinks.
 proto = [
     "dep:datafusion-proto-models",
     "datafusion-datasource/proto",
diff --git a/datafusion/datasource-json/src/source.rs 
b/datafusion/datasource-json/src/source.rs
index 8632d6b942..b7c2e5a45c 100644
--- a/datafusion/datasource-json/src/source.rs
+++ b/datafusion/datasource-json/src/source.rs
@@ -231,6 +231,60 @@ impl FileSource for JsonSource {
     fn file_type(&self) -> &str {
         "json"
     }
+
+    /// Emit a `JsonScan` node wrapping the shared base config.
+    #[cfg(feature = "proto")]
+    fn try_to_proto(
+        &self,
+        base: &FileScanConfig,
+        ctx: &datafusion_physical_plan::proto::ExecutionPlanEncodeCtx<'_>,
+    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
+        use datafusion_proto_models::protobuf;
+        use protobuf::physical_plan_node::PhysicalPlanType;
+
+        let node = protobuf::JsonScanExecNode {
+            base_conf: Some(base.try_to_proto(ctx)?),
+        };
+        Ok(Some(protobuf::PhysicalPlanNode {
+            physical_plan_type: Some(PhysicalPlanType::JsonScan(node)),
+        }))
+    }
+}
+
+#[cfg(feature = "proto")]
+impl JsonSource {
+    /// Reconstructs a `DataSourceExec` from a protobuf `JsonScan`.
+    ///
+    /// Defaults to newline-delimited JSON because protobuf does not encode 
the mode.
+    pub fn try_from_proto(
+        node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
+        ctx: &datafusion_physical_plan::proto::ExecutionPlanDecodeCtx<'_>,
+    ) -> Result<Arc<dyn ExecutionPlan>> {
+        use datafusion_datasource::file_scan_config::FileScanConfig;
+        use datafusion_datasource::source::DataSourceExec;
+        use datafusion_proto_models::protobuf;
+
+        let scan = match &node.physical_plan_type {
+            
Some(protobuf::physical_plan_node::PhysicalPlanType::JsonScan(scan)) => scan,
+            _ => {
+                return datafusion_common::internal_err!(
+                    "PhysicalPlanNode is not a JsonScan"
+                );
+            }
+        };
+
+        let base_conf = scan.base_conf.as_ref().ok_or_else(|| {
+            datafusion_common::internal_datafusion_err!(
+                "JsonScanExecNode is missing required field 'base_conf'"
+            )
+        })?;
+
+        let table_schema = 
FileScanConfig::parse_table_schema_from_proto(base_conf)?;
+        let source = Arc::new(JsonSource::new(table_schema));
+
+        let conf = FileScanConfig::try_from_proto(base_conf, ctx, source)?;
+        Ok(DataSourceExec::from_data_source(conf))
+    }
 }
 
 impl FileOpener for JsonOpener {
diff --git a/datafusion/proto/src/physical_plan/mod.rs 
b/datafusion/proto/src/physical_plan/mod.rs
index 743cbfbf51..ee2d431a41 100644
--- a/datafusion/proto/src/physical_plan/mod.rs
+++ b/datafusion/proto/src/physical_plan/mod.rs
@@ -1079,8 +1079,8 @@ pub trait PhysicalPlanNodeExt: Sized {
             PhysicalPlanType::CsvScan(_) => {
                 CsvSource::try_from_proto(self.node(), &decode_ctx)
             }
-            PhysicalPlanType::JsonScan(scan) => {
-                self.try_into_json_scan_physical_plan(scan, ctx, 
proto_converter)
+            PhysicalPlanType::JsonScan(_) => {
+                JsonSource::try_from_proto(self.node(), &decode_ctx)
             }
             PhysicalPlanType::ParquetScan(_) => {
                 #[cfg(feature = "parquet")]
@@ -1360,21 +1360,25 @@ pub trait PhysicalPlanNodeExt: Sized {
         CsvSource::try_from_proto(&node, &decode_ctx)
     }
 
+    #[deprecated(
+        since = "55.0.0",
+        note = "unused by DataFusion; `JsonSource` deserializes itself via 
`JsonSource::try_from_proto`"
+    )]
     fn try_into_json_scan_physical_plan(
         &self,
         scan: &protobuf::JsonScanExecNode,
         ctx: &PhysicalPlanDecodeContext<'_>,
         proto_converter: &dyn PhysicalProtoConverterExtension,
     ) -> Result<Arc<dyn ExecutionPlan>> {
-        let base_conf = scan.base_conf.as_ref().unwrap();
-        let table_schema = parse_table_schema_from_proto(base_conf)?;
-        let scan_conf = parse_protobuf_file_scan_config(
-            base_conf,
+        let node = protobuf::PhysicalPlanNode {
+            physical_plan_type: Some(PhysicalPlanType::JsonScan(scan.clone())),
+        };
+        let decoder = ConverterPlanDecoder {
             ctx,
             proto_converter,
-            Arc::new(JsonSource::new(table_schema)),
-        )?;
-        Ok(DataSourceExec::from_data_source(scan_conf))
+        };
+        let decode_ctx = ExecutionPlanDecodeCtx::new(&decoder);
+        JsonSource::try_from_proto(&node, &decode_ctx)
     }
 
     fn try_into_arrow_scan_physical_plan(
@@ -2476,23 +2480,6 @@ pub trait PhysicalPlanNodeExt: Sized {
     ) -> Result<Option<protobuf::PhysicalPlanNode>> {
         let data_source = data_source_exec.data_source();
 
-        if let Some(scan_conf) = data_source.downcast_ref::<FileScanConfig>() {
-            let source = scan_conf.file_source();
-            if let Some(_json_source) = source.downcast_ref::<JsonSource>() {
-                return Ok(Some(protobuf::PhysicalPlanNode {
-                    physical_plan_type: Some(PhysicalPlanType::JsonScan(
-                        protobuf::JsonScanExecNode {
-                            base_conf: Some(serialize_file_scan_config(
-                                scan_conf,
-                                codec,
-                                proto_converter,
-                            )?),
-                        },
-                    )),
-                }));
-            }
-        }
-
         if let Some(scan_conf) = data_source.downcast_ref::<FileScanConfig>() {
             let source = scan_conf.file_source();
             if let Some(_arrow_source) = source.downcast_ref::<ArrowSource>() {
diff --git a/datafusion/proto/tests/cases/roundtrip_physical_plan.rs 
b/datafusion/proto/tests/cases/roundtrip_physical_plan.rs
index 65a817b4cb..9efbd90f15 100644
--- a/datafusion/proto/tests/cases/roundtrip_physical_plan.rs
+++ b/datafusion/proto/tests/cases/roundtrip_physical_plan.rs
@@ -37,8 +37,8 @@ use datafusion::datasource::listing::{
 use datafusion::datasource::object_store::ObjectStoreUrl;
 use datafusion::datasource::physical_plan::{
     ArrowSource, CsvSource, FileGroup, FileOutputMode, FileScanConfig,
-    FileScanConfigBuilder, FileSinkConfig, ParquetSource, 
wrap_partition_type_in_dict,
-    wrap_partition_value_in_dict,
+    FileScanConfigBuilder, FileSinkConfig, JsonSource, ParquetSource,
+    wrap_partition_type_in_dict, wrap_partition_value_in_dict,
 };
 use datafusion::datasource::sink::{DataSink, DataSinkExec};
 use datafusion::datasource::source::DataSourceExec;
@@ -1363,6 +1363,21 @@ fn roundtrip_arrow_scan() -> Result<()> {
     roundtrip_test(DataSourceExec::from_data_source(scan_config))
 }
 
+#[test]
+fn roundtrip_json_scan() -> Result<()> {
+    let file_schema =
+        Arc::new(Schema::new(vec![Field::new("col", DataType::Utf8, false)]));
+    let file_source = 
Arc::new(JsonSource::new(TableSchema::from(&file_schema)));
+    let scan_config =
+        FileScanConfigBuilder::new(ObjectStoreUrl::local_filesystem(), 
file_source)
+            .with_file_groups(vec![FileGroup::new(vec![PartitionedFile::new(
+                "/path/to/file.json".to_string(),
+                1024,
+            )])])
+            .build();
+    roundtrip_test(DataSourceExec::from_data_source(scan_config))
+}
+
 #[test]
 fn roundtrip_csv_scan_preserves_format_options() -> Result<()> {
     use datafusion::common::config::CsvOptions;
@@ -1418,7 +1433,6 @@ fn roundtrip_csv_scan_preserves_format_options() -> 
Result<()> {
     assert!(csv_source.truncate_rows());
     Ok(())
 }
-
 #[tokio::test]
 async fn roundtrip_parquet_exec_with_table_partition_cols() -> Result<()> {
     let mut file_group =


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

Reply via email to