Skip to main content

vortex_array/arrays/chunked/compute/
take.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use vortex_buffer::BufferMut;
5use vortex_error::VortexResult;
6use vortex_mask::Mask;
7
8use crate::ArrayRef;
9use crate::Canonical;
10use crate::IntoArray;
11use crate::array::ArrayView;
12use crate::arrays::Chunked;
13use crate::arrays::ChunkedArray;
14use crate::arrays::PrimitiveArray;
15use crate::arrays::chunked::ChunkedArrayExt;
16use crate::arrays::dict::TakeExecute;
17use crate::builtins::ArrayBuiltins;
18use crate::dtype::DType;
19use crate::dtype::PType;
20use crate::executor::ExecutionCtx;
21use crate::validity::Validity;
22
23// TODO(joe): this is pretty unoptimized but better than before. We want canonical using a builder
24// we also want to return a chunked array ideally.
25fn take_chunked(
26    array: ArrayView<'_, Chunked>,
27    indices: &ArrayRef,
28    ctx: &mut ExecutionCtx,
29) -> VortexResult<ArrayRef> {
30    let indices = indices
31        .cast(DType::Primitive(PType::U64, indices.dtype().nullability()))?
32        .execute::<PrimitiveArray>(ctx)?;
33
34    let indices_mask = indices
35        .as_ref()
36        .validity()?
37        .execute_mask(indices.as_ref().len(), ctx)?;
38    let indices_values = indices.as_slice::<u64>();
39    let n = indices_values.len();
40
41    // 1. Sort (value, orig_pos) pairs so indices for the same chunk are contiguous.
42    //    Skip null indices — their final_take slots stay 0 and are masked null by validity.
43    let mut pairs: Vec<(u64, usize)> = indices_values
44        .iter()
45        .enumerate()
46        .filter(|&(i, _)| indices_mask.value(i))
47        .map(|(i, &v)| (v, i))
48        .collect();
49    pairs.sort_unstable();
50
51    // 2. Fused pass: walk sorted pairs against chunk boundaries.
52    //    - Dedup inline → build per-chunk filter masks
53    //    - Scatter final_take[orig_pos] = dedup_idx for every pair
54    let chunk_offsets = array.chunk_offsets();
55    let nchunks = array.nchunks();
56    let mut chunks = Vec::with_capacity(nchunks);
57    let mut final_take = BufferMut::<u64>::with_capacity(n);
58    final_take.push_n(0u64, n);
59
60    let mut cursor = 0usize;
61    let mut dedup_idx = 0u64;
62
63    for chunk_idx in 0..nchunks {
64        let chunk_start = chunk_offsets[chunk_idx];
65        let chunk_end = chunk_offsets[chunk_idx + 1];
66        let chunk_len = chunk_end - chunk_start;
67        let chunk_end_u64 = u64::try_from(chunk_end)?;
68
69        let range_end = cursor + pairs[cursor..].partition_point(|&(v, _)| v < chunk_end_u64);
70        let chunk_pairs = &pairs[cursor..range_end];
71
72        if !chunk_pairs.is_empty() {
73            let mut local_indices: Vec<usize> = Vec::new();
74            for (i, &(val, orig_pos)) in chunk_pairs.iter().enumerate() {
75                if cursor + i > 0 && val != pairs[cursor + i - 1].0 {
76                    dedup_idx += 1;
77                }
78                let local = usize::try_from(val)? - chunk_start;
79                if local_indices.last() != Some(&local) {
80                    local_indices.push(local);
81                }
82                final_take[orig_pos] = dedup_idx;
83            }
84
85            let filter_mask = Mask::from_indices(chunk_len, local_indices);
86            chunks.push(array.chunk(chunk_idx).filter(filter_mask)?);
87        }
88
89        cursor = range_end;
90    }
91
92    // SAFETY: every chunk came from a filter on a chunk with the same base dtype,
93    // unioned with the index nullability.
94    let flat = unsafe { ChunkedArray::new_unchecked(chunks, array.dtype().clone()) }
95        .into_array()
96        // TODO(joe): can we relax this.
97        .execute::<Canonical>(ctx)?
98        .into_array();
99
100    // 4. Single take to restore original order and expand duplicates.
101    //    Carry the original index validity so null indices produce null outputs.
102    let take_validity = Validity::from_mask(
103        indices
104            .as_ref()
105            .validity()?
106            .execute_mask(indices.as_ref().len(), ctx)?,
107        indices.dtype().nullability(),
108    );
109    flat.take(PrimitiveArray::new(final_take.freeze(), take_validity).into_array())
110}
111
112impl TakeExecute for Chunked {
113    fn take(
114        array: ArrayView<'_, Chunked>,
115        indices: &ArrayRef,
116        ctx: &mut ExecutionCtx,
117    ) -> VortexResult<Option<ArrayRef>> {
118        take_chunked(array, indices, ctx).map(Some)
119    }
120}
121
122#[cfg(test)]
123mod test {
124    use vortex_buffer::bitbuffer;
125    use vortex_buffer::buffer;
126    use vortex_error::VortexResult;
127
128    use crate::IntoArray;
129    use crate::VortexSessionExecute;
130    use crate::array_session;
131    use crate::arrays::BoolArray;
132    use crate::arrays::ChunkedArray;
133    use crate::arrays::PrimitiveArray;
134    use crate::arrays::StructArray;
135    use crate::arrays::chunked::ChunkedArrayExt;
136    use crate::assert_arrays_eq;
137    use crate::compute::conformance::take::test_take_conformance;
138    use crate::dtype::FieldNames;
139    use crate::dtype::Nullability;
140    use crate::validity::Validity;
141
142    #[test]
143    fn test_take() {
144        let mut ctx = array_session().create_execution_ctx();
145        let a = buffer![1i32, 2, 3].into_array();
146        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
147            .unwrap();
148        assert_eq!(arr.nchunks(), 3);
149        assert_eq!(arr.len(), 9);
150        let indices = buffer![0u64, 0, 6, 4].into_array();
151
152        let result = arr.take(indices).unwrap();
153        assert_arrays_eq!(result, PrimitiveArray::from_iter([1i32, 1, 1, 2]), &mut ctx);
154    }
155
156    #[test]
157    fn test_take_nullable_values() {
158        let mut ctx = array_session().create_execution_ctx();
159        let a = PrimitiveArray::new(buffer![1i32, 2, 3], Validity::AllValid).into_array();
160        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
161            .unwrap();
162        assert_eq!(arr.nchunks(), 3);
163        assert_eq!(arr.len(), 9);
164        let indices = PrimitiveArray::new(buffer![0u64, 0, 6, 4], Validity::NonNullable);
165
166        let result = arr.take(indices.into_array()).unwrap();
167        assert_arrays_eq!(
168            result,
169            PrimitiveArray::from_option_iter([1i32, 1, 1, 2].map(Some)),
170            &mut ctx
171        );
172    }
173
174    #[test]
175    fn test_take_nullable_indices() {
176        let mut ctx = array_session().create_execution_ctx();
177        let a = buffer![1i32, 2, 3].into_array();
178        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
179            .unwrap();
180        assert_eq!(arr.nchunks(), 3);
181        assert_eq!(arr.len(), 9);
182        let indices = PrimitiveArray::new(
183            buffer![0u64, 0, 6, 4],
184            Validity::Array(bitbuffer![1 0 0 1].into_array()),
185        );
186
187        let result = arr.take(indices.into_array()).unwrap();
188        assert_arrays_eq!(
189            result,
190            PrimitiveArray::from_option_iter([Some(1i32), None, None, Some(2)]),
191            &mut ctx
192        );
193    }
194
195    #[test]
196    fn test_take_nullable_struct() {
197        let mut ctx = array_session().create_execution_ctx();
198        let struct_array =
199            StructArray::try_new(FieldNames::default(), vec![], 100, Validity::NonNullable)
200                .unwrap();
201
202        let arr = ChunkedArray::from_iter(vec![
203            struct_array.clone().into_array(),
204            struct_array.into_array(),
205        ]);
206
207        let result = arr
208            .take(PrimitiveArray::from_option_iter(vec![Some(0), None, Some(101)]).into_array())
209            .unwrap();
210
211        let expect = StructArray::try_new(
212            FieldNames::default(),
213            vec![],
214            3,
215            Validity::Array(BoolArray::from_iter(vec![true, false, true]).into_array()),
216        )
217        .unwrap();
218        assert_arrays_eq!(result, expect, &mut ctx);
219    }
220
221    #[test]
222    fn test_empty_take() {
223        let mut ctx = array_session().create_execution_ctx();
224        let a = buffer![1i32, 2, 3].into_array();
225        let arr = ChunkedArray::try_new(vec![a.clone(), a.clone(), a.clone()], a.dtype().clone())
226            .unwrap();
227        assert_eq!(arr.nchunks(), 3);
228        assert_eq!(arr.len(), 9);
229
230        let indices = PrimitiveArray::empty::<u64>(Nullability::NonNullable);
231        let result = arr.take(indices.into_array()).unwrap();
232
233        assert!(result.is_empty());
234        assert_eq!(result.dtype(), arr.dtype());
235        assert_arrays_eq!(
236            result,
237            PrimitiveArray::empty::<i32>(Nullability::NonNullable),
238            &mut ctx
239        );
240    }
241
242    #[test]
243    fn test_take_shuffled_indices() -> VortexResult<()> {
244        let mut ctx = array_session().create_execution_ctx();
245        let c0 = buffer![0i32, 1, 2].into_array();
246        let c1 = buffer![3i32, 4, 5].into_array();
247        let c2 = buffer![6i32, 7, 8].into_array();
248        let arr = ChunkedArray::try_new(
249            vec![c0, c1, c2],
250            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
251                .dtype()
252                .clone(),
253        )?;
254
255        // Fully shuffled indices that cross every chunk boundary.
256        let indices = buffer![8u64, 0, 5, 3, 2, 7, 1, 6, 4].into_array();
257        let result = arr.take(indices)?;
258
259        assert_arrays_eq!(
260            result,
261            PrimitiveArray::from_iter([8i32, 0, 5, 3, 2, 7, 1, 6, 4]),
262            &mut ctx
263        );
264        Ok(())
265    }
266
267    #[test]
268    fn test_take_shuffled_large() -> VortexResult<()> {
269        let mut ctx = array_session().create_execution_ctx();
270        let nchunks: i32 = 100;
271        let chunk_len: i32 = 1_000;
272        let total = nchunks * chunk_len;
273
274        let chunks: Vec<_> = (0..nchunks)
275            .map(|c| {
276                let start = c * chunk_len;
277                PrimitiveArray::from_iter(start..start + chunk_len).into_array()
278            })
279            .collect();
280        let dtype = chunks[0].dtype().clone();
281        let arr = ChunkedArray::try_new(chunks, dtype)?;
282
283        // Fisher-Yates shuffle with a fixed seed for determinism.
284        let mut indices: Vec<u64> = (0..u64::try_from(total)?).collect();
285        let mut seed: u64 = 0xdeadbeef;
286        for i in (1..indices.len()).rev() {
287            seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
288            let j = (seed >> 33) as usize % (i + 1);
289            indices.swap(i, j);
290        }
291
292        let indices_arr = PrimitiveArray::new(
293            vortex_buffer::Buffer::from(indices.clone()),
294            Validity::NonNullable,
295        );
296        let result = arr.take(indices_arr.into_array())?;
297
298        // Verify every element.
299        let result = result.execute::<PrimitiveArray>(&mut ctx)?;
300        let result_vals = result.as_slice::<i32>();
301        for (pos, &idx) in indices.iter().enumerate() {
302            assert_eq!(
303                result_vals[pos],
304                i32::try_from(idx)?,
305                "mismatch at position {pos}"
306            );
307        }
308        Ok(())
309    }
310
311    #[test]
312    fn test_take_null_indices() -> VortexResult<()> {
313        let mut ctx = array_session().create_execution_ctx();
314        let c0 = buffer![10i32, 20, 30].into_array();
315        let c1 = buffer![40i32, 50, 60].into_array();
316        let arr = ChunkedArray::try_new(
317            vec![c0, c1],
318            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
319                .dtype()
320                .clone(),
321        )?;
322
323        // Indices with nulls scattered across chunk boundaries.
324        let indices =
325            PrimitiveArray::from_option_iter([Some(5u64), None, Some(0), Some(3), None, Some(2)]);
326        let result = arr.take(indices.into_array())?;
327
328        assert_arrays_eq!(
329            result,
330            PrimitiveArray::from_option_iter([
331                Some(60i32),
332                None,
333                Some(10),
334                Some(40),
335                None,
336                Some(30)
337            ]),
338            &mut ctx
339        );
340        Ok(())
341    }
342
343    #[test]
344    fn test_take_chunked_conformance() {
345        let a = buffer![1i32, 2, 3].into_array();
346        let b = buffer![4i32, 5].into_array();
347        let arr = ChunkedArray::try_new(
348            vec![a, b],
349            PrimitiveArray::empty::<i32>(Nullability::NonNullable)
350                .dtype()
351                .clone(),
352        )
353        .unwrap();
354        test_take_conformance(
355            &arr.into_array(),
356            &mut array_session().create_execution_ctx(),
357        );
358
359        // Test with nullable chunked array
360        let a = PrimitiveArray::from_option_iter([Some(1i32), None, Some(3)]);
361        let b = PrimitiveArray::from_option_iter([Some(4i32), Some(5)]);
362        let dtype = a.dtype().clone();
363        let arr = ChunkedArray::try_new(vec![a.into_array(), b.into_array()], dtype).unwrap();
364        test_take_conformance(
365            &arr.into_array(),
366            &mut array_session().create_execution_ctx(),
367        );
368
369        // Test with multiple identical chunks
370        let chunk = buffer![10i32, 20, 30, 40, 50].into_array();
371        let arr = ChunkedArray::try_new(
372            vec![chunk.clone(), chunk.clone(), chunk.clone()],
373            chunk.dtype().clone(),
374        )
375        .unwrap();
376        test_take_conformance(
377            &arr.into_array(),
378            &mut array_session().create_execution_ctx(),
379        );
380    }
381}