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 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
54pub 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
80pub 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
135fn 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 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}