use crate::SQLError;
use uqa_core::{
memory::{Produced, ProductionControl, ProductionVec},
DatumValue, DecimalValue, Value,
};
mod arrays;
mod compression;
mod fixed;
mod malformed;
mod records;
pub fn contains_datum(value: &Value) -> bool {
match value {
Value::Datum(_) => true,
Value::Array(array) => array.elements().iter().any(contains_datum),
Value::List(values) => values.iter().any(contains_datum),
Value::Row(values) => values.iter().any(contains_datum),
Value::Record(fields) => fields.iter().any(|(_, value)| contains_datum(value)),
Value::Map(values) => values.values().any(contains_datum),
_ => false,
}
}
pub(in crate::expr) fn record_type_oid(
datum: &DatumValue,
control: &ProductionControl<'_>,
) -> Result<u32, SQLError> {
let payload = payload(datum, control)?;
word(payload.bytes().get(4..).unwrap_or_default())
.ok_or_else(|| corrupt("invalid record header"))
}
pub(in crate::expr) fn array_shape(
datum: &DatumValue,
control: &ProductionControl<'_>,
) -> Result<arrays::ArrayShape, SQLError> {
control.check()?;
let payload = payload(datum, control)?;
arrays::ArrayShape::read(payload.bytes())
}
#[cfg(test)]
mod tests;
pub fn type_name(datum: &DatumValue) -> &'static str {
match datum.type_oid() {
18 => "\"char\"",
19 => "name",
700 => "real",
1042 => "character",
1043 => "character varying",
oid => crate::catalog::type_metadata::catalog_type_name(i64::from(oid)),
}
}
pub fn read(datum: &DatumValue) -> Result<Value, SQLError> {
read_with_catalog(datum, None)
}
pub fn read_with_catalog(
datum: &DatumValue,
engine: Option<&dyn super::EngineHook>,
) -> Result<Value, SQLError> {
read_with_catalog_and_control(datum, engine, &ProductionControl::uncontrolled())
.map(|value| value.into_uncontrolled().expect("ordinary datum read"))
}
pub fn copy_constant_field(value: &Value, ty: &crate::ColumnType) -> Result<Value, SQLError> {
let Value::Datum(datum) = value else {
return Ok(value.clone());
};
if crate::catalog::type_metadata::pg_type_len(ty) != -1 {
return Ok(value.clone());
}
let control = ProductionControl::uncontrolled();
let payload = payload(datum, &control)?;
let size = payload
.bytes()
.len()
.checked_add(4)
.and_then(|size| u32::try_from(size).ok())
.and_then(|size| size.checked_mul(4))
.ok_or_else(|| corrupt("invalid datum length"))?;
let mut bytes = size.to_le_bytes().to_vec();
bytes.extend_from_slice(payload.bytes());
Ok(Value::Datum(DatumValue::new(datum.type_oid(), 0, bytes)))
}
pub fn read_with_control(
datum: &DatumValue,
control: &ProductionControl<'_>,
) -> Result<Produced<Value>, SQLError> {
read_with_catalog_and_control(datum, None, control)
}
pub fn read_with_catalog_and_control(
datum: &DatumValue,
engine: Option<&dyn super::EngineHook>,
control: &ProductionControl<'_>,
) -> Result<Produced<Value>, SQLError> {
let catalog = engine.map(super::value_catalog::EngineValueCatalog);
read_with_value_catalog_and_control(
datum,
catalog
.as_ref()
.map(|catalog| catalog as &dyn super::SQLValueCatalog),
control,
)
}
pub fn read_with_value_catalog_and_control(
datum: &DatumValue,
catalog: Option<&dyn super::SQLValueCatalog>,
control: &ProductionControl<'_>,
) -> Result<Produced<Value>, SQLError> {
control.check()?;
if let Some(value) = fixed::read(datum, control) {
return value;
}
if crate::catalog::type_metadata::builtin_scalar_type(datum.type_oid()).is_none()
&& crate::catalog::type_metadata::builtin_array_element(datum.type_oid()).is_none()
{
if let Some(enums) = catalog {
if enums.enum_type_labels(datum.type_oid())?.is_some() {
let oid = enum_label_oid(datum)?;
return Ok(
control.retain_external_value(super::enums::enum_value_from_oid(
Some(enums),
oid,
)?)?,
);
}
}
}
let payload = payload(datum, control)?;
read_payload(datum.type_oid(), payload.bytes(), catalog, control)
}
pub(crate) fn enum_label_oid(datum: &DatumValue) -> Result<u32, SQLError> {
word(
datum
.bytes()
.get(datum.offset() as usize..)
.unwrap_or_default(),
)
.ok_or_else(|| corrupt("invalid datum length"))
}
pub(super) fn compare_jsonb_with_control(
left: &Value,
right: &Value,
control: &ProductionControl<'_>,
) -> Result<Option<std::cmp::Ordering>, SQLError> {
let physical = |value: &Value| matches!(value, Value::Datum(datum) if datum.type_oid() == 3802);
let jsonb = |value: &Value| physical(value) || matches!(value, Value::JsonB(_));
if !(jsonb(left) && jsonb(right) && (physical(left) || physical(right))) {
return Ok(None);
}
let left_bytes = match left {
Value::Datum(datum) => Some(payload(datum, control)?),
_ => None,
};
let right_bytes = match right {
Value::Datum(datum) => Some(payload(datum, control)?),
_ => None,
};
let left = match left {
Value::JsonB(text) => super::json::JsonbInput::Text(text),
_ => super::json::JsonbInput::Bytes(left_bytes.as_ref().expect("physical JSONB").bytes()),
};
let right = match right {
Value::JsonB(text) => super::json::JsonbInput::Text(text),
_ => super::json::JsonbInput::Bytes(right_bytes.as_ref().expect("physical JSONB").bytes()),
};
super::json::compare_jsonb_datums_with_control(left, right, control).map(Some)
}
fn read_payload(
oid: u32,
bytes: &[u8],
catalog: Option<&dyn super::SQLValueCatalog>,
control: &ProductionControl<'_>,
) -> Result<Produced<Value>, SQLError> {
if crate::catalog::type_metadata::builtin_array_element(oid).is_some() {
return arrays::read(bytes, catalog, control);
}
if crate::catalog::type_metadata::builtin_scalar_type(oid).is_none() {
if let Some(ty) = catalog
.map(|catalog| catalog.value_type_by_oid(oid))
.transpose()?
.flatten()
{
match ty {
crate::ColumnType::Array(_) => return arrays::read(bytes, catalog, control),
crate::ColumnType::Composite(_) => return records::read(bytes, catalog, control),
crate::ColumnType::Domain { base, .. } => {
return read_payload(
crate::catalog::type_metadata::pg_type_oid(&base) as u32,
bytes,
catalog,
control,
)
}
_ => {}
}
}
}
match oid {
17 => {
let mut output = ProductionVec::new(*control);
output.reserve(bytes.len())?;
for byte in bytes {
output.push_copy(*byte)?;
}
let (bytes, memory) = output.finish()?.into_parts();
Ok(control.finish(Value::Bytes(bytes), memory)?)
}
25 | 114 | 194 | 1042 | 1043 | 1790 => {
let text = std::str::from_utf8(bytes).map_err(|error| SQLError::Routine {
sqlstate: "22021".into(),
message: format!(
"invalid byte sequence for encoding \"UTF8\": 0x{:02x}",
bytes[error.valid_up_to()]
),
})?;
let (text, memory) = control.copy_text(text)?.into_parts();
let value = match oid {
114 => Value::Json(text),
1042 => Value::FixedChar(text),
_ => Value::Str(text),
};
Ok(control.finish(value, memory)?)
}
3802 => {
let (text, memory) =
super::json::decode_jsonb_datum_with_control(bytes, control)?.into_parts();
Ok(control.finish(Value::JsonB(text), memory)?)
}
1700 => {
let text =
crate::catalog::node_tree::decode_numeric_datum_with_control(bytes, control)?;
let (number, memory) = DecimalValue::parse_with_control(&text, control)?
.ok_or_else(|| corrupt("invalid numeric datum"))?
.into_parts();
Ok(control.finish(Value::Decimal(number), memory)?)
}
oid => Err(SQLError::Unsupported(format!(
"physical datum output for type OID {oid}"
))),
}
}
pub(super) fn binary_payload<'a>(
value: &'a Value,
control: &ProductionControl<'_>,
) -> Result<Option<Payload<'a>>, SQLError> {
control.check()?;
match value {
Value::Bytes(bytes) => Ok(Some(Payload::Borrowed(bytes))),
Value::Datum(datum) if datum.type_oid() == 17 => payload(datum, control).map(Some),
_ => Ok(None),
}
}
pub(super) fn binary_length(
value: &Value,
control: &ProductionControl<'_>,
) -> Result<Option<i64>, SQLError> {
control.check()?;
let datum = match value {
Value::Bytes(bytes) => return Ok(Some(bytes.len() as i64)),
Value::Datum(datum) if datum.type_oid() == 17 => datum,
_ => return Ok(None),
};
let bytes = datum
.bytes()
.get(datum.offset() as usize..)
.ok_or_else(|| corrupt("invalid datum length"))?;
let first = *bytes
.first()
.ok_or_else(|| corrupt("invalid datum length"))?;
let size = if first == 1 && bytes.get(1) == Some(&18) {
i64::from(
word(bytes.get(2..).unwrap_or_default())
.ok_or_else(|| corrupt("invalid datum length"))?,
) - 4
} else if first & 1 != 0 {
i64::from(first >> 1) - 1
} else if first & 3 == 2 {
i64::from(
word(bytes.get(4..).unwrap_or_default())
.ok_or_else(|| corrupt("invalid datum length"))?
& 0x3fff_ffff,
)
} else {
i64::from(word(bytes).ok_or_else(|| corrupt("invalid datum length"))? >> 2) - 4
};
Ok(Some(size))
}
pub(super) enum Payload<'a> {
Borrowed(&'a [u8]),
Owned(Produced<Vec<u8>>),
}
impl Payload<'_> {
pub(super) fn bytes(&self) -> &[u8] {
match self {
Self::Borrowed(bytes) => bytes,
Self::Owned(bytes) => bytes,
}
}
}
fn payload<'a>(
datum: &'a DatumValue,
control: &ProductionControl<'_>,
) -> Result<Payload<'a>, SQLError> {
let offset = datum.offset() as usize;
let bytes = datum
.bytes()
.get(offset..)
.ok_or_else(|| corrupt("invalid datum length"))?;
let first = *bytes
.first()
.ok_or_else(|| corrupt("invalid datum length"))?;
if first == 1 {
return Err(malformed::external(datum));
}
if first & 1 != 0 {
let length = usize::from(first >> 1);
return bytes
.get(1..length)
.map(Payload::Borrowed)
.ok_or_else(|| corrupt("invalid datum length"));
}
let header = word(bytes).ok_or_else(|| corrupt("invalid datum length"))?;
let length = (header >> 2) as usize;
if header & 3 == 2 {
let information = word(bytes.get(4..).unwrap_or_default()).unwrap_or(0);
let method = information >> 30;
let compressed = bytes
.get(8..length)
.ok_or_else(|| compression::corrupt(method))?;
return compression::decompress(
compressed,
(information & 0x3fff_ffff) as usize,
method,
control,
)
.map(Payload::Owned);
}
bytes
.get(4..length)
.map(Payload::Borrowed)
.ok_or_else(|| corrupt("invalid datum length"))
}
fn word(bytes: &[u8]) -> Option<u32> {
Some(u32::from_le_bytes(bytes.get(..4)?.try_into().ok()?))
}
fn corrupt(message: &str) -> SQLError {
SQLError::Routine {
sqlstate: "XX001".into(),
message: message.into(),
}
}