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    Type {
25        name: String,
26        from: Box<ColumnType>,
27        to: Box<ColumnType>,
28    },
29}
30
31impl AttributeChange {
32    fn apply(&self, fields: &mut Vec<(String, Value)>) {
33        match self {
34            Self::Add(name) => fields.push((name.clone(), Value::Null)),
35            Self::Drop(name) => fields.retain(|(field, _)| field != name),
36            Self::Rename { from, to } => {
37                for (field, _) in fields.iter_mut() {
38                    if field == from {
39                        field.clone_from(to);
40                    }
41                }
42            }
43            Self::Type { name, from, to } => {
44                if let Some((_, value)) = fields.iter_mut().find(|(field, _)| field == name) {
45                    if let Some(projected) = super::datum::reinterpret(value, from, to) {
46                        *value = projected;
47                    }
48                }
49            }
50        }
51    }
52}
53
54/// Whether values of `ty` can hold values of the composite type `target`.
55pub fn type_contains_composite(
56    ty: &ColumnType,
57    target: u32,
58    catalog: &dyn CompositeTypeCatalog,
59) -> Result<bool> {
60    Ok(match ty {
61        ColumnType::Domain { base, .. } | ColumnType::Array(base) => {
62            type_contains_composite(base, target, catalog)?
63        }
64        ColumnType::Composite(reference) => {
65            if reference.oid == target {
66                return Ok(true);
67            }
68            let descriptor = descriptor(Some(catalog), reference.oid)?;
69            for attribute in &descriptor.attributes {
70                if type_contains_composite(&attribute.ty, target, catalog)? {
71                    return Ok(true);
72                }
73            }
74            false
75        }
76        _ => false,
77    })
78}
79
80/// 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.
81pub fn apply_attribute_change(
82    value: Value,
83    ty: &ColumnType,
84    target: u32,
85    change: &AttributeChange,
86    catalog: &dyn CompositeTypeCatalog,
87) -> Result<Value> {
88    match (ty, value) {
89        (_, Value::Null) => Ok(Value::Null),
90        (ColumnType::Domain { base, .. }, value) => {
91            apply_attribute_change(value, base, target, change, catalog)
92        }
93        (ColumnType::Array(element), Value::Array(array)) => {
94            let lower_bounds = array.lower_bounds().to_vec();
95            let elements = array
96                .elements()
97                .iter()
98                .cloned()
99                .map(|value| apply_array_element(value, element, target, change, catalog))
100                .collect::<Result<Vec<_>>>()?;
101            ArrayValue::with_lower_bounds(elements, lower_bounds)
102                .map(|converted| converted.with_element_type_oid(array.element_type_oid()))
103                .map(Value::Array)
104                .ok_or_else(|| {
105                    SQLError::Internal("composite attribute change reshaped an array".into())
106                })
107        }
108        (ColumnType::Composite(reference), Value::Record(fields)) => {
109            let type_oid = fields.type_oid().unwrap_or(reference.oid);
110            let descriptor = descriptor(Some(catalog), type_oid)?;
111            let mut fields = fields
112                .into_iter()
113                .map(|(name, value)| {
114                    let value = match descriptor.attribute(&name) {
115                        Some((_, attribute)) => {
116                            apply_attribute_change(value, &attribute.ty, target, change, catalog)?
117                        }
118                        None => value,
119                    };
120                    Ok((name, value))
121                })
122                .collect::<Result<Vec<_>>>()?;
123            if type_oid == target {
124                change.apply(&mut fields);
125            }
126            Ok(Value::Record(uqa_core::RecordValue::from_parts(
127                fields,
128                Some(type_oid),
129            )))
130        }
131        (_, value) => Ok(value),
132    }
133}
134
135/// Multidimensional arrays nest their inner dimensions as lists of elements.
136fn apply_array_element(
137    value: Value,
138    element: &ColumnType,
139    target: u32,
140    change: &AttributeChange,
141    catalog: &dyn CompositeTypeCatalog,
142) -> Result<Value> {
143    match value {
144        Value::List(values) => values
145            .into_iter()
146            .map(|value| apply_array_element(value, element, target, change, catalog))
147            .collect::<Result<Vec<_>>>()
148            .map(Value::List),
149        value => apply_attribute_change(value, element, target, change, catalog),
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use std::collections::BTreeMap;
156    use std::sync::Arc;
157
158    use uqa_core::{ArrayValue, Value};
159
160    use super::{apply_attribute_change, type_contains_composite, AttributeChange};
161    use crate::ast::{ColumnType, CompositeTypeReference};
162    use crate::expr::composites::{
163        CompositeAttribute, CompositeTypeCatalog, CompositeTypeDescriptor,
164    };
165
166    struct Catalog(BTreeMap<u32, Arc<CompositeTypeDescriptor>>);
167
168    impl CompositeTypeCatalog for Catalog {
169        fn composite_type(
170            &self,
171            type_oid: u32,
172        ) -> crate::error::Result<Option<Arc<CompositeTypeDescriptor>>> {
173            Ok(self.0.get(&type_oid).cloned())
174        }
175    }
176
177    fn reference(oid: u32, name: &str) -> ColumnType {
178        ColumnType::Composite(CompositeTypeReference {
179            schema: "public".into(),
180            name: name.into(),
181            oid,
182            array_oid: oid + 1,
183            relation_oid: oid - 1,
184        })
185    }
186
187    fn catalog() -> Catalog {
188        let attribute = |name: &str, ty: ColumnType, number: i16| CompositeAttribute {
189            collation_oid: None,
190            name: name.into(),
191            ty,
192            number,
193        };
194        Catalog(BTreeMap::from([
195            (
196                20_002,
197                Arc::new(CompositeTypeDescriptor {
198                    dropped: Vec::new(),
199                    type_oid: 20_002,
200                    relation_oid: 20_001,
201                    attributes: vec![
202                        attribute("x", ColumnType::Integer, 1),
203                        attribute("y", ColumnType::Text, 2),
204                    ],
205                }),
206            ),
207            (
208                20_012,
209                Arc::new(CompositeTypeDescriptor {
210                    dropped: Vec::new(),
211                    type_oid: 20_012,
212                    relation_oid: 20_011,
213                    attributes: vec![
214                        attribute("p", reference(20_002, "pair"), 1),
215                        attribute(
216                            "ps",
217                            ColumnType::Array(Box::new(reference(20_002, "pair"))),
218                            2,
219                        ),
220                    ],
221                }),
222            ),
223        ]))
224    }
225
226    fn pair(x: i64, y: &str) -> Value {
227        Value::Record(
228            vec![
229                ("x".into(), Value::Int(x)),
230                ("y".into(), Value::Str(y.into())),
231            ]
232            .into(),
233        )
234    }
235
236    fn nested_value() -> Value {
237        Value::Record(
238            vec![
239                ("p".into(), pair(1, "a")),
240                (
241                    "ps".into(),
242                    Value::Array(ArrayValue::try_new(vec![pair(2, "b"), Value::Null]).unwrap()),
243                ),
244            ]
245            .into(),
246        )
247    }
248
249    #[test]
250    fn renames_follow_the_type_through_nested_composites_arrays_and_domains() {
251        let catalog = catalog();
252        let outer = reference(20_012, "outer_t");
253        let column = ColumnType::Domain {
254            collation_oid: None,
255            schema: "public".into(),
256            name: "outer_domain".into(),
257            oid: 20_020,
258            array_oid: None,
259            base: Box::new(outer.clone()),
260        };
261        assert!(type_contains_composite(&column, 20_002, &catalog).unwrap());
262        assert!(!type_contains_composite(&ColumnType::Integer, 20_002, &catalog).unwrap());
263        let renamed = apply_attribute_change(
264            nested_value(),
265            &column,
266            20_002,
267            &AttributeChange::Rename {
268                from: "y".into(),
269                to: "label".into(),
270            },
271            &catalog,
272        )
273        .unwrap();
274        let expected_pair = |x: i64, label: &str| {
275            Value::Record(
276                vec![
277                    ("x".into(), Value::Int(x)),
278                    ("label".into(), Value::Str(label.into())),
279                ]
280                .into(),
281            )
282        };
283        assert_eq!(
284            renamed,
285            Value::Record(
286                vec![
287                    ("p".into(), expected_pair(1, "a")),
288                    (
289                        "ps".into(),
290                        Value::Array(
291                            ArrayValue::try_new(vec![expected_pair(2, "b"), Value::Null]).unwrap()
292                        ),
293                    ),
294                ]
295                .into()
296            )
297        );
298    }
299
300    #[test]
301    fn drops_and_additions_change_only_values_of_the_changed_type() {
302        let catalog = catalog();
303        let outer = reference(20_012, "outer_t");
304        let dropped = apply_attribute_change(
305            nested_value(),
306            &outer,
307            20_002,
308            &AttributeChange::Drop("x".into()),
309            &catalog,
310        )
311        .unwrap();
312        let Value::Record(fields) = &dropped else {
313            panic!("composite value");
314        };
315        assert_eq!(
316            fields[0].1,
317            Value::Record(vec![("y".into(), Value::Str("a".into()))].into())
318        );
319        let added = apply_attribute_change(
320            pair(3, "c"),
321            &reference(20_002, "pair"),
322            20_002,
323            &AttributeChange::Add("z".into()),
324            &catalog,
325        )
326        .unwrap();
327        assert_eq!(
328            added,
329            Value::Record(
330                vec![
331                    ("x".into(), Value::Int(3)),
332                    ("y".into(), Value::Str("c".into())),
333                    ("z".into(), Value::Null),
334                ]
335                .into()
336            )
337        );
338        // The outer type's own change leaves nested values of other types alone.
339        let outer_change = apply_attribute_change(
340            nested_value(),
341            &outer,
342            20_012,
343            &AttributeChange::Drop("ps".into()),
344            &catalog,
345        )
346        .unwrap();
347        assert_eq!(
348            outer_change,
349            Value::Record(vec![("p".into(), pair(1, "a"))].into())
350        );
351        assert_eq!(
352            apply_attribute_change(
353                Value::Null,
354                &outer,
355                20_002,
356                &AttributeChange::Add("z".into()),
357                &catalog
358            )
359            .unwrap(),
360            Value::Null
361        );
362    }
363}