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::{ArrayValue, Value};
10
11use crate::error::{Result, SQLError};
12
13use super::cast_value_from;
14
15/// Parse a `PostgreSQL` array literal (`{1,2,3}`, `{"a b",NULL}`,
16/// `{{1,2},{3,4}}`) into nested lists of string/NULL values; the caller
17/// casts elements.
18pub fn parse_pg_array_literal(text: &str) -> Result<ArrayValue> {
19    let mut parser = PgArrayLiteralParser::new(text);
20    let (declared_dimensions, items) = parser.parse()?;
21    if let Err(error) = array_shape(&items) {
22        return Err(SQLError::Routine {
23            sqlstate: "22P02".into(),
24            message: format!("malformed array literal: \"{text}\" ({})", error.message()),
25        });
26    }
27    let array = ArrayValue::try_new(items).ok_or_else(|| SQLError::Routine {
28        sqlstate: "22P02".into(),
29        message: format!("malformed array literal: \"{text}\""),
30    })?;
31    let Some(declared_dimensions) = declared_dimensions else {
32        return Ok(array);
33    };
34    let declared_lengths = declared_dimensions
35        .iter()
36        .map(|(_, length)| *length)
37        .collect::<Vec<_>>();
38    if declared_lengths != array.dimensions() {
39        return Err(SQLError::Routine {
40            sqlstate: "22P02".into(),
41            message: format!(
42                "malformed array literal: \"{text}\" (specified array dimensions do not match array contents)"
43            ),
44        });
45    }
46    let lower_bounds = declared_dimensions
47        .into_iter()
48        .map(|(lower, _)| lower)
49        .collect();
50    ArrayValue::with_lower_bounds(array.into_elements(), lower_bounds).ok_or_else(|| {
51        SQLError::Routine {
52            sqlstate: "22P02".into(),
53            message: format!("malformed array literal: \"{text}\""),
54        }
55    })
56}
57
58pub(super) fn cast_array_elements(
59    items: &[Value],
60    element_type: &str,
61    source_element_type: Option<&str>,
62) -> Result<Vec<Value>> {
63    items
64        .iter()
65        .map(|item| match item {
66            Value::List(nested) => {
67                cast_array_elements(nested, element_type, source_element_type).map(Value::List)
68            }
69            other => cast_value_from(other, element_type, source_element_type),
70        })
71        .collect()
72}
73
74pub(super) struct PgArrayLiteralParser<'a> {
75    source: &'a str,
76    chars: std::iter::Peekable<std::str::Chars<'a>>,
77}
78
79type ParsedArrayLiteral = (Option<Vec<(i32, usize)>>, Vec<Value>);
80
81impl<'a> PgArrayLiteralParser<'a> {
82    fn new(source: &'a str) -> Self {
83        Self {
84            source,
85            chars: source.chars().peekable(),
86        }
87    }
88
89    fn parse(&mut self) -> Result<ParsedArrayLiteral> {
90        self.skip_whitespace();
91        let dimensions = self.parse_dimension_declaration()?;
92        let items = self.parse_array()?;
93        self.skip_whitespace();
94        if self.chars.peek().is_some() {
95            return Err(self.error("unexpected content after closing brace"));
96        }
97        Ok((dimensions, items))
98    }
99
100    fn parse_dimension_declaration(&mut self) -> Result<Option<Vec<(i32, usize)>>> {
101        if self.chars.peek() != Some(&'[') {
102            return Ok(None);
103        }
104        let mut dimensions = Vec::new();
105        while self.chars.next_if_eq(&'[').is_some() {
106            self.skip_whitespace();
107            let lower = self.parse_dimension_bound()?;
108            self.skip_whitespace();
109            if self.chars.next() != Some(':') {
110                return Err(self.error("array dimension must contain `:`"));
111            }
112            self.skip_whitespace();
113            let upper = self.parse_dimension_bound()?;
114            self.skip_whitespace();
115            if self.chars.next() != Some(']') {
116                return Err(self.error("array dimension is missing a closing `]`"));
117            }
118            if upper == i32::MAX {
119                return Err(SQLError::Routine {
120                    sqlstate: "54000".into(),
121                    message: format!("array upper bound is too large: {upper}"),
122                });
123            }
124            if upper < lower {
125                return Err(SQLError::Routine {
126                    sqlstate: "2202E".into(),
127                    message: "upper bound cannot be less than lower bound".into(),
128                });
129            }
130            let length = i64::from(upper)
131                .checked_sub(i64::from(lower))
132                .and_then(|difference| difference.checked_add(1))
133                .and_then(|length| usize::try_from(length).ok())
134                .ok_or_else(|| self.error("array dimension is out of range"))?;
135            dimensions.push((lower, length));
136            self.skip_whitespace();
137        }
138        if self.chars.next() != Some('=') {
139            return Err(self.error("array dimensions must be followed by `=`"));
140        }
141        self.skip_whitespace();
142        Ok(Some(dimensions))
143    }
144
145    fn parse_dimension_bound(&mut self) -> Result<i32> {
146        let mut text = String::new();
147        if self
148            .chars
149            .peek()
150            .is_some_and(|character| matches!(character, '+' | '-'))
151        {
152            text.push(self.chars.next().expect("peeked array bound sign"));
153        }
154        while self.chars.peek().is_some_and(char::is_ascii_digit) {
155            text.push(self.chars.next().expect("peeked array bound digit"));
156        }
157        if text.is_empty() || matches!(text.as_str(), "+" | "-") {
158            return Err(self.error("array dimension bound must be an integer"));
159        }
160        text.parse()
161            .map_err(|_| self.error("array dimension bound is out of range"))
162    }
163
164    fn parse_array(&mut self) -> Result<Vec<Value>> {
165        if self.chars.next() != Some('{') {
166            return Err(self.error("array value must start with `{`"));
167        }
168        self.skip_whitespace();
169        if self.chars.next_if_eq(&'}').is_some() {
170            return Ok(Vec::new());
171        }
172
173        let mut items = Vec::new();
174        loop {
175            self.skip_whitespace();
176            items.push(self.parse_element()?);
177            self.skip_whitespace();
178            match self.chars.next() {
179                Some(',') => {
180                    self.skip_whitespace();
181                    if matches!(self.chars.peek(), None | Some('}')) {
182                        return Err(self.error("array contains a missing element"));
183                    }
184                }
185                Some('}') => break,
186                Some(_) => {
187                    return Err(self.error("array elements must be separated by commas"));
188                }
189                None => return Err(self.error("array is missing a closing `}`")),
190            }
191        }
192        Ok(items)
193    }
194
195    fn parse_element(&mut self) -> Result<Value> {
196        match self.chars.peek() {
197            Some('{') => self.parse_array().map(Value::List),
198            Some('"') => self.parse_quoted_element().map(Value::Str),
199            Some(',') | Some('}') | None => Err(self.error("array contains a missing element")),
200            Some(_) => self.parse_unquoted_element(),
201        }
202    }
203
204    fn parse_quoted_element(&mut self) -> Result<String> {
205        let _opening_quote = self.chars.next();
206        let mut value = String::new();
207        loop {
208            match self.chars.next() {
209                Some('"') => return Ok(value),
210                Some('\\') => value.push(
211                    self.chars
212                        .next()
213                        .ok_or_else(|| self.error("quoted element ends with an escape"))?,
214                ),
215                Some(character) => value.push(character),
216                None => return Err(self.error("array contains an unterminated quoted element")),
217            }
218        }
219    }
220
221    fn parse_unquoted_element(&mut self) -> Result<Value> {
222        let mut value = String::new();
223        let mut significant_len = 0;
224        let mut was_escaped = false;
225        while let Some(character) = self.chars.peek().copied() {
226            match character {
227                ',' | '}' => break,
228                '{' | '"' => {
229                    return Err(self.error("array contains an unescaped special character"));
230                }
231                '\\' => {
232                    let _escape = self.chars.next();
233                    let escaped = self
234                        .chars
235                        .next()
236                        .ok_or_else(|| self.error("array element ends with an escape"))?;
237                    value.push(escaped);
238                    significant_len = value.len();
239                    was_escaped = true;
240                }
241                _ => {
242                    let _character = self.chars.next();
243                    value.push(character);
244                    if !character.is_whitespace() {
245                        significant_len = value.len();
246                    }
247                }
248            }
249        }
250        value.truncate(significant_len);
251        if value.is_empty() {
252            return Err(self.error("array contains a missing element"));
253        }
254        if !was_escaped && value.eq_ignore_ascii_case("null") {
255            Ok(Value::Null)
256        } else {
257            Ok(Value::Str(value))
258        }
259    }
260
261    fn skip_whitespace(&mut self) {
262        while self
263            .chars
264            .next_if(|character| character.is_whitespace())
265            .is_some()
266        {}
267    }
268
269    fn error(&self, detail: &str) -> SQLError {
270        SQLError::Routine {
271            sqlstate: "22P02".into(),
272            message: format!("malformed array literal: \"{}\" ({detail})", self.source),
273        }
274    }
275}
276
277#[derive(Clone, Copy, Debug, PartialEq, Eq)]
278pub(super) enum ArrayShapeError {
279    MixedNesting,
280    MismatchedDimensions,
281}
282
283impl ArrayShapeError {
284    fn message(self) -> &'static str {
285        match self {
286            Self::MixedNesting => "cannot mix nested arrays and scalar elements",
287            Self::MismatchedDimensions => "multidimensional arrays must have matching dimensions",
288        }
289    }
290}
291
292pub(super) fn array_shape(items: &[Value]) -> std::result::Result<Vec<usize>, ArrayShapeError> {
293    let mut dimensions = vec![items.len()];
294    let mut nested_shape: Option<Vec<usize>> = None;
295    let mut has_scalar = false;
296    for item in items {
297        if let Value::List(nested) = item {
298            let shape = array_shape(nested)?;
299            if has_scalar {
300                return Err(ArrayShapeError::MixedNesting);
301            }
302            if nested_shape
303                .as_ref()
304                .is_some_and(|expected| *expected != shape)
305            {
306                return Err(ArrayShapeError::MismatchedDimensions);
307            }
308            nested_shape = Some(shape);
309        } else {
310            if nested_shape.is_some() {
311                return Err(ArrayShapeError::MixedNesting);
312            }
313            has_scalar = true;
314        }
315    }
316    if let Some(shape) = nested_shape {
317        dimensions.extend(shape);
318    }
319    Ok(dimensions)
320}
321
322/// Return every dimension of a rectangular array value.
323///
324/// `PostgreSQL` arrays cannot mix scalar and nested elements or contain
325/// sub-arrays with different extents.
326pub fn array_dimensions(items: &[Value]) -> Result<Vec<usize>> {
327    array_shape(items).map_err(|error| SQLError::TypeMismatch(error.message().to_string()))
328}