llkv-sql 0.8.5-alpha

SQL interface for the LLKV toolkit.
Documentation
use std::sync::Arc;

use llkv_plan::{InsertConflictAction, InsertPlan, InsertSource, PlanValue};
use llkv_runtime::{RuntimeStatementResult, TEMPORARY_NAMESPACE_ID, TemporaryRuntimeNamespace};
use llkv_sql::SqlEngine;
use llkv_storage::pager::MemPager;

#[test]
fn temporary_tables_support_core_dml() {
    let engine = SqlEngine::new(Arc::new(MemPager::default()));

    let mut results = engine
        .execute("CREATE TEMPORARY TABLE integers(value INTEGER);")
        .expect("create temporary table succeeds");
    assert_eq!(results.len(), 1);
    assert!(matches!(
        results.remove(0),
        RuntimeStatementResult::CreateTable { table_name }
        if table_name.eq_ignore_ascii_case("integers")
    ));

    let mut results = engine
        .execute("INSERT INTO integers VALUES (1), (2);")
        .expect("insert into temporary table succeeds");
    assert_eq!(results.len(), 1);
    assert!(matches!(
        results.remove(0),
        RuntimeStatementResult::Insert {
            table_name,
            rows_inserted
        }
        if table_name.eq_ignore_ascii_case("integers") && rows_inserted == 2
    ));

    let registry = engine.session().namespace_registry();
    let namespace_id = registry
        .read()
        .expect("namespace registry lock")
        .namespace_for_table("integers");
    assert_eq!(namespace_id, TEMPORARY_NAMESPACE_ID);

    let mut results = engine
        .execute("CREATE UNIQUE INDEX \"uidx\" ON \"integers\" (\"value\");")
        .expect("create index on temporary table succeeds");
    assert_eq!(results.len(), 1);
    assert!(matches!(
        results.remove(0),
        RuntimeStatementResult::CreateIndex { table_name, .. }
        if table_name.eq_ignore_ascii_case("integers")
    ));

    let mut results = engine
        .execute("UPDATE integers SET value = value + 10 WHERE value = 1;")
        .expect("update temporary table succeeds");
    assert_eq!(results.len(), 1);
    assert!(matches!(
        results.remove(0),
        RuntimeStatementResult::Update {
            table_name,
            rows_updated
        }
        if table_name.eq_ignore_ascii_case("integers") && rows_updated == 1
    ));

    let mut results = engine
        .execute("DELETE FROM integers WHERE value = 11;")
        .expect("delete from temporary table succeeds");
    assert_eq!(results.len(), 1);
    assert!(matches!(
        results.remove(0),
        RuntimeStatementResult::Delete {
            table_name,
            rows_deleted
        }
        if table_name.eq_ignore_ascii_case("integers") && rows_deleted == 1
    ));

    let mut results = engine
        .execute("DROP TABLE integers;")
        .expect("drop temporary table succeeds");
    assert_eq!(results.len(), 1);
    assert!(matches!(results.remove(0), RuntimeStatementResult::NoOp));
}

#[test]
fn temporary_tables_allow_inserts_after_unique_index() {
    let engine = SqlEngine::new(Arc::new(MemPager::default()));

    let mut results = engine
        .execute("CREATE TEMPORARY TABLE numbers(i INTEGER, j TEXT);")
        .expect("create temporary table succeeds");
    assert_eq!(results.len(), 1);
    assert!(matches!(
        results.remove(0),
        RuntimeStatementResult::CreateTable { table_name }
        if table_name.eq_ignore_ascii_case("numbers")
    ));

    let mut results = engine
        .execute("CREATE UNIQUE INDEX idx_numbers_j ON numbers(j);")
        .expect("create unique index succeeds");
    assert_eq!(results.len(), 1);
    assert!(matches!(
        results.remove(0),
        RuntimeStatementResult::CreateIndex { table_name, .. }
        if table_name.eq_ignore_ascii_case("numbers")
    ));

    let mut results = engine
        .execute("SELECT * FROM numbers;")
        .expect("select from empty temp table succeeds");
    assert_eq!(results.len(), 1);
    assert!(matches!(
        results.remove(0),
        RuntimeStatementResult::Select { .. }
    ));

    let temp_namespace = engine
        .session()
        .namespace_registry()
        .read()
        .expect("namespace registry lock")
        .namespace::<TemporaryRuntimeNamespace>(TEMPORARY_NAMESPACE_ID)
        .expect("temporary namespace present");

    let temp_context = temp_namespace.context();
    let table = temp_context
        .lookup_table("numbers")
        .expect("temporary context lookup succeeds");
    let unique_flag = table
        .schema
        .columns
        .iter()
        .find(|col| col.name.eq_ignore_ascii_case("j"))
        .map(|col| col.unique)
        .unwrap_or_default();
    assert!(
        unique_flag,
        "unique index updates executor schema for column j"
    );

    let insert_plan = InsertPlan {
        table: "numbers".to_string(),
        columns: Vec::new(),
        source: InsertSource::Rows(vec![
            vec![PlanValue::Integer(1), PlanValue::String("a".into())],
            vec![PlanValue::Integer(2), PlanValue::String("b".into())],
        ]),
        on_conflict: InsertConflictAction::None,
    };

    let result = engine
        .session()
        .execute_insert_plan(insert_plan)
        .expect("insert now succeeds after catalog synchronization fix");

    assert!(
        matches!(result, RuntimeStatementResult::Insert { table_name, rows_inserted: 2 }
        if table_name.eq_ignore_ascii_case("numbers"))
    );
}