sql-orm 0.1.0

Public API crate for the sql-orm workspace.
Documentation
use std::collections::BTreeMap;

use sql_orm::prelude::*;

#[allow(dead_code)]
#[derive(Entity, Debug, Clone, PartialEq)]
#[orm(table = "customers", schema = "sales")]
struct Customer {
    #[orm(primary_key)]
    #[orm(identity)]
    id: i64,
    email: String,
    #[orm(nullable)]
    phone: Option<String>,
    active: bool,
}

#[derive(Insertable, Debug, Clone)]
#[orm(entity = Customer)]
struct NewCustomer {
    email: String,
    phone: Option<String>,
    active: bool,
}

#[derive(Changeset, Debug, Clone)]
#[orm(entity = Customer)]
struct UpdateCustomer {
    email: Option<String>,
    phone: Option<Option<String>>,
    active: Option<bool>,
}

struct TestRow {
    values: BTreeMap<&'static str, SqlValue>,
}

impl Row for TestRow {
    fn try_get(&self, column: &str) -> Result<Option<SqlValue>, OrmError> {
        Ok(self.values.get(column).cloned())
    }
}

#[derive(Debug, PartialEq)]
struct CustomerRecord {
    id: i64,
    email: String,
    phone: Option<String>,
    active: bool,
}

impl FromRow for CustomerRecord {
    fn from_row<R: Row>(row: &R) -> Result<Self, OrmError> {
        Ok(Self {
            id: row.get_required_typed::<i64>("id")?,
            email: row.get_required_typed::<String>("email")?,
            phone: row.try_get_typed::<Option<String>>("phone")?.flatten(),
            active: row.get_required_typed::<bool>("active")?,
        })
    }
}

#[test]
fn from_row_maps_required_and_nullable_columns() {
    let row = TestRow {
        values: BTreeMap::from([
            ("id", SqlValue::I64(7)),
            ("email", SqlValue::String("ana@example.com".to_string())),
            ("phone", SqlValue::Null),
            ("active", SqlValue::Bool(true)),
        ]),
    };

    let record = CustomerRecord::from_row(&row).expect("row mapping should succeed");

    assert_eq!(
        record,
        CustomerRecord {
            id: 7,
            email: "ana@example.com".to_string(),
            phone: None,
            active: true,
        }
    );
}

#[test]
fn entity_derive_generates_from_row_for_required_and_nullable_columns() {
    let row = TestRow {
        values: BTreeMap::from([
            ("id", SqlValue::I64(7)),
            ("email", SqlValue::String("ana@example.com".to_string())),
            ("phone", SqlValue::Null),
            ("active", SqlValue::Bool(true)),
        ]),
    };

    let customer = Customer::from_row(&row).expect("derived entity row mapping should succeed");

    assert_eq!(
        customer,
        Customer {
            id: 7,
            email: "ana@example.com".to_string(),
            phone: None,
            active: true,
        }
    );
}

#[test]
fn from_row_reports_missing_required_columns() {
    let row = TestRow {
        values: BTreeMap::from([("email", SqlValue::String("ana@example.com".to_string()))]),
    };

    let error = CustomerRecord::from_row(&row).expect_err("missing id must fail");

    assert_eq!(error.message(), "required column value was not present");
}

#[test]
fn from_row_reports_type_mismatches() {
    let row = TestRow {
        values: BTreeMap::from([
            ("id", SqlValue::String("7".to_string())),
            ("email", SqlValue::String("ana@example.com".to_string())),
            ("active", SqlValue::Bool(true)),
        ]),
    };

    let error = CustomerRecord::from_row(&row).expect_err("invalid id type must fail");

    assert_eq!(error.message(), "expected i64 value");
}

#[test]
fn insertable_extracts_persistible_values_in_field_order() {
    let new_customer = NewCustomer {
        email: "ana@example.com".to_string(),
        phone: None,
        active: true,
    };

    let values = <NewCustomer as Insertable<Customer>>::values(&new_customer);

    assert_eq!(
        values,
        vec![
            ColumnValue::new("email", SqlValue::String("ana@example.com".to_string())),
            ColumnValue::new("phone", SqlValue::TypedNull(SqlServerType::NVarChar)),
            ColumnValue::new("active", SqlValue::Bool(true)),
        ]
    );
}

#[test]
fn changeset_extracts_only_present_changes_and_preserves_nulls() {
    let update = UpdateCustomer {
        email: None,
        phone: Some(None),
        active: Some(false),
    };

    let changes = <UpdateCustomer as Changeset<Customer>>::changes(&update);

    assert_eq!(
        changes,
        vec![
            ColumnValue::new("phone", SqlValue::TypedNull(SqlServerType::NVarChar)),
            ColumnValue::new("active", SqlValue::Bool(false)),
        ]
    );
}