Add more array functions (#4184)

Co-authored-by: Micha de Vries <micha@devrie.sh>
Co-authored-by: Micha de Vries <mt.dev@hotmail.com>
Co-authored-by: Tobie Morgan Hitchcock <tobie@surrealdb.com>
This commit is contained in:
David Bottiau 2024-08-14 17:11:22 +02:00 committed by GitHub
parent ee8e6f00d7
commit 0069cba8a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 390 additions and 26 deletions

View file

@ -59,3 +59,11 @@ pub static EXPERIMENTAL_BEARER_ACCESS: Lazy<bool> =
// Run tests with bearer access enabled as it introduces new functionality that needs to be tested.
#[cfg(test)]
pub static EXPERIMENTAL_BEARER_ACCESS: Lazy<bool> = Lazy::new(|| true);
/// Used to limit allocation for builtin functions
pub static FUNCTION_ALLOCATION_LIMIT: Lazy<usize> = once_cell::sync::Lazy::new(|| {
let n = std::env::var("SURREAL_FUNCTION_ALLOCATION_LIMIT")
.map(|s| s.parse::<u32>().unwrap_or(20))
.unwrap_or(20);
2usize.pow(n)
});

View file

@ -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<Self, Error> {
arg.coerce_to_function()
}
}
impl FromArg for Regex {
fn from_arg(arg: Value) -> Result<Self, Error> {
arg.coerce_to_regex()
@ -242,6 +250,41 @@ impl<A: FromArg, B: FromArg> FromArgs for (A, Option<B>) {
}
}
// Some functions take 4 arguments, with the 3rd and 4th being optional.
impl<A: FromArg, B: FromArg, C: FromArg, D: FromArg> FromArgs for (A, B, Option<C>, Option<D>) {
fn from_args(name: &str, args: Vec<Value>) -> Result<Self, Error> {
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<T: FromArg, E: Fn() -> Error>(
name: &str,

View file

@ -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<Value, Error> {
match value {
@ -135,6 +155,45 @@ pub fn distinct((array,): (Array,)) -> Result<Value, Error> {
Ok(array.uniq().into())
}
pub fn fill(
(mut array, value, start, end): (Array, Value, Option<isize>, Option<isize>),
) -> Result<Value, Error> {
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<Value, Error> {
Ok(array
.iter()
@ -201,6 +260,10 @@ pub fn intersect((array, other): (Array, Array)) -> Result<Value, Error> {
Ok(array.intersect(other).into())
}
pub fn is_empty((array,): (Array,)) -> Result<Value, Error> {
Ok(array.is_empty().into())
}
pub fn join((arr, sep): (Array, String)) -> Result<Value, Error> {
Ok(arr.into_iter().map(Value::as_raw_string).collect::<Vec<_>>().join(&sep).into())
}
@ -289,6 +352,20 @@ pub fn logical_xor((lh, rh): (Array, Array)) -> Result<Value, Error> {
Ok(result_arr.into())
}
pub async fn map(
(stk, ctx, opt, doc): (&mut Stk, &Context<'_>, &Options, Option<&CursorDoc<'_>>),
(array, mapper): (Array, Closure),
) -> Result<Value, Error> {
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<Value, Error> {
Ok(array.matches(compare_val).into())
}
@ -315,6 +392,26 @@ pub fn push((mut array, value): (Array, Value)) -> Result<Value, Error> {
Ok(array.into())
}
pub fn range((start, count): (i64, i64)) -> Result<Value, Error> {
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::<Vec<_>>()).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<Value, Error> {
// Negative index means start from the back
if index < 0 {
@ -330,6 +427,11 @@ pub fn remove((mut array, mut index): (Array, i64)) -> Result<Value, Error> {
Ok(array.into())
}
pub fn repeat((value, count): (Value, usize)) -> Result<Value, Error> {
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<Value, Error> {
array.reverse();
Ok(array.into())
@ -337,7 +439,7 @@ pub fn reverse((mut array,): (Array,)) -> Result<Value, Error> {
pub fn shuffle((mut array,): (Array,)) -> Result<Value, Error> {
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<Value>)) -> Result<Value, Error>
}
}
pub fn swap((mut array, from, to): (Array, isize, isize)) -> Result<Value, Error> {
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<Value, Error> {
Ok(array.transpose().into())
}

View file

@ -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<Value>,
@ -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}; }}");

View file

@ -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

View file

@ -72,9 +72,8 @@ fn run(js_ctx: js::Ctx<'_>, name: &str, args: Vec<Value>) -> Result<Value> {
async fn fut(js_ctx: js::Ctx<'_>, name: &str, args: Vec<Value>) -> Result<Value> {
let this = js_ctx.globals().get::<_, OwnedBorrow<QueryContext>>(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)
})
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| {

View file

@ -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(())

View file

@ -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

View file

@ -97,6 +97,7 @@ pub(crate) static PATHS: phf::Map<UniCase<&'static str>, 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<UniCase<&'static str>, 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<UniCase<&'static str>, 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,

View file

@ -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("

View file

@ -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("

View file

@ -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