Merge 'Add instr(X,Y) scalar function' from Lauri Virtanen

Relates to issue #144
> ## instr(X,Y)
>
> The instr(X,Y) function finds the first occurrence of string Y within
string X and returns the number of prior characters plus 1, or 0 if Y is
nowhere found within X. Or, if X and Y are both BLOBs, then instr(X,Y)
returns one more than the number bytes prior to the first occurrence of
Y, or 0 if Y does not occur anywhere within X. If both arguments X and Y
to instr(X,Y) are non-NULL and are not BLOBs then both are interpreted
as strings. If either X or Y are NULL in instr(X,Y) then the result is
NULL.

Closes #357
This commit is contained in:
Pekka Enberg
2024-10-07 08:59:40 +03:00
5 changed files with 214 additions and 9 deletions

View File

@@ -72,7 +72,7 @@ This document describes the SQLite compatibility status of Limbo:
| hex(X) | Yes | |
| ifnull(X,Y) | Yes | |
| iif(X,Y,Z) | No | |
| instr(X,Y) | No | |
| instr(X,Y) | Yes | |
| last_insert_rowid() | No | |
| length(X) | Yes | |
| like(X,Y) | No | |

View File

@@ -53,6 +53,7 @@ pub enum ScalarFunc {
ConcatWs,
Glob,
IfNull,
Instr,
Like,
Abs,
Upper,
@@ -91,6 +92,7 @@ impl Display for ScalarFunc {
ScalarFunc::ConcatWs => "concat_ws".to_string(),
ScalarFunc::Glob => "glob".to_string(),
ScalarFunc::IfNull => "ifnull".to_string(),
ScalarFunc::Instr => "instr".to_string(),
ScalarFunc::Like => "like(2)".to_string(),
ScalarFunc::Abs => "abs".to_string(),
ScalarFunc::Upper => "upper".to_string(),
@@ -166,6 +168,7 @@ impl Func {
"concat_ws" => Ok(Func::Scalar(ScalarFunc::ConcatWs)),
"glob" => Ok(Func::Scalar(ScalarFunc::Glob)),
"ifnull" => Ok(Func::Scalar(ScalarFunc::IfNull)),
"instr" => Ok(Func::Scalar(ScalarFunc::Instr)),
"like" => Ok(Func::Scalar(ScalarFunc::Like)),
"abs" => Ok(Func::Scalar(ScalarFunc::Abs)),
"upper" => Ok(Func::Scalar(ScalarFunc::Upper)),

View File

@@ -1324,16 +1324,20 @@ pub fn translate_expr(
});
Ok(target_register)
}
ScalarFunc::Nullif => {
ScalarFunc::Nullif | ScalarFunc::Instr => {
let args = if let Some(args) = args {
if args.len() != 2 {
crate::bail_parse_error!(
"nullif function must have two argument"
"{} function must have two argument",
srf.to_string()
);
}
args
} else {
crate::bail_parse_error!("nullif function with no arguments");
crate::bail_parse_error!(
"{} function with no arguments",
srf.to_string()
);
};
let first_reg = program.alloc_register();

View File

@@ -1506,6 +1506,12 @@ impl Program {
state.registers[*dest] = result;
}
ScalarFunc::IfNull => {}
ScalarFunc::Instr => {
let reg_value = &state.registers[*start_reg];
let pattern_value = &state.registers[*start_reg + 1];
let result = exec_instr(reg_value, pattern_value);
state.registers[*dest] = result;
}
ScalarFunc::Like => {
let pattern = &state.registers[*start_reg];
let text = &state.registers[*start_reg + 1];
@@ -2170,6 +2176,43 @@ fn exec_substring(
}
}
fn exec_instr(reg: &OwnedValue, pattern: &OwnedValue) -> OwnedValue {
if reg == &OwnedValue::Null || pattern == &OwnedValue::Null {
return OwnedValue::Null;
}
if let (OwnedValue::Blob(reg), OwnedValue::Blob(pattern)) = (reg, pattern) {
let result = reg
.windows(pattern.len())
.position(|window| window == **pattern)
.map_or(0, |i| i + 1);
return OwnedValue::Integer(result as i64);
}
let reg_str;
let reg = match reg {
OwnedValue::Text(s) => s.as_str(),
_ => {
reg_str = reg.to_string();
reg_str.as_str()
}
};
let pattern_str;
let pattern = match pattern {
OwnedValue::Text(s) => s.as_str(),
_ => {
pattern_str = pattern.to_string();
pattern_str.as_str()
}
};
match reg.find(pattern) {
Some(position) => OwnedValue::Integer(position as i64 + 1),
None => OwnedValue::Integer(0),
}
}
fn exec_typeof(reg: &OwnedValue) -> OwnedValue {
match reg {
OwnedValue::Null => OwnedValue::Text(Rc::new("null".to_string())),
@@ -2338,11 +2381,11 @@ fn execute_sqlite_version(version_integer: i64) -> String {
mod tests {
use super::{
exec_abs, exec_char, exec_hex, exec_if, exec_length, exec_like, exec_lower, exec_ltrim,
exec_max, exec_min, exec_nullif, exec_quote, exec_random, exec_randomblob, exec_round,
exec_rtrim, exec_sign, exec_substring, exec_trim, exec_typeof, exec_unhex, exec_unicode,
exec_upper, exec_zeroblob, execute_sqlite_version, get_new_rowid, AggContext, Cursor,
CursorResult, LimboError, OwnedRecord, OwnedValue, Result,
exec_abs, exec_char, exec_hex, exec_if, exec_instr, exec_length, exec_like, exec_lower,
exec_ltrim, exec_max, exec_min, exec_nullif, exec_quote, exec_random, exec_randomblob,
exec_round, exec_rtrim, exec_sign, exec_substring, exec_trim, exec_typeof, exec_unhex,
exec_unicode, exec_upper, exec_zeroblob, execute_sqlite_version, get_new_rowid, AggContext,
Cursor, CursorResult, LimboError, OwnedRecord, OwnedValue, Result,
};
use mockall::{mock, predicate};
use rand::{rngs::mock::StepRng, thread_rng};
@@ -2994,6 +3037,109 @@ mod tests {
);
}
#[test]
fn test_exec_instr() {
let input = OwnedValue::Text(Rc::new(String::from("limbo")));
let pattern = OwnedValue::Text(Rc::new(String::from("im")));
let expected = OwnedValue::Integer(2);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Text(Rc::new(String::from("limbo")));
let pattern = OwnedValue::Text(Rc::new(String::from("limbo")));
let expected = OwnedValue::Integer(1);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Text(Rc::new(String::from("limbo")));
let pattern = OwnedValue::Text(Rc::new(String::from("o")));
let expected = OwnedValue::Integer(5);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Text(Rc::new(String::from("liiiiimbo")));
let pattern = OwnedValue::Text(Rc::new(String::from("ii")));
let expected = OwnedValue::Integer(2);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Text(Rc::new(String::from("limbo")));
let pattern = OwnedValue::Text(Rc::new(String::from("limboX")));
let expected = OwnedValue::Integer(0);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Text(Rc::new(String::from("limbo")));
let pattern = OwnedValue::Text(Rc::new(String::from("")));
let expected = OwnedValue::Integer(1);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Text(Rc::new(String::from("")));
let pattern = OwnedValue::Text(Rc::new(String::from("limbo")));
let expected = OwnedValue::Integer(0);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Text(Rc::new(String::from("")));
let pattern = OwnedValue::Text(Rc::new(String::from("")));
let expected = OwnedValue::Integer(1);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Null;
let pattern = OwnedValue::Null;
let expected = OwnedValue::Null;
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Text(Rc::new(String::from("limbo")));
let pattern = OwnedValue::Null;
let expected = OwnedValue::Null;
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Null;
let pattern = OwnedValue::Text(Rc::new(String::from("limbo")));
let expected = OwnedValue::Null;
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Integer(123);
let pattern = OwnedValue::Integer(2);
let expected = OwnedValue::Integer(2);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Integer(123);
let pattern = OwnedValue::Integer(5);
let expected = OwnedValue::Integer(0);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Float(12.34);
let pattern = OwnedValue::Float(2.3);
let expected = OwnedValue::Integer(2);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Float(12.34);
let pattern = OwnedValue::Float(5.6);
let expected = OwnedValue::Integer(0);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Float(12.34);
let pattern = OwnedValue::Text(Rc::new(String::from(".")));
let expected = OwnedValue::Integer(3);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Blob(Rc::new(vec![1, 2, 3, 4, 5]));
let pattern = OwnedValue::Blob(Rc::new(vec![3, 4]));
let expected = OwnedValue::Integer(3);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Blob(Rc::new(vec![1, 2, 3, 4, 5]));
let pattern = OwnedValue::Blob(Rc::new(vec![3, 2]));
let expected = OwnedValue::Integer(0);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Blob(Rc::new(vec![0x61, 0x62, 0x63, 0x64, 0x65]));
let pattern = OwnedValue::Text(Rc::new(String::from("cd")));
let expected = OwnedValue::Integer(3);
assert_eq!(exec_instr(&input, &pattern), expected);
let input = OwnedValue::Text(Rc::new(String::from("abcde")));
let pattern = OwnedValue::Blob(Rc::new(vec![0x63, 0x64]));
let expected = OwnedValue::Integer(3);
assert_eq!(exec_instr(&input, &pattern), expected);
}
#[test]
fn test_exec_sign() {
let input = OwnedValue::Integer(42);

View File

@@ -75,6 +75,58 @@ do_execsql_test ifnull-2 {
select ifnull(null, 2);
} {2}
do_execsql_test instr-str {
select instr('limbo', 'im');
} {2}
do_execsql_test instr-str-not-found {
select instr('limbo', 'xyz');
} {0}
do_execsql_test instr-blob {
select instr(x'000102', x'01');
} {2}
do_execsql_test instr-blob-not-found {
select instr(x'000102', x'10');
} {0}
do_execsql_test instr-null {
select instr(null, 'limbo');
} {}
do_execsql_test instr-integer {
select instr(123, 2);
} {2}
do_execsql_test instr-integer-not-found {
select instr(123, 5);
} {0}
do_execsql_test instr-integer-leading-zeros {
select instr(0001, 0);
} {0}
do_execsql_test instr-real {
select instr(12.34, 2.3);
} {2}
do_execsql_test instr-real-not-found {
select instr(12.34, 5);
} {0}
do_execsql_test instr-real-trailing-zeros {
select instr(1.10000, 0);
} {0}
do_execsql_test instr-blob {
select instr(x'01020304', x'02');
} {2}
do_execsql_test instr-blob-not-found {
select instr(x'01020304', x'05');
} {0}
do_execsql_test upper {
select upper('Limbo')
} {LIMBO}