use uqa_core::{ArrayValue, Value};
use super::{descriptor, CompositeTypeCatalog, Result, SQLError};
use crate::ast::ColumnType;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttributeChange {
Add(String),
Drop(String),
Rename {
from: String,
to: String,
},
}
impl AttributeChange {
fn apply(&self, fields: &mut Vec<(String, Value)>) {
match self {
Self::Add(name) => fields.push((name.clone(), Value::Null)),
Self::Drop(name) => fields.retain(|(field, _)| field != name),
Self::Rename { from, to } => {
for (field, _) in fields.iter_mut() {
if field == from {
field.clone_from(to);
}
}
}
}
}
}
pub fn type_contains_composite(
ty: &ColumnType,
target: u32,
catalog: &dyn CompositeTypeCatalog,
) -> Result<bool> {
Ok(match ty {
ColumnType::Domain { base, .. } | ColumnType::Array(base) => {
type_contains_composite(base, target, catalog)?
}
ColumnType::Composite(reference) => {
if reference.oid == target {
return Ok(true);
}
let descriptor = descriptor(Some(catalog), reference.oid)?;
for attribute in &descriptor.attributes {
if type_contains_composite(&attribute.ty, target, catalog)? {
return Ok(true);
}
}
false
}
_ => false,
})
}
pub fn apply_attribute_change(
value: Value,
ty: &ColumnType,
target: u32,
change: &AttributeChange,
catalog: &dyn CompositeTypeCatalog,
) -> Result<Value> {
match (ty, value) {
(_, Value::Null) => Ok(Value::Null),
(ColumnType::Domain { base, .. }, value) => {
apply_attribute_change(value, base, target, change, catalog)
}
(ColumnType::Array(element), Value::Array(array)) => {
let lower_bounds = array.lower_bounds().to_vec();
let elements = array
.elements()
.iter()
.cloned()
.map(|value| apply_array_element(value, element, target, change, catalog))
.collect::<Result<Vec<_>>>()?;
ArrayValue::with_lower_bounds(elements, lower_bounds)
.map(Value::Array)
.ok_or_else(|| {
SQLError::Internal("composite attribute change reshaped an array".into())
})
}
(ColumnType::Composite(reference), Value::Record(fields)) => {
let descriptor = descriptor(Some(catalog), reference.oid)?;
let mut fields = fields
.into_iter()
.map(|(name, value)| {
let value = match descriptor.attribute(&name) {
Some((_, attribute)) => {
apply_attribute_change(value, &attribute.ty, target, change, catalog)?
}
None => value,
};
Ok((name, value))
})
.collect::<Result<Vec<_>>>()?;
if reference.oid == target {
change.apply(&mut fields);
}
Ok(Value::Record(fields))
}
(_, value) => Ok(value),
}
}
fn apply_array_element(
value: Value,
element: &ColumnType,
target: u32,
change: &AttributeChange,
catalog: &dyn CompositeTypeCatalog,
) -> Result<Value> {
match value {
Value::List(values) => values
.into_iter()
.map(|value| apply_array_element(value, element, target, change, catalog))
.collect::<Result<Vec<_>>>()
.map(Value::List),
value => apply_attribute_change(value, element, target, change, catalog),
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::sync::Arc;
use uqa_core::{ArrayValue, Value};
use super::{apply_attribute_change, type_contains_composite, AttributeChange};
use crate::ast::{ColumnType, CompositeTypeReference};
use crate::expr::composites::{
CompositeAttribute, CompositeTypeCatalog, CompositeTypeDescriptor,
};
struct Catalog(BTreeMap<u32, Arc<CompositeTypeDescriptor>>);
impl CompositeTypeCatalog for Catalog {
fn composite_type(
&self,
type_oid: u32,
) -> crate::error::Result<Option<Arc<CompositeTypeDescriptor>>> {
Ok(self.0.get(&type_oid).cloned())
}
}
fn reference(oid: u32, name: &str) -> ColumnType {
ColumnType::Composite(CompositeTypeReference {
schema: "public".into(),
name: name.into(),
oid,
array_oid: oid + 1,
relation_oid: oid - 1,
})
}
fn catalog() -> Catalog {
let attribute = |name: &str, ty: ColumnType, number: i16| CompositeAttribute {
name: name.into(),
ty,
number,
};
Catalog(BTreeMap::from([
(
20_002,
Arc::new(CompositeTypeDescriptor {
type_oid: 20_002,
relation_oid: 20_001,
attributes: vec![
attribute("x", ColumnType::Integer, 1),
attribute("y", ColumnType::Text, 2),
],
}),
),
(
20_012,
Arc::new(CompositeTypeDescriptor {
type_oid: 20_012,
relation_oid: 20_011,
attributes: vec![
attribute("p", reference(20_002, "pair"), 1),
attribute(
"ps",
ColumnType::Array(Box::new(reference(20_002, "pair"))),
2,
),
],
}),
),
]))
}
fn pair(x: i64, y: &str) -> Value {
Value::Record(vec![
("x".into(), Value::Int(x)),
("y".into(), Value::Str(y.into())),
])
}
fn nested_value() -> Value {
Value::Record(vec![
("p".into(), pair(1, "a")),
(
"ps".into(),
Value::Array(ArrayValue::try_new(vec![pair(2, "b"), Value::Null]).unwrap()),
),
])
}
#[test]
fn renames_follow_the_type_through_nested_composites_arrays_and_domains() {
let catalog = catalog();
let outer = reference(20_012, "outer_t");
let column = ColumnType::Domain {
schema: "public".into(),
name: "outer_domain".into(),
oid: 20_020,
array_oid: None,
base: Box::new(outer.clone()),
};
assert!(type_contains_composite(&column, 20_002, &catalog).unwrap());
assert!(!type_contains_composite(&ColumnType::Integer, 20_002, &catalog).unwrap());
let renamed = apply_attribute_change(
nested_value(),
&column,
20_002,
&AttributeChange::Rename {
from: "y".into(),
to: "label".into(),
},
&catalog,
)
.unwrap();
let expected_pair = |x: i64, label: &str| {
Value::Record(vec![
("x".into(), Value::Int(x)),
("label".into(), Value::Str(label.into())),
])
};
assert_eq!(
renamed,
Value::Record(vec![
("p".into(), expected_pair(1, "a")),
(
"ps".into(),
Value::Array(
ArrayValue::try_new(vec![expected_pair(2, "b"), Value::Null]).unwrap()
),
),
])
);
}
#[test]
fn drops_and_additions_change_only_values_of_the_changed_type() {
let catalog = catalog();
let outer = reference(20_012, "outer_t");
let dropped = apply_attribute_change(
nested_value(),
&outer,
20_002,
&AttributeChange::Drop("x".into()),
&catalog,
)
.unwrap();
let Value::Record(fields) = &dropped else {
panic!("composite value");
};
assert_eq!(
fields[0].1,
Value::Record(vec![("y".into(), Value::Str("a".into()))])
);
let added = apply_attribute_change(
pair(3, "c"),
&reference(20_002, "pair"),
20_002,
&AttributeChange::Add("z".into()),
&catalog,
)
.unwrap();
assert_eq!(
added,
Value::Record(vec![
("x".into(), Value::Int(3)),
("y".into(), Value::Str("c".into())),
("z".into(), Value::Null),
])
);
let outer_change = apply_attribute_change(
nested_value(),
&outer,
20_012,
&AttributeChange::Drop("ps".into()),
&catalog,
)
.unwrap();
assert_eq!(
outer_change,
Value::Record(vec![("p".into(), pair(1, "a"))])
);
assert_eq!(
apply_attribute_change(
Value::Null,
&outer,
20_002,
&AttributeChange::Add("z".into()),
&catalog
)
.unwrap(),
Value::Null
);
}
}