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 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 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}