use citadel::{Argon2Profile, DatabaseBuilder};
use citadel_sql::{Connection, ExecutionResult, SqlError, Value};
fn create_db(dir: &std::path::Path) -> citadel::Database {
let db_path = dir.join("test.db");
DatabaseBuilder::new(db_path)
.passphrase(b"test-passphrase")
.argon2_profile(Argon2Profile::Iot)
.create()
.unwrap()
}
fn open_db(dir: &std::path::Path) -> citadel::Database {
let db_path = dir.join("test.db");
DatabaseBuilder::new(db_path)
.passphrase(b"test-passphrase")
.argon2_profile(Argon2Profile::Iot)
.open()
.unwrap()
}
fn assert_rows_affected(result: ExecutionResult, expected: u64) {
match result {
ExecutionResult::RowsAffected(n) => assert_eq!(n, expected),
other => panic!("expected RowsAffected({expected}), got {other:?}"),
}
}
fn assert_ok(result: ExecutionResult) {
match result {
ExecutionResult::Ok => {}
other => panic!("expected Ok, got {other:?}"),
}
}
fn setup_users_table(conn: &Connection) {
assert_ok(conn.execute(
"CREATE TABLE users (id INTEGER NOT NULL PRIMARY KEY, name TEXT, email TEXT, age INTEGER)"
).unwrap());
}
fn insert_users(conn: &Connection) {
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'alice@test.com', 30)",
)
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'bob@test.com', 25)",
)
.unwrap(),
1,
);
assert_rows_affected(conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (3, 'Charlie', 'charlie@test.com', 35)"
).unwrap(), 1);
}
#[test]
fn create_non_unique_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
}
#[test]
fn create_unique_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
}
#[test]
fn create_index_on_empty_table() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'alice@test.com', 30)",
)
.unwrap(),
1,
);
let qr = conn.query("SELECT * FROM users").unwrap();
assert_eq!(qr.rows.len(), 1);
}
#[test]
fn create_multi_column_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name_age ON users (name, age)")
.unwrap(),
);
}
#[test]
fn create_unique_multi_column_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_name_email ON users (name, email)")
.unwrap(),
);
}
#[test]
fn create_index_if_not_exists_on_existing() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX IF NOT EXISTS idx_name ON users (name)")
.unwrap(),
);
}
#[test]
fn create_index_if_not_exists_new() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX IF NOT EXISTS idx_name ON users (name)")
.unwrap(),
);
}
#[test]
fn create_index_on_nonexistent_table() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
let err = conn
.execute("CREATE INDEX idx_name ON ghost (name)")
.unwrap_err();
assert!(matches!(err, SqlError::TableNotFound(_)));
}
#[test]
fn create_index_on_nonexistent_column() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
let err = conn
.execute("CREATE INDEX idx_bad ON users (nonexistent)")
.unwrap_err();
assert!(matches!(err, SqlError::ColumnNotFound(_)));
}
#[test]
fn create_duplicate_index_name() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
let err = conn
.execute("CREATE INDEX idx_name ON users (name)")
.unwrap_err();
assert!(matches!(err, SqlError::IndexAlreadyExists(_)));
}
#[test]
fn create_unique_index_violates_existing_data() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'same@test.com', 30)",
)
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'same@test.com', 25)",
)
.unwrap(),
1,
);
let err = conn
.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn create_unique_index_allows_null_duplicates_in_existing_data() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', NULL, 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', NULL, 25)")
.unwrap(),
1,
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
}
#[test]
fn create_index_case_insensitive_name() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX IDX_NAME ON users (name)")
.unwrap(),
);
let err = conn
.execute("CREATE INDEX idx_name ON users (name)")
.unwrap_err();
assert!(matches!(err, SqlError::IndexAlreadyExists(_)));
}
#[test]
fn drop_index_basic() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(conn.execute("DROP INDEX idx_name").unwrap());
}
#[test]
fn drop_nonexistent_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
let err = conn.execute("DROP INDEX ghost_idx").unwrap_err();
assert!(matches!(err, SqlError::IndexNotFound(_)));
}
#[test]
fn drop_index_if_exists_nonexistent() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(conn.execute("DROP INDEX IF EXISTS ghost_idx").unwrap());
}
#[test]
fn drop_index_if_exists_existing() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(conn.execute("DROP INDEX IF EXISTS idx_name").unwrap());
}
#[test]
fn drop_and_recreate_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(conn.execute("DROP INDEX idx_name").unwrap());
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
}
#[test]
fn drop_index_case_insensitive() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(conn.execute("DROP INDEX IDX_NAME").unwrap());
}
#[test]
fn drop_table_cascades_indexes() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_ok(conn.execute("DROP TABLE users").unwrap());
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
}
#[test]
fn drop_table_if_exists_with_indexes() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(conn.execute("DROP TABLE IF EXISTS users").unwrap());
assert_ok(conn.execute("DROP TABLE IF EXISTS users").unwrap());
}
#[test]
fn insert_populates_non_unique_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'a@t.com', 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Alice', 'b@t.com', 25)")
.unwrap(),
1,
);
let qr = conn
.query("SELECT * FROM users WHERE name = 'Alice'")
.unwrap();
assert_eq!(qr.rows.len(), 2);
}
#[test]
fn insert_populates_unique_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'alice@t.com', 30)",
)
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'bob@t.com', 25)")
.unwrap(),
1,
);
}
#[test]
fn insert_violates_unique_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'same@t.com', 30)",
)
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'same@t.com', 25)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn insert_null_in_unique_index_allowed() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', NULL, 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', NULL, 25)")
.unwrap(),
1,
);
let qr = conn
.query("SELECT * FROM users WHERE email IS NULL")
.unwrap();
assert_eq!(qr.rows.len(), 2);
}
#[test]
fn insert_multiple_nulls_unique_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
for i in 1..=10 {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({i}, 'User{i}', NULL, {i})"
))
.unwrap(),
1,
);
}
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(10));
}
#[test]
fn insert_with_multiple_indexes_on_same_table() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_ok(conn.execute("CREATE INDEX idx_age ON users (age)").unwrap());
insert_users(&conn);
let qr = conn.query("SELECT * FROM users").unwrap();
assert_eq!(qr.rows.len(), 3);
}
#[test]
fn insert_unique_violation_on_multicolumn_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_name_age ON users (name, age)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'a@t.com', 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'b@t.com', 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (3, 'Alice', 'c@t.com', 25)")
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (4, 'Alice', 'd@t.com', 30)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn insert_multicolumn_unique_allows_partial_null() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_name_email ON users (name, email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', NULL, 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Alice', NULL, 25)")
.unwrap(),
1,
);
}
#[test]
fn delete_removes_index_entries() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(conn.execute("DELETE FROM users WHERE id = 1").unwrap(), 1);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap(),
1,
);
}
#[test]
fn delete_all_rows_cleans_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(conn.execute("DELETE FROM users").unwrap(), 3);
insert_users(&conn);
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(3));
}
#[test]
fn delete_with_where_cleans_correct_entries() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(conn.execute("DELETE FROM users WHERE age < 30").unwrap(), 1);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'bob@test.com', 40)",
)
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (5, 'Eve', 'alice@test.com', 22)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn delete_with_multiple_indexes() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_ok(conn.execute("CREATE INDEX idx_age ON users (age)").unwrap());
insert_users(&conn);
assert_rows_affected(conn.execute("DELETE FROM users WHERE id = 2").unwrap(), 1);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'bob@test.com', 25)",
)
.unwrap(),
1,
);
}
#[test]
fn delete_null_indexed_value() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', NULL, 30)")
.unwrap(),
1,
);
assert_rows_affected(conn.execute("DELETE FROM users WHERE id = 1").unwrap(), 1);
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(0));
}
#[test]
fn update_non_indexed_column_no_index_change() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(
conn.execute("UPDATE users SET age = 99 WHERE id = 1")
.unwrap(),
1,
);
let err = conn
.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn update_indexed_column_updates_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(
conn.execute("UPDATE users SET email = 'newalice@test.com' WHERE id = 1")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap(),
1,
);
let err = conn
.execute(
"INSERT INTO users (id, name, email, age) VALUES (5, 'Eve', 'newalice@test.com', 22)",
)
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn update_causes_unique_violation() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
let err = conn
.execute("UPDATE users SET email = 'bob@test.com' WHERE id = 1")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn update_pk_column_updates_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(
conn.execute("UPDATE users SET id = 100 WHERE id = 1")
.unwrap(),
1,
);
let err = conn
.execute(
"INSERT INTO users (id, name, email, age) VALUES (200, 'Dave', 'alice@test.com', 40)",
)
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
let qr = conn
.query("SELECT id, email FROM users WHERE id = 100")
.unwrap();
assert_eq!(qr.rows.len(), 1);
assert_eq!(qr.rows[0][1], Value::Text("alice@test.com".into()));
}
#[test]
fn update_sets_indexed_column_to_null() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(
conn.execute("UPDATE users SET email = NULL WHERE id = 1")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap(),
1,
);
}
#[test]
fn update_from_null_to_value_unique_check() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', NULL, 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'bob@test.com', 25)",
)
.unwrap(),
1,
);
let err = conn
.execute("UPDATE users SET email = 'bob@test.com' WHERE id = 1")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
assert_rows_affected(
conn.execute("UPDATE users SET email = 'alice@test.com' WHERE id = 1")
.unwrap(),
1,
);
}
#[test]
fn update_multiple_rows_indexed_column() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(
conn.execute("UPDATE users SET name = 'Everyone'").unwrap(),
3,
);
let qr = conn.query("SELECT name FROM users").unwrap();
for row in &qr.rows {
assert_eq!(row[0], Value::Text("Everyone".into()));
}
}
#[test]
fn update_multiple_rows_unique_violation() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
let err = conn
.execute("UPDATE users SET email = 'same@test.com'")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn update_with_multiple_indexes() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_ok(conn.execute("CREATE INDEX idx_age ON users (age)").unwrap());
insert_users(&conn);
assert_rows_affected(
conn.execute(
"UPDATE users SET name = 'Alicia', email = 'alicia@test.com', age = 31 WHERE id = 1",
)
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap(),
1,
);
}
#[test]
fn index_persists_across_reopen() {
let dir = tempfile::tempdir().unwrap();
{
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
}
{
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
let err = conn
.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'dave@test.com', 40)",
)
.unwrap(),
1,
);
}
}
#[test]
fn index_data_persists_after_insert_reopen() {
let dir = tempfile::tempdir().unwrap();
{
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
}
{
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(3));
let err = conn
.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'bob@test.com', 40)",
)
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
}
#[test]
fn create_index_reopen_then_insert() {
let dir = tempfile::tempdir().unwrap();
{
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
}
{
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
insert_users(&conn);
let err = conn
.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
}
#[test]
fn create_index_reopen_then_drop_index() {
let dir = tempfile::tempdir().unwrap();
{
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
}
{
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(conn.execute("DROP INDEX idx_email").unwrap());
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap(),
1,
);
}
}
#[test]
fn multiple_reopens_with_index_operations() {
let dir = tempfile::tempdir().unwrap();
{
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
}
{
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
}
{
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'dave@test.com', 40)",
)
.unwrap(),
1,
);
}
{
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(4));
let err = conn
.execute(
"INSERT INTO users (id, name, email, age) VALUES (5, 'Eve', 'dave@test.com', 22)",
)
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
}
#[test]
fn create_index_in_transaction_commit() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
conn.execute("BEGIN").unwrap();
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
conn.execute("COMMIT").unwrap();
let err = conn
.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn create_index_in_transaction_rollback() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
conn.execute("BEGIN").unwrap();
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
conn.execute("ROLLBACK").unwrap();
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
}
#[test]
fn drop_index_in_transaction_rollback() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
conn.execute("BEGIN").unwrap();
assert_ok(conn.execute("DROP INDEX idx_email").unwrap());
conn.execute("ROLLBACK").unwrap();
let err = conn
.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn insert_with_index_in_transaction_rollback() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
conn.execute("BEGIN").unwrap();
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'alice@t.com', 30)",
)
.unwrap(),
1,
);
conn.execute("ROLLBACK").unwrap();
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'alice@t.com', 30)",
)
.unwrap(),
1,
);
}
#[test]
fn update_with_index_in_transaction_commit() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
conn.execute("BEGIN").unwrap();
assert_rows_affected(
conn.execute("UPDATE users SET email = 'newalice@test.com' WHERE id = 1")
.unwrap(),
1,
);
conn.execute("COMMIT").unwrap();
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap(),
1,
);
}
#[test]
fn delete_with_index_in_transaction_commit() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
conn.execute("BEGIN").unwrap();
assert_rows_affected(conn.execute("DELETE FROM users WHERE id = 1").unwrap(), 1);
conn.execute("COMMIT").unwrap();
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap(),
1,
);
}
#[test]
fn mixed_ddl_dml_in_transaction() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
conn.execute("BEGIN").unwrap();
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'alice@t.com', 30)",
)
.unwrap(),
1,
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'bob@t.com', 25)")
.unwrap(),
1,
);
conn.execute("COMMIT").unwrap();
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(2));
}
#[test]
fn create_index_then_unique_violation_in_txn() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
conn.execute("BEGIN").unwrap();
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'a@t.com', 30)")
.unwrap(),
1,
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'a@t.com', 25)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn drop_table_cascade_in_transaction_commit() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
conn.execute("BEGIN").unwrap();
assert_ok(conn.execute("DROP TABLE users").unwrap());
conn.execute("COMMIT").unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
}
#[test]
fn drop_table_cascade_in_transaction_rollback() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
insert_users(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
conn.execute("BEGIN").unwrap();
assert_ok(conn.execute("DROP TABLE users").unwrap());
conn.execute("ROLLBACK").unwrap();
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(3));
let err = conn
.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn index_on_integer_column() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_age ON users (age)")
.unwrap(),
);
insert_users(&conn);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'd@t.com', 30)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn index_on_boolean_column() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute(
"CREATE TABLE flags (id INTEGER NOT NULL PRIMARY KEY, active BOOLEAN NOT NULL)",
)
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX idx_active ON flags (active)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO flags (id, active) VALUES (1, TRUE)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO flags (id, active) VALUES (2, FALSE)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO flags (id, active) VALUES (3, TRUE)")
.unwrap(),
1,
);
let qr = conn
.query("SELECT COUNT(*) FROM flags WHERE active = TRUE")
.unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(2));
}
#[test]
fn index_on_real_column() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE measurements (id INTEGER NOT NULL PRIMARY KEY, value REAL)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_value ON measurements (value)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO measurements (id, value) VALUES (1, 3.14)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO measurements (id, value) VALUES (2, 2.71)")
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO measurements (id, value) VALUES (3, 3.14)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn index_on_text_column() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'a@t.com', 30)")
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Alice', 'b@t.com', 25)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn index_on_nullable_text_with_mixed_values() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'A', 'a@t.com', 20)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'B', NULL, 25)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (3, 'C', 'c@t.com', 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (4, 'D', NULL, 35)")
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (5, 'E', 'a@t.com', 40)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (5, 'E', NULL, 40)")
.unwrap(),
1,
);
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(5));
}
#[test]
fn index_on_empty_string() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, '', 'a@t.com', 30)")
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (2, '', 'b@t.com', 25)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn multiple_indexes_on_same_column() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name1 ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX idx_name2 ON users (name)")
.unwrap(),
);
insert_users(&conn);
assert_ok(conn.execute("DROP INDEX idx_name1").unwrap());
let qr = conn
.query("SELECT * FROM users WHERE name = 'Alice'")
.unwrap();
assert_eq!(qr.rows.len(), 1);
}
#[test]
fn index_across_multiple_tables() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute(
"CREATE TABLE orders (id INTEGER NOT NULL PRIMARY KEY, user_id INTEGER, total REAL)",
)
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX idx_user_name ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX idx_order_user ON orders (user_id)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(
conn.execute("INSERT INTO orders (id, user_id, total) VALUES (1, 1, 99.99)")
.unwrap(),
1,
);
assert_ok(conn.execute("DROP TABLE orders").unwrap());
let qr = conn
.query("SELECT * FROM users WHERE name = 'Bob'")
.unwrap();
assert_eq!(qr.rows.len(), 1);
}
#[test]
fn create_drop_create_same_name_cycle() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
for _ in 0..5 {
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(conn.execute("DROP INDEX idx_name").unwrap());
}
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
}
#[test]
fn insert_delete_cycle_with_unique_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
for i in 0..20 {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({i}, 'User', 'u@t.com', {i})"
))
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(&format!("DELETE FROM users WHERE id = {i}"))
.unwrap(),
1,
);
}
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(0));
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (100, 'Final', 'u@t.com', 99)",
)
.unwrap(),
1,
);
}
#[test]
fn update_swap_indexed_values() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'a@t.com', 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'b@t.com', 25)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("UPDATE users SET email = 'temp@t.com' WHERE id = 1")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("UPDATE users SET email = 'a@t.com' WHERE id = 2")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("UPDATE users SET email = 'b@t.com' WHERE id = 1")
.unwrap(),
1,
);
let qr = conn.query("SELECT email FROM users WHERE id = 1").unwrap();
assert_eq!(qr.rows[0][0], Value::Text("b@t.com".into()));
let qr = conn.query("SELECT email FROM users WHERE id = 2").unwrap();
assert_eq!(qr.rows[0][0], Value::Text("a@t.com".into()));
}
#[test]
fn index_on_single_column_pk_table() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE simple (id INTEGER NOT NULL PRIMARY KEY)")
.unwrap(),
);
assert_ok(conn.execute("CREATE INDEX idx_id ON simple (id)").unwrap());
assert_rows_affected(
conn.execute("INSERT INTO simple (id) VALUES (1)").unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO simple (id) VALUES (2)").unwrap(),
1,
);
assert_rows_affected(conn.execute("DELETE FROM simple WHERE id = 1").unwrap(), 1);
let qr = conn.query("SELECT COUNT(*) FROM simple").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(1));
}
#[test]
fn unique_index_on_pk_column() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_pk ON users (id)")
.unwrap(),
);
insert_users(&conn);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Dup', 'dup@t.com', 99)")
.unwrap_err();
assert!(matches!(
err,
SqlError::DuplicateKey | SqlError::UniqueViolation(_)
));
}
#[test]
fn index_with_all_null_values() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
for i in 1..=5 {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({i}, 'User{i}', NULL, {i})"
))
.unwrap(),
1,
);
}
assert_rows_affected(conn.execute("DELETE FROM users").unwrap(), 5);
for i in 1..=5 {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({i}, 'User{i}', NULL, {i})"
))
.unwrap(),
1,
);
}
}
#[test]
fn stress_many_rows_with_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_ok(conn.execute("CREATE INDEX idx_age ON users (age)").unwrap());
for i in 0..200 {
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({i}, 'User{i}', 'u{i}@t.com', {})", i % 50)
).unwrap(), 1);
}
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(200));
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (999, 'Dup', 'u0@t.com', 0)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn stress_many_indexes_on_one_table() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_ok(conn.execute("CREATE INDEX idx_age ON users (age)").unwrap());
assert_ok(
conn.execute("CREATE INDEX idx_name_age ON users (name, age)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX idx_email_name ON users (email, name)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(
conn.execute(
"UPDATE users SET name = 'Alicia', email = 'alicia@t.com', age = 31 WHERE id = 1",
)
.unwrap(),
1,
);
assert_rows_affected(conn.execute("DELETE FROM users WHERE id = 2").unwrap(), 1);
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(2));
}
#[test]
fn stress_interleaved_crud_with_indexes() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
for i in 0..50 {
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({i}, 'User{i}', 'u{i}@t.com', {})", i + 20)
).unwrap(), 1);
}
for i in (0..50).step_by(2) {
assert_rows_affected(
conn.execute(&format!("DELETE FROM users WHERE id = {i}"))
.unwrap(),
1,
);
}
for i in (1..50).step_by(2) {
assert_rows_affected(
conn.execute(&format!(
"UPDATE users SET email = 'updated{i}@t.com' WHERE id = {i}"
))
.unwrap(),
1,
);
}
for i in (0..50).step_by(2) {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({i}, 'New{i}', 'u{i}@t.com', {})",
i + 100
))
.unwrap(),
1,
);
}
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(50));
}
#[test]
fn stress_create_populate_drop_cycle() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
for i in 0..50 {
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({i}, 'User{i}', 'u{i}@t.com', {i})")
).unwrap(), 1);
}
for _ in 0..5 {
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (999, 'Dup', 'u0@t.com', 0)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
assert_ok(conn.execute("DROP INDEX idx_email").unwrap());
}
}
#[test]
fn stress_index_with_transactions() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
for batch in 0..10 {
conn.execute("BEGIN").unwrap();
for i in 0..5 {
let id = batch * 5 + i;
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({id}, 'U{id}', 'u{id}@t.com', {id})")
).unwrap(), 1);
}
conn.execute("COMMIT").unwrap();
}
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(50));
}
#[test]
fn stress_index_persistence_cycle() {
let dir = tempfile::tempdir().unwrap();
{
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
for i in 0..20 {
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({i}, 'U{i}', 'u{i}@t.com', {i})")
).unwrap(), 1);
}
}
for batch in 1..=5 {
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
for i in 0..10 {
let id = 20 + (batch - 1) * 10 + i;
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({id}, 'U{id}', 'u{id}@t.com', {id})")
).unwrap(), 1);
}
}
{
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(70));
}
}
#[test]
fn composite_unique_index_enforces_full_combination() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute(
"CREATE TABLE products (id INTEGER NOT NULL PRIMARY KEY, category TEXT, sku TEXT)",
)
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_cat_sku ON products (category, sku)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO products (id, category, sku) VALUES (1, 'electronics', 'ABC')")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO products (id, category, sku) VALUES (2, 'electronics', 'DEF')")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO products (id, category, sku) VALUES (3, 'clothing', 'ABC')")
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO products (id, category, sku) VALUES (4, 'electronics', 'ABC')")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn composite_non_unique_index_allows_duplicates() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute(
"CREATE TABLE events (id INTEGER NOT NULL PRIMARY KEY, year INTEGER, month INTEGER)",
)
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX idx_ym ON events (year, month)")
.unwrap(),
);
for i in 0..10 {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO events (id, year, month) VALUES ({i}, 2024, 1)"
))
.unwrap(),
1,
);
}
let qr = conn.query("SELECT COUNT(*) FROM events").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(10));
}
#[test]
fn full_lifecycle_correctness() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
let qr = conn.query("SELECT * FROM users ORDER BY id").unwrap();
assert_eq!(qr.rows.len(), 3);
assert_eq!(qr.rows[0][1], Value::Text("Alice".into()));
assert_eq!(qr.rows[1][1], Value::Text("Bob".into()));
assert_eq!(qr.rows[2][1], Value::Text("Charlie".into()));
assert_rows_affected(
conn.execute("UPDATE users SET name = 'Alicia', email = 'alicia@test.com' WHERE id = 1")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap(),
1,
);
let err = conn
.execute(
"INSERT INTO users (id, name, email, age) VALUES (5, 'Eve', 'alicia@test.com', 22)",
)
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
assert_rows_affected(conn.execute("DELETE FROM users WHERE id = 2").unwrap(), 1);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (5, 'Eve', 'bob@test.com', 22)",
)
.unwrap(),
1,
);
assert_ok(conn.execute("DROP INDEX idx_name").unwrap());
let err = conn
.execute(
"INSERT INTO users (id, name, email, age) VALUES (6, 'Frank', 'charlie@test.com', 50)",
)
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(4));
}
#[test]
fn full_lifecycle_with_persistence() {
let dir = tempfile::tempdir().unwrap();
{
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
}
{
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_rows_affected(
conn.execute("UPDATE users SET email = 'newalice@test.com' WHERE id = 1")
.unwrap(),
1,
);
assert_rows_affected(conn.execute("DELETE FROM users WHERE id = 2").unwrap(), 1);
}
{
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(2));
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (5, 'Eve', 'bob@test.com', 22)",
)
.unwrap(),
1,
);
let err = conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (6, 'Frank', 'newalice@test.com', 50)"
).unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
let err = conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (7, 'Grace', 'charlie@test.com', 28)"
).unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
}
#[test]
fn composite_unique_null_in_first_column() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_name_email ON users (name, email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, NULL, 'a@t.com', 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, NULL, 'a@t.com', 25)")
.unwrap(),
1,
);
}
#[test]
fn composite_unique_null_in_second_column() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_name_email ON users (name, email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', NULL, 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Alice', NULL, 25)")
.unwrap(),
1,
);
}
#[test]
fn composite_unique_both_null() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_name_email ON users (name, email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, NULL, NULL, 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, NULL, NULL, 25)")
.unwrap(),
1,
);
}
#[test]
fn select_with_index_returns_correct_data() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(conn.execute("CREATE INDEX idx_age ON users (age)").unwrap());
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
let qr = conn.query("SELECT * FROM users ORDER BY id").unwrap();
assert_eq!(qr.rows.len(), 3);
assert_eq!(qr.rows[0][0], Value::Integer(1));
assert_eq!(qr.rows[0][1], Value::Text("Alice".into()));
assert_eq!(qr.rows[0][2], Value::Text("alice@test.com".into()));
assert_eq!(qr.rows[0][3], Value::Integer(30));
}
#[test]
fn aggregation_with_index_present() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(conn.execute("CREATE INDEX idx_age ON users (age)").unwrap());
insert_users(&conn);
let qr = conn
.query("SELECT COUNT(*), SUM(age), AVG(age) FROM users")
.unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(3));
assert_eq!(qr.rows[0][1], Value::Integer(90));
assert_eq!(qr.rows[0][2], Value::Real(30.0));
}
#[test]
fn distinct_with_index_present() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(conn.execute("CREATE INDEX idx_age ON users (age)").unwrap());
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'a@t.com', 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'b@t.com', 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (3, 'Charlie', 'c@t.com', 25)",
)
.unwrap(),
1,
);
let qr = conn
.query("SELECT DISTINCT age FROM users ORDER BY age")
.unwrap();
assert_eq!(qr.rows.len(), 2);
assert_eq!(qr.rows[0][0], Value::Integer(25));
assert_eq!(qr.rows[1][0], Value::Integer(30));
}
#[test]
fn order_by_with_index_present() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
insert_users(&conn);
let qr = conn.query("SELECT name FROM users ORDER BY name").unwrap();
assert_eq!(qr.rows[0][0], Value::Text("Alice".into()));
assert_eq!(qr.rows[1][0], Value::Text("Bob".into()));
assert_eq!(qr.rows[2][0], Value::Text("Charlie".into()));
}
#[test]
fn state_consistent_after_unique_violation_on_insert() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'a@t.com', 30)")
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'a@t.com', 25)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(1));
assert_eq!(
conn.query("SELECT name FROM users WHERE id = 1")
.unwrap()
.rows[0][0],
Value::Text("Alice".into())
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'b@t.com', 25)")
.unwrap(),
1,
);
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(2));
}
#[test]
fn state_consistent_after_unique_violation_on_update() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
let err = conn
.execute("UPDATE users SET email = 'bob@test.com' WHERE id = 1")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
let qr = conn.query("SELECT email FROM users WHERE id = 1").unwrap();
assert_eq!(qr.rows[0][0], Value::Text("alice@test.com".into()));
let qr = conn.query("SELECT email FROM users WHERE id = 2").unwrap();
assert_eq!(qr.rows[0][0], Value::Text("bob@test.com".into()));
assert_rows_affected(
conn.execute("UPDATE users SET email = 'newalice@test.com' WHERE id = 1")
.unwrap(),
1,
);
}
#[test]
fn state_consistent_after_create_unique_index_failure() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'same@t.com', 30)",
)
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'same@t.com', 25)",
)
.unwrap(),
1,
);
let err = conn
.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(2));
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (3, 'Charlie', 'same@t.com', 35)",
)
.unwrap(),
1,
);
assert_ok(
conn.execute("CREATE INDEX idx_email ON users (email)")
.unwrap(),
);
}
#[test]
fn multiple_violations_dont_corrupt_state() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'a@t.com', 30)")
.unwrap(),
1,
);
for i in 2..12 {
let err = conn
.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({i}, 'X', 'a@t.com', {i})"
))
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(1));
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'b@t.com', 25)")
.unwrap(),
1,
);
}
#[test]
fn update_pk_and_indexed_column_simultaneously() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(
conn.execute(
"UPDATE users SET id = 100, name = 'Alicia', email = 'alicia@t.com' WHERE id = 1",
)
.unwrap(),
1,
);
let qr = conn
.query("SELECT COUNT(*) FROM users WHERE id = 1")
.unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(0));
let qr = conn
.query("SELECT name, email FROM users WHERE id = 100")
.unwrap();
assert_eq!(qr.rows.len(), 1);
assert_eq!(qr.rows[0][0], Value::Text("Alicia".into()));
assert_eq!(qr.rows[0][1], Value::Text("alicia@t.com".into()));
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (4, 'Dave', 'alice@test.com', 40)",
)
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (5, 'Eve', 'alicia@t.com', 22)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn multi_row_insert_with_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (1, 'A', 'a@t.com', 20), (2, 'B', 'b@t.com', 25), (3, 'C', 'c@t.com', 30)"
).unwrap(), 3);
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(3));
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (4, 'D', 'a@t.com', 35)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn multi_row_insert_duplicate_within_batch() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
let err = conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (1, 'A', 'same@t.com', 20), (2, 'B', 'same@t.com', 25)"
).unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn delete_no_matching_rows_with_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(conn.execute("DELETE FROM users WHERE id = 999").unwrap(), 0);
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(3));
}
#[test]
fn update_no_matching_rows_with_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(
conn.execute("UPDATE users SET email = 'new@t.com' WHERE id = 999")
.unwrap(),
0,
);
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(3));
}
#[test]
fn update_to_same_value_no_change() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
insert_users(&conn);
assert_rows_affected(
conn.execute("UPDATE users SET email = 'alice@test.com' WHERE id = 1")
.unwrap(),
1,
);
let qr = conn.query("SELECT email FROM users WHERE id = 1").unwrap();
assert_eq!(qr.rows[0][0], Value::Text("alice@test.com".into()));
}
#[test]
fn complex_transaction_with_mixed_operations() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
conn.execute("BEGIN").unwrap();
assert_ok(conn.execute(
"CREATE TABLE products (id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, sku TEXT, price REAL)"
).unwrap());
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_sku ON products (sku)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX idx_price ON products (price)")
.unwrap(),
);
assert_rows_affected(
conn.execute(
"INSERT INTO products (id, name, sku, price) VALUES (1, 'Widget', 'WGT-001', 9.99)",
)
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO products (id, name, sku, price) VALUES (2, 'Gadget', 'GDG-001', 19.99)",
)
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO products (id, name, sku, price) VALUES (3, 'Doohickey', 'DHK-001', 29.99)",
)
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("UPDATE products SET price = 12.99 WHERE sku = 'WGT-001'")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("DELETE FROM products WHERE id = 2").unwrap(),
1,
);
assert_rows_affected(conn.execute(
"INSERT INTO products (id, name, sku, price) VALUES (4, 'Thingamajig', 'GDG-001', 39.99)"
).unwrap(), 1);
conn.execute("COMMIT").unwrap();
let qr = conn.query("SELECT COUNT(*) FROM products").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(3));
let qr = conn
.query("SELECT name FROM products WHERE id = 1")
.unwrap();
assert_eq!(qr.rows[0][0], Value::Text("Widget".into()));
let err = conn
.execute("INSERT INTO products (id, name, sku, price) VALUES (5, 'Copy', 'WGT-001', 1.99)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn complex_transaction_rollback_undoes_everything() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'alice@t.com', 30)",
)
.unwrap(),
1,
);
conn.execute("BEGIN").unwrap();
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'bob@t.com', 25)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("UPDATE users SET email = 'newalice@t.com' WHERE id = 1")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (3, 'Charlie', 'charlie@t.com', 35)",
)
.unwrap(),
1,
);
conn.execute("ROLLBACK").unwrap();
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(1));
let qr = conn.query("SELECT email FROM users WHERE id = 1").unwrap();
assert_eq!(qr.rows[0][0], Value::Text("alice@t.com".into()));
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'bob@t.com', 25)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (3, 'Charlie', 'charlie@t.com', 35)",
)
.unwrap(),
1,
);
}
#[test]
fn multiple_tables_independent_index_operations() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE users (id INTEGER NOT NULL PRIMARY KEY, email TEXT)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE products (id INTEGER NOT NULL PRIMARY KEY, sku TEXT)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE TABLE orders (id INTEGER NOT NULL PRIMARY KEY, ref_code TEXT)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_user_email ON users (email)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_product_sku ON products (sku)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_order_ref ON orders (ref_code)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, email) VALUES (1, 'user@t.com')")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO products (id, sku) VALUES (1, 'SKU-001')")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO orders (id, ref_code) VALUES (1, 'ORD-001')")
.unwrap(),
1,
);
assert_ok(conn.execute("DROP TABLE products").unwrap());
let err = conn
.execute("INSERT INTO users (id, email) VALUES (2, 'user@t.com')")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
let err = conn
.execute("INSERT INTO orders (id, ref_code) VALUES (2, 'ORD-001')")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
assert_ok(
conn.execute("CREATE TABLE products (id INTEGER NOT NULL PRIMARY KEY, sku TEXT)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_product_sku ON products (sku)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO products (id, sku) VALUES (1, 'SKU-001')")
.unwrap(),
1,
);
}
#[test]
fn index_correctness_after_bulk_update() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
for i in 0..20 {
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({i}, 'User{i}', 'u{i}@t.com', {})", 20 + i)
).unwrap(), 1);
}
for i in 0..20 {
assert_rows_affected(
conn.execute(&format!(
"UPDATE users SET email = 'new{i}@t.com' WHERE id = {i}"
))
.unwrap(),
1,
);
}
for i in 0..20 {
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({}, 'Reuse{i}', 'u{i}@t.com', {})", 100 + i, 50 + i)
).unwrap(), 1);
}
for i in 0..20 {
let err = conn
.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({}, 'Dup', 'new{i}@t.com', 99)",
200 + i
))
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(40));
}
#[test]
fn index_correctness_after_selective_delete() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
for i in 0..30 {
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({i}, 'User{i}', 'u{i}@t.com', {})", 20 + i)
).unwrap(), 1);
}
for i in (0..30).filter(|x| x % 3 == 0) {
assert_rows_affected(
conn.execute(&format!("DELETE FROM users WHERE id = {i}"))
.unwrap(),
1,
);
}
for i in (0..30).filter(|x| x % 3 == 0) {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({}, 'Reuse', 'u{i}@t.com', 99)",
100 + i
))
.unwrap(),
1,
);
}
for i in (0..30).filter(|x| x % 3 != 0) {
let err = conn
.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({}, 'Dup', 'u{i}@t.com', 99)",
200 + i
))
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
}
#[test]
fn transition_null_to_non_null_and_back() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', NULL, 30)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', NULL, 25)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("UPDATE users SET email = 'a@t.com' WHERE id = 1")
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (3, 'C', 'a@t.com', 20)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
assert_rows_affected(
conn.execute("UPDATE users SET email = NULL WHERE id = 1")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (3, 'C', 'a@t.com', 20)")
.unwrap(),
1,
);
let err = conn
.execute("UPDATE users SET email = 'a@t.com' WHERE id = 1")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn index_with_negative_and_zero_integers() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_age ON users (age)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'A', 'a@t.com', -100)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'B', 'b@t.com', 0)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO users (id, name, email, age) VALUES (3, 'C', 'c@t.com', 100)")
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (4, 'D', 'd@t.com', -100)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (5, 'E', 'e@t.com', 0)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (6, 'F', 'f@t.com', 100)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn index_with_float_edge_values() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE data (id INTEGER NOT NULL PRIMARY KEY, val REAL)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_val ON data (val)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO data (id, val) VALUES (1, 0.0)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO data (id, val) VALUES (2, -0.001)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO data (id, val) VALUES (3, 0.001)")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO data (id, val) VALUES (4, 999999.999)")
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO data (id, val) VALUES (5, 0.0)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn index_with_very_long_text() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute("CREATE TABLE data (id INTEGER NOT NULL PRIMARY KEY, val TEXT)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_val ON data (val)")
.unwrap(),
);
let long_a = "a".repeat(500);
let long_b = "b".repeat(500);
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO data (id, val) VALUES (1, '{long_a}')"
))
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO data (id, val) VALUES (2, '{long_b}')"
))
.unwrap(),
1,
);
let err = conn
.execute(&format!(
"INSERT INTO data (id, val) VALUES (3, '{long_a}')"
))
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
#[test]
fn stress_500_rows_full_crud_cycle_with_indexes() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX idx_name ON users (name)")
.unwrap(),
);
for i in 0..500 {
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({i}, 'User{i}', 'u{i}@t.com', {})", i % 80)
).unwrap(), 1);
}
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(500));
for i in 0..100 {
assert_rows_affected(
conn.execute(&format!("DELETE FROM users WHERE id = {i}"))
.unwrap(),
1,
);
}
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(400));
for i in 100..200 {
assert_rows_affected(
conn.execute(&format!(
"UPDATE users SET email = 'updated{i}@t.com' WHERE id = {i}"
))
.unwrap(),
1,
);
}
for i in 0..100 {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({}, 'New{i}', 'u{i}@t.com', {})",
500 + i,
i % 80
))
.unwrap(),
1,
);
}
for i in 100..110 {
let err = conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({}, 'Dup', 'updated{i}@t.com', 99)", 700 + i)
).unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
for i in 100..110 {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({}, 'Freed', 'u{i}@t.com', 99)",
800 + i
))
.unwrap(),
1,
);
}
let qr = conn.query("SELECT COUNT(*) FROM users").unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(510));
}
#[test]
fn stress_transaction_batches_with_index_verification() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
assert_ok(conn.execute("CREATE INDEX idx_age ON users (age)").unwrap());
for batch in 0..20 {
conn.execute("BEGIN").unwrap();
for i in 0..10 {
let id = batch * 10 + i;
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({id}, 'U{id}', 'u{id}@t.com', {id})")
).unwrap(), 1);
}
conn.execute("COMMIT").unwrap();
}
assert_eq!(
conn.query("SELECT COUNT(*) FROM users").unwrap().rows[0][0],
Value::Integer(200)
);
for batch in 0..5 {
conn.execute("BEGIN").unwrap();
for i in 0..10 {
let id = 200 + batch * 10 + i;
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({id}, 'R{id}', 'r{id}@t.com', {id})")
).unwrap(), 1);
}
conn.execute("ROLLBACK").unwrap();
}
assert_eq!(
conn.query("SELECT COUNT(*) FROM users").unwrap().rows[0][0],
Value::Integer(200)
);
for i in 0..10 {
let id = 200 + i;
assert_rows_affected(conn.execute(
&format!("INSERT INTO users (id, name, email, age) VALUES ({id}, 'Final{id}', 'r{id}@t.com', {id})")
).unwrap(), 1);
}
assert_eq!(
conn.query("SELECT COUNT(*) FROM users").unwrap().rows[0][0],
Value::Integer(210)
);
}
#[test]
fn stress_multiple_tables_multiple_indexes_persistence() {
let dir = tempfile::tempdir().unwrap();
{
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(
conn.execute(
"CREATE TABLE users (id INTEGER NOT NULL PRIMARY KEY, email TEXT, dept TEXT)",
)
.unwrap(),
);
assert_ok(conn.execute(
"CREATE TABLE orders (id INTEGER NOT NULL PRIMARY KEY, user_id INTEGER, status TEXT)"
).unwrap());
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_user_email ON users (email)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX idx_user_dept ON users (dept)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX idx_order_status ON orders (status)")
.unwrap(),
);
assert_ok(
conn.execute("CREATE INDEX idx_order_user ON orders (user_id)")
.unwrap(),
);
for i in 0..50 {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO users (id, email, dept) VALUES ({i}, 'u{i}@t.com', 'dept{}')",
i % 5
))
.unwrap(),
1,
);
}
for i in 0..100 {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO orders (id, user_id, status) VALUES ({i}, {}, '{}')",
i % 50,
if i % 3 == 0 {
"complete"
} else if i % 3 == 1 {
"pending"
} else {
"cancelled"
}
))
.unwrap(),
1,
);
}
}
{
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_eq!(
conn.query("SELECT COUNT(*) FROM users").unwrap().rows[0][0],
Value::Integer(50)
);
assert_eq!(
conn.query("SELECT COUNT(*) FROM orders").unwrap().rows[0][0],
Value::Integer(100)
);
let err = conn
.execute("INSERT INTO users (id, email, dept) VALUES (999, 'u0@t.com', 'x')")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
assert_ok(conn.execute("DROP INDEX idx_order_status").unwrap());
assert_rows_affected(
conn.execute("DELETE FROM orders WHERE id < 10").unwrap(),
10,
);
assert_rows_affected(
conn.execute("UPDATE users SET dept = 'fired' WHERE id < 5")
.unwrap(),
5,
);
}
{
let db = open_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_eq!(
conn.query("SELECT COUNT(*) FROM users").unwrap().rows[0][0],
Value::Integer(50)
);
assert_eq!(
conn.query("SELECT COUNT(*) FROM orders").unwrap().rows[0][0],
Value::Integer(90)
);
let qr = conn
.query("SELECT COUNT(*) FROM users WHERE dept = 'fired'")
.unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(5));
let err = conn
.execute("INSERT INTO users (id, email, dept) VALUES (999, 'u10@t.com', 'x')")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
assert_ok(
conn.execute("CREATE INDEX idx_order_status ON orders (status)")
.unwrap(),
);
}
}
#[test]
fn index_population_from_existing_large_dataset() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
for i in 0..300 {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({i}, 'U{i}', 'u{i}@t.com', {})",
i % 60
))
.unwrap(),
1,
);
}
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
let err = conn
.execute("INSERT INTO users (id, name, email, age) VALUES (999, 'Dup', 'u0@t.com', 0)")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
assert_rows_affected(
conn.execute(
"INSERT INTO users (id, name, email, age) VALUES (300, 'New', 'new@t.com', 0)",
)
.unwrap(),
1,
);
}
#[test]
fn update_all_rows_shifts_indexes() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
setup_users_table(&conn);
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
.unwrap(),
);
for i in 0..20 {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({i}, 'U{i}', 'u{i}@t.com', {i})"
))
.unwrap(),
1,
);
}
for i in 0..20 {
assert_rows_affected(
conn.execute(&format!(
"UPDATE users SET email = 'new_u{i}@t.com' WHERE id = {i}"
))
.unwrap(),
1,
);
}
for i in 0..20 {
assert_rows_affected(
conn.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({}, 'Old', 'u{i}@t.com', 99)",
100 + i
))
.unwrap(),
1,
);
let err = conn
.execute(&format!(
"INSERT INTO users (id, name, email, age) VALUES ({}, 'Dup', 'new_u{i}@t.com', 99)",
200 + i
))
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
}
}
#[test]
fn index_on_composite_pk_table() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(conn.execute(
"CREATE TABLE enrollments (student_id INTEGER NOT NULL, course_id INTEGER NOT NULL, grade TEXT, PRIMARY KEY (student_id, course_id))"
).unwrap());
assert_ok(
conn.execute("CREATE INDEX idx_grade ON enrollments (grade)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO enrollments (student_id, course_id, grade) VALUES (1, 101, 'A')")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO enrollments (student_id, course_id, grade) VALUES (1, 102, 'B')")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO enrollments (student_id, course_id, grade) VALUES (2, 101, 'A')")
.unwrap(),
1,
);
let qr = conn
.query("SELECT COUNT(*) FROM enrollments WHERE grade = 'A'")
.unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(2));
assert_rows_affected(
conn.execute("DELETE FROM enrollments WHERE student_id = 1 AND course_id = 101")
.unwrap(),
1,
);
let qr = conn
.query("SELECT COUNT(*) FROM enrollments WHERE grade = 'A'")
.unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(1));
}
#[test]
fn unique_index_on_composite_pk_table() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(conn.execute(
"CREATE TABLE enrollments (student_id INTEGER NOT NULL, course_id INTEGER NOT NULL, grade TEXT, PRIMARY KEY (student_id, course_id))"
).unwrap());
assert_ok(
conn.execute("CREATE UNIQUE INDEX idx_grade ON enrollments (grade)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO enrollments (student_id, course_id, grade) VALUES (1, 101, 'A')")
.unwrap(),
1,
);
let err = conn
.execute("INSERT INTO enrollments (student_id, course_id, grade) VALUES (2, 101, 'A')")
.unwrap_err();
assert!(matches!(err, SqlError::UniqueViolation(_)));
assert_rows_affected(
conn.execute(
"INSERT INTO enrollments (student_id, course_id, grade) VALUES (2, 101, NULL)",
)
.unwrap(),
1,
);
assert_rows_affected(
conn.execute(
"INSERT INTO enrollments (student_id, course_id, grade) VALUES (3, 101, NULL)",
)
.unwrap(),
1,
);
}
#[test]
fn update_composite_pk_with_index() {
let dir = tempfile::tempdir().unwrap();
let db = create_db(dir.path());
let conn = Connection::open(&db).unwrap();
assert_ok(conn.execute(
"CREATE TABLE enrollments (student_id INTEGER NOT NULL, course_id INTEGER NOT NULL, grade TEXT, PRIMARY KEY (student_id, course_id))"
).unwrap());
assert_ok(
conn.execute("CREATE INDEX idx_grade ON enrollments (grade)")
.unwrap(),
);
assert_rows_affected(
conn.execute("INSERT INTO enrollments (student_id, course_id, grade) VALUES (1, 101, 'B')")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("INSERT INTO enrollments (student_id, course_id, grade) VALUES (1, 102, 'C')")
.unwrap(),
1,
);
assert_rows_affected(
conn.execute("UPDATE enrollments SET grade = 'A' WHERE student_id = 1 AND course_id = 101")
.unwrap(),
1,
);
let qr = conn
.query("SELECT grade FROM enrollments WHERE student_id = 1 AND course_id = 101")
.unwrap();
assert_eq!(qr.rows[0][0], Value::Text("A".into()));
assert_rows_affected(
conn.execute(
"UPDATE enrollments SET student_id = 2 WHERE student_id = 1 AND course_id = 101",
)
.unwrap(),
1,
);
let qr = conn
.query("SELECT COUNT(*) FROM enrollments WHERE student_id = 2")
.unwrap();
assert_eq!(qr.rows[0][0], Value::Integer(1));
}