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 fb9ab68  Support dedicated vector storage writes (#484)
fb9ab68 is described below

commit fb9ab68b53a2930e4059bc70c6fed1aab6f5509f
Author: Jingsong Lee <[email protected]>
AuthorDate: Wed Jul 8 19:00:25 2026 +0800

    Support dedicated vector storage writes (#484)
---
 crates/paimon/src/spec/core_options.rs             |  40 +++
 crates/paimon/src/spec/schema.rs                   | 107 +++++++
 crates/paimon/src/table/data_evolution_reader.rs   |   5 +-
 crates/paimon/src/table/data_evolution_writer.rs   | 311 +++++++++++++++++----
 ...e_writer.rs => dedicated_format_file_writer.rs} | 100 ++++++-
 crates/paimon/src/table/mod.rs                     |   2 +-
 crates/paimon/src/table/source.rs                  |   2 +-
 crates/paimon/src/table/table_commit.rs            |   2 +-
 crates/paimon/src/table/table_scan.rs              |   2 +-
 crates/paimon/src/table/table_write.rs             | 190 +++++++++++--
 10 files changed, 664 insertions(+), 97 deletions(-)

diff --git a/crates/paimon/src/spec/core_options.rs 
b/crates/paimon/src/spec/core_options.rs
index 3d5d79b..8585e37 100644
--- a/crates/paimon/src/spec/core_options.rs
+++ b/crates/paimon/src/spec/core_options.rs
@@ -47,6 +47,8 @@ const COMMIT_MAX_RETRY_WAIT_OPTION: &str = 
"commit.max-retry-wait";
 const FILE_COMPRESSION_OPTION: &str = "file.compression";
 const FILE_COMPRESSION_ZSTD_LEVEL_OPTION: &str = "file.compression.zstd-level";
 const FILE_FORMAT_OPTION: &str = "file.format";
+const VECTOR_FILE_FORMAT_OPTION: &str = "vector.file.format";
+const VECTOR_TARGET_FILE_SIZE_OPTION: &str = "vector.target-file-size";
 const CHANGELOG_FILE_PREFIX_OPTION: &str = "changelog-file.prefix";
 const CHANGELOG_FILE_FORMAT_OPTION: &str = "changelog-file.format";
 const CHANGELOG_FILE_COMPRESSION_OPTION: &str = "changelog-file.compression";
@@ -710,6 +712,24 @@ impl<'a> CoreOptions<'a> {
             .unwrap_or_else(|| self.target_file_size())
     }
 
+    /// Dedicated vector-store file format, if configured.
+    ///
+    /// Java leaves this unset by default. When present, vector columns are
+    /// written to files named `*.vector.<format>`.
+    pub fn vector_file_format(&self) -> Option<&str> {
+        self.options
+            .get(VECTOR_FILE_FORMAT_OPTION)
+            .map(String::as_str)
+            .filter(|format| !format.trim().is_empty())
+    }
+
+    pub fn vector_target_file_size(&self) -> i64 {
+        self.options
+            .get(VECTOR_TARGET_FILE_SIZE_OPTION)
+            .and_then(|v| parse_memory_size(v))
+            .unwrap_or_else(|| self.target_file_size())
+    }
+
     /// File format for data files (e.g. "parquet", "orc", "avro", "vortex").
     /// Default is "parquet".
     pub fn file_format(&self) -> &str {
@@ -1224,6 +1244,26 @@ mod tests {
         assert_eq!(custom_core.changelog_file_stats_mode(), Some("counts"));
     }
 
+    #[test]
+    fn test_vector_file_options_defaults_and_overrides() {
+        let default_options =
+            HashMap::from([("target-file-size".to_string(), "32 
mb".to_string())]);
+        let default_core = CoreOptions::new(&default_options);
+        assert_eq!(default_core.vector_file_format(), None);
+        assert_eq!(default_core.vector_target_file_size(), 32 * 1024 * 1024);
+
+        let custom_options = HashMap::from([
+            (VECTOR_FILE_FORMAT_OPTION.to_string(), "vortex".to_string()),
+            (
+                VECTOR_TARGET_FILE_SIZE_OPTION.to_string(),
+                "64 mb".to_string(),
+            ),
+        ]);
+        let custom_core = CoreOptions::new(&custom_options);
+        assert_eq!(custom_core.vector_file_format(), Some("vortex"));
+        assert_eq!(custom_core.vector_target_file_size(), 64 * 1024 * 1024);
+    }
+
     #[test]
     fn test_commit_options_defaults() {
         let options = HashMap::new();
diff --git a/crates/paimon/src/spec/schema.rs b/crates/paimon/src/spec/schema.rs
index 75b35c9..cc3f17a 100644
--- a/crates/paimon/src/spec/schema.rs
+++ b/crates/paimon/src/spec/schema.rs
@@ -450,6 +450,11 @@ impl TableSchema {
             &new_schema.partition_keys,
             &new_schema.options,
         )?;
+        Schema::validate_vector_store_fields(
+            &new_schema.fields,
+            &new_schema.partition_keys,
+            &new_schema.options,
+        )?;
         PartialUpdateConfig::new(&new_schema.options)
             .validate_create_mode(!new_schema.primary_keys.is_empty())?;
         AggregationConfig::new(&new_schema.options)
@@ -798,6 +803,7 @@ impl Schema {
         let fields = Self::normalize_fields(&fields, &partition_keys, 
&primary_keys)?;
         Self::validate_key_field_types(&fields, &primary_keys, &options)?;
         Self::validate_blob_fields(&fields, &partition_keys, &options)?;
+        Self::validate_vector_store_fields(&fields, &partition_keys, 
&options)?;
         
PartialUpdateConfig::new(&options).validate_create_mode(!primary_keys.is_empty())?;
         AggregationConfig::new(&options).validate_create_mode(&primary_keys, 
&fields)?;
         Self::validate_first_row_changelog_producer(&options)?;
@@ -1049,6 +1055,52 @@ impl Schema {
         Ok(())
     }
 
+    fn validate_vector_store_fields(
+        fields: &[DataField],
+        partition_keys: &[String],
+        options: &HashMap<String, String>,
+    ) -> crate::Result<()> {
+        let vector_field_names = Self::top_level_vector_field_names(fields);
+        let core_options = CoreOptions::new(options);
+        if vector_field_names.is_empty() || 
core_options.vector_file_format().is_none() {
+            return Ok(());
+        }
+
+        if !core_options.data_evolution_enabled() {
+            return Err(crate::Error::ConfigInvalid {
+                message:
+                    "Data evolution config must enabled for table with 
dedicated VECTOR storage."
+                        .to_string(),
+            });
+        }
+        if !core_options.row_tracking_enabled() {
+            return Err(crate::Error::ConfigInvalid {
+                message:
+                    "Row tracking config must enabled for table with dedicated 
VECTOR storage."
+                        .to_string(),
+            });
+        }
+
+        if fields.len() == vector_field_names.len() {
+            return Err(crate::Error::ConfigInvalid {
+                message: "Table with dedicated VECTOR storage must have other 
normal columns."
+                    .to_string(),
+            });
+        }
+
+        let partition_key_set: HashSet<&str> = 
partition_keys.iter().map(String::as_str).collect();
+        if vector_field_names
+            .iter()
+            .any(|name| partition_key_set.contains(name))
+        {
+            return Err(crate::Error::ConfigInvalid {
+                message: "The VECTOR type column can not be part of partition 
keys.".to_string(),
+            });
+        }
+
+        Ok(())
+    }
+
     fn validate_first_row_changelog_producer(
         options: &HashMap<String, String>,
     ) -> crate::Result<()> {
@@ -1094,6 +1146,17 @@ impl Schema {
             .collect()
     }
 
+    /// Returns top-level Vector field names for dedicated vector-store checks.
+    fn top_level_vector_field_names(fields: &[DataField]) -> Vec<&str> {
+        fields
+            .iter()
+            .filter_map(|field| match field.data_type() {
+                DataType::Vector(_) => Some(field.name()),
+                _ => None,
+            })
+            .collect()
+    }
+
     /// Returns the set of names that appear more than once.
     pub fn duplicate_fields(names: &[String]) -> HashSet<String> {
         let mut seen = HashMap::new();
@@ -2069,6 +2132,50 @@ mod tests {
         assert_eq!(schema.fields().len(), 2);
     }
 
+    #[test]
+    fn test_vector_store_requires_data_evolution_and_row_tracking() {
+        let err = Schema::builder()
+            .column("id", DataType::Int(IntType::new()))
+            .column("embedding", vector_4f())
+            .option("vector.file.format", "vortex")
+            .build()
+            .unwrap_err();
+        assert!(
+            matches!(err, crate::Error::ConfigInvalid { ref message }
+                if message.contains("Data evolution config must enabled")),
+            "dedicated VECTOR storage should require data-evolution.enabled, 
got {err:?}"
+        );
+
+        let err = Schema::builder()
+            .column("id", DataType::Int(IntType::new()))
+            .column("embedding", vector_4f())
+            .option("vector.file.format", "vortex")
+            .option("data-evolution.enabled", "true")
+            .build()
+            .unwrap_err();
+        assert!(
+            matches!(err, crate::Error::ConfigInvalid { ref message }
+                if message.contains("Row tracking config must enabled")),
+            "dedicated VECTOR storage should require row-tracking.enabled, got 
{err:?}"
+        );
+    }
+
+    #[test]
+    fn test_vector_store_rejects_vector_only_table() {
+        let err = Schema::builder()
+            .column("embedding", vector_4f())
+            .option("vector.file.format", "vortex")
+            .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
+            .build()
+            .unwrap_err();
+        assert!(
+            matches!(err, crate::Error::ConfigInvalid { ref message }
+                if message.contains("must have other normal columns")),
+            "dedicated VECTOR storage should require a normal anchor file, got 
{err:?}"
+        );
+    }
+
     #[test]
     fn test_apply_changes_revalidates_vector_bucket_key() {
         let table_schema = TableSchema::new(
diff --git a/crates/paimon/src/table/data_evolution_reader.rs 
b/crates/paimon/src/table/data_evolution_reader.rs
index 7bebb5c..ccb9cae 100644
--- a/crates/paimon/src/table/data_evolution_reader.rs
+++ b/crates/paimon/src/table/data_evolution_reader.rs
@@ -24,7 +24,7 @@ use crate::arrow::format::FilePredicates;
 use crate::deletion_vector::{DeletionVector, DeletionVectorFactory};
 use crate::io::FileIO;
 use crate::spec::{DataField, DataFileMeta, DataType, Predicate, 
ROW_ID_FIELD_NAME};
-use crate::table::blob_file_writer::is_blob_file_name;
+use crate::table::dedicated_format_file_writer::is_blob_file_name;
 use crate::table::schema_manager::SchemaManager;
 use crate::table::ArrowRecordBatchStream;
 use crate::table::RowRange;
@@ -609,7 +609,8 @@ async fn resolve_descriptor_columns(
                 .downcast_ref::<arrow_array::BinaryArray>()
             {
                 let resolved =
-                    super::blob_file_writer::resolve_blob_column(bin_col, 
file_io).await?;
+                    
super::dedicated_format_file_writer::resolve_blob_column(bin_col, file_io)
+                        .await?;
                 columns.push(Arc::new(resolved));
                 changed = true;
                 continue;
diff --git a/crates/paimon/src/table/data_evolution_writer.rs 
b/crates/paimon/src/table/data_evolution_writer.rs
index 2228b78..49057e9 100644
--- a/crates/paimon/src/table/data_evolution_writer.rs
+++ b/crates/paimon/src/table/data_evolution_writer.rs
@@ -29,8 +29,8 @@
 use crate::deletion_vector::{DeletionVector, DeletionVectorFactory};
 use crate::io::FileIO;
 use crate::spec::{
-    BinaryRow, CoreOptions, DataField, DataFileMeta, DeletionVectorMeta, 
FileKind, IndexFileMeta,
-    IndexManifest, PartitionComputer,
+    BinaryRow, CoreOptions, DataField, DataFileMeta, DataType, 
DeletionVectorMeta, FileKind,
+    IndexFileMeta, IndexManifest, PartitionComputer,
 };
 use crate::table::commit_message::CommitMessage;
 use crate::table::data_file_writer::DataFileWriter;
@@ -48,6 +48,7 @@ use futures::TryStreamExt;
 use indexmap::IndexMap;
 use roaring::RoaringBitmap;
 use std::collections::{HashMap, HashSet};
+use std::sync::Arc;
 use uuid::Uuid;
 
 const DELETION_VECTORS_INDEX_TYPE: &str = "DELETION_VECTORS";
@@ -884,7 +885,9 @@ struct MatchedRow {
 // ---------------------------------------------------------------------------
 
 /// Key: (partition_bytes, bucket, first_row_id)
-type WriterKey = (Vec<u8>, i32, i64);
+type WriterBaseKey = (Vec<u8>, i32, i64);
+/// Key: (partition_bytes, bucket, first_row_id, file kind)
+type WriterKey = (Vec<u8>, i32, i64, PartialFileKind);
 type PartialCommitGroup = (Option<i64>, Vec<DataFileMeta>);
 
 /// Writer for data evolution partial-column files.
@@ -894,25 +897,41 @@ type PartialCommitGroup = (Option<i64>, 
Vec<DataFileMeta>);
 /// Each output file contains only the updated columns and shares the same 
`first_row_id` range
 /// as the original file, allowing the reader to merge columns at read time.
 ///
-/// Produces parquet files containing only the specified `write_columns`, with
+/// Produces files containing only the specified `write_columns`, with
 /// `file_source = APPEND (0)`, caller-supplied `first_row_id`, and 
`write_cols`.
+/// VECTOR columns use dedicated `*.vector.<format>` files when 
`vector.file.format`
+/// is configured.
 pub(crate) struct DataEvolutionPartialWriter {
     file_io: FileIO,
     table_location: String,
     partition_computer: PartitionComputer,
     partition_keys: Vec<String>,
     schema_id: i64,
-    target_file_size: i64,
     file_compression: String,
     file_compression_zstd_level: i32,
     write_buffer_size: i64,
-    file_format: String,
     format_options: HashMap<String, String>,
+    write_sets: Vec<PartialWriteSet>,
+    /// Writers keyed by (partition_bytes, bucket, first_row_id, file kind).
+    writers: HashMap<WriterKey, DataFileWriter>,
+    check_from_snapshots: HashMap<WriterBaseKey, i64>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+enum PartialFileKind {
+    Normal,
+    Vector,
+}
+
+#[derive(Clone)]
+struct PartialWriteSet {
+    kind: PartialFileKind,
+    target_file_size: i64,
+    file_format: String,
     write_fields: Vec<DataField>,
     write_columns: Vec<String>,
-    /// Writers keyed by (partition_bytes, bucket, first_row_id).
-    writers: HashMap<WriterKey, DataFileWriter>,
-    check_from_snapshots: HashMap<WriterKey, i64>,
+    column_indices: Vec<usize>,
+    schema: Arc<arrow_schema::Schema>,
 }
 
 impl DataEvolutionPartialWriter {
@@ -932,19 +951,7 @@ impl DataEvolutionPartialWriter {
 
         let partition_keys: Vec<String> = schema.partition_keys().to_vec();
         let fields = schema.fields();
-        let write_fields = write_columns
-            .iter()
-            .map(|column| {
-                fields
-                    .iter()
-                    .find(|field| field.name() == column)
-                    .cloned()
-                    .ok_or_else(|| crate::Error::DataInvalid {
-                        message: format!("Unknown data-evolution write column 
'{column}'"),
-                        source: None,
-                    })
-            })
-            .collect::<Result<Vec<_>>>()?;
+        let write_sets = Self::build_write_sets(&write_columns, fields, 
&core_options)?;
         let partition_computer = PartitionComputer::new(
             &partition_keys,
             fields,
@@ -958,19 +965,83 @@ impl DataEvolutionPartialWriter {
             partition_computer,
             partition_keys,
             schema_id: schema.id(),
-            target_file_size: core_options.target_file_size(),
             file_compression: core_options.file_compression().to_string(),
             file_compression_zstd_level: 
core_options.file_compression_zstd_level(),
             write_buffer_size: core_options.write_parquet_buffer_size(),
-            file_format: core_options.file_format().to_string(),
             format_options: schema.options().clone(),
-            write_fields,
-            write_columns,
+            write_sets,
             writers: HashMap::new(),
             check_from_snapshots: HashMap::new(),
         })
     }
 
+    fn build_write_sets(
+        write_columns: &[String],
+        fields: &[DataField],
+        core_options: &CoreOptions<'_>,
+    ) -> Result<Vec<PartialWriteSet>> {
+        let vector_file_format = core_options
+            .vector_file_format()
+            .map(|format| format.trim().to_ascii_lowercase());
+        let mut normal_fields = Vec::new();
+        let mut normal_columns = Vec::new();
+        let mut normal_indices = Vec::new();
+        let mut vector_fields = Vec::new();
+        let mut vector_columns = Vec::new();
+        let mut vector_indices = Vec::new();
+
+        for (idx, column) in write_columns.iter().enumerate() {
+            let field = fields
+                .iter()
+                .find(|field| field.name() == column)
+                .cloned()
+                .ok_or_else(|| crate::Error::DataInvalid {
+                    message: format!("Unknown data-evolution write column 
'{column}'"),
+                    source: None,
+                })?;
+
+            if vector_file_format.is_some() && matches!(field.data_type(), 
DataType::Vector(_)) {
+                vector_fields.push(field);
+                vector_columns.push(column.clone());
+                vector_indices.push(idx);
+            } else {
+                normal_fields.push(field);
+                normal_columns.push(column.clone());
+                normal_indices.push(idx);
+            }
+        }
+
+        let mut write_sets = Vec::new();
+        if !normal_fields.is_empty() {
+            let schema = 
crate::arrow::build_target_arrow_schema(&normal_fields)?;
+            write_sets.push(PartialWriteSet {
+                kind: PartialFileKind::Normal,
+                target_file_size: core_options.target_file_size(),
+                file_format: core_options.file_format().to_string(),
+                write_fields: normal_fields,
+                write_columns: normal_columns,
+                column_indices: normal_indices,
+                schema,
+            });
+        }
+
+        if !vector_fields.is_empty() {
+            let schema = 
crate::arrow::build_target_arrow_schema(&vector_fields)?;
+            let vector_file_format = vector_file_format.expect("vector fields 
require a format");
+            write_sets.push(PartialWriteSet {
+                kind: PartialFileKind::Vector,
+                target_file_size: core_options.vector_target_file_size(),
+                file_format: format!("vector.{vector_file_format}"),
+                write_fields: vector_fields,
+                write_columns: vector_columns,
+                column_indices: vector_indices,
+                schema,
+            });
+        }
+
+        Ok(write_sets)
+    }
+
     /// Write a partial-column batch for a specific partition, bucket, and row 
ID range.
     ///
     /// The `batch` must contain only the columns specified in `write_columns`.
@@ -987,55 +1058,85 @@ impl DataEvolutionPartialWriter {
             return Ok(());
         }
 
-        let key = (partition_bytes.clone(), bucket, first_row_id);
+        let base_key = (partition_bytes.clone(), bucket, first_row_id);
         self.check_from_snapshots
-            .entry(key.clone())
+            .entry(base_key.clone())
             .and_modify(|snapshot| *snapshot = 
(*snapshot).min(check_from_snapshot))
             .or_insert(check_from_snapshot);
-        if !self.writers.contains_key(&key) {
-            let partition_path = if self.partition_keys.is_empty() {
-                String::new()
-            } else {
-                let row = BinaryRow::from_serialized_bytes(&partition_bytes)?;
-                self.partition_computer.generate_partition_path(&row)?
-            };
 
-            let writer = DataFileWriter::new(
-                self.file_io.clone(),
-                self.table_location.clone(),
-                partition_path,
+        let partition_path = if self.partition_keys.is_empty() {
+            String::new()
+        } else {
+            let row = BinaryRow::from_serialized_bytes(&partition_bytes)?;
+            self.partition_computer.generate_partition_path(&row)?
+        };
+
+        for write_set in self.write_sets.clone() {
+            let key = (
+                partition_bytes.clone(),
                 bucket,
-                self.schema_id,
-                self.target_file_size,
-                self.file_compression.clone(),
-                self.file_compression_zstd_level,
-                self.write_buffer_size,
-                self.file_format.clone(),
-                self.write_fields.clone(),
-                self.format_options.clone(),
-                Some(0), // file_source: APPEND
-                Some(first_row_id),
-                Some(self.write_columns.clone()),
+                first_row_id,
+                write_set.kind,
             );
-            self.writers.insert(key.clone(), writer);
+            if !self.writers.contains_key(&key) {
+                let writer = DataFileWriter::new(
+                    self.file_io.clone(),
+                    self.table_location.clone(),
+                    partition_path.clone(),
+                    bucket,
+                    self.schema_id,
+                    write_set.target_file_size,
+                    self.file_compression.clone(),
+                    self.file_compression_zstd_level,
+                    self.write_buffer_size,
+                    write_set.file_format.clone(),
+                    write_set.write_fields.clone(),
+                    self.format_options.clone(),
+                    Some(0), // file_source: APPEND
+                    Some(first_row_id),
+                    Some(write_set.write_columns.clone()),
+                );
+                self.writers.insert(key.clone(), writer);
+            }
+
+            let projected_batch = Self::project_batch(&batch, &write_set)?;
+            let writer = self.writers.get_mut(&key).unwrap();
+            writer.write(&projected_batch).await?;
         }
 
-        let writer = self.writers.get_mut(&key).unwrap();
-        writer.write(&batch).await
+        Ok(())
+    }
+
+    fn project_batch(batch: &RecordBatch, write_set: &PartialWriteSet) -> 
Result<RecordBatch> {
+        let columns: Vec<ArrayRef> = write_set
+            .column_indices
+            .iter()
+            .map(|&idx| batch.column(idx).clone())
+            .collect();
+        RecordBatch::try_new(write_set.schema.clone(), columns).map_err(|e| {
+            crate::Error::DataInvalid {
+                message: format!(
+                    "Failed to project data-evolution columns {:?}: {e}",
+                    write_set.write_columns
+                ),
+                source: None,
+            }
+        })
     }
 
     /// Close all writers and collect CommitMessages for use with TableCommit.
     pub async fn prepare_commit(&mut self) -> Result<Vec<CommitMessage>> {
         let writers: Vec<(WriterKey, DataFileWriter)> = 
self.writers.drain().collect();
-        let mut check_from_snapshots = std::mem::take(&mut 
self.check_from_snapshots);
+        let check_from_snapshots = std::mem::take(&mut 
self.check_from_snapshots);
 
         let futures: Vec<_> = writers
             .into_iter()
             .map(|(key, mut writer)| {
-                let check_from_snapshot = check_from_snapshots.remove(&key);
+                let base_key = (key.0.clone(), key.1, key.2);
+                let check_from_snapshot = 
check_from_snapshots.get(&base_key).copied();
                 async move {
                     let files = writer.prepare_commit().await?;
-                    let (partition_bytes, bucket, _first_row_id) = key;
+                    let (partition_bytes, bucket, _first_row_id, _kind) = key;
                     Ok::<_, crate::Error>((partition_bytes, bucket, 
check_from_snapshot, files))
                 }
             })
@@ -1074,7 +1175,7 @@ mod tests {
     use super::*;
     use crate::catalog::Identifier;
     use crate::io::FileIOBuilder;
-    use crate::spec::{DataType, IntType, Schema, TableSchema, VarCharType};
+    use crate::spec::{DataType, FloatType, IntType, Schema, TableSchema, 
VarCharType, VectorType};
     use arrow_array::StringArray;
     use arrow_schema::{DataType as ArrowDataType, Field as ArrowField, Schema 
as ArrowSchema};
     use std::sync::Arc;
@@ -1128,6 +1229,22 @@ mod tests {
         TableSchema::new(0, &schema)
     }
 
+    fn test_vector_data_evolution_schema(vector_file_format: &str) -> 
TableSchema {
+        let vector_type = DataType::Vector(
+            VectorType::try_new(true, 2, 
DataType::Float(FloatType::new())).unwrap(),
+        );
+        let schema = Schema::builder()
+            .column("id", DataType::Int(IntType::new()))
+            .column("name", DataType::VarChar(VarCharType::string_type()))
+            .column("embedding", vector_type)
+            .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
+            .option("vector.file.format", vector_file_format)
+            .build()
+            .unwrap();
+        TableSchema::new(0, &schema)
+    }
+
     fn test_table(file_io: &FileIO, table_path: &str) -> Table {
         Table::new(
             file_io.clone(),
@@ -1158,6 +1275,36 @@ mod tests {
         RecordBatch::try_new(schema, 
vec![Arc::new(StringArray::from(names))]).unwrap()
     }
 
+    fn make_partial_name_vector_batch(names: Vec<&str>, vectors: 
Vec<Vec<f32>>) -> RecordBatch {
+        use arrow_array::builder::{FixedSizeListBuilder, Float32Builder};
+
+        let element_field = Arc::new(ArrowField::new("element", 
ArrowDataType::Float32, true));
+        let mut builder =
+            FixedSizeListBuilder::new(Float32Builder::new(), 
2).with_field(element_field.clone());
+        for vector in vectors {
+            assert_eq!(vector.len(), 2);
+            for value in vector {
+                builder.values().append_value(value);
+            }
+            builder.append(true);
+        }
+        let embedding = builder.finish();
+
+        let schema = Arc::new(ArrowSchema::new(vec![
+            ArrowField::new("name", ArrowDataType::Utf8, true),
+            ArrowField::new(
+                "embedding",
+                ArrowDataType::FixedSizeList(element_field, 2),
+                true,
+            ),
+        ]));
+        RecordBatch::try_new(
+            schema,
+            vec![Arc::new(StringArray::from(names)), Arc::new(embedding)],
+        )
+        .unwrap()
+    }
+
     fn make_matched_batch(row_ids: Vec<Option<i64>>, names: Vec<&str>) -> 
RecordBatch {
         let schema = Arc::new(ArrowSchema::new(vec![
             ArrowField::new("_ROW_ID", ArrowDataType::Int64, true),
@@ -1221,6 +1368,56 @@ mod tests {
         assert_eq!(meta.file_source, Some(0));
     }
 
+    #[tokio::test]
+    async fn test_write_partial_vector_column_uses_dedicated_file() {
+        let file_io = test_file_io();
+        let table_path = "memory:/test_de_write_vector";
+        setup_dirs(&file_io, table_path).await;
+
+        let table = Table::new(
+            file_io,
+            Identifier::new("default", "test_de_vector_table"),
+            table_path.to_string(),
+            test_vector_data_evolution_schema("parquet"),
+            None,
+        );
+        let mut writer = DataEvolutionPartialWriter::new(
+            &table,
+            vec!["name".to_string(), "embedding".to_string()],
+        )
+        .unwrap();
+
+        let batch = make_partial_name_vector_batch(
+            vec!["alice", "bob"],
+            vec![vec![1.0, 0.0], vec![0.0, 1.0]],
+        );
+        writer
+            .write_partial_batch(vec![], 0, 42, 7, batch)
+            .await
+            .unwrap();
+
+        let messages = writer.prepare_commit().await.unwrap();
+        assert_eq!(messages.len(), 1);
+        assert_eq!(messages[0].new_files.len(), 2);
+        assert_eq!(messages[0].check_from_snapshot, Some(7));
+
+        let normal_file = messages[0]
+            .new_files
+            .iter()
+            .find(|file| !file.file_name.contains(".vector."))
+            .unwrap();
+        let vector_file = messages[0]
+            .new_files
+            .iter()
+            .find(|file| file.file_name.ends_with(".vector.parquet"))
+            .unwrap();
+
+        assert_eq!(normal_file.first_row_id, Some(42));
+        assert_eq!(normal_file.write_cols, Some(vec!["name".to_string()]));
+        assert_eq!(vector_file.first_row_id, Some(42));
+        assert_eq!(vector_file.write_cols, 
Some(vec!["embedding".to_string()]));
+    }
+
     #[tokio::test]
     async fn test_different_first_row_id_creates_separate_files() {
         let file_io = test_file_io();
diff --git a/crates/paimon/src/table/blob_file_writer.rs 
b/crates/paimon/src/table/dedicated_format_file_writer.rs
similarity index 66%
rename from crates/paimon/src/table/blob_file_writer.rs
rename to crates/paimon/src/table/dedicated_format_file_writer.rs
index b6572f6..347d435 100644
--- a/crates/paimon/src/table/blob_file_writer.rs
+++ b/crates/paimon/src/table/dedicated_format_file_writer.rs
@@ -16,7 +16,7 @@
 // under the License.
 
 use crate::io::FileIO;
-use crate::spec::{BlobDescriptor, DataFileMeta};
+use crate::spec::{BlobDescriptor, DataField, DataFileMeta, DataType};
 use crate::table::data_file_writer::DataFileWriter;
 use crate::Result;
 use arrow_array::builder::BinaryBuilder;
@@ -34,22 +34,32 @@ struct BlobFieldWriter {
     column_index: usize,
 }
 
-/// Writes append-only data with blob columns split into separate `.blob` 
files.
+struct VectorFieldWriter {
+    writer: DataFileWriter,
+    field_names: Vec<String>,
+    column_indices: Vec<usize>,
+    schema: Arc<arrow_schema::Schema>,
+}
+
+/// Writes append-only data with columns split into dedicated file formats.
 ///
-/// Normal (non-blob) columns go to a parquet `DataFileWriter`.
-/// Each blob column (not in `blob_descriptor_fields`) gets its own 
`DataFileWriter`
-/// with `file_format = "blob"` and `write_cols = Some(vec![field_name])`.
+/// Remaining columns go to the table's normal append `DataFileWriter`.
+/// Each non-descriptor blob column gets its own `DataFileWriter` with
+/// `file_format = "blob"` and `write_cols = Some(vec![field_name])`.
+/// When `vector.file.format` is configured, all VECTOR columns are written
+/// together to a dedicated `*.vector.<format>` file.
 ///
 /// If a blob value is already a serialized `BlobDescriptor`, the actual data 
is
 /// resolved from the referenced URI and written to the `.blob` file.
-pub(crate) struct AppendBlobFileWriter {
+pub(crate) struct AppendDedicatedFormatFileWriter {
     normal_writer: DataFileWriter,
     blob_writers: Vec<BlobFieldWriter>,
+    vector_writer: Option<VectorFieldWriter>,
     normal_column_indices: Vec<usize>,
     normal_schema: Arc<arrow_schema::Schema>,
 }
 
-impl AppendBlobFileWriter {
+impl AppendDedicatedFormatFileWriter {
     #[allow(clippy::too_many_arguments)]
     pub(crate) fn new(
         file_io: FileIO,
@@ -63,8 +73,10 @@ impl AppendBlobFileWriter {
         file_compression_zstd_level: i32,
         write_buffer_size: i64,
         file_format: String,
+        vector_target_file_size: i64,
+        vector_file_format: Option<&str>,
         input_schema: &arrow_schema::Schema,
-        table_fields: &[crate::spec::DataField],
+        table_fields: &[DataField],
         format_options: &HashMap<String, String>,
         blob_descriptor_fields: &HashSet<String>,
     ) -> Self {
@@ -72,12 +84,23 @@ impl AppendBlobFileWriter {
         let mut normal_arrow_fields = Vec::new();
         let mut normal_table_fields = Vec::new();
         let mut blob_writers = Vec::new();
+        let mut vector_column_indices = Vec::new();
+        let mut vector_arrow_fields = Vec::new();
+        let mut vector_table_fields = Vec::new();
+        let mut vector_field_names = Vec::new();
 
         for (idx, field) in table_fields.iter().enumerate() {
             let is_blob = field.data_type().is_blob_type();
             let is_descriptor = blob_descriptor_fields.contains(field.name());
+            let is_dedicated_vector =
+                vector_file_format.is_some() && matches!(field.data_type(), 
DataType::Vector(_));
 
-            if is_blob && !is_descriptor {
+            if is_dedicated_vector {
+                vector_column_indices.push(idx);
+                vector_arrow_fields.push(input_schema.field(idx).clone());
+                vector_table_fields.push(field.clone());
+                vector_field_names.push(field.name().to_string());
+            } else if is_blob && !is_descriptor {
                 blob_writers.push(BlobFieldWriter {
                     writer: DataFileWriter::new(
                         file_io.clone(),
@@ -107,6 +130,37 @@ impl AppendBlobFileWriter {
         }
 
         let normal_schema = 
Arc::new(arrow_schema::Schema::new(normal_arrow_fields));
+        let vector_writer = if let Some(vector_file_format) = 
vector_file_format {
+            if vector_table_fields.is_empty() {
+                None
+            } else {
+                let vector_schema = 
Arc::new(arrow_schema::Schema::new(vector_arrow_fields));
+                Some(VectorFieldWriter {
+                    writer: DataFileWriter::new(
+                        file_io.clone(),
+                        table_location.clone(),
+                        partition_path.clone(),
+                        bucket,
+                        schema_id,
+                        vector_target_file_size,
+                        file_compression.clone(),
+                        file_compression_zstd_level,
+                        write_buffer_size,
+                        format!("vector.{}", 
vector_file_format.trim().to_ascii_lowercase()),
+                        vector_table_fields,
+                        format_options.clone(),
+                        Some(0),
+                        None,
+                        Some(vector_field_names.clone()),
+                    ),
+                    field_names: vector_field_names,
+                    column_indices: vector_column_indices,
+                    schema: vector_schema,
+                })
+            }
+        } else {
+            None
+        };
 
         let normal_writer = DataFileWriter::new(
             file_io.clone(),
@@ -129,6 +183,7 @@ impl AppendBlobFileWriter {
         Self {
             normal_writer,
             blob_writers,
+            vector_writer,
             normal_column_indices,
             normal_schema,
         }
@@ -170,6 +225,28 @@ impl AppendBlobFileWriter {
             blob_writer.writer.write(&blob_batch).await?;
         }
 
+        if let Some(vector_writer) = &mut self.vector_writer {
+            let vector_columns: Vec<Arc<dyn arrow_array::Array>> = 
vector_writer
+                .column_indices
+                .iter()
+                .map(|&idx| batch.column(idx).clone())
+                .collect();
+            let vector_batch =
+                match RecordBatch::try_new(vector_writer.schema.clone(), 
vector_columns) {
+                    Ok(batch) => batch,
+                    Err(e) => {
+                        return Err(crate::Error::DataInvalid {
+                            message: format!(
+                                "Failed to project vector columns {:?}: {e}",
+                                vector_writer.field_names
+                            ),
+                            source: None,
+                        });
+                    }
+                };
+            vector_writer.writer.write(&vector_batch).await?;
+        }
+
         Ok(())
     }
 
@@ -181,6 +258,11 @@ impl AppendBlobFileWriter {
             results.extend(blob_metas);
         }
 
+        if let Some(vector_writer) = &mut self.vector_writer {
+            let vector_metas = vector_writer.writer.prepare_commit().await?;
+            results.extend(vector_metas);
+        }
+
         Ok(results)
     }
 }
diff --git a/crates/paimon/src/table/mod.rs b/crates/paimon/src/table/mod.rs
index 70eef2d..28e3a6a 100644
--- a/crates/paimon/src/table/mod.rs
+++ b/crates/paimon/src/table/mod.rs
@@ -20,7 +20,6 @@
 pub(crate) mod aggregator;
 pub(crate) mod bin_pack;
 mod bitmap_global_index_reader;
-mod blob_file_writer;
 mod branch_manager;
 mod btree_global_index_build_builder;
 mod btree_global_index_drop_builder;
@@ -37,6 +36,7 @@ mod data_evolution_reader;
 pub mod data_evolution_writer;
 mod data_file_reader;
 mod data_file_writer;
+mod dedicated_format_file_writer;
 #[cfg(feature = "fulltext")]
 mod full_text_search_builder;
 pub(crate) mod global_index_build_common;
diff --git a/crates/paimon/src/table/source.rs 
b/crates/paimon/src/table/source.rs
index 88ccfd0..c4254b6 100644
--- a/crates/paimon/src/table/source.rs
+++ b/crates/paimon/src/table/source.rs
@@ -28,7 +28,7 @@ fn is_vector_store_file_name(file_name: &str) -> bool {
 }
 
 pub(crate) fn is_data_evolution_normal_file(file: &DataFileMeta) -> bool {
-    !crate::table::blob_file_writer::is_blob_file_name(&file.file_name)
+    
!crate::table::dedicated_format_file_writer::is_blob_file_name(&file.file_name)
         && !is_vector_store_file_name(&file.file_name)
 }
 
diff --git a/crates/paimon/src/table/table_commit.rs 
b/crates/paimon/src/table/table_commit.rs
index 453d8aa..76aaf50 100644
--- a/crates/paimon/src/table/table_commit.rs
+++ b/crates/paimon/src/table/table_commit.rs
@@ -2622,7 +2622,7 @@ fn ranges_overlap(left_start: i64, left_end: i64, 
right_start: i64, right_end: i
 }
 
 fn is_blob_data_file(file: &DataFileMeta) -> bool {
-    crate::table::blob_file_writer::is_blob_file_name(&file.file_name)
+    
crate::table::dedicated_format_file_writer::is_blob_file_name(&file.file_name)
 }
 
 fn is_vector_store_file(file: &DataFileMeta) -> bool {
diff --git a/crates/paimon/src/table/table_scan.rs 
b/crates/paimon/src/table/table_scan.rs
index 67184a2..71ad467 100644
--- a/crates/paimon/src/table/table_scan.rs
+++ b/crates/paimon/src/table/table_scan.rs
@@ -455,7 +455,7 @@ fn is_vector_store_file_name(file_name: &str) -> bool {
 }
 
 fn is_normal_data_file(file: &DataFileMeta) -> bool {
-    !crate::table::blob_file_writer::is_blob_file_name(&file.file_name)
+    
!crate::table::dedicated_format_file_writer::is_blob_file_name(&file.file_name)
         && !is_vector_store_file_name(&file.file_name)
 }
 
diff --git a/crates/paimon/src/table/table_write.rs 
b/crates/paimon/src/table/table_write.rs
index 157d3cb..1854c53 100644
--- a/crates/paimon/src/table/table_write.rs
+++ b/crates/paimon/src/table/table_write.rs
@@ -26,7 +26,6 @@ use crate::spec::{
     first_row_supports_changelog_producer, BinaryRow, ChangelogProducer, 
CoreOptions, DataField,
     DataType, MergeEngine, EMPTY_SERIALIZED_ROW, POSTPONE_BUCKET,
 };
-use crate::table::blob_file_writer::AppendBlobFileWriter;
 use crate::table::bucket_assigner::{BucketAssignerEnum, PartitionBucketKey};
 use crate::table::bucket_assigner_constant::ConstantBucketAssigner;
 use crate::table::bucket_assigner_cross::CrossPartitionAssigner;
@@ -35,6 +34,7 @@ use crate::table::bucket_assigner_fixed::FixedBucketAssigner;
 use crate::table::bucket_function::validate_bucket_function;
 use crate::table::commit_message::CommitMessage;
 use crate::table::data_file_writer::DataFileWriter;
+use 
crate::table::dedicated_format_file_writer::AppendDedicatedFormatFileWriter;
 use crate::table::kv_file_writer::{KeyValueFileWriter, KeyValueWriteConfig};
 use crate::table::partition_filter::PartitionFilter;
 use crate::table::postpone_file_writer::{PostponeFileWriter, 
PostponeWriteConfig};
@@ -48,7 +48,7 @@ use std::sync::Arc;
 /// Enum to hold either an append-only writer, a key-value writer, or a 
postpone writer.
 enum FileWriter {
     Append(DataFileWriter),
-    AppendBlob(AppendBlobFileWriter),
+    AppendDedicated(Box<AppendDedicatedFormatFileWriter>),
     KeyValue(KeyValueFileWriter),
     Postpone(PostponeFileWriter),
 }
@@ -57,7 +57,7 @@ impl FileWriter {
     async fn write(&mut self, batch: &RecordBatch) -> Result<()> {
         match self {
             FileWriter::Append(w) => w.write(batch).await,
-            FileWriter::AppendBlob(w) => w.write(batch).await,
+            FileWriter::AppendDedicated(w) => w.write(batch).await,
             FileWriter::KeyValue(w) => w.write(batch).await,
             FileWriter::Postpone(w) => w.write(batch).await,
         }
@@ -66,7 +66,9 @@ impl FileWriter {
     async fn prepare_commit(mut self) -> Result<PreparedFiles> {
         match self {
             FileWriter::Append(ref mut w) => 
w.prepare_commit().await.map(PreparedFiles::data),
-            FileWriter::AppendBlob(ref mut w) => 
w.prepare_commit().await.map(PreparedFiles::data),
+            FileWriter::AppendDedicated(ref mut w) => {
+                w.prepare_commit().await.map(PreparedFiles::data)
+            }
             FileWriter::KeyValue(ref mut w) => w.prepare_commit().await,
             FileWriter::Postpone(ref mut w) => 
w.prepare_commit().await.map(PreparedFiles::data),
         }
@@ -90,6 +92,7 @@ pub struct TableWrite {
     schema_id: i64,
     target_file_size: i64,
     blob_target_file_size: i64,
+    vector_target_file_size: i64,
     file_compression: String,
     file_compression_zstd_level: i32,
     write_buffer_size: i64,
@@ -110,8 +113,12 @@ pub struct TableWrite {
     is_overwrite: bool,
     /// Blob descriptor fields (stored inline in parquet, not as separate 
.blob files).
     blob_descriptor_fields: HashSet<String>,
-    /// Whether the table has non-descriptor blob fields requiring 
AppendBlobFileWriter.
+    /// Whether the table has non-descriptor blob fields requiring a 
dedicated-format writer.
     has_blob_fields: bool,
+    /// Dedicated vector-store file format, when configured.
+    vector_file_format: Option<String>,
+    /// Whether the table has VECTOR fields requiring dedicated vector files.
+    has_dedicated_vector_fields: bool,
 }
 
 impl TableWrite {
@@ -175,9 +182,11 @@ impl TableWrite {
         }
         let target_file_size = core_options.target_file_size();
         let blob_target_file_size = core_options.blob_target_file_size();
+        let vector_target_file_size = core_options.vector_target_file_size();
         let file_compression = core_options.file_compression().to_string();
         let file_compression_zstd_level = 
core_options.file_compression_zstd_level();
         let file_format = core_options.file_format().to_string();
+        let vector_file_format = 
core_options.vector_file_format().map(str::to_string);
         let changelog_file_prefix = 
core_options.changelog_file_prefix().to_string();
         let changelog_file_format = 
core_options.changelog_file_format().to_string();
         let changelog_file_compression = 
core_options.changelog_file_compression().to_string();
@@ -305,6 +314,11 @@ impl TableWrite {
             .fields()
             .iter()
             .any(|f| f.data_type().is_blob_type() && 
!blob_descriptor_fields.contains(f.name()));
+        let has_dedicated_vector_fields = vector_file_format.is_some()
+            && schema
+                .fields()
+                .iter()
+                .any(|f| matches!(f.data_type(), DataType::Vector(_)));
 
         Ok(Self {
             table: table.clone(),
@@ -314,6 +328,7 @@ impl TableWrite {
             schema_id: schema.id(),
             target_file_size,
             blob_target_file_size,
+            vector_target_file_size,
             file_compression,
             file_compression_zstd_level,
             write_buffer_size,
@@ -332,6 +347,8 @@ impl TableWrite {
             is_overwrite,
             blob_descriptor_fields,
             has_blob_fields,
+            vector_file_format,
+            has_dedicated_vector_fields,
         })
     }
 
@@ -624,25 +641,29 @@ impl TableWrite {
 
     /// Create an append-only writer for non-PK tables.
     fn create_append_writer(&self, partition_path: String, bucket: i32) -> 
Result<FileWriter> {
-        if self.has_blob_fields {
+        if self.has_blob_fields || self.has_dedicated_vector_fields {
             let fields = self.table.schema().fields();
             let input_schema = build_target_arrow_schema(fields)?;
-            Ok(FileWriter::AppendBlob(AppendBlobFileWriter::new(
-                self.table.file_io().clone(),
-                self.table.location().to_string(),
-                partition_path,
-                bucket,
-                self.schema_id,
-                self.target_file_size,
-                self.blob_target_file_size,
-                self.file_compression.clone(),
-                self.file_compression_zstd_level,
-                self.write_buffer_size,
-                self.file_format.clone(),
-                &input_schema,
-                fields,
-                self.table.schema().options(),
-                &self.blob_descriptor_fields,
+            Ok(FileWriter::AppendDedicated(Box::new(
+                AppendDedicatedFormatFileWriter::new(
+                    self.table.file_io().clone(),
+                    self.table.location().to_string(),
+                    partition_path,
+                    bucket,
+                    self.schema_id,
+                    self.target_file_size,
+                    self.blob_target_file_size,
+                    self.file_compression.clone(),
+                    self.file_compression_zstd_level,
+                    self.write_buffer_size,
+                    self.file_format.clone(),
+                    self.vector_target_file_size,
+                    self.vector_file_format.as_deref(),
+                    &input_schema,
+                    fields,
+                    self.table.schema().options(),
+                    &self.blob_descriptor_fields,
+                ),
             )))
         } else {
             Ok(FileWriter::Append(DataFileWriter::new(
@@ -745,9 +766,10 @@ mod tests {
     use crate::io::{FileIO, FileIOBuilder};
     use crate::spec::{
         bucket_dir_name, BigIntType, BinaryRowBuilder, BlobType, DataField, 
DataType, DecimalType,
-        FileKind, IndexManifest, IntType, LocalZonedTimestampType, Manifest, 
ManifestList, Schema,
-        TableSchema, TimestampType, TinyIntType, VarCharType, 
SEQUENCE_NUMBER_FIELD_ID,
-        SEQUENCE_NUMBER_FIELD_NAME, VALUE_KIND_FIELD_ID, VALUE_KIND_FIELD_NAME,
+        FileKind, FloatType, IndexManifest, IntType, LocalZonedTimestampType, 
Manifest,
+        ManifestList, Schema, TableSchema, TimestampType, TinyIntType, 
VarCharType, VectorType,
+        SEQUENCE_NUMBER_FIELD_ID, SEQUENCE_NUMBER_FIELD_NAME, 
VALUE_KIND_FIELD_ID,
+        VALUE_KIND_FIELD_NAME,
     };
     use crate::table::{SnapshotManager, TableCommit};
     use arrow_array::RecordBatchReader as _;
@@ -810,6 +832,21 @@ mod tests {
         TableSchema::new(0, &schema)
     }
 
+    fn test_vector_table_schema(vector_file_format: &str) -> TableSchema {
+        let vector_type = DataType::Vector(
+            VectorType::try_new(true, 2, 
DataType::Float(FloatType::new())).unwrap(),
+        );
+        let schema = Schema::builder()
+            .column("id", DataType::Int(IntType::new()))
+            .column("embedding", vector_type)
+            .option("data-evolution.enabled", "true")
+            .option("row-tracking.enabled", "true")
+            .option("vector.file.format", vector_file_format)
+            .build()
+            .unwrap();
+        TableSchema::new(0, &schema)
+    }
+
     async fn setup_dirs(file_io: &FileIO, table_path: &str) {
         file_io
             .mkdirs(&format!("{table_path}/snapshot/"))
@@ -836,6 +873,36 @@ mod tests {
         .unwrap()
     }
 
+    fn make_vector_batch(ids: Vec<i32>, vectors: Vec<Vec<f32>>) -> RecordBatch 
{
+        use arrow_array::builder::{FixedSizeListBuilder, Float32Builder};
+
+        let element_field = Arc::new(ArrowField::new("element", 
ArrowDataType::Float32, true));
+        let mut builder =
+            FixedSizeListBuilder::new(Float32Builder::new(), 
2).with_field(element_field.clone());
+        for vector in vectors {
+            assert_eq!(vector.len(), 2);
+            for value in vector {
+                builder.values().append_value(value);
+            }
+            builder.append(true);
+        }
+        let embedding = builder.finish();
+
+        let schema = Arc::new(ArrowSchema::new(vec![
+            ArrowField::new("id", ArrowDataType::Int32, false),
+            ArrowField::new(
+                "embedding",
+                ArrowDataType::FixedSizeList(element_field, 2),
+                true,
+            ),
+        ]));
+        RecordBatch::try_new(
+            schema,
+            vec![Arc::new(Int32Array::from(ids)), Arc::new(embedding)],
+        )
+        .unwrap()
+    }
+
     fn make_batch_with_value_kind(
         ids: Vec<i32>,
         values: Vec<i32>,
@@ -1156,6 +1223,79 @@ mod tests {
         assert_eq!(snapshot.id(), 1);
     }
 
+    async fn assert_vector_write_uses_dedicated_file(table_path: &str, 
vector_file_format: &str) {
+        let file_io = test_file_io();
+        setup_dirs(&file_io, table_path).await;
+
+        let table = Table::new(
+            file_io.clone(),
+            Identifier::new("default", "test_vector_table"),
+            table_path.to_string(),
+            test_vector_table_schema(vector_file_format),
+            None,
+        );
+
+        let mut table_write = TableWrite::new(&table, 
"test-user".to_string()).unwrap();
+        let batch = make_vector_batch(
+            vec![1, 2, 3],
+            vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![0.5, 0.5]],
+        );
+
+        table_write.write_arrow_batch(&batch).await.unwrap();
+        let messages = table_write.prepare_commit().await.unwrap();
+
+        assert_eq!(messages.len(), 1);
+        assert_eq!(messages[0].new_files.len(), 2);
+
+        let vector_suffix = format!(".vector.{vector_file_format}");
+        let normal_files: Vec<_> = messages[0]
+            .new_files
+            .iter()
+            .filter(|f| !f.file_name.contains(".vector."))
+            .collect();
+        let vector_files: Vec<_> = messages[0]
+            .new_files
+            .iter()
+            .filter(|f| f.file_name.ends_with(&vector_suffix))
+            .collect();
+
+        assert_eq!(normal_files.len(), 1);
+        assert_eq!(vector_files.len(), 1);
+        assert_eq!(normal_files[0].row_count, 3);
+        assert_eq!(vector_files[0].row_count, 3);
+        assert_eq!(
+            vector_files[0].write_cols,
+            Some(vec!["embedding".to_string()])
+        );
+
+        TableCommit::new(table, "test-user".to_string())
+            .commit(messages)
+            .await
+            .unwrap();
+        let snap_manager = SnapshotManager::new(file_io.clone(), 
table_path.to_string());
+        let snapshot = 
snap_manager.get_latest_snapshot().await.unwrap().unwrap();
+        assert_eq!(snapshot.id(), 1);
+    }
+
+    #[tokio::test]
+    async fn test_vector_write_uses_dedicated_parquet_file() {
+        assert_vector_write_uses_dedicated_file(
+            "memory:/test_vector_write_dedicated_parquet",
+            "parquet",
+        )
+        .await;
+    }
+
+    #[cfg(feature = "vortex")]
+    #[tokio::test]
+    async fn test_vector_write_uses_dedicated_vortex_file() {
+        assert_vector_write_uses_dedicated_file(
+            "memory:/test_vector_write_dedicated_vortex",
+            "vortex",
+        )
+        .await;
+    }
+
     #[test]
     fn test_allows_partial_update_fixed_bucket_table() {
         let table = Table::new(

Reply via email to