khive-storage 0.12.0

Storage capability contracts and backend-neutral request context.
Documentation
use khive_storage::types::{SqlColumn, SqlColumnError, SqlRow, SqlValue};

fn row(value: SqlValue) -> SqlRow {
    SqlRow {
        columns: vec![SqlColumn {
            name: "value".into(),
            value,
        }],
    }
}

fn absent() -> SqlRow {
    SqlRow { columns: vec![] }
}

fn refusal<T: std::fmt::Debug>(result: Result<T, SqlColumnError>, found: Option<&'static str>) {
    let error = result.expect_err("typed column must be refused");
    assert_eq!(error.column, "value");
    assert_eq!(error.found, found);
}

fn variants() -> Vec<(SqlValue, &'static str)> {
    vec![
        (SqlValue::Null, "Null"),
        (SqlValue::Bool(true), "Bool"),
        (SqlValue::Integer(7), "Integer"),
        (SqlValue::Float(7.5), "Float"),
        (SqlValue::Text("text".into()), "Text"),
        (SqlValue::Blob(vec![1, 2]), "Blob"),
        (SqlValue::Json(serde_json::json!({"key": 1})), "Json"),
        (SqlValue::Uuid(uuid::Uuid::nil()), "Uuid"),
        (
            SqlValue::Timestamp(chrono::DateTime::from_timestamp_micros(0).unwrap()),
            "Timestamp",
        ),
    ]
}

#[test]
fn required_text_accepts_text() {
    assert_eq!(
        row(SqlValue::Text("text".into())).text("value").unwrap(),
        "text"
    );
    assert_eq!(
        row(SqlValue::Text(String::new())).text("value").unwrap(),
        ""
    );
}

#[test]
fn required_text_rejects_null() {
    refusal(row(SqlValue::Null).text("value"), Some("Null"));
}
#[test]
fn required_text_rejects_absence() {
    refusal(absent().text("value"), None);
}
#[test]
fn required_text_rejects_other_variants() {
    for (value, name) in variants() {
        if name != "Text" && name != "Null" {
            refusal(row(value).text("value"), Some(name));
        }
    }
}

#[test]
fn required_i64_accepts_integer() {
    for value in [i64::MIN, 0, i64::MAX] {
        assert_eq!(row(SqlValue::Integer(value)).i64("value").unwrap(), value);
    }
}
#[test]
fn required_i64_rejects_null() {
    refusal(row(SqlValue::Null).i64("value"), Some("Null"));
}
#[test]
fn required_i64_rejects_absence() {
    refusal(absent().i64("value"), None);
}
#[test]
fn required_i64_rejects_float() {
    refusal(row(SqlValue::Float(7.5)).i64("value"), Some("Float"));
}
#[test]
fn required_i64_rejects_other_variants() {
    for (value, name) in variants() {
        if !matches!(name, "Integer" | "Null" | "Float") {
            refusal(row(value).i64("value"), Some(name));
        }
    }
}

#[test]
fn strict_optional_text_accepts_text_and_null() {
    assert_eq!(
        row(SqlValue::Text("text".into()))
            .opt_text("value")
            .unwrap(),
        Some("text")
    );
    assert_eq!(row(SqlValue::Null).opt_text("value").unwrap(), None);
}
#[test]
fn strict_optional_text_rejects_absence() {
    refusal(absent().opt_text("value"), None);
}
#[test]
fn strict_optional_text_rejects_other_variants() {
    for (value, name) in variants() {
        if !matches!(name, "Text" | "Null") {
            refusal(row(value).opt_text("value"), Some(name));
        }
    }
}

#[test]
fn strict_optional_i64_accepts_integer_and_null() {
    assert_eq!(row(SqlValue::Integer(7)).opt_i64("value").unwrap(), Some(7));
    assert_eq!(row(SqlValue::Null).opt_i64("value").unwrap(), None);
}
#[test]
fn strict_optional_i64_rejects_absence() {
    refusal(absent().opt_i64("value"), None);
}
#[test]
fn strict_optional_i64_rejects_float() {
    refusal(row(SqlValue::Float(7.5)).opt_i64("value"), Some("Float"));
}
#[test]
fn strict_optional_i64_rejects_other_variants() {
    for (value, name) in variants() {
        if !matches!(name, "Integer" | "Null" | "Float") {
            refusal(row(value).opt_i64("value"), Some(name));
        }
    }
}

#[test]
fn absent_tolerant_text_accepts_text_null_and_absence() {
    assert_eq!(
        row(SqlValue::Text("text".into()))
            .opt_text_or_absent("value")
            .unwrap(),
        Some("text")
    );
    assert_eq!(
        row(SqlValue::Null).opt_text_or_absent("value").unwrap(),
        None
    );
    assert_eq!(absent().opt_text_or_absent("value").unwrap(), None);
}
#[test]
fn absent_tolerant_text_rejects_other_variants() {
    for (value, name) in variants() {
        if !matches!(name, "Text" | "Null") {
            refusal(row(value).opt_text_or_absent("value"), Some(name));
        }
    }
}
#[test]
fn absent_tolerant_i64_accepts_integer_null_and_absence() {
    assert_eq!(
        row(SqlValue::Integer(7))
            .opt_i64_or_absent("value")
            .unwrap(),
        Some(7)
    );
    assert_eq!(
        row(SqlValue::Null).opt_i64_or_absent("value").unwrap(),
        None
    );
    assert_eq!(absent().opt_i64_or_absent("value").unwrap(), None);
}
#[test]
fn absent_tolerant_i64_rejects_float() {
    refusal(
        row(SqlValue::Float(7.5)).opt_i64_or_absent("value"),
        Some("Float"),
    );
}
#[test]
fn absent_tolerant_i64_rejects_other_variants() {
    for (value, name) in variants() {
        if !matches!(name, "Integer" | "Null" | "Float") {
            refusal(row(value).opt_i64_or_absent("value"), Some(name));
        }
    }
}

#[test]
fn accessors_preserve_first_exact_column_lookup() {
    let row = SqlRow {
        columns: vec![
            SqlColumn {
                name: "Value".into(),
                value: SqlValue::Text("case".into()),
            },
            SqlColumn {
                name: "value".into(),
                value: SqlValue::Text("first".into()),
            },
            SqlColumn {
                name: "value".into(),
                value: SqlValue::Integer(2),
            },
        ],
    };
    assert_eq!(row.text("value").unwrap(), "first");
    assert_eq!(row.text("Value").unwrap(), "case");
    assert_eq!(row.i64("value").unwrap_err().found, Some("Text"));
    assert!(row.text("VALUE").is_err());
}

#[test]
fn column_error_is_an_owned_standard_error() {
    let error = {
        let name = "value".to_owned();
        absent().text(&name).unwrap_err()
    };
    assert_eq!(error.column, "value");
    assert_eq!(error.to_string(), "SQL column value is absent");
    let _: &dyn std::error::Error = &error;
    assert_eq!(
        row(SqlValue::Float(2.5))
            .i64("value")
            .unwrap_err()
            .to_string(),
        "SQL column value has value Float"
    );
}

#[test]
fn uuid_accessors_accept_native_and_text_representations() {
    let expected = uuid::Uuid::from_u128(0x550e8400e29b41d4a716446655440000);
    for value in [expected, uuid::Uuid::nil()] {
        let row = row(SqlValue::Uuid(value));
        assert_eq!(row.uuid("value").unwrap(), value);
        assert_eq!(row.opt_uuid("value").unwrap(), Some(value));
    }
    for text in [
        "550e8400-e29b-41d4-a716-446655440000",
        "550E8400-E29B-41D4-A716-446655440000",
        "550e8400e29b41d4a716446655440000",
        "urn:uuid:550e8400-e29b-41d4-a716-446655440000",
        "{550e8400-e29b-41d4-a716-446655440000}",
    ] {
        let row = row(SqlValue::Text(text.into()));
        assert_eq!(row.uuid("value").unwrap(), expected);
        assert_eq!(row.opt_uuid("value").unwrap(), Some(expected));
    }
}

#[test]
fn uuid_accessors_distinguish_null_absence_and_invalid_values() {
    refusal(row(SqlValue::Null).uuid("value"), Some("Null"));
    assert_eq!(row(SqlValue::Null).opt_uuid("value").unwrap(), None);
    refusal(absent().uuid("value"), None);
    refusal(absent().opt_uuid("value"), None);
    for (value, name) in variants() {
        if !matches!(name, "Uuid" | "Null") {
            let row = row(value);
            refusal(row.uuid("value"), Some(name));
            refusal(row.opt_uuid("value"), Some(name));
        }
    }
    for text in ["", "550e8400-e29b-41d4-a716-44665544000z"] {
        let row = row(SqlValue::Text(text.into()));
        refusal(row.uuid("value"), Some("Text"));
        refusal(row.opt_uuid("value"), Some("Text"));
    }
}

#[test]
fn f64_accessors_preserve_floats_and_round_integers() {
    for value in [
        0.0,
        -0.0,
        7.5,
        f64::INFINITY,
        f64::NEG_INFINITY,
        f64::from_bits(0x7ff8000000000042),
    ] {
        let row = row(SqlValue::Float(value));
        assert_eq!(row.f64("value").unwrap().to_bits(), value.to_bits());
        assert_eq!(
            row.opt_f64("value").unwrap().unwrap().to_bits(),
            value.to_bits()
        );
    }
    for (value, expected) in [
        (0, 0.0),
        (-7, -7.0),
        (9_007_199_254_740_993, 9_007_199_254_740_992.0),
        (i64::MIN, -9_223_372_036_854_775_808.0),
        (i64::MAX, 9_223_372_036_854_775_808.0),
    ] {
        let row = row(SqlValue::Integer(value));
        assert_eq!(row.f64("value").unwrap(), expected);
        assert_eq!(row.opt_f64("value").unwrap(), Some(expected));
    }
}

#[test]
fn f64_accessors_distinguish_null_absence_and_wrong_variants() {
    refusal(row(SqlValue::Null).f64("value"), Some("Null"));
    assert_eq!(row(SqlValue::Null).opt_f64("value").unwrap(), None);
    refusal(absent().f64("value"), None);
    refusal(absent().opt_f64("value"), None);
    for (value, name) in variants() {
        if !matches!(name, "Float" | "Integer" | "Null") {
            let row = row(value);
            refusal(row.f64("value"), Some(name));
            refusal(row.opt_f64("value"), Some(name));
        }
    }
    refusal(row(SqlValue::Text("7.5".into())).f64("value"), Some("Text"));
    refusal(
        row(SqlValue::Text("7.5".into())).opt_f64("value"),
        Some("Text"),
    );
}

#[test]
fn lenient_accessors_preserve_values_and_ignore_all_other_variants() {
    for text in ["text", ""] {
        let row = row(SqlValue::Text(text.into()));
        assert_eq!(row.text_or_none("value"), Some(text));
    }
    for value in [i64::MIN, 0, i64::MAX] {
        assert_eq!(
            row(SqlValue::Integer(value)).i64_or_none("value"),
            Some(value)
        );
    }
    assert_eq!(absent().text_or_none("value"), None);
    assert_eq!(absent().i64_or_none("value"), None);
    for (value, name) in variants() {
        let row = row(value);
        if name != "Text" {
            assert_eq!(row.text_or_none("value"), None);
        }
        if name != "Integer" {
            assert_eq!(row.i64_or_none("value"), None);
        }
    }
}

#[test]
fn new_accessors_keep_first_exact_column_lookup() {
    let row = SqlRow {
        columns: vec![
            SqlColumn {
                name: "Value".into(),
                value: SqlValue::Uuid(uuid::Uuid::nil()),
            },
            SqlColumn {
                name: "value".into(),
                value: SqlValue::Bool(true),
            },
            SqlColumn {
                name: "value".into(),
                value: SqlValue::Text("550e8400-e29b-41d4-a716-446655440000".into()),
            },
            SqlColumn {
                name: "value".into(),
                value: SqlValue::Integer(7),
            },
        ],
    };
    assert_eq!(row.uuid("Value").unwrap(), uuid::Uuid::nil());
    refusal(row.uuid("value"), Some("Bool"));
    refusal(row.opt_uuid("value"), Some("Bool"));
    refusal(row.f64("value"), Some("Bool"));
    refusal(row.opt_f64("value"), Some("Bool"));
    assert_eq!(row.text_or_none("value"), None);
    assert_eq!(row.i64_or_none("value"), None);
    assert_eq!(row.uuid("VALUE").unwrap_err().found, None);
    assert_eq!(row.opt_uuid("VALUE").unwrap_err().found, None);
    assert_eq!(row.f64("VALUE").unwrap_err().found, None);
    assert_eq!(row.opt_f64("VALUE").unwrap_err().found, None);
    assert_eq!(row.text_or_none("VALUE"), None);
    assert_eq!(row.i64_or_none("VALUE"), None);
}