Merge 'Fix null compare operations not giving null' from Vrishabh

In limbo when we do any compare operations like `Eq, gt, lt, gte, lte`
with nulls , we were actually giving the result as true where as sqlite3
gives null. This is because if we had a null, we were incorrectly going
to conditional branch and not increment program by 1. Also the sqlite
generates `ZeroOrNull` op in these cases
(https://github.com/sqlite/sqlite/blob/version-3.45.3/src/expr.c#L4644)
but we were generating a Integer instruction. The below outputs can give
a clearer picture.
This PR aims to fix this.
sqlite3 output
```
SQLite version 3.48.0
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> select 8 = null;

sqlite> select 8 > null;

sqlite> explain select 8 > null;
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     6     0                    0
1     Integer        1     1     0                    0
2     Gt             3     4     2                    64
3     ZeroOrNull     2     1     3                    0
4     ResultRow      1     1     0                    0
5     Halt           0     0     0                    0
6     Integer        8     2     0                    0
7     Null           0     3     0                    0
8     Goto           0     1     0                    0
sqlite> explain select 8 = null;
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     6     0                    0
1     Integer        1     1     0                    0
2     Eq             3     4     2                    64
3     ZeroOrNull     2     1     3                    0
4     ResultRow      1     1     0                    0
5     Halt           0     0     0                    0
6     Integer        8     2     0                    0
7     Null           0     3     0                    0
8     Goto           0     1     0                    0
```
Limbo Output
```
Limbo v0.0.12
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database
limbo> select 8 = null;
1
limbo> select 8 > null;
1
limbo> explain select 8 > null;
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     8     0                    0   Start at 8
1     Integer            8     2     0                    0   r[2]=8
2     Null               0     3     0                    0   r[3]=NULL
3     Integer            1     1     0                    0   r[1]=1
4     Gt                 2     3     6                    0   if r[2]>r[3] goto 6
5     Integer            0     1     0                    0   r[1]=0
6     ResultRow          1     1     0                    0   output=r[1]
7     Halt               0     0     0                    0
8     Transaction        0     0     0                    0
9     Goto               0     1     0                    0
limbo> explain select 8 = null;
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     8     0                    0   Start at 8
1     Integer            8     2     0                    0   r[2]=8
2     Null               0     3     0                    0   r[3]=NULL
3     Integer            1     1     0                    0   r[1]=1
4     Eq                 2     3     6                    0   if r[2]==r[3] goto 6
5     Integer            0     1     0                    0   r[1]=0
6     ResultRow          1     1     0                    0   output=r[1]
7     Halt               0     0     0                    0
8     Transaction        0     0     0                    0
9     Goto               0     1     0                    0
limbo>
```
Limbo Output with this PR
```
Limbo v0.0.12
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database
limbo> select 8 = null;

limbo> select 8 > null;

limbo> explain select 8 > null;
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     8     0                    0   Start at 8
1     Integer            8     2     0                    0   r[2]=8
2     Null               0     3     0                    0   r[3]=NULL
3     Integer            1     1     0                    0   r[1]=1
4     Gt                 2     3     6                    0   if r[2]>r[3] goto 6
5     ZeroOrNull         2     1     3                    0   ((r[2]=NULL)|(r[3]=NULL)) ? r[1]=NULL : r[1]=0
6     ResultRow          1     1     0                    0   output=r[1]
7     Halt               0     0     0                    0
8     Transaction        0     0     0                    0
9     Goto               0     1     0                    0
limbo>  explain select 8 = null;
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     8     0                    0   Start at 8
1     Integer            8     2     0                    0   r[2]=8
2     Null               0     3     0                    0   r[3]=NULL
3     Integer            1     1     0                    0   r[1]=1
4     Eq                 2     3     6                    0   if r[2]==r[3] goto 6
5     ZeroOrNull         2     1     3                    0   ((r[2]=NULL)|(r[3]=NULL)) ? r[1]=NULL : r[1]=0
6     ResultRow          1     1     0                    0   output=r[1]
7     Halt               0     0     0                    0
8     Transaction        0     0     0                    0
9     Goto               0     1     0                    0
```

Closes #733
This commit is contained in:
Pekka Enberg
2025-01-19 09:09:12 +02:00
6 changed files with 147 additions and 12 deletions

View File

@@ -483,6 +483,7 @@ Modifiers:
| Variable | No |
| VerifyCookie | No |
| Yield | Yes |
| ZeroOrNull | Yes |
## Extensions

View File

@@ -453,7 +453,7 @@ pub fn translate_expr(
match op {
ast::Operator::NotEquals => {
let if_true_label = program.allocate_label();
wrap_eval_jump_expr(
wrap_eval_jump_expr_zero_or_null(
program,
Insn::Ne {
lhs: e1_reg,
@@ -462,11 +462,13 @@ pub fn translate_expr(
},
target_register,
if_true_label,
e1_reg,
e2_reg,
);
}
ast::Operator::Equals => {
let if_true_label = program.allocate_label();
wrap_eval_jump_expr(
wrap_eval_jump_expr_zero_or_null(
program,
Insn::Eq {
lhs: e1_reg,
@@ -475,11 +477,13 @@ pub fn translate_expr(
},
target_register,
if_true_label,
e1_reg,
e2_reg,
);
}
ast::Operator::Less => {
let if_true_label = program.allocate_label();
wrap_eval_jump_expr(
wrap_eval_jump_expr_zero_or_null(
program,
Insn::Lt {
lhs: e1_reg,
@@ -488,11 +492,13 @@ pub fn translate_expr(
},
target_register,
if_true_label,
e1_reg,
e2_reg,
);
}
ast::Operator::LessEquals => {
let if_true_label = program.allocate_label();
wrap_eval_jump_expr(
wrap_eval_jump_expr_zero_or_null(
program,
Insn::Le {
lhs: e1_reg,
@@ -501,11 +507,13 @@ pub fn translate_expr(
},
target_register,
if_true_label,
e1_reg,
e2_reg,
);
}
ast::Operator::Greater => {
let if_true_label = program.allocate_label();
wrap_eval_jump_expr(
wrap_eval_jump_expr_zero_or_null(
program,
Insn::Gt {
lhs: e1_reg,
@@ -514,11 +522,13 @@ pub fn translate_expr(
},
target_register,
if_true_label,
e1_reg,
e2_reg,
);
}
ast::Operator::GreaterEquals => {
let if_true_label = program.allocate_label();
wrap_eval_jump_expr(
wrap_eval_jump_expr_zero_or_null(
program,
Insn::Ge {
lhs: e1_reg,
@@ -527,6 +537,8 @@ pub fn translate_expr(
},
target_register,
if_true_label,
e1_reg,
e2_reg,
);
}
ast::Operator::Add => {
@@ -1796,6 +1808,27 @@ fn wrap_eval_jump_expr(
program.preassign_label_to_next_insn(if_true_label);
}
fn wrap_eval_jump_expr_zero_or_null(
program: &mut ProgramBuilder,
insn: Insn,
target_register: usize,
if_true_label: BranchOffset,
e1_reg: usize,
e2_reg: usize,
) {
program.emit_insn(Insn::Integer {
value: 1, // emit True by default
dest: target_register,
});
program.emit_insn(insn);
program.emit_insn(Insn::ZeroOrNull {
rg1: e1_reg,
rg2: e2_reg,
dest: target_register,
});
program.preassign_label_to_next_insn(if_true_label);
}
pub fn maybe_apply_affinity(col_type: Type, target_register: usize, program: &mut ProgramBuilder) {
if col_type == crate::schema::Type::Real {
program.emit_insn(Insn::RealAffinity {

View File

@@ -1071,6 +1071,18 @@ pub fn insn_to_str(
0,
format!("r[{}]=parameter({})", *dest, *index),
),
Insn::ZeroOrNull { rg1, rg2, dest } => (
"ZeroOrNull",
*rg1 as i32,
*dest as i32,
*rg2 as i32,
OwnedValue::build_text(Rc::new("".to_string())),
0,
format!(
"((r[{}]=NULL)|(r[{}]=NULL)) ? r[{}]=NULL : r[{}]=0",
rg1, rg2, dest, dest
),
),
};
format!(
"{:<4} {:<17} {:<4} {:<4} {:<4} {:<13} {:<2} {}",

View File

@@ -509,6 +509,13 @@ pub enum Insn {
index: NonZero<usize>,
dest: usize,
},
/// If either register is null put null else put 0
ZeroOrNull {
/// Source register (P1).
rg1: usize,
rg2: usize,
dest: usize,
},
}
fn cast_text_to_numerical(value: &str) -> OwnedValue {

View File

@@ -495,7 +495,7 @@ impl Program {
let target_pc = *target_pc;
match (&state.registers[lhs], &state.registers[rhs]) {
(_, OwnedValue::Null) | (OwnedValue::Null, _) => {
state.pc = target_pc.to_offset_int();
state.pc += 1;
}
_ => {
if state.registers[lhs] == state.registers[rhs] {
@@ -517,7 +517,7 @@ impl Program {
let target_pc = *target_pc;
match (&state.registers[lhs], &state.registers[rhs]) {
(_, OwnedValue::Null) | (OwnedValue::Null, _) => {
state.pc = target_pc.to_offset_int();
state.pc += 1;
}
_ => {
if state.registers[lhs] != state.registers[rhs] {
@@ -539,7 +539,7 @@ impl Program {
let target_pc = *target_pc;
match (&state.registers[lhs], &state.registers[rhs]) {
(_, OwnedValue::Null) | (OwnedValue::Null, _) => {
state.pc = target_pc.to_offset_int();
state.pc += 1;
}
_ => {
if state.registers[lhs] < state.registers[rhs] {
@@ -561,7 +561,7 @@ impl Program {
let target_pc = *target_pc;
match (&state.registers[lhs], &state.registers[rhs]) {
(_, OwnedValue::Null) | (OwnedValue::Null, _) => {
state.pc = target_pc.to_offset_int();
state.pc += 1;
}
_ => {
if state.registers[lhs] <= state.registers[rhs] {
@@ -583,7 +583,7 @@ impl Program {
let target_pc = *target_pc;
match (&state.registers[lhs], &state.registers[rhs]) {
(_, OwnedValue::Null) | (OwnedValue::Null, _) => {
state.pc = target_pc.to_offset_int();
state.pc += 1;
}
_ => {
if state.registers[lhs] > state.registers[rhs] {
@@ -605,7 +605,7 @@ impl Program {
let target_pc = *target_pc;
match (&state.registers[lhs], &state.registers[rhs]) {
(_, OwnedValue::Null) | (OwnedValue::Null, _) => {
state.pc = target_pc.to_offset_int();
state.pc += 1;
}
_ => {
if state.registers[lhs] >= state.registers[rhs] {
@@ -2255,6 +2255,16 @@ impl Program {
.clone();
state.pc += 1;
}
Insn::ZeroOrNull { rg1, rg2, dest } => {
if state.registers[*rg1] == OwnedValue::Null
|| state.registers[*rg2] == OwnedValue::Null
{
state.registers[*dest] = OwnedValue::Null
} else {
state.registers[*dest] = OwnedValue::Integer(0);
}
state.pc += 1;
}
}
}
}

View File

@@ -6,6 +6,7 @@ source $testdir/tester.tcl
foreach {testname lhs rhs ans} {
int-int-1 8 1 0
int-int-2 8 8 1
int-null 8 NULL {}
} {
do_execsql_test compare-eq-$testname "SELECT $lhs = $rhs" $::ans
}
@@ -13,6 +14,7 @@ foreach {testname lhs rhs ans} {
foreach {testname lhs rhs ans} {
float-float-1 8.0 1.0 0
float-float-2 8.0 8.0 1
float-null 8.0 NULL {}
} {
do_execsql_test compare-eq-$testname "SELECT $lhs = $rhs" $::ans
}
@@ -20,6 +22,16 @@ foreach {testname lhs rhs ans} {
foreach {testname lhs rhs ans} {
text-text-1 'a' 'b' 0
text-text-2 'a' 'a' 1
text-null 'a' NULL {}
} {
do_execsql_test compare-eq-$testname "SELECT $lhs = $rhs" $::ans
}
foreach {testname lhs rhs ans} {
null-int NULL 1 {}
null-float NULL 1.0 {}
null-text NULL 'a' {}
null-null NULL NULL {}
} {
do_execsql_test compare-eq-$testname "SELECT $lhs = $rhs" $::ans
}
@@ -27,6 +39,7 @@ foreach {testname lhs rhs ans} {
foreach {testname lhs rhs ans} {
int-int-1 8 1 1
int-int-2 8 8 0
int-null 8 NULL {}
} {
do_execsql_test compare-neq-$testname "SELECT $lhs <> $rhs" $::ans
}
@@ -34,6 +47,7 @@ foreach {testname lhs rhs ans} {
foreach {testname lhs rhs ans} {
float-float-1 8.0 1.0 1
float-float-2 8.0 8.0 0
float-null 8.0 NULL {}
} {
do_execsql_test compare-neq-$testname "SELECT $lhs <> $rhs" $::ans
}
@@ -41,6 +55,16 @@ foreach {testname lhs rhs ans} {
foreach {testname lhs rhs ans} {
text-text-1 'a' 'b' 1
text-text-2 'a' 'a' 0
text-null 'a' NULL {}
} {
do_execsql_test compare-neq-$testname "SELECT $lhs <> $rhs" $::ans
}
foreach {testname lhs rhs ans} {
null-int NULL 1 {}
null-float NULL 1.0 {}
null-text NULL 'a' {}
null-null NULL NULL {}
} {
do_execsql_test compare-neq-$testname "SELECT $lhs <> $rhs" $::ans
}
@@ -49,6 +73,7 @@ foreach {testname lhs rhs ans} {
int-int-1 1 8 0
int-int-2 1 1 0
int-int-3 8 0 1
int-null 8 NULL {}
} {
do_execsql_test compare-gt-$testname "SELECT $lhs > $rhs" $::ans
}
@@ -57,6 +82,7 @@ foreach {testname lhs rhs ans} {
float-float-1 1.0 2.0 0
float-float-2 1.0 1.0 0
float-float-3 7.0 6.0 1
float-null 8.0 NULL {}
} {
do_execsql_test compare-gt-$testname "SELECT $lhs > $rhs" $::ans
}
@@ -65,6 +91,16 @@ foreach {testname lhs rhs ans} {
text-text-1 'b' 'c' 0
text-text-2 'b' 'b' 0
text-text-3 'b' 'a' 1
text-null 'a' NULL {}
} {
do_execsql_test compare-gt-$testname "SELECT $lhs > $rhs" $::ans
}
foreach {testname lhs rhs ans} {
null-int NULL 1 {}
null-float NULL 1.0 {}
null-text NULL 'a' {}
null-null NULL NULL {}
} {
do_execsql_test compare-gt-$testname "SELECT $lhs > $rhs" $::ans
}
@@ -73,6 +109,7 @@ foreach {testname lhs rhs ans} {
int-int-1 1 8 0
int-int-2 1 1 1
int-int-3 8 0 1
int-null 8 NULL {}
} {
do_execsql_test compare-gte-$testname "SELECT $lhs >= $rhs" $::ans
}
@@ -81,6 +118,7 @@ foreach {testname lhs rhs ans} {
float-float-1 1.0 2.0 0
float-float-2 1.0 1.0 1
float-float-3 7.0 6.0 1
float-null 8.0 NULL {}
} {
do_execsql_test compare-gte-$testname "SELECT $lhs >= $rhs" $::ans
}
@@ -89,6 +127,16 @@ foreach {testname lhs rhs ans} {
text-text-1 'b' 'c' 0
text-text-2 'b' 'b' 1
text-text-3 'b' 'a' 1
text-null 'a' NULL {}
} {
do_execsql_test compare-gte-$testname "SELECT $lhs >= $rhs" $::ans
}
foreach {testname lhs rhs ans} {
null-int NULL 1 {}
null-float NULL 1.0 {}
null-text NULL 'a' {}
null-null NULL NULL {}
} {
do_execsql_test compare-gte-$testname "SELECT $lhs >= $rhs" $::ans
}
@@ -97,6 +145,7 @@ foreach {testname lhs rhs ans} {
int-int-1 1 8 1
int-int-2 1 1 0
int-int-3 8 0 0
int-null 8 NULL {}
} {
do_execsql_test compare-lt-$testname "SELECT $lhs < $rhs" $::ans
}
@@ -105,6 +154,7 @@ foreach {testname lhs rhs ans} {
float-float-1 1.0 2.0 1
float-float-2 1.0 1.0 0
float-float-3 7.0 6.0 0
float-null 8.0 NULL {}
} {
do_execsql_test compare-lt-$testname "SELECT $lhs < $rhs" $::ans
}
@@ -113,6 +163,16 @@ foreach {testname lhs rhs ans} {
text-text-1 'b' 'c' 1
text-text-2 'b' 'b' 0
text-text-3 'b' 'a' 0
text-null 'a' NULL {}
} {
do_execsql_test compare-lt-$testname "SELECT $lhs < $rhs" $::ans
}
foreach {testname lhs rhs ans} {
null-int NULL 1 {}
null-float NULL 1.0 {}
null-text NULL 'a' {}
null-null NULL NULL {}
} {
do_execsql_test compare-lt-$testname "SELECT $lhs < $rhs" $::ans
}
@@ -121,6 +181,7 @@ foreach {testname lhs rhs ans} {
int-int-1 1 8 1
int-int-2 1 1 1
int-int-3 8 0 0
int-null 8 NULL {}
} {
do_execsql_test compare-lte-$testname "SELECT $lhs <= $rhs" $::ans
}
@@ -129,6 +190,7 @@ foreach {testname lhs rhs ans} {
float-float-1 1.0 2.0 1
float-float-2 1.0 1.0 1
float-float-3 7.0 6.0 0
float-null 8.0 NULL {}
} {
do_execsql_test compare-lte-$testname "SELECT $lhs <= $rhs" $::ans
}
@@ -137,6 +199,16 @@ foreach {testname lhs rhs ans} {
text-text-1 'b' 'c' 1
text-text-2 'b' 'b' 1
text-text-3 'b' 'a' 0
text-null 'a' NULL {}
} {
do_execsql_test compare-lte-$testname "SELECT $lhs <= $rhs" $::ans
}
foreach {testname lhs rhs ans} {
null-int NULL 1 {}
null-float NULL 1.0 {}
null-text NULL 'a' {}
null-null NULL NULL {}
} {
do_execsql_test compare-lte-$testname "SELECT $lhs <= $rhs" $::ans
}