This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 2c6eca0  test: cover Mosaic read path combinations (#401)
2c6eca0 is described below

commit 2c6eca0c955906d97068465ff24208360b888935
Author: QuakeWang <[email protected]>
AuthorDate: Mon Jun 22 17:18:57 2026 +0800

    test: cover Mosaic read path combinations (#401)
---
 crates/paimon/src/arrow/format/mosaic.rs    | 461 +++++++++++++++++++++++++++-
 crates/paimon/src/table/data_file_reader.rs | 301 +++++++++++++++++-
 2 files changed, 757 insertions(+), 5 deletions(-)

diff --git a/crates/paimon/src/arrow/format/mosaic.rs 
b/crates/paimon/src/arrow/format/mosaic.rs
index f631fad..963226b 100644
--- a/crates/paimon/src/arrow/format/mosaic.rs
+++ b/crates/paimon/src/arrow/format/mosaic.rs
@@ -532,10 +532,15 @@ mod tests {
     use super::*;
     use crate::arrow::format::{FilePredicates, FormatFileReader};
     use crate::spec::{
-        ArrayType, DataType, Datum, IntType, Predicate, PredicateBuilder, 
RowType, TimestampType,
-        VarCharType,
+        ArrayType, BigIntType, BooleanType, DataType, DateType, Datum, 
DecimalType, DoubleType,
+        FloatType, IntType, LocalZonedTimestampType, Predicate, 
PredicateBuilder, RowType,
+        SmallIntType, TimeType, TimestampType, TinyIntType, VarBinaryType, 
VarCharType,
+    };
+    use arrow_array::{
+        Array, BinaryArray, BooleanArray, Date32Array, Decimal128Array, 
Float32Array, Float64Array,
+        Int16Array, Int32Array, Int64Array, Int8Array, StringArray, 
Time32MillisecondArray,
+        TimestampMicrosecondArray, TimestampMillisecondArray, 
TimestampNanosecondArray,
     };
-    use arrow_array::{Array, Int32Array, StringArray, 
TimestampMicrosecondArray};
     use arrow_schema::{DataType as ArrowDataType, Field, Schema};
     use bytes::Bytes;
     use futures::TryStreamExt;
@@ -1154,4 +1159,454 @@ mod tests {
             matches!(err, Error::Unsupported { message } if 
message.contains("Mosaic format does not support column 'nested'"))
         );
     }
+
+    fn full_type_fields() -> Vec<DataField> {
+        vec![
+            field(
+                0,
+                "f_bool",
+                DataType::Boolean(BooleanType::with_nullable(true)),
+            ),
+            field(
+                1,
+                "f_tinyint",
+                DataType::TinyInt(TinyIntType::with_nullable(true)),
+            ),
+            field(
+                2,
+                "f_smallint",
+                DataType::SmallInt(SmallIntType::with_nullable(true)),
+            ),
+            field(3, "f_int", DataType::Int(IntType::with_nullable(false))),
+            field(
+                4,
+                "f_bigint",
+                DataType::BigInt(BigIntType::with_nullable(true)),
+            ),
+            field(
+                5,
+                "f_float",
+                DataType::Float(FloatType::with_nullable(true)),
+            ),
+            field(
+                6,
+                "f_double",
+                DataType::Double(DoubleType::with_nullable(true)),
+            ),
+            field(7, "f_date", DataType::Date(DateType::with_nullable(true))),
+            field(
+                8,
+                "f_time",
+                DataType::Time(TimeType::with_nullable(true, 3).unwrap()),
+            ),
+            field(
+                9,
+                "f_string",
+                DataType::VarChar(VarCharType::with_nullable(true, 
20).unwrap()),
+            ),
+            field(
+                10,
+                "f_binary",
+                DataType::VarBinary(VarBinaryType::try_new(true, 20).unwrap()),
+            ),
+            field(
+                11,
+                "f_decimal_compact",
+                DataType::Decimal(DecimalType::with_nullable(true, 5, 
2).unwrap()),
+            ),
+            field(
+                12,
+                "f_decimal_large",
+                DataType::Decimal(DecimalType::with_nullable(true, 20, 
0).unwrap()),
+            ),
+            field(
+                13,
+                "f_ts3",
+                DataType::Timestamp(TimestampType::with_nullable(true, 
3).unwrap()),
+            ),
+            field(
+                14,
+                "f_ts6",
+                DataType::Timestamp(TimestampType::with_nullable(true, 
6).unwrap()),
+            ),
+            field(
+                15,
+                "f_ts9",
+                DataType::Timestamp(TimestampType::with_nullable(true, 
9).unwrap()),
+            ),
+            field(
+                16,
+                "f_ltz",
+                DataType::LocalZonedTimestamp(
+                    LocalZonedTimestampType::with_nullable(true, 6).unwrap(),
+                ),
+            ),
+        ]
+    }
+
+    /// Round-trips every scalar/temporal type Mosaic supports through write + 
read,
+    /// asserting values survive the format. ARRAY/MAP are intentionally 
excluded:
+    /// `paimon-mosaic-core` 0.1.0 does not support them and the reader 
rejects them.
+    #[tokio::test]
+    async fn test_read_full_types() {
+        let fields = full_type_fields();
+        let schema = build_target_arrow_schema(&fields).unwrap();
+        let batch = RecordBatch::try_new(
+            schema,
+            vec![
+                Arc::new(BooleanArray::from(vec![Some(true), Some(false)])),
+                Arc::new(Int8Array::from(vec![Some(1i8), Some(-2)])),
+                Arc::new(Int16Array::from(vec![Some(100i16), Some(-200)])),
+                Arc::new(Int32Array::from(vec![10, 20])),
+                Arc::new(Int64Array::from(vec![Some(1_000i64), Some(-2_000)])),
+                Arc::new(Float32Array::from(vec![Some(1.5f32), Some(-2.5)])),
+                Arc::new(Float64Array::from(vec![Some(3.25f64), Some(-4.75)])),
+                Arc::new(Date32Array::from(vec![Some(18_000), Some(19_000)])),
+                Arc::new(Time32MillisecondArray::from(vec![
+                    Some(3_600_000),
+                    Some(7_200_000),
+                ])),
+                Arc::new(StringArray::from(vec![Some("hello"), 
Some("mosaic")])),
+                Arc::new(BinaryArray::from_opt_vec(vec![
+                    Some(b"ab".as_ref()),
+                    Some(b"cd".as_ref()),
+                ])),
+                Arc::new(
+                    Decimal128Array::from(vec![Some(12_345i128), Some(-678)])
+                        .with_precision_and_scale(5, 2)
+                        .unwrap(),
+                ),
+                Arc::new(
+                    
Decimal128Array::from(vec![Some(12_345_678_901_234_567_890i128), Some(-1)])
+                        .with_precision_and_scale(20, 0)
+                        .unwrap(),
+                ),
+                Arc::new(TimestampMillisecondArray::from(vec![
+                    Some(1_700_000_000_000i64),
+                    Some(-1),
+                ])),
+                Arc::new(TimestampMicrosecondArray::from(vec![
+                    Some(1_700_000_000_000_000i64),
+                    Some(-1),
+                ])),
+                Arc::new(TimestampNanosecondArray::from(vec![
+                    Some(1_700_000_000_123_456_789i64),
+                    Some(-1),
+                ])),
+                Arc::new(
+                    
TimestampMicrosecondArray::from(vec![Some(1_700_000_000_000_000i64), Some(-1)])
+                        .with_timezone("UTC"),
+                ),
+            ],
+        )
+        .unwrap();
+
+        let data = write_mosaic(&batch);
+        let batches = read_batches(data, &fields, None).await.unwrap();
+
+        assert_eq!(batches.len(), 1);
+        let result = &batches[0];
+        assert_eq!(result.num_rows(), 2);
+        assert_eq!(result.num_columns(), fields.len());
+
+        let col = |i: usize| result.column(i);
+        let downcast = |i: usize| col(i).as_any();
+
+        assert!(downcast(0).downcast_ref::<BooleanArray>().unwrap().value(0));
+        assert!(!downcast(0).downcast_ref::<BooleanArray>().unwrap().value(1));
+        assert_eq!(
+            downcast(1).downcast_ref::<Int8Array>().unwrap().value(1),
+            -2
+        );
+        assert_eq!(
+            downcast(2).downcast_ref::<Int16Array>().unwrap().value(0),
+            100
+        );
+        assert_eq!(
+            downcast(3).downcast_ref::<Int32Array>().unwrap().values(),
+            &[10, 20]
+        );
+        assert_eq!(
+            downcast(4).downcast_ref::<Int64Array>().unwrap().value(1),
+            -2_000
+        );
+        assert_eq!(
+            downcast(5).downcast_ref::<Float32Array>().unwrap().value(0),
+            1.5
+        );
+        assert_eq!(
+            downcast(6).downcast_ref::<Float64Array>().unwrap().value(1),
+            -4.75
+        );
+        assert_eq!(
+            downcast(7).downcast_ref::<Date32Array>().unwrap().value(0),
+            18_000
+        );
+        assert_eq!(
+            downcast(8)
+                .downcast_ref::<Time32MillisecondArray>()
+                .unwrap()
+                .value(0),
+            3_600_000
+        );
+        assert_eq!(
+            downcast(9).downcast_ref::<StringArray>().unwrap().value(1),
+            "mosaic"
+        );
+        assert_eq!(
+            downcast(10).downcast_ref::<BinaryArray>().unwrap().value(0),
+            b"ab"
+        );
+        assert_eq!(
+            downcast(11)
+                .downcast_ref::<Decimal128Array>()
+                .unwrap()
+                .value(0),
+            12_345
+        );
+        assert_eq!(
+            downcast(12)
+                .downcast_ref::<Decimal128Array>()
+                .unwrap()
+                .value(0),
+            12_345_678_901_234_567_890
+        );
+        assert_eq!(
+            downcast(13)
+                .downcast_ref::<TimestampMillisecondArray>()
+                .unwrap()
+                .value(0),
+            1_700_000_000_000
+        );
+        assert_eq!(
+            downcast(14)
+                .downcast_ref::<TimestampMicrosecondArray>()
+                .unwrap()
+                .value(0),
+            1_700_000_000_000_000
+        );
+        assert_eq!(
+            downcast(15)
+                .downcast_ref::<TimestampNanosecondArray>()
+                .unwrap()
+                .value(0),
+            1_700_000_000_123_456_789
+        );
+        assert_eq!(
+            downcast(16)
+                .downcast_ref::<TimestampMicrosecondArray>()
+                .unwrap()
+                .value(0),
+            1_700_000_000_000_000
+        );
+        assert_eq!(
+            result.schema().field(16).data_type(),
+            &ArrowDataType::Timestamp(TimeUnit::Microsecond, 
Some("UTC".into()))
+        );
+    }
+
+    /// Null values in nullable columns must round-trip as nulls.
+    #[tokio::test]
+    async fn test_read_null_values() {
+        let fields = data_fields();
+        let batch = RecordBatch::try_new(
+            arrow_schema(),
+            vec![
+                Arc::new(Int32Array::from(vec![1, 2, 3])),
+                Arc::new(StringArray::from(vec![Some("a"), None, Some("c")])),
+                Arc::new(Int32Array::from(vec![Some(10), None, None])),
+            ],
+        )
+        .unwrap();
+        let data = write_mosaic(&batch);
+        let batches = read_batches(data, &fields, None).await.unwrap();
+
+        assert_eq!(batches.len(), 1);
+        let result = &batches[0];
+        assert_eq!(result.num_rows(), 3);
+
+        let names = result
+            .column(1)
+            .as_any()
+            .downcast_ref::<StringArray>()
+            .unwrap();
+        assert_eq!(names.value(0), "a");
+        assert!(names.is_null(1));
+        assert_eq!(names.value(2), "c");
+
+        let scores = result
+            .column(2)
+            .as_any()
+            .downcast_ref::<Int32Array>()
+            .unwrap();
+        assert_eq!(scores.value(0), 10);
+        assert_eq!(scores.null_count(), 2);
+    }
+
+    #[test]
+    fn test_mosaic_value_to_datum_conversions() {
+        let ts = |p| DataType::Timestamp(TimestampType::new(p).unwrap());
+        let ltz = |p| 
DataType::LocalZonedTimestamp(LocalZonedTimestampType::new(p).unwrap());
+
+        // Each supported variant maps to the matching Datum.
+        assert_eq!(
+            mosaic_value_to_datum(
+                &MosaicValue::Boolean(true),
+                &DataType::Boolean(BooleanType::new())
+            ),
+            Some(Datum::Bool(true))
+        );
+        assert_eq!(
+            mosaic_value_to_datum(
+                &MosaicValue::TinyInt(7),
+                &DataType::TinyInt(TinyIntType::new())
+            ),
+            Some(Datum::TinyInt(7))
+        );
+        assert_eq!(
+            mosaic_value_to_datum(
+                &MosaicValue::SmallInt(-9),
+                &DataType::SmallInt(SmallIntType::new())
+            ),
+            Some(Datum::SmallInt(-9))
+        );
+        assert_eq!(
+            mosaic_value_to_datum(&MosaicValue::Integer(42), 
&DataType::Int(IntType::new())),
+            Some(Datum::Int(42))
+        );
+        assert_eq!(
+            mosaic_value_to_datum(
+                &MosaicValue::BigInt(1_000),
+                &DataType::BigInt(BigIntType::new())
+            ),
+            Some(Datum::Long(1_000))
+        );
+        assert_eq!(
+            mosaic_value_to_datum(&MosaicValue::Float(1.5), 
&DataType::Float(FloatType::new())),
+            Some(Datum::Float(1.5))
+        );
+        assert_eq!(
+            mosaic_value_to_datum(
+                &MosaicValue::Double(2.5),
+                &DataType::Double(DoubleType::new())
+            ),
+            Some(Datum::Double(2.5))
+        );
+        assert_eq!(
+            mosaic_value_to_datum(&MosaicValue::Date(100), 
&DataType::Date(DateType::new())),
+            Some(Datum::Date(100))
+        );
+        assert_eq!(
+            mosaic_value_to_datum(
+                &MosaicValue::Time(200),
+                &DataType::Time(TimeType::new(3).unwrap())
+            ),
+            Some(Datum::Time(200))
+        );
+        assert_eq!(
+            mosaic_value_to_datum(
+                &MosaicValue::String(b"hi".to_vec()),
+                &DataType::VarChar(VarCharType::new(20).unwrap())
+            ),
+            Some(Datum::String("hi".to_string()))
+        );
+        assert_eq!(
+            mosaic_value_to_datum(
+                &MosaicValue::Bytes(vec![1, 2]),
+                &DataType::VarBinary(VarBinaryType::try_new(true, 20).unwrap())
+            ),
+            Some(Datum::Bytes(vec![1, 2]))
+        );
+        assert_eq!(
+            mosaic_value_to_datum(
+                &MosaicValue::DecimalCompact(1_000),
+                &DataType::Decimal(DecimalType::new(5, 2).unwrap())
+            ),
+            Some(Datum::Decimal {
+                unscaled: 1_000,
+                precision: 5,
+                scale: 2,
+            })
+        );
+
+        // Timestamp precision boundaries select the matching Mosaic encoding.
+        assert_eq!(
+            mosaic_value_to_datum(&MosaicValue::TimestampMillis(5), &ts(3)),
+            Some(Datum::Timestamp {
+                millis: 5,
+                nanos: 0
+            })
+        );
+        assert_eq!(
+            mosaic_value_to_datum(&MosaicValue::TimestampMillis(5), &ltz(3)),
+            Some(Datum::LocalZonedTimestamp {
+                millis: 5,
+                nanos: 0
+            })
+        );
+        assert_eq!(
+            mosaic_value_to_datum(&MosaicValue::TimestampMicros(1_500), 
&ts(6)),
+            Some(Datum::Timestamp {
+                millis: 1,
+                nanos: 500_000,
+            })
+        );
+        assert_eq!(
+            mosaic_value_to_datum(
+                &MosaicValue::TimestampNanos {
+                    millis: 1,
+                    nanos_of_milli: 2,
+                },
+                &ts(9)
+            ),
+            Some(Datum::Timestamp {
+                millis: 1,
+                nanos: 2
+            })
+        );
+
+        // Ambiguous or unsupported inputs must fail open (None).
+        assert_eq!(
+            mosaic_value_to_datum(&MosaicValue::Null, 
&DataType::Int(IntType::new())),
+            None
+        );
+        assert_eq!(
+            mosaic_value_to_datum(
+                &MosaicValue::Integer(1),
+                &DataType::BigInt(BigIntType::new())
+            ),
+            None,
+            "type mismatch must not convert"
+        );
+        assert_eq!(
+            mosaic_value_to_datum(
+                &MosaicValue::DecimalLarge(vec![0, 0]),
+                &DataType::Decimal(DecimalType::new(20, 0).unwrap())
+            ),
+            None,
+            "large decimal stats are not converted"
+        );
+        assert_eq!(
+            mosaic_value_to_datum(&MosaicValue::TimestampMillis(5), &ts(6)),
+            None,
+            "millis encoding must not satisfy a micros-precision type"
+        );
+    }
+
+    #[test]
+    fn test_split_batch() {
+        let chunks = split_batch(sample_batch(), 2);
+        assert_eq!(chunks.len(), 3);
+        assert_eq!(
+            chunks.iter().map(RecordBatch::num_rows).collect::<Vec<_>>(),
+            vec![2, 2, 1]
+        );
+
+        let unsplit = split_batch(sample_batch(), 0);
+        assert_eq!(unsplit.len(), 1);
+        assert_eq!(unsplit[0].num_rows(), 5);
+
+        let whole = split_batch(sample_batch(), 10);
+        assert_eq!(whole.len(), 1);
+    }
 }
diff --git a/crates/paimon/src/table/data_file_reader.rs 
b/crates/paimon/src/table/data_file_reader.rs
index f08ad91..d5d9f3e 100644
--- a/crates/paimon/src/table/data_file_reader.rs
+++ b/crates/paimon/src/table/data_file_reader.rs
@@ -470,13 +470,16 @@ mod tests {
     use crate::arrow::build_target_arrow_schema;
     use crate::io::FileIOBuilder;
     use crate::spec::stats::BinaryTableStats;
-    use crate::spec::{ArrayType, DataFileMeta, DataType, IntType, VarCharType};
-    use crate::table::source::DataSplitBuilder;
+    use crate::spec::{
+        ArrayType, DataFileMeta, DataType, Datum, IntType, Predicate, 
PredicateBuilder, VarCharType,
+    };
+    use crate::table::source::{DataSplitBuilder, DeletionFile};
     use arrow_array::{Int32Array, StringArray};
     use bytes::Bytes;
     use futures::TryStreamExt;
     use paimon_mosaic_core::spec::COMPRESSION_NONE;
     use paimon_mosaic_core::writer::{MosaicWriter, OutputFile, WriterOptions};
+    use roaring::RoaringBitmap;
     use std::io;
 
     struct MemOutputFile {
@@ -644,4 +647,298 @@ mod tests {
         assert_eq!(names.value(0), "a");
         assert_eq!(names.value(2), "c");
     }
+
+    fn pk_fields() -> Vec<DataField> {
+        vec![
+            data_field(0, "id", DataType::Int(IntType::with_nullable(false))),
+            data_field(
+                1,
+                "name",
+                DataType::VarChar(VarCharType::with_nullable(true, 
20).unwrap()),
+            ),
+        ]
+    }
+
+    fn pk_batch(ids: Vec<i32>, names: Vec<&str>) -> RecordBatch {
+        RecordBatch::try_new(
+            build_target_arrow_schema(&pk_fields()).unwrap(),
+            vec![
+                Arc::new(Int32Array::from(ids)),
+                Arc::new(StringArray::from(names)),
+            ],
+        )
+        .unwrap()
+    }
+
+    fn write_multi_row_group_mosaic(batches: &[RecordBatch], stats_columns: 
Vec<String>) -> Bytes {
+        let out = MemOutputFile::new();
+        let mut writer = MosaicWriter::new(
+            out,
+            batches[0].schema().as_ref(),
+            WriterOptions {
+                compression: COMPRESSION_NONE,
+                num_buckets: 2,
+                // One row group per written batch, so each batch carries its 
own stats.
+                row_group_max_size: 1,
+                stats_columns,
+                ..Default::default()
+            },
+        )
+        .unwrap();
+        for batch in batches {
+            writer.write_batch(batch).unwrap();
+        }
+        writer.close().unwrap();
+        Bytes::from(writer.output().data.to_vec())
+    }
+
+    fn write_parquet(batch: &RecordBatch) -> Bytes {
+        let mut buf = Vec::new();
+        let mut writer =
+            parquet::arrow::ArrowWriter::try_new(&mut buf, batch.schema(), 
None).unwrap();
+        writer.write(batch).unwrap();
+        writer.close().unwrap();
+        Bytes::from(buf)
+    }
+
+    /// Writes a Paimon deletion-vector blob and returns the `DeletionFile` 
pointing at it.
+    /// Layout matches [`DeletionVector::read_from_bytes`]:
+    /// `i32 bitmapLength (magic + bitmap) | i32 magic | bitmap bytes | i32 
crc`.
+    async fn write_deletion_file(
+        file_io: &crate::io::FileIO,
+        path: &str,
+        deleted_rows: &[u32],
+    ) -> DeletionFile {
+        // BitmapDeletionVector.MAGIC_NUMBER, see crate::deletion_vector.
+        const MAGIC_NUMBER: i32 = 1581511376;
+        let mut bitmap = RoaringBitmap::new();
+        for row in deleted_rows {
+            bitmap.insert(*row);
+        }
+        let mut bitmap_bytes = Vec::new();
+        bitmap.serialize_into(&mut bitmap_bytes).unwrap();
+
+        let bitmap_length = 4 + bitmap_bytes.len() as i32;
+        let mut blob = Vec::new();
+        blob.extend_from_slice(&bitmap_length.to_be_bytes());
+        blob.extend_from_slice(&MAGIC_NUMBER.to_be_bytes());
+        blob.extend_from_slice(&bitmap_bytes);
+        blob.extend_from_slice(&0i32.to_be_bytes()); // crc, skipped on read
+        file_io
+            .new_output(path)
+            .unwrap()
+            .write(Bytes::from(blob))
+            .await
+            .unwrap();
+
+        DeletionFile::new(
+            path.to_string(),
+            0,
+            bitmap_length as i64,
+            Some(deleted_rows.len() as i64),
+        )
+    }
+
+    fn collect_ids(batches: &[RecordBatch]) -> Vec<i32> {
+        batches
+            .iter()
+            .flat_map(|b| {
+                b.column(0)
+                    .as_any()
+                    .downcast_ref::<Int32Array>()
+                    .unwrap()
+                    .values()
+                    .to_vec()
+            })
+            .collect()
+    }
+
+    /// Deletion vectors are applied format-agnostically by `DataFileReader`; 
verify a
+    /// Mosaic file honors deleted rows end to end.
+    #[tokio::test]
+    async fn test_mosaic_with_deletion_vector() {
+        let fields = pk_fields();
+        let data = write_mosaic(&pk_batch(vec![1, 2, 3], vec!["a", "b", "c"]));
+
+        let file_io = FileIOBuilder::new("memory").build().unwrap();
+        let table_path = "memory:/mosaic_dv";
+        let bucket_path = format!("{table_path}/bucket-0");
+        let file_name = "part-0.mosaic";
+        file_io
+            .new_output(&format!("{bucket_path}/{file_name}"))
+            .unwrap()
+            .write(data.clone())
+            .await
+            .unwrap();
+        // Delete row index 1 (id = 2).
+        let dv = write_deletion_file(&file_io, 
&format!("{table_path}/index/dv-0"), &[1]).await;
+
+        let table_schema_id = 1;
+        let split = DataSplitBuilder::new()
+            .with_snapshot(1)
+            .with_partition(crate::spec::BinaryRow::new(0))
+            .with_bucket(0)
+            .with_bucket_path(bucket_path)
+            .with_total_buckets(1)
+            .with_data_files(vec![data_file(
+                file_name,
+                data.len() as i64,
+                3,
+                table_schema_id,
+            )])
+            .with_data_deletion_files(vec![Some(dv)])
+            .build()
+            .unwrap();
+        let schema_manager = SchemaManager::new(file_io.clone(), 
table_path.to_string());
+        let reader = DataFileReader::new(
+            file_io,
+            schema_manager,
+            table_schema_id,
+            fields.clone(),
+            fields.clone(),
+            Vec::new(),
+        );
+        let batches = reader
+            .read(&[split])
+            .unwrap()
+            .try_collect::<Vec<_>>()
+            .await
+            .unwrap();
+
+        assert_eq!(collect_ids(&batches), vec![1, 3]);
+    }
+
+    /// A Mosaic file and a Parquet file in the same split must both be read 
and concatenated.
+    #[tokio::test]
+    async fn test_mosaic_mixed_format_read() {
+        let fields = pk_fields();
+        let mosaic_data = write_mosaic(&pk_batch(vec![1, 2], vec!["a", "b"]));
+        let parquet_data = write_parquet(&pk_batch(vec![3, 4], vec!["c", 
"d"]));
+
+        let file_io = FileIOBuilder::new("memory").build().unwrap();
+        let table_path = "memory:/mosaic_mixed";
+        let bucket_path = format!("{table_path}/bucket-0");
+        for (name, data) in [
+            ("part-0.mosaic", &mosaic_data),
+            ("part-1.parquet", &parquet_data),
+        ] {
+            file_io
+                .new_output(&format!("{bucket_path}/{name}"))
+                .unwrap()
+                .write(data.clone())
+                .await
+                .unwrap();
+        }
+
+        let table_schema_id = 1;
+        let split = DataSplitBuilder::new()
+            .with_snapshot(1)
+            .with_partition(crate::spec::BinaryRow::new(0))
+            .with_bucket(0)
+            .with_bucket_path(bucket_path)
+            .with_total_buckets(1)
+            .with_data_files(vec![
+                data_file(
+                    "part-0.mosaic",
+                    mosaic_data.len() as i64,
+                    2,
+                    table_schema_id,
+                ),
+                data_file(
+                    "part-1.parquet",
+                    parquet_data.len() as i64,
+                    2,
+                    table_schema_id,
+                ),
+            ])
+            .build()
+            .unwrap();
+        let schema_manager = SchemaManager::new(file_io.clone(), 
table_path.to_string());
+        let reader = DataFileReader::new(
+            file_io,
+            schema_manager,
+            table_schema_id,
+            fields.clone(),
+            fields.clone(),
+            Vec::new(),
+        );
+        let batches = reader
+            .read(&[split])
+            .unwrap()
+            .try_collect::<Vec<_>>()
+            .await
+            .unwrap();
+
+        let mut ids = collect_ids(&batches);
+        ids.sort_unstable();
+        assert_eq!(ids, vec![1, 2, 3, 4]);
+    }
+
+    /// Row-group predicate pruning, deletion vectors and projection must 
compose correctly:
+    /// the predicate keeps one row group, the DV deletes one of its rows, 
projection keeps `id`.
+    #[tokio::test]
+    async fn test_mosaic_predicate_dv_projection_combination() {
+        let fields = pk_fields();
+        let data = write_multi_row_group_mosaic(
+            &[
+                pk_batch(vec![1, 2], vec!["a", "b"]),
+                pk_batch(vec![10, 11], vec!["c", "d"]),
+                pk_batch(vec![20, 21], vec!["e", "f"]),
+            ],
+            vec!["id".to_string()],
+        );
+
+        let file_io = FileIOBuilder::new("memory").build().unwrap();
+        let table_path = "memory:/mosaic_combo";
+        let bucket_path = format!("{table_path}/bucket-0");
+        let file_name = "part-0.mosaic";
+        file_io
+            .new_output(&format!("{bucket_path}/{file_name}"))
+            .unwrap()
+            .write(data.clone())
+            .await
+            .unwrap();
+        // Delete global row index 2 (id = 10, first row of the second row 
group).
+        let dv = write_deletion_file(&file_io, 
&format!("{table_path}/index/dv-0"), &[2]).await;
+
+        let table_schema_id = 1;
+        let split = DataSplitBuilder::new()
+            .with_snapshot(1)
+            .with_partition(crate::spec::BinaryRow::new(0))
+            .with_bucket(0)
+            .with_bucket_path(bucket_path)
+            .with_total_buckets(1)
+            .with_data_files(vec![data_file(
+                file_name,
+                data.len() as i64,
+                6,
+                table_schema_id,
+            )])
+            .with_data_deletion_files(vec![Some(dv)])
+            .build()
+            .unwrap();
+
+        let predicate: Predicate = PredicateBuilder::new(&fields)
+            .equal("id", Datum::Int(10))
+            .unwrap();
+        let read_type = vec![fields[0].clone()];
+        let schema_manager = SchemaManager::new(file_io.clone(), 
table_path.to_string());
+        let reader = DataFileReader::new(
+            file_io,
+            schema_manager,
+            table_schema_id,
+            fields.clone(),
+            read_type,
+            vec![predicate],
+        );
+        let batches = reader
+            .read(&[split])
+            .unwrap()
+            .try_collect::<Vec<_>>()
+            .await
+            .unwrap();
+
+        assert_eq!(batches.iter().map(|b| b.num_columns()).max(), Some(1));
+        assert_eq!(collect_ids(&batches), vec![11]);
+    }
 }

Reply via email to