1use std::io::BufReader;
21use std::pin::Pin;
22use std::sync::Arc;
23use std::task::{Context, Poll};
24
25use crate::file_format::JsonDecoder;
26use crate::utils::{ChannelReader, JsonArrayToNdjsonReader};
27
28use datafusion_common::error::{DataFusionError, Result};
29use datafusion_common::exec_datafusion_err;
30use datafusion_common::tree_node::TreeNodeRecursion;
31use datafusion_common_runtime::{JoinSet, SpawnedTask};
32use datafusion_datasource::boundary_stream::AlignedBoundaryStream;
33use datafusion_datasource::decoder::{DecoderDeserializer, deserialize_stream};
34use datafusion_datasource::file_compression_type::FileCompressionType;
35use datafusion_datasource::file_stream::{FileOpenFuture, FileOpener};
36use datafusion_datasource::projection::{ProjectionOpener, SplitProjection};
37use datafusion_datasource::{ListingTableUrl, PartitionedFile, as_file_source};
38use datafusion_physical_plan::projection::ProjectionExprs;
39use datafusion_physical_plan::{ExecutionPlan, ExecutionPlanProperties};
40
41use arrow::array::RecordBatch;
42use arrow::json::ReaderBuilder;
43use arrow::{datatypes::SchemaRef, json};
44use datafusion_datasource::file::FileSource;
45use datafusion_datasource::file_scan_config::FileScanConfig;
46use datafusion_execution::TaskContext;
47use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet;
48
49use futures::{Stream, StreamExt, TryStreamExt};
50use object_store::buffered::BufWriter;
51use object_store::{GetOptions, GetResultPayload, ObjectStore};
52use tokio::io::AsyncWriteExt;
53use tokio_stream::wrappers::ReceiverStream;
54
55const CHANNEL_BUFFER_SIZE: usize = 128;
58
59const JSON_CONVERTER_BUFFER_SIZE: usize = 2 * 1024 * 1024;
61
62struct JsonArrayStream {
72 inner: ReceiverStream<std::result::Result<RecordBatch, arrow::error::ArrowError>>,
73 _read_task: SpawnedTask<()>,
76 _parse_task: SpawnedTask<()>,
79}
80
81impl Stream for JsonArrayStream {
82 type Item = std::result::Result<RecordBatch, arrow::error::ArrowError>;
83
84 fn poll_next(
85 mut self: Pin<&mut Self>,
86 cx: &mut Context<'_>,
87 ) -> Poll<Option<Self::Item>> {
88 Pin::new(&mut self.inner).poll_next(cx)
89 }
90
91 fn size_hint(&self) -> (usize, Option<usize>) {
92 self.inner.size_hint()
93 }
94}
95pub struct JsonOpener {
101 batch_size: usize,
102 projected_schema: SchemaRef,
103 file_compression_type: FileCompressionType,
104 object_store: Arc<dyn ObjectStore>,
105 newline_delimited: bool,
108}
109
110impl JsonOpener {
111 pub fn new(
113 batch_size: usize,
114 projected_schema: SchemaRef,
115 file_compression_type: FileCompressionType,
116 object_store: Arc<dyn ObjectStore>,
117 newline_delimited: bool,
118 ) -> Self {
119 Self {
120 batch_size,
121 projected_schema,
122 file_compression_type,
123 object_store,
124 newline_delimited,
125 }
126 }
127}
128
129#[derive(Clone)]
131pub struct JsonSource {
132 table_schema: datafusion_datasource::TableSchema,
133 batch_size: Option<usize>,
134 metrics: ExecutionPlanMetricsSet,
135 projection: SplitProjection,
136 newline_delimited: bool,
139}
140
141impl JsonSource {
142 pub fn new(table_schema: impl Into<datafusion_datasource::TableSchema>) -> Self {
144 let table_schema = table_schema.into();
145 Self {
146 projection: SplitProjection::unprojected(&table_schema),
147 table_schema,
148 batch_size: None,
149 metrics: ExecutionPlanMetricsSet::new(),
150 newline_delimited: true,
151 }
152 }
153
154 pub fn with_newline_delimited(mut self, newline_delimited: bool) -> Self {
159 self.newline_delimited = newline_delimited;
160 self
161 }
162}
163
164impl From<JsonSource> for Arc<dyn FileSource> {
165 fn from(source: JsonSource) -> Self {
166 as_file_source(source)
167 }
168}
169
170impl FileSource for JsonSource {
171 fn create_file_opener(
172 &self,
173 object_store: Arc<dyn ObjectStore>,
174 base_config: &FileScanConfig,
175 _partition: usize,
176 ) -> Result<Arc<dyn FileOpener>> {
177 let file_schema = self.table_schema.file_schema();
179 let projected_schema =
180 Arc::new(file_schema.project(&self.projection.file_indices)?);
181
182 let mut opener = Arc::new(JsonOpener {
183 batch_size: self
184 .batch_size
185 .expect("Batch size must set before creating opener"),
186 projected_schema,
187 file_compression_type: base_config.file_compression_type,
188 object_store,
189 newline_delimited: self.newline_delimited,
190 }) as Arc<dyn FileOpener>;
191
192 opener = ProjectionOpener::try_new(
194 self.projection.clone(),
195 Arc::clone(&opener),
196 self.table_schema.file_schema(),
197 )?;
198
199 Ok(opener)
200 }
201
202 fn table_schema(&self) -> &datafusion_datasource::TableSchema {
203 &self.table_schema
204 }
205
206 fn with_batch_size(&self, batch_size: usize) -> Arc<dyn FileSource> {
207 let mut conf = self.clone();
208 conf.batch_size = Some(batch_size);
209 Arc::new(conf)
210 }
211
212 fn try_pushdown_projection(
213 &self,
214 projection: &ProjectionExprs,
215 ) -> Result<Option<Arc<dyn FileSource>>> {
216 let mut source = self.clone();
217 let new_projection = self.projection.source.try_merge(projection)?;
218 let split_projection =
219 SplitProjection::new(self.table_schema.file_schema(), &new_projection);
220 source.projection = split_projection;
221 Ok(Some(Arc::new(source)))
222 }
223
224 fn projection(&self) -> Option<&ProjectionExprs> {
225 Some(&self.projection.source)
226 }
227
228 fn metrics(&self) -> &ExecutionPlanMetricsSet {
229 &self.metrics
230 }
231
232 fn file_type(&self) -> &str {
233 "json"
234 }
235
236 fn apply_expressions(
237 &self,
238 f: &mut dyn FnMut(
239 &Arc<dyn datafusion_physical_plan::PhysicalExpr>,
240 ) -> Result<TreeNodeRecursion>,
241 ) -> Result<TreeNodeRecursion> {
242 datafusion_physical_plan::apply_expression_roots(self.projection.source.iter(), f)
243 }
244
245 #[cfg(feature = "proto")]
247 fn try_to_proto(
248 &self,
249 base: &FileScanConfig,
250 ctx: &datafusion_physical_plan::proto::ExecutionPlanEncodeCtx<'_>,
251 ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
252 use datafusion_proto_models::protobuf;
253 use protobuf::physical_plan_node::PhysicalPlanType;
254
255 let node = protobuf::JsonScanExecNode {
256 base_conf: Some(base.try_to_proto(ctx)?),
257 };
258 Ok(Some(protobuf::PhysicalPlanNode {
259 physical_plan_type: Some(PhysicalPlanType::JsonScan(node)),
260 }))
261 }
262}
263
264#[cfg(feature = "proto")]
265impl JsonSource {
266 pub fn try_from_proto(
270 node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
271 ctx: &datafusion_physical_plan::proto::ExecutionPlanDecodeCtx<'_>,
272 ) -> Result<Arc<dyn ExecutionPlan>> {
273 use datafusion_datasource::file_scan_config::FileScanConfig;
274 use datafusion_datasource::source::DataSourceExec;
275 use datafusion_proto_models::protobuf;
276
277 let scan = match &node.physical_plan_type {
278 Some(protobuf::physical_plan_node::PhysicalPlanType::JsonScan(scan)) => scan,
279 _ => {
280 return datafusion_common::internal_err!(
281 "PhysicalPlanNode is not a JsonScan"
282 );
283 }
284 };
285
286 let base_conf = scan.base_conf.as_ref().ok_or_else(|| {
287 datafusion_common::internal_datafusion_err!(
288 "JsonScanExecNode is missing required field 'base_conf'"
289 )
290 })?;
291
292 let table_schema = FileScanConfig::parse_table_schema_from_proto(base_conf)?;
293 let source = Arc::new(JsonSource::new(table_schema));
294
295 let conf = FileScanConfig::try_from_proto(base_conf, ctx, source)?;
296 Ok(DataSourceExec::from_data_source(conf))
297 }
298}
299
300impl FileOpener for JsonOpener {
301 fn open(&self, partitioned_file: PartitionedFile) -> Result<FileOpenFuture> {
313 let store = Arc::clone(&self.object_store);
314 let schema = Arc::clone(&self.projected_schema);
315 let batch_size = self.batch_size;
316 let file_compression_type = self.file_compression_type.to_owned();
317 let newline_delimited = self.newline_delimited;
318
319 if !newline_delimited && partitioned_file.range.is_some() {
321 return Err(DataFusionError::NotImplemented(
322 "JSON array format does not support range-based file scanning. \
323 Disable repartition_file_scans or use newline-delimited JSON format."
324 .to_string(),
325 ));
326 }
327
328 Ok(Box::pin(async move {
329 let file_size = partitioned_file.object_meta.size;
330 let location = &partitioned_file.object_meta.location;
331
332 if let Some(file_range) = partitioned_file.range.as_ref() {
333 let raw_start: u64 = file_range.start.try_into().map_err(|_| {
334 exec_datafusion_err!(
335 "Expected start range to fit in u64, got {}",
336 file_range.start
337 )
338 })?;
339 let raw_end: u64 = file_range.end.try_into().map_err(|_| {
340 exec_datafusion_err!(
341 "Expected end range to fit in u64, got {}",
342 file_range.end
343 )
344 })?;
345
346 let aligned_stream = AlignedBoundaryStream::new(
347 Arc::clone(&store),
348 location.clone(),
349 raw_start,
350 raw_end,
351 file_size,
352 b'\n',
353 )
354 .await?
355 .map_err(DataFusionError::from);
356
357 let decoder = ReaderBuilder::new(schema)
358 .with_batch_size(batch_size)
359 .build_decoder()?;
360 let input = file_compression_type
361 .convert_stream(aligned_stream.boxed())?
362 .fuse();
363 let stream = deserialize_stream(
364 input,
365 DecoderDeserializer::new(JsonDecoder::new(decoder)),
366 );
367 return Ok(stream.map_err(Into::into).boxed());
368 }
369
370 let options = GetOptions::default();
372 let result = store.get_opts(location, options).await?;
373
374 match result.payload {
375 #[cfg(not(target_arch = "wasm32"))]
376 GetResultPayload::File(file, _) => {
377 let bytes = file_compression_type.convert_read(file)?;
378
379 if newline_delimited {
380 let reader = BufReader::new(bytes);
382 let arrow_reader = ReaderBuilder::new(schema)
383 .with_batch_size(batch_size)
384 .build(reader)?;
385
386 Ok(futures::stream::iter(arrow_reader)
387 .map(|r| r.map_err(Into::into))
388 .boxed())
389 } else {
390 let ndjson_reader = JsonArrayToNdjsonReader::with_capacity(
392 bytes,
393 JSON_CONVERTER_BUFFER_SIZE,
394 );
395 let arrow_reader = ReaderBuilder::new(schema)
396 .with_batch_size(batch_size)
397 .build(ndjson_reader)?;
398
399 Ok(futures::stream::iter(arrow_reader)
400 .map(|r| r.map_err(Into::into))
401 .boxed())
402 }
403 }
404 GetResultPayload::Stream(s) => {
405 if newline_delimited {
406 let s = s.map_err(DataFusionError::from);
408 let decoder = ReaderBuilder::new(schema)
409 .with_batch_size(batch_size)
410 .build_decoder()?;
411 let input =
412 file_compression_type.convert_stream(s.boxed())?.fuse();
413 let stream = deserialize_stream(
414 input,
415 DecoderDeserializer::new(JsonDecoder::new(decoder)),
416 );
417 Ok(stream.map_err(Into::into).boxed())
418 } else {
419 let s = s.map_err(DataFusionError::from);
433 let decompressed_stream =
434 file_compression_type.convert_stream(s.boxed())?;
435
436 let (byte_tx, byte_rx) = tokio::sync::mpsc::channel::<bytes::Bytes>(
441 CHANNEL_BUFFER_SIZE,
442 );
443
444 let (result_tx, result_rx) = tokio::sync::mpsc::channel(2);
446 let error_tx = result_tx.clone();
447
448 let read_task = SpawnedTask::spawn(async move {
451 tokio::pin!(decompressed_stream);
452 while let Some(chunk) = decompressed_stream.next().await {
453 match chunk {
454 Ok(bytes) => {
455 if byte_tx.send(bytes).await.is_err() {
456 break; }
458 }
459 Err(e) => {
460 let _ = error_tx
461 .send(Err(
462 arrow::error::ArrowError::ExternalError(
463 Box::new(e),
464 ),
465 ))
466 .await;
467 break;
468 }
469 }
470 }
471 });
473
474 let parse_task = SpawnedTask::spawn_blocking(move || {
477 let channel_reader = ChannelReader::new(byte_rx);
478 let mut ndjson_reader =
479 JsonArrayToNdjsonReader::with_capacity(
480 channel_reader,
481 JSON_CONVERTER_BUFFER_SIZE,
482 );
483
484 match ReaderBuilder::new(schema)
485 .with_batch_size(batch_size)
486 .build(&mut ndjson_reader)
487 {
488 Ok(arrow_reader) => {
489 for batch_result in arrow_reader {
490 if result_tx.blocking_send(batch_result).is_err()
491 {
492 break; }
494 }
495 }
496 Err(e) => {
497 let _ = result_tx.blocking_send(Err(e));
498 }
499 }
500
501 if let Err(e) = ndjson_reader.validate_complete() {
503 let _ = result_tx.blocking_send(Err(
504 arrow::error::ArrowError::JsonError(e.to_string()),
505 ));
506 }
507 });
509
510 let stream = JsonArrayStream {
512 inner: ReceiverStream::new(result_rx),
513 _read_task: read_task,
514 _parse_task: parse_task,
515 };
516
517 Ok(stream.map(|r| r.map_err(Into::into)).boxed())
518 }
519 }
520 }
521 }))
522 }
523}
524
525pub async fn plan_to_json(
526 task_ctx: Arc<TaskContext>,
527 plan: Arc<dyn ExecutionPlan>,
528 path: impl AsRef<str>,
529) -> Result<()> {
530 let path = path.as_ref();
531 let parsed = ListingTableUrl::parse(path)?;
532 let object_store_url = parsed.object_store();
533 let store = task_ctx.runtime_env().object_store(&object_store_url)?;
534 let writer_buffer_size = task_ctx
535 .session_config()
536 .options()
537 .execution
538 .objectstore_writer_buffer_size;
539 let mut join_set = JoinSet::new();
540 for i in 0..plan.output_partitioning().partition_count() {
541 let storeref = Arc::clone(&store);
542 let plan: Arc<dyn ExecutionPlan> = Arc::clone(&plan);
543 let filename = format!("{}/part-{i}.json", parsed.prefix());
544 let file = object_store::path::Path::parse(filename)?;
545
546 let mut stream = plan.execute(i, Arc::clone(&task_ctx))?;
547 join_set.spawn(async move {
548 let mut buf_writer =
549 BufWriter::with_capacity(storeref, file.clone(), writer_buffer_size);
550
551 let mut buffer = Vec::with_capacity(1024);
552 while let Some(batch) = stream.next().await.transpose()? {
553 let mut writer = json::LineDelimitedWriter::new(buffer);
554 writer.write(&batch)?;
555 buffer = writer.into_inner();
556 buf_writer.write_all(&buffer).await?;
557 buffer.clear();
558 }
559
560 buf_writer.shutdown().await.map_err(DataFusionError::from)
561 });
562 }
563
564 while let Some(result) = join_set.join_next().await {
565 match result {
566 Ok(res) => res?, Err(e) => {
568 if e.is_panic() {
569 std::panic::resume_unwind(e.into_panic());
570 } else {
571 unreachable!();
572 }
573 }
574 }
575 }
576
577 Ok(())
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583 use crate::test_utils::{CHUNK_SIZES, make_chunked_store};
584 use arrow::array::{Int64Array, StringArray};
585 use arrow::compute;
586 use arrow::datatypes::{DataType, Field, Schema};
587 use arrow::record_batch::RecordBatch;
588 use bytes::Bytes;
589 use datafusion_datasource::FileRange;
590 use object_store::memory::InMemory;
591 use object_store::path::Path;
592 use object_store::{ObjectStoreExt, PutPayload};
593
594 fn test_schema() -> SchemaRef {
596 Arc::new(Schema::new(vec![
597 Field::new("id", DataType::Int64, true),
598 Field::new("name", DataType::Utf8, true),
599 ]))
600 }
601
602 #[tokio::test]
603 async fn test_json_array_from_file() -> Result<()> {
604 let json_data = r#"[{"id": 1, "name": "alice"}, {"id": 2, "name": "bob"}]"#;
606
607 let store = Arc::new(InMemory::new());
608 let path = Path::from("test.json");
609 store
610 .put(&path, PutPayload::from_static(json_data.as_bytes()))
611 .await?;
612
613 let opener = JsonOpener::new(
614 1024,
615 test_schema(),
616 FileCompressionType::UNCOMPRESSED,
617 store.clone(),
618 false, );
620
621 let meta = store.head(&path).await?;
622 let file = PartitionedFile::new(path.to_string(), meta.size);
623
624 let stream = opener.open(file)?.await?;
625 let batches: Vec<_> = stream.try_collect().await?;
626
627 assert_eq!(batches.len(), 1);
628 assert_eq!(batches[0].num_rows(), 2);
629
630 Ok(())
631 }
632
633 #[tokio::test]
634 async fn test_json_array_from_stream() -> Result<()> {
635 let json_data = r#"[{"id": 1, "name": "alice"}, {"id": 2, "name": "bob"}, {"id": 3, "name": "charlie"}]"#;
637
638 let store = Arc::new(InMemory::new());
640 let path = Path::from("test_stream.json");
641 store
642 .put(&path, PutPayload::from_static(json_data.as_bytes()))
643 .await?;
644
645 let opener = JsonOpener::new(
646 2, test_schema(),
648 FileCompressionType::UNCOMPRESSED,
649 store.clone(),
650 false, );
652
653 let meta = store.head(&path).await?;
654 let file = PartitionedFile::new(path.to_string(), meta.size);
655
656 let stream = opener.open(file)?.await?;
657 let batches: Vec<_> = stream.try_collect().await?;
658
659 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
660 assert_eq!(total_rows, 3);
661
662 Ok(())
663 }
664
665 #[tokio::test]
666 async fn test_json_array_nested_objects() -> Result<()> {
667 let schema = Arc::new(Schema::new(vec![
669 Field::new("id", DataType::Int64, true),
670 Field::new("data", DataType::Utf8, true),
671 ]));
672
673 let json_data = r#"[
674 {"id": 1, "data": "{\"nested\": true}"},
675 {"id": 2, "data": "[1, 2, 3]"}
676 ]"#;
677
678 let store = Arc::new(InMemory::new());
679 let path = Path::from("nested.json");
680 store
681 .put(&path, PutPayload::from_static(json_data.as_bytes()))
682 .await?;
683
684 let opener = JsonOpener::new(
685 1024,
686 schema,
687 FileCompressionType::UNCOMPRESSED,
688 store.clone(),
689 false,
690 );
691
692 let meta = store.head(&path).await?;
693 let file = PartitionedFile::new(path.to_string(), meta.size);
694
695 let stream = opener.open(file)?.await?;
696 let batches: Vec<_> = stream.try_collect().await?;
697
698 assert_eq!(batches[0].num_rows(), 2);
699
700 Ok(())
701 }
702
703 #[tokio::test]
704 async fn test_json_array_empty() -> Result<()> {
705 let json_data = "[]";
707
708 let store = Arc::new(InMemory::new());
709 let path = Path::from("empty.json");
710 store
711 .put(&path, PutPayload::from_static(json_data.as_bytes()))
712 .await?;
713
714 let opener = JsonOpener::new(
715 1024,
716 test_schema(),
717 FileCompressionType::UNCOMPRESSED,
718 store.clone(),
719 false,
720 );
721
722 let meta = store.head(&path).await?;
723 let file = PartitionedFile::new(path.to_string(), meta.size);
724
725 let stream = opener.open(file)?.await?;
726 let batches: Vec<_> = stream.try_collect().await?;
727
728 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
729 assert_eq!(total_rows, 0);
730
731 Ok(())
732 }
733
734 #[tokio::test]
735 async fn test_json_array_range_not_supported() {
736 let store = Arc::new(InMemory::new());
738 let path = Path::from("test.json");
739 store
740 .put(&path, PutPayload::from_static(b"[]"))
741 .await
742 .unwrap();
743
744 let opener = JsonOpener::new(
745 1024,
746 test_schema(),
747 FileCompressionType::UNCOMPRESSED,
748 store.clone(),
749 false, );
751
752 let meta = store.head(&path).await.unwrap();
753 let mut file = PartitionedFile::new(path.to_string(), meta.size);
754 file.range = Some(FileRange { start: 0, end: 10 });
755
756 let result = opener.open(file);
757 match result {
758 Ok(_) => panic!("Expected error for range-based JSON array scanning"),
759 Err(e) => {
760 assert!(
761 e.to_string().contains("does not support range-based"),
762 "Unexpected error message: {e}"
763 );
764 }
765 }
766 }
767
768 #[tokio::test]
769 async fn test_ndjson_still_works() -> Result<()> {
770 let json_data =
772 "{\"id\": 1, \"name\": \"alice\"}\n{\"id\": 2, \"name\": \"bob\"}\n";
773
774 let store = Arc::new(InMemory::new());
775 let path = Path::from("test.ndjson");
776 store
777 .put(&path, PutPayload::from_static(json_data.as_bytes()))
778 .await?;
779
780 let opener = JsonOpener::new(
781 1024,
782 test_schema(),
783 FileCompressionType::UNCOMPRESSED,
784 store.clone(),
785 true, );
787
788 let meta = store.head(&path).await?;
789 let file = PartitionedFile::new(path.to_string(), meta.size);
790
791 let stream = opener.open(file)?.await?;
792 let batches: Vec<_> = stream.try_collect().await?;
793
794 assert_eq!(batches.len(), 1);
795 assert_eq!(batches[0].num_rows(), 2);
796
797 Ok(())
798 }
799
800 #[tokio::test]
801 async fn test_json_array_large_file() -> Result<()> {
802 let mut json_data = String::from("[");
804 for i in 0..1000 {
805 if i > 0 {
806 json_data.push(',');
807 }
808 json_data.push_str(&format!(r#"{{"id": {i}, "name": "user{i}"}}"#));
809 }
810 json_data.push(']');
811
812 let store = Arc::new(InMemory::new());
813 let path = Path::from("large.json");
814 store
815 .put(&path, PutPayload::from(Bytes::from(json_data)))
816 .await?;
817
818 let opener = JsonOpener::new(
819 100, test_schema(),
821 FileCompressionType::UNCOMPRESSED,
822 store.clone(),
823 false,
824 );
825
826 let meta = store.head(&path).await?;
827 let file = PartitionedFile::new(path.to_string(), meta.size);
828
829 let stream = opener.open(file)?.await?;
830 let batches: Vec<_> = stream.try_collect().await?;
831
832 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
833 assert_eq!(total_rows, 1000);
834
835 assert!(batches.len() >= 10);
837
838 Ok(())
839 }
840
841 #[tokio::test]
842 async fn test_json_array_stream_cancellation() -> Result<()> {
843 let mut json_data = String::from("[");
845 for i in 0..10000 {
846 if i > 0 {
847 json_data.push(',');
848 }
849 json_data.push_str(&format!(r#"{{"id": {i}, "name": "user{i}"}}"#));
850 }
851 json_data.push(']');
852
853 let store = Arc::new(InMemory::new());
854 let path = Path::from("cancel_test.json");
855 store
856 .put(&path, PutPayload::from(Bytes::from(json_data)))
857 .await?;
858
859 let opener = JsonOpener::new(
860 10, test_schema(),
862 FileCompressionType::UNCOMPRESSED,
863 store.clone(),
864 false,
865 );
866
867 let meta = store.head(&path).await?;
868 let file = PartitionedFile::new(path.to_string(), meta.size);
869
870 let mut stream = opener.open(file)?.await?;
871
872 let first_batch = stream.next().await;
874 assert!(first_batch.is_some());
875
876 drop(stream);
878
879 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
881
882 Ok(())
884 }
885
886 fn get_partition_splits() -> Vec<usize> {
887 vec![1usize, 2, 3, 5, 7, 10]
888 }
889
890 async fn collect_partitioned_batches(
893 store: Arc<dyn ObjectStore>,
894 path: &Path,
895 file_size: u64,
896 num_partitions: usize,
897 ) -> Result<Vec<RecordBatch>> {
898 let mut all_batches = Vec::new();
899 for p in 0..num_partitions {
900 let start = (p as u64 * file_size) / num_partitions as u64;
901 let end = ((p as u64 + 1) * file_size) / num_partitions as u64;
902
903 let meta = store.head(path).await?;
904 let mut file = PartitionedFile::new(path.to_string(), meta.size);
905 file.range = Some(FileRange {
906 start: start as i64,
907 end: end as i64,
908 });
909
910 let opener = JsonOpener::new(
911 1024,
912 test_schema(),
913 FileCompressionType::UNCOMPRESSED,
914 Arc::clone(&store),
915 true,
916 );
917
918 let stream = opener.open(file)?.await?;
919 let batches: Vec<_> = stream.try_collect().await?;
920 all_batches.extend(batches);
921 }
922 Ok(all_batches)
923 }
924
925 fn concat_and_sort_by_id(batches: &[RecordBatch]) -> Result<RecordBatch> {
928 let schema = test_schema();
929 let combined = compute::concat_batches(&schema, batches)?;
930 let indices = compute::sort_to_indices(combined.column(0), None, None)?;
931 let sorted_cols: Vec<_> = combined
932 .columns()
933 .iter()
934 .map(|col| compute::take(col.as_ref(), &indices, None))
935 .collect::<std::result::Result<_, _>>()?;
936 Ok(RecordBatch::try_new(schema, sorted_cols)?)
937 }
938
939 #[tokio::test]
940 async fn test_ndjson_partitioned() -> Result<()> {
941 let num_rows: usize = 20;
943 let mut ndjson = String::new();
944 for i in 0..num_rows {
945 ndjson.push_str(&format!("{{\"id\": {i}, \"name\": \"user{i}\"}}\n"));
946 }
947 let ndjson_bytes = Bytes::from(ndjson);
948 let file_size = ndjson_bytes.len() as u64;
949
950 for &cs in CHUNK_SIZES {
951 let (store, path) = make_chunked_store(&ndjson_bytes, cs).await;
952
953 for num_partitions in get_partition_splits() {
954 let batches = collect_partitioned_batches(
955 Arc::clone(&store),
956 &path,
957 file_size,
958 num_partitions,
959 )
960 .await?;
961
962 let total: usize = batches.iter().map(|b| b.num_rows()).sum();
963 assert_eq!(
964 total, num_rows,
965 "Expected {num_rows} rows with {num_partitions} partitions"
966 );
967
968 let result = concat_and_sort_by_id(&batches)?;
969 let ids = result
970 .column(0)
971 .as_any()
972 .downcast_ref::<Int64Array>()
973 .unwrap();
974 let names = result
975 .column(1)
976 .as_any()
977 .downcast_ref::<StringArray>()
978 .unwrap();
979 for i in 0..num_rows {
980 assert_eq!(
981 ids.value(i),
982 i as i64,
983 "id mismatch at row {i} with {num_partitions} partitions"
984 );
985 assert_eq!(
986 names.value(i),
987 format!("user{i}"),
988 "name mismatch at row {i} with {num_partitions} partitions"
989 );
990 }
991 }
992 }
993
994 Ok(())
995 }
996
997 #[tokio::test]
998 async fn test_ndjson_partitioned_uneven_lines() -> Result<()> {
999 let rows: &[(&str, &str)] = &[
1002 ("1", "alice"),
1003 ("2", "bob-with-a-longer-name"),
1004 ("3", "charlie"),
1005 ("4", "x"),
1006 ("5", "diana-has-an-even-longer-name-here"),
1007 ("6", "ed"),
1008 ("7", "francesca"),
1009 ("8", "g"),
1010 ("9", "hector-the-magnificent"),
1011 ("10", "isabella"),
1012 ];
1013 let num_rows = rows.len();
1014
1015 let mut ndjson = String::new();
1016 for (id, name) in rows {
1017 ndjson.push_str(&format!("{{\"id\": {id}, \"name\": \"{name}\"}}\n"));
1018 }
1019 let ndjson_bytes = Bytes::from(ndjson);
1020 let file_size = ndjson_bytes.len() as u64;
1021
1022 for &cs in CHUNK_SIZES {
1023 let (store, path) = make_chunked_store(&ndjson_bytes, cs).await;
1024
1025 for num_partitions in get_partition_splits() {
1026 let batches = collect_partitioned_batches(
1027 Arc::clone(&store),
1028 &path,
1029 file_size,
1030 num_partitions,
1031 )
1032 .await?;
1033
1034 let total: usize = batches.iter().map(|b| b.num_rows()).sum();
1035 assert_eq!(
1036 total, num_rows,
1037 "Expected {num_rows} rows with {num_partitions} partitions"
1038 );
1039
1040 let result = concat_and_sort_by_id(&batches)?;
1041 let ids = result
1042 .column(0)
1043 .as_any()
1044 .downcast_ref::<Int64Array>()
1045 .unwrap();
1046 let names = result
1047 .column(1)
1048 .as_any()
1049 .downcast_ref::<StringArray>()
1050 .unwrap();
1051 for (i, (expected_id, expected_name)) in rows.iter().enumerate() {
1052 assert_eq!(
1053 ids.value(i),
1054 expected_id.parse::<i64>().unwrap(),
1055 "id mismatch at row {i} with {num_partitions} partitions"
1056 );
1057 assert_eq!(
1058 names.value(i),
1059 *expected_name,
1060 "name mismatch at row {i} with {num_partitions} partitions"
1061 );
1062 }
1063 }
1064 }
1065
1066 Ok(())
1067 }
1068
1069 #[tokio::test]
1070 async fn test_ndjson_partitioned_single_entry() -> Result<()> {
1071 let ndjson = r#"{"id": 1, "name": "alice"}"#;
1075 let ndjson_bytes = Bytes::from(ndjson);
1076 let file_size = ndjson_bytes.len() as u64;
1077
1078 for &cs in CHUNK_SIZES {
1079 let (store, path) = make_chunked_store(&ndjson_bytes, cs).await;
1080
1081 for num_partitions in get_partition_splits() {
1082 let batches = collect_partitioned_batches(
1083 Arc::clone(&store),
1084 &path,
1085 file_size,
1086 num_partitions,
1087 )
1088 .await?;
1089
1090 let total: usize = batches.iter().map(|b| b.num_rows()).sum();
1091 assert_eq!(
1092 total, 1,
1093 "Expected exactly 1 row with {num_partitions} partitions"
1094 );
1095
1096 let result = concat_and_sort_by_id(&batches)?;
1097 let ids = result
1098 .column(0)
1099 .as_any()
1100 .downcast_ref::<Int64Array>()
1101 .unwrap();
1102 let names = result
1103 .column(1)
1104 .as_any()
1105 .downcast_ref::<StringArray>()
1106 .unwrap();
1107 assert_eq!(ids.value(0), 1);
1108 assert_eq!(names.value(0), "alice");
1109 }
1110 }
1111
1112 Ok(())
1113 }
1114}