use hyperdb_api_core::types::{
oids, Date, Interval, Numeric, OffsetTimestamp, Oid, Time, Timestamp,
};
pub trait ToSqlParam: Send + Sync {
fn encode_param(&self) -> Option<Vec<u8>>;
fn sql_oid(&self) -> Oid {
Oid::new(0)
}
fn to_sql_literal(&self) -> String;
}
impl ToSqlParam for i16 {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.to_be_bytes().to_vec())
}
fn sql_oid(&self) -> Oid {
oids::SMALL_INT
}
fn to_sql_literal(&self) -> String {
self.to_string()
}
}
impl ToSqlParam for i32 {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.to_be_bytes().to_vec())
}
fn sql_oid(&self) -> Oid {
oids::INT
}
fn to_sql_literal(&self) -> String {
self.to_string()
}
}
impl ToSqlParam for i64 {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.to_be_bytes().to_vec())
}
fn sql_oid(&self) -> Oid {
oids::BIG_INT
}
fn to_sql_literal(&self) -> String {
self.to_string()
}
}
impl ToSqlParam for f32 {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.to_be_bytes().to_vec())
}
fn sql_oid(&self) -> Oid {
oids::FLOAT
}
fn to_sql_literal(&self) -> String {
if self.is_nan() {
"'NaN'".to_string()
} else if self.is_infinite() {
if *self > 0.0 {
"'Infinity'".to_string()
} else {
"'-Infinity'".to_string()
}
} else {
self.to_string()
}
}
}
impl ToSqlParam for f64 {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.to_be_bytes().to_vec())
}
fn sql_oid(&self) -> Oid {
oids::DOUBLE
}
fn to_sql_literal(&self) -> String {
if self.is_nan() {
"'NaN'".to_string()
} else if self.is_infinite() {
if *self > 0.0 {
"'Infinity'".to_string()
} else {
"'-Infinity'".to_string()
}
} else {
self.to_string()
}
}
}
impl ToSqlParam for bool {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(vec![u8::from(*self)])
}
fn sql_oid(&self) -> Oid {
oids::BOOL
}
fn to_sql_literal(&self) -> String {
if *self { "TRUE" } else { "FALSE" }.to_string()
}
}
impl ToSqlParam for str {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.as_bytes().to_vec())
}
fn sql_oid(&self) -> Oid {
oids::TEXT
}
fn to_sql_literal(&self) -> String {
format!("'{}'", self.replace('\'', "''"))
}
}
impl ToSqlParam for String {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.as_bytes().to_vec())
}
fn sql_oid(&self) -> Oid {
oids::TEXT
}
fn to_sql_literal(&self) -> String {
format!("'{}'", self.replace('\'', "''"))
}
}
impl ToSqlParam for &str {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.as_bytes().to_vec())
}
fn sql_oid(&self) -> Oid {
oids::TEXT
}
fn to_sql_literal(&self) -> String {
format!("'{}'", self.replace('\'', "''"))
}
}
impl<T: ToSqlParam> ToSqlParam for &T {
fn encode_param(&self) -> Option<Vec<u8>> {
(*self).encode_param()
}
fn sql_oid(&self) -> Oid {
(*self).sql_oid()
}
fn to_sql_literal(&self) -> String {
(*self).to_sql_literal()
}
}
impl<T: ToSqlParam> ToSqlParam for Option<T> {
fn encode_param(&self) -> Option<Vec<u8>> {
match self {
Some(value) => value.encode_param(),
None => None, }
}
fn sql_oid(&self) -> Oid {
match self {
Some(value) => value.sql_oid(),
None => Oid::new(0),
}
}
fn to_sql_literal(&self) -> String {
match self {
Some(value) => value.to_sql_literal(),
None => "NULL".to_string(),
}
}
}
impl ToSqlParam for Date {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.to_julian_day().to_be_bytes().to_vec())
}
fn sql_oid(&self) -> Oid {
oids::DATE
}
fn to_sql_literal(&self) -> String {
format!("DATE '{self}'")
}
}
impl ToSqlParam for Time {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.to_microseconds().to_be_bytes().to_vec())
}
fn sql_oid(&self) -> Oid {
oids::TIME
}
fn to_sql_literal(&self) -> String {
format!("TIME '{self}'")
}
}
impl ToSqlParam for Timestamp {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.to_microseconds().to_be_bytes().to_vec())
}
fn sql_oid(&self) -> Oid {
oids::TIMESTAMP
}
fn to_sql_literal(&self) -> String {
format!("TIMESTAMP '{self}'")
}
}
impl ToSqlParam for OffsetTimestamp {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.to_microseconds_utc().to_be_bytes().to_vec())
}
fn sql_oid(&self) -> Oid {
oids::TIMESTAMP_TZ
}
fn to_sql_literal(&self) -> String {
format!("TIMESTAMPTZ '{self}'")
}
}
impl ToSqlParam for [u8] {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.to_vec())
}
fn sql_oid(&self) -> Oid {
oids::BYTE_A
}
#[expect(
clippy::format_collect,
reason = "readable hex/string formatting loop; refactoring to fold! obscures intent"
)]
fn to_sql_literal(&self) -> String {
let hex_str: String = self.iter().map(|b| format!("{b:02x}")).collect();
format!("E'\\\\x{hex_str}'")
}
}
impl ToSqlParam for Vec<u8> {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.clone())
}
fn sql_oid(&self) -> Oid {
oids::BYTE_A
}
#[expect(
clippy::format_collect,
reason = "readable hex/string formatting loop; refactoring to fold! obscures intent"
)]
fn to_sql_literal(&self) -> String {
let hex_str: String = self.iter().map(|b| format!("{b:02x}")).collect();
format!("E'\\\\x{hex_str}'")
}
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
reason = "an i128 spans at most ~39 decimal digits → ≤10 base-10000 groups; \
ndigits and weight always fit in i16"
)]
fn pg_numeric_encode_unscaled(unscaled: i128) -> Vec<u8> {
let sign_neg = unscaled < 0;
let mut mag = unscaled.unsigned_abs();
let mut groups: Vec<i16> = Vec::new();
while mag > 0 {
groups.push((mag % 10000) as i16);
mag /= 10000;
}
groups.reverse();
let ndigits = groups.len() as i16;
let weight = if groups.is_empty() { 0 } else { ndigits - 1 };
let mut buf = Vec::with_capacity(8 + groups.len() * 2);
buf.extend_from_slice(&ndigits.to_be_bytes());
buf.extend_from_slice(&weight.to_be_bytes());
buf.extend_from_slice(&(if sign_neg { 0x4000_i16 } else { 0 }).to_be_bytes());
buf.extend_from_slice(&0_i16.to_be_bytes()); for g in groups {
buf.extend_from_slice(&g.to_be_bytes());
}
buf
}
impl ToSqlParam for Numeric {
fn encode_param(&self) -> Option<Vec<u8>> {
if self.scale() == 0 {
return Some(pg_numeric_encode_unscaled(self.unscaled_value()));
}
let mut buf = pg_numeric_encode_unscaled(self.unscaled_value());
let dscale = i16::from(self.scale()).to_be_bytes();
buf[6] = dscale[0];
buf[7] = dscale[1];
Some(buf)
}
fn sql_oid(&self) -> Oid {
oids::NUMERIC
}
fn to_sql_literal(&self) -> String {
self.to_string()
} }
impl ToSqlParam for Interval {
fn encode_param(&self) -> Option<Vec<u8>> {
let mut buf = Vec::with_capacity(16);
buf.extend_from_slice(&self.microseconds().to_be_bytes());
buf.extend_from_slice(&self.days().to_be_bytes());
buf.extend_from_slice(&self.months().to_be_bytes());
Some(buf)
}
fn sql_oid(&self) -> Oid {
oids::INTERVAL
}
fn to_sql_literal(&self) -> String {
format!("INTERVAL '{self}'")
}
}
impl ToSqlParam for serde_json::Value {
fn encode_param(&self) -> Option<Vec<u8>> {
Some(self.to_string().into_bytes())
}
fn sql_oid(&self) -> Oid {
oids::JSON
}
fn to_sql_literal(&self) -> String {
format!("'{}'", self.to_string().replace('\'', "''"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_i32_encoding() {
assert_eq!(42i32.encode_param(), Some(vec![0, 0, 0, 42]));
assert_eq!((-1i32).encode_param(), Some(vec![255, 255, 255, 255]));
}
#[test]
fn test_i64_encoding() {
assert_eq!(42i64.encode_param(), Some(vec![0, 0, 0, 0, 0, 0, 0, 42]));
}
#[test]
fn test_string_encoding() {
assert_eq!("hello".encode_param(), Some(b"hello".to_vec()));
assert_eq!(
String::from("world").encode_param(),
Some(b"world".to_vec())
);
}
#[test]
fn test_bool_encoding() {
assert_eq!(true.encode_param(), Some(vec![1]));
assert_eq!(false.encode_param(), Some(vec![0]));
}
#[test]
fn test_option_encoding() {
assert_eq!(Some(42i32).encode_param(), Some(vec![0, 0, 0, 42]));
assert_eq!(None::<i32>.encode_param(), None);
}
#[test]
fn test_reference_encoding() {
let value = 42i32;
assert_eq!(value.encode_param(), Some(vec![0, 0, 0, 42]));
assert_eq!((&&value).encode_param(), Some(vec![0, 0, 0, 42]));
}
#[test]
fn test_pg_numeric_encode_unscaled() {
assert_eq!(
pg_numeric_encode_unscaled(42),
vec![0, 1, 0, 0, 0, 0, 0, 0, 0, 42]
);
assert_eq!(pg_numeric_encode_unscaled(0), vec![0, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(
pg_numeric_encode_unscaled(-1),
vec![0, 1, 0, 0, 0x40, 0, 0, 0, 0, 1]
);
assert_eq!(
pg_numeric_encode_unscaled(123_456_789),
vec![
0, 3, 0, 2, 0, 0, 0, 0, 0, 1, 9, 41, 26, 133 ]
);
}
#[test]
fn test_numeric_scale0_encode_param() {
assert_eq!(
Numeric::new(42, 0).encode_param(),
Some(vec![0, 1, 0, 0, 0, 0, 0, 0, 0, 42])
);
}
#[test]
fn test_numeric_scaled_sets_dscale_for_rejection() {
let bytes = Numeric::new(123, 2).encode_param().expect("some");
assert_eq!(&bytes[6..8], &[0, 2], "dscale must equal the true scale");
assert_ne!(&bytes[6..8], &[0, 0], "must not look like a whole number");
}
#[test]
fn test_interval_encoding() {
let interval = Interval::new(2, 5, 0);
assert_eq!(
interval.encode_param(),
Some(vec![
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 2 ])
);
}
#[test]
fn test_json_encoding() {
let json = serde_json::json!({"a": 1});
assert_eq!(json.encode_param(), Some(br#"{"a":1}"#.to_vec()));
}
}