boxdb 0.3.2

An extended SQLite client with typed columns and readable storage
Documentation
#[allow(dead_code)]
#[path = "common/files.rs"]
mod files;
use boxdb::{Database, Format, Type, Value};

#[test]
fn decimal_is_the_only_public_exact_decimal_type_and_serialized_tag() {
    assert!(Type::parse("NUMERIC").is_err());
    assert!(serde_json::from_str::<Type>(r#"{"Numeric":[12,2]}"#).is_err());
    assert!(serde_json::from_str::<Value>(r#"{"Numeric":"12.50"}"#).is_err());
    let ty = Type::Decimal(Some((12, 2)));
    let value = Value::Decimal("12.50".into());
    assert_eq!(serde_json::to_string(&ty).unwrap(), r#"{"Decimal":[12,2]}"#);
    assert_eq!(
        serde_json::to_string(&value).unwrap(),
        r#"{"Decimal":"12.50"}"#
    );
    assert_eq!(
        serde_json::from_str::<Type>(r#"{"Decimal":[12,2]}"#).unwrap(),
        ty
    );
    assert_eq!(
        serde_json::from_str::<Value>(r#"{"Decimal":"12.50"}"#).unwrap(),
        value
    );
}

#[test]
fn numeric_declarations_are_rejected_while_decimal_remains_native() {
    let mut db = Database::memory();
    for sql in [
        "CREATE TABLE rejected(v NUMERIC)",
        "CREATE TABLE rejected(v NUMERIC(12,2))",
        "CREATE TABLE rejected(v NUMERIC(12,2)[])",
        "CREATE TABLE rejected(v NUMERIC PRECISION)",
    ] {
        assert!(db.execute(sql).is_err(), "{sql}");
    }
    db.execute("CREATE TABLE prices(amount DECIMAL(12,2)); INSERT INTO prices VALUES('12.50')")
        .unwrap();
    assert_eq!(
        db.execute("SELECT amount,typeof(amount),amount+CAST('0.25' AS DECIMAL) FROM prices")
            .unwrap()[0]
            .rows,
        vec![vec![
            Value::Decimal("12.50".into()),
            Value::Text("decimal".into()),
            Value::Decimal("12.75".into())
        ]]
    );
    assert!(
        db.execute("ALTER TABLE prices ADD COLUMN obsolete NUMERIC")
            .is_err()
    );
}

#[test]
fn decimal_values_and_arrays_survive_reopening_and_conversion() {
    let directory = tempfile::tempdir().unwrap();
    for format in [Format::Binary, Format::Readable] {
        let path = directory.path().join(format!("{format:?}"));
        let mut db = Database::open(&path, format).unwrap();
        db.execute("CREATE TABLE prices(amount DECIMAL(20,4), history DECIMAL(20,4)[])")
            .unwrap();
        let values = vec![
            Value::Decimal("1234567890123456.1234".into()),
            Value::Array(vec![Value::Decimal("0.1000".into()), Value::Null]),
        ];
        db.execute_params("INSERT INTO prices VALUES(?,?)", &values)
            .unwrap();
        let snapshot = files::converted_bytes(&mut db, format).unwrap();
        drop(db);
        let mut reopened = Database::open(&path, format).unwrap();
        assert_eq!(
            reopened.execute("SELECT * FROM prices").unwrap()[0].rows,
            vec![values.clone()]
        );
        let mut restored = files::open_bytes(&snapshot).unwrap();
        assert_eq!(
            restored.execute("SELECT * FROM prices").unwrap()[0].rows,
            vec![values]
        );
    }
    assert_eq!(
        serde_json::to_string(&Value::Decimal("12.50".into())).unwrap(),
        r#"{"Decimal":"12.50"}"#
    );
}

#[test]
fn decimal_declarations_preserve_storage_and_validation() {
    let mut db = Database::memory();
    db.execute("CREATE TABLE prices(n DECIMAL(12,2), d DECIMAL(12,2), history DECIMAL(12,2)[]); ALTER TABLE prices ADD COLUMN added DECIMAL(12,2)").unwrap();
    db.execute("INSERT INTO prices VALUES('12.5','12.5','[\"12.5\"]','12.5')")
        .unwrap();
    assert_eq!(
        db.execute("SELECT * FROM prices").unwrap()[0].rows,
        vec![vec![
            Value::Decimal("12.50".into()),
            Value::Decimal("12.50".into()),
            Value::Array(vec![Value::Decimal("12.50".into())]),
            Value::Decimal("12.50".into()),
        ]]
    );
    for column in ["n", "d", "added"] {
        assert!(
            db.execute(&format!("UPDATE prices SET {column}='1.234'"))
                .is_err()
        );
    }
}