change CompoundPredicate to generate a true_clause for a single row and not for column_values + tests

This commit is contained in:
pedrocarlo
2025-06-07 21:29:24 -03:00
parent 39b57552fd
commit b2fd5b9cd1
5 changed files with 266 additions and 101 deletions

View File

@@ -257,12 +257,8 @@ impl ArbitraryFrom<&Vec<&SimValue>> for ast::Expr {
return Self::Literal(ast::Literal::Null);
}
// TODO: for now just convert the value to an ast::Literal
let values = values
.iter()
.map(|value| ast::Expr::Literal((*value).into()))
.collect::<Vec<_>>();
pick(&values, rng).to_owned().clone()
let value = pick(&values, rng);
Expr::Literal((*value).into())
}
}

View File

@@ -16,7 +16,7 @@ use crate::{
};
impl Predicate {
/// Generate an [ast::Expr::Binary] [Predicate] from a column and [Value]
/// 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,
@@ -209,36 +209,30 @@ impl Predicate {
}
impl SimplePredicate {
/// Generates a true [ast::Expr::Binary] [SimplePredicate] from a [Table] for some values in the table
pub fn true_binary<R: rand::Rng>(rng: &mut R, table: &Table) -> Self {
/// 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_values = table
.rows
.iter()
.map(|r| &r[column_index])
.collect::<Vec<_>>();
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| {
let expr = if column_values.is_empty() {
Expr::arbitrary_from(rng, &column_values)
} else {
Expr::Literal((*pick(&column_values, rng)).into())
};
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),
Box::new(Expr::Literal(column_value.into())),
)
}),
Box::new(|rng| {
let lt_value = LTValue::arbitrary_from(rng, &column_values).0;
let lt_value = LTValue::arbitrary_from(rng, column_value).0;
Expr::Binary(
Box::new(Expr::Qualified(
ast::Name(table.name.clone()),
@@ -249,7 +243,7 @@ impl SimplePredicate {
)
}),
Box::new(|rng| {
let gt_value = GTValue::arbitrary_from(rng, &column_values).0;
let gt_value = GTValue::arbitrary_from(rng, column_value).0;
Expr::Binary(
Box::new(Expr::Qualified(
ast::Name(table.name.clone()),
@@ -265,35 +259,30 @@ impl SimplePredicate {
SimplePredicate(Predicate(expr))
}
/// Generates a false [ast::Expr::Binary] [SimplePredicate] from a [Table] for some values in the table
pub fn false_binary<R: rand::Rng>(rng: &mut R, table: &Table) -> Self {
/// 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_values = table
.rows
.iter()
.map(|r| &r[column_index])
.collect::<Vec<_>>();
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| {
let expr = if column_values.is_empty() {
Expr::arbitrary_from(rng, &column_values)
} else {
Expr::Literal((*pick(&column_values, rng)).into())
};
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),
Box::new(Expr::Literal(column_value.into())),
)
}),
Box::new(|rng| {
let gt_value = GTValue::arbitrary_from(rng, &column_values).0;
let gt_value = GTValue::arbitrary_from(rng, column_value).0;
Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name(table.name.clone()),
@@ -304,7 +293,7 @@ impl SimplePredicate {
)
}),
Box::new(|rng| {
let lt_value = LTValue::arbitrary_from(rng, &column_values).0;
let lt_value = LTValue::arbitrary_from(rng, column_value).0;
Expr::Binary(
Box::new(ast::Expr::Qualified(
ast::Name(table.name.clone()),
@@ -323,25 +312,37 @@ impl SimplePredicate {
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, true)).0)
.map(|_| SimplePredicate::arbitrary_from(rng, (table, row, true)).0)
.reduce(|accum, curr| {
// dbg!(&accum, &curr);
Predicate(Expr::Binary(
Box::new(accum.0),
ast::Operator::And,
Box::new(curr.0),
))
})
.unwrap_or(Predicate::true_()) // Empty And is True
.unwrap_or(Predicate::true_())
} else {
// Create a vector of random booleans
let mut booleans = (0..rng.gen_range(0..=3))
@@ -357,7 +358,7 @@ impl CompoundPredicate {
booleans
.iter()
.map(|b| SimplePredicate::arbitrary_from(rng, (table, *b)).0)
.map(|b| SimplePredicate::arbitrary_from(rng, (table, row, *b)).0)
.reduce(|accum, curr| {
Predicate(Expr::Binary(
Box::new(accum.0),
@@ -365,7 +366,7 @@ impl CompoundPredicate {
Box::new(curr.0),
))
})
.unwrap_or(Predicate::true_()) // Empty And is True
.unwrap_or(Predicate::false_())
}
} else {
// An OR for true requires at least one of its children to be true
@@ -383,7 +384,7 @@ impl CompoundPredicate {
booleans
.iter()
.map(|b| SimplePredicate::arbitrary_from(rng, (table, *b)).0)
.map(|b| SimplePredicate::arbitrary_from(rng, (table, row, *b)).0)
.reduce(|accum, curr| {
Predicate(Expr::Binary(
Box::new(accum.0),
@@ -391,10 +392,10 @@ impl CompoundPredicate {
Box::new(curr.0),
))
})
.unwrap_or(Predicate::false_()) // Empty Or is False
.unwrap_or(Predicate::true_())
} else {
(0..rng.gen_range(0..=3))
.map(|_| SimplePredicate::arbitrary_from(rng, (table, false)).0)
.map(|_| SimplePredicate::arbitrary_from(rng, (table, row, false)).0)
.reduce(|accum, curr| {
Predicate(Expr::Binary(
Box::new(accum.0),
@@ -402,7 +403,7 @@ impl CompoundPredicate {
Box::new(curr.0),
))
})
.unwrap_or(Predicate::false_()) // Empty Or is False
.unwrap_or(Predicate::false_())
}
};
Self(predicate)
@@ -504,7 +505,8 @@ mod tests {
})
.collect();
table.rows.extend(values.clone());
let predicate = SimplePredicate::true_binary(&mut rng, &table);
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))
@@ -531,7 +533,8 @@ mod tests {
})
.collect();
table.rows.extend(values.clone());
let predicate = SimplePredicate::false_binary(&mut rng, &table);
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))

View File

@@ -17,19 +17,22 @@ struct CompoundPredicate(Predicate);
#[derive(Debug)]
struct SimplePredicate(Predicate);
impl ArbitraryFrom<(&Table, bool)> for SimplePredicate {
fn arbitrary_from<R: Rng>(rng: &mut R, (table, predicate_value): (&Table, bool)) -> Self {
impl ArbitraryFrom<(&Table, &[SimValue], bool)> for SimplePredicate {
fn arbitrary_from<R: Rng>(
rng: &mut R,
(table, row, predicate_value): (&Table, &[SimValue], bool),
) -> Self {
let choice = rng.gen_range(0..2);
// Pick an operator
match predicate_value {
true => match choice {
0 => SimplePredicate::true_binary(rng, table),
1 => SimplePredicate::true_unary(rng, table),
0 => SimplePredicate::true_binary(rng, table, row),
1 => SimplePredicate::true_unary(rng, table, row),
_ => unreachable!(),
},
false => match choice {
0 => SimplePredicate::false_binary(rng, table),
1 => SimplePredicate::false_unary(rng, table),
0 => SimplePredicate::false_binary(rng, table, row),
1 => SimplePredicate::false_unary(rng, table, row),
_ => unreachable!(),
},
}
@@ -45,6 +48,12 @@ impl ArbitraryFrom<(&Table, bool)> for CompoundPredicate {
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
}
}
@@ -216,3 +225,164 @@ impl ArbitraryFrom<(&Table, &Vec<SimValue>)> for Predicate {
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

@@ -1,4 +1,4 @@
//! Contains code for generation for [ast::Expr::Unary] Predicate
//! 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]
@@ -100,21 +100,21 @@ impl ArbitraryFromMaybe<(&Vec<&SimValue>, bool)> for BitNotValue {
// 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) -> Self {
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_values = table
.rows
.iter()
.map(|r| &r[column_index])
.collect::<Vec<_>>();
let num_retries = column_values.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_values).map(|value| {
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()))
@@ -124,7 +124,7 @@ impl SimplePredicate {
(
num_retries,
Box::new(|rng| {
TrueValue::arbitrary_from_maybe(rng, &column_values).map(|value| {
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()))
@@ -134,20 +134,18 @@ impl SimplePredicate {
(
num_retries,
Box::new(|rng| {
BitNotValue::arbitrary_from_maybe(rng, (&column_values, true)).map(
|value| {
Expr::unary(
ast::UnaryOperator::BitwiseNot,
Expr::Literal(value.0.into()),
)
},
)
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_values).map(|value| {
FalseValue::arbitrary_from_maybe(rng, column_value).map(|value| {
assert!(!value.0.into_bool());
Expr::unary(ast::UnaryOperator::Not, Expr::Literal(value.0.into()))
})
@@ -162,22 +160,22 @@ impl SimplePredicate {
))
}
/// Generates a false [ast::Expr::Unary] [SimplePredicate] from a [Table] for some values in the table
pub fn false_unary<R: rand::Rng>(rng: &mut R, table: &Table) -> Self {
/// 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_values = table
.rows
.iter()
.map(|r| &r[column_index])
.collect::<Vec<_>>();
let num_retries = column_values.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_values).map(|value| {
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()))
@@ -187,7 +185,7 @@ impl SimplePredicate {
(
num_retries,
Box::new(|rng| {
FalseValue::arbitrary_from_maybe(rng, &column_values).map(|value| {
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()))
@@ -197,20 +195,18 @@ impl SimplePredicate {
(
num_retries,
Box::new(|rng| {
BitNotValue::arbitrary_from_maybe(rng, (&column_values, false)).map(
|value| {
Expr::unary(
ast::UnaryOperator::BitwiseNot,
Expr::Literal(value.0.into()),
)
},
)
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_values).map(|value| {
TrueValue::arbitrary_from_maybe(rng, column_value).map(|value| {
assert!(value.0.into_bool());
Expr::unary(ast::UnaryOperator::Not, Expr::Literal(value.0.into()))
})
@@ -221,7 +217,7 @@ impl SimplePredicate {
);
// If cannot generate a value
SimplePredicate(Predicate(
expr.unwrap_or(Expr::Literal(SimValue::TRUE.into())),
expr.unwrap_or(Expr::Literal(SimValue::FALSE.into())),
))
}
}
@@ -232,7 +228,7 @@ mod tests {
use rand_chacha::ChaCha8Rng;
use crate::{
generation::{predicate::SimplePredicate, Arbitrary, ArbitraryFrom as _},
generation::{pick, predicate::SimplePredicate, Arbitrary, ArbitraryFrom as _},
model::table::{SimValue, Table},
};
@@ -260,7 +256,8 @@ mod tests {
})
.collect();
table.rows.extend(values.clone());
let predicate = SimplePredicate::true_unary(&mut rng, &table);
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))
@@ -287,7 +284,8 @@ mod tests {
})
.collect();
table.rows.extend(values.clone());
let predicate = SimplePredicate::false_unary(&mut rng, &table);
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))
@@ -295,7 +293,4 @@ mod tests {
assert!(result, "Predicate: {:#?}\nSeed: {}", predicate, seed)
}
}
#[test]
fn test_x() {}
}

View File

@@ -48,6 +48,7 @@ pub fn expr_to_value(expr: &ast::Expr, row: &[SimValue], table: &Table) -> Optio
ast::Expr::Binary(lhs, op, rhs) => {
let lhs = expr_to_value(lhs, row, table)?;
let rhs = expr_to_value(rhs, row, table)?;
// dbg!(&lhs, &rhs);
Some(lhs.binary_compare(&rhs, *op))
}
ast::Expr::Like {