Skip to main content

vortex_arrow/executor/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4#![expect(
5    deprecated,
6    reason = "This module defines and implements a deprecated trait `ArrowArrayExecutor`"
7)]
8
9pub mod bool;
10mod byte;
11pub mod byte_view;
12mod decimal;
13mod dictionary;
14mod fixed_size_list;
15mod list;
16mod list_view;
17mod map;
18pub mod null;
19pub mod primitive;
20mod run_end;
21mod struct_;
22mod temporal;
23mod validity;
24
25use arrow_array::ArrayRef as ArrowArrayRef;
26use arrow_array::RecordBatch;
27use arrow_array::cast::AsArray;
28use arrow_array::types::*;
29use arrow_schema::DataType;
30use arrow_schema::Field;
31use arrow_schema::FieldRef;
32use arrow_schema::Schema;
33use itertools::Itertools;
34use vortex_array::ArrayRef;
35use vortex_array::ExecutionCtx;
36use vortex_array::arrays::List;
37use vortex_array::arrays::VarBin;
38use vortex_array::arrays::list::ListArraySlotsExt;
39use vortex_array::arrays::varbin::VarBinArraySlotsExt;
40use vortex_array::dtype::DType;
41use vortex_array::dtype::PType;
42use vortex_error::VortexResult;
43use vortex_error::vortex_bail;
44use vortex_error::vortex_ensure;
45
46use crate::dtype::to_data_type_naive;
47use crate::executor::bool::to_arrow_bool;
48use crate::executor::byte::to_arrow_byte_array;
49use crate::executor::byte_view::to_arrow_byte_view;
50use crate::executor::decimal::to_arrow_decimal;
51use crate::executor::dictionary::to_arrow_dictionary;
52use crate::executor::fixed_size_list::to_arrow_fixed_list;
53use crate::executor::list::to_arrow_list;
54use crate::executor::list_view::to_arrow_list_view;
55use crate::executor::map::to_arrow_map;
56use crate::executor::null::to_arrow_null;
57use crate::executor::primitive::to_arrow_primitive;
58use crate::executor::run_end::to_arrow_run_end;
59use crate::executor::struct_::to_arrow_struct;
60use crate::executor::temporal::to_arrow_date;
61use crate::executor::temporal::to_arrow_time;
62use crate::executor::temporal::to_arrow_timestamp;
63use crate::session::ArrowSessionExt;
64
65/// Trait for executing a Vortex array to produce an Arrow array.
66#[deprecated(note = "Use an `ArrowSession` to perform conversions to/from Arrow arrays")]
67pub trait ArrowArrayExecutor: Sized {
68    /// Execute the array to produce an Arrow array.
69    ///
70    /// If a [`DataType`] is given, the array will be converted to the desired Arrow type.
71    /// If `None`, the array's preferred (cheapest) Arrow type will be used.
72    #[deprecated(note = "Use an `ArrowSession` to perform conversions to/from Arrow arrays")]
73    fn execute_arrow(
74        self,
75        data_type: Option<&DataType>,
76        ctx: &mut ExecutionCtx,
77    ) -> VortexResult<ArrowArrayRef>;
78
79    /// Execute the array to produce an Arrow `RecordBatch` with the given schema.
80    #[deprecated(note = "Use an `ArrowSession` to perform conversions to/from Arrow arrays")]
81    fn execute_record_batch(
82        self,
83        schema: &Schema,
84        ctx: &mut ExecutionCtx,
85    ) -> VortexResult<RecordBatch> {
86        let array = self.execute_arrow(Some(&DataType::Struct(schema.fields.clone())), ctx)?;
87        Ok(RecordBatch::from(array.as_struct()))
88    }
89
90    /// Execute the array to produce Arrow `RecordBatch`'s with the given schema.
91    #[deprecated(note = "Use an `ArrowSession` to perform conversions to/from Arrow arrays")]
92    fn execute_record_batches(
93        self,
94        schema: &Schema,
95        ctx: &mut ExecutionCtx,
96    ) -> VortexResult<Vec<RecordBatch>>;
97}
98
99#[expect(deprecated, reason = "backward compatibility")]
100impl ArrowArrayExecutor for ArrayRef {
101    fn execute_arrow(
102        self,
103        data_type: Option<&DataType>,
104        ctx: &mut ExecutionCtx,
105    ) -> VortexResult<ArrowArrayRef> {
106        let target = data_type.map(|dt| Field::new("", dt.clone(), self.dtype().is_nullable()));
107        let session = ctx.session().clone();
108        session.arrow().execute_arrow(self, target.as_ref(), ctx)
109    }
110
111    fn execute_record_batches(
112        self,
113        schema: &Schema,
114        ctx: &mut ExecutionCtx,
115    ) -> VortexResult<Vec<RecordBatch>> {
116        self.to_array_iterator()
117            .map(|a| a?.execute_record_batch(schema, ctx))
118            .try_collect()
119    }
120}
121
122/// Execute an arbitrary Vortex array into an Arrow array, dispatched by [`DataType`]. This pathway
123/// is naive to any extension type information, and only seeks to map canonical Vortex arrays to
124/// some target Arrow physical encoding.
125///
126/// Public callers should go through the `ArrowSession` instead.
127pub(crate) fn execute_arrow_naive(
128    array: ArrayRef,
129    data_type: Option<&DataType>,
130    ctx: &mut ExecutionCtx,
131) -> VortexResult<ArrowArrayRef> {
132    let len = array.len();
133
134    let resolved_type: DataType = match data_type {
135        Some(dt) => dt.clone(),
136        None => infer_nearest_arrow_type(&array)?,
137    };
138
139    let arrow = match &resolved_type {
140        DataType::Null => to_arrow_null(array, ctx),
141        DataType::Boolean => to_arrow_bool(array, ctx),
142        DataType::Int8 => to_arrow_primitive::<Int8Type>(array, ctx),
143        DataType::Int16 => to_arrow_primitive::<Int16Type>(array, ctx),
144        DataType::Int32 => to_arrow_primitive::<Int32Type>(array, ctx),
145        DataType::Int64 => to_arrow_primitive::<Int64Type>(array, ctx),
146        DataType::UInt8 => to_arrow_primitive::<UInt8Type>(array, ctx),
147        DataType::UInt16 => to_arrow_primitive::<UInt16Type>(array, ctx),
148        DataType::UInt32 => to_arrow_primitive::<UInt32Type>(array, ctx),
149        DataType::UInt64 => to_arrow_primitive::<UInt64Type>(array, ctx),
150        DataType::Float16 => to_arrow_primitive::<Float16Type>(array, ctx),
151        DataType::Float32 => to_arrow_primitive::<Float32Type>(array, ctx),
152        DataType::Float64 => to_arrow_primitive::<Float64Type>(array, ctx),
153        DataType::Binary => to_arrow_byte_array::<BinaryType>(array, ctx),
154        DataType::LargeBinary => to_arrow_byte_array::<LargeBinaryType>(array, ctx),
155        DataType::Utf8 => to_arrow_byte_array::<Utf8Type>(array, ctx),
156        DataType::LargeUtf8 => to_arrow_byte_array::<LargeUtf8Type>(array, ctx),
157        DataType::BinaryView => to_arrow_byte_view::<BinaryViewType>(array, ctx),
158        DataType::Utf8View => to_arrow_byte_view::<StringViewType>(array, ctx),
159        // TODO(joe): pass down preferred
160        DataType::List(elements_field) => to_arrow_list::<i32>(array, elements_field, ctx),
161        // TODO(joe): pass down preferred
162        DataType::LargeList(elements_field) => to_arrow_list::<i64>(array, elements_field, ctx),
163        // TODO(joe): pass down preferred
164        DataType::FixedSizeList(elements_field, list_size) => {
165            to_arrow_fixed_list(array, *list_size, elements_field, ctx)
166        }
167        // TODO(joe): pass down preferred
168        DataType::ListView(elements_field) => to_arrow_list_view::<i32>(array, elements_field, ctx),
169        // TODO(joe): pass down preferred
170        DataType::LargeListView(elements_field) => {
171            to_arrow_list_view::<i64>(array, elements_field, ctx)
172        }
173        DataType::Struct(fields) => {
174            let fields = if data_type.is_none() {
175                None
176            } else {
177                Some(fields)
178            };
179            to_arrow_struct(array, fields, ctx)
180        }
181        // TODO(joe): pass down preferred
182        DataType::Dictionary(codes_type, values_type) => {
183            to_arrow_dictionary(array, codes_type, values_type, ctx)
184        }
185        dt @ DataType::Decimal32(..) => to_arrow_decimal(array, dt, ctx),
186        dt @ DataType::Decimal64(..) => to_arrow_decimal(array, dt, ctx),
187        dt @ DataType::Decimal128(..) => to_arrow_decimal(array, dt, ctx),
188        dt @ DataType::Decimal256(..) => to_arrow_decimal(array, dt, ctx),
189        // TODO(joe): pass down preferred
190        DataType::RunEndEncoded(ends_type, values_type) => {
191            to_arrow_run_end(array, ends_type.data_type(), values_type, ctx)
192        }
193        dt @ (DataType::Date32 | DataType::Date64) => to_arrow_date(array, dt, ctx),
194        dt @ (DataType::Time32(_) | DataType::Time64(_)) => to_arrow_time(array, dt, ctx),
195        dt @ DataType::Timestamp(..) => to_arrow_timestamp(array, dt, ctx),
196        DataType::Map(entries_field, keys_sorted) => {
197            to_arrow_map(array, entries_field, *keys_sorted, ctx)
198        }
199        DataType::FixedSizeBinary(_)
200        | DataType::Duration(_)
201        | DataType::Interval(_)
202        | DataType::Union(..) => {
203            vortex_bail!("Conversion to Arrow type {resolved_type} is not supported");
204        }
205    }?;
206
207    vortex_ensure!(
208        arrow.len() == len,
209        "Arrow array length does not match Vortex array length after conversion to {:?}",
210        arrow
211    );
212
213    Ok(arrow)
214}
215
216/// Determine the preferred (cheapest) Arrow type for an array.
217///
218/// For most arrays, this returns the canonical Arrow type from `dtype.to_arrow_dtype()`.
219/// However, some encodings have cheaper Arrow representations:
220/// - `VarBinArray`: Uses `Utf8`/`Binary` (offset-based) instead of `Utf8View`/`BinaryView`
221/// - `ListArray`: Uses `List` instead of `ListView`
222fn infer_nearest_arrow_type(array: &ArrayRef) -> VortexResult<DataType> {
223    // VarBinArray: use offset-based Binary/Utf8 instead of View types
224    if let Some(varbin) = array.as_opt::<VarBin>() {
225        let offsets_ptype = PType::try_from(varbin.offsets().dtype())?;
226        let use_large = matches!(offsets_ptype, PType::I64 | PType::U64);
227
228        return Ok(match (varbin.dtype(), use_large) {
229            (DType::Utf8(_), false) => DataType::Utf8,
230            (DType::Utf8(_), true) => DataType::LargeUtf8,
231            (DType::Binary(_), false) => DataType::Binary,
232            (DType::Binary(_), true) => DataType::LargeBinary,
233            _ => unreachable!("VarBinArray must have Utf8 or Binary dtype"),
234        });
235    }
236
237    // ListArray: use List with appropriate offset size
238    if let Some(list) = array.as_opt::<List>() {
239        let offsets_ptype = PType::try_from(list.offsets().dtype())?;
240        let use_large = matches!(offsets_ptype, PType::I64 | PType::U64);
241        // Recursively get the preferred type for elements
242        let elem_dtype = infer_nearest_arrow_type(list.elements())?;
243        let field = FieldRef::new(Field::new_list_field(
244            elem_dtype,
245            list.elements().dtype().is_nullable(),
246        ));
247
248        return Ok(if use_large {
249            DataType::LargeList(field)
250        } else {
251            DataType::List(field)
252        });
253    }
254
255    // Everything else: use canonical dtype conversion
256    to_data_type_naive(array.dtype())
257}
258
259#[cfg(test)]
260mod tests {
261    use std::sync::Arc;
262
263    use arrow_schema::DataType;
264    use arrow_schema::Field;
265    use arrow_schema::TimeUnit;
266    use rstest::rstest;
267    use vortex_array::ArrayRef;
268    use vortex_array::IntoArray;
269    use vortex_array::VortexSessionExecute;
270    use vortex_array::array_session;
271    use vortex_array::arrays::BoolArray;
272    use vortex_array::arrays::PrimitiveArray;
273    use vortex_array::arrays::VarBinViewArray;
274
275    use crate::ArrowSessionExt;
276
277    fn utf8() -> ArrayRef {
278        VarBinViewArray::from_iter_str(["a", "bb"]).into_array()
279    }
280
281    fn primitive() -> ArrayRef {
282        PrimitiveArray::from_iter([1i32, 2]).into_array()
283    }
284
285    fn boolean() -> ArrayRef {
286        BoolArray::from_iter([true, false]).into_array()
287    }
288
289    fn list_target() -> DataType {
290        DataType::List(Arc::new(Field::new("item", DataType::Int32, true)))
291    }
292
293    /// Must error rather than panic inside `execute::<T>`.
294    #[rstest]
295    #[case::bool_from_utf8(utf8(), DataType::Boolean)]
296    #[case::bool_from_primitive(primitive(), DataType::Boolean)]
297    #[case::null_from_utf8(utf8(), DataType::Null)]
298    #[case::null_from_primitive(primitive(), DataType::Null)]
299    #[case::decimal_from_utf8(utf8(), DataType::Decimal128(10, 2))]
300    #[case::decimal_from_primitive(primitive(), DataType::Decimal128(10, 2))]
301    #[case::list_from_utf8(utf8(), list_target())]
302    #[case::list_from_primitive(primitive(), list_target())]
303    #[case::list_view_from_primitive(
304        primitive(),
305        DataType::ListView(Arc::new(Field::new("item", DataType::Int32, true)))
306    )]
307    #[case::fixed_size_list_from_utf8(
308        utf8(),
309        DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Int32, true)), 2)
310    )]
311    #[case::byte_from_primitive(primitive(), DataType::Utf8)]
312    #[case::byte_from_bool(boolean(), DataType::Binary)]
313    fn incompatible_target_returns_error(#[case] array: ArrayRef, #[case] target: DataType) {
314        let session = array_session();
315        let mut ctx = session.create_execution_ctx();
316        let field = Field::new("f", target.clone(), array.dtype().is_nullable());
317
318        let result = session.arrow().execute_arrow(array, Some(&field), &mut ctx);
319
320        assert!(
321            result.is_err(),
322            "expected an error exporting to {target:?}, got Ok"
323        );
324    }
325
326    /// Cross-class conversions that are genuinely supported must keep working.
327    #[rstest]
328    #[case::bool_to_int32(boolean(), DataType::Int32)]
329    #[case::bool_to_float64(boolean(), DataType::Float64)]
330    #[case::primitive_to_int64(primitive(), DataType::Int64)]
331    #[case::primitive_to_date32(primitive(), DataType::Date32)]
332    #[case::primitive_to_timestamp(primitive(), DataType::Timestamp(TimeUnit::Microsecond, None))]
333    #[case::utf8_to_binary(utf8(), DataType::Binary)]
334    #[case::utf8_to_large_utf8(utf8(), DataType::LargeUtf8)]
335    fn supported_cross_class_target_still_works(#[case] array: ArrayRef, #[case] target: DataType) {
336        let session = array_session();
337        let mut ctx = session.create_execution_ctx();
338        let field = Field::new("f", target.clone(), array.dtype().is_nullable());
339
340        let result = session.arrow().execute_arrow(array, Some(&field), &mut ctx);
341
342        assert!(
343            result.is_ok(),
344            "expected {target:?} export to succeed, got {:?}",
345            result.err()
346        );
347    }
348}