mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-02 23:04:23 +01:00
remove previous predicate struct and rewrite generation with the new the struct
This commit is contained in:
@@ -4,6 +4,7 @@ use limbo_sqlite3_parser::ast::{
|
||||
|
||||
use crate::{
|
||||
generation::{gen_random_text, pick, pick_index, Arbitrary, ArbitraryFrom},
|
||||
model::table::Value,
|
||||
SimulatorEnv,
|
||||
};
|
||||
|
||||
@@ -56,6 +57,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Freestyling generation
|
||||
impl ArbitraryFrom<&SimulatorEnv> for Expr {
|
||||
fn arbitrary_from<R: rand::Rng>(rng: &mut R, t: &SimulatorEnv) -> Self {
|
||||
// Loop until we get an implmeneted expression
|
||||
@@ -267,6 +269,22 @@ impl ArbitraryFrom<&SimulatorEnv> for ast::Literal {
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a litreal value
|
||||
impl ArbitraryFrom<&Vec<&Value>> for ast::Expr {
|
||||
fn arbitrary_from<R: rand::Rng>(rng: &mut R, values: &Vec<&Value>) -> Self {
|
||||
if values.is_empty() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
impl ArbitraryFrom<&SimulatorEnv> for UnaryOperator {
|
||||
fn arbitrary_from<R: rand::Rng>(rng: &mut R, _t: &SimulatorEnv) -> Self {
|
||||
let choice = rng.gen_range(0..4);
|
||||
|
||||
@@ -5,6 +5,7 @@ use rand::{distributions::uniform::SampleUniform, Rng};
|
||||
|
||||
mod expr;
|
||||
pub mod plan;
|
||||
mod predicate;
|
||||
pub mod property;
|
||||
pub mod query;
|
||||
pub mod table;
|
||||
|
||||
@@ -6,7 +6,8 @@ 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,
|
||||
},
|
||||
|
||||
553
simulator/generation/predicate.rs
Normal file
553
simulator/generation/predicate.rs
Normal file
@@ -0,0 +1,553 @@
|
||||
use limbo_sqlite3_parser::ast::{self, Expr};
|
||||
use rand::{seq::SliceRandom as _, Rng};
|
||||
|
||||
use crate::model::{
|
||||
query::predicate::Predicate,
|
||||
table::{Table, Value},
|
||||
};
|
||||
|
||||
use super::{
|
||||
backtrack, one_of,
|
||||
table::{GTValue, LTValue, LikeValue},
|
||||
ArbitraryFrom, ArbitraryFromMaybe as _,
|
||||
};
|
||||
|
||||
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| {
|
||||
Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
ast::Name(column.name.clone()),
|
||||
)),
|
||||
ast::Operator::Equals,
|
||||
Box::new(Expr::arbitrary_from(rng, &column_values)),
|
||||
)
|
||||
}),
|
||||
Box::new(|rng| {
|
||||
let gt_value = GTValue::arbitrary_from(rng, &column_values).0;
|
||||
Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
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_values).0;
|
||||
Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
ast::Name(column.name.clone()),
|
||||
)),
|
||||
ast::Operator::Less,
|
||||
Box::new(Expr::Literal(lt_value.into())),
|
||||
)
|
||||
}),
|
||||
],
|
||||
rng,
|
||||
),
|
||||
false => one_of(
|
||||
vec![
|
||||
Box::new(|rng| {
|
||||
Expr::Binary(
|
||||
Box::new(Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
ast::Name(column.name.clone()),
|
||||
)),
|
||||
ast::Operator::NotEquals,
|
||||
Box::new(Expr::arbitrary_from(rng, &column_values)),
|
||||
)
|
||||
}),
|
||||
Box::new(|rng| {
|
||||
let lt_value = LTValue::arbitrary_from(rng, &column_values).0;
|
||||
Expr::Binary(
|
||||
Box::new(Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
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_values).0;
|
||||
Expr::Binary(
|
||||
Box::new(Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
ast::Name(column.name.clone()),
|
||||
)),
|
||||
ast::Operator::Less,
|
||||
Box::new(Expr::Literal(gt_value.into())),
|
||||
)
|
||||
}),
|
||||
],
|
||||
rng,
|
||||
),
|
||||
};
|
||||
|
||||
Self(Predicate(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 {
|
||||
(0..rng.gen_range(0..=3))
|
||||
.map(|_| SimplePredicate::arbitrary_from(rng, (table, true)).0)
|
||||
.reduce(|accum, curr| {
|
||||
Predicate(Expr::Binary(
|
||||
Box::new(accum.0),
|
||||
ast::Operator::And,
|
||||
Box::new(curr.0),
|
||||
))
|
||||
})
|
||||
.unwrap_or(Predicate::true_()) // Empty And is 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, *b)).0)
|
||||
.reduce(|accum, curr| {
|
||||
Predicate(Expr::Binary(
|
||||
Box::new(accum.0),
|
||||
ast::Operator::And,
|
||||
Box::new(curr.0),
|
||||
))
|
||||
})
|
||||
.unwrap_or(Predicate::true_()) // Empty And is True
|
||||
}
|
||||
} 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, *b)).0)
|
||||
.reduce(|accum, curr| {
|
||||
Predicate(Expr::Binary(
|
||||
Box::new(accum.0),
|
||||
ast::Operator::Or,
|
||||
Box::new(curr.0),
|
||||
))
|
||||
})
|
||||
.unwrap_or(Predicate::false_()) // Empty Or is False
|
||||
} else {
|
||||
(0..rng.gen_range(0..=3))
|
||||
.map(|_| SimplePredicate::arbitrary_from(rng, (table, false)).0)
|
||||
.reduce(|accum, curr| {
|
||||
Predicate(Expr::Binary(
|
||||
Box::new(accum.0),
|
||||
ast::Operator::Or,
|
||||
Box::new(curr.0),
|
||||
))
|
||||
})
|
||||
.unwrap_or(Predicate::false_()) // Empty Or is False
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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(Expr::Binary(
|
||||
Box::new(Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
ast::Name(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;
|
||||
Predicate(Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
ast::Name(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;
|
||||
Predicate(Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
ast::Name(column_name.to_string()),
|
||||
)),
|
||||
ast::Operator::Less,
|
||||
Box::new(Expr::Literal(lt_value.into())),
|
||||
))
|
||||
}),
|
||||
],
|
||||
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(Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
ast::Name(column.name.clone()),
|
||||
)),
|
||||
ast::Operator::Equals,
|
||||
Box::new(Expr::Literal(value.into())),
|
||||
)))
|
||||
}),
|
||||
),
|
||||
(
|
||||
1,
|
||||
Box::new(|rng| {
|
||||
let v = Value::arbitrary_from(rng, &column.column_type);
|
||||
if &v == value {
|
||||
None
|
||||
} else {
|
||||
Some(Predicate(Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
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(Predicate(Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
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(Predicate(Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
ast::Name(column.name.clone()),
|
||||
)),
|
||||
ast::Operator::Less,
|
||||
Box::new(Expr::Literal(gt_value.into())),
|
||||
)))
|
||||
}),
|
||||
),
|
||||
(
|
||||
1,
|
||||
Box::new(|rng| {
|
||||
LikeValue::arbitrary_from_maybe(rng, value).map(|like| {
|
||||
Predicate(Expr::Like {
|
||||
lhs: Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
/// 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(Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
ast::Name(column.name.clone()),
|
||||
)),
|
||||
ast::Operator::NotEquals,
|
||||
Box::new(Expr::Literal(value.into())),
|
||||
))
|
||||
}),
|
||||
Box::new(|rng| {
|
||||
let v = loop {
|
||||
let v = Value::arbitrary_from(rng, &column.column_type);
|
||||
if &v != value {
|
||||
break v;
|
||||
}
|
||||
};
|
||||
Predicate(Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
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;
|
||||
Predicate(Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
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;
|
||||
Predicate(Expr::Binary(
|
||||
Box::new(ast::Expr::Qualified(
|
||||
ast::Name("".to_string()),
|
||||
ast::Name(column.name.clone()),
|
||||
)),
|
||||
ast::Operator::Less,
|
||||
Box::new(Expr::Literal(lt_value.into())),
|
||||
))
|
||||
}),
|
||||
],
|
||||
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(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
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ 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,
|
||||
|
||||
@@ -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::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 {
|
||||
@@ -137,342 +134,6 @@ 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);
|
||||
|
||||
@@ -197,7 +197,7 @@ impl ArbitraryFrom<&Value> for GTValue {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LikeValue(pub(crate) String);
|
||||
pub(crate) struct LikeValue(pub(crate) Value);
|
||||
|
||||
impl ArbitraryFromMaybe<&Value> for LikeValue {
|
||||
fn arbitrary_from_maybe<R: Rng>(rng: &mut R, value: &Value) -> Option<Self> {
|
||||
@@ -221,7 +221,7 @@ impl ArbitraryFromMaybe<&Value> for LikeValue {
|
||||
}
|
||||
let index = rng.gen_range(0..t.len());
|
||||
t.insert(index, '%');
|
||||
Some(Self(t.into_iter().collect()))
|
||||
Some(Self(Value::Text(t.into_iter().collect())))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{model::table::Value, SimulatorEnv};
|
||||
|
||||
use super::select::Predicate;
|
||||
use super::predicate::Predicate;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct Delete {
|
||||
|
||||
@@ -18,7 +18,7 @@ macro_rules! assert_implemented_predicate_expr {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Predicate(ast::Expr);
|
||||
pub struct Predicate(pub ast::Expr);
|
||||
|
||||
impl Predicate {
|
||||
pub(crate) fn true_() -> Self {
|
||||
@@ -59,7 +59,8 @@ impl Predicate {
|
||||
}
|
||||
}
|
||||
ast::Expr::Literal(literal) => Value::from(literal).into_bool(),
|
||||
ast::Expr::Unary(unary_operator, expr) => todo!(),
|
||||
// TODO: next implement unary operator
|
||||
ast::Expr::Unary(..) => todo!(),
|
||||
expr => unimplemented!("{:?}", expr),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::Value, SimulatorEnv};
|
||||
|
||||
use super::predicate::Predicate;
|
||||
|
||||
/// `SELECT` distinctness
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -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(®ex_pattern)
|
||||
.case_insensitive(true)
|
||||
.dot_matches_new_line(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn exec_like(pattern: &str, text: &str) -> bool {
|
||||
let re = construct_like_regex(pattern);
|
||||
re.is_match(text)
|
||||
}
|
||||
|
||||
impl Predicate {
|
||||
pub(crate) fn true_() -> Self {
|
||||
Self::And(vec![])
|
||||
}
|
||||
|
||||
pub(crate) fn false_() -> Self {
|
||||
Self::Or(vec![])
|
||||
}
|
||||
|
||||
pub(crate) fn test(&self, row: &[Value], table: &Table) -> bool {
|
||||
let get_value = |name: &str| {
|
||||
table
|
||||
.columns
|
||||
.iter()
|
||||
.zip(row.iter())
|
||||
.find(|(column, _)| column.name == name)
|
||||
.map(|(_, value)| value)
|
||||
};
|
||||
|
||||
match self {
|
||||
Predicate::And(vec) => vec.iter().all(|p| p.test(row, table)),
|
||||
Predicate::Or(vec) => vec.iter().any(|p| p.test(row, table)),
|
||||
Predicate::Eq(column, value) => get_value(column) == Some(value),
|
||||
Predicate::Neq(column, value) => get_value(column) != Some(value),
|
||||
Predicate::Gt(column, value) => get_value(column).map(|v| v > value).unwrap_or(false),
|
||||
Predicate::Lt(column, value) => get_value(column).map(|v| v < value).unwrap_or(false),
|
||||
Predicate::Like(column, value) => get_value(column)
|
||||
.map(|v| exec_like(v.to_string().as_str(), value.as_str()))
|
||||
.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Predicate {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::And(predicates) => {
|
||||
if predicates.is_empty() {
|
||||
// todo: Make this TRUE when the bug is fixed
|
||||
write!(f, "TRUE")
|
||||
} else {
|
||||
write!(f, "(")?;
|
||||
for (i, p) in predicates.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(f, " AND ")?;
|
||||
}
|
||||
write!(f, "{}", p)?;
|
||||
}
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
||||
Self::Or(predicates) => {
|
||||
if predicates.is_empty() {
|
||||
write!(f, "FALSE")
|
||||
} else {
|
||||
write!(f, "(")?;
|
||||
for (i, p) in predicates.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(f, " OR ")?;
|
||||
}
|
||||
write!(f, "{}", p)?;
|
||||
}
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
||||
Self::Eq(name, value) => write!(f, "{} = {}", name, value),
|
||||
Self::Neq(name, value) => write!(f, "{} != {}", name, value),
|
||||
Self::Gt(name, value) => write!(f, "{} > {}", name, value),
|
||||
Self::Lt(name, value) => write!(f, "{} < {}", name, value),
|
||||
Self::Like(name, value) => write!(f, "{} LIKE '{}'", name, value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{model::table::Value, SimulatorEnv};
|
||||
|
||||
use super::select::Predicate;
|
||||
use super::predicate::Predicate;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct Update {
|
||||
|
||||
@@ -248,6 +248,30 @@ impl From<ast::Literal> for Value {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Value> for ast::Literal {
|
||||
fn from(value: Value) -> Self {
|
||||
match value {
|
||||
Value::Null => Self::Null,
|
||||
Value::Integer(i) => Self::Numeric(i.to_string()),
|
||||
Value::Float(f) => Self::Numeric(f.to_string()),
|
||||
Value::Text(string) => Self::String(string),
|
||||
Value::Blob(blob) => Self::Blob(hex::encode(blob)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Value> for ast::Literal {
|
||||
fn from(value: &Value) -> Self {
|
||||
match value {
|
||||
Value::Null => Self::Null,
|
||||
Value::Integer(i) => Self::Numeric(i.to_string()),
|
||||
Value::Float(f) => Self::Numeric(f.to_string()),
|
||||
Value::Text(string) => Self::String(string.clone()),
|
||||
Value::Blob(blob) => Self::Blob(hex::encode(blob)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Numeric> for Value {
|
||||
fn from(value: Numeric) -> Self {
|
||||
match value {
|
||||
|
||||
Reference in New Issue
Block a user