Skip to main content

vortex_array/arrays/chunked/vtable/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::hash::Hasher;
5
6use itertools::Itertools;
7use smallvec::SmallVec;
8use vortex_error::VortexExpect;
9use vortex_error::VortexResult;
10use vortex_error::vortex_bail;
11use vortex_error::vortex_ensure;
12use vortex_error::vortex_err;
13use vortex_error::vortex_panic;
14use vortex_session::VortexSession;
15use vortex_session::registry::CachedId;
16
17use crate::ArrayEq;
18use crate::ArrayHash;
19use crate::ArrayRef;
20use crate::Canonical;
21use crate::EqMode;
22use crate::ExecutionCtx;
23use crate::ExecutionResult;
24use crate::IntoArray;
25use crate::VortexSessionExecute;
26use crate::array::Array;
27use crate::array::ArrayId;
28use crate::array::ArrayParts;
29use crate::array::ArrayView;
30use crate::array::VTable;
31use crate::array::with_empty_buffers;
32use crate::arrays::PrimitiveArray;
33use crate::arrays::chunked::ChunkedArrayExt;
34use crate::arrays::chunked::ChunkedData;
35use crate::arrays::chunked::array::ChunkedSlots;
36use crate::arrays::chunked::compute::rules::PARENT_RULES;
37use crate::arrays::chunked::vtable::canonical::_canonicalize;
38use crate::buffer::BufferHandle;
39use crate::builders::ArrayBuilder;
40use crate::dtype::DType;
41use crate::dtype::Nullability;
42use crate::dtype::PType;
43use crate::serde::ArrayChildren;
44mod canonical;
45mod operations;
46mod validity;
47
48/// A [`Chunked`]-encoded Vortex array.
49pub type ChunkedArray = Array<Chunked>;
50
51#[derive(Clone, Debug)]
52pub struct Chunked;
53
54impl ArrayHash for ChunkedData {
55    fn array_hash<H: Hasher>(&self, _state: &mut H, _accuracy: EqMode) {
56        // Chunk offsets are cached derived data. Slot 0 already stores the logical offsets array,
57        // and ArrayData hashing includes every slot before TypedArrayData.
58    }
59}
60
61impl ArrayEq for ChunkedData {
62    fn array_eq(&self, _other: &Self, _accuracy: EqMode) -> bool {
63        // Chunk offsets are cached derived data. Slot 0 already stores the logical offsets array,
64        // and ArrayData equality compares every slot before TypedArrayData.
65        true
66    }
67}
68
69impl VTable for Chunked {
70    type TypedArrayData = ChunkedData;
71
72    type OperationsVTable = Self;
73    type ValidityVTable = Self;
74    fn id(&self) -> ArrayId {
75        static ID: CachedId = CachedId::new("vortex.chunked");
76        *ID
77    }
78
79    fn validate(
80        &self,
81        data: &ChunkedData,
82        dtype: &DType,
83        len: usize,
84        slots: &[Option<ArrayRef>],
85    ) -> VortexResult<()> {
86        vortex_ensure!(
87            !slots.is_empty(),
88            "ChunkedArray must have at least a chunk offsets slot"
89        );
90        let chunk_offsets = slots[ChunkedSlots::CHUNK_OFFSETS]
91            .as_ref()
92            .vortex_expect("validated chunk offsets slot");
93        vortex_ensure!(
94            chunk_offsets.dtype() == &DType::Primitive(PType::U64, Nullability::NonNullable),
95            "ChunkedArray chunk offsets must be non-nullable u64, found {}",
96            chunk_offsets.dtype()
97        );
98        vortex_ensure!(
99            chunk_offsets.len() == data.chunk_offsets.len(),
100            "ChunkedArray chunk offsets slot length {} does not match cached offsets length {}",
101            chunk_offsets.len(),
102            data.chunk_offsets.len()
103        );
104        vortex_ensure!(
105            data.chunk_offsets.len() == slots.len() - ChunkedSlots::CHUNKS_OFFSET + 1,
106            "ChunkedArray chunk offsets length {} does not match {} chunks",
107            data.chunk_offsets.len(),
108            slots.len() - ChunkedSlots::CHUNKS_OFFSET
109        );
110        vortex_ensure!(
111            data.chunk_offsets
112                .last()
113                .copied()
114                .vortex_expect("chunked arrays always have a leading 0 offset")
115                == len,
116            "ChunkedArray length {} does not match outer length {}",
117            data.chunk_offsets.last().copied().unwrap_or_default(),
118            len
119        );
120        for (idx, (start, end)) in data
121            .chunk_offsets
122            .iter()
123            .copied()
124            .tuple_windows()
125            .enumerate()
126        {
127            let chunk = slots[ChunkedSlots::CHUNKS_OFFSET + idx]
128                .as_ref()
129                .vortex_expect("validated chunk slot");
130            vortex_ensure!(
131                chunk.dtype() == dtype,
132                "ChunkedArray chunk dtype {} does not match outer dtype {}",
133                chunk.dtype(),
134                dtype
135            );
136            vortex_ensure!(
137                chunk.len() == end - start,
138                "ChunkedArray chunk {} len {} does not match offsets span {}",
139                idx,
140                chunk.len(),
141                end - start
142            );
143        }
144        Ok(())
145    }
146
147    fn nbuffers(_array: ArrayView<'_, Self>) -> usize {
148        0
149    }
150
151    fn buffer(_array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
152        vortex_panic!("ChunkedArray buffer index {idx} out of bounds")
153    }
154
155    fn buffer_name(_array: ArrayView<'_, Self>, idx: usize) -> Option<String> {
156        vortex_panic!("ChunkedArray buffer_name index {idx} out of bounds")
157    }
158
159    fn with_buffers(
160        &self,
161        array: ArrayView<'_, Self>,
162        buffers: &[BufferHandle],
163    ) -> VortexResult<ArrayParts<Self>> {
164        with_empty_buffers(self, array, buffers)
165    }
166
167    fn serialize(
168        _array: ArrayView<'_, Self>,
169        _session: &VortexSession,
170    ) -> VortexResult<Option<Vec<u8>>> {
171        Ok(Some(vec![]))
172    }
173
174    fn deserialize(
175        &self,
176        dtype: &DType,
177        len: usize,
178        metadata: &[u8],
179        _buffers: &[BufferHandle],
180        children: &dyn ArrayChildren,
181        session: &VortexSession,
182    ) -> VortexResult<ArrayParts<Self>> {
183        if !metadata.is_empty() {
184            vortex_bail!(
185                "ChunkedArray expects empty metadata, got {} bytes",
186                metadata.len()
187            );
188        }
189        if children.is_empty() {
190            vortex_bail!("Chunked array needs at least one child");
191        }
192
193        let nchunks = children.len() - 1;
194        let chunk_offsets = children.get(
195            ChunkedSlots::CHUNK_OFFSETS,
196            &DType::Primitive(PType::U64, Nullability::NonNullable),
197            nchunks + 1,
198        )?;
199        let mut ctx = session.create_execution_ctx();
200        let chunk_offsets_buf = chunk_offsets
201            .clone()
202            .execute::<PrimitiveArray>(&mut ctx)?
203            .to_buffer::<u64>();
204        let chunk_offsets_usize = chunk_offsets_buf
205            .iter()
206            .copied()
207            .map(|offset| {
208                usize::try_from(offset)
209                    .map_err(|_| vortex_err!("chunk offset {offset} exceeds usize range"))
210            })
211            .collect::<VortexResult<Vec<_>>>()?;
212        let mut slots = SmallVec::with_capacity(children.len());
213        slots.push(Some(chunk_offsets));
214        for (idx, (start, end)) in chunk_offsets_usize
215            .iter()
216            .copied()
217            .tuple_windows()
218            .enumerate()
219        {
220            let chunk_len = end - start;
221            slots.push(Some(children.get(
222                idx + ChunkedSlots::CHUNKS_OFFSET,
223                dtype,
224                chunk_len,
225            )?));
226        }
227
228        Ok(ArrayParts::new(
229            self.clone(),
230            dtype.clone(),
231            len,
232            ChunkedData::new(chunk_offsets_usize),
233        )
234        .with_slots(slots))
235    }
236
237    fn append_to_builder(
238        array: ArrayView<'_, Self>,
239        builder: &mut dyn ArrayBuilder,
240        ctx: &mut ExecutionCtx,
241    ) -> VortexResult<()> {
242        for chunk in array.iter_chunks() {
243            chunk.append_to_builder(builder, ctx)?;
244        }
245        Ok(())
246    }
247
248    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
249        match idx {
250            ChunkedSlots::CHUNK_OFFSETS => "chunk_offsets".to_string(),
251            n => format!("chunks[{}]", n - ChunkedSlots::CHUNKS_OFFSET),
252        }
253    }
254
255    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
256        match array.dtype() {
257            DType::Union(..) => {
258                todo!(
259                    "TODO(connor)[Union]: canonicalize chunked Union arrays by packing type IDs and \
260                     every sparse child along identical chunk boundaries"
261                )
262            }
263            // Struct, List, FixedSizeList, and Variant need child swizzling that the builder path
264            // cannot express.
265            DType::Struct(..) | DType::List(..) | DType::FixedSizeList(..) | DType::Variant(..) => {
266                // TODO(joe)[#7674]: iterative execution here too
267                Ok(ExecutionResult::done(_canonicalize(array.as_view(), ctx)?))
268            }
269            // For all other types, use the builder path via AppendChild.
270            _ => {
271                let slot_idx = array.next_builder_slot.max(ChunkedSlots::CHUNKS_OFFSET);
272                if slot_idx < array.slots().len() {
273                    Ok(ExecutionResult::append_child(
274                        array.with_next_builder_slot(slot_idx + 1),
275                        slot_idx,
276                    ))
277                } else {
278                    Ok(ExecutionResult::done(
279                        Canonical::empty(array.dtype()).into_array(),
280                    ))
281                }
282            }
283        }
284    }
285
286    fn reduce(array: ArrayView<'_, Self>) -> VortexResult<Option<ArrayRef>> {
287        Ok(match array.nchunks() {
288            0 => Some(Canonical::empty(array.dtype()).into_array()),
289            1 => Some(array.chunk(0).clone()),
290            _ => None,
291        })
292    }
293
294    fn reduce_parent(
295        array: ArrayView<'_, Self>,
296        parent: &ArrayRef,
297        child_idx: usize,
298    ) -> VortexResult<Option<ArrayRef>> {
299        PARENT_RULES.evaluate(array, parent, child_idx)
300    }
301}