mongreldb-kit 0.58.2

Schema-aware, storage-backed application persistence layer for MongrelDB with migrations, relational constraints, and a query builder.
Documentation
use mongreldb_kit::{Column, ColumnType, Database, Schema, Table};

fn database() -> (tempfile::TempDir, Database) {
    let directory = tempfile::tempdir().unwrap();
    let schema = Schema::new(vec![Table {
        id: 1,
        name: "items".into(),
        columns: vec![Column::new(1, "id", ColumnType::Int64)],
        primary_key: vec!["id".into()],
        indexes: vec![],
        foreign_keys: vec![],
        unique_constraints: vec![],
        check_constraints: vec![],
    }])
    .unwrap();
    let db = Database::create(directory.path(), schema).unwrap();
    (directory, db)
}

fn ids(db: &Database) -> Vec<i64> {
    db.sql_rows("SELECT id FROM items ORDER BY id")
        .unwrap()
        .into_iter()
        .map(|row| row["id"].as_i64().unwrap())
        .collect()
}

#[test]
fn sql_savepoint_rolls_back_only_later_writes() {
    let (_directory, db) = database();

    db.sql("BEGIN").unwrap();
    db.sql("INSERT INTO items VALUES (1)").unwrap();
    db.sql("SAVEPOINT sp1").unwrap();
    db.sql("INSERT INTO items VALUES (2)").unwrap();
    db.sql("ROLLBACK TO sp1").unwrap();
    db.sql("COMMIT").unwrap();

    assert_eq!(ids(&db), vec![1]);
}

#[test]
fn sql_nested_savepoint_retains_target_and_removes_later_savepoints() {
    let (_directory, db) = database();

    db.sql("BEGIN").unwrap();
    db.sql("INSERT INTO items VALUES (1)").unwrap();
    db.sql("SAVEPOINT sp1").unwrap();
    db.sql("INSERT INTO items VALUES (2)").unwrap();
    db.sql("SAVEPOINT sp2").unwrap();
    db.sql("INSERT INTO items VALUES (3)").unwrap();
    db.sql("ROLLBACK TO sp1").unwrap();
    db.sql("INSERT INTO items VALUES (4)").unwrap();
    db.sql("ROLLBACK TO sp1").unwrap();
    db.sql("INSERT INTO items VALUES (4)").unwrap();
    db.sql("COMMIT").unwrap();

    assert_eq!(ids(&db), vec![1, 4]);

    db.sql("BEGIN").unwrap();
    db.sql("SAVEPOINT outer").unwrap();
    db.sql("SAVEPOINT inner").unwrap();
    db.sql("ROLLBACK TO outer").unwrap();
    let error = db.sql("ROLLBACK TO inner").unwrap_err();
    assert!(error.to_string().contains("no savepoint named 'inner'"));
    db.sql("ROLLBACK").unwrap();
}

#[test]
fn sql_release_removes_target_and_nested_savepoints() {
    let (_directory, db) = database();

    for name in ["outer", "inner"] {
        db.sql("BEGIN").unwrap();
        db.sql("SAVEPOINT outer").unwrap();
        db.sql("SAVEPOINT inner").unwrap();
        db.sql("RELEASE SAVEPOINT outer").unwrap();
        let error = db.sql(&format!("ROLLBACK TO {name}")).unwrap_err();
        assert!(
            error
                .to_string()
                .contains(&format!("no savepoint named '{name}'")),
            "{error}"
        );
        db.sql("ROLLBACK").unwrap();
    }
}

#[test]
fn sql_rollback_to_recovers_an_aborted_transaction() {
    let (_directory, db) = database();

    db.sql("BEGIN").unwrap();
    db.sql("INSERT INTO items VALUES (1)").unwrap();
    db.sql("SAVEPOINT stable").unwrap();
    assert!(db
        .sql("INSERT INTO items VALUES (2); SELECT * FROM missing_table")
        .is_err());
    assert!(db
        .sql("COMMIT")
        .unwrap_err()
        .to_string()
        .contains("aborted"));

    db.sql("ROLLBACK TO stable").unwrap();
    db.sql("INSERT INTO items VALUES (3)").unwrap();
    db.sql("COMMIT").unwrap();

    assert_eq!(ids(&db), vec![1, 3]);
}