1use 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
29pub const MAX_ARRAY_DIMENSIONS: u8 = 6;
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
41pub struct Array<'a, T = Datum<'a>> {
42 pub(crate) elements: DatumList<'a, T>,
44 pub(crate) dims: ArrayDimensions<'a>,
46}
47
48impl<'a, T> Array<'a, T> {
49 pub fn dims(&self) -> ArrayDimensions<'a> {
51 self.dims
52 }
53
54 pub fn elements(&self) -> DatumList<'a, T> {
56 self.elements
57 }
58
59 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#[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 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 pub fn len(&self) -> usize {
97 self.ndims().into()
98 }
99
100 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#[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#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
161pub struct ArrayDimension {
162 pub lower_bound: isize,
165 pub length: usize,
167}
168
169impl ArrayDimension {
170 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#[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 TooManyDimensions(usize),
201 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#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
269pub struct PackedArrayDimension([u8; Self::SIZE]);
270
271#[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 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 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 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 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}