khive-db 0.13.0

SQLite storage backend: entities, edges, notes, events, FTS5, sqlite-vec vectors.
Documentation
use super::{execute_statements, SqlStatement, SqlValue, SqliteError};
use rusqlite::{ffi, Connection};

fn setup() -> Connection {
    let conn = Connection::open_in_memory().unwrap();
    conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY, value TEXT)")
        .unwrap();
    conn
}

fn statement(sql: &str) -> SqlStatement {
    SqlStatement::new(sql, vec![])
}

fn count(conn: &Connection) -> i64 {
    conn.query_row("SELECT COUNT(*) FROM t", [], |row| row.get(0))
        .unwrap()
}

#[test]
fn caller_owns_transaction_and_affected_count_is_not_commit() {
    let conn = setup();
    for statements in [vec![], vec![statement("INSERT INTO t(id) VALUES (1)")]] {
        assert!(matches!(
            execute_statements(&conn, &statements),
            Err(SqliteError::InvalidData(_))
        ));
        assert_eq!(count(&conn), 0);
    }
    conn.execute_batch("BEGIN IMMEDIATE").unwrap();
    assert_eq!(execute_statements(&conn, &[]).unwrap(), 0);
    let statements = [
        SqlStatement::new(
            "INSERT INTO t VALUES (?1, ?2); -- tail",
            vec![
                SqlValue::Integer(1),
                SqlValue::Text("COMMIT; -- data".into()),
            ],
        ),
        SqlStatement::new("INSERT INTO t VALUES (?1, ?2)", vec![SqlValue::Integer(2)]),
        statement("UPDATE t SET value = 'absent' WHERE id = 99"),
    ];
    assert_eq!(execute_statements(&conn, &statements).unwrap(), 2);
    let values: Vec<Option<String>> = conn
        .prepare("SELECT value FROM t ORDER BY id")
        .unwrap()
        .query_map([], |row| row.get(0))
        .unwrap()
        .collect::<Result<_, _>>()
        .unwrap();
    assert_eq!(values, [Some(String::from("COMMIT; -- data")), None]);
    assert!(!conn.is_autocommit());
    conn.execute_batch("ROLLBACK").unwrap();
    assert_eq!(count(&conn), 0);
    conn.execute_batch("BEGIN").unwrap();
    execute_statements(&conn, &[statement("INSERT INTO t(id) VALUES (3)")]).unwrap();
    conn.execute_batch("COMMIT").unwrap();
    assert_eq!(count(&conn), 1);
}

#[test]
fn explicit_control_and_executable_tails_refuse_before_writes() {
    let conn = setup();
    conn.execute_batch("BEGIN").unwrap();
    for control in [
        "BEGIN",
        "START TRANSACTION",
        "COMMIT",
        "END",
        "ROLLBACK",
        "ROLLBACK TO s",
        "SAVEPOINT s",
        "RELEASE s",
    ] {
        let statements = [
            statement("INSERT INTO t(id) VALUES (1)"),
            statement(&format!("\u{feff} ; -- prefix\n /* comment */ {control}")),
        ];
        let error = execute_statements(&conn, &statements).unwrap_err();
        assert!(matches!(error, SqliteError::InvalidData(message) if message.contains("index 1")));
        assert_eq!(count(&conn), 0);
        assert!(!conn.is_autocommit());
    }
    let statements = [
        statement("INSERT INTO t(id) VALUES (1)"),
        statement("INSERT INTO t(id) VALUES (2); COMMIT"),
    ];
    let error = execute_statements(&conn, &statements).unwrap_err();
    assert!(matches!(
        error,
        SqliteError::Rusqlite(rusqlite::Error::MultipleStatement)
    ));
    assert_eq!(count(&conn), 0);
    assert!(!conn.is_autocommit());
}

#[test]
fn native_bind_and_constraint_errors_leave_prior_writes_for_owner_rollback() {
    for (bad, expected) in [
        (
            SqlStatement::new(
                "INSERT INTO t(id) VALUES (?1)",
                vec![SqlValue::Integer(2), SqlValue::Integer(3)],
            ),
            ffi::SQLITE_RANGE,
        ),
        (
            statement("INSERT INTO t(id) VALUES (1)"),
            ffi::SQLITE_CONSTRAINT_PRIMARYKEY,
        ),
    ] {
        let conn = setup();
        conn.execute_batch("BEGIN").unwrap();
        let statements = [
            statement("INSERT INTO t(id) VALUES (1)"),
            bad,
            statement("INSERT INTO t(id) VALUES (3)"),
        ];
        let error = execute_statements(&conn, &statements).unwrap_err();
        let SqliteError::Rusqlite(error) = error else {
            panic!("native error lost: {error}");
        };
        assert_eq!(error.sqlite_error().unwrap().extended_code, expected);
        assert_eq!(count(&conn), 1);
        assert!(!conn.is_autocommit());
        conn.execute_batch("ROLLBACK").unwrap();
        assert_eq!(count(&conn), 0);
    }
}

#[test]
fn deferred_tail_refusal_precedes_that_element_but_not_earlier_ddl() {
    let conn = setup();
    conn.execute_batch("BEGIN").unwrap();
    let statements = [
        statement("CREATE TABLE later (id INTEGER)"),
        statement("INSERT INTO later VALUES (1); INSERT INTO t(id) VALUES (2)"),
    ];
    let error = execute_statements(&conn, &statements).unwrap_err();
    assert!(matches!(
        error,
        SqliteError::Rusqlite(rusqlite::Error::MultipleStatement)
    ));
    let later_count: i64 = conn
        .query_row("SELECT COUNT(*) FROM later", [], |r| r.get(0))
        .unwrap();
    assert_eq!(later_count, 0);
    assert_eq!(count(&conn), 0);
    assert!(!conn.is_autocommit());
    conn.execute_batch("ROLLBACK").unwrap();
    assert!(conn.prepare("SELECT * FROM later").is_err());
}

#[test]
fn existing_authorizer_remains_effective() {
    use rusqlite::hooks::{AuthAction, AuthContext, Authorization};
    let conn = setup();
    conn.execute_batch("BEGIN").unwrap();
    conn.authorizer(Some(|ctx: AuthContext<'_>| match ctx.action {
        AuthAction::Delete { .. } => Authorization::Deny,
        _ => Authorization::Allow,
    }))
    .unwrap();
    execute_statements(&conn, &[statement("INSERT INTO t(id) VALUES (1)")]).unwrap();
    let error = conn.execute("DELETE FROM t", []).unwrap_err();
    assert_eq!(
        error.sqlite_error().unwrap().extended_code,
        ffi::SQLITE_AUTH
    );
    assert_eq!(count(&conn), 1);
}

#[test]
fn every_sql_value_preserves_its_sqlite_representation() {
    use rusqlite::types::Value;
    let conn = Connection::open_in_memory().unwrap();
    conn.execute_batch("CREATE TABLE bound (value); BEGIN")
        .unwrap();
    let id = uuid::Uuid::from_u128(123);
    let timestamp = chrono::DateTime::from_timestamp_micros(1_234_567).unwrap();
    let values = [
        SqlValue::Null,
        SqlValue::Bool(true),
        SqlValue::Integer(-7),
        SqlValue::Float(0.25),
        SqlValue::Text("COMMIT; -- data".into()),
        SqlValue::Blob(vec![0, 255]),
        SqlValue::Json(serde_json::json!({"x": 1})),
        SqlValue::Uuid(id),
        SqlValue::Timestamp(timestamp),
    ];
    let statements: Vec<_> = values
        .into_iter()
        .map(|value| SqlStatement::new("INSERT INTO bound VALUES (?1)", vec![value]))
        .collect();
    assert_eq!(execute_statements(&conn, &statements).unwrap(), 9);
    conn.execute_batch("COMMIT").unwrap();
    let stored: Vec<Value> = conn
        .prepare("SELECT value FROM bound ORDER BY rowid")
        .unwrap()
        .query_map([], |row| row.get(0))
        .unwrap()
        .collect::<Result<_, _>>()
        .unwrap();
    assert_eq!(
        stored,
        [
            Value::Null,
            Value::Integer(1),
            Value::Integer(-7),
            Value::Real(0.25),
            Value::Text("COMMIT; -- data".into()),
            Value::Blob(vec![0, 255]),
            Value::Text(r#"{"x":1}"#.into()),
            Value::Text(id.to_string()),
            Value::Integer(1_234_567),
        ]
    );
}