Skip to main content

uqa_sql/assignment/
targets.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Type selection and duplicate-column rules for whole and subscripted assignments.
8
9use crate::{
10    ast::{AssignmentStep, AssignmentTarget},
11    ColumnType, SQLError,
12};
13use std::collections::BTreeMap;
14
15#[cfg(test)]
16mod tests;
17
18fn error(code: &str, message: String) -> SQLError {
19    SQLError::Routine {
20        sqlstate: code.into(),
21        message,
22    }
23}
24
25/// Whole-column writes may target an open schema. Partial writes require a declared container type before evaluating bounds or values.
26pub fn validate_assignment_type<E>(
27    target: &AssignmentTarget<E>,
28    declared: Option<&ColumnType>,
29) -> Result<(), SQLError> {
30    if declared.is_none() && !target.is_whole_column() {
31        array_assignment_type(target, &ColumnType::Named("unknown".into()))?;
32    }
33    Ok(())
34}
35
36/// Repeated partial targets compose, but a whole-column write cannot share its destination.
37pub fn validate_repeated_targets<'a, E: 'a>(
38    targets: impl IntoIterator<Item = &'a AssignmentTarget<E>>,
39    insert: bool,
40) -> Result<(), SQLError> {
41    let mut seen = BTreeMap::new();
42    for target in targets {
43        let whole = target.is_whole_column();
44        if let Some(previous) = seen.insert(&target.column, whole) {
45            if whole || previous {
46                return Err(if insert {
47                    error(
48                        "42701",
49                        format!("column \"{}\" specified more than once", target.column),
50                    )
51                } else {
52                    error(
53                        "42601",
54                        format!("multiple assignments to same column \"{}\"", target.column),
55                    )
56                });
57            }
58        }
59    }
60    Ok(())
61}
62
63/// The array container produced by subscripting, before restoring a domain or legacy vector type.
64pub fn array_assignment_type<E>(
65    target: &AssignmentTarget<E>,
66    declared: &ColumnType,
67) -> Result<ColumnType, SQLError> {
68    let mut base = declared;
69    while let ColumnType::Domain { base: inner, .. } = base {
70        base = inner;
71    }
72    if let Some(AssignmentStep::Field(field)) = target.indirection.first() {
73        return Err(error("42804", format!(
74            "cannot assign to field \"{field}\" of column \"{}\" because its type {} is not a composite type",
75            target.column, declared.sql_name()
76        )));
77    }
78    let array = match base {
79        ColumnType::Array(_) => base.clone(),
80        ColumnType::Int2Vector => ColumnType::Array(Box::new(ColumnType::SmallInteger)),
81        ColumnType::OidVector => ColumnType::Array(Box::new(ColumnType::Oid)),
82        _ => {
83            return Err(error(
84                "42804",
85                format!(
86                    "cannot subscript type {} because it does not support subscripting",
87                    declared.sql_name()
88                ),
89            ))
90        }
91    };
92    if let Some(AssignmentStep::Field(field)) = target
93        .indirection
94        .iter()
95        .find(|step| matches!(step, AssignmentStep::Field(_)))
96    {
97        let mut element = &array;
98        while let ColumnType::Array(inner) = element {
99            element = inner;
100        }
101        return Err(error("42804", format!(
102            "cannot assign to field \"{field}\" of column \"{}\" because its type {} is not a composite type",
103            target.column, element.sql_name()
104        )));
105    }
106    if target.indirection.len() > 6 {
107        return Err(error(
108            "54000",
109            format!(
110                "number of array dimensions ({}) exceeds the maximum allowed (6)",
111                target.indirection.len()
112            ),
113        ));
114    }
115    Ok(array)
116}
117
118/// One level of an assignment target: a composite field, or the consecutive subscripts that `transformAssignmentSubscripts` applies together.
119pub enum AssignmentLevel<'a, E> {
120    Field(&'a str),
121    Subscripts(&'a [AssignmentStep<E>]),
122}
123
124impl<E> AssignmentLevel<'_, E> {
125    /// Whether a subscript group writes a slice.
126    pub fn is_slice(&self) -> bool {
127        match self {
128            Self::Field(_) => false,
129            Self::Subscripts(steps) => steps
130                .iter()
131                .any(|step| matches!(step, AssignmentStep::Slice { .. })),
132        }
133    }
134}
135
136/// Group an assignment's indirection into fields and subscript groups, in order.
137pub fn assignment_levels<E>(steps: &[AssignmentStep<E>]) -> Vec<AssignmentLevel<'_, E>> {
138    let mut levels = Vec::new();
139    let mut position = 0;
140    while position < steps.len() {
141        if let AssignmentStep::Field(field) = &steps[position] {
142            levels.push(AssignmentLevel::Field(field));
143            position += 1;
144            continue;
145        }
146        let start = position;
147        while position < steps.len() && !matches!(steps[position], AssignmentStep::Field(_)) {
148            position += 1;
149        }
150        levels.push(AssignmentLevel::Subscripts(&steps[start..position]));
151    }
152    levels
153}
154
155/// Whether an assignment writes a composite field somewhere along its indirection.
156pub fn has_field_step<E>(target: &AssignmentTarget<E>) -> bool {
157    target
158        .indirection
159        .iter()
160        .any(|step| matches!(step, AssignmentStep::Field(_)))
161}
162
163/// The type each level of a field assignment applies to, followed by the type the assigned value must have, as `transformAssignmentIndirection` resolves them: a field names an attribute of a composite type or of a domain over one, and a subscript group selects an array element, or keeps the array for a slice.
164pub fn field_assignment_types<E>(
165    target: &AssignmentTarget<E>,
166    declared: &ColumnType,
167    composites: Option<&dyn crate::expr::composites::CompositeTypeCatalog>,
168) -> Result<Vec<ColumnType>, SQLError> {
169    let mut types = vec![declared.clone()];
170    for level in assignment_levels(&target.indirection) {
171        let current = types.last().expect("the declared type starts the levels");
172        let mut base = current;
173        while let ColumnType::Domain { base: inner, .. } = base {
174            base = inner;
175        }
176        let next = match level {
177            AssignmentLevel::Field(field) => {
178                let ColumnType::Composite(reference) = base else {
179                    return Err(error("42804", format!(
180                        "cannot assign to field \"{field}\" of column \"{}\" because its type {} is not a composite type",
181                        target.column, current.display_name()
182                    )));
183                };
184                let descriptor = crate::expr::composites::descriptor(composites, reference.oid)?;
185                let Some((_, attribute)) = descriptor.attribute(field) else {
186                    return Err(error("42703", format!(
187                        "cannot assign to field \"{field}\" of column \"{}\" because there is no such column in data type {}",
188                        target.column, current.display_name()
189                    )));
190                };
191                attribute.ty.clone()
192            }
193            AssignmentLevel::Subscripts(steps) => {
194                let array = match base {
195                    ColumnType::Array(_) => base.clone(),
196                    ColumnType::Int2Vector => ColumnType::Array(Box::new(ColumnType::SmallInteger)),
197                    ColumnType::OidVector => ColumnType::Array(Box::new(ColumnType::Oid)),
198                    _ => {
199                        return Err(error(
200                            "42804",
201                            format!(
202                                "cannot subscript type {} because it does not support subscripting",
203                                current.display_name()
204                            ),
205                        ))
206                    }
207                };
208                if steps.len() > 6 {
209                    return Err(error(
210                        "54000",
211                        format!(
212                            "number of array dimensions ({}) exceeds the maximum allowed (6)",
213                            steps.len()
214                        ),
215                    ));
216                }
217                if steps
218                    .iter()
219                    .any(|step| matches!(step, AssignmentStep::Slice { .. }))
220                {
221                    array
222                } else {
223                    let mut element = &array;
224                    while let ColumnType::Array(inner) = element {
225                        element = inner;
226                    }
227                    element.clone()
228                }
229            }
230        };
231        types.push(next);
232    }
233    Ok(types)
234}
235
236/// Slices consume an array; any number of element subscripts consumes one scalar element.
237pub fn assignment_value_type<E>(
238    target: &AssignmentTarget<E>,
239    declared: &ColumnType,
240) -> Result<ColumnType, SQLError> {
241    if target.is_whole_column() {
242        return Ok(declared.clone());
243    }
244    let array = array_assignment_type(target, declared)?;
245    if target
246        .indirection
247        .iter()
248        .any(|step| matches!(step, AssignmentStep::Slice { .. }))
249    {
250        Ok(array)
251    } else {
252        let mut element = &array;
253        while let ColumnType::Array(inner) = element {
254            element = inner;
255        }
256        Ok(element.clone())
257    }
258}
259
260/// Type errors retain `PostgreSQL`'s separate primary message and rewrite hint.
261pub fn validate_assignment_source<E>(
262    target: &AssignmentTarget<E>,
263    required: &ColumnType,
264    source: Option<&ColumnType>,
265) -> Result<(), SQLError> {
266    if let Some(source) = source {
267        if !crate::assignment_type_compatible(source, required) {
268            // `transformAssignmentIndirection` names the innermost field it assigns through.
269            let levels = assignment_levels(&target.indirection);
270            let name = levels
271                .iter()
272                .rev()
273                .find_map(|level| match level {
274                    AssignmentLevel::Field(field) => Some(*field),
275                    AssignmentLevel::Subscripts(_) => None,
276                })
277                .unwrap_or(&target.column);
278            let message = match levels.last() {
279                None => format!(
280                    "column \"{}\" is of type {} but expression is of type {}",
281                    target.column,
282                    required.display_name(),
283                    source.display_name()
284                ),
285                Some(AssignmentLevel::Field(field)) => format!(
286                    "subfield \"{field}\" is of type {} but expression is of type {}",
287                    required.display_name(),
288                    source.display_name()
289                ),
290                Some(AssignmentLevel::Subscripts(_)) => format!(
291                    "subscripted assignment to \"{name}\" requires type {} but expression is of type {}",
292                    required.display_name(),
293                    source.display_name()
294                ),
295            };
296            return Err(SQLError::Diagnostic {
297                sqlstate: "42804".into(),
298                message,
299                detail: None,
300                hint: Some("You will need to rewrite or cast the expression.".into()),
301            });
302        }
303    }
304    Ok(())
305}
306
307/// Subscripting yields an ordinary array; the declared destination must accept that container.
308pub fn validate_assignment_result<E>(
309    target: &AssignmentTarget<E>,
310    declared: &ColumnType,
311) -> Result<(), SQLError> {
312    // A field write rebuilds the column's own composite value.
313    if target.is_whole_column() || has_field_step(target) {
314        return Ok(());
315    }
316    let container = array_assignment_type(target, declared)?;
317    if !crate::type_resolution::explicit_type_compatible(&container, declared) {
318        return Err(error(
319            "42846",
320            format!(
321                "cannot cast type {} to {}",
322                container.sql_name(),
323                declared.sql_name()
324            ),
325        ));
326    }
327    Ok(())
328}
329
330pub fn validate_assignment_default<E>(target: &AssignmentTarget<E>) -> Result<(), SQLError> {
331    match target.indirection.first() {
332        None => Ok(()),
333        Some(AssignmentStep::Field(_)) => {
334            Err(error("0A000", "cannot set a subfield to DEFAULT".into()))
335        }
336        Some(_) => Err(error(
337            "0A000",
338            "cannot set an array element to DEFAULT".into(),
339        )),
340    }
341}