use crate::error::{Error, Result};
use crate::protocol::TypeTag;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Value {
Null,
Bool(bool),
Int(i64),
Float(f64),
Numeric(String),
Text(String),
Bytes(Vec<u8>),
Typed {
tag: TypeTag,
text: String,
},
}
impl Value {
#[must_use]
pub const fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
#[must_use]
pub const fn as_i64(&self) -> Option<i64> {
if let Self::Int(n) = self {
Some(*n)
} else {
None
}
}
#[must_use]
pub const fn as_f64(&self) -> Option<f64> {
if let Self::Float(n) = self {
Some(*n)
} else {
None
}
}
#[must_use]
pub const fn as_bool(&self) -> Option<bool> {
if let Self::Bool(b) = self {
Some(*b)
} else {
None
}
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::Text(s) | Self::Numeric(s) => Some(s),
Self::Typed { text, .. } => Some(text),
_ => None,
}
}
#[must_use]
pub fn as_bytes(&self) -> Option<&[u8]> {
if let Self::Bytes(b) = self {
Some(b)
} else {
None
}
}
pub(crate) fn decode(field: Option<Vec<u8>>, tag: TypeTag) -> Result<Self> {
let Some(bytes) = field else {
return Ok(Self::Null);
};
if tag == TypeTag::Bytes {
let text = String::from_utf8(bytes)
.map_err(|_| Error::Protocol("invalid UTF-8 in BYTEA field".to_owned()))?;
return Ok(Self::Bytes(decode_bytea_hex(&text)?));
}
let text = String::from_utf8(bytes)
.map_err(|_| Error::Protocol("invalid UTF-8 in value field".to_owned()))?;
Ok(match tag {
TypeTag::Bool => Self::Bool(matches!(text.as_str(), "true" | "t" | "TRUE" | "T")),
TypeTag::Int => text
.parse::<i64>()
.map(Self::Int)
.unwrap_or(Self::Text(text)),
TypeTag::Float => text
.parse::<f64>()
.map(Self::Float)
.unwrap_or(Self::Text(text)),
TypeTag::Numeric => Self::Numeric(text),
TypeTag::Text | TypeTag::Unknown => Self::Text(text),
TypeTag::Bytes => unreachable!("handled above"),
other => Self::Typed { tag: other, text },
})
}
}
fn decode_bytea_hex(s: &str) -> Result<Vec<u8>> {
let h = s.strip_prefix("\\x").unwrap_or(s);
if h.len() % 2 != 0 {
return Err(Error::Protocol(
"malformed BYTEA hex (odd length)".to_owned(),
));
}
(0..h.len())
.step_by(2)
.map(|i| {
u8::from_str_radix(&h[i..i + 2], 16)
.map_err(|_| Error::Protocol("malformed BYTEA hex".to_owned()))
})
.collect()
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Null => write!(f, "NULL"),
Self::Bool(b) => write!(f, "{b}"),
Self::Int(n) => write!(f, "{n}"),
Self::Float(n) => write!(f, "{n}"),
Self::Numeric(s) | Self::Text(s) => write!(f, "{s}"),
Self::Typed { text, .. } => write!(f, "{text}"),
Self::Bytes(b) => write!(
f,
"\\x{}",
b.iter().map(|x| format!("{x:02x}")).collect::<String>()
),
}
}
}
pub trait ToSql {
fn to_sql_text(&self) -> Option<Vec<u8>>;
}
macro_rules! to_sql_display {
($($t:ty),*) => {$(
impl ToSql for $t {
fn to_sql_text(&self) -> Option<Vec<u8>> {
Some(self.to_string().into_bytes())
}
}
)*};
}
to_sql_display!(i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
impl ToSql for bool {
fn to_sql_text(&self) -> Option<Vec<u8>> {
Some(if *self {
b"true".to_vec()
} else {
b"false".to_vec()
})
}
}
impl ToSql for str {
fn to_sql_text(&self) -> Option<Vec<u8>> {
Some(self.as_bytes().to_vec())
}
}
impl ToSql for String {
fn to_sql_text(&self) -> Option<Vec<u8>> {
Some(self.clone().into_bytes())
}
}
impl<T: ToSql> ToSql for Option<T> {
fn to_sql_text(&self) -> Option<Vec<u8>> {
self.as_ref().and_then(ToSql::to_sql_text)
}
}
impl ToSql for [u8] {
fn to_sql_text(&self) -> Option<Vec<u8>> {
Some(Value::Bytes(self.to_vec()).to_string().into_bytes())
}
}
impl ToSql for Vec<u8> {
fn to_sql_text(&self) -> Option<Vec<u8>> {
self.as_slice().to_sql_text()
}
}
impl<T: ToSql + ?Sized> ToSql for &T {
fn to_sql_text(&self) -> Option<Vec<u8>> {
(**self).to_sql_text()
}
}
impl ToSql for Value {
fn to_sql_text(&self) -> Option<Vec<u8>> {
match self {
Self::Null => None,
other => Some(other.to_string().into_bytes()),
}
}
}
pub trait FromSql: Sized {
fn from_sql(value: &Value) -> Result<Self>;
}
impl FromSql for Value {
fn from_sql(value: &Value) -> Result<Self> {
Ok(value.clone())
}
}
impl FromSql for i64 {
fn from_sql(value: &Value) -> Result<Self> {
match value {
Value::Int(n) => Ok(*n),
Value::Text(s) | Value::Numeric(s) => s
.parse()
.map_err(|_| Error::Conversion(format!("cannot read {s:?} as i64"))),
other => Err(Error::Conversion(format!("cannot read {other:?} as i64"))),
}
}
}
impl FromSql for f64 {
fn from_sql(value: &Value) -> Result<Self> {
match value {
Value::Float(n) => Ok(*n),
Value::Int(n) => Ok(*n as Self),
Value::Numeric(s) | Value::Text(s) => s
.parse()
.map_err(|_| Error::Conversion(format!("cannot read {s:?} as f64"))),
other => Err(Error::Conversion(format!("cannot read {other:?} as f64"))),
}
}
}
impl FromSql for bool {
fn from_sql(value: &Value) -> Result<Self> {
match value {
Value::Bool(b) => Ok(*b),
other => Err(Error::Conversion(format!("cannot read {other:?} as bool"))),
}
}
}
impl FromSql for String {
fn from_sql(value: &Value) -> Result<Self> {
match value {
Value::Null => Err(Error::Conversion("NULL into non-Option String".to_owned())),
other => Ok(other.to_string()),
}
}
}
impl FromSql for Vec<u8> {
fn from_sql(value: &Value) -> Result<Self> {
match value {
Value::Bytes(b) => Ok(b.clone()),
other => Err(Error::Conversion(format!("cannot read {other:?} as bytes"))),
}
}
}
impl<T: FromSql> FromSql for Option<T> {
fn from_sql(value: &Value) -> Result<Self> {
if value.is_null() {
Ok(None)
} else {
T::from_sql(value).map(Some)
}
}
}