Skip to content

Commit 9619f79

Browse files
authored
test(datafusion): add vortex SQL e2e (#321)
1 parent 2b58d25 commit 9619f79

5 files changed

Lines changed: 190 additions & 5 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ jobs:
126126
RUST_LOG: DEBUG
127127
RUST_BACKTRACE: full
128128

129+
- name: DataFusion Vortex Integration Test
130+
run: cargo test -p paimon-datafusion --features vortex --test vortex_tables
131+
env:
132+
RUST_LOG: DEBUG
133+
RUST_BACKTRACE: full
134+
129135
- name: Install uv
130136
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78
131137
with:

crates/integrations/datafusion/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ keywords = ["paimon", "datafusion", "integrations"]
2929

3030
[features]
3131
fulltext = ["paimon/fulltext"]
32+
vortex = ["paimon/vortex"]
3233

3334
[dependencies]
3435
async-trait = "0.1"

crates/integrations/datafusion/tests/common/mod.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,24 @@ pub async fn setup_sql_context() -> (TempDir, SQLContext) {
5454

5555
#[allow(dead_code)]
5656
pub async fn collect_id_name(sql_context: &SQLContext, sql: &str) -> Vec<(i32, String)> {
57+
let mut rows = collect_id_name_in_batch_order(sql_context, sql).await;
58+
rows.sort_by_key(|(id, _)| *id);
59+
rows
60+
}
61+
62+
#[allow(dead_code)]
63+
pub async fn collect_id_name_in_batch_order(
64+
sql_context: &SQLContext,
65+
sql: &str,
66+
) -> Vec<(i32, String)> {
5767
let batches = sql_context.sql(sql).await.unwrap().collect().await.unwrap();
68+
collect_id_name_from_batches_in_order(&batches)
69+
}
70+
71+
#[allow(dead_code)]
72+
pub fn collect_id_name_from_batches_in_order(batches: &[RecordBatch]) -> Vec<(i32, String)> {
5873
let mut rows = Vec::new();
59-
for batch in &batches {
74+
for batch in batches {
6075
let ids = batch
6176
.column_by_name("id")
6277
.and_then(|c| c.as_any().downcast_ref::<Int32Array>())
@@ -69,7 +84,6 @@ pub async fn collect_id_name(sql_context: &SQLContext, sql: &str) -> Vec<(i32, S
6984
rows.push((ids.value(i), names.value(i).to_string()));
7085
}
7186
}
72-
rows.sort_by_key(|(id, _)| *id);
7387
rows
7488
}
7589

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
#![cfg(feature = "vortex")]
19+
20+
//! Vortex file format SQL end-to-end tests.
21+
22+
mod common;
23+
24+
use std::path::Path;
25+
26+
#[tokio::test]
27+
async fn test_vortex_file_format_sql_e2e() {
28+
let (tmp, sql_context) = common::setup_sql_context().await;
29+
30+
common::exec(
31+
&sql_context,
32+
"CREATE TABLE paimon.test_db.t (
33+
id INT,
34+
name STRING
35+
) WITH (
36+
'file.format' = 'vortex'
37+
)",
38+
)
39+
.await;
40+
41+
common::exec(
42+
&sql_context,
43+
"INSERT INTO paimon.test_db.t VALUES (1, 'Alice'), (2, 'Bob')",
44+
)
45+
.await;
46+
47+
assert!(
48+
contains_vortex_file(tmp.path()),
49+
"expected Vortex data file"
50+
);
51+
52+
let rows = common::collect_id_name_in_batch_order(
53+
&sql_context,
54+
"SELECT id, name FROM paimon.test_db.t ORDER BY id",
55+
)
56+
.await;
57+
assert_eq!(rows, vec![(1, "Alice".to_string()), (2, "Bob".to_string())]);
58+
59+
let filtered = common::collect_id_name_in_batch_order(
60+
&sql_context,
61+
"SELECT id, name FROM paimon.test_db.t WHERE id = 2",
62+
)
63+
.await;
64+
assert_eq!(filtered, vec![(2, "Bob".to_string())]);
65+
}
66+
67+
fn contains_vortex_file(path: &Path) -> bool {
68+
let entries = std::fs::read_dir(path).expect("read warehouse dir");
69+
for entry in entries {
70+
let path = entry.expect("read dir entry").path();
71+
if path.is_dir() {
72+
if contains_vortex_file(&path) {
73+
return true;
74+
}
75+
} else if path.extension().is_some_and(|ext| ext == "vortex") {
76+
return true;
77+
}
78+
}
79+
false
80+
}

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

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::spec::{DataField, Datum, Predicate, PredicateOperator};
2121
use crate::table::{ArrowRecordBatchStream, RowRange};
2222
use crate::Error;
2323
use arrow_array::RecordBatch;
24-
use arrow_schema::SchemaRef;
24+
use arrow_schema::{DataType as ArrowDataType, SchemaRef};
2525
use async_trait::async_trait;
2626
use futures::future::BoxFuture;
2727
use futures::StreamExt;
@@ -472,7 +472,7 @@ fn vortex_array_to_record_batch(
472472
schema: &SchemaRef,
473473
) -> crate::Result<RecordBatch> {
474474
let arrow_array = vortex_array
475-
.into_arrow_preferred()
475+
.into_arrow(&ArrowDataType::Struct(schema.fields().clone()))
476476
.map_err(|e| Error::DataInvalid {
477477
message: format!("Failed to convert Vortex array to Arrow: {e}"),
478478
source: None,
@@ -486,6 +486,17 @@ fn vortex_array_to_record_batch(
486486
source: None,
487487
})?;
488488

489+
if struct_array.columns().len() != schema.fields().len() {
490+
return Err(Error::DataInvalid {
491+
message: format!(
492+
"Vortex column count {} does not match target schema column count {}",
493+
struct_array.columns().len(),
494+
schema.fields().len()
495+
),
496+
source: None,
497+
});
498+
}
499+
489500
RecordBatch::try_new(schema.clone(), struct_array.columns().to_vec()).map_err(|e| {
490501
Error::DataInvalid {
491502
message: format!("Failed to build RecordBatch from Vortex data: {e}"),
@@ -680,7 +691,8 @@ mod tests {
680691
use super::*;
681692
use crate::arrow::format::FormatFileWriter;
682693
use crate::io::FileIOBuilder;
683-
use arrow_array::Int32Array;
694+
use crate::spec::{DataField, DataType, VarCharType};
695+
use arrow_array::{Int32Array, StringArray};
684696
use arrow_schema::{DataType as ArrowDataType, Field as ArrowField, Schema as ArrowSchema};
685697

686698
fn test_arrow_schema() -> Arc<ArrowSchema> {
@@ -758,6 +770,78 @@ mod tests {
758770
assert_eq!(total_rows, 3);
759771
}
760772

773+
#[tokio::test]
774+
async fn test_vortex_reader_returns_utf8_for_string_schema() {
775+
let file_io = FileIOBuilder::new("memory").build().unwrap();
776+
let path = "memory:/test_vortex_utf8_schema.vortex";
777+
let output = file_io.new_output(path).unwrap();
778+
let schema = Arc::new(ArrowSchema::new(vec![
779+
ArrowField::new("id", ArrowDataType::Int32, false),
780+
ArrowField::new("name", ArrowDataType::Utf8, true),
781+
]));
782+
783+
let mut writer: Box<dyn FormatFileWriter> = Box::new(
784+
VortexFormatWriter::new(&output, schema.clone())
785+
.await
786+
.unwrap(),
787+
);
788+
let batch = RecordBatch::try_new(
789+
schema,
790+
vec![
791+
Arc::new(Int32Array::from(vec![1, 2])),
792+
Arc::new(StringArray::from(vec![Some("Alice"), Some("Bob")])),
793+
],
794+
)
795+
.unwrap();
796+
writer.write(&batch).await.unwrap();
797+
writer.close().await.unwrap();
798+
799+
let input = file_io.new_input(path).unwrap();
800+
let file_reader = input.reader().await.unwrap();
801+
let metadata = input.metadata().await.unwrap();
802+
let read_fields = vec![
803+
DataField::new(
804+
0,
805+
"id".to_string(),
806+
DataType::Int(crate::spec::IntType::new()),
807+
),
808+
DataField::new(
809+
1,
810+
"name".to_string(),
811+
DataType::VarChar(VarCharType::string_type()),
812+
),
813+
];
814+
815+
let reader = VortexFormatReader;
816+
let mut stream = reader
817+
.read_batch_stream(
818+
Box::new(file_reader),
819+
metadata.size,
820+
&read_fields,
821+
None,
822+
None,
823+
None,
824+
)
825+
.await
826+
.unwrap();
827+
828+
let mut names = Vec::new();
829+
while let Some(result) = stream.next().await {
830+
let batch = result.unwrap();
831+
assert_eq!(batch.schema().field(1).data_type(), &ArrowDataType::Utf8);
832+
assert_eq!(batch.column(1).data_type(), &ArrowDataType::Utf8);
833+
let name_col = batch
834+
.column(1)
835+
.as_any()
836+
.downcast_ref::<StringArray>()
837+
.unwrap();
838+
for i in 0..batch.num_rows() {
839+
names.push(name_col.value(i).to_string());
840+
}
841+
}
842+
assert_eq!(names, vec!["Alice".to_string(), "Bob".to_string()]);
843+
}
844+
761845
#[tokio::test]
762846
async fn test_vortex_writer_multiple_batches() {
763847
let file_io = FileIOBuilder::new("memory").build().unwrap();

0 commit comments

Comments
 (0)