Skip to content

Commit 76ca4c7

Browse files
committed
Merge 'Fix not evaling constant conditions when no tables in query' from Jussi Saurio
This PR is extracted from the sqlite fuzzing exploration effort in #1021 --- We were not evaluating constant conditions (e.g '1 IS NULL') when there were no tables referenced in the query, because our WHERE term evaluation was based on "during which loop" to evaluate them. However, when there are no tables, there are no loops, so they were never evaluated. Closes #1023
2 parents b25429d + 8e5499e commit 76ca4c7

File tree

6 files changed

+178
-45
lines changed

6 files changed

+178
-45
lines changed

core/translate/emitter.rs

+18
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::vdbe::{insn::Insn, BranchOffset};
1111
use crate::{Result, SymbolTable};
1212

1313
use super::aggregation::emit_ungrouped_aggregation;
14+
use super::expr::{translate_condition_expr, ConditionMetadata};
1415
use super::group_by::{emit_group_by, init_group_by, GroupByMetadata};
1516
use super::main_loop::{close_loop, emit_loop, init_loop, open_loop, LeftJoinMetadata, LoopLabels};
1617
use super::order_by::{emit_order_by, init_order_by, SortMetadata};
@@ -241,6 +242,23 @@ pub fn emit_query<'a>(
241242
&OperationMode::SELECT,
242243
)?;
243244

245+
for where_term in plan.where_clause.iter().filter(|wt| wt.is_constant()) {
246+
let jump_target_when_true = program.allocate_label();
247+
let condition_metadata = ConditionMetadata {
248+
jump_if_condition_is_true: false,
249+
jump_target_when_false: after_main_loop_label,
250+
jump_target_when_true,
251+
};
252+
translate_condition_expr(
253+
program,
254+
&plan.table_references,
255+
&where_term.expr,
256+
condition_metadata,
257+
&mut t_ctx.resolver,
258+
)?;
259+
program.resolve_label(jump_target_when_true, program.offset());
260+
}
261+
244262
// Set up main query execution loop
245263
open_loop(program, t_ctx, &plan.table_references, &plan.where_clause)?;
246264

core/translate/main_loop.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ pub fn open_loop(
237237
// E.g. SELECT foo FROM (SELECT bar as foo FROM t1) sub WHERE sub.foo > 10
238238
for cond in predicates
239239
.iter()
240-
.filter(|cond| cond.eval_at_loop == table_index)
240+
.filter(|cond| cond.should_eval_at_loop(table_index))
241241
{
242242
let jump_target_when_true = program.allocate_label();
243243
let condition_metadata = ConditionMetadata {
@@ -311,7 +311,7 @@ pub fn open_loop(
311311

312312
for cond in predicates
313313
.iter()
314-
.filter(|cond| cond.eval_at_loop == table_index)
314+
.filter(|cond| cond.should_eval_at_loop(table_index))
315315
{
316316
let jump_target_when_true = program.allocate_label();
317317
let condition_metadata = ConditionMetadata {
@@ -483,7 +483,7 @@ pub fn open_loop(
483483
}
484484
for cond in predicates
485485
.iter()
486-
.filter(|cond| cond.eval_at_loop == table_index)
486+
.filter(|cond| cond.should_eval_at_loop(table_index))
487487
{
488488
let jump_target_when_true = program.allocate_label();
489489
let condition_metadata = ConditionMetadata {

core/translate/optimizer.rs

+7-7
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ pub fn try_extract_index_search_expression(
499499
table_reference: &TableReference,
500500
available_indexes: &HashMap<String, Vec<Rc<Index>>>,
501501
) -> Result<Option<Search>> {
502-
if cond.eval_at_loop != table_index {
502+
if !cond.should_eval_at_loop(table_index) {
503503
return Ok(None);
504504
}
505505
match &mut cond.expr {
@@ -512,7 +512,7 @@ pub fn try_extract_index_search_expression(
512512
cmp_expr: WhereTerm {
513513
expr: rhs_owned,
514514
from_outer_join: cond.from_outer_join,
515-
eval_at_loop: cond.eval_at_loop,
515+
eval_at: cond.eval_at,
516516
},
517517
}));
518518
}
@@ -526,7 +526,7 @@ pub fn try_extract_index_search_expression(
526526
cmp_expr: WhereTerm {
527527
expr: rhs_owned,
528528
from_outer_join: cond.from_outer_join,
529-
eval_at_loop: cond.eval_at_loop,
529+
eval_at: cond.eval_at,
530530
},
531531
}));
532532
}
@@ -542,7 +542,7 @@ pub fn try_extract_index_search_expression(
542542
cmp_expr: WhereTerm {
543543
expr: lhs_owned,
544544
from_outer_join: cond.from_outer_join,
545-
eval_at_loop: cond.eval_at_loop,
545+
eval_at: cond.eval_at,
546546
},
547547
}));
548548
}
@@ -556,7 +556,7 @@ pub fn try_extract_index_search_expression(
556556
cmp_expr: WhereTerm {
557557
expr: lhs_owned,
558558
from_outer_join: cond.from_outer_join,
559-
eval_at_loop: cond.eval_at_loop,
559+
eval_at: cond.eval_at,
560560
},
561561
}));
562562
}
@@ -580,7 +580,7 @@ pub fn try_extract_index_search_expression(
580580
cmp_expr: WhereTerm {
581581
expr: rhs_owned,
582582
from_outer_join: cond.from_outer_join,
583-
eval_at_loop: cond.eval_at_loop,
583+
eval_at: cond.eval_at,
584584
},
585585
}));
586586
}
@@ -604,7 +604,7 @@ pub fn try_extract_index_search_expression(
604604
cmp_expr: WhereTerm {
605605
expr: lhs_owned,
606606
from_outer_join: cond.from_outer_join,
607-
eval_at_loop: cond.eval_at_loop,
607+
eval_at: cond.eval_at,
608608
},
609609
}));
610610
}

core/translate/plan.rs

+44-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use core::fmt;
22
use limbo_sqlite3_parser::ast;
33
use std::{
4+
cmp::Ordering,
45
fmt::{Display, Formatter},
56
rc::Rc,
67
};
@@ -60,10 +61,49 @@ pub struct WhereTerm {
6061
/// regardless of which tables it references.
6162
/// We also cannot e.g. short circuit the entire query in the optimizer if the condition is statically false.
6263
pub from_outer_join: bool,
63-
/// The loop index where to evaluate the condition.
64-
/// For example, in `SELECT * FROM u JOIN p WHERE u.id = 5`, the condition can already be evaluated at the first loop (idx 0),
65-
/// because that is the rightmost table that it references.
66-
pub eval_at_loop: usize,
64+
pub eval_at: EvalAt,
65+
}
66+
67+
impl WhereTerm {
68+
pub fn is_constant(&self) -> bool {
69+
self.eval_at == EvalAt::BeforeLoop
70+
}
71+
72+
pub fn should_eval_at_loop(&self, loop_idx: usize) -> bool {
73+
self.eval_at == EvalAt::Loop(loop_idx)
74+
}
75+
}
76+
77+
/// The loop index where to evaluate the condition.
78+
/// For example, in `SELECT * FROM u JOIN p WHERE u.id = 5`, the condition can already be evaluated at the first loop (idx 0),
79+
/// because that is the rightmost table that it references.
80+
///
81+
/// Conditions like 1=2 can be evaluated before the main loop is opened, because they are constant.
82+
/// In theory we should be able to statically analyze them all and reduce them to a single boolean value,
83+
/// but that is not implemented yet.
84+
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
85+
pub enum EvalAt {
86+
Loop(usize),
87+
BeforeLoop,
88+
}
89+
90+
#[allow(clippy::non_canonical_partial_ord_impl)]
91+
impl PartialOrd for EvalAt {
92+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
93+
match (self, other) {
94+
(EvalAt::Loop(a), EvalAt::Loop(b)) => a.partial_cmp(b),
95+
(EvalAt::BeforeLoop, EvalAt::BeforeLoop) => Some(Ordering::Equal),
96+
(EvalAt::BeforeLoop, _) => Some(Ordering::Less),
97+
(_, EvalAt::BeforeLoop) => Some(Ordering::Greater),
98+
}
99+
}
100+
}
101+
102+
impl Ord for EvalAt {
103+
fn cmp(&self, other: &Self) -> Ordering {
104+
self.partial_cmp(other)
105+
.expect("total ordering not implemented for EvalAt")
106+
}
67107
}
68108

69109
/// A query plan is either a SELECT or a DELETE (for now)

core/translate/planner.rs

+98-31
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::{
22
plan::{
3-
Aggregate, JoinInfo, Operation, Plan, ResultSetColumn, SelectPlan, SelectQueryType,
3+
Aggregate, EvalAt, JoinInfo, Operation, Plan, ResultSetColumn, SelectPlan, SelectQueryType,
44
TableReference, WhereTerm,
55
},
66
select::prepare_select_plan,
@@ -534,11 +534,11 @@ pub fn parse_where(
534534
bind_column_references(expr, table_references, result_columns)?;
535535
}
536536
for expr in predicates {
537-
let eval_at_loop = get_rightmost_table_referenced_in_expr(&expr)?;
537+
let eval_at = determine_where_to_eval_expr(&expr)?;
538538
out_where_clause.push(WhereTerm {
539539
expr,
540540
from_outer_join: false,
541-
eval_at_loop,
541+
eval_at,
542542
});
543543
}
544544
Ok(())
@@ -548,54 +548,121 @@ pub fn parse_where(
548548
}
549549

550550
/**
551-
Returns the rightmost table index that is referenced in the given AST expression.
552-
Rightmost = innermost loop.
553-
This is used to determine where we should evaluate a given condition expression,
554-
and it needs to be the rightmost table referenced in the expression, because otherwise
555-
the condition would be evaluated before a row is read from that table.
551+
Returns the earliest point at which a WHERE term can be evaluated.
552+
For expressions referencing tables, this is the innermost loop that contains a row for each
553+
table referenced in the expression.
554+
For expressions not referencing any tables (e.g. constants), this is before the main loop is
555+
opened, because they do not need any table data.
556556
*/
557-
fn get_rightmost_table_referenced_in_expr<'a>(predicate: &'a ast::Expr) -> Result<usize> {
558-
let mut max_table_idx = 0;
557+
fn determine_where_to_eval_expr<'a>(predicate: &'a ast::Expr) -> Result<EvalAt> {
558+
let mut eval_at: EvalAt = EvalAt::BeforeLoop;
559559
match predicate {
560560
ast::Expr::Binary(e1, _, e2) => {
561-
max_table_idx = max_table_idx.max(get_rightmost_table_referenced_in_expr(e1)?);
562-
max_table_idx = max_table_idx.max(get_rightmost_table_referenced_in_expr(e2)?);
561+
eval_at = eval_at.max(determine_where_to_eval_expr(e1)?);
562+
eval_at = eval_at.max(determine_where_to_eval_expr(e2)?);
563563
}
564-
ast::Expr::Column { table, .. } => {
565-
max_table_idx = max_table_idx.max(*table);
564+
ast::Expr::Column { table, .. } | ast::Expr::RowId { table, .. } => {
565+
eval_at = eval_at.max(EvalAt::Loop(*table));
566566
}
567567
ast::Expr::Id(_) => {
568568
/* Id referring to column will already have been rewritten as an Expr::Column */
569569
/* we only get here with literal 'true' or 'false' etc */
570570
}
571571
ast::Expr::Qualified(_, _) => {
572-
unreachable!("Qualified should be resolved to a Column before optimizer")
572+
unreachable!("Qualified should be resolved to a Column before resolving eval_at")
573573
}
574574
ast::Expr::Literal(_) => {}
575575
ast::Expr::Like { lhs, rhs, .. } => {
576-
max_table_idx = max_table_idx.max(get_rightmost_table_referenced_in_expr(lhs)?);
577-
max_table_idx = max_table_idx.max(get_rightmost_table_referenced_in_expr(rhs)?);
576+
eval_at = eval_at.max(determine_where_to_eval_expr(lhs)?);
577+
eval_at = eval_at.max(determine_where_to_eval_expr(rhs)?);
578578
}
579579
ast::Expr::FunctionCall {
580580
args: Some(args), ..
581581
} => {
582582
for arg in args {
583-
max_table_idx = max_table_idx.max(get_rightmost_table_referenced_in_expr(arg)?);
583+
eval_at = eval_at.max(determine_where_to_eval_expr(arg)?);
584584
}
585585
}
586586
ast::Expr::InList { lhs, rhs, .. } => {
587-
max_table_idx = max_table_idx.max(get_rightmost_table_referenced_in_expr(lhs)?);
587+
eval_at = eval_at.max(determine_where_to_eval_expr(lhs)?);
588588
if let Some(rhs_list) = rhs {
589589
for rhs_expr in rhs_list {
590-
max_table_idx =
591-
max_table_idx.max(get_rightmost_table_referenced_in_expr(rhs_expr)?);
590+
eval_at = eval_at.max(determine_where_to_eval_expr(rhs_expr)?);
592591
}
593592
}
594593
}
595-
_ => {}
594+
Expr::Between {
595+
lhs, start, end, ..
596+
} => {
597+
eval_at = eval_at.max(determine_where_to_eval_expr(lhs)?);
598+
eval_at = eval_at.max(determine_where_to_eval_expr(start)?);
599+
eval_at = eval_at.max(determine_where_to_eval_expr(end)?);
600+
}
601+
Expr::Case {
602+
base,
603+
when_then_pairs,
604+
else_expr,
605+
} => {
606+
if let Some(base) = base {
607+
eval_at = eval_at.max(determine_where_to_eval_expr(base)?);
608+
}
609+
for (when, then) in when_then_pairs {
610+
eval_at = eval_at.max(determine_where_to_eval_expr(when)?);
611+
eval_at = eval_at.max(determine_where_to_eval_expr(then)?);
612+
}
613+
if let Some(else_expr) = else_expr {
614+
eval_at = eval_at.max(determine_where_to_eval_expr(else_expr)?);
615+
}
616+
}
617+
Expr::Cast { expr, .. } => {
618+
eval_at = eval_at.max(determine_where_to_eval_expr(expr)?);
619+
}
620+
Expr::Collate(expr, _) => {
621+
eval_at = eval_at.max(determine_where_to_eval_expr(expr)?);
622+
}
623+
Expr::DoublyQualified(_, _, _) => {
624+
unreachable!("DoublyQualified should be resolved to a Column before resolving eval_at")
625+
}
626+
Expr::Exists(_) => {
627+
todo!("exists not supported yet")
628+
}
629+
Expr::FunctionCall { args, .. } => {
630+
for arg in args.as_ref().unwrap_or(&vec![]).iter() {
631+
eval_at = eval_at.max(determine_where_to_eval_expr(arg)?);
632+
}
633+
}
634+
Expr::FunctionCallStar { .. } => {}
635+
Expr::InSelect { .. } => {
636+
todo!("in select not supported yet")
637+
}
638+
Expr::InTable { .. } => {
639+
todo!("in table not supported yet")
640+
}
641+
Expr::IsNull(expr) => {
642+
eval_at = eval_at.max(determine_where_to_eval_expr(expr)?);
643+
}
644+
Expr::Name(_) => {}
645+
Expr::NotNull(expr) => {
646+
eval_at = eval_at.max(determine_where_to_eval_expr(expr)?);
647+
}
648+
Expr::Parenthesized(exprs) => {
649+
for expr in exprs.iter() {
650+
eval_at = eval_at.max(determine_where_to_eval_expr(expr)?);
651+
}
652+
}
653+
Expr::Raise(_, _) => {
654+
todo!("raise not supported yet")
655+
}
656+
Expr::Subquery(_) => {
657+
todo!("subquery not supported yet")
658+
}
659+
Expr::Unary(_, expr) => {
660+
eval_at = eval_at.max(determine_where_to_eval_expr(expr)?);
661+
}
662+
Expr::Variable(_) => {}
596663
}
597664

598-
Ok(max_table_idx)
665+
Ok(eval_at)
599666
}
600667

601668
fn parse_join<'a>(
@@ -679,15 +746,15 @@ fn parse_join<'a>(
679746
}
680747
for pred in preds {
681748
let cur_table_idx = scope.tables.len() - 1;
682-
let eval_at_loop = if outer {
683-
cur_table_idx
749+
let eval_at = if outer {
750+
EvalAt::Loop(cur_table_idx)
684751
} else {
685-
get_rightmost_table_referenced_in_expr(&pred)?
752+
determine_where_to_eval_expr(&pred)?
686753
};
687754
out_where_clause.push(WhereTerm {
688755
expr: pred,
689756
from_outer_join: outer,
690-
eval_at_loop,
757+
eval_at,
691758
});
692759
}
693760
}
@@ -749,15 +816,15 @@ fn parse_join<'a>(
749816
is_rowid_alias: right_col.is_rowid_alias,
750817
}),
751818
);
752-
let eval_at_loop = if outer {
753-
cur_table_idx
819+
let eval_at = if outer {
820+
EvalAt::Loop(cur_table_idx)
754821
} else {
755-
get_rightmost_table_referenced_in_expr(&expr)?
822+
determine_where_to_eval_expr(&expr)?
756823
};
757824
out_where_clause.push(WhereTerm {
758825
expr,
759826
from_outer_join: outer,
760-
eval_at_loop,
827+
eval_at,
761828
});
762829
}
763830
using = Some(distinct_names);

testing/where.test

+8
Original file line numberDiff line numberDiff line change
@@ -564,3 +564,11 @@ do_execsql_test where-binary-bitwise-and {
564564
do_execsql_test where-binary-bitwise-or {
565565
select count(*) from users where 2 | 1;
566566
} {10000}
567+
568+
do_execsql_test where-constant-condition-no-tables {
569+
select 1 where 1 IS NULL;
570+
} {}
571+
572+
do_execsql_test where-constant-condition-no-tables-2 {
573+
select 1 where 1 IS NOT NULL;
574+
} {1}

0 commit comments

Comments
 (0)