Merge 'Simulator Ast Generation + Simulator Unary Operator + Refactor to use limbo_core::Value in Simulator for massive code reuse' from Pedro Muniz

This PR is a Drop-In replacement to the Predicate defined in the
Simulator. Predicate is basically the same as our ast::Expr, but it
supports a small number of the SQL expression syntax. By creating a
NewType that wraps ast::Expr we can tap into our already mostly
correctly defined parser structs. This change will enable us to easily
add generation for more types of sql queries.
I also added an ArbitraryFrom impl for ast::Expr that can be used in a
freestyle way (for now) for differential testing.
This PR also aims to implement Unary Operator logic similar to the
Binary Operator logic we have for predicate. After this change we may
need to adjust the Logic for how some assertions are triggered.
<s>Sometimes the `Select-Select-Optimizer` property thinks that these
two queries should return the same thing:
```sql
SELECT (twinkling_winstanley.sensible_federations > x'66616e7461737469625e0f37879823db' AND twinkling_winstanley.sincere_niemeyer < -7428368947470022783) FROM twinkling_winstanley WHERE 1;

SELECT * FROM twinkling_winstanley WHERE twinkling_winstanley.sensible_federations > x'66616e7461737469625e0f37879823db' AND twinkling_winstanley.sincere_niemeyer < -7428368947470022783;
```
However after running the shrunk plan manually, the simulator was
incorrect in asserting that. Maybe this a bug a in the generation of
such a query? Not sure yet. </s>
<b>EDIT: The simulator was correctly catching a bug and I thought I was
the problem. The bug was in `exec_if` and I fixed it in this PR.</b>
I still need to expand the Unary Operator generation to other types of
predicates. For now, I just implemented it for `SimplePredicate` as I'm
trying to avoid to bloat even more this PR.
<b>EDIT: I decided to just have one PR open for all the changes I'm
making to make my life a bit easier and to avoid merge conflicts with my
own branches that I keep spawning for new code.</b>
PS: This should only be considered for merging after
https://github.com/tursodatabase/limbo/pull/1619 is merged. Then, I will
remove the draft status from this PR.

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

Closes #1674
This commit is contained in:
Jussi Saurio
2025-06-13 11:23:10 +03:00
34 changed files with 2187 additions and 697 deletions

1
.gitignore vendored
View File

@@ -19,7 +19,6 @@ env
dist/
.tmp/
*.db
**/*.db-wal
**/*.db-shm

6
Cargo.lock generated
View File

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

View File

@@ -1,6 +1,6 @@
use crate::Value;
mod nonnan;
pub mod nonnan;
use nonnan::NonNan;

View File

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

View File

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

View File

@@ -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<T> Arbitrary for Box<T>
where
T: Arbitrary,
{
fn arbitrary<R: rand::Rng>(rng: &mut R) -> Self {
Box::from(T::arbitrary(rng))
}
}
impl<A, T> ArbitraryFrom<A> for Box<T>
where
T: ArbitraryFrom<A>,
{
fn arbitrary_from<R: rand::Rng>(rng: &mut R, t: A) -> Self {
Box::from(T::arbitrary_from(rng, t))
}
}
impl<T> Arbitrary for Option<T>
where
T: Arbitrary,
{
fn arbitrary<R: rand::Rng>(rng: &mut R) -> Self {
rng.gen_bool(0.5).then_some(T::arbitrary(rng))
}
}
impl<A, T> ArbitraryFrom<A> for Option<T>
where
T: ArbitraryFrom<A>,
{
fn arbitrary_from<R: rand::Rng>(rng: &mut R, t: A) -> Self {
rng.gen_bool(0.5).then_some(T::arbitrary_from(rng, t))
}
}
impl<A: Copy, T> ArbitraryFrom<A> for Vec<T>
where
T: ArbitraryFrom<A>,
{
fn arbitrary_from<R: rand::Rng>(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<R: rand::Rng>(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<R: rand::Rng>(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<R: rand::Rng>(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<R: rand::Rng>(rng: &mut R) -> Self {
let choice = rng.gen_range(0..3);
CollateName(
match choice {
0 => "BINARY",
1 => "RTRIM",
2 => "NOCASE",
_ => unreachable!(),
}
.to_string(),
)
}
}
impl ArbitraryFrom<&SimulatorEnv> for QualifiedName {
fn arbitrary_from<R: rand::Rng>(rng: &mut R, t: &SimulatorEnv) -> Self {
// 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<R: rand::Rng>(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<R: rand::Rng>(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<R: rand::Rng>(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<R: rand::Rng>(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!(),
}
}
}

View File

@@ -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<Box<dyn Fn(&mut R) -> T + 'a>>,
/// The function takes a list of functions that return an Option<T>, along with number of retries
/// to make before giving up.
pub(crate) fn backtrack<'a, T, R: Rng>(
mut choices: Vec<(u32, Box<dyn Fn(&mut R) -> Option<T> + 'a>)>,
mut choices: Vec<(usize, Box<dyn Fn(&mut R) -> Option<T> + 'a>)>,
rng: &mut R,
) -> T {
) -> Option<T> {
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::<Vec<_>>();
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;

View File

@@ -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<Vec<Vec<Value>>>;
pub(crate) type ResultSet = Result<Vec<Vec<SimValue>>>;
#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct InteractionPlan {
@@ -105,7 +112,7 @@ pub(crate) enum Interactions {
}
impl Interactions {
pub(crate) fn name(&self) -> Option<String> {
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<Vec<Value>> {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec<Vec<SimValue>> {
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);

View File

@@ -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<R: rand::Rng>(
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<R: rand::Rng>(rng: &mut R, t: &Table, row: &Vec<SimValue>) -> 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<R: rand::Rng>(rng: &mut R, t: &Table, row: &Vec<SimValue>) -> 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<R: rand::Rng>(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<R: rand::Rng>(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<R: rand::Rng>(
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::<Vec<_>>();
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::<Vec<_>>();
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<Vec<SimValue>> = (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<Vec<SimValue>> = (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<Vec<SimValue>> = (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<Vec<SimValue>> = (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)
}
}
}

View File

@@ -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<A: AsRef<[SimValue]>> ArbitraryFrom<(&Table, A, bool)> for SimplePredicate {
fn arbitrary_from<R: Rng>(
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<R: Rng>(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<R: Rng>(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<R: Rng>(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<R: Rng>(rng: &mut R, (column_name, value): (&str, &SimValue)) -> Self {
Predicate::from_column_binary(rng, column_name, value)
}
}
impl ArbitraryFrom<(&Table, &Vec<SimValue>)> for Predicate {
fn arbitrary_from<R: Rng>(rng: &mut R, (t, row): (&Table, &Vec<SimValue>)) -> 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::<Vec<_>>();
let false_predicates = (0..=rng.gen_range(0..=3))
.map(|_| Predicate::false_binary(rng, t, row))
.collect::<Vec<_>>();
// 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::<Vec<_>>();
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<Vec<SimValue>> = (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<Vec<SimValue>> = (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<Vec<SimValue>> = (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<Vec<SimValue>> = (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<Vec<SimValue>> = (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)
}
}
}

View File

@@ -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<R: rand::Rng>(_rng: &mut R, value: &SimValue) -> Option<Self>
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<R: rand::Rng>(rng: &mut R, values: &Vec<&SimValue>) -> Option<Self>
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<R: rand::Rng>(_rng: &mut R, value: &SimValue) -> Option<Self>
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<R: rand::Rng>(rng: &mut R, values: &Vec<&SimValue>) -> Option<Self>
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<R: rand::Rng>(
_rng: &mut R,
(value, predicate): (&SimValue, bool),
) -> Option<Self>
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<R: rand::Rng>(
rng: &mut R,
(values, predicate): (&Vec<&SimValue>, bool),
) -> Option<Self>
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<R: rand::Rng>(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<R: rand::Rng>(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<Vec<SimValue>> = (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<Vec<SimValue>> = (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)
}
}
}

View File

@@ -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<ResultSet>, _: &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<ResultSet>, _: &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<R: rand::Rng>(
let table = pick(&env.tables, rng);
// Generate rows to insert
let rows = (0..rng.gen_range(1..=5))
.map(|_| Vec::<Value>::arbitrary_from(rng, table))
.map(|_| Vec::<SimValue>::arbitrary_from(rng, table))
.collect::<Vec<_>>();
// Pick a random row to select
@@ -696,10 +690,16 @@ fn property_select_select_optimizer<R: rand::Rng>(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)),
),
],

View File

@@ -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<R: Rng>(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<Vec<Value>> = (0..num_rows)
let values: Vec<Vec<SimValue>> = (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<R: Rng>(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::<Vec<_>>();
// 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<R: Rng>(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::<Vec<_>>();
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::<Vec<_>>();
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<R: Rng>(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<R: Rng>(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<R: Rng>(rng: &mut R, (t, row): (&Table, &Vec<Value>)) -> 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<R: Rng>(rng: &mut R, (t, row): (&Table, &Vec<Value>)) -> 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<Value>)> for Predicate {
fn arbitrary_from<R: Rng>(rng: &mut R, (t, row): (&Table, &Vec<Value>)) -> 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::<Vec<_>>();
let false_predicates = (0..=rng.gen_range(0..=3))
.map(|_| produce_false_predicate(rng, (t, row)))
.collect::<Vec<_>>();
// 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::<Vec<_>>();
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<R: Rng>(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();

View File

@@ -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<R: Rng>(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::<Vec<_>>();
// 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<Value> {
impl ArbitraryFrom<&Table> for Vec<SimValue> {
fn arbitrary_from<R: Rng>(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<R: Rng>(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<R: Rng>(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<R: Rng>(rng: &mut R, values: &Vec<&Value>) -> Self {
impl ArbitraryFrom<&Vec<&SimValue>> for LTValue {
fn arbitrary_from<R: Rng>(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<R: Rng>(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<R: Rng>(rng: &mut R, value: &SimValue) -> Self {
let new_value = match &value.0 {
Value::Integer(i) => Value::Integer(rng.gen_range(i64::MIN..*i - 1)),
Value::Float(f) => Value::Float(f - rng.gen_range(0.0..1e10)),
value @ 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::<Vec<_>>();
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::<String>();
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<R: Rng>(rng: &mut R, values: &Vec<&Value>) -> Self {
impl ArbitraryFrom<&Vec<&SimValue>> for GTValue {
fn arbitrary_from<R: Rng>(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<R: Rng>(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<R: Rng>(rng: &mut R, value: &SimValue) -> Self {
let new_value = match &value.0 {
Value::Integer(i) => Value::Integer(rng.gen_range(*i..i64::MAX)),
Value::Float(f) => Value::Float(rng.gen_range(*f..1e10)),
value @ 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::<Vec<_>>();
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::<String>();
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<R: Rng>(rng: &mut R, value: &Value) -> Option<Self> {
match value {
Value::Text(t) => {
impl ArbitraryFromMaybe<&SimValue> for LikeValue {
fn arbitrary_from_maybe<R: Rng>(rng: &mut R, value: &SimValue) -> Option<Self> {
match &value.0 {
value @ Value::Text(..) => {
let t = value.to_string();
let mut t = t.chars().collect::<Vec<_>>();
// 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::<String>(),
))))
}
_ => None,
}

View File

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

View File

@@ -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<Vec<Value>> {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec<Vec<SimValue>> {
if !env.tables.iter().any(|t| t.name == self.table.name) {
env.tables.push(self.table.clone());
}

View File

@@ -31,7 +31,7 @@ impl CreateIndex {
pub(crate) fn shadow(
&self,
_env: &mut crate::runner::env::SimulatorEnv,
) -> Vec<Vec<crate::model::table::Value>> {
) -> Vec<Vec<crate::model::table::SimValue>> {
// CREATE INDEX doesn't require any shadowing; we don't need to keep track
// in the simulator what indexes exist.
vec![]

View File

@@ -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<Vec<Value>> {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec<Vec<SimValue>> {
let table = env
.tables
.iter_mut()

View File

@@ -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<Vec<Value>> {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec<Vec<SimValue>> {
env.tables.retain(|t| t.name != self.table);
vec![]
}

View File

@@ -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<Vec<Value>>,
values: Vec<Vec<SimValue>>,
},
Select {
table: String,
@@ -19,7 +19,7 @@ pub(crate) enum Insert {
}
impl Insert {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec<Vec<Value>> {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec<Vec<SimValue>> {
match self {
Insert::Values { table, values } => {
if let Some(t) = env.tables.iter_mut().find(|t| &t.name == table) {

View File

@@ -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<Vec<Value>> {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec<Vec<SimValue>> {
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!()
}
}

View File

@@ -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<Table> 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<SimValue> {
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))
}
}

View File

@@ -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<Vec<Value>> {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec<Vec<SimValue>> {
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<Predicate>), // p1 AND p2 AND p3... AND pn
Or(Vec<Predicate>), // 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(&regex_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),
}
}
}

View File

@@ -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<Vec<Value>> {
pub(crate) fn shadow(&self, env: &mut SimulatorEnv) -> Vec<Vec<SimValue>> {
let table = env
.tables
.iter_mut()

View File

@@ -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<Vec<Value>>,
pub(crate) rows: Vec<Vec<SimValue>>,
pub(crate) name: String,
pub(crate) columns: Vec<Column>,
}
@@ -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<H: std::hash::Hasher>(&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<u8>),
}
impl PartialOrd for Value {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
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<ast::Literal> 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<SimValue> 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<bool> for SimValue {
fn from(value: bool) -> Self {
value.then_some(SimValue::TRUE).unwrap_or(SimValue::FALSE)
}
}
impl From<SimValue> 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<limbo_core::types::Value> 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())
}
}

View File

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

View File

@@ -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<Vec<Vec<Value>>> {
) -> rusqlite::Result<Vec<Vec<SimValue>>> {
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)
})?;

View File

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

View File

@@ -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<ResultSet>,
) -> Result<ExecutionContinuation> {
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);

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ YYNOERRORRECOVERY = []
YYCOVERAGE = []
NDEBUG = []
default = ["YYNOERRORRECOVERY", "NDEBUG"]
serde = ["dep:serde", "indexmap/serde", "bitflags/serde"]
[dependencies]
phf = { version = "0.11", features = ["uncased"] }

View File

@@ -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<With>,
@@ -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<With>,
@@ -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<With>,
@@ -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<YYCODETYPE> 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<With>,
@@ -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<OneSelect>,
@@ -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<SelectInner>),
@@ -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<Box<SelectTable>>, // 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<As>),
@@ -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<As>, Option<Indexed>),
@@ -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<Expr>,
@@ -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<Name>,
@@ -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<Name>);
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<Name>,
@@ -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<Name>,
@@ -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<Select>, Option<Upsert>),
@@ -1645,6 +1698,7 @@ pub enum InsertBody {
/// `UPDATE ... SET`
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Set {
/// column name(s)
pub col_names: DistinctNames,
@@ -1655,6 +1709,7 @@ pub struct Set {
/// `PRAGMA` body
// https://sqlite.org/syntax/pragma-stmt.html
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PragmaBody {
/// `=`
Equals(PragmaValue),
@@ -1669,6 +1724,7 @@ pub type PragmaValue = Expr; // TODO
// https://sqlite.org/pragma.html
#[derive(Clone, Debug, PartialEq, Eq, EnumIter, EnumString, strum::Display)]
#[strum(serialize_all = "snake_case")]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PragmaName {
/// set the autovacuum mode
AutoVacuum,
@@ -1694,6 +1750,7 @@ pub enum PragmaName {
/// `CREATE TRIGGER` time
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TriggerTime {
/// `BEFORE`
Before, // default
@@ -1705,6 +1762,7 @@ pub enum TriggerTime {
/// `CREATE TRIGGER` event
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TriggerEvent {
/// `DELETE`
Delete,
@@ -1720,6 +1778,7 @@ pub enum TriggerEvent {
// https://sqlite.org/lang_createtrigger.html
// https://sqlite.org/syntax/create-trigger-stmt.html
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TriggerCmd {
/// `UPDATE`
Update(Box<TriggerCmdUpdate>),
@@ -1733,6 +1792,7 @@ pub enum TriggerCmd {
/// `UPDATE` trigger command
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TriggerCmdUpdate {
/// `OR`
pub or_conflict: Option<ResolveType>,
@@ -1748,6 +1808,7 @@ pub struct TriggerCmdUpdate {
/// `INSERT` trigger command
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TriggerCmdInsert {
/// `OR`
pub or_conflict: Option<ResolveType>,
@@ -1765,6 +1826,7 @@ pub struct TriggerCmdInsert {
/// `DELETE` trigger command
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TriggerCmdDelete {
/// table name
pub tbl_name: Name,
@@ -1774,6 +1836,7 @@ pub struct TriggerCmdDelete {
/// Conflict resolution types
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ResolveType {
/// `ROLLBACK`
Rollback,
@@ -1803,6 +1866,7 @@ impl ResolveType {
// https://sqlite.org/lang_with.html
// https://sqlite.org/syntax/with-clause.html
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct With {
/// `RECURSIVE`
pub recursive: bool,
@@ -1812,6 +1876,7 @@ pub struct With {
/// CTE materialization
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Materialized {
/// No hint
Any,
@@ -1824,6 +1889,7 @@ pub enum Materialized {
/// CTE
// https://sqlite.org/syntax/common-table-expression.html
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CommonTableExpr {
/// table name
pub tbl_name: Name,
@@ -1849,6 +1915,7 @@ impl CommonTableExpr {
/// Column type
// https://sqlite.org/syntax/type-name.html
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Type {
/// type name
pub name: String, // TODO Validate: Ids+
@@ -1859,6 +1926,7 @@ pub struct Type {
/// Column type size limit(s)
// https://sqlite.org/syntax/type-name.html
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TypeSize {
/// maximum size
MaxSize(Box<Expr>),
@@ -1868,6 +1936,7 @@ pub enum TypeSize {
/// Transaction types
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TransactionType {
/// `DEFERRED`
Deferred, // default
@@ -1881,6 +1950,7 @@ pub enum TransactionType {
// https://sqlite.org/lang_upsert.html
// https://sqlite.org/syntax/upsert-clause.html
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Upsert {
/// conflict targets
pub index: Option<Box<UpsertIndex>>,
@@ -1892,6 +1962,7 @@ pub struct Upsert {
/// Upsert conflict targets
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct UpsertIndex {
/// columns
pub targets: Vec<SortedColumn>,
@@ -1901,6 +1972,7 @@ pub struct UpsertIndex {
/// Upsert `DO` action
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum UpsertDo {
/// `SET`
Set {
@@ -1915,6 +1987,7 @@ pub enum UpsertDo {
/// Function call tail
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FunctionTail {
/// `FILTER` clause
pub filter_clause: Option<Box<Expr>>,
@@ -1925,6 +1998,7 @@ pub struct FunctionTail {
/// Function call `OVER` clause
// https://sqlite.org/syntax/over-clause.html
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Over {
/// Window definition
Window(Window),
@@ -1934,6 +2008,7 @@ pub enum Over {
/// `OVER` window definition
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WindowDef {
/// window name
pub name: Name,
@@ -1944,6 +2019,7 @@ pub struct WindowDef {
/// Window definition
// https://sqlite.org/syntax/window-defn.html
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Window {
/// base window name
pub base: Option<Name>,
@@ -1958,6 +2034,7 @@ pub struct Window {
/// Frame specification
// https://sqlite.org/syntax/frame-spec.html
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FrameClause {
/// unit
pub mode: FrameMode,
@@ -1971,6 +2048,7 @@ pub struct FrameClause {
/// Frame modes
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum FrameMode {
/// `GROUPS`
Groups,
@@ -1982,6 +2060,7 @@ pub enum FrameMode {
/// Frame bounds
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum FrameBound {
/// `CURRENT ROW`
CurrentRow,
@@ -1997,6 +2076,7 @@ pub enum FrameBound {
/// Frame exclusions
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum FrameExclude {
/// `NO OTHERS`
NoOthers,

View File

@@ -77,6 +77,7 @@ impl ToSqlString for ast::OneSelect {
impl ToSqlString for ast::SelectInner {
fn to_sql_string<C: ToSqlContext>(&self, context: &C) -> String {
dbg!(&self);
let mut ret = Vec::with_capacity(2 + self.columns.len());
ret.push("SELECT".to_string());
if let Some(distinct) = self.distinctness {