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            name: name.into(),
190            ty,
191            number,
192        };
193        Catalog(BTreeMap::from([
194            (
195                20_002,
196                Arc::new(CompositeTypeDescriptor {
197                    dropped: Vec::new(),
198                    type_oid: 20_002,
199                    relation_oid: 20_001,
200                    attributes: vec![
201                        attribute("x", ColumnType::Integer, 1),
202                        attribute("y", ColumnType::Text, 2),
203                    ],
204                }),
205            ),
206            (
207                20_012,
208                Arc::new(CompositeTypeDescriptor {
209                    dropped: Vec::new(),
210                    type_oid: 20_012,
211                    relation_oid: 20_011,
212                    attributes: vec![
213                        attribute("p", reference(20_002, "pair"), 1),
214                        attribute(
215                            "ps",
216                            ColumnType::Array(Box::new(reference(20_002, "pair"))),
217                            2,
218                        ),
219                    ],
220                }),
221            ),
222        ]))
223    }
224
225    fn pair(x: i64, y: &str) -> Value {
226        Value::Record(
227            vec![
228                ("x".into(), Value::Int(x)),
229                ("y".into(), Value::Str(y.into())),
230            ]
231            .into(),
232        )
233    }
234
235    fn nested_value() -> Value {
236        Value::Record(
237            vec![
238                ("p".into(), pair(1, "a")),
239                (
240                    "ps".into(),
241                    Value::Array(ArrayValue::try_new(vec![pair(2, "b"), Value::Null]).unwrap()),
242                ),
243            ]
244            .into(),
245        )
246    }
247
248    #[test]
249    fn renames_follow_the_type_through_nested_composites_arrays_and_domains() {
250        let catalog = catalog();
251        let outer = reference(20_012, "outer_t");
252        let column = ColumnType::Domain {
253            schema: "public".into(),
254            name: "outer_domain".into(),
255            oid: 20_020,
256            array_oid: None,
257            base: Box::new(outer.clone()),
258        };
259        assert!(type_contains_composite(&column, 20_002, &catalog).unwrap());
260        assert!(!type_contains_composite(&ColumnType::Integer, 20_002, &catalog).unwrap());
261        let renamed = apply_attribute_change(
262            nested_value(),
263            &column,
264            20_002,
265            &AttributeChange::Rename {
266                from: "y".into(),
267                to: "label".into(),
268            },
269            &catalog,
270        )
271        .unwrap();
272        let expected_pair = |x: i64, label: &str| {
273            Value::Record(
274                vec![
275                    ("x".into(), Value::Int(x)),
276                    ("label".into(), Value::Str(label.into())),
277                ]
278                .into(),
279            )
280        };
281        assert_eq!(
282            renamed,
283            Value::Record(
284                vec![
285                    ("p".into(), expected_pair(1, "a")),
286                    (
287                        "ps".into(),
288                        Value::Array(
289                            ArrayValue::try_new(vec![expected_pair(2, "b"), Value::Null]).unwrap()
290                        ),
291                    ),
292                ]
293                .into()
294            )
295        );
296    }
297
298    #[test]
299    fn drops_and_additions_change_only_values_of_the_changed_type() {
300        let catalog = catalog();
301        let outer = reference(20_012, "outer_t");
302        let dropped = apply_attribute_change(
303            nested_value(),
304            &outer,
305            20_002,
306            &AttributeChange::Drop("x".into()),
307            &catalog,
308        )
309        .unwrap();
310        let Value::Record(fields) = &dropped else {
311            panic!("composite value");
312        };
313        assert_eq!(
314            fields[0].1,
315            Value::Record(vec![("y".into(), Value::Str("a".into()))].into())
316        );
317        let added = apply_attribute_change(
318            pair(3, "c"),
319            &reference(20_002, "pair"),
320            20_002,
321            &AttributeChange::Add("z".into()),
322            &catalog,
323        )
324        .unwrap();
325        assert_eq!(
326            added,
327            Value::Record(
328                vec![
329                    ("x".into(), Value::Int(3)),
330                    ("y".into(), Value::Str("c".into())),
331                    ("z".into(), Value::Null),
332                ]
333                .into()
334            )
335        );
336        // The outer type's own change leaves nested values of other types alone.
337        let outer_change = apply_attribute_change(
338            nested_value(),
339            &outer,
340            20_012,
341            &AttributeChange::Drop("ps".into()),
342            &catalog,
343        )
344        .unwrap();
345        assert_eq!(
346            outer_change,
347            Value::Record(vec![("p".into(), pair(1, "a"))].into())
348        );
349        assert_eq!(
350            apply_attribute_change(
351                Value::Null,
352                &outer,
353                20_002,
354                &AttributeChange::Add("z".into()),
355                &catalog
356            )
357            .unwrap(),
358            Value::Null
359        );
360    }
361}