mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-08 10:44:20 +01:00
Merge 'Simulator: Add Drop and pave the way for Schema changes' from Pedro Muniz
Depends on #3585 Some properties can have extensional queries that run in between the queries that the property aims to prove. These queries were generated eagerly together with the generation of the `Property`. This was okayish when we were not generating `Drop` queries, however with `Drop` statements in the game we could generate queries that reference dropped tables. Example: - Drop Table t; - Select * from t; The example above was possible because we update the simulator model only after we run the query, so we could generate queries with stale data. **WHAT CHANGED** - Stop generating queries eagerly in `Property`. - Introduce `Query::Placeholder` to signify that the `Query` should be generated in `PlanGenerator::next`. We then swap `Query::Placeholder` with whatever query we generate - This change is still compatible with MVCC as we still generate `Commit` queries when `PlanGenerator` encounters a `DDL` statement - Add `Property::AllTablesHaveExpectedContent` to check the tables in the DB after a Faulty property executes, instead of pre selecting the tables we want to check. We need to do this because a `FaultyQuery` may Drop a table, resulting in a ParseError later on in the checks. PS: In commit[`3c85040b4a483f4160d7324e664782a112a6a7a3`](https://github .com/tursodatabase/turso/commit/3c85040b4a483f4160d7324e664782a112a6a7a3 ), for correctness, I thought we had to clone the Simulator Tables every time we wanted to generate extensional queries. However, later on I reused the code of that commit and found a solution that could generalize easier to any type of schema change. This will make it easier for me add `ALTER TABLE` next. Closes #3605
This commit is contained in:
@@ -195,6 +195,7 @@ impl InteractionPlan {
|
||||
Query::Begin(_) => stats.begin_count += 1,
|
||||
Query::Commit(_) => stats.commit_count += 1,
|
||||
Query::Rollback(_) => stats.rollback_count += 1,
|
||||
Query::Placeholder => {}
|
||||
}
|
||||
}
|
||||
for interactions in &self.plan {
|
||||
@@ -238,6 +239,28 @@ impl InteractionPlan {
|
||||
env: &mut SimulatorEnv,
|
||||
) -> Option<Vec<Interaction>> {
|
||||
let num_interactions = env.opts.max_interactions as usize;
|
||||
// If last interaction needs to check all db tables, generate the Property to do so
|
||||
if let Some(i) = self.plan.last()
|
||||
&& i.check_tables()
|
||||
{
|
||||
let check_all_tables = Interactions::new(
|
||||
i.connection_index,
|
||||
InteractionsType::Property(Property::AllTableHaveExpectedContent {
|
||||
tables: env
|
||||
.connection_context(i.connection_index)
|
||||
.tables()
|
||||
.iter()
|
||||
.map(|t| t.name.clone())
|
||||
.collect(),
|
||||
}),
|
||||
);
|
||||
|
||||
let out_interactions = check_all_tables.interactions();
|
||||
|
||||
self.push(check_all_tables);
|
||||
return Some(out_interactions);
|
||||
}
|
||||
|
||||
if self.len() < num_interactions {
|
||||
let conn_index = env.choose_conn(rng);
|
||||
let interactions = if self.mvcc && !env.conn_in_transaction(conn_index) {
|
||||
@@ -292,16 +315,7 @@ impl InteractionPlan {
|
||||
self.push(interactions);
|
||||
Some(out_interactions)
|
||||
} else {
|
||||
// after we generated all interactions if some connection is still in a transaction, commit
|
||||
(0..env.connections.len())
|
||||
.find(|idx| env.conn_in_transaction(*idx))
|
||||
.map(|conn_index| {
|
||||
let query = Query::Commit(Commit);
|
||||
let interaction = Interactions::new(conn_index, InteractionsType::Query(query));
|
||||
let out_interactions = interaction.interactions();
|
||||
self.push(interaction);
|
||||
out_interactions
|
||||
})
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +327,7 @@ impl InteractionPlan {
|
||||
let iter = interactions.into_iter();
|
||||
PlanGenerator {
|
||||
plan: self,
|
||||
peek: None,
|
||||
iter,
|
||||
rng,
|
||||
}
|
||||
@@ -382,28 +397,145 @@ impl<T: InteractionPlanIterator> InteractionPlanIterator for &mut T {
|
||||
|
||||
pub struct PlanGenerator<'a, R: rand::Rng> {
|
||||
plan: &'a mut InteractionPlan,
|
||||
peek: Option<Interaction>,
|
||||
iter: <Vec<Interaction> as IntoIterator>::IntoIter,
|
||||
rng: &'a mut R,
|
||||
}
|
||||
|
||||
impl<'a, R: rand::Rng> PlanGenerator<'a, R> {
|
||||
fn next_interaction(&mut self, env: &mut SimulatorEnv) -> Option<Interaction> {
|
||||
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<Interaction> {
|
||||
self.iter.next().or_else(|| {
|
||||
// Iterator ended, try to create a new iterator
|
||||
// This will not be an infinte sequence because generate_next_interaction will eventually
|
||||
// stop generating
|
||||
let mut iter = self
|
||||
.plan
|
||||
.generate_next_interaction(self.rng, env)
|
||||
.map_or(Vec::new().into_iter(), |interactions| {
|
||||
interactions.into_iter()
|
||||
});
|
||||
let next = iter.next();
|
||||
self.iter = iter;
|
||||
let mvcc = self.plan.mvcc;
|
||||
match self.peek(env) {
|
||||
Some(peek_interaction) => {
|
||||
if mvcc && peek_interaction.is_ddl() {
|
||||
// try to commit a transaction as we cannot execute DDL statements in concurrent mode
|
||||
|
||||
next
|
||||
})
|
||||
let commit_connection = (0..env.connections.len())
|
||||
.find(|idx| env.conn_in_transaction(*idx))
|
||||
.map(|conn_index| {
|
||||
let query = Query::Commit(Commit);
|
||||
let interaction = Interactions::new(
|
||||
conn_index,
|
||||
InteractionsType::Query(query.clone()),
|
||||
);
|
||||
|
||||
// Connections are queued for commit on `generate_next_interaction` if Interactions::Query or Interactions::Property produce a DDL statement.
|
||||
// This means that the only way we will reach here, is if the DDL statement was created later in the extensional query of a Property
|
||||
let queries = self
|
||||
.plan
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.get_extensional_queries()
|
||||
.unwrap();
|
||||
queries.insert(0, query.clone());
|
||||
|
||||
self.plan.push(interaction);
|
||||
|
||||
Interaction::new(conn_index, InteractionType::Query(query))
|
||||
});
|
||||
if commit_connection.is_some() {
|
||||
return commit_connection;
|
||||
}
|
||||
}
|
||||
|
||||
self.peek.take()
|
||||
}
|
||||
None => {
|
||||
// after we generated all interactions if some connection is still in a transaction, commit
|
||||
(0..env.connections.len())
|
||||
.find(|idx| env.conn_in_transaction(*idx))
|
||||
.map(|conn_index| {
|
||||
let query = Query::Commit(Commit);
|
||||
let interaction =
|
||||
Interactions::new(conn_index, InteractionsType::Query(query));
|
||||
self.plan.push(interaction);
|
||||
|
||||
Interaction::new(conn_index, InteractionType::Query(Query::Commit(Commit)))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,6 +583,14 @@ impl Interactions {
|
||||
InteractionsType::Query(..) | InteractionsType::Fault(..) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the interaction needs to check the database tables
|
||||
pub fn check_tables(&self) -> bool {
|
||||
match &self.interactions {
|
||||
InteractionsType::Property(property) => property.check_tables(),
|
||||
InteractionsType::Query(..) | InteractionsType::Fault(..) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Interactions {
|
||||
@@ -766,6 +906,11 @@ impl InteractionType {
|
||||
|
||||
pub(crate) fn execute_query(&self, conn: &mut Arc<Connection>) -> ResultSet {
|
||||
if let Self::Query(query) = self {
|
||||
assert!(
|
||||
!matches!(query, Query::Placeholder),
|
||||
"simulation cannot have a placeholder Query for execution"
|
||||
);
|
||||
|
||||
let query_str = query.to_string();
|
||||
let rows = conn.query(&query_str);
|
||||
if rows.is_err() {
|
||||
@@ -1097,47 +1242,45 @@ impl ArbitraryFrom<(&SimulatorEnv, InteractionStats, usize)> for Interactions {
|
||||
let queries = possible_queries(conn_ctx.tables());
|
||||
let query_distr = QueryDistribution::new(queries, &remaining_);
|
||||
|
||||
let property_distr =
|
||||
PropertyDistribution::new(env, &remaining_, &query_distr, conn_ctx.opts());
|
||||
#[allow(clippy::type_complexity)]
|
||||
let mut choices: Vec<(u32, Box<dyn Fn(&mut R) -> 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<sql_generation::model::table::Table>,
|
||||
opts: &'a sql_generation::generation::Opts,
|
||||
}
|
||||
|
||||
impl<'a> PropertyGenContext<'a> {
|
||||
#[inline]
|
||||
fn new(tables: &'a Vec<Table>, opts: &'a Opts) -> Self {
|
||||
Self { tables, opts }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> GenerationContext for PropertyGenContext<'a> {
|
||||
fn tables(&self) -> &Vec<sql_generation::model::table::Table> {
|
||||
self.tables
|
||||
}
|
||||
|
||||
fn opts(&self) -> &sql_generation::generation::Opts {
|
||||
self.opts
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties are representations of executable specifications
|
||||
/// about the database behavior.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, strum::EnumDiscriminants)]
|
||||
@@ -86,6 +116,17 @@ pub enum Property {
|
||||
TableHasExpectedContent {
|
||||
table: String,
|
||||
},
|
||||
/// AllTablesHaveExpectedContent is a property in which the table
|
||||
/// must have the expected content, i.e. all the insertions and
|
||||
/// updates and deletions should have been persisted in the way
|
||||
/// we think they were.
|
||||
/// The execution of the property is as follows
|
||||
/// SELECT * FROM <t>
|
||||
/// ASSERT <expected_content>
|
||||
/// for each table in the simulator model
|
||||
AllTableHaveExpectedContent {
|
||||
tables: Vec<String>,
|
||||
},
|
||||
/// Double Create Failure is a property in which creating
|
||||
/// the same table twice leads to an error.
|
||||
/// The execution of the property is as follows
|
||||
@@ -192,11 +233,9 @@ pub enum Property {
|
||||
///
|
||||
FsyncNoWait {
|
||||
query: Query,
|
||||
tables: Vec<String>,
|
||||
},
|
||||
FaultyQuery {
|
||||
query: Query,
|
||||
tables: Vec<String>,
|
||||
},
|
||||
/// Property used to subsititute a property with its queries only
|
||||
Queries {
|
||||
@@ -210,12 +249,16 @@ pub struct InteractiveQueryInfo {
|
||||
end_with_commit: bool,
|
||||
}
|
||||
|
||||
type PropertyQueryGenFunc<'a, R, G> =
|
||||
fn(&mut R, &G, &QueryDistribution, &Property) -> Option<Query>;
|
||||
|
||||
impl Property {
|
||||
pub(crate) fn name(&self) -> &str {
|
||||
match self {
|
||||
Property::InsertValuesSelect { .. } => "Insert-Values-Select",
|
||||
Property::ReadYourUpdatesBack { .. } => "Read-Your-Updates-Back",
|
||||
Property::TableHasExpectedContent { .. } => "Table-Has-Expected-Content",
|
||||
Property::AllTableHaveExpectedContent { .. } => "All-Tables-Have-Expected-Content",
|
||||
Property::DoubleCreateFailure { .. } => "Double-Create-Failure",
|
||||
Property::SelectLimit { .. } => "Select-Limit",
|
||||
Property::DeleteSelect { .. } => "Delete-Select",
|
||||
@@ -229,6 +272,14 @@ impl Property {
|
||||
}
|
||||
}
|
||||
|
||||
/// Property Does some sort of fault injection
|
||||
pub fn check_tables(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Property::FsyncNoWait { .. } | Property::FaultyQuery { .. }
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_extensional_queries(&mut self) -> Option<&mut Vec<Query>> {
|
||||
match self {
|
||||
Property::InsertValuesSelect { queries, .. }
|
||||
@@ -242,7 +293,191 @@ impl Property {
|
||||
| Property::WhereTrueFalseNull { .. }
|
||||
| Property::UNIONAllPreservesCardinality { .. }
|
||||
| Property::ReadYourUpdatesBack { .. }
|
||||
| Property::TableHasExpectedContent { .. } => None,
|
||||
| Property::TableHasExpectedContent { .. }
|
||||
| Property::AllTableHaveExpectedContent { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_extensional_query_gen_function<R, G>(&self) -> PropertyQueryGenFunc<R, G>
|
||||
where
|
||||
R: rand::Rng + ?Sized,
|
||||
G: GenerationContext,
|
||||
{
|
||||
match self {
|
||||
Property::InsertValuesSelect { .. } => {
|
||||
// - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort)
|
||||
// - [x] The inserted row will not be deleted.
|
||||
// - [x] The inserted row will not be updated.
|
||||
// - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented)
|
||||
|rng: &mut R, ctx: &G, query_distr: &QueryDistribution, property: &Property| {
|
||||
let Property::InsertValuesSelect {
|
||||
insert, row_index, ..
|
||||
} = property
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
let query = Query::arbitrary_from(rng, ctx, query_distr);
|
||||
let table_name = insert.table();
|
||||
let table = ctx
|
||||
.tables()
|
||||
.iter()
|
||||
.find(|table| table.name == table_name)
|
||||
.unwrap();
|
||||
|
||||
let rows = insert.rows();
|
||||
let row = &rows[*row_index];
|
||||
|
||||
match &query {
|
||||
Query::Delete(Delete {
|
||||
table: t,
|
||||
predicate,
|
||||
}) if t == &table.name && predicate.test(row, table) => {
|
||||
// The inserted row will not be deleted.
|
||||
None
|
||||
}
|
||||
Query::Create(Create { table: t }) if t.name == table.name => {
|
||||
// There will be no errors in the middle interactions.
|
||||
// - Creating the same table is an error
|
||||
None
|
||||
}
|
||||
Query::Update(Update {
|
||||
table: t,
|
||||
set_values: _,
|
||||
predicate,
|
||||
}) if t == &table.name && predicate.test(row, table) => {
|
||||
// The inserted row will not be updated.
|
||||
None
|
||||
}
|
||||
Query::Drop(Drop { table: t }) if *t == table.name => {
|
||||
// Cannot drop the table we are inserting
|
||||
None
|
||||
}
|
||||
_ => Some(query),
|
||||
}
|
||||
}
|
||||
}
|
||||
Property::DoubleCreateFailure { .. } => {
|
||||
// The interactions in the middle has the following constraints;
|
||||
// - [x] There will be no errors in the middle interactions.(best effort)
|
||||
// - [ ] Table `t` will not be renamed or dropped.(todo: add this constraint once ALTER or DROP is implemented)
|
||||
|rng: &mut R, ctx: &G, query_distr: &QueryDistribution, property: &Property| {
|
||||
let Property::DoubleCreateFailure { create, .. } = property else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let table_name = create.table.name.clone();
|
||||
let table = ctx
|
||||
.tables()
|
||||
.iter()
|
||||
.find(|table| table.name == table_name)
|
||||
.unwrap();
|
||||
|
||||
let query = Query::arbitrary_from(rng, ctx, query_distr);
|
||||
match &query {
|
||||
Query::Create(Create { table: t }) if t.name == table.name => {
|
||||
// There will be no errors in the middle interactions.
|
||||
// - Creating the same table is an error
|
||||
None
|
||||
}
|
||||
Query::Drop(Drop { table: t }) if *t == table.name => {
|
||||
// Cannot Drop the created table
|
||||
None
|
||||
}
|
||||
_ => Some(query),
|
||||
}
|
||||
}
|
||||
}
|
||||
Property::DeleteSelect { .. } => {
|
||||
// - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort)
|
||||
// - [x] A row that holds for the predicate will not be inserted.
|
||||
// - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented)
|
||||
|
||||
|rng, ctx, query_distr, property| {
|
||||
let Property::DeleteSelect {
|
||||
table: table_name,
|
||||
predicate,
|
||||
..
|
||||
} = property
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let table_name = table_name.clone();
|
||||
let table = ctx
|
||||
.tables()
|
||||
.iter()
|
||||
.find(|table| table.name == table_name)
|
||||
.unwrap();
|
||||
let query = Query::arbitrary_from(rng, ctx, query_distr);
|
||||
match &query {
|
||||
Query::Insert(Insert::Values { table: t, values })
|
||||
if *t == table_name
|
||||
&& values.iter().any(|v| predicate.test(v, table)) =>
|
||||
{
|
||||
// A row that holds for the predicate will not be inserted.
|
||||
None
|
||||
}
|
||||
Query::Insert(Insert::Select {
|
||||
table: t,
|
||||
select: _,
|
||||
}) if t == &table.name => {
|
||||
// A row that holds for the predicate will not be inserted.
|
||||
None
|
||||
}
|
||||
Query::Update(Update { table: t, .. }) if t == &table.name => {
|
||||
// A row that holds for the predicate will not be updated.
|
||||
None
|
||||
}
|
||||
Query::Create(Create { table: t }) if t.name == table.name => {
|
||||
// There will be no errors in the middle interactions.
|
||||
// - Creating the same table is an error
|
||||
None
|
||||
}
|
||||
Query::Drop(Drop { table: t }) if *t == table.name => {
|
||||
// Cannot Drop the same table
|
||||
None
|
||||
}
|
||||
_ => Some(query),
|
||||
}
|
||||
}
|
||||
}
|
||||
Property::DropSelect { .. } => {
|
||||
// - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort)
|
||||
// - [-] The table `t` will not be created, no table will be renamed to `t`. (todo: update this constraint once ALTER is implemented)
|
||||
|rng, ctx, query_distr, property: &Property| {
|
||||
let Property::DropSelect {
|
||||
table: table_name, ..
|
||||
} = property
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let query = Query::arbitrary_from(rng, ctx, query_distr);
|
||||
if let Query::Create(Create { table: t }) = &query
|
||||
&& t.name == *table_name
|
||||
{
|
||||
// - The table `t` will not be created
|
||||
None
|
||||
} else {
|
||||
Some(query)
|
||||
}
|
||||
}
|
||||
}
|
||||
Property::Queries { .. } => {
|
||||
unreachable!("No extensional querie generation for `Property::Queries`")
|
||||
}
|
||||
Property::FsyncNoWait { .. } | Property::FaultyQuery { .. } => {
|
||||
unreachable!("No extensional queries")
|
||||
}
|
||||
Property::SelectLimit { .. }
|
||||
| Property::SelectSelectOptimizer { .. }
|
||||
| Property::WhereTrueFalseNull { .. }
|
||||
| Property::UNIONAllPreservesCardinality { .. }
|
||||
| Property::ReadYourUpdatesBack { .. }
|
||||
| Property::TableHasExpectedContent { .. }
|
||||
| Property::AllTableHaveExpectedContent { .. } => {
|
||||
unreachable!("No extensional queries")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,6 +486,9 @@ impl Property {
|
||||
/// and `interaction` cannot be serialized directly.
|
||||
pub(crate) fn interactions(&self, connection_index: usize) -> Vec<Interaction> {
|
||||
match self {
|
||||
Property::AllTableHaveExpectedContent { tables } => {
|
||||
assert_all_table_values(tables, connection_index).collect()
|
||||
}
|
||||
Property::TableHasExpectedContent { table } => {
|
||||
let table = table.to_string();
|
||||
let table_name = table.clone();
|
||||
@@ -695,17 +933,17 @@ impl Property {
|
||||
Ok(success) => Ok(Err(format!(
|
||||
"expected table creation to fail but it succeeded: {success:?}"
|
||||
))),
|
||||
Err(e) => {
|
||||
if e.to_string()
|
||||
.contains(&format!("Table {table_name} does not exist"))
|
||||
Err(e) => match e {
|
||||
e if e
|
||||
.to_string()
|
||||
.contains(&format!("no such table: {table_name}")) =>
|
||||
{
|
||||
Ok(Ok(()))
|
||||
} else {
|
||||
Ok(Err(format!(
|
||||
"expected table does not exist error, got: {e}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
_ => Ok(Err(format!(
|
||||
"expected table does not exist error, got: {e}"
|
||||
))),
|
||||
},
|
||||
}
|
||||
},
|
||||
));
|
||||
@@ -726,7 +964,7 @@ impl Property {
|
||||
.into_iter()
|
||||
.map(|q| Interaction::new(connection_index, InteractionType::Query(q))),
|
||||
);
|
||||
interactions.push(Interaction::new(connection_index, select));
|
||||
interactions.push(Interaction::new_ignore_error(connection_index, select));
|
||||
interactions.push(Interaction::new(connection_index, assertion));
|
||||
|
||||
interactions
|
||||
@@ -820,18 +1058,13 @@ impl Property {
|
||||
Interaction::new(connection_index, assertion),
|
||||
]
|
||||
}
|
||||
Property::FsyncNoWait { query, tables } => {
|
||||
let checks = assert_all_table_values(tables, connection_index);
|
||||
Vec::from_iter(
|
||||
std::iter::once(Interaction::new(
|
||||
connection_index,
|
||||
InteractionType::FsyncQuery(query.clone()),
|
||||
))
|
||||
.chain(checks),
|
||||
)
|
||||
Property::FsyncNoWait { query } => {
|
||||
vec![Interaction::new(
|
||||
connection_index,
|
||||
InteractionType::FsyncQuery(query.clone()),
|
||||
)]
|
||||
}
|
||||
Property::FaultyQuery { query, tables } => {
|
||||
let checks = assert_all_table_values(tables, connection_index);
|
||||
Property::FaultyQuery { query } => {
|
||||
let query_clone = query.clone();
|
||||
// A fault may not occur as we first signal we want a fault injected,
|
||||
// then when IO is called the fault triggers. It may happen that a fault is injected
|
||||
@@ -858,13 +1091,13 @@ impl Property {
|
||||
}
|
||||
},
|
||||
);
|
||||
let first = [
|
||||
[
|
||||
InteractionType::FaultyQuery(query.clone()),
|
||||
InteractionType::Assertion(assert),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|i| Interaction::new(connection_index, i));
|
||||
Vec::from_iter(first.chain(checks))
|
||||
.map(|i| Interaction::new(connection_index, i))
|
||||
.collect()
|
||||
}
|
||||
Property::WhereTrueFalseNull { select, predicate } => {
|
||||
let assumption = InteractionType::Assumption(Assertion::new(
|
||||
@@ -1214,10 +1447,11 @@ pub(crate) fn remaining(
|
||||
|
||||
fn property_insert_values_select<R: rand::Rng + ?Sized>(
|
||||
rng: &mut R,
|
||||
query_distr: &QueryDistribution,
|
||||
_query_distr: &QueryDistribution,
|
||||
ctx: &impl GenerationContext,
|
||||
mvcc: bool,
|
||||
) -> Property {
|
||||
assert!(!ctx.tables().is_empty());
|
||||
// Get a random table
|
||||
let table = pick(ctx.tables(), rng);
|
||||
// Generate rows to insert
|
||||
@@ -1230,10 +1464,10 @@ fn property_insert_values_select<R: rand::Rng + ?Sized>(
|
||||
let row = rows[row_index].clone();
|
||||
|
||||
// Insert the rows
|
||||
let insert_query = Insert::Values {
|
||||
let insert_query = Query::Insert(Insert::Values {
|
||||
table: table.name.clone(),
|
||||
values: rows,
|
||||
};
|
||||
});
|
||||
|
||||
// Choose if we want queries to be executed in an interactive transaction
|
||||
let interactive = if !mvcc && rng.random_bool(0.5) {
|
||||
@@ -1244,12 +1478,11 @@ fn property_insert_values_select<R: rand::Rng + ?Sized>(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Create random queries respecting the constraints
|
||||
let mut queries = Vec::new();
|
||||
// - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort)
|
||||
// - [x] The inserted row will not be deleted.
|
||||
// - [x] The inserted row will not be updated.
|
||||
// - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented)
|
||||
|
||||
let amount = rng.random_range(0..3);
|
||||
|
||||
let mut queries = Vec::with_capacity(amount + 2);
|
||||
|
||||
if let Some(ref interactive) = interactive {
|
||||
queries.push(Query::Begin(if interactive.start_with_immediate {
|
||||
Begin::Immediate
|
||||
@@ -1257,39 +1490,9 @@ fn property_insert_values_select<R: rand::Rng + ?Sized>(
|
||||
Begin::Deferred
|
||||
}));
|
||||
}
|
||||
for _ in 0..rng.random_range(0..3) {
|
||||
let query = Query::arbitrary_from(rng, ctx, query_distr);
|
||||
match &query {
|
||||
Query::Delete(Delete {
|
||||
table: t,
|
||||
predicate,
|
||||
}) => {
|
||||
// The inserted row will not be deleted.
|
||||
if t == &table.name && predicate.test(&row, table) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Query::Create(Create { table: t }) => {
|
||||
// There will be no errors in the middle interactions.
|
||||
// - Creating the same table is an error
|
||||
if t.name == table.name {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Query::Update(Update {
|
||||
table: t,
|
||||
set_values: _,
|
||||
predicate,
|
||||
}) => {
|
||||
// The inserted row will not be updated.
|
||||
if t == &table.name && predicate.test(&row, table) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
queries.push(query);
|
||||
}
|
||||
|
||||
queries.extend(std::iter::repeat_n(Query::Placeholder, amount));
|
||||
|
||||
if let Some(ref interactive) = interactive {
|
||||
queries.push(if interactive.end_with_commit {
|
||||
Query::Commit(Commit)
|
||||
@@ -1305,7 +1508,7 @@ fn property_insert_values_select<R: rand::Rng + ?Sized>(
|
||||
);
|
||||
|
||||
Property::InsertValuesSelect {
|
||||
insert: insert_query,
|
||||
insert: insert_query.unwrap_insert(),
|
||||
row_index,
|
||||
queries,
|
||||
select: select_query,
|
||||
@@ -1343,6 +1546,7 @@ fn property_table_has_expected_content<R: rand::Rng + ?Sized>(
|
||||
ctx: &impl GenerationContext,
|
||||
_mvcc: bool,
|
||||
) -> Property {
|
||||
assert!(!ctx.tables().is_empty());
|
||||
// Get a random table
|
||||
let table = pick(ctx.tables(), rng);
|
||||
Property::TableHasExpectedContent {
|
||||
@@ -1350,12 +1554,24 @@ fn property_table_has_expected_content<R: rand::Rng + ?Sized>(
|
||||
}
|
||||
}
|
||||
|
||||
fn property_all_tables_have_expected_content<R: rand::Rng + ?Sized>(
|
||||
_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<R: rand::Rng + ?Sized>(
|
||||
rng: &mut R,
|
||||
_query_distr: &QueryDistribution,
|
||||
ctx: &impl GenerationContext,
|
||||
_mvcc: bool,
|
||||
) -> Property {
|
||||
assert!(!ctx.tables().is_empty());
|
||||
// Get a random table
|
||||
let table = pick(ctx.tables(), rng);
|
||||
// Select the table
|
||||
@@ -1371,30 +1587,16 @@ fn property_select_limit<R: rand::Rng + ?Sized>(
|
||||
|
||||
fn property_double_create_failure<R: rand::Rng + ?Sized>(
|
||||
rng: &mut R,
|
||||
query_distr: &QueryDistribution,
|
||||
_query_distr: &QueryDistribution,
|
||||
ctx: &impl GenerationContext,
|
||||
_mvcc: bool,
|
||||
) -> Property {
|
||||
// Create the table
|
||||
let create_query = Create::arbitrary(rng, ctx);
|
||||
let table = &create_query.table;
|
||||
|
||||
// Create random queries respecting the constraints
|
||||
let mut queries = Vec::new();
|
||||
// The interactions in the middle has the following constraints;
|
||||
// - [x] There will be no errors in the middle interactions.(best effort)
|
||||
// - [ ] Table `t` will not be renamed or dropped.(todo: add this constraint once ALTER or DROP is implemented)
|
||||
for _ in 0..rng.random_range(0..3) {
|
||||
let query = Query::arbitrary_from(rng, ctx, query_distr);
|
||||
if let Query::Create(Create { table: t }) = &query {
|
||||
// There will be no errors in the middle interactions.
|
||||
// - Creating the same table is an error
|
||||
if t.name == table.name {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
queries.push(query);
|
||||
}
|
||||
let amount = rng.random_range(0..3);
|
||||
|
||||
let queries = vec![Query::Placeholder; amount];
|
||||
|
||||
Property::DoubleCreateFailure {
|
||||
create: create_query,
|
||||
@@ -1404,55 +1606,19 @@ fn property_double_create_failure<R: rand::Rng + ?Sized>(
|
||||
|
||||
fn property_delete_select<R: rand::Rng + ?Sized>(
|
||||
rng: &mut R,
|
||||
query_distr: &QueryDistribution,
|
||||
_query_distr: &QueryDistribution,
|
||||
ctx: &impl GenerationContext,
|
||||
_mvcc: bool,
|
||||
) -> Property {
|
||||
assert!(!ctx.tables().is_empty());
|
||||
// Get a random table
|
||||
let table = pick(ctx.tables(), rng);
|
||||
// Generate a random predicate
|
||||
let predicate = Predicate::arbitrary_from(rng, ctx, table);
|
||||
|
||||
// Create random queries respecting the constraints
|
||||
let mut queries = Vec::new();
|
||||
// - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort)
|
||||
// - [x] A row that holds for the predicate will not be inserted.
|
||||
// - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented)
|
||||
for _ in 0..rng.random_range(0..3) {
|
||||
let query = Query::arbitrary_from(rng, ctx, query_distr);
|
||||
match &query {
|
||||
Query::Insert(Insert::Values { table: t, values }) => {
|
||||
// A row that holds for the predicate will not be inserted.
|
||||
if t == &table.name && values.iter().any(|v| predicate.test(v, table)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Query::Insert(Insert::Select {
|
||||
table: t,
|
||||
select: _,
|
||||
}) => {
|
||||
// A row that holds for the predicate will not be inserted.
|
||||
if t == &table.name {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Query::Update(Update { table: t, .. }) => {
|
||||
// A row that holds for the predicate will not be updated.
|
||||
if t == &table.name {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Query::Create(Create { table: t }) => {
|
||||
// There will be no errors in the middle interactions.
|
||||
// - Creating the same table is an error
|
||||
if t.name == table.name {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
queries.push(query);
|
||||
}
|
||||
let amount = rng.random_range(0..3);
|
||||
|
||||
let queries = vec![Query::Placeholder; amount];
|
||||
|
||||
Property::DeleteSelect {
|
||||
table: table.name.clone(),
|
||||
@@ -1463,27 +1629,17 @@ fn property_delete_select<R: rand::Rng + ?Sized>(
|
||||
|
||||
fn property_drop_select<R: rand::Rng + ?Sized>(
|
||||
rng: &mut R,
|
||||
query_distr: &QueryDistribution,
|
||||
_query_distr: &QueryDistribution,
|
||||
ctx: &impl GenerationContext,
|
||||
_mvcc: bool,
|
||||
) -> Property {
|
||||
assert!(!ctx.tables().is_empty());
|
||||
// Get a random table
|
||||
let table = pick(ctx.tables(), rng);
|
||||
|
||||
// Create random queries respecting the constraints
|
||||
let mut queries = Vec::new();
|
||||
// - [x] There will be no errors in the middle interactions. (this constraint is impossible to check, so this is just best effort)
|
||||
// - [-] The table `t` will not be created, no table will be renamed to `t`. (todo: update this constraint once ALTER is implemented)
|
||||
for _ in 0..rng.random_range(0..3) {
|
||||
let query = Query::arbitrary_from(rng, ctx, query_distr);
|
||||
if let Query::Create(Create { table: t }) = &query {
|
||||
// - The table `t` will not be created
|
||||
if t.name == table.name {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
queries.push(query);
|
||||
}
|
||||
let amount = rng.random_range(0..3);
|
||||
|
||||
let queries = vec![Query::Placeholder; amount];
|
||||
|
||||
let select = Select::simple(
|
||||
table.name.clone(),
|
||||
@@ -1503,6 +1659,7 @@ fn property_select_select_optimizer<R: rand::Rng + ?Sized>(
|
||||
ctx: &impl GenerationContext,
|
||||
_mvcc: bool,
|
||||
) -> Property {
|
||||
assert!(!ctx.tables().is_empty());
|
||||
// Get a random table
|
||||
let table = pick(ctx.tables(), rng);
|
||||
// Generate a random predicate
|
||||
@@ -1526,6 +1683,7 @@ fn property_where_true_false_null<R: rand::Rng + ?Sized>(
|
||||
ctx: &impl GenerationContext,
|
||||
_mvcc: bool,
|
||||
) -> Property {
|
||||
assert!(!ctx.tables().is_empty());
|
||||
// Get a random table
|
||||
let table = pick(ctx.tables(), rng);
|
||||
// Generate a random predicate
|
||||
@@ -1547,6 +1705,7 @@ fn property_union_all_preserves_cardinality<R: rand::Rng + ?Sized>(
|
||||
ctx: &impl GenerationContext,
|
||||
_mvcc: bool,
|
||||
) -> Property {
|
||||
assert!(!ctx.tables().is_empty());
|
||||
// Get a random table
|
||||
let table = pick(ctx.tables(), rng);
|
||||
// Generate a random predicate
|
||||
@@ -1576,7 +1735,6 @@ fn property_fsync_no_wait<R: rand::Rng + ?Sized>(
|
||||
) -> Property {
|
||||
Property::FsyncNoWait {
|
||||
query: Query::arbitrary_from(rng, ctx, query_distr),
|
||||
tables: ctx.tables().iter().map(|t| t.name.clone()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1588,7 +1746,6 @@ fn property_faulty_query<R: rand::Rng + ?Sized>(
|
||||
) -> Property {
|
||||
Property::FaultyQuery {
|
||||
query: Query::arbitrary_from(rng, ctx, query_distr),
|
||||
tables: ctx.tables().iter().map(|t| t.name.clone()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1604,6 +1761,9 @@ impl PropertyDiscriminants {
|
||||
PropertyDiscriminants::InsertValuesSelect => property_insert_values_select,
|
||||
PropertyDiscriminants::ReadYourUpdatesBack => property_read_your_updates_back,
|
||||
PropertyDiscriminants::TableHasExpectedContent => property_table_has_expected_content,
|
||||
PropertyDiscriminants::AllTableHaveExpectedContent => {
|
||||
property_all_tables_have_expected_content
|
||||
}
|
||||
PropertyDiscriminants::DoubleCreateFailure => property_double_create_failure,
|
||||
PropertyDiscriminants::SelectLimit => property_select_limit,
|
||||
PropertyDiscriminants::DeleteSelect => property_delete_select,
|
||||
@@ -1621,10 +1781,16 @@ impl PropertyDiscriminants {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn weight(&self, env: &SimulatorEnv, remaining: &Remaining, opts: &Opts) -> u32 {
|
||||
pub fn weight(
|
||||
&self,
|
||||
env: &SimulatorEnv,
|
||||
remaining: &Remaining,
|
||||
ctx: &impl GenerationContext,
|
||||
) -> u32 {
|
||||
let opts = ctx.opts();
|
||||
match self {
|
||||
PropertyDiscriminants::InsertValuesSelect => {
|
||||
if !env.opts.disable_insert_values_select {
|
||||
if !env.opts.disable_insert_values_select && !ctx.tables().is_empty() {
|
||||
u32::min(remaining.select, remaining.insert).max(1)
|
||||
} else {
|
||||
0
|
||||
@@ -1633,7 +1799,15 @@ impl PropertyDiscriminants {
|
||||
PropertyDiscriminants::ReadYourUpdatesBack => {
|
||||
u32::min(remaining.select, remaining.insert).max(1)
|
||||
}
|
||||
PropertyDiscriminants::TableHasExpectedContent => remaining.select.max(1),
|
||||
PropertyDiscriminants::TableHasExpectedContent => {
|
||||
if !ctx.tables().is_empty() {
|
||||
remaining.select.max(1)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
// AllTableHaveExpectedContent should only be generated by Properties that inject faults
|
||||
PropertyDiscriminants::AllTableHaveExpectedContent => 0,
|
||||
PropertyDiscriminants::DoubleCreateFailure => {
|
||||
if !env.opts.disable_double_create_failure {
|
||||
remaining.create / 2
|
||||
@@ -1642,43 +1816,48 @@ impl PropertyDiscriminants {
|
||||
}
|
||||
}
|
||||
PropertyDiscriminants::SelectLimit => {
|
||||
if !env.opts.disable_select_limit {
|
||||
if !env.opts.disable_select_limit && !ctx.tables().is_empty() {
|
||||
remaining.select
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
PropertyDiscriminants::DeleteSelect => {
|
||||
if !env.opts.disable_delete_select {
|
||||
if !env.opts.disable_delete_select && !ctx.tables().is_empty() {
|
||||
u32::min(remaining.select, remaining.insert).min(remaining.delete)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
PropertyDiscriminants::DropSelect => {
|
||||
if !env.opts.disable_drop_select {
|
||||
// remaining.drop
|
||||
0
|
||||
if !env.opts.disable_drop_select && !ctx.tables().is_empty() {
|
||||
remaining.drop
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
PropertyDiscriminants::SelectSelectOptimizer => {
|
||||
if !env.opts.disable_select_optimizer {
|
||||
if !env.opts.disable_select_optimizer && !ctx.tables().is_empty() {
|
||||
remaining.select / 2
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
PropertyDiscriminants::WhereTrueFalseNull => {
|
||||
if opts.indexes && !env.opts.disable_where_true_false_null {
|
||||
if opts.indexes
|
||||
&& !env.opts.disable_where_true_false_null
|
||||
&& !ctx.tables().is_empty()
|
||||
{
|
||||
remaining.select / 2
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
PropertyDiscriminants::UNIONAllPreservesCardinality => {
|
||||
if opts.indexes && !env.opts.disable_union_all_preserves_cardinality {
|
||||
if opts.indexes
|
||||
&& !env.opts.disable_union_all_preserves_cardinality
|
||||
&& !ctx.tables().is_empty()
|
||||
{
|
||||
remaining.select / 3
|
||||
} else {
|
||||
0
|
||||
@@ -1727,6 +1906,7 @@ impl PropertyDiscriminants {
|
||||
QueryCapabilities::SELECT.union(QueryCapabilities::UPDATE)
|
||||
}
|
||||
PropertyDiscriminants::TableHasExpectedContent => QueryCapabilities::SELECT,
|
||||
PropertyDiscriminants::AllTableHaveExpectedContent => QueryCapabilities::SELECT,
|
||||
PropertyDiscriminants::DoubleCreateFailure => QueryCapabilities::CREATE,
|
||||
PropertyDiscriminants::SelectLimit => QueryCapabilities::SELECT,
|
||||
PropertyDiscriminants::DeleteSelect => {
|
||||
@@ -1762,22 +1942,21 @@ impl<'a> PropertyDistribution<'a> {
|
||||
env: &SimulatorEnv,
|
||||
remaining: &Remaining,
|
||||
query_distr: &'a QueryDistribution,
|
||||
opts: &Opts,
|
||||
) -> Self {
|
||||
ctx: &impl GenerationContext,
|
||||
) -> Result<Self, rand::distr::weighted::Error> {
|
||||
let properties = PropertyDiscriminants::can_generate(query_distr.items());
|
||||
let weights = WeightedIndex::new(
|
||||
properties
|
||||
.iter()
|
||||
.map(|property| property.weight(env, remaining, opts)),
|
||||
)
|
||||
.unwrap();
|
||||
.map(|property| property.weight(env, remaining, ctx)),
|
||||
)?;
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
properties,
|
||||
weights,
|
||||
query_distr,
|
||||
mvcc: env.profile.experimental_mvcc,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1816,6 +1995,49 @@ impl<'a> ArbitraryFrom<&PropertyDistribution<'a>> for Property {
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_queries<R: rand::Rng + ?Sized, F>(
|
||||
rng: &mut R,
|
||||
ctx: &impl GenerationContext,
|
||||
amount: usize,
|
||||
init_queries: &[&Query],
|
||||
func: F,
|
||||
) -> Vec<Query>
|
||||
where
|
||||
F: Fn(&mut R, PropertyGenContext) -> Option<Query>,
|
||||
{
|
||||
// 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<Table>, 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 {
|
||||
|
||||
@@ -29,7 +29,7 @@ fn random_create<R: rand::Rng + ?Sized>(rng: &mut R, conn_ctx: &impl GenerationC
|
||||
}
|
||||
|
||||
fn random_select<R: rand::Rng + ?Sized>(rng: &mut R, conn_ctx: &impl GenerationContext) -> Query {
|
||||
if rng.random_bool(0.7) {
|
||||
if !conn_ctx.tables().is_empty() && rng.random_bool(0.7) {
|
||||
Query::Select(Select::arbitrary(rng, conn_ctx))
|
||||
} else {
|
||||
// Random expression
|
||||
@@ -111,27 +111,36 @@ impl QueryDiscriminants {
|
||||
| QueryDiscriminants::Rollback => {
|
||||
unreachable!("transactional queries should not be generated")
|
||||
}
|
||||
QueryDiscriminants::Placeholder => {
|
||||
unreachable!("Query Placeholders should not be generated")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn weight(&self, remaining: &Remaining) -> u32 {
|
||||
match self {
|
||||
QueryDiscriminants::Create => remaining.create,
|
||||
QueryDiscriminants::Select => remaining.select + remaining.select / 3, // remaining.select / 3 is for the random_expr generation
|
||||
// remaining.select / 3 is for the random_expr generation
|
||||
// have a max of 1 so that we always generate at least a non zero weight for `QueryDistribution`
|
||||
QueryDiscriminants::Select => (remaining.select + remaining.select / 3).max(1),
|
||||
QueryDiscriminants::Insert => remaining.insert,
|
||||
QueryDiscriminants::Delete => remaining.delete,
|
||||
QueryDiscriminants::Update => remaining.update,
|
||||
QueryDiscriminants::Drop => 0,
|
||||
QueryDiscriminants::Drop => remaining.drop,
|
||||
QueryDiscriminants::CreateIndex => remaining.create_index,
|
||||
QueryDiscriminants::Begin
|
||||
| QueryDiscriminants::Commit
|
||||
| QueryDiscriminants::Rollback => {
|
||||
unreachable!("transactional queries should not be generated")
|
||||
}
|
||||
QueryDiscriminants::Placeholder => {
|
||||
unreachable!("Query Placeholders should not be generated")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct QueryDistribution {
|
||||
queries: &'static [QueryDiscriminants],
|
||||
weights: WeightedIndex<u32>,
|
||||
|
||||
@@ -32,9 +32,33 @@ pub enum Query {
|
||||
Begin(Begin),
|
||||
Commit(Commit),
|
||||
Rollback(Rollback),
|
||||
/// Placeholder query that still needs to be generated
|
||||
Placeholder,
|
||||
}
|
||||
|
||||
impl Query {
|
||||
pub fn as_create(&self) -> &Create {
|
||||
match self {
|
||||
Self::Create(create) => create,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_create(self) -> Create {
|
||||
match self {
|
||||
Self::Create(create) => create,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn unwrap_insert(self) -> Insert {
|
||||
match self {
|
||||
Self::Insert(insert) => insert,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dependencies(&self) -> IndexSet<String> {
|
||||
match self {
|
||||
Query::Select(select) => select.dependencies(),
|
||||
@@ -48,6 +72,7 @@ impl Query {
|
||||
IndexSet::from_iter([table_name.clone()])
|
||||
}
|
||||
Query::Begin(_) | Query::Commit(_) | Query::Rollback(_) => IndexSet::new(),
|
||||
Query::Placeholder => IndexSet::new(),
|
||||
}
|
||||
}
|
||||
pub fn uses(&self) -> Vec<String> {
|
||||
@@ -61,6 +86,7 @@ impl Query {
|
||||
| Query::Drop(Drop { table, .. }) => vec![table.clone()],
|
||||
Query::CreateIndex(CreateIndex { table_name, .. }) => vec![table_name.clone()],
|
||||
Query::Begin(..) | Query::Commit(..) | Query::Rollback(..) => vec![],
|
||||
Query::Placeholder => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +120,7 @@ impl Display for Query {
|
||||
Self::Begin(begin) => write!(f, "{begin}"),
|
||||
Self::Commit(commit) => write!(f, "{commit}"),
|
||||
Self::Rollback(rollback) => write!(f, "{rollback}"),
|
||||
Self::Placeholder => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +140,7 @@ impl Shadow for Query {
|
||||
Query::Begin(begin) => Ok(begin.shadow(env)),
|
||||
Query::Commit(commit) => Ok(commit.shadow(env)),
|
||||
Query::Rollback(rollback) => Ok(rollback.shadow(env)),
|
||||
Query::Placeholder => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,6 +187,9 @@ impl From<QueryDiscriminants> for QueryCapabilities {
|
||||
| QueryDiscriminants::Rollback => {
|
||||
unreachable!("QueryCapabilities do not apply to transaction queries")
|
||||
}
|
||||
QueryDiscriminants::Placeholder => {
|
||||
unreachable!("QueryCapabilities do not apply to query Placeholder")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,6 +270,7 @@ impl Shadow for Drop {
|
||||
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
|
||||
|
||||
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!(
|
||||
|
||||
@@ -93,11 +93,6 @@ impl Profile {
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
query: QueryProfile {
|
||||
create_table_weight: 0,
|
||||
create_index_weight: 4,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
@@ -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<Table>,
|
||||
transaction_tables: &'a mut Option<TransactionTables>,
|
||||
) -> Self {
|
||||
ShadowTablesMut {
|
||||
commited_tables,
|
||||
transaction_tables,
|
||||
}
|
||||
}
|
||||
|
||||
fn tables(&'a self) -> &'a Vec<Table> {
|
||||
self.transaction_tables
|
||||
.as_ref()
|
||||
|
||||
@@ -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![])
|
||||
|
||||
@@ -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 { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,13 @@ impl Insert {
|
||||
Insert::Values { table, .. } | Insert::Select { table, .. } => table,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rows(&self) -> &[Vec<SimValue>] {
|
||||
match self {
|
||||
Insert::Values { values, .. } => values,
|
||||
Insert::Select { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Insert {
|
||||
|
||||
Reference in New Issue
Block a user