| // 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 arrow::array::{ |
| Array, ArrayRef, BooleanBuilder, Float32Builder, Float64Builder, Int32Builder, Int64Builder, |
| RecordBatch, StringBuilder, StructArray, |
| }; |
| use arrow::datatypes::{DataType, Field, Schema}; |
| use datafusion::common::Result; |
| use datafusion::physical_expr::PhysicalExpr; |
| use datafusion::physical_plan::ColumnarValue; |
| use std::any::Any; |
| use std::fmt::{Debug, Display, Formatter}; |
| use std::sync::Arc; |
| |
| /// from_json function - parses JSON strings into structured types |
| #[derive(Debug, Eq)] |
| pub struct FromJson { |
| /// The JSON string input expression |
| expr: Arc<dyn PhysicalExpr>, |
| /// Target schema for parsing |
| schema: DataType, |
| /// Timezone for timestamp parsing (future use) |
| timezone: String, |
| } |
| |
| impl PartialEq for FromJson { |
| fn eq(&self, other: &Self) -> bool { |
| self.expr.eq(&other.expr) && self.schema == other.schema && self.timezone == other.timezone |
| } |
| } |
| |
| impl std::hash::Hash for FromJson { |
| fn hash<H: std::hash::Hasher>(&self, state: &mut H) { |
| self.expr.hash(state); |
| // Note: DataType doesn't implement Hash, so we hash its debug representation |
| format!("{:?}", self.schema).hash(state); |
| self.timezone.hash(state); |
| } |
| } |
| |
| impl FromJson { |
| pub fn new(expr: Arc<dyn PhysicalExpr>, schema: DataType, timezone: &str) -> Self { |
| Self { |
| expr, |
| schema, |
| timezone: timezone.to_owned(), |
| } |
| } |
| } |
| |
| impl Display for FromJson { |
| fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
| write!( |
| f, |
| "from_json({}, schema={:?}, timezone={})", |
| self.expr, self.schema, self.timezone |
| ) |
| } |
| } |
| |
| impl PartialEq<dyn Any> for FromJson { |
| fn eq(&self, other: &dyn Any) -> bool { |
| if let Some(other) = other.downcast_ref::<FromJson>() { |
| self.expr.eq(&other.expr) |
| && self.schema == other.schema |
| && self.timezone == other.timezone |
| } else { |
| false |
| } |
| } |
| } |
| |
| impl PhysicalExpr for FromJson { |
| fn as_any(&self) -> &dyn Any { |
| self |
| } |
| |
| fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
| Display::fmt(self, f) |
| } |
| |
| fn data_type(&self, _: &Schema) -> Result<DataType> { |
| Ok(self.schema.clone()) |
| } |
| |
| fn nullable(&self, _input_schema: &Schema) -> Result<bool> { |
| // Always nullable - parse errors return null in PERMISSIVE mode |
| Ok(true) |
| } |
| |
| fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> { |
| let input = self.expr.evaluate(batch)?.into_array(batch.num_rows())?; |
| Ok(ColumnarValue::Array(json_string_to_struct( |
| &input, |
| &self.schema, |
| )?)) |
| } |
| |
| fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> { |
| vec![&self.expr] |
| } |
| |
| fn with_new_children( |
| self: Arc<Self>, |
| children: Vec<Arc<dyn PhysicalExpr>>, |
| ) -> Result<Arc<dyn PhysicalExpr>> { |
| assert!(children.len() == 1); |
| Ok(Arc::new(Self::new( |
| Arc::clone(&children[0]), |
| self.schema.clone(), |
| &self.timezone, |
| ))) |
| } |
| } |
| |
| /// Parse JSON string array into struct array |
| fn json_string_to_struct(arr: &Arc<dyn Array>, schema: &DataType) -> Result<ArrayRef> { |
| use arrow::array::StringArray; |
| use arrow::buffer::NullBuffer; |
| |
| let string_array = arr.as_any().downcast_ref::<StringArray>().ok_or_else(|| { |
| datafusion::common::DataFusionError::Execution("from_json expects string input".to_string()) |
| })?; |
| |
| let DataType::Struct(fields) = schema else { |
| return Err(datafusion::common::DataFusionError::Execution( |
| "from_json requires struct schema".to_string(), |
| )); |
| }; |
| |
| let num_rows = string_array.len(); |
| let mut field_builders = create_field_builders(fields, num_rows)?; |
| let mut struct_nulls = vec![true; num_rows]; |
| for (row_idx, struct_null) in struct_nulls.iter_mut().enumerate() { |
| if string_array.is_null(row_idx) { |
| // Null input -> null struct |
| *struct_null = false; |
| append_null_to_all_builders(&mut field_builders); |
| } else { |
| let json_str = string_array.value(row_idx); |
| |
| // Parse JSON (PERMISSIVE mode: return null fields on error) |
| match serde_json::from_str::<serde_json::Value>(json_str) { |
| Ok(json_value) => { |
| if let serde_json::Value::Object(obj) = json_value { |
| // Struct is not null, extract each field |
| *struct_null = true; |
| for (field, builder) in fields.iter().zip(field_builders.iter_mut()) { |
| let field_value = obj.get(field.name()); |
| append_field_value(builder, field, field_value)?; |
| } |
| } else { |
| // Not an object -> struct with null fields |
| *struct_null = true; |
| append_null_to_all_builders(&mut field_builders); |
| } |
| } |
| Err(_) => { |
| // Parse error -> struct with null fields (PERMISSIVE mode) |
| *struct_null = true; |
| append_null_to_all_builders(&mut field_builders); |
| } |
| } |
| } |
| } |
| |
| let arrays: Vec<ArrayRef> = field_builders |
| .into_iter() |
| .map(finish_builder) |
| .collect::<Result<Vec<_>>>()?; |
| let null_buffer = NullBuffer::from(struct_nulls); |
| Ok(Arc::new(StructArray::new( |
| fields.clone(), |
| arrays, |
| Some(null_buffer), |
| ))) |
| } |
| |
| /// Builder enum for different data types |
| enum FieldBuilder { |
| Int32(Int32Builder), |
| Int64(Int64Builder), |
| Float32(Float32Builder), |
| Float64(Float64Builder), |
| Boolean(BooleanBuilder), |
| String(StringBuilder), |
| Struct { |
| fields: arrow::datatypes::Fields, |
| builders: Vec<FieldBuilder>, |
| null_buffer: Vec<bool>, |
| }, |
| } |
| |
| fn create_field_builders( |
| fields: &arrow::datatypes::Fields, |
| capacity: usize, |
| ) -> Result<Vec<FieldBuilder>> { |
| fields |
| .iter() |
| .map(|field| match field.data_type() { |
| DataType::Int32 => Ok(FieldBuilder::Int32(Int32Builder::with_capacity(capacity))), |
| DataType::Int64 => Ok(FieldBuilder::Int64(Int64Builder::with_capacity(capacity))), |
| DataType::Float32 => Ok(FieldBuilder::Float32(Float32Builder::with_capacity( |
| capacity, |
| ))), |
| DataType::Float64 => Ok(FieldBuilder::Float64(Float64Builder::with_capacity( |
| capacity, |
| ))), |
| DataType::Boolean => Ok(FieldBuilder::Boolean(BooleanBuilder::with_capacity( |
| capacity, |
| ))), |
| DataType::Utf8 => Ok(FieldBuilder::String(StringBuilder::with_capacity( |
| capacity, |
| capacity * 16, |
| ))), |
| DataType::Struct(nested_fields) => { |
| let nested_builders = create_field_builders(nested_fields, capacity)?; |
| Ok(FieldBuilder::Struct { |
| fields: nested_fields.clone(), |
| builders: nested_builders, |
| null_buffer: Vec::with_capacity(capacity), |
| }) |
| } |
| dt => Err(datafusion::common::DataFusionError::Execution(format!( |
| "Unsupported field type in from_json: {:?}", |
| dt |
| ))), |
| }) |
| .collect() |
| } |
| |
| fn append_null_to_all_builders(builders: &mut [FieldBuilder]) { |
| for builder in builders { |
| match builder { |
| FieldBuilder::Int32(b) => b.append_null(), |
| FieldBuilder::Int64(b) => b.append_null(), |
| FieldBuilder::Float32(b) => b.append_null(), |
| FieldBuilder::Float64(b) => b.append_null(), |
| FieldBuilder::Boolean(b) => b.append_null(), |
| FieldBuilder::String(b) => b.append_null(), |
| FieldBuilder::Struct { |
| builders: nested_builders, |
| null_buffer, |
| .. |
| } => { |
| // Append null to nested struct |
| null_buffer.push(false); |
| append_null_to_all_builders(nested_builders); |
| } |
| } |
| } |
| } |
| |
| fn append_field_value( |
| builder: &mut FieldBuilder, |
| field: &Field, |
| json_value: Option<&serde_json::Value>, |
| ) -> Result<()> { |
| use serde_json::Value; |
| |
| let value = match json_value { |
| Some(Value::Null) | None => { |
| // Missing field or explicit null -> append null |
| match builder { |
| FieldBuilder::Int32(b) => b.append_null(), |
| FieldBuilder::Int64(b) => b.append_null(), |
| FieldBuilder::Float32(b) => b.append_null(), |
| FieldBuilder::Float64(b) => b.append_null(), |
| FieldBuilder::Boolean(b) => b.append_null(), |
| FieldBuilder::String(b) => b.append_null(), |
| FieldBuilder::Struct { |
| builders: nested_builders, |
| null_buffer, |
| .. |
| } => { |
| null_buffer.push(false); |
| append_null_to_all_builders(nested_builders); |
| } |
| } |
| return Ok(()); |
| } |
| Some(v) => v, |
| }; |
| |
| match (builder, field.data_type()) { |
| (FieldBuilder::Int32(b), DataType::Int32) => { |
| if let Some(i) = value.as_i64() { |
| if i >= i32::MIN as i64 && i <= i32::MAX as i64 { |
| b.append_value(i as i32); |
| } else { |
| b.append_null(); // Overflow |
| } |
| } else { |
| b.append_null(); // Type mismatch |
| } |
| } |
| (FieldBuilder::Int64(b), DataType::Int64) => { |
| if let Some(i) = value.as_i64() { |
| b.append_value(i); |
| } else { |
| b.append_null(); |
| } |
| } |
| (FieldBuilder::Float32(b), DataType::Float32) => { |
| if let Some(f) = value.as_f64() { |
| b.append_value(f as f32); |
| } else { |
| b.append_null(); |
| } |
| } |
| (FieldBuilder::Float64(b), DataType::Float64) => { |
| if let Some(f) = value.as_f64() { |
| b.append_value(f); |
| } else { |
| b.append_null(); |
| } |
| } |
| (FieldBuilder::Boolean(b), DataType::Boolean) => { |
| if let Some(bool_val) = value.as_bool() { |
| b.append_value(bool_val); |
| } else { |
| b.append_null(); |
| } |
| } |
| (FieldBuilder::String(b), DataType::Utf8) => { |
| if let Some(s) = value.as_str() { |
| b.append_value(s); |
| } else { |
| // Stringify non-string values |
| b.append_value(value.to_string()); |
| } |
| } |
| ( |
| FieldBuilder::Struct { |
| fields: nested_fields, |
| builders: nested_builders, |
| null_buffer, |
| }, |
| DataType::Struct(_), |
| ) => { |
| // Handle nested struct |
| if let Some(obj) = value.as_object() { |
| // Non-null nested struct |
| null_buffer.push(true); |
| for (nested_field, nested_builder) in |
| nested_fields.iter().zip(nested_builders.iter_mut()) |
| { |
| let nested_value = obj.get(nested_field.name()); |
| append_field_value(nested_builder, nested_field, nested_value)?; |
| } |
| } else { |
| // Not an object -> null nested struct |
| null_buffer.push(false); |
| append_null_to_all_builders(nested_builders); |
| } |
| } |
| _ => { |
| return Err(datafusion::common::DataFusionError::Execution( |
| "Type mismatch in from_json".to_string(), |
| )); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn finish_builder(builder: FieldBuilder) -> Result<ArrayRef> { |
| Ok(match builder { |
| FieldBuilder::Int32(mut b) => Arc::new(b.finish()), |
| FieldBuilder::Int64(mut b) => Arc::new(b.finish()), |
| FieldBuilder::Float32(mut b) => Arc::new(b.finish()), |
| FieldBuilder::Float64(mut b) => Arc::new(b.finish()), |
| FieldBuilder::Boolean(mut b) => Arc::new(b.finish()), |
| FieldBuilder::String(mut b) => Arc::new(b.finish()), |
| FieldBuilder::Struct { |
| fields, |
| builders, |
| null_buffer, |
| } => { |
| let nested_arrays: Vec<ArrayRef> = builders |
| .into_iter() |
| .map(finish_builder) |
| .collect::<Result<Vec<_>>>()?; |
| let null_buf = arrow::buffer::NullBuffer::from(null_buffer); |
| Arc::new(StructArray::new(fields, nested_arrays, Some(null_buf))) |
| } |
| }) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use arrow::array::{Int32Array, StringArray}; |
| use arrow::datatypes::Fields; |
| |
| #[test] |
| fn test_simple_struct() -> Result<()> { |
| let schema = DataType::Struct(Fields::from(vec![ |
| Field::new("a", DataType::Int32, true), |
| Field::new("b", DataType::Utf8, true), |
| ])); |
| |
| let input: Arc<dyn Array> = Arc::new(StringArray::from(vec![ |
| Some(r#"{"a": 123, "b": "hello"}"#), |
| Some(r#"{"a": 456}"#), |
| Some(r#"invalid json"#), |
| None, |
| ])); |
| |
| let result = json_string_to_struct(&input, &schema)?; |
| let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap(); |
| |
| assert_eq!(struct_array.len(), 4); |
| |
| // First row |
| let a_array = struct_array |
| .column(0) |
| .as_any() |
| .downcast_ref::<Int32Array>() |
| .unwrap(); |
| assert_eq!(a_array.value(0), 123); |
| let b_array = struct_array |
| .column(1) |
| .as_any() |
| .downcast_ref::<StringArray>() |
| .unwrap(); |
| assert_eq!(b_array.value(0), "hello"); |
| |
| // Second row (missing field b) |
| assert_eq!(a_array.value(1), 456); |
| assert!(b_array.is_null(1)); |
| |
| // Third row (parse error -> struct NOT null, all fields null) |
| assert!(!struct_array.is_null(2), "Struct should not be null"); |
| assert!(a_array.is_null(2)); |
| assert!(b_array.is_null(2)); |
| |
| // Fourth row (null input -> struct IS null) |
| assert!(struct_array.is_null(3), "Struct itself should be null"); |
| |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_all_primitive_types() -> Result<()> { |
| let schema = DataType::Struct(Fields::from(vec![ |
| Field::new("i32", DataType::Int32, true), |
| Field::new("i64", DataType::Int64, true), |
| Field::new("f32", DataType::Float32, true), |
| Field::new("f64", DataType::Float64, true), |
| Field::new("bool", DataType::Boolean, true), |
| Field::new("str", DataType::Utf8, true), |
| ])); |
| |
| let input: Arc<dyn Array> = Arc::new(StringArray::from(vec![Some( |
| r#"{"i32":123,"i64":9999999999,"f32":1.5,"f64":2.5,"bool":true,"str":"test"}"#, |
| )])); |
| |
| let result = json_string_to_struct(&input, &schema)?; |
| let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap(); |
| |
| assert_eq!(struct_array.len(), 1); |
| |
| // Verify all types |
| let i32_array = struct_array |
| .column(0) |
| .as_any() |
| .downcast_ref::<Int32Array>() |
| .unwrap(); |
| assert_eq!(i32_array.value(0), 123); |
| |
| let i64_array = struct_array |
| .column(1) |
| .as_any() |
| .downcast_ref::<arrow::array::Int64Array>() |
| .unwrap(); |
| assert_eq!(i64_array.value(0), 9999999999); |
| |
| let f32_array = struct_array |
| .column(2) |
| .as_any() |
| .downcast_ref::<arrow::array::Float32Array>() |
| .unwrap(); |
| assert_eq!(f32_array.value(0), 1.5); |
| |
| let f64_array = struct_array |
| .column(3) |
| .as_any() |
| .downcast_ref::<arrow::array::Float64Array>() |
| .unwrap(); |
| assert_eq!(f64_array.value(0), 2.5); |
| |
| let bool_array = struct_array |
| .column(4) |
| .as_any() |
| .downcast_ref::<arrow::array::BooleanArray>() |
| .unwrap(); |
| assert!(bool_array.value(0)); |
| |
| let str_array = struct_array |
| .column(5) |
| .as_any() |
| .downcast_ref::<StringArray>() |
| .unwrap(); |
| assert_eq!(str_array.value(0), "test"); |
| |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_empty_and_null_json() -> Result<()> { |
| let schema = DataType::Struct(Fields::from(vec![ |
| Field::new("a", DataType::Int32, true), |
| Field::new("b", DataType::Utf8, true), |
| ])); |
| |
| let input: Arc<dyn Array> = Arc::new(StringArray::from(vec![ |
| Some(r#"{}"#), // Empty object |
| Some(r#"null"#), // JSON null |
| Some(r#"[]"#), // Array (not object) |
| Some(r#"123"#), // Number (not object) |
| ])); |
| |
| let result = json_string_to_struct(&input, &schema)?; |
| let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap(); |
| |
| assert_eq!(struct_array.len(), 4); |
| |
| let a_array = struct_array |
| .column(0) |
| .as_any() |
| .downcast_ref::<Int32Array>() |
| .unwrap(); |
| let b_array = struct_array |
| .column(1) |
| .as_any() |
| .downcast_ref::<StringArray>() |
| .unwrap(); |
| |
| // All rows should have non-null structs with null field values |
| for i in 0..4 { |
| assert!( |
| !struct_array.is_null(i), |
| "Row {} struct should not be null", |
| i |
| ); |
| assert!(a_array.is_null(i), "Row {} field a should be null", i); |
| assert!(b_array.is_null(i), "Row {} field b should be null", i); |
| } |
| |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_nested_struct() -> Result<()> { |
| let schema = DataType::Struct(Fields::from(vec![ |
| Field::new( |
| "outer", |
| DataType::Struct(Fields::from(vec![ |
| Field::new("inner_a", DataType::Int32, true), |
| Field::new("inner_b", DataType::Utf8, true), |
| ])), |
| true, |
| ), |
| Field::new("top_level", DataType::Int32, true), |
| ])); |
| |
| let input: Arc<dyn Array> = Arc::new(StringArray::from(vec![ |
| Some(r#"{"outer":{"inner_a":123,"inner_b":"hello"},"top_level":999}"#), |
| Some(r#"{"outer":{"inner_a":456},"top_level":888}"#), // Missing nested field |
| Some(r#"{"outer":null,"top_level":777}"#), // Null nested struct |
| Some(r#"{"top_level":666}"#), // Missing nested struct |
| ])); |
| |
| let result = json_string_to_struct(&input, &schema)?; |
| let struct_array = result.as_any().downcast_ref::<StructArray>().unwrap(); |
| |
| assert_eq!(struct_array.len(), 4); |
| |
| // Check outer struct |
| let outer_array = struct_array |
| .column(0) |
| .as_any() |
| .downcast_ref::<StructArray>() |
| .unwrap(); |
| let top_level_array = struct_array |
| .column(1) |
| .as_any() |
| .downcast_ref::<Int32Array>() |
| .unwrap(); |
| |
| // Row 0: Valid nested struct |
| assert!(!outer_array.is_null(0), "Nested struct should not be null"); |
| let inner_a_array = outer_array |
| .column(0) |
| .as_any() |
| .downcast_ref::<Int32Array>() |
| .unwrap(); |
| let inner_b_array = outer_array |
| .column(1) |
| .as_any() |
| .downcast_ref::<StringArray>() |
| .unwrap(); |
| assert_eq!(inner_a_array.value(0), 123); |
| assert_eq!(inner_b_array.value(0), "hello"); |
| assert_eq!(top_level_array.value(0), 999); |
| |
| // Row 1: Missing nested field |
| assert!(!outer_array.is_null(1)); |
| assert_eq!(inner_a_array.value(1), 456); |
| assert!(inner_b_array.is_null(1)); |
| assert_eq!(top_level_array.value(1), 888); |
| |
| // Row 2: Null nested struct |
| assert!(outer_array.is_null(2), "Nested struct should be null"); |
| assert_eq!(top_level_array.value(2), 777); |
| |
| // Row 3: Missing nested struct |
| assert!(outer_array.is_null(3), "Nested struct should be null"); |
| assert_eq!(top_level_array.value(3), 666); |
| |
| Ok(()) |
| } |
| } |