use super::super::{datum, CompositeTypeDescriptor};
use crate::catalog::type_metadata::{pg_type_align, pg_type_len, pg_type_oid, pg_type_storage};
use crate::ColumnType;
use uqa_core::{DatumValue, Value};
mod encoding;
pub(super) fn project(
fields: &[(String, Value)],
before: &CompositeTypeDescriptor,
after: &CompositeTypeDescriptor,
descriptors: &super::Descriptors,
) -> Option<Value> {
let (bytes, positions, start) = encoding::encode(fields, before, descriptors)?;
let backing = DatumValue::new(0, 0, bytes);
let bytes = backing.bytes();
let mut output = after
.attributes
.iter()
.map(|attribute| (attribute.name.clone(), Value::Null))
.collect::<Vec<_>>();
let mut offset = start;
for (original, position) in before.attributes.iter().zip(positions) {
let Some(position) = position else {
continue;
};
let (_, value) = fields.iter().find(|(name, _)| *name == original.name)?;
let current = after
.attributes
.iter()
.enumerate()
.find(|(_, attribute)| attribute.number == original.number);
let (length, alignment) = if let Some((_, attribute)) = current {
(
pg_type_len(&attribute.ty),
pg_type_align(&attribute.ty).as_bytes()[0],
)
} else {
let dropped = after
.dropped
.iter()
.find(|attribute| attribute.number == original.number)?;
(dropped.length, dropped.alignment)
};
if length != -1 || bytes.get(offset) == Some(&0) {
offset = align(offset, alignment)?;
}
if let Some((index, attribute)) = current {
let retained_array = (position == offset && original.ty != attribute.ty)
.then(|| retain_array_identity(value, &original.ty, &attribute.ty))
.flatten();
output[index].1 = if position == offset && original.ty == attribute.ty {
value.clone()
} else if let Some(array) = retained_array {
array
} else if length == -1 || !matches!(length, 1 | 2 | 4 | 8) {
Value::Datum(backing.field(base_oid(&attribute.ty)?, u32::try_from(offset).ok()?))
} else {
let length = width(length)?;
let end = offset.checked_add(length)?;
if let Some(bytes) = bytes.get(offset..end) {
let mut bits = [0; 8];
bits[..length].copy_from_slice(bytes);
datum::decode_bits(u64::from_le_bytes(bits), &attribute.ty)?
} else {
Value::Datum(
backing.field(base_oid(&attribute.ty)?, u32::try_from(offset).ok()?),
)
}
};
}
offset = offset.checked_add(if length == -1 {
bytes.get(offset..).and_then(variable_length).unwrap_or(0)
} else {
usize::try_from(length).ok()?
})?;
}
Some(Value::Record(uqa_core::RecordValue::from_parts(
output,
Some(before.type_oid),
)))
}
fn retain_array_identity(value: &Value, before: &ColumnType, after: &ColumnType) -> Option<Value> {
fn element_oid(ty: &ColumnType) -> Option<u32> {
match ty {
ColumnType::Domain { base, .. } => element_oid(base),
ColumnType::Array(element) => {
let mut leaf = element.as_ref();
while let ColumnType::Array(element) = leaf {
leaf = element;
}
Some(pg_type_oid(leaf) as u32)
}
_ => None,
}
}
element_oid(after)?;
let Value::Array(array) = value else {
return None;
};
let oid = array.element_type_oid().or(element_oid(before));
Some(Value::Array(array.clone().with_element_type_oid(oid)))
}
fn base_oid(ty: &ColumnType) -> Option<u32> {
match ty {
ColumnType::Domain { base, .. } => base_oid(base),
_ => u32::try_from(pg_type_oid(ty)).ok(),
}
}
fn variable_length(bytes: &[u8]) -> Option<usize> {
let first = *bytes.first()?;
if first == 1 {
return Some(match bytes.get(1) {
Some(1..=3) => 10,
Some(18) => 18,
_ => 2,
});
}
if first & 1 != 0 {
Some(usize::from(first >> 1))
} else {
Some((u32::from_le_bytes(bytes.get(..4)?.try_into().ok()?) >> 2) as usize)
}
}
fn width(length: i64) -> Option<usize> {
matches!(length, 1 | 2 | 4 | 8).then_some(length as usize)
}
fn align(offset: usize, alignment: u8) -> Option<usize> {
let mask = match alignment {
b'c' => 0,
b's' => 1,
b'i' => 3,
b'd' => 7,
_ => return None,
};
Some(offset.checked_add(mask)? & !mask)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{expr::composites::CompositeAttribute, ColumnType};
#[test]
fn width_changes_reposition_following_values_without_rewriting_the_original_tuple() {
let types = [
ColumnType::BigInteger,
ColumnType::Integer,
ColumnType::SmallInteger,
ColumnType::Boolean,
ColumnType::Integer,
];
let before = CompositeTypeDescriptor {
type_oid: 20_000,
relation_oid: 20_001,
dropped: Vec::new(),
attributes: types
.into_iter()
.enumerate()
.map(|(index, ty)| CompositeAttribute {
name: char::from(b'a' + index as u8).to_string(),
number: index as i16 + 1,
ty,
})
.collect(),
};
let values = [
Value::Int(72_623_859_790_382_856),
Value::Int(287_454_020),
Value::Int(21_862),
Value::Bool(true),
Value::Int(2_005_440_938),
];
let fields = before
.attributes
.iter()
.zip(values)
.map(|(attribute, value)| (attribute.name.clone(), value))
.collect::<Vec<_>>();
let mut after = before.clone();
after.attributes[0].ty = ColumnType::Integer;
let expected = [
Value::Int(84_281_096),
Value::Int(16_909_060),
Value::Int(13_124),
Value::Bool(true),
Value::Int(87_398),
];
assert_eq!(
project(&fields, &before, &after, &super::super::Descriptors::new()),
Some(Value::Record(
before
.attributes
.iter()
.zip(expected)
.map(|(attribute, value)| (attribute.name.clone(), value))
.collect()
))
);
assert_eq!(
project(&fields, &before, &before, &super::super::Descriptors::new()),
Some(Value::Record(fields.into()))
);
}
#[test]
fn tuple_reads_never_extend_past_the_admitted_bytes() {
let before = CompositeTypeDescriptor {
type_oid: 20_000,
relation_oid: 20_001,
dropped: Vec::new(),
attributes: vec![CompositeAttribute {
name: "a".into(),
number: 1,
ty: ColumnType::Integer,
}],
};
let mut after = before.clone();
after.attributes[0].ty = ColumnType::BigInteger;
let Value::Record(fields) = project(
&[("a".into(), Value::Int(4))],
&before,
&after,
&super::super::Descriptors::new(),
)
.unwrap() else {
panic!("retained record")
};
let Value::Datum(field) = &fields[0].1 else {
panic!("unread fixed field")
};
assert_eq!(field.bytes().len(), 28);
assert_eq!(field.offset(), 24);
assert_eq!(field.type_oid(), 20);
let error = crate::expr::datums::read(field).unwrap_err();
assert_eq!(error.sqlstate(), Some("XX001"));
assert_eq!(error.to_string(), "invalid datum length");
}
}