Skip to content

Commit 1491ee0

Browse files
authored
fix: support missing columns in Mosaic reads (#389)
1 parent 4822696 commit 1491ee0

2 files changed

Lines changed: 343 additions & 51 deletions

File tree

crates/paimon/src/arrow/format/mosaic.rs

Lines changed: 161 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use async_trait::async_trait;
2828
use bytes::Bytes;
2929
use futures::StreamExt;
3030
use paimon_mosaic_core::reader::{InputFile, MosaicReader, ReaderAccess};
31+
use std::collections::HashSet;
3132
use std::io;
3233

3334
pub(crate) struct MosaicFormatReader;
@@ -46,16 +47,28 @@ impl FormatFileReader for MosaicFormatReader {
4647
row_selection: Option<Vec<RowRange>>,
4748
) -> crate::Result<ArrowRecordBatchStream> {
4849
// Mosaic predicates are currently residual; callers must re-check them for exact filtering.
49-
let target_schema = build_target_arrow_schema(read_fields)?;
50-
validate_mosaic_schema(&target_schema)?;
51-
5250
let file_bytes = reader.read(0..file_size).await?;
5351
let mosaic_reader = MosaicReader::new(MemoryInputFile::new(file_bytes), file_size)
5452
.map_err(mosaic_read_error)?;
55-
let projected_names = read_fields
53+
54+
let file_column_names = mosaic_reader
55+
.schema()
56+
.columns
57+
.iter()
58+
.map(|column| column.name.as_str())
59+
.collect::<HashSet<_>>();
60+
let existing_read_fields = read_fields
61+
.iter()
62+
.filter(|field| file_column_names.contains(field.name()))
63+
.cloned()
64+
.collect::<Vec<_>>();
65+
let read_schema = build_target_arrow_schema(&existing_read_fields)?;
66+
validate_mosaic_schema(&read_schema)?;
67+
let projected_names = existing_read_fields
5668
.iter()
5769
.map(|field| field.name().to_string())
5870
.collect::<Vec<_>>();
71+
let all_projected_columns_missing = !read_fields.is_empty() && projected_names.is_empty();
5972
let batch_size = batch_size.unwrap_or(DEFAULT_BATCH_SIZE);
6073

6174
Ok(try_stream! {
@@ -82,24 +95,25 @@ impl FormatFileReader for MosaicFormatReader {
8295
}
8396
}
8497

85-
let mut row_group_reader = if projected_names.is_empty() {
86-
mosaic_reader
87-
.row_group_reader_by_names(row_group_index, &[])
88-
.map_err(mosaic_read_error)?
98+
let batch = if all_projected_columns_missing {
99+
let row_count = selected_indices
100+
.as_ref()
101+
.map_or(row_group_rows, UInt64Array::len);
102+
empty_batch(read_schema.clone(), row_count)?
89103
} else {
90104
let names = projected_names
91105
.iter()
92106
.map(String::as_str)
93107
.collect::<Vec<_>>();
94-
mosaic_reader
108+
let mut row_group_reader = mosaic_reader
95109
.row_group_reader_by_names(row_group_index, &names)
96-
.map_err(mosaic_read_error)?
97-
};
110+
.map_err(mosaic_read_error)?;
98111

99-
let batch = row_group_reader
100-
.read_columns()
101-
.map_err(mosaic_read_error)?;
102-
let batch = take_rows(batch, selected_indices.as_ref(), &target_schema)?;
112+
let batch = row_group_reader
113+
.read_columns()
114+
.map_err(mosaic_read_error)?;
115+
take_rows(batch, selected_indices.as_ref(), &read_schema)?
116+
};
103117
for chunk in split_batch(batch, batch_size) {
104118
yield chunk;
105119
}
@@ -251,15 +265,7 @@ fn take_rows(
251265
};
252266

253267
if batch.num_columns() == 0 {
254-
return RecordBatch::try_new_with_options(
255-
target_schema.clone(),
256-
Vec::new(),
257-
&RecordBatchOptions::new().with_row_count(Some(indices.len())),
258-
)
259-
.map_err(|e| Error::UnexpectedError {
260-
message: format!("Failed to build empty Mosaic RecordBatch: {e}"),
261-
source: Some(Box::new(e)),
262-
});
268+
return empty_batch(target_schema.clone(), indices.len());
263269
}
264270

265271
let columns = batch
@@ -287,15 +293,7 @@ fn ensure_schema(batch: RecordBatch, target_schema: &SchemaRef) -> crate::Result
287293
}
288294

289295
if batch.num_columns() == 0 {
290-
return RecordBatch::try_new_with_options(
291-
target_schema.clone(),
292-
Vec::new(),
293-
&RecordBatchOptions::new().with_row_count(Some(batch.num_rows())),
294-
)
295-
.map_err(|e| Error::UnexpectedError {
296-
message: format!("Failed to build empty Mosaic RecordBatch: {e}"),
297-
source: Some(Box::new(e)),
298-
});
296+
return empty_batch(target_schema.clone(), batch.num_rows());
299297
}
300298

301299
RecordBatch::try_new(target_schema.clone(), batch.columns().to_vec()).map_err(|e| {
@@ -306,6 +304,18 @@ fn ensure_schema(batch: RecordBatch, target_schema: &SchemaRef) -> crate::Result
306304
})
307305
}
308306

307+
fn empty_batch(schema: SchemaRef, row_count: usize) -> crate::Result<RecordBatch> {
308+
RecordBatch::try_new_with_options(
309+
schema,
310+
Vec::new(),
311+
&RecordBatchOptions::new().with_row_count(Some(row_count)),
312+
)
313+
.map_err(|e| Error::UnexpectedError {
314+
message: format!("Failed to build empty Mosaic RecordBatch: {e}"),
315+
source: Some(Box::new(e)),
316+
})
317+
}
318+
309319
fn split_batch(batch: RecordBatch, batch_size: usize) -> Vec<RecordBatch> {
310320
if batch_size == 0 || batch.num_rows() <= batch_size {
311321
return vec![batch];
@@ -400,6 +410,10 @@ mod tests {
400410
]
401411
}
402412

413+
fn field(id: i32, name: &str, data_type: DataType) -> DataField {
414+
DataField::new(id, name.to_string(), data_type)
415+
}
416+
403417
fn arrow_schema() -> SchemaRef {
404418
Arc::new(Schema::new(vec![
405419
Field::new("id", ArrowDataType::Int32, false),
@@ -526,32 +540,128 @@ mod tests {
526540
}
527541

528542
#[tokio::test]
529-
async fn test_unsupported_type_returns_error() {
530-
let unsupported = vec![DataField::new(
543+
async fn test_read_projection_with_missing_column() {
544+
let fields = data_fields();
545+
let projected = vec![
546+
fields[0].clone(),
547+
field(3, "new_score", DataType::Int(IntType::with_nullable(true))),
548+
fields[1].clone(),
549+
];
550+
let data = write_mosaic(&sample_batch());
551+
let batches = read_batches(data, &projected, None).await.unwrap();
552+
553+
assert_eq!(batches.len(), 1);
554+
assert_eq!(batches[0].num_rows(), 5);
555+
assert_eq!(batches[0].num_columns(), 2);
556+
assert_eq!(batches[0].schema().field(0).name(), "id");
557+
assert_eq!(batches[0].schema().field(1).name(), "name");
558+
let ids = batches[0]
559+
.column(0)
560+
.as_any()
561+
.downcast_ref::<Int32Array>()
562+
.unwrap();
563+
assert_eq!(ids.values(), &[1, 2, 3, 4, 5]);
564+
}
565+
566+
#[tokio::test]
567+
async fn test_read_projection_with_missing_unsupported_column() {
568+
let fields = data_fields();
569+
let projected = vec![
570+
fields[0].clone(),
571+
field(
572+
3,
573+
"new_items",
574+
DataType::Array(ArrayType::new(DataType::Int(IntType::new()))),
575+
),
576+
];
577+
let data = write_mosaic(&sample_batch());
578+
let batches = read_batches(data, &projected, None).await.unwrap();
579+
580+
assert_eq!(batches.len(), 1);
581+
assert_eq!(batches[0].num_rows(), 5);
582+
assert_eq!(batches[0].num_columns(), 1);
583+
assert_eq!(batches[0].schema().field(0).name(), "id");
584+
}
585+
586+
#[tokio::test]
587+
async fn test_read_projection_with_existing_unsupported_column_returns_error() {
588+
let projected = vec![field(
531589
0,
532-
"items".to_string(),
590+
"id",
533591
DataType::Array(ArrayType::new(DataType::Int(IntType::new()))),
534592
)];
535-
let result = MosaicFormatReader
536-
.read_batch_stream(
537-
Box::new(TestFileRead { data: Bytes::new() }),
538-
0,
539-
&unsupported,
540-
None,
541-
None,
542-
None,
543-
)
544-
.await;
545-
let err = match result {
546-
Ok(_) => panic!("expected unsupported Mosaic type error"),
547-
Err(err) => err,
548-
};
593+
let data = write_mosaic(&sample_batch());
594+
let err = read_batches(data, &projected, None).await.unwrap_err();
549595

550596
assert!(
551-
matches!(err, Error::Unsupported { message } if message.contains("Mosaic format does not support column 'items'"))
597+
matches!(err, Error::Unsupported { message } if message.contains("Mosaic format does not support column 'id'"))
552598
);
553599
}
554600

601+
#[tokio::test]
602+
async fn test_read_projection_all_columns_missing() {
603+
let projected = vec![
604+
field(3, "new_score", DataType::Int(IntType::with_nullable(true))),
605+
field(
606+
4,
607+
"new_name",
608+
DataType::VarChar(VarCharType::with_nullable(true, 20).unwrap()),
609+
),
610+
];
611+
let data = write_mosaic(&sample_batch());
612+
let batches = read_batches(data, &projected, None).await.unwrap();
613+
614+
assert_eq!(batches.len(), 1);
615+
assert_eq!(batches[0].num_rows(), 5);
616+
assert_eq!(batches[0].num_columns(), 0);
617+
assert!(batches[0].schema().fields().is_empty());
618+
}
619+
620+
#[tokio::test]
621+
async fn test_read_projection_all_columns_missing_with_row_selection() {
622+
let projected = vec![field(
623+
3,
624+
"new_score",
625+
DataType::Int(IntType::with_nullable(true)),
626+
)];
627+
let data = write_mosaic(&sample_batch());
628+
let batches = read_batches(data, &projected, Some(vec![RowRange::new(1, 3)]))
629+
.await
630+
.unwrap();
631+
632+
assert_eq!(batches.len(), 1);
633+
assert_eq!(batches[0].num_rows(), 3);
634+
assert_eq!(batches[0].num_columns(), 0);
635+
}
636+
637+
#[tokio::test]
638+
async fn test_read_projection_with_missing_column_and_row_selection() {
639+
let fields = data_fields();
640+
let projected = vec![
641+
fields[2].clone(),
642+
field(3, "new_id", DataType::Int(IntType::with_nullable(true))),
643+
];
644+
let data = write_mosaic(&sample_batch());
645+
let batches = read_batches(
646+
data,
647+
&projected,
648+
Some(vec![RowRange::new(0, 1), RowRange::new(4, 4)]),
649+
)
650+
.await
651+
.unwrap();
652+
653+
assert_eq!(batches.len(), 1);
654+
assert_eq!(batches[0].num_rows(), 3);
655+
assert_eq!(batches[0].num_columns(), 1);
656+
assert_eq!(batches[0].schema().field(0).name(), "score");
657+
let scores = batches[0]
658+
.column(0)
659+
.as_any()
660+
.downcast_ref::<Int32Array>()
661+
.unwrap();
662+
assert_eq!(scores.values(), &[10, 20, 50]);
663+
}
664+
555665
#[test]
556666
fn test_validate_row_type_as_unsupported() {
557667
let unsupported = vec![DataField::new(

0 commit comments

Comments
 (0)