use std::fs;
use std::path::Path;
use stoolap::Database;
fn qi64(db: &Database, sql: &str) -> i64 {
db.query_one::<i64, _>(sql, ()).unwrap()
}
fn qf64(db: &Database, sql: &str) -> f64 {
db.query_one::<f64, _>(sql, ()).unwrap()
}
fn count_rows(db: &Database, sql: &str) -> i64 {
let mut rows = db.query(sql, ()).unwrap();
let mut n = 0i64;
while rows.next().is_some() {
n += 1;
}
n
}
fn remove_lock_file(base_dir: &Path, db_name: &str) {
let lock = base_dir.join(db_name).join("db.lock");
let _ = fs::remove_file(lock);
}
#[test]
fn test_add_column_nullable_after_checkpoint() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_null", dir.path().display());
{
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t1 (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
(),
)
.unwrap();
db.execute("INSERT INTO t1 VALUES (1, 'Alice')", ())
.unwrap();
db.execute("INSERT INTO t1 VALUES (2, 'Bob')", ()).unwrap();
db.execute("INSERT INTO t1 VALUES (3, 'Carol')", ())
.unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t1 ADD COLUMN score INTEGER", ())
.unwrap();
let null_count = qi64(&db, "SELECT COUNT(*) FROM t1 WHERE score IS NULL");
assert_eq!(null_count, 3, "Cold rows should have NULL for new column");
db.execute("INSERT INTO t1 VALUES (4, 'Dave', 99)", ())
.unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t1"), 4);
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t1 WHERE score IS NULL"), 3);
assert_eq!(qi64(&db, "SELECT score FROM t1 WHERE id = 4"), 99);
assert_eq!(count_rows(&db, "SELECT * FROM t1"), 4);
db.close().unwrap();
}
{
let db = Database::open(&dsn).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t1"), 4);
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t1 WHERE score IS NULL"), 3);
assert_eq!(qi64(&db, "SELECT score FROM t1 WHERE id = 4"), 99);
db.execute("INSERT INTO t1 VALUES (5, 'Eve', 50)", ())
.unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t1"), 5);
assert_eq!(qi64(&db, "SELECT score FROM t1 WHERE id = 5"), 50);
db.close().unwrap();
}
}
#[test]
fn test_add_column_with_default_after_checkpoint() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_def", dir.path().display());
{
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t2 (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
(),
)
.unwrap();
db.execute("INSERT INTO t2 VALUES (1, 'Alice')", ())
.unwrap();
db.execute("INSERT INTO t2 VALUES (2, 'Bob')", ()).unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t2 ADD COLUMN level INTEGER DEFAULT 42", ())
.unwrap();
let level1 = qi64(&db, "SELECT level FROM t2 WHERE id = 1");
assert_eq!(
level1, 42,
"Cold row should get default value via individual read"
);
let level2 = qi64(&db, "SELECT level FROM t2 WHERE id = 2");
assert_eq!(
level2, 42,
"Cold row should get default value via individual read"
);
db.execute("INSERT INTO t2 (id, name) VALUES (3, 'Carol')", ())
.unwrap();
assert_eq!(qi64(&db, "SELECT level FROM t2 WHERE id = 3"), 42);
db.execute("INSERT INTO t2 VALUES (4, 'Dave', 100)", ())
.unwrap();
assert_eq!(qi64(&db, "SELECT level FROM t2 WHERE id = 4"), 100);
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t2"), 4);
db.close().unwrap();
}
{
let db = Database::open(&dsn).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t2"), 4);
assert_eq!(qi64(&db, "SELECT level FROM t2 WHERE id = 1"), 42);
assert_eq!(qi64(&db, "SELECT level FROM t2 WHERE id = 2"), 42);
assert_eq!(qi64(&db, "SELECT level FROM t2 WHERE id = 3"), 42);
assert_eq!(qi64(&db, "SELECT level FROM t2 WHERE id = 4"), 100);
db.close().unwrap();
}
}
#[test]
fn test_multiple_add_columns_across_checkpoints() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_multi", dir.path().display());
{
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t3 (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
(),
)
.unwrap();
db.execute("INSERT INTO t3 VALUES (1, 'row1')", ()).unwrap();
db.execute("INSERT INTO t3 VALUES (2, 'row2')", ()).unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t3 ADD COLUMN col_a INTEGER", ())
.unwrap();
db.execute("INSERT INTO t3 VALUES (3, 'row3', 30)", ())
.unwrap();
db.execute("INSERT INTO t3 VALUES (4, 'row4', 40)", ())
.unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t3 ADD COLUMN col_b TEXT", ())
.unwrap();
db.execute("INSERT INTO t3 VALUES (5, 'row5', 50, 'five')", ())
.unwrap();
db.execute("INSERT INTO t3 VALUES (6, 'row6', 60, 'six')", ())
.unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t3"), 6);
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t3 WHERE col_a IS NULL"), 2);
assert_eq!(
qi64(&db, "SELECT COUNT(*) FROM t3 WHERE col_a IS NOT NULL"),
4
);
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t3 WHERE col_b IS NULL"), 4);
assert_eq!(
qi64(&db, "SELECT COUNT(*) FROM t3 WHERE col_b IS NOT NULL"),
2
);
db.close().unwrap();
}
{
let db = Database::open(&dsn).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t3"), 6);
assert_eq!(
qi64(&db, "SELECT COUNT(*) FROM t3 WHERE col_a IS NULL"),
2,
"Rows 1-2 should still have NULL col_a after reopen"
);
assert_eq!(qi64(&db, "SELECT col_a FROM t3 WHERE id = 3"), 30);
assert_eq!(qi64(&db, "SELECT col_a FROM t3 WHERE id = 5"), 50);
assert_eq!(
qi64(&db, "SELECT COUNT(*) FROM t3 WHERE col_b IS NULL"),
4,
"Rows 1-4 should have NULL col_b after reopen"
);
let col_b_val: String = db
.query_one("SELECT col_b FROM t3 WHERE id = 5", ())
.unwrap();
assert_eq!(col_b_val, "five");
let col_b_val: String = db
.query_one("SELECT col_b FROM t3 WHERE id = 6", ())
.unwrap();
assert_eq!(col_b_val, "six");
db.execute("INSERT INTO t3 VALUES (7, 'row7', 70, 'seven')", ())
.unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t3"), 7);
db.close().unwrap();
}
}
#[test]
fn test_add_column_then_update_cold_rows() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_upd", dir.path().display());
{
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t4 (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
(),
)
.unwrap();
db.execute("INSERT INTO t4 VALUES (1, 'Alice')", ())
.unwrap();
db.execute("INSERT INTO t4 VALUES (2, 'Bob')", ()).unwrap();
db.execute("INSERT INTO t4 VALUES (3, 'Carol')", ())
.unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t4 ADD COLUMN age INTEGER", ())
.unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t4 WHERE age IS NULL"), 3);
db.execute("UPDATE t4 SET age = 30 WHERE id = 1", ())
.unwrap();
db.execute("UPDATE t4 SET age = 25 WHERE id = 2", ())
.unwrap();
db.execute("UPDATE t4 SET age = 35 WHERE id = 3", ())
.unwrap();
assert_eq!(qi64(&db, "SELECT age FROM t4 WHERE id = 1"), 30);
assert_eq!(qi64(&db, "SELECT age FROM t4 WHERE id = 2"), 25);
assert_eq!(qi64(&db, "SELECT age FROM t4 WHERE id = 3"), 35);
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t4 WHERE age IS NULL"), 0);
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
assert_eq!(qi64(&db, "SELECT age FROM t4 WHERE id = 1"), 30);
assert_eq!(qi64(&db, "SELECT SUM(age) FROM t4"), 90);
db.close().unwrap();
}
{
let db = Database::open(&dsn).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t4"), 3);
assert_eq!(qi64(&db, "SELECT age FROM t4 WHERE id = 1"), 30);
assert_eq!(qi64(&db, "SELECT age FROM t4 WHERE id = 2"), 25);
assert_eq!(qi64(&db, "SELECT age FROM t4 WHERE id = 3"), 35);
assert_eq!(qi64(&db, "SELECT SUM(age) FROM t4"), 90);
db.close().unwrap();
}
}
#[test]
fn test_recovery_after_close_with_wal_data() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_recv", dir.path().display());
{
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t5 (id INTEGER PRIMARY KEY, val INTEGER NOT NULL)",
(),
)
.unwrap();
db.execute("BEGIN", ()).unwrap();
let stmt = db.prepare("INSERT INTO t5 VALUES ($1, $2)").unwrap();
for i in 0..100i64 {
stmt.execute((i, i * 10)).unwrap();
}
db.execute("COMMIT", ()).unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("BEGIN", ()).unwrap();
for i in 100..150i64 {
stmt.execute((i, i * 10)).unwrap();
}
db.execute("COMMIT", ()).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t5"), 150);
db.close().unwrap();
}
{
let db = Database::open(&dsn).unwrap();
let count = qi64(&db, "SELECT COUNT(*) FROM t5");
assert_eq!(count, 150, "All 150 rows should survive close+reopen");
assert_eq!(qi64(&db, "SELECT val FROM t5 WHERE id = 0"), 0);
assert_eq!(qi64(&db, "SELECT val FROM t5 WHERE id = 99"), 990);
assert_eq!(qi64(&db, "SELECT val FROM t5 WHERE id = 100"), 1000);
assert_eq!(qi64(&db, "SELECT val FROM t5 WHERE id = 149"), 1490);
db.execute("INSERT INTO t5 VALUES (200, 2000)", ()).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t5"), 151);
db.close().unwrap();
}
}
#[test]
fn test_recovery_preserves_schema_evolution() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_evo", dir.path().display());
{
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t6 (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
(),
)
.unwrap();
db.execute("INSERT INTO t6 VALUES (1, 'Alice')", ())
.unwrap();
db.execute("INSERT INTO t6 VALUES (2, 'Bob')", ()).unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t6 ADD COLUMN email TEXT DEFAULT 'unknown'", ())
.unwrap();
db.execute("INSERT INTO t6 VALUES (3, 'Carol', 'carol@test.com')", ())
.unwrap();
db.execute("INSERT INTO t6 VALUES (4, 'Dave', 'dave@test.com')", ())
.unwrap();
db.close().unwrap();
}
{
let db = Database::open(&dsn).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t6"), 4);
let email3: String = db
.query_one("SELECT email FROM t6 WHERE id = 3", ())
.unwrap();
assert_eq!(email3, "carol@test.com");
let email1: String = db
.query_one("SELECT email FROM t6 WHERE id = 1", ())
.unwrap();
assert_eq!(email1, "unknown");
db.close().unwrap();
}
{
let db = Database::open(&dsn).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t6"), 4);
let email2: String = db
.query_one("SELECT email FROM t6 WHERE id = 2", ())
.unwrap();
assert_eq!(email2, "unknown");
let email4: String = db
.query_one("SELECT email FROM t6 WHERE id = 4", ())
.unwrap();
assert_eq!(email4, "dave@test.com");
db.execute("INSERT INTO t6 VALUES (5, 'Eve', 'eve@test.com')", ())
.unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t6"), 5);
db.close().unwrap();
}
}
#[test]
fn test_column_projection_after_schema_evolution() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_proj", dir.path().display());
{
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t7 (id INTEGER PRIMARY KEY, a TEXT NOT NULL, b INTEGER NOT NULL)",
(),
)
.unwrap();
db.execute("INSERT INTO t7 VALUES (1, 'x', 10)", ())
.unwrap();
db.execute("INSERT INTO t7 VALUES (2, 'y', 20)", ())
.unwrap();
db.execute("INSERT INTO t7 VALUES (3, 'z', 30)", ())
.unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t7 ADD COLUMN c TEXT", ()).unwrap();
db.execute("INSERT INTO t7 VALUES (4, 'w', 40, 'new')", ())
.unwrap();
assert_eq!(
qi64(&db, "SELECT COUNT(*) FROM t7 WHERE c IS NULL"),
3,
"Cold rows 1-3 should have NULL c"
);
assert_eq!(
qi64(&db, "SELECT COUNT(*) FROM t7 WHERE c IS NOT NULL"),
1,
"Only row 4 should have non-NULL c"
);
let mut rows = db.query("SELECT id, c FROM t7 ORDER BY id", ()).unwrap();
let mut found_new = false;
for _ in 0..4 {
let row = rows.next().unwrap().unwrap();
let id: i64 = row.get(0).unwrap();
if id == 4 {
let c_val: String = row.get(1).unwrap();
assert_eq!(c_val, "new", "Row 4 c should be 'new'");
found_new = true;
}
}
assert!(found_new, "Should have found row 4 with c='new'");
drop(rows);
let mut rows = db.query("SELECT id, b FROM t7 ORDER BY id", ()).unwrap();
let r1 = rows.next().unwrap().unwrap();
assert_eq!(r1.get::<i64>(0).unwrap(), 1);
assert_eq!(r1.get::<i64>(1).unwrap(), 10);
let r4 = rows.nth(2).unwrap().unwrap();
assert_eq!(r4.get::<i64>(0).unwrap(), 4);
assert_eq!(r4.get::<i64>(1).unwrap(), 40);
drop(rows);
db.close().unwrap();
}
{
let db = Database::open(&dsn).unwrap();
let null_c = qi64(&db, "SELECT COUNT(*) FROM t7 WHERE c IS NULL");
assert_eq!(null_c, 3, "3 cold rows should have NULL c after reopen");
let val_c: String = db.query_one("SELECT c FROM t7 WHERE id = 4", ()).unwrap();
assert_eq!(val_c, "new");
assert_eq!(qi64(&db, "SELECT SUM(b) FROM t7"), 100);
db.close().unwrap();
}
}
#[test]
fn test_aggregation_on_new_column_mixed_hot_cold() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_agg", dir.path().display());
{
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t8 (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
(),
)
.unwrap();
db.execute("INSERT INTO t8 VALUES (1, 'a')", ()).unwrap();
db.execute("INSERT INTO t8 VALUES (2, 'b')", ()).unwrap();
db.execute("INSERT INTO t8 VALUES (3, 'c')", ()).unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t8 ADD COLUMN score FLOAT", ())
.unwrap();
db.execute("INSERT INTO t8 VALUES (4, 'd', 10.5)", ())
.unwrap();
db.execute("INSERT INTO t8 VALUES (5, 'e', 20.5)", ())
.unwrap();
db.execute("INSERT INTO t8 VALUES (6, 'f', 30.0)", ())
.unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t8"), 6);
let count_score = qi64(&db, "SELECT COUNT(score) FROM t8");
assert_eq!(
count_score, 3,
"COUNT(score) should only count the 3 hot rows with non-NULL score"
);
let sum = qf64(&db, "SELECT SUM(score) FROM t8");
assert!(
(sum - 61.0).abs() < 0.01,
"SUM(score) should be 61.0 (10.5+20.5+30.0), got {}",
sum
);
let avg = qf64(&db, "SELECT AVG(score) FROM t8");
let expected_avg = 61.0 / 3.0;
assert!(
(avg - expected_avg).abs() < 0.01,
"AVG(score) should be ~{:.4}, got {}",
expected_avg,
avg
);
let min = qf64(&db, "SELECT MIN(score) FROM t8");
assert!(
(min - 10.5).abs() < 0.01,
"MIN(score) should be 10.5, got {}",
min
);
let max = qf64(&db, "SELECT MAX(score) FROM t8");
assert!(
(max - 30.0).abs() < 0.01,
"MAX(score) should be 30.0, got {}",
max
);
db.close().unwrap();
}
{
let db = Database::open(&dsn).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t8"), 6);
assert_eq!(qi64(&db, "SELECT COUNT(score) FROM t8"), 3);
let sum = qf64(&db, "SELECT SUM(score) FROM t8");
assert!(
(sum - 61.0).abs() < 0.01,
"SUM(score) after reopen should be 61.0, got {}",
sum
);
db.close().unwrap();
}
}
#[test]
fn test_recovery_dirty_shutdown_drop_without_close() {
let dir = tempfile::tempdir().unwrap();
let db_name = "schema_dirty";
let dsn = format!("file://{}/{}", dir.path().display(), db_name);
{
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t9 (id INTEGER PRIMARY KEY, val TEXT NOT NULL)",
(),
)
.unwrap();
db.execute("BEGIN", ()).unwrap();
let stmt = db.prepare("INSERT INTO t9 VALUES ($1, $2)").unwrap();
for i in 0..50i64 {
stmt.execute((i, format!("val_{}", i))).unwrap();
}
db.execute("COMMIT", ()).unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("BEGIN", ()).unwrap();
for i in 50..80i64 {
stmt.execute((i, format!("val_{}", i))).unwrap();
}
db.execute("COMMIT", ()).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t9"), 80);
drop(db);
}
remove_lock_file(dir.path(), db_name);
{
let db = Database::open(&dsn).unwrap();
let count = qi64(&db, "SELECT COUNT(*) FROM t9");
assert_eq!(count, 80, "All 80 rows should survive drop+reopen");
let v0: String = db.query_one("SELECT val FROM t9 WHERE id = 0", ()).unwrap();
assert_eq!(v0, "val_0");
let v79: String = db
.query_one("SELECT val FROM t9 WHERE id = 79", ())
.unwrap();
assert_eq!(v79, "val_79");
db.close().unwrap();
}
}
#[test]
fn test_add_column_default_text_multi_checkpoint_reopen() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_deftxt", dir.path().display());
{
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t10 (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
(),
)
.unwrap();
db.execute("INSERT INTO t10 VALUES (1, 'one')", ()).unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute(
"ALTER TABLE t10 ADD COLUMN status TEXT DEFAULT 'pending'",
(),
)
.unwrap();
db.execute("INSERT INTO t10 VALUES (2, 'two', 'active')", ())
.unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t10 ADD COLUMN priority INTEGER DEFAULT 0", ())
.unwrap();
db.execute("INSERT INTO t10 VALUES (3, 'three', 'done', 5)", ())
.unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t10"), 3);
let s1: String = db
.query_one("SELECT status FROM t10 WHERE id = 1", ())
.unwrap();
assert_eq!(s1, "pending");
assert_eq!(qi64(&db, "SELECT priority FROM t10 WHERE id = 1"), 0);
let s2: String = db
.query_one("SELECT status FROM t10 WHERE id = 2", ())
.unwrap();
assert_eq!(s2, "active");
assert_eq!(qi64(&db, "SELECT priority FROM t10 WHERE id = 2"), 0);
let s3: String = db
.query_one("SELECT status FROM t10 WHERE id = 3", ())
.unwrap();
assert_eq!(s3, "done");
assert_eq!(qi64(&db, "SELECT priority FROM t10 WHERE id = 3"), 5);
db.close().unwrap();
}
{
let db = Database::open(&dsn).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t10"), 3);
let s1: String = db
.query_one("SELECT status FROM t10 WHERE id = 1", ())
.unwrap();
assert_eq!(s1, "pending", "Default text should persist for cold row");
assert_eq!(
qi64(&db, "SELECT priority FROM t10 WHERE id = 1"),
0,
"Default integer should persist for cold row"
);
let s2: String = db
.query_one("SELECT status FROM t10 WHERE id = 2", ())
.unwrap();
assert_eq!(s2, "active");
assert_eq!(qi64(&db, "SELECT priority FROM t10 WHERE id = 3"), 5);
db.close().unwrap();
}
}
#[test]
fn test_filtered_query_on_new_column_cold_data() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_filter", dir.path().display());
{
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t11 (id INTEGER PRIMARY KEY, group_name TEXT NOT NULL)",
(),
)
.unwrap();
for i in 1..=10 {
db.execute(
&format!(
"INSERT INTO t11 VALUES ({}, 'group_{}')",
i,
if i % 2 == 0 { "even" } else { "odd" }
),
(),
)
.unwrap();
}
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t11 ADD COLUMN score INTEGER DEFAULT 0", ())
.unwrap();
db.execute("UPDATE t11 SET score = 100 WHERE id <= 5", ())
.unwrap();
for i in 11..=15 {
db.execute(
&format!("INSERT INTO t11 VALUES ({}, 'group_new', {})", i, i * 10),
(),
)
.unwrap();
}
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t11"), 15);
let high_score = qi64(&db, "SELECT COUNT(*) FROM t11 WHERE score >= 100");
assert_eq!(
high_score, 10,
"5 updated cold + 5 new hot rows with score >= 100"
);
let default_score = qi64(&db, "SELECT COUNT(*) FROM t11 WHERE score = 0");
assert_eq!(
default_score, 5,
"5 cold rows 6-10 should have default score 0"
);
db.close().unwrap();
}
{
let db = Database::open(&dsn).unwrap();
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t11"), 15);
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t11 WHERE score >= 100"), 10);
assert_eq!(qi64(&db, "SELECT COUNT(*) FROM t11 WHERE score = 0"), 5);
db.close().unwrap();
}
}
#[test]
fn test_order_by_new_column_mixed_data() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_order", dir.path().display());
{
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t12 (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
(),
)
.unwrap();
db.execute("INSERT INTO t12 VALUES (1, 'first')", ())
.unwrap();
db.execute("INSERT INTO t12 VALUES (2, 'second')", ())
.unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t12 ADD COLUMN rank INTEGER", ())
.unwrap();
db.execute("INSERT INTO t12 VALUES (3, 'third', 1)", ())
.unwrap();
db.execute("INSERT INTO t12 VALUES (4, 'fourth', 2)", ())
.unwrap();
let ordered_count = count_rows(&db, "SELECT * FROM t12 ORDER BY rank ASC");
assert_eq!(
ordered_count, 4,
"All rows should appear in ORDER BY result"
);
let ordered_count = count_rows(&db, "SELECT * FROM t12 ORDER BY rank DESC");
assert_eq!(ordered_count, 4);
let filtered = count_rows(
&db,
"SELECT * FROM t12 WHERE rank IS NOT NULL ORDER BY rank",
);
assert_eq!(filtered, 2, "Only rows with non-NULL rank");
db.close().unwrap();
}
}
#[test]
fn test_create_index_after_drop_add_column_with_cold_data() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_idx", dir.path().display());
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t_idx (id INTEGER PRIMARY KEY, name TEXT, email TEXT)",
(),
)
.unwrap();
db.execute(
"INSERT INTO t_idx VALUES (1, 'Alice', 'alice@test.com')",
(),
)
.unwrap();
db.execute("INSERT INTO t_idx VALUES (2, 'Bob', 'bob@test.com')", ())
.unwrap();
db.execute(
"INSERT INTO t_idx VALUES (3, 'Carol', 'carol@test.com')",
(),
)
.unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t_idx DROP COLUMN email", ())
.unwrap();
db.execute("ALTER TABLE t_idx ADD COLUMN phone TEXT", ())
.unwrap();
db.execute("INSERT INTO t_idx VALUES (4, 'Dave', '555-1234')", ())
.unwrap();
db.execute("CREATE INDEX idx_phone ON t_idx (phone)", ())
.expect("CREATE INDEX after schema evolution should succeed");
let phone: String = db
.query_one("SELECT COALESCE(phone, 'none') FROM t_idx WHERE id = 1", ())
.unwrap();
assert_eq!(phone, "none", "Cold row should have NULL phone");
let phone: String = db
.query_one("SELECT phone FROM t_idx WHERE id = 4", ())
.unwrap();
assert_eq!(phone, "555-1234", "Hot row should have phone value");
db.close().unwrap();
}
#[test]
fn test_create_unique_index_after_schema_evolution() {
let dir = tempfile::tempdir().unwrap();
let dsn = format!("file://{}/schema_uniq", dir.path().display());
let db = Database::open(&dsn).unwrap();
db.execute(
"CREATE TABLE t_uniq (id INTEGER PRIMARY KEY, col_a TEXT, col_b TEXT)",
(),
)
.unwrap();
db.execute("INSERT INTO t_uniq VALUES (1, 'x', 'dup')", ())
.unwrap();
db.execute("INSERT INTO t_uniq VALUES (2, 'y', 'dup')", ())
.unwrap();
db.execute("PRAGMA CHECKPOINT", ()).unwrap();
db.execute("ALTER TABLE t_uniq DROP COLUMN col_b", ())
.unwrap();
db.execute("ALTER TABLE t_uniq ADD COLUMN col_c TEXT", ())
.unwrap();
db.execute("INSERT INTO t_uniq VALUES (3, 'z', 'unique_val')", ())
.unwrap();
db.execute("CREATE UNIQUE INDEX idx_uniq_c ON t_uniq (col_c)", ())
.expect("Unique index on new column should succeed (cold rows are NULL)");
db.close().unwrap();
}