From 75e851f8c609bbdaefe2ab325de1838b8ea003fd Mon Sep 17 00:00:00 2001 From: Moritz Hoffmann Date: Tue, 7 Oct 2025 10:38:22 +0200 Subject: [PATCH 1/2] Allow null values in multi-dimensional arrays Allow constructing multi-dimensional arrays with null values instead of panicking. Similarly to PostgreSQL, treat null elements as zero-dimensional arrays. Fixes MaterializeInc/database-issues#9757 Signed-off-by: Moritz Hoffmann --- src/expr/src/scalar/func/variadic.rs | 48 ++++++++++++++++++++++------ test/sqllogictest/arrays.slt | 10 ++++++ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/expr/src/scalar/func/variadic.rs b/src/expr/src/scalar/func/variadic.rs index af646735dff28..9939917ecdd06 100644 --- a/src/expr/src/scalar/func/variadic.rs +++ b/src/expr/src/scalar/func/variadic.rs @@ -23,7 +23,7 @@ use md5::Md5; use mz_lowertest::MzReflect; use mz_ore::cast::{CastFrom, ReinterpretCast}; use mz_pgtz::timezone::TimezoneSpec; -use mz_repr::adt::array::ArrayDimension; +use mz_repr::adt::array::{ArrayDimension, InvalidArrayError}; use mz_repr::adt::mz_acl_item::{AclItem, AclMode, MzAclItem}; use mz_repr::adt::range::{InvalidRangeError, Range, RangeBound, parse_range_bound_flags}; use mz_repr::adt::system::Oid; @@ -77,21 +77,49 @@ pub fn and<'a>( /// the SQL type system, rather than checked here at runtime.) /// /// If all input arrays are zero-dimensional arrays, then the output is a zero- -/// dimensional array. Otherwise the lower bound of the additional dimension is +/// dimensional array. Otherwise, the lower bound of the additional dimension is /// one and the length of the new dimension is equal to `datums.len()`. +/// +/// Null elements are allowed and considered to be zero-dimensional arrays. fn array_create_multidim<'a>( datums: &[Datum<'a>], temp_storage: &'a RowArena, ) -> Result, EvalError> { - // Per PostgreSQL, if all input arrays are zero dimensional, so is the - // output. - if datums.iter().all(|d| d.unwrap_array().dims().is_empty()) { - let dims = &[]; - let datums = &[]; - let datum = temp_storage.try_make_datum(|packer| packer.try_push_array(dims, datums))?; - return Ok(datum); + // `true` if any element null or has a dimension of 0. + let mut have_empty = false; + let mut dim = None; + for datum in datums { + if datum.is_null() { + have_empty = true; + continue; + } + let actual = datum.unwrap_array().dims().len(); + if actual == 0 { + have_empty = true; + } + match dim { + Some(expected) if actual != expected => { + // All non-null arrays must have the same dimensionality. + return Err(InvalidArrayError::WrongCardinality { actual, expected }.into()); + } + _ => dim = Some(actual), + } + } + if have_empty { + return match dim { + // Per PostgreSQL, if all input arrays are zero dimensional, so is the + // output. + None | Some(0) => { + Ok(temp_storage.try_make_datum(|packer| packer.try_push_array(&[], &[]))?) + } + // Mixing zero-dimensional arrays with non-zero-dimensional arrays is + // not allowed. + Some(expected) => { + let actual = 0; + Err(InvalidArrayError::WrongCardinality { actual, expected }.into()) + } + }; } - let mut dims = vec![ArrayDimension { lower_bound: 1, length: datums.len(), diff --git a/test/sqllogictest/arrays.slt b/test/sqllogictest/arrays.slt index 3d3dde90c8168..5bfafd8d979d0 100644 --- a/test/sqllogictest/arrays.slt +++ b/test/sqllogictest/arrays.slt @@ -1460,3 +1460,13 @@ statement ok CREATE MATERIALIZED VIEW json_mv AS ( SELECT * FROM jsons WHERE random_id = CAST(payload->>'my_field' AS uuid[])[random_index] ) + +# Regression test for issue #9757 + +query error db error: ERROR: number of array elements \(0\) does not match declared cardinality \(1\) +SELECT ARRAY[NULL::BIGINT[], ARRAY[1::BIGINT, 2::BIGINT]]; + +query T +SELECT ARRAY[NULL::BIGINT[], ARRAY[]::BIGINT[], NULL::BIGINT[]]; +---- +{} From 9db54d04356da50cfc4f35f542da44d4e70a1597 Mon Sep 17 00:00:00 2001 From: Moritz Hoffmann Date: Thu, 9 Oct 2025 16:22:03 +0200 Subject: [PATCH 2/2] Catch invalid dimesions Signed-off-by: Moritz Hoffmann --- src/expr/src/scalar/func/variadic.rs | 57 +++++++++++++--------------- src/repr/src/adt/array.rs | 6 +++ test/sqllogictest/arrays.slt | 7 +++- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/expr/src/scalar/func/variadic.rs b/src/expr/src/scalar/func/variadic.rs index 9939917ecdd06..dbd341cf0a982 100644 --- a/src/expr/src/scalar/func/variadic.rs +++ b/src/expr/src/scalar/func/variadic.rs @@ -23,7 +23,7 @@ use md5::Md5; use mz_lowertest::MzReflect; use mz_ore::cast::{CastFrom, ReinterpretCast}; use mz_pgtz::timezone::TimezoneSpec; -use mz_repr::adt::array::{ArrayDimension, InvalidArrayError}; +use mz_repr::adt::array::{ArrayDimension, ArrayDimensions, InvalidArrayError}; use mz_repr::adt::mz_acl_item::{AclItem, AclMode, MzAclItem}; use mz_repr::adt::range::{InvalidRangeError, Range, RangeBound, parse_range_bound_flags}; use mz_repr::adt::system::Oid; @@ -85,41 +85,38 @@ fn array_create_multidim<'a>( datums: &[Datum<'a>], temp_storage: &'a RowArena, ) -> Result, EvalError> { - // `true` if any element null or has a dimension of 0. - let mut have_empty = false; - let mut dim = None; + let mut dim: Option = None; for datum in datums { - if datum.is_null() { - have_empty = true; - continue; - } - let actual = datum.unwrap_array().dims().len(); - if actual == 0 { - have_empty = true; - } - match dim { - Some(expected) if actual != expected => { - // All non-null arrays must have the same dimensionality. + let actual_dims = match datum { + Datum::Null => ArrayDimensions::default(), + Datum::Array(arr) => arr.dims(), + d => panic!("unexpected datum {d}"), + }; + if let Some(expected) = &dim { + if actual_dims.ndims() != expected.ndims() { + let actual = actual_dims.ndims().into(); + let expected = expected.ndims().into(); + // All input arrays must have the same dimensionality. + return Err(InvalidArrayError::WrongCardinality { actual, expected }.into()); + } + if let Some((e, a)) = expected + .into_iter() + .zip_eq(actual_dims.into_iter()) + .find(|(e, a)| e != a) + { + let actual = a.length; + let expected = e.length; + // All input arrays must have the same dimensionality. return Err(InvalidArrayError::WrongCardinality { actual, expected }.into()); } - _ => dim = Some(actual), } + dim = Some(actual_dims); } - if have_empty { - return match dim { - // Per PostgreSQL, if all input arrays are zero dimensional, so is the - // output. - None | Some(0) => { - Ok(temp_storage.try_make_datum(|packer| packer.try_push_array(&[], &[]))?) - } - // Mixing zero-dimensional arrays with non-zero-dimensional arrays is - // not allowed. - Some(expected) => { - let actual = 0; - Err(InvalidArrayError::WrongCardinality { actual, expected }.into()) - } - }; + // Per PostgreSQL, if all input arrays are zero dimensional, so is the output. + if dim.as_ref().map_or(true, ArrayDimensions::is_empty) { + return Ok(temp_storage.try_make_datum(|packer| packer.try_push_array(&[], &[]))?); } + let mut dims = vec![ArrayDimension { lower_bound: 1, length: datums.len(), diff --git a/src/repr/src/adt/array.rs b/src/repr/src/adt/array.rs index b69250a318d53..c9a6f6401e462 100644 --- a/src/repr/src/adt/array.rs +++ b/src/repr/src/adt/array.rs @@ -54,6 +54,12 @@ pub struct ArrayDimensions<'a> { pub(crate) data: &'a [u8], } +impl Default for ArrayDimensions<'static> { + fn default() -> Self { + Self { data: &[] } + } +} + impl ArrayDimensions<'_> { /// Returns the number of dimensions in the array as a [`u8`]. pub fn ndims(&self) -> u8 { diff --git a/test/sqllogictest/arrays.slt b/test/sqllogictest/arrays.slt index 5bfafd8d979d0..465494973e446 100644 --- a/test/sqllogictest/arrays.slt +++ b/test/sqllogictest/arrays.slt @@ -1461,12 +1461,15 @@ CREATE MATERIALIZED VIEW json_mv AS ( SELECT * FROM jsons WHERE random_id = CAST(payload->>'my_field' AS uuid[])[random_index] ) -# Regression test for issue #9757 +# Regression test for issue MaterializeInc/database-issues#9757 -query error db error: ERROR: number of array elements \(0\) does not match declared cardinality \(1\) +query error db error: ERROR: number of array elements \(1\) does not match declared cardinality \(0\) SELECT ARRAY[NULL::BIGINT[], ARRAY[1::BIGINT, 2::BIGINT]]; query T SELECT ARRAY[NULL::BIGINT[], ARRAY[]::BIGINT[], NULL::BIGINT[]]; ---- {} + +query error db error: ERROR: number of array elements \(3\) does not match declared cardinality \(2\) +SELECT ARRAY[ARRAY[1, 2], ARRAY[3, 4, 5], ARRAY[6]];