Skip to main content

rudb_arrow/
array.rs

1//! One column, in Arrow's memory layout.
2//!
3//! Arrow says an array is a length, a null count, and a list of buffers whose meaning comes from
4//! the type: a validity bitmap first, then offsets for a variable width type, then the values. That
5//! is what this builds, as owned little endian bytes, which is the form the C data interface hands
6//! across a boundary and the form a reader on the other side of one already knows how to read.
7//!
8//! Nothing here is zero copy yet and the crate's own description promises it is where the layouts
9//! permit. Two of the three buffers already do permit it. Our validity bitmap is the same LSB first
10//! layout as Arrow's, and a run of `i32` is a run of `i32`, so the copy is a memcpy that a later
11//! change can drop once there is an owner to hand the pages to. The one that cannot is `VARCHAR`:
12//! we store a sixteen byte view and an arena and Arrow's `u` is offsets and a contiguous run of
13//! bytes, and no arrangement of the two is the other one.
14
15use rudb_common::{Error, LogicalType, Result};
16use rudb_vector::{Data, Validity, Vector};
17
18use crate::types::DataType;
19
20/// A column of values, in Arrow's layout.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Array {
23    data_type: DataType,
24    len: usize,
25    null_count: usize,
26    validity: Option<Vec<u8>>,
27    offsets: Option<Vec<u8>>,
28    values: Vec<u8>,
29}
30
31impl Array {
32    /// The Arrow array a vector becomes.
33    ///
34    /// The vector is flattened first, so a constant, a sequence and a dictionary all arrive here as
35    /// the values they stand for. Arrow has a run end encoding and a dictionary array of its own
36    /// and they are worth having later, but an export that sometimes hands back a dictionary is an
37    /// export every reader has to have two paths for, and the reader is the one we are trying to
38    /// make cheap.
39    ///
40    /// # Errors
41    ///
42    /// For a type with no Arrow counterpart, and for a vector whose values are not the layout its
43    /// type says they are.
44    pub fn of(vector: &Vector) -> Result<Self> {
45        // flatten: Arrow is somebody else's format and this function's whole job is to hand data
46        // over in it. The doc above says why the export is always flat rather than sometimes a
47        // dictionary, and that decision is what makes this copy the point of the function instead
48        // of a cost it failed to avoid.
49        let flat = vector.flatten()?;
50        let data_type = DataType::of(flat.logical_type())?;
51        let len = flat.len();
52        let null_count = len - flat.validity().count_valid(len);
53        let validity = bitmap(flat.validity(), len);
54        let empty = Data::Empty;
55        let data = flat.data().unwrap_or(&empty);
56        let (values, offsets) = match &data_type {
57            // The null type has no buffers at all, so there is nothing to read and nothing to
58            // write. Everything in it is null by being that type.
59            DataType::Null => (Vec::new(), None),
60            DataType::Boolean => (bits(data, len), None),
61            DataType::Utf8 | DataType::Binary => {
62                let (values, offsets) = varlen(data, len)?;
63                (values, Some(offsets))
64            }
65            DataType::Interval => (intervals(data, len)?, None),
66            DataType::Decimal128 { .. } => (decimals(data, len, flat.logical_type())?, None),
67            other => {
68                let width = other.width().ok_or_else(|| {
69                    Error::internal(format!("{other:?} has no width and no buffer of its own"))
70                })?;
71                (fixed(data, len, width)?, None)
72            }
73        };
74        Ok(Self { data_type, len, null_count, validity, offsets, values })
75    }
76
77    /// An array of this type with no values in it.
78    ///
79    /// A variable width type still gets its offsets buffer, holding the single zero that says the
80    /// first value would start at the beginning. Arrow's rule is that offsets are one longer than
81    /// the array, and an array of nothing is the case where forgetting it is easiest and where a
82    /// reader that trusts the rule reads past the end.
83    #[must_use]
84    pub fn empty(data_type: DataType) -> Self {
85        let offsets = (data_type.buffer_count() == 3).then(|| 0i32.to_le_bytes().to_vec());
86        Self { data_type, len: 0, null_count: 0, validity: None, offsets, values: Vec::new() }
87    }
88
89    /// What the column holds.
90    #[must_use]
91    pub fn data_type(&self) -> &DataType {
92        &self.data_type
93    }
94
95    /// How many values.
96    #[must_use]
97    pub fn len(&self) -> usize {
98        self.len
99    }
100
101    /// Whether there are none.
102    #[must_use]
103    pub fn is_empty(&self) -> bool {
104        self.len == 0
105    }
106
107    /// How many of them are null.
108    #[must_use]
109    pub fn null_count(&self) -> usize {
110        self.null_count
111    }
112
113    /// The validity bitmap, or nothing when nothing is null.
114    ///
115    /// Nothing is what Arrow means by a null pointer in the first buffer slot, and it is the case
116    /// worth keeping rather than materializing: a reader that sees it can skip the per value check
117    /// for the whole array, which is the same reason `Validity::AllValid` exists on our side.
118    #[must_use]
119    pub fn validity(&self) -> Option<&[u8]> {
120        self.validity.as_deref()
121    }
122
123    /// The offsets buffer, for the variable width types, as little endian `i32`.
124    #[must_use]
125    pub fn offsets(&self) -> Option<&[u8]> {
126        self.offsets.as_deref()
127    }
128
129    /// The values buffer.
130    #[must_use]
131    pub fn values(&self) -> &[u8] {
132        &self.values
133    }
134
135    /// The buffers in the order the C data interface lists them.
136    ///
137    /// Two or three of them, and which is which is the type's business rather than the caller's,
138    /// which is why this exists next to the three accessors above.
139    #[must_use]
140    pub fn buffers(&self) -> Vec<Option<&[u8]>> {
141        match self.data_type.buffer_count() {
142            0 => Vec::new(),
143            3 => vec![self.validity(), self.offsets(), Some(self.values())],
144            _ => vec![self.validity(), Some(self.values())],
145        }
146    }
147}
148
149/// The validity bitmap as bytes, or nothing when there is nothing to say.
150///
151/// Our bitmap is already Arrow's: one bit per value, set meaning valid, least significant bit
152/// first. So the only work is writing the words out little endian, and on a little endian machine
153/// that is the bytes they already are.
154fn bitmap(validity: &Validity, len: usize) -> Option<Vec<u8>> {
155    if !validity.has_nulls(len) {
156        return None;
157    }
158    let bytes = len.div_ceil(8);
159    let mut out = vec![0u8; bytes];
160    for index in 0..len {
161        if validity.is_valid(index) {
162            out[index / 8] |= 1 << (index % 8);
163        }
164    }
165    Some(out)
166}
167
168/// A boolean column, which Arrow stores as a bit per value rather than a byte per value.
169fn bits(data: &Data, len: usize) -> Vec<u8> {
170    let mut out = vec![0u8; len.div_ceil(8)];
171    if let Data::Bool(values) = data {
172        for (index, &value) in values.as_slice().iter().take(len).enumerate() {
173            if value {
174                out[index / 8] |= 1 << (index % 8);
175            }
176        }
177    }
178    out
179}
180
181/// A string or blob column, as offsets and one run of bytes.
182///
183/// This is the copy that cannot be avoided. Our strings are views into an arena, in whatever order
184/// they were written and with the short ones not in the arena at all, and Arrow's are back to back
185/// in row order with an offset each.
186fn varlen(data: &Data, len: usize) -> Result<(Vec<u8>, Vec<u8>)> {
187    let mut values = Vec::new();
188    let mut offsets = Vec::with_capacity((len + 1) * 4);
189    offsets.extend_from_slice(&0i32.to_le_bytes());
190    for index in 0..len {
191        if let Data::Varlen(column) = data {
192            if let Some(bytes) = column.bytes(index) {
193                values.extend_from_slice(bytes);
194            }
195        }
196        // A null still gets an offset, and it is the same one as the value before it, which is what
197        // makes a null and an empty string the same two numbers and the validity bitmap the only
198        // thing that tells them apart. That is Arrow's rule and not a shortcut here.
199        let so_far = i32::try_from(values.len()).map_err(|_| {
200            Error::not_implemented(
201                "a column of strings longer than two gigabytes, which 32 bit offsets cannot \
202                 address, and which is what Arrow has LargeUtf8 for",
203            )
204        })?;
205        offsets.extend_from_slice(&so_far.to_le_bytes());
206    }
207    Ok((values, offsets))
208}
209
210/// An interval column, as Arrow's month day nano triple.
211///
212/// Ours is months, days and microseconds, and Arrow's third field is nanoseconds, so the only
213/// conversion is the factor of a thousand. It cannot overflow for any interval a query can produce:
214/// the microseconds field is an `i64` and a thousand times it is still inside an `i128`, but Arrow
215/// stores it in an `i64`, so an interval of more than about 292 years of microseconds saturates
216/// rather than wrapping. DuckDB has the same limit from the same arithmetic.
217fn intervals(data: &Data, len: usize) -> Result<Vec<u8>> {
218    let mut out = Vec::with_capacity(len * 16);
219    if let Data::Interval(values) = data {
220        for &(months, days, micros) in values.as_slice().iter().take(len) {
221            out.extend_from_slice(&months.to_le_bytes());
222            out.extend_from_slice(&days.to_le_bytes());
223            out.extend_from_slice(&micros.saturating_mul(1_000).to_le_bytes());
224        }
225    }
226    out.resize(len * 16, 0);
227    Ok(out)
228}
229
230/// A decimal column, or a `HUGEINT` one, widened to the 128 bits Arrow stores a decimal in.
231///
232/// Our decimals live in the narrowest integer that holds the precision, which is what makes a
233/// `DECIMAL(4, 2)` four times cheaper to add than a 128 bit one. Arrow has one decimal width, so
234/// the export widens. `Data::signed_at` is the widening, and it is there rather than here because
235/// four other places need the same five arms.
236fn decimals(data: &Data, len: usize, ty: &LogicalType) -> Result<Vec<u8>> {
237    let mut out = Vec::with_capacity(len * 16);
238    for index in 0..len {
239        let value = match data {
240            Data::Empty => 0,
241            _ => data.signed_at(index).ok_or_else(|| {
242                Error::internal(format!("{ty} is stored as something that is not an integer"))
243            })?,
244        };
245        out.extend_from_slice(&value.to_le_bytes());
246    }
247    Ok(out)
248}
249
250/// A fixed width column, as the bytes it already is.
251fn fixed(data: &Data, len: usize, width: usize) -> Result<Vec<u8>> {
252    let mut out = Vec::with_capacity(len * width);
253    macro_rules! pack {
254        ($values:expr) => {
255            for value in $values.as_slice().iter().take(len) {
256                out.extend_from_slice(&value.to_le_bytes());
257            }
258        };
259    }
260    match data {
261        // A vector where every value is null keeps no values at all, and Arrow still wants a buffer
262        // of the right size under the bitmap that says to ignore it. The resize below writes it.
263        Data::Empty => {}
264        Data::Int8(values) => pack!(values),
265        Data::Int16(values) => pack!(values),
266        Data::Int32(values) => pack!(values),
267        Data::Int64(values) => pack!(values),
268        Data::Int128(values) => pack!(values),
269        Data::UInt8(values) => pack!(values),
270        Data::UInt16(values) => pack!(values),
271        Data::UInt32(values) => pack!(values),
272        Data::UInt64(values) => pack!(values),
273        Data::UInt128(values) => pack!(values),
274        Data::Float32(values) => pack!(values),
275        Data::Float64(values) => pack!(values),
276        other => {
277            return Err(Error::internal(format!(
278                "{other:?} is not a fixed width layout and reached the fixed width path"
279            )));
280        }
281    }
282    if out.len() > len * width {
283        return Err(Error::internal(format!(
284            "a column of {len} values of {width} bytes came to {} bytes",
285            out.len()
286        )));
287    }
288    out.resize(len * width, 0);
289    Ok(out)
290}
291
292#[cfg(test)]
293mod tests {
294    use rudb_common::{LogicalType, Value};
295    use rudb_vector::Vector;
296
297    use super::{Array, DataType};
298    use crate::types::TimeUnit;
299
300    fn vector(ty: LogicalType, values: &[Value]) -> Vector {
301        Vector::from_values(ty, values).expect("the values are of the type")
302    }
303
304    #[test]
305    fn an_integer_column_is_four_little_endian_bytes_per_value() {
306        let array = Array::of(&vector(
307            LogicalType::Integer,
308            &[Value::Integer(1), Value::Integer(-2), Value::Integer(3)],
309        ))
310        .expect("an integer maps onto Arrow");
311        assert_eq!(array.data_type(), &DataType::Int32);
312        assert_eq!(array.len(), 3);
313        assert_eq!(array.null_count(), 0);
314        assert_eq!(array.values(), &[1, 0, 0, 0, 254, 255, 255, 255, 3, 0, 0, 0]);
315    }
316
317    #[test]
318    fn a_column_with_no_nulls_has_no_validity_bitmap_at_all() {
319        let array = Array::of(&vector(LogicalType::BigInt, &[Value::BigInt(7)]))
320            .expect("a bigint maps onto Arrow");
321        assert_eq!(array.validity(), None);
322        assert_eq!(array.buffers().len(), 2);
323        assert_eq!(array.buffers()[0], None);
324    }
325
326    #[test]
327    fn a_null_sets_its_bit_to_zero_and_leaves_the_value_slot_readable() {
328        let array = Array::of(&vector(
329            LogicalType::Integer,
330            &[Value::Integer(1), Value::Null, Value::Integer(3)],
331        ))
332        .expect("an integer maps onto Arrow");
333        assert_eq!(array.null_count(), 1);
334        // Bits 0 and 2 set, bit 1 clear, and the byte is padded with zeros up to eight bits.
335        assert_eq!(array.validity(), Some(&[0b0000_0101u8][..]));
336        // Arrow says the value under a null is undefined rather than absent, so the buffer is still
337        // the full three values wide and a reader that ignores the bitmap reads something.
338        assert_eq!(array.values().len(), 12);
339    }
340
341    #[test]
342    fn a_boolean_column_is_packed_a_bit_per_value() {
343        let values: Vec<Value> =
344            [true, false, true, true, false, false, false, true, true].map(Value::Boolean).to_vec();
345        let array =
346            Array::of(&vector(LogicalType::Boolean, &values)).expect("a boolean maps onto Arrow");
347        assert_eq!(array.len(), 9);
348        assert_eq!(array.values(), &[0b1000_1101u8, 0b0000_0001]);
349    }
350
351    #[test]
352    fn a_string_column_is_offsets_and_one_run_of_bytes() {
353        let array = Array::of(&vector(
354            LogicalType::Varchar,
355            &[
356                Value::Varchar("a".to_string()),
357                Value::Varchar("bc".to_string()),
358                Value::Varchar(String::new()),
359            ],
360        ))
361        .expect("a varchar maps onto Arrow");
362        assert_eq!(array.data_type(), &DataType::Utf8);
363        assert_eq!(array.values(), b"abc");
364        assert_eq!(offsets(&array), vec![0, 1, 3, 3]);
365        assert_eq!(array.buffers().len(), 3);
366    }
367
368    #[test]
369    fn a_null_string_gets_the_offset_of_the_one_before_it() {
370        let array = Array::of(&vector(
371            LogicalType::Varchar,
372            &[Value::Varchar("ab".to_string()), Value::Null, Value::Varchar("c".to_string())],
373        ))
374        .expect("a varchar maps onto Arrow");
375        // A null and an empty string are the same pair of offsets. The bitmap is the only thing
376        // that tells them apart, which is Arrow's rule rather than a shortcut here.
377        assert_eq!(offsets(&array), vec![0, 2, 2, 3]);
378        assert_eq!(array.values(), b"abc");
379        assert_eq!(array.validity(), Some(&[0b0000_0101u8][..]));
380    }
381
382    #[test]
383    fn a_string_longer_than_the_inline_prefix_survives_the_arena() {
384        let long = "the quick brown fox jumps over the lazy dog";
385        let array = Array::of(&vector(LogicalType::Varchar, &[Value::Varchar(long.to_string())]))
386            .expect("a varchar maps onto Arrow");
387        assert_eq!(array.values(), long.as_bytes());
388    }
389
390    #[test]
391    fn a_hugeint_is_widened_to_the_decimal_arrow_stores_it_in() {
392        let array = Array::of(&vector(LogicalType::HugeInt, &[Value::HugeInt(-1)]))
393            .expect("a hugeint maps onto Arrow");
394        assert_eq!(array.data_type(), &DataType::Decimal128 { precision: 38, scale: 0 });
395        assert_eq!(array.values(), &[0xff; 16]);
396    }
397
398    #[test]
399    fn a_narrow_decimal_is_widened_to_sixteen_bytes_and_keeps_its_scale() {
400        let array = Array::of(&vector(
401            LogicalType::Decimal { width: 4, scale: 2 },
402            &[Value::Decimal { unscaled: 1234, width: 4, scale: 2 }],
403        ))
404        .expect("a decimal maps onto Arrow");
405        assert_eq!(array.data_type(), &DataType::Decimal128 { precision: 4, scale: 2 });
406        assert_eq!(array.values().len(), 16);
407        assert_eq!(i128::from_le_bytes(array.values().try_into().expect("sixteen bytes")), 1234);
408    }
409
410    #[test]
411    fn an_interval_turns_its_microseconds_into_arrows_nanoseconds() {
412        let array = Array::of(&vector(
413            LogicalType::Interval,
414            &[Value::Interval { months: 1, days: 2, micros: 3 }],
415        ))
416        .expect("an interval maps onto Arrow");
417        assert_eq!(array.values()[0..4], 1i32.to_le_bytes());
418        assert_eq!(array.values()[4..8], 2i32.to_le_bytes());
419        assert_eq!(array.values()[8..16], 3_000i64.to_le_bytes());
420    }
421
422    #[test]
423    fn a_timestamp_keeps_the_microseconds_it_already_counts_in() {
424        let array = Array::of(&vector(LogicalType::Timestamp, &[Value::Timestamp(1_700_000)]))
425            .expect("a timestamp maps onto Arrow");
426        assert_eq!(array.data_type(), &DataType::Timestamp(TimeUnit::Microsecond, None));
427        assert_eq!(array.values(), 1_700_000i64.to_le_bytes());
428    }
429
430    #[test]
431    fn the_null_type_has_no_buffers_and_nothing_in_them() {
432        let array = Array::of(&vector(LogicalType::Null, &[Value::Null, Value::Null]))
433            .expect("the null type maps onto Arrow");
434        assert_eq!(array.data_type(), &DataType::Null);
435        assert_eq!(array.len(), 2);
436        assert_eq!(array.null_count(), 2);
437        assert!(array.buffers().is_empty());
438        assert!(array.values().is_empty());
439    }
440
441    #[test]
442    fn a_constant_vector_is_flattened_into_the_values_it_stands_for() {
443        let array = Array::of(&Vector::constant(LogicalType::Integer, Value::Integer(9), 4))
444            .expect("an integer maps onto Arrow");
445        assert_eq!(array.len(), 4);
446        assert_eq!(array.values(), &[9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0]);
447    }
448
449    #[test]
450    fn a_column_of_nothing_but_nulls_still_has_a_values_buffer_the_right_size() {
451        let array = Array::of(&vector(LogicalType::BigInt, &[Value::Null, Value::Null]))
452            .expect("a bigint maps onto Arrow");
453        assert_eq!(array.null_count(), 2);
454        assert_eq!(array.values(), &[0u8; 16]);
455        assert_eq!(array.validity(), Some(&[0u8][..]));
456    }
457
458    #[test]
459    fn an_empty_string_array_still_carries_the_leading_offset() {
460        let array = Array::empty(DataType::Utf8);
461        assert!(array.is_empty());
462        assert_eq!(array.offsets(), Some(&0i32.to_le_bytes()[..]));
463        assert_eq!(array.buffers().len(), 3);
464    }
465
466    #[test]
467    fn a_type_with_no_arrow_counterpart_is_refused_rather_than_guessed_at() {
468        let error = Array::of(&Vector::constant(LogicalType::Uuid, Value::Null, 1))
469            .expect_err("uuid has no Arrow type here yet");
470        assert!(error.to_string().contains("UUID"), "{error}");
471    }
472
473    fn offsets(array: &Array) -> Vec<i32> {
474        array
475            .offsets()
476            .expect("a variable width array has offsets")
477            .chunks_exact(4)
478            .map(|bytes| i32::from_le_bytes(bytes.try_into().expect("four bytes")))
479            .collect()
480    }
481}