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:
Jussi Saurio
2025-10-07 22:41:53 +03:00
committed by GitHub
9 changed files with 665 additions and 251 deletions

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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>,

View File

@@ -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!(

View File

@@ -93,11 +93,6 @@ impl Profile {
},
..Default::default()
},
query: QueryProfile {
create_table_weight: 0,
create_index_weight: 4,
..Default::default()
},
..Default::default()
};

View File

@@ -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()

View File

@@ -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![])

View File

@@ -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 { .. } => {}
}
}
}

View File

@@ -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 {