Skip to main content

polars_core/chunked_array/ops/
gather.rs

1#![allow(unsafe_op_in_unsafe_fn)]
2use std::sync::OnceLock;
3
4use polars_arrow::bitmap::Bitmap;
5use polars_arrow::bitmap::bitmask::BitMask;
6use polars_compute::gather::take_unchecked;
7use polars_error::polars_ensure;
8use polars_utils::index::check_bounds;
9
10use crate::prelude::*;
11use crate::series::IsSorted;
12use crate::utils::Container;
13
14pub fn check_bounds_nulls(idx: &PrimitiveArray<IdxSize>, len: IdxSize) -> PolarsResult<()> {
15    let mask = BitMask::from_bitmap(idx.validity().unwrap());
16
17    // We iterate in chunks to make the inner loop branch-free.
18    for (block_idx, block) in idx.values().chunks(32).enumerate() {
19        let mut in_bounds = 0;
20        for (i, x) in block.iter().enumerate() {
21            in_bounds |= ((*x < len) as u32) << i;
22        }
23        let m = mask.get_u32(32 * block_idx);
24        polars_ensure!(m == m & in_bounds, ComputeError: "gather indices are out of bounds");
25    }
26    Ok(())
27}
28
29pub fn check_bounds_ca(indices: &IdxCa, len: IdxSize) -> PolarsResult<()> {
30    let all_valid = indices.downcast_iter().all(|a| {
31        if a.null_count() == 0 {
32            check_bounds(a.values(), len).is_ok()
33        } else {
34            check_bounds_nulls(a, len).is_ok()
35        }
36    });
37    polars_ensure!(all_valid, OutOfBounds: "gather indices are out of bounds");
38    Ok(())
39}
40
41impl<T: PolarsDataType, I: AsRef<[IdxSize]> + ?Sized> ChunkTake<I> for ChunkedArray<T>
42where
43    ChunkedArray<T>: ChunkTakeUnchecked<I>,
44{
45    /// Gather values from ChunkedArray by index.
46    fn take(&self, indices: &I) -> PolarsResult<Self> {
47        check_bounds(indices.as_ref(), self.len() as IdxSize)?;
48
49        // SAFETY: we just checked the indices are valid.
50        Ok(unsafe { self.take_unchecked(indices) })
51    }
52}
53
54impl<T: PolarsDataType> ChunkTake<IdxCa> for ChunkedArray<T>
55where
56    ChunkedArray<T>: ChunkTakeUnchecked<IdxCa>,
57{
58    /// Gather values from ChunkedArray by index.
59    fn take(&self, indices: &IdxCa) -> PolarsResult<Self> {
60        check_bounds_ca(indices, self.len() as IdxSize)?;
61
62        // SAFETY: we just checked the indices are valid.
63        Ok(unsafe { self.take_unchecked(indices) })
64    }
65}
66
67/// Computes cumulative lengths for efficient branchless binary search
68/// lookup. The first element is always 0, and the last length of arrs
69/// is always ignored (as we already checked that all indices are
70/// in-bounds we don't need to check against the last length).
71fn cumulative_lengths<A: StaticArray>(arrs: &[&A]) -> Vec<IdxSize> {
72    let mut ret = Vec::with_capacity(arrs.len());
73    let mut cumsum: IdxSize = 0;
74    for arr in arrs {
75        ret.push(cumsum);
76        cumsum = cumsum.checked_add(arr.len().try_into().unwrap()).unwrap();
77    }
78    ret
79}
80
81#[rustfmt::skip]
82#[inline]
83fn resolve_chunked_idx(idx: IdxSize, cumlens: &[IdxSize]) -> (usize, usize) {
84    let chunk_idx = cumlens.partition_point(|cl| idx >= *cl) - 1;
85    (chunk_idx, (idx - cumlens[chunk_idx]) as usize)
86}
87
88#[inline]
89unsafe fn target_value_unchecked<'a, A: StaticArray>(
90    targets: &[&'a A],
91    cumlens: &[IdxSize],
92    idx: IdxSize,
93) -> A::ValueT<'a> {
94    let (chunk_idx, arr_idx) = resolve_chunked_idx(idx, cumlens);
95    let arr = targets.get_unchecked(chunk_idx);
96    arr.value_unchecked(arr_idx)
97}
98
99#[inline]
100unsafe fn target_get_unchecked<'a, A: StaticArray>(
101    targets: &[&'a A],
102    cumlens: &[IdxSize],
103    idx: IdxSize,
104) -> Option<A::ValueT<'a>> {
105    let (chunk_idx, arr_idx) = resolve_chunked_idx(idx, cumlens);
106    let arr = targets.get_unchecked(chunk_idx);
107    arr.get_unchecked(arr_idx)
108}
109
110unsafe fn gather_idx_array_unchecked<A: StaticArray>(
111    dtype: ArrowDataType,
112    targets: &[&A],
113    has_nulls: bool,
114    indices: &[IdxSize],
115) -> A {
116    let it = indices.iter().copied();
117    if targets.len() == 1 {
118        let target = targets.first().unwrap();
119        if has_nulls {
120            it.map(|i| target.get_unchecked(i as usize))
121                .collect_arr_trusted_with_dtype(dtype)
122        } else if let Some(sl) = target.as_slice() {
123            // Avoid the Arc overhead from value_unchecked.
124            it.map(|i| sl.get_unchecked(i as usize).clone())
125                .collect_arr_trusted_with_dtype(dtype)
126        } else {
127            it.map(|i| target.value_unchecked(i as usize))
128                .collect_arr_trusted_with_dtype(dtype)
129        }
130    } else {
131        let cumlens = cumulative_lengths(targets);
132        if has_nulls {
133            it.map(|i| target_get_unchecked(targets, &cumlens, i))
134                .collect_arr_trusted_with_dtype(dtype)
135        } else {
136            it.map(|i| target_value_unchecked(targets, &cumlens, i))
137                .collect_arr_trusted_with_dtype(dtype)
138        }
139    }
140}
141
142impl<T: PolarsDataType, I: AsRef<[IdxSize]> + ?Sized> ChunkTakeUnchecked<I> for ChunkedArray<T>
143where
144    T: PolarsDataType<HasViews = FalseT, IsStruct = FalseT, IsNested = FalseT>,
145{
146    /// Gather values from ChunkedArray by index.
147    unsafe fn take_unchecked(&self, indices: &I) -> Self {
148        let ca = self;
149        let targets: Vec<_> = ca.downcast_iter().collect();
150        let arr = gather_idx_array_unchecked(
151            ca.dtype().to_arrow(CompatLevel::newest()),
152            &targets,
153            ca.null_count() > 0,
154            indices.as_ref(),
155        );
156        ChunkedArray::from_chunk_iter_like(ca, [arr])
157    }
158}
159
160/// The sorted flag of `out`, the gather of `ca` at `indices`. If both have nulls, they can end
161/// up at both ends of `out`, and then it is not sorted.
162fn gather_sorted_flag<T: PolarsDataType>(
163    ca: &ChunkedArray<T>,
164    indices: &IdxCa,
165    out: &ChunkedArray<T>,
166) -> IsSorted {
167    let flag = _update_gather_sorted_flag(ca.is_sorted_flag(), indices.is_sorted_flag());
168    if flag == IsSorted::Not || ca.null_count() == 0 || indices.null_count() == 0 {
169        return flag;
170    }
171    // `out` has no sorted flag yet, so these scan its validity.
172    let (Some(first), Some(last)) = (out.first_non_null(), out.last_non_null()) else {
173        return flag;
174    };
175    let null_count = out.null_count();
176    if first == null_count || out.len() - 1 - last == null_count {
177        flag
178    } else {
179        IsSorted::Not
180    }
181}
182
183pub fn _update_gather_sorted_flag(sorted_arr: IsSorted, sorted_idx: IsSorted) -> IsSorted {
184    use crate::series::IsSorted::*;
185    match (sorted_arr, sorted_idx) {
186        (_, Not) => Not,
187        (Not, _) => Not,
188        (Ascending, Ascending) => Ascending,
189        (Ascending, Descending) => Descending,
190        (Descending, Ascending) => Descending,
191        (Descending, Descending) => Ascending,
192    }
193}
194
195impl<T: PolarsDataType> ChunkTakeUnchecked<IdxCa> for ChunkedArray<T>
196where
197    T: PolarsDataType<HasViews = FalseT, IsStruct = FalseT, IsNested = FalseT>,
198{
199    /// Gather values from ChunkedArray by index.
200    unsafe fn take_unchecked(&self, indices: &IdxCa) -> Self {
201        let ca = self;
202        let targets_have_nulls = ca.null_count() > 0;
203        let targets: Vec<_> = ca.downcast_iter().collect();
204
205        let chunks = indices.downcast_iter().map(|idx_arr| {
206            let dtype = ca.dtype().to_arrow(CompatLevel::newest());
207            if idx_arr.null_count() == 0 {
208                gather_idx_array_unchecked(dtype, &targets, targets_have_nulls, idx_arr.values())
209            } else if targets.len() == 1 {
210                let target = targets.first().unwrap();
211                if targets_have_nulls {
212                    idx_arr
213                        .iter()
214                        .map(|i| target.get_unchecked(*i? as usize))
215                        .collect_arr_trusted_with_dtype(dtype)
216                } else {
217                    idx_arr
218                        .iter()
219                        .map(|i| Some(target.value_unchecked(*i? as usize)))
220                        .collect_arr_trusted_with_dtype(dtype)
221                }
222            } else {
223                let cumlens = cumulative_lengths(&targets);
224                if targets_have_nulls {
225                    idx_arr
226                        .iter()
227                        .map(|i| target_get_unchecked(&targets, &cumlens, *i?))
228                        .collect_arr_trusted_with_dtype(dtype)
229                } else {
230                    idx_arr
231                        .iter()
232                        .map(|i| Some(target_value_unchecked(&targets, &cumlens, *i?)))
233                        .collect_arr_trusted_with_dtype(dtype)
234                }
235            }
236        });
237
238        let mut out = ChunkedArray::from_chunk_iter_like(ca, chunks);
239        let sorted_flag = gather_sorted_flag(ca, indices, &out);
240
241        out.set_sorted_flag(sorted_flag);
242        out
243    }
244}
245
246impl ChunkTakeUnchecked<IdxCa> for BinaryChunked {
247    /// Gather values from ChunkedArray by index.
248    unsafe fn take_unchecked(&self, indices: &IdxCa) -> Self {
249        let ca = self;
250        let targets_have_nulls = ca.null_count() > 0;
251        let targets: Vec<_> = ca.downcast_iter().collect();
252
253        let chunks = indices.downcast_iter().map(|idx_arr| {
254            let dtype = ca.dtype().to_arrow(CompatLevel::newest());
255            if targets.len() == 1 {
256                let target = targets.first().unwrap();
257                take_unchecked(&**target, idx_arr)
258            } else {
259                let cumlens = cumulative_lengths(&targets);
260                if targets_have_nulls {
261                    let arr: BinaryViewArray = idx_arr
262                        .iter()
263                        .map(|i| target_get_unchecked(&targets, &cumlens, *i?))
264                        .collect_arr_trusted_with_dtype(dtype);
265                    arr.to_boxed()
266                } else {
267                    let arr: BinaryViewArray = idx_arr
268                        .iter()
269                        .map(|i| Some(target_value_unchecked(&targets, &cumlens, *i?)))
270                        .collect_arr_trusted_with_dtype(dtype);
271                    arr.to_boxed()
272                }
273            }
274        });
275
276        let mut out = ChunkedArray::from_chunks(ca.name().clone(), chunks.collect());
277        let sorted_flag = gather_sorted_flag(ca, indices, &out);
278        out.set_sorted_flag(sorted_flag);
279        out
280    }
281}
282
283impl ChunkTakeUnchecked<IdxCa> for StringChunked {
284    unsafe fn take_unchecked(&self, indices: &IdxCa) -> Self {
285        let ca = self;
286        let targets_have_nulls = ca.null_count() > 0;
287        let targets: Vec<_> = ca.downcast_iter().collect();
288
289        let chunks = indices.downcast_iter().map(|idx_arr| {
290            let dtype = ca.dtype().to_arrow(CompatLevel::newest());
291            if targets.len() == 1 {
292                let target = targets.first().unwrap();
293                take_unchecked(&**target, idx_arr)
294            } else {
295                let cumlens = cumulative_lengths(&targets);
296                if targets_have_nulls {
297                    let arr: Utf8ViewArray = idx_arr
298                        .iter()
299                        .map(|i| target_get_unchecked(&targets, &cumlens, *i?))
300                        .collect_arr_trusted_with_dtype(dtype);
301                    arr.to_boxed()
302                } else {
303                    let arr: Utf8ViewArray = idx_arr
304                        .iter()
305                        .map(|i| Some(target_value_unchecked(&targets, &cumlens, *i?)))
306                        .collect_arr_trusted_with_dtype(dtype);
307                    arr.to_boxed()
308                }
309            }
310        });
311
312        let mut out = ChunkedArray::from_chunks(ca.name().clone(), chunks.collect());
313        let sorted_flag = gather_sorted_flag(ca, indices, &out);
314        out.set_sorted_flag(sorted_flag);
315        out
316    }
317}
318
319impl<I: AsRef<[IdxSize]> + ?Sized> ChunkTakeUnchecked<I> for BinaryChunked {
320    /// Gather values from ChunkedArray by index.
321    unsafe fn take_unchecked(&self, indices: &I) -> Self {
322        let indices = IdxCa::mmap_slice(PlSmallStr::EMPTY, indices.as_ref());
323        self.take_unchecked(&indices)
324    }
325}
326
327impl<I: AsRef<[IdxSize]> + ?Sized> ChunkTakeUnchecked<I> for StringChunked {
328    /// Gather values from ChunkedArray by index.
329    unsafe fn take_unchecked(&self, indices: &I) -> Self {
330        let indices = IdxCa::mmap_slice(PlSmallStr::EMPTY, indices.as_ref());
331        self.take_unchecked(&indices)
332    }
333}
334
335#[cfg(feature = "dtype-struct")]
336impl ChunkTakeUnchecked<IdxCa> for StructChunked {
337    unsafe fn take_unchecked(&self, indices: &IdxCa) -> Self {
338        let a = self.rechunk();
339        let index = indices.rechunk();
340
341        let chunks = a
342            .downcast_iter()
343            .zip(index.downcast_iter())
344            .map(|(arr, idx)| take_unchecked(arr, idx))
345            .collect::<Vec<_>>();
346        self.copy_with_chunks(chunks)
347    }
348}
349
350#[cfg(feature = "dtype-struct")]
351impl<I: AsRef<[IdxSize]> + ?Sized> ChunkTakeUnchecked<I> for StructChunked {
352    unsafe fn take_unchecked(&self, indices: &I) -> Self {
353        let idx = IdxCa::mmap_slice(PlSmallStr::EMPTY, indices.as_ref());
354        self.take_unchecked(&idx)
355    }
356}
357
358impl IdxCa {
359    pub fn with_nullable_idx<T, F: FnOnce(&IdxCa) -> T>(idx: &[NullableIdxSize], f: F) -> T {
360        let validity: Bitmap = idx.iter().map(|idx| !idx.is_null_idx()).collect_trusted();
361        let idx = bytemuck::cast_slice::<_, IdxSize>(idx);
362        let arr = unsafe { polars_arrow::ffi::mmap::slice(idx) };
363        let arr = arr.with_validity_typed(Some(validity));
364        let ca = IdxCa::with_chunk(PlSmallStr::EMPTY, arr);
365
366        f(&ca)
367    }
368}
369
370#[cfg(feature = "dtype-array")]
371impl ChunkTakeUnchecked<IdxCa> for ArrayChunked {
372    unsafe fn take_unchecked(&self, indices: &IdxCa) -> Self {
373        // Taking nested types by value is expensive, so at a certain len[n] ratio
374        // we rechunk first, so that we can memcopy internally
375        if self.n_chunks() > 1 && should_rechunk(self.len(), indices.len()) {
376            let chunks = vec![take_unchecked(
377                self.rechunk().downcast_as_array(),
378                indices.rechunk().downcast_as_array(),
379            )];
380            return self.copy_with_chunks(chunks);
381        }
382        let ca = self;
383        let targets_have_nulls = ca.null_count() > 0;
384        let targets: Vec<_> = ca.downcast_iter().collect();
385
386        let chunks = indices.downcast_iter().map(|idx_arr| {
387            let dtype = ca.dtype().to_arrow(CompatLevel::newest());
388            if targets.len() == 1 {
389                let target = targets.first().unwrap();
390                take_unchecked(&**target, idx_arr)
391            } else {
392                let cumlens = cumulative_lengths(&targets);
393                if targets_have_nulls {
394                    let arr: FixedSizeListArray = idx_arr
395                        .iter()
396                        .map(|i| target_get_unchecked(&targets, &cumlens, *i?))
397                        .collect_arr_trusted_with_dtype(dtype);
398                    arr.to_boxed()
399                } else {
400                    let arr: FixedSizeListArray = idx_arr
401                        .iter()
402                        .map(|i| Some(target_value_unchecked(&targets, &cumlens, *i?)))
403                        .collect_arr_trusted_with_dtype(dtype);
404                    arr.to_boxed()
405                }
406            }
407        });
408
409        let mut out = ca.with_chunks(chunks.collect());
410        let sorted_flag = gather_sorted_flag(ca, indices, &out);
411        out.set_sorted_flag(sorted_flag);
412        out
413    }
414}
415
416#[cfg(feature = "dtype-array")]
417impl<I: AsRef<[IdxSize]> + ?Sized> ChunkTakeUnchecked<I> for ArrayChunked {
418    unsafe fn take_unchecked(&self, indices: &I) -> Self {
419        let idx = IdxCa::mmap_slice(PlSmallStr::EMPTY, indices.as_ref());
420        self.take_unchecked(&idx)
421    }
422}
423
424impl ChunkTakeUnchecked<IdxCa> for ListChunked {
425    unsafe fn take_unchecked(&self, indices: &IdxCa) -> Self {
426        // Taking nested types by value is expensive, so at a certain len[n] ratio
427        // we rechunk first, so that we can memcopy internally
428        if self.n_chunks() > 1 && should_rechunk(self.len(), indices.len()) {
429            let chunks = vec![take_unchecked(
430                self.rechunk().downcast_as_array(),
431                indices.rechunk().downcast_as_array(),
432            )];
433            return self.copy_with_chunks(chunks);
434        }
435        let ca = self;
436        let targets_have_nulls = ca.null_count() > 0;
437        let targets: Vec<_> = ca.downcast_iter().collect();
438
439        let chunks = indices.downcast_iter().map(|idx_arr| {
440            let dtype = ca.dtype().to_arrow(CompatLevel::newest());
441            if targets.len() == 1 {
442                let target = targets.first().unwrap();
443                take_unchecked(&**target, idx_arr)
444            } else {
445                let cumlens = cumulative_lengths(&targets);
446                if targets_have_nulls {
447                    let arr: ListArray<i64> = idx_arr
448                        .iter()
449                        .map(|i| target_get_unchecked(&targets, &cumlens, *i?))
450                        .collect_arr_trusted_with_dtype(dtype);
451                    arr.to_boxed()
452                } else {
453                    let arr: ListArray<i64> = idx_arr
454                        .iter()
455                        .map(|i| Some(target_value_unchecked(&targets, &cumlens, *i?)))
456                        .collect_arr_trusted_with_dtype(dtype);
457                    arr.to_boxed()
458                }
459            }
460        });
461
462        let mut out = ca.with_chunks(chunks.collect());
463        let sorted_flag = gather_sorted_flag(ca, indices, &out);
464        out.set_sorted_flag(sorted_flag);
465        out
466    }
467}
468
469impl<I: AsRef<[IdxSize]> + ?Sized> ChunkTakeUnchecked<I> for ListChunked {
470    unsafe fn take_unchecked(&self, indices: &I) -> Self {
471        let idx = IdxCa::mmap_slice(PlSmallStr::EMPTY, indices.as_ref());
472        self.take_unchecked(&idx)
473    }
474}
475
476fn should_rechunk(n_values: usize, n_indices: usize) -> bool {
477    n_indices > 0 && { (n_values / n_indices) > gather_ratio() }
478}
479
480fn gather_ratio() -> usize {
481    return *GATHER_RECHUNK_RATIO.get_or_init(|| {
482        const NAME: &str = "POLARS_GATHER_RECHUNK_RATIO";
483        std::env::var(NAME)
484            .map(|x| {
485                x.parse::<usize>()
486                    .unwrap_or_else(|_| panic!("invalid value for {NAME}: {x}"))
487            })
488            .unwrap_or(const { 64 })
489    });
490
491    static GATHER_RECHUNK_RATIO: OnceLock<usize> = OnceLock::new();
492}