citadeldb-sql 0.16.1

SQL parser, planner, and executor for Citadel encrypted database
Documentation
use citadel::{Argon2Profile, DatabaseBuilder};
use citadel_sql::{Connection, ExecutionResult, SqlError, Value};

fn create_db(dir: &std::path::Path) -> citadel::Database {
    DatabaseBuilder::new(dir.join("test.db"))
        .passphrase(b"x")
        .argon2_profile(Argon2Profile::Iot)
        .create()
        .unwrap()
}

fn assert_ok(r: ExecutionResult) {
    matches!(r, ExecutionResult::Ok | ExecutionResult::RowsAffected(_));
}

#[test]
fn strict_table_accepts_exact_type() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    assert_ok(
        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER) STRICT")
            .unwrap(),
    );
    assert_ok(conn.execute("INSERT INTO t VALUES (1, 42)").unwrap());
    let qr = conn.query("SELECT n FROM t WHERE id = 1").unwrap();
    assert_eq!(qr.rows[0][0], Value::Integer(42));
}

#[test]
fn strict_table_rejects_text_to_integer_lossy() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER) STRICT")
        .unwrap();
    let err = conn.execute("INSERT INTO t VALUES (1, 'xyz')").unwrap_err();
    assert!(matches!(err, SqlError::TypeMismatch { .. }));
}

#[test]
fn strict_table_rejects_lossy_leading_zeros() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER) STRICT")
        .unwrap();
    let err = conn
        .execute("INSERT INTO t VALUES (1, '000123')")
        .unwrap_err();
    assert!(matches!(err, SqlError::TypeMismatch { .. }));
}

#[test]
fn strict_table_accepts_lossless_text_to_integer() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER) STRICT")
        .unwrap();
    conn.execute("INSERT INTO t VALUES (1, '42')").unwrap();
    let qr = conn.query("SELECT n FROM t WHERE id = 1").unwrap();
    assert_eq!(qr.rows[0][0], Value::Integer(42));
}

#[test]
fn strict_table_rejects_real_to_integer_lossy() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER) STRICT")
        .unwrap();
    let err = conn.execute("INSERT INTO t VALUES (1, 5.5)").unwrap_err();
    assert!(matches!(err, SqlError::TypeMismatch { .. }));
}

#[test]
fn strict_table_accepts_real_to_integer_whole() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER) STRICT")
        .unwrap();
    conn.execute("INSERT INTO t VALUES (1, 5.0)").unwrap();
    let qr = conn.query("SELECT n FROM t").unwrap();
    assert_eq!(qr.rows[0][0], Value::Integer(5));
}

#[test]
fn strict_table_accepts_null() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER) STRICT")
        .unwrap();
    conn.execute("INSERT INTO t VALUES (1, NULL)").unwrap();
    let qr = conn.query("SELECT n FROM t").unwrap();
    assert_eq!(qr.rows[0][0], Value::Null);
}

#[test]
fn strict_table_update_rejects_lossy() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER) STRICT")
        .unwrap();
    conn.execute("INSERT INTO t VALUES (1, 42)").unwrap();
    let err = conn
        .execute("UPDATE t SET n = 'xyz' WHERE id = 1")
        .unwrap_err();
    assert!(matches!(err, SqlError::TypeMismatch { .. }));
}

#[test]
fn non_strict_table_truncates_real_to_integer() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER)")
        .unwrap();
    conn.execute("INSERT INTO t VALUES (1, 5.5)").unwrap();
    let qr = conn.query("SELECT n FROM t").unwrap();
    assert_eq!(qr.rows[0][0], Value::Integer(5));
}

#[test]
fn strict_table_persists_across_reopen() {
    let dir = tempfile::tempdir().unwrap();
    {
        let db = create_db(dir.path());
        let conn = Connection::open(&db).unwrap();
        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, n INTEGER) STRICT")
            .unwrap();
    }
    let db = DatabaseBuilder::new(dir.path().join("test.db"))
        .passphrase(b"x")
        .argon2_profile(Argon2Profile::Iot)
        .open()
        .unwrap();
    let conn = Connection::open(&db).unwrap();
    let err = conn.execute("INSERT INTO t VALUES (1, 'xyz')").unwrap_err();
    assert!(matches!(err, SqlError::TypeMismatch { .. }));
}