Skip to main content

mz_repr/adt/
array.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! A multi-dimensional array data type.
11
12use std::cmp::Ordering;
13use std::error::Error;
14use std::{fmt, mem};
15
16use mz_ore::cast::CastFrom;
17use mz_persist_types::columnar::FixedSizeCodec;
18use mz_proto::{RustType, TryFromProtoError};
19#[cfg(any(test, feature = "proptest"))]
20use proptest_derive::Arbitrary;
21use serde::{Deserialize, Serialize};
22
23use crate::Datum;
24use crate::row::DatumList;
25use crate::scalar::SqlScalarType;
26
27include!(concat!(env!("OUT_DIR"), "/mz_repr.adt.array.rs"));
28
29/// The maximum number of dimensions permitted in an array.
30pub const MAX_ARRAY_DIMENSIONS: u8 = 6;
31
32/// A variable-length multi-dimensional array.
33///
34/// The type parameter `T` represents the element type of the array. It is a
35/// phantom parameter propagated through the inner [`DatumList`] — the actual
36/// elements are stored as serialized bytes and `T` is not enforced at
37/// runtime. It is up to the caller to ensure `T` matches the actual element
38/// type. The default `T = Datum<'a>` means existing code that writes
39/// `Array<'a>` continues to work unchanged.
40#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
41pub struct Array<'a, T = Datum<'a>> {
42    /// The elements in the array.
43    pub(crate) elements: DatumList<'a, T>,
44    /// The dimensions of the array.
45    pub(crate) dims: ArrayDimensions<'a>,
46}
47
48impl<'a, T> Array<'a, T> {
49    /// Returns the dimensions of the array.
50    pub fn dims(&self) -> ArrayDimensions<'a> {
51        self.dims
52    }
53
54    /// Returns the elements of the array.
55    pub fn elements(&self) -> DatumList<'a, T> {
56        self.elements
57    }
58
59    /// Returns true if this array's dimensions are valid for the Int2Vector type.
60    /// Int2Vector is 1-D; empty arrays use 0 dimensions (PostgreSQL convention).
61    pub fn has_int2vector_dims(&self) -> bool {
62        self.dims().len() == 1
63            || (self.dims().len() == 0 && self.elements().iter().next().is_none())
64    }
65}
66
67impl<'a> crate::scalar::SqlContainerType for Array<'a, Datum<'a>> {
68    fn unwrap_element_type(container: &SqlScalarType) -> &SqlScalarType {
69        container.unwrap_array_element_type()
70    }
71    fn wrap_element_type(element: SqlScalarType) -> SqlScalarType {
72        SqlScalarType::Array(Box::new(element))
73    }
74}
75
76/// The dimensions of an [`Array`].
77#[derive(Clone, Copy, Eq, PartialEq, Hash)]
78pub struct ArrayDimensions<'a> {
79    pub(crate) data: &'a [u8],
80}
81
82impl Default for ArrayDimensions<'static> {
83    fn default() -> Self {
84        Self { data: &[] }
85    }
86}
87
88impl ArrayDimensions<'_> {
89    /// Returns the number of dimensions in the array as a [`u8`].
90    pub fn ndims(&self) -> u8 {
91        let ndims = self.data.len() / (mem::size_of::<usize>() * 2);
92        ndims.try_into().expect("ndims is known to fit in a u8")
93    }
94
95    /// Returns the number of the dimensions in the array as a [`usize`].
96    pub fn len(&self) -> usize {
97        self.ndims().into()
98    }
99
100    /// Reports whether the number of dimensions in the array is zero.
101    pub fn is_empty(&self) -> bool {
102        self.len() == 0
103    }
104}
105
106impl Ord for ArrayDimensions<'_> {
107    fn cmp(&self, other: &Self) -> Ordering {
108        self.ndims().cmp(&other.ndims())
109    }
110}
111
112impl PartialOrd for ArrayDimensions<'_> {
113    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
114        Some(self.cmp(other))
115    }
116}
117
118impl fmt::Debug for ArrayDimensions<'_> {
119    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120        f.debug_list().entries(*self).finish()
121    }
122}
123
124impl<'a> IntoIterator for ArrayDimensions<'a> {
125    type Item = ArrayDimension;
126    type IntoIter = ArrayDimensionsIter<'a>;
127
128    fn into_iter(self) -> ArrayDimensionsIter<'a> {
129        ArrayDimensionsIter { data: self.data }
130    }
131}
132
133/// An iterator over the dimensions in an [`ArrayDimensions`].
134#[derive(Debug)]
135pub struct ArrayDimensionsIter<'a> {
136    data: &'a [u8],
137}
138
139impl Iterator for ArrayDimensionsIter<'_> {
140    type Item = ArrayDimension;
141
142    fn next(&mut self) -> Option<ArrayDimension> {
143        if self.data.is_empty() {
144            None
145        } else {
146            let sz = mem::size_of::<usize>();
147            let lower_bound = isize::from_ne_bytes(self.data[..sz].try_into().unwrap());
148            self.data = &self.data[sz..];
149            let length = usize::from_ne_bytes(self.data[..sz].try_into().unwrap());
150            self.data = &self.data[sz..];
151            Some(ArrayDimension {
152                lower_bound,
153                length,
154            })
155        }
156    }
157}
158
159/// The specification of one dimension of an [`Array`].
160#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
161pub struct ArrayDimension {
162    /// The "logical" index at which this dimension begins. This value has no bearing on the
163    /// physical layout of the data, only how users want to use its indices (which may be negative).
164    pub lower_bound: isize,
165    /// The number of elements in this array.
166    pub length: usize,
167}
168
169impl ArrayDimension {
170    /// Presents the "logical indices" of the array, i.e. those that are revealed to the user.
171    ///
172    /// # Panics
173    /// - If the array contain more than [`isize::MAX`] elements (i.e. more than 9EB of data).
174    pub fn dimension_bounds(&self) -> (isize, isize) {
175        (
176            self.lower_bound,
177            self.lower_bound
178                + isize::try_from(self.length).expect("fewer than isize::MAX elements")
179                - 1,
180        )
181    }
182}
183
184/// An error that can occur when constructing an array.
185#[derive(
186    Clone,
187    Copy,
188    Debug,
189    Eq,
190    PartialEq,
191    Hash,
192    Ord,
193    PartialOrd,
194    Serialize,
195    Deserialize
196)]
197#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
198pub enum InvalidArrayError {
199    /// The number of dimensions in the array exceeds [`MAX_ARRAY_DIMENSIONS]`.
200    TooManyDimensions(usize),
201    /// The number of array elements does not match the cardinality derived from
202    /// its dimensions.
203    WrongCardinality { actual: usize, expected: usize },
204}
205
206impl fmt::Display for InvalidArrayError {
207    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
208        match self {
209            InvalidArrayError::TooManyDimensions(n) => write!(
210                f,
211                "number of array dimensions ({}) exceeds the maximum allowed ({})",
212                n, MAX_ARRAY_DIMENSIONS
213            ),
214            InvalidArrayError::WrongCardinality { actual, expected } => write!(
215                f,
216                "number of array elements ({}) does not match declared cardinality ({})",
217                actual, expected
218            ),
219        }
220    }
221}
222
223impl Error for InvalidArrayError {
224    fn source(&self) -> Option<&(dyn Error + 'static)> {
225        None
226    }
227}
228
229impl RustType<ProtoInvalidArrayError> for InvalidArrayError {
230    fn into_proto(&self) -> ProtoInvalidArrayError {
231        use Kind::*;
232        use proto_invalid_array_error::*;
233        let kind = match self {
234            InvalidArrayError::TooManyDimensions(dims) => TooManyDimensions(dims.into_proto()),
235            InvalidArrayError::WrongCardinality { actual, expected } => {
236                WrongCardinality(ProtoWrongCardinality {
237                    actual: actual.into_proto(),
238                    expected: expected.into_proto(),
239                })
240            }
241        };
242        ProtoInvalidArrayError { kind: Some(kind) }
243    }
244
245    fn from_proto(proto: ProtoInvalidArrayError) -> Result<Self, TryFromProtoError> {
246        use proto_invalid_array_error::Kind::*;
247        match proto.kind {
248            Some(kind) => match kind {
249                TooManyDimensions(dims) => Ok(InvalidArrayError::TooManyDimensions(
250                    usize::from_proto(dims)?,
251                )),
252                WrongCardinality(v) => Ok(InvalidArrayError::WrongCardinality {
253                    actual: usize::from_proto(v.actual)?,
254                    expected: usize::from_proto(v.expected)?,
255                }),
256            },
257            None => Err(TryFromProtoError::missing_field(
258                "`ProtoInvalidArrayError::kind`",
259            )),
260        }
261    }
262}
263
264/// An encoded packed variant of [`ArrayDimension`].
265///
266/// We uphold the variant that [`PackedArrayDimension`] sorts the same as
267/// [`ArrayDimension`].
268#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
269pub struct PackedArrayDimension([u8; Self::SIZE]);
270
271// `as` conversions are okay here because we're doing bit level logic to make
272// sure the sort order of the packed binary is correct. This is implementation
273// is proptest-ed below.
274#[allow(clippy::as_conversions)]
275impl FixedSizeCodec<ArrayDimension> for PackedArrayDimension {
276    const SIZE: usize = 16;
277
278    fn as_bytes(&self) -> &[u8] {
279        &self.0
280    }
281
282    fn from_bytes(slice: &[u8]) -> Result<Self, String> {
283        let buf: [u8; Self::SIZE] = slice.try_into().map_err(|_| {
284            format!(
285                "size for PackedArrayDimension is {} bytes, got {}",
286                Self::SIZE,
287                slice.len()
288            )
289        })?;
290        Ok(PackedArrayDimension(buf))
291    }
292
293    fn from_value(value: ArrayDimension) -> Self {
294        let mut buf = [0; 16];
295
296        let lower_bound = (i64::cast_from(value.lower_bound) as u64) ^ (0x8000_0000_0000_0000u64);
297        buf[..8].copy_from_slice(&lower_bound.to_be_bytes());
298        let length = u64::cast_from(value.length);
299        buf[8..].copy_from_slice(&length.to_be_bytes());
300
301        PackedArrayDimension(buf)
302    }
303
304    fn into_value(self) -> ArrayDimension {
305        let mut lower_bound: [u8; 8] = self.0[..8].try_into().unwrap();
306        lower_bound.copy_from_slice(&self.0[..8]);
307        let lower_bound = u64::from_be_bytes(lower_bound) ^ 0x8000_0000_0000_0000u64;
308
309        let mut length: [u8; 8] = self.0[8..].try_into().unwrap();
310        length.copy_from_slice(&self.0[8..]);
311        let length = u64::from_be_bytes(length);
312
313        ArrayDimension {
314            lower_bound: isize::cast_from(lower_bound as i64),
315            length: usize::cast_from(length),
316        }
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use std::iter::empty;
323
324    use mz_ore::assert_ok;
325    use mz_proto::protobuf_roundtrip;
326    use proptest::prelude::*;
327
328    use crate::Datum;
329    use crate::row::Row;
330
331    use super::*;
332
333    #[mz_ore::test]
334    fn test_has_int2vector_dims() {
335        // 1-D array with elements: valid for int2vector
336        let mut row = Row::default();
337        row.packer()
338            .try_push_array(
339                &[ArrayDimension {
340                    lower_bound: 1,
341                    length: 2,
342                }],
343                [Datum::Int16(1), Datum::Int16(2)],
344            )
345            .unwrap();
346        let arr = row.unpack_first().unwrap_array();
347        assert!(
348            arr.has_int2vector_dims(),
349            "1-D array should have int2vector dims"
350        );
351
352        // 1-D empty array (length 0): valid
353        let mut row = Row::default();
354        row.packer()
355            .try_push_array(
356                &[ArrayDimension {
357                    lower_bound: 1,
358                    length: 0,
359                }],
360                empty::<Datum>(),
361            )
362            .unwrap();
363        let arr = row.unpack_first().unwrap_array();
364        assert!(
365            arr.has_int2vector_dims(),
366            "1-D empty array should have int2vector dims"
367        );
368
369        // 0-D empty array (PostgreSQL convention for empty): valid
370        let mut row = Row::default();
371        row.packer().try_push_array(&[], empty::<Datum>()).unwrap();
372        let arr = row.unpack_first().unwrap_array();
373        assert!(
374            arr.has_int2vector_dims(),
375            "0-D empty array should have int2vector dims"
376        );
377
378        // 2-D array: invalid for int2vector
379        let mut row = Row::default();
380        row.packer()
381            .try_push_array(
382                &[
383                    ArrayDimension {
384                        lower_bound: 1,
385                        length: 1,
386                    },
387                    ArrayDimension {
388                        lower_bound: 1,
389                        length: 2,
390                    },
391                ],
392                [Datum::Int16(1), Datum::Int16(2)],
393            )
394            .unwrap();
395        let arr = row.unpack_first().unwrap_array();
396        assert!(
397            !arr.has_int2vector_dims(),
398            "2-D array should not have int2vector dims"
399        );
400    }
401
402    proptest! {
403        #[mz_ore::test]
404        fn invalid_array_error_protobuf_roundtrip(expect in any::<InvalidArrayError>()) {
405            let actual = protobuf_roundtrip::<_, ProtoInvalidArrayError>(&expect);
406            assert_ok!(actual);
407            assert_eq!(actual.unwrap(), expect);
408        }
409    }
410
411    fn arb_array_dimension() -> impl Strategy<Value = ArrayDimension> {
412        (any::<isize>(), any::<usize>()).prop_map(|(lower, length)| ArrayDimension {
413            lower_bound: lower,
414            length,
415        })
416    }
417
418    #[mz_ore::test]
419    fn proptest_packed_array_dimension_roundtrip() {
420        fn test(og: ArrayDimension) {
421            let packed = PackedArrayDimension::from_value(og);
422            let rnd = packed.into_value();
423            assert_eq!(og, rnd);
424        }
425
426        proptest!(|(dim in arb_array_dimension())| test(dim))
427    }
428
429    #[mz_ore::test]
430    fn proptest_packed_array_dimension_sorts() {
431        fn test(mut og: Vec<ArrayDimension>) {
432            let mut packed: Vec<_> = og
433                .iter()
434                .copied()
435                .map(PackedArrayDimension::from_value)
436                .collect();
437
438            packed.sort();
439            og.sort();
440
441            let rnd: Vec<_> = packed
442                .into_iter()
443                .map(PackedArrayDimension::into_value)
444                .collect();
445            assert_eq!(og, rnd);
446        }
447
448        let strat = proptest::collection::vec(arb_array_dimension(), 0..16);
449        proptest!(|(dim in strat)| test(dim))
450    }
451}