wasi-pg-client 0.1.3

PostgreSQL client library for WASI Preview 2
Documentation
use postgres_protocol::types;
use postgres_types::Type;

use super::{Error, Format, Result};

pub trait FromSql: Sized {
    fn from_sql(ty: &Type, raw: Option<&[u8]>, format: Format) -> Result<Self>;
}

fn unexpected_null<T>() -> Result<T> {
    Err(Error::Conversion("unexpected NULL".into()))
}

fn decode_hex(s: &str) -> Result<Vec<u8>> {
    let chars: Vec<char> = s.chars().filter(|c| !c.is_whitespace()).collect();
    if chars.len() % 2 != 0 {
        return Err(Error::InvalidDataFormat(
            "hex string has an odd number of digits".into(),
        ));
    }

    let mut out = Vec::with_capacity(chars.len() / 2);
    let mut i = 0;
    while i < chars.len() {
        let h = chars[i];
        let l = chars[i + 1];
        let hi = h
            .to_digit(16)
            .ok_or_else(|| Error::Conversion(format!("invalid hex char: {h}")))?;
        let lo = l
            .to_digit(16)
            .ok_or_else(|| Error::Conversion(format!("invalid hex char: {l}")))?;
        out.push((hi * 16 + lo) as u8);
        i += 2;
    }
    Ok(out)
}

fn decode_bytea_escape(bytes: &[u8]) -> Result<Vec<u8>> {
    let mut out = Vec::with_capacity(bytes.len());
    let mut i = 0;

    while i < bytes.len() {
        if bytes[i] != b'\\' {
            out.push(bytes[i]);
            i += 1;
            continue;
        }

        i += 1;
        if i >= bytes.len() {
            return Err(Error::InvalidDataFormat(
                "trailing backslash in bytea escape format".into(),
            ));
        }

        match bytes[i] {
            b'\\' => {
                out.push(b'\\');
                i += 1;
            }
            b'0'..=b'7' => {
                if i + 2 >= bytes.len() {
                    return Err(Error::InvalidDataFormat(
                        "incomplete octal escape in bytea text format".into(),
                    ));
                }
                let oct = &bytes[i..i + 3];
                if !oct.iter().all(|b| matches!(b, b'0'..=b'7')) {
                    return Err(Error::InvalidDataFormat(
                        "invalid octal escape in bytea text format".into(),
                    ));
                }
                let value = ((oct[0] - b'0') << 6) | ((oct[1] - b'0') << 3) | (oct[2] - b'0');
                out.push(value);
                i += 3;
            }
            other => {
                return Err(Error::InvalidDataFormat(format!(
                    "invalid bytea escape sequence: \\{}",
                    other as char
                )));
            }
        }
    }

    Ok(out)
}

fn as_text(raw: Option<&[u8]>) -> Result<&str> {
    match raw {
        Some(b) => std::str::from_utf8(b).map_err(Error::Utf8Error),
        None => unexpected_null(),
    }
}

impl FromSql for bool {
    fn from_sql(_ty: &Type, raw: Option<&[u8]>, format: Format) -> Result<Self> {
        match format {
            Format::Text => match as_text(raw)? {
                "t" | "true" | "TRUE" | "True" | "1" | "yes" | "YES" | "on" | "ON" => Ok(true),
                "f" | "false" | "FALSE" | "False" | "0" | "no" | "NO" | "off" | "OFF" => Ok(false),
                s => Err(Error::Conversion(format!("invalid bool text: {s}"))),
            },
            Format::Binary => types::bool_from_sql(raw.ok_or_else(|| {
                std::io::Error::new(std::io::ErrorKind::InvalidData, "unexpected NULL")
            })?)
            .map_err(|e| Error::Conversion(e.to_string())),
        }
    }
}

impl FromSql for i8 {
    fn from_sql(_ty: &Type, raw: Option<&[u8]>, format: Format) -> Result<Self> {
        match format {
            Format::Text => {
                let s = as_text(raw)?;
                s.parse::<i8>().map_err(Error::ParseIntError)
            }
            Format::Binary => types::char_from_sql(raw.ok_or_else(|| {
                std::io::Error::new(std::io::ErrorKind::InvalidData, "unexpected NULL")
            })?)
            .map_err(|e| Error::Conversion(e.to_string())),
        }
    }
}

impl FromSql for i16 {
    fn from_sql(_ty: &Type, raw: Option<&[u8]>, format: Format) -> Result<Self> {
        match format {
            Format::Text => {
                let s = as_text(raw)?;
                s.parse::<i16>().map_err(Error::ParseIntError)
            }
            Format::Binary => types::int2_from_sql(raw.ok_or_else(|| {
                std::io::Error::new(std::io::ErrorKind::InvalidData, "unexpected NULL")
            })?)
            .map_err(|e| Error::Conversion(e.to_string())),
        }
    }
}

impl FromSql for i32 {
    fn from_sql(_ty: &Type, raw: Option<&[u8]>, format: Format) -> Result<Self> {
        match format {
            Format::Text => {
                let s = as_text(raw)?;
                s.parse::<i32>().map_err(Error::ParseIntError)
            }
            Format::Binary => types::int4_from_sql(raw.ok_or_else(|| {
                std::io::Error::new(std::io::ErrorKind::InvalidData, "unexpected NULL")
            })?)
            .map_err(|e| Error::Conversion(e.to_string())),
        }
    }
}

impl FromSql for i64 {
    fn from_sql(_ty: &Type, raw: Option<&[u8]>, format: Format) -> Result<Self> {
        match format {
            Format::Text => {
                let s = as_text(raw)?;
                s.parse::<i64>()
                    .map_err(|e| Error::Conversion(e.to_string()))
            }
            Format::Binary => types::int8_from_sql(raw.ok_or_else(|| {
                std::io::Error::new(std::io::ErrorKind::InvalidData, "unexpected NULL")
            })?)
            .map_err(|e| Error::Conversion(e.to_string())),
        }
    }
}

impl FromSql for u32 {
    fn from_sql(_ty: &Type, raw: Option<&[u8]>, format: Format) -> Result<Self> {
        match format {
            Format::Text => {
                let s = as_text(raw)?;
                s.parse::<u32>()
                    .map_err(|e| Error::Conversion(e.to_string()))
            }
            Format::Binary => types::oid_from_sql(raw.ok_or_else(|| {
                std::io::Error::new(std::io::ErrorKind::InvalidData, "unexpected NULL")
            })?)
            .map_err(|e| Error::Conversion(e.to_string())),
        }
    }
}

impl FromSql for f32 {
    fn from_sql(_ty: &Type, raw: Option<&[u8]>, format: Format) -> Result<Self> {
        match format {
            Format::Text => {
                let s = as_text(raw)?;
                s.parse::<f32>().map_err(Error::ParseFloatError)
            }
            Format::Binary => types::float4_from_sql(raw.ok_or_else(|| {
                std::io::Error::new(std::io::ErrorKind::InvalidData, "unexpected NULL")
            })?)
            .map_err(|e| Error::Conversion(e.to_string())),
        }
    }
}

impl FromSql for f64 {
    fn from_sql(_ty: &Type, raw: Option<&[u8]>, format: Format) -> Result<Self> {
        match format {
            Format::Text => {
                let s = as_text(raw)?;
                s.parse::<f64>().map_err(Error::ParseFloatError)
            }
            Format::Binary => types::float8_from_sql(raw.ok_or_else(|| {
                std::io::Error::new(std::io::ErrorKind::InvalidData, "unexpected NULL")
            })?)
            .map_err(|e| Error::Conversion(e.to_string())),
        }
    }
}

impl FromSql for String {
    fn from_sql(_ty: &Type, raw: Option<&[u8]>, format: Format) -> Result<Self> {
        match raw {
            Some(bytes) => match format {
                Format::Text => Ok(std::str::from_utf8(bytes)
                    .map_err(Error::Utf8Error)?
                    .to_string()),
                Format::Binary => types::text_from_sql(bytes)
                    .map(|s| s.to_string())
                    .map_err(|e| Error::Conversion(e.to_string())),
            },
            None => unexpected_null(),
        }
    }
}

impl FromSql for Vec<u8> {
    fn from_sql(_ty: &Type, raw: Option<&[u8]>, format: Format) -> Result<Self> {
        match raw {
            Some(bytes) => match format {
                Format::Text => {
                    if bytes.len() >= 2 && bytes[0] == b'\\' && bytes[1] == b'x' {
                        let hex = std::str::from_utf8(&bytes[2..]).map_err(Error::Utf8Error)?;
                        decode_hex(hex.trim())
                            .map_err(|e| Error::Conversion(format!("invalid bytea hex: {e}")))
                    } else {
                        decode_bytea_escape(bytes)
                    }
                }
                Format::Binary => Ok(types::bytea_from_sql(bytes).to_vec()),
            },
            None => unexpected_null(),
        }
    }
}

impl<T: FromSql> FromSql for Option<T> {
    fn from_sql(ty: &Type, raw: Option<&[u8]>, format: Format) -> Result<Self> {
        match raw {
            Some(bytes) => T::from_sql(ty, Some(bytes), format).map(Some),
            None => Ok(None),
        }
    }
}