Files
turso/sql_generation/generation/predicate/unary.rs
Pekka Enberg 9f6c11a74f sql_generation: Fix predicate column indexing
The number of columns in the row can be less than the number of columns in the
table so fix out of bounds error in indexing.
2025-09-08 11:59:45 +03:00

334 lines
11 KiB
Rust

//! 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 turso_parser::ast::{self, Expr};
use crate::{
generation::{
backtrack, pick, predicate::SimplePredicate, ArbitraryFromMaybe, GenerationContext,
},
model::{
query::predicate::Predicate,
table::{SimValue, TableContext},
},
};
pub struct TrueValue(pub SimValue);
impl ArbitraryFromMaybe<&SimValue> for TrueValue {
fn arbitrary_from_maybe<R: rand::Rng, C: GenerationContext>(
_rng: &mut R,
_context: &C,
value: &SimValue,
) -> Option<Self>
where
Self: Sized,
{
// If the Value is a true value return it else you cannot return a true Value
value.as_bool().then_some(Self(value.clone()))
}
}
impl ArbitraryFromMaybe<&Vec<&SimValue>> for TrueValue {
fn arbitrary_from_maybe<R: rand::Rng, C: GenerationContext>(
rng: &mut R,
context: &C,
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, context, *value)
}
}
pub struct FalseValue(pub SimValue);
impl ArbitraryFromMaybe<&SimValue> for FalseValue {
fn arbitrary_from_maybe<R: rand::Rng, C: GenerationContext>(
_rng: &mut R,
_context: &C,
value: &SimValue,
) -> Option<Self>
where
Self: Sized,
{
// If the Value is a false value return it else you cannot return a false Value
(!value.as_bool()).then_some(Self(value.clone()))
}
}
impl ArbitraryFromMaybe<&Vec<&SimValue>> for FalseValue {
fn arbitrary_from_maybe<R: rand::Rng, C: GenerationContext>(
rng: &mut R,
context: &C,
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, context, *value)
}
}
#[allow(dead_code)]
pub struct BitNotValue(pub SimValue);
impl ArbitraryFromMaybe<(&SimValue, bool)> for BitNotValue {
fn arbitrary_from_maybe<R: rand::Rng, C: GenerationContext>(
_rng: &mut R,
_context: &C,
(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.as_bool() == predicate).then_some(BitNotValue(value.clone()))
}
}
impl ArbitraryFromMaybe<(&Vec<&SimValue>, bool)> for BitNotValue {
fn arbitrary_from_maybe<R: rand::Rng, C: GenerationContext>(
rng: &mut R,
context: &C,
(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, context, (*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 [TableContext] for some values in the table
pub fn true_unary<R: rand::Rng, C: GenerationContext, T: TableContext>(
rng: &mut R,
context: &C,
_table: &T,
row: &[SimValue],
) -> Self {
// Pick a random column
let column_index = rng.random_range(0..row.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, context, column_value).map(|value| {
assert!(value.0.as_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.as_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, context, column_value).map(|value| {
assert!(!value.0.as_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 [TableContext] for a row in the table
pub fn false_unary<R: rand::Rng, C: GenerationContext, T: TableContext>(
rng: &mut R,
context: &C,
_table: &T,
row: &[SimValue],
) -> Self {
// Avoid creation of NULLs
if row.is_empty() {
return SimplePredicate(Predicate(Expr::Literal(SimValue::FALSE.into())));
}
// Pick a random column
let column_index = rng.random_range(0..row.len());
let column_value = &row[column_index];
let num_retries = row.len();
let expr = backtrack(
vec![
// (
// num_retries,
// Box::new(|rng| {
// FalseValue::arbitrary_from_maybe(rng, column_value).map(|value| {
// assert!(!value.0.as_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.as_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, context, column_value).map(|value| {
assert!(value.0.as_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, tests::TestContext, 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);
let context = &TestContext::default();
for _ in 0..10000 {
let mut table = Table::arbitrary(&mut rng, context);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table
.columns
.iter()
.map(|c| SimValue::arbitrary_from(&mut rng, context, &c.column_type))
.collect()
})
.collect();
table.rows.extend(values.clone());
let row = pick(&table.rows, &mut rng);
let predicate = SimplePredicate::true_unary(&mut rng, context, &table, row);
let result = values
.iter()
.map(|row| predicate.0.test(row, &table))
.reduce(|accum, curr| accum || curr)
.unwrap_or(false);
assert!(result, "Predicate: {predicate:#?}\nSeed: {seed}")
}
}
#[test]
fn fuzz_false_unary_simple_predicate() {
let seed = get_seed();
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let context = &TestContext::default();
for _ in 0..10000 {
let mut table = Table::arbitrary(&mut rng, context);
let num_rows = rng.random_range(1..10);
let values: Vec<Vec<SimValue>> = (0..num_rows)
.map(|_| {
table
.columns
.iter()
.map(|c| SimValue::arbitrary_from(&mut rng, context, &c.column_type))
.collect()
})
.collect();
table.rows.extend(values.clone());
let row = pick(&table.rows, &mut rng);
let predicate = SimplePredicate::false_unary(&mut rng, context, &table, row);
let result = values
.iter()
.map(|row| predicate.0.test(row, &table))
.any(|res| !res);
assert!(result, "Predicate: {predicate:#?}\nSeed: {seed}")
}
}
}