use oxisql_core::{ArrayElementType, Value};
use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
use tokio_postgres::types::{FromSql, Kind, Type};
use super::pgoutput::{ColumnSpec, RelationBody, TupleColumn, TupleData};
use crate::error::PgError;
const UNIX_EPOCH_JDN: i32 = 2_440_588;
#[derive(Debug, Clone, PartialEq)]
pub enum CellValue {
Value(Value),
UnchangedToast,
}
pub fn tuple_to_values(rel: &RelationBody, tuple: &TupleData) -> Result<Vec<CellValue>, PgError> {
if rel.columns.len() != tuple.columns.len() {
return Err(PgError::Protocol(format!(
"relation '{}.{}' (rel_id {}) has {} column(s) but its tuple has {} column(s)",
rel.namespace,
rel.name,
rel.rel_id,
rel.columns.len(),
tuple.columns.len(),
)));
}
rel.columns
.iter()
.zip(tuple.columns.iter())
.map(|(col, cell)| decode_cell(col, cell))
.collect()
}
fn decode_cell(col: &ColumnSpec, cell: &TupleColumn) -> Result<CellValue, PgError> {
match cell {
TupleColumn::Null => Ok(CellValue::Value(Value::Null)),
TupleColumn::UnchangedToast => Ok(CellValue::UnchangedToast),
TupleColumn::Binary(raw) => binary_to_value(col.type_oid, raw).map(CellValue::Value),
TupleColumn::Text(s) => text_to_value(col.type_oid, s).map(CellValue::Value),
}
}
fn text_to_value(type_oid: u32, s: &str) -> Result<Value, PgError> {
let Some(ty) = Type::from_oid(type_oid) else {
return Ok(Value::Text(s.to_string()));
};
if let Kind::Array(elem_ty) = ty.kind() {
return decode_text_array(elem_ty, s);
}
match ty {
Type::BOOL => parse_bool(s).map(Value::Bool),
Type::INT2 => parse_int::<i16>(s, "int2").map(Value::I64),
Type::INT4 => parse_int::<i32>(s, "int4").map(Value::I64),
Type::INT8 => parse_int::<i64>(s, "int8").map(Value::I64),
Type::FLOAT4 => parse_float4(s).map(Value::F64),
Type::FLOAT8 => parse_float8(s).map(Value::F64),
Type::TEXT | Type::VARCHAR | Type::BPCHAR | Type::NAME => Ok(Value::Text(s.to_string())),
Type::BYTEA => parse_bytea(s).map(Value::Blob),
Type::DATE => parse_date(s).map(Value::Date),
Type::TIMESTAMP => parse_timestamp(s).map(Value::Timestamp),
Type::TIMESTAMPTZ => parse_timestamptz(s).map(Value::Timestamp),
Type::TIME => parse_time(s).map(Value::Time),
Type::UUID => parse_uuid(s).map(Value::Uuid),
Type::JSON | Type::JSONB => Ok(Value::Json(s.to_string())),
Type::NUMERIC => Ok(Value::Decimal(s.to_string())),
Type::INTERVAL => Ok(Value::Text(s.to_string())),
_ => Ok(Value::Text(s.to_string())),
}
}
fn parse_bool(s: &str) -> Result<bool, PgError> {
match s {
"t" => Ok(true),
"f" => Ok(false),
other => Err(PgError::TypeConversion(format!(
"invalid bool text: {other:?} (expected 't' or 'f')"
))),
}
}
fn parse_int<T>(s: &str, pg_type_name: &str) -> Result<i64, PgError>
where
T: std::str::FromStr,
i64: From<T>,
T::Err: std::fmt::Display,
{
s.parse::<T>()
.map(i64::from)
.map_err(|e| PgError::TypeConversion(format!("invalid {pg_type_name} text {s:?}: {e}")))
}
fn parse_float4(s: &str) -> Result<f64, PgError> {
match s.parse::<f32>() {
Ok(f) => Ok(f64::from(f)),
Err(_) => parse_pg_special_float(s, "float4"),
}
}
fn parse_float8(s: &str) -> Result<f64, PgError> {
match s.parse::<f64>() {
Ok(f) => Ok(f),
Err(_) => parse_pg_special_float(s, "float8"),
}
}
fn parse_pg_special_float(s: &str, pg_type_name: &str) -> Result<f64, PgError> {
match s {
"Infinity" => Ok(f64::INFINITY),
"-Infinity" => Ok(f64::NEG_INFINITY),
"NaN" => Ok(f64::NAN),
_ => Err(PgError::TypeConversion(format!(
"invalid {pg_type_name} text: {s:?}"
))),
}
}
fn parse_bytea(s: &str) -> Result<Vec<u8>, PgError> {
let Some(hex) = s.strip_prefix("\\x") else {
return Err(PgError::TypeConversion(format!(
"bytea text {s:?} is missing the '\\x' prefix (requires bytea_output=hex, \
the PostgreSQL default since 9.0)"
)));
};
if hex.len() % 2 != 0 {
return Err(PgError::TypeConversion(format!(
"bytea text {s:?} has an odd-length hex payload ({} hex character(s))",
hex.len()
)));
}
let mut bytes = Vec::with_capacity(hex.len() / 2);
for pair in hex.as_bytes().chunks_exact(2) {
let hi = (pair[0] as char).to_digit(16);
let lo = (pair[1] as char).to_digit(16);
let (Some(hi), Some(lo)) = (hi, lo) else {
return Err(PgError::TypeConversion(format!(
"bytea text {s:?} contains a non-hex-digit character"
)));
};
let byte = u8::try_from((hi << 4) | lo).map_err(|_| {
PgError::TypeConversion(format!(
"bytea text {s:?} contains a non-hex-digit character"
))
})?;
bytes.push(byte);
}
Ok(bytes)
}
fn parse_date(s: &str) -> Result<i32, PgError> {
let date = parse_ymd(s)?;
Ok(date.to_julian_day() - UNIX_EPOCH_JDN)
}
fn parse_ymd(s: &str) -> Result<Date, PgError> {
let parts: Vec<&str> = s.split('-').collect();
let (year_str, month_str, day_str) = match parts.as_slice() {
[y, m, d] => (*y, *m, *d),
_ => {
return Err(PgError::TypeConversion(format!(
"invalid date text {s:?}: expected 'YYYY-MM-DD'"
)));
}
};
let year: i32 = year_str
.parse()
.map_err(|e| PgError::TypeConversion(format!("invalid date year in {s:?}: {e}")))?;
let month_num: u8 = month_str
.parse()
.map_err(|e| PgError::TypeConversion(format!("invalid date month in {s:?}: {e}")))?;
let day: u8 = day_str
.parse()
.map_err(|e| PgError::TypeConversion(format!("invalid date day in {s:?}: {e}")))?;
let month = Month::try_from(month_num)
.map_err(|e| PgError::TypeConversion(format!("invalid date month in {s:?}: {e}")))?;
Date::from_calendar_date(year, month, day)
.map_err(|e| PgError::TypeConversion(format!("invalid date {s:?}: {e}")))
}
fn parse_time(s: &str) -> Result<i64, PgError> {
let t = parse_hms(s)?;
let (h, m, sec, ns) = t.as_hms_nano();
let us = i64::from(h) * 3_600_000_000
+ i64::from(m) * 60_000_000
+ i64::from(sec) * 1_000_000
+ i64::from(ns) / 1_000;
Ok(us)
}
fn parse_hms(s: &str) -> Result<Time, PgError> {
let parts: Vec<&str> = s.split(':').collect();
let (hour_str, minute_str, sec_field) = match parts.as_slice() {
[h, m, sec] => (*h, *m, *sec),
_ => {
return Err(PgError::TypeConversion(format!(
"invalid time text {s:?}: expected 'HH:MM:SS[.ffffff]'"
)));
}
};
let hour: u8 = hour_str
.parse()
.map_err(|e| PgError::TypeConversion(format!("invalid time hour in {s:?}: {e}")))?;
let minute: u8 = minute_str
.parse()
.map_err(|e| PgError::TypeConversion(format!("invalid time minute in {s:?}: {e}")))?;
let (sec_str, micros) = split_fractional_seconds(sec_field)?;
let second: u8 = sec_str
.parse()
.map_err(|e| PgError::TypeConversion(format!("invalid time second in {s:?}: {e}")))?;
Time::from_hms_micro(hour, minute, second, micros)
.map_err(|e| PgError::TypeConversion(format!("invalid time {s:?}: {e}")))
}
fn split_fractional_seconds(s: &str) -> Result<(&str, u32), PgError> {
let Some((sec, frac)) = s.split_once('.') else {
return Ok((s, 0));
};
if frac.is_empty() || frac.len() > 6 || !frac.bytes().all(|b| b.is_ascii_digit()) {
return Err(PgError::TypeConversion(format!(
"invalid fractional seconds {s:?}: expected 1 to 6 digits after '.'"
)));
}
let mut padded = frac.to_string();
padded.push_str(&"0".repeat(6 - frac.len()));
let micros: u32 = padded
.parse()
.map_err(|e| PgError::TypeConversion(format!("invalid fractional seconds {s:?}: {e}")))?;
Ok((sec, micros))
}
fn parse_timestamp(s: &str) -> Result<i64, PgError> {
let (date_str, time_str) = split_date_and_rest(s)?;
let date = parse_ymd(date_str)?;
let time = parse_hms(time_str)?;
let dt = PrimitiveDateTime::new(date, time);
let us = dt.assume_utc().unix_timestamp_nanos() / 1_000;
#[allow(clippy::cast_possible_truncation)]
Ok(us as i64)
}
fn parse_timestamptz(s: &str) -> Result<i64, PgError> {
let (date_str, rest) = split_date_and_rest(s)?;
let date = parse_ymd(date_str)?;
let (time_str, offset) = split_time_and_offset(rest)?;
let time = parse_hms(time_str)?;
let dt = PrimitiveDateTime::new(date, time).assume_offset(offset);
let us = dt.unix_timestamp_nanos() / 1_000;
#[allow(clippy::cast_possible_truncation)]
Ok(us as i64)
}
fn split_date_and_rest(s: &str) -> Result<(&str, &str), PgError> {
s.split_once(' ').ok_or_else(|| {
PgError::TypeConversion(format!(
"invalid timestamp text {s:?}: expected a space between date and time"
))
})
}
fn split_time_and_offset(s: &str) -> Result<(&str, UtcOffset), PgError> {
let sign_pos = s.find(['+', '-']).ok_or_else(|| {
PgError::TypeConversion(format!(
"invalid timestamptz text {s:?}: missing '+'/'-' UTC offset"
))
})?;
let (time_str, signed_offset) = s.split_at(sign_pos);
let negative = signed_offset.starts_with('-');
let offset = parse_offset(&signed_offset[1..], negative, s)?;
Ok((time_str, offset))
}
fn parse_offset(digits: &str, negative: bool, original: &str) -> Result<UtcOffset, PgError> {
let component = |p: &str| -> Result<i8, PgError> {
p.parse::<i8>().map_err(|e| {
PgError::TypeConversion(format!(
"invalid UTC offset in timestamptz text {original:?}: {e}"
))
})
};
let parts: Vec<&str> = digits.split(':').collect();
let (hours, minutes, seconds) = match parts.as_slice() {
[h] => (component(h)?, 0, 0),
[h, m] => (component(h)?, component(m)?, 0),
[h, m, sec] => (component(h)?, component(m)?, component(sec)?),
_ => {
return Err(PgError::TypeConversion(format!(
"invalid UTC offset in timestamptz text {original:?}: expected \
'±HH', '±HH:MM', or '±HH:MM:SS'"
)));
}
};
let sign: i8 = if negative { -1 } else { 1 };
UtcOffset::from_hms(sign * hours, sign * minutes, sign * seconds).map_err(|e| {
PgError::TypeConversion(format!(
"invalid UTC offset in timestamptz text {original:?}: {e}"
))
})
}
fn parse_uuid(s: &str) -> Result<u128, PgError> {
uuid::Uuid::parse_str(s)
.map(|u| u.as_u128())
.map_err(|e| PgError::TypeConversion(format!("invalid uuid text {s:?}: {e}")))
}
fn binary_to_value(type_oid: u32, raw: &[u8]) -> Result<Value, PgError> {
let Some(ty) = Type::from_oid(type_oid) else {
return Ok(Value::Text(String::from_utf8_lossy(raw).into_owned()));
};
if let Kind::Array(elem_ty) = ty.kind() {
return binary_array_to_value(&ty, elem_ty, raw);
}
match ty {
Type::BOOL => decode_binary::<bool>(&ty, raw).map(Value::Bool),
Type::INT2 => decode_binary::<i16>(&ty, raw).map(|n| Value::I64(i64::from(n))),
Type::INT4 => decode_binary::<i32>(&ty, raw).map(|n| Value::I64(i64::from(n))),
Type::INT8 => decode_binary::<i64>(&ty, raw).map(Value::I64),
Type::FLOAT4 => decode_binary::<f32>(&ty, raw).map(|f| Value::F64(f64::from(f))),
Type::FLOAT8 => decode_binary::<f64>(&ty, raw).map(Value::F64),
Type::TEXT | Type::VARCHAR | Type::BPCHAR | Type::NAME => {
decode_binary::<String>(&ty, raw).map(Value::Text)
}
Type::BYTEA => decode_binary::<Vec<u8>>(&ty, raw).map(Value::Blob),
Type::DATE => decode_binary::<Date>(&ty, raw).map(date_to_value),
Type::TIMESTAMP => {
decode_binary::<PrimitiveDateTime>(&ty, raw).map(primitive_datetime_to_value)
}
Type::TIMESTAMPTZ => {
decode_binary::<OffsetDateTime>(&ty, raw).map(offset_datetime_to_value)
}
Type::TIME => decode_binary::<Time>(&ty, raw).map(time_of_day_to_value),
Type::UUID => decode_binary::<uuid::Uuid>(&ty, raw).map(|u| Value::Uuid(u.as_u128())),
Type::JSON => decode_json_element(raw, false).map(Value::Json),
Type::JSONB => decode_json_element(raw, true).map(Value::Json),
Type::NUMERIC => decode_binary_numeric(raw).map(Value::Decimal),
Type::INTERVAL => decode_binary_interval(raw).map(Value::Text),
ref other => {
if <String as FromSql>::accepts(other) {
decode_binary::<String>(other, raw).map(Value::Text)
} else {
Ok(Value::Text(format!("<opaque:{}>", other.name())))
}
}
}
}
fn decode_binary<'a, T: FromSql<'a>>(ty: &Type, raw: &'a [u8]) -> Result<T, PgError> {
T::from_sql(ty, raw).map_err(|e| {
PgError::TypeConversion(format!(
"invalid binary payload for type {} (OID {}): {e}",
ty.name(),
ty.oid()
))
})
}
fn date_to_value(d: Date) -> Value {
Value::Date(d.to_julian_day() - UNIX_EPOCH_JDN)
}
fn time_of_day_to_value(t: Time) -> Value {
let (h, m, s, ns) = t.as_hms_nano();
let us = i64::from(h) * 3_600_000_000
+ i64::from(m) * 60_000_000
+ i64::from(s) * 1_000_000
+ i64::from(ns) / 1_000;
Value::Time(us)
}
fn primitive_datetime_to_value(dt: PrimitiveDateTime) -> Value {
let us = dt.assume_utc().unix_timestamp_nanos() / 1_000;
#[allow(clippy::cast_possible_truncation)]
Value::Timestamp(us as i64)
}
fn offset_datetime_to_value(dt: OffsetDateTime) -> Value {
let us = dt.unix_timestamp_nanos() / 1_000;
#[allow(clippy::cast_possible_truncation)]
Value::Timestamp(us as i64)
}
fn decode_json_element(raw: &[u8], strip_version_byte: bool) -> Result<String, PgError> {
let payload = if strip_version_byte {
let Some((&version, rest)) = raw.split_first() else {
return Err(PgError::TypeConversion(
"JSONB binary payload is empty (missing the leading version byte)".to_string(),
));
};
if version != 1 {
return Err(PgError::TypeConversion(format!(
"JSONB binary payload has an unsupported version byte {version} (expected 1)"
)));
}
rest
} else {
raw
};
String::from_utf8(payload.to_vec()).map_err(|e| {
PgError::TypeConversion(format!(
"{} binary payload is not valid UTF-8: {e}",
if strip_version_byte { "JSONB" } else { "JSON" }
))
})
}
fn split_checked<'a>(buf: &'a [u8], n: usize, what: &str) -> Result<(&'a [u8], &'a [u8]), PgError> {
if buf.len() < n {
return Err(PgError::TypeConversion(format!(
"{what} truncated: expected {n} more byte(s), found {}",
buf.len()
)));
}
Ok(buf.split_at(n))
}
fn read_be_i16(buf: &mut &[u8], what: &str) -> Result<i16, PgError> {
let (head, tail) = split_checked(buf, 2, what)?;
*buf = tail;
Ok(i16::from_be_bytes([head[0], head[1]]))
}
fn read_be_u16(buf: &mut &[u8], what: &str) -> Result<u16, PgError> {
let (head, tail) = split_checked(buf, 2, what)?;
*buf = tail;
Ok(u16::from_be_bytes([head[0], head[1]]))
}
fn read_be_i32(buf: &mut &[u8], what: &str) -> Result<i32, PgError> {
let (head, tail) = split_checked(buf, 4, what)?;
*buf = tail;
Ok(i32::from_be_bytes([head[0], head[1], head[2], head[3]]))
}
fn read_be_u32(buf: &mut &[u8], what: &str) -> Result<u32, PgError> {
let (head, tail) = split_checked(buf, 4, what)?;
*buf = tail;
Ok(u32::from_be_bytes([head[0], head[1], head[2], head[3]]))
}
fn read_be_i64(buf: &mut &[u8], what: &str) -> Result<i64, PgError> {
let (head, tail) = split_checked(buf, 8, what)?;
*buf = tail;
Ok(i64::from_be_bytes([
head[0], head[1], head[2], head[3], head[4], head[5], head[6], head[7],
]))
}
fn decode_binary_numeric(raw: &[u8]) -> Result<String, PgError> {
let mut buf = raw;
let ndigits = read_be_i16(&mut buf, "NUMERIC ndigits")?;
let weight = read_be_i16(&mut buf, "NUMERIC weight")?;
let sign = read_be_u16(&mut buf, "NUMERIC sign")?;
let dscale = read_be_u16(&mut buf, "NUMERIC dscale")?;
const SIGN_NAN: u16 = 0xC000;
const SIGN_NEG: u16 = 0x4000;
if sign == SIGN_NAN {
return Ok("NaN".to_string());
}
let ndigits = usize::try_from(ndigits).map_err(|_| {
PgError::TypeConversion(format!(
"NUMERIC binary payload has a negative ndigits count ({ndigits})"
))
})?;
let mut digits: Vec<u16> = Vec::with_capacity(ndigits);
for _ in 0..ndigits {
let digit = read_be_u16(&mut buf, "NUMERIC digit group")?;
if digit > 9999 {
return Err(PgError::TypeConversion(format!(
"NUMERIC binary payload has an out-of-range digit group \
({digit}, expected 0..=9999)"
)));
}
digits.push(digit);
}
let dscale = usize::from(dscale);
let int_groups = usize::try_from((i32::from(weight) + 1).max(0)).unwrap_or(0);
let mut integer_part = String::new();
for (i, digit) in (0..int_groups).map(|i| (i, digits.get(i).copied().unwrap_or(0))) {
if i == 0 {
integer_part.push_str(&digit.to_string());
} else {
integer_part.push_str(&format!("{digit:04}"));
}
}
if integer_part.is_empty() {
integer_part.push('0');
}
let mut fraction_part = String::new();
if dscale > 0 {
let mut remaining_scale = dscale;
let frac_groups_needed = dscale.div_ceil(4);
for i in 0..frac_groups_needed {
let digit = digits.get(int_groups + i).copied().unwrap_or(0);
let group_str = format!("{digit:04}");
let take = remaining_scale.min(4);
fraction_part.push_str(&group_str[..take]);
remaining_scale = remaining_scale.saturating_sub(4);
}
}
let mut result = if sign == SIGN_NEG {
format!("-{integer_part}")
} else {
integer_part
};
if !fraction_part.is_empty() {
result.push('.');
result.push_str(&fraction_part);
}
Ok(result)
}
fn decode_binary_interval(raw: &[u8]) -> Result<String, PgError> {
let mut buf = raw;
let micros = read_be_i64(&mut buf, "INTERVAL microseconds")?;
let days = read_be_i32(&mut buf, "INTERVAL days")?;
let months = read_be_i32(&mut buf, "INTERVAL months")?;
let years = months / 12;
let rem_months = months % 12;
let hours = micros / 3_600_000_000;
let rem_micros = micros % 3_600_000_000;
let minutes = rem_micros / 60_000_000;
let rem_micros2 = rem_micros % 60_000_000;
let secs = rem_micros2 / 1_000_000;
let frac_micros = (rem_micros2 % 1_000_000).unsigned_abs();
let mut parts: Vec<String> = Vec::new();
if years != 0 {
parts.push(format!("{years}Y"));
}
if rem_months != 0 {
parts.push(format!("{rem_months}M"));
}
if days != 0 {
parts.push(format!("{days}D"));
}
let has_time = hours != 0 || minutes != 0 || secs != 0 || frac_micros != 0;
if has_time {
if frac_micros > 0 {
parts.push(format!(
"{hours:02}:{minutes:02}:{secs:02}.{frac_micros:06}"
));
} else {
parts.push(format!("{hours:02}:{minutes:02}:{secs:02}"));
}
}
if parts.is_empty() {
Ok("00:00:00".to_string())
} else {
Ok(parts.join(" "))
}
}
fn binary_array_to_value(ty: &Type, elem_ty: &Type, raw: &[u8]) -> Result<Value, PgError> {
let Some(element_type) = array_element_type(elem_ty) else {
return Ok(Value::Array(decode_opaque_binary_array(raw)?));
};
decode_typed_binary_array(ty, element_type, raw)
}
fn decode_typed_binary_array(
ty: &Type,
element_type: ArrayElementType,
raw: &[u8],
) -> Result<Value, PgError> {
let values: Vec<Value> = match element_type {
ArrayElementType::Bool => decode_binary::<Vec<Option<bool>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, Value::Bool))
.collect(),
ArrayElementType::Int2 => decode_binary::<Vec<Option<i16>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, |n| Value::I64(i64::from(n))))
.collect(),
ArrayElementType::Int4 => decode_binary::<Vec<Option<i32>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, |n| Value::I64(i64::from(n))))
.collect(),
ArrayElementType::Int8 => decode_binary::<Vec<Option<i64>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, Value::I64))
.collect(),
ArrayElementType::Float4 => decode_binary::<Vec<Option<f32>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, |f| Value::F64(f64::from(f))))
.collect(),
ArrayElementType::Float8 => decode_binary::<Vec<Option<f64>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, Value::F64))
.collect(),
ArrayElementType::Text => decode_binary::<Vec<Option<String>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, Value::Text))
.collect(),
ArrayElementType::Bytea => decode_binary::<Vec<Option<Vec<u8>>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, Value::Blob))
.collect(),
ArrayElementType::Date => decode_binary::<Vec<Option<Date>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, date_to_value))
.collect(),
ArrayElementType::Time => decode_binary::<Vec<Option<Time>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, time_of_day_to_value))
.collect(),
ArrayElementType::Timestamp => decode_binary::<Vec<Option<PrimitiveDateTime>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, primitive_datetime_to_value))
.collect(),
ArrayElementType::TimestampTz => decode_binary::<Vec<Option<OffsetDateTime>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, offset_datetime_to_value))
.collect(),
ArrayElementType::Uuid => decode_binary::<Vec<Option<uuid::Uuid>>>(ty, raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, |u| Value::Uuid(u.as_u128())))
.collect(),
ArrayElementType::Json => decode_json_array(raw, false)?,
ArrayElementType::Jsonb => decode_json_array(raw, true)?,
_ => decode_opaque_binary_array(raw)?,
};
Ok(Value::TypedArray {
element_type,
values,
})
}
fn raw_array_elements(raw: &[u8]) -> Result<Vec<Option<Vec<u8>>>, PgError> {
let mut buf = raw;
let ndim = read_be_i32(&mut buf, "array ndim")?;
let _flags = read_be_i32(&mut buf, "array flags")?;
let _element_oid = read_be_u32(&mut buf, "array element OID")?;
if ndim == 0 {
return Ok(Vec::new());
}
if ndim != 1 {
return Err(PgError::TypeConversion(format!(
"binary array structural decoding only supports 0- or \
1-dimensional arrays, found {ndim} dimension(s)"
)));
}
let dim_size = read_be_i32(&mut buf, "array dimension size")?;
let _lower_bound = read_be_i32(&mut buf, "array dimension lower bound")?;
let dim_size = usize::try_from(dim_size).map_err(|_| {
PgError::TypeConversion(format!("array dimension size is negative ({dim_size})"))
})?;
let mut elements = Vec::new();
for _ in 0..dim_size {
let elem_len = read_be_i32(&mut buf, "array element length")?;
if elem_len < 0 {
elements.push(None);
continue;
}
let elem_len = usize::try_from(elem_len).map_err(|_| {
PgError::TypeConversion(format!("array element length is negative ({elem_len})"))
})?;
let (elem_bytes, rest) = split_checked(buf, elem_len, "array element data")?;
buf = rest;
elements.push(Some(elem_bytes.to_vec()));
}
Ok(elements)
}
fn decode_opaque_binary_array(raw: &[u8]) -> Result<Vec<Value>, PgError> {
Ok(raw_array_elements(raw)?
.into_iter()
.map(|opt| opt.map_or(Value::Null, Value::Blob))
.collect())
}
fn decode_json_array(raw: &[u8], strip_version_byte: bool) -> Result<Vec<Value>, PgError> {
raw_array_elements(raw)?
.into_iter()
.map(|opt| match opt {
Some(bytes) => decode_json_element(&bytes, strip_version_byte).map(Value::Json),
None => Ok(Value::Null),
})
.collect()
}
fn array_element_type(elem_ty: &Type) -> Option<ArrayElementType> {
match *elem_ty {
Type::BOOL => Some(ArrayElementType::Bool),
Type::INT2 => Some(ArrayElementType::Int2),
Type::INT4 => Some(ArrayElementType::Int4),
Type::INT8 => Some(ArrayElementType::Int8),
Type::FLOAT4 => Some(ArrayElementType::Float4),
Type::FLOAT8 => Some(ArrayElementType::Float8),
Type::TEXT | Type::VARCHAR => Some(ArrayElementType::Text),
Type::BYTEA => Some(ArrayElementType::Bytea),
Type::DATE => Some(ArrayElementType::Date),
Type::TIME => Some(ArrayElementType::Time),
Type::TIMESTAMP => Some(ArrayElementType::Timestamp),
Type::TIMESTAMPTZ => Some(ArrayElementType::TimestampTz),
Type::UUID => Some(ArrayElementType::Uuid),
Type::JSON => Some(ArrayElementType::Json),
Type::JSONB => Some(ArrayElementType::Jsonb),
Type::NUMERIC => Some(ArrayElementType::Decimal),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq)]
enum ArrayElem {
Null,
Scalar(String),
Nested(Vec<ArrayElem>),
}
fn decode_text_array(elem_ty: &Type, s: &str) -> Result<Value, PgError> {
let mut pos = skip_dimension_prefix(elem_ty, s)?;
let node = parse_element(elem_ty, s, &mut pos)?;
skip_ascii_ws(s, &mut pos);
if pos != s.len() {
return Err(array_syntax_error(
elem_ty,
s,
&format!("unexpected trailing content at byte offset {pos}"),
));
}
let ArrayElem::Nested(items) = node else {
return Err(array_syntax_error(
elem_ty,
s,
"array literal must be a brace-delimited '{...}' group",
));
};
let is_flat = items.iter().all(|it| !matches!(it, ArrayElem::Nested(_)));
let values = items
.into_iter()
.map(|it| array_node_to_value(elem_ty, it))
.collect::<Result<Vec<_>, _>>()?;
Ok(match (is_flat, array_element_type(elem_ty)) {
(true, Some(element_type)) => Value::TypedArray {
element_type,
values,
},
_ => Value::Array(values),
})
}
fn array_node_to_value(elem_ty: &Type, node: ArrayElem) -> Result<Value, PgError> {
match node {
ArrayElem::Null => Ok(Value::Null),
ArrayElem::Scalar(text) => text_to_value(elem_ty.oid(), &text),
ArrayElem::Nested(items) => items
.into_iter()
.map(|it| array_node_to_value(elem_ty, it))
.collect::<Result<Vec<_>, _>>()
.map(Value::Array),
}
}
fn array_syntax_error(elem_ty: &Type, s: &str, detail: &str) -> PgError {
PgError::TypeConversion(format!(
"malformed array literal for element type {} (OID {}): {detail} (input: {s:?})",
elem_ty.name(),
elem_ty.oid(),
))
}
fn skip_dimension_prefix(elem_ty: &Type, s: &str) -> Result<usize, PgError> {
let bytes = s.as_bytes();
let mut pos = 0usize;
let mut saw_prefix = false;
while bytes.get(pos) == Some(&b'[') {
saw_prefix = true;
pos += 1;
pos = skip_signed_digits(bytes, pos).ok_or_else(|| {
array_syntax_error(
elem_ty,
s,
"invalid dimension lower bound in '[l:u]' prefix",
)
})?;
if bytes.get(pos) != Some(&b':') {
return Err(array_syntax_error(
elem_ty,
s,
"expected ':' in '[l:u]' dimension prefix",
));
}
pos += 1;
pos = skip_signed_digits(bytes, pos).ok_or_else(|| {
array_syntax_error(
elem_ty,
s,
"invalid dimension upper bound in '[l:u]' prefix",
)
})?;
if bytes.get(pos) != Some(&b']') {
return Err(array_syntax_error(
elem_ty,
s,
"expected ']' to close a '[l:u]' dimension prefix",
));
}
pos += 1;
}
if saw_prefix {
if bytes.get(pos) != Some(&b'=') {
return Err(array_syntax_error(
elem_ty,
s,
"expected '=' after the '[l:u]' dimension prefix(es)",
));
}
pos += 1;
}
Ok(pos)
}
fn skip_signed_digits(bytes: &[u8], mut pos: usize) -> Option<usize> {
if bytes.get(pos) == Some(&b'-') {
pos += 1;
}
let start = pos;
while matches!(bytes.get(pos), Some(b'0'..=b'9')) {
pos += 1;
}
if pos == start {
None
} else {
Some(pos)
}
}
fn skip_ascii_ws(s: &str, pos: &mut usize) {
let bytes = s.as_bytes();
while matches!(bytes.get(*pos), Some(b' ' | b'\t' | b'\n' | b'\r')) {
*pos += 1;
}
}
fn parse_element(elem_ty: &Type, s: &str, pos: &mut usize) -> Result<ArrayElem, PgError> {
skip_ascii_ws(s, pos);
match s.as_bytes().get(*pos) {
Some(b'{') => parse_braces(elem_ty, s, pos).map(ArrayElem::Nested),
Some(b'"') => parse_quoted(elem_ty, s, pos).map(ArrayElem::Scalar),
Some(_) => parse_bareword(elem_ty, s, pos),
None => Err(array_syntax_error(
elem_ty,
s,
"expected an array element, found end of input",
)),
}
}
fn parse_braces(elem_ty: &Type, s: &str, pos: &mut usize) -> Result<Vec<ArrayElem>, PgError> {
*pos += 1;
let mut items = Vec::new();
skip_ascii_ws(s, pos);
if s.as_bytes().get(*pos) == Some(&b'}') {
*pos += 1;
return Ok(items);
}
loop {
items.push(parse_element(elem_ty, s, pos)?);
skip_ascii_ws(s, pos);
match s.as_bytes().get(*pos) {
Some(b',') => {
*pos += 1;
}
Some(b'}') => {
*pos += 1;
break;
}
Some(&other) => {
return Err(array_syntax_error(
elem_ty,
s,
&format!(
"expected ',' or '}}' at byte offset {}, found {:?}",
*pos, other as char
),
));
}
None => {
return Err(array_syntax_error(
elem_ty,
s,
"unterminated '{' (missing closing '}')",
));
}
}
}
Ok(items)
}
fn parse_quoted(elem_ty: &Type, s: &str, pos: &mut usize) -> Result<String, PgError> {
*pos += 1;
let bytes = s.as_bytes();
let mut out = String::new();
let mut segment_start = *pos;
loop {
match bytes.get(*pos) {
None => {
return Err(array_syntax_error(
elem_ty,
s,
"unterminated quoted array element (missing closing '\"')",
));
}
Some(b'"') => {
out.push_str(&s[segment_start..*pos]);
*pos += 1;
return Ok(out);
}
Some(b'\\') => {
out.push_str(&s[segment_start..*pos]);
*pos += 1;
match bytes.get(*pos) {
Some(b'"') => out.push('"'),
Some(b'\\') => out.push('\\'),
_ => {
return Err(array_syntax_error(
elem_ty,
s,
"invalid escape sequence in a quoted array element \
(expected \\\" or \\\\)",
));
}
}
*pos += 1;
segment_start = *pos;
}
Some(_) => *pos += 1,
}
}
}
fn parse_bareword(elem_ty: &Type, s: &str, pos: &mut usize) -> Result<ArrayElem, PgError> {
let bytes = s.as_bytes();
let start = *pos;
loop {
match bytes.get(*pos) {
None | Some(b',') | Some(b'}') => break,
Some(b'{') => {
return Err(array_syntax_error(
elem_ty,
s,
"unexpected '{' inside an unquoted array element",
));
}
Some(_) => *pos += 1,
}
}
let trimmed = s[start..*pos].trim_matches(|c: char| matches!(c, ' ' | '\t' | '\n' | '\r'));
if trimmed.is_empty() {
return Err(array_syntax_error(
elem_ty,
s,
"empty unquoted array element",
));
}
if trimmed.eq_ignore_ascii_case("null") {
Ok(ArrayElem::Null)
} else {
Ok(ArrayElem::Scalar(trimmed.to_string()))
}
}
#[cfg(test)]
mod tests;