// Copyright 2024 RustFS Team // // Licensed 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::{ arrow::{ datatypes::{DataType, Field, Schema}, error::ArrowError, }, common::{DataFusionError, Result as DFResult}, }; use serde::de::{self, DeserializeSeed, MapAccess, SeqAccess, Visitor}; use std::{ collections::HashMap, fmt, io::{self, Cursor}, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, }; const CANCELLATION_CHECK_ITEMS: usize = 1024; const READER_BUFFER_BYTES: usize = 64 * 1024; pub(super) fn infer_schema( bytes: &[u8], expected_records: usize, cancellation: &AtomicBool, max_schema_bytes: usize, ) -> DFResult<(Schema, usize, usize)> { let mut context = InferenceContext { cancellation, items_since_check: 0, }; let mut fields = InferredObject::default(); let mut records = 0; for row in bytes.split_inclusive(|byte| *byte == b'\n') { if row.is_empty() { continue; } context.check_now()?; let reader = io::BufReader::with_capacity( READER_BUFFER_BYTES, CancellableReader { inner: Cursor::new(row), cancellation, }, ); let mut deserializer = serde_json::Deserializer::from_reader(reader); let inferred = InferSeed { context: &mut context } .deserialize(&mut deserializer) .map_err(|error| schema_parse_error(error, cancellation))?; deserializer.end().map_err(|error| schema_parse_error(error, cancellation))?; let InferredType::Object(record) = inferred else { return Err(DataFusionError::Execution( "JSON DOCUMENT schema inference expected an object row".to_string(), )); }; if record.retained_size() > max_schema_bytes { return Err(schema_complexity_error()); } fields.merge(record, &mut context)?; if fields.retained_size() > max_schema_bytes { return Err(schema_complexity_error()); } records += 1; } if records != expected_records { return Err(DataFusionError::Execution(format!( "JSON DOCUMENT schema prefix contained {expected_records} records but inference read {records}" ))); } let schema = Schema::new(fields.into_fields(&mut context)?); let schema_bytes = std::mem::size_of::() .checked_add(schema.fields().size()) .ok_or_else(schema_complexity_error)?; if schema_bytes > max_schema_bytes { return Err(schema_complexity_error()); } Ok((schema, records, schema_bytes)) } fn schema_parse_error(error: serde_json::Error, cancellation: &AtomicBool) -> DataFusionError { if cancellation.load(Ordering::Acquire) { DataFusionError::Execution("JSON DOCUMENT schema inference canceled".to_string()) } else { schema_json_error(format!("JSON DOCUMENT schema inference failed: {error}")) } } fn schema_json_error(message: impl Into) -> DataFusionError { DataFusionError::ArrowError(Box::new(ArrowError::JsonError(message.into())), None) } fn schema_complexity_error() -> DataFusionError { DataFusionError::ResourcesExhausted("JSON DOCUMENT schema exceeds the inference complexity limit".to_string()) } struct CancellableReader<'a> { inner: Cursor<&'a [u8]>, cancellation: &'a AtomicBool, } impl io::Read for CancellableReader<'_> { fn read(&mut self, buffer: &mut [u8]) -> io::Result { if self.cancellation.load(Ordering::Acquire) { return Err(io::Error::new(io::ErrorKind::Interrupted, "JSON DOCUMENT schema inference canceled")); } io::Read::read(&mut self.inner, buffer) } } struct InferenceContext<'a> { cancellation: &'a AtomicBool, items_since_check: usize, } impl InferenceContext<'_> { fn checkpoint(&mut self) -> Result<(), E> { self.items_since_check += 1; if self.items_since_check < CANCELLATION_CHECK_ITEMS { return Ok(()); } self.items_since_check = 0; if self.cancellation.load(Ordering::Acquire) { Err(E::custom("JSON DOCUMENT schema inference canceled")) } else { Ok(()) } } fn check_now(&self) -> DFResult<()> { if self.cancellation.load(Ordering::Acquire) { Err(DataFusionError::Execution("JSON DOCUMENT schema inference canceled".to_string())) } else { Ok(()) } } fn checkpoint_df(&mut self) -> DFResult<()> { self.items_since_check += 1; if self.items_since_check < CANCELLATION_CHECK_ITEMS { return Ok(()); } self.items_since_check = 0; self.check_now() } } struct InferSeed<'a, 'b> { context: &'a mut InferenceContext<'b>, } impl<'de> DeserializeSeed<'de> for InferSeed<'_, '_> { type Value = InferredType; fn deserialize(self, deserializer: D) -> Result where D: serde::Deserializer<'de>, { deserializer.deserialize_any(InferVisitor { context: self.context }) } } struct InferVisitor<'a, 'b> { context: &'a mut InferenceContext<'b>, } impl<'de> Visitor<'de> for InferVisitor<'_, '_> { type Value = InferredType; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("a JSON value") } fn visit_unit(self) -> Result { Ok(InferredType::Any) } fn visit_none(self) -> Result { Ok(InferredType::Any) } fn visit_bool(self, _value: bool) -> Result { Ok(InferredType::Scalar(ScalarKinds::BOOLEAN)) } fn visit_i64(self, _value: i64) -> Result { Ok(InferredType::Scalar(ScalarKinds::INTEGER)) } fn visit_u64(self, value: u64) -> Result { Ok(InferredType::Scalar(if i64::try_from(value).is_ok() { ScalarKinds::INTEGER } else { ScalarKinds::FLOAT })) } fn visit_f64(self, _value: f64) -> Result { Ok(InferredType::Scalar(ScalarKinds::FLOAT)) } fn visit_str(self, _value: &str) -> Result { Ok(InferredType::Scalar(ScalarKinds::STRING)) } fn visit_string(self, _value: String) -> Result { Ok(InferredType::Scalar(ScalarKinds::STRING)) } fn visit_seq(self, mut sequence: A) -> Result where A: SeqAccess<'de>, { let mut inner = InferredType::Any; let mut first = true; while let Some(value) = sequence.next_element_seed(InferSeed { context: self.context })? { self.context.checkpoint()?; if first && matches!(value, InferredType::Any) { inner = InferredType::Scalar(ScalarKinds::default()); } else { inner.merge_array_element(value, self.context).map_err(de::Error::custom)?; } first = false; } Ok(InferredType::Array(Box::new(inner))) } fn visit_map(self, mut map: A) -> Result where A: MapAccess<'de>, { let mut object = InferredObject::default(); while let Some(key) = map.next_key::()? { self.context.checkpoint()?; let value = map.next_value_seed(InferSeed { context: self.context })?; object.insert_record_field(key, value); } Ok(InferredType::Object(object)) } } #[derive(Clone, Copy, Debug, Default)] struct ScalarKinds(u8); impl ScalarKinds { const BOOLEAN: Self = Self(1 << 0); const INTEGER: Self = Self(1 << 1); const FLOAT: Self = Self(1 << 2); const STRING: Self = Self(1 << 3); fn merge(&mut self, other: Self) { self.0 |= other.0; } fn data_type(self) -> DataType { match self.0 { value if value == Self::BOOLEAN.0 => DataType::Boolean, value if value == Self::INTEGER.0 => DataType::Int64, value if value == Self::FLOAT.0 || value == (Self::INTEGER.0 | Self::FLOAT.0) => DataType::Float64, value if value == Self::STRING.0 => DataType::Utf8, _ => DataType::Utf8, } } } #[derive(Debug)] enum InferredType { Scalar(ScalarKinds), Array(Box), Object(InferredObject), Any, } impl InferredType { fn merge(&mut self, other: Self, context: &mut InferenceContext<'_>) -> DFResult<()> { match (self, other) { (Self::Array(current), Self::Array(other)) => current.merge(*other, context), (Self::Scalar(current), Self::Scalar(other)) => { current.merge(other); Ok(()) } (Self::Object(current), Self::Object(other)) => current.merge(other, context), (current @ Self::Any, value) => { *current = value; Ok(()) } (_, Self::Any) => Ok(()), (Self::Array(inner), scalar @ Self::Scalar(_)) => inner.merge(scalar, context), (current @ Self::Scalar(_), Self::Array(mut inner)) => { let scalar = std::mem::replace(current, Self::Any); inner.merge(scalar, context)?; *current = Self::Array(inner); Ok(()) } (current, other) => Err(schema_json_error(format!( "incompatible JSON types during schema inference: {} and {}", current.kind(), other.kind() ))), } } fn merge_array_element(&mut self, other: Self, context: &mut InferenceContext<'_>) -> DFResult<()> { match (self, other) { (current @ Self::Any, value) => { *current = value; Ok(()) } (_, Self::Any) => Ok(()), (Self::Scalar(current), Self::Scalar(other)) => { current.merge(other); Ok(()) } (Self::Object(current), Self::Object(other)) => current.merge(other, context), (Self::Array(current), Self::Array(other)) => current.merge(*other, context), (current, other) => Err(schema_json_error(format!( "incompatible JSON array elements during schema inference: {} and {}", current.kind(), other.kind() ))), } } fn kind(&self) -> &'static str { match self { Self::Scalar(_) => "scalar", Self::Array(_) => "array", Self::Object(_) => "object", Self::Any => "null", } } fn into_data_type(self, context: &mut InferenceContext<'_>) -> DFResult { context.check_now()?; match self { Self::Scalar(kinds) => Ok(kinds.data_type()), Self::Array(inner) => Ok(DataType::List(Arc::new(Field::new_list_field(inner.into_data_type(context)?, true)))), Self::Object(object) => Ok(DataType::Struct(object.into_fields(context)?.into())), Self::Any => Ok(DataType::Null), } } fn retained_size(&self) -> usize { std::mem::size_of::() + match self { Self::Array(inner) => inner.retained_size(), Self::Object(object) => object.retained_size(), Self::Scalar(_) | Self::Any => 0, } } } #[derive(Debug, Default)] struct InferredObject { indexes: HashMap, usize>, fields: Vec<(Arc, InferredType)>, } impl InferredObject { fn insert_record_field(&mut self, key: String, value: InferredType) { if let Some(index) = self.indexes.get(key.as_str()).copied() { self.fields[index].1 = value; return; } let key: Arc = Arc::from(key); let index = self.fields.len(); self.indexes.insert(Arc::clone(&key), index); self.fields.push((key, value)); } fn merge(&mut self, other: Self, context: &mut InferenceContext<'_>) -> DFResult<()> { for (key, value) in other.fields { context.checkpoint_df()?; if let Some(index) = self.indexes.get(key.as_ref()).copied() { self.fields[index].1.merge(value, context)?; } else { let index = self.fields.len(); self.indexes.insert(Arc::clone(&key), index); self.fields.push((key, value)); } } Ok(()) } fn into_fields(self, context: &mut InferenceContext<'_>) -> DFResult> { self.fields .into_iter() .map(|(name, inferred)| { context.check_now()?; Ok(Field::new(name.as_ref(), inferred.into_data_type(context)?, true)) }) .collect() } fn retained_size(&self) -> usize { let index_entry = std::mem::size_of::<(Arc, usize)>() + 16; let mut bytes = std::mem::size_of::() .saturating_add(self.indexes.capacity().saturating_mul(index_entry)) .saturating_add( self.fields .capacity() .saturating_mul(std::mem::size_of::<(Arc, InferredType)>()), ); for (name, inferred) in &self.fields { bytes = bytes.saturating_add(name.len()).saturating_add(inferred.retained_size()); } bytes } } #[cfg(test)] mod tests { use super::*; use datafusion::arrow::json::reader::infer_json_schema as arrow_infer_json_schema; const TEST_SCHEMA_LIMIT: usize = 4 * 1024 * 1024; fn assert_matches_arrow(input: &[u8]) { let records = input.iter().filter(|byte| **byte == b'\n').count(); let (expected, expected_records) = arrow_infer_json_schema(io::BufReader::new(input), Some(records)) .expect("Arrow should infer the compatibility fixture"); let (actual, actual_records, _) = infer_schema(input, records, &AtomicBool::new(false), TEST_SCHEMA_LIMIT) .expect("streaming inference should accept the compatibility fixture"); assert_eq!(actual_records, expected_records); assert_eq!(actual, expected); } #[test] fn inference_matches_arrow_for_nested_and_coerced_types() { assert_matches_arrow( br#"{"a":1,"values":[1,2],"nested":{"enabled":true},"nullable":null} {"a":1.5,"values":3,"nested":{"name":"ok"},"nullable":"set"} "#, ); assert_matches_arrow( br#"{"matrix":[[1,2],[3]],"objects":[{"id":1},{"name":"two"}],"empty":[]} {"matrix":[[4.5]],"objects":[],"empty":[null]} "#, ); } #[test] fn inference_matches_arrow_for_null_first_arrays_and_duplicate_keys() { assert_matches_arrow( br#"{"values":[null,1,2],"duplicate":1,"duplicate":"last"} "#, ); let input = br#"{"values":[null,{"id":1}]} "#; let records = 1; assert!(arrow_infer_json_schema(io::BufReader::new(input.as_slice()), Some(records)).is_err()); assert!(infer_schema(input, records, &AtomicBool::new(false), TEST_SCHEMA_LIMIT).is_err()); } #[test] fn inference_checkpoints_observe_cancellation() { let cancellation = AtomicBool::new(true); let mut context = InferenceContext { cancellation: &cancellation, items_since_check: CANCELLATION_CHECK_ITEMS - 1, }; let error = context .checkpoint::() .expect_err("the item checkpoint must observe cancellation"); assert!(error.to_string().contains("schema inference canceled")); } #[test] fn incompatible_types_preserve_arrow_json_error_classification() { let error = infer_schema(b"{\"a\":{}}\n{\"a\":1}\n", 2, &AtomicBool::new(false), TEST_SCHEMA_LIMIT) .expect_err("object and scalar fields must remain incompatible"); assert!(matches!( error, DataFusionError::ArrowError(source, None) if matches!(*source, ArrowError::JsonError(_)) )); } }