diff --git a/Cargo.lock b/Cargo.lock index ed065f587..2ddd72015 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2346,6 +2346,7 @@ name = "limbo_sim" version = "0.2.0" dependencies = [ "anyhow", + "bitflags 2.9.4", "bitmaps", "chrono", "clap", diff --git a/simulator/Cargo.toml b/simulator/Cargo.toml index 09401f2cc..7fd5dbeff 100644 --- a/simulator/Cargo.toml +++ b/simulator/Cargo.toml @@ -46,3 +46,4 @@ either = "1.15.0" similar = { workspace = true } similar-asserts = { workspace = true } bitmaps = { workspace = true } +bitflags.workspace = true diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index 3c07d73e3..fb03ee42a 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -8,6 +8,7 @@ use std::{ }; use indexmap::IndexSet; +use rand::distr::weighted::WeightedIndex; use serde::{Deserialize, Serialize}; use sql_generation::{ @@ -26,7 +27,7 @@ use turso_core::{Connection, Result, StepResult}; use crate::{ SimulatorEnv, - generation::Shadow, + generation::{Shadow, property::possiple_properties, query::possible_queries}, model::Query, runner::env::{ShadowTablesMut, SimConnection, SimulationType}, }; @@ -1077,26 +1078,27 @@ fn random_create(rng: &mut R, env: &SimulatorEnv, conn_index: usiz Interactions::new(conn_index, InteractionsType::Query(Query::Create(create))) } -fn random_read(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions { - Interactions::new( - conn_index, - InteractionsType::Query(Query::Select(Select::arbitrary( - rng, - &env.connection_context(conn_index), - ))), - ) +fn random_select(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions { + if rng.random_bool(0.7) { + Interactions::new( + conn_index, + InteractionsType::Query(Query::Select(Select::arbitrary( + rng, + &env.connection_context(conn_index), + ))), + ) + } else { + // Random expression + Interactions::new( + conn_index, + InteractionsType::Query(Query::Select( + SelectFree::arbitrary(rng, &env.connection_context(conn_index)).0, + )), + ) + } } -fn random_expr(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions { - Interactions::new( - conn_index, - InteractionsType::Query(Query::Select( - SelectFree::arbitrary(rng, &env.connection_context(conn_index)).0, - )), - ) -} - -fn random_write(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions { +fn random_insert(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions { Interactions::new( conn_index, InteractionsType::Query(Query::Insert(Insert::arbitrary( @@ -1164,14 +1166,14 @@ fn random_create_index( )) } -fn random_fault(rng: &mut R, env: &SimulatorEnv) -> Interactions { +fn random_fault(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions { let faults = if env.opts.disable_reopen_database { vec![Fault::Disconnect] } else { vec![Fault::Disconnect, Fault::ReopenDatabase] }; let fault = faults[rng.random_range(0..faults.len())]; - Interactions::new(env.choose_conn(rng), InteractionsType::Fault(fault)) + Interactions::new(conn_index, InteractionsType::Fault(fault)) } impl ArbitraryFrom<(&SimulatorEnv, InteractionStats, usize)> for Interactions { @@ -1186,10 +1188,24 @@ impl ArbitraryFrom<(&SimulatorEnv, InteractionStats, usize)> for Interactions { &stats, env.profile.experimental_mvcc, ); + + // TODO: find a way to be more efficient and pass the weights and properties down to the ArbitraryFrom functions + let queries = possible_queries(conn_ctx.tables()); + let query_weights = + WeightedIndex::new(queries.iter().map(|query| query.weight(&remaining_))).unwrap(); + + let properties = possiple_properties(conn_ctx.tables()); + let property_weights = WeightedIndex::new( + properties + .iter() + .map(|property| property.weight(env, &remaining_, conn_ctx.opts())), + ) + .unwrap(); + frequency( vec![ ( - u32::min(remaining_.select, remaining_.insert) + remaining_.create, + property_weights.total_weight(), Box::new(|rng: &mut R| { Interactions::new( conn_index, @@ -1202,52 +1218,25 @@ impl ArbitraryFrom<(&SimulatorEnv, InteractionStats, usize)> for Interactions { }), ), ( - remaining_.select, - Box::new(|rng: &mut R| random_read(rng, env, conn_index)), - ), - ( - remaining_.select / 3, - Box::new(|rng: &mut R| random_expr(rng, env, conn_index)), - ), - ( - remaining_.insert, - Box::new(|rng: &mut R| random_write(rng, env, conn_index)), - ), - ( - remaining_.create, - Box::new(|rng: &mut R| random_create(rng, env, conn_index)), - ), - ( - remaining_.create_index, + query_weights.total_weight(), Box::new(|rng: &mut R| { - if let Some(interaction) = random_create_index(rng, env, conn_index) { - interaction - } else { - // if no tables exist, we can't create an index, so fallback to creating a table - random_create(rng, env, conn_index) - } + Interactions::new( + conn_index, + InteractionsType::Query(Query::arbitrary_from( + rng, + conn_ctx, + &remaining_, + )), + ) }), ), - ( - remaining_.delete, - Box::new(|rng: &mut R| random_delete(rng, env, conn_index)), - ), - ( - remaining_.update, - Box::new(|rng: &mut R| random_update(rng, env, conn_index)), - ), - ( - // remaining_.drop, - 0, - Box::new(|rng: &mut R| random_drop(rng, env, conn_index)), - ), ( remaining_ .select .min(remaining_.insert) .min(remaining_.create) .max(1), - Box::new(|rng: &mut R| random_fault(rng, env)), + Box::new(|rng: &mut R| random_fault(rng, env, conn_index)), ), ], rng, diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index a113dc939..8fe3f486f 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -1,6 +1,7 @@ +use rand::distr::{Distribution, weighted::WeightedIndex}; use serde::{Deserialize, Serialize}; use sql_generation::{ - generation::{Arbitrary, ArbitraryFrom, GenerationContext, frequency, pick, pick_index}, + generation::{Arbitrary, ArbitraryFrom, GenerationContext, Opts, pick, pick_index}, model::{ query::{ Create, Delete, Drop, Insert, Select, @@ -9,16 +10,17 @@ use sql_generation::{ transaction::{Begin, Commit, Rollback}, update::Update, }, - table::SimValue, + table::{SimValue, Table}, }, }; +use strum::IntoEnumIterator; use turso_core::{LimboError, types}; use turso_parser::ast::{self, Distinctness}; use crate::{ common::print_diff, - generation::{Shadow as _, plan::InteractionType}, - model::Query, + generation::{Shadow as _, plan::InteractionType, query::possible_queries}, + model::{Query, QueryCapabilities, QueryDiscriminants}, profiles::query::QueryProfile, runner::env::SimulatorEnv, }; @@ -27,7 +29,8 @@ use super::plan::{Assertion, Interaction, InteractionStats, ResultSet}; /// Properties are representations of executable specifications /// about the database behavior. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, strum::EnumDiscriminants)] +#[strum_discriminants(derive(strum::EnumIter))] pub enum Property { /// Insert-Select is a property in which the inserted row /// must be in the resulting rows of a select query that has a @@ -1308,7 +1311,9 @@ fn property_insert_values_select( fn property_read_your_updates_back( rng: &mut R, + _remaining: &Remaining, ctx: &impl GenerationContext, + _mvcc: bool, ) -> Property { // e.g. UPDATE t SET a=1, b=2 WHERE c=1; let update = Update::arbitrary(rng, ctx); @@ -1330,7 +1335,9 @@ fn property_read_your_updates_back( fn property_table_has_expected_content( rng: &mut R, + _remaining: &Remaining, ctx: &impl GenerationContext, + _mvcc: bool, ) -> Property { // Get a random table let table = pick(ctx.tables(), rng); @@ -1339,7 +1346,12 @@ fn property_table_has_expected_content( } } -fn property_select_limit(rng: &mut R, ctx: &impl GenerationContext) -> Property { +fn property_select_limit( + rng: &mut R, + _remaining: &Remaining, + ctx: &impl GenerationContext, + _mvcc: bool, +) -> Property { // Get a random table let table = pick(ctx.tables(), rng); // Select the table @@ -1357,6 +1369,7 @@ fn property_double_create_failure( rng: &mut R, remaining: &Remaining, ctx: &impl GenerationContext, + _mvcc: bool, ) -> Property { // Create the table let create_query = Create::arbitrary(rng, ctx); @@ -1389,6 +1402,7 @@ fn property_delete_select( rng: &mut R, remaining: &Remaining, ctx: &impl GenerationContext, + _mvcc: bool, ) -> Property { // Get a random table let table = pick(ctx.tables(), rng); @@ -1447,6 +1461,7 @@ fn property_drop_select( rng: &mut R, remaining: &Remaining, ctx: &impl GenerationContext, + _mvcc: bool, ) -> Property { // Get a random table let table = pick(ctx.tables(), rng); @@ -1480,7 +1495,9 @@ fn property_drop_select( fn property_select_select_optimizer( rng: &mut R, + _remaining: &Remaining, ctx: &impl GenerationContext, + _mvcc: bool, ) -> Property { // Get a random table let table = pick(ctx.tables(), rng); @@ -1501,7 +1518,9 @@ fn property_select_select_optimizer( fn property_where_true_false_null( rng: &mut R, + _remaining: &Remaining, ctx: &impl GenerationContext, + _mvcc: bool, ) -> Property { // Get a random table let table = pick(ctx.tables(), rng); @@ -1520,7 +1539,9 @@ fn property_where_true_false_null( fn property_union_all_preserves_cardinality( rng: &mut R, + _remaining: &Remaining, ctx: &impl GenerationContext, + _mvcc: bool, ) -> Property { // Get a random table let table = pick(ctx.tables(), rng); @@ -1547,6 +1568,7 @@ fn property_fsync_no_wait( rng: &mut R, remaining: &Remaining, ctx: &impl GenerationContext, + _mvcc: bool, ) -> Property { Property::FsyncNoWait { query: Query::arbitrary_from(rng, ctx, remaining), @@ -1558,6 +1580,7 @@ fn property_faulty_query( rng: &mut R, remaining: &Remaining, ctx: &impl GenerationContext, + _mvcc: bool, ) -> Property { Property::FaultyQuery { query: Query::arbitrary_from(rng, ctx, remaining), @@ -1565,6 +1588,161 @@ fn property_faulty_query( } } +type PropertyGenFunc = fn(&mut R, &Remaining, &G, bool) -> Property; + +impl PropertyDiscriminants { + pub fn gen_function(&self) -> PropertyGenFunc + where + R: rand::Rng, + G: GenerationContext, + { + match self { + PropertyDiscriminants::InsertValuesSelect => property_insert_values_select, + PropertyDiscriminants::ReadYourUpdatesBack => property_read_your_updates_back, + PropertyDiscriminants::TableHasExpectedContent => property_table_has_expected_content, + PropertyDiscriminants::DoubleCreateFailure => property_double_create_failure, + PropertyDiscriminants::SelectLimit => property_select_limit, + PropertyDiscriminants::DeleteSelect => property_delete_select, + PropertyDiscriminants::DropSelect => property_drop_select, + PropertyDiscriminants::SelectSelectOptimizer => property_select_select_optimizer, + PropertyDiscriminants::WhereTrueFalseNull => property_where_true_false_null, + PropertyDiscriminants::UNIONAllPreservesCardinality => { + property_union_all_preserves_cardinality + } + PropertyDiscriminants::FsyncNoWait => property_fsync_no_wait, + PropertyDiscriminants::FaultyQuery => property_faulty_query, + PropertyDiscriminants::Queries => { + unreachable!("should not try to generate queries property") + } + } + } + + pub fn weight(&self, env: &SimulatorEnv, remaining: &Remaining, opts: &Opts) -> u32 { + match self { + PropertyDiscriminants::InsertValuesSelect => { + if !env.opts.disable_insert_values_select { + u32::min(remaining.select, remaining.insert).max(1) + } else { + 0 + } + } + PropertyDiscriminants::ReadYourUpdatesBack => { + u32::min(remaining.select, remaining.insert).max(1) + } + PropertyDiscriminants::TableHasExpectedContent => remaining.select.max(1), + PropertyDiscriminants::DoubleCreateFailure => { + if !env.opts.disable_double_create_failure { + remaining.create / 2 + } else { + 0 + } + } + PropertyDiscriminants::SelectLimit => { + if !env.opts.disable_select_limit { + remaining.select + } else { + 0 + } + } + PropertyDiscriminants::DeleteSelect => { + if !env.opts.disable_delete_select { + u32::min(remaining.select, remaining.insert).min(remaining.delete) + } else { + 0 + } + } + PropertyDiscriminants::DropSelect => { + if !env.opts.disable_drop_select { + // remaining.drop + 0 + } else { + 0 + } + } + PropertyDiscriminants::SelectSelectOptimizer => { + if !env.opts.disable_select_optimizer { + remaining.select / 2 + } else { + 0 + } + } + PropertyDiscriminants::WhereTrueFalseNull => { + if opts.indexes && !env.opts.disable_where_true_false_null { + remaining.select / 2 + } else { + 0 + } + } + PropertyDiscriminants::UNIONAllPreservesCardinality => { + if opts.indexes && !env.opts.disable_union_all_preserves_cardinality { + remaining.select / 3 + } else { + 0 + } + } + PropertyDiscriminants::FsyncNoWait => { + if env.profile.io.enable && !env.opts.disable_fsync_no_wait { + 50 // Freestyle number + } else { + 0 + } + } + PropertyDiscriminants::FaultyQuery => { + if env.profile.io.enable + && env.profile.io.fault.enable + && !env.opts.disable_faulty_query + { + 20 + } else { + 0 + } + } + PropertyDiscriminants::Queries => { + unreachable!("queries property should not be generated") + } + } + } + + fn can_generate(queries: &[QueryDiscriminants]) -> Vec { + let queries_capabilities = QueryCapabilities::from_list_queries(queries); + + PropertyDiscriminants::iter() + .filter(|property| queries_capabilities.contains(property.requirements())) + .collect() + } + + pub const fn requirements(&self) -> QueryCapabilities { + match self { + PropertyDiscriminants::InsertValuesSelect => { + QueryCapabilities::SELECT.union(QueryCapabilities::INSERT) + } + PropertyDiscriminants::ReadYourUpdatesBack => { + QueryCapabilities::SELECT.union(QueryCapabilities::UPDATE) + } + PropertyDiscriminants::TableHasExpectedContent => QueryCapabilities::SELECT, + PropertyDiscriminants::DoubleCreateFailure => QueryCapabilities::CREATE, + PropertyDiscriminants::SelectLimit => QueryCapabilities::SELECT, + PropertyDiscriminants::DeleteSelect => { + QueryCapabilities::SELECT.union(QueryCapabilities::DELETE) + } + PropertyDiscriminants::DropSelect => { + QueryCapabilities::SELECT.union(QueryCapabilities::DROP) + } + PropertyDiscriminants::SelectSelectOptimizer => QueryCapabilities::SELECT, + PropertyDiscriminants::WhereTrueFalseNull => QueryCapabilities::SELECT, + PropertyDiscriminants::UNIONAllPreservesCardinality => QueryCapabilities::SELECT, + PropertyDiscriminants::FsyncNoWait => QueryCapabilities::all(), + PropertyDiscriminants::FaultyQuery => QueryCapabilities::all(), + PropertyDiscriminants::Queries => panic!("queries property should not be generated"), + } + } +} + +pub fn possiple_properties(tables: &[Table]) -> Vec { + let queries = possible_queries(tables); + PropertyDiscriminants::can_generate(queries) +} + impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property { fn arbitrary_from( rng: &mut R, @@ -1579,110 +1757,19 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property { env.profile.experimental_mvcc, ); - #[allow(clippy::type_complexity)] - let choices: Vec<(_, Box Property>)> = vec![ - ( - if !env.opts.disable_insert_values_select { - u32::min(remaining_.select, remaining_.insert).max(1) - } else { - 0 - }, - Box::new(|rng: &mut R| { - property_insert_values_select( - rng, - &remaining_, - conn_ctx, - env.profile.experimental_mvcc, - ) - }), - ), - ( - remaining_.select.max(1), - Box::new(|rng: &mut R| property_table_has_expected_content(rng, conn_ctx)), - ), - ( - u32::min(remaining_.select, remaining_.insert).max(1), - Box::new(|rng: &mut R| property_read_your_updates_back(rng, conn_ctx)), - ), - ( - if !env.opts.disable_double_create_failure { - remaining_.create / 2 - } else { - 0 - }, - Box::new(|rng: &mut R| property_double_create_failure(rng, &remaining_, conn_ctx)), - ), - ( - if !env.opts.disable_select_limit { - remaining_.select - } else { - 0 - }, - Box::new(|rng: &mut R| property_select_limit(rng, conn_ctx)), - ), - ( - if !env.opts.disable_delete_select { - u32::min(remaining_.select, remaining_.insert).min(remaining_.delete) - } else { - 0 - }, - Box::new(|rng: &mut R| property_delete_select(rng, &remaining_, conn_ctx)), - ), - ( - if !env.opts.disable_drop_select { - // remaining_.drop - 0 - } else { - 0 - }, - Box::new(|rng: &mut R| property_drop_select(rng, &remaining_, conn_ctx)), - ), - ( - if !env.opts.disable_select_optimizer { - remaining_.select / 2 - } else { - 0 - }, - Box::new(|rng: &mut R| property_select_select_optimizer(rng, conn_ctx)), - ), - ( - if opts.indexes && !env.opts.disable_where_true_false_null { - remaining_.select / 2 - } else { - 0 - }, - Box::new(|rng: &mut R| property_where_true_false_null(rng, conn_ctx)), - ), - ( - if opts.indexes && !env.opts.disable_union_all_preserves_cardinality { - remaining_.select / 3 - } else { - 0 - }, - Box::new(|rng: &mut R| property_union_all_preserves_cardinality(rng, conn_ctx)), - ), - ( - if env.profile.io.enable && !env.opts.disable_fsync_no_wait { - 50 // Freestyle number - } else { - 0 - }, - Box::new(|rng: &mut R| property_fsync_no_wait(rng, &remaining_, conn_ctx)), - ), - ( - if env.profile.io.enable - && env.profile.io.fault.enable - && !env.opts.disable_faulty_query - { - 20 - } else { - 0 - }, - Box::new(|rng: &mut R| property_faulty_query(rng, &remaining_, conn_ctx)), - ), - ]; + let properties = possiple_properties(conn_ctx.tables()); + let weights = WeightedIndex::new( + properties + .iter() + .map(|property| property.weight(env, &remaining_, opts)), + ) + .unwrap(); - frequency(choices, rng) + let idx = weights.sample(rng); + let property_fn = properties[idx].gen_function(); + let property = (property_fn)(rng, &remaining_, conn_ctx, env.profile.experimental_mvcc); + + property } } diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index f7d5ac478..5bb3a62ef 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -76,18 +76,12 @@ fn random_create_index(rng: &mut R, conn_ctx: &impl GenerationCont /// Possible queries that can be generated given the table state /// /// Does not take into account transactional statements -pub fn possible_queries(tables: &[Table]) -> Vec { - let mut queries = vec![QueryDiscriminants::Select, QueryDiscriminants::Create]; - if !tables.is_empty() { - queries.extend([ - QueryDiscriminants::Insert, - QueryDiscriminants::Update, - QueryDiscriminants::Delete, - QueryDiscriminants::Drop, - QueryDiscriminants::CreateIndex, - ]); +pub const fn possible_queries(tables: &[Table]) -> &'static [QueryDiscriminants] { + if tables.is_empty() { + &[QueryDiscriminants::Select, QueryDiscriminants::Create] + } else { + QueryDiscriminants::ALL_NO_TRANSACTION } - queries } type QueryGenFunc = fn(&mut R, &G) -> Query; diff --git a/simulator/model/mod.rs b/simulator/model/mod.rs index 807e0f2a1..510922f6b 100644 --- a/simulator/model/mod.rs +++ b/simulator/model/mod.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use anyhow::Context; +use bitflags::bitflags; use indexmap::IndexSet; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -19,6 +20,7 @@ use crate::{generation::Shadow, runner::env::ShadowTablesMut}; // This type represents the potential queries on the database. #[derive(Debug, Clone, Serialize, Deserialize, strum::EnumDiscriminants)] +#[strum_discriminants(derive(strum::VariantArray, strum::EnumIter))] pub enum Query { Create(Create), Select(Select), @@ -115,6 +117,74 @@ impl Shadow for Query { } } +bitflags! { + pub struct QueryCapabilities: u32 { + const CREATE = 1 << 0; + const SELECT = 1 << 1; + const INSERT = 1 << 2; + const DELETE = 1 << 3; + const UPDATE = 1 << 4; + const DROP = 1 << 5; + const CREATE_INDEX = 1 << 6; + } +} + +impl QueryCapabilities { + // TODO: can be const fn in the future + pub fn from_list_queries(queries: &[QueryDiscriminants]) -> Self { + queries + .iter() + .fold(Self::empty(), |accum, q| accum.union(q.into())) + } +} + +impl From<&QueryDiscriminants> for QueryCapabilities { + fn from(value: &QueryDiscriminants) -> Self { + (*value).into() + } +} + +impl From for QueryCapabilities { + fn from(value: QueryDiscriminants) -> Self { + match value { + QueryDiscriminants::Create => Self::CREATE, + QueryDiscriminants::Select => Self::SELECT, + QueryDiscriminants::Insert => Self::INSERT, + QueryDiscriminants::Delete => Self::DELETE, + QueryDiscriminants::Update => Self::UPDATE, + QueryDiscriminants::Drop => Self::DROP, + QueryDiscriminants::CreateIndex => Self::CREATE_INDEX, + QueryDiscriminants::Begin + | QueryDiscriminants::Commit + | QueryDiscriminants::Rollback => { + unreachable!("QueryCapabilities do not apply to transaction queries") + } + } + } +} + +impl QueryDiscriminants { + pub const ALL_NO_TRANSACTION: &[QueryDiscriminants] = &[ + QueryDiscriminants::Select, + QueryDiscriminants::Create, + QueryDiscriminants::Insert, + QueryDiscriminants::Update, + QueryDiscriminants::Delete, + QueryDiscriminants::Drop, + QueryDiscriminants::CreateIndex, + ]; + + #[inline] + pub fn is_transaction(&self) -> bool { + matches!(self, Self::Begin | Self::Commit | Self::Rollback) + } + + #[inline] + pub fn is_ddl(&self) -> bool { + matches!(self, Self::Create | Self::CreateIndex | Self::Drop) + } +} + impl Shadow for Create { type Result = anyhow::Result>>;