mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-20 15:35:29 +01:00
Merge 'Add some missing supported features in WHERE clause' from Jussi Saurio
This PR is sourced from the fuzzing exploration PR in https://github.com/tursodatabase/limbo/pull/1021 **Adds missing support:** Support all the same literals in WHERE clause position as in SELECT position Support CAST in WHERE clause position Support FunctionCall in WHERE clause position Support Column in WHERE clause position Support Rowid in WHERE clause position Support CASE in WHERE clause position Support LIKE in SELECT position Support Unary expressions in WHERE clause position Support rest of the Binary expressions in WHERE clause position Support TEXT in remainder operations **Fix:** Remove incorrect constant folding optimization for NULL **Testing utils:** Enhance sqlite fuzzer to mostly be able to work with the same set of possible expressions in both SELECT and WHERE clause position Closes #1024
This commit is contained in:
@@ -237,7 +237,19 @@ pub fn translate_condition_expr(
|
||||
resolver,
|
||||
)?;
|
||||
}
|
||||
ast::Expr::Binary(lhs, op, rhs) => {
|
||||
ast::Expr::Binary(lhs, op, rhs)
|
||||
if matches!(
|
||||
op,
|
||||
ast::Operator::Greater
|
||||
| ast::Operator::GreaterEquals
|
||||
| ast::Operator::Less
|
||||
| ast::Operator::LessEquals
|
||||
| ast::Operator::Equals
|
||||
| ast::Operator::NotEquals
|
||||
| ast::Operator::Is
|
||||
| ast::Operator::IsNot
|
||||
) =>
|
||||
{
|
||||
let lhs_reg = program.alloc_register();
|
||||
let rhs_reg = program.alloc_register();
|
||||
translate_and_mark(program, Some(referenced_tables), lhs, lhs_reg, resolver)?;
|
||||
@@ -267,35 +279,24 @@ pub fn translate_condition_expr(
|
||||
ast::Operator::IsNot => {
|
||||
emit_cmp_null_insn!(program, condition_metadata, Ne, Eq, lhs_reg, rhs_reg)
|
||||
}
|
||||
_ => {
|
||||
todo!("op {:?} not implemented", op);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
ast::Expr::Literal(lit) => match lit {
|
||||
ast::Literal::Numeric(val) => {
|
||||
let maybe_int = val.parse::<i64>();
|
||||
if let Ok(int_value) = maybe_int {
|
||||
let reg = program.alloc_register();
|
||||
program.emit_insn(Insn::Integer {
|
||||
value: int_value,
|
||||
dest: reg,
|
||||
});
|
||||
emit_cond_jump(program, condition_metadata, reg);
|
||||
} else {
|
||||
crate::bail_parse_error!("unsupported literal type in condition");
|
||||
}
|
||||
}
|
||||
ast::Literal::String(string) => {
|
||||
let reg = program.alloc_register();
|
||||
program.emit_insn(Insn::String8 {
|
||||
value: string.clone(),
|
||||
dest: reg,
|
||||
});
|
||||
emit_cond_jump(program, condition_metadata, reg);
|
||||
}
|
||||
unimpl => todo!("literal {:?} not implemented", unimpl),
|
||||
},
|
||||
ast::Expr::Binary(_, _, _) => {
|
||||
let result_reg = program.alloc_register();
|
||||
translate_expr(program, Some(referenced_tables), expr, result_reg, resolver)?;
|
||||
emit_cond_jump(program, condition_metadata, result_reg);
|
||||
}
|
||||
ast::Expr::Literal(_)
|
||||
| ast::Expr::Cast { .. }
|
||||
| ast::Expr::FunctionCall { .. }
|
||||
| ast::Expr::Column { .. }
|
||||
| ast::Expr::RowId { .. }
|
||||
| ast::Expr::Case { .. } => {
|
||||
let reg = program.alloc_register();
|
||||
translate_expr(program, Some(referenced_tables), expr, reg, resolver)?;
|
||||
emit_cond_jump(program, condition_metadata, reg);
|
||||
}
|
||||
ast::Expr::InList { lhs, not, rhs } => {
|
||||
// lhs is e.g. a column reference
|
||||
// rhs is an Option<Vec<Expr>>
|
||||
@@ -410,49 +411,9 @@ pub fn translate_condition_expr(
|
||||
program.resolve_label(jump_target_when_true, program.offset());
|
||||
}
|
||||
}
|
||||
ast::Expr::Like {
|
||||
lhs,
|
||||
not,
|
||||
op,
|
||||
rhs,
|
||||
escape: _,
|
||||
} => {
|
||||
ast::Expr::Like { not, .. } => {
|
||||
let cur_reg = program.alloc_register();
|
||||
match op {
|
||||
ast::LikeOperator::Like | ast::LikeOperator::Glob => {
|
||||
let start_reg = program.alloc_registers(2);
|
||||
let mut constant_mask = 0;
|
||||
translate_and_mark(
|
||||
program,
|
||||
Some(referenced_tables),
|
||||
lhs,
|
||||
start_reg + 1,
|
||||
resolver,
|
||||
)?;
|
||||
let _ =
|
||||
translate_expr(program, Some(referenced_tables), rhs, start_reg, resolver)?;
|
||||
if matches!(rhs.as_ref(), ast::Expr::Literal(_)) {
|
||||
program.mark_last_insn_constant();
|
||||
constant_mask = 1;
|
||||
}
|
||||
let func = match op {
|
||||
ast::LikeOperator::Like => ScalarFunc::Like,
|
||||
ast::LikeOperator::Glob => ScalarFunc::Glob,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
program.emit_insn(Insn::Function {
|
||||
constant_mask,
|
||||
start_reg,
|
||||
dest: cur_reg,
|
||||
func: FuncCtx {
|
||||
func: Func::Scalar(func),
|
||||
arg_count: 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
ast::LikeOperator::Match => todo!(),
|
||||
ast::LikeOperator::Regexp => todo!(),
|
||||
}
|
||||
translate_like_base(program, Some(referenced_tables), expr, cur_reg, resolver)?;
|
||||
if !*not {
|
||||
emit_cond_jump(program, condition_metadata, cur_reg);
|
||||
} else if condition_metadata.jump_if_condition_is_true {
|
||||
@@ -500,7 +461,17 @@ pub fn translate_condition_expr(
|
||||
target_pc: condition_metadata.jump_target_when_false,
|
||||
});
|
||||
}
|
||||
_ => todo!("op {:?} not implemented", expr),
|
||||
ast::Expr::Unary(_, _) => {
|
||||
// This is an inefficient implementation for op::NOT, because translate_expr() will emit an Insn::Not,
|
||||
// and then we immediately emit an Insn::If/Insn::IfNot for the conditional jump. In reality we would not
|
||||
// like to emit the negation instruction Insn::Not at all, since we could just emit the "opposite" jump instruction
|
||||
// directly. However, using translate_expr() directly simplifies our conditional jump code for unary expressions,
|
||||
// and we'd rather be correct than maximally efficient, for now.
|
||||
let expr_reg = program.alloc_register();
|
||||
translate_expr(program, Some(referenced_tables), expr, expr_reg, resolver)?;
|
||||
emit_cond_jump(program, condition_metadata, expr_reg);
|
||||
}
|
||||
other => todo!("expression {:?} not implemented", other),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1969,7 +1940,21 @@ pub fn translate_expr(
|
||||
ast::Expr::InSelect { .. } => todo!(),
|
||||
ast::Expr::InTable { .. } => todo!(),
|
||||
ast::Expr::IsNull(_) => todo!(),
|
||||
ast::Expr::Like { .. } => todo!(),
|
||||
ast::Expr::Like { not, .. } => {
|
||||
let like_reg = if *not {
|
||||
program.alloc_register()
|
||||
} else {
|
||||
target_register
|
||||
};
|
||||
translate_like_base(program, referenced_tables, expr, like_reg, resolver)?;
|
||||
if *not {
|
||||
program.emit_insn(Insn::Not {
|
||||
reg: like_reg,
|
||||
dest: target_register,
|
||||
});
|
||||
}
|
||||
Ok(target_register)
|
||||
}
|
||||
ast::Expr::Literal(lit) => match lit {
|
||||
ast::Literal::Numeric(val) => {
|
||||
let maybe_int = val.parse::<i64>();
|
||||
@@ -2159,6 +2144,59 @@ pub fn translate_expr(
|
||||
}
|
||||
}
|
||||
|
||||
/// The base logic for translating LIKE and GLOB expressions.
|
||||
/// The logic for handling "NOT LIKE" is different depending on whether the expression
|
||||
/// is a conditional jump or not. This is why the caller handles the "NOT LIKE" behavior;
|
||||
/// see [translate_condition_expr] and [translate_expr] for implementations.
|
||||
fn translate_like_base(
|
||||
program: &mut ProgramBuilder,
|
||||
referenced_tables: Option<&[TableReference]>,
|
||||
expr: &ast::Expr,
|
||||
target_register: usize,
|
||||
resolver: &Resolver,
|
||||
) -> Result<usize> {
|
||||
let ast::Expr::Like {
|
||||
lhs,
|
||||
op,
|
||||
rhs,
|
||||
escape: _,
|
||||
..
|
||||
} = expr
|
||||
else {
|
||||
crate::bail_parse_error!("expected Like expression");
|
||||
};
|
||||
match op {
|
||||
ast::LikeOperator::Like | ast::LikeOperator::Glob => {
|
||||
let start_reg = program.alloc_registers(2);
|
||||
let mut constant_mask = 0;
|
||||
translate_and_mark(program, referenced_tables, lhs, start_reg + 1, resolver)?;
|
||||
let _ = translate_expr(program, referenced_tables, rhs, start_reg, resolver)?;
|
||||
if matches!(rhs.as_ref(), ast::Expr::Literal(_)) {
|
||||
program.mark_last_insn_constant();
|
||||
constant_mask = 1;
|
||||
}
|
||||
let func = match op {
|
||||
ast::LikeOperator::Like => ScalarFunc::Like,
|
||||
ast::LikeOperator::Glob => ScalarFunc::Glob,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
program.emit_insn(Insn::Function {
|
||||
constant_mask,
|
||||
start_reg,
|
||||
dest: target_register,
|
||||
func: FuncCtx {
|
||||
func: Func::Scalar(func),
|
||||
arg_count: 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
ast::LikeOperator::Match => todo!(),
|
||||
ast::LikeOperator::Regexp => todo!(),
|
||||
}
|
||||
|
||||
Ok(target_register)
|
||||
}
|
||||
|
||||
/// Emits a whole insn for a function call.
|
||||
/// Assumes the number of parameters is valid for the given function.
|
||||
/// Returns the target register for the function.
|
||||
|
||||
@@ -367,7 +367,6 @@ impl Optimizable for ast::Expr {
|
||||
fn check_constant(&self) -> Result<Option<ConstantPredicate>> {
|
||||
match self {
|
||||
Self::Literal(lit) => match lit {
|
||||
ast::Literal::Null => Ok(Some(ConstantPredicate::AlwaysFalse)),
|
||||
ast::Literal::Numeric(b) => {
|
||||
if let Ok(int_value) = b.parse::<i64>() {
|
||||
return Ok(Some(if int_value == 0 {
|
||||
|
||||
@@ -151,3 +151,12 @@ do_execsql_test select_bin_shl {
|
||||
-997623670|-1995247340|-1021566638080|-1071190259091374080
|
||||
997623670|1995247340|1021566638080|1071190259091374080
|
||||
-997623670|-1995247340|-1021566638080|-1071190259091374080}
|
||||
|
||||
# Test LIKE in SELECT position
|
||||
do_execsql_test select-like-expression {
|
||||
select 'bar' like 'bar%'
|
||||
} {1}
|
||||
|
||||
do_execsql_test select-not-like-expression {
|
||||
select 'bar' not like 'bar%'
|
||||
} {0}
|
||||
@@ -472,3 +472,95 @@ foreach {operator} {
|
||||
# NULL comparison AND id=1
|
||||
do_execsql_test where-binary-one-operand-null-and-$operator "select first_name from users where first_name $operator NULL AND id = 1" {}
|
||||
}
|
||||
|
||||
# Test literals in WHERE clause
|
||||
do_execsql_test where-literal-string {
|
||||
select count(*) from users where 'yes';
|
||||
} {0}
|
||||
|
||||
# FIXME: should return 0
|
||||
#do_execsql_test where-literal-number {
|
||||
# select count(*) from users where x'DEADBEEF';
|
||||
#} {0}
|
||||
|
||||
# Test CAST in WHERE clause
|
||||
do_execsql_test where-cast-string-to-int {
|
||||
select count(*) from users where cast('1' as integer);
|
||||
} {10000}
|
||||
|
||||
do_execsql_test where-cast-float-to-int {
|
||||
select count(*) from users where cast('0' as integer);
|
||||
} {0}
|
||||
|
||||
# Test FunctionCall in WHERE clause
|
||||
do_execsql_test where-function-length {
|
||||
select count(*) from users where length(first_name);
|
||||
} {10000}
|
||||
|
||||
# Test CASE in WHERE clause
|
||||
do_execsql_test where-case-simple {
|
||||
select count(*) from users where
|
||||
case when age > 0 then 1 else 0 end;
|
||||
} {10000}
|
||||
|
||||
do_execsql_test where-case-searched {
|
||||
select count(*) from users where
|
||||
case age
|
||||
when 0 then 0
|
||||
else 1
|
||||
end;
|
||||
} {10000}
|
||||
|
||||
# Test unary operators in WHERE clause
|
||||
do_execsql_test where-unary-not {
|
||||
select count(*) from users where not (id = 1);
|
||||
} {9999}
|
||||
|
||||
do_execsql_test where-unary-plus {
|
||||
select count(*) from users where +1;
|
||||
} {10000}
|
||||
|
||||
do_execsql_test where-unary-minus {
|
||||
select count(*) from users where -1;
|
||||
} {10000}
|
||||
|
||||
do_execsql_test where-unary-bitnot {
|
||||
select count(*) from users where ~1;
|
||||
} {10000}
|
||||
|
||||
# Test binary math operators in WHERE clause
|
||||
do_execsql_test where-binary-add {
|
||||
select count(*) from users where 1 + 1;
|
||||
} {10000}
|
||||
|
||||
do_execsql_test where-binary-subtract {
|
||||
select count(*) from users where 2 - 1;
|
||||
} {10000}
|
||||
|
||||
do_execsql_test where-binary-multiply {
|
||||
select count(*) from users where 2 * 1;
|
||||
} {10000}
|
||||
|
||||
do_execsql_test where-binary-divide {
|
||||
select count(*) from users where 2 / 2;
|
||||
} {10000}
|
||||
|
||||
do_execsql_test where-binary-modulo {
|
||||
select count(*) from users where 3 % 2;
|
||||
} {10000}
|
||||
|
||||
do_execsql_test where-binary-shift-left {
|
||||
select count(*) from users where 1 << 1;
|
||||
} {10000}
|
||||
|
||||
do_execsql_test where-binary-shift-right {
|
||||
select count(*) from users where 2 >> 1;
|
||||
} {10000}
|
||||
|
||||
do_execsql_test where-binary-bitwise-and {
|
||||
select count(*) from users where 3 & 1;
|
||||
} {10000}
|
||||
|
||||
do_execsql_test where-binary-bitwise-or {
|
||||
select count(*) from users where 2 | 1;
|
||||
} {10000}
|
||||
|
||||
@@ -10,9 +10,11 @@ mod tests {
|
||||
|
||||
use crate::{
|
||||
common::TempDatabase,
|
||||
fuzz::grammar_generator::{rand_int, rand_str, GrammarGenerator},
|
||||
fuzz::grammar_generator::{const_str, rand_int, rand_str, GrammarGenerator},
|
||||
};
|
||||
|
||||
use super::grammar_generator::SymbolHandle;
|
||||
|
||||
fn rng_from_time() -> (ChaCha8Rng, u64) {
|
||||
let seed = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -451,15 +453,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn logical_expression_fuzz_run() {
|
||||
let _ = env_logger::try_init();
|
||||
let g = GrammarGenerator::new();
|
||||
struct TestTable {
|
||||
pub name: &'static str,
|
||||
pub columns: Vec<&'static str>,
|
||||
}
|
||||
|
||||
/// Expressions that can be used in both SELECT and WHERE positions.
|
||||
struct CommonBuilders {
|
||||
pub bin_op: SymbolHandle,
|
||||
pub unary_infix_op: SymbolHandle,
|
||||
pub scalar: SymbolHandle,
|
||||
pub paren: SymbolHandle,
|
||||
pub coalesce_expr: SymbolHandle,
|
||||
pub cast_expr: SymbolHandle,
|
||||
pub case_expr: SymbolHandle,
|
||||
pub cmp_op: SymbolHandle,
|
||||
pub number: SymbolHandle,
|
||||
}
|
||||
|
||||
/// Expressions that can be used only in WHERE position due to Limbo limitations.
|
||||
struct PredicateBuilders {
|
||||
pub in_op: SymbolHandle,
|
||||
}
|
||||
|
||||
fn common_builders(g: &GrammarGenerator, tables: Option<&[TestTable]>) -> CommonBuilders {
|
||||
let (expr, expr_builder) = g.create_handle();
|
||||
let (bin_op, bin_op_builder) = g.create_handle();
|
||||
let (unary_infix_op, unary_infix_op_builder) = g.create_handle();
|
||||
let (scalar, scalar_builder) = g.create_handle();
|
||||
let (paren, paren_builder) = g.create_handle();
|
||||
let (like_pattern, like_pattern_builder) = g.create_handle();
|
||||
let (glob_pattern, glob_pattern_builder) = g.create_handle();
|
||||
let (coalesce_expr, coalesce_expr_builder) = g.create_handle();
|
||||
let (cast_expr, cast_expr_builder) = g.create_handle();
|
||||
let (case_expr, case_expr_builder) = g.create_handle();
|
||||
let (cmp_op, cmp_op_builder) = g.create_handle();
|
||||
let (column, column_builder) = g.create_handle();
|
||||
|
||||
paren_builder
|
||||
.concat("")
|
||||
@@ -486,7 +515,6 @@ mod tests {
|
||||
.push(expr)
|
||||
.build();
|
||||
|
||||
let (like_pattern, like_pattern_builder) = g.create_handle();
|
||||
like_pattern_builder
|
||||
.choice()
|
||||
.option_str("%")
|
||||
@@ -495,7 +523,6 @@ mod tests {
|
||||
.repeat(1..10, "")
|
||||
.build();
|
||||
|
||||
let (glob_pattern, glob_pattern_builder) = g.create_handle();
|
||||
glob_pattern_builder
|
||||
.choice()
|
||||
.option_str("*")
|
||||
@@ -505,7 +532,6 @@ mod tests {
|
||||
.repeat(1..10, "")
|
||||
.build();
|
||||
|
||||
let (coalesce_expr, coalesce_expr_builder) = g.create_handle();
|
||||
coalesce_expr_builder
|
||||
.concat("")
|
||||
.push_str("COALESCE(")
|
||||
@@ -513,7 +539,6 @@ mod tests {
|
||||
.push_str(")")
|
||||
.build();
|
||||
|
||||
let (cast_expr, cast_expr_builder) = g.create_handle();
|
||||
cast_expr_builder
|
||||
.concat(" ")
|
||||
.push_str("CAST ( (")
|
||||
@@ -524,7 +549,6 @@ mod tests {
|
||||
.push_str(")")
|
||||
.build();
|
||||
|
||||
let (case_expr, case_expr_builder) = g.create_handle();
|
||||
case_expr_builder
|
||||
.concat(" ")
|
||||
.push_str("CASE (")
|
||||
@@ -593,21 +617,192 @@ mod tests {
|
||||
)
|
||||
.build();
|
||||
|
||||
let number = g
|
||||
.create()
|
||||
.choice()
|
||||
.option_symbol(rand_int(-0xff..0x100))
|
||||
.option_symbol(rand_int(-0xffff..0x10000))
|
||||
.option_symbol(rand_int(-0xffffff..0x1000000))
|
||||
.option_symbol(rand_int(-0xffffffff..0x100000000))
|
||||
.option_symbol(rand_int(-0xffffffffffff..0x1000000000000))
|
||||
.build();
|
||||
|
||||
let mut column_builder = column_builder
|
||||
.choice()
|
||||
.option(
|
||||
g.create()
|
||||
.concat(" ")
|
||||
.push_str("(")
|
||||
.push(column)
|
||||
.push_str(")")
|
||||
.build(),
|
||||
)
|
||||
.option(number)
|
||||
.option(
|
||||
g.create()
|
||||
.concat(" ")
|
||||
.push_str("(")
|
||||
.push(column)
|
||||
.push(
|
||||
g.create()
|
||||
.choice()
|
||||
.options_str([
|
||||
"+", "-", "*", "/", "||", "=", "<>", ">", "<", ">=", "<=", "IS",
|
||||
"IS NOT",
|
||||
])
|
||||
.build(),
|
||||
)
|
||||
.push(column)
|
||||
.push_str(")")
|
||||
.build(),
|
||||
);
|
||||
|
||||
if let Some(tables) = tables {
|
||||
for table in tables.iter() {
|
||||
for column in table.columns.iter() {
|
||||
column_builder = column_builder
|
||||
.option_symbol_w(const_str(&format!("{}.{}", table.name, column)), 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
column_builder.build();
|
||||
|
||||
cmp_op_builder
|
||||
.concat(" ")
|
||||
.push(column)
|
||||
.push(
|
||||
g.create()
|
||||
.choice()
|
||||
.options_str(["=", "<>", ">", "<", ">=", "<=", "IS", "IS NOT"])
|
||||
.build(),
|
||||
)
|
||||
.push(column)
|
||||
.build();
|
||||
|
||||
expr_builder
|
||||
.choice()
|
||||
.option_w(cast_expr, 1.0)
|
||||
.option_w(case_expr, 1.0)
|
||||
.option_w(unary_infix_op, 2.0)
|
||||
.option_w(bin_op, 3.0)
|
||||
.option_w(unary_infix_op, 2.0)
|
||||
.option_w(paren, 2.0)
|
||||
.option_w(scalar, 4.0)
|
||||
// unfortunately, sqlite behaves weirdly when IS operator is used with TRUE/FALSE constants
|
||||
// e.g. 8 IS TRUE == 1 (although 8 = TRUE == 0)
|
||||
// so, we do not use TRUE/FALSE constants as they will produce diff with sqlite results
|
||||
.option_w(coalesce_expr, 1.0)
|
||||
.option_w(cast_expr, 1.0)
|
||||
.option_w(case_expr, 1.0)
|
||||
.option_w(cmp_op, 1.0)
|
||||
.options_str(["1", "0", "NULL", "2.0", "1.5", "-0.5", "-2.0", "(1 / 0)"])
|
||||
.build();
|
||||
|
||||
let sql = g.create().concat(" ").push_str("SELECT").push(expr).build();
|
||||
CommonBuilders {
|
||||
bin_op,
|
||||
unary_infix_op,
|
||||
scalar,
|
||||
paren,
|
||||
coalesce_expr,
|
||||
cast_expr,
|
||||
case_expr,
|
||||
cmp_op,
|
||||
number,
|
||||
}
|
||||
}
|
||||
|
||||
fn predicate_builders(g: &GrammarGenerator, tables: Option<&[TestTable]>) -> PredicateBuilders {
|
||||
let (in_op, in_op_builder) = g.create_handle();
|
||||
let (column, column_builder) = g.create_handle();
|
||||
let mut column_builder = column_builder
|
||||
.choice()
|
||||
.option(
|
||||
g.create()
|
||||
.concat(" ")
|
||||
.push_str("(")
|
||||
.push(column)
|
||||
.push_str(")")
|
||||
.build(),
|
||||
)
|
||||
.option_symbol(rand_int(-0xffffffff..0x100000000))
|
||||
.option(
|
||||
g.create()
|
||||
.concat(" ")
|
||||
.push_str("(")
|
||||
.push(column)
|
||||
.push(g.create().choice().options_str(["+", "-"]).build())
|
||||
.push(column)
|
||||
.push_str(")")
|
||||
.build(),
|
||||
);
|
||||
|
||||
if let Some(tables) = tables {
|
||||
for table in tables.iter() {
|
||||
for column in table.columns.iter() {
|
||||
column_builder = column_builder
|
||||
.option_symbol_w(const_str(&format!("{}.{}", table.name, column)), 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
column_builder.build();
|
||||
|
||||
in_op_builder
|
||||
.concat(" ")
|
||||
.push(column)
|
||||
.push(g.create().choice().options_str(["IN", "NOT IN"]).build())
|
||||
.push_str("(")
|
||||
.push(
|
||||
g.create()
|
||||
.concat("")
|
||||
.push(column)
|
||||
.repeat(1..5, ", ")
|
||||
.build(),
|
||||
)
|
||||
.push_str(")")
|
||||
.build();
|
||||
|
||||
PredicateBuilders { in_op }
|
||||
}
|
||||
|
||||
fn build_logical_expr(
|
||||
g: &GrammarGenerator,
|
||||
common: &CommonBuilders,
|
||||
predicate: Option<&PredicateBuilders>,
|
||||
) -> SymbolHandle {
|
||||
let (handle, builder) = g.create_handle();
|
||||
let mut builder = builder
|
||||
.choice()
|
||||
.option_w(common.cast_expr, 1.0)
|
||||
.option_w(common.case_expr, 1.0)
|
||||
.option_w(common.cmp_op, 1.0)
|
||||
.option_w(common.coalesce_expr, 1.0)
|
||||
.option_w(common.unary_infix_op, 2.0)
|
||||
.option_w(common.bin_op, 3.0)
|
||||
.option_w(common.paren, 2.0)
|
||||
.option_w(common.scalar, 4.0)
|
||||
// unfortunately, sqlite behaves weirdly when IS operator is used with TRUE/FALSE constants
|
||||
// e.g. 8 IS TRUE == 1 (although 8 = TRUE == 0)
|
||||
// so, we do not use TRUE/FALSE constants as they will produce diff with sqlite results
|
||||
.options_str(["1", "0", "NULL", "2.0", "1.5", "-0.5", "-2.0", "(1 / 0)"]);
|
||||
|
||||
if let Some(predicate) = predicate {
|
||||
builder = builder.option_w(predicate.in_op, 1.0);
|
||||
}
|
||||
|
||||
builder.build();
|
||||
|
||||
handle
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn logical_expression_fuzz_run() {
|
||||
let _ = env_logger::try_init();
|
||||
let g = GrammarGenerator::new();
|
||||
let builders = common_builders(&g, None);
|
||||
let expr = build_logical_expr(&g, &builders, None);
|
||||
|
||||
let sql = g
|
||||
.create()
|
||||
.concat(" ")
|
||||
.push_str("SELECT ")
|
||||
.push(expr)
|
||||
.build();
|
||||
|
||||
let db = TempDatabase::new_empty();
|
||||
let limbo_conn = db.connect_limbo();
|
||||
@@ -663,105 +858,33 @@ mod tests {
|
||||
pub fn table_logical_expression_fuzz_run() {
|
||||
let _ = env_logger::try_init();
|
||||
let g = GrammarGenerator::new();
|
||||
let (expr, expr_builder) = g.create_handle();
|
||||
let (value, value_builder) = g.create_handle();
|
||||
let (cmp_op, cmp_op_builder) = g.create_handle();
|
||||
let (bin_op, bin_op_builder) = g.create_handle();
|
||||
let (in_op, in_op_builder) = g.create_handle();
|
||||
let (paren, paren_builder) = g.create_handle();
|
||||
|
||||
let number = g
|
||||
.create()
|
||||
.choice()
|
||||
.option_symbol(rand_int(-0xff..0x100))
|
||||
.option_symbol(rand_int(-0xffff..0x10000))
|
||||
.option_symbol(rand_int(-0xffffff..0x1000000))
|
||||
.option_symbol(rand_int(-0xffffffff..0x100000000))
|
||||
.option_symbol(rand_int(-0xffffffffffff..0x1000000000000))
|
||||
.build();
|
||||
value_builder
|
||||
.choice()
|
||||
.option(
|
||||
g.create()
|
||||
.concat(" ")
|
||||
.push_str("(")
|
||||
.push(value)
|
||||
.push_str(")")
|
||||
.build(),
|
||||
)
|
||||
.option(number)
|
||||
.options_str(["x", "y", "z"])
|
||||
.option(
|
||||
g.create()
|
||||
.concat(" ")
|
||||
.push_str("(")
|
||||
.push(value)
|
||||
.push(g.create().choice().options_str(["+", "-"]).build())
|
||||
.push(value)
|
||||
.push_str(")")
|
||||
.build(),
|
||||
)
|
||||
.build();
|
||||
|
||||
paren_builder
|
||||
.concat("")
|
||||
.push_str("(")
|
||||
.push(expr)
|
||||
.push_str(")")
|
||||
.build();
|
||||
|
||||
cmp_op_builder
|
||||
.concat(" ")
|
||||
.push(value)
|
||||
.push(
|
||||
g.create()
|
||||
.choice()
|
||||
.options_str(["=", "<>", ">", "<", ">=", "<=", "IS", "IS NOT"])
|
||||
.build(),
|
||||
)
|
||||
.push(value)
|
||||
.build();
|
||||
|
||||
bin_op_builder
|
||||
.concat(" ")
|
||||
.push(expr)
|
||||
.push(g.create().choice().options_str(["AND", "OR"]).build())
|
||||
.push(expr)
|
||||
.build();
|
||||
|
||||
in_op_builder
|
||||
.concat(" ")
|
||||
.push(value)
|
||||
.push(g.create().choice().options_str(["IN", "NOT IN"]).build())
|
||||
.push_str("(")
|
||||
.push(g.create().concat("").push(value).repeat(1..5, ", ").build())
|
||||
.push_str(")")
|
||||
.build();
|
||||
|
||||
expr_builder
|
||||
.choice()
|
||||
.options_str(["1", "0"])
|
||||
.option_w(paren, 10.0)
|
||||
.option_w(cmp_op, 10.0)
|
||||
.option_w(bin_op, 10.0)
|
||||
.option_w(in_op, 10.0)
|
||||
.build();
|
||||
let tables = vec![TestTable {
|
||||
name: "t",
|
||||
columns: vec!["x", "y", "z"],
|
||||
}];
|
||||
let builders = common_builders(&g, Some(&tables));
|
||||
let predicate = predicate_builders(&g, Some(&tables));
|
||||
let expr = build_logical_expr(&g, &builders, Some(&predicate));
|
||||
|
||||
let db = TempDatabase::new_empty();
|
||||
let limbo_conn = db.connect_limbo();
|
||||
let sqlite_conn = rusqlite::Connection::open_in_memory().unwrap();
|
||||
assert_eq!(
|
||||
limbo_exec_rows(&db, &limbo_conn, "CREATE TABLE t(x, y, z)"),
|
||||
sqlite_exec_rows(&sqlite_conn, "CREATE TABLE t(x, y, z)")
|
||||
);
|
||||
for table in tables.iter() {
|
||||
let query = format!("CREATE TABLE {} ({})", table.name, table.columns.join(", "));
|
||||
assert_eq!(
|
||||
limbo_exec_rows(&db, &limbo_conn, &query),
|
||||
sqlite_exec_rows(&sqlite_conn, &query)
|
||||
);
|
||||
}
|
||||
|
||||
let (mut rng, seed) = rng_from_time();
|
||||
log::info!("seed: {}", seed);
|
||||
|
||||
for _ in 0..100 {
|
||||
let (x, y, z) = (
|
||||
g.generate(&mut rng, number, 1),
|
||||
g.generate(&mut rng, number, 1),
|
||||
g.generate(&mut rng, number, 1),
|
||||
g.generate(&mut rng, builders.number, 1),
|
||||
g.generate(&mut rng, builders.number, 1),
|
||||
g.generate(&mut rng, builders.number, 1),
|
||||
);
|
||||
let query = format!("INSERT INTO t VALUES ({}, {}, {})", x, y, z);
|
||||
log::info!("insert: {}", query);
|
||||
@@ -778,7 +901,7 @@ mod tests {
|
||||
.push(expr)
|
||||
.build();
|
||||
|
||||
for _ in 0..128 {
|
||||
for _ in 0..1024 {
|
||||
let query = g.generate(&mut rng, sql, 50);
|
||||
log::info!("query: {}", query);
|
||||
let limbo = limbo_exec_rows(&db, &limbo_conn, &query);
|
||||
|
||||
Reference in New Issue
Block a user