Merge 'Enhancement to Sim Snapshot isolation code' from Pedro Muniz

- Made the code around snapshot isolation more ergonomic with each
connections having its own transaction state. Also when shadowing, we
pass a ShadowTablesMut object that dynamically uses the committed tables
or the connection tables depending on the transaction state.
- added begin concurrent transaction before every property when mvcc is
enabled (this is just so we can have some mvcc code be tested using the
simulator under Begin Concurrent, I have not yet implemented the logic
to have concurrent transactions in the simulator)
- some small enhancements to shrinking
TODOs
- have proper logic to have concurrent transactions without WriteWrite
conflicts. This means when generating the plans we need to make sure
that we do not generate rows that will conflict with rows in other
transactions. This is slightly more powerful than what we do in the
fuzzer, as we just have `WriteWriteConflict` as an acceptable error
there. By baking this `NoConflict` approach to the simulator, we can
continuously test the what does not trigger a WriteWriteConflict and
snapshot isolation.

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #3226
This commit is contained in:
Jussi Saurio
2025-09-22 12:48:36 +03:00
committed by GitHub
13 changed files with 658 additions and 360 deletions

1
Cargo.lock generated
View File

@@ -2153,6 +2153,7 @@ dependencies = [
"chrono",
"clap",
"dirs 6.0.0",
"either",
"env_logger 0.11.7",
"garde",
"hex",

View File

@@ -44,3 +44,4 @@ json5 = { version = "0.4.1" }
strum = { workspace = true }
parking_lot = { workspace = true }
indexmap = { workspace = true }
either = "1.15.0"

View File

@@ -1,6 +1,4 @@
use sql_generation::generation::GenerationContext;
use crate::runner::env::{SimulatorEnv, SimulatorTables};
use crate::runner::env::ShadowTablesMut;
pub mod plan;
pub mod property;
@@ -17,25 +15,5 @@ pub mod query;
/// might return a vector of rows that were inserted into the table.
pub(crate) trait Shadow {
type Result;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result;
}
impl GenerationContext for SimulatorEnv {
fn tables(&self) -> &Vec<sql_generation::model::table::Table> {
&self.tables.tables
}
fn opts(&self) -> &sql_generation::generation::Opts {
&self.profile.query.gen_opts
}
}
impl GenerationContext for &mut SimulatorEnv {
fn tables(&self) -> &Vec<sql_generation::model::table::Table> {
&self.tables.tables
}
fn opts(&self) -> &sql_generation::generation::Opts {
&self.profile.query.gen_opts
}
fn shadow(&self, tables: &mut ShadowTablesMut<'_>) -> Self::Result;
}

View File

@@ -13,7 +13,11 @@ use serde::{Deserialize, Serialize};
use sql_generation::{
generation::{Arbitrary, ArbitraryFrom, GenerationContext, frequency, query::SelectFree},
model::{
query::{Create, CreateIndex, Delete, Drop, Insert, Select, update::Update},
query::{
Create, CreateIndex, Delete, Drop, Insert, Select,
transaction::{Begin, Commit},
update::Update,
},
table::SimValue,
},
};
@@ -23,7 +27,7 @@ use crate::{
SimulatorEnv,
generation::Shadow,
model::Query,
runner::env::{SimConnection, SimulationType, SimulatorTables},
runner::env::{ShadowTablesMut, SimConnection, SimulationType},
};
use super::property::{Property, remaining};
@@ -32,10 +36,46 @@ pub(crate) type ResultSet = Result<Vec<Vec<SimValue>>>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct InteractionPlan {
pub(crate) plan: Vec<Interactions>,
pub plan: Vec<Interactions>,
pub mvcc: bool,
}
impl InteractionPlan {
pub(crate) fn new(mvcc: bool) -> Self {
Self {
plan: Vec::new(),
mvcc,
}
}
pub fn new_with(plan: Vec<Interactions>, mvcc: bool) -> Self {
Self { plan, mvcc }
}
#[inline]
pub fn plan(&self) -> &[Interactions] {
&self.plan
}
// TODO: this is just simplified logic so we can get something rolling with begin concurrent
// transactions in the simulator. Ideally when we generate the plan we will have begin and commits statements across interactions
pub fn push(&mut self, interactions: Interactions) {
if self.mvcc {
let conn_index = interactions.connection_index;
let begin = Interactions::new(
conn_index,
InteractionsType::Query(Query::Begin(Begin::Concurrent)),
);
let commit =
Interactions::new(conn_index, InteractionsType::Query(Query::Commit(Commit)));
self.plan.push(begin);
self.plan.push(interactions);
self.plan.push(commit);
} else {
self.plan.push(interactions);
}
}
/// Compute via diff computes a a plan from a given `.plan` file without the need to parse
/// sql. This is possible because there are two versions of the plan file, one that is human
/// readable and one that is serialized as JSON. Under watch mode, the users will be able to
@@ -121,6 +161,109 @@ impl InteractionPlan {
})
.collect()
}
pub(crate) fn stats(&self) -> InteractionStats {
let mut stats = InteractionStats {
select_count: 0,
insert_count: 0,
delete_count: 0,
update_count: 0,
create_count: 0,
create_index_count: 0,
drop_count: 0,
begin_count: 0,
commit_count: 0,
rollback_count: 0,
};
fn query_stat(q: &Query, stats: &mut InteractionStats) {
match q {
Query::Select(_) => stats.select_count += 1,
Query::Insert(_) => stats.insert_count += 1,
Query::Delete(_) => stats.delete_count += 1,
Query::Create(_) => stats.create_count += 1,
Query::Drop(_) => stats.drop_count += 1,
Query::Update(_) => stats.update_count += 1,
Query::CreateIndex(_) => stats.create_index_count += 1,
Query::Begin(_) => stats.begin_count += 1,
Query::Commit(_) => stats.commit_count += 1,
Query::Rollback(_) => stats.rollback_count += 1,
}
}
for interactions in &self.plan {
match &interactions.interactions {
InteractionsType::Property(property) => {
for interaction in &property.interactions(interactions.connection_index) {
if let InteractionType::Query(query) = &interaction.interaction {
query_stat(query, &mut stats);
}
}
}
InteractionsType::Query(query) => {
query_stat(query, &mut stats);
}
InteractionsType::Fault(_) => {}
}
}
stats
}
pub fn generate_plan<R: rand::Rng>(rng: &mut R, env: &mut SimulatorEnv) -> Self {
let mut plan = InteractionPlan::new(env.profile.experimental_mvcc);
let num_interactions = env.opts.max_interactions as usize;
// First create at least one table
let create_query = Create::arbitrary(rng, &env.connection_context(0));
env.committed_tables.push(create_query.table.clone());
// initial query starts at 0th connection
plan.plan.push(Interactions::new(
0,
InteractionsType::Query(Query::Create(create_query)),
));
while plan.len() < num_interactions {
tracing::debug!("Generating interaction {}/{}", plan.len(), num_interactions);
let interactions = {
let conn_index = env.choose_conn(rng);
let conn_ctx = &env.connection_context(conn_index);
Interactions::arbitrary_from(rng, conn_ctx, (env, plan.stats(), conn_index))
};
interactions.shadow(&mut env.get_conn_tables_mut(interactions.connection_index));
plan.push(interactions);
}
tracing::info!("Generated plan with {} interactions", plan.plan.len());
plan
}
}
impl Deref for InteractionPlan {
type Target = [Interactions];
fn deref(&self) -> &Self::Target {
&self.plan
}
}
impl DerefMut for InteractionPlan {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.plan
}
}
impl IntoIterator for InteractionPlan {
type Item = Interactions;
type IntoIter = <Vec<Interactions> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.plan.into_iter()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
@@ -172,7 +315,7 @@ pub enum InteractionsType {
impl Shadow for Interactions {
type Result = ();
fn shadow(&self, tables: &mut SimulatorTables) {
fn shadow(&self, tables: &mut ShadowTablesMut) {
match &self.interactions {
InteractionsType::Property(property) => {
let initial_tables = tables.clone();
@@ -180,7 +323,7 @@ impl Shadow for Interactions {
let res = interaction.shadow(tables);
if res.is_err() {
// If any interaction fails, we reset the tables to the initial state
*tables = initial_tables.clone();
**tables = initial_tables.clone();
break;
}
}
@@ -368,89 +511,6 @@ impl Display for Fault {
}
}
impl InteractionPlan {
pub(crate) fn new() -> Self {
Self { plan: Vec::new() }
}
pub(crate) fn stats(&self) -> InteractionStats {
let mut stats = InteractionStats {
select_count: 0,
insert_count: 0,
delete_count: 0,
update_count: 0,
create_count: 0,
create_index_count: 0,
drop_count: 0,
begin_count: 0,
commit_count: 0,
rollback_count: 0,
};
fn query_stat(q: &Query, stats: &mut InteractionStats) {
match q {
Query::Select(_) => stats.select_count += 1,
Query::Insert(_) => stats.insert_count += 1,
Query::Delete(_) => stats.delete_count += 1,
Query::Create(_) => stats.create_count += 1,
Query::Drop(_) => stats.drop_count += 1,
Query::Update(_) => stats.update_count += 1,
Query::CreateIndex(_) => stats.create_index_count += 1,
Query::Begin(_) => stats.begin_count += 1,
Query::Commit(_) => stats.commit_count += 1,
Query::Rollback(_) => stats.rollback_count += 1,
}
}
for interactions in &self.plan {
match &interactions.interactions {
InteractionsType::Property(property) => {
for interaction in &property.interactions(interactions.connection_index) {
if let InteractionType::Query(query) = &interaction.interaction {
query_stat(query, &mut stats);
}
}
}
InteractionsType::Query(query) => {
query_stat(query, &mut stats);
}
InteractionsType::Fault(_) => {}
}
}
stats
}
pub fn generate_plan<R: rand::Rng>(rng: &mut R, env: &mut SimulatorEnv) -> Self {
let mut plan = InteractionPlan::new();
let num_interactions = env.opts.max_interactions as usize;
// First create at least one table
let create_query = Create::arbitrary(rng, env);
env.tables.push(create_query.table.clone());
// initial query starts at 0th connection
plan.plan.push(Interactions::new(
0,
InteractionsType::Query(Query::Create(create_query)),
));
while plan.plan.len() < num_interactions {
tracing::debug!(
"Generating interaction {}/{}",
plan.plan.len(),
num_interactions
);
let interactions = Interactions::arbitrary_from(rng, env, (env, plan.stats()));
interactions.shadow(&mut env.tables);
plan.plan.push(interactions);
}
tracing::info!("Generated plan with {} interactions", plan.plan.len());
plan
}
}
#[derive(Debug, Clone)]
pub struct Interaction {
pub connection_index: usize,
@@ -520,7 +580,7 @@ impl Display for InteractionType {
impl Shadow for InteractionType {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
fn shadow(&self, env: &mut ShadowTablesMut) -> Self::Result {
match self {
Self::Query(query) => query.shadow(env),
Self::FsyncQuery(query) => {
@@ -834,66 +894,90 @@ fn reopen_database(env: &mut SimulatorEnv) {
};
}
fn random_create<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
let mut create = Create::arbitrary(rng, env);
while env.tables.iter().any(|t| t.name == create.table.name) {
create = Create::arbitrary(rng, env);
fn random_create<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions {
let conn_ctx = env.connection_context(conn_index);
let mut create = Create::arbitrary(rng, &conn_ctx);
while conn_ctx
.tables()
.iter()
.any(|t| t.name == create.table.name)
{
create = Create::arbitrary(rng, &conn_ctx);
}
Interactions::new(conn_index, InteractionsType::Query(Query::Create(create)))
}
fn random_read<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions {
Interactions::new(
env.choose_conn(rng),
InteractionsType::Query(Query::Create(create)),
conn_index,
InteractionsType::Query(Query::Select(Select::arbitrary(
rng,
&env.connection_context(conn_index),
))),
)
}
fn random_read<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
fn random_expr<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions {
Interactions::new(
env.choose_conn(rng),
InteractionsType::Query(Query::Select(Select::arbitrary(rng, env))),
conn_index,
InteractionsType::Query(Query::Select(
SelectFree::arbitrary(rng, &env.connection_context(conn_index)).0,
)),
)
}
fn random_expr<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
fn random_write<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions {
Interactions::new(
env.choose_conn(rng),
InteractionsType::Query(Query::Select(SelectFree::arbitrary(rng, env).0)),
conn_index,
InteractionsType::Query(Query::Insert(Insert::arbitrary(
rng,
&env.connection_context(conn_index),
))),
)
}
fn random_write<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
fn random_delete<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions {
Interactions::new(
env.choose_conn(rng),
InteractionsType::Query(Query::Insert(Insert::arbitrary(rng, env))),
conn_index,
InteractionsType::Query(Query::Delete(Delete::arbitrary(
rng,
&env.connection_context(conn_index),
))),
)
}
fn random_delete<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
fn random_update<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions {
Interactions::new(
env.choose_conn(rng),
InteractionsType::Query(Query::Delete(Delete::arbitrary(rng, env))),
conn_index,
InteractionsType::Query(Query::Update(Update::arbitrary(
rng,
&env.connection_context(conn_index),
))),
)
}
fn random_update<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
fn random_drop<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv, conn_index: usize) -> Interactions {
Interactions::new(
env.choose_conn(rng),
InteractionsType::Query(Query::Update(Update::arbitrary(rng, env))),
conn_index,
InteractionsType::Query(Query::Drop(Drop::arbitrary(
rng,
&env.connection_context(conn_index),
))),
)
}
fn random_drop<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
Interactions::new(
env.choose_conn(rng),
InteractionsType::Query(Query::Drop(Drop::arbitrary(rng, env))),
)
}
fn random_create_index<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Option<Interactions> {
if env.tables.is_empty() {
fn random_create_index<R: rand::Rng>(
rng: &mut R,
env: &SimulatorEnv,
conn_index: usize,
) -> Option<Interactions> {
let conn_ctx = env.connection_context(conn_index);
if conn_ctx.tables().is_empty() {
return None;
}
let mut create_index = CreateIndex::arbitrary(rng, env);
while env
.tables
let mut create_index = CreateIndex::arbitrary(rng, &conn_ctx);
while conn_ctx
.tables()
.iter()
.find(|t| t.name == create_index.table_name)
.expect("table should exist")
@@ -901,11 +985,11 @@ fn random_create_index<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Option<
.iter()
.any(|i| i == &create_index.index_name)
{
create_index = CreateIndex::arbitrary(rng, env);
create_index = CreateIndex::arbitrary(rng, &conn_ctx);
}
Some(Interactions::new(
env.choose_conn(rng),
conn_index,
InteractionsType::Query(Query::CreateIndex(create_index)),
))
}
@@ -920,23 +1004,28 @@ fn random_fault<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
Interactions::new(env.choose_conn(rng), InteractionsType::Fault(fault))
}
impl ArbitraryFrom<(&SimulatorEnv, InteractionStats)> for Interactions {
impl ArbitraryFrom<(&SimulatorEnv, InteractionStats, usize)> for Interactions {
fn arbitrary_from<R: rand::Rng, C: GenerationContext>(
rng: &mut R,
_context: &C,
(env, stats): (&SimulatorEnv, InteractionStats),
conn_ctx: &C,
(env, stats, conn_index): (&SimulatorEnv, InteractionStats, usize),
) -> Self {
let remaining_ = remaining(env.opts.max_interactions, &env.profile.query, &stats);
let remaining_ = remaining(
env.opts.max_interactions,
&env.profile.query,
&stats,
env.profile.experimental_mvcc,
);
frequency(
vec![
(
u32::min(remaining_.select, remaining_.insert) + remaining_.create,
Box::new(|rng: &mut R| {
Interactions::new(
env.choose_conn(rng),
conn_index,
InteractionsType::Property(Property::arbitrary_from(
rng,
env,
conn_ctx,
(env, &stats),
)),
)
@@ -944,43 +1033,43 @@ impl ArbitraryFrom<(&SimulatorEnv, InteractionStats)> for Interactions {
),
(
remaining_.select,
Box::new(|rng: &mut R| random_read(rng, env)),
Box::new(|rng: &mut R| random_read(rng, env, conn_index)),
),
(
remaining_.select / 3,
Box::new(|rng: &mut R| random_expr(rng, env)),
Box::new(|rng: &mut R| random_expr(rng, env, conn_index)),
),
(
remaining_.insert,
Box::new(|rng: &mut R| random_write(rng, env)),
Box::new(|rng: &mut R| random_write(rng, env, conn_index)),
),
(
remaining_.create,
Box::new(|rng: &mut R| random_create(rng, env)),
Box::new(|rng: &mut R| random_create(rng, env, conn_index)),
),
(
remaining_.create_index,
Box::new(|rng: &mut R| {
if let Some(interaction) = random_create_index(rng, env) {
if let Some(interaction) = random_create_index(rng, env, conn_index) {
interaction
} else {
// if no tables exist, we can't create an index, so fallback to creating a table
random_create(rng, env)
random_create(rng, env, conn_index)
}
}),
),
(
remaining_.delete,
Box::new(|rng: &mut R| random_delete(rng, env)),
Box::new(|rng: &mut R| random_delete(rng, env, conn_index)),
),
(
remaining_.update,
Box::new(|rng: &mut R| random_update(rng, env)),
Box::new(|rng: &mut R| random_update(rng, env, conn_index)),
),
(
// remaining_.drop,
0,
Box::new(|rng: &mut R| random_drop(rng, env)),
Box::new(|rng: &mut R| random_drop(rng, env, conn_index)),
),
(
remaining_

View File

@@ -226,7 +226,8 @@ impl Property {
let assumption = InteractionType::Assumption(Assertion::new(
format!("table {} exists", table.clone()),
move |_: &Vec<ResultSet>, env: &mut SimulatorEnv| {
if env.tables.iter().any(|t| t.name == table_name) {
let conn_tables = env.get_conn_tables(connection_index);
if conn_tables.iter().any(|t| t.name == table_name) {
Ok(Ok(()))
} else {
Ok(Err(format!("table {table_name} does not exist")))
@@ -246,8 +247,8 @@ impl Property {
let Ok(rows) = rows else {
return Ok(Err(format!("expected rows but got error: {rows:?}")));
};
let sim_table = env
.tables
let conn_tables = env.get_conn_tables(connection_index);
let sim_table = conn_tables
.iter()
.find(|t| t.name == table)
.expect("table should be in enviroment");
@@ -283,7 +284,8 @@ impl Property {
let assumption = InteractionType::Assumption(Assertion::new(
format!("table {} exists", table.clone()),
move |_: &Vec<ResultSet>, env: &mut SimulatorEnv| {
if env.tables.iter().any(|t| t.name == table.clone()) {
let conn_tables = env.get_conn_tables(connection_index);
if conn_tables.iter().any(|t| t.name == table.clone()) {
Ok(Ok(()))
} else {
Ok(Err(format!("table {} does not exist", table.clone())))
@@ -360,7 +362,8 @@ impl Property {
{
let table_name = table.clone();
move |_: &Vec<ResultSet>, env: &mut SimulatorEnv| {
if env.tables.iter().any(|t| t.name == table_name) {
let conn_tables = env.get_conn_tables(connection_index);
if conn_tables.iter().any(|t| t.name == table_name) {
Ok(Ok(()))
} else {
Ok(Err(format!("table {table_name} does not exist")))
@@ -429,7 +432,8 @@ impl Property {
let assumption = InteractionType::Assumption(Assertion::new(
"Double-Create-Failure should not be called on an existing table".to_string(),
move |_: &Vec<ResultSet>, env: &mut SimulatorEnv| {
if !env.tables.iter().any(|t| t.name == table_name) {
let conn_tables = env.get_conn_tables(connection_index);
if !conn_tables.iter().any(|t| t.name == table_name) {
Ok(Ok(()))
} else {
Ok(Err(format!("table {table_name} already exists")))
@@ -484,15 +488,16 @@ impl Property {
{
let table_name = select.dependencies();
move |_: &Vec<ResultSet>, env: &mut SimulatorEnv| {
let conn_tables = env.get_conn_tables(connection_index);
if table_name
.iter()
.all(|table| env.tables.iter().any(|t| t.name == *table))
.all(|table| conn_tables.iter().any(|t| t.name == *table))
{
Ok(Ok(()))
} else {
let missing_tables = table_name
.iter()
.filter(|t| !env.tables.iter().any(|t2| t2.name == **t))
.filter(|t| !conn_tables.iter().any(|t2| t2.name == **t))
.collect::<Vec<&String>>();
Ok(Err(format!("missing tables: {missing_tables:?}")))
}
@@ -544,12 +549,13 @@ impl Property {
{
let table = table.clone();
move |_: &Vec<ResultSet>, env: &mut SimulatorEnv| {
if env.tables.iter().any(|t| t.name == table) {
let conn_tables = env.get_conn_tables(connection_index);
if conn_tables.iter().any(|t| t.name == table) {
Ok(Ok(()))
} else {
{
let available_tables: Vec<String> =
env.tables.iter().map(|t| t.name.clone()).collect();
conn_tables.iter().map(|t| t.name.clone()).collect();
Ok(Err(format!(
"table \'{table}\' not found. Available tables: {available_tables:?}"
)))
@@ -617,12 +623,13 @@ impl Property {
{
let table = table.clone();
move |_, env: &mut SimulatorEnv| {
if env.tables.iter().any(|t| t.name == table) {
let conn_tables = env.get_conn_tables(connection_index);
if conn_tables.iter().any(|t| t.name == table) {
Ok(Ok(()))
} else {
{
let available_tables: Vec<String> =
env.tables.iter().map(|t| t.name.clone()).collect();
conn_tables.iter().map(|t| t.name.clone()).collect();
Ok(Err(format!(
"table \'{table}\' not found. Available tables: {available_tables:?}"
)))
@@ -684,12 +691,13 @@ impl Property {
{
let table = table.clone();
move |_: &Vec<ResultSet>, env: &mut SimulatorEnv| {
if env.tables.iter().any(|t| t.name == table) {
let conn_tables = env.get_conn_tables(connection_index);
if conn_tables.iter().any(|t| t.name == table) {
Ok(Ok(()))
} else {
{
let available_tables: Vec<String> =
env.tables.iter().map(|t| t.name.clone()).collect();
conn_tables.iter().map(|t| t.name.clone()).collect();
Ok(Err(format!(
"table \'{table}\' not found. Available tables: {available_tables:?}"
)))
@@ -788,7 +796,8 @@ impl Property {
let last = stack.last().unwrap();
match last {
Ok(_) => {
let _ = query_clone.shadow(&mut env.tables);
let _ = query_clone
.shadow(&mut env.get_conn_tables_mut(connection_index));
Ok(Ok(()))
}
Err(err) => {
@@ -821,15 +830,16 @@ impl Property {
{
let tables = select.dependencies();
move |_: &Vec<ResultSet>, env: &mut SimulatorEnv| {
let conn_tables = env.get_conn_tables(connection_index);
if tables
.iter()
.all(|table| env.tables.iter().any(|t| t.name == *table))
.all(|table| conn_tables.iter().any(|t| t.name == *table))
{
Ok(Ok(()))
} else {
let missing_tables = tables
.iter()
.filter(|t| !env.tables.iter().any(|t2| t2.name == **t))
.filter(|t| !conn_tables.iter().any(|t2| t2.name == **t))
.collect::<Vec<&String>>();
Ok(Err(format!("missing tables: {missing_tables:?}")))
}
@@ -1030,7 +1040,8 @@ fn assert_all_table_values(
let assertion = InteractionType::Assertion(Assertion::new(format!("table {table} should contain all of its expected values"), {
let table = table.clone();
move |stack: &Vec<ResultSet>, env: &mut SimulatorEnv| {
let table = env.tables.iter().find(|t| t.name == table).ok_or_else(|| {
let conn_ctx = env.get_conn_tables(connection_index);
let table = conn_ctx.iter().find(|t| t.name == table).ok_or_else(|| {
LimboError::InternalError(format!(
"table {table} should exist in simulator env"
))
@@ -1090,6 +1101,7 @@ pub(crate) fn remaining(
max_interactions: u32,
opts: &QueryProfile,
stats: &InteractionStats,
mvcc: bool,
) -> Remaining {
let total_weight = opts.select_weight
+ opts.create_table_weight
@@ -1116,7 +1128,7 @@ pub(crate) fn remaining(
let remaining_create = total_create
.checked_sub(stats.create_count)
.unwrap_or_default();
let remaining_create_index = total_create_index
let mut remaining_create_index = total_create_index
.checked_sub(stats.create_index_count)
.unwrap_or_default();
let remaining_delete = total_delete
@@ -1127,6 +1139,11 @@ pub(crate) fn remaining(
.unwrap_or_default();
let remaining_drop = total_drop.checked_sub(stats.drop_count).unwrap_or_default();
if mvcc {
// TODO: index not supported yet for mvcc
remaining_create_index = 0;
}
Remaining {
select: remaining_select,
insert: remaining_insert,
@@ -1140,14 +1157,14 @@ pub(crate) fn remaining(
fn property_insert_values_select<R: rand::Rng>(
rng: &mut R,
env: &SimulatorEnv,
remaining: &Remaining,
ctx: &impl GenerationContext,
) -> Property {
// Get a random table
let table = pick(&env.tables, rng);
let table = pick(ctx.tables(), rng);
// Generate rows to insert
let rows = (0..rng.random_range(1..=5))
.map(|_| Vec::<SimValue>::arbitrary_from(rng, env, table))
.map(|_| Vec::<SimValue>::arbitrary_from(rng, ctx, table))
.collect::<Vec<_>>();
// Pick a random row to select
@@ -1176,12 +1193,14 @@ fn property_insert_values_select<R: rand::Rng>(
// - [x] The inserted row will not be updated.
// - [ ] The table `t` will not be renamed, dropped, or altered. (todo: add this constraint once ALTER or DROP is implemented)
if let Some(ref interactive) = interactive {
queries.push(Query::Begin(Begin {
immediate: interactive.start_with_immediate,
queries.push(Query::Begin(if interactive.start_with_immediate {
Begin::Immediate
} else {
Begin::Deferred
}));
}
for _ in 0..rng.random_range(0..3) {
let query = Query::arbitrary_from(rng, env, remaining);
let query = Query::arbitrary_from(rng, ctx, remaining);
match &query {
Query::Delete(Delete {
table: t,
@@ -1224,7 +1243,7 @@ fn property_insert_values_select<R: rand::Rng>(
// Select the row
let select_query = Select::simple(
table.name.clone(),
Predicate::arbitrary_from(rng, env, (table, &row)),
Predicate::arbitrary_from(rng, ctx, (table, &row)),
);
Property::InsertValuesSelect {
@@ -1236,9 +1255,12 @@ fn property_insert_values_select<R: rand::Rng>(
}
}
fn property_read_your_updates_back<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Property {
fn property_read_your_updates_back<R: rand::Rng>(
rng: &mut R,
ctx: &impl GenerationContext,
) -> Property {
// e.g. UPDATE t SET a=1, b=2 WHERE c=1;
let update = Update::arbitrary(rng, env);
let update = Update::arbitrary(rng, ctx);
// e.g. SELECT a, b FROM t WHERE c=1;
let select = Select::single(
update.table().to_string(),
@@ -1255,22 +1277,25 @@ fn property_read_your_updates_back<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv
Property::ReadYourUpdatesBack { update, select }
}
fn property_table_has_expected_content<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Property {
fn property_table_has_expected_content<R: rand::Rng>(
rng: &mut R,
ctx: &impl GenerationContext,
) -> Property {
// Get a random table
let table = pick(&env.tables, rng);
let table = pick(ctx.tables(), rng);
Property::TableHasExpectedContent {
table: table.name.clone(),
}
}
fn property_select_limit<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Property {
fn property_select_limit<R: rand::Rng>(rng: &mut R, ctx: &impl GenerationContext) -> Property {
// Get a random table
let table = pick(&env.tables, rng);
let table = pick(ctx.tables(), rng);
// Select the table
let select = Select::single(
table.name.clone(),
vec![ResultColumn::Star],
Predicate::arbitrary_from(rng, env, table),
Predicate::arbitrary_from(rng, ctx, table),
Some(rng.random_range(1..=5)),
Distinctness::All,
);
@@ -1279,11 +1304,11 @@ fn property_select_limit<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Prope
fn property_double_create_failure<R: rand::Rng>(
rng: &mut R,
env: &SimulatorEnv,
remaining: &Remaining,
ctx: &impl GenerationContext,
) -> Property {
// Create the table
let create_query = Create::arbitrary(rng, env);
let create_query = Create::arbitrary(rng, ctx);
let table = &create_query.table;
// Create random queries respecting the constraints
@@ -1292,7 +1317,7 @@ fn property_double_create_failure<R: rand::Rng>(
// - [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, env, remaining);
let query = Query::arbitrary_from(rng, ctx, remaining);
if let Query::Create(Create { table: t }) = &query {
// There will be no errors in the middle interactions.
// - Creating the same table is an error
@@ -1311,13 +1336,13 @@ fn property_double_create_failure<R: rand::Rng>(
fn property_delete_select<R: rand::Rng>(
rng: &mut R,
env: &SimulatorEnv,
remaining: &Remaining,
ctx: &impl GenerationContext,
) -> Property {
// Get a random table
let table = pick(&env.tables, rng);
let table = pick(ctx.tables(), rng);
// Generate a random predicate
let predicate = Predicate::arbitrary_from(rng, env, table);
let predicate = Predicate::arbitrary_from(rng, ctx, table);
// Create random queries respecting the constraints
let mut queries = Vec::new();
@@ -1325,7 +1350,7 @@ fn property_delete_select<R: rand::Rng>(
// - [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, env, remaining);
let query = Query::arbitrary_from(rng, ctx, remaining);
match &query {
Query::Insert(Insert::Values { table: t, values }) => {
// A row that holds for the predicate will not be inserted.
@@ -1369,18 +1394,18 @@ fn property_delete_select<R: rand::Rng>(
fn property_drop_select<R: rand::Rng>(
rng: &mut R,
env: &SimulatorEnv,
remaining: &Remaining,
ctx: &impl GenerationContext,
) -> Property {
// Get a random table
let table = pick(&env.tables, rng);
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, env, remaining);
let query = Query::arbitrary_from(rng, ctx, remaining);
if let Query::Create(Create { table: t }) = &query {
// - The table `t` will not be created
if t.name == table.name {
@@ -1392,7 +1417,7 @@ fn property_drop_select<R: rand::Rng>(
let select = Select::simple(
table.name.clone(),
Predicate::arbitrary_from(rng, env, table),
Predicate::arbitrary_from(rng, ctx, table),
);
Property::DropSelect {
@@ -1402,11 +1427,14 @@ fn property_drop_select<R: rand::Rng>(
}
}
fn property_select_select_optimizer<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Property {
fn property_select_select_optimizer<R: rand::Rng>(
rng: &mut R,
ctx: &impl GenerationContext,
) -> Property {
// Get a random table
let table = pick(&env.tables, rng);
let table = pick(ctx.tables(), rng);
// Generate a random predicate
let predicate = Predicate::arbitrary_from(rng, env, table);
let predicate = Predicate::arbitrary_from(rng, ctx, table);
// Transform into a Binary predicate to force values to be casted to a bool
let expr = ast::Expr::Binary(
Box::new(predicate.0),
@@ -1420,12 +1448,15 @@ fn property_select_select_optimizer<R: rand::Rng>(rng: &mut R, env: &SimulatorEn
}
}
fn property_where_true_false_null<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Property {
fn property_where_true_false_null<R: rand::Rng>(
rng: &mut R,
ctx: &impl GenerationContext,
) -> Property {
// Get a random table
let table = pick(&env.tables, rng);
let table = pick(ctx.tables(), rng);
// Generate a random predicate
let p1 = Predicate::arbitrary_from(rng, env, table);
let p2 = Predicate::arbitrary_from(rng, env, table);
let p1 = Predicate::arbitrary_from(rng, ctx, table);
let p2 = Predicate::arbitrary_from(rng, ctx, table);
// Create the select query
let select = Select::simple(table.name.clone(), p1);
@@ -1438,13 +1469,13 @@ fn property_where_true_false_null<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv)
fn property_union_all_preserves_cardinality<R: rand::Rng>(
rng: &mut R,
env: &SimulatorEnv,
ctx: &impl GenerationContext,
) -> Property {
// Get a random table
let table = pick(&env.tables, rng);
let table = pick(ctx.tables(), rng);
// Generate a random predicate
let p1 = Predicate::arbitrary_from(rng, env, table);
let p2 = Predicate::arbitrary_from(rng, env, table);
let p1 = Predicate::arbitrary_from(rng, ctx, table);
let p2 = Predicate::arbitrary_from(rng, ctx, table);
// Create the select query
let select = Select::single(
@@ -1463,34 +1494,39 @@ fn property_union_all_preserves_cardinality<R: rand::Rng>(
fn property_fsync_no_wait<R: rand::Rng>(
rng: &mut R,
env: &SimulatorEnv,
remaining: &Remaining,
ctx: &impl GenerationContext,
) -> Property {
Property::FsyncNoWait {
query: Query::arbitrary_from(rng, env, remaining),
tables: env.tables.iter().map(|t| t.name.clone()).collect(),
query: Query::arbitrary_from(rng, ctx, remaining),
tables: ctx.tables().iter().map(|t| t.name.clone()).collect(),
}
}
fn property_faulty_query<R: rand::Rng>(
rng: &mut R,
env: &SimulatorEnv,
remaining: &Remaining,
ctx: &impl GenerationContext,
) -> Property {
Property::FaultyQuery {
query: Query::arbitrary_from(rng, env, remaining),
tables: env.tables.iter().map(|t| t.name.clone()).collect(),
query: Query::arbitrary_from(rng, ctx, remaining),
tables: ctx.tables().iter().map(|t| t.name.clone()).collect(),
}
}
impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property {
fn arbitrary_from<R: rand::Rng, C: GenerationContext>(
rng: &mut R,
context: &C,
conn_ctx: &C,
(env, stats): (&SimulatorEnv, &InteractionStats),
) -> Self {
let opts = context.opts();
let remaining_ = remaining(env.opts.max_interactions, &env.profile.query, stats);
let opts = conn_ctx.opts();
let remaining_ = remaining(
env.opts.max_interactions,
&env.profile.query,
stats,
env.profile.experimental_mvcc,
);
frequency(
vec![
@@ -1500,15 +1536,17 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property {
} else {
0
},
Box::new(|rng: &mut R| property_insert_values_select(rng, env, &remaining_)),
Box::new(|rng: &mut R| {
property_insert_values_select(rng, &remaining_, conn_ctx)
}),
),
(
remaining_.select,
Box::new(|rng: &mut R| property_table_has_expected_content(rng, env)),
Box::new(|rng: &mut R| property_table_has_expected_content(rng, conn_ctx)),
),
(
u32::min(remaining_.select, remaining_.insert),
Box::new(|rng: &mut R| property_read_your_updates_back(rng, env)),
Box::new(|rng: &mut R| property_read_your_updates_back(rng, conn_ctx)),
),
(
if !env.opts.disable_double_create_failure {
@@ -1516,7 +1554,9 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property {
} else {
0
},
Box::new(|rng: &mut R| property_double_create_failure(rng, env, &remaining_)),
Box::new(|rng: &mut R| {
property_double_create_failure(rng, &remaining_, conn_ctx)
}),
),
(
if !env.opts.disable_select_limit {
@@ -1524,7 +1564,7 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property {
} else {
0
},
Box::new(|rng: &mut R| property_select_limit(rng, env)),
Box::new(|rng: &mut R| property_select_limit(rng, conn_ctx)),
),
(
if !env.opts.disable_delete_select {
@@ -1532,7 +1572,7 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property {
} else {
0
},
Box::new(|rng: &mut R| property_delete_select(rng, env, &remaining_)),
Box::new(|rng: &mut R| property_delete_select(rng, &remaining_, conn_ctx)),
),
(
if !env.opts.disable_drop_select {
@@ -1541,7 +1581,7 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property {
} else {
0
},
Box::new(|rng: &mut R| property_drop_select(rng, env, &remaining_)),
Box::new(|rng: &mut R| property_drop_select(rng, &remaining_, conn_ctx)),
),
(
if !env.opts.disable_select_optimizer {
@@ -1549,7 +1589,7 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property {
} else {
0
},
Box::new(|rng: &mut R| property_select_select_optimizer(rng, env)),
Box::new(|rng: &mut R| property_select_select_optimizer(rng, conn_ctx)),
),
(
if opts.indexes && !env.opts.disable_where_true_false_null {
@@ -1557,7 +1597,7 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property {
} else {
0
},
Box::new(|rng: &mut R| property_where_true_false_null(rng, env)),
Box::new(|rng: &mut R| property_where_true_false_null(rng, conn_ctx)),
),
(
if opts.indexes && !env.opts.disable_union_all_preserves_cardinality {
@@ -1565,7 +1605,7 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property {
} else {
0
},
Box::new(|rng: &mut R| property_union_all_preserves_cardinality(rng, env)),
Box::new(|rng: &mut R| property_union_all_preserves_cardinality(rng, conn_ctx)),
),
(
if env.profile.io.enable && !env.opts.disable_fsync_no_wait {
@@ -1573,7 +1613,7 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property {
} else {
0
},
Box::new(|rng: &mut R| property_fsync_no_wait(rng, env, &remaining_)),
Box::new(|rng: &mut R| property_fsync_no_wait(rng, &remaining_, conn_ctx)),
),
(
if env.profile.io.enable
@@ -1584,7 +1624,7 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property {
} else {
0
},
Box::new(|rng: &mut R| property_faulty_query(rng, env, &remaining_)),
Box::new(|rng: &mut R| property_faulty_query(rng, &remaining_, conn_ctx)),
),
],
rng,

View File

@@ -330,7 +330,7 @@ fn run_simulator(
tracing::trace!(
"adding bug to bugbase, seed: {}, plan: {}, error: {}",
env.opts.seed,
plan.plan.len(),
plan.len(),
error
);
bugbase
@@ -361,8 +361,8 @@ fn run_simulator(
tracing::info!(
"shrinking succeeded, reduced the plan from {} to {}",
plan.plan.len(),
final_plan.plan.len()
plan.len(),
final_plan.len()
);
// Save the shrunk database
if let Some(bugbase) = bugbase.as_deref_mut() {

View File

@@ -15,7 +15,7 @@ use sql_generation::model::{
};
use turso_parser::ast::Distinctness;
use crate::{generation::Shadow, runner::env::SimulatorTables};
use crate::{generation::Shadow, runner::env::ShadowTablesMut};
// This type represents the potential queries on the database.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -83,7 +83,7 @@ impl Display for Query {
impl Shadow for Query {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
fn shadow(&self, env: &mut ShadowTablesMut) -> Self::Result {
match self {
Query::Create(create) => create.shadow(env),
Query::Insert(insert) => insert.shadow(env),
@@ -102,7 +102,7 @@ impl Shadow for Query {
impl Shadow for Create {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
fn shadow(&self, tables: &mut ShadowTablesMut) -> Self::Result {
if !tables.iter().any(|t| t.name == self.table.name) {
tables.push(self.table.clone());
Ok(vec![])
@@ -117,9 +117,8 @@ impl Shadow for Create {
impl Shadow for CreateIndex {
type Result = Vec<Vec<SimValue>>;
fn shadow(&self, env: &mut SimulatorTables) -> Vec<Vec<SimValue>> {
env.tables
.iter_mut()
fn shadow(&self, env: &mut ShadowTablesMut) -> Vec<Vec<SimValue>> {
env.iter_mut()
.find(|t| t.name == self.table_name)
.unwrap()
.indexes
@@ -131,8 +130,8 @@ impl Shadow for CreateIndex {
impl Shadow for Delete {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
let table = tables.tables.iter_mut().find(|t| t.name == self.table);
fn shadow(&self, tables: &mut ShadowTablesMut) -> Self::Result {
let table = tables.iter_mut().find(|t| t.name == self.table);
if let Some(table) = table {
// If the table exists, we can delete from it
@@ -153,7 +152,7 @@ impl Shadow for Delete {
impl Shadow for Drop {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
fn shadow(&self, tables: &mut ShadowTablesMut) -> Self::Result {
if !tables.iter().any(|t| t.name == self.table) {
// If the table does not exist, we return an error
return Err(anyhow::anyhow!(
@@ -162,7 +161,7 @@ impl Shadow for Drop {
));
}
tables.tables.retain(|t| t.name != self.table);
tables.retain(|t| t.name != self.table);
Ok(vec![])
}
@@ -171,10 +170,10 @@ impl Shadow for Drop {
impl Shadow for Insert {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
fn shadow(&self, tables: &mut ShadowTablesMut) -> Self::Result {
match self {
Insert::Values { table, values } => {
if let Some(t) = tables.tables.iter_mut().find(|t| &t.name == table) {
if let Some(t) = tables.iter_mut().find(|t| &t.name == table) {
t.rows.extend(values.clone());
} else {
return Err(anyhow::anyhow!(
@@ -185,7 +184,7 @@ impl Shadow for Insert {
}
Insert::Select { table, select } => {
let rows = select.shadow(tables)?;
if let Some(t) = tables.tables.iter_mut().find(|t| &t.name == table) {
if let Some(t) = tables.iter_mut().find(|t| &t.name == table) {
t.rows.extend(rows);
} else {
return Err(anyhow::anyhow!(
@@ -202,9 +201,7 @@ impl Shadow for Insert {
impl Shadow for FromClause {
type Result = anyhow::Result<JoinTable>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
let tables = &mut env.tables;
fn shadow(&self, tables: &mut ShadowTablesMut) -> Self::Result {
let first_table = tables
.iter()
.find(|t| t.name == self.table)
@@ -259,7 +256,7 @@ impl Shadow for FromClause {
impl Shadow for SelectInner {
type Result = anyhow::Result<JoinTable>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
fn shadow(&self, env: &mut ShadowTablesMut) -> Self::Result {
if let Some(from) = &self.from {
let mut join_table = from.shadow(env)?;
let col_count = join_table.columns().count();
@@ -327,7 +324,7 @@ impl Shadow for SelectInner {
impl Shadow for Select {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
fn shadow(&self, env: &mut ShadowTablesMut) -> Self::Result {
let first_result = self.body.select.shadow(env)?;
let mut rows = first_result.rows;
@@ -357,26 +354,26 @@ impl Shadow for Select {
impl Shadow for Begin {
type Result = Vec<Vec<SimValue>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
tables.snapshot = Some(tables.tables.clone());
fn shadow(&self, tables: &mut ShadowTablesMut) -> Self::Result {
// FIXME: currently the snapshot is taken eagerly
// this is wrong for Deffered transactions
tables.create_snapshot();
vec![]
}
}
impl Shadow for Commit {
type Result = Vec<Vec<SimValue>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
tables.snapshot = None;
fn shadow(&self, tables: &mut ShadowTablesMut) -> Self::Result {
tables.apply_snapshot();
vec![]
}
}
impl Shadow for Rollback {
type Result = Vec<Vec<SimValue>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
if let Some(tables_) = tables.snapshot.take() {
tables.tables = tables_;
}
fn shadow(&self, tables: &mut ShadowTablesMut) -> Self::Result {
tables.delete_snapshot();
vec![]
}
}
@@ -384,8 +381,8 @@ impl Shadow for Rollback {
impl Shadow for Update {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
let table = tables.tables.iter_mut().find(|t| t.name == self.table);
fn shadow(&self, tables: &mut ShadowTablesMut) -> Self::Result {
let table = tables.iter_mut().find(|t| t.name == self.table);
let table = if let Some(table) = table {
table

View File

@@ -59,8 +59,8 @@ pub(crate) fn execute_interactions(
let mut env = env.lock().unwrap();
let mut rusqlite_env = rusqlite_env.lock().unwrap();
env.tables.clear();
rusqlite_env.tables.clear();
env.clear_tables();
rusqlite_env.clear_tables();
let now = std::time::Instant::now();

View File

@@ -89,8 +89,8 @@ pub(crate) fn execute_plans(
let mut env = env.lock().unwrap();
let mut doublecheck_env = doublecheck_env.lock().unwrap();
env.tables.clear();
doublecheck_env.tables.clear();
env.clear_tables();
doublecheck_env.clear_tables();
let now = std::time::Instant::now();

View File

@@ -1,6 +1,6 @@
use std::fmt::Display;
use std::mem;
use std::ops::Deref;
use std::ops::{Deref, DerefMut};
use std::panic::UnwindSafe;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -8,6 +8,7 @@ use std::sync::Arc;
use garde::Validate;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use sql_generation::generation::GenerationContext;
use sql_generation::model::table::Table;
use turso_core::Database;
@@ -31,6 +32,79 @@ pub(crate) enum SimulationPhase {
Shrink,
}
#[derive(Debug)]
pub struct ShadowTables<'a> {
commited_tables: &'a Vec<Table>,
transaction_tables: Option<&'a Vec<Table>>,
}
#[derive(Debug)]
pub struct ShadowTablesMut<'a> {
commited_tables: &'a mut Vec<Table>,
transaction_tables: &'a mut Option<Vec<Table>>,
}
impl<'a> ShadowTables<'a> {
fn tables(&self) -> &'a Vec<Table> {
self.transaction_tables.map_or(self.commited_tables, |v| v)
}
}
impl<'a> Deref for ShadowTables<'a> {
type Target = Vec<Table>;
fn deref(&self) -> &Self::Target {
self.tables()
}
}
impl<'a, 'b> ShadowTablesMut<'a>
where
'a: 'b,
{
fn tables(&'a self) -> &'a Vec<Table> {
self.transaction_tables
.as_ref()
.unwrap_or(self.commited_tables)
}
fn tables_mut(&'b mut self) -> &'b mut Vec<Table> {
self.transaction_tables
.as_mut()
.unwrap_or(self.commited_tables)
}
pub fn create_snapshot(&mut self) {
*self.transaction_tables = Some(self.commited_tables.clone());
}
pub fn apply_snapshot(&mut self) {
// TODO: as we do not have concurrent tranasactions yet in the simulator
// there is no conflict we are ignoring conflict problems right now
if let Some(transation_tables) = self.transaction_tables.take() {
*self.commited_tables = transation_tables
}
}
pub fn delete_snapshot(&mut self) {
*self.transaction_tables = None;
}
}
impl<'a> Deref for ShadowTablesMut<'a> {
type Target = Vec<Table>;
fn deref(&self) -> &Self::Target {
self.tables()
}
}
impl<'a> DerefMut for ShadowTablesMut<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.tables_mut()
}
}
#[derive(Debug, Clone)]
pub(crate) struct SimulatorTables {
pub(crate) tables: Vec<Table>,
@@ -71,8 +145,12 @@ pub(crate) struct SimulatorEnv {
pub(crate) paths: Paths,
pub(crate) type_: SimulationType,
pub(crate) phase: SimulationPhase,
pub(crate) tables: SimulatorTables,
pub memory_io: bool,
/// If connection state is None, means we are not in a transaction
pub connection_tables: Vec<Option<Vec<Table>>>,
// Table data that is committed into the database or wal
pub committed_tables: Vec<Table>,
}
impl UnwindSafe for SimulatorEnv {}
@@ -81,10 +159,6 @@ impl SimulatorEnv {
pub(crate) fn clone_without_connections(&self) -> Self {
SimulatorEnv {
opts: self.opts.clone(),
tables: self.tables.clone(),
connections: (0..self.connections.len())
.map(|_| SimConnection::Disconnected)
.collect(),
io: self.io.clone(),
db: self.db.clone(),
rng: self.rng.clone(),
@@ -93,11 +167,17 @@ impl SimulatorEnv {
phase: self.phase,
memory_io: self.memory_io,
profile: self.profile.clone(),
connections: (0..self.connections.len())
.map(|_| SimConnection::Disconnected)
.collect(),
// TODO: not sure if connection_tables should be recreated instead
connection_tables: self.connection_tables.clone(),
committed_tables: self.committed_tables.clone(),
}
}
pub(crate) fn clear(&mut self) {
self.tables.clear();
self.clear_tables();
self.connections.iter_mut().for_each(|c| c.disconnect());
self.rng = ChaCha8Rng::seed_from_u64(self.opts.seed);
@@ -284,7 +364,6 @@ impl SimulatorEnv {
SimulatorEnv {
opts,
tables: SimulatorTables::new(),
connections,
paths,
rng,
@@ -294,6 +373,8 @@ impl SimulatorEnv {
phase: SimulationPhase::Test,
memory_io: cli_opts.memory_io,
profile: profile.clone(),
committed_tables: Vec::new(),
connection_tables: vec![None; profile.max_connections],
}
}
@@ -327,6 +408,55 @@ impl SimulatorEnv {
}
};
}
/// Clears the commited tables and the connection tables
pub fn clear_tables(&mut self) {
self.committed_tables.clear();
self.connection_tables.iter_mut().for_each(|t| {
if let Some(t) = t {
t.clear();
}
});
}
// TODO: does not yet create the appropriate context to avoid WriteWriteConflitcs
pub fn connection_context(&self, conn_index: usize) -> impl GenerationContext {
struct ConnectionGenContext<'a> {
tables: &'a Vec<sql_generation::model::table::Table>,
opts: &'a sql_generation::generation::Opts,
}
impl<'a> GenerationContext for ConnectionGenContext<'a> {
fn tables(&self) -> &Vec<sql_generation::model::table::Table> {
self.tables
}
fn opts(&self) -> &sql_generation::generation::Opts {
self.opts
}
}
let tables = self.get_conn_tables(conn_index).tables();
ConnectionGenContext {
opts: &self.profile.query.gen_opts,
tables,
}
}
pub fn get_conn_tables<'a>(&'a self, conn_index: usize) -> ShadowTables<'a> {
ShadowTables {
transaction_tables: self.connection_tables.get(conn_index).unwrap().as_ref(),
commited_tables: &self.committed_tables,
}
}
pub fn get_conn_tables_mut<'a>(&'a mut self, conn_index: usize) -> ShadowTablesMut<'a> {
ShadowTablesMut {
transaction_tables: self.connection_tables.get_mut(conn_index).unwrap(),
commited_tables: &mut self.committed_tables,
}
}
}
pub trait ConnectionTrait

View File

@@ -65,7 +65,7 @@ pub(crate) fn execute_interactions(
env.clear_poison();
let mut env = env.lock().unwrap();
env.tables.clear();
env.clear_tables();
for _tick in 0..env.opts.ticks {
tracing::trace!("Executing tick {}", _tick);
@@ -186,7 +186,10 @@ pub fn execute_interaction_turso(
tracing::error!(?results);
}
stack.push(results);
limbo_integrity_check(conn)?;
// TODO: skip integrity check with mvcc
if !env.profile.experimental_mvcc {
limbo_integrity_check(conn)?;
}
}
InteractionType::FsyncQuery(query) => {
let results = interaction.execute_fsync_query(conn.clone(), env);
@@ -227,10 +230,13 @@ pub fn execute_interaction_turso(
stack.push(results);
// Reset fault injection
env.io.inject_fault(false);
limbo_integrity_check(&conn)?;
// TODO: skip integrity check with mvcc
if !env.profile.experimental_mvcc {
limbo_integrity_check(&conn)?;
}
}
}
let _ = interaction.shadow(&mut env.tables);
let _ = interaction.shadow(&mut env.get_conn_tables_mut(interaction.connection_index));
Ok(ExecutionContinuation::NextInteraction)
}
@@ -323,7 +329,7 @@ fn execute_interaction_rusqlite(
}
}
let _ = interaction.shadow(&mut env.tables);
let _ = interaction.shadow(&mut env.get_conn_tables_mut(interaction.connection_index));
Ok(ExecutionContinuation::NextInteraction)
}

View File

@@ -33,7 +33,7 @@ impl InteractionPlan {
break;
}
match &all_interactions[idx].1.interaction {
InteractionType::Query(query) => {
InteractionType::Query(query) | InteractionType::FaultyQuery(query) => {
depending_tables = query.dependencies();
break;
}
@@ -54,77 +54,126 @@ impl InteractionPlan {
}
}
let before = self.plan.len();
let before = self.len();
// Remove all properties after the failing one
plan.plan.truncate(secondary_interactions_index + 1);
let mut idx = 0;
// Remove all properties that do not use the failing tables
plan.plan.retain_mut(|interactions| {
let retain = if idx == secondary_interactions_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
.uses()
.iter()
.any(|t| depending_tables.contains(t));
if has_table {
// Remove the extensional parts of the properties
if let InteractionsType::Property(p) = &mut interactions.interactions {
match p {
Property::InsertValuesSelect { queries, .. }
| Property::DoubleCreateFailure { queries, .. }
| Property::DeleteSelect { queries, .. }
| Property::DropSelect { queries, .. } => {
queries.clear();
}
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::SelectLimit { .. }
| Property::SelectSelectOptimizer { .. }
| Property::WhereTrueFalseNull { .. }
| Property::UNIONAllPreservesCardinality { .. }
| Property::ReadYourUpdatesBack { .. }
| Property::TableHasExpectedContent { .. } => {}
}
// means we errored in some fault on transaction statement so just maintain the statements from before the failing one
if !depending_tables.is_empty() {
let mut idx = 0;
// Remove all properties that do not use the failing tables
plan.plan.retain_mut(|interactions| {
let retain = if idx == secondary_interactions_index {
if let InteractionsType::Property(
Property::FsyncNoWait { tables, .. } | Property::FaultyQuery { tables, .. },
) = &mut interactions.interactions
{
tables.retain(|table| depending_tables.contains(table));
}
// Check again after query clear if the interactions still uses the failing table
has_table = interactions
true
} else if matches!(
interactions.interactions,
InteractionsType::Query(Query::Begin(..))
| InteractionsType::Query(Query::Commit(..))
| InteractionsType::Query(Query::Rollback(..))
) {
true
} else {
let mut has_table = interactions
.uses()
.iter()
.any(|t| depending_tables.contains(t));
}
let is_fault = matches!(interactions.interactions, InteractionsType::Fault(..));
is_fault
|| (has_table
&& !matches!(
interactions.interactions,
InteractionsType::Query(Query::Select(_))
| InteractionsType::Property(Property::SelectLimit { .. })
| InteractionsType::Property(
Property::SelectSelectOptimizer { .. }
)
))
};
idx += 1;
retain
});
let after = plan.plan.len();
if has_table {
// Remove the extensional parts of the properties
if let InteractionsType::Property(p) = &mut interactions.interactions {
match p {
Property::InsertValuesSelect { queries, .. }
| Property::DoubleCreateFailure { queries, .. }
| Property::DeleteSelect { queries, .. }
| Property::DropSelect { queries, .. } => {
queries.clear();
}
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::SelectLimit { .. }
| Property::SelectSelectOptimizer { .. }
| Property::WhereTrueFalseNull { .. }
| Property::UNIONAllPreservesCardinality { .. }
| Property::ReadYourUpdatesBack { .. }
| Property::TableHasExpectedContent { .. } => {}
}
}
// Check again after query clear if the interactions still uses the failing table
has_table = interactions
.uses()
.iter()
.any(|t| depending_tables.contains(t));
}
let is_fault = matches!(interactions.interactions, InteractionsType::Fault(..));
is_fault
|| (has_table
&& !matches!(
interactions.interactions,
InteractionsType::Query(Query::Select(_))
| InteractionsType::Property(Property::SelectLimit { .. })
| InteractionsType::Property(
Property::SelectSelectOptimizer { .. }
)
))
};
idx += 1;
retain
});
// Comprise of idxs of Begin interactions
let mut begin_idx = Vec::new();
// Comprise of idxs of the intereactions Commit and Rollback
let mut end_tx_idx = Vec::new();
for (idx, interactions) in plan.plan.iter().enumerate() {
match &interactions.interactions {
InteractionsType::Query(Query::Begin(..)) => {
begin_idx.push(idx);
}
InteractionsType::Query(Query::Commit(..))
| InteractionsType::Query(Query::Rollback(..)) => {
let last_begin = begin_idx.last().unwrap() + 1;
if last_begin == idx {
end_tx_idx.push(idx);
}
}
_ => {}
}
}
// remove interactions if its just a Begin Commit/Rollback with no queries in the middle
let mut range_transactions = end_tx_idx.into_iter().peekable();
let mut idx = 0;
plan.plan.retain_mut(|_| {
let mut retain = true;
if let Some(txn_interaction_idx) = range_transactions.peek().copied() {
if txn_interaction_idx == idx {
range_transactions.next();
}
if txn_interaction_idx == idx || txn_interaction_idx.saturating_sub(1) == idx {
retain = false;
}
}
idx += 1;
retain
});
}
let after = plan.len();
tracing::info!(
"Shrinking interaction plan from {} to {} properties",
@@ -184,7 +233,7 @@ impl InteractionPlan {
}
}
let before = self.plan.len();
let before = self.len();
plan.plan.truncate(secondary_interactions_index + 1);
@@ -196,8 +245,8 @@ impl InteractionPlan {
| Property::DoubleCreateFailure { queries, .. }
| Property::DeleteSelect { queries, .. }
| Property::DropSelect { queries, .. } => {
let mut temp_plan = InteractionPlan {
plan: queries
let mut temp_plan = InteractionPlan::new_with(
queries
.iter()
.map(|q| {
Interactions::new(
@@ -206,7 +255,8 @@ impl InteractionPlan {
)
})
.collect(),
};
self.mvcc,
);
temp_plan = InteractionPlan::iterative_shrink(
temp_plan,
@@ -218,7 +268,6 @@ impl InteractionPlan {
//temp_plan = Self::shrink_queries(temp_plan, failing_execution, result, env);
*queries = temp_plan
.plan
.into_iter()
.filter_map(|i| match i.interactions {
InteractionsType::Query(q) => Some(q),
@@ -247,7 +296,7 @@ impl InteractionPlan {
secondary_interactions_index,
);
let after = plan.plan.len();
let after = plan.len();
tracing::info!(
"Shrinking interaction plan from {} to {} properties",
@@ -266,7 +315,7 @@ impl InteractionPlan {
env: Arc<Mutex<SimulatorEnv>>,
secondary_interaction_index: usize,
) -> InteractionPlan {
for i in (0..plan.plan.len()).rev() {
for i in (0..plan.len()).rev() {
if i == secondary_interaction_index {
continue;
}

View File

@@ -3,8 +3,10 @@ use std::fmt::Display;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Begin {
pub immediate: bool,
pub enum Begin {
Deferred,
Immediate,
Concurrent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -15,7 +17,12 @@ pub struct Rollback;
impl Display for Begin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BEGIN {}", if self.immediate { "IMMEDIATE" } else { "" })
let keyword = match self {
Begin::Deferred => "",
Begin::Immediate => "IMMEDIATE",
Begin::Concurrent => "CONCURRENT",
};
write!(f, "BEGIN {keyword}")
}
}