diff --git a/COMPAT.md b/COMPAT.md index 086ce1667..c3c97e683 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -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 | | diff --git a/core/function.rs b/core/function.rs index 9dbfa8693..9bd7bdc3a 100644 --- a/core/function.rs +++ b/core/function.rs @@ -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)), diff --git a/core/translate/expr.rs b/core/translate/expr.rs index c5d70e264..1ab35e6ce 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -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 => { diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 4dddbe285..cd37d7798 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -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(®, &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())); diff --git a/testing/scalar-functions.test b/testing/scalar-functions.test index 7933df79f..284e46aef 100755 --- a/testing/scalar-functions.test +++ b/testing/scalar-functions.test @@ -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}