From eb00226cfe9ddae7e6f3c6e6997deca050edc6b1 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Sun, 8 Dec 2024 14:03:45 -0800 Subject: [PATCH 1/4] Add support for CASE expressions. There's two forms of case: CASE (WHEN [bool expr] THEN [value])+ (ELSE [value])? END which checks a series of boolean conditions, and: CASE expr (WHEN [expr] THEN [value})+ (ELSE [value])? END Which checks a series of equality conditions. This implements support for both. Note that the ELSE is optional, and will be equivalent to `ELSE null` if not specified. sqlite3 gives the implementation as: sqlite> explain select case a WHEN a THEN b WHEN c THEN d ELSE 0 END from casetest; addr opcode p1 p2 p3 p4 p5 comment ---- ------------- ---- ---- ---- ------------- -- ------------- 0 Init 0 16 0 0 Start at 16 1 OpenRead 0 3 0 4 0 root=3 iDb=0; casetest 2 Rewind 0 15 0 0 3 Column 0 0 2 0 r[2]= cursor 0 column 0 4 Column 0 0 3 0 r[3]= cursor 0 column 0 5 Ne 3 8 2 BINARY-8 83 if r[2]!=r[3] goto 8 6 Column 0 1 1 0 r[1]= cursor 0 column 1 7 Goto 0 13 0 0 8 Column 0 2 3 0 r[3]= cursor 0 column 2 9 Ne 3 12 2 BINARY-8 83 if r[2]!=r[3] goto 12 10 Column 0 3 1 0 r[1]= cursor 0 column 3 11 Goto 0 13 0 0 12 Integer 0 1 0 0 r[1]=0 13 ResultRow 1 1 0 0 output=r[1] 14 Next 0 3 0 1 15 Halt 0 0 0 0 16 Transaction 0 0 2 0 1 usesStmtJournal=0 17 Goto 0 1 0 0 and after this patch, limbo gives: addr opcode p1 p2 p3 p4 p5 comment ---- ----------------- ---- ---- ---- ------------- -- ------- 0 Init 0 18 0 0 Start at 18 1 OpenReadAsync 0 4 0 0 table=casetest, root=4 2 OpenReadAwait 0 0 0 0 3 RewindAsync 0 0 0 0 4 RewindAwait 0 17 0 0 Rewind table casetest 5 Column 0 0 2 0 r[2]=casetest.a 6 Column 0 0 3 0 r[3]=casetest.a 7 Ne 2 3 10 0 if r[2]!=r[3] goto 10 8 Column 0 1 1 0 r[1]=casetest.b 9 Goto 0 14 0 0 10 Column 0 2 3 0 r[3]=casetest.c 11 Ne 2 3 14 0 if r[2]!=r[3] goto 14 12 Column 0 3 1 0 r[1]=casetest.d 13 Goto 0 14 0 0 14 ResultRow 1 1 0 0 output=r[1] 15 NextAsync 0 0 0 0 16 NextAwait 0 5 0 0 17 Halt 0 0 0 0 18 Transaction 0 0 0 0 19 Integer 0 1 0 0 r[1]=0 20 Goto 0 1 0 0 And then as there's nowhere to annotate this new support in COMPAT.md, I added a corresponding heading for SELECT expressions and what is/isn't supported. --- COMPAT.md | 28 +++++++++++++- core/translate/expr.rs | 87 +++++++++++++++++++++++++++++++++++++++++- core/vdbe/mod.rs | 1 + testing/select.test | 18 ++++++++- 4 files changed, 131 insertions(+), 3 deletions(-) diff --git a/COMPAT.md b/COMPAT.md index e909c794e..0de0b7ff7 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -60,6 +60,33 @@ This document describes the SQLite compatibility status of Limbo: | VACUUM | No | | | WITH clause | No | | +### SELECT Expressions + +Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html). + +| Syntax | Status | Comment | +|------------------------------|---------|---------| +| literals | Yes | | +| schema.table.column | Partial | Schemas aren't supported | +| unary operator | Partial | `-` supported, `+~` aren't | +| binary operator | Partial | Only `%`, `!<`, and `!>` are unsupported | +| agg() FILTER (WHERE ...) | No | Is incorrectly ignored | +| ... OVER (...) | No | Is incorrectly ignored | +| (expr) | Yes | | +| CAST (expr AS type) | Yes | | +| COLLATE | No | | +| (NOT) LIKE | No | | +| (NOT) GLOB | No | | +| (NOT) REGEXP | No | | +| (NOT) MATCH | No | | +| IS (NOT) | No | | +| IS (NOT) DISTINCT FROM | No | | +| (NOT) BETWEEN ... AND ... | No | | +| (NOT) IN (subquery) | No | | +| (NOT) EXISTS (subquery) | No | | +| CASE WHEN THEN ELSE END | Yes | | +| RAISE | No | | + ## SQL functions ### Scalar functions @@ -139,7 +166,6 @@ This document describes the SQLite compatibility status of Limbo: | sum(X) | Yes | | | total(X) | Yes | | - ### Date and time functions | Function | Status | Comment | diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 6c0b4437d..e9bb68113 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -698,7 +698,92 @@ pub fn translate_expr( } Ok(target_register) } - ast::Expr::Case { .. } => todo!(), + ast::Expr::Case { + base, + when_then_pairs, + else_expr, + } => { + // There's two forms of CASE, one which does quality against the then values: + // CASE [expr] WHEN [value] ELSE [value] END + // And one which evaluates a series of boolean predicates: + // CASE (WHEN [bool_expr] THEN [value])+ END + let return_label = program.allocate_label(); + let mut next_case_label = program.allocate_label(); + let base_reg = base.as_ref().map(|_| program.alloc_register()); + let expr_reg = program.alloc_register(); + if let Some(base_expr) = base { + translate_expr( + program, + referenced_tables, + base_expr, + base_reg.unwrap(), + precomputed_exprs_to_registers, + )?; + }; + for (when_expr, then_expr) in when_then_pairs { + translate_expr( + program, + referenced_tables, + when_expr, + expr_reg, + precomputed_exprs_to_registers, + )?; + match base_reg { + Some(base_reg) => program.emit_insn_with_label_dependency( + Insn::Ne { + lhs: base_reg, + rhs: expr_reg, + target_pc: next_case_label, + }, + next_case_label, + ), + None => program.emit_insn_with_label_dependency( + Insn::IfNot { + reg: expr_reg, + target_pc: next_case_label, + null_reg: 1, + }, + next_case_label, + ), + }; + translate_expr( + program, + referenced_tables, + then_expr, + target_register, + precomputed_exprs_to_registers, + )?; + program.emit_insn_with_label_dependency( + Insn::Goto { + target_pc: return_label, + }, + return_label, + ); + program.preassign_label_to_next_insn(next_case_label); + next_case_label = program.allocate_label(); + } + match else_expr { + Some(expr) => { + translate_expr( + program, + referenced_tables, + expr, + target_register, + precomputed_exprs_to_registers, + )?; + } + // If ELSE isn't specified, it means ELSE null + None => { + program.emit_insn(Insn::Null { + dest: target_register, + dest_end: None, + }); + } + }; + program.mark_last_insn_constant(); + program.preassign_label_to_next_insn(return_label); + Ok(target_register) + } ast::Expr::Cast { expr, type_name } => { let type_name = type_name.as_ref().unwrap(); // TODO: why is this optional? let reg_expr = program.alloc_register(); diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 906f43f73..36ddc53b4 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -599,6 +599,7 @@ impl ProgramState { } } +#[derive(Debug)] pub struct Program { pub max_registers: usize, pub insns: Vec, diff --git a/testing/select.test b/testing/select.test index 58c30e663..3795a77cf 100755 --- a/testing/select.test +++ b/testing/select.test @@ -57,4 +57,20 @@ do_execsql_test seekrowid { do_execsql_test select_parenthesized { select (price + 100) from products limit 1; -} {179.0} \ No newline at end of file +} {179.0} + +do_execsql_test select_case_base_else { + select case when 0 then 0 when 1 then 1 else 2 end; +} {1} + +do_execsql_test select_case_noelse_null { + select case when 0 then 0 end; +} {} + +do_execsql_test select_base_case_else { + select case 1 when 0 then 0 when 1 then 1 else 2 end; +} {1} + +do_execsql_test select_base_case_noelse_null { + select case 4 when 0 then 0 when 1 then 1 end; +} {} From c2e3957d738b3c9e105330fd77a42f57be51a538 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Sun, 8 Dec 2024 14:12:45 -0800 Subject: [PATCH 2/4] I misunderstood what a constant instruction was --- core/translate/expr.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index e9bb68113..4be4417cd 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -780,7 +780,6 @@ pub fn translate_expr( }); } }; - program.mark_last_insn_constant(); program.preassign_label_to_next_insn(return_label); Ok(target_register) } From f7bb7f8deeca781bc73d30955d5fcc7fe0395bd5 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Sun, 8 Dec 2024 14:20:23 -0800 Subject: [PATCH 3/4] Fix typo and improve comment --- core/translate/expr.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 4be4417cd..0ac834489 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -703,10 +703,12 @@ pub fn translate_expr( when_then_pairs, else_expr, } => { - // There's two forms of CASE, one which does quality against the then values: + // There's two forms of CASE, one which does quality against the when values: // CASE [expr] WHEN [value] ELSE [value] END // And one which evaluates a series of boolean predicates: // CASE (WHEN [bool_expr] THEN [value])+ END + // This just changes which sort of branching instruction to issue, after we + // generate the expression if needed. let return_label = program.allocate_label(); let mut next_case_label = program.allocate_label(); let base_reg = base.as_ref().map(|_| program.alloc_register()); From 88c862ce4d70a97997593b897521ae46fa066b04 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Tue, 10 Dec 2024 19:59:54 -0800 Subject: [PATCH 4/4] Comments, resolve label better, make tests more fun --- core/translate/expr.rs | 19 ++++++++++++++----- testing/select.test | 10 +++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 0ac834489..28636c18c 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -703,14 +703,18 @@ pub fn translate_expr( when_then_pairs, else_expr, } => { - // There's two forms of CASE, one which does quality against the when values: - // CASE [expr] WHEN [value] ELSE [value] END + // There's two forms of CASE, one which checks a base expression for equality + // against the WHEN values, and returns the corresponding THEN value if it matches: + // CASE 2 WHEN 1 THEN 'one' WHEN 2 THEN 'two' ELSE 'many' END // And one which evaluates a series of boolean predicates: - // CASE (WHEN [bool_expr] THEN [value])+ END + // CASE WHEN is_good THEN 'good' WHEN is_bad THEN 'bad' ELSE 'okay' END // This just changes which sort of branching instruction to issue, after we // generate the expression if needed. let return_label = program.allocate_label(); let mut next_case_label = program.allocate_label(); + // Only allocate a reg to hold the base expression if one was provided. + // And base_reg then becomes the flag we check to see which sort of + // case statement we're processing. let base_reg = base.as_ref().map(|_| program.alloc_register()); let expr_reg = program.alloc_register(); if let Some(base_expr) = base { @@ -731,6 +735,7 @@ pub fn translate_expr( precomputed_exprs_to_registers, )?; match base_reg { + // CASE 1 WHEN 0 THEN 0 ELSE 1 becomes 1==0, Ne branch to next clause Some(base_reg) => program.emit_insn_with_label_dependency( Insn::Ne { lhs: base_reg, @@ -739,6 +744,7 @@ pub fn translate_expr( }, next_case_label, ), + // CASE WHEN 0 THEN 0 ELSE 1 becomes ifnot 0 branch to next clause None => program.emit_insn_with_label_dependency( Insn::IfNot { reg: expr_reg, @@ -748,6 +754,7 @@ pub fn translate_expr( next_case_label, ), }; + // THEN... translate_expr( program, referenced_tables, @@ -761,6 +768,8 @@ pub fn translate_expr( }, return_label, ); + // This becomes either the next WHEN, or in the last WHEN/THEN, we're + // assured to have at least one instruction corresponding to the ELSE immediately follow. program.preassign_label_to_next_insn(next_case_label); next_case_label = program.allocate_label(); } @@ -774,7 +783,7 @@ pub fn translate_expr( precomputed_exprs_to_registers, )?; } - // If ELSE isn't specified, it means ELSE null + // If ELSE isn't specified, it means ELSE null. None => { program.emit_insn(Insn::Null { dest: target_register, @@ -782,7 +791,7 @@ pub fn translate_expr( }); } }; - program.preassign_label_to_next_insn(return_label); + program.resolve_label(return_label, program.offset()); Ok(target_register) } ast::Expr::Cast { expr, type_name } => { diff --git a/testing/select.test b/testing/select.test index 3795a77cf..f4c3b9232 100755 --- a/testing/select.test +++ b/testing/select.test @@ -60,17 +60,17 @@ do_execsql_test select_parenthesized { } {179.0} do_execsql_test select_case_base_else { - select case when 0 then 0 when 1 then 1 else 2 end; -} {1} + select case when 0 then 'false' when 1 then 'true' else 'null' end; +} {true} do_execsql_test select_case_noelse_null { select case when 0 then 0 end; } {} do_execsql_test select_base_case_else { - select case 1 when 0 then 0 when 1 then 1 else 2 end; -} {1} + select case 1 when 0 then 'zero' when 1 then 'one' else 'two' end; +} {one} do_execsql_test select_base_case_noelse_null { - select case 4 when 0 then 0 when 1 then 1 end; + select case 'null else' when 0 then 0 when 1 then 1 end; } {}