diff --git a/Cargo.lock b/Cargo.lock index 81a4300c0..e599b2f8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 646cd977c..0dbb4b1fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/parser/src/ast.rs b/parser/src/ast.rs index 768c80dcc..06a74f9c4 100644 --- a/parser/src/ast.rs +++ b/parser/src/ast.rs @@ -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))] diff --git a/simulator/Cargo.toml b/simulator/Cargo.toml index 20696fa91..f01896716 100644 --- a/simulator/Cargo.toml +++ b/simulator/Cargo.toml @@ -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 } diff --git a/simulator/generation/mod.rs b/simulator/generation/mod.rs index 6d944b065..79bdf506f 100644 --- a/simulator/generation/mod.rs +++ b/simulator/generation/mod.rs @@ -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 T + 'a>; -type Choice<'a, R, T> = (usize, Box Option + '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(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(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 { - fn arbitrary_from(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 { - fn arbitrary_sized_from(rng: &mut R, t: T, size: usize) -> Self; -} - -/// ArbitraryFromMaybe trait for fallibally generating random values from a given value -pub trait ArbitraryFromMaybe { - fn arbitrary_from_maybe(rng: &mut R, t: T) -> Option - 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)>, - rng: &mut R, -) -> T { - let total = choices.iter().map(|(weight, _)| *weight).sum::(); - 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 { + &self.tables.tables } - unreachable!() -} - -/// one_of is a helper function for composing different generators with equal probability of occurrence. -pub(crate) fn one_of(choices: Vec>, 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, along with number of retries -/// to make before giving up. -pub(crate) fn backtrack(mut choices: Vec>, rng: &mut R) -> Option { - loop { - // If there are no more choices left, we give up - let choices_ = choices - .iter() - .enumerate() - .filter(|(_, (retries, _))| *retries > 0) - .collect::>(); - 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(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( - range: std::ops::Range, - n: usize, - rng: &mut R, -) -> Vec { - use rand::seq::SliceRandom; - let mut items: Vec = 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(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("-", "_") - } -} diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index c27becad5..47763657a 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -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>>; @@ -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(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) } diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index 4725aa384..288c4e75d 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -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( // 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::::arbitrary_from(rng, table)) .collect::>(); @@ -1088,10 +1081,10 @@ fn property_insert_values_select( }; // 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( 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(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( // 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( // - [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( 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 diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index 1abd41b1b..bb1344c2a 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -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(rng: &mut R) -> Self { - Create { - table: Table::arbitrary(rng), - } - } -} - -impl ArbitraryFrom<&Vec> for FromClause { - fn arbitrary_from(rng: &mut R, tables: &Vec
) -> 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(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::>(); - let order_by_col_count = - (rng.gen::() * rng.gen::() * (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( - 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::>(); - - 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::>(); - 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(rng: &mut R) -> Self { - match rng.gen_range(0..=5) { - 0..4 => Distinctness::All, - _ => Distinctness::Distinct, - } - } -} -impl Arbitrary for CompoundOperator { - fn arbitrary(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(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(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 = (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(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> = (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(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(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(rng: &mut R, (env, remaining): (&SimulatorEnv, &Remaining)) -> Self { @@ -355,43 +36,3 @@ impl ArbitraryFrom<(&SimulatorEnv, &Remaining)> for Query { ) } } - -fn pick_unique( - items: &[T], - count: usize, - rng: &mut impl rand::Rng, -) -> Vec -where - ::Owned: PartialEq, -{ - let mut picked: Vec = 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(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), - } - } -} diff --git a/simulator/main.rs b/simulator/main.rs index d2a31f099..0aaaf8be7 100644 --- a/simulator/main.rs +++ b/simulator/main.rs @@ -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); diff --git a/simulator/model/mod.rs b/simulator/model/mod.rs index e68355ee4..ce249baf5 100644 --- a/simulator/model/mod.rs +++ b/simulator/model/mod.rs @@ -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 { + 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 { + 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>>; + + 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>>; + + 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>; + fn shadow(&self, env: &mut SimulatorTables) -> Vec> { + 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>>; + + 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>>; + + 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>>; + + 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; + 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::>(); + // 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::>(); + + 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; + + 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>>; + + 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>; + fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result { + tables.snapshot = Some(tables.tables.clone()); + vec![] + } +} + +impl Shadow for Commit { + type Result = Vec>; + fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result { + tables.snapshot = None; + vec![] + } +} + +impl Shadow for Rollback { + type Result = Vec>; + 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>>; + + 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![]) + } +} diff --git a/simulator/model/query/create.rs b/simulator/model/query/create.rs deleted file mode 100644 index ab0cd9789..000000000 --- a/simulator/model/query/create.rs +++ /dev/null @@ -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>>; - - 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, ")") - } -} diff --git a/simulator/model/query/create_index.rs b/simulator/model/query/create_index.rs deleted file mode 100644 index 276396d4e..000000000 --- a/simulator/model/query/create_index.rs +++ /dev/null @@ -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>; - fn shadow(&self, env: &mut SimulatorTables) -> Vec> { - 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::>() - .join(", ") - ) - } -} - -impl ArbitraryFrom<&SimulatorEnv> for CreateIndex { - fn arbitrary_from(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::>(); - - let index_name = format!( - "idx_{}_{}", - table.name, - gen_random_text(rng).chars().take(8).collect::() - ); - - CreateIndex { - index_name, - table_name: table.name.clone(), - columns, - } - } -} diff --git a/simulator/model/query/delete.rs b/simulator/model/query/delete.rs deleted file mode 100644 index 265cdfe96..000000000 --- a/simulator/model/query/delete.rs +++ /dev/null @@ -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>>; - - 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) - } -} diff --git a/simulator/model/query/drop.rs b/simulator/model/query/drop.rs deleted file mode 100644 index 2b4379ff9..000000000 --- a/simulator/model/query/drop.rs +++ /dev/null @@ -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>>; - - 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) - } -} diff --git a/simulator/model/query/mod.rs b/simulator/model/query/mod.rs deleted file mode 100644 index 38e44073e..000000000 --- a/simulator/model/query/mod.rs +++ /dev/null @@ -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 { - 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 { - 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>>; - - 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!() - } -} diff --git a/simulator/model/query/select.rs b/simulator/model/query/select.rs deleted file mode 100644 index 2dcc762a8..000000000 --- a/simulator/model/query/select.rs +++ /dev/null @@ -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, -} - -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, - where_clause: Predicate, - limit: Option, - 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 { - 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, - /// compounds - pub compounds: Vec, -} - -#[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, - /// `FROM` clause - pub from: Option, - /// `WHERE` clause - pub where_clause: Predicate, - /// `ORDER BY` clause - pub order_by: Option, -} - -#[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, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct FromClause { - /// table - pub table: String, - /// `JOIN`ed tables - pub joins: Vec, -} - -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 { - 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; - 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::>(); - // 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::>(); - - 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; - - 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>>; - - 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() {} -} diff --git a/simulator/model/query/transaction.rs b/simulator/model/query/transaction.rs deleted file mode 100644 index a73fb076e..000000000 --- a/simulator/model/query/transaction.rs +++ /dev/null @@ -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>; - fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result { - tables.snapshot = Some(tables.tables.clone()); - vec![] - } -} - -impl Shadow for Commit { - type Result = Vec>; - fn shadow(&self, tables: &mut SimulatorTables) -> Self::Result { - tables.snapshot = None; - vec![] - } -} - -impl Shadow for Rollback { - type Result = Vec>; - 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") - } -} diff --git a/simulator/model/query/update.rs b/simulator/model/query/update.rs deleted file mode 100644 index a4cc13fa8..000000000 --- a/simulator/model/query/update.rs +++ /dev/null @@ -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>>; - - 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(()) - } -} diff --git a/simulator/runner/clock.rs b/simulator/runner/clock.rs index ef687c5c1..871a01346 100644 --- a/simulator/runner/clock.rs +++ b/simulator/runner/clock.rs @@ -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 diff --git a/simulator/runner/differential.rs b/simulator/runner/differential.rs index 7d37babe7..5723418c1 100644 --- a/simulator/runner/differential.rs +++ b/simulator/runner/differential.rs @@ -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, }; diff --git a/simulator/runner/doublecheck.rs b/simulator/runner/doublecheck.rs index 5ba98ca50..7c9d33b4e 100644 --- a/simulator/runner/doublecheck.rs +++ b/simulator/runner/doublecheck.rs @@ -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, }; diff --git a/simulator/runner/env.rs b/simulator/runner/env.rs index f5787bc57..a29adc591 100644 --- a/simulator/runner/env.rs +++ b/simulator/runner/env.rs @@ -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, diff --git a/simulator/runner/execution.rs b/simulator/runner/execution.rs index 9cbac3826..fa3dcbff9 100644 --- a/simulator/runner/execution.rs +++ b/simulator/runner/execution.rs @@ -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 _, }; diff --git a/simulator/runner/file.rs b/simulator/runner/file.rs index c8c5ff4fa..bbda05b1d 100644 --- a/simulator/runner/file.rs +++ b/simulator/runner/file.rs @@ -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, @@ -100,10 +100,10 @@ impl SimulatorFile { fn generate_latency_duration(&self) -> Option { 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() }) } diff --git a/simulator/runner/mod.rs b/simulator/runner/mod.rs index b56335da5..3eef78331 100644 --- a/simulator/runner/mod.rs +++ b/simulator/runner/mod.rs @@ -9,3 +9,5 @@ pub mod execution; pub mod file; pub mod io; pub mod watch; + +pub const FAULT_ERROR_MSG: &str = "Injected Fault"; diff --git a/simulator/runner/watch.rs b/simulator/runner/watch.rs index 90e8edc68..feab80af1 100644 --- a/simulator/runner/watch.rs +++ b/simulator/runner/watch.rs @@ -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, }; diff --git a/simulator/shrink/plan.rs b/simulator/shrink/plan.rs index f08ccbb5a..bccd07afd 100644 --- a/simulator/shrink/plan.rs +++ b/simulator/shrink/plan.rs @@ -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, diff --git a/sql_generation/Cargo.toml b/sql_generation/Cargo.toml new file mode 100644 index 000000000..d84d08380 --- /dev/null +++ b/sql_generation/Cargo.toml @@ -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" diff --git a/simulator/generation/expr.rs b/sql_generation/generation/expr.rs similarity index 85% rename from simulator/generation/expr.rs rename to sql_generation/generation/expr.rs index 682c38d5c..c07d81414 100644 --- a/simulator/generation/expr.rs +++ b/sql_generation/generation/expr.rs @@ -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 Arbitrary for Box @@ -34,7 +33,7 @@ where T: Arbitrary, { fn arbitrary(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, { fn arbitrary_sized_from(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, { fn arbitrary_from(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(rng: &mut R, t: &SimulatorEnv, size: usize) -> Self { +impl ArbitrarySizedFrom<&C> for Expr { + fn arbitrary_sized_from(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(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(rng: &mut R, t: &SimulatorEnv) -> Self { +impl ArbitraryFrom<&C> for QualifiedName { + fn arbitrary_from(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(rng: &mut R, _t: &SimulatorEnv) -> Self { - let choice = rng.gen_range(0..4); +impl ArbitraryFrom<&C> for LikeOperator { + fn arbitrary_from(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(rng: &mut R, _t: &SimulatorEnv) -> Self { +impl ArbitraryFrom<&C> for ast::Literal { + fn arbitrary_from(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(rng: &mut R, _t: &SimulatorEnv) -> Self { - let choice = rng.gen_range(0..4); +impl ArbitraryFrom<&C> for UnaryOperator { + fn arbitrary_from(rng: &mut R, _t: &C) -> Self { + let choice = rng.random_range(0..4); match choice { 0 => Self::BitwiseNot, 1 => Self::Negative, diff --git a/sql_generation/generation/mod.rs b/sql_generation/generation/mod.rs new file mode 100644 index 000000000..25bd7ec09 --- /dev/null +++ b/sql_generation/generation/mod.rs @@ -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
; + fn opts(&self) -> Opts; +} + +type ArbitraryFromFunc<'a, R, T> = Box T + 'a>; +type Choice<'a, R, T> = (usize, Box Option + '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(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(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 { + fn arbitrary_from(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 { + fn arbitrary_sized_from(rng: &mut R, t: T, size: usize) -> Self; +} + +/// ArbitraryFromMaybe trait for fallibally generating random values from a given value +pub trait ArbitraryFromMaybe { + fn arbitrary_from_maybe(rng: &mut R, t: T) -> Option + 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( + choices: Vec<(N, ArbitraryFromFunc)>, + rng: &mut R, +) -> T { + let total = choices.iter().map(|(weight, _)| *weight).sum::(); + 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(choices: Vec>, 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, along with number of retries +/// to make before giving up. +pub fn backtrack(mut choices: Vec>, rng: &mut R) -> Option { + loop { + // If there are no more choices left, we give up + let choices_ = choices + .iter() + .enumerate() + .filter(|(_, (retries, _))| *retries > 0) + .collect::>(); + 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(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(range: std::ops::Range, n: usize, rng: &mut R) -> Vec { + use rand::seq::SliceRandom; + let mut items: Vec = 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(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( + items: &[T], + count: usize, + rng: &mut impl rand::Rng, +) -> Vec +where + ::Owned: PartialEq, +{ + let mut picked: Vec = Vec::new(); + while picked.len() < count { + let item = pick(items, rng); + if !picked.contains(&item.to_owned()) { + picked.push(item.to_owned()); + } + } + picked +} diff --git a/simulator/generation/predicate/binary.rs b/sql_generation/generation/predicate/binary.rs similarity index 87% rename from simulator/generation/predicate/binary.rs rename to sql_generation/generation/predicate/binary.rs index f8ba27236..29c1727a9 100644 --- a/simulator/generation/predicate/binary.rs +++ b/sql_generation/generation/predicate/binary.rs @@ -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(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(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::>(); - 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::>(); // 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::>(); 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::>(); 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> = (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> = (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> = (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> = (0..num_rows) .map(|_| { table diff --git a/simulator/generation/predicate/mod.rs b/sql_generation/generation/predicate/mod.rs similarity index 95% rename from simulator/generation/predicate/mod.rs rename to sql_generation/generation/predicate/mod.rs index 5c5887818..b919ad0bd 100644 --- a/simulator/generation/predicate/mod.rs +++ b/sql_generation/generation/predicate/mod.rs @@ -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, T: TableContext> ArbitraryFrom<(&T, A, bool)> for Sim fn arbitrary_from(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 ArbitraryFrom<(&T, bool)> for CompoundPredicate { impl ArbitraryFrom<&T> for Predicate { fn arbitrary_from(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)> 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::>(); - 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::>(); @@ -92,7 +92,7 @@ impl ArbitraryFrom<(&Table, &Vec)> 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> = (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> = (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> = (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> = (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> = (0..num_rows) .map(|_| { table diff --git a/simulator/generation/predicate/unary.rs b/sql_generation/generation/predicate/unary.rs similarity index 97% rename from simulator/generation/predicate/unary.rs rename to sql_generation/generation/predicate/unary.rs index f7f374b6e..62c6d7d65 100644 --- a/simulator/generation/predicate/unary.rs +++ b/sql_generation/generation/predicate/unary.rs @@ -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::>(); // 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::>(); // 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> = (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> = (0..num_rows) .map(|_| { table diff --git a/sql_generation/generation/query.rs b/sql_generation/generation/query.rs new file mode 100644 index 000000000..d7840a001 --- /dev/null +++ b/sql_generation/generation/query.rs @@ -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(rng: &mut R) -> Self { + Create { + table: Table::arbitrary(rng), + } + } +} + +impl ArbitraryFrom<&Vec
> for FromClause { + fn arbitrary_from(rng: &mut R, tables: &Vec
) -> 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 ArbitraryFrom<&C> for SelectInner { + fn arbitrary_from(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::>(); + let order_by_col_count = + (rng.random::() * rng.random::() * (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 ArbitrarySizedFrom<&C> for SelectInner { + fn arbitrary_sized_from(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::>(); + + 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::>(); + 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(rng: &mut R) -> Self { + match rng.random_range(0..=5) { + 0..4 => Distinctness::All, + _ => Distinctness::Distinct, + } + } +} +impl Arbitrary for CompoundOperator { + fn arbitrary(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 ArbitraryFrom<&C> for SelectFree { + fn arbitrary_from(rng: &mut R, env: &C) -> Self { + let expr = Predicate(Expr::arbitrary_sized_from(rng, env, 8)); + let select = Select::expr(expr); + Self(select) + } +} + +impl ArbitraryFrom<&C> for Select { + fn arbitrary_from(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 = (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<&C> for Insert { + fn arbitrary_from(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> = (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<&C> for Delete { + fn arbitrary_from(rng: &mut R, env: &C) -> Self { + let table = pick(env.tables(), rng); + Self { + table: table.name.clone(), + predicate: Predicate::arbitrary_from(rng, table), + } + } +} + +impl ArbitraryFrom<&C> for Drop { + fn arbitrary_from(rng: &mut R, env: &C) -> Self { + let table = pick(env.tables(), rng); + Self { + table: table.name.clone(), + } + } +} + +impl ArbitraryFrom<&C> for CreateIndex { + fn arbitrary_from(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::>(); + + let index_name = format!( + "idx_{}_{}", + table.name, + gen_random_text(rng).chars().take(8).collect::() + ); + + CreateIndex { + index_name, + table_name: table.name.clone(), + columns, + } + } +} + +impl ArbitraryFrom<&C> for Update { + fn arbitrary_from(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), + } + } +} diff --git a/simulator/generation/table.rs b/sql_generation/generation/table.rs similarity index 80% rename from simulator/generation/table.rs rename to sql_generation/generation/table.rs index 66f48b5ad..d21397cbe 100644 --- a/simulator/generation/table.rs +++ b/sql_generation/generation/table.rs @@ -19,11 +19,11 @@ impl Arbitrary for Table { fn arbitrary(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(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(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(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::>(); - 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(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(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::>(); - 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(rng: &mut R, value: &SimValue) -> Option { @@ -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::(), diff --git a/sql_generation/lib.rs b/sql_generation/lib.rs new file mode 100644 index 000000000..f52cdebdf --- /dev/null +++ b/sql_generation/lib.rs @@ -0,0 +1,2 @@ +pub mod generation; +pub mod model; diff --git a/sql_generation/model/mod.rs b/sql_generation/model/mod.rs new file mode 100644 index 000000000..a29f56382 --- /dev/null +++ b/sql_generation/model/mod.rs @@ -0,0 +1,2 @@ +pub mod query; +pub mod table; diff --git a/sql_generation/model/query/create.rs b/sql_generation/model/query/create.rs new file mode 100644 index 000000000..607d5fe8d --- /dev/null +++ b/sql_generation/model/query/create.rs @@ -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, ")") + } +} diff --git a/sql_generation/model/query/create_index.rs b/sql_generation/model/query/create_index.rs new file mode 100644 index 000000000..db9d15a04 --- /dev/null +++ b/sql_generation/model/query/create_index.rs @@ -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::>() + .join(", ") + ) + } +} diff --git a/sql_generation/model/query/delete.rs b/sql_generation/model/query/delete.rs new file mode 100644 index 000000000..89ebd61b8 --- /dev/null +++ b/sql_generation/model/query/delete.rs @@ -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) + } +} diff --git a/sql_generation/model/query/drop.rs b/sql_generation/model/query/drop.rs new file mode 100644 index 000000000..0d0ef31bb --- /dev/null +++ b/sql_generation/model/query/drop.rs @@ -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) + } +} diff --git a/simulator/model/query/insert.rs b/sql_generation/model/query/insert.rs similarity index 52% rename from simulator/model/query/insert.rs rename to sql_generation/model/query/insert.rs index 3dc8659df..d69921388 100644 --- a/simulator/model/query/insert.rs +++ b/sql_generation/model/query/insert.rs @@ -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>, @@ -18,40 +18,8 @@ pub(crate) enum Insert { }, } -impl Shadow for Insert { - type Result = anyhow::Result>>; - - 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, } diff --git a/sql_generation/model/query/mod.rs b/sql_generation/model/query/mod.rs new file mode 100644 index 000000000..5bf0cecde --- /dev/null +++ b/sql_generation/model/query/mod.rs @@ -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!() + } +} diff --git a/simulator/model/query/predicate.rs b/sql_generation/model/query/predicate.rs similarity index 86% rename from simulator/model/query/predicate.rs rename to sql_generation/model/query/predicate.rs index c55c45417..30b671d72 100644 --- a/simulator/model/query/predicate.rs +++ b/sql_generation/model/query/predicate.rs @@ -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) -> Self { + pub fn and(predicates: Vec) -> Self { if predicates.is_empty() { Self::true_() } else if predicates.len() == 1 { @@ -44,7 +45,7 @@ impl Predicate { } } - pub(crate) fn or(predicates: Vec) -> Self { + pub fn or(predicates: Vec) -> 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 { + pub fn eval(&self, row: &[SimValue], table: &Table) -> Option { expr_to_value(&self.0, row, table) } - pub(crate) fn test(&self, row: &[SimValue], table: &T) -> bool { + pub fn test(&self, row: &[SimValue], table: &T) -> bool { let value = expr_to_value(&self.0, row, table); value.is_some_and(|value| value.as_bool()) } diff --git a/sql_generation/model/query/select.rs b/sql_generation/model/query/select.rs new file mode 100644 index 000000000..6c34888ff --- /dev/null +++ b/sql_generation/model/query/select.rs @@ -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, +} + +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, + where_clause: Predicate, + limit: Option, + 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 { + 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, + /// compounds + pub compounds: Vec, +} + +#[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, + /// `FROM` clause + pub from: Option, + /// `WHERE` clause + pub where_clause: Predicate, + /// `ORDER BY` clause + pub order_by: Option, +} + +#[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, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct FromClause { + /// table + pub table: String, + /// `JOIN`ed tables + pub joins: Vec, +} + +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 { + 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::>(); + // 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::>(); + + 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() {} +} diff --git a/sql_generation/model/query/transaction.rs b/sql_generation/model/query/transaction.rs new file mode 100644 index 000000000..1114200a0 --- /dev/null +++ b/sql_generation/model/query/transaction.rs @@ -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") + } +} diff --git a/sql_generation/model/query/update.rs b/sql_generation/model/query/update.rs new file mode 100644 index 000000000..412731bbe --- /dev/null +++ b/sql_generation/model/query/update.rs @@ -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(()) + } +} diff --git a/simulator/model/table.rs b/sql_generation/model/table.rs similarity index 93% rename from simulator/model/table.rs rename to sql_generation/model/table.rs index b69a197d0..87057b42b 100644 --- a/simulator/model/table.rs +++ b/sql_generation/model/table.rs @@ -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, - pub(crate) rows: Vec>, - pub(crate) indexes: Vec, +pub struct Table { + pub name: String, + pub columns: Vec, + pub rows: Vec>, + pub indexes: Vec, } 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>, } -fn float_to_string(float: &f64, serializer: S) -> Result -where - S: serde::Serializer, -{ - serializer.serialize_str(&format!("{float}")) -} - -fn string_to_float<'de, D>(deserializer: D) -> Result -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!(