From 6bad5d04ce323b79290529b45c77ba99347288aa Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Mon, 6 Oct 2025 14:28:03 -0300 Subject: [PATCH] generate extensional queries when iterating over the next interaction, not when generating the property. This is necessary as the extensional queries can modify schema and thus could cause the next queries to fail because the DB enviroment context was not updated on generation time. Rule of thumb: queries should never be generated in bulk, always one a a time so the enviroment can be shadowed accordingly --- simulator/generation/plan.rs | 157 +++++++++++-- simulator/generation/property.rs | 328 +++++++++++++++++---------- sql_generation/model/query/insert.rs | 7 + 3 files changed, 343 insertions(+), 149 deletions(-) diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index 12935d3d3..df68a17b8 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -293,16 +293,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 } } @@ -314,6 +305,7 @@ impl InteractionPlan { let iter = interactions.into_iter(); PlanGenerator { plan: self, + peek: None, iter, rng, } @@ -383,28 +375,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))) + }) + } + } } } diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index b82eab8bf..e3a2f0f8a 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -240,6 +240,9 @@ 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 { @@ -276,6 +279,186 @@ impl Property { } } + 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 { .. } => 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. @@ -1244,7 +1427,7 @@ pub(crate) fn remaining( fn property_insert_values_select( rng: &mut R, - query_distr: &QueryDistribution, + _query_distr: &QueryDistribution, ctx: &impl GenerationContext, mvcc: bool, ) -> Property { @@ -1278,50 +1461,19 @@ fn property_insert_values_select( let amount = rng.random_range(0..3); - // - [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 mut queries = generate_queries(rng, ctx, amount, &[&insert_query], |rng, ctx| { - let query = Query::arbitrary_from(rng, &ctx, query_distr); - 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), - } - }); + 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.insert( - 0, - Query::Begin(if interactive.start_with_immediate { - Begin::Immediate - } else { - Begin::Deferred - }), - ); queries.push(if interactive.end_with_commit { Query::Commit(Commit) } else { @@ -1404,44 +1556,26 @@ 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 = Query::Create(Create::arbitrary(rng, ctx)); - let table = &create_query.as_create().table; + let create_query = Create::arbitrary(rng, ctx); let amount = rng.random_range(0..3); - // 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) - let queries = generate_queries(rng, ctx, amount, &[&create_query], |rng, ctx| { - 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), - } - }); + let queries = vec![Query::Placeholder; amount]; Property::DoubleCreateFailure { - create: create_query.unwrap_create(), + create: create_query, queries, } } fn property_delete_select( rng: &mut R, - query_distr: &QueryDistribution, + _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { @@ -1453,46 +1587,7 @@ fn property_delete_select( let amount = rng.random_range(0..3); - let delete = Query::Delete(Delete { - predicate: predicate.clone(), - table: table.name.clone(), - }); - - // - [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) - let queries = generate_queries(rng, ctx, amount, &[&delete], |rng, tmp_ctx| { - let query = Query::arbitrary_from(rng, &tmp_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), - } - }); + let queries = vec![Query::Placeholder; amount]; Property::DeleteSelect { table: table.name.clone(), @@ -1503,7 +1598,7 @@ fn property_delete_select( fn property_drop_select( rng: &mut R, - query_distr: &QueryDistribution, + _query_distr: &QueryDistribution, ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { @@ -1511,26 +1606,9 @@ fn property_drop_select( // Get a random table let table = pick(ctx.tables(), rng); - let drop = Query::Drop(Drop { - table: table.name.clone(), - }); - let amount = rng.random_range(0..3); - // Create random queries respecting the constraints - let queries = generate_queries(rng, ctx, amount, &[&drop], |rng, tmp_ctx| { - // - [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) - let query = Query::arbitrary_from(rng, &tmp_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) - } - }); + let queries = vec![Query::Placeholder; amount]; let select = Select::simple( table.name.clone(), 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 {