switchy_database 0.5.0

Switchy database package
#![cfg(all(feature = "sqlite-rusqlite", feature = "schema"))]

use std::sync::Arc;
use switchy_database::{
    Database,
    query::{FilterableQuery, where_not_like},
    rusqlite::RusqliteDatabase,
    schema::{Column, DataType},
};

fn column(name: &str, data_type: DataType) -> Column {
    Column {
        name: name.into(),
        data_type,
        nullable: false,
        auto_increment: false,
        default: None,
    }
}

#[tokio::test]
async fn auto_increment_does_not_reuse_deleted_ids() {
    let connection = rusqlite::Connection::open_in_memory().unwrap();
    let db = RusqliteDatabase::new(vec![Arc::new(switchy_async::sync::Mutex::new(connection))]);
    let mut id = column("id", DataType::BigInt);
    id.auto_increment = true;
    db.create_table("events")
        .column(id)
        .column(column("payload", DataType::Text))
        .primary_key("id")
        .execute(&db)
        .await
        .unwrap();
    db.insert("events")
        .value("payload", "first")
        .execute(&db)
        .await
        .unwrap();
    assert_eq!(db.exec_delete_count(&db.delete("events")).await.unwrap(), 1);
    db.insert("events")
        .value("payload", "second")
        .execute(&db)
        .await
        .unwrap();
    assert_eq!(
        db.select("events")
            .where_eq("id", 2)
            .execute(&db)
            .await
            .unwrap()
            .len(),
        1
    );
    let mut invalid = column("id", DataType::Text);
    invalid.auto_increment = true;
    assert!(matches!(
        db.create_table("invalid")
            .column(invalid)
            .primary_key("id")
            .execute(&db)
            .await,
        Err(switchy_database::DatabaseError::InvalidSchema(_))
    ));
    db.close().await.unwrap();
}

#[tokio::test]
async fn usage_constraints_and_bounded_discovery() {
    let connection = rusqlite::Connection::open_in_memory().unwrap();
    let db = RusqliteDatabase::new(vec![Arc::new(switchy_async::sync::Mutex::new(connection))]);
    db.create_table("usage_meta")
        .column(column("id", DataType::BigInt))
        .check_integer_equals("id", 1)
        .execute(&db)
        .await
        .unwrap();
    db.insert("usage_meta")
        .value("id", 1)
        .execute(&db)
        .await
        .unwrap();
    assert!(
        db.insert("usage_meta")
            .value("id", 2)
            .execute(&db)
            .await
            .is_err()
    );
    db.create_table("usage_rows")
        .column(column("session_id", DataType::Text))
        .column(column("request_key", DataType::Text))
        .column(column("staged", DataType::BigInt))
        .unique_columns(["session_id", "request_key", "staged"])
        .execute(&db)
        .await
        .unwrap();
    for staged in [0, 1] {
        db.insert("usage_rows")
            .value("session_id", "s")
            .value("request_key", "r")
            .value("staged", staged)
            .execute(&db)
            .await
            .unwrap();
    }
    assert!(
        db.insert("usage_rows")
            .value("session_id", "s")
            .value("request_key", "r")
            .value("staged", 1)
            .execute(&db)
            .await
            .is_err()
    );
    let rows = db
        .select("sqlite_master")
        .where_eq("type", "table")
        .where_not_like("name", "sqlite_%")
        .limit(3)
        .execute(&db)
        .await
        .unwrap();
    assert_eq!(rows.len(), 2);
    let rows = db
        .select("sqlite_master")
        .filter(Box::new(where_not_like("name", "%' OR 1=1 --")))
        .limit(1)
        .execute(&db)
        .await
        .unwrap();
    assert_eq!(rows.len(), 1);
}