sqlx-odbc 0.0.3

ODBC driver implementation for SQLx.
Documentation
use crate::value::OdbcValueRef;
use crate::{DataTypeExt, Odbc, OdbcArgumentValue, OdbcTypeInfo};
use serde::de::DeserializeOwned;
use serde::Serialize;
use sqlx_core::decode::Decode;
use sqlx_core::encode::{Encode, IsNull};
use sqlx_core::error::BoxDynError;
use sqlx_core::types::{Json, Type};

impl<T: ?Sized> Type<Odbc> for Json<T> {
    fn type_info() -> OdbcTypeInfo {
        OdbcTypeInfo::varchar(None)
    }

    fn compatible(ty: &OdbcTypeInfo) -> bool {
        ty.data_type().accepts_character_data()
            || ty.data_type().accepts_numeric_data()
            || ty.data_type().accepts_binary_data()
            || matches!(
                ty.data_type(),
                odbc_api::DataType::Other { .. } | odbc_api::DataType::Unknown
            )
    }
}

impl<'q, T> Encode<'q, Odbc> for Json<T>
where
    T: Serialize + ?Sized,
{
    fn encode_by_ref(&self, buf: &mut Vec<OdbcArgumentValue>) -> Result<IsNull, BoxDynError> {
        buf.push(OdbcArgumentValue::Text(serde_json::to_string(self)?));
        Ok(IsNull::No)
    }
}

impl<'r, T> Decode<'r, Odbc> for Json<T>
where
    T: DeserializeOwned,
{
    fn decode(value: OdbcValueRef<'r>) -> Result<Self, BoxDynError> {
        if let Some(text) = value.as_str() {
            return serde_json::from_str(text.trim()).map_err(Into::into);
        }

        if let Some(bytes) = value.as_bytes() {
            return serde_json::from_slice(bytes).map_err(Into::into);
        }

        if let Some(integer) = value.as_i64() {
            return serde_json::from_value(serde_json::Value::from(integer)).map_err(Into::into);
        }

        if let Some(float) = value.as_f64() {
            return serde_json::from_value(serde_json::Value::from(float)).map_err(Into::into);
        }

        Err("ODBC: cannot decode JSON".into())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{OdbcValue, OdbcValueKind};
    use serde_json::{json, Value as JsonValue};
    use sqlx_core::type_info::TypeInfo;
    use sqlx_core::value::Value;

    #[test]
    fn json_type_compatibility_matches_old_odbc() {
        assert!(<Json<JsonValue> as Type<Odbc>>::compatible(
            &OdbcTypeInfo::varchar(None)
        ));
        assert!(<Json<JsonValue> as Type<Odbc>>::compatible(
            &OdbcTypeInfo::char(None)
        ));
        assert!(<Json<JsonValue> as Type<Odbc>>::compatible(
            &OdbcTypeInfo::INTEGER
        ));
        assert!(<Json<JsonValue> as Type<Odbc>>::compatible(
            &OdbcTypeInfo::varbinary(None)
        ));
    }

    #[test]
    fn json_value_decodes_old_odbc_forms() -> Result<(), BoxDynError> {
        for (value, expected) in [
            (
                OdbcValue::new(OdbcValueKind::Text(
                    r#"{"name":"test","value":42}"#.to_owned(),
                )),
                json!({"name": "test", "value": 42}),
            ),
            (
                OdbcValue::new(OdbcValueKind::Binary(br#""hello""#.to_vec())),
                json!("hello"),
            ),
            (OdbcValue::new(OdbcValueKind::BigInt(42)), json!(42)),
            (OdbcValue::new(OdbcValueKind::Double(3.5)), json!(3.5)),
        ] {
            assert_eq!(
                <JsonValue as Decode<Odbc>>::decode(value.as_ref())?,
                expected
            );
        }

        Ok(())
    }

    #[test]
    fn json_value_rejects_invalid_text() {
        let value = OdbcValue::new(OdbcValueKind::Text(r#"{"invalid": json,}"#.to_owned()));

        assert!(<JsonValue as Decode<Odbc>>::decode(value.as_ref()).is_err());
    }

    #[test]
    fn json_encodes_as_text() -> Result<(), BoxDynError> {
        let mut buf = Vec::new();
        let value = json!({"name": "test"});

        let result = <JsonValue as Encode<Odbc>>::encode(value, &mut buf)?;

        assert!(matches!(result, IsNull::No));
        let [OdbcArgumentValue::Text(text)] = &buf[..] else {
            panic!("expected one text argument");
        };
        assert_eq!(
            serde_json::from_str::<JsonValue>(text)?,
            json!({"name": "test"})
        );
        Ok(())
    }

    #[test]
    fn json_type_info_name_matches_old_odbc() {
        assert_eq!(
            <Json<JsonValue> as Type<Odbc>>::type_info().name(),
            "VARCHAR"
        );
    }
}