use fsqlite_core::connection::{
Connection, hot_path_profile_enabled, hot_path_profile_snapshot, reset_hot_path_profile,
set_hot_path_profile_enabled,
};
use std::sync::{LazyLock, Mutex, MutexGuard};
static HOT_PATH_PROFILE_TEST_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
fn lock_profile_test_mutex() -> MutexGuard<'static, ()> {
match HOT_PATH_PROFILE_TEST_LOCK.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
struct HotPathProfileTestGuard {
_lock: MutexGuard<'static, ()>,
previous_enabled: bool,
}
impl HotPathProfileTestGuard {
fn new() -> Self {
let lock = lock_profile_test_mutex();
let previous_enabled = hot_path_profile_enabled();
reset_hot_path_profile();
set_hot_path_profile_enabled(true);
Self {
_lock: lock,
previous_enabled,
}
}
}
impl Drop for HotPathProfileTestGuard {
fn drop(&mut self) {
reset_hot_path_profile();
set_hot_path_profile_enabled(self.previous_enabled);
}
}
fn cache_snapshot() -> (u64, u64, u64, u64, u64, u64) {
let s = hot_path_profile_snapshot();
(
s.parser.parse_cache_hits,
s.parser.parse_cache_misses,
s.parser.compiled_cache_hits,
s.parser.compiled_cache_misses,
s.parser.prepared_cache_hits,
s.parser.prepared_cache_misses,
)
}
#[test]
fn test_parse_cache_hits_on_repeated_select() {
let _profile_guard = HotPathProfileTestGuard::new();
let conn = Connection::open(":memory:").unwrap();
conn.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, val TEXT)")
.unwrap();
conn.execute("INSERT INTO t VALUES(1, 'a')").unwrap();
let rows1 = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
let (ph1, _, _, _, _, _) = cache_snapshot();
let rows2 = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
let (ph2, _, _, _, _, _) = cache_snapshot();
assert!(
ph2 > ph1,
"parse cache hits should increase on repeated SQL: before={ph1}, after={ph2}"
);
assert_eq!(
rows1, rows2,
"repeated query must produce identical results"
);
}
#[test]
fn test_ddl_invalidates_schema_bound_reuse_and_recovers() {
let _profile_guard = HotPathProfileTestGuard::new();
let conn = Connection::open(":memory:").unwrap();
conn.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, val TEXT)")
.unwrap();
conn.execute("INSERT INTO t VALUES(1, 'before')").unwrap();
let stmt = conn.prepare("SELECT val FROM t WHERE id = ?1").unwrap();
let _ = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
let (ph_pre, _, _, _, _, _) = cache_snapshot();
let _ = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
let (ph_warm, _, _, _, _, _) = cache_snapshot();
assert!(
ph_warm > ph_pre,
"cache should be warm: ph_pre={ph_pre}, ph_warm={ph_warm}"
);
conn.execute("ALTER TABLE t ADD COLUMN extra INTEGER DEFAULT 0")
.unwrap();
let result = stmt.query_with_params(&[fsqlite_types::SqliteValue::Integer(1)]);
match result {
Err(fsqlite_error::FrankenError::SchemaChanged) => {}
Ok(rows) => {
assert_eq!(rows.len(), 1, "transparent re-prepare should return 1 row");
assert_eq!(
rows[0].get(0),
Some(&fsqlite_types::SqliteValue::Text("before".into())),
"transparent re-prepare should preserve the original column"
);
}
Err(other) => panic!("unexpected prepared-statement error after DDL: {other:?}"),
}
let rows = conn.query("SELECT val, extra FROM t WHERE id = 1").unwrap();
assert_eq!(
rows.len(),
1,
"query should return the row after ALTER TABLE"
);
assert_eq!(
rows[0].get(0),
Some(&fsqlite_types::SqliteValue::Text("before".into())),
"existing column should remain readable after ALTER TABLE"
);
assert_eq!(
rows[0].get(1),
Some(&fsqlite_types::SqliteValue::Integer(0)),
"new column should expose its DEFAULT value"
);
let (ph_before_reuse, _, _, _, _, _) = cache_snapshot();
let _ = conn.query("SELECT val, extra FROM t WHERE id = 1").unwrap();
let (ph_after_reuse, _, _, _, _, _) = cache_snapshot();
assert!(
ph_after_reuse > ph_before_reuse,
"cache should recover after DDL: {ph_before_reuse} -> {ph_after_reuse}"
);
}
#[test]
fn test_rollback_savepoint_preserves_cache() {
let _profile_guard = HotPathProfileTestGuard::new();
let conn = Connection::open(":memory:").unwrap();
conn.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, val TEXT)")
.unwrap();
conn.execute("INSERT INTO t VALUES(1, 'original')").unwrap();
let _ = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
let _ = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
let (ph_pre, _, _, _, _, _) = cache_snapshot();
conn.execute("SAVEPOINT sp1").unwrap();
conn.execute("INSERT INTO t VALUES(2, 'temp')").unwrap();
conn.execute("ROLLBACK TO sp1").unwrap();
conn.execute("RELEASE sp1").unwrap();
let _ = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
let (ph_post, _, _, _, _, _) = cache_snapshot();
assert!(
ph_post > ph_pre,
"parse cache hits should increase after rollback with unchanged schema: before={ph_pre}, after={ph_post}"
);
let rows = conn.query("SELECT COUNT(*) FROM t").unwrap();
let count = rows[0].get(0).unwrap();
assert_eq!(
count,
&fsqlite_types::SqliteValue::Integer(1),
"rollback should undo the INSERT, leaving 1 row"
);
}
#[test]
fn test_schema_generation_invalidates_prepared() {
let conn = Connection::open(":memory:").unwrap();
conn.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, val TEXT)")
.unwrap();
conn.execute("INSERT INTO t VALUES(1, 'v1')").unwrap();
let stmt = conn.prepare("SELECT val FROM t WHERE id = ?1").unwrap();
let rows1 = stmt
.query_with_params(&[fsqlite_types::SqliteValue::Integer(1)])
.unwrap();
assert!(!rows1.is_empty(), "prepared statement should return rows");
conn.execute("CREATE TABLE t2(x INTEGER)").unwrap();
let result = stmt.query_with_params(&[fsqlite_types::SqliteValue::Integer(1)]);
match result {
Err(fsqlite_error::FrankenError::SchemaChanged) => {
}
Ok(rows) => {
assert!(
!rows.is_empty(),
"re-prepared result should still be correct"
);
}
Err(other) => {
panic!("unexpected error after schema change: {other:?}");
}
}
}
#[test]
fn test_prepared_cache_hits_on_repeated_insert() {
let _profile_guard = HotPathProfileTestGuard::new();
let conn = Connection::open(":memory:").unwrap();
conn.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, val TEXT)")
.unwrap();
conn.execute("INSERT INTO t VALUES(1, 'a')").unwrap();
let (_, _, _, _, ph1, _) = cache_snapshot();
conn.execute("INSERT INTO t VALUES(1, 'a')")
.unwrap_or_default(); let (_, _, _, _, ph2, _) = cache_snapshot();
assert!(
ph2 > ph1,
"identical SQL should produce a prepared cache hit: before={ph1}, after={ph2}"
);
}
#[test]
fn test_file_backed_cache_invalidation() {
let _profile_guard = HotPathProfileTestGuard::new();
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_str().unwrap();
let conn = Connection::open(path).unwrap();
conn.execute("PRAGMA journal_mode = WAL").unwrap();
conn.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, val TEXT)")
.unwrap();
conn.execute("INSERT INTO t VALUES(1, 'file-backed')")
.unwrap();
let stmt = conn.prepare("SELECT val FROM t WHERE id = ?1").unwrap();
let _ = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
let (ph1, _, _, _, _, _) = cache_snapshot();
let _ = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
let (ph2, _, _, _, _, _) = cache_snapshot();
assert!(
ph2 > ph1,
"file-backed: parse cache should hit on repeated query: {ph1} -> {ph2}"
);
conn.execute("CREATE TABLE t2(x INTEGER)").unwrap();
let result = stmt.query_with_params(&[fsqlite_types::SqliteValue::Integer(1)]);
match result {
Err(fsqlite_error::FrankenError::SchemaChanged) => {}
Ok(rows) => {
assert_eq!(
rows.len(),
1,
"file-backed: transparent re-prepare should return 1 row"
);
assert_eq!(
rows[0].get(0),
Some(&fsqlite_types::SqliteValue::Text("file-backed".into())),
"file-backed: transparent re-prepare should preserve row data"
);
}
Err(other) => {
panic!("file-backed: unexpected prepared-statement error after DDL: {other:?}")
}
}
let rows = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
assert_eq!(
rows.len(),
1,
"file-backed: result must be correct after DDL"
);
assert_eq!(
rows[0].get(0),
Some(&fsqlite_types::SqliteValue::Text("file-backed".into())),
"file-backed: row data must survive DDL"
);
let (ph_pre_reuse, _, _, _, _, _) = cache_snapshot();
let _ = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
let (ph_post_reuse, _, _, _, _, _) = cache_snapshot();
assert!(
ph_post_reuse > ph_pre_reuse,
"file-backed: cache should recover after DDL: {ph_pre_reuse} -> {ph_post_reuse}"
);
}
#[test]
fn test_churn_measurement_scorecard() {
let _profile_guard = HotPathProfileTestGuard::new();
let conn = Connection::open(":memory:").unwrap();
conn.execute("CREATE TABLE bench(id INTEGER PRIMARY KEY, name TEXT, score INTEGER)")
.unwrap();
let insert_sql = "INSERT INTO bench VALUES(1, 'test', 42)";
let select_sql = "SELECT name, score FROM bench WHERE id = 1";
let delete_sql = "DELETE FROM bench WHERE id = 1";
conn.execute(insert_sql).unwrap();
let _ = conn.query(select_sql).unwrap();
let snap_cold = hot_path_profile_snapshot();
conn.execute(delete_sql).unwrap();
reset_hot_path_profile();
for _ in 0..100 {
conn.execute(insert_sql).unwrap();
let _ = conn.query(select_sql).unwrap();
conn.execute(delete_sql).unwrap();
}
let snap_warm = hot_path_profile_snapshot();
eprintln!("=== bd-db300.4.2.3 Churn Measurement Scorecard ===");
eprintln!("Cold (first iteration):");
eprintln!(
" parse: hits={:>4} misses={:>4}",
snap_cold.parser.parse_cache_hits, snap_cold.parser.parse_cache_misses
);
eprintln!(
" compiled: hits={:>4} misses={:>4}",
snap_cold.parser.compiled_cache_hits, snap_cold.parser.compiled_cache_misses
);
eprintln!(
" prepared: hits={:>4} misses={:>4}",
snap_cold.parser.prepared_cache_hits, snap_cold.parser.prepared_cache_misses
);
eprintln!("Warm (100 iterations, 3 statements each = 300 statement dispatches):");
eprintln!(
" parse: hits={:>4} misses={:>4} hit_rate={:.1}%",
snap_warm.parser.parse_cache_hits,
snap_warm.parser.parse_cache_misses,
100.0 * snap_warm.parser.parse_cache_hits as f64
/ (snap_warm.parser.parse_cache_hits + snap_warm.parser.parse_cache_misses).max(1)
as f64,
);
eprintln!(
" compiled: hits={:>4} misses={:>4} hit_rate={:.1}%",
snap_warm.parser.compiled_cache_hits,
snap_warm.parser.compiled_cache_misses,
100.0 * snap_warm.parser.compiled_cache_hits as f64
/ (snap_warm.parser.compiled_cache_hits + snap_warm.parser.compiled_cache_misses).max(1)
as f64,
);
eprintln!(
" prepared: hits={:>4} misses={:>4}",
snap_warm.parser.prepared_cache_hits, snap_warm.parser.prepared_cache_misses
);
eprintln!(
" parse_time_ns={}, compile_time_ns={}",
snap_warm.parser.parse_time_ns, snap_warm.parser.compile_time_ns
);
eprintln!("=== END SCORECARD ===");
eprintln!(
" fast_path={}, slow_path={}",
snap_warm.parser.fast_path_executions, snap_warm.parser.slow_path_executions
);
assert!(
snap_warm.parser.parse_cache_hits > snap_warm.parser.parse_cache_misses,
"warm loop: parse cache hits ({}) should exceed misses ({})",
snap_warm.parser.parse_cache_hits,
snap_warm.parser.parse_cache_misses
);
}
#[test]
fn test_rollback_transaction_with_ddl_preserves_cache() {
let _profile_guard = HotPathProfileTestGuard::new();
let conn = Connection::open(":memory:").unwrap();
conn.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, val TEXT)")
.unwrap();
conn.execute("INSERT INTO t VALUES(1, 'stable')").unwrap();
let _ = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
let _ = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
conn.execute("BEGIN").unwrap();
conn.execute("CREATE TABLE t_temp(x INTEGER)").unwrap();
conn.execute("ROLLBACK").unwrap();
let rows = conn.query("SELECT val FROM t WHERE id = 1").unwrap();
assert_eq!(rows.len(), 1, "rolled-back DDL should not affect data");
assert_eq!(
rows[0].get(0),
Some(&fsqlite_types::SqliteValue::Text("stable".into())),
"result must be correct after rolled-back DDL"
);
let result = conn.query("SELECT * FROM t_temp");
assert!(result.is_err(), "t_temp should not exist after ROLLBACK");
}
#[test]
fn test_rapid_schema_churn_invalidation_and_recovery() {
let _profile_guard = HotPathProfileTestGuard::new();
let conn = Connection::open(":memory:").unwrap();
conn.execute("CREATE TABLE stable(id INTEGER PRIMARY KEY, val TEXT)")
.unwrap();
conn.execute("INSERT INTO stable VALUES(1, 'anchor')")
.unwrap();
let _ = conn.query("SELECT val FROM stable WHERE id = 1").unwrap();
let _ = conn.query("SELECT val FROM stable WHERE id = 1").unwrap();
for i in 0..5 {
conn.execute(&format!("CREATE TABLE churn_{i}(x INTEGER)"))
.unwrap();
}
for i in 0..5 {
conn.execute(&format!("DROP TABLE churn_{i}")).unwrap();
}
conn.execute("ALTER TABLE stable ADD COLUMN extra INTEGER DEFAULT 0")
.unwrap();
let rows = conn.query("SELECT val FROM stable WHERE id = 1").unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0].get(0),
Some(&fsqlite_types::SqliteValue::Text("anchor".into())),
"stable table data must survive schema churn"
);
let (ph_pre, _, _, _, _, _) = cache_snapshot();
let _ = conn.query("SELECT val FROM stable WHERE id = 1").unwrap();
let (ph_post, _, _, _, _, _) = cache_snapshot();
assert!(
ph_post > ph_pre,
"cache should recover after schema churn: {ph_pre} -> {ph_post}"
);
}
#[test]
fn test_reprepare_after_schema_changed_returns_correct_results() {
let conn = Connection::open(":memory:").unwrap();
conn.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, val TEXT)")
.unwrap();
conn.execute("INSERT INTO t VALUES(1, 'original')").unwrap();
let stmt = conn.prepare("SELECT val FROM t WHERE id = ?1").unwrap();
let rows = stmt
.query_with_params(&[fsqlite_types::SqliteValue::Integer(1)])
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(
rows[0].get(0),
Some(&fsqlite_types::SqliteValue::Text("original".into())),
"prepared query should read the original value before schema churn"
);
conn.execute("ALTER TABLE t ADD COLUMN extra INTEGER DEFAULT 42")
.unwrap();
let result = stmt.query_with_params(&[fsqlite_types::SqliteValue::Integer(1)]);
let schema_changed = matches!(result, Err(fsqlite_error::FrankenError::SchemaChanged));
if schema_changed {
let stmt2 = conn
.prepare("SELECT val, extra FROM t WHERE id = ?1")
.unwrap();
let rows2 = stmt2
.query_with_params(&[fsqlite_types::SqliteValue::Integer(1)])
.unwrap();
assert_eq!(rows2.len(), 1, "re-prepared query should return 1 row");
assert_eq!(
rows2[0].get(0),
Some(&fsqlite_types::SqliteValue::Text("original".into())),
"re-prepared query should preserve the original column value"
);
assert_eq!(
rows2[0].get(1),
Some(&fsqlite_types::SqliteValue::Integer(42)),
"new column should have DEFAULT 42"
);
} else {
let rows = result.unwrap();
assert_eq!(rows.len(), 1, "transparent re-prepare should return 1 row");
assert_eq!(
rows[0].get(0),
Some(&fsqlite_types::SqliteValue::Text("original".into())),
"transparent re-prepare should preserve the original value"
);
}
}