pg-upsert 0.1.1

PostgreSQL UPSERT operations using sqlx
Documentation
use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use sqlx::Arguments;
use sqlx::postgres::PgArguments;

#[derive(Debug, Clone)]
pub enum FieldValue {
    Int32(i32),
    Int64(i64),
    Float32(f32),
    Float64(f64),
    Bool(bool),
    String(String),
    Numeric(String),
    Bytes(Vec<u8>),
    Date(NaiveDate),
    Time(NaiveTime),
    DateTime(NaiveDateTime),
    DateTimeUtc(DateTime<Utc>),
    Null,
}

impl FieldValue {
    pub fn bind_to(&self, args: &mut PgArguments) {
        match self {
            FieldValue::Int32(v) => args.add(v).unwrap(),
            FieldValue::Int64(v) => args.add(v).unwrap(),
            FieldValue::Float32(v) => args.add(v).unwrap(),
            FieldValue::Float64(v) => args.add(v).unwrap(),
            FieldValue::Bool(v) => args.add(v).unwrap(),
            FieldValue::String(v) => args.add(v).unwrap(),
            FieldValue::Numeric(v) => args.add(v).unwrap(),
            FieldValue::Bytes(v) => args.add(v).unwrap(),
            FieldValue::Date(v) => args.add(v).unwrap(),
            FieldValue::Time(v) => args.add(v).unwrap(),
            FieldValue::DateTime(v) => args.add(v).unwrap(),
            FieldValue::DateTimeUtc(v) => args.add(v).unwrap(),
            FieldValue::Null => args.add(None::<i32>).unwrap(),
        }
    }
}

impl From<i32> for FieldValue {
    fn from(v: i32) -> Self {
        FieldValue::Int32(v)
    }
}

impl From<i64> for FieldValue {
    fn from(v: i64) -> Self {
        FieldValue::Int64(v)
    }
}

impl From<f32> for FieldValue {
    fn from(v: f32) -> Self {
        FieldValue::Float32(v)
    }
}

impl From<f64> for FieldValue {
    fn from(v: f64) -> Self {
        FieldValue::Float64(v)
    }
}

impl From<bool> for FieldValue {
    fn from(v: bool) -> Self {
        FieldValue::Bool(v)
    }
}

impl From<String> for FieldValue {
    fn from(v: String) -> Self {
        FieldValue::String(v)
    }
}

impl From<&str> for FieldValue {
    fn from(v: &str) -> Self {
        FieldValue::String(v.to_owned())
    }
}

impl From<Vec<u8>> for FieldValue {
    fn from(v: Vec<u8>) -> Self {
        FieldValue::Bytes(v)
    }
}

impl From<NaiveDate> for FieldValue {
    fn from(v: NaiveDate) -> Self {
        FieldValue::Date(v)
    }
}

impl From<NaiveTime> for FieldValue {
    fn from(v: NaiveTime) -> Self {
        FieldValue::Time(v)
    }
}

impl From<NaiveDateTime> for FieldValue {
    fn from(v: NaiveDateTime) -> Self {
        FieldValue::DateTime(v)
    }
}

impl From<DateTime<Utc>> for FieldValue {
    fn from(v: DateTime<Utc>) -> Self {
        FieldValue::DateTimeUtc(v)
    }
}

impl<T: Into<FieldValue>> From<Option<T>> for FieldValue {
    fn from(v: Option<T>) -> Self {
        match v {
            Some(val) => val.into(),
            None => FieldValue::Null,
        }
    }
}

#[derive(Debug, Clone)]
pub struct Field {
    pub name: String,
    pub value: FieldValue,
}

impl Field {
    pub fn new(name: impl Into<String>, value: impl Into<FieldValue>) -> Self {
        Self {
            name: name.into(),
            value: value.into(),
        }
    }
}

#[derive(Debug, Clone, Default)]
pub struct UpsertOptions {
    pub version_field: Option<String>,
    pub do_nothing_on_conflict: bool,
}

impl UpsertOptions {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_version_field(mut self, field: impl Into<String>) -> Self {
        self.version_field = Some(field.into());
        self
    }

    pub fn with_do_nothing_on_conflict(mut self, do_nothing: bool) -> Self {
        self.do_nothing_on_conflict = do_nothing;
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_date_field_conversion() {
        let date = NaiveDate::from_ymd_opt(2025, 12, 26).unwrap();
        let field_value: FieldValue = date.into();
        assert!(matches!(field_value, FieldValue::Date(_)));
    }

    #[test]
    fn test_time_field_conversion() {
        let time = NaiveTime::from_hms_opt(14, 30, 0).unwrap();
        let field_value: FieldValue = time.into();
        assert!(matches!(field_value, FieldValue::Time(_)));
    }

    #[test]
    fn test_datetime_field_conversion() {
        let datetime = NaiveDate::from_ymd_opt(2025, 12, 26)
            .unwrap()
            .and_hms_opt(14, 30, 0)
            .unwrap();
        let field_value: FieldValue = datetime.into();
        assert!(matches!(field_value, FieldValue::DateTime(_)));
    }

    #[test]
    fn test_datetime_utc_field_conversion() {
        let datetime = DateTime::<Utc>::from_timestamp(1735225800, 0).unwrap();
        let field_value: FieldValue = datetime.into();
        assert!(matches!(field_value, FieldValue::DateTimeUtc(_)));
    }

    #[test]
    fn test_optional_date_field() {
        let some_date: Option<NaiveDate> = Some(NaiveDate::from_ymd_opt(2025, 12, 26).unwrap());
        let field_value: FieldValue = some_date.into();
        assert!(matches!(field_value, FieldValue::Date(_)));

        let none_date: Option<NaiveDate> = None;
        let field_value: FieldValue = none_date.into();
        assert!(matches!(field_value, FieldValue::Null));
    }

    #[test]
    fn test_numeric_field_from_string() {
        let numeric_value = String::from("123.456789");
        let field_value = FieldValue::Numeric(numeric_value.clone());
        assert!(matches!(field_value, FieldValue::Numeric(_)));
        if let FieldValue::Numeric(v) = field_value {
            assert_eq!(v, "123.456789");
        }
    }

    #[test]
    fn test_numeric_field_high_precision() {
        let high_precision = "99999999999999999999999999.999999999999";
        let field_value = FieldValue::Numeric(high_precision.to_string());
        if let FieldValue::Numeric(v) = field_value {
            assert_eq!(v, high_precision);
        }
    }
}