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.
This commit is contained in:
Alex Miller
2024-12-08 14:03:45 -08:00
parent 9bc3ccc394
commit eb00226cfe
4 changed files with 131 additions and 3 deletions

View File

@@ -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 |

View File

@@ -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();

View File

@@ -599,6 +599,7 @@ impl ProgramState {
}
}
#[derive(Debug)]
pub struct Program {
pub max_registers: usize,
pub insns: Vec<Insn>,

View File

@@ -57,4 +57,20 @@ do_execsql_test seekrowid {
do_execsql_test select_parenthesized {
select (price + 100) from products limit 1;
} {179.0}
} {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;
} {}