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(_));
}

fn setup(conn: &Connection) {
    assert_ok(
        conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
            .unwrap(),
    );
    conn.execute("INSERT INTO t VALUES (1, 'Alice'), (2, 'BOB'), (3, 'charlie'), (4, 'alice')")
        .unwrap();
}

#[test]
fn collate_nocase_expression_equality() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup(&conn);
    let qr = conn
        .query("SELECT id FROM t WHERE name = 'alice' COLLATE NOCASE ORDER BY id")
        .unwrap();
    assert_eq!(qr.rows.len(), 2);
    assert_eq!(qr.rows[0][0], Value::Integer(1));
    assert_eq!(qr.rows[1][0], Value::Integer(4));
}

#[test]
fn collate_binary_expression_equality() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup(&conn);
    let qr = conn
        .query("SELECT id FROM t WHERE name = 'alice' COLLATE BINARY")
        .unwrap();
    assert_eq!(qr.rows.len(), 1);
    assert_eq!(qr.rows[0][0], Value::Integer(4));
}

#[test]
fn collate_rtrim_expression_equality() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    assert_ok(
        conn.execute("CREATE TABLE r (id INTEGER PRIMARY KEY, s TEXT)")
            .unwrap(),
    );
    conn.execute("INSERT INTO r VALUES (1, 'abc'), (2, 'abc  '), (3, 'abcx')")
        .unwrap();
    let qr = conn
        .query("SELECT id FROM r WHERE s = 'abc' COLLATE RTRIM ORDER BY id")
        .unwrap();
    assert_eq!(qr.rows.len(), 2);
    assert_eq!(qr.rows[0][0], Value::Integer(1));
    assert_eq!(qr.rows[1][0], Value::Integer(2));
}

#[test]
fn collate_unsupported_name_rejected() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup(&conn);
    let err = conn
        .execute("SELECT * FROM t WHERE name = 'a' COLLATE FRENCH")
        .unwrap_err();
    assert!(matches!(err, SqlError::Unsupported(_)));
}

#[test]
fn order_by_column_collation_nocase() {
    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, name TEXT COLLATE NOCASE)")
        .unwrap();
    conn.execute("INSERT INTO t VALUES (1, 'banana'), (2, 'Apple'), (3, 'cherry'), (4, 'apricot')")
        .unwrap();
    let qr = conn.query("SELECT id FROM t ORDER BY name").unwrap();
    assert_eq!(qr.rows.len(), 4);
    assert_eq!(qr.rows[0][0], Value::Integer(2));
    assert_eq!(qr.rows[1][0], Value::Integer(4));
    assert_eq!(qr.rows[2][0], Value::Integer(1));
    assert_eq!(qr.rows[3][0], Value::Integer(3));
}

#[test]
fn order_by_explicit_collate_nocase() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup(&conn);
    let qr = conn
        .query("SELECT id FROM t ORDER BY name COLLATE NOCASE")
        .unwrap();
    assert_eq!(qr.rows.len(), 4);
    assert_eq!(qr.rows[0][0], Value::Integer(1));
    assert_eq!(qr.rows[1][0], Value::Integer(4));
    assert_eq!(qr.rows[2][0], Value::Integer(2));
    assert_eq!(qr.rows[3][0], Value::Integer(3));
}

#[test]
fn unique_index_nocase_rejects_case_variant() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE u (id INTEGER PRIMARY KEY, name TEXT COLLATE NOCASE UNIQUE)")
        .unwrap();
    conn.execute("INSERT INTO u VALUES (1, 'Alice')").unwrap();
    let err = conn
        .execute("INSERT INTO u VALUES (2, 'ALICE')")
        .unwrap_err();
    assert!(matches!(err, SqlError::UniqueViolation(_)), "{err:?}");
}

#[test]
fn unique_index_explicit_collate_in_create_index() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE u (id INTEGER PRIMARY KEY, name TEXT)")
        .unwrap();
    conn.execute("CREATE UNIQUE INDEX u_name ON u (name COLLATE NOCASE)")
        .unwrap();
    conn.execute("INSERT INTO u VALUES (1, 'Alice')").unwrap();
    let err = conn
        .execute("INSERT INTO u VALUES (2, 'alice')")
        .unwrap_err();
    assert!(matches!(err, SqlError::UniqueViolation(_)), "{err:?}");
}

#[test]
fn create_table_with_column_collate_persists() {
    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, name TEXT COLLATE NOCASE)")
            .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();
    conn.execute("INSERT INTO t VALUES (1, 'Alice')").unwrap();
    let qr = conn.query("SELECT id FROM t WHERE name = 'alice'").unwrap();
    assert_eq!(qr.rows.len(), 1);
}