Merge 'Add support for nullif scalar function' from Kim Seon Woo

### EXPLAIN nullif(1,2)
<img width="1339" alt="image" src="https://github.com/user-attachments/assets/08230797-914d-4922-b52c-5b2b2b4c2a12">

### Issue
https://github.com/penberg/limbo/issues/144

Closes #299
This commit is contained in:
Pekka Enberg
2024-08-20 20:47:35 +03:00
5 changed files with 107 additions and 5 deletions

View File

@@ -74,7 +74,7 @@ This document describes the SQLite compatibility status of Limbo:
| iif(X,Y,Z) | No | |
| instr(X,Y) | No | |
| last_insert_rowid() | No | |
| length(X) | No | |
| length(X) | Yes | |
| like(X,Y) | No | |
| like(X,Y,Z) | No | |
| likelihood(X,Y) | No | |
@@ -86,7 +86,7 @@ This document describes the SQLite compatibility status of Limbo:
| ltrim(X,Y) | Yes | |
| max(X,Y,...) | Yes | |
| min(X,Y,...) | Yes | |
| nullif(X,Y) | No | |
| nullif(X,Y) | Yes | |
| octet_length(X) | No | |
| printf(FORMAT,...) | No | |
| quote(X) | Yes | |

View File

@@ -56,6 +56,7 @@ pub enum ScalarFunc {
Length,
Min,
Max,
Nullif,
Substr,
Substring,
Date,
@@ -83,6 +84,7 @@ impl ToString for ScalarFunc {
ScalarFunc::Length => "length".to_string(),
ScalarFunc::Min => "min".to_string(),
ScalarFunc::Max => "max".to_string(),
ScalarFunc::Nullif => "nullif".to_string(),
ScalarFunc::Substr => "substr".to_string(),
ScalarFunc::Substring => "substring".to_string(),
ScalarFunc::Date => "date".to_string(),
@@ -110,6 +112,7 @@ impl Func {
"max" if arg_count > 1 => Ok(Func::Scalar(ScalarFunc::Max)),
"min" if arg_count == 0 || arg_count == 1 => Ok(Func::Agg(AggFunc::Min)),
"min" if arg_count > 1 => Ok(Func::Scalar(ScalarFunc::Min)),
"nullif" if arg_count == 2 => Ok(Func::Scalar(ScalarFunc::Nullif)),
"string_agg" => Ok(Func::Agg(AggFunc::StringAgg)),
"sum" => Ok(Func::Agg(AggFunc::Sum)),
"total" => Ok(Func::Agg(AggFunc::Total)),

View File

@@ -1067,6 +1067,43 @@ pub fn translate_expr(
});
Ok(target_register)
}
ScalarFunc::Nullif => {
let args = if let Some(args) = args {
if args.len() != 2 {
crate::bail_parse_error!(
"nullif function must have two argument"
);
}
args
} else {
crate::bail_parse_error!("nullif function with no arguments");
};
let func_reg = program.alloc_register();
let first_reg = program.alloc_register();
translate_expr(
program,
referenced_tables,
&args[0],
first_reg,
cursor_hint,
)?;
let second_reg = program.alloc_register();
translate_expr(
program,
referenced_tables,
&args[1],
second_reg,
cursor_hint,
)?;
program.emit_insn(Insn::Function {
start_reg: func_reg,
dest: target_register,
func: crate::vdbe::Func::Scalar(srf),
});
Ok(target_register)
}
}
}
None => {

View File

@@ -1321,6 +1321,13 @@ impl Program {
}
state.pc += 1;
}
Func::Scalar(ScalarFunc::Nullif) => {
let start_reg = *start_reg;
let first_value = &state.registers[start_reg + 1];
let second_value = &state.registers[start_reg + 2];
state.registers[*dest] = exec_nullif(first_value, second_value);
state.pc += 1;
}
Func::Scalar(ScalarFunc::Substr) | Func::Scalar(ScalarFunc::Substring) => {
let start_reg = *start_reg;
let str_value = &state.registers[start_reg];
@@ -1717,6 +1724,14 @@ fn exec_minmax<'a>(
regs.into_iter().reduce(|a, b| op(a, b)).cloned()
}
fn exec_nullif(first_value: &OwnedValue, second_value: &OwnedValue) -> OwnedValue {
if first_value != second_value {
first_value.clone()
} else {
OwnedValue::Null
}
}
fn exec_substring(
str_value: &OwnedValue,
start_value: &OwnedValue,
@@ -1867,9 +1882,9 @@ fn exec_if(reg: &OwnedValue, null_reg: &OwnedValue, not: bool) -> bool {
mod tests {
use super::{
exec_abs, exec_char, exec_if, exec_length, exec_like, exec_lower, exec_ltrim, exec_minmax,
exec_quote, exec_random, exec_round, exec_rtrim, exec_substring, exec_trim, exec_unicode,
exec_upper, get_new_rowid, Cursor, CursorResult, LimboError, OwnedRecord, OwnedValue,
Result,
exec_nullif, exec_quote, exec_random, exec_round, exec_rtrim, exec_substring, exec_trim,
exec_unicode, exec_upper, get_new_rowid, Cursor, CursorResult, LimboError, OwnedRecord,
OwnedValue, Result,
};
use mockall::{mock, predicate};
use rand::{rngs::mock::StepRng, thread_rng};
@@ -2298,6 +2313,41 @@ mod tests {
assert!(!exec_if(&reg, &null_reg, true));
}
#[test]
fn test_nullif() {
assert_eq!(
exec_nullif(&OwnedValue::Integer(1), &OwnedValue::Integer(1)),
OwnedValue::Null
);
assert_eq!(
exec_nullif(&OwnedValue::Float(1.1), &OwnedValue::Float(1.1)),
OwnedValue::Null
);
assert_eq!(
exec_nullif(
&OwnedValue::Text(Rc::new("limbo".to_string())),
&OwnedValue::Text(Rc::new("limbo".to_string()))
),
OwnedValue::Null
);
assert_eq!(
exec_nullif(&OwnedValue::Integer(1), &OwnedValue::Integer(2)),
OwnedValue::Integer(1)
);
assert_eq!(
exec_nullif(&OwnedValue::Float(1.1), &OwnedValue::Float(1.2)),
OwnedValue::Float(1.1)
);
assert_eq!(
exec_nullif(
&OwnedValue::Text(Rc::new("limbo".to_string())),
&OwnedValue::Text(Rc::new("limb".to_string()))
),
OwnedValue::Text(Rc::new("limbo".to_string()))
);
}
#[test]
fn test_substring() {
let str_value = OwnedValue::Text(Rc::new("limbo".to_string()));

View File

@@ -279,6 +279,18 @@ do_execsql_test max-null {
select max(null,null)
} {}
do_execsql_test nullif {
select nullif(1, 2)
} {1}
do_execsql_test nullif {
select nullif(1, 1)
} {}
do_execsql_test nullif {
select nullif('limbo', 'limbo')
} {}
do_execsql_test substr-3-args {
SELECT substr('limbo', 1, 3);
} {lim}