use std::sync::{Arc, RwLock};
use alopex_core::KVStore;
use alopex_core::KVTransaction;
use alopex_core::kv::memory::MemoryKV;
use alopex_core::types::TxnMode;
use alopex_sql::AlopexDialect;
use alopex_sql::Catalog;
use alopex_sql::Parser;
use alopex_sql::Planner;
use alopex_sql::SqlValue;
use alopex_sql::catalog::{CatalogOverlay, PersistentCatalog, TxnCatalogView};
use alopex_sql::executor::{ExecutionResult, Executor};
use alopex_sql::storage::TxnBridge;
fn run_sql_in_txn(
store: Arc<MemoryKV>,
catalog: Arc<RwLock<PersistentCatalog<MemoryKV>>>,
mode: TxnMode,
sql: &str,
) -> ExecutionResult {
let dialect = AlopexDialect;
let stmts = Parser::parse_sql(&dialect, sql).expect("parse");
assert!(!stmts.is_empty(), "sql must contain at least one statement");
let mut txn = store.begin(mode).expect("begin");
let mut overlay = CatalogOverlay::new();
let mut borrowed = TxnBridge::<MemoryKV>::wrap_external(&mut txn, mode, &mut overlay);
let mut executor: Executor<_, _> = Executor::new(store.clone(), catalog.clone());
let mut last = ExecutionResult::Success;
for stmt in &stmts {
let plan = {
let catalog_guard = catalog.read().expect("catalog lock poisoned");
let (_, overlay) = borrowed.split_parts();
let view = TxnCatalogView::new(&*catalog_guard, &*overlay);
let planner = Planner::new(&view);
planner.plan(stmt).expect("plan")
};
last = executor
.execute_in_txn(plan, &mut borrowed)
.expect("execute_in_txn");
}
drop(borrowed);
txn.commit_self().expect("commit");
if mode == TxnMode::ReadWrite {
let mut catalog_guard = catalog.write().expect("catalog lock poisoned");
catalog_guard.apply_overlay(overlay);
}
last
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn persistence_test_catalog_survives_restart_with_flush() {
let dir = tempfile::tempdir().unwrap();
let wal_path = dir.path().join("catalog_flush.wal");
{
let store = Arc::new(MemoryKV::open(&wal_path).unwrap());
let catalog = Arc::new(RwLock::new(PersistentCatalog::load(store.clone()).unwrap()));
run_sql_in_txn(
store.clone(),
catalog,
TxnMode::ReadWrite,
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);",
);
store.flush().unwrap();
}
{
let store = Arc::new(MemoryKV::open(&wal_path).unwrap());
let catalog = PersistentCatalog::load(store.clone()).unwrap();
assert!(catalog.table_exists("users"));
}
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn persistence_test_data_survives_restart_with_flush() {
let dir = tempfile::tempdir().unwrap();
let wal_path = dir.path().join("data_flush.wal");
{
let store = Arc::new(MemoryKV::open(&wal_path).unwrap());
let catalog = Arc::new(RwLock::new(PersistentCatalog::load(store.clone()).unwrap()));
run_sql_in_txn(
store.clone(),
catalog,
TxnMode::ReadWrite,
r#"
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO users (id, name) VALUES (1, 'alice');
"#,
);
store.flush().unwrap();
}
{
let store = Arc::new(MemoryKV::open(&wal_path).unwrap());
let catalog = Arc::new(RwLock::new(PersistentCatalog::load(store.clone()).unwrap()));
let result = run_sql_in_txn(
store,
catalog,
TxnMode::ReadOnly,
"SELECT id, name FROM users ORDER BY id;",
);
match result {
ExecutionResult::Query(q) => {
assert_eq!(q.rows.len(), 1);
assert_eq!(q.rows[0][0], SqlValue::Integer(1));
assert_eq!(q.rows[0][1], SqlValue::Text("alice".into()));
}
other => panic!("expected query result, got {other:?}"),
}
}
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn persistence_test_catalog_survives_restart_wal_only() {
let dir = tempfile::tempdir().unwrap();
let wal_path = dir.path().join("catalog_wal_only.wal");
{
let store = Arc::new(MemoryKV::open(&wal_path).unwrap());
let catalog = Arc::new(RwLock::new(PersistentCatalog::load(store.clone()).unwrap()));
run_sql_in_txn(
store,
catalog,
TxnMode::ReadWrite,
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);",
);
}
{
let store = Arc::new(MemoryKV::open(&wal_path).unwrap());
let catalog = PersistentCatalog::load(store.clone()).unwrap();
assert!(catalog.table_exists("users"));
}
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn persistence_test_id_counter_consistent_after_restart() {
let dir = tempfile::tempdir().unwrap();
let wal_path = dir.path().join("id_counter.wal");
{
let store = Arc::new(MemoryKV::open(&wal_path).unwrap());
let catalog = Arc::new(RwLock::new(PersistentCatalog::load(store.clone()).unwrap()));
run_sql_in_txn(
store.clone(),
catalog,
TxnMode::ReadWrite,
"CREATE TABLE t1 (id INTEGER PRIMARY KEY);",
);
store.flush().unwrap();
}
let store = Arc::new(MemoryKV::open(&wal_path).unwrap());
let catalog = Arc::new(RwLock::new(PersistentCatalog::load(store.clone()).unwrap()));
let t1_id = catalog.read().unwrap().get_table("t1").unwrap().table_id;
run_sql_in_txn(
store.clone(),
catalog.clone(),
TxnMode::ReadWrite,
"CREATE TABLE t2 (id INTEGER PRIMARY KEY);",
);
let t2_id = catalog.read().unwrap().get_table("t2").unwrap().table_id;
assert!(t2_id > t1_id);
}
#[cfg_attr(not(feature = "lane_ci"), ignore)]
#[test]
fn persistence_test_wal_truncation_recovery_without_hooks() {
let dir = tempfile::tempdir().unwrap();
let wal_path = dir.path().join("truncate.wal");
{
let store = Arc::new(MemoryKV::open(&wal_path).unwrap());
let catalog = Arc::new(RwLock::new(PersistentCatalog::load(store.clone()).unwrap()));
run_sql_in_txn(
store.clone(),
catalog,
TxnMode::ReadWrite,
r#"
CREATE TABLE t1 (id INTEGER PRIMARY KEY);
INSERT INTO t1 (id) VALUES (1);
"#,
);
store.flush().unwrap();
}
{
let store = Arc::new(MemoryKV::open(&wal_path).unwrap());
let catalog = Arc::new(RwLock::new(PersistentCatalog::load(store.clone()).unwrap()));
run_sql_in_txn(
store,
catalog,
TxnMode::ReadWrite,
"CREATE TABLE t2 (id INTEGER PRIMARY KEY);",
);
}
{
let bytes = std::fs::read(&wal_path).unwrap();
let mut pos = 0usize;
let mut last_start = None::<usize>;
let mut last_len = 0usize;
while pos + 8 <= bytes.len() {
let len = u32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap()) as usize;
let record_total = 8usize.saturating_add(len);
if pos + record_total > bytes.len() {
break;
}
last_start = Some(pos);
last_len = len;
pos += record_total;
}
let last_start = last_start.expect("wal must contain at least one full record");
assert!(last_len > 0, "wal record body must not be empty");
let new_len = (last_start + 8 + last_len - 1) as u64;
let file = std::fs::OpenOptions::new()
.write(true)
.open(&wal_path)
.unwrap();
file.set_len(new_len).unwrap();
}
{
let store = Arc::new(MemoryKV::open(&wal_path).unwrap());
let catalog = Arc::new(RwLock::new(PersistentCatalog::load(store.clone()).unwrap()));
let result = run_sql_in_txn(
store,
catalog.clone(),
TxnMode::ReadOnly,
"SELECT id FROM t1;",
);
match result {
ExecutionResult::Query(q) => assert_eq!(q.rows.len(), 1),
other => panic!("expected query result, got {other:?}"),
}
let catalog_guard = catalog.read().unwrap();
assert!(!catalog_guard.table_exists("t2"));
}
}