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/// Slices consume an array; any number of element subscripts consumes one scalar element.
119pub fn assignment_value_type<E>(
120    target: &AssignmentTarget<E>,
121    declared: &ColumnType,
122) -> Result<ColumnType, SQLError> {
123    if target.is_whole_column() {
124        return Ok(declared.clone());
125    }
126    let array = array_assignment_type(target, declared)?;
127    if target
128        .indirection
129        .iter()
130        .any(|step| matches!(step, AssignmentStep::Slice { .. }))
131    {
132        Ok(array)
133    } else {
134        let mut element = &array;
135        while let ColumnType::Array(inner) = element {
136            element = inner;
137        }
138        Ok(element.clone())
139    }
140}
141
142/// Type errors retain `PostgreSQL`'s separate primary message and rewrite hint.
143pub fn validate_assignment_source<E>(
144    target: &AssignmentTarget<E>,
145    required: &ColumnType,
146    source: Option<&ColumnType>,
147) -> Result<(), SQLError> {
148    if let Some(source) = source {
149        if !crate::assignment_type_compatible(source, required) {
150            let message = if target.is_whole_column() {
151                format!(
152                    "column \"{}\" is of type {} but expression is of type {}",
153                    target.column,
154                    required.sql_name(),
155                    source.sql_name()
156                )
157            } else {
158                format!("subscripted assignment to \"{}\" requires type {} but expression is of type {}", target.column, required.sql_name(), source.sql_name())
159            };
160            return Err(SQLError::Diagnostic {
161                sqlstate: "42804".into(),
162                message,
163                detail: None,
164                hint: Some("You will need to rewrite or cast the expression.".into()),
165            });
166        }
167    }
168    Ok(())
169}
170
171/// Subscripting yields an ordinary array; the declared destination must accept that container.
172pub fn validate_assignment_result<E>(
173    target: &AssignmentTarget<E>,
174    declared: &ColumnType,
175) -> Result<(), SQLError> {
176    if target.is_whole_column() {
177        return Ok(());
178    }
179    let container = array_assignment_type(target, declared)?;
180    if !crate::type_resolution::explicit_type_compatible(&container, declared) {
181        return Err(error(
182            "42846",
183            format!(
184                "cannot cast type {} to {}",
185                container.sql_name(),
186                declared.sql_name()
187            ),
188        ));
189    }
190    Ok(())
191}
192
193pub fn validate_assignment_default<E>(target: &AssignmentTarget<E>) -> Result<(), SQLError> {
194    match target.indirection.first() {
195        None => Ok(()),
196        Some(AssignmentStep::Field(_)) => {
197            Err(error("0A000", "cannot set a subfield to DEFAULT".into()))
198        }
199        Some(_) => Err(error(
200            "0A000",
201            "cannot set an array element to DEFAULT".into(),
202        )),
203    }
204}