use vibesql_types::{MySqlModeFlags, SqlMode, SqlValue};
use crate::change_events::ChangeEvent;
use crate::database::Database;
#[test]
fn test_set_sql_mode_changes_mode() {
let mut db = Database::new();
assert!(matches!(db.sql_mode(), SqlMode::MySQL { .. }));
db.set_sql_mode(SqlMode::SQLite);
assert!(matches!(db.sql_mode(), SqlMode::SQLite));
db.set_sql_mode(SqlMode::MySQL { flags: MySqlModeFlags::default() });
assert!(matches!(db.sql_mode(), SqlMode::MySQL { .. }));
}
#[test]
fn test_set_sql_mode_updates_session_variable() {
let mut db = Database::new();
db.set_sql_mode(SqlMode::SQLite);
let sql_mode_var = db.get_session_variable("SQL_MODE");
assert!(sql_mode_var.is_some());
if let Some(SqlValue::Varchar(mode_str)) = sql_mode_var {
assert_eq!(mode_str.as_str(), "SQLITE");
} else {
panic!("Expected SQL_MODE to be a Varchar");
}
}
#[test]
fn test_set_sql_mode_mysql_with_flags() {
let mut db = Database::new();
db.set_sql_mode(SqlMode::MySQL {
flags: MySqlModeFlags {
pipes_as_concat: true,
ansi_quotes: true,
strict_mode: true,
sqlite_division_semantics: false,
},
});
let sql_mode_var = db.get_session_variable("SQL_MODE");
assert!(sql_mode_var.is_some());
if let Some(SqlValue::Varchar(mode_str)) = sql_mode_var {
assert!(mode_str.contains("STRICT_TRANS_TABLES"));
assert!(mode_str.contains("PIPES_AS_CONCAT"));
assert!(mode_str.contains("ANSI_QUOTES"));
} else {
panic!("Expected SQL_MODE to be a Varchar");
}
}
#[test]
fn test_set_sql_mode_mysql_default_flags() {
let mut db = Database::new();
db.set_sql_mode(SqlMode::MySQL { flags: MySqlModeFlags::default() });
let sql_mode_var = db.get_session_variable("SQL_MODE");
assert!(sql_mode_var.is_some());
if let Some(SqlValue::Varchar(mode_str)) = sql_mode_var {
assert!(
mode_str.contains("NO_ZERO_IN_DATE") || mode_str.contains("NO_ENGINE_SUBSTITUTION")
);
} else {
panic!("Expected SQL_MODE to be a Varchar");
}
}
#[test]
fn test_sql_mode_affects_subsequent_queries() {
let mut db = Database::new();
assert!(matches!(db.sql_mode(), SqlMode::MySQL { .. }));
db.set_sql_mode(SqlMode::SQLite);
let mode = db.sql_mode();
assert!(matches!(mode, SqlMode::SQLite));
}
#[test]
fn test_change_events_disabled_by_default() {
let db = Database::new();
assert!(!db.change_events_enabled());
assert!(db.subscribe_changes().is_none());
}
#[test]
fn test_enable_change_events() {
let mut db = Database::new();
let _rx = db.enable_change_events(16);
assert!(db.change_events_enabled());
assert!(db.subscribe_changes().is_some());
}
#[test]
fn test_insert_emits_change_event() {
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
let mut db = Database::new();
let mut rx = db.enable_change_events(16);
let schema = TableSchema::new(
"users".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"name".to_string(),
DataType::Varchar { max_length: Some(50) },
false,
),
],
);
db.create_table(schema).unwrap();
let row = crate::Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("Alice")),
]);
db.insert_row("users", row).unwrap();
let events = rx.recv_all();
assert_eq!(events.len(), 1);
match &events[0] {
ChangeEvent::Insert { table_name, row_index, .. } => {
assert_eq!(*row_index, 0);
assert_eq!(table_name, "users");
}
_ => panic!("Expected Insert event, got {:?}", events[0]),
}
}
#[test]
fn test_batch_insert_emits_multiple_events() {
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
let mut db = Database::new();
let mut rx = db.enable_change_events(16);
let schema = TableSchema::new(
"products".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"name".to_string(),
DataType::Varchar { max_length: Some(50) },
false,
),
],
);
db.create_table(schema).unwrap();
let rows = vec![
crate::Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("Product A")),
]),
crate::Row::new(vec![
SqlValue::Integer(2),
SqlValue::Varchar(arcstr::ArcStr::from("Product B")),
]),
crate::Row::new(vec![
SqlValue::Integer(3),
SqlValue::Varchar(arcstr::ArcStr::from("Product C")),
]),
];
db.insert_rows_batch("products", rows).unwrap();
let events = rx.recv_all();
assert_eq!(events.len(), 3);
for (i, event) in events.iter().enumerate() {
assert!(matches!(event, ChangeEvent::Insert { row_index, .. } if *row_index == i));
}
}
#[test]
fn test_update_emits_change_event() {
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
let mut db = Database::new();
let schema = TableSchema::with_primary_key(
"users".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"name".to_string(),
DataType::Varchar { max_length: Some(50) },
false,
),
],
vec!["id".to_string()],
);
db.create_table(schema).unwrap();
let row = crate::Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("Alice")),
]);
db.insert_row("users", row).unwrap();
let mut rx = db.enable_change_events(16);
db.update_row_by_pk(
"users",
SqlValue::Integer(1),
vec![("name", SqlValue::Varchar(arcstr::ArcStr::from("Alice Smith")))],
)
.unwrap();
let events = rx.recv_all();
assert_eq!(events.len(), 1);
assert!(matches!(&events[0], ChangeEvent::Update { row_index: 0, .. }));
}
#[test]
fn test_multiple_subscribers() {
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
let mut db = Database::new();
let mut rx1 = db.enable_change_events(16);
let mut rx2 = db.subscribe_changes().unwrap();
let schema = TableSchema::new(
"test".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
let row = crate::Row::new(vec![SqlValue::Integer(1)]);
db.insert_row("test", row).unwrap();
assert_eq!(rx1.recv_all().len(), 1);
assert_eq!(rx2.recv_all().len(), 1);
}
#[test]
fn test_no_panic_on_lagged_receiver() {
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
let mut db = Database::new();
let _rx = db.enable_change_events(2);
let schema = TableSchema::new(
"test".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
for i in 0..10 {
let row = crate::Row::new(vec![SqlValue::Integer(i)]);
db.insert_row("test", row).unwrap();
}
}
#[test]
fn test_notify_deletes() {
let mut db = Database::new();
let mut rx = db.enable_change_events(16);
db.notify_deletes("users", &[0, 2, 5]);
let events = rx.recv_all();
assert_eq!(events.len(), 3);
assert!(
matches!(&events[0], ChangeEvent::Delete { table_name, row_index: 0, .. } if table_name == "users")
);
assert!(
matches!(&events[1], ChangeEvent::Delete { table_name, row_index: 2, .. } if table_name == "users")
);
assert!(
matches!(&events[2], ChangeEvent::Delete { table_name, row_index: 5, .. } if table_name == "users")
);
}
#[test]
fn test_notify_update() {
let mut db = Database::new();
let mut rx = db.enable_change_events(16);
db.notify_update("products", 42);
let events = rx.recv_all();
assert_eq!(events.len(), 1);
assert!(
matches!(&events[0], ChangeEvent::Update { table_name, row_index: 42, .. } if table_name == "products")
);
}
#[test]
fn test_persistence_disabled_by_default() {
let db = Database::new();
assert!(!db.persistence_enabled());
assert!(db.persistence_stats().is_none());
}
#[test]
fn test_enable_persistence() {
use std::io::Cursor;
use crate::wal::{PersistenceConfig, PersistenceEngine};
let mut db = Database::new();
assert!(!db.persistence_enabled());
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
db.enable_persistence(engine);
assert!(db.persistence_enabled());
assert!(db.persistence_stats().is_some());
}
#[test]
fn test_persistence_emits_insert_entries() {
use std::io::Cursor;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::wal::{PersistenceConfig, PersistenceEngine};
let mut db = Database::new();
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
db.enable_persistence(engine);
let schema = TableSchema::new(
"users".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"name".to_string(),
DataType::Varchar { max_length: Some(50) },
false,
),
],
);
db.create_table(schema).unwrap();
let row1 = crate::Row::new(vec![
SqlValue::Integer(1),
SqlValue::Varchar(arcstr::ArcStr::from("Alice")),
]);
let row2 =
crate::Row::new(vec![SqlValue::Integer(2), SqlValue::Varchar(arcstr::ArcStr::from("Bob"))]);
db.insert_row("users", row1).unwrap();
db.insert_row("users", row2).unwrap();
let stats = db.persistence_stats().unwrap();
assert_eq!(stats.entries_sent, 3);
}
#[test]
fn test_persistence_emits_transaction_entries() {
use std::io::Cursor;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::wal::{PersistenceConfig, PersistenceEngine};
let mut db = Database::new();
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
db.enable_persistence(engine);
let schema = TableSchema::new(
"test".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
db.begin_transaction().unwrap();
let row = crate::Row::new(vec![SqlValue::Integer(1)]);
db.insert_row("test", row).unwrap();
db.commit_transaction().unwrap();
let stats = db.persistence_stats().unwrap();
assert_eq!(stats.entries_sent, 4);
}
#[test]
fn test_sync_persistence_no_op_when_disabled() {
let db = Database::new();
assert!(db.sync_persistence().is_ok());
}
#[test]
fn test_emit_wal_delete() {
use std::io::Cursor;
use crate::wal::{PersistenceConfig, PersistenceEngine};
let mut db = Database::new();
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
db.enable_persistence(engine);
db.emit_wal_delete("users", 0, vec![SqlValue::Integer(1)]);
db.emit_wal_delete("users", 1, vec![SqlValue::Integer(2)]);
let stats = db.persistence_stats().unwrap();
assert_eq!(stats.entries_sent, 2);
}
#[test]
fn test_emit_wal_create_index() {
use std::io::Cursor;
use crate::wal::{PersistenceConfig, PersistenceEngine};
let mut db = Database::new();
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
db.enable_persistence(engine);
db.emit_wal_create_index(1, "idx_users_email", "users", vec![1], false);
let stats = db.persistence_stats().unwrap();
assert_eq!(stats.entries_sent, 1);
}
#[test]
fn test_emit_wal_drop_index() {
use std::io::Cursor;
use crate::wal::{PersistenceConfig, PersistenceEngine};
let mut db = Database::new();
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::default()).unwrap();
db.enable_persistence(engine);
db.emit_wal_drop_index(1, "idx_users_email");
let stats = db.persistence_stats().unwrap();
assert_eq!(stats.entries_sent, 1);
}
#[test]
fn test_emit_wal_no_op_when_disabled() {
let db = Database::new();
db.emit_wal_delete("users", 0, vec![SqlValue::Integer(1)]);
db.emit_wal_create_index(1, "idx", "table", vec![0], false);
db.emit_wal_drop_index(1, "idx");
assert!(db.persistence_stats().is_none());
}
#[test]
fn test_begin_transaction_with_default_durability() {
use crate::wal::TransactionDurability;
let mut db = Database::new();
db.begin_transaction_with_durability(TransactionDurability::Default).unwrap();
assert!(db.in_transaction());
assert!(db.in_transaction());
db.rollback_transaction().unwrap();
}
#[test]
fn test_begin_transaction_with_force_durable() {
use crate::wal::TransactionDurability;
let mut db = Database::new();
db.begin_transaction_with_durability(TransactionDurability::ForceDurable).unwrap();
assert!(db.in_transaction());
db.rollback_transaction().unwrap();
}
#[test]
fn test_begin_transaction_with_allow_lazy() {
use crate::wal::TransactionDurability;
let mut db = Database::new();
db.begin_transaction_with_durability(TransactionDurability::AllowLazy).unwrap();
assert!(db.in_transaction());
db.rollback_transaction().unwrap();
}
#[test]
fn test_begin_transaction_with_force_volatile() {
use crate::wal::TransactionDurability;
let mut db = Database::new();
db.begin_transaction_with_durability(TransactionDurability::ForceVolatile).unwrap();
assert!(db.in_transaction());
db.rollback_transaction().unwrap();
}
#[test]
fn test_durability_hint_cleared_on_commit() {
use crate::wal::TransactionDurability;
let mut db = Database::new();
db.begin_transaction_with_durability(TransactionDurability::ForceDurable).unwrap();
assert!(db.in_transaction());
db.commit_transaction().unwrap();
assert!(!db.in_transaction());
}
#[test]
fn test_durability_hint_cleared_on_rollback() {
use crate::wal::TransactionDurability;
let mut db = Database::new();
db.begin_transaction_with_durability(TransactionDurability::ForceDurable).unwrap();
assert!(db.in_transaction());
db.rollback_transaction().unwrap();
assert!(!db.in_transaction());
}
#[test]
fn test_force_durable_triggers_sync() {
use std::io::Cursor;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::wal::{PersistenceConfig, PersistenceEngine, TransactionDurability};
let mut db = Database::new();
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::lazy()).unwrap();
db.enable_persistence(engine);
let schema = TableSchema::new(
"test".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
db.begin_transaction_with_durability(TransactionDurability::ForceDurable).unwrap();
let row = crate::Row::new(vec![SqlValue::Integer(1)]);
db.insert_row("test", row).unwrap();
db.commit_transaction().unwrap();
let stats = db.persistence_stats().unwrap();
assert!(stats.explicit_flushes >= 1, "ForceDurable should trigger an explicit flush on commit");
}
#[test]
fn test_default_durability_respects_lazy_mode() {
use std::io::Cursor;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::wal::{PersistenceConfig, PersistenceEngine, TransactionDurability};
let mut db = Database::new();
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::lazy()).unwrap();
db.enable_persistence(engine);
let schema = TableSchema::new(
"test".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
let initial_stats = db.persistence_stats().unwrap();
let initial_explicit_flushes = initial_stats.explicit_flushes;
db.begin_transaction_with_durability(TransactionDurability::Default).unwrap();
let row = crate::Row::new(vec![SqlValue::Integer(1)]);
db.insert_row("test", row).unwrap();
db.commit_transaction().unwrap();
let final_stats = db.persistence_stats().unwrap();
assert_eq!(
final_stats.explicit_flushes, initial_explicit_flushes,
"Default durability in lazy mode should not trigger explicit flush on commit"
);
}
#[test]
fn test_durability_hint_no_panic_without_persistence() {
use crate::wal::TransactionDurability;
let mut db = Database::new();
db.begin_transaction_with_durability(TransactionDurability::ForceDurable).unwrap();
assert!(db.in_transaction());
db.commit_transaction().unwrap();
assert!(!db.in_transaction());
}
#[test]
fn test_allow_lazy_downgrades_durable_mode() {
use std::io::Cursor;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
use crate::wal::{PersistenceConfig, PersistenceEngine, TransactionDurability};
let mut db = Database::new();
let buf = Vec::new();
let cursor = Cursor::new(buf);
let engine = PersistenceEngine::with_writer(cursor, PersistenceConfig::durable()).unwrap();
db.enable_persistence(engine);
let schema = TableSchema::new(
"test".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
let initial_stats = db.persistence_stats().unwrap();
let initial_explicit_flushes = initial_stats.explicit_flushes;
db.begin_transaction_with_durability(TransactionDurability::AllowLazy).unwrap();
let row = crate::Row::new(vec![SqlValue::Integer(1)]);
db.insert_row("test", row).unwrap();
db.commit_transaction().unwrap();
let final_stats = db.persistence_stats().unwrap();
assert_eq!(
final_stats.explicit_flushes, initial_explicit_flushes,
"AllowLazy should downgrade durable mode and not trigger explicit flush on commit"
);
}
#[test]
fn rollback_restores_index_manager_state() {
use vibesql_ast::IndexColumn;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
let mut db = Database::new();
let schema = TableSchema::new(
"t".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
db.begin_transaction().unwrap();
db.create_index(
"idx_t_id".to_string(),
"t".to_string(),
false,
vec![IndexColumn::new_column("id".to_string(), vibesql_ast::OrderDirection::Asc)],
)
.unwrap();
assert!(db.index_exists("idx_t_id"), "index exists inside the transaction");
db.rollback_transaction().unwrap();
assert!(
!db.index_exists("idx_t_id"),
"ROLLBACK must restore the IndexManager, removing the index created in the txn"
);
}
#[test]
fn commit_keeps_index_manager_state() {
use vibesql_ast::IndexColumn;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
let mut db = Database::new();
let schema = TableSchema::new(
"t".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
db.begin_transaction().unwrap();
db.create_index(
"idx_t_id".to_string(),
"t".to_string(),
false,
vec![IndexColumn::new_column("id".to_string(), vibesql_ast::OrderDirection::Asc)],
)
.unwrap();
db.commit_transaction().unwrap();
assert!(db.index_exists("idx_t_id"), "COMMIT must keep the index created in the txn");
}
#[test]
fn read_only_transaction_does_not_clone_operations_snapshot() {
use vibesql_ast::IndexColumn;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
let mut db = Database::new();
let schema = TableSchema::new(
"t".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
db.create_index(
"idx_t_id".to_string(),
"t".to_string(),
false,
vec![IndexColumn::new_column("id".to_string(), vibesql_ast::OrderDirection::Asc)],
)
.unwrap();
for i in 0..256 {
db.insert_row("t", crate::Row::new(vec![SqlValue::Integer(i)])).unwrap();
}
let before = db.operations_snapshot_clones();
db.begin_transaction().unwrap();
assert!(db.index_exists("idx_t_id"), "index visible inside the read-only txn");
let _ = db.list_indexes();
db.rollback_transaction().unwrap();
assert_eq!(
db.operations_snapshot_clones(),
before,
"a read-only transaction must not deep-clone the Operations snapshot (#5419)"
);
}
#[test]
fn mutating_transaction_clones_operations_snapshot_once_and_rolls_back() {
use vibesql_ast::IndexColumn;
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_types::DataType;
let mut db = Database::new();
let schema = TableSchema::new(
"t".to_string(),
vec![ColumnSchema::new("id".to_string(), DataType::Integer, false)],
);
db.create_table(schema).unwrap();
db.create_index(
"idx_t_id".to_string(),
"t".to_string(),
false,
vec![IndexColumn::new_column("id".to_string(), vibesql_ast::OrderDirection::Asc)],
)
.unwrap();
let before = db.operations_snapshot_clones();
db.begin_transaction().unwrap();
db.insert_row("t", crate::Row::new(vec![SqlValue::Integer(1)])).unwrap();
assert_eq!(
db.operations_snapshot_clones(),
before + 1,
"first index mutation must take exactly one snapshot clone"
);
db.insert_row("t", crate::Row::new(vec![SqlValue::Integer(2)])).unwrap();
db.create_index(
"idx_t_id2".to_string(),
"t".to_string(),
false,
vec![IndexColumn::new_column("id".to_string(), vibesql_ast::OrderDirection::Asc)],
)
.unwrap();
assert_eq!(
db.operations_snapshot_clones(),
before + 1,
"subsequent mutations must reuse the existing snapshot, not re-clone"
);
db.rollback_transaction().unwrap();
assert!(
!db.index_exists("idx_t_id2"),
"ROLLBACK must restore Operations: index created in the txn is removed (#5413 preserved)"
);
assert!(
db.index_exists("idx_t_id"),
"the pre-transaction index must survive ROLLBACK"
);
assert_eq!(db.get_table("t").map(|t| t.row_count()), Some(0), "rolled-back rows are gone");
}