use nodedb_types::{NdbDateTime, Value};
use super::error::DecodeReason;
const PG_EPOCH_OFFSET_MICROS: i64 = 946_684_800_000_000;
fn fixed<const N: usize>(raw: &[u8]) -> Result<[u8; N], DecodeReason> {
raw.try_into().map_err(|_| DecodeReason::Width {
expected: N,
found: raw.len(),
})
}
pub(super) fn boolean(raw: &[u8]) -> Result<Value, DecodeReason> {
match fixed::<1>(raw)? {
[0] => Ok(Value::Bool(false)),
[1] => Ok(Value::Bool(true)),
_ => Err(DecodeReason::BoolByte),
}
}
pub(super) fn int2(raw: &[u8]) -> Result<Value, DecodeReason> {
Ok(Value::Integer(i64::from(i16::from_be_bytes(fixed(raw)?))))
}
pub(super) fn int4(raw: &[u8]) -> Result<Value, DecodeReason> {
Ok(Value::Integer(i64::from(i32::from_be_bytes(fixed(raw)?))))
}
pub(super) fn int8(raw: &[u8]) -> Result<Value, DecodeReason> {
Ok(Value::Integer(i64::from_be_bytes(fixed(raw)?)))
}
pub(super) fn float4(raw: &[u8]) -> Result<Value, DecodeReason> {
Ok(Value::Float(f64::from(f32::from_be_bytes(fixed(raw)?))))
}
pub(super) fn float8(raw: &[u8]) -> Result<Value, DecodeReason> {
Ok(Value::Float(f64::from_be_bytes(fixed(raw)?)))
}
fn instant(raw: &[u8]) -> Result<NdbDateTime, DecodeReason> {
let pg_micros = i64::from_be_bytes(fixed(raw)?);
pg_micros
.checked_add(PG_EPOCH_OFFSET_MICROS)
.map(NdbDateTime::from_micros)
.ok_or(DecodeReason::OutOfRange { what: "timestamp" })
}
pub(super) fn timestamp(raw: &[u8]) -> Result<Value, DecodeReason> {
instant(raw).map(Value::NaiveDateTime)
}
pub(super) fn timestamptz(raw: &[u8]) -> Result<Value, DecodeReason> {
instant(raw).map(Value::DateTime)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decodes_bool() {
assert_eq!(boolean(&[1]), Ok(Value::Bool(true)));
assert_eq!(boolean(&[0]), Ok(Value::Bool(false)));
assert_eq!(boolean(&[2]), Err(DecodeReason::BoolByte));
assert_eq!(
boolean(b"t"),
Err(DecodeReason::BoolByte),
"text `t` under a binary bool is refused"
);
assert_eq!(
boolean(&[]),
Err(DecodeReason::Width {
expected: 1,
found: 0
})
);
}
#[test]
fn decodes_integers() {
assert_eq!(int2(&(-7i16).to_be_bytes()), Ok(Value::Integer(-7)));
assert_eq!(
int4(&i32::MAX.to_be_bytes()),
Ok(Value::Integer(i64::from(i32::MAX)))
);
assert_eq!(int8(&i64::MIN.to_be_bytes()), Ok(Value::Integer(i64::MIN)));
assert_eq!(
int4(b"42"),
Err(DecodeReason::Width {
expected: 4,
found: 2
})
);
assert_eq!(
int8(&[0; 4]),
Err(DecodeReason::Width {
expected: 8,
found: 4
})
);
}
#[test]
fn decodes_floats_including_non_finite() {
assert_eq!(float4(&1.5f32.to_be_bytes()), Ok(Value::Float(1.5)));
assert_eq!(float8(&(-2.25f64).to_be_bytes()), Ok(Value::Float(-2.25)));
assert_eq!(
float8(&f64::INFINITY.to_be_bytes()),
Ok(Value::Float(f64::INFINITY))
);
assert_eq!(
float4(&f32::NEG_INFINITY.to_be_bytes()),
Ok(Value::Float(f64::NEG_INFINITY))
);
let Ok(Value::Float(nan)) = float8(&f64::NAN.to_be_bytes()) else {
panic!("binary NaN decodes as a float");
};
assert!(nan.is_nan());
assert_eq!(
float4(&[0; 8]),
Err(DecodeReason::Width {
expected: 4,
found: 8
})
);
}
#[test]
fn decodes_timestamps_from_the_postgres_epoch() {
let unix_micros = 1_583_402_400_000_000i64;
let raw = (unix_micros - PG_EPOCH_OFFSET_MICROS).to_be_bytes();
let at = NdbDateTime::from_micros(unix_micros);
assert_eq!(timestamp(&raw), Ok(Value::NaiveDateTime(at)));
assert_eq!(timestamptz(&raw), Ok(Value::DateTime(at)));
assert_eq!(
timestamptz(&i64::MAX.to_be_bytes()),
Err(DecodeReason::OutOfRange { what: "timestamp" })
);
assert_eq!(
timestamp(b"2020"),
Err(DecodeReason::Width {
expected: 8,
found: 4
})
);
}
}