diff --git a/COMPAT.md b/COMPAT.md index b4a276dc7..0bacfc0e3 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -68,6 +68,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 @@ -147,7 +174,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 2808d428c..0d84815a5 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -698,7 +698,102 @@ 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 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 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 { + 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 { + // 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, + rhs: expr_reg, + target_pc: next_case_label, + }, + 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, + target_pc: next_case_label, + null_reg: 1, + }, + next_case_label, + ), + }; + // THEN... + 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, + ); + // 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(); + } + 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.resolve_label(return_label, program.offset()); + 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 3028fef38..ca6309a11 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..f4c3b9232 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 '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 'zero' when 1 then 'one' else 'two' end; +} {one} + +do_execsql_test select_base_case_noelse_null { + select case 'null else' when 0 then 0 when 1 then 1 end; +} {}