Skip to content

Commit da19bca

Browse files
authored
Add Arrow to Vortex conversion to C FFI (#7906)
Accepting arrow is useful to support integration with existing systems Signed-off-by: Robert Kruszewski <github@robertk.io>
1 parent 919e31e commit da19bca

6 files changed

Lines changed: 217 additions & 0 deletions

File tree

vortex-ffi/cbindgen.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ header = """
2222
// #include "nanoarrow/common/inline_types.h"
2323
// #define USE_OWN_ARROW
2424
// typedef struct ArrowSchema FFI_ArrowSchema;
25+
// typedef struct ArrowArray FFI_ArrowArray;
2526
// typedef struct ArrowArrayStream FFI_ArrowArrayStream;
2627
// #include "vortex.h"
2728
//
@@ -57,6 +58,7 @@ struct ArrowArrayStream {
5758
void* private_data;
5859
};
5960
typedef struct ArrowSchema FFI_ArrowSchema;
61+
typedef struct ArrowArray FFI_ArrowArray;
6062
typedef struct ArrowArrayStream FFI_ArrowArrayStream;
6163
#endif
6264
"""

vortex-ffi/cinclude/vortex.h

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// #include "nanoarrow/common/inline_types.h"
1515
// #define USE_OWN_ARROW
1616
// typedef struct ArrowSchema FFI_ArrowSchema;
17+
// typedef struct ArrowArray FFI_ArrowArray;
1718
// typedef struct ArrowArrayStream FFI_ArrowArrayStream;
1819
// #include "vortex.h"
1920
//
@@ -49,6 +50,7 @@ struct ArrowArrayStream {
4950
void *private_data;
5051
};
5152
typedef struct ArrowSchema FFI_ArrowSchema;
53+
typedef struct ArrowArray FFI_ArrowArray;
5254
typedef struct ArrowArrayStream FFI_ArrowArrayStream;
5355
#endif
5456

@@ -787,6 +789,34 @@ const vx_array *vx_array_new_primitive(vx_ptype ptype,
787789
const vx_validity *validity,
788790
vx_error **error);
789791

792+
/**
793+
* Create a Vortex array by importing an Arrow array via the Arrow C Data Interface.
794+
*
795+
* `array` and `schema` together describe a single Arrow array (the standard Arrow C Data
796+
* Interface pair, e.g. as produced by exporting a record batch). Both are *consumed*: their
797+
* `release` callbacks are invoked by this function and the caller must not use or release them
798+
* afterwards.
799+
*
800+
* `nullable` controls the top-level nullability of the resulting array's dtype. For an Arrow
801+
* record batch (which has no top-level validity) pass `false`.
802+
*
803+
* The imported buffers are referenced zero-copy where possible; the returned array keeps the
804+
* Arrow data alive until it is freed with [`vx_array_free`].
805+
*
806+
* On error, returns NULL and sets `error_out`.
807+
*
808+
* Example:
809+
*
810+
* // export an Arrow record batch into (array, schema), then:
811+
* vx_error* error = NULL;
812+
* const vx_array* vx = vx_array_from_arrow(&array, &schema, false, &error);
813+
* // ... push it to a sink or write it ...
814+
* vx_array_free(vx);
815+
*
816+
*/
817+
const vx_array *
818+
vx_array_from_arrow(FFI_ArrowArray *array, FFI_ArrowSchema *schema, bool nullable, vx_error **error_out);
819+
790820
uint8_t vx_array_get_u8(const vx_array *array, size_t index);
791821

792822
uint8_t vx_array_get_storage_u8(const vx_array *array, size_t index);
@@ -1078,6 +1108,18 @@ const vx_string *vx_dtype_time_zone(const DType *dtype);
10781108
*/
10791109
int vx_dtype_to_arrow_schema(const vx_dtype *dtype, FFI_ArrowSchema *schema, vx_error **err);
10801110

1111+
/**
1112+
* Create a Vortex dtype from an Arrow C Data Interface schema.
1113+
*
1114+
* `schema` must point to a valid `ArrowSchema` describing a struct (record-batch) schema. It is
1115+
* *consumed*: its `release` callback is invoked by this function and the caller must not use or
1116+
* release it afterwards. The returned dtype is a non-nullable struct, mirroring how Arrow record
1117+
* batches map to Vortex arrays.
1118+
*
1119+
* On error, returns NULL and sets `err`.
1120+
*/
1121+
const vx_dtype *vx_dtype_from_arrow_schema(FFI_ArrowSchema *schema, vx_error **err);
1122+
10811123
/**
10821124
* Free an owned [`vx_error`] object.
10831125
*/

vortex-ffi/examples/scan_to_arrow.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#define USE_OWN_ARROW
77
typedef struct ArrowSchema FFI_ArrowSchema;
8+
typedef struct ArrowArray FFI_ArrowArray;
89
typedef struct ArrowArrayStream FFI_ArrowArrayStream;
910
#include "vortex.h"
1011

vortex-ffi/src/array.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ use std::ffi::c_void;
77
use std::ptr;
88
use std::sync::Arc;
99

10+
use arrow_array::array::make_array;
11+
use arrow_array::ffi::FFI_ArrowArray;
12+
use arrow_array::ffi::FFI_ArrowSchema;
13+
use arrow_array::ffi::from_ffi;
1014
use paste::paste;
1115
use vortex::array::ArrayRef;
1216
use vortex::array::IntoArray;
@@ -16,6 +20,7 @@ use vortex::array::arrays::NullArray;
1620
use vortex::array::arrays::PrimitiveArray;
1721
use vortex::array::arrays::StructArray;
1822
use vortex::array::arrays::struct_::StructArrayExt;
23+
use vortex::array::arrow::FromArrowArray;
1924
use vortex::array::validity::Validity;
2025
use vortex::buffer::Buffer;
2126
use vortex::dtype::DType;
@@ -326,6 +331,49 @@ pub extern "C-unwind" fn vx_array_new_primitive(
326331
}
327332
}
328333

334+
/// Create a Vortex array by importing an Arrow array via the Arrow C Data Interface.
335+
///
336+
/// `array` and `schema` together describe a single Arrow array (the standard Arrow C Data
337+
/// Interface pair, e.g. as produced by exporting a record batch). Both are *consumed*: their
338+
/// `release` callbacks are invoked by this function and the caller must not use or release them
339+
/// afterwards.
340+
///
341+
/// `nullable` controls the top-level nullability of the resulting array's dtype. For an Arrow
342+
/// record batch (which has no top-level validity) pass `false`.
343+
///
344+
/// The imported buffers are referenced zero-copy where possible; the returned array keeps the
345+
/// Arrow data alive until it is freed with [`vx_array_free`].
346+
///
347+
/// On error, returns NULL and sets `error_out`.
348+
///
349+
/// Example:
350+
///
351+
/// // export an Arrow record batch into (array, schema), then:
352+
/// vx_error* error = NULL;
353+
/// const vx_array* vx = vx_array_from_arrow(&array, &schema, false, &error);
354+
/// // ... push it to a sink or write it ...
355+
/// vx_array_free(vx);
356+
///
357+
#[unsafe(no_mangle)]
358+
pub unsafe extern "C-unwind" fn vx_array_from_arrow(
359+
array: *mut FFI_ArrowArray,
360+
schema: *mut FFI_ArrowSchema,
361+
nullable: bool,
362+
error_out: *mut *mut vx_error,
363+
) -> *const vx_array {
364+
try_or_default(error_out, || {
365+
vortex_ensure!(!array.is_null(), "null arrow array");
366+
vortex_ensure!(!schema.is_null(), "null arrow schema");
367+
let ffi_array = unsafe { ptr::replace(array, FFI_ArrowArray::empty()) };
368+
let ffi_schema = unsafe { ptr::replace(schema, FFI_ArrowSchema::empty()) };
369+
let array_data = unsafe { from_ffi(ffi_array, &ffi_schema) }?;
370+
drop(ffi_schema);
371+
let arrow_array = make_array(array_data);
372+
let vortex_array = ArrayRef::from_arrow(arrow_array.as_ref(), nullable)?;
373+
Ok(vx_array::new(Arc::new(vortex_array)))
374+
})
375+
}
376+
329377
macro_rules! ffiarray_get_ptype {
330378
($ptype:ident) => {
331379
paste! {
@@ -809,4 +857,67 @@ mod tests {
809857
// Note: dtype_ptr is now invalid - this test documents the lifetime pattern
810858
// In real usage, don't access dtype_ptr after freeing the array
811859
}
860+
861+
#[test]
862+
#[cfg_attr(miri, ignore)]
863+
fn test_from_arrow_roundtrip() {
864+
use arrow_array::Array as ArrowArrayTrait;
865+
use arrow_array::Int32Array;
866+
use arrow_array::RecordBatch;
867+
use arrow_array::StringArray;
868+
use arrow_array::ffi::to_ffi;
869+
use arrow_schema::DataType;
870+
use arrow_schema::Field;
871+
use arrow_schema::Schema as ArrowSchema;
872+
873+
let schema = Arc::new(ArrowSchema::new(vec![
874+
Field::new("a", DataType::Int32, false),
875+
Field::new("b", DataType::Utf8, true),
876+
]));
877+
let batch = RecordBatch::try_new(
878+
schema,
879+
vec![
880+
Arc::new(Int32Array::from(vec![1, 2, 3])),
881+
Arc::new(StringArray::from(vec![Some("x"), None, Some("z")])),
882+
],
883+
)
884+
.unwrap();
885+
886+
let data = ArrowArrayTrait::into_data(arrow_array::StructArray::from(batch));
887+
let (mut ffi_array, mut ffi_schema) = to_ffi(&data).unwrap();
888+
889+
let mut error = ptr::null_mut();
890+
let vx = unsafe {
891+
vx_array_from_arrow(
892+
&raw mut ffi_array,
893+
&raw mut ffi_schema,
894+
false,
895+
&raw mut error,
896+
)
897+
};
898+
assert_no_error(error);
899+
assert!(!vx.is_null());
900+
901+
unsafe {
902+
assert!(vx_array_has_dtype(vx, vx_dtype_variant::DTYPE_STRUCT));
903+
assert_eq!(vx_array_len(vx), 3);
904+
assert!(!vx_array_is_nullable(vx));
905+
906+
let a = vx_array_get_field(vx, 0, &raw mut error);
907+
assert_no_error(error);
908+
assert!(vx_array_is_primitive(a, vx_ptype::PTYPE_I32));
909+
assert_eq!(vx_array_get_i32(a, 0), 1);
910+
assert_eq!(vx_array_get_i32(a, 2), 3);
911+
vx_array_free(a);
912+
913+
let b = vx_array_get_field(vx, 1, &raw mut error);
914+
assert_no_error(error);
915+
assert!(vx_array_has_dtype(b, vx_dtype_variant::DTYPE_UTF8));
916+
assert!(vx_array_element_is_invalid(b, 1, &raw mut error));
917+
assert_no_error(error);
918+
vx_array_free(b);
919+
920+
vx_array_free(vx);
921+
}
922+
}
812923
}

vortex-ffi/src/dtype.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ use std::ptr;
66
use std::sync::Arc;
77

88
use arrow_array::ffi::FFI_ArrowSchema;
9+
use arrow_schema::Schema;
910
use vortex::dtype::DType;
1011
use vortex::dtype::DecimalDType;
12+
use vortex::dtype::arrow::FromArrowType;
1113
use vortex::error::VortexExpect;
14+
use vortex::error::vortex_ensure;
1215
use vortex::error::vortex_panic;
1316
use vortex::extension::datetime::AnyTemporal;
1417
use vortex::extension::datetime::Date;
@@ -17,6 +20,7 @@ use vortex::extension::datetime::Timestamp;
1720

1821
use crate::arc_wrapper;
1922
use crate::error::try_or;
23+
use crate::error::try_or_default;
2024
use crate::error::vx_error;
2125
use crate::ptype::vx_ptype;
2226
use crate::string::vx_string;
@@ -347,6 +351,28 @@ pub unsafe extern "C-unwind" fn vx_dtype_to_arrow_schema(
347351
})
348352
}
349353

354+
/// Create a Vortex dtype from an Arrow C Data Interface schema.
355+
///
356+
/// `schema` must point to a valid `ArrowSchema` describing a struct (record-batch) schema. It is
357+
/// *consumed*: its `release` callback is invoked by this function and the caller must not use or
358+
/// release it afterwards. The returned dtype is a non-nullable struct, mirroring how Arrow record
359+
/// batches map to Vortex arrays.
360+
///
361+
/// On error, returns NULL and sets `err`.
362+
#[unsafe(no_mangle)]
363+
pub unsafe extern "C-unwind" fn vx_dtype_from_arrow_schema(
364+
schema: *mut FFI_ArrowSchema,
365+
err: *mut *mut vx_error,
366+
) -> *const vx_dtype {
367+
try_or_default(err, || {
368+
vortex_ensure!(!schema.is_null(), "null arrow schema");
369+
let ffi_schema = unsafe { ptr::replace(schema, FFI_ArrowSchema::empty()) };
370+
let arrow_schema = Schema::try_from(&ffi_schema)?;
371+
drop(ffi_schema);
372+
Ok(vx_dtype::new(Arc::new(DType::from_arrow(&arrow_schema))))
373+
})
374+
}
375+
350376
#[cfg(test)]
351377
#[expect(clippy::cast_possible_truncation)]
352378
mod tests {
@@ -744,4 +770,38 @@ mod tests {
744770
vx_array_free(vx_arr);
745771
}
746772
}
773+
774+
#[test]
775+
fn test_dtype_from_arrow_schema() {
776+
use arrow_schema::DataType;
777+
use arrow_schema::Field;
778+
779+
let arrow_schema = Schema::new(vec![
780+
Field::new("a", DataType::Int64, false),
781+
Field::new("b", DataType::Utf8, true),
782+
]);
783+
let mut ffi_schema = FFI_ArrowSchema::try_from(&arrow_schema).unwrap();
784+
785+
let mut error = ptr::null_mut();
786+
let dtype = unsafe { vx_dtype_from_arrow_schema(&raw mut ffi_schema, &raw mut error) };
787+
assert!(error.is_null());
788+
assert!(!dtype.is_null());
789+
790+
unsafe {
791+
assert_eq!(vx_dtype_get_variant(dtype), vx_dtype_variant::DTYPE_STRUCT);
792+
assert!(!vx_dtype_is_nullable(dtype));
793+
let fields = vx_dtype_struct_dtype(dtype);
794+
assert_eq!(vx_struct_fields_nfields(fields), 2);
795+
let f0 = vx_struct_fields_field_dtype(fields, 0);
796+
assert_eq!(vx_dtype_get_variant(f0), vx_dtype_variant::DTYPE_PRIMITIVE);
797+
assert_eq!(vx_dtype_primitive_ptype(f0), vx_ptype::PTYPE_I64);
798+
assert!(!vx_dtype_is_nullable(f0));
799+
vx_dtype_free(f0);
800+
let f1 = vx_struct_fields_field_dtype(fields, 1);
801+
assert_eq!(vx_dtype_get_variant(f1), vx_dtype_variant::DTYPE_UTF8);
802+
assert!(vx_dtype_is_nullable(f1));
803+
vx_dtype_free(f1);
804+
vx_dtype_free(dtype);
805+
}
806+
}
747807
}

vortex-ffi/test/scan.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <unistd.h>
1212

1313
using FFI_ArrowArrayStream = ArrowArrayStream;
14+
using FFI_ArrowArray = ArrowArray;
1415
using FFI_ArrowSchema = ArrowSchema;
1516
#define USE_OWN_ARROW 1
1617
#include <vortex.h>

0 commit comments

Comments
 (0)