diff --git a/core/src/cnf/mod.rs b/core/src/cnf/mod.rs index 4a7be646..3d9d6027 100644 --- a/core/src/cnf/mod.rs +++ b/core/src/cnf/mod.rs @@ -59,3 +59,11 @@ pub static EXPERIMENTAL_BEARER_ACCESS: Lazy = // Run tests with bearer access enabled as it introduces new functionality that needs to be tested. #[cfg(test)] pub static EXPERIMENTAL_BEARER_ACCESS: Lazy = Lazy::new(|| true); + +/// Used to limit allocation for builtin functions +pub static FUNCTION_ALLOCATION_LIMIT: Lazy = once_cell::sync::Lazy::new(|| { + let n = std::env::var("SURREAL_FUNCTION_ALLOCATION_LIMIT") + .map(|s| s.parse::().unwrap_or(20)) + .unwrap_or(20); + 2usize.pow(n) +}); diff --git a/core/src/fnc/args.rs b/core/src/fnc/args.rs index d0c80925..a9704b50 100644 --- a/core/src/fnc/args.rs +++ b/core/src/fnc/args.rs @@ -1,6 +1,8 @@ use crate::err::Error; use crate::sql::value::Value; -use crate::sql::{Array, Bytes, Datetime, Duration, Kind, Number, Object, Regex, Strand, Thing}; +use crate::sql::{ + Array, Bytes, Closure, Datetime, Duration, Kind, Number, Object, Regex, Strand, Thing, +}; use std::vec::IntoIter; /// Implemented by types that are commonly used, in a certain way, as arguments. @@ -14,6 +16,12 @@ impl FromArg for Value { } } +impl FromArg for Closure { + fn from_arg(arg: Value) -> Result { + arg.coerce_to_function() + } +} + impl FromArg for Regex { fn from_arg(arg: Value) -> Result { arg.coerce_to_regex() @@ -242,6 +250,41 @@ impl FromArgs for (A, Option) { } } +// Some functions take 4 arguments, with the 3rd and 4th being optional. +impl FromArgs for (A, B, Option, Option) { + fn from_args(name: &str, args: Vec) -> Result { + let err = || Error::InvalidArguments { + name: name.to_owned(), + message: String::from("Expected 2, 3 or 4 arguments."), + }; + // Process the function arguments + let mut args = args.into_iter(); + // Process the first argument + let a = A::from_arg(args.next().ok_or_else(err)?).map_err(|e| Error::InvalidArguments { + name: name.to_owned(), + message: format!("Argument 1 was the wrong type. {e}"), + })?; + let b = B::from_arg(args.next().ok_or_else(err)?).map_err(|e| Error::InvalidArguments { + name: name.to_owned(), + message: format!("Argument 2 was the wrong type. {e}"), + })?; + let c = match args.next() { + Some(c) => Some(C::from_arg(c)?), + None => None, + }; + let d = match args.next() { + Some(d) => Some(D::from_arg(d)?), + None => None, + }; + // Process additional function arguments + if args.next().is_some() { + // Too many arguments + return Err(err()); + } + Ok((a, b, c, d)) + } +} + #[inline] fn get_arg Error>( name: &str, diff --git a/core/src/fnc/array.rs b/core/src/fnc/array.rs index 197e981a..cf5cf05b 100644 --- a/core/src/fnc/array.rs +++ b/core/src/fnc/array.rs @@ -1,3 +1,7 @@ +use crate::cnf::FUNCTION_ALLOCATION_LIMIT; +use crate::ctx::Context; +use crate::dbs::Options; +use crate::doc::CursorDoc; use crate::err::Error; use crate::sql::array::Array; use crate::sql::array::Clump; @@ -12,8 +16,24 @@ use crate::sql::array::Union; use crate::sql::array::Uniq; use crate::sql::array::Windows; use crate::sql::value::Value; +use crate::sql::Closure; +use crate::sql::Function; use rand::prelude::SliceRandom; +use reblessive::tree::Stk; +use std::mem::size_of_val; + +/// Returns an error if an array of this length is too much to allocate. +fn limit(name: &str, n: usize) -> Result<(), Error> { + if n > *FUNCTION_ALLOCATION_LIMIT { + Err(Error::InvalidArguments { + name: name.to_owned(), + message: format!("Output must not exceed {} bytes.", *FUNCTION_ALLOCATION_LIMIT), + }) + } else { + Ok(()) + } +} pub fn add((mut array, value): (Array, Value)) -> Result { match value { @@ -135,6 +155,45 @@ pub fn distinct((array,): (Array,)) -> Result { Ok(array.uniq().into()) } +pub fn fill( + (mut array, value, start, end): (Array, Value, Option, Option), +) -> Result { + let min = 0; + let max = array.len(); + let negative_max = -(max as isize); + + let start = match start { + Some(start) if negative_max <= start && start < 0 => (start + max as isize) as usize, + Some(start) if start < negative_max => 0, + Some(start) => start as usize, + None => min, + }; + let end = match end { + Some(end) if negative_max <= end && end < 0 => (end + max as isize) as usize, + Some(end) if end < negative_max => 0, + Some(end) => end as usize, + None => max, + }; + + if start == min && end >= max { + array.fill(value); + } else if end > start { + let end_minus_one = end - 1; + + for i in start..end_minus_one { + if let Some(elem) = array.get_mut(i) { + *elem = value.clone(); + } + } + + if let Some(last_elem) = array.get_mut(end_minus_one) { + *last_elem = value; + } + } + + Ok(array.into()) +} + pub fn filter_index((array, value): (Array, Value)) -> Result { Ok(array .iter() @@ -201,6 +260,10 @@ pub fn intersect((array, other): (Array, Array)) -> Result { Ok(array.intersect(other).into()) } +pub fn is_empty((array,): (Array,)) -> Result { + Ok(array.is_empty().into()) +} + pub fn join((arr, sep): (Array, String)) -> Result { Ok(arr.into_iter().map(Value::as_raw_string).collect::>().join(&sep).into()) } @@ -289,6 +352,20 @@ pub fn logical_xor((lh, rh): (Array, Array)) -> Result { Ok(result_arr.into()) } +pub async fn map( + (stk, ctx, opt, doc): (&mut Stk, &Context<'_>, &Options, Option<&CursorDoc<'_>>), + (array, mapper): (Array, Closure), +) -> Result { + let mut array = array; + for i in 0..array.len() { + let v = array.get(i).unwrap(); + let fnc = Function::Anonymous(mapper.clone().into(), vec![v.to_owned(), i.into()]); + array[i] = fnc.compute(stk, ctx, opt, doc).await?; + } + + Ok(array.into()) +} + pub fn matches((array, compare_val): (Array, Value)) -> Result { Ok(array.matches(compare_val).into()) } @@ -315,6 +392,26 @@ pub fn push((mut array, value): (Array, Value)) -> Result { Ok(array.into()) } +pub fn range((start, count): (i64, i64)) -> Result { + if count < 0 { + return Err(Error::InvalidArguments { + name: String::from("array::range"), + message: format!( + "Argument 1 was the wrong type. Expected a positive number but found {count}" + ), + }); + } + + if let Some(end) = start.checked_add(count - 1) { + Ok(Array((start..=end).map(Value::from).collect::>()).into()) + } else { + Err(Error::InvalidArguments { + name: String::from("array::range"), + message: String::from("The range overflowed the maximum value for an integer"), + }) + } +} + pub fn remove((mut array, mut index): (Array, i64)) -> Result { // Negative index means start from the back if index < 0 { @@ -330,6 +427,11 @@ pub fn remove((mut array, mut index): (Array, i64)) -> Result { Ok(array.into()) } +pub fn repeat((value, count): (Value, usize)) -> Result { + limit("array::repeat", size_of_val(&value).saturating_mul(count))?; + Ok(Array(std::iter::repeat(value).take(count).collect()).into()) +} + pub fn reverse((mut array,): (Array,)) -> Result { array.reverse(); Ok(array.into()) @@ -337,7 +439,7 @@ pub fn reverse((mut array,): (Array,)) -> Result { pub fn shuffle((mut array,): (Array,)) -> Result { let mut rng = rand::thread_rng(); - array.0.shuffle(&mut rng); + array.shuffle(&mut rng); Ok(array.into()) } @@ -392,6 +494,37 @@ pub fn sort((mut array, order): (Array, Option)) -> Result } } +pub fn swap((mut array, from, to): (Array, isize, isize)) -> Result { + let min = 0; + let max = array.len(); + let negative_max = -(max as isize); + + let from = match from { + from if from < negative_max || from >= max as isize => Err(Error::InvalidArguments { + name: String::from("array::swap"), + message: format!( + "Argument 1 is out of range. Expected a number between {negative_max} and {max}" + ), + }), + from if negative_max <= from && from < min => Ok((from + max as isize) as usize), + from => Ok(from as usize), + }?; + + let to = match to { + to if to < negative_max || to >= max as isize => Err(Error::InvalidArguments { + name: String::from("array::swap"), + message: format!( + "Argument 2 is out of range. Expected a number between {negative_max} and {max}" + ), + }), + to if negative_max <= to && to < min => Ok((to + max as isize) as usize), + to => Ok(to as usize), + }?; + + array.swap(from, to); + Ok(array.into()) +} + pub fn transpose((array,): (Array,)) -> Result { Ok(array.transpose().into()) } diff --git a/core/src/fnc/mod.rs b/core/src/fnc/mod.rs index f89ed27a..2b1e06fa 100644 --- a/core/src/fnc/mod.rs +++ b/core/src/fnc/mod.rs @@ -52,8 +52,9 @@ pub async fn run( || name.starts_with("crypto::bcrypt") || name.starts_with("crypto::pbkdf2") || name.starts_with("crypto::scrypt") + || name.starts_with("array::map") { - stk.run(|stk| asynchronous(stk, ctx, Some(opt), doc, name, args)).await + stk.run(|stk| asynchronous(stk, ctx, opt, doc, name, args)).await } else { synchronous(ctx, doc, name, args) } @@ -110,6 +111,7 @@ pub fn synchronous( "array::concat" => array::concat, "array::difference" => array::difference, "array::distinct" => array::distinct, + "array::fill" => array::fill, "array::filter_index" => array::filter_index, "array::find_index" => array::find_index, "array::first" => array::first, @@ -117,6 +119,7 @@ pub fn synchronous( "array::group" => array::group, "array::insert" => array::insert, "array::intersect" => array::intersect, + "array::is_empty" => array::is_empty, "array::join" => array::join, "array::last" => array::last, "array::len" => array::len, @@ -129,11 +132,14 @@ pub fn synchronous( "array::pop" => array::pop, "array::prepend" => array::prepend, "array::push" => array::push, + "array::range" => array::range, "array::remove" => array::remove, + "array::repeat" => array::repeat, "array::reverse" => array::reverse, "array::shuffle" => array::shuffle, "array::slice" => array::slice, "array::sort" => array::sort, + "array::swap" => array::swap, "array::transpose" => array::transpose, "array::union" => array::union, "array::sort::asc" => array::sort::asc, @@ -409,8 +415,10 @@ pub fn synchronous( } /// Attempts to run any synchronous function. -pub fn idiom( +pub async fn idiom( + stk: &mut Stk, ctx: &Context<'_>, + opt: &Options, doc: Option<&CursorDoc<'_>>, value: Value, name: &str, @@ -438,6 +446,7 @@ pub fn idiom( "concat" => array::concat, "difference" => array::difference, "distinct" => array::distinct, + "fill" => array::fill, "filter_index" => array::filter_index, "find_index" => array::find_index, "first" => array::first, @@ -445,6 +454,7 @@ pub fn idiom( "group" => array::group, "insert" => array::insert, "intersect" => array::intersect, + "is_empty" => array::is_empty, "join" => array::join, "last" => array::last, "len" => array::len, @@ -452,6 +462,7 @@ pub fn idiom( "logical_or" => array::logical_or, "logical_xor" => array::logical_xor, "matches" => array::matches, + "map" => array::map((stk, ctx, opt, doc)).await, "max" => array::max, "min" => array::min, "pop" => array::pop, @@ -462,6 +473,7 @@ pub fn idiom( "shuffle" => array::shuffle, "slice" => array::slice, "sort" => array::sort, + "swap" => array::swap, "transpose" => array::transpose, "union" => array::union, "sort_asc" => array::sort::asc, @@ -684,6 +696,8 @@ pub fn idiom( "to_record" => r#type::record, "to_string" => r#type::string, "to_uuid" => r#type::uuid, + // + "repeat" => array::repeat, ) } v => v, @@ -694,7 +708,7 @@ pub fn idiom( pub async fn asynchronous( stk: &mut Stk, ctx: &Context<'_>, - opt: Option<&Options>, + opt: &Options, doc: Option<&CursorDoc<'_>>, name: &str, args: Vec, @@ -719,6 +733,8 @@ pub async fn asynchronous( name, args, "no such builtin function found", + "array::map" => array::map((stk, ctx, opt, doc)).await, + // "crypto::argon2::compare" => (cpu_intensive) crypto::argon2::cmp.await, "crypto::argon2::generate" => (cpu_intensive) crypto::argon2::gen.await, "crypto::bcrypt::compare" => (cpu_intensive) crypto::bcrypt::cmp.await, @@ -735,15 +751,15 @@ pub async fn asynchronous( "http::patch" => http::patch(ctx).await, "http::delete" => http::delete(ctx).await, // - "search::analyze" => search::analyze((stk,ctx, opt)).await, + "search::analyze" => search::analyze((stk,ctx, Some(opt))).await, "search::score" => search::score((ctx, doc)).await, "search::highlight" => search::highlight((ctx, doc)).await, "search::offsets" => search::offsets((ctx, doc)).await, // "sleep" => sleep::sleep(ctx).await, // - "type::field" => r#type::field((stk,ctx, opt, doc)).await, - "type::fields" => r#type::fields((stk,ctx, opt, doc)).await, + "type::field" => r#type::field((stk,ctx, Some(opt), doc)).await, + "type::fields" => r#type::fields((stk,ctx, Some(opt), doc)).await, ) } @@ -773,6 +789,9 @@ mod tests { #[tokio::test] async fn implementations_are_present() { + #[cfg(all(feature = "scripting", feature = "kv-mem"))] + let excluded_from_scripting = &["array::map"]; + // Accumulate and display all problems at once to avoid a test -> fix -> test -> fix cycle. let mut problems = Vec::new(); @@ -780,7 +799,7 @@ mod tests { let fnc_mod = include_str!("mod.rs"); // Patch out idiom methods - let re = Regex::new(r"(?ms)pub fn idiom\(.*}\n+///").unwrap(); + let re = Regex::new(r"(?ms)pub async fn idiom\(.*}\n+///").unwrap(); let fnc_no_idiom = re.replace(fnc_mod, ""); for line in fnc_no_idiom.lines() { @@ -823,6 +842,10 @@ mod tests { { use crate::sql::Value; + if excluded_from_scripting.contains(&name) { + continue; + } + let name = name.replace("::", "."); let sql = format!("RETURN function() {{ return typeof surrealdb.functions.{name}; }}"); diff --git a/core/src/fnc/script/modules/surrealdb/functions/array.rs b/core/src/fnc/script/modules/surrealdb/functions/array.rs index 9bf1600c..1f240dd3 100644 --- a/core/src/fnc/script/modules/surrealdb/functions/array.rs +++ b/core/src/fnc/script/modules/surrealdb/functions/array.rs @@ -23,6 +23,7 @@ impl_module_def!( "concat" => run, "difference" => run, "distinct" => run, + "fill" => run, "filter_index" => run, "find_index" => run, "first" => run, @@ -30,6 +31,7 @@ impl_module_def!( "group" => run, "insert" => run, "intersect" => run, + "is_empty" => run, "join" => run, "knn" => run, "last" => run, @@ -43,11 +45,14 @@ impl_module_def!( "pop" => run, "push" => run, "prepend" => run, + "range" => run, "remove" => run, + "repeat" => run, "reverse" => run, "shuffle" => run, "slice" => run, "sort" => (sort::Package), + "swap" => run, "transpose" => run, "union" => run, "windows" => run diff --git a/core/src/fnc/script/modules/surrealdb/functions/mod.rs b/core/src/fnc/script/modules/surrealdb/functions/mod.rs index 2fe18836..4a7ec236 100644 --- a/core/src/fnc/script/modules/surrealdb/functions/mod.rs +++ b/core/src/fnc/script/modules/surrealdb/functions/mod.rs @@ -72,10 +72,9 @@ fn run(js_ctx: js::Ctx<'_>, name: &str, args: Vec) -> Result { async fn fut(js_ctx: js::Ctx<'_>, name: &str, args: Vec) -> Result { let this = js_ctx.globals().get::<_, OwnedBorrow>(QUERY_DATA_PROP_NAME)?; // Process the called function - let res = Stk::enter_run(|stk| { - fnc::asynchronous(stk, this.context, Some(this.opt), this.doc, name, args) - }) - .await; + let res = + Stk::enter_run(|stk| fnc::asynchronous(stk, this.context, this.opt, this.doc, name, args)) + .await; // Convert any response error res.map_err(|err| { js::Exception::from_message(js_ctx, &err.to_string()) diff --git a/core/src/fnc/string.rs b/core/src/fnc/string.rs index 9a293b0c..7f4a7b45 100644 --- a/core/src/fnc/string.rs +++ b/core/src/fnc/string.rs @@ -1,3 +1,4 @@ +use crate::cnf::FUNCTION_ALLOCATION_LIMIT; use crate::err::Error; use crate::fnc::util::string; use crate::sql::value::Value; @@ -5,11 +6,10 @@ use crate::sql::Regex; /// Returns `true` if a string of this length is too much to allocate. fn limit(name: &str, n: usize) -> Result<(), Error> { - const LIMIT: usize = 2usize.pow(20); - if n > LIMIT { + if n > *FUNCTION_ALLOCATION_LIMIT { Err(Error::InvalidArguments { name: name.to_owned(), - message: format!("Output must not exceed {LIMIT} bytes."), + message: format!("Output must not exceed {} bytes.", *FUNCTION_ALLOCATION_LIMIT), }) } else { Ok(()) diff --git a/core/src/sql/value/get.rs b/core/src/sql/value/get.rs index c1f69fc0..4cf28f3f 100644 --- a/core/src/sql/value/get.rs +++ b/core/src/sql/value/get.rs @@ -60,7 +60,11 @@ impl Value { stk.run(|stk| obj.get(stk, ctx, opt, doc, path)).await } Part::Method(name, args) => { - let v = idiom(ctx, doc, v.clone().into(), name, args.clone())?; + let v = stk + .run(|stk| { + idiom(stk, ctx, opt, doc, v.clone().into(), name, args.clone()) + }) + .await?; stk.run(|stk| v.get(stk, ctx, opt, doc, path.next())).await } // Otherwise return none @@ -141,7 +145,11 @@ impl Value { stk.run(|stk| obj.get(stk, ctx, opt, doc, path.next())).await } Part::Method(name, args) => { - let res = idiom(ctx, doc, v.clone().into(), name, args.clone()); + let res = stk + .run(|stk| { + idiom(stk, ctx, opt, doc, v.clone().into(), name, args.clone()) + }) + .await; let res = match &res { Ok(_) => res, Err(Error::InvalidFunction { @@ -214,7 +222,11 @@ impl Value { _ => Ok(Value::None), }, Part::Method(name, args) => { - let v = idiom(ctx, doc, v.clone().into(), name, args.clone())?; + let v = stk + .run(|stk| { + idiom(stk, ctx, opt, doc, v.clone().into(), name, args.clone()) + }) + .await?; stk.run(|stk| v.get(stk, ctx, opt, doc, path.next())).await } _ => stk @@ -302,7 +314,19 @@ impl Value { } } Part::Method(name, args) => { - let v = idiom(ctx, doc, v.clone().into(), name, args.clone())?; + let v = stk + .run(|stk| { + idiom( + stk, + ctx, + opt, + doc, + v.clone().into(), + name, + args.clone(), + ) + }) + .await?; stk.run(|stk| v.get(stk, ctx, opt, doc, path.next())).await } // This is a remote field expression @@ -325,7 +349,9 @@ impl Value { stk.run(|stk| v.get(stk, ctx, opt, None, path.next())).await } Part::Method(name, args) => { - let v = idiom(ctx, doc, v.clone(), name, args.clone())?; + let v = stk + .run(|stk| idiom(stk, ctx, opt, doc, v.clone(), name, args.clone())) + .await?; stk.run(|stk| v.get(stk, ctx, opt, doc, path.next())).await } // Only continue processing the path from the point that it contains a method diff --git a/core/src/syn/parser/builtin.rs b/core/src/syn/parser/builtin.rs index c7c637fe..6fa4859f 100644 --- a/core/src/syn/parser/builtin.rs +++ b/core/src/syn/parser/builtin.rs @@ -97,6 +97,7 @@ pub(crate) static PATHS: phf::Map, PathKind> = phf_map! { UniCase::ascii("array::concat") => PathKind::Function, UniCase::ascii("array::difference") => PathKind::Function, UniCase::ascii("array::distinct") => PathKind::Function, + UniCase::ascii("array::fill") => PathKind::Function, UniCase::ascii("array::filter_index") => PathKind::Function, UniCase::ascii("array::find_index") => PathKind::Function, UniCase::ascii("array::first") => PathKind::Function, @@ -104,6 +105,7 @@ pub(crate) static PATHS: phf::Map, PathKind> = phf_map! { UniCase::ascii("array::group") => PathKind::Function, UniCase::ascii("array::insert") => PathKind::Function, UniCase::ascii("array::intersect") => PathKind::Function, + UniCase::ascii("array::is_empty") => PathKind::Function, UniCase::ascii("array::join") => PathKind::Function, UniCase::ascii("array::last") => PathKind::Function, UniCase::ascii("array::len") => PathKind::Function, @@ -111,16 +113,20 @@ pub(crate) static PATHS: phf::Map, PathKind> = phf_map! { UniCase::ascii("array::logical_or") => PathKind::Function, UniCase::ascii("array::logical_xor") => PathKind::Function, UniCase::ascii("array::matches") => PathKind::Function, + UniCase::ascii("array::map") => PathKind::Function, UniCase::ascii("array::max") => PathKind::Function, UniCase::ascii("array::min") => PathKind::Function, UniCase::ascii("array::pop") => PathKind::Function, UniCase::ascii("array::prepend") => PathKind::Function, UniCase::ascii("array::push") => PathKind::Function, UniCase::ascii("array::remove") => PathKind::Function, + UniCase::ascii("array::repeat") => PathKind::Function, + UniCase::ascii("array::range") => PathKind::Function, UniCase::ascii("array::reverse") => PathKind::Function, UniCase::ascii("array::shuffle") => PathKind::Function, UniCase::ascii("array::slice") => PathKind::Function, UniCase::ascii("array::sort") => PathKind::Function, + UniCase::ascii("array::swap") => PathKind::Function, UniCase::ascii("array::transpose") => PathKind::Function, UniCase::ascii("array::union") => PathKind::Function, UniCase::ascii("array::sort::asc") => PathKind::Function, diff --git a/lib/fuzz/fuzz_targets/fuzz_executor.dict b/lib/fuzz/fuzz_targets/fuzz_executor.dict index 31bded60..86630bbe 100644 --- a/lib/fuzz/fuzz_targets/fuzz_executor.dict +++ b/lib/fuzz/fuzz_targets/fuzz_executor.dict @@ -166,6 +166,7 @@ "array::concat(" "array::difference(" "array::distinct(" +"array::fill(" "array::filter_index(" "array::find_index(" "array::first(" @@ -173,6 +174,7 @@ "array::group(" "array::insert(" "array::intersect(" +"array::is_empty(" "array::join(" "array::last(" "array::len(" @@ -180,18 +182,22 @@ "array::logical_or(" "array::logical_xor(" "array::matches(" +"array::map(" "array::max(" "array::min(" "array::pop(" "array::prepend(" "array::push(" +"array::range(" "array::remove(" +"array::repeat(" "array::reverse(" "array::shuffle(" "array::slice(" "array::sort(" "array::sort::asc(" "array::sort::desc(" +"array::swap(" "array::transpose(" "array::union(" "count(" diff --git a/lib/fuzz/fuzz_targets/fuzz_sql_parser.dict b/lib/fuzz/fuzz_targets/fuzz_sql_parser.dict index bb10f546..4a8afc4b 100644 --- a/lib/fuzz/fuzz_targets/fuzz_sql_parser.dict +++ b/lib/fuzz/fuzz_targets/fuzz_sql_parser.dict @@ -165,6 +165,7 @@ "array::concat(" "array::difference(" "array::distinct(" +"array::fill(" "array::filter_index(" "array::find_index(" "array::first(" @@ -172,6 +173,7 @@ "array::group(" "array::insert(" "array::intersect(" +"array::is_empty(" "array::join(" "array::last(" "array::len(" @@ -179,18 +181,22 @@ "array::logical_or(" "array::logical_xor(" "array::matches(" +"array::map(" "array::max(" "array::min(" "array::pop(" "array::prepend(" "array::push(" +"array::range(" "array::remove(" +"array::repeat(" "array::reverse(" "array::shuffle(" "array::slice(" "array::sort(" "array::sort::asc(" "array::sort::desc(" +"array::swap(" "array::transpose(" "array::union(" "count(" diff --git a/lib/tests/function.rs b/lib/tests/function.rs index 41590d89..9aa4bd11 100644 --- a/lib/tests/function.rs +++ b/lib/tests/function.rs @@ -279,6 +279,32 @@ async fn function_array_distinct() -> Result<(), Error> { Ok(()) } +#[tokio::test] +async fn function_array_fill() -> Result<(), Error> { + let sql = r#" + RETURN array::fill([1,2,3,4,5], 10); + RETURN array::fill([1,2,3,4,5], 10, 0, 7); + RETURN array::fill([1,NONE,NONE,NONE,NONE], 10, 1); + RETURN array::fill([1,NONE,3,4,5], 10, 1, 2); + RETURN array::fill([1,2,3,4,5], 10, 1, 1); + RETURN array::fill([1,2,3,4,5], 10, 7, 7); + RETURN array::fill([1,2,3,4,5], 10, 7, 9); + RETURN array::fill([1,2,NONE,4,5], 10, -3, -2); + "#; + // + Test::new(sql) + .await? + .expect_val("[10,10,10,10,10]")? + .expect_val("[10,10,10,10,10]")? + .expect_val("[1,10,10,10,10]")? + .expect_val("[1,10,3,4,5]")? + .expect_val("[1,2,3,4,5]")? + .expect_val("[1,2,3,4,5]")? + .expect_val("[1,2,3,4,5]")? + .expect_val("[1,2,10,4,5]")?; + Ok(()) +} + #[tokio::test] async fn function_array_filter_index() -> Result<(), Error> { let sql = r#"RETURN array::filter_index([0, 1, 2], 1); @@ -448,6 +474,17 @@ async fn function_array_intersect() -> Result<(), Error> { Ok(()) } +#[tokio::test] +async fn function_array_is_empty() -> Result<(), Error> { + let sql = r#" + RETURN array::is_empty([]); + RETURN array::is_empty([1,2,3,4,5]); + "#; + // + Test::new(sql).await?.expect_val("true")?.expect_val("false")?; + Ok(()) +} + #[tokio::test] async fn function_string_join_arr() -> Result<(), Error> { let sql = r#" @@ -567,6 +604,16 @@ RETURN array::logical_xor([0, 1], []);"#, Ok(()) } +#[tokio::test] +async fn function_array_map() -> Result<(), Error> { + let sql = r#" + RETURN array::map([1,2,3], |$n, $i| $n + $i); + "#; + // + Test::new(sql).await?.expect_val("[1, 3, 5]")?; + Ok(()) +} + #[tokio::test] async fn function_array_matches() -> Result<(), Error> { test_queries( @@ -729,6 +776,27 @@ async fn function_array_push() -> Result<(), Error> { Ok(()) } +#[tokio::test] +async fn function_array_range() -> Result<(), Error> { + let sql = r#" + RETURN array::range(1, 10); + RETURN array::range(3, 1); + RETURN array::range(44, 0); + RETURN array::range(0, -1); + RETURN array::range(0, -256); + RETURN array::range(9223372036854775800, 100); + "#; + // + Test::new(sql).await? + .expect_val("[1,2,3,4,5,6,7,8,9,10]")? + .expect_val("[3]")? + .expect_val("[]")? + .expect_error("Incorrect arguments for function array::range(). Argument 1 was the wrong type. Expected a positive number but found -1")? + .expect_error("Incorrect arguments for function array::range(). Argument 1 was the wrong type. Expected a positive number but found -256")? + .expect_error("Incorrect arguments for function array::range(). The range overflowed the maximum value for an integer")?; + Ok(()) +} + #[tokio::test] async fn function_array_remove() -> Result<(), Error> { let sql = r#" @@ -758,6 +826,27 @@ async fn function_array_remove() -> Result<(), Error> { Ok(()) } +#[tokio::test] +async fn function_array_repeat() -> Result<(), Error> { + let sql = r#" + RETURN array::repeat(1, 10); + RETURN array::repeat("hello", 2); + RETURN array::repeat(NONE, 3); + RETURN array::repeat(44, 0); + RETURN array::repeat(0, -1); + RETURN array::repeat(0, -256); + "#; + // + Test::new(sql).await? + .expect_val("[1,1,1,1,1,1,1,1,1,1]")? + .expect_val(r#"["hello","hello"]"#)? + .expect_val("[NONE,NONE,NONE]")? + .expect_val("[]")? + .expect_error("Incorrect arguments for function array::repeat(). Output must not exceed 1048576 bytes.")? + .expect_error("Incorrect arguments for function array::repeat(). Output must not exceed 1048576 bytes.")?; + Ok(()) +} + #[tokio::test] async fn function_array_reverse() -> Result<(), Error> { let sql = r#" @@ -986,6 +1075,27 @@ async fn function_array_sort_desc() -> Result<(), Error> { Ok(()) } +#[tokio::test] +async fn function_array_swap() -> Result<(), Error> { + let sql = r#" + RETURN array::swap([1,2,3,4,5], 1, 2); + RETURN array::swap([1,2,3,4,5], 1, 1); + RETURN array::swap([1,2,3,4,5], -1, -2); + RETURN array::swap([1,2,3,4,5], -5, -4); + RETURN array::swap([1,2,3,4,5], 8, 1); + RETURN array::swap([1,2,3,4,5], 1, -8); + "#; + // + Test::new(sql).await? + .expect_val("[1,3,2,4,5]")? + .expect_val("[1,2,3,4,5]")? + .expect_val("[1,2,3,5,4]")? + .expect_val("[2,1,3,4,5]")? + .expect_error("Incorrect arguments for function array::swap(). Argument 1 is out of range. Expected a number between -5 and 5")? + .expect_error("Incorrect arguments for function array::swap(). Argument 2 is out of range. Expected a number between -5 and 5")?; + Ok(()) +} + #[tokio::test] async fn function_array_transpose() -> Result<(), Error> { let sql = r#" @@ -6086,7 +6196,7 @@ pub async fn function_http_get_from_script() -> Result<(), Error> { #[cfg(not(feature = "http"))] #[tokio::test] -pub async fn function_http_disabled() { +pub async fn function_http_disabled() -> Result<(), Error> { Test::new( r#" RETURN http::get({}); @@ -6097,8 +6207,7 @@ pub async fn function_http_disabled() { RETURN http::delete({}); "#, ) - .await - .unwrap() + .await? .expect_errors(&[ "Remote HTTP request functions are not enabled", "Remote HTTP request functions are not enabled", @@ -6106,8 +6215,8 @@ pub async fn function_http_disabled() { "Remote HTTP request functions are not enabled", "Remote HTTP request functions are not enabled", "Remote HTTP request functions are not enabled", - ]) - .unwrap(); + ])?; + Ok(()) } // Tests for custom defined functions