From 7f93f64fc51d807f5f7566c48216121fb07330a4 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Sun, 5 Oct 2025 19:32:48 -0300 Subject: [PATCH 01/11] enable Drop statements --- simulator/generation/property.rs | 3 +-- simulator/generation/query.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index b00a1114d..5ad72c5c6 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -1657,8 +1657,7 @@ impl PropertyDiscriminants { } PropertyDiscriminants::DropSelect => { if !env.opts.disable_drop_select { - // remaining.drop - 0 + remaining.drop } else { 0 } diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index 88cd95126..a78df68c6 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -121,7 +121,7 @@ impl QueryDiscriminants { 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 From 3e8867c8f5e46a47d7cc7821471e1c60a2f37c00 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Mon, 6 Oct 2025 00:10:55 -0300 Subject: [PATCH 02/11] `DropSelect` property should only fail when error is not a parse error on the table name --- simulator/generation/property.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index 5ad72c5c6..ff7856474 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -691,21 +691,21 @@ impl Property { format!("select query should result in an error for table '{table}'"), move |stack: &Vec, _| { let last = stack.last().unwrap(); + dbg!(last); match last { 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 { + LimboError::ParseError(e) + if e.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 +726,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 From 4fc7be5042b7df705b6480646ee58dcfb4fafb8b Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Mon, 6 Oct 2025 00:19:24 -0300 Subject: [PATCH 03/11] as we have DROP table now, if we want to generate extensional queries eagerly, without affecting how we document interactions with MVCC, we need to travel `forward` in time and shadow queries eagerly so we can generate queries correctly. This involves cloning the tables unfortunately which is inneficient but correct --- simulator/generation/property.rs | 189 ++++++++++++++++++++++--------- simulator/model/mod.rs | 24 ++++ simulator/runner/env.rs | 12 ++ 3 files changed, 174 insertions(+), 51 deletions(-) diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index ff7856474..2f8dcc43c 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)] @@ -1230,10 +1260,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,21 +1274,15 @@ fn property_insert_values_select( } else { None }; - // Create random queries respecting the constraints - let mut queries = Vec::new(); + + 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) - if let Some(ref interactive) = interactive { - queries.push(Query::Begin(if interactive.start_with_immediate { - Begin::Immediate - } else { - Begin::Deferred - })); - } - for _ in 0..rng.random_range(0..3) { - let query = Query::arbitrary_from(rng, ctx, query_distr); + 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, @@ -1266,14 +1290,14 @@ fn property_insert_values_select( }) => { // The inserted row will not be deleted. if t == &table.name && predicate.test(&row, table) { - continue; + return None; } } 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; + return None; } } Query::Update(Update { @@ -1283,14 +1307,23 @@ fn property_insert_values_select( }) => { // The inserted row will not be updated. if t == &table.name && predicate.test(&row, table) { - continue; + return None; } } _ => (), } - queries.push(query); - } + Some(query) + }); + 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 { @@ -1305,7 +1338,7 @@ fn property_insert_values_select( ); Property::InsertValuesSelect { - insert: insert_query, + insert: insert_query.unwrap_insert(), row_index, queries, select: select_query, @@ -1376,28 +1409,28 @@ fn property_double_create_failure( _mvcc: bool, ) -> Property { // Create the table - let create_query = Create::arbitrary(rng, ctx); - let table = &create_query.table; + let create_query = Query::Create(Create::arbitrary(rng, ctx)); + let table = &create_query.as_create().table; + + let amount = rng.random_range(0..3); - // 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); + let queries = generate_queries(rng, ctx, amount, &[&create_query], |rng, ctx| { + 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; + return None; } } - queries.push(query); - } + Some(query) + }); Property::DoubleCreateFailure { - create: create_query, + create: create_query.unwrap_create(), queries, } } @@ -1413,18 +1446,23 @@ fn property_delete_select( // Generate a random predicate let predicate = Predicate::arbitrary_from(rng, ctx, table); - // Create random queries respecting the constraints - let mut queries = Vec::new(); + 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) - for _ in 0..rng.random_range(0..3) { - let query = Query::arbitrary_from(rng, ctx, query_distr); + 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 }) => { // A row that holds for the predicate will not be inserted. if t == &table.name && values.iter().any(|v| predicate.test(v, table)) { - continue; + return None; } } Query::Insert(Insert::Select { @@ -1433,26 +1471,26 @@ fn property_delete_select( }) => { // A row that holds for the predicate will not be inserted. if t == &table.name { - continue; + return None; } } Query::Update(Update { table: t, .. }) => { // A row that holds for the predicate will not be updated. if t == &table.name { - continue; + return None; } } 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; + return None; } } _ => (), } - queries.push(query); - } + Some(query) + }); Property::DeleteSelect { table: table.name.clone(), @@ -1470,20 +1508,26 @@ 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 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 { + 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 - if t.name == table.name { - continue; - } + None + } else { + Some(query) } - queries.push(query); - } + }); let select = Select::simple( table.name.clone(), @@ -1815,6 +1859,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/model/mod.rs b/simulator/model/mod.rs index 510922f6b..4af61b353 100644 --- a/simulator/model/mod.rs +++ b/simulator/model/mod.rs @@ -35,6 +35,28 @@ pub enum Query { } 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(), @@ -102,6 +124,7 @@ impl Shadow for Query { type Result = anyhow::Result>>; fn shadow(&self, env: &mut ShadowTablesMut) -> Self::Result { + tracing::info!("SHADOW {:?}", self); match self { Query::Create(create) => create.shadow(env), Query::Insert(insert) => insert.shadow(env), @@ -239,6 +262,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/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() From 07cc1c548be6dd3c2095a7fcc9c3efe136668c7b Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Mon, 6 Oct 2025 13:10:02 -0300 Subject: [PATCH 04/11] adjust query generation to avoid DROP for certain extensional queries --- simulator/generation/property.rs | 74 ++++++++++++++++---------------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index 2f8dcc43c..3fef90b51 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -1287,32 +1287,29 @@ fn property_insert_values_select( Query::Delete(Delete { table: t, predicate, - }) => { + }) if t == &table.name && predicate.test(&row, table) => { // The inserted row will not be deleted. - if t == &table.name && predicate.test(&row, table) { - return None; - } + None } - Query::Create(Create { table: t }) => { + 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 - if t.name == table.name { - return None; - } + None } Query::Update(Update { table: t, set_values: _, predicate, - }) => { + }) if t == &table.name && predicate.test(&row, table) => { // The inserted row will not be updated. - if t == &table.name && predicate.test(&row, table) { - return None; - } + None } - _ => (), + Query::Drop(Drop { table: t }) if *t == table.name => { + // Cannot drop the table we are inserting + None + } + _ => Some(query), } - Some(query) }); if let Some(ref interactive) = interactive { @@ -1419,14 +1416,18 @@ fn property_double_create_failure( // - [ ] 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); - 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 { - return None; + 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), } - Some(query) }); Property::DoubleCreateFailure { @@ -1459,37 +1460,34 @@ fn property_delete_select( 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 }) => { + 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. - if t == &table.name && values.iter().any(|v| predicate.test(v, table)) { - return None; - } + None } Query::Insert(Insert::Select { table: t, select: _, - }) => { + }) if t == &table.name => { // A row that holds for the predicate will not be inserted. - if t == &table.name { - return None; - } + None } - Query::Update(Update { table: t, .. }) => { + Query::Update(Update { table: t, .. }) if t == &table.name => { // A row that holds for the predicate will not be updated. - if t == &table.name { - return None; - } + None } - Query::Create(Create { table: t }) => { + 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 - if t.name == table.name { - return None; - } + None } - _ => (), + Query::Drop(Drop { table: t }) if *t == table.name => { + // Cannot Drop the same table + None + } + _ => Some(query), } - Some(query) }); Property::DeleteSelect { From 7eb504baefcfc71b6e00c8445773571151a5a394 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Mon, 6 Oct 2025 14:00:38 -0300 Subject: [PATCH 05/11] certain properties cannot be generated if there are no tables in the current context --- simulator/generation/plan.rs | 3 +- simulator/generation/property.rs | 48 ++++++++++++++++++++++++-------- simulator/generation/query.rs | 2 +- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index 05373312f..3e6e1ab9e 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -1097,8 +1097,7 @@ 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()); + let property_distr = PropertyDistribution::new(env, &remaining_, &query_distr, conn_ctx); frequency( vec![ diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index 3fef90b51..b82eab8bf 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -1248,6 +1248,7 @@ fn property_insert_values_select( 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 @@ -1373,6 +1374,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 { @@ -1386,6 +1388,7 @@ fn property_select_limit( ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { + assert!(!ctx.tables().is_empty()); // Get a random table let table = pick(ctx.tables(), rng); // Select the table @@ -1442,6 +1445,7 @@ fn property_delete_select( 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 @@ -1503,6 +1507,7 @@ fn property_drop_select( ctx: &impl GenerationContext, _mvcc: bool, ) -> Property { + assert!(!ctx.tables().is_empty()); // Get a random table let table = pick(ctx.tables(), rng); @@ -1545,6 +1550,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 @@ -1568,6 +1574,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 @@ -1589,6 +1596,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 @@ -1663,10 +1671,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 @@ -1675,7 +1689,13 @@ 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 + } + } PropertyDiscriminants::DoubleCreateFailure => { if !env.opts.disable_double_create_failure { remaining.create / 2 @@ -1684,42 +1704,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 { + 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 @@ -1803,13 +1829,13 @@ impl<'a> PropertyDistribution<'a> { env: &SimulatorEnv, remaining: &Remaining, query_distr: &'a QueryDistribution, - opts: &Opts, + ctx: &impl GenerationContext, ) -> Self { let properties = PropertyDiscriminants::can_generate(query_distr.items()); let weights = WeightedIndex::new( properties .iter() - .map(|property| property.weight(env, remaining, opts)), + .map(|property| property.weight(env, remaining, ctx)), ) .unwrap(); diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index a78df68c6..068e4f5e7 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 From 6d5443d4f02b46bb7764b1c43060b347d41ee899 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Mon, 6 Oct 2025 14:20:56 -0300 Subject: [PATCH 06/11] add Query::Placeholder --- simulator/generation/plan.rs | 6 ++++++ simulator/generation/query.rs | 6 ++++++ simulator/model/mod.rs | 10 +++++++++- simulator/runner/execution.rs | 3 +++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index 3e6e1ab9e..12935d3d3 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 { @@ -766,6 +767,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() { diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index 068e4f5e7..3408dca3b 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -111,6 +111,9 @@ impl QueryDiscriminants { | QueryDiscriminants::Rollback => { unreachable!("transactional queries should not be generated") } + QueryDiscriminants::Placeholder => { + unreachable!("Query Placeholders should not be generated") + } } } @@ -128,6 +131,9 @@ impl QueryDiscriminants { | QueryDiscriminants::Rollback => { unreachable!("transactional queries should not be generated") } + QueryDiscriminants::Placeholder => { + unreachable!("Query Placeholders should not be generated") + } } } } diff --git a/simulator/model/mod.rs b/simulator/model/mod.rs index 4af61b353..9e3d29db2 100644 --- a/simulator/model/mod.rs +++ b/simulator/model/mod.rs @@ -32,6 +32,8 @@ pub enum Query { Begin(Begin), Commit(Commit), Rollback(Rollback), + /// Placeholder query that still needs to be generated + Placeholder, } impl Query { @@ -70,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 { @@ -83,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![], } } @@ -116,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(()), } } } @@ -124,7 +129,6 @@ impl Shadow for Query { type Result = anyhow::Result>>; fn shadow(&self, env: &mut ShadowTablesMut) -> Self::Result { - tracing::info!("SHADOW {:?}", self); match self { Query::Create(create) => create.shadow(env), Query::Insert(insert) => insert.shadow(env), @@ -136,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![]), } } } @@ -182,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") + } } } } 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![]) From 6bad5d04ce323b79290529b45c77ba99347288aa Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Mon, 6 Oct 2025 14:28:03 -0300 Subject: [PATCH 07/11] 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 { From 21fc8bae2a691141a595baa380cd9edae8a6de78 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Tue, 7 Oct 2025 12:15:57 -0300 Subject: [PATCH 08/11] `Property::FaultyQuery` and `FsyncNoWait` stored a list of tables to check the on the database. Again, the FaultyQuery could be a Drop Table which meant that we could be running a SELECT on an inexistent table. To solve this, just insert a Property that check all the tables in the db after a Faulty Property --- simulator/generation/plan.rs | 30 +++++++++++++ simulator/generation/property.rs | 72 +++++++++++++++++++++++--------- simulator/shrink/plan.rs | 19 +++------ 3 files changed, 87 insertions(+), 34 deletions(-) diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index df68a17b8..836c24780 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -239,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) { @@ -561,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 { diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index e3a2f0f8a..8d6b4cb79 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -116,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 @@ -222,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 { @@ -249,6 +258,7 @@ impl Property { 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", @@ -262,6 +272,11 @@ 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, .. } @@ -275,7 +290,8 @@ impl Property { | Property::WhereTrueFalseNull { .. } | Property::UNIONAllPreservesCardinality { .. } | Property::ReadYourUpdatesBack { .. } - | Property::TableHasExpectedContent { .. } => None, + | Property::TableHasExpectedContent { .. } + | Property::AllTableHaveExpectedContent { .. } => None, } } @@ -455,7 +471,10 @@ impl Property { | Property::WhereTrueFalseNull { .. } | Property::UNIONAllPreservesCardinality { .. } | Property::ReadYourUpdatesBack { .. } - | Property::TableHasExpectedContent { .. } => unreachable!("No extensional queries"), + | Property::TableHasExpectedContent { .. } + | Property::AllTableHaveExpectedContent { .. } => { + unreachable!("No extensional queries") + } } } @@ -464,6 +483,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(); @@ -1033,18 +1055,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 @@ -1071,13 +1088,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( @@ -1534,6 +1551,17 @@ 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, @@ -1704,7 +1732,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(), } } @@ -1716,7 +1743,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(), } } @@ -1732,6 +1758,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, @@ -1774,6 +1803,8 @@ impl PropertyDiscriminants { 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 @@ -1872,6 +1903,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 => { 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 { .. } => {} } } } From c578f7ba96249e233753a4547a36624591587b29 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Tue, 7 Oct 2025 13:31:27 -0300 Subject: [PATCH 09/11] Faultless should produce any type of query, just not faulty --- simulator/profiles/mod.rs | 5 ----- 1 file changed, 5 deletions(-) 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() }; From 3b2583c540c67ebf8276a550df8dd17ff574ba57 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Tue, 7 Oct 2025 13:31:27 -0300 Subject: [PATCH 10/11] adjust Interaction generation to take into account possibilty of `PropertyDistribution` to have 0 Weights --- simulator/generation/plan.rs | 79 ++++++++++++++++---------------- simulator/generation/property.rs | 14 +++--- simulator/generation/query.rs | 5 +- 3 files changed, 51 insertions(+), 47 deletions(-) diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index 836c24780..a468e87ec 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -1242,46 +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); + #[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 8d6b4cb79..9df45db2e 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -274,7 +274,10 @@ impl Property { /// Property Does some sort of fault injection pub fn check_tables(&self) -> bool { - matches!(self, Property::FsyncNoWait { .. } | Property::FaultyQuery { .. }) + matches!( + self, + Property::FsyncNoWait { .. } | Property::FaultyQuery { .. } + ) } pub fn get_extensional_queries(&mut self) -> Option<&mut Vec> { @@ -1940,21 +1943,20 @@ impl<'a> PropertyDistribution<'a> { remaining: &Remaining, query_distr: &'a QueryDistribution, ctx: &impl GenerationContext, - ) -> Self { + ) -> Result { let properties = PropertyDiscriminants::can_generate(query_distr.items()); let weights = WeightedIndex::new( properties .iter() .map(|property| property.weight(env, remaining, ctx)), - ) - .unwrap(); + )?; - Self { + Ok(Self { properties, weights, query_distr, mvcc: env.profile.experimental_mvcc, - } + }) } } diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index 3408dca3b..914b44b35 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -120,7 +120,9 @@ impl QueryDiscriminants { 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, @@ -138,6 +140,7 @@ impl QueryDiscriminants { } } +#[derive(Debug)] pub(super) struct QueryDistribution { queries: &'static [QueryDiscriminants], weights: WeightedIndex, From 300d918040344bcfe23bba204031cd3f585542ed Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Tue, 7 Oct 2025 14:58:56 -0300 Subject: [PATCH 11/11] fix differential check for parse error --- simulator/generation/property.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index 9df45db2e..47b352406 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -929,14 +929,14 @@ impl Property { format!("select query should result in an error for table '{table}'"), move |stack: &Vec, _| { let last = stack.last().unwrap(); - dbg!(last); match last { Ok(success) => Ok(Err(format!( "expected table creation to fail but it succeeded: {success:?}" ))), Err(e) => match e { - LimboError::ParseError(e) - if e.contains(&format!("no such table: {table_name}")) => + e if e + .to_string() + .contains(&format!("no such table: {table_name}")) => { Ok(Ok(())) }