1#![allow(clippy::disallowed_types)] use std::collections::HashMap;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::Arc;
12
13use arrow_array::builder::{
14 BooleanBuilder, Float32Builder, Float64Builder, Int16Builder, Int32Builder, Int64Builder,
15 Int8Builder, LargeBinaryBuilder, LargeStringBuilder, ListBuilder, StringBuilder,
16 TimestampMicrosecondBuilder, TimestampMillisecondBuilder, TimestampNanosecondBuilder,
17 TimestampSecondBuilder, UInt16Builder, UInt32Builder, UInt64Builder, UInt8Builder,
18};
19use arrow_array::{ArrayRef, RecordBatch};
20use arrow_schema::{DataType, SchemaRef, TimeUnit};
21
22use crate::schema::error::{SchemaError, SchemaResult};
23use crate::schema::json::jsonb::JsonbEncoder;
24use crate::schema::traits::FormatDecoder;
25use crate::schema::types::RawRecord;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum UnknownFieldStrategy {
30 Ignore,
32 CollectExtra,
34 Reject,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum TypeMismatchStrategy {
41 Null,
43 Coerce,
45 Reject,
47}
48
49impl TypeMismatchStrategy {
50 #[must_use]
52 pub fn from_enforcement_str(s: &str) -> Option<Self> {
53 match s.to_lowercase().as_str() {
54 "coerce" => Some(Self::Coerce),
55 "strict" => Some(Self::Reject),
56 "permissive" => Some(Self::Null),
57 _ => None,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
65pub enum EpochUnit {
66 Seconds,
68 #[default]
70 Millis,
71 Micros,
73 Nanos,
75}
76
77impl EpochUnit {
78 #[must_use]
80 pub fn from_str_opt(s: &str) -> Option<Self> {
81 match s.trim().to_lowercase().as_str() {
82 "seconds" => Some(Self::Seconds),
83 "millis" => Some(Self::Millis),
84 "micros" => Some(Self::Micros),
85 "nanos" => Some(Self::Nanos),
86 _ => None,
87 }
88 }
89
90 const fn nanos_per(self) -> i64 {
92 match self {
93 Self::Seconds => 1_000_000_000,
94 Self::Millis => 1_000_000,
95 Self::Micros => 1_000,
96 Self::Nanos => 1,
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
103enum ColumnExtraction {
104 DefaultPath,
106 CustomPath { segments: Vec<String> },
108}
109
110#[derive(Debug, Clone)]
112pub struct JsonDecoderConfig {
113 pub unknown_fields: UnknownFieldStrategy,
115
116 pub type_mismatch: TypeMismatchStrategy,
118
119 pub timestamp_formats: Vec<String>,
123
124 pub nested_as_jsonb: bool,
128
129 pub json_path: Option<Vec<String>>,
133
134 pub json_column_paths: HashMap<String, Vec<String>>,
138
139 pub numeric_timestamp_units: HashMap<String, EpochUnit>,
142
143 pub json_explode: Option<Vec<String>>,
147}
148
149impl Default for JsonDecoderConfig {
150 fn default() -> Self {
151 Self {
152 unknown_fields: UnknownFieldStrategy::Ignore,
153 type_mismatch: TypeMismatchStrategy::Coerce,
154 timestamp_formats: vec![
155 "iso8601".into(),
156 "%Y-%m-%dT%H:%M:%S%.fZ".into(),
157 "%Y-%m-%dT%H:%M:%S%.f%:z".into(),
158 "%Y-%m-%d %H:%M:%S%.f".into(),
159 "%Y-%m-%d %H:%M:%S".into(),
160 ],
161 nested_as_jsonb: false,
162 json_path: None,
163 json_column_paths: HashMap::new(),
164 numeric_timestamp_units: HashMap::new(),
165 json_explode: None,
166 }
167 }
168}
169
170impl JsonDecoderConfig {
171 pub fn from_connector_config(
183 config: &crate::config::ConnectorConfig,
184 schema: &SchemaRef,
185 ) -> SchemaResult<Self> {
186 let mut cfg = Self::default();
187
188 if let Some(path) = config.get("json.path") {
189 cfg.json_path = Some(parse_path_option("json.path", path)?);
190 }
191
192 let mut col_props: Vec<_> = config
193 .properties_with_prefix("json.column.")
194 .into_iter()
195 .collect();
196 col_props.sort_unstable_by(|left, right| left.0.cmp(&right.0));
197 for (col_name, val) in col_props {
198 if let Some(col) = col_name.strip_suffix(".epoch_unit") {
203 let unit =
204 EpochUnit::from_str_opt(&val).ok_or_else(|| SchemaError::InvalidConfig {
205 key: format!("json.column.{col}.epoch_unit"),
206 message: format!(
207 "invalid epoch unit '{val}'; expected seconds, millis, micros, or nanos"
208 ),
209 })?;
210 let field =
211 schema
212 .field_with_name(col)
213 .map_err(|_| SchemaError::InvalidConfig {
214 key: format!("json.column.{col}.epoch_unit"),
215 message: format!("column '{col}' is not present in the source schema"),
216 })?;
217 if !matches!(field.data_type(), DataType::Timestamp(_, _)) {
218 return Err(SchemaError::InvalidConfig {
219 key: format!("json.column.{col}.epoch_unit"),
220 message: format!(
221 "column '{col}' must be a Timestamp, found {:?}",
222 field.data_type()
223 ),
224 });
225 }
226 cfg.numeric_timestamp_units.insert(col.to_string(), unit);
227 continue;
228 }
229 schema
230 .field_with_name(&col_name)
231 .map_err(|_| SchemaError::InvalidConfig {
232 key: format!("json.column.{col_name}"),
233 message: format!("column '{col_name}' is not present in the source schema"),
234 })?;
235 cfg.json_column_paths.insert(
236 col_name.clone(),
237 parse_path_option(&format!("json.column.{col_name}"), &val)?,
238 );
239 }
240
241 if let Some(explode) = config.get("json.explode") {
242 let columns: Vec<String> = explode
243 .split(',')
244 .map(str::trim)
245 .map(ToString::to_string)
246 .collect();
247 if columns.is_empty() || columns.iter().any(String::is_empty) {
248 return Err(SchemaError::InvalidConfig {
249 key: "json.explode".into(),
250 message: "expected one or more comma-separated schema columns".into(),
251 });
252 }
253 let mut seen = std::collections::HashSet::with_capacity(columns.len());
254 for column in &columns {
255 if !seen.insert(column) {
256 return Err(SchemaError::InvalidConfig {
257 key: "json.explode".into(),
258 message: format!("column '{column}' is listed more than once"),
259 });
260 }
261 schema
262 .field_with_name(column)
263 .map_err(|_| SchemaError::InvalidConfig {
264 key: "json.explode".into(),
265 message: format!("column '{column}' is not present in the source schema"),
266 })?;
267 }
268 cfg.json_explode = Some(columns);
269 }
270
271 if let Some(enforcement) = config.get("schema.enforcement") {
272 cfg.type_mismatch = TypeMismatchStrategy::from_enforcement_str(enforcement)
273 .ok_or_else(|| SchemaError::InvalidConfig {
274 key: "schema.enforcement".into(),
275 message: format!(
276 "invalid value '{enforcement}'; expected lenient, coerce, or strict"
277 ),
278 })?;
279 }
280 if let Some(v) = config.get("nested.as.jsonb") {
281 cfg.nested_as_jsonb = match v.to_ascii_lowercase().as_str() {
282 "true" => true,
283 "false" => false,
284 _ => {
285 return Err(SchemaError::InvalidConfig {
286 key: "nested.as.jsonb".into(),
287 message: format!("invalid boolean '{v}'; expected true or false"),
288 });
289 }
290 };
291 }
292 Ok(cfg)
293 }
294}
295
296fn parse_path_option(key: &str, value: &str) -> Result<Vec<String>, SchemaError> {
297 let segments: Vec<String> = value
298 .split('.')
299 .map(str::trim)
300 .map(ToString::to_string)
301 .collect();
302 if segments.is_empty() || segments.iter().any(String::is_empty) {
303 return Err(SchemaError::InvalidConfig {
304 key: key.into(),
305 message: "path must contain non-empty dot-separated segments".into(),
306 });
307 }
308 Ok(segments)
309}
310
311pub struct JsonDecoder {
318 schema: SchemaRef,
320 config: JsonDecoderConfig,
322 field_indices: Vec<(String, usize)>,
324 mismatch_count: AtomicU64,
326 column_extractions: Vec<ColumnExtraction>,
328 column_epoch_units: Vec<EpochUnit>,
331 explode_col_indices: Option<Vec<Option<usize>>>,
336}
337
338#[allow(clippy::missing_fields_in_debug)]
339impl std::fmt::Debug for JsonDecoder {
340 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341 f.debug_struct("JsonDecoder")
342 .field("schema", &self.schema)
343 .field("config", &self.config)
344 .field(
345 "mismatch_count",
346 &self.mismatch_count.load(Ordering::Relaxed),
347 )
348 .finish()
349 }
350}
351
352impl JsonDecoder {
353 #[must_use]
355 pub fn new(schema: SchemaRef) -> Self {
356 Self::with_config(schema, JsonDecoderConfig::default())
357 }
358
359 #[must_use]
361 pub fn with_config(schema: SchemaRef, config: JsonDecoderConfig) -> Self {
362 let field_indices: Vec<(String, usize)> = schema
363 .fields()
364 .iter()
365 .enumerate()
366 .map(|(i, f)| (f.name().clone(), i))
367 .collect();
368
369 let column_extractions: Vec<ColumnExtraction> = schema
370 .fields()
371 .iter()
372 .map(|f| {
373 let col_name = f.name();
374 if let Some(path_segments) = config.json_column_paths.get(col_name.as_str()) {
375 ColumnExtraction::CustomPath {
376 segments: path_segments.clone(),
377 }
378 } else {
379 ColumnExtraction::DefaultPath
380 }
381 })
382 .collect();
383
384 let column_epoch_units: Vec<EpochUnit> = schema
385 .fields()
386 .iter()
387 .map(|f| {
388 config
389 .numeric_timestamp_units
390 .get(f.name().as_str())
391 .copied()
392 .unwrap_or_default()
393 })
394 .collect();
395
396 let explode_col_indices = config.json_explode.as_ref().map(|names| {
397 names
398 .iter()
399 .map(|name| {
400 field_indices
401 .iter()
402 .find(|(n, _)| n == name)
403 .map(|(_, idx)| *idx)
404 })
405 .collect()
406 });
407
408 Self {
409 schema,
410 config,
411 field_indices,
412 mismatch_count: AtomicU64::new(0),
413 column_extractions,
414 column_epoch_units,
415 explode_col_indices,
416 }
417 }
418
419 pub fn mismatch_count(&self) -> u64 {
421 self.mismatch_count.load(Ordering::Relaxed)
422 }
423}
424
425impl FormatDecoder for JsonDecoder {
426 fn output_schema(&self) -> SchemaRef {
427 self.schema.clone()
428 }
429
430 fn decode_batch(&self, records: &[RawRecord]) -> SchemaResult<RecordBatch> {
431 let values: Vec<&[u8]> = records.iter().map(|r| r.value.as_slice()).collect();
432 self.decode_slices(&values)
433 }
434
435 fn format_name(&self) -> &'static str {
436 "json"
437 }
438}
439
440impl JsonDecoder {
441 #[allow(clippy::too_many_lines)]
448 pub fn decode_slices(&self, values: &[&[u8]]) -> SchemaResult<RecordBatch> {
449 self.decode_slices_bounded(values, usize::MAX)
450 }
451
452 pub fn decode_slices_bounded(
459 &self,
460 values: &[&[u8]],
461 max_rows: usize,
462 ) -> SchemaResult<RecordBatch> {
463 if values.is_empty() {
464 return Ok(RecordBatch::new_empty(self.schema.clone()));
465 }
466
467 let num_fields = self.schema.fields().len();
468 let capacity = values.len();
469
470 let mut builders = create_builders(&self.schema, capacity);
472
473 let collect_extra = matches!(
475 self.config.unknown_fields,
476 UnknownFieldStrategy::CollectExtra
477 );
478 let mut extra_builder = if collect_extra {
479 Some(LargeBinaryBuilder::with_capacity(capacity, capacity * 64))
480 } else {
481 None
482 };
483
484 let mut jsonb_encoder = if self.config.nested_as_jsonb {
485 Some(JsonbEncoder::new())
486 } else {
487 None
488 };
489
490 let mut populated = vec![false; num_fields];
492 let mut output_rows = 0usize;
493
494 for &bytes in values {
495 let value: serde_json::Value = serde_json::from_slice(bytes)
496 .map_err(|e| SchemaError::DecodeError(format!("JSON parse error: {e}")))?;
497
498 let default_target: &serde_json::Value =
504 match navigate_path_opt(&value, self.config.json_path.as_deref()) {
505 Some(v) => v,
506 None => continue,
507 };
508
509 if let Some(ref col_indices) = self.explode_col_indices {
510 let arr = default_target.as_array().ok_or_else(|| {
512 SchemaError::DecodeError("json.explode target must be an array".into())
513 })?;
514 output_rows = output_rows
515 .checked_add(arr.len())
516 .filter(|rows| *rows <= max_rows)
517 .ok_or_else(|| {
518 SchemaError::DecodeError(format!(
519 "JSON output exceeds the {max_rows}-row batch limit"
520 ))
521 })?;
522 for element in arr {
523 populated.fill(false);
524 match element {
525 serde_json::Value::Array(items) => {
526 for (pos, col_idx_opt) in col_indices.iter().enumerate() {
527 if let Some(col_idx) = col_idx_opt {
528 let val = items.get(pos).unwrap_or(&serde_json::Value::Null);
529 let field = &self.schema.fields()[*col_idx];
530 append_value(
531 &mut builders[*col_idx],
532 field.data_type(),
533 val,
534 &self.config,
535 &self.mismatch_count,
536 jsonb_encoder.as_mut(),
537 self.column_epoch_units[*col_idx],
538 )?;
539 populated[*col_idx] = true;
540 }
541 }
542 }
543 serde_json::Value::Object(obj) => {
544 for (col_idx, (col_name, _)) in self.field_indices.iter().enumerate() {
545 if let Some(val) = obj.get(col_name.as_str()) {
546 let field = &self.schema.fields()[col_idx];
547 append_value(
548 &mut builders[col_idx],
549 field.data_type(),
550 val,
551 &self.config,
552 &self.mismatch_count,
553 jsonb_encoder.as_mut(),
554 self.column_epoch_units[col_idx],
555 )?;
556 populated[col_idx] = true;
557 }
558 }
559 }
560 _ => {
561 return Err(SchemaError::DecodeError(
562 "json.explode array elements must be arrays or objects".into(),
563 ));
564 }
565 }
566 for (col_idx, was_populated) in populated.iter().enumerate() {
568 if !was_populated {
569 append_null(&mut builders[col_idx]);
570 }
571 }
572 if let Some(ref mut eb) = extra_builder {
574 eb.append_null();
575 }
576 }
577 } else {
578 output_rows = output_rows
580 .checked_add(1)
581 .filter(|rows| *rows <= max_rows)
582 .ok_or_else(|| {
583 SchemaError::DecodeError(format!(
584 "JSON output exceeds the {max_rows}-row batch limit"
585 ))
586 })?;
587 let default_obj = default_target.as_object().ok_or_else(|| {
588 SchemaError::DecodeError("JSON value must be an object".into())
589 })?;
590
591 populated.fill(false);
592
593 let mut extra_fields: Option<serde_json::Map<String, serde_json::Value>> =
595 if collect_extra {
596 Some(serde_json::Map::new())
597 } else {
598 None
599 };
600
601 for (col_idx, (col_name, _)) in self.field_indices.iter().enumerate() {
603 match &self.column_extractions[col_idx] {
604 ColumnExtraction::DefaultPath => {
605 if let Some(val) = default_obj.get(col_name.as_str()) {
606 populated[col_idx] = true;
607 let field = &self.schema.fields()[col_idx];
608 append_value(
609 &mut builders[col_idx],
610 field.data_type(),
611 val,
612 &self.config,
613 &self.mismatch_count,
614 jsonb_encoder.as_mut(),
615 self.column_epoch_units[col_idx],
616 )?;
617 }
618 }
619 ColumnExtraction::CustomPath { segments } => {
620 let extracted = navigate_path(&value, segments);
622 if let Some(val) = extracted {
623 populated[col_idx] = true;
624 let field = &self.schema.fields()[col_idx];
625 append_value(
626 &mut builders[col_idx],
627 field.data_type(),
628 val,
629 &self.config,
630 &self.mismatch_count,
631 jsonb_encoder.as_mut(),
632 self.column_epoch_units[col_idx],
633 )?;
634 }
635 }
636 }
637 }
638
639 if collect_extra {
641 for (key, val) in default_obj {
642 if self.field_index(key).is_none() {
643 match self.config.unknown_fields {
644 UnknownFieldStrategy::CollectExtra => {
645 if let Some(ref mut extra) = extra_fields {
646 extra.insert(key.clone(), val.clone());
647 }
648 }
649 UnknownFieldStrategy::Reject => {
650 return Err(SchemaError::DecodeError(format!(
651 "unknown field '{key}' not in schema"
652 )));
653 }
654 UnknownFieldStrategy::Ignore => {}
655 }
656 }
657 }
658 } else if matches!(self.config.unknown_fields, UnknownFieldStrategy::Reject) {
659 for key in default_obj.keys() {
660 if self.field_index(key).is_none() {
661 return Err(SchemaError::DecodeError(format!(
662 "unknown field '{key}' not in schema"
663 )));
664 }
665 }
666 }
667
668 for (col_idx, was_populated) in populated.iter().enumerate() {
670 if !was_populated {
671 append_null(&mut builders[col_idx]);
672 }
673 }
674
675 if let Some(ref mut eb) = extra_builder {
677 if let Some(ref extra) = extra_fields {
678 if extra.is_empty() {
679 eb.append_null();
680 } else {
681 let mut enc = jsonb_encoder
682 .as_mut()
683 .map_or_else(JsonbEncoder::new, |_| JsonbEncoder::new());
684 let bytes = enc.encode(&serde_json::Value::Object(extra.clone()));
685 eb.append_value(&bytes);
686 }
687 } else {
688 eb.append_null();
689 }
690 }
691 }
692 }
693
694 let mut columns: Vec<ArrayRef> = builders.into_iter().map(|mut b| b.finish()).collect();
696
697 let final_schema = if let Some(mut eb) = extra_builder {
699 columns.push(Arc::new(eb.finish()));
700 let mut fields = self.schema.fields().to_vec();
701 fields.push(Arc::new(arrow_schema::Field::new(
702 "_extra",
703 DataType::LargeBinary,
704 true,
705 )));
706 Arc::new(arrow_schema::Schema::new(fields))
707 } else {
708 self.schema.clone()
709 };
710
711 RecordBatch::try_new(final_schema, columns)
712 .map_err(|e| SchemaError::DecodeError(format!("RecordBatch construction: {e}")))
713 }
714
715 fn field_index(&self, name: &str) -> Option<usize> {
718 self.field_indices
719 .iter()
720 .find(|(n, _)| n == name)
721 .map(|(_, idx)| *idx)
722 }
723}
724
725fn navigate_path<'a>(
728 root: &'a serde_json::Value,
729 segments: &[String],
730) -> Option<&'a serde_json::Value> {
731 let mut current = root;
732 for segment in segments {
733 current = current.get(segment.as_str())?;
734 }
735 Some(current)
736}
737
738fn navigate_path_opt<'a>(
741 root: &'a serde_json::Value,
742 segments: Option<&[String]>,
743) -> Option<&'a serde_json::Value> {
744 match segments {
745 Some(s) => navigate_path(root, s),
746 None => Some(root),
747 }
748}
749
750trait ColumnBuilder: Send {
754 fn finish(&mut self) -> ArrayRef;
755 fn append_null_value(&mut self);
756 fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
757}
758
759macro_rules! impl_column_builder {
760 ($builder:ty, $array:ty) => {
761 impl ColumnBuilder for $builder {
762 fn finish(&mut self) -> ArrayRef {
763 Arc::new(<$builder>::finish(self))
764 }
765 fn append_null_value(&mut self) {
766 self.append_null();
767 }
768 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
769 self
770 }
771 }
772 };
773}
774
775impl_column_builder!(BooleanBuilder, arrow_array::BooleanArray);
776impl_column_builder!(Int8Builder, arrow_array::Int8Array);
777impl_column_builder!(Int16Builder, arrow_array::Int16Array);
778impl_column_builder!(Int32Builder, arrow_array::Int32Array);
779impl_column_builder!(Int64Builder, arrow_array::Int64Array);
780impl_column_builder!(UInt8Builder, arrow_array::UInt8Array);
781impl_column_builder!(UInt16Builder, arrow_array::UInt16Array);
782impl_column_builder!(UInt32Builder, arrow_array::UInt32Array);
783impl_column_builder!(UInt64Builder, arrow_array::UInt64Array);
784impl_column_builder!(Float32Builder, arrow_array::Float32Array);
785impl_column_builder!(Float64Builder, arrow_array::Float64Array);
786impl_column_builder!(StringBuilder, arrow_array::StringArray);
787impl_column_builder!(ListBuilder<StringBuilder>, arrow_array::ListArray);
788impl_column_builder!(LargeStringBuilder, arrow_array::LargeStringArray);
789impl_column_builder!(LargeBinaryBuilder, arrow_array::LargeBinaryArray);
790impl_column_builder!(TimestampSecondBuilder, arrow_array::TimestampSecondArray);
791impl_column_builder!(
792 TimestampMillisecondBuilder,
793 arrow_array::TimestampMillisecondArray
794);
795impl_column_builder!(
796 TimestampMicrosecondBuilder,
797 arrow_array::TimestampMicrosecondArray
798);
799impl_column_builder!(
800 TimestampNanosecondBuilder,
801 arrow_array::TimestampNanosecondArray
802);
803
804fn create_builders(schema: &SchemaRef, capacity: usize) -> Vec<Box<dyn ColumnBuilder>> {
805 schema
806 .fields()
807 .iter()
808 .map(|f| create_builder(f.data_type(), capacity))
809 .collect()
810}
811
812fn create_builder(data_type: &DataType, capacity: usize) -> Box<dyn ColumnBuilder> {
813 match data_type {
814 DataType::Boolean => Box::new(BooleanBuilder::with_capacity(capacity)),
815 DataType::Int8 => Box::new(Int8Builder::with_capacity(capacity)),
816 DataType::Int16 => Box::new(Int16Builder::with_capacity(capacity)),
817 DataType::Int32 => Box::new(Int32Builder::with_capacity(capacity)),
818 DataType::Int64 => Box::new(Int64Builder::with_capacity(capacity)),
819 DataType::UInt8 => Box::new(UInt8Builder::with_capacity(capacity)),
820 DataType::UInt16 => Box::new(UInt16Builder::with_capacity(capacity)),
821 DataType::UInt32 => Box::new(UInt32Builder::with_capacity(capacity)),
822 DataType::UInt64 => Box::new(UInt64Builder::with_capacity(capacity)),
823 DataType::Float32 => Box::new(Float32Builder::with_capacity(capacity)),
824 DataType::Float64 => Box::new(Float64Builder::with_capacity(capacity)),
825 DataType::LargeUtf8 => Box::new(LargeStringBuilder::with_capacity(capacity, capacity * 32)),
826 DataType::LargeBinary => {
827 Box::new(LargeBinaryBuilder::with_capacity(capacity, capacity * 64))
828 }
829 DataType::Timestamp(TimeUnit::Second, tz) => {
830 let builder =
831 TimestampSecondBuilder::with_capacity(capacity).with_timezone_opt(tz.clone());
832 Box::new(builder)
833 }
834 DataType::Timestamp(TimeUnit::Millisecond, tz) => {
835 let builder =
836 TimestampMillisecondBuilder::with_capacity(capacity).with_timezone_opt(tz.clone());
837 Box::new(builder)
838 }
839 DataType::Timestamp(TimeUnit::Microsecond, tz) => {
840 let builder =
841 TimestampMicrosecondBuilder::with_capacity(capacity).with_timezone_opt(tz.clone());
842 Box::new(builder)
843 }
844 DataType::Timestamp(TimeUnit::Nanosecond, tz) => {
845 let builder =
846 TimestampNanosecondBuilder::with_capacity(capacity).with_timezone_opt(tz.clone());
847 Box::new(builder)
848 }
849 DataType::List(field) if matches!(field.data_type(), DataType::Utf8) => {
850 Box::new(ListBuilder::new(StringBuilder::new()))
851 }
852 _ => Box::new(StringBuilder::with_capacity(capacity, capacity * 32)),
854 }
855}
856
857fn append_null(builder: &mut Box<dyn ColumnBuilder>) {
858 builder.append_null_value();
859}
860
861#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
863fn append_value(
864 builder: &mut Box<dyn ColumnBuilder>,
865 target_type: &DataType,
866 value: &serde_json::Value,
867 config: &JsonDecoderConfig,
868 mismatch_count: &AtomicU64,
869 jsonb_encoder: Option<&mut JsonbEncoder>,
870 numeric_ts_unit: EpochUnit,
871) -> SchemaResult<()> {
872 if value.is_null() {
873 builder.append_null_value();
874 return Ok(());
875 }
876
877 match target_type {
878 DataType::Boolean => {
879 let b = builder
880 .as_any_mut()
881 .downcast_mut::<BooleanBuilder>()
882 .unwrap();
883 match extract_bool(value, config) {
884 Ok(v) => b.append_value(v),
885 Err(e) => handle_mismatch(builder, config, mismatch_count, &e)?,
886 }
887 }
888 DataType::Int8 => {
889 let b = builder.as_any_mut().downcast_mut::<Int8Builder>().unwrap();
890 match extract_i8(value, config) {
891 Ok(v) => b.append_value(v),
892 Err(e) => handle_mismatch(builder, config, mismatch_count, &e)?,
893 }
894 }
895 DataType::Int16 => {
896 let b = builder.as_any_mut().downcast_mut::<Int16Builder>().unwrap();
897 match extract_i16(value, config) {
898 Ok(v) => b.append_value(v),
899 Err(e) => handle_mismatch(builder, config, mismatch_count, &e)?,
900 }
901 }
902 DataType::Int32 => {
903 let b = builder.as_any_mut().downcast_mut::<Int32Builder>().unwrap();
904 match extract_i32(value, config) {
905 Ok(v) => b.append_value(v),
906 Err(e) => handle_mismatch(builder, config, mismatch_count, &e)?,
907 }
908 }
909 DataType::Int64 => {
910 let b = builder.as_any_mut().downcast_mut::<Int64Builder>().unwrap();
911 match extract_i64(value, config) {
912 Ok(v) => b.append_value(v),
913 Err(e) => handle_mismatch(builder, config, mismatch_count, &e)?,
914 }
915 }
916 DataType::UInt8 => {
917 let b = builder.as_any_mut().downcast_mut::<UInt8Builder>().unwrap();
918 match extract_u8(value, config) {
919 Ok(v) => b.append_value(v),
920 Err(e) => handle_mismatch(builder, config, mismatch_count, &e)?,
921 }
922 }
923 DataType::UInt16 => {
924 let b = builder
925 .as_any_mut()
926 .downcast_mut::<UInt16Builder>()
927 .unwrap();
928 match extract_u16(value, config) {
929 Ok(v) => b.append_value(v),
930 Err(e) => handle_mismatch(builder, config, mismatch_count, &e)?,
931 }
932 }
933 DataType::UInt32 => {
934 let b = builder
935 .as_any_mut()
936 .downcast_mut::<UInt32Builder>()
937 .unwrap();
938 match extract_u32(value, config) {
939 Ok(v) => b.append_value(v),
940 Err(e) => handle_mismatch(builder, config, mismatch_count, &e)?,
941 }
942 }
943 DataType::UInt64 => {
944 let b = builder
945 .as_any_mut()
946 .downcast_mut::<UInt64Builder>()
947 .unwrap();
948 match extract_u64(value, config) {
949 Ok(v) => b.append_value(v),
950 Err(e) => handle_mismatch(builder, config, mismatch_count, &e)?,
951 }
952 }
953 DataType::Float32 => {
954 let b = builder
955 .as_any_mut()
956 .downcast_mut::<Float32Builder>()
957 .unwrap();
958 match extract_f32(value, config) {
959 Ok(v) => b.append_value(v),
960 Err(e) => handle_mismatch(builder, config, mismatch_count, &e)?,
961 }
962 }
963 DataType::Float64 => {
964 let b = builder
965 .as_any_mut()
966 .downcast_mut::<Float64Builder>()
967 .unwrap();
968 match extract_f64(value, config) {
969 Ok(v) => b.append_value(v),
970 Err(e) => handle_mismatch(builder, config, mismatch_count, &e)?,
971 }
972 }
973 DataType::LargeUtf8 => {
974 let b = builder
975 .as_any_mut()
976 .downcast_mut::<LargeStringBuilder>()
977 .unwrap();
978 let s = value_to_string(value);
979 b.append_value(&s);
980 }
981 DataType::LargeBinary => {
982 let b = builder
983 .as_any_mut()
984 .downcast_mut::<LargeBinaryBuilder>()
985 .unwrap();
986 if let Some(enc) = jsonb_encoder {
987 let bytes = enc.encode(value);
988 b.append_value(&bytes);
989 } else {
990 let bytes = serde_json::to_vec(value).unwrap_or_default();
992 b.append_value(&bytes);
993 }
994 }
995 DataType::Timestamp(unit, _) => {
996 match extract_timestamp(value, config, *unit, numeric_ts_unit) {
997 Ok(ts) => append_timestamp(builder, *unit, ts),
998 Err(e) => handle_mismatch(builder, config, mismatch_count, &e)?,
999 }
1000 }
1001 DataType::List(field) if matches!(field.data_type(), DataType::Utf8) => {
1002 if let Some(items) = value.as_array() {
1003 let b = builder
1004 .as_any_mut()
1005 .downcast_mut::<ListBuilder<StringBuilder>>()
1006 .unwrap();
1007 for item in items {
1008 if let Some(s) = item.as_str() {
1009 b.values().append_value(s);
1010 } else if item.is_null() {
1011 b.values().append_null();
1012 } else {
1013 b.values().append_value(value_to_string(item));
1014 }
1015 }
1016 b.append(true);
1017 } else {
1018 handle_mismatch(
1021 builder,
1022 config,
1023 mismatch_count,
1024 &format!("expected array, got {}", json_type_name(value)),
1025 )?;
1026 }
1027 }
1028 _ => {
1030 let b = builder
1031 .as_any_mut()
1032 .downcast_mut::<StringBuilder>()
1033 .unwrap();
1034 let s = value_to_string(value);
1035 b.append_value(&s);
1036 }
1037 }
1038
1039 Ok(())
1040}
1041
1042fn handle_mismatch(
1043 builder: &mut Box<dyn ColumnBuilder>,
1044 config: &JsonDecoderConfig,
1045 mismatch_count: &AtomicU64,
1046 error_msg: &str,
1047) -> SchemaResult<()> {
1048 match config.type_mismatch {
1049 TypeMismatchStrategy::Null => {
1050 mismatch_count.fetch_add(1, Ordering::Relaxed);
1051 builder.append_null_value();
1052 Ok(())
1053 }
1054 TypeMismatchStrategy::Coerce => {
1055 Err(SchemaError::DecodeError(format!(
1057 "type coercion failed: {error_msg}"
1058 )))
1059 }
1060 TypeMismatchStrategy::Reject => Err(SchemaError::DecodeError(format!(
1061 "type mismatch: {error_msg}"
1062 ))),
1063 }
1064}
1065
1066fn extract_bool(value: &serde_json::Value, config: &JsonDecoderConfig) -> Result<bool, String> {
1069 if let Some(b) = value.as_bool() {
1070 return Ok(b);
1071 }
1072 if matches!(config.type_mismatch, TypeMismatchStrategy::Coerce) {
1073 if let Some(s) = value.as_str() {
1074 match s.to_ascii_lowercase().as_str() {
1075 "true" | "1" | "yes" => return Ok(true),
1076 "false" | "0" | "no" => return Ok(false),
1077 _ => {}
1078 }
1079 }
1080 if let Some(n) = value.as_i64() {
1081 return Ok(n != 0);
1082 }
1083 }
1084 Err(format!("expected boolean, got {}", json_type_name(value)))
1085}
1086
1087fn extract_i8(value: &serde_json::Value, config: &JsonDecoderConfig) -> Result<i8, String> {
1088 if let Some(n) = value.as_i64() {
1089 if let Ok(v) = i8::try_from(n) {
1090 return Ok(v);
1091 }
1092 return Err(format!("integer {n} out of i8 range"));
1093 }
1094 if matches!(config.type_mismatch, TypeMismatchStrategy::Coerce) {
1095 if let Some(s) = value.as_str() {
1096 if let Ok(v) = s.parse::<i8>() {
1097 return Ok(v);
1098 }
1099 }
1100 if let Some(f) = value.as_f64() {
1101 #[allow(clippy::cast_possible_truncation)]
1102 let v = f as i8;
1103 return Ok(v);
1104 }
1105 }
1106 Err(format!("expected i8, got {}", json_type_name(value)))
1107}
1108
1109fn extract_i16(value: &serde_json::Value, config: &JsonDecoderConfig) -> Result<i16, String> {
1110 if let Some(n) = value.as_i64() {
1111 if let Ok(v) = i16::try_from(n) {
1112 return Ok(v);
1113 }
1114 return Err(format!("integer {n} out of i16 range"));
1115 }
1116 if matches!(config.type_mismatch, TypeMismatchStrategy::Coerce) {
1117 if let Some(s) = value.as_str() {
1118 if let Ok(v) = s.parse::<i16>() {
1119 return Ok(v);
1120 }
1121 }
1122 if let Some(f) = value.as_f64() {
1123 #[allow(clippy::cast_possible_truncation)]
1124 let v = f as i16;
1125 return Ok(v);
1126 }
1127 }
1128 Err(format!("expected i16, got {}", json_type_name(value)))
1129}
1130
1131fn extract_i32(value: &serde_json::Value, config: &JsonDecoderConfig) -> Result<i32, String> {
1132 if let Some(n) = value.as_i64() {
1133 if let Ok(v) = i32::try_from(n) {
1134 return Ok(v);
1135 }
1136 return Err(format!("integer {n} out of i32 range"));
1137 }
1138 if matches!(config.type_mismatch, TypeMismatchStrategy::Coerce) {
1139 if let Some(s) = value.as_str() {
1140 if let Ok(v) = s.parse::<i32>() {
1141 return Ok(v);
1142 }
1143 }
1144 if let Some(f) = value.as_f64() {
1145 #[allow(clippy::cast_possible_truncation)]
1146 return Ok(f as i32);
1147 }
1148 }
1149 Err(format!("expected i32, got {}", json_type_name(value)))
1150}
1151
1152fn extract_i64(value: &serde_json::Value, config: &JsonDecoderConfig) -> Result<i64, String> {
1153 if let Some(n) = value.as_i64() {
1154 return Ok(n);
1155 }
1156 if let Some(n) = value.as_u64() {
1157 if let Ok(v) = i64::try_from(n) {
1158 return Ok(v);
1159 }
1160 return Err(format!("u64 {n} out of i64 range"));
1161 }
1162 if matches!(config.type_mismatch, TypeMismatchStrategy::Coerce) {
1163 if let Some(s) = value.as_str() {
1164 if let Ok(v) = s.parse::<i64>() {
1165 return Ok(v);
1166 }
1167 }
1168 if let Some(f) = value.as_f64() {
1169 #[allow(clippy::cast_possible_truncation)]
1170 return Ok(f as i64);
1171 }
1172 }
1173 Err(format!("expected i64, got {}", json_type_name(value)))
1174}
1175
1176fn extract_f32(value: &serde_json::Value, config: &JsonDecoderConfig) -> Result<f32, String> {
1177 if let Some(f) = value.as_f64() {
1178 #[allow(clippy::cast_possible_truncation)]
1179 return Ok(f as f32);
1180 }
1181 if let Some(n) = value.as_i64() {
1182 #[allow(clippy::cast_precision_loss)]
1183 return Ok(n as f32);
1184 }
1185 if matches!(config.type_mismatch, TypeMismatchStrategy::Coerce) {
1186 if let Some(s) = value.as_str() {
1187 if let Ok(v) = s.parse::<f32>() {
1188 return Ok(v);
1189 }
1190 }
1191 }
1192 Err(format!("expected f32, got {}", json_type_name(value)))
1193}
1194
1195fn extract_f64(value: &serde_json::Value, config: &JsonDecoderConfig) -> Result<f64, String> {
1196 if let Some(f) = value.as_f64() {
1197 return Ok(f);
1198 }
1199 if let Some(n) = value.as_i64() {
1200 #[allow(clippy::cast_precision_loss)]
1201 return Ok(n as f64);
1202 }
1203 if matches!(config.type_mismatch, TypeMismatchStrategy::Coerce) {
1204 if let Some(s) = value.as_str() {
1205 if let Ok(v) = s.parse::<f64>() {
1206 return Ok(v);
1207 }
1208 }
1209 }
1210 Err(format!("expected f64, got {}", json_type_name(value)))
1211}
1212
1213fn extract_u8(value: &serde_json::Value, config: &JsonDecoderConfig) -> Result<u8, String> {
1214 if let Some(n) = value.as_u64() {
1215 if let Ok(v) = u8::try_from(n) {
1216 return Ok(v);
1217 }
1218 return Err(format!("integer {n} out of u8 range"));
1219 }
1220 if let Some(n) = value.as_i64() {
1221 if let Ok(v) = u8::try_from(n) {
1222 return Ok(v);
1223 }
1224 return Err(format!("integer {n} out of u8 range"));
1225 }
1226 if matches!(config.type_mismatch, TypeMismatchStrategy::Coerce) {
1227 if let Some(s) = value.as_str() {
1228 if let Ok(v) = s.parse::<u8>() {
1229 return Ok(v);
1230 }
1231 }
1232 }
1233 Err(format!("expected u8, got {}", json_type_name(value)))
1234}
1235
1236fn extract_u16(value: &serde_json::Value, config: &JsonDecoderConfig) -> Result<u16, String> {
1237 if let Some(n) = value.as_u64() {
1238 if let Ok(v) = u16::try_from(n) {
1239 return Ok(v);
1240 }
1241 return Err(format!("integer {n} out of u16 range"));
1242 }
1243 if let Some(n) = value.as_i64() {
1244 if let Ok(v) = u16::try_from(n) {
1245 return Ok(v);
1246 }
1247 return Err(format!("integer {n} out of u16 range"));
1248 }
1249 if matches!(config.type_mismatch, TypeMismatchStrategy::Coerce) {
1250 if let Some(s) = value.as_str() {
1251 if let Ok(v) = s.parse::<u16>() {
1252 return Ok(v);
1253 }
1254 }
1255 }
1256 Err(format!("expected u16, got {}", json_type_name(value)))
1257}
1258
1259fn extract_u32(value: &serde_json::Value, config: &JsonDecoderConfig) -> Result<u32, String> {
1260 if let Some(n) = value.as_u64() {
1261 if let Ok(v) = u32::try_from(n) {
1262 return Ok(v);
1263 }
1264 return Err(format!("integer {n} out of u32 range"));
1265 }
1266 if let Some(n) = value.as_i64() {
1267 if let Ok(v) = u32::try_from(n) {
1268 return Ok(v);
1269 }
1270 return Err(format!("integer {n} out of u32 range"));
1271 }
1272 if matches!(config.type_mismatch, TypeMismatchStrategy::Coerce) {
1273 if let Some(s) = value.as_str() {
1274 if let Ok(v) = s.parse::<u32>() {
1275 return Ok(v);
1276 }
1277 }
1278 }
1279 Err(format!("expected u32, got {}", json_type_name(value)))
1280}
1281
1282fn extract_u64(value: &serde_json::Value, config: &JsonDecoderConfig) -> Result<u64, String> {
1283 if let Some(n) = value.as_u64() {
1284 return Ok(n);
1285 }
1286 if let Some(n) = value.as_i64() {
1287 if let Ok(v) = u64::try_from(n) {
1288 return Ok(v);
1289 }
1290 return Err(format!("integer {n} out of u64 range"));
1291 }
1292 if matches!(config.type_mismatch, TypeMismatchStrategy::Coerce) {
1293 if let Some(s) = value.as_str() {
1294 if let Ok(v) = s.parse::<u64>() {
1295 return Ok(v);
1296 }
1297 }
1298 }
1299 Err(format!("expected u64, got {}", json_type_name(value)))
1300}
1301
1302fn extract_timestamp(
1309 value: &serde_json::Value,
1310 config: &JsonDecoderConfig,
1311 unit: TimeUnit,
1312 from: EpochUnit,
1313) -> Result<i64, String> {
1314 if let Some(n) = value.as_i64() {
1315 return checked_epoch_to_unit(n, from, unit);
1316 }
1317 if let Some(f) = value.as_f64() {
1318 const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
1324 if !f.is_finite() || !(-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&f) {
1325 return Err(format!(
1326 "timestamp {f} is not exactly representable as an integer"
1327 ));
1328 }
1329 if f.fract() != 0.0 {
1330 return Err(format!(
1331 "fractional timestamp {f} is not supported; provide an integer in {from:?} units"
1332 ));
1333 }
1334 let v = format!("{f:.0}")
1335 .parse::<i64>()
1336 .map_err(|error| format!("invalid integral timestamp {f}: {error}"))?;
1337 return checked_epoch_to_unit(v, from, unit);
1338 }
1339
1340 if let Some(s) = value.as_str() {
1341 for fmt in &config.timestamp_formats {
1342 if fmt == "iso8601" {
1343 if let Ok(nanos) = arrow_cast::parse::string_to_timestamp_nanos(s) {
1344 return Ok(nanos_to_unit(nanos, unit));
1345 }
1346 continue;
1347 }
1348 if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(s, fmt) {
1349 let nanos = ndt.and_utc().timestamp_nanos_opt().unwrap_or(0);
1350 return Ok(nanos_to_unit(nanos, unit));
1351 }
1352 }
1353 return Err(format!("cannot parse timestamp from string: {s}"));
1354 }
1355
1356 Err(format!("expected timestamp, got {}", json_type_name(value)))
1357}
1358
1359fn checked_epoch_to_unit(value: i64, from: EpochUnit, to: TimeUnit) -> Result<i64, String> {
1364 let from_ns = from.nanos_per();
1365 let to_ns = match to {
1366 TimeUnit::Second => 1_000_000_000,
1367 TimeUnit::Millisecond => 1_000_000,
1368 TimeUnit::Microsecond => 1_000,
1369 TimeUnit::Nanosecond => 1,
1370 };
1371 if from_ns >= to_ns {
1372 let factor = from_ns / to_ns;
1374 value
1375 .checked_mul(factor)
1376 .ok_or_else(|| format!("timestamp {value} ({from:?}) out of i64 {to:?} range"))
1377 } else {
1378 let factor = to_ns / from_ns;
1379 Ok(value / factor)
1380 }
1381}
1382
1383fn nanos_to_unit(nanos: i64, unit: TimeUnit) -> i64 {
1385 match unit {
1386 TimeUnit::Second => nanos / 1_000_000_000,
1387 TimeUnit::Millisecond => nanos / 1_000_000,
1388 TimeUnit::Microsecond => nanos / 1_000,
1389 TimeUnit::Nanosecond => nanos,
1390 }
1391}
1392
1393fn append_timestamp(builder: &mut Box<dyn ColumnBuilder>, unit: TimeUnit, value: i64) {
1395 match unit {
1396 TimeUnit::Second => {
1397 builder
1398 .as_any_mut()
1399 .downcast_mut::<TimestampSecondBuilder>()
1400 .unwrap()
1401 .append_value(value);
1402 }
1403 TimeUnit::Millisecond => {
1404 builder
1405 .as_any_mut()
1406 .downcast_mut::<TimestampMillisecondBuilder>()
1407 .unwrap()
1408 .append_value(value);
1409 }
1410 TimeUnit::Microsecond => {
1411 builder
1412 .as_any_mut()
1413 .downcast_mut::<TimestampMicrosecondBuilder>()
1414 .unwrap()
1415 .append_value(value);
1416 }
1417 TimeUnit::Nanosecond => {
1418 builder
1419 .as_any_mut()
1420 .downcast_mut::<TimestampNanosecondBuilder>()
1421 .unwrap()
1422 .append_value(value);
1423 }
1424 }
1425}
1426
1427fn value_to_string(value: &serde_json::Value) -> String {
1428 match value {
1429 serde_json::Value::String(s) => s.clone(),
1430 other => other.to_string(),
1431 }
1432}
1433
1434fn json_type_name(value: &serde_json::Value) -> &'static str {
1435 match value {
1436 serde_json::Value::Null => "null",
1437 serde_json::Value::Bool(_) => "boolean",
1438 serde_json::Value::Number(_) => "number",
1439 serde_json::Value::String(_) => "string",
1440 serde_json::Value::Array(_) => "array",
1441 serde_json::Value::Object(_) => "object",
1442 }
1443}
1444
1445#[cfg(test)]
1446mod tests {
1447 use super::*;
1448 use arrow_array::cast::AsArray;
1449 use arrow_schema::{Field, Schema};
1450
1451 fn make_schema(fields: Vec<(&str, DataType, bool)>) -> SchemaRef {
1452 Arc::new(Schema::new(
1453 fields
1454 .into_iter()
1455 .map(|(name, dt, nullable)| Field::new(name, dt, nullable))
1456 .collect::<Vec<_>>(),
1457 ))
1458 }
1459
1460 fn json_record(json: &str) -> RawRecord {
1461 RawRecord::new(json.as_bytes().to_vec())
1462 }
1463
1464 #[test]
1467 fn test_decode_empty_batch() {
1468 let schema = make_schema(vec![("id", DataType::Int64, false)]);
1469 let decoder = JsonDecoder::new(schema.clone());
1470 let batch = decoder.decode_batch(&[]).unwrap();
1471 assert_eq!(batch.num_rows(), 0);
1472 assert_eq!(batch.schema(), schema);
1473 }
1474
1475 #[test]
1476 fn test_decode_single_record() {
1477 let schema = make_schema(vec![
1478 ("id", DataType::Int64, false),
1479 ("name", DataType::Utf8, true),
1480 ]);
1481 let decoder = JsonDecoder::new(schema);
1482 let records = vec![json_record(r#"{"id": 42, "name": "Alice"}"#)];
1483 let batch = decoder.decode_batch(&records).unwrap();
1484
1485 assert_eq!(batch.num_rows(), 1);
1486 assert_eq!(
1487 batch
1488 .column(0)
1489 .as_primitive::<arrow_array::types::Int64Type>()
1490 .value(0),
1491 42
1492 );
1493 assert_eq!(batch.column(1).as_string::<i32>().value(0), "Alice");
1494 }
1495
1496 #[test]
1497 fn test_decode_multiple_records() {
1498 let schema = make_schema(vec![
1499 ("x", DataType::Int64, false),
1500 ("y", DataType::Float64, false),
1501 ]);
1502 let decoder = JsonDecoder::new(schema);
1503 let records = vec![
1504 json_record(r#"{"x": 1, "y": 1.5}"#),
1505 json_record(r#"{"x": 2, "y": 2.5}"#),
1506 json_record(r#"{"x": 3, "y": 3.5}"#),
1507 ];
1508 let batch = decoder.decode_batch(&records).unwrap();
1509
1510 assert_eq!(batch.num_rows(), 3);
1511 let x_col = batch
1512 .column(0)
1513 .as_primitive::<arrow_array::types::Int64Type>();
1514 assert_eq!(x_col.value(0), 1);
1515 assert_eq!(x_col.value(1), 2);
1516 assert_eq!(x_col.value(2), 3);
1517 }
1518
1519 #[test]
1520 fn test_decode_all_types() {
1521 let schema = make_schema(vec![
1522 ("bool_col", DataType::Boolean, false),
1523 ("int_col", DataType::Int64, false),
1524 ("float_col", DataType::Float64, false),
1525 ("str_col", DataType::Utf8, false),
1526 ]);
1527 let decoder = JsonDecoder::new(schema);
1528 let records = vec![json_record(
1529 r#"{"bool_col": true, "int_col": 42, "float_col": 3.14, "str_col": "hello"}"#,
1530 )];
1531 let batch = decoder.decode_batch(&records).unwrap();
1532
1533 assert_eq!(batch.num_rows(), 1);
1534 assert!(batch.column(0).as_boolean().value(0));
1535 assert_eq!(
1536 batch
1537 .column(1)
1538 .as_primitive::<arrow_array::types::Int64Type>()
1539 .value(0),
1540 42
1541 );
1542 let f = batch
1543 .column(2)
1544 .as_primitive::<arrow_array::types::Float64Type>()
1545 .value(0);
1546 assert!((f - 3.14).abs() < f64::EPSILON);
1547 assert_eq!(batch.column(3).as_string::<i32>().value(0), "hello");
1548 }
1549
1550 #[test]
1553 fn test_decode_null_values() {
1554 let schema = make_schema(vec![
1555 ("a", DataType::Int64, true),
1556 ("b", DataType::Utf8, true),
1557 ]);
1558 let decoder = JsonDecoder::new(schema);
1559 let records = vec![json_record(r#"{"a": null, "b": null}"#)];
1560 let batch = decoder.decode_batch(&records).unwrap();
1561
1562 assert!(batch.column(0).is_null(0));
1563 assert!(batch.column(1).is_null(0));
1564 }
1565
1566 #[test]
1567 fn test_decode_missing_field_becomes_null() {
1568 let schema = make_schema(vec![
1569 ("a", DataType::Int64, true),
1570 ("b", DataType::Utf8, true),
1571 ]);
1572 let decoder = JsonDecoder::new(schema);
1573 let records = vec![json_record(r#"{"a": 1}"#)]; let batch = decoder.decode_batch(&records).unwrap();
1575
1576 assert_eq!(
1577 batch
1578 .column(0)
1579 .as_primitive::<arrow_array::types::Int64Type>()
1580 .value(0),
1581 1
1582 );
1583 assert!(batch.column(1).is_null(0));
1584 }
1585
1586 #[test]
1589 fn test_mismatch_null_strategy() {
1590 let schema = make_schema(vec![("x", DataType::Int64, true)]);
1591 let config = JsonDecoderConfig {
1592 type_mismatch: TypeMismatchStrategy::Null,
1593 ..Default::default()
1594 };
1595 let decoder = JsonDecoder::with_config(schema, config);
1596 let records = vec![json_record(r#"{"x": "not_a_number"}"#)];
1597 let batch = decoder.decode_batch(&records).unwrap();
1598
1599 assert!(batch.column(0).is_null(0));
1600 assert_eq!(decoder.mismatch_count(), 1);
1601 }
1602
1603 #[test]
1604 fn test_mismatch_coerce_strategy() {
1605 let schema = make_schema(vec![("x", DataType::Int64, true)]);
1606 let config = JsonDecoderConfig {
1607 type_mismatch: TypeMismatchStrategy::Coerce,
1608 ..Default::default()
1609 };
1610 let decoder = JsonDecoder::with_config(schema, config);
1611 let records = vec![json_record(r#"{"x": "123"}"#)];
1612 let batch = decoder.decode_batch(&records).unwrap();
1613
1614 assert_eq!(
1615 batch
1616 .column(0)
1617 .as_primitive::<arrow_array::types::Int64Type>()
1618 .value(0),
1619 123
1620 );
1621 }
1622
1623 #[test]
1624 fn test_mismatch_reject_strategy() {
1625 let schema = make_schema(vec![("x", DataType::Int64, false)]);
1626 let config = JsonDecoderConfig {
1627 type_mismatch: TypeMismatchStrategy::Reject,
1628 ..Default::default()
1629 };
1630 let decoder = JsonDecoder::with_config(schema, config);
1631 let records = vec![json_record(r#"{"x": "not_a_number"}"#)];
1632 let result = decoder.decode_batch(&records);
1633
1634 assert!(result.is_err());
1635 assert!(result.unwrap_err().to_string().contains("type mismatch"));
1636 }
1637
1638 #[test]
1641 fn test_unknown_fields_ignore() {
1642 let schema = make_schema(vec![("a", DataType::Int64, false)]);
1643 let decoder = JsonDecoder::new(schema);
1644 let records = vec![json_record(r#"{"a": 1, "unknown": "value"}"#)];
1645 let batch = decoder.decode_batch(&records).unwrap();
1646
1647 assert_eq!(batch.num_columns(), 1);
1648 assert_eq!(batch.num_rows(), 1);
1649 }
1650
1651 #[test]
1652 fn test_unknown_fields_reject() {
1653 let schema = make_schema(vec![("a", DataType::Int64, false)]);
1654 let config = JsonDecoderConfig {
1655 unknown_fields: UnknownFieldStrategy::Reject,
1656 ..Default::default()
1657 };
1658 let decoder = JsonDecoder::with_config(schema, config);
1659 let records = vec![json_record(r#"{"a": 1, "unknown": "value"}"#)];
1660 let result = decoder.decode_batch(&records);
1661
1662 assert!(result.is_err());
1663 assert!(result.unwrap_err().to_string().contains("unknown field"));
1664 }
1665
1666 #[test]
1667 fn test_unknown_fields_collect_extra() {
1668 let schema = make_schema(vec![("a", DataType::Int64, false)]);
1669 let config = JsonDecoderConfig {
1670 unknown_fields: UnknownFieldStrategy::CollectExtra,
1671 ..Default::default()
1672 };
1673 let decoder = JsonDecoder::with_config(schema, config);
1674 let records = vec![json_record(r#"{"a": 1, "extra1": "v1", "extra2": 42}"#)];
1675 let batch = decoder.decode_batch(&records).unwrap();
1676
1677 assert_eq!(batch.num_columns(), 2);
1679 assert_eq!(batch.schema().field(1).name(), "_extra");
1680 assert!(!batch.column(1).is_null(0));
1681 }
1682
1683 #[test]
1686 fn test_decode_timestamp_iso8601() {
1687 let schema = make_schema(vec![(
1688 "ts",
1689 DataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into())),
1690 false,
1691 )]);
1692 let decoder = JsonDecoder::new(schema);
1693 let records = vec![json_record(r#"{"ts": "2025-01-15T10:30:00Z"}"#)];
1694 let batch = decoder.decode_batch(&records).unwrap();
1695
1696 assert!(!batch.column(0).is_null(0));
1697 }
1698
1699 #[test]
1700 fn test_decode_timestamp_epoch_millis() {
1701 let schema = make_schema(vec![(
1702 "ts",
1703 DataType::Timestamp(TimeUnit::Nanosecond, None),
1704 false,
1705 )]);
1706 let decoder = JsonDecoder::new(schema);
1707 let records = vec![json_record(r#"{"ts": 1705312200000}"#)];
1708 let batch = decoder.decode_batch(&records).unwrap();
1709
1710 let ts_col = batch
1711 .column(0)
1712 .as_primitive::<arrow_array::types::TimestampNanosecondType>();
1713 assert_eq!(ts_col.value(0), 1_705_312_200_000_000_000);
1715 }
1716
1717 #[test]
1718 fn test_decode_out_of_range_float_timestamp_is_rejected() {
1719 let schema = make_schema(vec![(
1723 "ts",
1724 DataType::Timestamp(TimeUnit::Millisecond, None),
1725 false,
1726 )]);
1727 let decoder = JsonDecoder::new(schema);
1728 let records = vec![json_record(r#"{"ts": 1e30}"#)];
1729 let result = decoder.decode_batch(&records);
1730
1731 assert!(result.is_err());
1732 assert!(result
1733 .unwrap_err()
1734 .to_string()
1735 .contains("not exactly representable"));
1736 }
1737
1738 #[test]
1739 fn test_decode_timestamp_overflow_on_nanosecond_scaling_is_rejected() {
1740 let schema = make_schema(vec![(
1743 "ts",
1744 DataType::Timestamp(TimeUnit::Nanosecond, None),
1745 false,
1746 )]);
1747 let decoder = JsonDecoder::new(schema);
1748 let records = vec![json_record(r#"{"ts": 9999999999999999}"#)];
1749 let result = decoder.decode_batch(&records);
1750
1751 assert!(result.is_err());
1752 assert!(result
1753 .unwrap_err()
1754 .to_string()
1755 .contains("out of i64 Nanosecond range"));
1756 }
1757
1758 #[test]
1761 fn test_decode_nested_object_as_json_string() {
1762 let schema = make_schema(vec![("data", DataType::LargeBinary, true)]);
1763 let decoder = JsonDecoder::new(schema);
1764 let records = vec![json_record(r#"{"data": {"nested": true}}"#)];
1765 let batch = decoder.decode_batch(&records).unwrap();
1766
1767 assert!(!batch.column(0).is_null(0));
1768 }
1769
1770 #[test]
1773 fn test_decode_invalid_json() {
1774 let schema = make_schema(vec![("a", DataType::Int64, false)]);
1775 let decoder = JsonDecoder::new(schema);
1776 let records = vec![RawRecord::new(b"not json".to_vec())];
1777 let result = decoder.decode_batch(&records);
1778
1779 assert!(result.is_err());
1780 assert!(result.unwrap_err().to_string().contains("JSON parse error"));
1781 }
1782
1783 #[test]
1784 fn test_decode_non_object_json() {
1785 let schema = make_schema(vec![("a", DataType::Int64, false)]);
1786 let decoder = JsonDecoder::new(schema);
1787 let records = vec![json_record("[1, 2, 3]")];
1788 let result = decoder.decode_batch(&records);
1789
1790 assert!(result.is_err());
1791 assert!(result
1792 .unwrap_err()
1793 .to_string()
1794 .contains("must be an object"));
1795 }
1796
1797 #[test]
1800 fn test_format_name() {
1801 let schema = make_schema(vec![("a", DataType::Int64, false)]);
1802 let decoder = JsonDecoder::new(schema);
1803 assert_eq!(decoder.format_name(), "json");
1804 }
1805
1806 #[test]
1807 fn test_output_schema() {
1808 let schema = make_schema(vec![
1809 ("a", DataType::Int64, false),
1810 ("b", DataType::Utf8, true),
1811 ]);
1812 let decoder = JsonDecoder::new(schema.clone());
1813 assert_eq!(decoder.output_schema(), schema);
1814 }
1815
1816 #[test]
1817 fn test_decode_one() {
1818 let schema = make_schema(vec![("x", DataType::Int64, false)]);
1819 let decoder = JsonDecoder::new(schema);
1820 let record = json_record(r#"{"x": 99}"#);
1821 let batch = decoder.decode_one(&record).unwrap();
1822 assert_eq!(batch.num_rows(), 1);
1823 assert_eq!(
1824 batch
1825 .column(0)
1826 .as_primitive::<arrow_array::types::Int64Type>()
1827 .value(0),
1828 99
1829 );
1830 }
1831
1832 #[test]
1835 fn test_decode_int_from_float_json() {
1836 let schema = make_schema(vec![("x", DataType::Int64, true)]);
1839 let decoder = JsonDecoder::new(schema);
1840 let records = vec![json_record(r#"{"x": 42.0}"#)];
1841 let batch = decoder.decode_batch(&records).unwrap();
1842 assert_eq!(
1843 batch
1844 .column(0)
1845 .as_primitive::<arrow_array::types::Int64Type>()
1846 .value(0),
1847 42
1848 );
1849 }
1850
1851 #[test]
1852 fn test_decode_float_from_int_json() {
1853 let schema = make_schema(vec![("x", DataType::Float64, false)]);
1855 let decoder = JsonDecoder::new(schema);
1856 let records = vec![json_record(r#"{"x": 42}"#)];
1857 let batch = decoder.decode_batch(&records).unwrap();
1858 let val = batch
1859 .column(0)
1860 .as_primitive::<arrow_array::types::Float64Type>()
1861 .value(0);
1862 assert!((val - 42.0).abs() < f64::EPSILON);
1863 }
1864
1865 #[test]
1868 fn test_decode_string_number_to_float64() {
1869 let schema = make_schema(vec![("price", DataType::Float64, false)]);
1870 let decoder = JsonDecoder::new(schema);
1871 let records = vec![json_record(r#"{"price": "187.52"}"#)];
1872 let batch = decoder.decode_batch(&records).unwrap();
1873 let val = batch
1874 .column(0)
1875 .as_primitive::<arrow_array::types::Float64Type>()
1876 .value(0);
1877 assert!((val - 187.52).abs() < f64::EPSILON);
1878 }
1879
1880 #[test]
1881 fn test_decode_string_to_int() {
1882 let schema = make_schema(vec![("qty", DataType::Int32, false)]);
1883 let decoder = JsonDecoder::new(schema);
1884 let records = vec![json_record(r#"{"qty": "100"}"#)];
1885 let batch = decoder.decode_batch(&records).unwrap();
1886 assert_eq!(
1887 batch
1888 .column(0)
1889 .as_primitive::<arrow_array::types::Int32Type>()
1890 .value(0),
1891 100
1892 );
1893 }
1894
1895 #[test]
1896 fn test_decode_epoch_millis_to_timestamp_millis() {
1897 let schema = make_schema(vec![(
1898 "ts",
1899 DataType::Timestamp(TimeUnit::Millisecond, None),
1900 false,
1901 )]);
1902 let decoder = JsonDecoder::new(schema);
1903 let records = vec![json_record(r#"{"ts": 1705312200000}"#)];
1904 let batch = decoder.decode_batch(&records).unwrap();
1905 let ts_col = batch
1906 .column(0)
1907 .as_primitive::<arrow_array::types::TimestampMillisecondType>();
1908 assert_eq!(ts_col.value(0), 1_705_312_200_000);
1909 }
1910
1911 #[test]
1912 fn test_decode_int_to_float_promotion() {
1913 let schema = make_schema(vec![("val", DataType::Float64, false)]);
1914 let decoder = JsonDecoder::new(schema);
1915 let records = vec![json_record(r#"{"val": 100}"#)];
1916 let batch = decoder.decode_batch(&records).unwrap();
1917 let val = batch
1918 .column(0)
1919 .as_primitive::<arrow_array::types::Float64Type>()
1920 .value(0);
1921 assert!((val - 100.0).abs() < f64::EPSILON);
1922 }
1923
1924 #[test]
1925 fn test_decode_string_boolean() {
1926 let schema = make_schema(vec![("active", DataType::Boolean, false)]);
1927 let decoder = JsonDecoder::new(schema);
1928 let records = vec![json_record(r#"{"active": "true"}"#)];
1929 let batch = decoder.decode_batch(&records).unwrap();
1930 assert!(batch.column(0).as_boolean().value(0));
1931 }
1932
1933 #[test]
1934 fn test_coerce_fails_on_unconvertible() {
1935 let schema = make_schema(vec![("x", DataType::Int64, true)]);
1937 let decoder = JsonDecoder::new(schema);
1938 let records = vec![json_record(r#"{"x": "not_a_number"}"#)];
1939 let result = decoder.decode_batch(&records);
1940 assert!(result.is_err());
1941 assert!(result
1942 .unwrap_err()
1943 .to_string()
1944 .contains("type coercion failed"));
1945 }
1946
1947 #[test]
1948 fn test_enforcement_str_parsing() {
1949 assert_eq!(
1950 TypeMismatchStrategy::from_enforcement_str("coerce"),
1951 Some(TypeMismatchStrategy::Coerce)
1952 );
1953 assert_eq!(
1954 TypeMismatchStrategy::from_enforcement_str("STRICT"),
1955 Some(TypeMismatchStrategy::Reject)
1956 );
1957 assert_eq!(
1958 TypeMismatchStrategy::from_enforcement_str("Permissive"),
1959 Some(TypeMismatchStrategy::Null)
1960 );
1961 assert_eq!(TypeMismatchStrategy::from_enforcement_str("unknown"), None);
1962 }
1963
1964 #[test]
1967 fn test_decode_i8_and_u8() {
1968 let schema = make_schema(vec![
1969 ("signed", DataType::Int8, false),
1970 ("unsigned", DataType::UInt8, false),
1971 ]);
1972 let decoder = JsonDecoder::new(schema);
1973 let records = vec![json_record(r#"{"signed": -5, "unsigned": 200}"#)];
1974 let batch = decoder.decode_batch(&records).unwrap();
1975 assert_eq!(
1976 batch
1977 .column(0)
1978 .as_primitive::<arrow_array::types::Int8Type>()
1979 .value(0),
1980 -5
1981 );
1982 assert_eq!(
1983 batch
1984 .column(1)
1985 .as_primitive::<arrow_array::types::UInt8Type>()
1986 .value(0),
1987 200
1988 );
1989 }
1990
1991 #[test]
1994 fn test_json_path_single() {
1995 let schema = make_schema(vec![("id", DataType::Int64, false)]);
1996 let config = JsonDecoderConfig {
1997 json_path: Some(vec!["data".into()]),
1998 ..Default::default()
1999 };
2000 let decoder = JsonDecoder::with_config(schema, config);
2001 let records = vec![json_record(r#"{"data":{"id":1}}"#)];
2002 let batch = decoder.decode_batch(&records).unwrap();
2003 assert_eq!(batch.num_rows(), 1);
2004 assert_eq!(
2005 batch
2006 .column(0)
2007 .as_primitive::<arrow_array::types::Int64Type>()
2008 .value(0),
2009 1
2010 );
2011 }
2012
2013 #[test]
2014 fn test_json_path_multi() {
2015 let schema = make_schema(vec![("id", DataType::Int64, false)]);
2016 let config = JsonDecoderConfig {
2017 json_path: Some(vec!["a".into(), "b".into()]),
2018 ..Default::default()
2019 };
2020 let decoder = JsonDecoder::with_config(schema, config);
2021 let records = vec![json_record(r#"{"a":{"b":{"id":1}}}"#)];
2022 let batch = decoder.decode_batch(&records).unwrap();
2023 assert_eq!(
2024 batch
2025 .column(0)
2026 .as_primitive::<arrow_array::types::Int64Type>()
2027 .value(0),
2028 1
2029 );
2030 }
2031
2032 #[test]
2035 fn test_json_path_missing_skips_record() {
2036 let schema = make_schema(vec![("id", DataType::Int64, false)]);
2037 let config = JsonDecoderConfig {
2038 json_path: Some(vec!["data".into()]),
2039 ..Default::default()
2040 };
2041 let decoder = JsonDecoder::with_config(schema, config);
2042 let records = vec![
2043 json_record(r#"{"success":true,"op":"subscribe"}"#), json_record(r#"{"data":{"id":42}}"#),
2045 ];
2046 let batch = decoder.decode_batch(&records).unwrap();
2047 assert_eq!(batch.num_rows(), 1);
2048 assert_eq!(
2049 batch
2050 .column(0)
2051 .as_primitive::<arrow_array::types::Int64Type>()
2052 .value(0),
2053 42
2054 );
2055 }
2056
2057 #[test]
2060 fn test_json_column_custom_path() {
2061 let schema = make_schema(vec![
2062 ("p", DataType::Float64, false),
2063 ("stream_name", DataType::Utf8, true),
2064 ]);
2065 let mut col_paths = HashMap::new();
2066 col_paths.insert("stream_name".into(), vec!["stream".into()]);
2067 let config = JsonDecoderConfig {
2068 json_path: Some(vec!["data".into()]),
2069 json_column_paths: col_paths,
2070 ..Default::default()
2071 };
2072 let decoder = JsonDecoder::with_config(schema, config);
2073 let records = vec![json_record(
2074 r#"{"stream":"btcusdt@trade","data":{"p":67523.0}}"#,
2075 )];
2076 let batch = decoder.decode_batch(&records).unwrap();
2077 assert_eq!(batch.num_rows(), 1);
2078 let price = batch
2079 .column(0)
2080 .as_primitive::<arrow_array::types::Float64Type>()
2081 .value(0);
2082 assert!((price - 67523.0).abs() < f64::EPSILON);
2083 assert_eq!(batch.column(1).as_string::<i32>().value(0), "btcusdt@trade");
2084 }
2085
2086 #[test]
2087 fn test_json_column_deep_path() {
2088 let schema = make_schema(vec![
2089 ("p", DataType::Float64, false),
2090 ("ts", DataType::Int64, true),
2091 ]);
2092 let mut col_paths = HashMap::new();
2093 col_paths.insert("ts".into(), vec!["meta".into(), "timestamp".into()]);
2094 let config = JsonDecoderConfig {
2095 json_path: Some(vec!["data".into()]),
2096 json_column_paths: col_paths,
2097 ..Default::default()
2098 };
2099 let decoder = JsonDecoder::with_config(schema, config);
2100 let records = vec![json_record(
2101 r#"{"meta":{"timestamp":123456},"data":{"p":99.5}}"#,
2102 )];
2103 let batch = decoder.decode_batch(&records).unwrap();
2104 assert_eq!(batch.num_rows(), 1);
2105 assert_eq!(
2106 batch
2107 .column(1)
2108 .as_primitive::<arrow_array::types::Int64Type>()
2109 .value(0),
2110 123456
2111 );
2112 }
2113
2114 #[test]
2115 fn test_json_column_missing_returns_null() {
2116 let schema = make_schema(vec![
2117 ("id", DataType::Int64, false),
2118 ("missing_col", DataType::Utf8, true),
2119 ]);
2120 let mut col_paths = HashMap::new();
2121 col_paths.insert("missing_col".into(), vec!["nowhere".into(), "gone".into()]);
2122 let config = JsonDecoderConfig {
2123 json_column_paths: col_paths,
2124 ..Default::default()
2125 };
2126 let decoder = JsonDecoder::with_config(schema, config);
2127 let records = vec![json_record(r#"{"id":42}"#)];
2128 let batch = decoder.decode_batch(&records).unwrap();
2129 assert_eq!(
2130 batch
2131 .column(0)
2132 .as_primitive::<arrow_array::types::Int64Type>()
2133 .value(0),
2134 42
2135 );
2136 assert!(batch.column(1).is_null(0));
2137 }
2138
2139 #[test]
2140 fn test_json_column_path_to_nested_string_array() {
2141 use arrow_array::Array;
2142 use arrow_schema::Field;
2143 let schema = make_schema(vec![(
2144 "tags",
2145 DataType::List(Arc::new(Field::new("item", DataType::Utf8, true))),
2146 true,
2147 )]);
2148 let mut col_paths = HashMap::new();
2149 col_paths.insert("tags".into(), vec!["record".into(), "tags".into()]);
2150 let config = JsonDecoderConfig {
2151 json_column_paths: col_paths,
2152 ..Default::default()
2153 };
2154 let decoder = JsonDecoder::with_config(schema, config);
2155 let records = vec![json_record(r#"{"record":{"tags":["en","es"]}}"#)];
2156 let batch = decoder.decode_batch(&records).unwrap();
2157
2158 let list = batch
2159 .column(0)
2160 .as_any()
2161 .downcast_ref::<arrow_array::ListArray>()
2162 .expect("List column");
2163 let vals = list.value(0);
2164 let strs = vals.as_string::<i32>();
2165 assert_eq!(strs.len(), 2);
2166 assert_eq!(strs.value(0), "en");
2167 assert_eq!(strs.value(1), "es");
2168 }
2169
2170 #[test]
2171 fn test_list_column_non_array_honors_reject_strategy() {
2172 use arrow_schema::Field;
2173 let schema = make_schema(vec![(
2174 "tags",
2175 DataType::List(Arc::new(Field::new("item", DataType::Utf8, true))),
2176 true,
2177 )]);
2178 let config = JsonDecoderConfig {
2179 type_mismatch: TypeMismatchStrategy::Reject,
2180 ..Default::default()
2181 };
2182 let decoder = JsonDecoder::with_config(schema, config);
2183 let records = vec![json_record(r#"{"tags": "en"}"#)];
2186 let result = decoder.decode_batch(&records);
2187
2188 assert!(result.is_err());
2189 assert!(result.unwrap_err().to_string().contains("type mismatch"));
2190 }
2191
2192 #[test]
2195 fn test_json_explode_arrays() {
2196 let schema = make_schema(vec![
2197 ("price", DataType::Utf8, true),
2198 ("qty", DataType::Utf8, true),
2199 ]);
2200 let config = JsonDecoderConfig {
2201 json_explode: Some(vec!["price".into(), "qty".into()]),
2202 ..Default::default()
2203 };
2204 let decoder = JsonDecoder::with_config(schema, config);
2205 let records = vec![json_record(r#"[["67523","1.5"],["67522","0.8"]]"#)];
2206 let batch = decoder.decode_batch(&records).unwrap();
2207 assert_eq!(batch.num_rows(), 2);
2208 assert_eq!(batch.column(0).as_string::<i32>().value(0), "67523");
2209 assert_eq!(batch.column(0).as_string::<i32>().value(1), "67522");
2210 assert_eq!(batch.column(1).as_string::<i32>().value(0), "1.5");
2211 assert_eq!(batch.column(1).as_string::<i32>().value(1), "0.8");
2212 }
2213
2214 #[test]
2215 fn test_json_explode_objects() {
2216 let schema = make_schema(vec![
2217 ("id", DataType::Int64, true),
2218 ("name", DataType::Utf8, true),
2219 ]);
2220 let config = JsonDecoderConfig {
2221 json_explode: Some(vec!["id".into(), "name".into()]),
2222 ..Default::default()
2223 };
2224 let decoder = JsonDecoder::with_config(schema, config);
2225 let records = vec![json_record(
2226 r#"[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]"#,
2227 )];
2228 let batch = decoder.decode_batch(&records).unwrap();
2229 assert_eq!(batch.num_rows(), 2);
2230 assert_eq!(
2231 batch
2232 .column(0)
2233 .as_primitive::<arrow_array::types::Int64Type>()
2234 .value(0),
2235 1
2236 );
2237 assert_eq!(batch.column(1).as_string::<i32>().value(1), "Bob");
2238 }
2239
2240 #[test]
2243 fn test_json_path_plus_explode() {
2244 let schema = make_schema(vec![
2245 ("price", DataType::Utf8, true),
2246 ("qty", DataType::Utf8, true),
2247 ]);
2248 let config = JsonDecoderConfig {
2249 json_path: Some(vec!["bids".into()]),
2250 json_explode: Some(vec!["price".into(), "qty".into()]),
2251 ..Default::default()
2252 };
2253 let decoder = JsonDecoder::with_config(schema, config);
2254 let records = vec![json_record(r#"{"bids":[["67523","1.5"],["67522","0.8"]]}"#)];
2255 let batch = decoder.decode_batch(&records).unwrap();
2256 assert_eq!(batch.num_rows(), 2);
2257 assert_eq!(batch.column(0).as_string::<i32>().value(0), "67523");
2258 assert_eq!(batch.column(1).as_string::<i32>().value(1), "0.8");
2259 }
2260
2261 #[test]
2264 fn test_from_connector_config() {
2265 let mut config = crate::config::ConnectorConfig::new("json");
2266 config.set("json.path", "data.trade");
2267 config.set("json.column.stream_name", "stream");
2268 config.set("json.column.ts", "meta.timestamp");
2269 config.set("json.explode", "price, qty");
2270 config.set("schema.enforcement", "strict");
2271 config.set("nested.as.jsonb", "true");
2272
2273 let schema = make_schema(vec![
2274 ("stream_name", DataType::Utf8, true),
2275 ("ts", DataType::Timestamp(TimeUnit::Microsecond, None), true),
2276 ("price", DataType::Utf8, true),
2277 ("qty", DataType::Utf8, true),
2278 ]);
2279 let cfg = JsonDecoderConfig::from_connector_config(&config, &schema).unwrap();
2280 assert_eq!(cfg.json_path, Some(vec!["data".into(), "trade".into()]));
2281 assert_eq!(
2282 cfg.json_column_paths.get("stream_name"),
2283 Some(&vec!["stream".into()])
2284 );
2285 assert_eq!(
2286 cfg.json_column_paths.get("ts"),
2287 Some(&vec!["meta".into(), "timestamp".into()])
2288 );
2289 assert_eq!(cfg.json_explode, Some(vec!["price".into(), "qty".into()]));
2290 assert_eq!(cfg.type_mismatch, TypeMismatchStrategy::Reject);
2291 assert!(cfg.nested_as_jsonb);
2292 }
2293
2294 #[test]
2295 fn connector_config_rejects_invalid_projection_options() {
2296 let schema = make_schema(vec![
2297 ("id", DataType::Int64, false),
2298 ("ts", DataType::Timestamp(TimeUnit::Microsecond, None), true),
2299 ]);
2300 for (key, value) in [
2301 ("json.path", "data..event"),
2302 ("json.column.missing", "payload.value"),
2303 ("json.column.id", "payload..value"),
2304 ("json.explode", "id,missing"),
2305 ("json.explode", "id,id"),
2306 ("schema.enforcement", "strcit"),
2307 ("nested.as.jsonb", "yes"),
2308 ] {
2309 let mut config = crate::config::ConnectorConfig::new("json");
2310 config.set(key, value);
2311 let error = JsonDecoderConfig::from_connector_config(&config, &schema)
2312 .unwrap_err()
2313 .to_string();
2314 assert!(error.contains(key), "{key}={value}: {error}");
2315 }
2316 }
2317
2318 #[test]
2319 fn test_default_config_unchanged() {
2320 let schema = make_schema(vec![
2321 ("a", DataType::Int64, false),
2322 ("b", DataType::Utf8, true),
2323 ]);
2324 let decoder = JsonDecoder::new(schema);
2325 let records = vec![
2326 json_record(r#"{"a": 1, "b": "hello"}"#),
2327 json_record(r#"{"a": 2, "b": "world"}"#),
2328 ];
2329 let batch = decoder.decode_batch(&records).unwrap();
2330 assert_eq!(batch.num_rows(), 2);
2331 assert_eq!(
2332 batch
2333 .column(0)
2334 .as_primitive::<arrow_array::types::Int64Type>()
2335 .value(0),
2336 1
2337 );
2338 assert_eq!(batch.column(1).as_string::<i32>().value(1), "world");
2339 }
2340
2341 #[test]
2342 fn test_unknown_fields_with_path() {
2343 let schema = make_schema(vec![("a", DataType::Int64, false)]);
2344 let config = JsonDecoderConfig {
2345 json_path: Some(vec!["data".into()]),
2346 unknown_fields: UnknownFieldStrategy::CollectExtra,
2347 ..Default::default()
2348 };
2349 let decoder = JsonDecoder::with_config(schema, config);
2350 let records = vec![json_record(r#"{"data":{"a":1,"extra":"value"}}"#)];
2351 let batch = decoder.decode_batch(&records).unwrap();
2352 assert_eq!(batch.num_columns(), 2); assert_eq!(batch.schema().field(1).name(), "_extra");
2354 assert!(!batch.column(1).is_null(0));
2355 }
2356
2357 #[test]
2360 fn from_connector_config_parses_epoch_unit_without_polluting_paths() {
2361 use crate::config::ConnectorConfig;
2362 let mut props = std::collections::HashMap::new();
2363 props.insert("json.column.evt".to_string(), "time_us".to_string());
2364 props.insert(
2365 "json.column.evt.epoch_unit".to_string(),
2366 "micros".to_string(),
2367 );
2368 let cc = ConnectorConfig::with_properties("json", props);
2369 let schema = make_schema(vec![(
2370 "evt",
2371 DataType::Timestamp(TimeUnit::Microsecond, None),
2372 false,
2373 )]);
2374 let cfg = JsonDecoderConfig::from_connector_config(&cc, &schema).unwrap();
2375 assert_eq!(
2376 cfg.numeric_timestamp_units.get("evt"),
2377 Some(&EpochUnit::Micros)
2378 );
2379 assert!(cfg.json_column_paths.contains_key("evt"));
2381 assert!(!cfg.json_column_paths.contains_key("evt.epoch_unit"));
2382 }
2383
2384 #[test]
2385 fn from_connector_config_rejects_noncanonical_epoch_units() {
2386 for value in ["epoch_micros", "minutes"] {
2387 let mut config = crate::config::ConnectorConfig::new("source");
2388 config.set("json.column.evt.epoch_unit", value);
2389 let schema = make_schema(vec![(
2390 "evt",
2391 DataType::Timestamp(TimeUnit::Microsecond, None),
2392 false,
2393 )]);
2394 let error = JsonDecoderConfig::from_connector_config(&config, &schema)
2395 .unwrap_err()
2396 .to_string();
2397 assert!(error.contains("json.column.evt.epoch_unit"), "{error}");
2398 assert!(error.contains(value), "{error}");
2399 }
2400 }
2401
2402 #[test]
2403 fn from_connector_config_rejects_unknown_or_non_timestamp_epoch_columns() {
2404 let schema = make_schema(vec![
2405 (
2406 "ts",
2407 DataType::Timestamp(TimeUnit::Microsecond, None),
2408 false,
2409 ),
2410 ("id", DataType::Int64, false),
2411 ]);
2412
2413 for (column, expected) in [
2414 ("missing", "not present in the source schema"),
2415 ("id", "must be a Timestamp"),
2416 ] {
2417 let mut config = crate::config::ConnectorConfig::new("source");
2418 config.set(format!("json.column.{column}.epoch_unit"), "micros");
2419 let error = JsonDecoderConfig::from_connector_config(&config, &schema)
2420 .unwrap_err()
2421 .to_string();
2422 assert!(error.contains(column), "{error}");
2423 assert!(error.contains(expected), "{error}");
2424 }
2425 }
2426
2427 #[test]
2428 fn integer_epoch_conversion_covers_every_source_and_arrow_unit() {
2429 let source_values = [
2430 (EpochUnit::Seconds, 2),
2431 (EpochUnit::Millis, 2_000),
2432 (EpochUnit::Micros, 2_000_000),
2433 (EpochUnit::Nanos, 2_000_000_000),
2434 ];
2435 let target_values = [
2436 (TimeUnit::Second, 2),
2437 (TimeUnit::Millisecond, 2_000),
2438 (TimeUnit::Microsecond, 2_000_000),
2439 (TimeUnit::Nanosecond, 2_000_000_000),
2440 ];
2441
2442 for (from, value) in source_values {
2443 for (to, expected) in &target_values {
2444 assert_eq!(
2445 checked_epoch_to_unit(value, from, *to).unwrap(),
2446 *expected,
2447 "from={from:?}, to={to:?}"
2448 );
2449 }
2450 }
2451 }
2452
2453 #[test]
2454 fn integer_epoch_downscale_matches_arrow_for_negative_values() {
2455 assert_eq!(
2456 checked_epoch_to_unit(-1_500, EpochUnit::Nanos, TimeUnit::Microsecond).unwrap(),
2457 -1
2458 );
2459 assert_eq!(
2460 checked_epoch_to_unit(-1_500, EpochUnit::Micros, TimeUnit::Millisecond).unwrap(),
2461 -1
2462 );
2463 assert_eq!(
2464 checked_epoch_to_unit(-2, EpochUnit::Seconds, TimeUnit::Nanosecond).unwrap(),
2465 -2_000_000_000
2466 );
2467 }
2468
2469 #[test]
2470 fn integer_epoch_upscale_rejects_positive_and_negative_overflow() {
2471 assert!(checked_epoch_to_unit(i64::MAX, EpochUnit::Seconds, TimeUnit::Nanosecond).is_err());
2472 assert!(checked_epoch_to_unit(i64::MIN, EpochUnit::Millis, TimeUnit::Microsecond).is_err());
2473 }
2474
2475 #[test]
2476 fn fractional_numeric_epoch_is_rejected_instead_of_rounded() {
2477 let schema = make_schema(vec![(
2478 "ts",
2479 DataType::Timestamp(TimeUnit::Millisecond, None),
2480 false,
2481 )]);
2482 let decoder = JsonDecoder::new(schema);
2483 let error = decoder
2484 .decode_batch(&[json_record(r#"{"ts": 1.5}"#)])
2485 .unwrap_err()
2486 .to_string();
2487 assert!(error.contains("fractional timestamp"), "{error}");
2488 }
2489
2490 #[test]
2491 fn integral_float_epoch_requires_exact_integer_range() {
2492 let schema = make_schema(vec![(
2493 "ts",
2494 DataType::Timestamp(TimeUnit::Millisecond, None),
2495 false,
2496 )]);
2497 let decoder = JsonDecoder::new(schema);
2498
2499 let batch = decoder
2500 .decode_batch(&[json_record(r#"{"ts": 1000.0}"#)])
2501 .unwrap();
2502 let timestamps = batch
2503 .column(0)
2504 .as_primitive::<arrow_array::types::TimestampMillisecondType>();
2505 assert_eq!(timestamps.value(0), 1_000);
2506
2507 let error = decoder
2508 .decode_batch(&[json_record(r#"{"ts": 9.007199254740992e15}"#)])
2509 .unwrap_err()
2510 .to_string();
2511 assert!(error.contains("not exactly representable"), "{error}");
2512 }
2513
2514 #[test]
2515 fn numeric_micros_decodes_into_millis_timestamp_column() {
2516 let schema = make_schema(vec![(
2520 "ts",
2521 DataType::Timestamp(TimeUnit::Millisecond, None),
2522 false,
2523 )]);
2524 let mut units = HashMap::new();
2525 units.insert("ts".to_string(), EpochUnit::Micros);
2526 let config = JsonDecoderConfig {
2527 numeric_timestamp_units: units,
2528 ..Default::default()
2529 };
2530 let decoder = JsonDecoder::with_config(schema, config);
2531 let records = vec![json_record(r#"{"ts": 1779019200123456}"#)];
2533 let batch = decoder.decode_batch(&records).unwrap();
2534 let col = batch
2535 .column(0)
2536 .as_primitive::<arrow_array::types::TimestampMillisecondType>();
2537 assert_eq!(col.value(0), 1_779_019_200_123);
2538
2539 let schema2 = make_schema(vec![(
2541 "ts",
2542 DataType::Timestamp(TimeUnit::Millisecond, None),
2543 false,
2544 )]);
2545 let d2 = JsonDecoder::new(schema2);
2546 let b2 = d2.decode_batch(&records).unwrap();
2547 let c2 = b2
2548 .column(0)
2549 .as_primitive::<arrow_array::types::TimestampMillisecondType>();
2550 assert_eq!(c2.value(0), 1_779_019_200_123_456);
2551 }
2552}