1use uqa_core::{ArrayValue, Value};
10
11use super::{descriptor, CompositeTypeCatalog, Result, SQLError};
12use crate::ast::ColumnType;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum AttributeChange {
17 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
42pub 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
68pub 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
118fn 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 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}