//! FIXME: With the current API and generation logic in plan.rs, //! for Properties that have intermediary queries we need to CLONE the current Context tables //! to properly generate queries, as we need to shadow after each query generated to make sure we are generating //! queries that are valid. This is specially valid with DROP and ALTER TABLE in the mix, because with outdated context //! we can generate queries that reference tables that do not exist. This is not a correctness issue, but more of //! an optimization issue that is good to point out for the future use rand::distr::{Distribution, weighted::WeightedIndex}; use serde::{Deserialize, Serialize}; use sql_generation::{ generation::{Arbitrary, ArbitraryFrom, GenerationContext, pick, pick_index}, model::{ query::{ Create, Delete, Drop, Insert, Select, alter_table::{AlterTable, AlterTableType}, predicate::Predicate, select::{CompoundOperator, CompoundSelect, ResultColumn, SelectBody, SelectInner}, transaction::{Begin, Commit, Rollback}, update::Update, }, table::SimValue, }, }; use strum::IntoEnumIterator; use turso_core::{LimboError, types}; use turso_parser::ast::{self, Distinctness}; use crate::{ common::print_diff, generation::{ Shadow as _, WeightedDistribution, plan::InteractionType, query::QueryDistribution, }, model::{Query, QueryCapabilities, QueryDiscriminants}, profiles::query::QueryProfile, runner::env::SimulatorEnv, }; use super::plan::{Assertion, Interaction, InteractionStats, ResultSet}; /// Properties are representations of executable specifications /// about the database behavior. #[derive(Debug, Clone, Serialize, Deserialize, strum::EnumDiscriminants)] #[strum_discriminants(derive(strum::EnumIter))] pub enum Property { /// Insert-Select is a property in which the inserted row /// must be in the resulting rows of a select query that has a /// where clause that matches the inserted row. /// The execution of the property is as follows /// INSERT INTO VALUES (...) /// I_0 /// I_1 /// ... /// I_n /// SELECT * FROM WHERE /// The interactions in the middle has the following constraints; /// - There will be no errors in the middle interactions. /// - The inserted row will not be deleted. /// - The inserted row will not be updated. /// - The table `t` will not be renamed, dropped, or altered. InsertValuesSelect { /// The insert query insert: Insert, /// Selected row index row_index: usize, /// Additional interactions in the middle of the property queries: Vec, /// The select query select: Select, /// Interactive query information if any interactive: Option, }, /// ReadYourUpdatesBack is a property in which the updated rows /// must be in the resulting rows of a select query that has a /// where clause that matches the updated row. /// The execution of the property is as follows /// UPDATE SET WHERE /// SELECT FROM WHERE /// These interactions are executed in immediate succession /// just to verify the property that our updates did what they /// were supposed to do. ReadYourUpdatesBack { update: Update, select: Select, }, /// TableHasExpectedContent is a property in which the table /// must have the expected content, i.e. all the insertions and /// updates and deletions should have been persisted in the way /// we think they were. /// The execution of the property is as follows /// SELECT * FROM /// ASSERT TableHasExpectedContent { table: String, }, /// AllTablesHaveExpectedContent is a property in which the table /// must have the expected content, i.e. all the insertions and /// updates and deletions should have been persisted in the way /// we think they were. /// The execution of the property is as follows /// SELECT * FROM /// ASSERT /// for each table in the simulator model AllTableHaveExpectedContent { tables: Vec, }, /// Double Create Failure is a property in which creating /// the same table twice leads to an error. /// The execution of the property is as follows /// CREATE TABLE (...) /// I_0 /// I_1 /// ... /// I_n /// CREATE TABLE (...) -> Error /// The interactions in the middle has the following constraints; /// - There will be no errors in the middle interactions. /// - Table `t` will not be renamed or dropped. DoubleCreateFailure { /// The create query create: Create, /// Additional interactions in the middle of the property queries: Vec, }, /// Select Limit is a property in which the select query /// has a limit clause that is respected by the query. /// The execution of the property is as follows /// SELECT * FROM WHERE LIMIT /// This property is a single-interaction property. /// The interaction has the following constraints; /// - The select query will respect the limit clause. SelectLimit { /// The select query select: Select, }, /// Delete-Select is a property in which the deleted row /// must not be in the resulting rows of a select query that has a /// where clause that matches the deleted row. In practice, `p1` of /// the delete query will be used as the predicate for the select query, /// hence the select should return NO ROWS. /// The execution of the property is as follows /// DELETE FROM WHERE /// I_0 /// I_1 /// ... /// I_n /// SELECT * FROM WHERE /// The interactions in the middle has the following constraints; /// - There will be no errors in the middle interactions. /// - A row that holds for the predicate will not be inserted. /// - The table `t` will not be renamed, dropped, or altered. DeleteSelect { table: String, predicate: Predicate, queries: Vec, }, /// Drop-Select is a property in which selecting from a dropped table /// should result in an error. /// The execution of the property is as follows /// DROP TABLE /// I_0 /// I_1 /// ... /// I_n /// SELECT * FROM WHERE -> Error /// The interactions in the middle has the following constraints; /// - There will be no errors in the middle interactions. /// - The table `t` will not be created, no table will be renamed to `t`. DropSelect { table: String, queries: Vec, select: Select, }, /// Select-Select-Optimizer is a property in which we test the optimizer by /// running two equivalent select queries, one with `SELECT from ` /// and the other with `SELECT * from WHERE `. As highlighted by /// Rigger et al. in Non-Optimizing Reference Engine Construction(NoREC), SQLite /// tends to optimize `where` statements while keeping the result column expressions /// unoptimized. This property is used to test the optimizer. The property is successful /// if the two queries return the same number of rows. SelectSelectOptimizer { table: String, predicate: Predicate, }, /// Where-True-False-Null is a property that tests the boolean logic implementation /// in the database. It relies on the fact that `P == true || P == false || P == null` should return true, /// as SQLite uses a ternary logic system. This property is invented in "Finding Bugs in Database Systems via Query Partitioning" /// by Rigger et al. and it is canonically called Ternary Logic Partitioning (TLP). WhereTrueFalseNull { select: Select, predicate: Predicate, }, /// UNION-ALL-Preserves-Cardinality is a property that tests the UNION ALL operator /// implementation in the database. It relies on the fact that `SELECT * FROM WHERE UNION ALL SELECT * FROM WHERE ` /// should return the same number of rows as `SELECT FROM WHERE `. /// > The property is succesfull when the UNION ALL of 2 select queries returns the same number of rows /// > as the sum of the two select queries. UNIONAllPreservesCardinality { select: Select, where_clause: Predicate, }, /// FsyncNoWait is a property which tests if we do not loose any data after not waiting for fsync. /// /// # Interactions /// - Executes the `query` without waiting for fsync /// - Drop all connections and Reopen the database /// - Execute the `query` again /// - Query tables to assert that the values were inserted /// FsyncNoWait { query: Query, }, FaultyQuery { query: Query, }, /// Property used to subsititute a property with its queries only Queries { queries: Vec, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InteractiveQueryInfo { start_with_immediate: bool, end_with_commit: bool, } type PropertyQueryGenFunc<'a, R, G> = fn(&mut R, &G, &QueryDistribution, &Property) -> Option; impl Property { pub(crate) fn name(&self) -> &str { match self { Property::InsertValuesSelect { .. } => "Insert-Values-Select", Property::ReadYourUpdatesBack { .. } => "Read-Your-Updates-Back", Property::TableHasExpectedContent { .. } => "Table-Has-Expected-Content", Property::AllTableHaveExpectedContent { .. } => "All-Tables-Have-Expected-Content", Property::DoubleCreateFailure { .. } => "Double-Create-Failure", Property::SelectLimit { .. } => "Select-Limit", Property::DeleteSelect { .. } => "Delete-Select", Property::DropSelect { .. } => "Drop-Select", Property::SelectSelectOptimizer { .. } => "Select-Select-Optimizer", Property::WhereTrueFalseNull { .. } => "Where-True-False-Null", Property::FsyncNoWait { .. } => "FsyncNoWait", Property::FaultyQuery { .. } => "FaultyQuery", Property::UNIONAllPreservesCardinality { .. } => "UNION-All-Preserves-Cardinality", Property::Queries { .. } => "Queries", } } /// Property Does some sort of fault injection pub fn check_tables(&self) -> bool { matches!( self, Property::FsyncNoWait { .. } | Property::FaultyQuery { .. } ) } pub fn get_extensional_queries(&mut self) -> Option<&mut Vec> { match self { Property::InsertValuesSelect { queries, .. } | Property::DoubleCreateFailure { queries, .. } | Property::DeleteSelect { queries, .. } | Property::DropSelect { queries, .. } | Property::Queries { queries } => Some(queries), Property::FsyncNoWait { .. } | Property::FaultyQuery { .. } => None, Property::SelectLimit { .. } | Property::SelectSelectOptimizer { .. } | Property::WhereTrueFalseNull { .. } | Property::UNIONAllPreservesCardinality { .. } | Property::ReadYourUpdatesBack { .. } | Property::TableHasExpectedContent { .. } | Property::AllTableHaveExpectedContent { .. } => None, } } pub(super) fn get_extensional_query_gen_function(&self) -> PropertyQueryGenFunc where R: rand::Rng + ?Sized, G: GenerationContext, { match self { Property::InsertValuesSelect { .. } => { // - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort) // - [x] The inserted row will not be deleted. // - [x] The inserted row will not be updated. // - [x] The table `t` will not be renamed, dropped, or altered. |rng: &mut R, ctx: &G, query_distr: &QueryDistribution, property: &Property| { let Property::InsertValuesSelect { insert, row_index, .. } = property else { unreachable!(); }; let query = Query::arbitrary_from(rng, ctx, query_distr); let table_name = insert.table(); let table = ctx .tables() .iter() .find(|table| table.name == table_name) .unwrap(); let rows = insert.rows(); let row = &rows[*row_index]; match &query { Query::Delete(Delete { table: t, predicate, }) if t == &table.name && predicate.test(row, table) => { // The inserted row will not be deleted. None } Query::Create(Create { table: t }) if t.name == table.name => { // There will be no errors in the middle interactions. // - Creating the same table is an error None } Query::Update(Update { table: t, set_values: _, predicate, }) if t == &table.name && predicate.test(row, table) => { // The inserted row will not be updated. None } Query::Drop(Drop { table: t }) if *t == table.name => { // Cannot drop the table we are inserting None } Query::AlterTable(AlterTable { table_name: t, .. }) if *t == table.name => { // Cannot alter the table we are inserting None } _ => Some(query), } } } Property::DoubleCreateFailure { .. } => { // The interactions in the middle has the following constraints; // - [x] There will be no errors in the middle interactions.(best effort) // - [x] Table `t` will not be renamed or dropped. |rng: &mut R, ctx: &G, query_distr: &QueryDistribution, property: &Property| { let Property::DoubleCreateFailure { create, .. } = property else { unreachable!() }; let table_name = create.table.name.clone(); let table = ctx .tables() .iter() .find(|table| table.name == table_name) .unwrap(); let query = Query::arbitrary_from(rng, ctx, query_distr); match &query { Query::Create(Create { table: t }) if t.name == table.name => { // There will be no errors in the middle interactions. // - Creating the same table is an error None } Query::Drop(Drop { table: t }) if *t == table.name => { // Cannot Drop the created table None } Query::AlterTable(AlterTable { table_name: t, .. }) if *t == table.name => { // Cannot alter the table we created None } _ => Some(query), } } } Property::DeleteSelect { .. } => { // - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort) // - [x] A row that holds for the predicate will not be inserted. // - [x] The table `t` will not be renamed, dropped, or altered. |rng, ctx, query_distr, property| { let Property::DeleteSelect { table: table_name, predicate, .. } = property else { unreachable!() }; let table_name = table_name.clone(); let table = ctx .tables() .iter() .find(|table| table.name == table_name) .unwrap(); let query = Query::arbitrary_from(rng, ctx, query_distr); match &query { Query::Insert(Insert::Values { table: t, values }) if *t == table_name && values.iter().any(|v| predicate.test(v, table)) => { // A row that holds for the predicate will not be inserted. None } Query::Insert(Insert::Select { table: t, select: _, }) if t == &table.name => { // A row that holds for the predicate will not be inserted. None } Query::Update(Update { table: t, .. }) if t == &table.name => { // A row that holds for the predicate will not be updated. None } Query::Create(Create { table: t }) if t.name == table.name => { // There will be no errors in the middle interactions. // - Creating the same table is an error None } Query::Drop(Drop { table: t }) if *t == table.name => { // Cannot Drop the same table None } Query::AlterTable(AlterTable { table_name: t, .. }) if *t == table.name => { // Cannot alter the same table None } _ => Some(query), } } } Property::DropSelect { .. } => { // - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort) // - [x] The table `t` will not be created, no table will be renamed to `t`. |rng, ctx, query_distr, property: &Property| { let Property::DropSelect { table: table_name, .. } = property else { unreachable!() }; let query = Query::arbitrary_from(rng, ctx, query_distr); match &query { Query::Create(Create { table: t }) if t.name == *table_name => { // - The table `t` will not be created None } Query::AlterTable(AlterTable { table_name: t, alter_table_type: AlterTableType::RenameTo { new_name }, }) if t == table_name || new_name == table_name => { // no table will be renamed to `t` None } _ => Some(query), } } } Property::Queries { .. } => { unreachable!("No extensional querie generation for `Property::Queries`") } Property::FsyncNoWait { .. } | Property::FaultyQuery { .. } => { unreachable!("No extensional queries") } Property::SelectLimit { .. } | Property::SelectSelectOptimizer { .. } | Property::WhereTrueFalseNull { .. } | Property::UNIONAllPreservesCardinality { .. } | Property::ReadYourUpdatesBack { .. } | Property::TableHasExpectedContent { .. } | Property::AllTableHaveExpectedContent { .. } => { unreachable!("No extensional queries") } } } /// interactions construct a list of interactions, which is an executable representation of the property. /// the requirement of property -> vec conversion emerges from the need to serialize the property, /// and `interaction` cannot be serialized directly. pub(crate) fn interactions(&self, connection_index: usize) -> Vec { match self { Property::AllTableHaveExpectedContent { tables } => { assert_all_table_values(tables, connection_index).collect() } Property::TableHasExpectedContent { table } => { let table = table.to_string(); let table_name = table.clone(); let assumption = InteractionType::Assumption(Assertion::new( format!("table {} exists", table.clone()), move |_: &Vec, env: &mut SimulatorEnv| { let conn_tables = env.get_conn_tables(connection_index); if conn_tables.iter().any(|t| t.name == table_name) { Ok(Ok(())) } else { Ok(Err(format!("table {table_name} does not exist"))) } }, )); let select_interaction = InteractionType::Query(Query::Select(Select::simple( table.clone(), Predicate::true_(), ))); let assertion = InteractionType::Assertion(Assertion::new( format!("table {} should have the expected content", table.clone()), move |stack: &Vec, env| { let rows = stack.last().unwrap(); let Ok(rows) = rows else { return Ok(Err(format!("expected rows but got error: {rows:?}"))); }; let conn_tables = env.get_conn_tables(connection_index); let sim_table = conn_tables .iter() .find(|t| t.name == table) .expect("table should be in enviroment"); if rows.len() != sim_table.rows.len() { print_diff(&sim_table.rows, rows, "simulator", "database"); return Ok(Err(format!( "expected {} rows but got {} for table {}", sim_table.rows.len(), rows.len(), table.clone() ))); } for expected_row in sim_table.rows.iter() { if !rows.contains(expected_row) { print_diff(&sim_table.rows, rows, "simulator", "database"); return Ok(Err(format!( "expected row {:?} not found in table {}", expected_row, table.clone() ))); } } Ok(Ok(())) }, )); vec![ Interaction::new(connection_index, assumption), Interaction::new(connection_index, select_interaction), Interaction::new(connection_index, assertion), ] } Property::ReadYourUpdatesBack { update, select } => { let table = update.table().to_string(); let assumption = InteractionType::Assumption(Assertion::new( format!("table {} exists", table.clone()), move |_: &Vec, env: &mut SimulatorEnv| { let conn_tables = env.get_conn_tables(connection_index); if conn_tables.iter().any(|t| t.name == table.clone()) { Ok(Ok(())) } else { Ok(Err(format!("table {} does not exist", table.clone()))) } }, )); let update_interaction = InteractionType::Query(Query::Update(update.clone())); let select_interaction = InteractionType::Query(Query::Select(select.clone())); let update = update.clone(); let table = update.table().to_string(); let assertion = InteractionType::Assertion(Assertion::new( format!( "updated rows should be found and have the updated values for table {}", table.clone() ), move |stack: &Vec, _| { let rows = stack.last().unwrap(); match rows { Ok(rows) => { for row in rows { for (i, (col, val)) in update.set_values.iter().enumerate() { if &row[i] != val { let update_rows = update .set_values .iter() .map(|(_, val)| val.clone()) .collect::>(); print_diff( &[row.to_vec()], &[update_rows], "database", "update-clause", ); return Ok(Err(format!( "updated row {} has incorrect value for column {col}: expected {val}, got {}", i, row[i] ))); } } } Ok(Ok(())) } Err(err) => Err(LimboError::InternalError(err.to_string())), } }, )); vec![ Interaction::new(connection_index, assumption), Interaction::new(connection_index, update_interaction), Interaction::new(connection_index, select_interaction), Interaction::new(connection_index, assertion), ] } Property::InsertValuesSelect { insert, row_index, queries, select, interactive, } => { let (table, values) = if let Insert::Values { table, values } = insert { (table, values) } else { unreachable!( "insert query should be Insert::Values for Insert-Values-Select property" ) }; // Check that the insert query has at least 1 value assert!( !values.is_empty(), "insert query should have at least 1 value" ); // Pick a random row within the insert values let row = values[*row_index].clone(); // Assume that the table exists let assumption = InteractionType::Assumption(Assertion::new( format!("table {} exists", insert.table()), { let table_name = table.clone(); move |_: &Vec, env: &mut SimulatorEnv| { let conn_tables = env.get_conn_tables(connection_index); if conn_tables.iter().any(|t| t.name == table_name) { Ok(Ok(())) } else { Ok(Err(format!("table {table_name} does not exist"))) } } }, )); let assertion = InteractionType::Assertion(Assertion::new( format!( "row [{:?}] should be found in table {}, interactive={} commit={}, rollback={}", row.iter().map(|v| v.to_string()).collect::>(), insert.table(), interactive.is_some(), interactive .as_ref() .map(|i| i.end_with_commit) .unwrap_or(false), interactive .as_ref() .map(|i| !i.end_with_commit) .unwrap_or(false), ), move |stack: &Vec, _| { let rows = stack.last().unwrap(); match rows { Ok(rows) => { let found = rows.iter().any(|r| r == &row); if found { Ok(Ok(())) } else { Ok(Err(format!( "row [{:?}] not found in table", row.iter().map(|v| v.to_string()).collect::>() ))) } } Err(err) => Err(LimboError::InternalError(err.to_string())), } }, )); let mut interactions = Vec::new(); interactions.push(Interaction::new(connection_index, assumption)); interactions.push(Interaction::new( connection_index, InteractionType::Query(Query::Insert(insert.clone())), )); interactions.extend( queries .clone() .into_iter() .map(|q| Interaction::new(connection_index, InteractionType::Query(q))), ); interactions.push(Interaction::new( connection_index, InteractionType::Query(Query::Select(select.clone())), )); interactions.push(Interaction::new(connection_index, assertion)); interactions } Property::DoubleCreateFailure { create, queries } => { let table_name = create.table.name.clone(); let assumption = InteractionType::Assumption(Assertion::new( "Double-Create-Failure should not be called on an existing table".to_string(), move |_: &Vec, env: &mut SimulatorEnv| { let conn_tables = env.get_conn_tables(connection_index); if !conn_tables.iter().any(|t| t.name == table_name) { Ok(Ok(())) } else { Ok(Err(format!("table {table_name} already exists"))) } }, )); let cq1 = InteractionType::Query(Query::Create(create.clone())); let cq2 = InteractionType::Query(Query::Create(create.clone())); let table_name = create.table.name.clone(); let assertion = InteractionType::Assertion(Assertion::new("creating two tables with the name should result in a failure for the second query" .to_string(), move |stack: &Vec, env| { let last = stack.last().unwrap(); match last { Ok(success) => Ok(Err(format!("expected table creation to fail but it succeeded: {success:?}"))), Err(e) => { if e.to_string().to_lowercase().contains(&format!("table {table_name} already exists")) { // On error we rollback the transaction if there is any active here env.rollback_conn(connection_index); Ok(Ok(())) } else { Ok(Err(format!("expected table already exists error, got: {e}"))) } } } }) ); let mut interactions = Vec::new(); interactions.push(Interaction::new(connection_index, assumption)); interactions.push(Interaction::new(connection_index, cq1)); interactions.extend( queries .clone() .into_iter() .map(|q| Interaction::new(connection_index, InteractionType::Query(q))), ); interactions.push(Interaction::new_ignore_error(connection_index, cq2)); interactions.push(Interaction::new(connection_index, assertion)); interactions } Property::SelectLimit { select } => { let assumption = InteractionType::Assumption(Assertion::new( format!( "table ({}) exists", select .dependencies() .into_iter() .collect::>() .join(", ") ), { let table_name = select.dependencies(); move |_: &Vec, env: &mut SimulatorEnv| { let conn_tables = env.get_conn_tables(connection_index); if table_name .iter() .all(|table| conn_tables.iter().any(|t| t.name == *table)) { Ok(Ok(())) } else { let missing_tables = table_name .iter() .filter(|t| !conn_tables.iter().any(|t2| t2.name == **t)) .collect::>(); Ok(Err(format!("missing tables: {missing_tables:?}"))) } } }, )); let limit = select .limit .expect("Property::SelectLimit without a LIMIT clause"); let assertion = InteractionType::Assertion(Assertion::new( "select query should respect the limit clause".to_string(), move |stack: &Vec, _| { let last = stack.last().unwrap(); match last { Ok(rows) => { if limit >= rows.len() { Ok(Ok(())) } else { Ok(Err(format!( "limit {} violated: got {} rows", limit, rows.len() ))) } } Err(_) => Ok(Ok(())), } }, )); vec![ Interaction::new(connection_index, assumption), Interaction::new( connection_index, InteractionType::Query(Query::Select(select.clone())), ), Interaction::new(connection_index, assertion), ] } Property::DeleteSelect { table, predicate, queries, } => { let assumption = InteractionType::Assumption(Assertion::new( format!("table {table} exists"), { let table = table.clone(); move |_: &Vec, env: &mut SimulatorEnv| { let conn_tables = env.get_conn_tables(connection_index); if conn_tables.iter().any(|t| t.name == table) { Ok(Ok(())) } else { { let available_tables: Vec = conn_tables.iter().map(|t| t.name.clone()).collect(); Ok(Err(format!( "table \'{table}\' not found. Available tables: {available_tables:?}" ))) } } } }, )); let delete = InteractionType::Query(Query::Delete(Delete { table: table.clone(), predicate: predicate.clone(), })); let select = InteractionType::Query(Query::Select(Select::simple( table.clone(), predicate.clone(), ))); let assertion = InteractionType::Assertion(Assertion::new( format!("`{select}` should return no values for table `{table}`",), move |stack: &Vec, _| { let rows = stack.last().unwrap(); match rows { Ok(rows) => { if rows.is_empty() { Ok(Ok(())) } else { Ok(Err(format!( "expected no rows but got {} rows: {:?}", rows.len(), rows.iter() .map(|r| print_row(r)) .collect::>() .join(", ") ))) } } Err(err) => Err(LimboError::InternalError(err.to_string())), } }, )); let mut interactions = Vec::new(); interactions.push(Interaction::new(connection_index, assumption)); interactions.push(Interaction::new(connection_index, delete)); interactions.extend( queries .clone() .into_iter() .map(|q| Interaction::new(connection_index, InteractionType::Query(q))), ); interactions.push(Interaction::new(connection_index, select)); interactions.push(Interaction::new(connection_index, assertion)); interactions } Property::DropSelect { table, queries, select, } => { let assumption = InteractionType::Assumption(Assertion::new( format!("table {table} exists"), { let table = table.clone(); move |_, env: &mut SimulatorEnv| { let conn_tables = env.get_conn_tables(connection_index); if conn_tables.iter().any(|t| t.name == table) { Ok(Ok(())) } else { { let available_tables: Vec = conn_tables.iter().map(|t| t.name.clone()).collect(); Ok(Err(format!( "table \'{table}\' not found. Available tables: {available_tables:?}" ))) } } } }, )); let table_name = table.clone(); let assertion = InteractionType::Assertion(Assertion::new( format!("select query should result in an error for table '{table}'"), move |stack: &Vec, _| { let last = stack.last().unwrap(); match last { Ok(success) => Ok(Err(format!( "expected table creation to fail but it succeeded: {success:?}" ))), Err(e) => match e { e if e .to_string() .contains(&format!("no such table: {table_name}")) => { Ok(Ok(())) } _ => Ok(Err(format!( "expected table does not exist error, got: {e}" ))), }, } }, )); let drop = InteractionType::Query(Query::Drop(Drop { table: table.clone(), })); let select = InteractionType::Query(Query::Select(select.clone())); let mut interactions = Vec::new(); interactions.push(Interaction::new(connection_index, assumption)); interactions.push(Interaction::new(connection_index, drop)); interactions.extend( queries .clone() .into_iter() .map(|q| Interaction::new(connection_index, InteractionType::Query(q))), ); interactions.push(Interaction::new_ignore_error(connection_index, select)); interactions.push(Interaction::new(connection_index, assertion)); interactions } Property::SelectSelectOptimizer { table, predicate } => { let assumption = InteractionType::Assumption(Assertion::new( format!("table {table} exists"), { let table = table.clone(); move |_: &Vec, env: &mut SimulatorEnv| { let conn_tables = env.get_conn_tables(connection_index); if conn_tables.iter().any(|t| t.name == table) { Ok(Ok(())) } else { { let available_tables: Vec = conn_tables.iter().map(|t| t.name.clone()).collect(); Ok(Err(format!( "table \'{table}\' not found. Available tables: {available_tables:?}" ))) } } } }, )); let select1 = InteractionType::Query(Query::Select(Select::single( table.clone(), vec![ResultColumn::Expr(predicate.clone())], Predicate::true_(), None, Distinctness::All, ))); let select2_query = Query::Select(Select::simple(table.clone(), predicate.clone())); let select2 = InteractionType::Query(select2_query); let assertion = InteractionType::Assertion(Assertion::new( "select queries should return the same amount of results".to_string(), move |stack: &Vec, _| { let select_star = stack.last().unwrap(); let select_predicate = stack.get(stack.len() - 2).unwrap(); match (select_predicate, select_star) { (Ok(rows1), Ok(rows2)) => { // If rows1 results have more than 1 column, there is a problem if rows1.iter().any(|vs| vs.len() > 1) { return Err(LimboError::InternalError( "Select query without the star should return only one column".to_string(), )); } // Count the 1s in the select query without the star let rows1_count = rows1 .iter() .filter(|vs| { let v = vs.first().unwrap(); v.as_bool() }) .count(); tracing::debug!( "select1 returned {} rows, select2 returned {} rows", rows1_count, rows2.len() ); if rows1_count == rows2.len() { Ok(Ok(())) } else { Ok(Err(format!( "row counts don't match: {} vs {}", rows1_count, rows2.len() ))) } } (Err(e1), Err(e2)) => { tracing::debug!("Error in select1 AND select2: {}, {}", e1, e2); Ok(Ok(())) } (Err(e), _) | (_, Err(e)) => { tracing::error!("Error in select1 OR select2: {}", e); Err(LimboError::InternalError(e.to_string())) } } }, )); vec![ Interaction::new(connection_index, assumption), Interaction::new(connection_index, select1), Interaction::new(connection_index, select2), Interaction::new(connection_index, assertion), ] } Property::FsyncNoWait { query } => { vec![Interaction::new( connection_index, InteractionType::FsyncQuery(query.clone()), )] } Property::FaultyQuery { query } => { let query_clone = query.clone(); // A fault may not occur as we first signal we want a fault injected, // then when IO is called the fault triggers. It may happen that a fault is injected // but no IO happens right after it let assert = Assertion::new( "fault occured".to_string(), move |stack, env: &mut SimulatorEnv| { let last = stack.last().unwrap(); match last { Ok(_) => { let _ = query_clone .shadow(&mut env.get_conn_tables_mut(connection_index)); Ok(Ok(())) } Err(err) => { // We cannot make any assumptions about the error content; all we are about is, if the statement errored, // we don't shadow the results into the simulator env, i.e. we assume whatever the statement did was rolled back. tracing::error!("Fault injection produced error: {err}"); // On error we rollback the transaction if there is any active here env.rollback_conn(connection_index); Ok(Ok(())) } } }, ); [ InteractionType::FaultyQuery(query.clone()), InteractionType::Assertion(assert), ] .into_iter() .map(|i| Interaction::new(connection_index, i)) .collect() } Property::WhereTrueFalseNull { select, predicate } => { let assumption = InteractionType::Assumption(Assertion::new( format!( "tables ({}) exists", select .dependencies() .into_iter() .collect::>() .join(", ") ), { let tables = select.dependencies(); move |_: &Vec, env: &mut SimulatorEnv| { let conn_tables = env.get_conn_tables(connection_index); if tables .iter() .all(|table| conn_tables.iter().any(|t| t.name == *table)) { Ok(Ok(())) } else { let missing_tables = tables .iter() .filter(|t| !conn_tables.iter().any(|t2| t2.name == **t)) .collect::>(); Ok(Err(format!("missing tables: {missing_tables:?}"))) } } }, )); let old_predicate = select.body.select.where_clause.clone(); let p_true = Predicate::and(vec![old_predicate.clone(), predicate.clone()]); let p_false = Predicate::and(vec![ old_predicate.clone(), Predicate::not(predicate.clone()), ]); let p_null = Predicate::and(vec![ old_predicate.clone(), Predicate::is(predicate.clone(), Predicate::null()), ]); let select_tlp = Select { body: SelectBody { select: Box::new(SelectInner { distinctness: select.body.select.distinctness, columns: select.body.select.columns.clone(), from: select.body.select.from.clone(), where_clause: p_true, order_by: None, }), compounds: vec![ CompoundSelect { operator: CompoundOperator::UnionAll, select: Box::new(SelectInner { distinctness: select.body.select.distinctness, columns: select.body.select.columns.clone(), from: select.body.select.from.clone(), where_clause: p_false, order_by: None, }), }, CompoundSelect { operator: CompoundOperator::UnionAll, select: Box::new(SelectInner { distinctness: select.body.select.distinctness, columns: select.body.select.columns.clone(), from: select.body.select.from.clone(), where_clause: p_null, order_by: None, }), }, ], }, limit: None, }; let select = InteractionType::Query(Query::Select(select.clone())); let select_tlp = InteractionType::Query(Query::Select(select_tlp)); // select and select_tlp should return the same rows let assertion = InteractionType::Assertion(Assertion::new( "select and select_tlp should return the same rows".to_string(), move |stack: &Vec, _: &mut SimulatorEnv| { if stack.len() < 2 { return Err(LimboError::InternalError( "Not enough result sets on the stack".to_string(), )); } let select_result_set = stack.get(stack.len() - 2).unwrap(); let select_tlp_result_set = stack.last().unwrap(); match (select_result_set, select_tlp_result_set) { (Ok(select_rows), Ok(select_tlp_rows)) => { if select_rows.len() != select_tlp_rows.len() { return Ok(Err(format!( "row count mismatch: select returned {} rows, select_tlp returned {} rows", select_rows.len(), select_tlp_rows.len() ))); } // Check if any row in select_rows is not in select_tlp_rows for row in select_rows.iter() { if !select_tlp_rows.iter().any(|r| r == row) { tracing::debug!( "select and select_tlp returned different rows, ({}) is in select but not in select_tlp", row.iter() .map(|v| v.to_string()) .collect::>() .join(", ") ); return Ok(Err(format!( "row mismatch: row [{}] exists in select results but not in select_tlp results", print_row(row) ))); } } // Check if any row in select_tlp_rows is not in select_rows for row in select_tlp_rows.iter() { if !select_rows.iter().any(|r| r == row) { tracing::debug!( "select and select_tlp returned different rows, ({}) is in select_tlp but not in select", row.iter() .map(|v| v.to_string()) .collect::>() .join(", ") ); return Ok(Err(format!( "row mismatch: row [{}] exists in select_tlp but not in select", print_row(row) ))); } } // If we reach here, the rows are the same tracing::trace!( "select and select_tlp returned the same rows: {:?}", select_rows ); Ok(Ok(())) } (Err(e), _) | (_, Err(e)) => { tracing::error!("Error in select or select_tlp: {}", e); Err(LimboError::InternalError(e.to_string())) } } }, )); vec![ Interaction::new(connection_index, assumption), Interaction::new(connection_index, select), Interaction::new(connection_index, select_tlp), Interaction::new(connection_index, assertion), ] } Property::UNIONAllPreservesCardinality { select, where_clause, } => { let s1 = select.clone(); let mut s2 = select.clone(); s2.body.select.where_clause = where_clause.clone(); let s3 = Select::compound(s1.clone(), s2.clone(), CompoundOperator::UnionAll); vec![ InteractionType::Query(Query::Select(s1.clone())), InteractionType::Query(Query::Select(s2.clone())), InteractionType::Query(Query::Select(s3.clone())), InteractionType::Assertion(Assertion::new( "UNION ALL should preserve cardinality".to_string(), move |stack: &Vec, _: &mut SimulatorEnv| { if stack.len() < 3 { return Err(LimboError::InternalError( "Not enough result sets on the stack".to_string(), )); } let select1 = stack.get(stack.len() - 3).unwrap(); let select2 = stack.get(stack.len() - 2).unwrap(); let union_all = stack.last().unwrap(); match (select1, select2, union_all) { (Ok(rows1), Ok(rows2), Ok(union_rows)) => { let count1 = rows1.len(); let count2 = rows2.len(); let union_count = union_rows.len(); if union_count == count1 + count2 { Ok(Ok(())) } else { Ok(Err(format!( "UNION ALL should preserve cardinality but it didn't: {count1} + {count2} != {union_count}" ))) } } (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => { tracing::error!("Error in select queries: {}", e); Err(LimboError::InternalError(e.to_string())) } } }, )), ].into_iter().map(|i| Interaction::new(connection_index, i)).collect() } Property::Queries { queries } => queries .clone() .into_iter() .map(|query| Interaction::new(connection_index, InteractionType::Query(query))) .collect(), } } } fn assert_all_table_values( tables: &[String], connection_index: usize, ) -> impl Iterator + use<'_> { tables.iter().flat_map(move |table| { let select = InteractionType::Query(Query::Select(Select::simple( table.clone(), Predicate::true_(), ))); let assertion = InteractionType::Assertion(Assertion::new(format!("table {table} should contain all of its expected values"), { let table = table.clone(); move |stack: &Vec, env: &mut SimulatorEnv| { let conn_ctx = env.get_conn_tables(connection_index); let table = conn_ctx.iter().find(|t| t.name == table).ok_or_else(|| { LimboError::InternalError(format!( "table {table} should exist in simulator env" )) })?; let last = stack.last().unwrap(); match last { Ok(vals) => { // Check if all values in the table are present in the result set // Find a value in the table that is not in the result set let model_contains_db = table.rows.iter().find(|v| { !vals.contains(v) }); let db_contains_model = vals.iter().find(|v| { !table.rows.contains(v) }); if let Some(model_contains_db) = model_contains_db { tracing::debug!( "table {} does not contain the expected values, the simulator model has more rows than the database: {:?}", table.name, print_row(model_contains_db) ); print_diff(&table.rows, vals, "simulator", "database"); Ok(Err(format!("table {} does not contain the expected values, the simulator model has more rows than the database: {:?}", table.name, print_row(model_contains_db)))) } else if let Some(db_contains_model) = db_contains_model { tracing::debug!( "table {} does not contain the expected values, the database has more rows than the simulator model: {:?}", table.name, print_row(db_contains_model) ); print_diff(&table.rows, vals, "simulator", "database"); Ok(Err(format!("table {} does not contain the expected values, the database has more rows than the simulator model: {:?}", table.name, print_row(db_contains_model)))) } else { Ok(Ok(())) } } Err(err) => Err(LimboError::InternalError(format!("{err}"))), } } })); [select, assertion].into_iter().map(move |i| Interaction::new(connection_index, i)) }) } #[derive(Debug)] pub(super) struct Remaining { pub select: u32, pub insert: u32, pub create: u32, pub create_index: u32, pub delete: u32, pub update: u32, pub drop: u32, pub alter_table: u32, pub drop_index: u32, } pub(super) fn remaining( max_interactions: u32, opts: &QueryProfile, stats: &InteractionStats, mvcc: bool, context: &impl GenerationContext, ) -> Remaining { let total_weight = opts.total_weight(); let total_select = (max_interactions * opts.select_weight) / total_weight; let total_insert = (max_interactions * opts.insert_weight) / total_weight; let total_create = (max_interactions * opts.create_table_weight) / total_weight; let total_create_index = (max_interactions * opts.create_index_weight) / total_weight; let total_delete = (max_interactions * opts.delete_weight) / total_weight; let total_update = (max_interactions * opts.update_weight) / total_weight; let total_drop = (max_interactions * opts.drop_table_weight) / total_weight; let total_alter_table = (max_interactions * opts.alter_table_weight) / total_weight; let total_drop_index = (max_interactions * opts.drop_index) / total_weight; let remaining_select = total_select .checked_sub(stats.select_count) .unwrap_or_default(); let remaining_insert = total_insert .checked_sub(stats.insert_count) .unwrap_or_default(); let remaining_create = total_create .checked_sub(stats.create_count) .unwrap_or_default(); let mut remaining_create_index = total_create_index .checked_sub(stats.create_index_count) .unwrap_or_default(); let remaining_delete = total_delete .checked_sub(stats.delete_count) .unwrap_or_default(); let remaining_update = total_update .checked_sub(stats.update_count) .unwrap_or_default(); let remaining_drop = total_drop.checked_sub(stats.drop_count).unwrap_or_default(); let remaining_alter_table = total_alter_table .checked_sub(stats.alter_table_count) .unwrap_or_default(); let mut remaining_drop_index = total_drop_index .checked_sub(stats.alter_table_count) .unwrap_or_default(); if mvcc { // TODO: index not supported yet for mvcc remaining_create_index = 0; remaining_drop_index = 0; } // if there are no indexes do not allow creation of drop_index if !context .tables() .iter() .any(|table| !table.indexes.is_empty()) { remaining_drop_index = 0; } Remaining { select: remaining_select, insert: remaining_insert, create: remaining_create, create_index: remaining_create_index, delete: remaining_delete, drop: remaining_drop, update: remaining_update, alter_table: remaining_alter_table, drop_index: remaining_drop_index, } } fn property_insert_values_select( rng: &mut R, _query_distr: &QueryDistribution, ctx: &impl GenerationContext, mvcc: bool, ) -> Property { assert!(!ctx.tables().is_empty()); // Get a random table let table = pick(ctx.tables(), rng); // Generate rows to insert let rows = (0..rng.random_range(1..=5)) .map(|_| Vec::::arbitrary_from(rng, ctx, table)) .collect::>(); // Pick a random row to select let row_index = pick_index(rows.len(), rng); let row = rows[row_index].clone(); // Insert the rows let insert_query = Query::Insert(Insert::Values { table: table.name.clone(), values: rows, }); // Choose if we want queries to be executed in an interactive transaction let interactive = if !mvcc && rng.random_bool(0.5) { Some(InteractiveQueryInfo { start_with_immediate: rng.random_bool(0.5), end_with_commit: rng.random_bool(0.5), }) } else { None }; let amount = rng.random_range(0..3); let mut queries = Vec::with_capacity(amount + 2); if let Some(ref interactive) = interactive { queries.push(Query::Begin(if interactive.start_with_immediate { Begin::Immediate } else { Begin::Deferred })); } queries.extend(std::iter::repeat_n(Query::Placeholder, amount)); if let Some(ref interactive) = interactive { queries.push(if interactive.end_with_commit { Query::Commit(Commit) } else { Query::Rollback(Rollback) }); } // Select the row let select_query = Select::simple( table.name.clone(), Predicate::arbitrary_from(rng, ctx, (table, &row)), ); Property::InsertValuesSelect { insert: insert_query.unwrap_insert(), row_index, queries, select: select_query, interactive, } } fn property_read_your_updates_back( rng: &mut R, _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { // e.g. UPDATE t SET a=1, b=2 WHERE c=1; let update = Update::arbitrary(rng, ctx); // e.g. SELECT a, b FROM t WHERE c=1; let select = Select::single( update.table().to_string(), update .set_values .iter() .map(|(col, _)| ResultColumn::Column(col.clone())) .collect(), update.predicate.clone(), None, Distinctness::All, ); Property::ReadYourUpdatesBack { update, select } } fn property_table_has_expected_content( rng: &mut R, _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { assert!(!ctx.tables().is_empty()); // Get a random table let table = pick(ctx.tables(), rng); Property::TableHasExpectedContent { table: table.name.clone(), } } fn property_all_tables_have_expected_content( _rng: &mut R, _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { Property::AllTableHaveExpectedContent { tables: ctx.tables().iter().map(|t| t.name.clone()).collect(), } } fn property_select_limit( rng: &mut R, _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { assert!(!ctx.tables().is_empty()); // Get a random table let table = pick(ctx.tables(), rng); // Select the table let select = Select::single( table.name.clone(), vec![ResultColumn::Star], Predicate::arbitrary_from(rng, ctx, table), Some(rng.random_range(1..=5)), Distinctness::All, ); Property::SelectLimit { select } } fn property_double_create_failure( rng: &mut R, _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { // Create the table let create_query = Create::arbitrary(rng, ctx); let amount = rng.random_range(0..3); let queries = vec![Query::Placeholder; amount]; Property::DoubleCreateFailure { create: create_query, queries, } } fn property_delete_select( rng: &mut R, _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { assert!(!ctx.tables().is_empty()); // Get a random table let table = pick(ctx.tables(), rng); // Generate a random predicate let predicate = Predicate::arbitrary_from(rng, ctx, table); let amount = rng.random_range(0..3); let queries = vec![Query::Placeholder; amount]; Property::DeleteSelect { table: table.name.clone(), predicate, queries, } } fn property_drop_select( rng: &mut R, _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { assert!(!ctx.tables().is_empty()); // Get a random table let table = pick(ctx.tables(), rng); let amount = rng.random_range(0..3); let queries = vec![Query::Placeholder; amount]; let select = Select::simple( table.name.clone(), Predicate::arbitrary_from(rng, ctx, table), ); Property::DropSelect { table: table.name.clone(), queries, select, } } fn property_select_select_optimizer( rng: &mut R, _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { assert!(!ctx.tables().is_empty()); // Get a random table let table = pick(ctx.tables(), rng); // Generate a random predicate let predicate = Predicate::arbitrary_from(rng, ctx, table); // Transform into a Binary predicate to force values to be casted to a bool let expr = ast::Expr::Binary( Box::new(predicate.0), ast::Operator::And, Box::new(Predicate::true_().0), ); Property::SelectSelectOptimizer { table: table.name.clone(), predicate: Predicate(expr), } } fn property_where_true_false_null( rng: &mut R, _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { assert!(!ctx.tables().is_empty()); // Get a random table let table = pick(ctx.tables(), rng); // Generate a random predicate let p1 = Predicate::arbitrary_from(rng, ctx, table); let p2 = Predicate::arbitrary_from(rng, ctx, table); // Create the select query let select = Select::simple(table.name.clone(), p1); Property::WhereTrueFalseNull { select, predicate: p2, } } fn property_union_all_preserves_cardinality( rng: &mut R, _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { assert!(!ctx.tables().is_empty()); // Get a random table let table = pick(ctx.tables(), rng); // Generate a random predicate let p1 = Predicate::arbitrary_from(rng, ctx, table); let p2 = Predicate::arbitrary_from(rng, ctx, table); // Create the select query let select = Select::single( table.name.clone(), vec![ResultColumn::Star], p1, None, Distinctness::All, ); Property::UNIONAllPreservesCardinality { select, where_clause: p2, } } fn property_fsync_no_wait( rng: &mut R, query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { Property::FsyncNoWait { query: Query::arbitrary_from(rng, ctx, query_distr), } } fn property_faulty_query( rng: &mut R, query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { Property::FaultyQuery { query: Query::arbitrary_from(rng, ctx, query_distr), } } type PropertyGenFunc = fn(&mut R, &QueryDistribution, &G, bool) -> Property; impl PropertyDiscriminants { fn gen_function(&self) -> PropertyGenFunc where R: rand::Rng + ?Sized, G: GenerationContext, { match self { PropertyDiscriminants::InsertValuesSelect => property_insert_values_select, PropertyDiscriminants::ReadYourUpdatesBack => property_read_your_updates_back, PropertyDiscriminants::TableHasExpectedContent => property_table_has_expected_content, PropertyDiscriminants::AllTableHaveExpectedContent => { property_all_tables_have_expected_content } PropertyDiscriminants::DoubleCreateFailure => property_double_create_failure, PropertyDiscriminants::SelectLimit => property_select_limit, PropertyDiscriminants::DeleteSelect => property_delete_select, PropertyDiscriminants::DropSelect => property_drop_select, PropertyDiscriminants::SelectSelectOptimizer => property_select_select_optimizer, PropertyDiscriminants::WhereTrueFalseNull => property_where_true_false_null, PropertyDiscriminants::UNIONAllPreservesCardinality => { property_union_all_preserves_cardinality } PropertyDiscriminants::FsyncNoWait => property_fsync_no_wait, PropertyDiscriminants::FaultyQuery => property_faulty_query, PropertyDiscriminants::Queries => { unreachable!("should not try to generate queries property") } } } fn weight( &self, env: &SimulatorEnv, remaining: &Remaining, ctx: &impl GenerationContext, ) -> u32 { let opts = ctx.opts(); match self { PropertyDiscriminants::InsertValuesSelect => { if !env.opts.disable_insert_values_select && !ctx.tables().is_empty() { u32::min(remaining.select, remaining.insert).max(1) } else { 0 } } PropertyDiscriminants::ReadYourUpdatesBack => { u32::min(remaining.select, remaining.insert).max(1) } PropertyDiscriminants::TableHasExpectedContent => { if !ctx.tables().is_empty() { remaining.select.max(1) } else { 0 } } // AllTableHaveExpectedContent should only be generated by Properties that inject faults PropertyDiscriminants::AllTableHaveExpectedContent => 0, PropertyDiscriminants::DoubleCreateFailure => { if !env.opts.disable_double_create_failure { remaining.create / 2 } else { 0 } } PropertyDiscriminants::SelectLimit => { if !env.opts.disable_select_limit && !ctx.tables().is_empty() { remaining.select } else { 0 } } PropertyDiscriminants::DeleteSelect => { if !env.opts.disable_delete_select && !ctx.tables().is_empty() { u32::min(remaining.select, remaining.insert).min(remaining.delete) } else { 0 } } PropertyDiscriminants::DropSelect => { if !env.opts.disable_drop_select && !ctx.tables().is_empty() { remaining.drop } else { 0 } } PropertyDiscriminants::SelectSelectOptimizer => { if !env.opts.disable_select_optimizer && !ctx.tables().is_empty() { remaining.select / 2 } else { 0 } } PropertyDiscriminants::WhereTrueFalseNull => { if opts.indexes && !env.opts.disable_where_true_false_null && !ctx.tables().is_empty() { remaining.select / 2 } else { 0 } } PropertyDiscriminants::UNIONAllPreservesCardinality => { if opts.indexes && !env.opts.disable_union_all_preserves_cardinality && !ctx.tables().is_empty() { remaining.select / 3 } else { 0 } } PropertyDiscriminants::FsyncNoWait => { if env.profile.io.enable && !env.opts.disable_fsync_no_wait { 50 // Freestyle number } else { 0 } } PropertyDiscriminants::FaultyQuery => { if env.profile.io.enable && env.profile.io.fault.enable && !env.opts.disable_faulty_query { 20 } else { 0 } } PropertyDiscriminants::Queries => { unreachable!("queries property should not be generated") } } } fn can_generate(queries: &[QueryDiscriminants]) -> Vec { let queries_capabilities = QueryCapabilities::from_list_queries(queries); PropertyDiscriminants::iter() .filter(|property| { !matches!(property, PropertyDiscriminants::Queries) && queries_capabilities.contains(property.requirements()) }) .collect() } pub const fn requirements(&self) -> QueryCapabilities { match self { PropertyDiscriminants::InsertValuesSelect => { QueryCapabilities::SELECT.union(QueryCapabilities::INSERT) } PropertyDiscriminants::ReadYourUpdatesBack => { QueryCapabilities::SELECT.union(QueryCapabilities::UPDATE) } PropertyDiscriminants::TableHasExpectedContent => QueryCapabilities::SELECT, PropertyDiscriminants::AllTableHaveExpectedContent => QueryCapabilities::SELECT, PropertyDiscriminants::DoubleCreateFailure => QueryCapabilities::CREATE, PropertyDiscriminants::SelectLimit => QueryCapabilities::SELECT, PropertyDiscriminants::DeleteSelect => { QueryCapabilities::SELECT.union(QueryCapabilities::DELETE) } PropertyDiscriminants::DropSelect => { QueryCapabilities::SELECT.union(QueryCapabilities::DROP) } PropertyDiscriminants::SelectSelectOptimizer => QueryCapabilities::SELECT, PropertyDiscriminants::WhereTrueFalseNull => QueryCapabilities::SELECT, PropertyDiscriminants::UNIONAllPreservesCardinality => QueryCapabilities::SELECT, PropertyDiscriminants::FsyncNoWait => QueryCapabilities::all(), PropertyDiscriminants::FaultyQuery => QueryCapabilities::all(), PropertyDiscriminants::Queries => panic!("queries property should not be generated"), } } } pub(super) struct PropertyDistribution<'a> { properties: Vec, weights: WeightedIndex, query_distr: &'a QueryDistribution, mvcc: bool, } impl<'a> PropertyDistribution<'a> { pub fn new( env: &SimulatorEnv, remaining: &Remaining, query_distr: &'a QueryDistribution, ctx: &impl GenerationContext, ) -> Result { let properties = PropertyDiscriminants::can_generate(query_distr.items()); let weights = WeightedIndex::new( properties .iter() .map(|property| property.weight(env, remaining, ctx)), )?; Ok(Self { properties, weights, query_distr, mvcc: env.profile.experimental_mvcc, }) } } impl<'a> WeightedDistribution for PropertyDistribution<'a> { type Item = PropertyDiscriminants; type GenItem = Property; fn items(&self) -> &[Self::Item] { &self.properties } fn weights(&self) -> &WeightedIndex { &self.weights } fn sample( &self, rng: &mut R, conn_ctx: &C, ) -> Self::GenItem { let properties = &self.properties; let idx = self.weights.sample(rng); let property_fn = properties[idx].gen_function(); (property_fn)(rng, self.query_distr, conn_ctx, self.mvcc) } } impl<'a> ArbitraryFrom<&PropertyDistribution<'a>> for Property { fn arbitrary_from( rng: &mut R, conn_ctx: &C, property_distr: &PropertyDistribution<'a>, ) -> Self { property_distr.sample(rng, conn_ctx) } } fn print_row(row: &[SimValue]) -> String { row.iter() .map(|v| match &v.0 { types::Value::Null => "NULL".to_string(), types::Value::Integer(i) => i.to_string(), types::Value::Float(f) => f.to_string(), types::Value::Text(t) => t.to_string(), types::Value::Blob(b) => format!( "X'{}'", b.iter() .fold(String::new(), |acc, b| acc + &format!("{b:02X}")) ), }) .collect::>() .join(", ") }