use super::{SQLError, Value};
use uqa_core::ArrayValue;
const MAX_ARRAY_DIMENSIONS: usize = 6;
#[derive(Default)]
pub(super) struct ArrayInputShape {
first: Option<(Vec<usize>, Vec<i32>)>,
}
impl ArrayInputShape {
pub(super) fn accept(&mut self, value: &Value) -> Result<(), SQLError> {
let array = match value {
Value::Null => {
return Err(SQLError::Routine {
sqlstate: "22004".into(),
message: "cannot accumulate null arrays".into(),
})
}
Value::Array(array) => array,
other => {
return Err(SQLError::Internal(format!(
"array_agg over arrays received a non-array input {other:?}"
)))
}
};
let dimensions = array.dimensions();
let lower_bounds = array.lower_bounds();
let Some((first_dimensions, first_lower_bounds)) = &self.first else {
if dimensions.is_empty() {
return Err(array_subscript_error("cannot accumulate empty arrays"));
}
if dimensions.len() + 1 > MAX_ARRAY_DIMENSIONS {
return Err(SQLError::Routine {
sqlstate: "54000".into(),
message: format!(
"number of array dimensions ({}) exceeds the maximum allowed ({MAX_ARRAY_DIMENSIONS})",
dimensions.len() + 1
),
});
}
self.first = Some((dimensions.to_vec(), lower_bounds.to_vec()));
return Ok(());
};
if first_dimensions.as_slice() != dimensions
|| first_lower_bounds.as_slice() != lower_bounds
{
return Err(array_subscript_error(
"cannot accumulate arrays of different dimensionality",
));
}
Ok(())
}
pub(super) fn stack(values: Vec<Value>) -> Result<Value, SQLError> {
let mut shape = Self::default();
for value in &values {
shape.accept(value)?;
}
let Some((_, lower_bounds)) = shape.first else {
return Ok(Value::Null);
};
let lower_bounds = std::iter::once(1).chain(lower_bounds).collect();
ArrayValue::with_lower_bounds(values, lower_bounds)
.map(Value::Array)
.ok_or_else(|| {
SQLError::Internal("accumulated arrays do not form a rectangular array".into())
})
}
}
fn array_subscript_error(message: &str) -> SQLError {
SQLError::Routine {
sqlstate: "2202E".into(),
message: message.into(),
}
}
#[cfg(test)]
mod tests {
use super::ArrayInputShape;
use uqa_core::{ArrayValue, Value};
fn array(elements: Vec<Value>) -> Value {
Value::Array(ArrayValue::try_new(elements).unwrap())
}
#[test]
fn inputs_follow_accum_array_result_arr() {
let mut shape = ArrayInputShape::default();
assert_eq!(
shape.accept(&array(Vec::new())).unwrap_err().sqlstate(),
Some("2202E")
);
shape.accept(&array(vec![Value::Int(1)])).unwrap();
let mismatch = shape
.accept(&array(vec![Value::Int(1), Value::Int(2)]))
.unwrap_err();
assert_eq!(mismatch.sqlstate(), Some("2202E"));
assert_eq!(
mismatch.to_string(),
"cannot accumulate arrays of different dimensionality"
);
let null = shape.accept(&Value::Null).unwrap_err();
assert_eq!(null.sqlstate(), Some("22004"));
assert_eq!(null.to_string(), "cannot accumulate null arrays");
}
#[test]
fn stacking_keeps_the_input_lower_bounds() {
let shifted = |values: Vec<Value>| {
Value::Array(ArrayValue::with_lower_bounds(values, vec![0]).unwrap())
};
let Value::Array(stacked) = ArrayInputShape::stack(vec![
shifted(vec![Value::Int(1), Value::Int(2)]),
shifted(vec![Value::Int(3), Value::Null]),
])
.unwrap() else {
panic!("stacked arrays produce an array");
};
assert_eq!(stacked.dimensions(), [2, 2]);
assert_eq!(stacked.lower_bounds(), [1, 0]);
}
}