Skip to main content

uqa_sql/expr/casting/
array.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `PostgreSQL` array literal parsing, shape validation, and element conversion.
8
9use uqa_core::{
10    memory::{Produced, ProductionControl, ProductionString, ProductionVec},
11    ArrayValue, Value, ValueRetentionError,
12};
13
14use crate::error::{Result, SQLError};
15
16use super::cast_value_from_with_control;
17
18/// Binary array coercion retains even an empty explicit dimension; element conversion constructs an ordinary dimensionless empty array.
19pub(super) fn binary_compatible_elements(
20    source: Option<&str>,
21    target: &str,
22    control: &ProductionControl<'_>,
23) -> Result<bool> {
24    let Some(source) = source else {
25        return Ok(true);
26    };
27    let source = crate::ColumnType::from_sql_name_with_control(source, control)?;
28    let target = crate::ColumnType::from_sql_name_with_control(target, control)?;
29    Ok(*source == *target
30        || crate::type_resolution::cast_catalog_entry_with_control(&source, &target, control)?
31            .is_some_and(|entry| entry.method == crate::type_resolution::CastMethod::Binary))
32}
33
34type ArrayDimensions = Produced<Vec<(i32, usize)>>;
35
36/// Parse a `PostgreSQL` array literal (`{1,2,3}`, `{"a b",NULL}`,
37/// `{{1,2},{3,4}}`) into nested lists of string/NULL values; the caller
38/// casts elements.
39pub fn parse_pg_array_literal(text: &str) -> Result<ArrayValue> {
40    parse_pg_array_literal_with_control(text, &ProductionControl::uncontrolled())?
41        .into_uncontrolled()
42        .map_err(|_| SQLError::Internal("ordinary array parse owner".into()))
43}
44
45pub fn parse_pg_array_literal_with_control(
46    text: &str,
47    control: &ProductionControl<'_>,
48) -> Result<Produced<ArrayValue>> {
49    let mut parser = PgArrayLiteralParser::new(text, *control);
50    let (declared_dimensions, items) = parser.parse()?;
51    match array_shape_with_control(&items, control) {
52        Ok(shape) => drop(shape),
53        Err(ShapeProductionError::Shape(error)) => return Err(parser.error(error.message())),
54        Err(ShapeProductionError::Control(error)) => return Err(error.into()),
55    }
56    let array = match &declared_dimensions {
57        Some(declared) => {
58            let mut bounds = ProductionVec::new(*control);
59            bounds.reserve(declared.len())?;
60            for (lower, _) in &**declared {
61                bounds.push_copy(*lower)?;
62            }
63            ArrayValue::with_lower_bounds_with_control(items, bounds.finish()?, control)?
64        }
65        None => ArrayValue::try_new_with_control(items, control)?,
66    }
67    .ok_or_else(|| parser.error("specified array dimensions do not match array contents"))?;
68    if let Some(declared) = declared_dimensions {
69        if !declared
70            .iter()
71            .map(|(_, length)| *length)
72            .eq(array.dimensions().iter().copied())
73        {
74            return Err(parser.error("specified array dimensions do not match array contents"));
75        }
76    }
77    Ok(array)
78}
79
80pub(super) fn cast_array_elements(
81    items: &[Value],
82    element_type: &str,
83    source_element_type: Option<&str>,
84    control: &ProductionControl<'_>,
85) -> Result<Produced<Vec<Value>>> {
86    let mut output = ProductionVec::new(*control);
87    output.reserve(items.len())?;
88    for item in items {
89        let value = match item {
90            Value::List(nested) => {
91                let (nested, memory) =
92                    cast_array_elements(nested, element_type, source_element_type, control)?
93                        .into_parts();
94                control.finish(Value::List(nested), memory)?
95            }
96            other => {
97                cast_value_from_with_control(other, element_type, source_element_type, control)?
98            }
99        };
100        output.push_produced(value)?;
101    }
102    Ok(output.finish()?)
103}
104
105pub(super) struct PgArrayLiteralParser<'a, 'control> {
106    control: ProductionControl<'control>,
107    source: &'a str,
108    chars: std::iter::Peekable<std::str::Chars<'a>>,
109}
110
111type ParsedArrayLiteral = (Option<ArrayDimensions>, Produced<Vec<Value>>);
112
113impl<'a, 'control> PgArrayLiteralParser<'a, 'control> {
114    fn new(source: &'a str, control: ProductionControl<'control>) -> Self {
115        Self {
116            control,
117            source,
118            chars: source.chars().peekable(),
119        }
120    }
121
122    fn parse(&mut self) -> Result<ParsedArrayLiteral> {
123        self.skip_whitespace()?;
124        let dimensions = self.parse_dimension_declaration()?;
125        let items = self.parse_array()?;
126        self.skip_whitespace()?;
127        if self.chars.peek().is_some() {
128            return Err(self.error("unexpected content after closing brace"));
129        }
130        Ok((dimensions, items))
131    }
132
133    fn parse_dimension_declaration(&mut self) -> Result<Option<ArrayDimensions>> {
134        if self.chars.peek() != Some(&'[') {
135            return Ok(None);
136        }
137        let mut dimensions = ProductionVec::new(self.control);
138        while self.chars.next_if_eq(&'[').is_some() {
139            self.skip_whitespace()?;
140            let lower = self.parse_dimension_bound()?;
141            self.skip_whitespace()?;
142            if self.chars.next() != Some(':') {
143                return Err(self.error("array dimension must contain `:`"));
144            }
145            self.skip_whitespace()?;
146            let upper = self.parse_dimension_bound()?;
147            self.skip_whitespace()?;
148            if self.chars.next() != Some(']') {
149                return Err(self.error("array dimension is missing a closing `]`"));
150            }
151            if upper == i32::MAX {
152                return Err(SQLError::Routine {
153                    sqlstate: "54000".into(),
154                    message: format!("array upper bound is too large: {upper}"),
155                });
156            }
157            if upper < lower {
158                return Err(SQLError::Routine {
159                    sqlstate: "2202E".into(),
160                    message: "upper bound cannot be less than lower bound".into(),
161                });
162            }
163            let length = i64::from(upper)
164                .checked_sub(i64::from(lower))
165                .and_then(|difference| difference.checked_add(1))
166                .and_then(|length| usize::try_from(length).ok())
167                .ok_or_else(|| self.error("array dimension is out of range"))?;
168            dimensions.push_copy((lower, length))?;
169            self.skip_whitespace()?;
170        }
171        if self.chars.next() != Some('=') {
172            return Err(self.error("array dimensions must be followed by `=`"));
173        }
174        self.skip_whitespace()?;
175        Ok(Some(dimensions.finish()?))
176    }
177
178    fn parse_dimension_bound(&mut self) -> Result<i32> {
179        let mut text = ProductionString::new(self.control);
180        if self
181            .chars
182            .peek()
183            .is_some_and(|character| matches!(character, '+' | '-'))
184        {
185            text.push(self.chars.next().expect("peeked array bound sign"))?;
186        }
187        while self.chars.peek().is_some_and(char::is_ascii_digit) {
188            text.push(self.chars.next().expect("peeked array bound digit"))?;
189        }
190        if text.is_empty() || matches!(&*text, "+" | "-") {
191            return Err(self.error("array dimension bound must be an integer"));
192        }
193        text.parse()
194            .map_err(|_| self.error("array dimension bound is out of range"))
195    }
196
197    fn parse_array(&mut self) -> Result<Produced<Vec<Value>>> {
198        if self.chars.next() != Some('{') {
199            return Err(SQLError::Diagnostic {
200                sqlstate: "22P02".into(),
201                message: format!("malformed array literal: \"{}\"", self.source),
202                detail: Some("Array value must start with \"{\" or dimension information.".into()),
203                hint: None,
204            });
205        }
206        self.skip_whitespace()?;
207        if self.chars.next_if_eq(&'}').is_some() {
208            return Ok(ProductionVec::new(self.control).finish()?);
209        }
210
211        let mut items = ProductionVec::new(self.control);
212        loop {
213            self.skip_whitespace()?;
214            items.push_produced(self.parse_element()?)?;
215            self.skip_whitespace()?;
216            match self.chars.next() {
217                Some(',') => {
218                    self.skip_whitespace()?;
219                    if matches!(self.chars.peek(), None | Some('}')) {
220                        return Err(self.error("array contains a missing element"));
221                    }
222                }
223                Some('}') => break,
224                Some(_) => {
225                    return Err(self.error("array elements must be separated by commas"));
226                }
227                None => return Err(self.error("array is missing a closing `}`")),
228            }
229        }
230        Ok(items.finish()?)
231    }
232
233    fn parse_element(&mut self) -> Result<Produced<Value>> {
234        match self.chars.peek() {
235            Some('{') => {
236                let (value, memory) = self.parse_array()?.into_parts();
237                Ok(self.control.finish(Value::List(value), memory)?)
238            }
239            Some('"') => {
240                let (value, memory) = self.parse_quoted_element()?.into_parts();
241                Ok(self.control.finish(Value::Str(value), memory)?)
242            }
243            Some(',') | Some('}') | None => Err(self.error("array contains a missing element")),
244            Some(_) => self.parse_unquoted_element(),
245        }
246    }
247
248    fn parse_quoted_element(&mut self) -> Result<Produced<String>> {
249        let _opening_quote = self.chars.next();
250        let mut value = ProductionString::new(self.control);
251        loop {
252            match self.chars.next() {
253                Some('"') => return Ok(value.finish()?),
254                Some('\\') => value.push(
255                    self.chars
256                        .next()
257                        .ok_or_else(|| self.error("quoted element ends with an escape"))?,
258                )?,
259                Some(character) => value.push(character)?,
260                None => return Err(self.error("array contains an unterminated quoted element")),
261            }
262        }
263    }
264
265    fn parse_unquoted_element(&mut self) -> Result<Produced<Value>> {
266        let mut value = ProductionString::new(self.control);
267        let mut significant_len = 0;
268        let mut was_escaped = false;
269        while let Some(character) = self.chars.peek().copied() {
270            match character {
271                ',' | '}' => break,
272                '{' | '"' => {
273                    return Err(self.error("array contains an unescaped special character"));
274                }
275                '\\' => {
276                    let _escape = self.chars.next();
277                    let escaped = self
278                        .chars
279                        .next()
280                        .ok_or_else(|| self.error("array element ends with an escape"))?;
281                    value.push(escaped)?;
282                    significant_len = value.len();
283                    was_escaped = true;
284                }
285                _ => {
286                    let _character = self.chars.next();
287                    value.push(character)?;
288                    if !character.is_whitespace() {
289                        significant_len = value.len();
290                    }
291                }
292            }
293        }
294        let value = value.finish()?;
295        let significant = &value[..significant_len];
296        if significant.is_empty() {
297            return Err(self.error("array contains a missing element"));
298        }
299        if !was_escaped && significant.eq_ignore_ascii_case("null") {
300            Ok(self
301                .control
302                .finish(Value::Null, self.control.empty_reservation())?)
303        } else {
304            let (mut value, memory) = value.into_parts();
305            value.truncate(significant_len);
306            Ok(self.control.finish(Value::Str(value), memory)?)
307        }
308    }
309
310    fn skip_whitespace(&mut self) -> Result<()> {
311        self.control.check()?;
312        while self
313            .chars
314            .next_if(|character| character.is_whitespace())
315            .is_some()
316        {
317            self.control.check()?;
318        }
319        Ok(())
320    }
321
322    fn error(&self, detail: &str) -> SQLError {
323        SQLError::Routine {
324            sqlstate: "22P02".into(),
325            message: format!("malformed array literal: \"{}\" ({detail})", self.source),
326        }
327    }
328}
329
330#[derive(Clone, Copy, Debug, PartialEq, Eq)]
331pub(super) enum ArrayShapeError {
332    MixedNesting,
333    MismatchedDimensions,
334}
335
336impl ArrayShapeError {
337    fn message(self) -> &'static str {
338        match self {
339            Self::MixedNesting => "cannot mix nested arrays and scalar elements",
340            Self::MismatchedDimensions => "multidimensional arrays must have matching dimensions",
341        }
342    }
343}
344
345pub(super) fn array_shape(items: &[Value]) -> std::result::Result<Vec<usize>, ArrayShapeError> {
346    match array_shape_with_control(items, &ProductionControl::uncontrolled()) {
347        Ok(shape) => Ok(shape.into_uncontrolled().expect("ordinary array shape")),
348        Err(ShapeProductionError::Shape(error)) => Err(error),
349        Err(ShapeProductionError::Control(_)) => unreachable!("ordinary shape production"),
350    }
351}
352
353enum ShapeProductionError {
354    Shape(ArrayShapeError),
355    Control(ValueRetentionError),
356}
357impl From<ValueRetentionError> for ShapeProductionError {
358    fn from(error: ValueRetentionError) -> Self {
359        Self::Control(error)
360    }
361}
362
363fn array_shape_with_control(
364    items: &[Value],
365    control: &ProductionControl<'_>,
366) -> std::result::Result<Produced<Vec<usize>>, ShapeProductionError> {
367    let mut dimensions = ProductionVec::new(*control);
368    dimensions.push_copy(items.len())?;
369    let mut nested_shape: Option<Produced<Vec<usize>>> = None;
370    let mut has_scalar = false;
371    for item in items {
372        control.check()?;
373        if let Value::List(nested) = item {
374            let shape = array_shape_with_control(nested, control)?;
375            if has_scalar {
376                return Err(ShapeProductionError::Shape(ArrayShapeError::MixedNesting));
377            }
378            if nested_shape
379                .as_ref()
380                .is_some_and(|expected| **expected != *shape)
381            {
382                return Err(ShapeProductionError::Shape(
383                    ArrayShapeError::MismatchedDimensions,
384                ));
385            }
386            nested_shape = Some(shape);
387        } else {
388            if nested_shape.is_some() {
389                return Err(ShapeProductionError::Shape(ArrayShapeError::MixedNesting));
390            }
391            has_scalar = true;
392        }
393    }
394    if let Some(shape) = nested_shape {
395        for length in &*shape {
396            dimensions.push_copy(*length)?;
397        }
398    }
399    Ok(dimensions.finish()?)
400}
401
402/// Return every dimension of a rectangular array value.
403///
404/// `PostgreSQL` arrays cannot mix scalar and nested elements or contain
405/// sub-arrays with different extents.
406pub fn array_dimensions(items: &[Value]) -> Result<Vec<usize>> {
407    array_shape(items).map_err(|error| SQLError::TypeMismatch(error.message().to_string()))
408}