dactyl-db 0.8.2

Interchangeably read and write to a lightweight Rust local store or cloud-hosted Vercel Neon instances behind one facade.
Documentation
#![cfg(feature = "sqlite")]

use dactyl_db::{
    AdapterErrorKind, Connection, Datastore, DatastoreRoute, Parameter, StorageContext,
};
use serde_json::json;
use tempfile::NamedTempFile;

#[test]
fn sqlite_read_write_is_the_only_driver_contract() {
    let file = NamedTempFile::new().expect("temp sqlite file");
    let db = Connection::open(DatastoreRoute::sqlite(file.path().to_string_lossy())).unwrap();
    assert_eq!(db.datastore(), Datastore::Sqlite);

    db.write(
        "create table app (id integer primary key, name text, enabled integer, payload blob)",
        &[],
    )
    .unwrap();
    assert_eq!(
        db.write(
            "insert into app (name, enabled, payload) values ($1, $2, $3)",
            &[
                Parameter::Text("opened".into()),
                Parameter::Bool(true),
                Parameter::Blob(vec![1, 2, 3]),
            ],
        )
        .unwrap(),
        1
    );

    let rows = db
        .read("select id, name, enabled, payload from app", &[])
        .unwrap();
    let row = &rows.as_slice()[0];
    assert_eq!(row.get_int("id").unwrap(), 1);
    assert_eq!(row.get_str("name").unwrap(), "opened");
    assert!(row.get_bool("enabled").unwrap());
    assert_eq!(
        row.get_json("payload").unwrap(),
        serde_json::json!([1, 2, 3])
    );
}

#[test]
fn sqlite_errors_are_typed_without_string_parsing() {
    let file = NamedTempFile::new().expect("temp sqlite file");
    let db = Connection::open(DatastoreRoute::sqlite(file.path().to_string_lossy())).unwrap();
    db.write(
        "create table app (id integer primary key, name text unique)",
        &[],
    )
    .unwrap();
    db.write(
        "insert into app (name) values ($1)",
        &[Parameter::Text("a".into())],
    )
    .unwrap();

    let error = db
        .write(
            "insert into app (name) values ($1)",
            &[Parameter::Text("a".into())],
        )
        .unwrap_err();
    assert!(matches!(
        error,
        dactyl_db::DactylError::Adapter {
            kind: AdapterErrorKind::Constraint,
            ..
        }
    ));
}

#[test]
fn sqlite_ignores_remote_context_without_changing_local_results() {
    let file = NamedTempFile::new().unwrap();
    let path = file.path().to_string_lossy().into_owned();
    let without = Connection::open(DatastoreRoute::sqlite(&path)).unwrap();
    without
        .write(
            "create table app (id integer primary key, name text unique)",
            &[],
        )
        .unwrap();
    without
        .write(
            "insert into app (name) values ($1)",
            &[Parameter::Text("local".into())],
        )
        .unwrap();
    let with = Connection::open_with_context(
        DatastoreRoute::sqlite(&path),
        Some(
            StorageContext::new(
                1,
                json!({
                    "org_id": "should-not-be-required",
                    "user_id": "should-not-be-required",
                    "repository_id": "should-not-be-required",
                    "opaque_target": "remote",
                    "opaque_session": "ignored"
                }),
            )
            .unwrap(),
        ),
    )
    .unwrap();
    assert_eq!(with.context().unwrap().version(), 1);
    assert_eq!(
        with.read("select name from app", &[]).unwrap().as_slice()[0]
            .get_str("name")
            .unwrap(),
        without
            .read("select name from app", &[])
            .unwrap()
            .as_slice()[0]
            .get_str("name")
            .unwrap()
    );
    let error = with
        .write(
            "insert into app (name) values ($1)",
            &[Parameter::Text("local".into())],
        )
        .unwrap_err();
    assert_eq!(error.adapter_kind(), Some(AdapterErrorKind::Constraint));
}