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 00a3660  feat: add Mosaic data file reader (#386)
00a3660 is described below

commit 00a36605eaa298d62acd39a7add0ee46e086eb43
Author: QuakeWang <[email protected]>
AuthorDate: Tue Jun 16 13:53:30 2026 +0800

    feat: add Mosaic data file reader (#386)
---
 .github/workflows/ci.yml                  |   6 +-
 crates/integrations/datafusion/Cargo.toml |   1 +
 crates/paimon/Cargo.toml                  |   2 +
 crates/paimon/src/arrow/format/mod.rs     |  22 +-
 crates/paimon/src/arrow/format/mosaic.rs  | 573 ++++++++++++++++++++++++++++++
 5 files changed, 600 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 038ece7..3fd54d1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -58,7 +58,7 @@ jobs:
         run: cargo fmt --all -- --check
 
       - name: Clippy
-        run: cargo clippy --all-targets --workspace --features fulltext,vortex 
-- -D warnings
+        run: cargo clippy --all-targets --workspace --features 
fulltext,vortex,mosaic -- -D warnings
 
   build:
     runs-on: ${{ matrix.os }}
@@ -71,7 +71,7 @@ jobs:
     steps:
       - uses: actions/checkout@v6
       - name: Build
-        run: cargo build --features fulltext,vortex
+        run: cargo build --features fulltext,vortex,mosaic
 
   unit:
     runs-on: ${{ matrix.os }}
@@ -85,7 +85,7 @@ jobs:
       - uses: actions/checkout@v6
 
       - name: Test
-        run: cargo test -p paimon --all-targets --features fulltext,vortex
+        run: cargo test -p paimon --all-targets --features 
fulltext,vortex,mosaic
         env:
           RUST_LOG: DEBUG
           RUST_BACKTRACE: full
diff --git a/crates/integrations/datafusion/Cargo.toml 
b/crates/integrations/datafusion/Cargo.toml
index 3e7392d..a0a9bf5 100644
--- a/crates/integrations/datafusion/Cargo.toml
+++ b/crates/integrations/datafusion/Cargo.toml
@@ -29,6 +29,7 @@ keywords = ["paimon", "datafusion", "integrations"]
 
 [features]
 fulltext = ["paimon/fulltext"]
+mosaic = ["paimon/mosaic"]
 vortex = ["paimon/vortex"]
 
 [dependencies]
diff --git a/crates/paimon/Cargo.toml b/crates/paimon/Cargo.toml
index 0741a50..2a45ff5 100644
--- a/crates/paimon/Cargo.toml
+++ b/crates/paimon/Cargo.toml
@@ -42,6 +42,7 @@ storage-all = [
     "storage-hdfs",
 ]
 fulltext = ["tantivy", "tempfile"]
+mosaic = ["dep:paimon-mosaic-core"]
 vortex = ["dep:vortex"]
 
 storage-memory = ["opendal/services-memory"]
@@ -101,6 +102,7 @@ uuid = { version = "1", features = ["v4"] }
 urlencoding = "2.1"
 tantivy = { version = "0.22", optional = true }
 tempfile = { version = "3", optional = true }
+paimon-mosaic-core = { version = "0.1.0", optional = true }
 vortex = { version = "0.68", features = ["tokio"], optional = true }
 libloading = "0.9"
 # Keep CI on the dependency set that passed before unicode-segmentation 1.13.3.
diff --git a/crates/paimon/src/arrow/format/mod.rs 
b/crates/paimon/src/arrow/format/mod.rs
index e568bc5..2bd4add 100644
--- a/crates/paimon/src/arrow/format/mod.rs
+++ b/crates/paimon/src/arrow/format/mod.rs
@@ -17,6 +17,8 @@
 
 mod avro;
 pub(crate) mod blob;
+#[cfg(feature = "mosaic")]
+mod mosaic;
 mod orc;
 mod parquet;
 #[cfg(feature = "vortex")]
@@ -106,18 +108,36 @@ pub(crate) fn create_format_reader(
     } else if lower.ends_with(".avro") {
         Ok(Box::new(avro::AvroFormatReader))
     } else {
+        #[cfg(feature = "mosaic")]
+        if lower.ends_with(".mosaic") {
+            return Ok(Box::new(mosaic::MosaicFormatReader));
+        }
         #[cfg(feature = "vortex")]
         if lower.ends_with(".vortex") {
             return Ok(Box::new(vortex::VortexFormatReader));
         }
         Err(Error::Unsupported {
             message: format!(
-                "unsupported file format: expected .parquet, .blob, .orc, or 
.avro, got: {path}"
+                "unsupported file format: expected {}, got: {path}",
+                supported_read_formats().join(", ")
             ),
         })
     }
 }
 
+fn supported_read_formats() -> Vec<&'static str> {
+    vec![
+        ".parquet",
+        ".blob",
+        ".orc",
+        ".avro",
+        #[cfg(feature = "mosaic")]
+        ".mosaic",
+        #[cfg(feature = "vortex")]
+        ".vortex",
+    ]
+}
+
 /// Create a format writer that streams directly to storage.
 pub(crate) async fn create_format_writer(
     output: &OutputFile,
diff --git a/crates/paimon/src/arrow/format/mosaic.rs 
b/crates/paimon/src/arrow/format/mosaic.rs
new file mode 100644
index 0000000..b7e3c47
--- /dev/null
+++ b/crates/paimon/src/arrow/format/mosaic.rs
@@ -0,0 +1,573 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use super::{FilePredicates, FormatFileReader};
+use crate::arrow::build_target_arrow_schema;
+use crate::io::FileRead;
+use crate::spec::DataField;
+use crate::table::{ArrowRecordBatchStream, RowRange};
+use crate::Error;
+use arrow_array::{ArrayRef, RecordBatch, RecordBatchOptions, UInt64Array};
+use arrow_schema::{DataType as ArrowDataType, SchemaRef, TimeUnit};
+use async_stream::try_stream;
+use async_trait::async_trait;
+use bytes::Bytes;
+use futures::StreamExt;
+use paimon_mosaic_core::reader::{InputFile, MosaicReader, ReaderAccess};
+use std::io;
+
+pub(crate) struct MosaicFormatReader;
+
+const DEFAULT_BATCH_SIZE: usize = 8192;
+
+#[async_trait]
+impl FormatFileReader for MosaicFormatReader {
+    async fn read_batch_stream(
+        &self,
+        reader: Box<dyn FileRead>,
+        file_size: u64,
+        read_fields: &[DataField],
+        _predicates: Option<&FilePredicates>,
+        batch_size: Option<usize>,
+        row_selection: Option<Vec<RowRange>>,
+    ) -> crate::Result<ArrowRecordBatchStream> {
+        // Mosaic predicates are currently residual; callers must re-check 
them for exact filtering.
+        let target_schema = build_target_arrow_schema(read_fields)?;
+        validate_mosaic_schema(&target_schema)?;
+
+        let file_bytes = reader.read(0..file_size).await?;
+        let mosaic_reader = 
MosaicReader::new(MemoryInputFile::new(file_bytes), file_size)
+            .map_err(mosaic_read_error)?;
+        let projected_names = read_fields
+            .iter()
+            .map(|field| field.name().to_string())
+            .collect::<Vec<_>>();
+        let batch_size = batch_size.unwrap_or(DEFAULT_BATCH_SIZE);
+
+        Ok(try_stream! {
+            let mut row_group_start = 0usize;
+            for row_group_index in 0..mosaic_reader.num_row_groups() {
+                let row_group_rows = mosaic_reader
+                    .row_group_num_rows(row_group_index)
+                    .map_err(mosaic_read_error)?;
+                let selected_indices = selected_indices_for_row_group(
+                    row_group_rows,
+                    row_group_start,
+                    row_selection.as_deref(),
+                )?;
+                row_group_start = row_group_start
+                    .checked_add(row_group_rows)
+                    .ok_or_else(|| Error::DataInvalid {
+                        message: "Mosaic row group row count 
overflow".to_string(),
+                        source: None,
+                    })?;
+
+                if let Some(indices) = selected_indices.as_ref() {
+                    if indices.is_empty() {
+                        continue;
+                    }
+                }
+
+                let mut row_group_reader = if projected_names.is_empty() {
+                    mosaic_reader
+                        .row_group_reader_by_names(row_group_index, &[])
+                        .map_err(mosaic_read_error)?
+                } else {
+                    let names = projected_names
+                        .iter()
+                        .map(String::as_str)
+                        .collect::<Vec<_>>();
+                    mosaic_reader
+                        .row_group_reader_by_names(row_group_index, &names)
+                        .map_err(mosaic_read_error)?
+                };
+
+                let batch = row_group_reader
+                    .read_columns()
+                    .map_err(mosaic_read_error)?;
+                let batch = take_rows(batch, selected_indices.as_ref(), 
&target_schema)?;
+                for chunk in split_batch(batch, batch_size) {
+                    yield chunk;
+                }
+            }
+        }
+        .boxed())
+    }
+}
+
+#[derive(Clone)]
+struct MemoryInputFile {
+    data: Bytes,
+}
+
+impl MemoryInputFile {
+    fn new(data: Bytes) -> Self {
+        Self { data }
+    }
+}
+
+impl InputFile for MemoryInputFile {
+    fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<()> {
+        let offset = usize::try_from(offset).map_err(|_| {
+            io::Error::new(
+                io::ErrorKind::InvalidInput,
+                "mosaic read offset exceeds usize",
+            )
+        })?;
+        let end = offset.checked_add(buf.len()).ok_or_else(|| {
+            io::Error::new(io::ErrorKind::InvalidInput, "mosaic read range 
overflows")
+        })?;
+        let src = self.data.get(offset..end).ok_or_else(|| {
+            io::Error::new(
+                io::ErrorKind::UnexpectedEof,
+                "mosaic read range exceeds file size",
+            )
+        })?;
+        buf.copy_from_slice(src);
+        Ok(())
+    }
+}
+
+fn validate_mosaic_schema(schema: &SchemaRef) -> crate::Result<()> {
+    for field in schema.fields() {
+        validate_mosaic_arrow_type(field.data_type()).map_err(|message| 
Error::Unsupported {
+            message: format!(
+                "Mosaic format does not support column '{}' with type {:?}: 
{message}",
+                field.name(),
+                field.data_type()
+            ),
+        })?;
+    }
+    Ok(())
+}
+
+fn validate_mosaic_arrow_type(data_type: &ArrowDataType) -> Result<(), String> 
{
+    match data_type {
+        ArrowDataType::Boolean
+        | ArrowDataType::Int8
+        | ArrowDataType::Int16
+        | ArrowDataType::Int32
+        | ArrowDataType::Int64
+        | ArrowDataType::Float32
+        | ArrowDataType::Float64
+        | ArrowDataType::Date32
+        | ArrowDataType::Utf8
+        | ArrowDataType::Binary => Ok(()),
+        ArrowDataType::Time32(TimeUnit::Millisecond) => Ok(()),
+        ArrowDataType::Decimal128(precision, _) => {
+            if *precision == 0 || *precision > 38 {
+                Err(format!(
+                    "Decimal precision must be in 1..=38, got {precision}"
+                ))
+            } else {
+                Ok(())
+            }
+        }
+        ArrowDataType::Timestamp(
+            TimeUnit::Millisecond | TimeUnit::Microsecond | 
TimeUnit::Nanosecond,
+            _,
+        ) => Ok(()),
+        ArrowDataType::Struct(fields) if is_timestamp_nanos_struct(fields) => 
Ok(()),
+        other => Err(format!("unsupported Arrow type {other:?}")),
+    }
+}
+
+fn is_timestamp_nanos_struct(fields: &arrow_schema::Fields) -> bool {
+    fields.len() == 2
+        && fields[0].name() == "millis"
+        && *fields[0].data_type() == ArrowDataType::Int64
+        && fields[1].name() == "nanos_of_milli"
+        && *fields[1].data_type() == ArrowDataType::Int32
+}
+
+fn selected_indices_for_row_group(
+    row_group_rows: usize,
+    row_group_start: usize,
+    row_selection: Option<&[RowRange]>,
+) -> crate::Result<Option<UInt64Array>> {
+    let Some(row_selection) = row_selection else {
+        return Ok(None);
+    };
+
+    let row_group_end =
+        row_group_start
+            .checked_add(row_group_rows)
+            .ok_or_else(|| Error::DataInvalid {
+                message: "Mosaic row group row range overflow".to_string(),
+                source: None,
+            })?;
+
+    let mut indices = Vec::new();
+    for range in row_selection {
+        let from = usize::try_from(range.from()).map_err(|e| 
Error::DataInvalid {
+            message: format!(
+                "Invalid negative Mosaic row selection start: {}",
+                range.from()
+            ),
+            source: Some(Box::new(e)),
+        })?;
+        let to_inclusive = usize::try_from(range.to()).map_err(|e| 
Error::DataInvalid {
+            message: format!("Invalid negative Mosaic row selection end: {}", 
range.to()),
+            source: Some(Box::new(e)),
+        })?;
+        let to = to_inclusive
+            .checked_add(1)
+            .ok_or_else(|| Error::DataInvalid {
+                message: "Mosaic row selection end overflows".to_string(),
+                source: None,
+            })?;
+        let start = from.max(row_group_start);
+        let end = to.min(row_group_end);
+        if start >= end {
+            continue;
+        }
+        indices.extend((start - row_group_start..end - 
row_group_start).map(|idx| idx as u64));
+    }
+
+    Ok(Some(UInt64Array::from(indices)))
+}
+
+fn take_rows(
+    batch: RecordBatch,
+    indices: Option<&UInt64Array>,
+    target_schema: &SchemaRef,
+) -> crate::Result<RecordBatch> {
+    let Some(indices) = indices else {
+        return ensure_schema(batch, target_schema);
+    };
+
+    if batch.num_columns() == 0 {
+        return RecordBatch::try_new_with_options(
+            target_schema.clone(),
+            Vec::new(),
+            &RecordBatchOptions::new().with_row_count(Some(indices.len())),
+        )
+        .map_err(|e| Error::UnexpectedError {
+            message: format!("Failed to build empty Mosaic RecordBatch: {e}"),
+            source: Some(Box::new(e)),
+        });
+    }
+
+    let columns = batch
+        .columns()
+        .iter()
+        .map(|column| {
+            arrow_select::take::take(column.as_ref(), indices, 
None).map_err(|e| {
+                Error::UnexpectedError {
+                    message: format!("Failed to apply Mosaic row selection: 
{e}"),
+                    source: Some(Box::new(e)),
+                }
+            })
+        })
+        .collect::<crate::Result<Vec<ArrayRef>>>()?;
+
+    RecordBatch::try_new(target_schema.clone(), columns).map_err(|e| 
Error::UnexpectedError {
+        message: format!("Failed to build Mosaic RecordBatch: {e}"),
+        source: Some(Box::new(e)),
+    })
+}
+
+fn ensure_schema(batch: RecordBatch, target_schema: &SchemaRef) -> 
crate::Result<RecordBatch> {
+    if batch.schema().as_ref() == target_schema.as_ref() {
+        return Ok(batch);
+    }
+
+    if batch.num_columns() == 0 {
+        return RecordBatch::try_new_with_options(
+            target_schema.clone(),
+            Vec::new(),
+            &RecordBatchOptions::new().with_row_count(Some(batch.num_rows())),
+        )
+        .map_err(|e| Error::UnexpectedError {
+            message: format!("Failed to build empty Mosaic RecordBatch: {e}"),
+            source: Some(Box::new(e)),
+        });
+    }
+
+    RecordBatch::try_new(target_schema.clone(), 
batch.columns().to_vec()).map_err(|e| {
+        Error::UnexpectedError {
+            message: format!("Failed to align Mosaic RecordBatch schema: {e}"),
+            source: Some(Box::new(e)),
+        }
+    })
+}
+
+fn split_batch(batch: RecordBatch, batch_size: usize) -> Vec<RecordBatch> {
+    if batch_size == 0 || batch.num_rows() <= batch_size {
+        return vec![batch];
+    }
+
+    let mut batches = Vec::new();
+    let mut offset = 0;
+    while offset < batch.num_rows() {
+        let len = batch_size.min(batch.num_rows() - offset);
+        batches.push(batch.slice(offset, len));
+        offset += len;
+    }
+    batches
+}
+
+fn mosaic_read_error(error: io::Error) -> Error {
+    Error::DataInvalid {
+        message: format!("Failed to read Mosaic file: {error}"),
+        source: Some(Box::new(error)),
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::arrow::format::FormatFileReader;
+    use crate::spec::{ArrayType, DataType, IntType, RowType, VarCharType};
+    use arrow_array::{Array, Int32Array, StringArray};
+    use arrow_schema::{DataType as ArrowDataType, Field, Schema};
+    use bytes::Bytes;
+    use futures::TryStreamExt;
+    use paimon_mosaic_core::spec::COMPRESSION_NONE;
+    use paimon_mosaic_core::writer::{MosaicWriter, OutputFile, WriterOptions};
+    use std::ops::Range;
+    use std::sync::Arc;
+
+    struct TestFileRead {
+        data: Bytes,
+    }
+
+    #[async_trait]
+    impl FileRead for TestFileRead {
+        async fn read(&self, range: Range<u64>) -> crate::Result<Bytes> {
+            let start = usize::try_from(range.start).unwrap();
+            let end = usize::try_from(range.end).unwrap();
+            Ok(self.data.slice(start..end))
+        }
+    }
+
+    struct MemOutputFile {
+        data: Vec<u8>,
+    }
+
+    impl MemOutputFile {
+        fn new() -> Self {
+            Self { data: Vec::new() }
+        }
+    }
+
+    impl OutputFile for MemOutputFile {
+        fn write(&mut self, data: &[u8]) -> io::Result<()> {
+            self.data.extend_from_slice(data);
+            Ok(())
+        }
+
+        fn flush(&mut self) -> io::Result<()> {
+            Ok(())
+        }
+
+        fn pos(&self) -> u64 {
+            self.data.len() as u64
+        }
+    }
+
+    fn data_fields() -> Vec<DataField> {
+        vec![
+            DataField::new(
+                0,
+                "id".to_string(),
+                DataType::Int(IntType::with_nullable(false)),
+            ),
+            DataField::new(
+                1,
+                "name".to_string(),
+                DataType::VarChar(VarCharType::with_nullable(true, 
20).unwrap()),
+            ),
+            DataField::new(
+                2,
+                "score".to_string(),
+                DataType::Int(IntType::with_nullable(true)),
+            ),
+        ]
+    }
+
+    fn arrow_schema() -> SchemaRef {
+        Arc::new(Schema::new(vec![
+            Field::new("id", ArrowDataType::Int32, false),
+            Field::new("name", ArrowDataType::Utf8, true),
+            Field::new("score", ArrowDataType::Int32, true),
+        ]))
+    }
+
+    fn sample_batch() -> RecordBatch {
+        RecordBatch::try_new(
+            arrow_schema(),
+            vec![
+                Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5])),
+                Arc::new(StringArray::from(vec!["a", "b", "c", "d", "e"])),
+                Arc::new(Int32Array::from(vec![10, 20, 30, 40, 50])),
+            ],
+        )
+        .unwrap()
+    }
+
+    fn write_mosaic(batch: &RecordBatch) -> Bytes {
+        let out = MemOutputFile::new();
+        let mut writer = MosaicWriter::new(
+            out,
+            batch.schema().as_ref(),
+            WriterOptions {
+                compression: COMPRESSION_NONE,
+                num_buckets: 2,
+                row_group_max_size: u64::MAX,
+                ..Default::default()
+            },
+        )
+        .unwrap();
+        writer.write_batch(batch).unwrap();
+        writer.close().unwrap();
+        Bytes::from(writer.output().data.to_vec())
+    }
+
+    async fn read_batches(
+        data: Bytes,
+        read_fields: &[DataField],
+        row_selection: Option<Vec<RowRange>>,
+    ) -> crate::Result<Vec<RecordBatch>> {
+        let file_size = data.len() as u64;
+        MosaicFormatReader
+            .read_batch_stream(
+                Box::new(TestFileRead { data }),
+                file_size,
+                read_fields,
+                None,
+                None,
+                row_selection,
+            )
+            .await?
+            .try_collect()
+            .await
+    }
+
+    #[tokio::test]
+    async fn test_read_basic_mosaic_file() {
+        let data = write_mosaic(&sample_batch());
+        let batches = read_batches(data, &data_fields(), None).await.unwrap();
+
+        assert_eq!(batches.len(), 1);
+        assert_eq!(batches[0].num_rows(), 5);
+        assert_eq!(batches[0].schema().fields().len(), 3);
+        let ids = batches[0]
+            .column(0)
+            .as_any()
+            .downcast_ref::<Int32Array>()
+            .unwrap();
+        assert_eq!(ids.value(0), 1);
+        assert_eq!(ids.value(4), 5);
+    }
+
+    #[tokio::test]
+    async fn test_read_projection_order() {
+        let fields = data_fields();
+        let projected = vec![fields[2].clone(), fields[0].clone()];
+        let data = write_mosaic(&sample_batch());
+        let batches = read_batches(data, &projected, None).await.unwrap();
+
+        assert_eq!(batches.len(), 1);
+        assert_eq!(batches[0].schema().field(0).name(), "score");
+        assert_eq!(batches[0].schema().field(1).name(), "id");
+        let scores = batches[0]
+            .column(0)
+            .as_any()
+            .downcast_ref::<Int32Array>()
+            .unwrap();
+        assert_eq!(scores.value(2), 30);
+    }
+
+    #[tokio::test]
+    async fn test_read_empty_projection() {
+        let data = write_mosaic(&sample_batch());
+        let batches = read_batches(data, &[], None).await.unwrap();
+
+        assert_eq!(batches.len(), 1);
+        assert_eq!(batches[0].num_columns(), 0);
+        assert_eq!(batches[0].num_rows(), 5);
+    }
+
+    #[tokio::test]
+    async fn test_read_row_selection() {
+        let fields = data_fields();
+        let data = write_mosaic(&sample_batch());
+        let batches = read_batches(
+            data,
+            &fields,
+            Some(vec![RowRange::new(1, 2), RowRange::new(4, 4)]),
+        )
+        .await
+        .unwrap();
+
+        assert_eq!(batches.len(), 1);
+        assert_eq!(batches[0].num_rows(), 3);
+        let ids = batches[0]
+            .column(0)
+            .as_any()
+            .downcast_ref::<Int32Array>()
+            .unwrap();
+        assert_eq!(ids.values(), &[2, 3, 5]);
+    }
+
+    #[tokio::test]
+    async fn test_unsupported_type_returns_error() {
+        let unsupported = vec![DataField::new(
+            0,
+            "items".to_string(),
+            DataType::Array(ArrayType::new(DataType::Int(IntType::new()))),
+        )];
+        let result = MosaicFormatReader
+            .read_batch_stream(
+                Box::new(TestFileRead { data: Bytes::new() }),
+                0,
+                &unsupported,
+                None,
+                None,
+                None,
+            )
+            .await;
+        let err = match result {
+            Ok(_) => panic!("expected unsupported Mosaic type error"),
+            Err(err) => err,
+        };
+
+        assert!(
+            matches!(err, Error::Unsupported { message } if 
message.contains("Mosaic format does not support column 'items'"))
+        );
+    }
+
+    #[test]
+    fn test_validate_row_type_as_unsupported() {
+        let unsupported = vec![DataField::new(
+            0,
+            "nested".to_string(),
+            DataType::Row(RowType::new(vec![DataField::new(
+                1,
+                "v".to_string(),
+                DataType::Int(IntType::new()),
+            )])),
+        )];
+        let schema = build_target_arrow_schema(&unsupported).unwrap();
+        let err = validate_mosaic_schema(&schema).unwrap_err();
+
+        assert!(
+            matches!(err, Error::Unsupported { message } if 
message.contains("Mosaic format does not support column 'nested'"))
+        );
+    }
+}

Reply via email to