boxdb 0.3.1

An extended SQLite client with typed columns and readable storage
Documentation
use boxdb::{Database, Format, Value};
use std::error::Error as _;

#[test]
fn unique_errors_preserve_boxdb_codes_and_source() {
    let mut db = Database::try_memory().unwrap();
    db.execute("CREATE TABLE users(name TEXT UNIQUE); INSERT INTO users VALUES('Ada')")
        .unwrap();
    let error = db
        .execute_params("INSERT INTO users VALUES(?)", &[Value::Text("Ada".into())])
        .unwrap_err();

    assert_eq!(error.code(), "BOXDB_CONSTRAINT_UNIQUE");
    assert_eq!(error.result_code(), Some(19));
    assert_eq!(error.extended_code(), Some(2067));
    assert_eq!(error.to_string(), "UNIQUE constraint failed: users.name");
    assert!(error.source().unwrap().is::<rusqlite::Error>());
}

#[test]
fn sql_errors_preserve_primary_and_other_constraint_codes() {
    let mut db = Database::memory();
    db.execute("CREATE TABLE parents(id INTEGER PRIMARY KEY); CREATE TABLE children(id INTEGER PRIMARY KEY, parent INTEGER REFERENCES parents(id), name TEXT NOT NULL, n INTEGER CHECK(n > 0)); INSERT INTO children VALUES(1,NULL,'Ada',1)").unwrap();
    for (sql, name, primary, extended) in [
        (
            "INSERT INTO children VALUES(1,NULL,'Grace',1)",
            "BOXDB_CONSTRAINT_PRIMARYKEY",
            19,
            1555,
        ),
        (
            "INSERT INTO children VALUES(2,NULL,NULL,1)",
            "BOXDB_CONSTRAINT_NOTNULL",
            19,
            1299,
        ),
        (
            "INSERT INTO children VALUES(2,NULL,'Grace',0)",
            "BOXDB_CONSTRAINT_CHECK",
            19,
            275,
        ),
        (
            "INSERT INTO children VALUES(2,99,'Grace',1)",
            "BOXDB_CONSTRAINT_FOREIGNKEY",
            19,
            787,
        ),
        ("SELECT * FROM missing", "BOXDB_ERROR", 1, 1),
        ("SELECT FROM", "BOXDB_ERROR", 1, 1),
    ] {
        let error = db.execute(sql).unwrap_err();
        assert_eq!(error.code(), name, "{sql}");
        assert_eq!(error.result_code(), Some(primary), "{sql}");
        assert_eq!(error.extended_code(), Some(extended), "{sql}");
    }
}

#[test]
fn deferred_foreign_key_errors_have_the_same_codes_in_both_formats() {
    let temp = tempfile::tempdir().unwrap();
    for (name, format) in [("binary", Format::Binary), ("readable", Format::Readable)] {
        let mut db = Database::open(temp.path().join(name), format).unwrap();
        db.execute("CREATE TABLE parents(id INTEGER PRIMARY KEY); CREATE TABLE children(parent INTEGER REFERENCES parents(id) DEFERRABLE INITIALLY DEFERRED)").unwrap();
        let error = db.execute("INSERT INTO children VALUES(99)").unwrap_err();
        assert_eq!(error.code(), "BOXDB_CONSTRAINT_FOREIGNKEY", "{name}");
        assert_eq!(error.result_code(), Some(19), "{name}");
        assert_eq!(error.extended_code(), Some(787), "{name}");
        assert_eq!(error.to_string(), "FOREIGN KEY constraint failed");
    }
}

#[test]
fn preparation_opening_and_boxdb_validation_keep_distinct_codes() {
    let mut db = Database::memory();
    let error = db.statement_info("SELECT FROM").unwrap_err();
    assert_eq!(error.code(), "BOXDB_ERROR");
    assert_eq!(error.result_code(), Some(1));
    assert_eq!(error.extended_code(), Some(1));

    let error = db.statement_info("SELECT 1; SELECT 2").unwrap_err();
    assert_eq!(error.code(), "BOXDB_VALIDATION");
    assert_eq!(error.result_code(), None);
    assert_eq!(error.extended_code(), None);

    let temp = tempfile::tempdir().unwrap();
    let error = match Database::open(temp.path().join("missing/data.db"), Format::Binary) {
        Ok(_) => panic!("opening in a missing directory must fail"),
        Err(error) => error,
    };
    assert_eq!(error.code(), "BOXDB_CANTOPEN");
    assert_eq!(error.result_code(), Some(14));
    assert_eq!(error.extended_code(), Some(14));
}

#[test]
fn adding_context_preserves_result_codes_and_source() {
    let mut db = Database::memory();
    let error = db
        .statement_info("SELECT * FROM missing")
        .unwrap_err()
        .with_context("migration 42 up");
    assert_eq!(error.to_string(), "migration 42 up: no such table: missing");
    assert_eq!(error.code(), "BOXDB_ERROR");
    assert_eq!(error.result_code(), Some(1));
    assert_eq!(error.extended_code(), Some(1));
    assert!(error.source().unwrap().is::<rusqlite::Error>());
}

#[test]
fn validation_and_io_failures_keep_categories_and_original_sources() {
    let mut db = Database::memory();
    let validation = db.statement_info("SELECT 1; SELECT 2").unwrap_err();
    assert_eq!(validation.code(), "BOXDB_VALIDATION");
    assert_eq!(validation.result_code(), None);
    assert_eq!(validation.extended_code(), None);

    let temp = tempfile::tempdir().unwrap();
    let io = match Database::open(temp.path().join("missing/data.yaml"), Format::Readable) {
        Ok(_) => panic!("opening in a missing directory must fail"),
        Err(error) => error.with_context("open readable database"),
    };
    assert_eq!(io.code(), "BOXDB_IO");
    assert_eq!(io.result_code(), None);
    assert_eq!(
        io.source()
            .unwrap()
            .downcast_ref::<std::io::Error>()
            .unwrap()
            .kind(),
        std::io::ErrorKind::NotFound
    );
}

#[test]
fn invalid_json_casts_keep_the_parser_cause() {
    let error = boxdb::Type::Json.cast(Value::Text("{".into())).unwrap_err();
    assert_eq!(error.code(), "BOXDB_VALIDATION");
    assert!(error.source().unwrap().is::<serde_json::Error>());
}

#[test]
fn parameter_count_errors_are_validation_failures_with_the_original_source() {
    let mut db = Database::memory();
    let error = db.execute_native("SELECT ?", &[]).unwrap_err();
    assert_eq!(error.code(), "BOXDB_VALIDATION");
    assert_eq!(error.result_code(), None);
    assert!(error.source().unwrap().is::<rusqlite::Error>());
}

#[test]
fn readable_lock_errors_preserve_the_os_cause_without_engine_codes() {
    let temp = tempfile::tempdir().unwrap();
    let path = temp.path().join("data.yaml");
    let _writer = Database::open(&path, Format::Readable).unwrap();
    let error = match Database::open(&path, Format::Readable) {
        Ok(_) => panic!("a second readable writer must fail"),
        Err(error) => error,
    };
    assert_eq!(error.code(), "BOXDB_LOCKED");
    assert_eq!(error.result_code(), None);
    assert!(error.source().unwrap().is::<std::io::Error>());
}

#[test]
fn publication_failures_keep_io_sources_and_make_the_connection_unavailable() {
    let temp = tempfile::tempdir().unwrap();
    let path = temp.path().join("data.yaml");
    let mut db = Database::open(&path, Format::Readable).unwrap();
    db.execute("CREATE TABLE t(n INT64)").unwrap();
    std::fs::remove_file(&path).unwrap();
    std::fs::create_dir(&path).unwrap();
    let error = db.execute("INSERT INTO t VALUES(1)").unwrap_err();
    assert_eq!(error.code(), "BOXDB_IO");
    assert!(error.source().unwrap().is::<std::io::Error>());
    assert_eq!(
        db.execute("SELECT * FROM t").unwrap_err().code(),
        "BOXDB_UNAVAILABLE"
    );
}