Skip to main content

uqa_sql/expr/composites/
changes.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Stored composite values follow their type's attributes. `PostgreSQL` stores fields by attribute number, so adding, dropping and renaming an attribute changes no stored tuple; a value here carries its type's current attribute names, so each change rewrites the values of the type wherever a declared type nests it: directly, in arrays, under domains and inside other composite types.
8
9use uqa_core::{ArrayValue, Value};
10
11use super::{descriptor, CompositeTypeCatalog, Result, SQLError};
12use crate::ast::ColumnType;
13
14/// A change to one composite type's attributes, applied to the type's values.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum AttributeChange {
17    /// A new last attribute, which existing values hold as NULL.
18    Add(String),
19    Drop(String),
20    Rename {
21        from: String,
22        to: String,
23    },
24}
25
26impl AttributeChange {
27    fn apply(&self, fields: &mut Vec<(String, Value)>) {
28        match self {
29            Self::Add(name) => fields.push((name.clone(), Value::Null)),
30            Self::Drop(name) => fields.retain(|(field, _)| field != name),
31            Self::Rename { from, to } => {
32                for (field, _) in fields.iter_mut() {
33                    if field == from {
34                        field.clone_from(to);
35                    }
36                }
37            }
38        }
39    }
40}
41
42/// Whether values of `ty` can hold values of the composite type `target`.
43pub fn type_contains_composite(
44    ty: &ColumnType,
45    target: u32,
46    catalog: &dyn CompositeTypeCatalog,
47) -> Result<bool> {
48    Ok(match ty {
49        ColumnType::Domain { base, .. } | ColumnType::Array(base) => {
50            type_contains_composite(base, target, catalog)?
51        }
52        ColumnType::Composite(reference) => {
53            if reference.oid == target {
54                return Ok(true);
55            }
56            let descriptor = descriptor(Some(catalog), reference.oid)?;
57            for attribute in &descriptor.attributes {
58                if type_contains_composite(&attribute.ty, target, catalog)? {
59                    return Ok(true);
60                }
61            }
62            false
63        }
64        _ => false,
65    })
66}
67
68/// Apply `change` to every value of the composite type `target` in `value`, which has the declared type `ty`. Nested composite values are rewritten through their types' attributes before the change.
69pub fn apply_attribute_change(
70    value: Value,
71    ty: &ColumnType,
72    target: u32,
73    change: &AttributeChange,
74    catalog: &dyn CompositeTypeCatalog,
75) -> Result<Value> {
76    match (ty, value) {
77        (_, Value::Null) => Ok(Value::Null),
78        (ColumnType::Domain { base, .. }, value) => {
79            apply_attribute_change(value, base, target, change, catalog)
80        }
81        (ColumnType::Array(element), Value::Array(array)) => {
82            let lower_bounds = array.lower_bounds().to_vec();
83            let elements = array
84                .elements()
85                .iter()
86                .cloned()
87                .map(|value| apply_array_element(value, element, target, change, catalog))
88                .collect::<Result<Vec<_>>>()?;
89            ArrayValue::with_lower_bounds(elements, lower_bounds)
90                .map(Value::Array)
91                .ok_or_else(|| {
92                    SQLError::Internal("composite attribute change reshaped an array".into())
93                })
94        }
95        (ColumnType::Composite(reference), Value::Record(fields)) => {
96            let descriptor = descriptor(Some(catalog), reference.oid)?;
97            let mut fields = fields
98                .into_iter()
99                .map(|(name, value)| {
100                    let value = match descriptor.attribute(&name) {
101                        Some((_, attribute)) => {
102                            apply_attribute_change(value, &attribute.ty, target, change, catalog)?
103                        }
104                        None => value,
105                    };
106                    Ok((name, value))
107                })
108                .collect::<Result<Vec<_>>>()?;
109            if reference.oid == target {
110                change.apply(&mut fields);
111            }
112            Ok(Value::Record(fields))
113        }
114        (_, value) => Ok(value),
115    }
116}
117
118/// Multidimensional arrays nest their inner dimensions as lists of elements.
119fn apply_array_element(
120    value: Value,
121    element: &ColumnType,
122    target: u32,
123    change: &AttributeChange,
124    catalog: &dyn CompositeTypeCatalog,
125) -> Result<Value> {
126    match value {
127        Value::List(values) => values
128            .into_iter()
129            .map(|value| apply_array_element(value, element, target, change, catalog))
130            .collect::<Result<Vec<_>>>()
131            .map(Value::List),
132        value => apply_attribute_change(value, element, target, change, catalog),
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use std::collections::BTreeMap;
139    use std::sync::Arc;
140
141    use uqa_core::{ArrayValue, Value};
142
143    use super::{apply_attribute_change, type_contains_composite, AttributeChange};
144    use crate::ast::{ColumnType, CompositeTypeReference};
145    use crate::expr::composites::{
146        CompositeAttribute, CompositeTypeCatalog, CompositeTypeDescriptor,
147    };
148
149    struct Catalog(BTreeMap<u32, Arc<CompositeTypeDescriptor>>);
150
151    impl CompositeTypeCatalog for Catalog {
152        fn composite_type(
153            &self,
154            type_oid: u32,
155        ) -> crate::error::Result<Option<Arc<CompositeTypeDescriptor>>> {
156            Ok(self.0.get(&type_oid).cloned())
157        }
158    }
159
160    fn reference(oid: u32, name: &str) -> ColumnType {
161        ColumnType::Composite(CompositeTypeReference {
162            schema: "public".into(),
163            name: name.into(),
164            oid,
165            array_oid: oid + 1,
166            relation_oid: oid - 1,
167        })
168    }
169
170    fn catalog() -> Catalog {
171        let attribute = |name: &str, ty: ColumnType, number: i16| CompositeAttribute {
172            name: name.into(),
173            ty,
174            number,
175        };
176        Catalog(BTreeMap::from([
177            (
178                20_002,
179                Arc::new(CompositeTypeDescriptor {
180                    type_oid: 20_002,
181                    relation_oid: 20_001,
182                    attributes: vec![
183                        attribute("x", ColumnType::Integer, 1),
184                        attribute("y", ColumnType::Text, 2),
185                    ],
186                }),
187            ),
188            (
189                20_012,
190                Arc::new(CompositeTypeDescriptor {
191                    type_oid: 20_012,
192                    relation_oid: 20_011,
193                    attributes: vec![
194                        attribute("p", reference(20_002, "pair"), 1),
195                        attribute(
196                            "ps",
197                            ColumnType::Array(Box::new(reference(20_002, "pair"))),
198                            2,
199                        ),
200                    ],
201                }),
202            ),
203        ]))
204    }
205
206    fn pair(x: i64, y: &str) -> Value {
207        Value::Record(vec![
208            ("x".into(), Value::Int(x)),
209            ("y".into(), Value::Str(y.into())),
210        ])
211    }
212
213    fn nested_value() -> Value {
214        Value::Record(vec![
215            ("p".into(), pair(1, "a")),
216            (
217                "ps".into(),
218                Value::Array(ArrayValue::try_new(vec![pair(2, "b"), Value::Null]).unwrap()),
219            ),
220        ])
221    }
222
223    #[test]
224    fn renames_follow_the_type_through_nested_composites_arrays_and_domains() {
225        let catalog = catalog();
226        let outer = reference(20_012, "outer_t");
227        let column = ColumnType::Domain {
228            schema: "public".into(),
229            name: "outer_domain".into(),
230            oid: 20_020,
231            array_oid: None,
232            base: Box::new(outer.clone()),
233        };
234        assert!(type_contains_composite(&column, 20_002, &catalog).unwrap());
235        assert!(!type_contains_composite(&ColumnType::Integer, 20_002, &catalog).unwrap());
236        let renamed = apply_attribute_change(
237            nested_value(),
238            &column,
239            20_002,
240            &AttributeChange::Rename {
241                from: "y".into(),
242                to: "label".into(),
243            },
244            &catalog,
245        )
246        .unwrap();
247        let expected_pair = |x: i64, label: &str| {
248            Value::Record(vec![
249                ("x".into(), Value::Int(x)),
250                ("label".into(), Value::Str(label.into())),
251            ])
252        };
253        assert_eq!(
254            renamed,
255            Value::Record(vec![
256                ("p".into(), expected_pair(1, "a")),
257                (
258                    "ps".into(),
259                    Value::Array(
260                        ArrayValue::try_new(vec![expected_pair(2, "b"), Value::Null]).unwrap()
261                    ),
262                ),
263            ])
264        );
265    }
266
267    #[test]
268    fn drops_and_additions_change_only_values_of_the_changed_type() {
269        let catalog = catalog();
270        let outer = reference(20_012, "outer_t");
271        let dropped = apply_attribute_change(
272            nested_value(),
273            &outer,
274            20_002,
275            &AttributeChange::Drop("x".into()),
276            &catalog,
277        )
278        .unwrap();
279        let Value::Record(fields) = &dropped else {
280            panic!("composite value");
281        };
282        assert_eq!(
283            fields[0].1,
284            Value::Record(vec![("y".into(), Value::Str("a".into()))])
285        );
286        let added = apply_attribute_change(
287            pair(3, "c"),
288            &reference(20_002, "pair"),
289            20_002,
290            &AttributeChange::Add("z".into()),
291            &catalog,
292        )
293        .unwrap();
294        assert_eq!(
295            added,
296            Value::Record(vec![
297                ("x".into(), Value::Int(3)),
298                ("y".into(), Value::Str("c".into())),
299                ("z".into(), Value::Null),
300            ])
301        );
302        // The outer type's own change leaves nested values of other types alone.
303        let outer_change = apply_attribute_change(
304            nested_value(),
305            &outer,
306            20_012,
307            &AttributeChange::Drop("ps".into()),
308            &catalog,
309        )
310        .unwrap();
311        assert_eq!(
312            outer_change,
313            Value::Record(vec![("p".into(), pair(1, "a"))])
314        );
315        assert_eq!(
316            apply_attribute_change(
317                Value::Null,
318                &outer,
319                20_002,
320                &AttributeChange::Add("z".into()),
321                &catalog
322            )
323            .unwrap(),
324            Value::Null
325        );
326    }
327}