Merge 'Decouple SQL generation from Simulator crate' from Pedro Muniz

Decouple Sql generation code from simulator code, so that it can
potentially be reused for fuzzing on other crates and to create a
`GenerationContext` trait so that it becomes easier to create
`Simulation Profiles`. Ideally in further PRs, I want to expand the
`GenerationContext` trait so we can guide the generation with context
from the simulation profile.
Depends on #2789 .

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

Closes #2793
This commit is contained in:
Pekka Enberg
2025-08-26 09:41:58 +03:00
committed by GitHub
48 changed files with 1891 additions and 1793 deletions

35
Cargo.lock generated
View File

@@ -119,6 +119,15 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "anarchist-readable-name-generator-lib"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a645c34bad5551ed4b2496536985efdc4373b097c0e57abf2eb14774538278"
dependencies = [
"rand 0.9.2",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -2125,7 +2134,6 @@ dependencies = [
name = "limbo_sim"
version = "0.1.4"
dependencies = [
"anarchist-readable-name-generator-lib",
"anyhow",
"chrono",
"clap",
@@ -2135,17 +2143,18 @@ dependencies = [
"itertools 0.14.0",
"log",
"notify",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand 0.9.2",
"rand_chacha 0.9.0",
"regex",
"regex-syntax 0.8.5",
"rusqlite",
"serde",
"serde_json",
"sql_generation",
"tracing",
"tracing-subscriber",
"turso_core",
"turso_sqlite3_parser",
"turso_parser",
]
[[package]]
@@ -3462,6 +3471,22 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d372029cb5195f9ab4e4b9aef550787dce78b124fcaee8d82519925defcd6f0d"
[[package]]
name = "sql_generation"
version = "0.1.4"
dependencies = [
"anarchist-readable-name-generator-lib 0.2.0",
"anyhow",
"hex",
"itertools 0.14.0",
"rand 0.9.2",
"rand_chacha 0.9.0",
"serde",
"tracing",
"turso_core",
"turso_parser",
]
[[package]]
name = "sqlparser_bench"
version = "0.1.0"
@@ -4159,7 +4184,7 @@ dependencies = [
name = "turso_stress"
version = "0.1.4"
dependencies = [
"anarchist-readable-name-generator-lib",
"anarchist-readable-name-generator-lib 0.1.2",
"antithesis_sdk",
"clap",
"hex",

View File

@@ -29,6 +29,7 @@ members = [
"parser",
"sync/engine",
"sync/javascript",
"sql_generation",
]
exclude = ["perf/latency/limbo"]
@@ -56,6 +57,7 @@ limbo_regexp = { path = "extensions/regexp", version = "0.1.4" }
turso_sqlite3_parser = { path = "vendored/sqlite3-parser", version = "0.1.4" }
limbo_uuid = { path = "extensions/uuid", version = "0.1.4" }
turso_parser = { path = "parser" }
sql_generation = { path = "sql_generation" }
strum = { version = "0.26", features = ["derive"] }
strum_macros = "0.26"
serde = "1.0"
@@ -63,6 +65,9 @@ serde_json = "1.0"
anyhow = "1.0.98"
mimalloc = { version = "0.1.47", default-features = false }
rusqlite = { version = "0.37.0", features = ["bundled"] }
itertools = "0.14.0"
rand = "0.9.2"
tracing = "0.1.41"
[profile.release]
debug = "line-tables-only"

View File

@@ -3,6 +3,8 @@ pub mod fmt;
use strum_macros::{EnumIter, EnumString};
use crate::ast::fmt::ToTokens;
/// `?` or `$` Prepared statement arg placeholder(s)
#[derive(Default)]
pub struct ParameterInfo {
@@ -982,6 +984,41 @@ impl std::fmt::Display for QualifiedName {
}
}
impl QualifiedName {
/// Constructor
pub fn single(name: Name) -> Self {
Self {
db_name: None,
name,
alias: None,
}
}
/// Constructor
pub fn fullname(db_name: Name, name: Name) -> Self {
Self {
db_name: Some(db_name),
name,
alias: None,
}
}
/// Constructor
pub fn xfullname(db_name: Name, name: Name, alias: Name) -> Self {
Self {
db_name: Some(db_name),
name,
alias: Some(alias),
}
}
/// Constructor
pub fn alias(name: Name, alias: Name) -> Self {
Self {
db_name: None,
name,
alias: Some(alias),
}
}
}
/// `ALTER TABLE` body
// https://sqlite.org/lang_altertable.html
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -1153,7 +1190,7 @@ bitflags::bitflags! {
}
/// Sort orders
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SortOrder {
/// `ASC`
@@ -1162,6 +1199,12 @@ pub enum SortOrder {
Desc,
}
impl core::fmt::Display for SortOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_fmt(f)
}
}
/// `NULLS FIRST` or `NULLS LAST`
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

View File

@@ -16,15 +16,14 @@ path = "main.rs"
[dependencies]
turso_core = { path = "../core", features = ["simulator"]}
rand = "0.8.5"
rand_chacha = "0.3.1"
rand = { workspace = true }
rand_chacha = "0.9.0"
log = "0.4.20"
env_logger = "0.10.1"
regex = "1.11.1"
regex-syntax = { version = "0.8.5", default-features = false, features = [
"unicode",
] }
anarchist-readable-name-generator-lib = "=0.1.2"
clap = { version = "4.5", features = ["derive"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
@@ -32,9 +31,10 @@ notify = "8.0.0"
rusqlite.workspace = true
dirs = "6.0.0"
chrono = { version = "0.4.40", features = ["serde"] }
tracing = "0.1.41"
tracing = { workspace = true }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
anyhow.workspace = true
turso_sqlite3_parser = { workspace = true, features = ["serde"]}
hex = "0.4.3"
itertools = "0.14.0"
sql_generation = { workspace = true }
turso_parser = { workspace = true }

View File

@@ -1,65 +1,10 @@
use std::{iter::Sum, ops::SubAssign};
use sql_generation::generation::GenerationContext;
use anarchist_readable_name_generator_lib::readable_name_custom;
use rand::{distributions::uniform::SampleUniform, Rng};
use crate::runner::env::{SimulatorEnv, SimulatorTables};
use crate::runner::env::SimulatorTables;
mod expr;
pub mod plan;
mod predicate;
pub mod property;
pub mod query;
pub mod table;
type ArbitraryFromFunc<'a, R, T> = Box<dyn Fn(&mut R) -> T + 'a>;
type Choice<'a, R, T> = (usize, Box<dyn Fn(&mut R) -> Option<T> + 'a>);
/// Arbitrary trait for generating random values
/// An implementation of arbitrary is assumed to be a uniform sampling of
/// the possible values of the type, with a bias towards smaller values for
/// practicality.
pub trait Arbitrary {
fn arbitrary<R: Rng>(rng: &mut R) -> Self;
}
/// ArbitrarySized trait for generating random values of a specific size
/// An implementation of arbitrary_sized is assumed to be a uniform sampling of
/// the possible values of the type, with a bias towards smaller values for
/// practicality, but with the additional constraint that the generated value
/// must fit in the given size. This is useful for generating values that are
/// constrained by a specific size, such as integers or strings.
pub trait ArbitrarySized {
fn arbitrary_sized<R: Rng>(rng: &mut R, size: usize) -> Self;
}
/// ArbitraryFrom trait for generating random values from a given value
/// ArbitraryFrom allows for constructing relations, where the generated
/// value is dependent on the given value. These relations could be constraints
/// such as generating an integer within an interval, or a value that fits in a table,
/// or a predicate satisfying a given table row.
pub trait ArbitraryFrom<T> {
fn arbitrary_from<R: Rng>(rng: &mut R, t: T) -> Self;
}
/// ArbitrarySizedFrom trait for generating random values from a given value
/// ArbitrarySizedFrom allows for constructing relations, where the generated
/// value is dependent on the given value and a size constraint. These relations
/// could be constraints such as generating an integer within an interval,
/// or a value that fits in a table, or a predicate satisfying a given table row,
/// but with the additional constraint that the generated value must fit in the given size.
/// This is useful for generating values that are constrained by a specific size,
/// such as integers or strings, while still being dependent on the given value.
pub trait ArbitrarySizedFrom<T> {
fn arbitrary_sized_from<R: Rng>(rng: &mut R, t: T, size: usize) -> Self;
}
/// ArbitraryFromMaybe trait for fallibally generating random values from a given value
pub trait ArbitraryFromMaybe<T> {
fn arbitrary_from_maybe<R: Rng>(rng: &mut R, t: T) -> Option<Self>
where
Self: Sized;
}
/// Shadow trait for types that can be "shadowed" in the simulator environment.
/// Shadowing is a process of applying a transformation to the simulator environment
@@ -75,108 +20,14 @@ pub(crate) trait Shadow {
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result;
}
/// Frequency is a helper function for composing different generators with different frequency
/// of occurrences.
/// The type signature for the `N` parameter is a bit complex, but it
/// roughly corresponds to a type that can be summed, compared, subtracted and sampled, which are
/// the operations we require for the implementation.
// todo: switch to a simpler type signature that can accommodate all integer and float types, which
// should be enough for our purposes.
pub(crate) fn frequency<
T,
R: Rng,
N: Sum + PartialOrd + Copy + Default + SampleUniform + SubAssign,
>(
choices: Vec<(N, ArbitraryFromFunc<R, T>)>,
rng: &mut R,
) -> T {
let total = choices.iter().map(|(weight, _)| *weight).sum::<N>();
let mut choice = rng.gen_range(N::default()..total);
for (weight, f) in choices {
if choice < weight {
return f(rng);
}
choice -= weight;
impl GenerationContext for SimulatorEnv {
fn tables(&self) -> &Vec<sql_generation::model::table::Table> {
&self.tables.tables
}
unreachable!()
}
/// one_of is a helper function for composing different generators with equal probability of occurrence.
pub(crate) fn one_of<T, R: Rng>(choices: Vec<ArbitraryFromFunc<R, T>>, rng: &mut R) -> T {
let index = rng.gen_range(0..choices.len());
choices[index](rng)
}
/// backtrack is a helper function for composing different "failable" generators.
/// The function takes a list of functions that return an Option<T>, along with number of retries
/// to make before giving up.
pub(crate) fn backtrack<T, R: Rng>(mut choices: Vec<Choice<R, T>>, rng: &mut R) -> Option<T> {
loop {
// If there are no more choices left, we give up
let choices_ = choices
.iter()
.enumerate()
.filter(|(_, (retries, _))| *retries > 0)
.collect::<Vec<_>>();
if choices_.is_empty() {
tracing::trace!("backtrack: no more choices left");
return None;
}
// Run a one_of on the remaining choices
let (choice_index, choice) = pick(&choices_, rng);
let choice_index = *choice_index;
// If the choice returns None, we decrement the number of retries and try again
let result = choice.1(rng);
if result.is_some() {
return result;
} else {
choices[choice_index].0 -= 1;
fn opts(&self) -> sql_generation::generation::Opts {
sql_generation::generation::Opts {
indexes: self.opts.experimental_indexes,
}
}
}
/// pick is a helper function for uniformly picking a random element from a slice
pub(crate) fn pick<'a, T, R: Rng>(choices: &'a [T], rng: &mut R) -> &'a T {
let index = rng.gen_range(0..choices.len());
&choices[index]
}
/// pick_index is typically used for picking an index from a slice to later refer to the element
/// at that index.
pub(crate) fn pick_index<R: Rng>(choices: usize, rng: &mut R) -> usize {
rng.gen_range(0..choices)
}
/// pick_n_unique is a helper function for uniformly picking N unique elements from a range.
/// The elements themselves are usize, typically representing indices.
pub(crate) fn pick_n_unique<R: Rng>(
range: std::ops::Range<usize>,
n: usize,
rng: &mut R,
) -> Vec<usize> {
use rand::seq::SliceRandom;
let mut items: Vec<usize> = range.collect();
items.shuffle(rng);
items.into_iter().take(n).collect()
}
/// gen_random_text uses `anarchist_readable_name_generator_lib` to generate random
/// readable names for tables, columns, text values etc.
pub(crate) fn gen_random_text<T: Rng>(rng: &mut T) -> String {
let big_text = rng.gen_ratio(1, 1000);
if big_text {
// let max_size: u64 = 2 * 1024 * 1024 * 1024;
let max_size: u64 = 2 * 1024;
let size = rng.gen_range(1024..max_size);
let mut name = String::with_capacity(size as usize);
for i in 0..size {
name.push(((i % 26) as u8 + b'A') as char);
}
name
} else {
let name = readable_name_custom("_", rng);
name.replace("-", "_")
}
}

View File

@@ -8,14 +8,18 @@ use std::{
use serde::{Deserialize, Serialize};
use sql_generation::{
generation::{frequency, query::SelectFree, Arbitrary, ArbitraryFrom},
model::{
query::{update::Update, Create, CreateIndex, Delete, Drop, Insert, Select},
table::SimValue,
},
};
use turso_core::{Connection, Result, StepResult};
use crate::{
generation::{query::SelectFree, Shadow},
model::{
query::{update::Update, Create, CreateIndex, Delete, Drop, Insert, Query, Select},
table::SimValue,
},
generation::Shadow,
model::Query,
runner::{
env::{SimConnection, SimulationType, SimulatorTables},
io::SimulatorIO,
@@ -23,8 +27,6 @@ use crate::{
SimulatorEnv,
};
use crate::generation::{frequency, Arbitrary, ArbitraryFrom};
use super::property::{remaining, Property};
pub(crate) type ResultSet = Result<Vec<Vec<SimValue>>>;
@@ -661,7 +663,7 @@ impl Interaction {
.iter()
.any(|file| file.sync_completion.borrow().is_some())
};
let inject_fault = env.rng.gen_bool(current_prob);
let inject_fault = env.rng.random_bool(current_prob);
// TODO: avoid for now injecting faults when syncing
if inject_fault && !syncing {
env.io.inject_fault(true);
@@ -811,7 +813,7 @@ fn random_fault<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Interactions {
} else {
vec![Fault::Disconnect, Fault::ReopenDatabase]
};
let fault = faults[rng.gen_range(0..faults.len())].clone();
let fault = faults[rng.random_range(0..faults.len())].clone();
Interactions::Fault(fault)
}

View File

@@ -1,30 +1,23 @@
use serde::{Deserialize, Serialize};
use turso_core::{types, LimboError};
use turso_sqlite3_parser::ast::{self};
use crate::{
generation::Shadow as _,
use sql_generation::{
generation::{frequency, pick, pick_index, ArbitraryFrom},
model::{
query::{
predicate::Predicate,
select::{
CompoundOperator, CompoundSelect, Distinctness, ResultColumn, SelectBody,
SelectInner,
},
select::{CompoundOperator, CompoundSelect, ResultColumn, SelectBody, SelectInner},
transaction::{Begin, Commit, Rollback},
update::Update,
Create, Delete, Drop, Insert, Query, Select,
Create, Delete, Drop, Insert, Select,
},
table::SimValue,
},
runner::env::SimulatorEnv,
};
use turso_core::{types, LimboError};
use turso_parser::ast::{self, Distinctness};
use super::{
frequency, pick, pick_index,
plan::{Assertion, Interaction, InteractionStats, ResultSet},
ArbitraryFrom,
};
use crate::{generation::Shadow as _, model::Query, runner::env::SimulatorEnv};
use super::plan::{Assertion, Interaction, InteractionStats, ResultSet};
/// Properties are representations of executable specifications
/// about the database behavior.
@@ -1073,7 +1066,7 @@ fn property_insert_values_select<R: rand::Rng>(
// Get a random table
let table = pick(&env.tables, rng);
// Generate rows to insert
let rows = (0..rng.gen_range(1..=5))
let rows = (0..rng.random_range(1..=5))
.map(|_| Vec::<SimValue>::arbitrary_from(rng, table))
.collect::<Vec<_>>();
@@ -1088,10 +1081,10 @@ fn property_insert_values_select<R: rand::Rng>(
};
// Choose if we want queries to be executed in an interactive transaction
let interactive = if rng.gen_bool(0.5) {
let interactive = if rng.random_bool(0.5) {
Some(InteractiveQueryInfo {
start_with_immediate: rng.gen_bool(0.5),
end_with_commit: rng.gen_bool(0.5),
start_with_immediate: rng.random_bool(0.5),
end_with_commit: rng.random_bool(0.5),
})
} else {
None
@@ -1107,7 +1100,7 @@ fn property_insert_values_select<R: rand::Rng>(
immediate: interactive.start_with_immediate,
}));
}
for _ in 0..rng.gen_range(0..3) {
for _ in 0..rng.random_range(0..3) {
let query = Query::arbitrary_from(rng, (env, remaining));
match &query {
Query::Delete(Delete {
@@ -1198,7 +1191,7 @@ fn property_select_limit<R: rand::Rng>(rng: &mut R, env: &SimulatorEnv) -> Prope
table.name.clone(),
vec![ResultColumn::Star],
Predicate::arbitrary_from(rng, table),
Some(rng.gen_range(1..=5)),
Some(rng.random_range(1..=5)),
Distinctness::All,
);
Property::SelectLimit { select }
@@ -1221,7 +1214,7 @@ fn property_double_create_failure<R: rand::Rng>(
// 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.gen_range(0..3) {
for _ in 0..rng.random_range(0..3) {
let query = Query::arbitrary_from(rng, (env, remaining));
if let Query::Create(Create { table: t }) = &query {
// There will be no errors in the middle interactions.
@@ -1254,7 +1247,7 @@ fn property_delete_select<R: rand::Rng>(
// - [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.gen_range(0..3) {
for _ in 0..rng.random_range(0..3) {
let query = Query::arbitrary_from(rng, (env, remaining));
match &query {
Query::Insert(Insert::Values { table: t, values }) => {
@@ -1309,7 +1302,7 @@ fn property_drop_select<R: rand::Rng>(
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.gen_range(0..3) {
for _ in 0..rng.random_range(0..3) {
let query = Query::arbitrary_from(rng, (env, remaining));
if let Query::Create(Create { table: t }) = &query {
// - The table `t` will not be created

View File

@@ -1,330 +1,11 @@
use crate::generation::{Arbitrary, ArbitraryFrom, ArbitrarySizedFrom, Shadow};
use crate::model::query::predicate::Predicate;
use crate::model::query::select::{
CompoundOperator, CompoundSelect, Distinctness, FromClause, OrderBy, ResultColumn, SelectBody,
SelectInner,
};
use crate::model::query::update::Update;
use crate::model::query::{Create, Delete, Drop, Insert, Query, Select};
use crate::model::table::{JoinTable, JoinType, JoinedTable, SimValue, Table, TableContext};
use crate::SimulatorEnv;
use itertools::Itertools;
use crate::{model::Query, SimulatorEnv};
use rand::Rng;
use turso_sqlite3_parser::ast::{Expr, SortOrder};
use sql_generation::{
generation::{frequency, Arbitrary, ArbitraryFrom},
model::query::{update::Update, Create, Delete, Insert, Select},
};
use super::property::Remaining;
use super::{backtrack, frequency, pick};
impl Arbitrary for Create {
fn arbitrary<R: Rng>(rng: &mut R) -> Self {
Create {
table: Table::arbitrary(rng),
}
}
}
impl ArbitraryFrom<&Vec<Table>> for FromClause {
fn arbitrary_from<R: Rng>(rng: &mut R, tables: &Vec<Table>) -> Self {
let num_joins = match rng.gen_range(0..=100) {
0..=90 => 0,
91..=97 => 1,
98..=100 => 2,
_ => unreachable!(),
};
let mut tables = tables.clone();
let mut table = pick(&tables, rng).clone();
tables.retain(|t| t.name != table.name);
let name = table.name.clone();
let mut table_context = JoinTable {
tables: Vec::new(),
rows: Vec::new(),
};
let joins: Vec<_> = (0..num_joins)
.filter_map(|_| {
if tables.is_empty() {
return None;
}
let join_table = pick(&tables, rng).clone();
let joined_table_name = join_table.name.clone();
tables.retain(|t| t.name != join_table.name);
table_context.rows = table_context
.rows
.iter()
.cartesian_product(join_table.rows.iter())
.map(|(t_row, j_row)| {
let mut row = t_row.clone();
row.extend(j_row.clone());
row
})
.collect();
// TODO: inneficient. use a Deque to push_front?
table_context.tables.insert(0, join_table);
for row in &mut table.rows {
assert_eq!(
row.len(),
table.columns.len(),
"Row length does not match column length after join"
);
}
let predicate = Predicate::arbitrary_from(rng, &table);
Some(JoinedTable {
table: joined_table_name,
join_type: JoinType::Inner,
on: predicate,
})
})
.collect();
FromClause { table: name, joins }
}
}
impl ArbitraryFrom<&SimulatorEnv> for SelectInner {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
let from = FromClause::arbitrary_from(rng, &env.tables);
let mut tables = env.tables.clone();
// todo: this is a temporary hack because env is not separated from the tables
let join_table = from
.shadow(&mut tables)
.expect("Failed to shadow FromClause");
let cuml_col_count = join_table.columns().count();
let order_by = 'order_by: {
if rng.gen_bool(0.3) {
let order_by_table_candidates = from
.joins
.iter()
.map(|j| j.table.clone())
.chain(std::iter::once(from.table.clone()))
.collect::<Vec<_>>();
let order_by_col_count =
(rng.gen::<f64>() * rng.gen::<f64>() * (cuml_col_count as f64)) as usize; // skew towards 0
if order_by_col_count == 0 {
break 'order_by None;
}
let mut col_names = std::collections::HashSet::new();
let mut order_by_cols = Vec::new();
while order_by_cols.len() < order_by_col_count {
let table = pick(&order_by_table_candidates, rng);
let table = tables.iter().find(|t| t.name == *table).unwrap();
let col = pick(&table.columns, rng);
let col_name = format!("{}.{}", table.name, col.name);
if col_names.insert(col_name.clone()) {
order_by_cols.push((
col_name,
if rng.gen_bool(0.5) {
SortOrder::Asc
} else {
SortOrder::Desc
},
));
}
}
Some(OrderBy {
columns: order_by_cols,
})
} else {
None
}
};
SelectInner {
distinctness: if env.opts.experimental_indexes {
Distinctness::arbitrary(rng)
} else {
Distinctness::All
},
columns: vec![ResultColumn::Star],
from: Some(from),
where_clause: Predicate::arbitrary_from(rng, &join_table),
order_by,
}
}
}
impl ArbitrarySizedFrom<&SimulatorEnv> for SelectInner {
fn arbitrary_sized_from<R: Rng>(
rng: &mut R,
env: &SimulatorEnv,
num_result_columns: usize,
) -> Self {
let mut select_inner = SelectInner::arbitrary_from(rng, env);
let select_from = &select_inner.from.as_ref().unwrap();
let table_names = select_from
.joins
.iter()
.map(|j| j.table.clone())
.chain(std::iter::once(select_from.table.clone()))
.collect::<Vec<_>>();
let flat_columns_names = table_names
.iter()
.flat_map(|t| {
env.tables
.iter()
.find(|table| table.name == *t)
.unwrap()
.columns
.iter()
.map(|c| format!("{}.{}", t.clone(), c.name))
})
.collect::<Vec<_>>();
let selected_columns = pick_unique(&flat_columns_names, num_result_columns, rng);
let mut columns = Vec::new();
for column_name in selected_columns {
columns.push(ResultColumn::Column(column_name.clone()));
}
select_inner.columns = columns;
select_inner
}
}
impl Arbitrary for Distinctness {
fn arbitrary<R: Rng>(rng: &mut R) -> Self {
match rng.gen_range(0..=5) {
0..4 => Distinctness::All,
_ => Distinctness::Distinct,
}
}
}
impl Arbitrary for CompoundOperator {
fn arbitrary<R: Rng>(rng: &mut R) -> Self {
match rng.gen_range(0..=1) {
0 => CompoundOperator::Union,
1 => CompoundOperator::UnionAll,
_ => unreachable!(),
}
}
}
/// SelectFree is a wrapper around Select that allows for arbitrary generation
/// of selects without requiring a specific environment, which is useful for generating
/// arbitrary expressions without referring to the tables.
pub(crate) struct SelectFree(pub(crate) Select);
impl ArbitraryFrom<&SimulatorEnv> for SelectFree {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
let expr = Predicate(Expr::arbitrary_sized_from(rng, env, 8));
let select = Select::expr(expr);
Self(select)
}
}
impl ArbitraryFrom<&SimulatorEnv> for Select {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
// Generate a number of selects based on the query size
// If experimental indexes are enabled, we can have selects with compounds
// Otherwise, we just have a single select with no compounds
let num_compound_selects = if env.opts.experimental_indexes {
match rng.gen_range(0..=100) {
0..=95 => 0,
96..=99 => 1,
100 => 2,
_ => unreachable!(),
}
} else {
0
};
let min_column_count_across_tables =
env.tables.iter().map(|t| t.columns.len()).min().unwrap();
let num_result_columns = rng.gen_range(1..=min_column_count_across_tables);
let mut first = SelectInner::arbitrary_sized_from(rng, env, num_result_columns);
let mut rest: Vec<SelectInner> = (0..num_compound_selects)
.map(|_| SelectInner::arbitrary_sized_from(rng, env, num_result_columns))
.collect();
if !rest.is_empty() {
// ORDER BY is not supported in compound selects yet
first.order_by = None;
for s in &mut rest {
s.order_by = None;
}
}
Self {
body: SelectBody {
select: Box::new(first),
compounds: rest
.into_iter()
.map(|s| CompoundSelect {
operator: CompoundOperator::arbitrary(rng),
select: Box::new(s),
})
.collect(),
},
limit: None,
}
}
}
impl ArbitraryFrom<&SimulatorEnv> for Insert {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
let gen_values = |rng: &mut R| {
let table = pick(&env.tables, rng);
let num_rows = rng.gen_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table
.columns
.iter()
.map(|c| SimValue::arbitrary_from(rng, &c.column_type))
.collect()
})
.collect();
Some(Insert::Values {
table: table.name.clone(),
values,
})
};
let _gen_select = |rng: &mut R| {
// Find a non-empty table
let select_table = env.tables.iter().find(|t| !t.rows.is_empty())?;
let row = pick(&select_table.rows, rng);
let predicate = Predicate::arbitrary_from(rng, (select_table, row));
// Pick another table to insert into
let select = Select::simple(select_table.name.clone(), predicate);
let table = pick(&env.tables, rng);
Some(Insert::Select {
table: table.name.clone(),
select: Box::new(select),
})
};
// TODO: Add back gen_select when https://github.com/tursodatabase/turso/issues/2129 is fixed.
// Backtrack here cannot return None
backtrack(vec![(1, Box::new(gen_values))], rng).unwrap()
}
}
impl ArbitraryFrom<&SimulatorEnv> for Delete {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
let table = pick(&env.tables, rng);
Self {
table: table.name.clone(),
predicate: Predicate::arbitrary_from(rng, table),
}
}
}
impl ArbitraryFrom<&SimulatorEnv> for Drop {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
let table = pick(&env.tables, rng);
Self {
table: table.name.clone(),
}
}
}
impl ArbitraryFrom<(&SimulatorEnv, &Remaining)> for Query {
fn arbitrary_from<R: Rng>(rng: &mut R, (env, remaining): (&SimulatorEnv, &Remaining)) -> Self {
@@ -355,43 +36,3 @@ impl ArbitraryFrom<(&SimulatorEnv, &Remaining)> for Query {
)
}
}
fn pick_unique<T: ToOwned + PartialEq>(
items: &[T],
count: usize,
rng: &mut impl rand::Rng,
) -> Vec<T::Owned>
where
<T as ToOwned>::Owned: PartialEq,
{
let mut picked: Vec<T::Owned> = Vec::new();
while picked.len() < count {
let item = pick(items, rng);
if !picked.contains(&item.to_owned()) {
picked.push(item.to_owned());
}
}
picked
}
impl ArbitraryFrom<&SimulatorEnv> for Update {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
let table = pick(&env.tables, rng);
let num_cols = rng.gen_range(1..=table.columns.len());
let columns = pick_unique(&table.columns, num_cols, rng);
let set_values: Vec<(String, SimValue)> = columns
.iter()
.map(|column| {
(
column.name.clone(),
SimValue::arbitrary_from(rng, &column.column_type),
)
})
.collect();
Update {
table: table.name.clone(),
set_values,
predicate: Predicate::arbitrary_from(rng, table),
}
}
}

View File

@@ -2,7 +2,6 @@
use anyhow::anyhow;
use clap::Parser;
use generation::plan::{Interaction, InteractionPlan, InteractionPlanState};
use generation::ArbitraryFrom;
use notify::event::{DataChange, ModifyKind};
use notify::{EventKind, RecursiveMode, Watcher};
use rand::prelude::*;
@@ -11,6 +10,7 @@ use runner::cli::{SimulatorCLI, SimulatorCommand};
use runner::env::SimulatorEnv;
use runner::execution::{execute_plans, Execution, ExecutionHistory, ExecutionResult};
use runner::{differential, watch};
use sql_generation::generation::ArbitraryFrom;
use std::any::Any;
use std::backtrace::Backtrace;
use std::fs::OpenOptions;
@@ -507,7 +507,7 @@ fn setup_simulation(
(seed, env, plans)
} else {
let seed = cli_opts.seed.unwrap_or_else(|| {
let mut rng = rand::thread_rng();
let mut rng = rand::rng();
rng.next_u64()
});
tracing::info!("seed={}", seed);

View File

@@ -1,4 +1,417 @@
pub mod query;
pub mod table;
use std::{collections::HashSet, fmt::Display};
pub(crate) const FAULT_ERROR_MSG: &str = "Injected fault";
use anyhow::Context;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use sql_generation::model::{
query::{
select::{CompoundOperator, FromClause, ResultColumn, SelectInner},
transaction::{Begin, Commit, Rollback},
update::Update,
Create, CreateIndex, Delete, Drop, EmptyContext, Insert, Select,
},
table::{JoinTable, JoinType, SimValue, Table, TableContext},
};
use turso_parser::ast::{fmt::ToTokens, Distinctness};
use crate::{generation::Shadow, runner::env::SimulatorTables};
// This type represents the potential queries on the database.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Query {
Create(Create),
Select(Select),
Insert(Insert),
Delete(Delete),
Update(Update),
Drop(Drop),
CreateIndex(CreateIndex),
Begin(Begin),
Commit(Commit),
Rollback(Rollback),
}
impl Query {
pub fn dependencies(&self) -> HashSet<String> {
match self {
Query::Select(select) => select.dependencies(),
Query::Create(_) => HashSet::new(),
Query::Insert(Insert::Select { table, .. })
| Query::Insert(Insert::Values { table, .. })
| Query::Delete(Delete { table, .. })
| Query::Update(Update { table, .. })
| Query::Drop(Drop { table, .. }) => HashSet::from_iter([table.clone()]),
Query::CreateIndex(CreateIndex { table_name, .. }) => {
HashSet::from_iter([table_name.clone()])
}
Query::Begin(_) | Query::Commit(_) | Query::Rollback(_) => HashSet::new(),
}
}
pub fn uses(&self) -> Vec<String> {
match self {
Query::Create(Create { table }) => vec![table.name.clone()],
Query::Select(select) => select.dependencies().into_iter().collect(),
Query::Insert(Insert::Select { table, .. })
| Query::Insert(Insert::Values { table, .. })
| Query::Delete(Delete { table, .. })
| Query::Update(Update { table, .. })
| Query::Drop(Drop { table, .. }) => vec![table.clone()],
Query::CreateIndex(CreateIndex { table_name, .. }) => vec![table_name.clone()],
Query::Begin(..) | Query::Commit(..) | Query::Rollback(..) => vec![],
}
}
}
impl Display for Query {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Create(create) => write!(f, "{create}"),
Self::Select(select) => write!(f, "{select}"),
Self::Insert(insert) => write!(f, "{insert}"),
Self::Delete(delete) => write!(f, "{delete}"),
Self::Update(update) => write!(f, "{update}"),
Self::Drop(drop) => write!(f, "{drop}"),
Self::CreateIndex(create_index) => write!(f, "{create_index}"),
Self::Begin(begin) => write!(f, "{begin}"),
Self::Commit(commit) => write!(f, "{commit}"),
Self::Rollback(rollback) => write!(f, "{rollback}"),
}
}
}
impl Shadow for Query {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
match self {
Query::Create(create) => create.shadow(env),
Query::Insert(insert) => insert.shadow(env),
Query::Delete(delete) => delete.shadow(env),
Query::Select(select) => select.shadow(env),
Query::Update(update) => update.shadow(env),
Query::Drop(drop) => drop.shadow(env),
Query::CreateIndex(create_index) => Ok(create_index.shadow(env)),
Query::Begin(begin) => Ok(begin.shadow(env)),
Query::Commit(commit) => Ok(commit.shadow(env)),
Query::Rollback(rollback) => Ok(rollback.shadow(env)),
}
}
}
impl Shadow for Create {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
if !tables.iter().any(|t| t.name == self.table.name) {
tables.push(self.table.clone());
Ok(vec![])
} else {
Err(anyhow::anyhow!(
"Table {} already exists. CREATE TABLE statement ignored.",
self.table.name
))
}
}
}
impl Shadow for CreateIndex {
type Result = Vec<Vec<SimValue>>;
fn shadow(&self, env: &mut SimulatorTables) -> Vec<Vec<SimValue>> {
env.tables
.iter_mut()
.find(|t| t.name == self.table_name)
.unwrap()
.indexes
.push(self.index_name.clone());
vec![]
}
}
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);
if let Some(table) = table {
// If the table exists, we can delete from it
let t2 = table.clone();
table.rows.retain_mut(|r| !self.predicate.test(r, &t2));
} else {
// If the table does not exist, we return an error
return Err(anyhow::anyhow!(
"Table {} does not exist. DELETE statement ignored.",
self.table
));
}
Ok(vec![])
}
}
impl Shadow for Drop {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, tables: &mut SimulatorTables) -> 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!(
"Table {} does not exist. DROP statement ignored.",
self.table
));
}
tables.tables.retain(|t| t.name != self.table);
Ok(vec![])
}
}
impl Shadow for Insert {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
match self {
Insert::Values { table, values } => {
if let Some(t) = tables.tables.iter_mut().find(|t| &t.name == table) {
t.rows.extend(values.clone());
} else {
return Err(anyhow::anyhow!(
"Table {} does not exist. INSERT statement ignored.",
table
));
}
}
Insert::Select { table, select } => {
let rows = select.shadow(tables)?;
if let Some(t) = tables.tables.iter_mut().find(|t| &t.name == table) {
t.rows.extend(rows);
} else {
return Err(anyhow::anyhow!(
"Table {} does not exist. INSERT statement ignored.",
table
));
}
}
}
Ok(vec![])
}
}
impl Shadow for FromClause {
type Result = anyhow::Result<JoinTable>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
let tables = &mut env.tables;
let first_table = tables
.iter()
.find(|t| t.name == self.table)
.context("Table not found")?;
let mut join_table = JoinTable {
tables: vec![first_table.clone()],
rows: Vec::new(),
};
for join in &self.joins {
let joined_table = tables
.iter()
.find(|t| t.name == join.table)
.context("Joined table not found")?;
join_table.tables.push(joined_table.clone());
match join.join_type {
JoinType::Inner => {
// Implement inner join logic
let join_rows = joined_table
.rows
.iter()
.filter(|row| join.on.test(row, joined_table))
.cloned()
.collect::<Vec<_>>();
// take a cartesian product of the rows
let all_row_pairs = join_table
.rows
.clone()
.into_iter()
.cartesian_product(join_rows.iter());
for (row1, row2) in all_row_pairs {
let row = row1.iter().chain(row2.iter()).cloned().collect::<Vec<_>>();
let is_in = join.on.test(&row, &join_table);
if is_in {
join_table.rows.push(row);
}
}
}
_ => todo!(),
}
}
Ok(join_table)
}
}
impl Shadow for SelectInner {
type Result = anyhow::Result<JoinTable>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
if let Some(from) = &self.from {
let mut join_table = from.shadow(env)?;
let col_count = join_table.columns().count();
for row in &mut join_table.rows {
assert_eq!(
row.len(),
col_count,
"Row length does not match column length after join"
);
}
let join_clone = join_table.clone();
join_table
.rows
.retain(|row| self.where_clause.test(row, &join_clone));
if self.distinctness == Distinctness::Distinct {
join_table.rows.sort_unstable();
join_table.rows.dedup();
}
Ok(join_table)
} else {
assert!(self
.columns
.iter()
.all(|col| matches!(col, ResultColumn::Expr(_))));
// If `WHERE` is false, just return an empty table
if !self.where_clause.test(&[], &Table::anonymous(vec![])) {
return Ok(JoinTable {
tables: Vec::new(),
rows: Vec::new(),
});
}
// Compute the results of the column expressions and make a row
let mut row = Vec::new();
for col in &self.columns {
match col {
ResultColumn::Expr(expr) => {
let value = expr.eval(&[], &Table::anonymous(vec![]));
if let Some(value) = value {
row.push(value);
} else {
return Err(anyhow::anyhow!(
"Failed to evaluate expression in free select ({})",
expr.0.format_with_context(&EmptyContext {}).unwrap()
));
}
}
_ => unreachable!("Only expressions are allowed in free selects"),
}
}
Ok(JoinTable {
tables: Vec::new(),
rows: vec![row],
})
}
}
}
impl Shadow for Select {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
let first_result = self.body.select.shadow(env)?;
let mut rows = first_result.rows;
for compound in self.body.compounds.iter() {
let compound_results = compound.select.shadow(env)?;
match compound.operator {
CompoundOperator::Union => {
// Union means we need to combine the results, removing duplicates
let mut new_rows = compound_results.rows;
new_rows.extend(rows.clone());
new_rows.sort_unstable();
new_rows.dedup();
rows = new_rows;
}
CompoundOperator::UnionAll => {
// Union all means we just concatenate the results
rows.extend(compound_results.rows.into_iter());
}
}
}
Ok(rows)
}
}
impl Shadow for Begin {
type Result = Vec<Vec<SimValue>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
tables.snapshot = Some(tables.tables.clone());
vec![]
}
}
impl Shadow for Commit {
type Result = Vec<Vec<SimValue>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
tables.snapshot = None;
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_;
}
vec![]
}
}
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);
let table = if let Some(table) = table {
table
} else {
return Err(anyhow::anyhow!(
"Table {} does not exist. UPDATE statement ignored.",
self.table
));
};
let t2 = table.clone();
for row in table
.rows
.iter_mut()
.filter(|r| self.predicate.test(r, &t2))
{
for (column, set_value) in &self.set_values {
if let Some((idx, _)) = table
.columns
.iter()
.enumerate()
.find(|(_, c)| &c.name == column)
{
row[idx] = set_value.clone();
}
}
}
Ok(vec![])
}
}

View File

@@ -1,45 +0,0 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use crate::{
generation::Shadow,
model::table::{SimValue, Table},
runner::env::SimulatorTables,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Create {
pub(crate) table: Table,
}
impl Shadow for Create {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
if !tables.iter().any(|t| t.name == self.table.name) {
tables.push(self.table.clone());
Ok(vec![])
} else {
Err(anyhow::anyhow!(
"Table {} already exists. CREATE TABLE statement ignored.",
self.table.name
))
}
}
}
impl Display for Create {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CREATE TABLE {} (", self.table.name)?;
for (i, column) in self.table.columns.iter().enumerate() {
if i != 0 {
write!(f, ",")?;
}
write!(f, "{} {}", column.name, column.column_type)?;
}
write!(f, ")")
}
}

View File

@@ -1,106 +0,0 @@
use crate::{
generation::{gen_random_text, pick, pick_n_unique, ArbitraryFrom, Shadow},
model::table::SimValue,
runner::env::{SimulatorEnv, SimulatorTables},
};
use rand::Rng;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum SortOrder {
Asc,
Desc,
}
impl std::fmt::Display for SortOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SortOrder::Asc => write!(f, "ASC"),
SortOrder::Desc => write!(f, "DESC"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub(crate) struct CreateIndex {
pub(crate) index_name: String,
pub(crate) table_name: String,
pub(crate) columns: Vec<(String, SortOrder)>,
}
impl Shadow for CreateIndex {
type Result = Vec<Vec<SimValue>>;
fn shadow(&self, env: &mut SimulatorTables) -> Vec<Vec<SimValue>> {
env.tables
.iter_mut()
.find(|t| t.name == self.table_name)
.unwrap()
.indexes
.push(self.index_name.clone());
vec![]
}
}
impl std::fmt::Display for CreateIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"CREATE INDEX {} ON {} ({})",
self.index_name,
self.table_name,
self.columns
.iter()
.map(|(name, order)| format!("{name} {order}"))
.collect::<Vec<String>>()
.join(", ")
)
}
}
impl ArbitraryFrom<&SimulatorEnv> for CreateIndex {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &SimulatorEnv) -> Self {
assert!(
!env.tables.is_empty(),
"Cannot create an index when no tables exist in the environment."
);
let table = pick(&env.tables, rng);
if table.columns.is_empty() {
panic!(
"Cannot create an index on table '{}' as it has no columns.",
table.name
);
}
let num_columns_to_pick = rng.gen_range(1..=table.columns.len());
let picked_column_indices = pick_n_unique(0..table.columns.len(), num_columns_to_pick, rng);
let columns = picked_column_indices
.into_iter()
.map(|i| {
let column = &table.columns[i];
(
column.name.clone(),
if rng.gen_bool(0.5) {
SortOrder::Asc
} else {
SortOrder::Desc
},
)
})
.collect::<Vec<(String, SortOrder)>>();
let index_name = format!(
"idx_{}_{}",
table.name,
gen_random_text(rng).chars().take(8).collect::<String>()
);
CreateIndex {
index_name,
table_name: table.name.clone(),
columns,
}
}
}

View File

@@ -1,41 +0,0 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use crate::{generation::Shadow, model::table::SimValue, runner::env::SimulatorTables};
use super::predicate::Predicate;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Delete {
pub(crate) table: String,
pub(crate) predicate: Predicate,
}
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);
if let Some(table) = table {
// If the table exists, we can delete from it
let t2 = table.clone();
table.rows.retain_mut(|r| !self.predicate.test(r, &t2));
} else {
// If the table does not exist, we return an error
return Err(anyhow::anyhow!(
"Table {} does not exist. DELETE statement ignored.",
self.table
));
}
Ok(vec![])
}
}
impl Display for Delete {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "DELETE FROM {} WHERE {}", self.table, self.predicate)
}
}

View File

@@ -1,34 +0,0 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use crate::{generation::Shadow, model::table::SimValue, runner::env::SimulatorTables};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Drop {
pub(crate) table: String,
}
impl Shadow for Drop {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, tables: &mut SimulatorTables) -> 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!(
"Table {} does not exist. DROP statement ignored.",
self.table
));
}
tables.tables.retain(|t| t.name != self.table);
Ok(vec![])
}
}
impl Display for Drop {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "DROP TABLE {}", self.table)
}
}

View File

@@ -1,129 +0,0 @@
use std::{collections::HashSet, fmt::Display};
pub(crate) use create::Create;
pub(crate) use create_index::CreateIndex;
pub(crate) use delete::Delete;
pub(crate) use drop::Drop;
pub(crate) use insert::Insert;
pub(crate) use select::Select;
use serde::{Deserialize, Serialize};
use turso_sqlite3_parser::to_sql_string::ToSqlContext;
use update::Update;
use crate::{
generation::Shadow,
model::{
query::transaction::{Begin, Commit, Rollback},
table::SimValue,
},
runner::env::SimulatorTables,
};
pub mod create;
pub mod create_index;
pub mod delete;
pub mod drop;
pub mod insert;
pub mod predicate;
pub mod select;
pub mod transaction;
pub mod update;
// This type represents the potential queries on the database.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum Query {
Create(Create),
Select(Select),
Insert(Insert),
Delete(Delete),
Update(Update),
Drop(Drop),
CreateIndex(CreateIndex),
Begin(Begin),
Commit(Commit),
Rollback(Rollback),
}
impl Query {
pub(crate) fn dependencies(&self) -> HashSet<String> {
match self {
Query::Select(select) => select.dependencies(),
Query::Create(_) => HashSet::new(),
Query::Insert(Insert::Select { table, .. })
| Query::Insert(Insert::Values { table, .. })
| Query::Delete(Delete { table, .. })
| Query::Update(Update { table, .. })
| Query::Drop(Drop { table, .. }) => HashSet::from_iter([table.clone()]),
Query::CreateIndex(CreateIndex { table_name, .. }) => {
HashSet::from_iter([table_name.clone()])
}
Query::Begin(_) | Query::Commit(_) | Query::Rollback(_) => HashSet::new(),
}
}
pub(crate) fn uses(&self) -> Vec<String> {
match self {
Query::Create(Create { table }) => vec![table.name.clone()],
Query::Select(select) => select.dependencies().into_iter().collect(),
Query::Insert(Insert::Select { table, .. })
| Query::Insert(Insert::Values { table, .. })
| Query::Delete(Delete { table, .. })
| Query::Update(Update { table, .. })
| Query::Drop(Drop { table, .. }) => vec![table.clone()],
Query::CreateIndex(CreateIndex { table_name, .. }) => vec![table_name.clone()],
Query::Begin(..) | Query::Commit(..) | Query::Rollback(..) => vec![],
}
}
}
impl Shadow for Query {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
match self {
Query::Create(create) => create.shadow(env),
Query::Insert(insert) => insert.shadow(env),
Query::Delete(delete) => delete.shadow(env),
Query::Select(select) => select.shadow(env),
Query::Update(update) => update.shadow(env),
Query::Drop(drop) => drop.shadow(env),
Query::CreateIndex(create_index) => Ok(create_index.shadow(env)),
Query::Begin(begin) => Ok(begin.shadow(env)),
Query::Commit(commit) => Ok(commit.shadow(env)),
Query::Rollback(rollback) => Ok(rollback.shadow(env)),
}
}
}
impl Display for Query {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Create(create) => write!(f, "{create}"),
Self::Select(select) => write!(f, "{select}"),
Self::Insert(insert) => write!(f, "{insert}"),
Self::Delete(delete) => write!(f, "{delete}"),
Self::Update(update) => write!(f, "{update}"),
Self::Drop(drop) => write!(f, "{drop}"),
Self::CreateIndex(create_index) => write!(f, "{create_index}"),
Self::Begin(begin) => write!(f, "{begin}"),
Self::Commit(commit) => write!(f, "{commit}"),
Self::Rollback(rollback) => write!(f, "{rollback}"),
}
}
}
/// Used to print sql strings that already have all the context it needs
pub(crate) struct EmptyContext;
impl ToSqlContext for EmptyContext {
fn get_column_name(
&self,
_table_id: turso_sqlite3_parser::ast::TableInternalId,
_col_idx: usize,
) -> String {
unreachable!()
}
fn get_table_name(&self, _id: turso_sqlite3_parser::ast::TableInternalId) -> &str {
unreachable!()
}
}

View File

@@ -1,497 +0,0 @@
use std::{collections::HashSet, fmt::Display};
use anyhow::Context;
pub use ast::Distinctness;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use turso_sqlite3_parser::ast::{self, fmt::ToTokens, SortOrder};
use crate::{
generation::Shadow,
model::{
query::EmptyContext,
table::{JoinTable, JoinType, JoinedTable, SimValue, Table, TableContext},
},
runner::env::SimulatorTables,
};
use super::predicate::Predicate;
/// `SELECT` or `RETURNING` result column
// https://sqlite.org/syntax/result-column.html
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ResultColumn {
/// expression
Expr(Predicate),
/// `*`
Star,
/// column name
Column(String),
}
impl Display for ResultColumn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResultColumn::Expr(expr) => write!(f, "({expr})"),
ResultColumn::Star => write!(f, "*"),
ResultColumn::Column(name) => write!(f, "{name}"),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Select {
pub(crate) body: SelectBody,
pub(crate) limit: Option<usize>,
}
impl Select {
pub fn simple(table: String, where_clause: Predicate) -> Self {
Self::single(
table,
vec![ResultColumn::Star],
where_clause,
None,
Distinctness::All,
)
}
pub fn expr(expr: Predicate) -> Self {
Select {
body: SelectBody {
select: Box::new(SelectInner {
distinctness: Distinctness::All,
columns: vec![ResultColumn::Expr(expr)],
from: None,
where_clause: Predicate::true_(),
order_by: None,
}),
compounds: Vec::new(),
},
limit: None,
}
}
pub fn single(
table: String,
result_columns: Vec<ResultColumn>,
where_clause: Predicate,
limit: Option<usize>,
distinct: Distinctness,
) -> Self {
Select {
body: SelectBody {
select: Box::new(SelectInner {
distinctness: distinct,
columns: result_columns,
from: Some(FromClause {
table,
joins: Vec::new(),
}),
where_clause,
order_by: None,
}),
compounds: Vec::new(),
},
limit,
}
}
pub fn compound(left: Select, right: Select, operator: CompoundOperator) -> Self {
let mut body = left.body;
body.compounds.push(CompoundSelect {
operator,
select: Box::new(right.body.select.as_ref().clone()),
});
Select {
body,
limit: left.limit.or(right.limit),
}
}
pub(crate) fn dependencies(&self) -> HashSet<String> {
if self.body.select.from.is_none() {
return HashSet::new();
}
let from = self.body.select.from.as_ref().unwrap();
let mut tables = HashSet::new();
tables.insert(from.table.clone());
tables.extend(from.dependencies());
for compound in &self.body.compounds {
tables.extend(
compound
.select
.from
.as_ref()
.map(|f| f.dependencies())
.unwrap_or(vec![])
.into_iter(),
);
}
tables
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SelectBody {
/// first select
pub select: Box<SelectInner>,
/// compounds
pub compounds: Vec<CompoundSelect>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct OrderBy {
pub columns: Vec<(String, SortOrder)>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SelectInner {
/// `DISTINCT`
pub distinctness: Distinctness,
/// columns
pub columns: Vec<ResultColumn>,
/// `FROM` clause
pub from: Option<FromClause>,
/// `WHERE` clause
pub where_clause: Predicate,
/// `ORDER BY` clause
pub order_by: Option<OrderBy>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum CompoundOperator {
/// `UNION`
Union,
/// `UNION ALL`
UnionAll,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CompoundSelect {
/// operator
pub operator: CompoundOperator,
/// select
pub select: Box<SelectInner>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FromClause {
/// table
pub table: String,
/// `JOIN`ed tables
pub joins: Vec<JoinedTable>,
}
impl FromClause {
fn to_sql_ast(&self) -> ast::FromClause {
ast::FromClause {
select: Some(Box::new(ast::SelectTable::Table(
ast::QualifiedName::single(ast::Name::from_str(&self.table)),
None,
None,
))),
joins: if self.joins.is_empty() {
None
} else {
Some(
self.joins
.iter()
.map(|join| ast::JoinedSelectTable {
operator: match join.join_type {
JoinType::Inner => {
ast::JoinOperator::TypedJoin(Some(ast::JoinType::INNER))
}
JoinType::Left => {
ast::JoinOperator::TypedJoin(Some(ast::JoinType::LEFT))
}
JoinType::Right => {
ast::JoinOperator::TypedJoin(Some(ast::JoinType::RIGHT))
}
JoinType::Full => {
ast::JoinOperator::TypedJoin(Some(ast::JoinType::OUTER))
}
JoinType::Cross => {
ast::JoinOperator::TypedJoin(Some(ast::JoinType::CROSS))
}
},
table: ast::SelectTable::Table(
ast::QualifiedName::single(ast::Name::from_str(&join.table)),
None,
None,
),
constraint: Some(ast::JoinConstraint::On(join.on.0.clone())),
})
.collect(),
)
},
op: None, // FIXME: this is a temporary fix, we should remove this field
}
}
pub(crate) fn dependencies(&self) -> Vec<String> {
let mut deps = vec![self.table.clone()];
for join in &self.joins {
deps.push(join.table.clone());
}
deps
}
}
impl Shadow for FromClause {
type Result = anyhow::Result<JoinTable>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
let tables = &mut env.tables;
let first_table = tables
.iter()
.find(|t| t.name == self.table)
.context("Table not found")?;
let mut join_table = JoinTable {
tables: vec![first_table.clone()],
rows: Vec::new(),
};
for join in &self.joins {
let joined_table = tables
.iter()
.find(|t| t.name == join.table)
.context("Joined table not found")?;
join_table.tables.push(joined_table.clone());
match join.join_type {
JoinType::Inner => {
// Implement inner join logic
let join_rows = joined_table
.rows
.iter()
.filter(|row| join.on.test(row, joined_table))
.cloned()
.collect::<Vec<_>>();
// take a cartesian product of the rows
let all_row_pairs = join_table
.rows
.clone()
.into_iter()
.cartesian_product(join_rows.iter());
for (row1, row2) in all_row_pairs {
let row = row1.iter().chain(row2.iter()).cloned().collect::<Vec<_>>();
let is_in = join.on.test(&row, &join_table);
if is_in {
join_table.rows.push(row);
}
}
}
_ => todo!(),
}
}
Ok(join_table)
}
}
impl Shadow for SelectInner {
type Result = anyhow::Result<JoinTable>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
if let Some(from) = &self.from {
let mut join_table = from.shadow(env)?;
let col_count = join_table.columns().count();
for row in &mut join_table.rows {
assert_eq!(
row.len(),
col_count,
"Row length does not match column length after join"
);
}
let join_clone = join_table.clone();
join_table
.rows
.retain(|row| self.where_clause.test(row, &join_clone));
if self.distinctness == Distinctness::Distinct {
join_table.rows.sort_unstable();
join_table.rows.dedup();
}
Ok(join_table)
} else {
assert!(self
.columns
.iter()
.all(|col| matches!(col, ResultColumn::Expr(_))));
// If `WHERE` is false, just return an empty table
if !self.where_clause.test(&[], &Table::anonymous(vec![])) {
return Ok(JoinTable {
tables: Vec::new(),
rows: Vec::new(),
});
}
// Compute the results of the column expressions and make a row
let mut row = Vec::new();
for col in &self.columns {
match col {
ResultColumn::Expr(expr) => {
let value = expr.eval(&[], &Table::anonymous(vec![]));
if let Some(value) = value {
row.push(value);
} else {
return Err(anyhow::anyhow!(
"Failed to evaluate expression in free select ({})",
expr.0.format_with_context(&EmptyContext {}).unwrap()
));
}
}
_ => unreachable!("Only expressions are allowed in free selects"),
}
}
Ok(JoinTable {
tables: Vec::new(),
rows: vec![row],
})
}
}
}
impl Shadow for Select {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, env: &mut SimulatorTables) -> Self::Result {
let first_result = self.body.select.shadow(env)?;
let mut rows = first_result.rows;
for compound in self.body.compounds.iter() {
let compound_results = compound.select.shadow(env)?;
match compound.operator {
CompoundOperator::Union => {
// Union means we need to combine the results, removing duplicates
let mut new_rows = compound_results.rows;
new_rows.extend(rows.clone());
new_rows.sort_unstable();
new_rows.dedup();
rows = new_rows;
}
CompoundOperator::UnionAll => {
// Union all means we just concatenate the results
rows.extend(compound_results.rows.into_iter());
}
}
}
Ok(rows)
}
}
impl Select {
pub fn to_sql_ast(&self) -> ast::Select {
ast::Select {
with: None,
body: ast::SelectBody {
select: Box::new(ast::OneSelect::Select(Box::new(ast::SelectInner {
distinctness: if self.body.select.distinctness == Distinctness::Distinct {
Some(ast::Distinctness::Distinct)
} else {
None
},
columns: self
.body
.select
.columns
.iter()
.map(|col| match col {
ResultColumn::Expr(expr) => {
ast::ResultColumn::Expr(expr.0.clone(), None)
}
ResultColumn::Star => ast::ResultColumn::Star,
ResultColumn::Column(name) => ast::ResultColumn::Expr(
ast::Expr::Id(ast::Name::Ident(name.clone())),
None,
),
})
.collect(),
from: self.body.select.from.as_ref().map(|f| f.to_sql_ast()),
where_clause: Some(self.body.select.where_clause.0.clone()),
group_by: None,
window_clause: None,
}))),
compounds: Some(
self.body
.compounds
.iter()
.map(|compound| ast::CompoundSelect {
operator: match compound.operator {
CompoundOperator::Union => ast::CompoundOperator::Union,
CompoundOperator::UnionAll => ast::CompoundOperator::UnionAll,
},
select: Box::new(ast::OneSelect::Select(Box::new(ast::SelectInner {
distinctness: Some(compound.select.distinctness),
columns: compound
.select
.columns
.iter()
.map(|col| match col {
ResultColumn::Expr(expr) => {
ast::ResultColumn::Expr(expr.0.clone(), None)
}
ResultColumn::Star => ast::ResultColumn::Star,
ResultColumn::Column(name) => ast::ResultColumn::Expr(
ast::Expr::Id(ast::Name::Ident(name.clone())),
None,
),
})
.collect(),
from: compound.select.from.as_ref().map(|f| f.to_sql_ast()),
where_clause: Some(compound.select.where_clause.0.clone()),
group_by: None,
window_clause: None,
}))),
})
.collect(),
),
},
order_by: self.body.select.order_by.as_ref().map(|o| {
o.columns
.iter()
.map(|(name, order)| ast::SortedColumn {
expr: ast::Expr::Id(ast::Name::Ident(name.clone())),
order: match order {
SortOrder::Asc => Some(ast::SortOrder::Asc),
SortOrder::Desc => Some(ast::SortOrder::Desc),
},
nulls: None,
})
.collect()
}),
limit: self.limit.map(|l| {
Box::new(ast::Limit {
expr: ast::Expr::Literal(ast::Literal::Numeric(l.to_string())),
offset: None,
})
}),
}
}
}
impl Display for Select {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_sql_ast().to_fmt_with_context(f, &EmptyContext {})
}
}
#[cfg(test)]
mod select_tests {
#[test]
fn test_select_display() {}
}

View File

@@ -1,60 +0,0 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use crate::{generation::Shadow, model::table::SimValue, runner::env::SimulatorTables};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Begin {
pub(crate) immediate: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Commit;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Rollback;
impl Shadow for Begin {
type Result = Vec<Vec<SimValue>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
tables.snapshot = Some(tables.tables.clone());
vec![]
}
}
impl Shadow for Commit {
type Result = Vec<Vec<SimValue>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
tables.snapshot = None;
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_;
}
vec![]
}
}
impl Display for Begin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BEGIN {}", if self.immediate { "IMMEDIATE" } else { "" })
}
}
impl Display for Commit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "COMMIT")
}
}
impl Display for Rollback {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ROLLBACK")
}
}

View File

@@ -1,71 +0,0 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use crate::{generation::Shadow, model::table::SimValue, runner::env::SimulatorTables};
use super::predicate::Predicate;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct Update {
pub(crate) table: String,
pub(crate) set_values: Vec<(String, SimValue)>, // Pair of value for set expressions => SET name=value
pub(crate) predicate: Predicate,
}
impl Update {
pub fn table(&self) -> &str {
&self.table
}
}
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);
let table = if let Some(table) = table {
table
} else {
return Err(anyhow::anyhow!(
"Table {} does not exist. UPDATE statement ignored.",
self.table
));
};
let t2 = table.clone();
for row in table
.rows
.iter_mut()
.filter(|r| self.predicate.test(r, &t2))
{
for (column, set_value) in &self.set_values {
if let Some((idx, _)) = table
.columns
.iter()
.enumerate()
.find(|(_, c)| &c.name == column)
{
row[idx] = set_value.clone();
}
}
}
Ok(vec![])
}
}
impl Display for Update {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "UPDATE {} SET ", self.table)?;
for (i, (name, value)) in self.set_values.iter().enumerate() {
if i != 0 {
write!(f, ", ")?;
}
write!(f, "{name} = {value}")?;
}
write!(f, " WHERE {}", self.predicate)?;
Ok(())
}
}

View File

@@ -27,7 +27,7 @@ impl SimulatorClock {
let nanos = self
.rng
.borrow_mut()
.gen_range(self.min_tick..self.max_tick);
.random_range(self.min_tick..self.max_tick);
let nanos = std::time::Duration::from_micros(nanos);
*time += nanos;
*time

View File

@@ -1,14 +1,14 @@
use std::sync::{Arc, Mutex};
use sql_generation::{generation::pick_index, model::table::SimValue};
use turso_core::Value;
use crate::{
generation::{
pick_index,
plan::{Interaction, InteractionPlanState, ResultSet},
Shadow as _,
},
model::{query::Query, table::SimValue},
model::Query,
runner::execution::ExecutionContinuation,
InteractionPlan,
};

View File

@@ -3,9 +3,10 @@ use std::{
sync::{Arc, Mutex},
};
use sql_generation::generation::pick_index;
use crate::{
generation::{pick_index, plan::InteractionPlanState},
runner::execution::ExecutionContinuation,
generation::plan::InteractionPlanState, runner::execution::ExecutionContinuation,
InteractionPlan,
};

View File

@@ -7,10 +7,9 @@ use std::sync::Arc;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use sql_generation::model::table::Table;
use turso_core::Database;
use crate::model::table::Table;
use crate::runner::io::SimulatorIO;
use super::cli::SimulatorCLI;
@@ -173,29 +172,29 @@ impl SimulatorEnv {
let mut delete_percent = 0.0;
let mut update_percent = 0.0;
let read_percent = rng.gen_range(0.0..=total);
let read_percent = rng.random_range(0.0..=total);
let write_percent = total - read_percent;
if !cli_opts.disable_create {
// Create percent should be 5-15% of the write percent
create_percent = rng.gen_range(0.05..=0.15) * write_percent;
create_percent = rng.random_range(0.05..=0.15) * write_percent;
}
if !cli_opts.disable_create_index {
// Create indexpercent should be 2-5% of the write percent
create_index_percent = rng.gen_range(0.02..=0.05) * write_percent;
create_index_percent = rng.random_range(0.02..=0.05) * write_percent;
}
if !cli_opts.disable_drop {
// Drop percent should be 2-5% of the write percent
drop_percent = rng.gen_range(0.02..=0.05) * write_percent;
drop_percent = rng.random_range(0.02..=0.05) * write_percent;
}
if !cli_opts.disable_delete {
// Delete percent should be 10-20% of the write percent
delete_percent = rng.gen_range(0.1..=0.2) * write_percent;
delete_percent = rng.random_range(0.1..=0.2) * write_percent;
}
if !cli_opts.disable_update {
// Update percent should be 10-20% of the write percent
// TODO: freestyling the percentage
update_percent = rng.gen_range(0.1..=0.2) * write_percent;
update_percent = rng.random_range(0.1..=0.2) * write_percent;
}
let write_percent = write_percent
@@ -220,10 +219,10 @@ impl SimulatorEnv {
let opts = SimulatorOpts {
seed,
ticks: rng.gen_range(cli_opts.minimum_tests..=cli_opts.maximum_tests),
ticks: rng.random_range(cli_opts.minimum_tests..=cli_opts.maximum_tests),
max_connections: 1, // TODO: for now let's use one connection as we didn't implement
// correct transactions processing
max_tables: rng.gen_range(0..128),
max_tables: rng.random_range(0..128),
create_percent,
create_index_percent,
read_percent,
@@ -243,7 +242,7 @@ impl SimulatorEnv {
disable_fsync_no_wait: cli_opts.disable_fsync_no_wait,
disable_faulty_query: cli_opts.disable_faulty_query,
page_size: 4096, // TODO: randomize this too
max_interactions: rng.gen_range(cli_opts.minimum_tests..=cli_opts.maximum_tests),
max_interactions: rng.random_range(cli_opts.minimum_tests..=cli_opts.maximum_tests),
max_time_simulation: cli_opts.maximum_time,
disable_reopen_database: cli_opts.disable_reopen_database,
latency_probability: cli_opts.latency_probability,

View File

@@ -1,10 +1,10 @@
use std::sync::{Arc, Mutex};
use sql_generation::generation::pick_index;
use tracing::instrument;
use turso_core::{Connection, LimboError, Result, StepResult};
use crate::generation::{
pick_index,
plan::{Interaction, InteractionPlan, InteractionPlanState, ResultSet},
Shadow as _,
};

View File

@@ -9,7 +9,7 @@ use rand_chacha::ChaCha8Rng;
use tracing::{instrument, Level};
use turso_core::{File, Result};
use crate::{model::FAULT_ERROR_MSG, runner::clock::SimulatorClock};
use crate::runner::{clock::SimulatorClock, FAULT_ERROR_MSG};
pub(crate) struct SimulatorFile {
pub path: String,
pub(crate) inner: Arc<dyn File>,
@@ -100,10 +100,10 @@ impl SimulatorFile {
fn generate_latency_duration(&self) -> Option<turso_core::Instant> {
let mut rng = self.rng.borrow_mut();
// Chance to introduce some latency
rng.gen_bool(self.latency_probability as f64 / 100.0)
rng.random_bool(self.latency_probability as f64 / 100.0)
.then(|| {
let now = self.clock.now();
let sum = now + std::time::Duration::from_millis(rng.gen_range(5..20));
let sum = now + std::time::Duration::from_millis(rng.random_range(5..20));
sum.into()
})
}

View File

@@ -9,3 +9,5 @@ pub mod execution;
pub mod file;
pub mod io;
pub mod watch;
pub const FAULT_ERROR_MSG: &str = "Injected Fault";

View File

@@ -1,10 +1,9 @@
use std::sync::{Arc, Mutex};
use sql_generation::generation::pick_index;
use crate::{
generation::{
pick_index,
plan::{Interaction, InteractionPlanState},
},
generation::plan::{Interaction, InteractionPlanState},
runner::execution::ExecutionContinuation,
};

View File

@@ -1,9 +1,9 @@
use crate::model::query::Query;
use crate::{
generation::{
plan::{Interaction, InteractionPlan, Interactions},
property::Property,
},
model::Query,
run_simulation,
runner::execution::Execution,
SandboxedResult, SimulatorEnv,

24
sql_generation/Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
[package]
name = "sql_generation"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
path = "lib.rs"
[dependencies]
hex = "0.4.3"
serde = { workspace = true, features = ["derive"] }
turso_core = { workspace = true, features = ["simulator"] }
turso_parser = { workspace = true, features = ["serde"] }
rand = { workspace = true }
anarchist-readable-name-generator-lib = "0.2.0"
itertools = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
rand_chacha = "0.9.0"

View File

@@ -1,14 +1,13 @@
use turso_sqlite3_parser::ast::{
use turso_parser::ast::{
self, Expr, LikeOperator, Name, Operator, QualifiedName, Type, UnaryOperator,
};
use crate::{
generation::{
frequency, gen_random_text, one_of, pick, pick_index, Arbitrary, ArbitraryFrom,
ArbitrarySizedFrom,
ArbitrarySizedFrom, GenerationContext,
},
model::table::SimValue,
SimulatorEnv,
};
impl<T> Arbitrary for Box<T>
@@ -34,7 +33,7 @@ where
T: Arbitrary,
{
fn arbitrary<R: rand::Rng>(rng: &mut R) -> Self {
rng.gen_bool(0.5).then_some(T::arbitrary(rng))
rng.random_bool(0.5).then_some(T::arbitrary(rng))
}
}
@@ -43,7 +42,7 @@ where
T: ArbitrarySizedFrom<A>,
{
fn arbitrary_sized_from<R: rand::Rng>(rng: &mut R, t: A, size: usize) -> Self {
rng.gen_bool(0.5)
rng.random_bool(0.5)
.then_some(T::arbitrary_sized_from(rng, t, size))
}
}
@@ -53,14 +52,14 @@ where
T: ArbitraryFrom<A>,
{
fn arbitrary_from<R: rand::Rng>(rng: &mut R, t: A) -> Self {
let size = rng.gen_range(0..5);
let size = rng.random_range(0..5);
(0..size).map(|_| T::arbitrary_from(rng, t)).collect()
}
}
// Freestyling generation
impl ArbitrarySizedFrom<&SimulatorEnv> for Expr {
fn arbitrary_sized_from<R: rand::Rng>(rng: &mut R, t: &SimulatorEnv, size: usize) -> Self {
impl<C: GenerationContext> ArbitrarySizedFrom<&C> for Expr {
fn arbitrary_sized_from<R: rand::Rng>(rng: &mut R, t: &C, size: usize) -> Self {
frequency(
vec![
(
@@ -200,36 +199,23 @@ impl Arbitrary for Type {
}
}
struct CollateName(String);
impl Arbitrary for CollateName {
fn arbitrary<R: rand::Rng>(rng: &mut R) -> Self {
let choice = rng.gen_range(0..3);
CollateName(
match choice {
0 => "BINARY",
1 => "RTRIM",
2 => "NOCASE",
_ => unreachable!(),
}
.to_string(),
)
}
}
impl ArbitraryFrom<&SimulatorEnv> for QualifiedName {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, t: &SimulatorEnv) -> Self {
impl<C: GenerationContext> ArbitraryFrom<&C> for QualifiedName {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, t: &C) -> Self {
// TODO: for now just generate table name
let table_idx = pick_index(t.tables.len(), rng);
let table = &t.tables[table_idx];
let table_idx = pick_index(t.tables().len(), rng);
let table = &t.tables()[table_idx];
// TODO: for now forego alias
Self::single(Name::from_str(&table.name))
Self {
db_name: None,
name: Name::new(&table.name),
alias: None,
}
}
}
impl ArbitraryFrom<&SimulatorEnv> for LikeOperator {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &SimulatorEnv) -> Self {
let choice = rng.gen_range(0..4);
impl<C: GenerationContext> ArbitraryFrom<&C> for LikeOperator {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &C) -> Self {
let choice = rng.random_range(0..4);
match choice {
0 => LikeOperator::Glob,
1 => LikeOperator::Like,
@@ -241,17 +227,17 @@ impl ArbitraryFrom<&SimulatorEnv> for LikeOperator {
}
// Current implementation does not take into account the columns affinity nor if table is Strict
impl ArbitraryFrom<&SimulatorEnv> for ast::Literal {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &SimulatorEnv) -> Self {
impl<C: GenerationContext> ArbitraryFrom<&C> for ast::Literal {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &C) -> Self {
loop {
let choice = rng.gen_range(0..5);
let choice = rng.random_range(0..5);
let lit = match choice {
0 => ast::Literal::Numeric({
let integer = rng.gen_bool(0.5);
let integer = rng.random_bool(0.5);
if integer {
rng.gen_range(i64::MIN..i64::MAX).to_string()
rng.random_range(i64::MIN..i64::MAX).to_string()
} else {
rng.gen_range(-1e10..1e10).to_string()
rng.random_range(-1e10..1e10).to_string()
}
}),
1 => ast::Literal::String(format!("'{}'", gen_random_text(rng))),
@@ -279,9 +265,9 @@ impl ArbitraryFrom<&Vec<&SimValue>> for ast::Expr {
}
}
impl ArbitraryFrom<&SimulatorEnv> for UnaryOperator {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &SimulatorEnv) -> Self {
let choice = rng.gen_range(0..4);
impl<C: GenerationContext> ArbitraryFrom<&C> for UnaryOperator {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &C) -> Self {
let choice = rng.random_range(0..4);
match choice {
0 => Self::BitwiseNot,
1 => Self::Negative,

View File

@@ -0,0 +1,188 @@
use std::{iter::Sum, ops::SubAssign};
use anarchist_readable_name_generator_lib::readable_name_custom;
use rand::{distr::uniform::SampleUniform, Rng};
use crate::model::table::Table;
pub mod expr;
pub mod predicate;
pub mod query;
pub mod table;
#[derive(Debug, Clone, Copy)]
pub struct Opts {
/// Indexes enabled
pub indexes: bool,
}
/// Trait used to provide context to generation functions
pub trait GenerationContext {
fn tables(&self) -> &Vec<Table>;
fn opts(&self) -> Opts;
}
type ArbitraryFromFunc<'a, R, T> = Box<dyn Fn(&mut R) -> T + 'a>;
type Choice<'a, R, T> = (usize, Box<dyn Fn(&mut R) -> Option<T> + 'a>);
/// Arbitrary trait for generating random values
/// An implementation of arbitrary is assumed to be a uniform sampling of
/// the possible values of the type, with a bias towards smaller values for
/// practicality.
pub trait Arbitrary {
fn arbitrary<R: Rng>(rng: &mut R) -> Self;
}
/// ArbitrarySized trait for generating random values of a specific size
/// An implementation of arbitrary_sized is assumed to be a uniform sampling of
/// the possible values of the type, with a bias towards smaller values for
/// practicality, but with the additional constraint that the generated value
/// must fit in the given size. This is useful for generating values that are
/// constrained by a specific size, such as integers or strings.
pub trait ArbitrarySized {
fn arbitrary_sized<R: Rng>(rng: &mut R, size: usize) -> Self;
}
/// ArbitraryFrom trait for generating random values from a given value
/// ArbitraryFrom allows for constructing relations, where the generated
/// value is dependent on the given value. These relations could be constraints
/// such as generating an integer within an interval, or a value that fits in a table,
/// or a predicate satisfying a given table row.
pub trait ArbitraryFrom<T> {
fn arbitrary_from<R: Rng>(rng: &mut R, t: T) -> Self;
}
/// ArbitrarySizedFrom trait for generating random values from a given value
/// ArbitrarySizedFrom allows for constructing relations, where the generated
/// value is dependent on the given value and a size constraint. These relations
/// could be constraints such as generating an integer within an interval,
/// or a value that fits in a table, or a predicate satisfying a given table row,
/// but with the additional constraint that the generated value must fit in the given size.
/// This is useful for generating values that are constrained by a specific size,
/// such as integers or strings, while still being dependent on the given value.
pub trait ArbitrarySizedFrom<T> {
fn arbitrary_sized_from<R: Rng>(rng: &mut R, t: T, size: usize) -> Self;
}
/// ArbitraryFromMaybe trait for fallibally generating random values from a given value
pub trait ArbitraryFromMaybe<T> {
fn arbitrary_from_maybe<R: Rng>(rng: &mut R, t: T) -> Option<Self>
where
Self: Sized;
}
/// Frequency is a helper function for composing different generators with different frequency
/// of occurrences.
/// The type signature for the `N` parameter is a bit complex, but it
/// roughly corresponds to a type that can be summed, compared, subtracted and sampled, which are
/// the operations we require for the implementation.
// todo: switch to a simpler type signature that can accommodate all integer and float types, which
// should be enough for our purposes.
pub fn frequency<T, R: Rng, N: Sum + PartialOrd + Copy + Default + SampleUniform + SubAssign>(
choices: Vec<(N, ArbitraryFromFunc<R, T>)>,
rng: &mut R,
) -> T {
let total = choices.iter().map(|(weight, _)| *weight).sum::<N>();
let mut choice = rng.random_range(N::default()..total);
for (weight, f) in choices {
if choice < weight {
return f(rng);
}
choice -= weight;
}
unreachable!()
}
/// one_of is a helper function for composing different generators with equal probability of occurrence.
pub fn one_of<T, R: Rng>(choices: Vec<ArbitraryFromFunc<R, T>>, rng: &mut R) -> T {
let index = rng.random_range(0..choices.len());
choices[index](rng)
}
/// backtrack is a helper function for composing different "failable" generators.
/// The function takes a list of functions that return an Option<T>, along with number of retries
/// to make before giving up.
pub fn backtrack<T, R: Rng>(mut choices: Vec<Choice<R, T>>, rng: &mut R) -> Option<T> {
loop {
// If there are no more choices left, we give up
let choices_ = choices
.iter()
.enumerate()
.filter(|(_, (retries, _))| *retries > 0)
.collect::<Vec<_>>();
if choices_.is_empty() {
tracing::trace!("backtrack: no more choices left");
return None;
}
// Run a one_of on the remaining choices
let (choice_index, choice) = pick(&choices_, rng);
let choice_index = *choice_index;
// If the choice returns None, we decrement the number of retries and try again
let result = choice.1(rng);
if result.is_some() {
return result;
} else {
choices[choice_index].0 -= 1;
}
}
}
/// pick is a helper function for uniformly picking a random element from a slice
pub fn pick<'a, T, R: Rng>(choices: &'a [T], rng: &mut R) -> &'a T {
let index = rng.random_range(0..choices.len());
&choices[index]
}
/// pick_index is typically used for picking an index from a slice to later refer to the element
/// at that index.
pub fn pick_index<R: Rng>(choices: usize, rng: &mut R) -> usize {
rng.random_range(0..choices)
}
/// pick_n_unique is a helper function for uniformly picking N unique elements from a range.
/// The elements themselves are usize, typically representing indices.
pub fn pick_n_unique<R: Rng>(range: std::ops::Range<usize>, n: usize, rng: &mut R) -> Vec<usize> {
use rand::seq::SliceRandom;
let mut items: Vec<usize> = range.collect();
items.shuffle(rng);
items.into_iter().take(n).collect()
}
/// gen_random_text uses `anarchist_readable_name_generator_lib` to generate random
/// readable names for tables, columns, text values etc.
pub fn gen_random_text<T: Rng>(rng: &mut T) -> String {
let big_text = rng.random_ratio(1, 1000);
if big_text {
// let max_size: u64 = 2 * 1024 * 1024 * 1024;
let max_size: u64 = 2 * 1024;
let size = rng.random_range(1024..max_size);
let mut name = String::with_capacity(size as usize);
for i in 0..size {
name.push(((i % 26) as u8 + b'A') as char);
}
name
} else {
let name = readable_name_custom("_", rng);
name.replace("-", "_")
}
}
pub fn pick_unique<T: ToOwned + PartialEq>(
items: &[T],
count: usize,
rng: &mut impl rand::Rng,
) -> Vec<T::Owned>
where
<T as ToOwned>::Owned: PartialEq,
{
let mut picked: Vec<T::Owned> = Vec::new();
while picked.len() < count {
let item = pick(items, rng);
if !picked.contains(&item.to_owned()) {
picked.push(item.to_owned());
}
}
picked
}

View File

@@ -1,6 +1,6 @@
//! Contains code for generation for [ast::Expr::Binary] Predicate
use turso_sqlite3_parser::ast::{self, Expr};
use turso_parser::ast::{self, Expr};
use crate::{
generation::{
@@ -56,7 +56,7 @@ impl Predicate {
/// Produces a true [ast::Expr::Binary] [Predicate] that is true for the provided row in the given table
pub fn true_binary<R: rand::Rng>(rng: &mut R, t: &Table, row: &[SimValue]) -> Predicate {
// Pick a column
let column_index = rng.gen_range(0..t.columns.len());
let column_index = rng.random_range(0..t.columns.len());
let mut column = t.columns[column_index].clone();
let value = &row[column_index];
@@ -82,8 +82,8 @@ impl Predicate {
Box::new(|_| {
Some(Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::new(&table_name),
ast::Name::new(&column.name),
)),
ast::Operator::Equals,
Box::new(Expr::Literal(value.into())),
@@ -99,8 +99,8 @@ impl Predicate {
} else {
Some(Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::new(&table_name),
ast::Name::new(&column.name),
)),
ast::Operator::NotEquals,
Box::new(Expr::Literal(v.into())),
@@ -114,8 +114,8 @@ impl Predicate {
let lt_value = LTValue::arbitrary_from(rng, value).0;
Some(Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::new(&table_name),
ast::Name::new(&column.name),
)),
ast::Operator::Greater,
Box::new(Expr::Literal(lt_value.into())),
@@ -128,8 +128,8 @@ impl Predicate {
let gt_value = GTValue::arbitrary_from(rng, value).0;
Some(Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::new(&table_name),
ast::Name::new(&column.name),
)),
ast::Operator::Less,
Box::new(Expr::Literal(gt_value.into())),
@@ -143,8 +143,8 @@ impl Predicate {
LikeValue::arbitrary_from_maybe(rng, value).map(|like| {
Expr::Like {
lhs: Box::new(ast::Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::new(&table_name),
ast::Name::new(&column.name),
)),
not: false, // TODO: also generate this value eventually
op: ast::LikeOperator::Like,
@@ -164,7 +164,7 @@ impl Predicate {
/// Produces an [ast::Expr::Binary] [Predicate] that is false for the provided row in the given table
pub fn false_binary<R: rand::Rng>(rng: &mut R, t: &Table, row: &[SimValue]) -> Predicate {
// Pick a column
let column_index = rng.gen_range(0..t.columns.len());
let column_index = rng.random_range(0..t.columns.len());
let mut column = t.columns[column_index].clone();
let mut table_name = t.name.clone();
let value = &row[column_index];
@@ -188,8 +188,8 @@ impl Predicate {
Box::new(|_| {
Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::new(&table_name),
ast::Name::new(&column.name),
)),
ast::Operator::NotEquals,
Box::new(Expr::Literal(value.into())),
@@ -204,8 +204,8 @@ impl Predicate {
};
Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::new(&table_name),
ast::Name::new(&column.name),
)),
ast::Operator::Equals,
Box::new(Expr::Literal(v.into())),
@@ -215,8 +215,8 @@ impl Predicate {
let gt_value = GTValue::arbitrary_from(rng, value).0;
Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::new(&table_name),
ast::Name::new(&column.name),
)),
ast::Operator::Greater,
Box::new(Expr::Literal(gt_value.into())),
@@ -226,8 +226,8 @@ impl Predicate {
let lt_value = LTValue::arbitrary_from(rng, value).0;
Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(&table_name),
ast::Name::from_str(&column.name),
ast::Name::new(&table_name),
ast::Name::new(&column.name),
)),
ast::Operator::Less,
Box::new(Expr::Literal(lt_value.into())),
@@ -249,7 +249,7 @@ impl SimplePredicate {
) -> Self {
// Pick a random column
let columns = table.columns().collect::<Vec<_>>();
let column_index = rng.gen_range(0..columns.len());
let column_index = rng.random_range(0..columns.len());
let column = columns[column_index];
let column_value = &row[column_index];
let table_name = column.table_name;
@@ -263,8 +263,8 @@ impl SimplePredicate {
Box::new(|_rng| {
Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(table_name),
ast::Name::from_str(&column.column.name),
ast::Name::new(table_name),
ast::Name::new(&column.column.name),
)),
ast::Operator::Equals,
Box::new(Expr::Literal(column_value.into())),
@@ -274,8 +274,8 @@ impl SimplePredicate {
let lt_value = LTValue::arbitrary_from(rng, column_value).0;
Expr::Binary(
Box::new(Expr::Qualified(
ast::Name::from_str(table_name),
ast::Name::from_str(&column.column.name),
ast::Name::new(table_name),
ast::Name::new(&column.column.name),
)),
ast::Operator::Greater,
Box::new(Expr::Literal(lt_value.into())),
@@ -285,8 +285,8 @@ impl SimplePredicate {
let gt_value = GTValue::arbitrary_from(rng, column_value).0;
Expr::Binary(
Box::new(Expr::Qualified(
ast::Name::from_str(table_name),
ast::Name::from_str(&column.column.name),
ast::Name::new(table_name),
ast::Name::new(&column.column.name),
)),
ast::Operator::Less,
Box::new(Expr::Literal(gt_value.into())),
@@ -306,7 +306,7 @@ impl SimplePredicate {
) -> Self {
let columns = table.columns().collect::<Vec<_>>();
// Pick a random column
let column_index = rng.gen_range(0..columns.len());
let column_index = rng.random_range(0..columns.len());
let column = columns[column_index];
let column_value = &row[column_index];
let table_name = column.table_name;
@@ -320,8 +320,8 @@ impl SimplePredicate {
Box::new(|_rng| {
Expr::Binary(
Box::new(Expr::Qualified(
ast::Name::from_str(table_name),
ast::Name::from_str(&column.column.name),
ast::Name::new(table_name),
ast::Name::new(&column.column.name),
)),
ast::Operator::NotEquals,
Box::new(Expr::Literal(column_value.into())),
@@ -331,8 +331,8 @@ impl SimplePredicate {
let gt_value = GTValue::arbitrary_from(rng, column_value).0;
Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(table_name),
ast::Name::from_str(&column.column.name),
ast::Name::new(table_name),
ast::Name::new(&column.column.name),
)),
ast::Operator::Greater,
Box::new(Expr::Literal(gt_value.into())),
@@ -342,8 +342,8 @@ impl SimplePredicate {
let lt_value = LTValue::arbitrary_from(rng, column_value).0;
Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name::from_str(table_name),
ast::Name::from_str(&column.column.name),
ast::Name::new(table_name),
ast::Name::new(&column.column.name),
)),
ast::Operator::Less,
Box::new(Expr::Literal(lt_value.into())),
@@ -376,11 +376,11 @@ impl CompoundPredicate {
}
let row = pick(rows, rng);
let predicate = if rng.gen_bool(0.7) {
let predicate = if rng.random_bool(0.7) {
// An AND for true requires each of its children to be true
// An AND for false requires at least one of its children to be false
if predicate_value {
(0..rng.gen_range(1..=3))
(0..rng.random_range(1..=3))
.map(|_| SimplePredicate::arbitrary_from(rng, (table, row, true)).0)
.reduce(|accum, curr| {
Predicate(Expr::Binary(
@@ -392,15 +392,15 @@ impl CompoundPredicate {
.unwrap_or(Predicate::true_())
} else {
// Create a vector of random booleans
let mut booleans = (0..rng.gen_range(1..=3))
.map(|_| rng.gen_bool(0.5))
let mut booleans = (0..rng.random_range(1..=3))
.map(|_| rng.random_bool(0.5))
.collect::<Vec<_>>();
let len = booleans.len();
// Make sure at least one of them is false
if booleans.iter().all(|b| *b) {
booleans[rng.gen_range(0..len)] = false;
booleans[rng.random_range(0..len)] = false;
}
booleans
@@ -420,13 +420,13 @@ impl CompoundPredicate {
// An OR for false requires each of its children to be false
if predicate_value {
// Create a vector of random booleans
let mut booleans = (0..rng.gen_range(1..=3))
.map(|_| rng.gen_bool(0.5))
let mut booleans = (0..rng.random_range(1..=3))
.map(|_| rng.random_bool(0.5))
.collect::<Vec<_>>();
let len = booleans.len();
// Make sure at least one of them is true
if booleans.iter().all(|b| !*b) {
booleans[rng.gen_range(0..len)] = true;
booleans[rng.random_range(0..len)] = true;
}
booleans
@@ -441,7 +441,7 @@ impl CompoundPredicate {
})
.unwrap_or(Predicate::true_())
} else {
(0..rng.gen_range(1..=3))
(0..rng.random_range(1..=3))
.map(|_| SimplePredicate::arbitrary_from(rng, (table, row, false)).0)
.reduce(|accum, curr| {
Predicate(Expr::Binary(
@@ -483,7 +483,7 @@ mod tests {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
for _ in 0..10000 {
let table = Table::arbitrary(&mut rng);
let num_rows = rng.gen_range(1..10);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table
@@ -509,7 +509,7 @@ mod tests {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
for _ in 0..10000 {
let table = Table::arbitrary(&mut rng);
let num_rows = rng.gen_range(1..10);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table
@@ -535,7 +535,7 @@ mod tests {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
for _ in 0..10000 {
let mut table = Table::arbitrary(&mut rng);
let num_rows = rng.gen_range(1..10);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table
@@ -563,7 +563,7 @@ mod tests {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
for _ in 0..10000 {
let mut table = Table::arbitrary(&mut rng);
let num_rows = rng.gen_range(1..10);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table

View File

@@ -1,5 +1,5 @@
use rand::{seq::SliceRandom as _, Rng};
use turso_sqlite3_parser::ast::{self, Expr};
use turso_parser::ast::{self, Expr};
use crate::model::{
query::predicate::Predicate,
@@ -8,8 +8,8 @@ use crate::model::{
use super::{one_of, ArbitraryFrom};
mod binary;
mod unary;
pub mod binary;
pub mod unary;
#[derive(Debug)]
struct CompoundPredicate(Predicate);
@@ -21,7 +21,7 @@ impl<A: AsRef<[SimValue]>, T: TableContext> ArbitraryFrom<(&T, A, bool)> for Sim
fn arbitrary_from<R: Rng>(rng: &mut R, (table, row, predicate_value): (&T, A, bool)) -> Self {
let row = row.as_ref();
// Pick an operator
let choice = rng.gen_range(0..2);
let choice = rng.random_range(0..2);
// Pick an operator
match predicate_value {
true => match choice {
@@ -46,7 +46,7 @@ impl<T: TableContext> ArbitraryFrom<(&T, bool)> for CompoundPredicate {
impl<T: TableContext> ArbitraryFrom<&T> for Predicate {
fn arbitrary_from<R: Rng>(rng: &mut R, table: &T) -> Self {
let predicate_value = rng.gen_bool(0.5);
let predicate_value = rng.random_bool(0.5);
Predicate::arbitrary_from(rng, (table, predicate_value)).parens()
}
}
@@ -70,11 +70,11 @@ impl ArbitraryFrom<(&Table, &Vec<SimValue>)> for Predicate {
// are true, some that are false, combiend them in ways that correspond to the creation of a true predicate
// Produce some true and false predicates
let mut true_predicates = (1..=rng.gen_range(1..=4))
let mut true_predicates = (1..=rng.random_range(1..=4))
.map(|_| Predicate::true_binary(rng, t, row))
.collect::<Vec<_>>();
let false_predicates = (0..=rng.gen_range(0..=3))
let false_predicates = (0..=rng.random_range(0..=3))
.map(|_| Predicate::false_binary(rng, t, row))
.collect::<Vec<_>>();
@@ -92,7 +92,7 @@ impl ArbitraryFrom<(&Table, &Vec<SimValue>)> for Predicate {
while !predicates.is_empty() {
// Create a new predicate from at least 1 and at most 3 predicates
let context =
predicates[0..rng.gen_range(0..=usize::min(3, predicates.len()))].to_vec();
predicates[0..rng.random_range(0..=usize::min(3, predicates.len()))].to_vec();
// Shift `predicates` to remove the predicates in the context
predicates = predicates[context.len()..].to_vec();
@@ -251,7 +251,7 @@ mod tests {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
for _ in 0..10000 {
let table = Table::arbitrary(&mut rng);
let num_rows = rng.gen_range(1..10);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table
@@ -277,7 +277,7 @@ mod tests {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
for _ in 0..10000 {
let table = Table::arbitrary(&mut rng);
let num_rows = rng.gen_range(1..10);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table
@@ -303,7 +303,7 @@ mod tests {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
for _ in 0..10000 {
let table = Table::arbitrary(&mut rng);
let num_rows = rng.gen_range(1..10);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table
@@ -329,7 +329,7 @@ mod tests {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
for _ in 0..10000 {
let mut table = Table::arbitrary(&mut rng);
let num_rows = rng.gen_range(1..10);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table
@@ -356,7 +356,7 @@ mod tests {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
for _ in 0..10000 {
let mut table = Table::arbitrary(&mut rng);
let num_rows = rng.gen_range(1..10);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table

View File

@@ -2,7 +2,7 @@
//! TODO: for now just generating [ast::Literal], but want to also generate Columns and any
//! arbitrary [ast::Expr]
use turso_sqlite3_parser::ast::{self, Expr};
use turso_parser::ast::{self, Expr};
use crate::{
generation::{backtrack, pick, predicate::SimplePredicate, ArbitraryFromMaybe},
@@ -64,6 +64,7 @@ impl ArbitraryFromMaybe<&Vec<&SimValue>> for FalseValue {
}
}
#[allow(dead_code)]
pub struct BitNotValue(pub SimValue);
impl ArbitraryFromMaybe<(&SimValue, bool)> for BitNotValue {
@@ -107,7 +108,7 @@ impl SimplePredicate {
) -> Self {
let columns = table.columns().collect::<Vec<_>>();
// Pick a random column
let column_index = rng.gen_range(0..columns.len());
let column_index = rng.random_range(0..columns.len());
let column_value = &row[column_index];
let num_retries = row.len();
// Avoid creation of NULLs
@@ -173,7 +174,7 @@ impl SimplePredicate {
) -> Self {
let columns = table.columns().collect::<Vec<_>>();
// Pick a random column
let column_index = rng.gen_range(0..columns.len());
let column_index = rng.random_range(0..columns.len());
let column_value = &row[column_index];
let num_retries = row.len();
// Avoid creation of NULLs
@@ -255,7 +256,7 @@ mod tests {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
for _ in 0..10000 {
let mut table = Table::arbitrary(&mut rng);
let num_rows = rng.gen_range(1..10);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table
@@ -283,7 +284,7 @@ mod tests {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
for _ in 0..10000 {
let mut table = Table::arbitrary(&mut rng);
let num_rows = rng.gen_range(1..10);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table

View File

@@ -0,0 +1,391 @@
use crate::generation::{
gen_random_text, pick_n_unique, pick_unique, Arbitrary, ArbitraryFrom, ArbitrarySizedFrom,
GenerationContext,
};
use crate::model::query::predicate::Predicate;
use crate::model::query::select::{
CompoundOperator, CompoundSelect, Distinctness, FromClause, OrderBy, ResultColumn, SelectBody,
SelectInner,
};
use crate::model::query::update::Update;
use crate::model::query::{Create, CreateIndex, Delete, Drop, Insert, Select};
use crate::model::table::{JoinTable, JoinType, JoinedTable, SimValue, Table, TableContext};
use itertools::Itertools;
use rand::Rng;
use turso_parser::ast::{Expr, SortOrder};
use super::{backtrack, pick};
impl Arbitrary for Create {
fn arbitrary<R: Rng>(rng: &mut R) -> Self {
Create {
table: Table::arbitrary(rng),
}
}
}
impl ArbitraryFrom<&Vec<Table>> for FromClause {
fn arbitrary_from<R: Rng>(rng: &mut R, tables: &Vec<Table>) -> Self {
let num_joins = match rng.random_range(0..=100) {
0..=90 => 0,
91..=97 => 1,
98..=100 => 2,
_ => unreachable!(),
};
let mut tables = tables.clone();
let mut table = pick(&tables, rng).clone();
tables.retain(|t| t.name != table.name);
let name = table.name.clone();
let mut table_context = JoinTable {
tables: Vec::new(),
rows: Vec::new(),
};
let joins: Vec<_> = (0..num_joins)
.filter_map(|_| {
if tables.is_empty() {
return None;
}
let join_table = pick(&tables, rng).clone();
let joined_table_name = join_table.name.clone();
tables.retain(|t| t.name != join_table.name);
table_context.rows = table_context
.rows
.iter()
.cartesian_product(join_table.rows.iter())
.map(|(t_row, j_row)| {
let mut row = t_row.clone();
row.extend(j_row.clone());
row
})
.collect();
// TODO: inneficient. use a Deque to push_front?
table_context.tables.insert(0, join_table);
for row in &mut table.rows {
assert_eq!(
row.len(),
table.columns.len(),
"Row length does not match column length after join"
);
}
let predicate = Predicate::arbitrary_from(rng, &table);
Some(JoinedTable {
table: joined_table_name,
join_type: JoinType::Inner,
on: predicate,
})
})
.collect();
FromClause { table: name, joins }
}
}
impl<C: GenerationContext> ArbitraryFrom<&C> for SelectInner {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
let from = FromClause::arbitrary_from(rng, env.tables());
let tables = env.tables().clone();
let join_table = from.into_join_table(&tables);
let cuml_col_count = join_table.columns().count();
let order_by = 'order_by: {
if rng.random_bool(0.3) {
let order_by_table_candidates = from
.joins
.iter()
.map(|j| j.table.clone())
.chain(std::iter::once(from.table.clone()))
.collect::<Vec<_>>();
let order_by_col_count =
(rng.random::<f64>() * rng.random::<f64>() * (cuml_col_count as f64)) as usize; // skew towards 0
if order_by_col_count == 0 {
break 'order_by None;
}
let mut col_names = std::collections::HashSet::new();
let mut order_by_cols = Vec::new();
while order_by_cols.len() < order_by_col_count {
let table = pick(&order_by_table_candidates, rng);
let table = tables.iter().find(|t| t.name == *table).unwrap();
let col = pick(&table.columns, rng);
let col_name = format!("{}.{}", table.name, col.name);
if col_names.insert(col_name.clone()) {
order_by_cols.push((
col_name,
if rng.random_bool(0.5) {
SortOrder::Asc
} else {
SortOrder::Desc
},
));
}
}
Some(OrderBy {
columns: order_by_cols,
})
} else {
None
}
};
SelectInner {
distinctness: if env.opts().indexes {
Distinctness::arbitrary(rng)
} else {
Distinctness::All
},
columns: vec![ResultColumn::Star],
from: Some(from),
where_clause: Predicate::arbitrary_from(rng, &join_table),
order_by,
}
}
}
impl<C: GenerationContext> ArbitrarySizedFrom<&C> for SelectInner {
fn arbitrary_sized_from<R: Rng>(rng: &mut R, env: &C, num_result_columns: usize) -> Self {
let mut select_inner = SelectInner::arbitrary_from(rng, env);
let select_from = &select_inner.from.as_ref().unwrap();
let table_names = select_from
.joins
.iter()
.map(|j| j.table.clone())
.chain(std::iter::once(select_from.table.clone()))
.collect::<Vec<_>>();
let flat_columns_names = table_names
.iter()
.flat_map(|t| {
env.tables()
.iter()
.find(|table| table.name == *t)
.unwrap()
.columns
.iter()
.map(|c| format!("{}.{}", t.clone(), c.name))
})
.collect::<Vec<_>>();
let selected_columns = pick_unique(&flat_columns_names, num_result_columns, rng);
let mut columns = Vec::new();
for column_name in selected_columns {
columns.push(ResultColumn::Column(column_name.clone()));
}
select_inner.columns = columns;
select_inner
}
}
impl Arbitrary for Distinctness {
fn arbitrary<R: Rng>(rng: &mut R) -> Self {
match rng.random_range(0..=5) {
0..4 => Distinctness::All,
_ => Distinctness::Distinct,
}
}
}
impl Arbitrary for CompoundOperator {
fn arbitrary<R: Rng>(rng: &mut R) -> Self {
match rng.random_range(0..=1) {
0 => CompoundOperator::Union,
1 => CompoundOperator::UnionAll,
_ => unreachable!(),
}
}
}
/// SelectFree is a wrapper around Select that allows for arbitrary generation
/// of selects without requiring a specific environment, which is useful for generating
/// arbitrary expressions without referring to the tables.
pub struct SelectFree(pub Select);
impl<C: GenerationContext> ArbitraryFrom<&C> for SelectFree {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
let expr = Predicate(Expr::arbitrary_sized_from(rng, env, 8));
let select = Select::expr(expr);
Self(select)
}
}
impl<C: GenerationContext> ArbitraryFrom<&C> for Select {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
// Generate a number of selects based on the query size
// If experimental indexes are enabled, we can have selects with compounds
// Otherwise, we just have a single select with no compounds
let num_compound_selects = if env.opts().indexes {
match rng.random_range(0..=100) {
0..=95 => 0,
96..=99 => 1,
100 => 2,
_ => unreachable!(),
}
} else {
0
};
let min_column_count_across_tables =
env.tables().iter().map(|t| t.columns.len()).min().unwrap();
let num_result_columns = rng.random_range(1..=min_column_count_across_tables);
let mut first = SelectInner::arbitrary_sized_from(rng, env, num_result_columns);
let mut rest: Vec<SelectInner> = (0..num_compound_selects)
.map(|_| SelectInner::arbitrary_sized_from(rng, env, num_result_columns))
.collect();
if !rest.is_empty() {
// ORDER BY is not supported in compound selects yet
first.order_by = None;
for s in &mut rest {
s.order_by = None;
}
}
Self {
body: SelectBody {
select: Box::new(first),
compounds: rest
.into_iter()
.map(|s| CompoundSelect {
operator: CompoundOperator::arbitrary(rng),
select: Box::new(s),
})
.collect(),
},
limit: None,
}
}
}
impl<C: GenerationContext> ArbitraryFrom<&C> for Insert {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
let gen_values = |rng: &mut R| {
let table = pick(env.tables(), rng);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table
.columns
.iter()
.map(|c| SimValue::arbitrary_from(rng, &c.column_type))
.collect()
})
.collect();
Some(Insert::Values {
table: table.name.clone(),
values,
})
};
let _gen_select = |rng: &mut R| {
// Find a non-empty table
let select_table = env.tables().iter().find(|t| !t.rows.is_empty())?;
let row = pick(&select_table.rows, rng);
let predicate = Predicate::arbitrary_from(rng, (select_table, row));
// Pick another table to insert into
let select = Select::simple(select_table.name.clone(), predicate);
let table = pick(env.tables(), rng);
Some(Insert::Select {
table: table.name.clone(),
select: Box::new(select),
})
};
// TODO: Add back gen_select when https://github.com/tursodatabase/turso/issues/2129 is fixed.
// Backtrack here cannot return None
backtrack(vec![(1, Box::new(gen_values))], rng).unwrap()
}
}
impl<C: GenerationContext> ArbitraryFrom<&C> for Delete {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
let table = pick(env.tables(), rng);
Self {
table: table.name.clone(),
predicate: Predicate::arbitrary_from(rng, table),
}
}
}
impl<C: GenerationContext> ArbitraryFrom<&C> for Drop {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
let table = pick(env.tables(), rng);
Self {
table: table.name.clone(),
}
}
}
impl<C: GenerationContext> ArbitraryFrom<&C> for CreateIndex {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
assert!(
!env.tables().is_empty(),
"Cannot create an index when no tables exist in the environment."
);
let table = pick(env.tables(), rng);
if table.columns.is_empty() {
panic!(
"Cannot create an index on table '{}' as it has no columns.",
table.name
);
}
let num_columns_to_pick = rng.random_range(1..=table.columns.len());
let picked_column_indices = pick_n_unique(0..table.columns.len(), num_columns_to_pick, rng);
let columns = picked_column_indices
.into_iter()
.map(|i| {
let column = &table.columns[i];
(
column.name.clone(),
if rng.random_bool(0.5) {
SortOrder::Asc
} else {
SortOrder::Desc
},
)
})
.collect::<Vec<(String, SortOrder)>>();
let index_name = format!(
"idx_{}_{}",
table.name,
gen_random_text(rng).chars().take(8).collect::<String>()
);
CreateIndex {
index_name,
table_name: table.name.clone(),
columns,
}
}
}
impl<C: GenerationContext> ArbitraryFrom<&C> for Update {
fn arbitrary_from<R: Rng>(rng: &mut R, env: &C) -> Self {
let table = pick(env.tables(), rng);
let num_cols = rng.random_range(1..=table.columns.len());
let columns = pick_unique(&table.columns, num_cols, rng);
let set_values: Vec<(String, SimValue)> = columns
.iter()
.map(|column| {
(
column.name.clone(),
SimValue::arbitrary_from(rng, &column.column_type),
)
})
.collect();
Update {
table: table.name.clone(),
set_values,
predicate: Predicate::arbitrary_from(rng, table),
}
}
}

View File

@@ -19,11 +19,11 @@ impl Arbitrary for Table {
fn arbitrary<R: Rng>(rng: &mut R) -> Self {
let name = Name::arbitrary(rng).0;
let columns = loop {
let large_table = rng.gen_bool(0.1);
let large_table = rng.random_bool(0.1);
let column_size = if large_table {
rng.gen_range(64..125) // todo: make this higher (128+)
rng.random_range(64..125) // todo: make this higher (128+)
} else {
rng.gen_range(1..=10)
rng.random_range(1..=10)
};
let columns = (1..=column_size)
.map(|_| Column::arbitrary(rng))
@@ -90,8 +90,8 @@ impl ArbitraryFrom<&Vec<&SimValue>> for SimValue {
impl ArbitraryFrom<&ColumnType> for SimValue {
fn arbitrary_from<R: Rng>(rng: &mut R, column_type: &ColumnType) -> Self {
let value = match column_type {
ColumnType::Integer => Value::Integer(rng.gen_range(i64::MIN..i64::MAX)),
ColumnType::Float => Value::Float(rng.gen_range(-1e10..1e10)),
ColumnType::Integer => Value::Integer(rng.random_range(i64::MIN..i64::MAX)),
ColumnType::Float => Value::Float(rng.random_range(-1e10..1e10)),
ColumnType::Text => Value::build_text(gen_random_text(rng)),
ColumnType::Blob => Value::Blob(gen_random_text(rng).as_bytes().to_vec()),
};
@@ -99,7 +99,7 @@ impl ArbitraryFrom<&ColumnType> for SimValue {
}
}
pub(crate) struct LTValue(pub(crate) SimValue);
pub struct LTValue(pub SimValue);
impl ArbitraryFrom<&Vec<&SimValue>> for LTValue {
fn arbitrary_from<R: Rng>(rng: &mut R, values: &Vec<&SimValue>) -> Self {
@@ -116,21 +116,21 @@ impl ArbitraryFrom<&Vec<&SimValue>> for LTValue {
impl ArbitraryFrom<&SimValue> for LTValue {
fn arbitrary_from<R: Rng>(rng: &mut R, value: &SimValue) -> Self {
let new_value = match &value.0 {
Value::Integer(i) => Value::Integer(rng.gen_range(i64::MIN..*i - 1)),
Value::Float(f) => Value::Float(f - rng.gen_range(0.0..1e10)),
Value::Integer(i) => Value::Integer(rng.random_range(i64::MIN..*i - 1)),
Value::Float(f) => Value::Float(f - rng.random_range(0.0..1e10)),
value @ Value::Text(..) => {
// Either shorten the string, or make at least one character smaller and mutate the rest
let mut t = value.to_string();
if rng.gen_bool(0.01) {
if rng.random_bool(0.01) {
t.pop();
Value::build_text(t)
} else {
let mut t = t.chars().map(|c| c as u32).collect::<Vec<_>>();
let index = rng.gen_range(0..t.len());
let index = rng.random_range(0..t.len());
t[index] -= 1;
// Mutate the rest of the string
for val in t.iter_mut().skip(index + 1) {
*val = rng.gen_range('a' as u32..='z' as u32);
*val = rng.random_range('a' as u32..='z' as u32);
}
let t = t
.into_iter()
@@ -142,15 +142,15 @@ impl ArbitraryFrom<&SimValue> for LTValue {
Value::Blob(b) => {
// Either shorten the blob, or make at least one byte smaller and mutate the rest
let mut b = b.clone();
if rng.gen_bool(0.01) {
if rng.random_bool(0.01) {
b.pop();
Value::Blob(b)
} else {
let index = rng.gen_range(0..b.len());
let index = rng.random_range(0..b.len());
b[index] -= 1;
// Mutate the rest of the blob
for val in b.iter_mut().skip(index + 1) {
*val = rng.gen_range(0..=255);
*val = rng.random_range(0..=255);
}
Value::Blob(b)
}
@@ -161,7 +161,7 @@ impl ArbitraryFrom<&SimValue> for LTValue {
}
}
pub(crate) struct GTValue(pub(crate) SimValue);
pub struct GTValue(pub SimValue);
impl ArbitraryFrom<&Vec<&SimValue>> for GTValue {
fn arbitrary_from<R: Rng>(rng: &mut R, values: &Vec<&SimValue>) -> Self {
@@ -178,21 +178,21 @@ impl ArbitraryFrom<&Vec<&SimValue>> for GTValue {
impl ArbitraryFrom<&SimValue> for GTValue {
fn arbitrary_from<R: Rng>(rng: &mut R, value: &SimValue) -> Self {
let new_value = match &value.0 {
Value::Integer(i) => Value::Integer(rng.gen_range(*i..i64::MAX)),
Value::Float(f) => Value::Float(rng.gen_range(*f..1e10)),
Value::Integer(i) => Value::Integer(rng.random_range(*i..i64::MAX)),
Value::Float(f) => Value::Float(rng.random_range(*f..1e10)),
value @ Value::Text(..) => {
// Either lengthen the string, or make at least one character smaller and mutate the rest
let mut t = value.to_string();
if rng.gen_bool(0.01) {
t.push(rng.gen_range(0..=255) as u8 as char);
if rng.random_bool(0.01) {
t.push(rng.random_range(0..=255) as u8 as char);
Value::build_text(t)
} else {
let mut t = t.chars().map(|c| c as u32).collect::<Vec<_>>();
let index = rng.gen_range(0..t.len());
let index = rng.random_range(0..t.len());
t[index] += 1;
// Mutate the rest of the string
for val in t.iter_mut().skip(index + 1) {
*val = rng.gen_range('a' as u32..='z' as u32);
*val = rng.random_range('a' as u32..='z' as u32);
}
let t = t
.into_iter()
@@ -204,15 +204,15 @@ impl ArbitraryFrom<&SimValue> for GTValue {
Value::Blob(b) => {
// Either lengthen the blob, or make at least one byte smaller and mutate the rest
let mut b = b.clone();
if rng.gen_bool(0.01) {
b.push(rng.gen_range(0..=255));
if rng.random_bool(0.01) {
b.push(rng.random_range(0..=255));
Value::Blob(b)
} else {
let index = rng.gen_range(0..b.len());
let index = rng.random_range(0..b.len());
b[index] += 1;
// Mutate the rest of the blob
for val in b.iter_mut().skip(index + 1) {
*val = rng.gen_range(0..=255);
*val = rng.random_range(0..=255);
}
Value::Blob(b)
}
@@ -223,7 +223,7 @@ impl ArbitraryFrom<&SimValue> for GTValue {
}
}
pub(crate) struct LikeValue(pub(crate) SimValue);
pub struct LikeValue(pub SimValue);
impl ArbitraryFromMaybe<&SimValue> for LikeValue {
fn arbitrary_from_maybe<R: Rng>(rng: &mut R, value: &SimValue) -> Option<Self> {
@@ -235,18 +235,18 @@ impl ArbitraryFromMaybe<&SimValue> for LikeValue {
// insert one `%` for the whole substring
let mut i = 0;
while i < t.len() {
if rng.gen_bool(0.1) {
if rng.random_bool(0.1) {
t[i] = '_';
} else if rng.gen_bool(0.05) {
} else if rng.random_bool(0.05) {
t[i] = '%';
// skip a list of characters
for _ in 0..rng.gen_range(0..=3.min(t.len() - i - 1)) {
for _ in 0..rng.random_range(0..=3.min(t.len() - i - 1)) {
t.remove(i + 1);
}
}
i += 1;
}
let index = rng.gen_range(0..t.len());
let index = rng.random_range(0..t.len());
t.insert(index, '%');
Some(Self(SimValue(Value::build_text(
t.into_iter().collect::<String>(),

2
sql_generation/lib.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod generation;
pub mod model;

View File

@@ -0,0 +1,2 @@
pub mod query;
pub mod table;

View File

@@ -0,0 +1,25 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use crate::model::table::Table;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Create {
pub table: Table,
}
impl Display for Create {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CREATE TABLE {} (", self.table.name)?;
for (i, column) in self.table.columns.iter().enumerate() {
if i != 0 {
write!(f, ",")?;
}
write!(f, "{} {}", column.name, column.column_type)?;
}
write!(f, ")")
}
}

View File

@@ -0,0 +1,25 @@
use serde::{Deserialize, Serialize};
use turso_parser::ast::SortOrder;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct CreateIndex {
pub index_name: String,
pub table_name: String,
pub columns: Vec<(String, SortOrder)>,
}
impl std::fmt::Display for CreateIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"CREATE INDEX {} ON {} ({})",
self.index_name,
self.table_name,
self.columns
.iter()
.map(|(name, order)| format!("{name} {order}"))
.collect::<Vec<String>>()
.join(", ")
)
}
}

View File

@@ -0,0 +1,17 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use super::predicate::Predicate;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Delete {
pub table: String,
pub predicate: Predicate,
}
impl Display for Delete {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "DELETE FROM {} WHERE {}", self.table, self.predicate)
}
}

View File

@@ -0,0 +1,14 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Drop {
pub table: String,
}
impl Display for Drop {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "DROP TABLE {}", self.table)
}
}

View File

@@ -2,12 +2,12 @@ use std::fmt::Display;
use serde::{Deserialize, Serialize};
use crate::{generation::Shadow, model::table::SimValue, runner::env::SimulatorTables};
use crate::model::table::SimValue;
use super::select::Select;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub(crate) enum Insert {
pub enum Insert {
Values {
table: String,
values: Vec<Vec<SimValue>>,
@@ -18,40 +18,8 @@ pub(crate) enum Insert {
},
}
impl Shadow for Insert {
type Result = anyhow::Result<Vec<Vec<SimValue>>>;
fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result {
match self {
Insert::Values { table, values } => {
if let Some(t) = tables.tables.iter_mut().find(|t| &t.name == table) {
t.rows.extend(values.clone());
} else {
return Err(anyhow::anyhow!(
"Table {} does not exist. INSERT statement ignored.",
table
));
}
}
Insert::Select { table, select } => {
let rows = select.shadow(tables)?;
if let Some(t) = tables.tables.iter_mut().find(|t| &t.name == table) {
t.rows.extend(rows);
} else {
return Err(anyhow::anyhow!(
"Table {} does not exist. INSERT statement ignored.",
table
));
}
}
}
Ok(vec![])
}
}
impl Insert {
pub(crate) fn table(&self) -> &str {
pub fn table(&self) -> &str {
match self {
Insert::Values { table, .. } | Insert::Select { table, .. } => table,
}

View File

@@ -0,0 +1,34 @@
pub use create::Create;
pub use create_index::CreateIndex;
pub use delete::Delete;
pub use drop::Drop;
pub use insert::Insert;
pub use select::Select;
use turso_parser::ast::fmt::ToSqlContext;
pub mod create;
pub mod create_index;
pub mod delete;
pub mod drop;
pub mod insert;
pub mod predicate;
pub mod select;
pub mod transaction;
pub mod update;
/// Used to print sql strings that already have all the context it needs
pub struct EmptyContext;
impl ToSqlContext for EmptyContext {
fn get_column_name(
&self,
_table_id: turso_parser::ast::TableInternalId,
_col_idx: usize,
) -> String {
unreachable!()
}
fn get_table_name(&self, _id: turso_parser::ast::TableInternalId) -> &str {
unreachable!()
}
}

View File

@@ -1,7 +1,7 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use turso_sqlite3_parser::ast::{self, fmt::ToTokens};
use turso_parser::ast::{self, fmt::ToTokens};
use crate::model::table::{SimValue, Table, TableContext};
@@ -9,27 +9,28 @@ use crate::model::table::{SimValue, Table, TableContext};
pub struct Predicate(pub ast::Expr);
impl Predicate {
pub(crate) fn true_() -> Self {
pub fn true_() -> Self {
Self(ast::Expr::Literal(ast::Literal::Keyword(
"TRUE".to_string(),
)))
}
pub(crate) fn false_() -> Self {
pub fn false_() -> Self {
Self(ast::Expr::Literal(ast::Literal::Keyword(
"FALSE".to_string(),
)))
}
pub(crate) fn null() -> Self {
pub fn null() -> Self {
Self(ast::Expr::Literal(ast::Literal::Null))
}
pub(crate) fn not(predicate: Predicate) -> Self {
#[allow(clippy::should_implement_trait)]
pub fn not(predicate: Predicate) -> Self {
let expr = ast::Expr::Unary(ast::UnaryOperator::Not, Box::new(predicate.0));
Self(expr).parens()
}
pub(crate) fn and(predicates: Vec<Predicate>) -> Self {
pub fn and(predicates: Vec<Predicate>) -> Self {
if predicates.is_empty() {
Self::true_()
} else if predicates.len() == 1 {
@@ -44,7 +45,7 @@ impl Predicate {
}
}
pub(crate) fn or(predicates: Vec<Predicate>) -> Self {
pub fn or(predicates: Vec<Predicate>) -> Self {
if predicates.is_empty() {
Self::false_()
} else if predicates.len() == 1 {
@@ -59,26 +60,26 @@ impl Predicate {
}
}
pub(crate) fn eq(lhs: Predicate, rhs: Predicate) -> Self {
pub fn eq(lhs: Predicate, rhs: Predicate) -> Self {
let expr = ast::Expr::Binary(Box::new(lhs.0), ast::Operator::Equals, Box::new(rhs.0));
Self(expr).parens()
}
pub(crate) fn is(lhs: Predicate, rhs: Predicate) -> Self {
pub fn is(lhs: Predicate, rhs: Predicate) -> Self {
let expr = ast::Expr::Binary(Box::new(lhs.0), ast::Operator::Is, Box::new(rhs.0));
Self(expr).parens()
}
pub(crate) fn parens(self) -> Self {
pub fn parens(self) -> Self {
let expr = ast::Expr::Parenthesized(vec![self.0]);
Self(expr)
}
pub(crate) fn eval(&self, row: &[SimValue], table: &Table) -> Option<SimValue> {
pub fn eval(&self, row: &[SimValue], table: &Table) -> Option<SimValue> {
expr_to_value(&self.0, row, table)
}
pub(crate) fn test<T: TableContext>(&self, row: &[SimValue], table: &T) -> bool {
pub fn test<T: TableContext>(&self, row: &[SimValue], table: &T) -> bool {
let value = expr_to_value(&self.0, row, table);
value.is_some_and(|value| value.as_bool())
}

View File

@@ -0,0 +1,378 @@
use std::{collections::HashSet, fmt::Display};
pub use ast::Distinctness;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use turso_parser::ast::{self, fmt::ToTokens, SortOrder};
use crate::model::{
query::EmptyContext,
table::{JoinTable, JoinType, JoinedTable, Table},
};
use super::predicate::Predicate;
/// `SELECT` or `RETURNING` result column
// https://sqlite.org/syntax/result-column.html
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
pub enum ResultColumn {
/// expression
Expr(Predicate),
/// `*`
Star,
/// column name
Column(String),
}
impl Display for ResultColumn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResultColumn::Expr(expr) => write!(f, "({expr})"),
ResultColumn::Star => write!(f, "*"),
ResultColumn::Column(name) => write!(f, "{name}"),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Select {
pub body: SelectBody,
pub limit: Option<usize>,
}
impl Select {
pub fn simple(table: String, where_clause: Predicate) -> Self {
Self::single(
table,
vec![ResultColumn::Star],
where_clause,
None,
Distinctness::All,
)
}
pub fn expr(expr: Predicate) -> Self {
Select {
body: SelectBody {
select: Box::new(SelectInner {
distinctness: Distinctness::All,
columns: vec![ResultColumn::Expr(expr)],
from: None,
where_clause: Predicate::true_(),
order_by: None,
}),
compounds: Vec::new(),
},
limit: None,
}
}
pub fn single(
table: String,
result_columns: Vec<ResultColumn>,
where_clause: Predicate,
limit: Option<usize>,
distinct: Distinctness,
) -> Self {
Select {
body: SelectBody {
select: Box::new(SelectInner {
distinctness: distinct,
columns: result_columns,
from: Some(FromClause {
table,
joins: Vec::new(),
}),
where_clause,
order_by: None,
}),
compounds: Vec::new(),
},
limit,
}
}
pub fn compound(left: Select, right: Select, operator: CompoundOperator) -> Self {
let mut body = left.body;
body.compounds.push(CompoundSelect {
operator,
select: Box::new(right.body.select.as_ref().clone()),
});
Select {
body,
limit: left.limit.or(right.limit),
}
}
pub fn dependencies(&self) -> HashSet<String> {
if self.body.select.from.is_none() {
return HashSet::new();
}
let from = self.body.select.from.as_ref().unwrap();
let mut tables = HashSet::new();
tables.insert(from.table.clone());
tables.extend(from.dependencies());
for compound in &self.body.compounds {
tables.extend(
compound
.select
.from
.as_ref()
.map(|f| f.dependencies())
.unwrap_or(vec![])
.into_iter(),
);
}
tables
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SelectBody {
/// first select
pub select: Box<SelectInner>,
/// compounds
pub compounds: Vec<CompoundSelect>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct OrderBy {
pub columns: Vec<(String, SortOrder)>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SelectInner {
/// `DISTINCT`
pub distinctness: Distinctness,
/// columns
pub columns: Vec<ResultColumn>,
/// `FROM` clause
pub from: Option<FromClause>,
/// `WHERE` clause
pub where_clause: Predicate,
/// `ORDER BY` clause
pub order_by: Option<OrderBy>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum CompoundOperator {
/// `UNION`
Union,
/// `UNION ALL`
UnionAll,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CompoundSelect {
/// operator
pub operator: CompoundOperator,
/// select
pub select: Box<SelectInner>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FromClause {
/// table
pub table: String,
/// `JOIN`ed tables
pub joins: Vec<JoinedTable>,
}
impl FromClause {
fn to_sql_ast(&self) -> ast::FromClause {
ast::FromClause {
select: Box::new(ast::SelectTable::Table(
ast::QualifiedName::single(ast::Name::new(&self.table)),
None,
None,
)),
joins: self
.joins
.iter()
.map(|join| ast::JoinedSelectTable {
operator: match join.join_type {
JoinType::Inner => ast::JoinOperator::TypedJoin(Some(ast::JoinType::INNER)),
JoinType::Left => ast::JoinOperator::TypedJoin(Some(ast::JoinType::LEFT)),
JoinType::Right => ast::JoinOperator::TypedJoin(Some(ast::JoinType::RIGHT)),
JoinType::Full => ast::JoinOperator::TypedJoin(Some(ast::JoinType::OUTER)),
JoinType::Cross => ast::JoinOperator::TypedJoin(Some(ast::JoinType::CROSS)),
},
table: Box::new(ast::SelectTable::Table(
ast::QualifiedName::single(ast::Name::new(&join.table)),
None,
None,
)),
constraint: Some(ast::JoinConstraint::On(Box::new(join.on.0.clone()))),
})
.collect(),
}
}
pub fn dependencies(&self) -> Vec<String> {
let mut deps = vec![self.table.clone()];
for join in &self.joins {
deps.push(join.table.clone());
}
deps
}
pub fn into_join_table(&self, tables: &[Table]) -> JoinTable {
let first_table = tables
.iter()
.find(|t| t.name == self.table)
.expect("Table not found");
let mut join_table = JoinTable {
tables: vec![first_table.clone()],
rows: Vec::new(),
};
for join in &self.joins {
let joined_table = tables
.iter()
.find(|t| t.name == join.table)
.expect("Joined table not found");
join_table.tables.push(joined_table.clone());
match join.join_type {
JoinType::Inner => {
// Implement inner join logic
let join_rows = joined_table
.rows
.iter()
.filter(|row| join.on.test(row, joined_table))
.cloned()
.collect::<Vec<_>>();
// take a cartesian product of the rows
let all_row_pairs = join_table
.rows
.clone()
.into_iter()
.cartesian_product(join_rows.iter());
for (row1, row2) in all_row_pairs {
let row = row1.iter().chain(row2.iter()).cloned().collect::<Vec<_>>();
let is_in = join.on.test(&row, &join_table);
if is_in {
join_table.rows.push(row);
}
}
}
_ => todo!(),
}
}
join_table
}
}
impl Select {
pub fn to_sql_ast(&self) -> ast::Select {
ast::Select {
with: None,
body: ast::SelectBody {
select: ast::OneSelect::Select {
distinctness: if self.body.select.distinctness == Distinctness::Distinct {
Some(ast::Distinctness::Distinct)
} else {
None
},
columns: self
.body
.select
.columns
.iter()
.map(|col| match col {
ResultColumn::Expr(expr) => {
ast::ResultColumn::Expr(expr.0.clone().into_boxed(), None)
}
ResultColumn::Star => ast::ResultColumn::Star,
ResultColumn::Column(name) => ast::ResultColumn::Expr(
ast::Expr::Id(ast::Name::Ident(name.clone())).into_boxed(),
None,
),
})
.collect(),
from: self.body.select.from.as_ref().map(|f| f.to_sql_ast()),
where_clause: Some(self.body.select.where_clause.0.clone().into_boxed()),
group_by: None,
window_clause: Vec::new(),
},
compounds: self
.body
.compounds
.iter()
.map(|compound| ast::CompoundSelect {
operator: match compound.operator {
CompoundOperator::Union => ast::CompoundOperator::Union,
CompoundOperator::UnionAll => ast::CompoundOperator::UnionAll,
},
select: ast::OneSelect::Select {
distinctness: Some(compound.select.distinctness),
columns: compound
.select
.columns
.iter()
.map(|col| match col {
ResultColumn::Expr(expr) => {
ast::ResultColumn::Expr(expr.0.clone().into_boxed(), None)
}
ResultColumn::Star => ast::ResultColumn::Star,
ResultColumn::Column(name) => ast::ResultColumn::Expr(
ast::Expr::Id(ast::Name::Ident(name.clone())).into_boxed(),
None,
),
})
.collect(),
from: compound.select.from.as_ref().map(|f| f.to_sql_ast()),
where_clause: Some(compound.select.where_clause.0.clone().into_boxed()),
group_by: None,
window_clause: Vec::new(),
},
})
.collect(),
},
order_by: self
.body
.select
.order_by
.as_ref()
.map(|o| {
o.columns
.iter()
.map(|(name, order)| ast::SortedColumn {
expr: ast::Expr::Id(ast::Name::Ident(name.clone())).into_boxed(),
order: match order {
SortOrder::Asc => Some(ast::SortOrder::Asc),
SortOrder::Desc => Some(ast::SortOrder::Desc),
},
nulls: None,
})
.collect()
})
.unwrap_or_default(),
limit: self.limit.map(|l| ast::Limit {
expr: ast::Expr::Literal(ast::Literal::Numeric(l.to_string())).into_boxed(),
offset: None,
}),
}
}
}
impl Display for Select {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_sql_ast().to_fmt_with_context(f, &EmptyContext {})
}
}
#[cfg(test)]
mod select_tests {
#[test]
fn test_select_display() {}
}

View File

@@ -0,0 +1,32 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Begin {
pub immediate: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Commit;
#[derive(Debug, Clone, Serialize, Deserialize)]
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 { "" })
}
}
impl Display for Commit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "COMMIT")
}
}
impl Display for Rollback {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ROLLBACK")
}
}

View File

@@ -0,0 +1,34 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use crate::model::table::SimValue;
use super::predicate::Predicate;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Update {
pub table: String,
pub set_values: Vec<(String, SimValue)>, // Pair of value for set expressions => SET name=value
pub predicate: Predicate,
}
impl Update {
pub fn table(&self) -> &str {
&self.table
}
}
impl Display for Update {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "UPDATE {} SET ", self.table)?;
for (i, (name, value)) in self.set_values.iter().enumerate() {
if i != 0 {
write!(f, ", ")?;
}
write!(f, "{name} = {value}")?;
}
write!(f, " WHERE {}", self.predicate)?;
Ok(())
}
}

View File

@@ -2,11 +2,11 @@ use std::{fmt::Display, hash::Hash, ops::Deref};
use serde::{Deserialize, Serialize};
use turso_core::{numeric::Numeric, types};
use turso_sqlite3_parser::ast;
use turso_parser::ast;
use crate::model::query::predicate::Predicate;
pub(crate) struct Name(pub(crate) String);
pub struct Name(pub String);
impl Deref for Name {
type Target = str;
@@ -41,11 +41,11 @@ impl TableContext for Table {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Table {
pub(crate) name: String,
pub(crate) columns: Vec<Column>,
pub(crate) rows: Vec<Vec<SimValue>>,
pub(crate) indexes: Vec<String>,
pub struct Table {
pub name: String,
pub columns: Vec<Column>,
pub rows: Vec<Vec<SimValue>>,
pub indexes: Vec<String>,
}
impl Table {
@@ -60,11 +60,11 @@ impl Table {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Column {
pub(crate) name: String,
pub(crate) column_type: ColumnType,
pub(crate) primary: bool,
pub(crate) unique: bool,
pub struct Column {
pub name: String,
pub column_type: ColumnType,
pub primary: bool,
pub unique: bool,
}
// Uniquely defined by name in this case
@@ -83,7 +83,7 @@ impl PartialEq for Column {
impl Eq for Column {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) enum ColumnType {
pub enum ColumnType {
Integer,
Float,
Text,
@@ -136,23 +136,8 @@ pub struct JoinTable {
pub rows: Vec<Vec<SimValue>>,
}
fn float_to_string<S>(float: &f64, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("{float}"))
}
fn string_to_float<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize)]
pub(crate) struct SimValue(pub turso_core::Value);
pub struct SimValue(pub turso_core::Value);
fn to_sqlite_blob(bytes: &[u8]) -> String {
format!(