Skip to main content

uqa_sql/ast/
array_access.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Array syntax retains absent endpoints independently of nullable SQL values.
8
9use serde::{Deserialize, Serialize};
10
11use super::FunctionDispatch;
12use crate::SQLError;
13
14/// `PostgreSQL`'s admitted array-subscript rank, checked after input analysis.
15pub const MAX_ARRAY_SUBSCRIPT_DIMENSIONS: usize = 6;
16
17/// Only absent syntax sets a bit. A supplied NULL is an ordinary bound expression.
18#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub struct ArraySliceBounds {
20    omitted_lower: u8,
21    omitted_upper: u8,
22}
23
24impl ArraySliceBounds {
25    pub fn omit(&mut self, dimension: usize, lower: bool, upper: bool) {
26        // An excessive rank must reach ordered type analysis before its rank error.
27        if dimension < MAX_ARRAY_SUBSCRIPT_DIMENSIONS {
28            self.omitted_lower |= u8::from(lower) << dimension;
29            self.omitted_upper |= u8::from(upper) << dimension;
30        }
31    }
32
33    #[must_use]
34    pub fn is_omitted(self, dimension: usize, lower: bool) -> bool {
35        let bits = if lower {
36            self.omitted_lower
37        } else {
38            self.omitted_upper
39        };
40        dimension < MAX_ARRAY_SUBSCRIPT_DIMENSIONS && bits & (1 << dimension) != 0
41    }
42}
43
44/// Borrowed argument positions shared by analysis, evaluation and reconstruction.
45#[derive(Debug, Clone, Copy)]
46pub struct ArrayAccess {
47    dimensions: usize,
48    bounds: Option<ArraySliceBounds>,
49}
50
51impl ArrayAccess {
52    /// Legacy slice plans use a bare NULL node for omitted syntax; typed and computed NULLs remain supplied expressions. Their original source SQL was not persisted.
53    pub fn from_dispatch(
54        dispatch: FunctionDispatch,
55        arguments: usize,
56        is_bare_null: impl Fn(usize) -> bool,
57    ) -> Option<Result<Self, SQLError>> {
58        let slice = match dispatch {
59            FunctionDispatch::ArraySubscripts => false,
60            FunctionDispatch::ArraySlices | FunctionDispatch::ArraySlicesWithBounds(_) => true,
61            _ => return None,
62        };
63        Some((|| {
64            let count = arguments
65                .checked_sub(1)
66                .filter(|count| *count > 0)
67                .ok_or_else(|| SQLError::Internal("array access has no index".into()))?;
68            if slice && !count.is_multiple_of(2) {
69                return Err(SQLError::Internal(
70                    "array slice has an incomplete bound pair".into(),
71                ));
72            }
73            let dimensions = if slice { count / 2 } else { count };
74            let bounds = if slice {
75                let bounds = if let FunctionDispatch::ArraySlicesWithBounds(bounds) = dispatch {
76                    bounds
77                } else {
78                    let mut bounds = ArraySliceBounds::default();
79                    for dimension in 0..dimensions.min(MAX_ARRAY_SUBSCRIPT_DIMENSIONS) {
80                        bounds.omit(
81                            dimension,
82                            is_bare_null(1 + dimension * 2),
83                            is_bare_null(2 + dimension * 2),
84                        );
85                    }
86                    bounds
87                };
88                Some(bounds)
89            } else {
90                None
91            };
92            Ok(Self { dimensions, bounds })
93        })())
94    }
95
96    pub fn validate_dimensions(self) -> Result<(), SQLError> {
97        if self.dimensions > MAX_ARRAY_SUBSCRIPT_DIMENSIONS {
98            return Err(SQLError::Routine {
99                sqlstate: "54000".into(),
100                message: format!("number of array dimensions ({}) exceeds the maximum allowed ({MAX_ARRAY_SUBSCRIPT_DIMENSIONS})", self.dimensions),
101            });
102        }
103        Ok(())
104    }
105
106    #[must_use]
107    pub const fn slice_bounds(self) -> Option<ArraySliceBounds> {
108        self.bounds
109    }
110
111    #[must_use]
112    pub const fn bound_count(self) -> usize {
113        self.dimensions * if self.bounds.is_some() { 2 } else { 1 }
114    }
115
116    #[must_use]
117    pub fn is_omitted(self, argument: usize) -> bool {
118        argument > 0
119            && self
120                .bounds
121                .is_some_and(|bounds| bounds.is_omitted((argument - 1) / 2, argument % 2 == 1))
122    }
123
124    /// Execution visits all upper bounds, then all lower bounds. Analysis retains written lower/upper order instead.
125    pub fn evaluation_order(self) -> impl Iterator<Item = usize> {
126        (0..self.bound_count())
127            .map(move |position| {
128                if self.bounds.is_none() {
129                    position + 1
130                } else if position < self.dimensions {
131                    position * 2 + 2
132                } else {
133                    (position - self.dimensions) * 2 + 1
134                }
135            })
136            .filter(move |position| !self.is_omitted(*position))
137    }
138}