Skip to main content

polars_utils/
index.rs

1#![allow(unsafe_op_in_unsafe_fn)]
2use std::fmt::{Debug, Formatter};
3
4use polars_error::{PolarsResult, polars_ensure};
5
6use crate::nulls::IsNull;
7
8#[cfg(not(feature = "bigidx"))]
9pub type IdxSize = u32;
10#[cfg(feature = "bigidx")]
11pub type IdxSize = u64;
12
13/// Avoids clippy::unnecessary_cast when compiling with bigidx enabled.
14#[inline]
15pub const fn idxsize_to_u64(
16    #[cfg(feature = "bigidx")] val: u64,
17    #[cfg(not(feature = "bigidx"))] val: u32,
18) -> u64 {
19    #[cfg(feature = "bigidx")]
20    {
21        val
22    }
23    #[cfg(not(feature = "bigidx"))]
24    {
25        val as u64
26    }
27}
28
29/// Avoids clippy::useless_conversion when compiling with bigidx enabled.
30#[inline(always)]
31pub fn idxsize_try_from<T>(x: T) -> Result<IdxSize, <IdxSize as TryFrom<T>>::Error>
32where
33    IdxSize: TryFrom<T>,
34{
35    IdxSize::try_from(x)
36}
37
38#[cfg(not(feature = "bigidx"))]
39pub type NonZeroIdxSize = std::num::NonZeroU32;
40#[cfg(feature = "bigidx")]
41pub type NonZeroIdxSize = std::num::NonZeroU64;
42
43#[cfg(not(feature = "bigidx"))]
44pub type AtomicIdxSize = std::sync::atomic::AtomicU32;
45#[cfg(feature = "bigidx")]
46pub type AtomicIdxSize = std::sync::atomic::AtomicU64;
47
48#[derive(Clone, Copy)]
49#[repr(transparent)]
50pub struct NullableIdxSize {
51    pub inner: IdxSize,
52}
53
54impl PartialEq<Self> for NullableIdxSize {
55    fn eq(&self, other: &Self) -> bool {
56        self.inner == other.inner
57    }
58}
59
60impl Eq for NullableIdxSize {}
61
62unsafe impl bytemuck::Zeroable for NullableIdxSize {}
63unsafe impl bytemuck::AnyBitPattern for NullableIdxSize {}
64unsafe impl bytemuck::NoUninit for NullableIdxSize {}
65
66impl Debug for NullableIdxSize {
67    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
68        write!(f, "{:?}", self.inner)
69    }
70}
71
72impl NullableIdxSize {
73    #[inline(always)]
74    pub fn is_null_idx(&self) -> bool {
75        // The left/right join maintain_order algorithms depend on the special value for sorting
76        self.inner == IdxSize::MAX
77    }
78
79    #[inline(always)]
80    pub const fn null() -> Self {
81        Self {
82            inner: IdxSize::MAX,
83        }
84    }
85
86    #[inline(always)]
87    pub fn idx(&self) -> IdxSize {
88        self.inner
89    }
90
91    #[inline(always)]
92    pub fn to_opt(&self) -> Option<IdxSize> {
93        if self.is_null_idx() {
94            None
95        } else {
96            Some(self.idx())
97        }
98    }
99}
100
101impl From<IdxSize> for NullableIdxSize {
102    #[inline(always)]
103    fn from(value: IdxSize) -> Self {
104        Self { inner: value }
105    }
106}
107
108pub trait Bounded {
109    fn len(&self) -> usize;
110
111    fn is_empty(&self) -> bool {
112        self.len() == 0
113    }
114}
115
116pub trait NullCount {
117    fn null_count(&self) -> usize {
118        0
119    }
120}
121
122impl<T: NullCount> NullCount for &T {
123    fn null_count(&self) -> usize {
124        (*self).null_count()
125    }
126}
127
128impl<T> Bounded for &[T] {
129    fn len(&self) -> usize {
130        <[T]>::len(self)
131    }
132}
133
134impl<T> NullCount for &[T] {
135    fn null_count(&self) -> usize {
136        0
137    }
138}
139
140pub trait Indexable {
141    type Item: IsNull;
142
143    fn get(&self, i: usize) -> Self::Item;
144
145    /// # Safety
146    /// Doesn't do any bound checks.
147    unsafe fn get_unchecked(&self, i: usize) -> Self::Item;
148}
149
150impl<T: Copy + IsNull> Indexable for &[T] {
151    type Item = T;
152
153    fn get(&self, i: usize) -> Self::Item {
154        self[i]
155    }
156
157    /// # Safety
158    /// Doesn't do any bound checks.
159    unsafe fn get_unchecked(&self, i: usize) -> Self::Item {
160        *<[T]>::get_unchecked(self, i)
161    }
162}
163
164pub fn check_bounds(idx: &[IdxSize], len: IdxSize) -> PolarsResult<()> {
165    // We iterate in large uninterrupted chunks to help auto-vectorization.
166    let Some(max_idx) = idx.iter().copied().max() else {
167        return Ok(());
168    };
169
170    polars_ensure!(max_idx < len, OutOfBounds: "indices are out of bounds");
171    Ok(())
172}
173
174pub trait ToIdx {
175    fn to_idx(self, len: u64) -> IdxSize;
176}
177
178macro_rules! impl_to_idx {
179    ($ty:ty) => {
180        impl ToIdx for $ty {
181            #[inline]
182            fn to_idx(self, _len: u64) -> IdxSize {
183                self as IdxSize
184            }
185        }
186    };
187    ($ty:ty, $ity:ty) => {
188        impl ToIdx for $ty {
189            #[inline]
190            fn to_idx(self, len: u64) -> IdxSize {
191                let idx = self as $ity;
192                if idx < 0 {
193                    (idx + len as $ity) as IdxSize
194                } else {
195                    idx as IdxSize
196                }
197            }
198        }
199    };
200}
201
202impl_to_idx!(u8);
203impl_to_idx!(u16);
204impl_to_idx!(u32);
205impl_to_idx!(u64);
206impl_to_idx!(i8, i16);
207impl_to_idx!(i16, i32);
208impl_to_idx!(i32, i64);
209impl_to_idx!(i64, i64);
210
211// Allows for 2^24 (~16M) chunks
212// Leaves 2^40 (~1T) rows per chunk
213const DEFAULT_CHUNK_BITS: u64 = 24;
214
215#[derive(Clone, Copy)]
216#[repr(transparent)]
217pub struct ChunkId<const CHUNK_BITS: u64 = DEFAULT_CHUNK_BITS> {
218    swizzled: u64,
219}
220
221impl<const CHUNK_BITS: u64> Debug for ChunkId<CHUNK_BITS> {
222    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
223        if self.is_null() {
224            write!(f, "NULL")
225        } else {
226            let (chunk, row) = self.extract();
227            write!(f, "({chunk}, {row})")
228        }
229    }
230}
231
232impl<const CHUNK_BITS: u64> ChunkId<CHUNK_BITS> {
233    #[inline(always)]
234    pub const fn null() -> Self {
235        Self { swizzled: u64::MAX }
236    }
237
238    #[inline(always)]
239    pub fn is_null(&self) -> bool {
240        self.swizzled == u64::MAX
241    }
242
243    #[inline(always)]
244    #[allow(clippy::unnecessary_cast)]
245    pub fn store(chunk: IdxSize, row: IdxSize) -> Self {
246        debug_assert!(chunk < !(u64::MAX << CHUNK_BITS) as IdxSize);
247        let swizzled = ((row as u64) << CHUNK_BITS) | chunk as u64;
248
249        Self { swizzled }
250    }
251
252    #[inline(always)]
253    #[allow(clippy::unnecessary_cast)]
254    pub fn extract(self) -> (IdxSize, IdxSize) {
255        let row = (self.swizzled >> CHUNK_BITS) as IdxSize;
256        let mask = (1u64 << CHUNK_BITS) - 1;
257        let chunk = (self.swizzled & mask) as IdxSize;
258        (chunk, row)
259    }
260
261    #[inline(always)]
262    pub fn inner_mut(&mut self) -> &mut u64 {
263        &mut self.swizzled
264    }
265
266    pub fn from_inner(inner: u64) -> Self {
267        Self { swizzled: inner }
268    }
269
270    pub fn into_inner(self) -> u64 {
271        self.swizzled
272    }
273}
274
275#[cfg(test)]
276mod test {
277    use super::*;
278
279    #[test]
280    fn test_chunk_idx() {
281        let chunk = 213908;
282        let row = 813457;
283
284        let ci: ChunkId = ChunkId::store(chunk, row);
285        let (c, r) = ci.extract();
286
287        assert_eq!(c, chunk);
288        assert_eq!(r, row);
289    }
290}