From b93e01d59fe0cbaadf748617a3ea92fec843586a Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 16 Feb 2025 08:46:50 +0200 Subject: [PATCH 01/15] expr.rs: Cast: call translate_expr() from translate_condition_expr() --- core/translate/expr.rs | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 6553eecfe..bfc57a440 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -272,30 +272,11 @@ pub fn translate_condition_expr( } } } - ast::Expr::Literal(lit) => match lit { - ast::Literal::Numeric(val) => { - let maybe_int = val.parse::(); - 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::Literal(_) | ast::Expr::Cast { .. } => { + 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> From 7023ffc215511fc56e99174f8755b6aa434eb6f7 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 16 Feb 2025 08:59:07 +0200 Subject: [PATCH 02/15] expr.rs: FunctionCall: call translate_expr() from translate_condition_expr() --- core/translate/expr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index bfc57a440..59a9a7e0a 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -272,7 +272,7 @@ pub fn translate_condition_expr( } } } - ast::Expr::Literal(_) | ast::Expr::Cast { .. } => { + ast::Expr::Literal(_) | ast::Expr::Cast { .. } | ast::Expr::FunctionCall { .. } => { let reg = program.alloc_register(); translate_expr(program, Some(referenced_tables), expr, reg, resolver)?; emit_cond_jump(program, condition_metadata, reg); From d91ba9573beed70534c9e0e66eb8694991021e74 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 16 Feb 2025 08:59:34 +0200 Subject: [PATCH 03/15] expr.rs: Column: call translate_expr() from translate_condition_expr() --- core/translate/expr.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 59a9a7e0a..3b3650761 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -272,7 +272,10 @@ pub fn translate_condition_expr( } } } - ast::Expr::Literal(_) | ast::Expr::Cast { .. } | ast::Expr::FunctionCall { .. } => { + ast::Expr::Literal(_) + | ast::Expr::Cast { .. } + | ast::Expr::FunctionCall { .. } + | ast::Expr::Column { .. } => { let reg = program.alloc_register(); translate_expr(program, Some(referenced_tables), expr, reg, resolver)?; emit_cond_jump(program, condition_metadata, reg); From 4f384e3a0272524bbd9ceb8ba2c22646f2610f9c Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 16 Feb 2025 08:59:55 +0200 Subject: [PATCH 04/15] expr.rs: Rowid: call translate_expr() from translate_condition_expr() --- core/translate/expr.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 3b3650761..84b57b34e 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -275,7 +275,8 @@ pub fn translate_condition_expr( ast::Expr::Literal(_) | ast::Expr::Cast { .. } | ast::Expr::FunctionCall { .. } - | ast::Expr::Column { .. } => { + | ast::Expr::Column { .. } + | ast::Expr::RowId { .. } => { let reg = program.alloc_register(); translate_expr(program, Some(referenced_tables), expr, reg, resolver)?; emit_cond_jump(program, condition_metadata, reg); From c6b8100d64cb673b4509c45a26e022178d393275 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 16 Feb 2025 09:15:45 +0200 Subject: [PATCH 05/15] expr.rs: Case: call translate_expr() from translate_condition_expr() --- core/translate/expr.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 84b57b34e..bb5965fde 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -276,7 +276,8 @@ pub fn translate_condition_expr( | ast::Expr::Cast { .. } | ast::Expr::FunctionCall { .. } | ast::Expr::Column { .. } - | ast::Expr::RowId { .. } => { + | 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); From dc852fee8c97848f99b28f4bbf63e32450343071 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 16 Feb 2025 10:14:02 +0200 Subject: [PATCH 06/15] expr.rs: Like: use shared impl in translate_expr() and translate_condition_expr() --- core/translate/expr.rs | 109 +++++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index bb5965fde..156221c1f 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -396,49 +396,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 { @@ -1955,7 +1915,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::(); @@ -2145,6 +2119,55 @@ pub fn translate_expr( } } +fn translate_like_base( + program: &mut ProgramBuilder, + referenced_tables: Option<&[TableReference]>, + expr: &ast::Expr, + target_register: usize, + resolver: &Resolver, +) -> Result { + 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. From 28ad12699fab0735417a54d004c7edf28de91937 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 16 Feb 2025 10:29:34 +0200 Subject: [PATCH 07/15] expr.rs: Unary: use shared impl in translate_expr() and translate_condition_expr() --- core/translate/expr.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 156221c1f..e02ef73d6 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -446,7 +446,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(()) } From 9bf5b9609fffd7b0dfc36dfd1ebbf3ef79be6064 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 16 Feb 2025 11:26:58 +0200 Subject: [PATCH 08/15] expr.rs: Binary: use translate_expr()'s impl for currently unsupported ops in translate_condition_expr() --- core/translate/expr.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index e02ef73d6..da9b7a282 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -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,11 +279,14 @@ 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::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 { .. } From 447f91e5eedc1e1600ed370d0cc79522f0bd32b5 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 16 Feb 2025 11:27:38 +0200 Subject: [PATCH 09/15] optimizer.rs: remove constant folding optimization for NULL since it's incorrect --- core/translate/optimizer.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/core/translate/optimizer.rs b/core/translate/optimizer.rs index 971e7e790..7b765edb2 100644 --- a/core/translate/optimizer.rs +++ b/core/translate/optimizer.rs @@ -367,7 +367,6 @@ impl Optimizable for ast::Expr { fn check_constant(&self) -> Result> { 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::() { return Ok(Some(if int_value == 0 { From e07007896fababe42f36ebfb8d41c47b1f28c4d3 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 16 Feb 2025 11:37:13 +0200 Subject: [PATCH 10/15] tests/fuzz: use mostly the same expression options in select and predicate position --- tests/integration/fuzz/mod.rs | 338 +++++++++++++++++++++++----------- 1 file changed, 230 insertions(+), 108 deletions(-) diff --git a/tests/integration/fuzz/mod.rs b/tests/integration/fuzz/mod.rs index fdfcf6d0c..b10522557 100644 --- a/tests/integration/fuzz/mod.rs +++ b/tests/integration/fuzz/mod.rs @@ -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,185 @@ 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(["+", "-"]).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) + // .option_w(in_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 +851,39 @@ 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() { + assert_eq!( + limbo_exec_rows( + &db, + &limbo_conn, + &format!("CREATE TABLE {} ({})", table.name, table.columns.join(", ")) + ), + sqlite_exec_rows( + &sqlite_conn, + &format!("CREATE TABLE {} ({})", table.name, table.columns.join(", ")) + ) + ); + } + 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 +900,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); From 12242ad359e4958ff3aee1e63dd270e309353091 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sun, 16 Feb 2025 11:52:31 +0200 Subject: [PATCH 11/15] Add more TCL tests for exprs in select/where positions --- testing/select.test | 9 +++++ testing/where.test | 92 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/testing/select.test b/testing/select.test index 60f2e5bc2..f085ffe91 100755 --- a/testing/select.test +++ b/testing/select.test @@ -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} \ No newline at end of file diff --git a/testing/where.test b/testing/where.test index e67fd84c7..54a6b0eba 100755 --- a/testing/where.test +++ b/testing/where.test @@ -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} From bece5b601a3c35dddfb979cc11fcc5004c46271c Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Mon, 17 Feb 2025 10:55:26 +0200 Subject: [PATCH 12/15] Add comment about translate_like_base --- core/translate/expr.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index da9b7a282..247aa124a 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -2144,6 +2144,10 @@ 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]>, From 6940ca84bd79efe76b7648462709d7dd5d6de5af Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Mon, 17 Feb 2025 10:57:58 +0200 Subject: [PATCH 13/15] Add more column column binary op possibilities to fuzzer --- tests/integration/fuzz/mod.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/integration/fuzz/mod.rs b/tests/integration/fuzz/mod.rs index b10522557..529ec0f6c 100644 --- a/tests/integration/fuzz/mod.rs +++ b/tests/integration/fuzz/mod.rs @@ -643,7 +643,15 @@ mod tests { .concat(" ") .push_str("(") .push(column) - .push(g.create().choice().options_str(["+", "-"]).build()) + .push( + g.create() + .choice() + .options_str([ + "+", "-", "*", "/", "||", "=", "<>", ">", "<", ">=", "<=", "IS", + "IS NOT", + ]) + .build(), + ) .push(column) .push_str(")") .build(), From 7eaa3f4da0b433b45b4bcfbd6ef9fec458ab7fce Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Mon, 17 Feb 2025 10:58:38 +0200 Subject: [PATCH 14/15] Extract variable --- tests/integration/fuzz/mod.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/integration/fuzz/mod.rs b/tests/integration/fuzz/mod.rs index 529ec0f6c..e6c29d32d 100644 --- a/tests/integration/fuzz/mod.rs +++ b/tests/integration/fuzz/mod.rs @@ -871,16 +871,10 @@ mod tests { let limbo_conn = db.connect_limbo(); let sqlite_conn = rusqlite::Connection::open_in_memory().unwrap(); for table in tables.iter() { + let query = format!("CREATE TABLE {} ({})", table.name, table.columns.join(", ")); assert_eq!( - limbo_exec_rows( - &db, - &limbo_conn, - &format!("CREATE TABLE {} ({})", table.name, table.columns.join(", ")) - ), - sqlite_exec_rows( - &sqlite_conn, - &format!("CREATE TABLE {} ({})", table.name, table.columns.join(", ")) - ) + limbo_exec_rows(&db, &limbo_conn, &query), + sqlite_exec_rows(&sqlite_conn, &query) ); } From 55ff1d2061bc525cfb27e2cfe63810de0a8acc74 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Mon, 17 Feb 2025 10:59:04 +0200 Subject: [PATCH 15/15] remove comment --- tests/integration/fuzz/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/fuzz/mod.rs b/tests/integration/fuzz/mod.rs index e6c29d32d..7360fb863 100644 --- a/tests/integration/fuzz/mod.rs +++ b/tests/integration/fuzz/mod.rs @@ -690,7 +690,6 @@ mod tests { .option_w(cast_expr, 1.0) .option_w(case_expr, 1.0) .option_w(cmp_op, 1.0) - // .option_w(in_op, 1.0) .options_str(["1", "0", "NULL", "2.0", "1.5", "-0.5", "-2.0", "(1 / 0)"]) .build();