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(self.error("array value must start with `{`"));
200        }
201        self.skip_whitespace()?;
202        if self.chars.next_if_eq(&'}').is_some() {
203            return Ok(ProductionVec::new(self.control).finish()?);
204        }
205
206        let mut items = ProductionVec::new(self.control);
207        loop {
208            self.skip_whitespace()?;
209            items.push_produced(self.parse_element()?)?;
210            self.skip_whitespace()?;
211            match self.chars.next() {
212                Some(',') => {
213                    self.skip_whitespace()?;
214                    if matches!(self.chars.peek(), None | Some('}')) {
215                        return Err(self.error("array contains a missing element"));
216                    }
217                }
218                Some('}') => break,
219                Some(_) => {
220                    return Err(self.error("array elements must be separated by commas"));
221                }
222                None => return Err(self.error("array is missing a closing `}`")),
223            }
224        }
225        Ok(items.finish()?)
226    }
227
228    fn parse_element(&mut self) -> Result<Produced<Value>> {
229        match self.chars.peek() {
230            Some('{') => {
231                let (value, memory) = self.parse_array()?.into_parts();
232                Ok(self.control.finish(Value::List(value), memory)?)
233            }
234            Some('"') => {
235                let (value, memory) = self.parse_quoted_element()?.into_parts();
236                Ok(self.control.finish(Value::Str(value), memory)?)
237            }
238            Some(',') | Some('}') | None => Err(self.error("array contains a missing element")),
239            Some(_) => self.parse_unquoted_element(),
240        }
241    }
242
243    fn parse_quoted_element(&mut self) -> Result<Produced<String>> {
244        let _opening_quote = self.chars.next();
245        let mut value = ProductionString::new(self.control);
246        loop {
247            match self.chars.next() {
248                Some('"') => return Ok(value.finish()?),
249                Some('\\') => value.push(
250                    self.chars
251                        .next()
252                        .ok_or_else(|| self.error("quoted element ends with an escape"))?,
253                )?,
254                Some(character) => value.push(character)?,
255                None => return Err(self.error("array contains an unterminated quoted element")),
256            }
257        }
258    }
259
260    fn parse_unquoted_element(&mut self) -> Result<Produced<Value>> {
261        let mut value = ProductionString::new(self.control);
262        let mut significant_len = 0;
263        let mut was_escaped = false;
264        while let Some(character) = self.chars.peek().copied() {
265            match character {
266                ',' | '}' => break,
267                '{' | '"' => {
268                    return Err(self.error("array contains an unescaped special character"));
269                }
270                '\\' => {
271                    let _escape = self.chars.next();
272                    let escaped = self
273                        .chars
274                        .next()
275                        .ok_or_else(|| self.error("array element ends with an escape"))?;
276                    value.push(escaped)?;
277                    significant_len = value.len();
278                    was_escaped = true;
279                }
280                _ => {
281                    let _character = self.chars.next();
282                    value.push(character)?;
283                    if !character.is_whitespace() {
284                        significant_len = value.len();
285                    }
286                }
287            }
288        }
289        let value = value.finish()?;
290        let significant = &value[..significant_len];
291        if significant.is_empty() {
292            return Err(self.error("array contains a missing element"));
293        }
294        if !was_escaped && significant.eq_ignore_ascii_case("null") {
295            Ok(self
296                .control
297                .finish(Value::Null, self.control.empty_reservation())?)
298        } else {
299            let (mut value, memory) = value.into_parts();
300            value.truncate(significant_len);
301            Ok(self.control.finish(Value::Str(value), memory)?)
302        }
303    }
304
305    fn skip_whitespace(&mut self) -> Result<()> {
306        self.control.check()?;
307        while self
308            .chars
309            .next_if(|character| character.is_whitespace())
310            .is_some()
311        {
312            self.control.check()?;
313        }
314        Ok(())
315    }
316
317    fn error(&self, detail: &str) -> SQLError {
318        SQLError::Routine {
319            sqlstate: "22P02".into(),
320            message: format!("malformed array literal: \"{}\" ({detail})", self.source),
321        }
322    }
323}
324
325#[derive(Clone, Copy, Debug, PartialEq, Eq)]
326pub(super) enum ArrayShapeError {
327    MixedNesting,
328    MismatchedDimensions,
329}
330
331impl ArrayShapeError {
332    fn message(self) -> &'static str {
333        match self {
334            Self::MixedNesting => "cannot mix nested arrays and scalar elements",
335            Self::MismatchedDimensions => "multidimensional arrays must have matching dimensions",
336        }
337    }
338}
339
340pub(super) fn array_shape(items: &[Value]) -> std::result::Result<Vec<usize>, ArrayShapeError> {
341    match array_shape_with_control(items, &ProductionControl::uncontrolled()) {
342        Ok(shape) => Ok(shape.into_uncontrolled().expect("ordinary array shape")),
343        Err(ShapeProductionError::Shape(error)) => Err(error),
344        Err(ShapeProductionError::Control(_)) => unreachable!("ordinary shape production"),
345    }
346}
347
348enum ShapeProductionError {
349    Shape(ArrayShapeError),
350    Control(ValueRetentionError),
351}
352impl From<ValueRetentionError> for ShapeProductionError {
353    fn from(error: ValueRetentionError) -> Self {
354        Self::Control(error)
355    }
356}
357
358fn array_shape_with_control(
359    items: &[Value],
360    control: &ProductionControl<'_>,
361) -> std::result::Result<Produced<Vec<usize>>, ShapeProductionError> {
362    let mut dimensions = ProductionVec::new(*control);
363    dimensions.push_copy(items.len())?;
364    let mut nested_shape: Option<Produced<Vec<usize>>> = None;
365    let mut has_scalar = false;
366    for item in items {
367        control.check()?;
368        if let Value::List(nested) = item {
369            let shape = array_shape_with_control(nested, control)?;
370            if has_scalar {
371                return Err(ShapeProductionError::Shape(ArrayShapeError::MixedNesting));
372            }
373            if nested_shape
374                .as_ref()
375                .is_some_and(|expected| **expected != *shape)
376            {
377                return Err(ShapeProductionError::Shape(
378                    ArrayShapeError::MismatchedDimensions,
379                ));
380            }
381            nested_shape = Some(shape);
382        } else {
383            if nested_shape.is_some() {
384                return Err(ShapeProductionError::Shape(ArrayShapeError::MixedNesting));
385            }
386            has_scalar = true;
387        }
388    }
389    if let Some(shape) = nested_shape {
390        for length in &*shape {
391            dimensions.push_copy(*length)?;
392        }
393    }
394    Ok(dimensions.finish()?)
395}
396
397/// Return every dimension of a rectangular array value.
398///
399/// `PostgreSQL` arrays cannot mix scalar and nested elements or contain
400/// sub-arrays with different extents.
401pub fn array_dimensions(items: &[Value]) -> Result<Vec<usize>> {
402    array_shape(items).map_err(|error| SQLError::TypeMismatch(error.message().to_string()))
403}