diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index 05373312f..a468e87ec 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -195,6 +195,7 @@ impl InteractionPlan { Query::Begin(_) => stats.begin_count += 1, Query::Commit(_) => stats.commit_count += 1, Query::Rollback(_) => stats.rollback_count += 1, + Query::Placeholder => {} } } for interactions in &self.plan { @@ -238,6 +239,28 @@ impl InteractionPlan { env: &mut SimulatorEnv, ) -> Option> { let num_interactions = env.opts.max_interactions as usize; + // If last interaction needs to check all db tables, generate the Property to do so + if let Some(i) = self.plan.last() + && i.check_tables() + { + let check_all_tables = Interactions::new( + i.connection_index, + InteractionsType::Property(Property::AllTableHaveExpectedContent { + tables: env + .connection_context(i.connection_index) + .tables() + .iter() + .map(|t| t.name.clone()) + .collect(), + }), + ); + + let out_interactions = check_all_tables.interactions(); + + self.push(check_all_tables); + return Some(out_interactions); + } + if self.len() < num_interactions { let conn_index = env.choose_conn(rng); let interactions = if self.mvcc && !env.conn_in_transaction(conn_index) { @@ -292,16 +315,7 @@ impl InteractionPlan { self.push(interactions); Some(out_interactions) } else { - // after we generated all interactions if some connection is still in a transaction, commit - (0..env.connections.len()) - .find(|idx| env.conn_in_transaction(*idx)) - .map(|conn_index| { - let query = Query::Commit(Commit); - let interaction = Interactions::new(conn_index, InteractionsType::Query(query)); - let out_interactions = interaction.interactions(); - self.push(interaction); - out_interactions - }) + None } } @@ -313,6 +327,7 @@ impl InteractionPlan { let iter = interactions.into_iter(); PlanGenerator { plan: self, + peek: None, iter, rng, } @@ -382,28 +397,145 @@ impl InteractionPlanIterator for &mut T { pub struct PlanGenerator<'a, R: rand::Rng> { plan: &'a mut InteractionPlan, + peek: Option, iter: as IntoIterator>::IntoIter, rng: &'a mut R, } +impl<'a, R: rand::Rng> PlanGenerator<'a, R> { + fn next_interaction(&mut self, env: &mut SimulatorEnv) -> Option { + self.iter + .next() + .or_else(|| { + // Iterator ended, try to create a new iterator + // This will not be an infinte sequence because generate_next_interaction will eventually + // stop generating + let mut iter = self + .plan + .generate_next_interaction(self.rng, env) + .map_or(Vec::new().into_iter(), |interactions| { + interactions.into_iter() + }); + let next = iter.next(); + self.iter = iter; + + next + }) + .map(|interaction| { + // Certain properties can generate intermediate queries + // we need to generate them here and substitute + if let InteractionType::Query(Query::Placeholder) = &interaction.interaction { + let stats = self.plan.stats(); + + let remaining_ = remaining( + env.opts.max_interactions, + &env.profile.query, + &stats, + env.profile.experimental_mvcc, + ); + + let InteractionsType::Property(property) = + &mut self.plan.last_mut().unwrap().interactions + else { + unreachable!("only properties have extensional queries"); + }; + + let conn_ctx = env.connection_context(interaction.connection_index); + + let queries = possible_queries(conn_ctx.tables()); + let query_distr = QueryDistribution::new(queries, &remaining_); + + let query_gen = property.get_extensional_query_gen_function(); + + let mut count = 0; + let new_query = loop { + if count > 1_000_000 { + panic!("possible infinite loop in query generation"); + } + if let Some(new_query) = + (query_gen)(self.rng, &conn_ctx, &query_distr, property) + { + let queries = property.get_extensional_queries().unwrap(); + let query = queries + .iter_mut() + .find(|query| matches!(query, Query::Placeholder)) + .expect("Placeholder should be present in extensional queries"); + *query = new_query.clone(); + break new_query; + } + count += 1; + }; + Interaction::new( + interaction.connection_index, + InteractionType::Query(new_query), + ) + } else { + interaction + } + }) + } + + fn peek(&mut self, env: &mut SimulatorEnv) -> Option<&Interaction> { + if self.peek.is_none() { + self.peek = self.next_interaction(env); + } + self.peek.as_ref() + } +} + impl<'a, R: rand::Rng> InteractionPlanIterator for PlanGenerator<'a, R> { /// try to generate the next [Interactions] and store it fn next(&mut self, env: &mut SimulatorEnv) -> Option { - self.iter.next().or_else(|| { - // Iterator ended, try to create a new iterator - // This will not be an infinte sequence because generate_next_interaction will eventually - // stop generating - let mut iter = self - .plan - .generate_next_interaction(self.rng, env) - .map_or(Vec::new().into_iter(), |interactions| { - interactions.into_iter() - }); - let next = iter.next(); - self.iter = iter; + let mvcc = self.plan.mvcc; + match self.peek(env) { + Some(peek_interaction) => { + if mvcc && peek_interaction.is_ddl() { + // try to commit a transaction as we cannot execute DDL statements in concurrent mode - next - }) + let commit_connection = (0..env.connections.len()) + .find(|idx| env.conn_in_transaction(*idx)) + .map(|conn_index| { + let query = Query::Commit(Commit); + let interaction = Interactions::new( + conn_index, + InteractionsType::Query(query.clone()), + ); + + // Connections are queued for commit on `generate_next_interaction` if Interactions::Query or Interactions::Property produce a DDL statement. + // This means that the only way we will reach here, is if the DDL statement was created later in the extensional query of a Property + let queries = self + .plan + .last_mut() + .unwrap() + .get_extensional_queries() + .unwrap(); + queries.insert(0, query.clone()); + + self.plan.push(interaction); + + Interaction::new(conn_index, InteractionType::Query(query)) + }); + if commit_connection.is_some() { + return commit_connection; + } + } + + self.peek.take() + } + None => { + // after we generated all interactions if some connection is still in a transaction, commit + (0..env.connections.len()) + .find(|idx| env.conn_in_transaction(*idx)) + .map(|conn_index| { + let query = Query::Commit(Commit); + let interaction = + Interactions::new(conn_index, InteractionsType::Query(query)); + self.plan.push(interaction); + + Interaction::new(conn_index, InteractionType::Query(Query::Commit(Commit))) + }) + } + } } } @@ -451,6 +583,14 @@ impl Interactions { InteractionsType::Query(..) | InteractionsType::Fault(..) => None, } } + + /// Whether the interaction needs to check the database tables + pub fn check_tables(&self) -> bool { + match &self.interactions { + InteractionsType::Property(property) => property.check_tables(), + InteractionsType::Query(..) | InteractionsType::Fault(..) => false, + } + } } impl Deref for Interactions { @@ -766,6 +906,11 @@ impl InteractionType { pub(crate) fn execute_query(&self, conn: &mut Arc) -> ResultSet { if let Self::Query(query) = self { + assert!( + !matches!(query, Query::Placeholder), + "simulation cannot have a placeholder Query for execution" + ); + let query_str = query.to_string(); let rows = conn.query(&query_str); if rows.is_err() { @@ -1097,47 +1242,45 @@ impl ArbitraryFrom<(&SimulatorEnv, InteractionStats, usize)> for Interactions { let queries = possible_queries(conn_ctx.tables()); let query_distr = QueryDistribution::new(queries, &remaining_); - let property_distr = - PropertyDistribution::new(env, &remaining_, &query_distr, conn_ctx.opts()); + #[allow(clippy::type_complexity)] + let mut choices: Vec<(u32, Box Interactions>)> = vec![ + ( + query_distr.weights().total_weight(), + Box::new(|rng: &mut R| { + Interactions::new( + conn_index, + InteractionsType::Query(Query::arbitrary_from(rng, conn_ctx, &query_distr)), + ) + }), + ), + ( + remaining_ + .select + .min(remaining_.insert) + .min(remaining_.create) + .max(1), + Box::new(|rng: &mut R| random_fault(rng, env, conn_index)), + ), + ]; - frequency( - vec![ - ( - property_distr.weights().total_weight(), - Box::new(|rng: &mut R| { - Interactions::new( - conn_index, - InteractionsType::Property(Property::arbitrary_from( - rng, - conn_ctx, - &property_distr, - )), - ) - }), - ), - ( - query_distr.weights().total_weight(), - Box::new(|rng: &mut R| { - Interactions::new( - conn_index, - InteractionsType::Query(Query::arbitrary_from( - rng, - conn_ctx, - &query_distr, - )), - ) - }), - ), - ( - remaining_ - .select - .min(remaining_.insert) - .min(remaining_.create) - .max(1), - Box::new(|rng: &mut R| random_fault(rng, env, conn_index)), - ), - ], - rng, - ) + if let Ok(property_distr) = + PropertyDistribution::new(env, &remaining_, &query_distr, conn_ctx) + { + choices.push(( + property_distr.weights().total_weight(), + Box::new(move |rng: &mut R| { + Interactions::new( + conn_index, + InteractionsType::Property(Property::arbitrary_from( + rng, + conn_ctx, + &property_distr, + )), + ) + }), + )); + }; + + frequency(choices, rng) } } diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index b00a1114d..47b352406 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -1,3 +1,10 @@ +//! 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::{ @@ -26,11 +33,34 @@ use crate::{ }, model::{Query, QueryCapabilities, QueryDiscriminants}, profiles::query::QueryProfile, - runner::env::SimulatorEnv, + runner::env::{ShadowTablesMut, SimulatorEnv}, }; use super::plan::{Assertion, Interaction, InteractionStats, ResultSet}; +#[derive(Debug, Clone, Copy)] +struct PropertyGenContext<'a> { + tables: &'a Vec, + opts: &'a sql_generation::generation::Opts, +} + +impl<'a> PropertyGenContext<'a> { + #[inline] + fn new(tables: &'a Vec, opts: &'a Opts) -> Self { + Self { tables, opts } + } +} + +impl<'a> GenerationContext for PropertyGenContext<'a> { + fn tables(&self) -> &Vec { + self.tables + } + + fn opts(&self) -> &sql_generation::generation::Opts { + self.opts + } +} + /// Properties are representations of executable specifications /// about the database behavior. #[derive(Debug, Clone, Serialize, Deserialize, strum::EnumDiscriminants)] @@ -86,6 +116,17 @@ pub enum Property { 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 @@ -192,11 +233,9 @@ pub enum Property { /// FsyncNoWait { query: Query, - tables: Vec, }, FaultyQuery { query: Query, - tables: Vec, }, /// Property used to subsititute a property with its queries only Queries { @@ -210,12 +249,16 @@ pub struct InteractiveQueryInfo { 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", @@ -229,6 +272,14 @@ impl Property { } } + /// 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, .. } @@ -242,7 +293,191 @@ impl Property { | Property::WhereTrueFalseNull { .. } | Property::UNIONAllPreservesCardinality { .. } | Property::ReadYourUpdatesBack { .. } - | Property::TableHasExpectedContent { .. } => None, + | 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. + // - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented) + |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 + } + _ => 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) + // - [ ] Table `t` will not be renamed or dropped.(todo: add this constraint once ALTER or DROP is implemented) + |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 + } + _ => 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. + // - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented) + + |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 + } + _ => 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) + // - [-] The table `t` will not be created, no table will be renamed to `t`. (todo: update this constraint once ALTER is implemented) + |rng, ctx, query_distr, property: &Property| { + let Property::DropSelect { + table: table_name, .. + } = property + else { + unreachable!() + }; + + let query = Query::arbitrary_from(rng, ctx, query_distr); + if let Query::Create(Create { table: t }) = &query + && t.name == *table_name + { + // - The table `t` will not be created + None + } else { + 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") + } } } @@ -251,6 +486,9 @@ impl 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(); @@ -695,17 +933,17 @@ impl Property { Ok(success) => Ok(Err(format!( "expected table creation to fail but it succeeded: {success:?}" ))), - Err(e) => { - if e.to_string() - .contains(&format!("Table {table_name} does not exist")) + Err(e) => match e { + e if e + .to_string() + .contains(&format!("no such table: {table_name}")) => { Ok(Ok(())) - } else { - Ok(Err(format!( - "expected table does not exist error, got: {e}" - ))) } - } + _ => Ok(Err(format!( + "expected table does not exist error, got: {e}" + ))), + }, } }, )); @@ -726,7 +964,7 @@ impl Property { .into_iter() .map(|q| Interaction::new(connection_index, InteractionType::Query(q))), ); - interactions.push(Interaction::new(connection_index, select)); + interactions.push(Interaction::new_ignore_error(connection_index, select)); interactions.push(Interaction::new(connection_index, assertion)); interactions @@ -820,18 +1058,13 @@ impl Property { Interaction::new(connection_index, assertion), ] } - Property::FsyncNoWait { query, tables } => { - let checks = assert_all_table_values(tables, connection_index); - Vec::from_iter( - std::iter::once(Interaction::new( - connection_index, - InteractionType::FsyncQuery(query.clone()), - )) - .chain(checks), - ) + Property::FsyncNoWait { query } => { + vec![Interaction::new( + connection_index, + InteractionType::FsyncQuery(query.clone()), + )] } - Property::FaultyQuery { query, tables } => { - let checks = assert_all_table_values(tables, connection_index); + 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 @@ -858,13 +1091,13 @@ impl Property { } }, ); - let first = [ + [ InteractionType::FaultyQuery(query.clone()), InteractionType::Assertion(assert), ] .into_iter() - .map(|i| Interaction::new(connection_index, i)); - Vec::from_iter(first.chain(checks)) + .map(|i| Interaction::new(connection_index, i)) + .collect() } Property::WhereTrueFalseNull { select, predicate } => { let assumption = InteractionType::Assumption(Assertion::new( @@ -1214,10 +1447,11 @@ pub(crate) fn remaining( fn property_insert_values_select( rng: &mut R, - query_distr: &QueryDistribution, + _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 @@ -1230,10 +1464,10 @@ fn property_insert_values_select( let row = rows[row_index].clone(); // Insert the rows - let insert_query = Insert::Values { + 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) { @@ -1244,12 +1478,11 @@ fn property_insert_values_select( } else { None }; - // Create random queries respecting the constraints - let mut queries = Vec::new(); - // - [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. - // - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented) + + 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 @@ -1257,39 +1490,9 @@ fn property_insert_values_select( Begin::Deferred })); } - for _ in 0..rng.random_range(0..3) { - let query = Query::arbitrary_from(rng, ctx, query_distr); - match &query { - Query::Delete(Delete { - table: t, - predicate, - }) => { - // The inserted row will not be deleted. - if t == &table.name && predicate.test(&row, table) { - continue; - } - } - Query::Create(Create { table: t }) => { - // There will be no errors in the middle interactions. - // - Creating the same table is an error - if t.name == table.name { - continue; - } - } - Query::Update(Update { - table: t, - set_values: _, - predicate, - }) => { - // The inserted row will not be updated. - if t == &table.name && predicate.test(&row, table) { - continue; - } - } - _ => (), - } - queries.push(query); - } + + 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) @@ -1305,7 +1508,7 @@ fn property_insert_values_select( ); Property::InsertValuesSelect { - insert: insert_query, + insert: insert_query.unwrap_insert(), row_index, queries, select: select_query, @@ -1343,6 +1546,7 @@ fn property_table_has_expected_content( ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { + assert!(!ctx.tables().is_empty()); // Get a random table let table = pick(ctx.tables(), rng); Property::TableHasExpectedContent { @@ -1350,12 +1554,24 @@ fn property_table_has_expected_content( } } +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 @@ -1371,30 +1587,16 @@ fn property_select_limit( fn property_double_create_failure( rng: &mut R, - query_distr: &QueryDistribution, + _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { // Create the table let create_query = Create::arbitrary(rng, ctx); - let table = &create_query.table; - // Create random queries respecting the constraints - let mut queries = Vec::new(); - // The interactions in the middle has the following constraints; - // - [x] There will be no errors in the middle interactions.(best effort) - // - [ ] Table `t` will not be renamed or dropped.(todo: add this constraint once ALTER or DROP is implemented) - for _ in 0..rng.random_range(0..3) { - let query = Query::arbitrary_from(rng, ctx, query_distr); - if let Query::Create(Create { table: t }) = &query { - // There will be no errors in the middle interactions. - // - Creating the same table is an error - if t.name == table.name { - continue; - } - } - queries.push(query); - } + let amount = rng.random_range(0..3); + + let queries = vec![Query::Placeholder; amount]; Property::DoubleCreateFailure { create: create_query, @@ -1404,55 +1606,19 @@ fn property_double_create_failure( fn property_delete_select( rng: &mut R, - query_distr: &QueryDistribution, + _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); - // Create random queries respecting the constraints - let mut queries = Vec::new(); - // - [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. - // - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented) - for _ in 0..rng.random_range(0..3) { - let query = Query::arbitrary_from(rng, ctx, query_distr); - match &query { - Query::Insert(Insert::Values { table: t, values }) => { - // A row that holds for the predicate will not be inserted. - if t == &table.name && values.iter().any(|v| predicate.test(v, table)) { - continue; - } - } - Query::Insert(Insert::Select { - table: t, - select: _, - }) => { - // A row that holds for the predicate will not be inserted. - if t == &table.name { - continue; - } - } - Query::Update(Update { table: t, .. }) => { - // A row that holds for the predicate will not be updated. - if t == &table.name { - continue; - } - } - Query::Create(Create { table: t }) => { - // There will be no errors in the middle interactions. - // - Creating the same table is an error - if t.name == table.name { - continue; - } - } - _ => (), - } - queries.push(query); - } + let amount = rng.random_range(0..3); + + let queries = vec![Query::Placeholder; amount]; Property::DeleteSelect { table: table.name.clone(), @@ -1463,27 +1629,17 @@ fn property_delete_select( fn property_drop_select( rng: &mut R, - query_distr: &QueryDistribution, + _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { + assert!(!ctx.tables().is_empty()); // Get a random table let table = pick(ctx.tables(), rng); - // Create random queries respecting the constraints - let mut queries = Vec::new(); - // - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort) - // - [-] The table `t` will not be created, no table will be renamed to `t`. (todo: update this constraint once ALTER is implemented) - for _ in 0..rng.random_range(0..3) { - let query = Query::arbitrary_from(rng, ctx, query_distr); - if let Query::Create(Create { table: t }) = &query { - // - The table `t` will not be created - if t.name == table.name { - continue; - } - } - queries.push(query); - } + let amount = rng.random_range(0..3); + + let queries = vec![Query::Placeholder; amount]; let select = Select::simple( table.name.clone(), @@ -1503,6 +1659,7 @@ fn property_select_select_optimizer( 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 @@ -1526,6 +1683,7 @@ fn property_where_true_false_null( 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 @@ -1547,6 +1705,7 @@ fn property_union_all_preserves_cardinality( 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 @@ -1576,7 +1735,6 @@ fn property_fsync_no_wait( ) -> Property { Property::FsyncNoWait { query: Query::arbitrary_from(rng, ctx, query_distr), - tables: ctx.tables().iter().map(|t| t.name.clone()).collect(), } } @@ -1588,7 +1746,6 @@ fn property_faulty_query( ) -> Property { Property::FaultyQuery { query: Query::arbitrary_from(rng, ctx, query_distr), - tables: ctx.tables().iter().map(|t| t.name.clone()).collect(), } } @@ -1604,6 +1761,9 @@ impl PropertyDiscriminants { 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, @@ -1621,10 +1781,16 @@ impl PropertyDiscriminants { } } - pub fn weight(&self, env: &SimulatorEnv, remaining: &Remaining, opts: &Opts) -> u32 { + pub 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 { + if !env.opts.disable_insert_values_select && !ctx.tables().is_empty() { u32::min(remaining.select, remaining.insert).max(1) } else { 0 @@ -1633,7 +1799,15 @@ impl PropertyDiscriminants { PropertyDiscriminants::ReadYourUpdatesBack => { u32::min(remaining.select, remaining.insert).max(1) } - PropertyDiscriminants::TableHasExpectedContent => remaining.select.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 @@ -1642,43 +1816,48 @@ impl PropertyDiscriminants { } } PropertyDiscriminants::SelectLimit => { - if !env.opts.disable_select_limit { + if !env.opts.disable_select_limit && !ctx.tables().is_empty() { remaining.select } else { 0 } } PropertyDiscriminants::DeleteSelect => { - if !env.opts.disable_delete_select { + 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 { - // remaining.drop - 0 + if !env.opts.disable_drop_select && !ctx.tables().is_empty() { + remaining.drop } else { 0 } } PropertyDiscriminants::SelectSelectOptimizer => { - if !env.opts.disable_select_optimizer { + 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 { + 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 { + if opts.indexes + && !env.opts.disable_union_all_preserves_cardinality + && !ctx.tables().is_empty() + { remaining.select / 3 } else { 0 @@ -1727,6 +1906,7 @@ impl PropertyDiscriminants { QueryCapabilities::SELECT.union(QueryCapabilities::UPDATE) } PropertyDiscriminants::TableHasExpectedContent => QueryCapabilities::SELECT, + PropertyDiscriminants::AllTableHaveExpectedContent => QueryCapabilities::SELECT, PropertyDiscriminants::DoubleCreateFailure => QueryCapabilities::CREATE, PropertyDiscriminants::SelectLimit => QueryCapabilities::SELECT, PropertyDiscriminants::DeleteSelect => { @@ -1762,22 +1942,21 @@ impl<'a> PropertyDistribution<'a> { env: &SimulatorEnv, remaining: &Remaining, query_distr: &'a QueryDistribution, - opts: &Opts, - ) -> Self { + ctx: &impl GenerationContext, + ) -> Result { let properties = PropertyDiscriminants::can_generate(query_distr.items()); let weights = WeightedIndex::new( properties .iter() - .map(|property| property.weight(env, remaining, opts)), - ) - .unwrap(); + .map(|property| property.weight(env, remaining, ctx)), + )?; - Self { + Ok(Self { properties, weights, query_distr, mvcc: env.profile.experimental_mvcc, - } + }) } } @@ -1816,6 +1995,49 @@ impl<'a> ArbitraryFrom<&PropertyDistribution<'a>> for Property { } } +fn generate_queries( + rng: &mut R, + ctx: &impl GenerationContext, + amount: usize, + init_queries: &[&Query], + func: F, +) -> Vec +where + F: Fn(&mut R, PropertyGenContext) -> Option, +{ + // Create random queries respecting the constraints + let mut queries = Vec::new(); + + let range = 0..amount; + if !range.is_empty() { + let mut tmp_tables = ctx.tables().clone(); + + for query in init_queries { + tmp_shadow(&mut tmp_tables, query); + } + + for _ in range { + let tmp_ctx = PropertyGenContext::new(&tmp_tables, ctx.opts()); + + let Some(query) = func(rng, tmp_ctx) else { + continue; + }; + + tmp_shadow(&mut tmp_tables, &query); + + queries.push(query); + } + } + queries +} + +fn tmp_shadow(tmp_tables: &mut Vec
, query: &Query) { + let mut tx_tables = None; + let mut tmp_shadow_tables = ShadowTablesMut::new(tmp_tables, &mut tx_tables); + + let _ = query.shadow(&mut tmp_shadow_tables); +} + fn print_row(row: &[SimValue]) -> String { row.iter() .map(|v| match &v.0 { diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index 88cd95126..914b44b35 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -29,7 +29,7 @@ fn random_create(rng: &mut R, conn_ctx: &impl GenerationC } fn random_select(rng: &mut R, conn_ctx: &impl GenerationContext) -> Query { - if rng.random_bool(0.7) { + if !conn_ctx.tables().is_empty() && rng.random_bool(0.7) { Query::Select(Select::arbitrary(rng, conn_ctx)) } else { // Random expression @@ -111,27 +111,36 @@ impl QueryDiscriminants { | QueryDiscriminants::Rollback => { unreachable!("transactional queries should not be generated") } + QueryDiscriminants::Placeholder => { + unreachable!("Query Placeholders should not be generated") + } } } pub fn weight(&self, remaining: &Remaining) -> u32 { match self { QueryDiscriminants::Create => remaining.create, - QueryDiscriminants::Select => remaining.select + remaining.select / 3, // remaining.select / 3 is for the random_expr generation + // remaining.select / 3 is for the random_expr generation + // have a max of 1 so that we always generate at least a non zero weight for `QueryDistribution` + QueryDiscriminants::Select => (remaining.select + remaining.select / 3).max(1), QueryDiscriminants::Insert => remaining.insert, QueryDiscriminants::Delete => remaining.delete, QueryDiscriminants::Update => remaining.update, - QueryDiscriminants::Drop => 0, + QueryDiscriminants::Drop => remaining.drop, QueryDiscriminants::CreateIndex => remaining.create_index, QueryDiscriminants::Begin | QueryDiscriminants::Commit | QueryDiscriminants::Rollback => { unreachable!("transactional queries should not be generated") } + QueryDiscriminants::Placeholder => { + unreachable!("Query Placeholders should not be generated") + } } } } +#[derive(Debug)] pub(super) struct QueryDistribution { queries: &'static [QueryDiscriminants], weights: WeightedIndex, diff --git a/simulator/model/mod.rs b/simulator/model/mod.rs index 510922f6b..9e3d29db2 100644 --- a/simulator/model/mod.rs +++ b/simulator/model/mod.rs @@ -32,9 +32,33 @@ pub enum Query { Begin(Begin), Commit(Commit), Rollback(Rollback), + /// Placeholder query that still needs to be generated + Placeholder, } impl Query { + pub fn as_create(&self) -> &Create { + match self { + Self::Create(create) => create, + _ => unreachable!(), + } + } + + pub fn unwrap_create(self) -> Create { + match self { + Self::Create(create) => create, + _ => unreachable!(), + } + } + + #[inline] + pub fn unwrap_insert(self) -> Insert { + match self { + Self::Insert(insert) => insert, + _ => unreachable!(), + } + } + pub fn dependencies(&self) -> IndexSet { match self { Query::Select(select) => select.dependencies(), @@ -48,6 +72,7 @@ impl Query { IndexSet::from_iter([table_name.clone()]) } Query::Begin(_) | Query::Commit(_) | Query::Rollback(_) => IndexSet::new(), + Query::Placeholder => IndexSet::new(), } } pub fn uses(&self) -> Vec { @@ -61,6 +86,7 @@ impl Query { | Query::Drop(Drop { table, .. }) => vec![table.clone()], Query::CreateIndex(CreateIndex { table_name, .. }) => vec![table_name.clone()], Query::Begin(..) | Query::Commit(..) | Query::Rollback(..) => vec![], + Query::Placeholder => vec![], } } @@ -94,6 +120,7 @@ impl Display for Query { Self::Begin(begin) => write!(f, "{begin}"), Self::Commit(commit) => write!(f, "{commit}"), Self::Rollback(rollback) => write!(f, "{rollback}"), + Self::Placeholder => Ok(()), } } } @@ -113,6 +140,7 @@ impl Shadow for Query { Query::Begin(begin) => Ok(begin.shadow(env)), Query::Commit(commit) => Ok(commit.shadow(env)), Query::Rollback(rollback) => Ok(rollback.shadow(env)), + Query::Placeholder => Ok(vec![]), } } } @@ -159,6 +187,9 @@ impl From for QueryCapabilities { | QueryDiscriminants::Rollback => { unreachable!("QueryCapabilities do not apply to transaction queries") } + QueryDiscriminants::Placeholder => { + unreachable!("QueryCapabilities do not apply to query Placeholder") + } } } } @@ -239,6 +270,7 @@ impl Shadow for Drop { type Result = anyhow::Result>>; fn shadow(&self, tables: &mut ShadowTablesMut) -> Self::Result { + tracing::info!("dropping {:?}", self); if !tables.iter().any(|t| t.name == self.table) { // If the table does not exist, we return an error return Err(anyhow::anyhow!( diff --git a/simulator/profiles/mod.rs b/simulator/profiles/mod.rs index 8c8d1f670..e4ea1dc06 100644 --- a/simulator/profiles/mod.rs +++ b/simulator/profiles/mod.rs @@ -93,11 +93,6 @@ impl Profile { }, ..Default::default() }, - query: QueryProfile { - create_table_weight: 0, - create_index_weight: 4, - ..Default::default() - }, ..Default::default() }; diff --git a/simulator/runner/env.rs b/simulator/runner/env.rs index 52b57052a..300b08c84 100644 --- a/simulator/runner/env.rs +++ b/simulator/runner/env.rs @@ -83,6 +83,18 @@ impl<'a, 'b> ShadowTablesMut<'a> where 'a: 'b, { + /// Creation of [ShadowTablesMut] outside of [SimulatorEnv] should be done sparingly and carefully. + /// Should only need to call this function if we need to do shadowing in a temporary model table + pub fn new( + commited_tables: &'a mut Vec
, + transaction_tables: &'a mut Option, + ) -> Self { + ShadowTablesMut { + commited_tables, + transaction_tables, + } + } + fn tables(&'a self) -> &'a Vec
{ self.transaction_tables .as_ref() diff --git a/simulator/runner/execution.rs b/simulator/runner/execution.rs index e877a972f..e3cfef375 100644 --- a/simulator/runner/execution.rs +++ b/simulator/runner/execution.rs @@ -368,6 +368,9 @@ fn execute_query_rusqlite( } Ok(result) } + Query::Placeholder => { + unreachable!("simulation cannot have a placeholder Query for execution") + } _ => { connection.execute(query.to_string().as_str(), ())?; Ok(vec![]) diff --git a/simulator/shrink/plan.rs b/simulator/shrink/plan.rs index 93f2f1702..6da5d93e8 100644 --- a/simulator/shrink/plan.rs +++ b/simulator/shrink/plan.rs @@ -101,12 +101,6 @@ impl InteractionPlan { // Remove all properties that do not use the failing tables self.retain_mut(|interactions| { let retain = if idx == failing_interaction_index { - if let InteractionsType::Property( - Property::FsyncNoWait { tables, .. } | Property::FaultyQuery { tables, .. }, - ) = &mut interactions.interactions - { - tables.retain(|table| depending_tables.contains(table)); - } true } else { let mut has_table = interactions @@ -128,14 +122,10 @@ impl InteractionPlan { | Property::Queries { queries } => { extensional_queries.append(queries); } - Property::FsyncNoWait { tables, query } - | Property::FaultyQuery { tables, query } => { - if !query.uses().iter().any(|t| depending_tables.contains(t)) { - tables.clear(); - } else { - tables.retain(|table| depending_tables.contains(table)); - } + Property::AllTableHaveExpectedContent { tables } => { + tables.retain(|table| depending_tables.contains(table)); } + Property::FsyncNoWait { .. } | Property::FaultyQuery { .. } => {} Property::SelectLimit { .. } | Property::SelectSelectOptimizer { .. } | Property::WhereTrueFalseNull { .. } @@ -350,7 +340,8 @@ impl InteractionPlan { | Property::FaultyQuery { .. } | Property::FsyncNoWait { .. } | Property::ReadYourUpdatesBack { .. } - | Property::TableHasExpectedContent { .. } => {} + | Property::TableHasExpectedContent { .. } + | Property::AllTableHaveExpectedContent { .. } => {} } } } diff --git a/sql_generation/model/query/insert.rs b/sql_generation/model/query/insert.rs index d69921388..4e5994f14 100644 --- a/sql_generation/model/query/insert.rs +++ b/sql_generation/model/query/insert.rs @@ -24,6 +24,13 @@ impl Insert { Insert::Values { table, .. } | Insert::Select { table, .. } => table, } } + + pub fn rows(&self) -> &[Vec] { + match self { + Insert::Values { values, .. } => values, + Insert::Select { .. } => unreachable!(), + } + } } impl Display for Insert {