• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vortex-data / vortex / 16811793386

07 Aug 2025 05:45PM UTC coverage: 84.877% (+0.03%) from 84.847%
16811793386

push

github

web-flow
chore: Bump msrv to 1.89 (#4159)

Mostly motivated by wanting to remove the horrible `ResutExt::unnest` I
introduced a few months ago, but also let chains are cool.

Signed-off-by: Adam Gutglick <adam@spiraldb.com>

107 of 157 new or added lines in 34 files covered. (68.15%)

2 existing lines in 2 files now uncovered.

50629 of 59650 relevant lines covered (84.88%)

567844.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.12
/vortex-array/src/arrays/bool/array.rs
1
// SPDX-License-Identifier: Apache-2.0
2
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3

4
use arrow_array::BooleanArray;
5
use arrow_buffer::{BooleanBuffer, BooleanBufferBuilder, MutableBuffer};
6
use vortex_dtype::DType;
7
use vortex_error::{VortexResult, vortex_panic};
8

9
use crate::Canonical;
10
use crate::arrays::{BoolVTable, bool};
11
use crate::builders::ArrayBuilder;
12
use crate::stats::{ArrayStats, StatsSetRef};
13
use crate::validity::Validity;
14
use crate::vtable::{ArrayVTable, CanonicalVTable, ValidityHelper};
15

16
/// A boolean array that stores true/false values in a compact bit-packed format.
17
///
18
/// This mirrors the Apache Arrow Boolean array encoding, where each boolean value
19
/// is stored as a single bit rather than a full byte.
20
///
21
/// The data layout uses:
22
/// - A bit-packed buffer where each bit represents one boolean value (0 = false, 1 = true)
23
/// - An optional validity child array, which must be of type `Bool(NonNullable)`, where true values
24
///   indicate valid and false indicates null. if the i-th value is null in the validity child,
25
///   the i-th packed bit in the buffer may be 0 or 1, i.e. it is undefined.
26
/// - Bit-level slicing is supported with minimal overhead
27
///
28
/// # Examples
29
///
30
/// ```
31
/// use vortex_array::arrays::BoolArray;
32
/// use vortex_array::IntoArray;
33
///
34
/// // Create from iterator using FromIterator impl
35
/// let array: BoolArray = [true, false, true, false].into_iter().collect();
36
///
37
/// // Slice the array
38
/// let sliced = array.slice(1, 3).unwrap();
39
/// assert_eq!(sliced.len(), 2);
40
///
41
/// // Access individual values
42
/// let value = array.scalar_at(0).unwrap();
43
/// assert_eq!(value, true.into());
44
/// ```
45
#[derive(Clone, Debug)]
46
pub struct BoolArray {
47
    dtype: DType,
48
    buffer: BooleanBuffer,
49
    pub(crate) validity: Validity,
50
    pub(crate) stats_set: ArrayStats,
51
}
52

53
impl BoolArray {
54
    /// Create a new BoolArray from a set of indices and a length.
55
    /// All indices must be less than the length.
56
    pub fn from_indices<I: IntoIterator<Item = usize>>(
50✔
57
        length: usize,
50✔
58
        indices: I,
50✔
59
        validity: Validity,
50✔
60
    ) -> Self {
50✔
61
        let mut buffer = MutableBuffer::new_null(length);
50✔
62
        indices
50✔
63
            .into_iter()
50✔
64
            .for_each(|idx| arrow_buffer::bit_util::set_bit(&mut buffer, idx));
102✔
65
        Self::new(
50✔
66
            BooleanBufferBuilder::new_from_buffer(buffer, length).finish(),
50✔
67
            validity,
50✔
68
        )
69
    }
50✔
70

71
    /// Creates a new [`BoolArray`] from a [`BooleanBuffer`] and [`Validity`], without checking
72
    /// any invariants.
73
    pub fn new(buffer: BooleanBuffer, validity: Validity) -> Self {
270,542✔
74
        if let Some(len) = validity.maybe_len()
270,542✔
75
            && buffer.len() != len
17,913✔
76
        {
NEW
77
            vortex_panic!(
×
NEW
78
                "Buffer and validity length mismatch: buffer={}, validity={}",
×
NEW
79
                buffer.len(),
×
80
                len
81
            );
82
        }
270,542✔
83

84
        // Shrink the buffer to remove any whole bytes.
85
        let buffer = buffer.shrink_offset();
270,542✔
86
        Self {
270,542✔
87
            dtype: DType::Bool(validity.nullability()),
270,542✔
88
            buffer,
270,542✔
89
            validity,
270,542✔
90
            stats_set: ArrayStats::default(),
270,542✔
91
        }
270,542✔
92
    }
270,542✔
93

94
    /// Returns the underlying [`BooleanBuffer`] of the array.
95
    pub fn boolean_buffer(&self) -> &BooleanBuffer {
21,797,593✔
96
        assert!(
21,797,593✔
97
            self.buffer.offset() < 8,
21,797,593✔
98
            "Offset must be <8, did we forget to call shrink_offset? Found {}",
×
99
            self.buffer.offset()
×
100
        );
101
        &self.buffer
21,797,593✔
102
    }
21,797,593✔
103

104
    /// Get a mutable version of this array.
105
    ///
106
    /// If the caller holds the only reference to the underlying buffer the underlying buffer is returned
107
    /// otherwise a copy is created.
108
    ///
109
    /// The second value of the tuple is a bit_offset of first value in first byte of the returned builder
110
    pub fn into_boolean_builder(self) -> (BooleanBufferBuilder, usize) {
2,908✔
111
        let offset = self.buffer.offset();
2,908✔
112
        let len = self.buffer.len();
2,908✔
113
        let arrow_buffer = self.buffer.into_inner();
2,908✔
114
        let mutable_buf = if arrow_buffer.ptr_offset() == 0 {
2,908✔
115
            arrow_buffer.into_mutable().unwrap_or_else(|b| {
2,908✔
116
                let mut buf = MutableBuffer::with_capacity(b.len());
85✔
117
                buf.extend_from_slice(b.as_slice());
85✔
118
                buf
85✔
119
            })
85✔
120
        } else {
121
            let mut buf = MutableBuffer::with_capacity(arrow_buffer.len());
×
122
            buf.extend_from_slice(arrow_buffer.as_slice());
×
123
            buf
×
124
        };
125

126
        (
2,908✔
127
            BooleanBufferBuilder::new_from_buffer(mutable_buf, offset + len),
2,908✔
128
            offset,
2,908✔
129
        )
2,908✔
130
    }
2,908✔
131
}
132

133
impl From<BooleanBuffer> for BoolArray {
134
    fn from(value: BooleanBuffer) -> Self {
74,961✔
135
        Self::new(value, Validity::NonNullable)
74,961✔
136
    }
74,961✔
137
}
138

139
impl FromIterator<bool> for BoolArray {
140
    fn from_iter<T: IntoIterator<Item = bool>>(iter: T) -> Self {
57,946✔
141
        Self::new(BooleanBuffer::from_iter(iter), Validity::NonNullable)
57,946✔
142
    }
57,946✔
143
}
144

145
impl FromIterator<Option<bool>> for BoolArray {
146
    fn from_iter<I: IntoIterator<Item = Option<bool>>>(iter: I) -> Self {
43✔
147
        let (buffer, nulls) = BooleanArray::from_iter(iter).into_parts();
43✔
148

149
        Self::new(
43✔
150
            buffer,
43✔
151
            nulls.map(Validity::from).unwrap_or(Validity::AllValid),
43✔
152
        )
153
    }
43✔
154
}
155

156
impl ValidityHelper for BoolArray {
157
    fn validity(&self) -> &Validity {
22,068,450✔
158
        &self.validity
22,068,450✔
159
    }
22,068,450✔
160
}
161

162
impl ArrayVTable<BoolVTable> for BoolVTable {
163
    fn len(array: &BoolArray) -> usize {
44,526,270✔
164
        array.buffer.len()
44,526,270✔
165
    }
44,526,270✔
166

167
    fn dtype(array: &BoolArray) -> &DType {
43,973,466✔
168
        &array.dtype
43,973,466✔
169
    }
43,973,466✔
170

171
    fn stats(array: &BoolArray) -> StatsSetRef<'_> {
844,590✔
172
        array.stats_set.to_ref(array.as_ref())
844,590✔
173
    }
844,590✔
174
}
175

176
impl CanonicalVTable<BoolVTable> for BoolVTable {
177
    fn canonicalize(array: &BoolArray) -> VortexResult<Canonical> {
164,113✔
178
        Ok(Canonical::Bool(array.clone()))
164,113✔
179
    }
164,113✔
180

181
    fn append_to_builder(array: &BoolArray, builder: &mut dyn ArrayBuilder) -> VortexResult<()> {
22✔
182
        builder.extend_from_array(array.as_ref())
22✔
183
    }
22✔
184
}
185

186
pub trait BooleanBufferExt {
187
    /// Slice any full bytes from the buffer, leaving the offset < 8.
188
    fn shrink_offset(self) -> Self;
189
}
190

191
impl BooleanBufferExt for BooleanBuffer {
192
    fn shrink_offset(self) -> Self {
270,542✔
193
        let byte_offset = self.offset() / 8;
270,542✔
194
        let bit_offset = self.offset() % 8;
270,542✔
195
        let len = self.len();
270,542✔
196
        let buffer = self
270,542✔
197
            .into_inner()
270,542✔
198
            .slice_with_length(byte_offset, (len + bit_offset).div_ceil(8));
270,542✔
199
        BooleanBuffer::new(buffer, bit_offset, len)
270,542✔
200
    }
270,542✔
201
}
202

203
#[cfg(test)]
204
mod tests {
205
    use arrow_buffer::{BooleanBuffer, BooleanBufferBuilder};
206
    use vortex_buffer::buffer;
207

208
    use crate::arrays::{BoolArray, PrimitiveArray};
209
    use crate::patches::Patches;
210
    use crate::validity::Validity;
211
    use crate::vtable::ValidityHelper;
212
    use crate::{Array, IntoArray, ToCanonical};
213

214
    #[test]
215
    fn bool_array() {
1✔
216
        let arr = BoolArray::from_iter([true, false, true]);
1✔
217
        let scalar = bool::try_from(&arr.scalar_at(0).unwrap()).unwrap();
1✔
218
        assert!(scalar);
1✔
219
    }
1✔
220

221
    #[test]
222
    fn test_all_some_iter() {
1✔
223
        let arr = BoolArray::from_iter([Some(true), Some(false)]);
1✔
224

225
        assert!(matches!(arr.validity(), Validity::AllValid));
1✔
226

227
        let scalar = bool::try_from(&arr.scalar_at(0).unwrap()).unwrap();
1✔
228
        assert!(scalar);
1✔
229
        let scalar = bool::try_from(&arr.scalar_at(1).unwrap()).unwrap();
1✔
230
        assert!(!scalar);
1✔
231
    }
1✔
232

233
    #[test]
234
    fn test_bool_from_iter() {
1✔
235
        let arr = BoolArray::from_iter([Some(true), Some(true), None, Some(false), None]);
1✔
236

237
        let scalar = bool::try_from(&arr.scalar_at(0).unwrap()).unwrap();
1✔
238
        assert!(scalar);
1✔
239

240
        let scalar = bool::try_from(&arr.scalar_at(1).unwrap()).unwrap();
1✔
241
        assert!(scalar);
1✔
242

243
        let scalar = arr.scalar_at(2).unwrap();
1✔
244
        assert!(scalar.is_null());
1✔
245

246
        let scalar = bool::try_from(&arr.scalar_at(3).unwrap()).unwrap();
1✔
247
        assert!(!scalar);
1✔
248

249
        let scalar = arr.scalar_at(4).unwrap();
1✔
250
        assert!(scalar.is_null());
1✔
251
    }
1✔
252

253
    #[test]
254
    fn patch_sliced_bools() {
1✔
255
        let arr = {
1✔
256
            let mut builder = BooleanBufferBuilder::new(12);
1✔
257
            builder.append(false);
1✔
258
            builder.append_n(11, true);
1✔
259
            BoolArray::from(builder.finish())
1✔
260
        };
261
        let sliced = arr.slice(4, 12).unwrap();
1✔
262
        let sliced_len = sliced.len();
1✔
263
        let (values, offset) = sliced.to_bool().unwrap().into_boolean_builder();
1✔
264
        assert_eq!(offset, 4);
1✔
265
        assert_eq!(values.as_slice(), &[254, 15]);
1✔
266

267
        // patch the underlying array
268
        let patches = Patches::new(
1✔
269
            arr.len(),
1✔
270
            0,
271
            buffer![4u32].into_array(), // This creates a non-nullable array
1✔
272
            BoolArray::from(BooleanBuffer::new_unset(1)).into_array(),
1✔
273
        );
274
        let arr = arr.patch(&patches).unwrap();
1✔
275
        let arr_len = arr.len();
1✔
276
        let (values, offset) = arr.to_bool().unwrap().into_boolean_builder();
1✔
277
        assert_eq!(offset, 0);
1✔
278
        assert_eq!(values.len(), arr_len + offset);
1✔
279
        assert_eq!(values.as_slice(), &[238, 15]);
1✔
280

281
        // the slice should be unchanged
282
        let (values, offset) = sliced.to_bool().unwrap().into_boolean_builder();
1✔
283
        assert_eq!(offset, 4);
1✔
284
        assert_eq!(values.len(), sliced_len + offset);
1✔
285
        assert_eq!(values.as_slice(), &[254, 15]); // unchanged
1✔
286
    }
1✔
287

288
    #[test]
289
    fn slice_array_in_middle() {
1✔
290
        let arr = BoolArray::from(BooleanBuffer::new_set(16));
1✔
291
        let sliced = arr.slice(4, 12).unwrap();
1✔
292
        let sliced_len = sliced.len();
1✔
293
        let (values, offset) = sliced.to_bool().unwrap().into_boolean_builder();
1✔
294
        assert_eq!(offset, 4);
1✔
295
        assert_eq!(values.len(), sliced_len + offset);
1✔
296
        assert_eq!(values.as_slice(), &[255, 15]);
1✔
297
    }
1✔
298

299
    #[test]
300
    #[should_panic]
301
    fn patch_bools_owned() {
1✔
302
        let buffer = buffer![255u8; 2];
1✔
303
        let buf = BooleanBuffer::new(buffer.into_arrow_buffer(), 0, 15);
1✔
304
        let arr = BoolArray::new(buf, Validity::NonNullable);
1✔
305
        let buf_ptr = arr.boolean_buffer().sliced().as_ptr();
1✔
306

307
        let patches = Patches::new(
1✔
308
            arr.len(),
1✔
309
            0,
310
            PrimitiveArray::new(buffer![0u32], Validity::AllValid).into_array(),
1✔
311
            BoolArray::from(BooleanBuffer::new_unset(1)).into_array(),
1✔
312
        );
313
        let arr = arr.patch(&patches).unwrap();
1✔
314
        assert_eq!(arr.boolean_buffer().sliced().as_ptr(), buf_ptr);
1✔
315

316
        let (values, _byte_bit_offset) = arr.to_bool().unwrap().into_boolean_builder();
317
        assert_eq!(values.as_slice(), &[254, 127]);
318
    }
319
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc