diff --git a/.gitignore b/.gitignore index 29c961707..fb1557339 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,6 @@ env dist/ .tmp/ - *.db **/*.db-wal **/*.db-shm diff --git a/Cargo.lock b/Cargo.lock index f6fbb4826..cd3ade11e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,6 +240,9 @@ name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +dependencies = [ + "serde", +] [[package]] name = "blake3" @@ -1428,6 +1431,7 @@ checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown 0.15.2", + "serde", ] [[package]] @@ -1960,7 +1964,9 @@ dependencies = [ "clap", "dirs 6.0.0", "env_logger 0.10.2", + "hex", "limbo_core", + "limbo_sqlite3_parser", "log", "notify", "rand 0.8.5", diff --git a/core/numeric.rs b/core/numeric/mod.rs similarity index 99% rename from core/numeric.rs rename to core/numeric/mod.rs index 27264ded6..ba1127f66 100644 --- a/core/numeric.rs +++ b/core/numeric/mod.rs @@ -1,6 +1,6 @@ use crate::Value; -mod nonnan; +pub mod nonnan; use nonnan::NonNan; diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index 6618c9056..b2fae63db 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -5818,12 +5818,10 @@ impl Value { // exec_if returns whether you should jump pub fn exec_if(&self, jump_if_null: bool, not: bool) -> bool { - match self { - Value::Integer(0) | Value::Float(0.0) => not, - Value::Integer(_) | Value::Float(_) => !not, - Value::Null => jump_if_null, - _ => false, - } + Numeric::from(self) + .try_into_bool() + .map(|jump| if not { !jump } else { jump }) + .unwrap_or(jump_if_null) } pub fn exec_cast(&self, datatype: &str) -> Value { diff --git a/simulator/Cargo.toml b/simulator/Cargo.toml index 852a39cb7..b15f32b56 100644 --- a/simulator/Cargo.toml +++ b/simulator/Cargo.toml @@ -15,7 +15,7 @@ name = "limbo_sim" path = "main.rs" [dependencies] -limbo_core = { path = "../core" } +limbo_core = { path = "../core", features = ["simulator"]} rand = "0.8.5" rand_chacha = "0.3.1" log = "0.4.20" @@ -35,4 +35,5 @@ chrono = { version = "0.4.40", features = ["serde"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } anyhow.workspace = true - +limbo_sqlite3_parser = { workspace = true, features = ["serde"]} +hex = "0.4.3" diff --git a/simulator/generation/expr.rs b/simulator/generation/expr.rs new file mode 100644 index 000000000..d3c09f869 --- /dev/null +++ b/simulator/generation/expr.rs @@ -0,0 +1,276 @@ +use limbo_sqlite3_parser::ast::{ + self, Expr, LikeOperator, Name, Operator, QualifiedName, Type, UnaryOperator, +}; + +use crate::{ + generation::{gen_random_text, pick, pick_index, Arbitrary, ArbitraryFrom}, + model::table::SimValue, + SimulatorEnv, +}; + +impl Arbitrary for Box +where + T: Arbitrary, +{ + fn arbitrary(rng: &mut R) -> Self { + Box::from(T::arbitrary(rng)) + } +} + +impl ArbitraryFrom for Box +where + T: ArbitraryFrom, +{ + fn arbitrary_from(rng: &mut R, t: A) -> Self { + Box::from(T::arbitrary_from(rng, t)) + } +} + +impl Arbitrary for Option +where + T: Arbitrary, +{ + fn arbitrary(rng: &mut R) -> Self { + rng.gen_bool(0.5).then_some(T::arbitrary(rng)) + } +} + +impl ArbitraryFrom for Option +where + T: ArbitraryFrom, +{ + fn arbitrary_from(rng: &mut R, t: A) -> Self { + rng.gen_bool(0.5).then_some(T::arbitrary_from(rng, t)) + } +} + +impl ArbitraryFrom for Vec +where + T: ArbitraryFrom, +{ + fn arbitrary_from(rng: &mut R, t: A) -> Self { + let size = rng.gen_range(0..5); + (0..size) + .into_iter() + .map(|_| T::arbitrary_from(rng, t)) + .collect() + } +} + +// Freestyling generation +impl ArbitraryFrom<&SimulatorEnv> for Expr { + fn arbitrary_from(rng: &mut R, t: &SimulatorEnv) -> Self { + let choice = rng.gen_range(0..13); + let expr = match choice { + 0 => Expr::Between { + lhs: Box::arbitrary_from(rng, t), + not: rng.gen_bool(0.5), + start: Box::arbitrary_from(rng, t), + end: Box::arbitrary_from(rng, t), + }, + 1 => Expr::Binary( + Box::arbitrary_from(rng, t), + Operator::arbitrary(rng), + Box::arbitrary_from(rng, t), + ), + 2 => Expr::Case { + base: Option::arbitrary_from(rng, t), + when_then_pairs: { + let size = rng.gen_range(0..5); + (0..size) + .into_iter() + .map(|_| (Self::arbitrary_from(rng, t), Self::arbitrary_from(rng, t))) + .collect() + }, + else_expr: Option::arbitrary_from(rng, t), + }, + 3 => Expr::Cast { + expr: Box::arbitrary_from(rng, t), + type_name: Option::arbitrary(rng), + }, + 4 => Expr::Collate(Box::arbitrary_from(rng, t), CollateName::arbitrary(rng).0), + 5 => Expr::InList { + lhs: Box::arbitrary_from(rng, t), + not: rng.gen_bool(0.5), + rhs: Option::arbitrary_from(rng, t), + }, + 6 => Expr::IsNull(Box::arbitrary_from(rng, t)), + 7 => { + let op = LikeOperator::arbitrary_from(rng, t); + let escape = if matches!(op, LikeOperator::Like) { + Option::arbitrary_from(rng, t) + } else { + None + }; + Expr::Like { + lhs: Box::arbitrary_from(rng, t), + not: rng.gen_bool(0.5), + op, + rhs: Box::arbitrary_from(rng, t), + escape, + } + } + 8 => Expr::Literal(ast::Literal::arbitrary_from(rng, t)), + 9 => Expr::NotNull(Box::arbitrary_from(rng, t)), + // TODO: only supports one paranthesized expression + 10 => Expr::Parenthesized(vec![Expr::arbitrary_from(rng, t)]), + 11 => { + let table_idx = pick_index(t.tables.len(), rng); + let table = &t.tables[table_idx]; + let col_idx = pick_index(table.columns.len(), rng); + let col = &table.columns[col_idx]; + Expr::Qualified(Name(table.name.clone()), Name(col.name.clone())) + } + 12 => Expr::Unary( + UnaryOperator::arbitrary_from(rng, t), + Box::arbitrary_from(rng, t), + ), + // TODO: skip Exists for now + // TODO: skip Function Call for now + // TODO: skip Function Call Star for now + // TODO: skip ID for now + // TODO: skip InSelect as still need to implement ArbitratyFrom for Select + // TODO: skip InTable + // TODO: skip Name + // TODO: Skip DoublyQualified for now + // TODO: skip Raise + // TODO: skip subquery + _ => unreachable!(), + }; + expr + } +} + +impl Arbitrary for Operator { + fn arbitrary(rng: &mut R) -> Self { + let choice = rng.gen_range(0..23); + match choice { + 0 => Operator::Add, + 1 => Operator::And, + 2 => Operator::ArrowRight, + 3 => Operator::ArrowRightShift, + 4 => Operator::BitwiseAnd, + 5 => Operator::BitwiseNot, + 6 => Operator::BitwiseOr, + 7 => Operator::Concat, + 8 => Operator::Divide, + 9 => Operator::Equals, + 10 => Operator::Greater, + 11 => Operator::GreaterEquals, + 12 => Operator::Is, + 13 => Operator::IsNot, + 14 => Operator::LeftShift, + 15 => Operator::Less, + 16 => Operator::LessEquals, + 17 => Operator::Modulus, + 18 => Operator::Multiply, + 19 => Operator::NotEquals, + 20 => Operator::Or, + 21 => Operator::RightShift, + 22 => Operator::Subtract, + _ => unreachable!(), + } + } +} + +impl Arbitrary for Type { + fn arbitrary(rng: &mut R) -> Self { + let name = pick(&["INT", "INTEGER", "REAL", "TEXT", "BLOB", "ANY"], rng).to_string(); + Self { + name, + size: None, // TODO: come back later here + } + } +} + +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 { + // TODO: for now just generate table name + let table_idx = pick_index(t.tables.len(), rng); + let table = &t.tables[table_idx]; + // TODO: for now forego alias + Self::single(Name(table.name.clone())) + } +} + +impl ArbitraryFrom<&SimulatorEnv> for LikeOperator { + fn arbitrary_from(rng: &mut R, _t: &SimulatorEnv) -> Self { + let choice = rng.gen_range(0..4); + match choice { + 0 => LikeOperator::Glob, + 1 => LikeOperator::Like, + 2 => LikeOperator::Match, + 3 => LikeOperator::Regexp, + _ => unreachable!(), + } + } +} + +// 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 { + loop { + let choice = rng.gen_range(0..5); + let lit = match choice { + 0 => ast::Literal::Numeric({ + let integer = rng.gen_bool(0.5); + if integer { + rng.gen_range(i64::MIN..i64::MAX).to_string() + } else { + rng.gen_range(-1e10..1e10).to_string() + } + }), + 1 => ast::Literal::String(format!("'{}'", gen_random_text(rng))), + 2 => ast::Literal::Blob(hex::encode(gen_random_text(rng).as_bytes())), + // TODO: skip Keyword + 3 => continue, + 4 => ast::Literal::Null, + // TODO: Ignore Date stuff for now + _ => continue, + }; + break lit; + } + } +} + +// Creates a litreal value +impl ArbitraryFrom<&Vec<&SimValue>> for ast::Expr { + fn arbitrary_from(rng: &mut R, values: &Vec<&SimValue>) -> Self { + if values.is_empty() { + return Self::Literal(ast::Literal::Null); + } + // TODO: for now just convert the value to an ast::Literal + let value = pick(&values, rng); + Expr::Literal((*value).into()) + } +} + +impl ArbitraryFrom<&SimulatorEnv> for UnaryOperator { + fn arbitrary_from(rng: &mut R, _t: &SimulatorEnv) -> Self { + let choice = rng.gen_range(0..4); + match choice { + 0 => Self::BitwiseNot, + 1 => Self::Negative, + 2 => Self::Not, + 3 => Self::Positive, + _ => unreachable!(), + } + } +} diff --git a/simulator/generation/mod.rs b/simulator/generation/mod.rs index ac1f97f60..8e61472ce 100644 --- a/simulator/generation/mod.rs +++ b/simulator/generation/mod.rs @@ -3,7 +3,9 @@ use std::{iter::Sum, ops::SubAssign}; use anarchist_readable_name_generator_lib::readable_name_custom; use rand::{distributions::uniform::SampleUniform, Rng}; +mod expr; pub mod plan; +mod predicate; pub mod property; pub mod query; pub mod table; @@ -71,9 +73,9 @@ pub(crate) fn one_of<'a, T, R: Rng>(choices: Vec T + 'a>>, /// 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<'a, T, R: Rng>( - mut choices: Vec<(u32, Box Option + 'a>)>, + mut choices: Vec<(usize, Box Option + 'a>)>, rng: &mut R, -) -> T { +) -> Option { loop { // If there are no more choices left, we give up let choices_ = choices @@ -82,14 +84,15 @@ pub(crate) fn backtrack<'a, T, R: Rng>( .filter(|(_, (retries, _))| *retries > 0) .collect::>(); if choices_.is_empty() { - panic!("backtrack: no more choices left"); + 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 let Some(result) = result { + if result.is_some() { return result; } else { choices[choice_index].0 -= 1; diff --git a/simulator/generation/plan.rs b/simulator/generation/plan.rs index 9d07ccb14..4bb408885 100644 --- a/simulator/generation/plan.rs +++ b/simulator/generation/plan.rs @@ -1,4 +1,10 @@ -use std::{collections::HashSet, fmt::Display, path::Path, rc::Rc, vec}; +use std::{ + collections::HashSet, + fmt::{Debug, Display}, + path::Path, + rc::Rc, + vec, +}; use limbo_core::{Connection, Result, StepResult, IO}; use serde::{Deserialize, Serialize}; @@ -6,11 +12,12 @@ use serde::{Deserialize, Serialize}; use crate::{ model::{ query::{ - select::{Distinctness, Predicate, ResultColumn}, + predicate::Predicate, + select::{Distinctness, ResultColumn}, update::Update, Create, CreateIndex, Delete, Drop, Insert, Query, Select, }, - table::Value, + table::SimValue, }, runner::{env::SimConnection, io::SimulatorIO}, SimulatorEnv, @@ -20,7 +27,7 @@ use crate::generation::{frequency, Arbitrary, ArbitraryFrom}; use super::property::{remaining, Property}; -pub(crate) type ResultSet = Result>>; +pub(crate) type ResultSet = Result>>; #[derive(Clone, Serialize, Deserialize)] pub(crate) struct InteractionPlan { @@ -105,7 +112,7 @@ pub(crate) enum Interactions { } impl Interactions { - pub(crate) fn name(&self) -> Option { + pub(crate) fn name(&self) -> Option<&str> { match self { Interactions::Property(property) => Some(property.name()), Interactions::Query(_) => None, @@ -225,6 +232,7 @@ impl Display for InteractionStats { } } +#[derive(Debug)] pub(crate) enum Interaction { Query(Query), Assumption(Assertion), @@ -254,6 +262,14 @@ pub(crate) struct Assertion { pub(crate) message: String, } +impl Debug for Assertion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Assertion") + .field("message", &self.message) + .finish() + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) enum Fault { Disconnect, @@ -482,7 +498,7 @@ impl ArbitraryFrom<&mut SimulatorEnv> for InteractionPlan { } impl Interaction { - pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { + pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { match self { Self::Query(query) => query.shadow(env), Self::Assumption(_) | Self::Assertion(_) | Self::Fault(_) => vec![], @@ -511,13 +527,7 @@ impl Interaction { let row = rows.row().unwrap(); let mut r = Vec::new(); for v in row.get_values() { - let v = match v { - limbo_core::Value::Null => Value::Null, - limbo_core::Value::Integer(i) => Value::Integer(*i), - limbo_core::Value::Float(f) => Value::Float(*f), - limbo_core::Value::Text(t) => Value::Text(t.as_str().to_string()), - limbo_core::Value::Blob(b) => Value::Blob(b.to_vec()), - }; + let v = v.into(); r.push(v); } out.push(r); diff --git a/simulator/generation/predicate/binary.rs b/simulator/generation/predicate/binary.rs new file mode 100644 index 000000000..ef6379738 --- /dev/null +++ b/simulator/generation/predicate/binary.rs @@ -0,0 +1,544 @@ +//! Contains code for generation for [ast::Expr::Binary] Predicate + +use limbo_sqlite3_parser::ast::{self, Expr}; + +use crate::{ + generation::{ + backtrack, one_of, pick, + predicate::{CompoundPredicate, SimplePredicate}, + table::{GTValue, LTValue, LikeValue}, + ArbitraryFrom, ArbitraryFromMaybe as _, + }, + model::{ + query::predicate::Predicate, + table::{SimValue, Table}, + }, +}; + +impl Predicate { + /// Generate an [ast::Expr::Binary] [Predicate] from a column and [SimValue] + pub fn from_column_binary( + rng: &mut R, + column_name: &str, + value: &SimValue, + ) -> Predicate { + let expr = one_of( + vec![ + Box::new(|_| { + Expr::Binary( + Box::new(Expr::Id(ast::Id(column_name.to_string()))), + ast::Operator::Equals, + Box::new(Expr::Literal(value.into())), + ) + }), + Box::new(|rng| { + let gt_value = GTValue::arbitrary_from(rng, value).0; + Expr::Binary( + Box::new(Expr::Id(ast::Id(column_name.to_string()))), + ast::Operator::Greater, + Box::new(Expr::Literal(gt_value.into())), + ) + }), + Box::new(|rng| { + let lt_value = LTValue::arbitrary_from(rng, value).0; + Expr::Binary( + Box::new(Expr::Id(ast::Id(column_name.to_string()))), + ast::Operator::Less, + Box::new(Expr::Literal(lt_value.into())), + ) + }), + ], + rng, + ); + Predicate(expr) + } + + /// 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: &Vec) -> Predicate { + // Pick a column + let column_index = rng.gen_range(0..t.columns.len()); + let column = &t.columns[column_index]; + let value = &row[column_index]; + let expr = backtrack( + vec![ + ( + 1, + Box::new(|_| { + Some(Expr::Binary( + Box::new(ast::Expr::Qualified( + ast::Name(t.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::Equals, + Box::new(Expr::Literal(value.into())), + )) + }), + ), + ( + 1, + Box::new(|rng| { + let v = SimValue::arbitrary_from(rng, &column.column_type); + if &v == value { + None + } else { + Some(Expr::Binary( + Box::new(ast::Expr::Qualified( + ast::Name(t.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::NotEquals, + Box::new(Expr::Literal(v.into())), + )) + } + }), + ), + ( + 1, + Box::new(|rng| { + let lt_value = LTValue::arbitrary_from(rng, value).0; + Some(Expr::Binary( + Box::new(ast::Expr::Qualified( + ast::Name(t.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::Greater, + Box::new(Expr::Literal(lt_value.into())), + )) + }), + ), + ( + 1, + Box::new(|rng| { + let gt_value = GTValue::arbitrary_from(rng, value).0; + Some(Expr::Binary( + Box::new(ast::Expr::Qualified( + ast::Name(t.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::Less, + Box::new(Expr::Literal(gt_value.into())), + )) + }), + ), + ( + 1, + Box::new(|rng| { + // TODO: generation for Like and Glob expressions should be extracted to different module + LikeValue::arbitrary_from_maybe(rng, value).map(|like| { + Expr::Like { + lhs: Box::new(ast::Expr::Qualified( + ast::Name(t.name.clone()), + ast::Name(column.name.clone()), + )), + not: false, // TODO: also generate this value eventually + op: ast::LikeOperator::Like, + rhs: Box::new(Expr::Literal(like.0.into())), + escape: None, // TODO: implement + } + }) + }), + ), + ], + rng, + ); + // Backtrack will always return Some here + Predicate(expr.unwrap()) + } + + /// 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: &Vec) -> Predicate { + // Pick a column + let column_index = rng.gen_range(0..t.columns.len()); + let column = &t.columns[column_index]; + let value = &row[column_index]; + let expr = one_of( + vec![ + Box::new(|_| { + Expr::Binary( + Box::new(ast::Expr::Qualified( + ast::Name(t.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::NotEquals, + Box::new(Expr::Literal(value.into())), + ) + }), + Box::new(|rng| { + let v = loop { + let v = SimValue::arbitrary_from(rng, &column.column_type); + if &v != value { + break v; + } + }; + Expr::Binary( + Box::new(ast::Expr::Qualified( + ast::Name(t.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::Equals, + Box::new(Expr::Literal(v.into())), + ) + }), + Box::new(|rng| { + let gt_value = GTValue::arbitrary_from(rng, value).0; + Expr::Binary( + Box::new(ast::Expr::Qualified( + ast::Name(t.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::Greater, + Box::new(Expr::Literal(gt_value.into())), + ) + }), + Box::new(|rng| { + let lt_value = LTValue::arbitrary_from(rng, value).0; + Expr::Binary( + Box::new(ast::Expr::Qualified( + ast::Name(t.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::Less, + Box::new(Expr::Literal(lt_value.into())), + ) + }), + ], + rng, + ); + Predicate(expr) + } +} + +impl SimplePredicate { + /// Generates a true [ast::Expr::Binary] [SimplePredicate] from a [Table] for a row in the table + pub fn true_binary(rng: &mut R, table: &Table, row: &[SimValue]) -> Self { + // Pick a random column + let column_index = rng.gen_range(0..table.columns.len()); + let column = &table.columns[column_index]; + let column_value = &row[column_index]; + // Avoid creation of NULLs + if row.is_empty() { + return SimplePredicate(Predicate(Expr::Literal(SimValue::TRUE.into()))); + } + let expr = one_of( + vec![ + Box::new(|_rng| { + Expr::Binary( + Box::new(ast::Expr::Qualified( + ast::Name(table.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::Equals, + Box::new(Expr::Literal(column_value.into())), + ) + }), + Box::new(|rng| { + let lt_value = LTValue::arbitrary_from(rng, column_value).0; + Expr::Binary( + Box::new(Expr::Qualified( + ast::Name(table.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::Greater, + Box::new(Expr::Literal(lt_value.into())), + ) + }), + Box::new(|rng| { + let gt_value = GTValue::arbitrary_from(rng, column_value).0; + Expr::Binary( + Box::new(Expr::Qualified( + ast::Name(table.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::Less, + Box::new(Expr::Literal(gt_value.into())), + ) + }), + ], + rng, + ); + SimplePredicate(Predicate(expr)) + } + + /// Generates a false [ast::Expr::Binary] [SimplePredicate] from a [Table] for a row in the table + pub fn false_binary(rng: &mut R, table: &Table, row: &[SimValue]) -> Self { + // Pick a random column + let column_index = rng.gen_range(0..table.columns.len()); + let column = &table.columns[column_index]; + let column_value = &row[column_index]; + // Avoid creation of NULLs + if row.is_empty() { + return SimplePredicate(Predicate(Expr::Literal(SimValue::FALSE.into()))); + } + let expr = one_of( + vec![ + Box::new(|_rng| { + Expr::Binary( + Box::new(Expr::Qualified( + ast::Name(table.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::NotEquals, + Box::new(Expr::Literal(column_value.into())), + ) + }), + Box::new(|rng| { + let gt_value = GTValue::arbitrary_from(rng, column_value).0; + Expr::Binary( + Box::new(ast::Expr::Qualified( + ast::Name(table.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::Greater, + Box::new(Expr::Literal(gt_value.into())), + ) + }), + Box::new(|rng| { + let lt_value = LTValue::arbitrary_from(rng, column_value).0; + Expr::Binary( + Box::new(ast::Expr::Qualified( + ast::Name(table.name.clone()), + ast::Name(column.name.clone()), + )), + ast::Operator::Less, + Box::new(Expr::Literal(lt_value.into())), + ) + }), + ], + rng, + ); + SimplePredicate(Predicate(expr)) + } +} + +impl CompoundPredicate { + /// Decide if you want to create an AND or an OR + /// + /// Creates a Compound Predicate that is TRUE or FALSE for at least a single row + pub fn from_table_binary( + rng: &mut R, + table: &Table, + predicate_value: bool, + ) -> Self { + // Cannot pick a row if the table is empty + if table.rows.is_empty() { + return Self( + predicate_value + .then_some(Predicate::true_()) + .unwrap_or(Predicate::false_()), + ); + } + let row = pick(&table.rows, rng); + let predicate = if rng.gen_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(0..=3)) + .map(|_| SimplePredicate::arbitrary_from(rng, (table, row, true)).0) + .reduce(|accum, curr| { + Predicate(Expr::Binary( + Box::new(accum.0), + ast::Operator::And, + Box::new(curr.0), + )) + }) + .unwrap_or(Predicate::true_()) + } else { + // Create a vector of random booleans + let mut booleans = (0..rng.gen_range(0..=3)) + .map(|_| rng.gen_bool(0.5)) + .collect::>(); + + let len = booleans.len(); + + // Make sure at least one of them is false + if !booleans.is_empty() && booleans.iter().all(|b| *b) { + booleans[rng.gen_range(0..len)] = false; + } + + booleans + .iter() + .map(|b| SimplePredicate::arbitrary_from(rng, (table, row, *b)).0) + .reduce(|accum, curr| { + Predicate(Expr::Binary( + Box::new(accum.0), + ast::Operator::And, + Box::new(curr.0), + )) + }) + .unwrap_or(Predicate::false_()) + } + } else { + // An OR for true requires at least one of its children to be true + // 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(0..=3)) + .map(|_| rng.gen_bool(0.5)) + .collect::>(); + let len = booleans.len(); + // Make sure at least one of them is true + if !booleans.is_empty() && booleans.iter().all(|b| !*b) { + booleans[rng.gen_range(0..len)] = true; + } + + booleans + .iter() + .map(|b| SimplePredicate::arbitrary_from(rng, (table, row, *b)).0) + .reduce(|accum, curr| { + Predicate(Expr::Binary( + Box::new(accum.0), + ast::Operator::Or, + Box::new(curr.0), + )) + }) + .unwrap_or(Predicate::true_()) + } else { + (0..rng.gen_range(0..=3)) + .map(|_| SimplePredicate::arbitrary_from(rng, (table, row, false)).0) + .reduce(|accum, curr| { + Predicate(Expr::Binary( + Box::new(accum.0), + ast::Operator::Or, + Box::new(curr.0), + )) + }) + .unwrap_or(Predicate::false_()) + } + }; + Self(predicate) + } +} + +#[cfg(test)] +mod tests { + use rand::{Rng as _, SeedableRng as _}; + use rand_chacha::ChaCha8Rng; + + use crate::{ + generation::{pick, predicate::SimplePredicate, Arbitrary, ArbitraryFrom as _}, + model::{ + query::predicate::{expr_to_value, Predicate}, + table::{SimValue, Table}, + }, + }; + + fn get_seed() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + } + + #[test] + fn fuzz_true_binary_predicate() { + let seed = get_seed(); + 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 values: Vec> = (0..num_rows) + .map(|_| { + table + .columns + .iter() + .map(|c| SimValue::arbitrary_from(&mut rng, &c.column_type)) + .collect() + }) + .collect(); + let row = pick(&values, &mut rng); + let predicate = Predicate::true_binary(&mut rng, &table, row); + let value = expr_to_value(&predicate.0, row, &table); + assert!( + value.as_ref().map_or(false, |value| value.into_bool()), + "Predicate: {:#?}\nValue: {:#?}\nSeed: {}", + predicate, + value, + seed + ) + } + } + + #[test] + fn fuzz_false_binary_predicate() { + let seed = get_seed(); + 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 values: Vec> = (0..num_rows) + .map(|_| { + table + .columns + .iter() + .map(|c| SimValue::arbitrary_from(&mut rng, &c.column_type)) + .collect() + }) + .collect(); + let row = pick(&values, &mut rng); + let predicate = Predicate::false_binary(&mut rng, &table, row); + let value = expr_to_value(&predicate.0, row, &table); + assert!( + !value.as_ref().map_or(false, |value| value.into_bool()), + "Predicate: {:#?}\nValue: {:#?}\nSeed: {}", + predicate, + value, + seed + ) + } + } + + #[test] + fn fuzz_true_binary_simple_predicate() { + let seed = get_seed(); + 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 values: Vec> = (0..num_rows) + .map(|_| { + table + .columns + .iter() + .map(|c| SimValue::arbitrary_from(&mut rng, &c.column_type)) + .collect() + }) + .collect(); + table.rows.extend(values.clone()); + let row = pick(&table.rows, &mut rng); + let predicate = SimplePredicate::true_binary(&mut rng, &table, row); + let result = values + .iter() + .map(|row| predicate.0.test(row, &table)) + .reduce(|accum, curr| accum || curr) + .unwrap_or(false); + assert!(result, "Predicate: {:#?}\nSeed: {}", predicate, seed) + } + } + + #[test] + fn fuzz_false_binary_simple_predicate() { + let seed = get_seed(); + 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 values: Vec> = (0..num_rows) + .map(|_| { + table + .columns + .iter() + .map(|c| SimValue::arbitrary_from(&mut rng, &c.column_type)) + .collect() + }) + .collect(); + table.rows.extend(values.clone()); + let row = pick(&table.rows, &mut rng); + let predicate = SimplePredicate::false_binary(&mut rng, &table, row); + let result = values + .iter() + .map(|row| predicate.0.test(row, &table)) + .any(|res| !res); + assert!(result, "Predicate: {:#?}\nSeed: {}", predicate, seed) + } + } +} diff --git a/simulator/generation/predicate/mod.rs b/simulator/generation/predicate/mod.rs new file mode 100644 index 000000000..846f1d23a --- /dev/null +++ b/simulator/generation/predicate/mod.rs @@ -0,0 +1,390 @@ +use limbo_sqlite3_parser::ast::{self, Expr}; +use rand::{seq::SliceRandom as _, Rng}; + +use crate::model::{ + query::predicate::Predicate, + table::{SimValue, Table}, +}; + +use super::{one_of, ArbitraryFrom}; + +mod binary; +mod unary; + +#[derive(Debug)] +struct CompoundPredicate(Predicate); + +#[derive(Debug)] +struct SimplePredicate(Predicate); + +impl> ArbitraryFrom<(&Table, A, bool)> for SimplePredicate { + fn arbitrary_from( + rng: &mut R, + (table, row, predicate_value): (&Table, A, bool), + ) -> Self { + let row = row.as_ref(); + // Pick an operator + let choice = rng.gen_range(0..2); + // Pick an operator + match predicate_value { + true => match choice { + 0 => SimplePredicate::true_binary(rng, table, row), + 1 => SimplePredicate::true_unary(rng, table, row), + _ => unreachable!(), + }, + false => match choice { + 0 => SimplePredicate::false_binary(rng, table, row), + 1 => SimplePredicate::false_unary(rng, table, row), + _ => unreachable!(), + }, + } + } +} + +impl ArbitraryFrom<(&Table, bool)> for CompoundPredicate { + fn arbitrary_from(rng: &mut R, (table, predicate_value): (&Table, bool)) -> Self { + CompoundPredicate::from_table_binary(rng, table, predicate_value) + } +} + +impl ArbitraryFrom<&Table> for Predicate { + fn arbitrary_from(rng: &mut R, table: &Table) -> Self { + let predicate_value = rng.gen_bool(0.5); + Predicate::arbitrary_from(rng, (table, predicate_value)) + } +} + +impl ArbitraryFrom<(&Table, bool)> for Predicate { + fn arbitrary_from(rng: &mut R, (table, predicate_value): (&Table, bool)) -> Self { + CompoundPredicate::arbitrary_from(rng, (table, predicate_value)).0 + } +} + +impl ArbitraryFrom<(&str, &SimValue)> for Predicate { + fn arbitrary_from(rng: &mut R, (column_name, value): (&str, &SimValue)) -> Self { + Predicate::from_column_binary(rng, column_name, value) + } +} + +impl ArbitraryFrom<(&Table, &Vec)> for Predicate { + fn arbitrary_from(rng: &mut R, (t, row): (&Table, &Vec)) -> Self { + // We want to produce a predicate that is true for the row + // We can do this by creating several predicates that + // 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)) + .map(|_| Predicate::true_binary(rng, t, row)) + .collect::>(); + + let false_predicates = (0..=rng.gen_range(0..=3)) + .map(|_| Predicate::false_binary(rng, t, row)) + .collect::>(); + + // Start building a top level predicate from a true predicate + let mut result = true_predicates.pop().unwrap(); + + let mut predicates = true_predicates + .iter() + .map(|p| (true, p.clone())) + .chain(false_predicates.iter().map(|p| (false, p.clone()))) + .collect::>(); + + predicates.shuffle(rng); + + 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(); + // Shift `predicates` to remove the predicates in the context + predicates = predicates[context.len()..].to_vec(); + + // `result` is true, so we have the following three options to make a true predicate: + // T or F + // T or T + // T and T + + result = one_of( + vec![ + // T or (X1 or X2 or ... or Xn) + Box::new(|_| { + Predicate(Expr::Binary( + Box::new(result.0.clone()), + ast::Operator::Or, + Box::new( + context + .iter() + .map(|(_, p)| p.clone()) + .reduce(|accum, curr| { + Predicate(Expr::Binary( + Box::new(accum.0), + ast::Operator::Or, + Box::new(curr.0), + )) + }) + .unwrap_or(Predicate::false_()) + .0, + ), + )) + }), + // T or (T1 and T2 and ... and Tn) + Box::new(|_| { + Predicate(Expr::Binary( + Box::new(result.0.clone()), + ast::Operator::Or, + Box::new( + context + .iter() + .map(|(_, p)| p.clone()) + .reduce(|accum, curr| { + Predicate(Expr::Binary( + Box::new(accum.0), + ast::Operator::And, + Box::new(curr.0), + )) + }) + .unwrap_or(Predicate::true_()) + .0, + ), + )) + }), + // T and T + Box::new(|_| { + // Check if all the predicates in the context are true + if context.iter().all(|(b, _)| *b) { + // T and (X1 or X2 or ... or Xn) + Predicate(Expr::Binary( + Box::new(result.0.clone()), + ast::Operator::And, + Box::new( + context + .iter() + .map(|(_, p)| p.clone()) + .reduce(|accum, curr| { + Predicate(Expr::Binary( + Box::new(accum.0), + ast::Operator::And, + Box::new(curr.0), + )) + }) + .unwrap_or(Predicate::true_()) + .0, + ), + )) + } + // Check if there is at least one true predicate + else if context.iter().any(|(b, _)| *b) { + // T and (X1 or X2 or ... or Xn) + Predicate(Expr::Binary( + Box::new(result.0.clone()), + ast::Operator::And, + Box::new( + context + .iter() + .map(|(_, p)| p.clone()) + .reduce(|accum, curr| { + Predicate(Expr::Binary( + Box::new(accum.0), + ast::Operator::Or, + Box::new(curr.0), + )) + }) + .unwrap_or(Predicate::false_()) + .0, + ), + )) + // Predicate::And(vec![ + // result.clone(), + // Predicate::Or(context.iter().map(|(_, p)| p.clone()).collect()), + // ]) + } else { + // T and (X1 or X2 or ... or Xn or TRUE) + Predicate(Expr::Binary( + Box::new(result.0.clone()), + ast::Operator::And, + Box::new( + context + .iter() + .map(|(_, p)| p.clone()) + .chain(std::iter::once(Predicate::true_())) + .reduce(|accum, curr| { + Predicate(Expr::Binary( + Box::new(accum.0), + ast::Operator::Or, + Box::new(curr.0), + )) + }) + .unwrap() // Chain guarantees at least one value + .0, + ), + )) + } + }), + ], + rng, + ); + } + result + } +} + +#[cfg(test)] +mod tests { + use rand::{Rng as _, SeedableRng as _}; + use rand_chacha::ChaCha8Rng; + + use crate::{ + generation::{pick, predicate::SimplePredicate, Arbitrary, ArbitraryFrom as _}, + model::{ + query::predicate::{expr_to_value, Predicate}, + table::{SimValue, Table}, + }, + }; + + fn get_seed() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + } + + #[test] + fn fuzz_arbitrary_table_true_simple_predicate() { + let seed = get_seed(); + 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 values: Vec> = (0..num_rows) + .map(|_| { + table + .columns + .iter() + .map(|c| SimValue::arbitrary_from(&mut rng, &c.column_type)) + .collect() + }) + .collect(); + let row = pick(&values, &mut rng); + let predicate = SimplePredicate::arbitrary_from(&mut rng, (&table, row, true)).0; + let value = expr_to_value(&predicate.0, row, &table); + assert!( + value.as_ref().map_or(false, |value| value.into_bool()), + "Predicate: {:#?}\nValue: {:#?}\nSeed: {}", + predicate, + value, + seed + ) + } + } + + #[test] + fn fuzz_arbitrary_table_false_simple_predicate() { + let seed = get_seed(); + 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 values: Vec> = (0..num_rows) + .map(|_| { + table + .columns + .iter() + .map(|c| SimValue::arbitrary_from(&mut rng, &c.column_type)) + .collect() + }) + .collect(); + let row = pick(&values, &mut rng); + let predicate = SimplePredicate::arbitrary_from(&mut rng, (&table, row, false)).0; + let value = expr_to_value(&predicate.0, row, &table); + assert!( + !value.as_ref().map_or(false, |value| value.into_bool()), + "Predicate: {:#?}\nValue: {:#?}\nSeed: {}", + predicate, + value, + seed + ) + } + } + + #[test] + fn fuzz_arbitrary_row_table_predicate() { + let seed = get_seed(); + 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 values: Vec> = (0..num_rows) + .map(|_| { + table + .columns + .iter() + .map(|c| SimValue::arbitrary_from(&mut rng, &c.column_type)) + .collect() + }) + .collect(); + let row = pick(&values, &mut rng); + let predicate = Predicate::arbitrary_from(&mut rng, (&table, row)); + let value = expr_to_value(&predicate.0, row, &table); + assert!( + value.as_ref().map_or(false, |value| value.into_bool()), + "Predicate: {:#?}\nValue: {:#?}\nSeed: {}", + predicate, + value, + seed + ) + } + } + + #[test] + fn fuzz_arbitrary_true_table_predicate() { + let seed = get_seed(); + 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 values: Vec> = (0..num_rows) + .map(|_| { + table + .columns + .iter() + .map(|c| SimValue::arbitrary_from(&mut rng, &c.column_type)) + .collect() + }) + .collect(); + table.rows.extend(values.clone()); + let predicate = Predicate::arbitrary_from(&mut rng, (&table, true)); + let result = values + .iter() + .map(|row| predicate.test(row, &table)) + .reduce(|accum, curr| accum || curr) + .unwrap_or(false); + assert!(result, "Predicate: {:#?}\nSeed: {}", predicate, seed) + } + } + + #[test] + fn fuzz_arbitrary_false_table_predicate() { + let seed = get_seed(); + 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 values: Vec> = (0..num_rows) + .map(|_| { + table + .columns + .iter() + .map(|c| SimValue::arbitrary_from(&mut rng, &c.column_type)) + .collect() + }) + .collect(); + table.rows.extend(values.clone()); + let predicate = Predicate::arbitrary_from(&mut rng, (&table, false)); + let result = values + .iter() + .map(|row| predicate.test(row, &table)) + .any(|res| !res); + assert!(result, "Predicate: {:#?}\nSeed: {}", predicate, seed) + } + } +} diff --git a/simulator/generation/predicate/unary.rs b/simulator/generation/predicate/unary.rs new file mode 100644 index 000000000..8f090a206 --- /dev/null +++ b/simulator/generation/predicate/unary.rs @@ -0,0 +1,296 @@ +//! Contains code regarding generation for [ast::Expr::Unary] Predicate +//! TODO: for now just generating [ast::Literal], but want to also generate Columns and any +//! arbitrary [ast::Expr] + +use limbo_sqlite3_parser::ast::{self, Expr}; + +use crate::{ + generation::{backtrack, pick, predicate::SimplePredicate, ArbitraryFromMaybe}, + model::{ + query::predicate::Predicate, + table::{SimValue, Table}, + }, +}; + +pub struct TrueValue(pub SimValue); + +impl ArbitraryFromMaybe<&SimValue> for TrueValue { + fn arbitrary_from_maybe(_rng: &mut R, value: &SimValue) -> Option + where + Self: Sized, + { + // If the Value is a true value return it else you cannot return a true Value + value.into_bool().then_some(Self(value.clone())) + } +} + +impl ArbitraryFromMaybe<&Vec<&SimValue>> for TrueValue { + fn arbitrary_from_maybe(rng: &mut R, values: &Vec<&SimValue>) -> Option + where + Self: Sized, + { + if values.is_empty() { + return Some(Self(SimValue::TRUE)); + } + + let value = pick(values, rng); + Self::arbitrary_from_maybe(rng, *value) + } +} + +pub struct FalseValue(pub SimValue); + +impl ArbitraryFromMaybe<&SimValue> for FalseValue { + fn arbitrary_from_maybe(_rng: &mut R, value: &SimValue) -> Option + where + Self: Sized, + { + // If the Value is a false value return it else you cannot return a false Value + (!value.into_bool()).then_some(Self(value.clone())) + } +} + +impl ArbitraryFromMaybe<&Vec<&SimValue>> for FalseValue { + fn arbitrary_from_maybe(rng: &mut R, values: &Vec<&SimValue>) -> Option + where + Self: Sized, + { + if values.is_empty() { + return Some(Self(SimValue::FALSE)); + } + + let value = pick(values, rng); + Self::arbitrary_from_maybe(rng, *value) + } +} + +pub struct BitNotValue(pub SimValue); + +impl ArbitraryFromMaybe<(&SimValue, bool)> for BitNotValue { + fn arbitrary_from_maybe( + _rng: &mut R, + (value, predicate): (&SimValue, bool), + ) -> Option + where + Self: Sized, + { + let bit_not_val = value.unary_exec(ast::UnaryOperator::BitwiseNot); + // If you bit not the Value and it meets the predicate return Some, else None + (bit_not_val.into_bool() == predicate).then_some(BitNotValue(value.clone())) + } +} + +impl ArbitraryFromMaybe<(&Vec<&SimValue>, bool)> for BitNotValue { + fn arbitrary_from_maybe( + rng: &mut R, + (values, predicate): (&Vec<&SimValue>, bool), + ) -> Option + where + Self: Sized, + { + if values.is_empty() { + return None; + } + + let value = pick(values, rng); + Self::arbitrary_from_maybe(rng, (*value, predicate)) + } +} + +// TODO: have some more complex generation with columns names here as well +impl SimplePredicate { + /// Generates a true [ast::Expr::Unary] [SimplePredicate] from a [Table] for some values in the table + pub fn true_unary(rng: &mut R, table: &Table, row: &[SimValue]) -> Self { + // Pick a random column + let column_index = rng.gen_range(0..table.columns.len()); + let column_value = &row[column_index]; + let num_retries = row.len(); + // Avoid creation of NULLs + if row.is_empty() { + return SimplePredicate(Predicate(Expr::Literal(SimValue::TRUE.into()))); + } + let expr = backtrack( + vec![ + ( + num_retries, + Box::new(|rng| { + TrueValue::arbitrary_from_maybe(rng, column_value).map(|value| { + assert!(value.0.into_bool()); + // Positive is a no-op in Sqlite + Expr::unary(ast::UnaryOperator::Positive, Expr::Literal(value.0.into())) + }) + }), + ), + ( + num_retries, + Box::new(|rng| { + TrueValue::arbitrary_from_maybe(rng, column_value).map(|value| { + assert!(value.0.into_bool()); + // True Value with negative is still True + Expr::unary(ast::UnaryOperator::Negative, Expr::Literal(value.0.into())) + }) + }), + ), + ( + num_retries, + Box::new(|rng| { + BitNotValue::arbitrary_from_maybe(rng, (column_value, true)).map(|value| { + Expr::unary( + ast::UnaryOperator::BitwiseNot, + Expr::Literal(value.0.into()), + ) + }) + }), + ), + ( + num_retries, + Box::new(|rng| { + FalseValue::arbitrary_from_maybe(rng, column_value).map(|value| { + assert!(!value.0.into_bool()); + Expr::unary(ast::UnaryOperator::Not, Expr::Literal(value.0.into())) + }) + }), + ), + ], + rng, + ); + // If cannot generate a value + SimplePredicate(Predicate( + expr.unwrap_or(Expr::Literal(SimValue::TRUE.into())), + )) + } + + /// Generates a false [ast::Expr::Unary] [SimplePredicate] from a [Table] for a row in the table + pub fn false_unary(rng: &mut R, table: &Table, row: &[SimValue]) -> Self { + // Pick a random column + let column_index = rng.gen_range(0..table.columns.len()); + let column_value = &row[column_index]; + let num_retries = row.len(); + // Avoid creation of NULLs + if row.is_empty() { + return SimplePredicate(Predicate(Expr::Literal(SimValue::FALSE.into()))); + } + let expr = backtrack( + vec![ + ( + num_retries, + Box::new(|rng| { + FalseValue::arbitrary_from_maybe(rng, column_value).map(|value| { + assert!(!value.0.into_bool()); + // Positive is a no-op in Sqlite + Expr::unary(ast::UnaryOperator::Positive, Expr::Literal(value.0.into())) + }) + }), + ), + ( + num_retries, + Box::new(|rng| { + FalseValue::arbitrary_from_maybe(rng, column_value).map(|value| { + assert!(!value.0.into_bool()); + // True Value with negative is still True + Expr::unary(ast::UnaryOperator::Negative, Expr::Literal(value.0.into())) + }) + }), + ), + ( + num_retries, + Box::new(|rng| { + BitNotValue::arbitrary_from_maybe(rng, (column_value, false)).map(|value| { + Expr::unary( + ast::UnaryOperator::BitwiseNot, + Expr::Literal(value.0.into()), + ) + }) + }), + ), + ( + num_retries, + Box::new(|rng| { + TrueValue::arbitrary_from_maybe(rng, column_value).map(|value| { + assert!(value.0.into_bool()); + Expr::unary(ast::UnaryOperator::Not, Expr::Literal(value.0.into())) + }) + }), + ), + ], + rng, + ); + // If cannot generate a value + SimplePredicate(Predicate( + expr.unwrap_or(Expr::Literal(SimValue::FALSE.into())), + )) + } +} + +#[cfg(test)] +mod tests { + use rand::{Rng as _, SeedableRng as _}; + use rand_chacha::ChaCha8Rng; + + use crate::{ + generation::{pick, predicate::SimplePredicate, Arbitrary, ArbitraryFrom as _}, + model::table::{SimValue, Table}, + }; + + fn get_seed() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + } + + #[test] + fn fuzz_true_unary_simple_predicate() { + let seed = get_seed(); + 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 values: Vec> = (0..num_rows) + .map(|_| { + table + .columns + .iter() + .map(|c| SimValue::arbitrary_from(&mut rng, &c.column_type)) + .collect() + }) + .collect(); + table.rows.extend(values.clone()); + let row = pick(&table.rows, &mut rng); + let predicate = SimplePredicate::true_unary(&mut rng, &table, row); + let result = values + .iter() + .map(|row| predicate.0.test(row, &table)) + .reduce(|accum, curr| accum || curr) + .unwrap_or(false); + assert!(result, "Predicate: {:#?}\nSeed: {}", predicate, seed) + } + } + + #[test] + fn fuzz_false_unary_simple_predicate() { + let seed = get_seed(); + 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 values: Vec> = (0..num_rows) + .map(|_| { + table + .columns + .iter() + .map(|c| SimValue::arbitrary_from(&mut rng, &c.column_type)) + .collect() + }) + .collect(); + table.rows.extend(values.clone()); + let row = pick(&table.rows, &mut rng); + let predicate = SimplePredicate::false_unary(&mut rng, &table, row); + let result = values + .iter() + .map(|row| predicate.0.test(row, &table)) + .any(|res| !res); + assert!(result, "Predicate: {:#?}\nSeed: {}", predicate, seed) + } + } +} diff --git a/simulator/generation/property.rs b/simulator/generation/property.rs index 6f47610f2..0694a3d86 100644 --- a/simulator/generation/property.rs +++ b/simulator/generation/property.rs @@ -1,13 +1,15 @@ use limbo_core::LimboError; +use limbo_sqlite3_parser::ast; use serde::{Deserialize, Serialize}; use crate::{ model::{ query::{ - select::{Distinctness, Predicate, ResultColumn}, + predicate::Predicate, + select::{Distinctness, ResultColumn}, Create, Delete, Drop, Insert, Query, Select, }, - table::Value, + table::SimValue, }, runner::env::SimulatorEnv, }; @@ -128,14 +130,14 @@ pub(crate) enum Property { } impl Property { - pub(crate) fn name(&self) -> String { + pub(crate) fn name(&self) -> &str { match self { - Property::InsertValuesSelect { .. } => "Insert-Values-Select".to_string(), - Property::DoubleCreateFailure { .. } => "Double-Create-Failure".to_string(), - Property::SelectLimit { .. } => "Select-Limit".to_string(), - Property::DeleteSelect { .. } => "Delete-Select".to_string(), - Property::DropSelect { .. } => "Drop-Select".to_string(), - Property::SelectSelectOptimizer { .. } => "Select-Select-Optimizer".to_string(), + Property::InsertValuesSelect { .. } => "Insert-Values-Select", + Property::DoubleCreateFailure { .. } => "Double-Create-Failure", + Property::SelectLimit { .. } => "Select-Limit", + Property::DeleteSelect { .. } => "Delete-Select", + Property::DropSelect { .. } => "Drop-Select", + Property::SelectSelectOptimizer { .. } => "Select-Select-Optimizer", } } /// interactions construct a list of interactions, which is an executable representation of the property. @@ -287,20 +289,6 @@ impl Property { }), }); - let assertion = Interaction::Assertion(Assertion { - message: format!( - "select '{}' should return no values for table '{}'", - predicate, table, - ), - func: Box::new(move |stack: &Vec, _: &SimulatorEnv| { - let rows = stack.last().unwrap(); - match rows { - Ok(rows) => Ok(rows.is_empty()), - Err(err) => Err(LimboError::InternalError(err.to_string())), - } - }), - }); - let delete = Interaction::Query(Query::Delete(Delete { table: table.clone(), predicate: predicate.clone(), @@ -314,6 +302,17 @@ impl Property { distinct: Distinctness::All, })); + let assertion = Interaction::Assertion(Assertion { + message: format!("`{}` should return no values for table `{}`", select, table,), + func: Box::new(move |stack: &Vec, _: &SimulatorEnv| { + let rows = stack.last().unwrap(); + match rows { + Ok(rows) => Ok(rows.is_empty()), + Err(err) => Err(LimboError::InternalError(err.to_string())), + } + }), + }); + let mut interactions = Vec::new(); interactions.push(assumption); interactions.push(delete); @@ -382,7 +381,6 @@ impl Property { } }), }); - let select1 = Interaction::Query(Query::Select(Select { table: table.clone(), result_columns: vec![ResultColumn::Expr(predicate.clone())], @@ -391,13 +389,14 @@ impl Property { distinct: Distinctness::All, })); - let select2 = Interaction::Query(Query::Select(Select { + let select2_query = Query::Select(Select { table: table.clone(), result_columns: vec![ResultColumn::Star], predicate: predicate.clone(), limit: None, distinct: Distinctness::All, - })); + }); + let select2 = Interaction::Query(select2_query); let assertion = Interaction::Assertion(Assertion { message: "select queries should return the same amount of results".to_string(), @@ -413,19 +412,14 @@ impl Property { )); } // Count the 1s in the select query without the star - let rows1 = rows1 + let rows1_count = rows1 .iter() .filter(|vs| { let v = vs.first().unwrap(); - if let Value::Integer(i) = v { - *i == 1 - } else { - false - } + v.into_bool() }) .count(); - - Ok(rows1 == rows2.len()) + Ok(rows1_count == rows2.len()) } _ => Ok(false), } @@ -495,7 +489,7 @@ fn property_insert_values_select( let table = pick(&env.tables, rng); // Generate rows to insert let rows = (0..rng.gen_range(1..=5)) - .map(|_| Vec::::arbitrary_from(rng, table)) + .map(|_| Vec::::arbitrary_from(rng, table)) .collect::>(); // Pick a random row to select @@ -696,10 +690,16 @@ fn property_select_select_optimizer(rng: &mut R, env: &SimulatorEn let table = pick(&env.tables, rng); // Generate a random predicate let predicate = Predicate::arbitrary_from(rng, table); + // Transform into a Binary predicate to force values to be casted to a bool + let expr = ast::Expr::Binary( + Box::new(predicate.0), + ast::Operator::And, + Box::new(Predicate::true_().0), + ); Property::SelectSelectOptimizer { table: table.name.clone(), - predicate, + predicate: Predicate(expr), } } @@ -712,28 +712,52 @@ impl ArbitraryFrom<(&SimulatorEnv, &InteractionStats)> for Property { frequency( vec![ ( - f64::min(remaining_.read, remaining_.write), + if !env.opts.disable_insert_values_select { + f64::min(remaining_.read, remaining_.write) + } else { + 0.0 + }, Box::new(|rng: &mut R| property_insert_values_select(rng, env, &remaining_)), ), ( - remaining_.create / 2.0, + if !env.opts.disable_double_create_failure { + remaining_.create / 2.0 + } else { + 0.0 + }, Box::new(|rng: &mut R| property_double_create_failure(rng, env, &remaining_)), ), ( - remaining_.read, + if !env.opts.disable_select_limit { + remaining_.read + } else { + 0.0 + }, Box::new(|rng: &mut R| property_select_limit(rng, env)), ), ( - f64::min(remaining_.read, remaining_.write).min(remaining_.delete), + if !env.opts.disable_delete_select { + f64::min(remaining_.read, remaining_.write).min(remaining_.delete) + } else { + 0.0 + }, Box::new(|rng: &mut R| property_delete_select(rng, env, &remaining_)), ), ( - // remaining_.drop, - 0.0, + if !env.opts.disable_drop_select { + // remaining_.drop + 0.0 + } else { + 0.0 + }, Box::new(|rng: &mut R| property_drop_select(rng, env, &remaining_)), ), ( - remaining_.read / 2.0, + if !env.opts.disable_select_optimizer { + remaining_.read / 2.0 + } else { + 0.0 + }, Box::new(|rng: &mut R| property_select_select_optimizer(rng, env)), ), ], diff --git a/simulator/generation/query.rs b/simulator/generation/query.rs index 315212f8b..09d0f6421 100644 --- a/simulator/generation/query.rs +++ b/simulator/generation/query.rs @@ -1,19 +1,16 @@ use std::collections::HashSet; -use crate::generation::table::{GTValue, LTValue}; -use crate::generation::{one_of, Arbitrary, ArbitraryFrom}; - -use crate::model::query::select::{Distinctness, Predicate, ResultColumn}; +use crate::generation::{Arbitrary, ArbitraryFrom}; +use crate::model::query::predicate::Predicate; +use crate::model::query::select::{Distinctness, ResultColumn}; use crate::model::query::update::Update; use crate::model::query::{Create, Delete, Drop, Insert, Query, Select}; -use crate::model::table::{Table, Value}; +use crate::model::table::{SimValue, Table}; use crate::SimulatorEnv; -use rand::seq::SliceRandom as _; use rand::Rng; use super::property::Remaining; -use super::table::LikeValue; -use super::{backtrack, frequency, pick, ArbitraryFromMaybe}; +use super::{backtrack, frequency, pick}; impl Arbitrary for Create { fn arbitrary(rng: &mut R) -> Self { @@ -41,12 +38,12 @@ impl ArbitraryFrom<&SimulatorEnv> for Insert { 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) + let values: Vec> = (0..num_rows) .map(|_| { table .columns .iter() - .map(|c| Value::arbitrary_from(rng, &c.column_type)) + .map(|c| SimValue::arbitrary_from(rng, &c.column_type)) .collect() }) .collect(); @@ -81,6 +78,7 @@ impl ArbitraryFrom<&SimulatorEnv> for Insert { }) }; + // Backtrack here cannot return None backtrack( vec![ (1, Box::new(|rng| gen_values(rng))), @@ -89,6 +87,7 @@ impl ArbitraryFrom<&SimulatorEnv> for Insert { ], rng, ) + .unwrap() } } @@ -137,348 +136,12 @@ impl ArbitraryFrom<(&SimulatorEnv, &Remaining)> for Query { } } -struct CompoundPredicate(Predicate); -struct SimplePredicate(Predicate); - -impl ArbitraryFrom<(&Table, bool)> for SimplePredicate { - fn arbitrary_from(rng: &mut R, (table, predicate_value): (&Table, bool)) -> Self { - // Pick a random column - let column_index = rng.gen_range(0..table.columns.len()); - let column = &table.columns[column_index]; - let column_values = table - .rows - .iter() - .map(|r| &r[column_index]) - .collect::>(); - // Pick an operator - let operator = match predicate_value { - true => one_of( - vec![ - Box::new(|rng| { - Predicate::Eq( - column.name.clone(), - Value::arbitrary_from(rng, &column_values), - ) - }), - Box::new(|rng| { - Predicate::Gt( - column.name.clone(), - GTValue::arbitrary_from(rng, &column_values).0, - ) - }), - Box::new(|rng| { - Predicate::Lt( - column.name.clone(), - LTValue::arbitrary_from(rng, &column_values).0, - ) - }), - ], - rng, - ), - false => one_of( - vec![ - Box::new(|rng| { - Predicate::Neq( - column.name.clone(), - Value::arbitrary_from(rng, &column.column_type), - ) - }), - Box::new(|rng| { - Predicate::Gt( - column.name.clone(), - LTValue::arbitrary_from(rng, &column_values).0, - ) - }), - Box::new(|rng| { - Predicate::Lt( - column.name.clone(), - GTValue::arbitrary_from(rng, &column_values).0, - ) - }), - ], - rng, - ), - }; - - Self(operator) - } -} - -impl ArbitraryFrom<(&Table, bool)> for CompoundPredicate { - fn arbitrary_from(rng: &mut R, (table, predicate_value): (&Table, bool)) -> Self { - // Decide if you want to create an AND or an OR - Self(if rng.gen_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 { - Predicate::And( - (0..rng.gen_range(0..=3)) - .map(|_| SimplePredicate::arbitrary_from(rng, (table, true)).0) - .collect(), - ) - } else { - // Create a vector of random booleans - let mut booleans = (0..rng.gen_range(0..=3)) - .map(|_| rng.gen_bool(0.5)) - .collect::>(); - - let len = booleans.len(); - - // Make sure at least one of them is false - if !booleans.is_empty() && booleans.iter().all(|b| *b) { - booleans[rng.gen_range(0..len)] = false; - } - - Predicate::And( - booleans - .iter() - .map(|b| SimplePredicate::arbitrary_from(rng, (table, *b)).0) - .collect(), - ) - } - } else { - // An OR for true requires at least one of its children to be true - // 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(0..=3)) - .map(|_| rng.gen_bool(0.5)) - .collect::>(); - let len = booleans.len(); - // Make sure at least one of them is true - if !booleans.is_empty() && booleans.iter().all(|b| !*b) { - booleans[rng.gen_range(0..len)] = true; - } - - Predicate::Or( - booleans - .iter() - .map(|b| SimplePredicate::arbitrary_from(rng, (table, *b)).0) - .collect(), - ) - } else { - Predicate::Or( - (0..rng.gen_range(0..=3)) - .map(|_| SimplePredicate::arbitrary_from(rng, (table, false)).0) - .collect(), - ) - } - }) - } -} - -impl ArbitraryFrom<&Table> for Predicate { - fn arbitrary_from(rng: &mut R, table: &Table) -> Self { - let predicate_value = rng.gen_bool(0.5); - CompoundPredicate::arbitrary_from(rng, (table, predicate_value)).0 - } -} - -impl ArbitraryFrom<(&str, &Value)> for Predicate { - fn arbitrary_from(rng: &mut R, (column_name, value): (&str, &Value)) -> Self { - one_of( - vec![ - Box::new(|_| Predicate::Eq(column_name.to_string(), (*value).clone())), - Box::new(|rng| { - Self::Gt( - column_name.to_string(), - GTValue::arbitrary_from(rng, value).0, - ) - }), - Box::new(|rng| { - Self::Lt( - column_name.to_string(), - LTValue::arbitrary_from(rng, value).0, - ) - }), - ], - rng, - ) - } -} - -/// Produces a predicate that is true for the provided row in the given table -fn produce_true_predicate(rng: &mut R, (t, row): (&Table, &Vec)) -> Predicate { - // Pick a column - let column_index = rng.gen_range(0..t.columns.len()); - let column = &t.columns[column_index]; - let value = &row[column_index]; - backtrack( - vec![ - ( - 1, - Box::new(|_| Some(Predicate::Eq(column.name.clone(), value.clone()))), - ), - ( - 1, - Box::new(|rng| { - let v = Value::arbitrary_from(rng, &column.column_type); - if &v == value { - None - } else { - Some(Predicate::Neq(column.name.clone(), v)) - } - }), - ), - ( - 1, - Box::new(|rng| { - Some(Predicate::Gt( - column.name.clone(), - LTValue::arbitrary_from(rng, value).0, - )) - }), - ), - ( - 1, - Box::new(|rng| { - Some(Predicate::Lt( - column.name.clone(), - GTValue::arbitrary_from(rng, value).0, - )) - }), - ), - ( - 1, - Box::new(|rng| { - LikeValue::arbitrary_from_maybe(rng, value) - .map(|like| Predicate::Like(column.name.clone(), like.0)) - }), - ), - ], - rng, - ) -} - -/// Produces a predicate that is false for the provided row in the given table -fn produce_false_predicate(rng: &mut R, (t, row): (&Table, &Vec)) -> Predicate { - // Pick a column - let column_index = rng.gen_range(0..t.columns.len()); - let column = &t.columns[column_index]; - let value = &row[column_index]; - one_of( - vec![ - Box::new(|_| Predicate::Neq(column.name.clone(), value.clone())), - Box::new(|rng| { - let v = loop { - let v = Value::arbitrary_from(rng, &column.column_type); - if &v != value { - break v; - } - }; - Predicate::Eq(column.name.clone(), v) - }), - Box::new(|rng| { - Predicate::Gt(column.name.clone(), GTValue::arbitrary_from(rng, value).0) - }), - Box::new(|rng| { - Predicate::Lt(column.name.clone(), LTValue::arbitrary_from(rng, value).0) - }), - ], - rng, - ) -} - -impl ArbitraryFrom<(&Table, &Vec)> for Predicate { - fn arbitrary_from(rng: &mut R, (t, row): (&Table, &Vec)) -> Self { - // We want to produce a predicate that is true for the row - // We can do this by creating several predicates that - // 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)) - .map(|_| produce_true_predicate(rng, (t, row))) - .collect::>(); - - let false_predicates = (0..=rng.gen_range(0..=3)) - .map(|_| produce_false_predicate(rng, (t, row))) - .collect::>(); - - // Start building a top level predicate from a true predicate - let mut result = true_predicates.pop().unwrap(); - - let mut predicates = true_predicates - .iter() - .map(|p| (true, p.clone())) - .chain(false_predicates.iter().map(|p| (false, p.clone()))) - .collect::>(); - - predicates.shuffle(rng); - - 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(); - // Shift `predicates` to remove the predicates in the context - predicates = predicates[context.len()..].to_vec(); - - // `result` is true, so we have the following three options to make a true predicate: - // T or F - // T or T - // T and T - - result = one_of( - vec![ - // T or (X1 or X2 or ... or Xn) - Box::new(|_| { - Predicate::Or(vec![ - result.clone(), - Predicate::Or(context.iter().map(|(_, p)| p.clone()).collect()), - ]) - }), - // T or (T1 and T2 and ... and Tn) - Box::new(|_| { - Predicate::Or(vec![ - result.clone(), - Predicate::And(context.iter().map(|(_, p)| p.clone()).collect()), - ]) - }), - // T and T - Box::new(|_| { - // Check if all the predicates in the context are true - if context.iter().all(|(b, _)| *b) { - // T and (X1 or X2 or ... or Xn) - Predicate::And(vec![ - result.clone(), - Predicate::And(context.iter().map(|(_, p)| p.clone()).collect()), - ]) - } - // Check if there is at least one true predicate - else if context.iter().any(|(b, _)| *b) { - // T and (X1 or X2 or ... or Xn) - Predicate::And(vec![ - result.clone(), - Predicate::Or(context.iter().map(|(_, p)| p.clone()).collect()), - ]) - } else { - // T and (X1 or X2 or ... or Xn or TRUE) - Predicate::And(vec![ - result.clone(), - Predicate::Or( - context - .iter() - .map(|(_, p)| p.clone()) - .chain(std::iter::once(Predicate::true_())) - .collect(), - ), - ]) - } - }), - ], - rng, - ); - } - - result - } -} - impl ArbitraryFrom<&SimulatorEnv> for Update { fn arbitrary_from(rng: &mut R, env: &SimulatorEnv) -> Self { let table = pick(&env.tables, rng); let mut seen = HashSet::new(); let num_cols = rng.gen_range(1..=table.columns.len()); - let set_values: Vec<(String, Value)> = (0..num_cols) + let set_values: Vec<(String, SimValue)> = (0..num_cols) .map(|_| { let column = loop { let column = pick(&table.columns, rng); @@ -490,7 +153,7 @@ impl ArbitraryFrom<&SimulatorEnv> for Update { seen.insert(column.name.clone()); ( column.name.clone(), - Value::arbitrary_from(rng, &column.column_type), + SimValue::arbitrary_from(rng, &column.column_type), ) }) .collect(); diff --git a/simulator/generation/table.rs b/simulator/generation/table.rs index 415b4b03a..45b2a5a73 100644 --- a/simulator/generation/table.rs +++ b/simulator/generation/table.rs @@ -1,7 +1,10 @@ +use std::collections::HashSet; + +use limbo_core::Value; use rand::Rng; use crate::generation::{gen_random_text, pick, readable_name_custom, Arbitrary, ArbitraryFrom}; -use crate::model::table::{Column, ColumnType, Name, Table, Value}; +use crate::model::table::{Column, ColumnType, Name, SimValue, Table}; use super::ArbitraryFromMaybe; @@ -15,9 +18,20 @@ impl Arbitrary for Name { impl Arbitrary for Table { fn arbitrary(rng: &mut R) -> Self { let name = Name::arbitrary(rng).0; - let columns = (1..=rng.gen_range(1..10)) - .map(|_| Column::arbitrary(rng)) - .collect(); + let columns = loop { + let columns = (1..=rng.gen_range(1..10)) + .map(|_| Column::arbitrary(rng)) + .collect::>(); + // TODO: see if there is a better way to detect duplicates here + let mut set = HashSet::with_capacity(columns.len()); + set.extend(columns.iter()); + // Has repeated column name inside so generate again + if set.len() != columns.len() { + continue; + } + break columns; + }; + Table { rows: Vec::new(), name, @@ -45,62 +59,64 @@ impl Arbitrary for ColumnType { } } -impl ArbitraryFrom<&Table> for Vec { +impl ArbitraryFrom<&Table> for Vec { fn arbitrary_from(rng: &mut R, table: &Table) -> Self { let mut row = Vec::new(); for column in table.columns.iter() { - let value = Value::arbitrary_from(rng, &column.column_type); + let value = SimValue::arbitrary_from(rng, &column.column_type); row.push(value); } row } } -impl ArbitraryFrom<&Vec<&Value>> for Value { +impl ArbitraryFrom<&Vec<&SimValue>> for SimValue { fn arbitrary_from(rng: &mut R, values: &Vec<&Self>) -> Self { if values.is_empty() { - return Self::Null; + return Self(Value::Null); } pick(values, rng).to_owned().clone() } } -impl ArbitraryFrom<&ColumnType> for Value { +impl ArbitraryFrom<&ColumnType> for SimValue { fn arbitrary_from(rng: &mut R, column_type: &ColumnType) -> Self { - match column_type { - ColumnType::Integer => Self::Integer(rng.gen_range(i64::MIN..i64::MAX)), - ColumnType::Float => Self::Float(rng.gen_range(-1e10..1e10)), - ColumnType::Text => Self::Text(gen_random_text(rng)), - ColumnType::Blob => Self::Blob(gen_random_text(rng).as_bytes().to_vec()), - } + 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::Text => Value::build_text(gen_random_text(rng)), + ColumnType::Blob => Value::Blob(gen_random_text(rng).as_bytes().to_vec()), + }; + SimValue(value) } } -pub(crate) struct LTValue(pub(crate) Value); +pub(crate) struct LTValue(pub(crate) SimValue); -impl ArbitraryFrom<&Vec<&Value>> for LTValue { - fn arbitrary_from(rng: &mut R, values: &Vec<&Value>) -> Self { +impl ArbitraryFrom<&Vec<&SimValue>> for LTValue { + fn arbitrary_from(rng: &mut R, values: &Vec<&SimValue>) -> Self { if values.is_empty() { - return Self(Value::Null); + return Self(SimValue(Value::Null)); } - let value = pick(values, rng); - Self::arbitrary_from(rng, *value) + // Get value less than all values + let value = Value::exec_min(values.iter().map(|value| &value.0)); + Self::arbitrary_from(rng, &SimValue(value)) } } -impl ArbitraryFrom<&Value> for LTValue { - fn arbitrary_from(rng: &mut R, value: &Value) -> Self { - match value { - Value::Integer(i) => Self(Value::Integer(rng.gen_range(i64::MIN..*i - 1))), - Value::Float(f) => Self(Value::Float(f - rng.gen_range(0.0..1e10))), - Value::Text(t) => { +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 @ Value::Text(..) => { // Either shorten the string, or make at least one character smaller and mutate the rest - let mut t = t.clone(); + let mut t = value.to_string(); if rng.gen_bool(0.01) { t.pop(); - Self(Value::Text(t)) + Value::build_text(t) } else { let mut t = t.chars().map(|c| c as u32).collect::>(); let index = rng.gen_range(0..t.len()); @@ -113,7 +129,7 @@ impl ArbitraryFrom<&Value> for LTValue { .into_iter() .map(|c| char::from_u32(c).unwrap_or('z')) .collect::(); - Self(Value::Text(t)) + Value::build_text(t) } } Value::Blob(b) => { @@ -121,7 +137,7 @@ impl ArbitraryFrom<&Value> for LTValue { let mut b = b.clone(); if rng.gen_bool(0.01) { b.pop(); - Self(Value::Blob(b)) + Value::Blob(b) } else { let index = rng.gen_range(0..b.len()); b[index] -= 1; @@ -129,38 +145,40 @@ impl ArbitraryFrom<&Value> for LTValue { for i in (index + 1)..b.len() { b[i] = rng.gen_range(0..=255); } - Self(Value::Blob(b)) + Value::Blob(b) } } _ => unreachable!(), - } + }; + Self(SimValue(new_value)) } } -pub(crate) struct GTValue(pub(crate) Value); +pub(crate) struct GTValue(pub(crate) SimValue); -impl ArbitraryFrom<&Vec<&Value>> for GTValue { - fn arbitrary_from(rng: &mut R, values: &Vec<&Value>) -> Self { +impl ArbitraryFrom<&Vec<&SimValue>> for GTValue { + fn arbitrary_from(rng: &mut R, values: &Vec<&SimValue>) -> Self { if values.is_empty() { - return Self(Value::Null); + return Self(SimValue(Value::Null)); } + // Get value greater than all values + let value = Value::exec_max(values.iter().map(|value| &value.0)); - let value = pick(values, rng); - Self::arbitrary_from(rng, *value) + Self::arbitrary_from(rng, &SimValue(value)) } } -impl ArbitraryFrom<&Value> for GTValue { - fn arbitrary_from(rng: &mut R, value: &Value) -> Self { - match value { - Value::Integer(i) => Self(Value::Integer(rng.gen_range(*i..i64::MAX))), - Value::Float(f) => Self(Value::Float(rng.gen_range(*f..1e10))), - Value::Text(t) => { +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 @ Value::Text(..) => { // Either lengthen the string, or make at least one character smaller and mutate the rest - let mut t = t.clone(); + let mut t = value.to_string(); if rng.gen_bool(0.01) { t.push(rng.gen_range(0..=255) as u8 as char); - Self(Value::Text(t)) + Value::build_text(t) } else { let mut t = t.chars().map(|c| c as u32).collect::>(); let index = rng.gen_range(0..t.len()); @@ -173,7 +191,7 @@ impl ArbitraryFrom<&Value> for GTValue { .into_iter() .map(|c| char::from_u32(c).unwrap_or('a')) .collect::(); - Self(Value::Text(t)) + Value::build_text(t) } } Value::Blob(b) => { @@ -181,7 +199,7 @@ impl ArbitraryFrom<&Value> for GTValue { let mut b = b.clone(); if rng.gen_bool(0.01) { b.push(rng.gen_range(0..=255)); - Self(Value::Blob(b)) + Value::Blob(b) } else { let index = rng.gen_range(0..b.len()); b[index] += 1; @@ -189,20 +207,22 @@ impl ArbitraryFrom<&Value> for GTValue { for i in (index + 1)..b.len() { b[i] = rng.gen_range(0..=255); } - Self(Value::Blob(b)) + Value::Blob(b) } } _ => unreachable!(), - } + }; + Self(SimValue(new_value)) } } -pub(crate) struct LikeValue(pub(crate) String); +pub(crate) struct LikeValue(pub(crate) SimValue); -impl ArbitraryFromMaybe<&Value> for LikeValue { - fn arbitrary_from_maybe(rng: &mut R, value: &Value) -> Option { - match value { - Value::Text(t) => { +impl ArbitraryFromMaybe<&SimValue> for LikeValue { + fn arbitrary_from_maybe(rng: &mut R, value: &SimValue) -> Option { + match &value.0 { + value @ Value::Text(..) => { + let t = value.to_string(); let mut t = t.chars().collect::>(); // Remove a number of characters, either insert `_` for each character removed, or // insert one `%` for the whole substring @@ -221,7 +241,9 @@ impl ArbitraryFromMaybe<&Value> for LikeValue { } let index = rng.gen_range(0..t.len()); t.insert(index, '%'); - Some(Self(t.into_iter().collect())) + Some(Self(SimValue(Value::build_text( + t.into_iter().collect::(), + )))) } _ => None, } diff --git a/simulator/main.rs b/simulator/main.rs index 5cf8de426..2feb1bfc8 100644 --- a/simulator/main.rs +++ b/simulator/main.rs @@ -17,6 +17,7 @@ use std::fs::OpenOptions; use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::{mpsc, Arc, Mutex}; +use tracing_subscriber::field::MakeExt; use tracing_subscriber::EnvFilter; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -359,6 +360,8 @@ fn run_simulator( ) => { if e1 != e2 { tracing::error!( + ?shrunk, + ?result, "shrinking failed, the error was not properly reproduced" ); if let Some(bugbase) = bugbase { @@ -391,7 +394,11 @@ fn run_simulator( unreachable!("shrinking should never be called on a correct simulation") } _ => { - tracing::error!("shrinking failed, the error was not properly reproduced"); + tracing::error!( + ?shrunk, + ?result, + "shrinking failed, the error was not properly reproduced" + ); if let Some(bugbase) = bugbase { bugbase .add_bug(seed, plans[0].clone(), Some(error.clone()), cli_opts) @@ -701,7 +708,8 @@ fn init_logger() { .with_ansi(true) .with_line_number(true) .without_time() - .with_thread_ids(false), + .with_thread_ids(false) + .map_fmt_fields(|f| f.debug_alt()), ) .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) .with( @@ -710,7 +718,8 @@ fn init_logger() { .with_ansi(false) .with_line_number(true) .without_time() - .with_thread_ids(false), + .with_thread_ids(false) + .map_fmt_fields(|f| f.debug_alt()), ) .try_init(); } diff --git a/simulator/model/query/create.rs b/simulator/model/query/create.rs index 172fd9c9d..eba5842ab 100644 --- a/simulator/model/query/create.rs +++ b/simulator/model/query/create.rs @@ -3,7 +3,7 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; use crate::{ - model::table::{Table, Value}, + model::table::{SimValue, Table}, SimulatorEnv, }; @@ -13,7 +13,7 @@ pub(crate) struct Create { } impl Create { - pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { + pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { if !env.tables.iter().any(|t| t.name == self.table.name) { env.tables.push(self.table.clone()); } diff --git a/simulator/model/query/create_index.rs b/simulator/model/query/create_index.rs index ea003e7f4..25ea7e795 100644 --- a/simulator/model/query/create_index.rs +++ b/simulator/model/query/create_index.rs @@ -31,7 +31,7 @@ impl CreateIndex { pub(crate) fn shadow( &self, _env: &mut crate::runner::env::SimulatorEnv, - ) -> Vec> { + ) -> Vec> { // CREATE INDEX doesn't require any shadowing; we don't need to keep track // in the simulator what indexes exist. vec![] diff --git a/simulator/model/query/delete.rs b/simulator/model/query/delete.rs index f6f224fb8..e375a62ed 100644 --- a/simulator/model/query/delete.rs +++ b/simulator/model/query/delete.rs @@ -2,9 +2,9 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; -use crate::{model::table::Value, SimulatorEnv}; +use crate::{model::table::SimValue, SimulatorEnv}; -use super::select::Predicate; +use super::predicate::Predicate; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub(crate) struct Delete { @@ -13,7 +13,7 @@ pub(crate) struct Delete { } impl Delete { - pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { + pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { let table = env .tables .iter_mut() diff --git a/simulator/model/query/drop.rs b/simulator/model/query/drop.rs index b38c9dc71..2731586da 100644 --- a/simulator/model/query/drop.rs +++ b/simulator/model/query/drop.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; -use crate::{model::table::Value, SimulatorEnv}; +use crate::{model::table::SimValue, SimulatorEnv}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub(crate) struct Drop { @@ -10,7 +10,7 @@ pub(crate) struct Drop { } impl Drop { - pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { + pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { env.tables.retain(|t| t.name != self.table); vec![] } diff --git a/simulator/model/query/insert.rs b/simulator/model/query/insert.rs index 109c9df3a..47ad98e47 100644 --- a/simulator/model/query/insert.rs +++ b/simulator/model/query/insert.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; -use crate::{model::table::Value, SimulatorEnv}; +use crate::{model::table::SimValue, SimulatorEnv}; use super::select::Select; @@ -10,7 +10,7 @@ use super::select::Select; pub(crate) enum Insert { Values { table: String, - values: Vec>, + values: Vec>, }, Select { table: String, @@ -19,7 +19,7 @@ pub(crate) enum Insert { } impl Insert { - pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { + pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { match self { Insert::Values { table, values } => { if let Some(t) = env.tables.iter_mut().find(|t| &t.name == table) { diff --git a/simulator/model/query/mod.rs b/simulator/model/query/mod.rs index ed947ca9b..31c3d52ce 100644 --- a/simulator/model/query/mod.rs +++ b/simulator/model/query/mod.rs @@ -5,17 +5,19 @@ pub(crate) use create_index::CreateIndex; pub(crate) use delete::Delete; pub(crate) use drop::Drop; pub(crate) use insert::Insert; +use limbo_sqlite3_parser::to_sql_string::ToSqlContext; pub(crate) use select::Select; use serde::{Deserialize, Serialize}; use update::Update; -use crate::{model::table::Value, runner::env::SimulatorEnv}; +use crate::{model::table::SimValue, runner::env::SimulatorEnv}; pub mod create; pub mod create_index; pub mod delete; pub mod drop; pub mod insert; +pub mod predicate; pub mod select; pub mod update; @@ -59,7 +61,7 @@ impl Query { } } - pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { + pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { match self { Query::Create(create) => create.shadow(env), Query::Insert(insert) => insert.shadow(env), @@ -85,3 +87,20 @@ impl Display for Query { } } } + +/// Used to print sql strings that already have all the context it needs +struct EmptyContext; + +impl ToSqlContext for EmptyContext { + fn get_column_name( + &self, + _table_id: limbo_sqlite3_parser::ast::TableInternalId, + _col_idx: usize, + ) -> &str { + unreachable!() + } + + fn get_table_name(&self, _id: limbo_sqlite3_parser::ast::TableInternalId) -> &str { + unreachable!() + } +} diff --git a/simulator/model/query/predicate.rs b/simulator/model/query/predicate.rs new file mode 100644 index 000000000..9e3c0f4eb --- /dev/null +++ b/simulator/model/query/predicate.rs @@ -0,0 +1,78 @@ +use std::fmt::Display; + +use limbo_sqlite3_parser::{ast, to_sql_string::ToSqlString}; +use serde::{Deserialize, Serialize}; + +use crate::model::{ + query::EmptyContext, + table::{SimValue, Table}, +}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Predicate(pub ast::Expr); + +impl Predicate { + pub(crate) fn true_() -> Self { + Self(ast::Expr::Literal(ast::Literal::Numeric("1".to_string()))) + } + + pub(crate) fn false_() -> Self { + Self(ast::Expr::Literal(ast::Literal::Numeric("0".to_string()))) + } + + pub(crate) fn test(&self, row: &[SimValue], table: &Table) -> bool { + let value = expr_to_value(&self.0, row, table); + value.map_or(false, |value| value.into_bool()) + } +} + +// TODO: In the future pass a Vec to support resolving a value from another table +// This function attempts to convert an simpler easily computable expression into values +// TODO: In the future, we can try to expand this computation if we want to support harder properties that require us +// to already know more values before hand +pub fn expr_to_value(expr: &ast::Expr, row: &[SimValue], table: &Table) -> Option { + match expr { + ast::Expr::DoublyQualified(_, _, ast::Name(col_name)) + | ast::Expr::Qualified(_, ast::Name(col_name)) + | ast::Expr::Id(ast::Id(col_name)) => { + assert_eq!(row.len(), table.columns.len()); + table + .columns + .iter() + .zip(row.iter()) + .find(|(column, _)| column.name == *col_name) + .map(|(_, value)| value) + .cloned() + } + ast::Expr::Literal(literal) => Some(literal.into()), + ast::Expr::Binary(lhs, op, rhs) => { + let lhs = expr_to_value(lhs, row, table)?; + let rhs = expr_to_value(rhs, row, table)?; + Some(lhs.binary_compare(&rhs, *op)) + } + ast::Expr::Like { + lhs, + not, + op, + rhs, + escape: _, // TODO: support escape + } => { + let lhs = expr_to_value(lhs, row, table)?; + let rhs = expr_to_value(rhs, row, table)?; + let res = lhs.like_compare(&rhs, *op); + let value: SimValue = if *not { !res } else { res }.into(); + Some(value) + } + ast::Expr::Unary(op, expr) => { + let value = expr_to_value(expr, row, table)?; + Some(value.unary_exec(*op)) + } + _ => unreachable!("{:?}", expr), + } +} + +impl Display for Predicate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.to_sql_string(&EmptyContext)) + } +} diff --git a/simulator/model/query/select.rs b/simulator/model/query/select.rs index 8dfaf9aa8..f51db22ed 100644 --- a/simulator/model/query/select.rs +++ b/simulator/model/query/select.rs @@ -1,12 +1,10 @@ use std::fmt::Display; -use regex::{Regex, RegexBuilder}; use serde::{Deserialize, Serialize}; -use crate::{ - model::table::{Table, Value}, - SimulatorEnv, -}; +use crate::{model::table::SimValue, SimulatorEnv}; + +use super::predicate::Predicate; /// `SELECT` distinctness #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -48,7 +46,7 @@ pub(crate) struct Select { } impl Select { - pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { + pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { let table = env.tables.iter().find(|t| t.name == self.table.as_str()); if let Some(table) = table { table @@ -80,123 +78,3 @@ impl Display for Select { ) } } - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub(crate) enum Predicate { - And(Vec), // p1 AND p2 AND p3... AND pn - Or(Vec), // p1 OR p2 OR p3... OR pn - Eq(String, Value), // column = Value - Neq(String, Value), // column != Value - Gt(String, Value), // column > Value - Lt(String, Value), // column < Value - Like(String, String), // column LIKE Value -} - -/// This function is a duplication of the exec_like function in core/vdbe/mod.rs at commit 9b9d5f9b4c9920e066ef1237c80878f4c3968524 -/// Any updates to the original function should be reflected here, otherwise the test will be incorrect. -fn construct_like_regex(pattern: &str) -> Regex { - let mut regex_pattern = String::with_capacity(pattern.len() * 2); - - regex_pattern.push('^'); - - for c in pattern.chars() { - match c { - '\\' => regex_pattern.push_str("\\\\"), - '%' => regex_pattern.push_str(".*"), - '_' => regex_pattern.push('.'), - ch => { - if regex_syntax::is_meta_character(c) { - regex_pattern.push('\\'); - } - regex_pattern.push(ch); - } - } - } - - regex_pattern.push('$'); - - RegexBuilder::new(®ex_pattern) - .case_insensitive(true) - .dot_matches_new_line(true) - .build() - .unwrap() -} - -fn exec_like(pattern: &str, text: &str) -> bool { - let re = construct_like_regex(pattern); - re.is_match(text) -} - -impl Predicate { - pub(crate) fn true_() -> Self { - Self::And(vec![]) - } - - pub(crate) fn false_() -> Self { - Self::Or(vec![]) - } - - pub(crate) fn test(&self, row: &[Value], table: &Table) -> bool { - let get_value = |name: &str| { - table - .columns - .iter() - .zip(row.iter()) - .find(|(column, _)| column.name == name) - .map(|(_, value)| value) - }; - - match self { - Predicate::And(vec) => vec.iter().all(|p| p.test(row, table)), - Predicate::Or(vec) => vec.iter().any(|p| p.test(row, table)), - Predicate::Eq(column, value) => get_value(column) == Some(value), - Predicate::Neq(column, value) => get_value(column) != Some(value), - Predicate::Gt(column, value) => get_value(column).map(|v| v > value).unwrap_or(false), - Predicate::Lt(column, value) => get_value(column).map(|v| v < value).unwrap_or(false), - Predicate::Like(column, value) => get_value(column) - .map(|v| exec_like(v.to_string().as_str(), value.as_str())) - .unwrap_or(false), - } - } -} - -impl Display for Predicate { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::And(predicates) => { - if predicates.is_empty() { - // todo: Make this TRUE when the bug is fixed - write!(f, "TRUE") - } else { - write!(f, "(")?; - for (i, p) in predicates.iter().enumerate() { - if i != 0 { - write!(f, " AND ")?; - } - write!(f, "{}", p)?; - } - write!(f, ")") - } - } - Self::Or(predicates) => { - if predicates.is_empty() { - write!(f, "FALSE") - } else { - write!(f, "(")?; - for (i, p) in predicates.iter().enumerate() { - if i != 0 { - write!(f, " OR ")?; - } - write!(f, "{}", p)?; - } - write!(f, ")") - } - } - Self::Eq(name, value) => write!(f, "{} = {}", name, value), - Self::Neq(name, value) => write!(f, "{} != {}", name, value), - Self::Gt(name, value) => write!(f, "{} > {}", name, value), - Self::Lt(name, value) => write!(f, "{} < {}", name, value), - Self::Like(name, value) => write!(f, "{} LIKE '{}'", name, value), - } - } -} diff --git a/simulator/model/query/update.rs b/simulator/model/query/update.rs index acfc1e8e4..7bcff95e2 100644 --- a/simulator/model/query/update.rs +++ b/simulator/model/query/update.rs @@ -2,19 +2,19 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; -use crate::{model::table::Value, SimulatorEnv}; +use crate::{model::table::SimValue, SimulatorEnv}; -use super::select::Predicate; +use super::predicate::Predicate; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub(crate) struct Update { pub(crate) table: String, - pub(crate) set_values: Vec<(String, Value)>, // Pair of value for set expressions => SET name=value + pub(crate) set_values: Vec<(String, SimValue)>, // Pair of value for set expressions => SET name=value pub(crate) predicate: Predicate, } impl Update { - pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { + pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec> { let table = env .tables .iter_mut() diff --git a/simulator/model/table.rs b/simulator/model/table.rs index ff3e2e5bf..e304cab0d 100644 --- a/simulator/model/table.rs +++ b/simulator/model/table.rs @@ -1,5 +1,7 @@ -use std::{fmt::Display, ops::Deref}; +use std::{fmt::Display, hash::Hash, ops::Deref}; +use limbo_core::{numeric::Numeric, types}; +use limbo_sqlite3_parser::ast; use serde::{Deserialize, Serialize}; pub(crate) struct Name(pub(crate) String); @@ -14,7 +16,7 @@ impl Deref for Name { #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct Table { - pub(crate) rows: Vec>, + pub(crate) rows: Vec>, pub(crate) name: String, pub(crate) columns: Vec, } @@ -27,6 +29,21 @@ pub(crate) struct Column { pub(crate) unique: bool, } +// Uniquely defined by name in this case +impl Hash for Column { + fn hash(&self, state: &mut H) { + self.name.hash(state); + } +} + +impl PartialEq for Column { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Eq for Column {} + #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) enum ColumnType { Integer, @@ -61,35 +78,8 @@ where s.parse().map_err(serde::de::Error::custom) } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub(crate) enum Value { - Null, - Integer(i64), - // we use custom serialization to preserve float precision - #[serde( - serialize_with = "float_to_string", - deserialize_with = "string_to_float" - )] - Float(f64), - Text(String), - Blob(Vec), -} - -impl PartialOrd for Value { - fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (Self::Null, Self::Null) => Some(std::cmp::Ordering::Equal), - (Self::Null, _) => Some(std::cmp::Ordering::Less), - (_, Self::Null) => Some(std::cmp::Ordering::Greater), - (Self::Integer(i1), Self::Integer(i2)) => i1.partial_cmp(i2), - (Self::Float(f1), Self::Float(f2)) => f1.partial_cmp(f2), - (Self::Text(t1), Self::Text(t2)) => t1.partial_cmp(t2), - (Self::Blob(b1), Self::Blob(b2)) => b1.partial_cmp(b2), - // todo: add type coercions here - _ => None, - } - } -} +#[derive(Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)] +pub(crate) struct SimValue(pub limbo_core::Value); fn to_sqlite_blob(bytes: &[u8]) -> String { format!( @@ -100,14 +90,175 @@ fn to_sqlite_blob(bytes: &[u8]) -> String { ) } -impl Display for Value { +impl Display for SimValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Null => write!(f, "NULL"), - Self::Integer(i) => write!(f, "{}", i), - Self::Float(fl) => write!(f, "{}", fl), - Self::Text(t) => write!(f, "'{}'", t), - Self::Blob(b) => write!(f, "{}", to_sqlite_blob(b)), + match &self.0 { + types::Value::Null => write!(f, "NULL"), + types::Value::Integer(i) => write!(f, "{}", i), + types::Value::Float(fl) => write!(f, "{}", fl), + value @ types::Value::Text(..) => write!(f, "'{}'", value), + types::Value::Blob(b) => write!(f, "{}", to_sqlite_blob(b)), } } } + +impl SimValue { + pub const FALSE: Self = SimValue(types::Value::Integer(0)); + pub const TRUE: Self = SimValue(types::Value::Integer(1)); + + pub fn into_bool(&self) -> bool { + Numeric::from(&self.0).try_into_bool().unwrap_or_default() + } + + // TODO: support more predicates + /// Returns a Result of a Binary Operation + /// + /// TODO: forget collations for now + /// TODO: have the [ast::Operator::Equals], [ast::Operator::NotEquals], [ast::Operator::Greater], + /// [ast::Operator::GreaterEquals], [ast::Operator::Less], [ast::Operator::LessEquals] function to be extracted + /// into its functions in limbo_core so that it can be used here + pub fn binary_compare(&self, other: &Self, operator: ast::Operator) -> SimValue { + match operator { + ast::Operator::Add => self.0.exec_add(&other.0).into(), + ast::Operator::And => self.0.exec_and(&other.0).into(), + ast::Operator::ArrowRight => todo!(), + ast::Operator::ArrowRightShift => todo!(), + ast::Operator::BitwiseAnd => self.0.exec_bit_and(&other.0).into(), + ast::Operator::BitwiseOr => self.0.exec_bit_or(&other.0).into(), + ast::Operator::BitwiseNot => todo!(), // TODO: Do not see any function usage of this operator in Core + ast::Operator::Concat => self.0.exec_concat(&other.0).into(), + ast::Operator::Equals => (self == other).into(), + ast::Operator::Divide => self.0.exec_divide(&other.0).into(), + ast::Operator::Greater => (self > other).into(), + ast::Operator::GreaterEquals => (self >= other).into(), + // TODO: Should attempt to extract `Is` and `IsNot` handling in a function in Core + ast::Operator::Is => todo!(), + ast::Operator::IsNot => todo!(), + ast::Operator::LeftShift => self.0.exec_shift_left(&other.0).into(), + ast::Operator::Less => (self < other).into(), + ast::Operator::LessEquals => (self <= other).into(), + ast::Operator::Modulus => self.0.exec_remainder(&other.0).into(), + ast::Operator::Multiply => self.0.exec_multiply(&other.0).into(), + ast::Operator::NotEquals => (self != other).into(), + ast::Operator::Or => self.0.exec_or(&other.0).into(), + ast::Operator::RightShift => self.0.exec_shift_right(&other.0).into(), + ast::Operator::Subtract => self.0.exec_subtract(&other.0).into(), + } + } + + // TODO: support more operators. Copy the implementation for exec_glob + pub fn like_compare(&self, other: &Self, operator: ast::LikeOperator) -> bool { + match operator { + ast::LikeOperator::Glob => todo!(), + ast::LikeOperator::Like => { + // TODO: support ESCAPE `expr` option in AST + // TODO: regex cache + types::Value::exec_like( + None, + other.0.to_string().as_str(), + self.0.to_string().as_str(), + ) + } + ast::LikeOperator::Match => todo!(), + ast::LikeOperator::Regexp => todo!(), + } + } + + pub fn unary_exec(&self, operator: ast::UnaryOperator) -> SimValue { + let new_value = match operator { + ast::UnaryOperator::BitwiseNot => self.0.exec_bit_not(), + ast::UnaryOperator::Negative => { + SimValue(types::Value::Integer(0)) + .binary_compare(self, ast::Operator::Subtract) + .0 + } + ast::UnaryOperator::Not => self.0.exec_boolean_not(), + ast::UnaryOperator::Positive => self.0.clone(), + }; + Self(new_value) + } +} + +impl From for SimValue { + fn from(value: ast::Literal) -> Self { + Self::from(&value) + } +} + +/// Sanitaizes a string literal by removing single quote at front and back +/// and escaping double single quotes +fn sanitize_string(input: &str) -> String { + input[1..input.len() - 1].replace("''", "'").to_string() +} + +impl From<&ast::Literal> for SimValue { + fn from(value: &ast::Literal) -> Self { + let new_value = match value { + ast::Literal::Null => types::Value::Null, + ast::Literal::Numeric(number) => Numeric::from(number).into(), + // TODO: see how to avoid sanitizing here + ast::Literal::String(string) => types::Value::build_text(sanitize_string(&string)), + ast::Literal::Blob(blob) => types::Value::Blob( + blob.as_bytes() + .chunks_exact(2) + .map(|pair| { + // We assume that sqlite3-parser has already validated that + // the input is valid hex string, thus unwrap is safe. + let hex_byte = std::str::from_utf8(pair).unwrap(); + u8::from_str_radix(hex_byte, 16).unwrap() + }) + .collect(), + ), + lit => unimplemented!("{:?}", lit), + }; + Self(new_value) + } +} + +impl From for ast::Literal { + fn from(value: SimValue) -> Self { + Self::from(&value) + } +} + +impl From<&SimValue> for ast::Literal { + fn from(value: &SimValue) -> Self { + match &value.0 { + types::Value::Null => Self::Null, + types::Value::Integer(i) => Self::Numeric(i.to_string()), + types::Value::Float(f) => Self::Numeric(f.to_string()), + text @ types::Value::Text(..) => Self::String(format!("'{}'", text)), + types::Value::Blob(blob) => Self::Blob(hex::encode(blob)), + } + } +} + +impl From for SimValue { + fn from(value: bool) -> Self { + value.then_some(SimValue::TRUE).unwrap_or(SimValue::FALSE) + } +} + +impl From for limbo_core::types::Value { + fn from(value: SimValue) -> Self { + value.0 + } +} + +impl From<&SimValue> for limbo_core::types::Value { + fn from(value: &SimValue) -> Self { + value.0.clone() + } +} + +impl From for SimValue { + fn from(value: limbo_core::types::Value) -> Self { + Self(value) + } +} + +impl From<&limbo_core::types::Value> for SimValue { + fn from(value: &limbo_core::types::Value) -> Self { + Self(value.clone()) + } +} diff --git a/simulator/runner/cli.rs b/simulator/runner/cli.rs index b47baafc8..c76d7367d 100644 --- a/simulator/runner/cli.rs +++ b/simulator/runner/cli.rs @@ -58,6 +58,30 @@ pub struct SimulatorCLI { pub disable_create_index: bool, #[clap(long, help = "disable DROP Statement", default_value_t = false)] pub disable_drop: bool, + #[clap( + long, + help = "disable Insert-Values-Select Property", + default_value_t = false + )] + pub disable_insert_values_select: bool, + #[clap( + long, + help = "disable Double-Create-Failure Property", + default_value_t = false + )] + pub disable_double_create_failure: bool, + #[clap(long, help = "disable Select-Limit Property", default_value_t = false)] + pub disable_select_limit: bool, + #[clap(long, help = "disable Delete-Select Property", default_value_t = false)] + pub disable_delete_select: bool, + #[clap(long, help = "disable Drop-Select Property", default_value_t = false)] + pub disable_drop_select: bool, + #[clap( + long, + help = "disable Select-Select-Optimizer Property", + default_value_t = false + )] + pub disable_select_optimizer: bool, } #[derive(Parser, Debug, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)] diff --git a/simulator/runner/differential.rs b/simulator/runner/differential.rs index 2da5d81ce..443b07675 100644 --- a/simulator/runner/differential.rs +++ b/simulator/runner/differential.rs @@ -1,11 +1,13 @@ use std::sync::{Arc, Mutex}; +use limbo_core::Value; + use crate::{ generation::{ pick_index, plan::{Interaction, InteractionPlanState, ResultSet}, }, - model::{query::Query, table::Value}, + model::{query::Query, table::SimValue}, runner::execution::ExecutionContinuation, InteractionPlan, }; @@ -60,7 +62,7 @@ pub(crate) fn run_simulation( fn execute_query_rusqlite( connection: &rusqlite::Connection, query: &Query, -) -> rusqlite::Result>> { +) -> rusqlite::Result>> { match query { Query::Create(create) => { connection.execute(create.to_string().as_str(), ())?; @@ -73,13 +75,14 @@ fn execute_query_rusqlite( let mut values = vec![]; for i in 0..columns { let value = row.get_unwrap(i); - match value { - rusqlite::types::Value::Null => values.push(Value::Null), - rusqlite::types::Value::Integer(i) => values.push(Value::Integer(i)), - rusqlite::types::Value::Real(f) => values.push(Value::Float(f)), - rusqlite::types::Value::Text(s) => values.push(Value::Text(s)), - rusqlite::types::Value::Blob(b) => values.push(Value::Blob(b)), - } + let value = match value { + rusqlite::types::Value::Null => Value::Null, + rusqlite::types::Value::Integer(i) => Value::Integer(i), + rusqlite::types::Value::Real(f) => Value::Float(f), + rusqlite::types::Value::Text(s) => Value::build_text(s), + rusqlite::types::Value::Blob(b) => Value::Blob(b), + }; + values.push(SimValue(value)); } Ok(values) })?; diff --git a/simulator/runner/env.rs b/simulator/runner/env.rs index bd252fd5e..b5a06d046 100644 --- a/simulator/runner/env.rs +++ b/simulator/runner/env.rs @@ -110,6 +110,12 @@ impl SimulatorEnv { delete_percent, drop_percent, update_percent, + disable_select_optimizer: cli_opts.disable_select_optimizer, + disable_insert_values_select: cli_opts.disable_insert_values_select, + disable_double_create_failure: cli_opts.disable_double_create_failure, + disable_select_limit: cli_opts.disable_select_limit, + disable_delete_select: cli_opts.disable_delete_select, + disable_drop_select: cli_opts.disable_drop_select, page_size: 4096, // TODO: randomize this too max_interactions: rng.gen_range(cli_opts.minimum_tests..=cli_opts.maximum_tests), max_time_simulation: cli_opts.maximum_time, @@ -215,6 +221,14 @@ pub(crate) struct SimulatorOpts { pub(crate) delete_percent: f64, pub(crate) update_percent: f64, pub(crate) drop_percent: f64, + + pub(crate) disable_select_optimizer: bool, + pub(crate) disable_insert_values_select: bool, + pub(crate) disable_double_create_failure: bool, + pub(crate) disable_select_limit: bool, + pub(crate) disable_delete_select: bool, + pub(crate) disable_drop_select: bool, + pub(crate) max_interactions: usize, pub(crate) page_size: usize, pub(crate) max_time_simulation: usize, diff --git a/simulator/runner/execution.rs b/simulator/runner/execution.rs index a1ca084e3..25a0c2afb 100644 --- a/simulator/runner/execution.rs +++ b/simulator/runner/execution.rs @@ -1,6 +1,7 @@ use std::sync::{Arc, Mutex}; use limbo_core::{LimboError, Result}; +use tracing::instrument; use crate::generation::{ pick_index, @@ -173,17 +174,15 @@ pub(crate) enum ExecutionContinuation { NextProperty, } +#[instrument(skip(env, interaction, stack), fields(interaction = %interaction))] pub(crate) fn execute_interaction( env: &mut SimulatorEnv, connection_index: usize, interaction: &Interaction, stack: &mut Vec, ) -> Result { - tracing::info!( - "execute_interaction(connection_index={}, interaction={})", - connection_index, - interaction - ); + // Leave this empty info! here to print the span of the execution + tracing::info!(""); match interaction { Interaction::Query(_) => { let conn = match &mut env.connections[connection_index] { @@ -192,7 +191,6 @@ pub(crate) fn execute_interaction( SimConnection::Disconnected => unreachable!(), }; - tracing::debug!("{}", interaction); let results = interaction.execute_query(conn, &env.io); tracing::debug!("{:?}", results); stack.push(results); diff --git a/simulator/runner/file.rs b/simulator/runner/file.rs index d676608b1..090be055f 100644 --- a/simulator/runner/file.rs +++ b/simulator/runner/file.rs @@ -31,30 +31,34 @@ impl SimulatorFile { self.fault.replace(fault); } - pub(crate) fn print_stats(&self) { - tracing::info!("op calls faults"); - tracing::info!("--------- -------- --------"); - tracing::info!( - "pread {:8} {:8}", - *self.nr_pread_calls.borrow(), - *self.nr_pread_faults.borrow() - ); - tracing::info!( - "pwrite {:8} {:8}", - *self.nr_pwrite_calls.borrow(), - *self.nr_pwrite_faults.borrow() - ); - tracing::info!( - "sync {:8} {:8}", - *self.nr_sync_calls.borrow(), - 0 // No fault counter for sync - ); - tracing::info!("--------- -------- --------"); + pub(crate) fn stats_table(&self) -> String { let sum_calls = *self.nr_pread_calls.borrow() + *self.nr_pwrite_calls.borrow() + *self.nr_sync_calls.borrow(); let sum_faults = *self.nr_pread_faults.borrow() + *self.nr_pwrite_faults.borrow(); - tracing::info!("total {:8} {:8}", sum_calls, sum_faults); + let stats_table = [ + "op calls faults".to_string(), + "--------- -------- --------".to_string(), + format!( + "pread {:8} {:8}", + *self.nr_pread_calls.borrow(), + *self.nr_pread_faults.borrow() + ), + format!( + "pwrite {:8} {:8}", + *self.nr_pwrite_calls.borrow(), + *self.nr_pwrite_faults.borrow() + ), + format!( + "sync {:8} {:8}", + *self.nr_sync_calls.borrow(), + 0 // No fault counter for sync + ), + "--------- -------- --------".to_string(), + format!("total {:8} {:8}", sum_calls, sum_faults), + ]; + let table = stats_table.join("\n"); + table } } diff --git a/simulator/runner/io.rs b/simulator/runner/io.rs index 6318e711c..3fea3dda0 100644 --- a/simulator/runner/io.rs +++ b/simulator/runner/io.rs @@ -45,9 +45,7 @@ impl SimulatorIO { pub(crate) fn print_stats(&self) { tracing::info!("run_once faults: {}", self.nr_run_once_faults.borrow()); for file in self.files.borrow().iter() { - tracing::info!(""); - tracing::info!("==========================="); - file.print_stats(); + tracing::info!("\n===========================\n{}", file.stats_table()); } } } diff --git a/vendored/sqlite3-parser/Cargo.toml b/vendored/sqlite3-parser/Cargo.toml index f432c345b..43ba78283 100644 --- a/vendored/sqlite3-parser/Cargo.toml +++ b/vendored/sqlite3-parser/Cargo.toml @@ -22,6 +22,7 @@ YYNOERRORRECOVERY = [] YYCOVERAGE = [] NDEBUG = [] default = ["YYNOERRORRECOVERY", "NDEBUG"] +serde = ["dep:serde", "indexmap/serde", "bitflags/serde"] [dependencies] phf = { version = "0.11", features = ["uncased"] } diff --git a/vendored/sqlite3-parser/src/parser/ast/mod.rs b/vendored/sqlite3-parser/src/parser/ast/mod.rs index 6d92c07ba..e213ccdfd 100644 --- a/vendored/sqlite3-parser/src/parser/ast/mod.rs +++ b/vendored/sqlite3-parser/src/parser/ast/mod.rs @@ -69,6 +69,7 @@ pub(crate) enum ExplainKind { /// SQL statement // https://sqlite.org/syntax/sql-stmt.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Stmt { /// `ALTER TABLE`: table name, body AlterTable(Box<(QualifiedName, AlterTableBody)>), @@ -193,6 +194,7 @@ pub enum Stmt { /// `CREATE VIRTUAL TABLE` #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct CreateVirtualTable { /// `IF NOT EXISTS` pub if_not_exists: bool, @@ -206,6 +208,7 @@ pub struct CreateVirtualTable { /// `CREATE TRIGGER #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct CreateTrigger { /// `TEMPORARY` pub temporary: bool, @@ -229,6 +232,7 @@ pub struct CreateTrigger { /// `INSERT` #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Insert { /// CTE pub with: Option, @@ -246,6 +250,7 @@ pub struct Insert { /// `UPDATE` clause #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Update { /// CTE pub with: Option, @@ -271,6 +276,7 @@ pub struct Update { /// `DELETE` #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Delete { /// CTE pub with: Option, @@ -290,6 +296,7 @@ pub struct Delete { #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// Internal ID of a table reference. /// /// Used by [Expr::Column] and [Expr::RowId] to refer to a table. @@ -332,6 +339,7 @@ impl std::fmt::Display for TableInternalId { /// SQL expression // https://sqlite.org/syntax/expr.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Expr { /// `BETWEEN` Between { @@ -570,6 +578,7 @@ impl Expr { /// SQL literal #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Literal { /// Number Numeric(String), @@ -608,6 +617,7 @@ impl Literal { /// Textual comparison operator in an expression #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum LikeOperator { /// `GLOB` Glob, @@ -640,6 +650,7 @@ impl LikeOperator { /// SQL operators #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Operator { /// `+` Add, @@ -750,6 +761,7 @@ impl Operator { /// Unary operators #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum UnaryOperator { /// bitwise negation (`~`) BitwiseNot, @@ -777,6 +789,7 @@ impl From for UnaryOperator { // https://sqlite.org/lang_select.html // https://sqlite.org/syntax/factored-select-stmt.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Select { /// CTE pub with: Option, @@ -790,6 +803,7 @@ pub struct Select { /// `SELECT` body #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SelectBody { /// first select pub select: Box, @@ -821,6 +835,7 @@ impl SelectBody { /// Compound select #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct CompoundSelect { /// operator pub operator: CompoundOperator, @@ -831,6 +846,7 @@ pub struct CompoundSelect { /// Compound operators // https://sqlite.org/syntax/compound-operator.html #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum CompoundOperator { /// `UNION` Union, @@ -845,6 +861,7 @@ pub enum CompoundOperator { /// `SELECT` core // https://sqlite.org/syntax/select-core.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum OneSelect { /// `SELECT` Select(Box), @@ -853,6 +870,7 @@ pub enum OneSelect { } #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// `SELECT` core pub struct SelectInner { /// `DISTINCT` @@ -872,6 +890,7 @@ pub struct SelectInner { /// `SELECT` ... `FROM` clause // https://sqlite.org/syntax/join-clause.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct FromClause { /// table pub select: Option>, // FIXME mandatory @@ -928,6 +947,7 @@ impl FromClause { /// `SELECT` distinctness #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Distinctness { /// `DISTINCT` Distinct, @@ -938,6 +958,7 @@ pub enum Distinctness { /// `SELECT` or `RETURNING` result column // https://sqlite.org/syntax/result-column.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ResultColumn { /// expression Expr(Expr, Option), @@ -949,6 +970,7 @@ pub enum ResultColumn { /// Alias #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum As { /// `AS` As(Name), @@ -959,6 +981,7 @@ pub enum As { /// `JOIN` clause // https://sqlite.org/syntax/join-clause.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct JoinedSelectTable { /// operator pub operator: JoinOperator, @@ -971,6 +994,7 @@ pub struct JoinedSelectTable { /// Table or subquery // https://sqlite.org/syntax/table-or-subquery.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum SelectTable { /// table Table(QualifiedName, Option, Option), @@ -985,6 +1009,7 @@ pub enum SelectTable { /// Join operators // https://sqlite.org/syntax/join-operator.html #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum JoinOperator { /// `,` Comma, @@ -1028,6 +1053,7 @@ impl JoinOperator { bitflags::bitflags! { /// `JOIN` types #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct JoinType: u8 { /// `INNER` const INNER = 0x01; @@ -1072,6 +1098,8 @@ impl TryFrom<&[u8]> for JoinType { /// `JOIN` constraint #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] + pub enum JoinConstraint { /// `ON` On(Expr), @@ -1081,6 +1109,7 @@ pub enum JoinConstraint { /// `GROUP BY` #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct GroupBy { /// expressions pub exprs: Vec, @@ -1090,6 +1119,7 @@ pub struct GroupBy { /// identifier or one of several keywords or `INDEXED` #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Id(pub String); impl Id { @@ -1103,6 +1133,7 @@ impl Id { /// identifier or string or `CROSS` or `FULL` or `INNER` or `LEFT` or `NATURAL` or `OUTER` or `RIGHT`. #[derive(Clone, Debug, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Name(pub String); // TODO distinction between Name and "Name"/[Name]/`Name` impl Name { @@ -1199,6 +1230,7 @@ impl PartialEq<&str> for Name { /// Qualified name #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct QualifiedName { /// schema pub db_name: Option, @@ -1245,6 +1277,7 @@ impl QualifiedName { /// Ordered set of distinct column names #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct DistinctNames(IndexSet); impl DistinctNames { @@ -1280,6 +1313,7 @@ impl Deref for DistinctNames { /// `ALTER TABLE` body // https://sqlite.org/lang_altertable.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum AlterTableBody { /// `RENAME TO`: new table name RenameTo(Name), @@ -1300,6 +1334,7 @@ pub enum AlterTableBody { // https://sqlite.org/lang_createtable.html // https://sqlite.org/syntax/create-table-stmt.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum CreateTableBody { /// columns and constraints ColumnsAndConstraints { @@ -1332,6 +1367,7 @@ impl CreateTableBody { /// Table column definition // https://sqlite.org/syntax/column-def.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ColumnDefinition { /// column name pub col_name: Name, @@ -1403,6 +1439,7 @@ impl ColumnDefinition { /// Named column constraint // https://sqlite.org/syntax/column-constraint.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct NamedColumnConstraint { /// constraint name pub name: Option, @@ -1413,6 +1450,7 @@ pub struct NamedColumnConstraint { /// Column constraint // https://sqlite.org/syntax/column-constraint.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ColumnConstraint { /// `PRIMARY KEY` PrimaryKey { @@ -1462,6 +1500,7 @@ pub enum ColumnConstraint { /// Named table constraint // https://sqlite.org/syntax/table-constraint.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct NamedTableConstraint { /// constraint name pub name: Option, @@ -1472,6 +1511,7 @@ pub struct NamedTableConstraint { /// Table constraint // https://sqlite.org/syntax/table-constraint.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TableConstraint { /// `PRIMARY KEY` PrimaryKey { @@ -1505,6 +1545,7 @@ pub enum TableConstraint { bitflags::bitflags! { /// `CREATE TABLE` options #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TableOptions: u8 { /// None const NONE = 0; @@ -1517,6 +1558,7 @@ bitflags::bitflags! { /// Sort orders #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum SortOrder { /// `ASC` Asc, @@ -1526,6 +1568,7 @@ pub enum SortOrder { /// `NULLS FIRST` or `NULLS LAST` #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum NullsOrder { /// `NULLS FIRST` First, @@ -1536,6 +1579,7 @@ pub enum NullsOrder { /// `REFERENCES` clause // https://sqlite.org/syntax/foreign-key-clause.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ForeignKeyClause { /// foreign table name pub tbl_name: Name, @@ -1547,6 +1591,7 @@ pub struct ForeignKeyClause { /// foreign-key reference args #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum RefArg { /// `ON DELETE` OnDelete(RefAct), @@ -1560,6 +1605,7 @@ pub enum RefArg { /// foreign-key reference actions #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum RefAct { /// `SET NULL` SetNull, @@ -1575,6 +1621,7 @@ pub enum RefAct { /// foreign-key defer clause #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct DeferSubclause { /// `DEFERRABLE` pub deferrable: bool, @@ -1584,6 +1631,7 @@ pub struct DeferSubclause { /// `INITIALLY` `DEFERRED` / `IMMEDIATE` #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum InitDeferredPred { /// `INITIALLY DEFERRED` InitiallyDeferred, @@ -1594,6 +1642,7 @@ pub enum InitDeferredPred { /// Indexed column // https://sqlite.org/syntax/indexed-column.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct IndexedColumn { /// column name pub col_name: Name, @@ -1605,6 +1654,7 @@ pub struct IndexedColumn { /// `INDEXED BY` / `NOT INDEXED` #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Indexed { /// `INDEXED BY`: idx name IndexedBy(Name), @@ -1614,6 +1664,7 @@ pub enum Indexed { /// Sorted column #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SortedColumn { /// expression pub expr: Expr, @@ -1625,6 +1676,7 @@ pub struct SortedColumn { /// `LIMIT` #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Limit { /// count pub expr: Expr, @@ -1636,6 +1688,7 @@ pub struct Limit { // https://sqlite.org/lang_insert.html // https://sqlite.org/syntax/insert-stmt.html #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum InsertBody { /// `SELECT` or `VALUES` Select(Box