diff --git a/server/src/engine/storage/v2/impls/tests/model_driver.rs b/server/src/engine/storage/v2/impls/tests/model_driver.rs index 78c7ceeb..435ca65d 100644 --- a/server/src/engine/storage/v2/impls/tests/model_driver.rs +++ b/server/src/engine/storage/v2/impls/tests/model_driver.rs @@ -27,7 +27,7 @@ use { crate::{ engine::{ - core::{dml, model::Model, space::Space, EntityIDRef}, + core::{dml, index::RowData, model::Model, space::Space, EntityID, EntityIDRef}, data::lit::Lit, error::QueryResult, fractal::{test_utils::TestGlobal, GlobalInstanceLike}, @@ -44,15 +44,42 @@ use { crossbeam_epoch::pin, }; -fn create_model_and_space(global: &TestGlobal, create_model: &str) -> QueryResult<()> { +const TEST_DATASET_SIZE: usize = 1000; +const TEST_UPDATE_DATASET_SIZE: usize = 8200; // this peculiar size to force the buffer to flush + +fn create_test_kv_strings(change_count: usize) -> Vec<(String, String)> { + (1..=change_count) + .map(|i| { + ( + format!("user-{i:0>change_count$}"), + format!("password-{i:0>change_count$}"), + ) + }) + .collect() +} + +fn create_test_kv_int(change_count: usize) -> Vec<(u64, String)> { + (0..change_count) + .map(|i| (i as u64, format!("password-{i:0>change_count$}"))) + .collect() +} + +fn create_model_and_space( + global: &TestGlobal, + create_model: &str, +) -> QueryResult { let tokens = lex_insecure(create_model.as_bytes()).unwrap(); let create_model: CreateModel = ast::parse_ast_node_full(&tokens[2..]).unwrap(); + let mdl_name = EntityID::new( + create_model.model_name.space(), + create_model.model_name.entity(), + ); // first create space let create_space_str = format!("create space {}", create_model.model_name.space()); let create_space_tokens = lex_insecure(create_space_str.as_bytes()).unwrap(); let create_space: CreateSpace = ast::parse_ast_node_full(&create_space_tokens[2..]).unwrap(); Space::transactional_exec_create(global, create_space)?; - Model::transactional_exec_create(global, create_model).map(|_| ()) + Model::transactional_exec_create(global, create_model).map(|_| mdl_name) } fn run_insert(global: &TestGlobal, insert: &str) -> QueryResult<()> { @@ -67,171 +94,229 @@ fn run_update(global: &TestGlobal, update: &str) -> QueryResult<()> { dml::update(global, insert) } -#[test] -fn empty_model_data() { - test_utils::with_variable("empty_model_data", |log_name| { - // create and close - { - let global = TestGlobal::new_with_vfs_driver(log_name); - let _ = create_model_and_space( - &global, - "create model milky_way.solar_system(planet_name: string, population: uint64)", - ) - .unwrap(); - } - // open - { - let global = TestGlobal::new_with_vfs_driver(log_name); - drop(global); - } - }) +fn auto_hook(msg: &str, f: impl Fn() -> T) -> T { + let hook = std::panic::take_hook(); + let decl_owned = msg.to_owned(); + std::panic::set_hook(Box::new(move |pinfo| { + eprintln!("panic due to `{decl_owned}`: {pinfo}") + })); + let r = f(); + std::panic::set_hook(hook); + r } -fn create_test_kv(change_count: usize) -> Vec<(String, String)> { - (1..=change_count) - .map(|i| { - ( - format!("user-{i:0>change_count$}"), - format!("password-{i:0>change_count$}"), - ) +fn create_and_close(log_name: &str, decl: &str) { + auto_hook(decl, || { + test_utils::with_variable(log_name, |log_name| { + // create and close + { + let global = TestGlobal::new_with_vfs_driver(log_name); + let _ = create_model_and_space(&global, decl).unwrap(); + } + // open + { + let global = TestGlobal::new_with_vfs_driver(log_name); + drop(global); + } }) - .collect() + }) } -#[test] -fn model_data_inserts() { - test_utils::with_variable(("model_data_inserts", 1000), |(log_name, change_count)| { - let key_values = create_test_kv(change_count); - // create, insert and close - { - let mut global = TestGlobal::new_with_vfs_driver(log_name); - global.set_max_data_pressure(change_count); - let _ = create_model_and_space( - &global, - "create model apps.social(user_name: string, password: string)", - ) - .unwrap(); - for (username, password) in key_values.iter() { - run_insert( - &global, - &format!("insert into apps.social('{username}', '{password}')"), - ) - .unwrap(); +fn run_sample_inserts( + log_name: &str, + decl: &str, + key_values: Vec<(K, V)>, + make_insert_query: impl Fn(&K, &V) -> String, + as_pk: for<'a> fn(&'a K) -> Lit<'a>, + check_row: impl Fn(&K, &V, &RowData), +) { + auto_hook(decl, || { + test_utils::with_variable(log_name, |log_name| { + // create, insert and close + let mdl_name; + { + let mut global = TestGlobal::new_with_vfs_driver(log_name); + global.set_max_data_pressure(key_values.len()); + mdl_name = create_model_and_space(&global, decl).unwrap(); + for (username, password) in key_values.iter() { + run_insert(&global, &make_insert_query(username, password)).unwrap(); + } } - } - // reopen and verify 100 times - test_utils::multi_run(100, || { - let global = TestGlobal::new_with_vfs_driver(log_name); - global.load_model_drivers().unwrap(); - global - .state() - .with_model(EntityIDRef::new("apps", "social"), |model| { - let g = pin(); - for (username, password) in key_values.iter() { - assert_eq!( - model - .primary_index() - .select(Lit::new_str(username.as_str()), &g) - .unwrap() - .d_data() - .read() - .fields() - .get("password") - .unwrap() - .str(), - password.as_str() - ) - } - Ok(()) - }) - .unwrap() + // reopen and verify 100 times + test_utils::multi_run(100, || { + let global = TestGlobal::new_with_vfs_driver(log_name); + global.load_model_drivers().unwrap(); + global + .state() + .with_model( + EntityIDRef::new(mdl_name.space(), mdl_name.entity()), + |model| { + let g = pin(); + for (username, password) in key_values.iter() { + let row = model + .primary_index() + .select(as_pk(username), &g) + .unwrap() + .d_data() + .read(); + check_row(username, password, &row) + } + Ok(()) + }, + ) + .unwrap() + }) }) }) } -#[test] -fn model_data_updates() { - test_utils::with_variable(("model_data_updates", 8200), |(log_name, n)| { - let key_values = create_test_kv(n); - /* - - we first open the log and then insert n values - - we then reopen the log 100 times, changing n / 100 values every time (we set the string to an empty one) - - we finally reopen the log and check if all the keys have empty string as the password - */ - { - // insert n values - let mut global = TestGlobal::new_with_vfs_driver(log_name); - global.set_max_data_pressure(n); - let _ = create_model_and_space( - &global, - "create model apps.social(user_name: string, password: string)", - ) - .unwrap(); - for (username, password) in key_values.iter() { - run_insert( - &global, - &format!("insert into apps.social('{username}', '{password}')"), - ) - .unwrap(); - } - } - { - // reopen and update multiple times - // this effectively opens the log 100 times - let changes_per_cycle = n / 10; - let reopen_count = n / changes_per_cycle; - // now update values - let mut actual_position = 0; - for _ in 0..reopen_count { +fn run_sample_updates( + log_name: &str, + decl: &str, + key_values: Vec<(K, V)>, + make_insert_query: impl Fn(&K, &V) -> String, + make_update_query: impl Fn(&K, &V) -> String, + as_pk: for<'a> fn(&'a K) -> Lit<'a>, + check_row: impl Fn(&K, &V, &RowData), +) { + auto_hook(decl, || { + test_utils::with_variable((log_name, TEST_UPDATE_DATASET_SIZE), |(log_name, n)| { + /* + - we first open the log and then insert n values + - we then reopen the log 100 times, changing n / 100 values every time (we set the string to an empty one) + - we finally reopen the log and check if all the keys have empty string as the password + */ + let mdl_name; + { + // insert n values let mut global = TestGlobal::new_with_vfs_driver(log_name); - global.set_max_data_pressure(changes_per_cycle); - global.load_model_drivers().unwrap(); - let mut j = 0; - for _ in 0..changes_per_cycle { - let (username, _) = &key_values[actual_position]; - run_update( - &global, - &format!( - "update apps.social set password = '' where user_name = '{username}'" - ), - ) - .unwrap(); - actual_position += 1; - j += 1; + global.set_max_data_pressure(n); + mdl_name = create_model_and_space(&global, decl).unwrap(); + for (username, password) in key_values.iter() { + run_insert(&global, &make_insert_query(username, password)).unwrap(); } - assert_eq!(j, changes_per_cycle); - drop(global); } - assert_eq!(actual_position, n); - } - { - let global = TestGlobal::new_with_vfs_driver(log_name); - global.load_model_drivers().unwrap(); - for (txn_id, (username, _)) in key_values - .iter() - .enumerate() - .map(|(i, x)| ((i + n) as u64, x)) { - global - .state() - .with_model(EntityIDRef::new("apps", "social"), |model| { - let g = pin(); - let row = model - .primary_index() - .select(Lit::new_str(username.as_str()), &g) - .unwrap() - .d_data() - .read(); - let pass = row.fields().get("password").unwrap().str(); - assert!( - pass.is_empty(), - "failed for {username} because pass is {pass}", - ); - assert_eq!(row.get_txn_revised().value_u64(), txn_id); - Ok(()) - }) - .unwrap(); + // reopen and update multiple times + // this effectively opens the log 100 times + let changes_per_cycle = n / 10; + let reopen_count = n / changes_per_cycle; + // now update values + let mut actual_position = 0; + for _ in 0..reopen_count { + let mut global = TestGlobal::new_with_vfs_driver(log_name); + global.set_max_data_pressure(changes_per_cycle); + global.load_model_drivers().unwrap(); + let mut j = 0; + for _ in 0..changes_per_cycle { + let (username, pass) = &key_values[actual_position]; + run_update(&global, &make_update_query(username, pass)).unwrap(); + actual_position += 1; + j += 1; + } + assert_eq!(j, changes_per_cycle); + drop(global); + } + assert_eq!(actual_position, n); } - } + { + let global = TestGlobal::new_with_vfs_driver(log_name); + global.load_model_drivers().unwrap(); + for (txn_id, (username, password)) in key_values + .iter() + .enumerate() + .map(|(i, x)| ((i + n) as u64, x)) + { + global + .state() + .with_model( + EntityIDRef::new(mdl_name.space(), mdl_name.entity()), + |model| { + let g = pin(); + let row = model + .primary_index() + .select(as_pk(username), &g) + .unwrap() + .d_data() + .read(); + check_row(username, password, &row); + assert_eq!(row.get_txn_revised().value_u64(), txn_id); + Ok(()) + }, + ) + .unwrap(); + } + } + }) }) } + +/* + test runs +*/ + +#[test] +fn empty_model_data() { + create_and_close( + "empty_model_data_variable_index_key", + "create model milky_way.solar_system(planet_name: string, population: uint64)", + ); + create_and_close( + "empty_model_data_fixed_index_key", + "create model milky_way.solar_system(planet_id: uint64, population: uint64)", + ); +} + +#[test] +fn model_data_inserts() { + run_sample_inserts( + "model_data_inserts_variable_pk", + "create model apps.social(user_name: string, password: string)", + create_test_kv_strings(TEST_DATASET_SIZE), + |k, v| format!("insert into apps.social('{k}', '{v}')"), + |k| Lit::new_str(k), + |_, v, row| assert_eq!(row.fields().get("password").unwrap().str(), v), + ); + run_sample_inserts( + "model_data_inserts_fixed_pk", + "create model apps.social(user_id: uint64, password: string)", + create_test_kv_int(TEST_DATASET_SIZE), + |k, v| format!("insert into apps.social({k}, '{v}')"), + |k| Lit::new_uint(*k), + |_, v, row| assert_eq!(row.fields().get("password").unwrap().str(), v), + ) +} + +#[test] +fn model_data_updates() { + run_sample_updates( + "model_data_updates_variable_key", + "create model apps.social(user_name: string, password: string)", + create_test_kv_strings(TEST_UPDATE_DATASET_SIZE), + |k, v| format!("insert into apps.social('{k}', '{v}')"), + |k, _| format!("update apps.social set password = '' where user_name = '{k}'"), + |k| Lit::new_str(k), + |username, _, row| { + let pass = row.fields().get("password").unwrap().str(); + assert!( + pass.is_empty(), + "failed for {username} because pass is {pass}", + ); + }, + ); + run_sample_updates( + "model_data_updates_fixed_key", + "create model apps.social(user_name: uint64, password: string)", + create_test_kv_int(TEST_UPDATE_DATASET_SIZE), + |k, v| format!("insert into apps.social({k}, '{v}')"), + |k, _| format!("update apps.social set password = '' where user_name = {k}"), + |k| Lit::new_uint(*k), + |username, _, row| { + let pass = row.fields().get("password").unwrap().str(); + assert!( + pass.is_empty(), + "failed for {username} because pass is {pass}", + ); + }, + ); +}