a3s-orm 0.2.0

A type-safe, executor-neutral SQL query builder for Rust
Documentation
use a3s_orm::{DecodeError, FromRow, FromValue, Row, SqlArray, Value};

#[derive(Debug)]
struct Values(Vec<Value>);

impl Row for Values {
    fn value(&self, index: usize) -> Option<&Value> {
        self.0.get(index)
    }
}

#[test]
fn converts_primitive_values_and_reports_their_kinds() {
    let values = [
        Value::from(true),
        Value::from(-1_i8),
        Value::from(-2_i16),
        Value::from(-3_i32),
        Value::from(-4_i64),
        Value::from(-5_isize),
        Value::from(1_u8),
        Value::from(2_u16),
        Value::from(3_u32),
        Value::from(4_u64),
        Value::from(5_usize),
        Value::from(1.25_f32),
        Value::from(2.5_f64),
        Value::from("borrowed"),
        Value::from(String::from("owned")),
        Value::from(vec![1_u8, 2]),
        Value::from(None::<i64>),
        Value::from(Some(7_i64)),
        Value::from(SqlArray::from(vec![1_i32, 2])),
    ];
    let kinds: Vec<_> = values.iter().map(Value::kind).collect();
    assert_eq!(
        kinds,
        [
            "boolean",
            "signed integer",
            "signed integer",
            "signed integer",
            "signed integer",
            "signed integer",
            "unsigned integer",
            "unsigned integer",
            "unsigned integer",
            "unsigned integer",
            "unsigned integer",
            "floating-point number",
            "floating-point number",
            "string",
            "string",
            "bytes",
            "null",
            "signed integer",
            "array",
        ]
    );
}

#[test]
fn decodes_all_core_scalar_representations() {
    assert!(bool::from_value(&Value::Bool(true), 0).unwrap());
    assert!(!bool::from_value(&Value::I64(0), 0).unwrap());
    assert!(bool::from_value(&Value::I64(1), 0).unwrap());

    macro_rules! signed {
        ($($ty:ty),+ $(,)?) => {$(
            assert_eq!(<$ty>::from_value(&Value::I64(7), 0).unwrap(), 7);
        )+};
    }
    macro_rules! unsigned {
        ($($ty:ty),+ $(,)?) => {$(
            assert_eq!(<$ty>::from_value(&Value::U64(7), 0).unwrap(), 7);
            assert_eq!(<$ty>::from_value(&Value::I64(7), 0).unwrap(), 7);
        )+};
    }
    signed!(i8, i16, i32, i64, isize);
    unsigned!(u8, u16, u32, u64, usize);

    assert_eq!(f64::from_value(&Value::F64(1.5), 0).unwrap(), 1.5);
    assert_eq!(f64::from_value(&Value::I64(2), 0).unwrap(), 2.0);
    assert_eq!(f32::from_value(&Value::F64(1.5), 0).unwrap(), 1.5);
    assert_eq!(String::from_value(&Value::from("text"), 0).unwrap(), "text");
    assert_eq!(
        Vec::<u8>::from_value(&Value::Bytes(vec![1, 2]), 0).unwrap(),
        [1, 2]
    );
    assert_eq!(Option::<i64>::from_value(&Value::Null, 0).unwrap(), None);
    assert_eq!(
        Option::<i64>::from_value(&Value::I64(9), 0).unwrap(),
        Some(9)
    );
    assert_eq!(
        SqlArray::<i32>::from_value(&Value::Array(vec![Value::I64(1), Value::I64(2)]), 0).unwrap(),
        SqlArray(vec![1, 2])
    );
}

#[test]
fn decoding_returns_contextual_errors() {
    assert_eq!(
        bool::from_value(&Value::String("no".into()), 3),
        Err(DecodeError::TypeMismatch {
            index: 3,
            expected: "boolean",
            actual: "string",
        })
    );
    assert_eq!(
        i8::from_value(&Value::I64(128), 4),
        Err(DecodeError::IntegerOverflow {
            index: 4,
            target: "i8"
        })
    );
    assert_eq!(
        u8::from_value(&Value::I64(-1), 5),
        Err(DecodeError::IntegerOverflow {
            index: 5,
            target: "u8"
        })
    );
    assert!(matches!(
        SqlArray::<i32>::from_value(
            &Value::Array(vec![Value::I64(1), Value::String("bad".into())]),
            6
        ),
        Err(DecodeError::ArrayElement {
            index: 6,
            element: 1,
            ..
        })
    ));
    assert!(matches!(
        SqlArray::<i32>::from_value(&Value::I64(1), 7),
        Err(DecodeError::TypeMismatch { index: 7, .. })
    ));
    assert_eq!(
        i64::from_row(&Values(vec![])),
        Err(DecodeError::MissingColumn { index: 0 })
    );
}

#[test]
fn decodes_supported_tuple_arities() {
    let row = Values(vec![
        Value::I64(1),
        Value::I64(2),
        Value::I64(3),
        Value::I64(4),
        Value::I64(5),
        Value::I64(6),
        Value::I64(7),
        Value::I64(8),
    ]);
    assert_eq!(<(i64, i64)>::from_row(&row).unwrap(), (1, 2));
    assert_eq!(<(i64, i64, i64)>::from_row(&row).unwrap(), (1, 2, 3));
    assert_eq!(
        <(i64, i64, i64, i64)>::from_row(&row).unwrap(),
        (1, 2, 3, 4)
    );
    assert_eq!(
        <(i64, i64, i64, i64, i64)>::from_row(&row).unwrap(),
        (1, 2, 3, 4, 5)
    );
    assert_eq!(
        <(i64, i64, i64, i64, i64, i64)>::from_row(&row).unwrap(),
        (1, 2, 3, 4, 5, 6)
    );
    assert_eq!(
        <(i64, i64, i64, i64, i64, i64, i64)>::from_row(&row).unwrap(),
        (1, 2, 3, 4, 5, 6, 7)
    );
    assert_eq!(
        <(i64, i64, i64, i64, i64, i64, i64, i64)>::from_row(&row).unwrap(),
        (1, 2, 3, 4, 5, 6, 7, 8)
    );
}

#[cfg(all(
    feature = "uuid",
    feature = "json",
    feature = "chrono",
    feature = "decimal"
))]
#[test]
fn converts_and_decodes_feature_gated_values() {
    let uuid = uuid::Uuid::nil();
    let json = serde_json::json!({"ok": true});
    let date = chrono::NaiveDate::from_ymd_opt(2026, 7, 12).unwrap();
    let time = chrono::NaiveTime::from_hms_opt(10, 20, 30).unwrap();
    let datetime = date.and_time(time);
    let utc = datetime.and_utc();
    let decimal = rust_decimal::Decimal::new(1234, 2);

    macro_rules! round_trip {
        ($value:expr, $ty:ty, $kind:literal) => {{
            let encoded = Value::from($value.clone());
            assert_eq!(encoded.kind(), $kind);
            assert_eq!(<$ty>::from_value(&encoded, 0).unwrap(), $value);
        }};
    }
    round_trip!(uuid, uuid::Uuid, "uuid");
    round_trip!(json, serde_json::Value, "json");
    round_trip!(date, chrono::NaiveDate, "date");
    round_trip!(time, chrono::NaiveTime, "time");
    round_trip!(datetime, chrono::NaiveDateTime, "timestamp");
    round_trip!(
        utc,
        chrono::DateTime<chrono::Utc>,
        "timestamp with time zone"
    );
    round_trip!(decimal, rust_decimal::Decimal, "decimal");
}