From 93a9ba3cb66730bcef962c892ed8291876c044d4 Mon Sep 17 00:00:00 2001 From: Tobie Morgan Hitchcock Date: Tue, 27 Aug 2024 16:34:40 +0100 Subject: [PATCH] Improve `array` function implementations, and improve `value::diff()` and `value::patch()` functions (#4612) --- core/src/fnc/array.rs | 198 +++++++++++++--- core/src/fnc/mod.rs | 215 ++++++++++-------- .../modules/surrealdb/functions/array.rs | 17 +- .../modules/surrealdb/functions/string.rs | 4 +- .../modules/surrealdb/functions/value.rs | 7 +- core/src/fnc/shared.rs | 16 -- core/src/fnc/string.rs | 19 +- core/src/fnc/value.rs | 47 +++- core/src/sql/array.rs | 6 + core/src/sql/value/value.rs | 11 + core/src/syn/parser/builtin.rs | 14 +- sdk/fuzz/fuzz_targets/fuzz_executor.dict | 10 +- sdk/fuzz/fuzz_targets/fuzz_sql_parser.dict | 10 +- sdk/tests/function.rs | 56 +++-- 14 files changed, 443 insertions(+), 187 deletions(-) delete mode 100644 core/src/fnc/shared.rs diff --git a/core/src/fnc/array.rs b/core/src/fnc/array.rs index 08e3323c..fce0e676 100644 --- a/core/src/fnc/array.rs +++ b/core/src/fnc/array.rs @@ -18,7 +18,6 @@ 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; @@ -54,12 +53,56 @@ pub fn add((mut array, value): (Array, Value)) -> Result { } } -pub fn all((array,): (Array,)) -> Result { - Ok(array.iter().all(Value::is_truthy).into()) +pub async fn all( + (stk, ctx, opt, doc): (&mut Stk, &Context, Option<&Options>, Option<&CursorDoc>), + (array, check): (Array, Option), +) -> Result { + Ok(match check { + Some(closure) if closure.is_closure() => { + if let Some(opt) = opt { + for val in array.iter() { + let arg = val.compute(stk, ctx, opt, doc).await?; + let fnc = Function::Anonymous(closure.clone(), vec![arg]); + if fnc.compute(stk, ctx, opt, doc).await?.is_truthy() { + continue; + } else { + return Ok(Value::Bool(false)); + } + } + Value::Bool(true) + } else { + Value::None + } + } + Some(value) => array.iter().all(|v: &Value| *v == value).into(), + None => array.iter().all(Value::is_truthy).into(), + }) } -pub fn any((array,): (Array,)) -> Result { - Ok(array.iter().any(Value::is_truthy).into()) +pub async fn any( + (stk, ctx, opt, doc): (&mut Stk, &Context, Option<&Options>, Option<&CursorDoc>), + (array, check): (Array, Option), +) -> Result { + Ok(match check { + Some(closure) if closure.is_closure() => { + if let Some(opt) = opt { + for val in array.iter() { + let arg = val.compute(stk, ctx, opt, doc).await?; + let fnc = Function::Anonymous(closure.clone(), vec![arg]); + if fnc.compute(stk, ctx, opt, doc).await?.is_truthy() { + return Ok(Value::Bool(true)); + } else { + continue; + } + } + Value::Bool(false) + } else { + Value::None + } + } + Some(value) => array.iter().any(|v: &Value| *v == value).into(), + None => array.iter().any(Value::is_truthy).into(), + }) } pub fn append((mut array, value): (Array, Value)) -> Result { @@ -194,27 +237,119 @@ pub fn fill( Ok(array.into()) } -pub fn filter_index((array, value): (Array, Value)) -> Result { - Ok(array - .iter() - .enumerate() - .filter_map(|(i, v)| { - if *v == value { - Some(Value::from(i)) +pub async fn filter( + (stk, ctx, opt, doc): (&mut Stk, &Context, Option<&Options>, Option<&CursorDoc>), + (array, check): (Array, Value), +) -> Result { + Ok(match check { + closure if closure.is_closure() => { + if let Some(opt) = opt { + let mut res = Vec::with_capacity(array.len()); + for val in array.iter() { + let arg = val.compute(stk, ctx, opt, doc).await?; + let fnc = Function::Anonymous(closure.clone(), vec![arg.clone()]); + if fnc.compute(stk, ctx, opt, doc).await?.is_truthy() { + res.push(arg) + } + } + Value::from(res) } else { - None + Value::None } - }) - .collect::>() - .into()) + } + value => array.into_iter().filter(|v: &Value| *v == value).collect::>().into(), + }) } -pub fn find_index((array, value): (Array, Value)) -> Result { - Ok(array - .iter() - .enumerate() - .find(|(_i, v)| **v == value) - .map_or(Value::Null, |(i, _v)| i.into())) +pub async fn filter_index( + (stk, ctx, opt, doc): (&mut Stk, &Context, Option<&Options>, Option<&CursorDoc>), + (array, value): (Array, Value), +) -> Result { + Ok(match value { + closure if closure.is_closure() => { + if let Some(opt) = opt { + let mut res = Vec::with_capacity(array.len()); + for (i, val) in array.iter().enumerate() { + let arg = val.compute(stk, ctx, opt, doc).await?; + let fnc = Function::Anonymous(closure.clone(), vec![arg, i.into()]); + if fnc.compute(stk, ctx, opt, doc).await?.is_truthy() { + res.push(i); + } + } + Value::from(res) + } else { + Value::None + } + } + value => array + .iter() + .enumerate() + .filter_map(|(i, v)| { + if *v == value { + Some(Value::from(i)) + } else { + None + } + }) + .collect::>() + .into(), + }) +} + +pub async fn find( + (stk, ctx, opt, doc): (&mut Stk, &Context, Option<&Options>, Option<&CursorDoc>), + (array, value): (Array, Value), +) -> Result { + Ok(match value { + closure if closure.is_closure() => { + if let Some(opt) = opt { + for val in array.iter() { + let arg = val.compute(stk, ctx, opt, doc).await?; + let fnc = Function::Anonymous(closure.clone(), vec![arg.clone()]); + if fnc.compute(stk, ctx, opt, doc).await?.is_truthy() { + return Ok(arg); + } + } + Value::None + } else { + Value::None + } + } + value => array.into_iter().find(|v: &Value| *v == value).into(), + }) +} + +pub async fn find_index( + (stk, ctx, opt, doc): (&mut Stk, &Context, Option<&Options>, Option<&CursorDoc>), + (array, value): (Array, Value), +) -> Result { + Ok(match value { + closure if closure.is_closure() => { + if let Some(opt) = opt { + for (i, val) in array.iter().enumerate() { + let arg = val.compute(stk, ctx, opt, doc).await?; + let fnc = Function::Anonymous(closure.clone(), vec![arg, i.into()]); + if fnc.compute(stk, ctx, opt, doc).await?.is_truthy() { + return Ok(i.into()); + } + } + Value::None + } else { + Value::None + } + } + value => array + .iter() + .enumerate() + .find_map(|(i, v)| { + if *v == value { + Some(Value::from(i)) + } else { + None + } + }) + .into(), + }) } pub fn first((array,): (Array,)) -> Result { @@ -353,17 +488,20 @@ pub fn logical_xor((lh, rh): (Array, Array)) -> Result { } pub async fn map( - (stk, ctx, opt, doc): (&mut Stk, &Context, &Options, Option<&CursorDoc>), + (stk, ctx, opt, doc): (&mut Stk, &Context, Option<&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?; + if let Some(opt) = opt { + let mut res = Vec::with_capacity(array.len()); + for (i, val) in array.into_iter().enumerate() { + let arg = val.compute(stk, ctx, opt, doc).await?; + let fnc = Function::Anonymous(mapper.clone().into(), vec![arg, i.into()]); + res.push(fnc.compute(stk, ctx, opt, doc).await?); + } + Ok(res.into()) + } else { + Ok(Value::None) } - - Ok(array.into()) } pub fn matches((array, compare_val): (Array, Value)) -> Result { diff --git a/core/src/fnc/mod.rs b/core/src/fnc/mod.rs index c15b7e3a..7ff47f45 100644 --- a/core/src/fnc/mod.rs +++ b/core/src/fnc/mod.rs @@ -27,7 +27,6 @@ pub mod record; pub mod script; pub mod search; pub mod session; -pub mod shared; pub mod sleep; pub mod string; pub mod time; @@ -46,16 +45,28 @@ pub async fn run( args: Vec, ) -> Result { if name.eq("sleep") - || name.starts_with("search") + || name.eq("array::all") + || name.eq("array::any") + || name.eq("array::every") + || name.eq("array::filter_index") + || name.eq("array::filter") + || name.eq("array::find_index") + || name.eq("array::find") + || name.eq("array::includes") + || name.eq("array::index_of") + || name.eq("array::map") + || name.eq("array::some") + || name.eq("record::exists") + || name.eq("type::field") + || name.eq("type::fields") + || name.eq("value::diff") + || name.eq("value::patch") || name.starts_with("http") - || name.starts_with("type::field") - || name.starts_with("type::fields") + || name.starts_with("search") || name.starts_with("crypto::argon2") || name.starts_with("crypto::bcrypt") || name.starts_with("crypto::pbkdf2") || name.starts_with("crypto::scrypt") - || name.starts_with("array::map") - || name.starts_with("record::exists") { stk.run(|stk| asynchronous(stk, ctx, opt, doc, name, args)).await } else { @@ -99,9 +110,8 @@ pub fn synchronous( name, args, "no such builtin function found", + // "array::add" => array::add, - "array::all" => array::all, - "array::any" => array::any, "array::append" => array::append, "array::at" => array::at, "array::boolean_and" => array::boolean_and, @@ -115,8 +125,6 @@ pub fn synchronous( "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, "array::flatten" => array::flatten, "array::group" => array::group, @@ -276,7 +284,7 @@ pub fn synchronous( // "string::concat" => string::concat, "string::contains" => string::contains, - "string::endsWith" => string::ends_with, + "string::ends_with" => string::ends_with, "string::join" => string::join, "string::len" => string::len, "string::lowercase" => string::lowercase, @@ -287,7 +295,7 @@ pub fn synchronous( "string::slice" => string::slice, "string::slug" => string::slug, "string::split" => string::split, - "string::startsWith" => string::starts_with, + "string::starts_with" => string::starts_with, "string::trim" => string::trim, "string::uppercase" => string::uppercase, "string::words" => string::words, @@ -395,9 +403,6 @@ pub fn synchronous( "type::is::string" => r#type::is::string, "type::is::uuid" => r#type::is::uuid, // - "value::diff" => value::diff, - "value::patch" => value::patch, - // "vector::add" => vector::add, "vector::angle" => vector::angle, "vector::cross" => vector::cross, @@ -423,6 +428,81 @@ pub fn synchronous( ) } +/// Attempts to run any asynchronous function. +pub async fn asynchronous( + stk: &mut Stk, + ctx: &Context, + opt: &Options, + doc: Option<&CursorDoc>, + name: &str, + args: Vec, +) -> Result { + // Wrappers return a function as opposed to a value so that the dispatch! method can always + // perform a function call. + #[cfg(not(target_arch = "wasm32"))] + fn cpu_intensive( + function: impl FnOnce() -> R + Send + 'static, + ) -> impl FnOnce() -> executor::Task { + || crate::exe::spawn(async move { function() }) + } + + #[cfg(target_arch = "wasm32")] + fn cpu_intensive( + function: impl FnOnce() -> R + Send + 'static, + ) -> impl FnOnce() -> std::future::Ready { + || std::future::ready(function()) + } + + dispatch!( + name, + args, + "no such builtin function found", + // + "array::all" => array::all((stk, ctx, Some(opt), doc)).await, + "array::any" => array::any((stk, ctx, Some(opt), doc)).await, + "array::every" => array::all((stk, ctx, Some(opt), doc)).await, + "array::filter" => array::filter((stk, ctx, Some(opt), doc)).await, + "array::filter_index" => array::filter_index((stk, ctx, Some(opt), doc)).await, + "array::find" => array::find((stk, ctx, Some(opt), doc)).await, + "array::find_index" => array::find_index((stk, ctx, Some(opt), doc)).await, + "array::includes" => array::any((stk, ctx, Some(opt), doc)).await, + "array::index_of" => array::find_index((stk, ctx, Some(opt), doc)).await, + "array::map" => array::map((stk, ctx, Some(opt), doc)).await, + "array::some" => array::any((stk, ctx, Some(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, + "crypto::bcrypt::generate" => (cpu_intensive) crypto::bcrypt::gen.await, + "crypto::pbkdf2::compare" => (cpu_intensive) crypto::pbkdf2::cmp.await, + "crypto::pbkdf2::generate" => (cpu_intensive) crypto::pbkdf2::gen.await, + "crypto::scrypt::compare" => (cpu_intensive) crypto::scrypt::cmp.await, + "crypto::scrypt::generate" => (cpu_intensive) crypto::scrypt::gen.await, + // + "http::head" => http::head(ctx).await, + "http::get" => http::get(ctx).await, + "http::put" => http::put(ctx).await, + "http::post" => http::post(ctx).await, + "http::patch" => http::patch(ctx).await, + "http::delete" => http::delete(ctx).await, + // + "record::exists" => record::exists((stk, ctx, Some(opt), doc)).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, Some(opt), doc)).await, + "type::fields" => r#type::fields((stk, ctx, Some(opt), doc)).await, + // + "value::diff" => value::diff((stk, ctx, Some(opt), doc)).await, + "value::patch" => value::patch((stk, ctx, Some(opt), doc)).await, + ) +} + /// Attempts to run any synchronous function. pub async fn idiom( stk: &mut Stk, @@ -440,9 +520,10 @@ pub async fn idiom( name, args.clone(), "no such method found for the array type", + // "add" => array::add, - "all" => array::all, - "any" => array::any, + "all" => array::all((stk, ctx, Some(opt), doc)).await, + "any" => array::any((stk, ctx, Some(opt), doc)).await, "append" => array::append, "at" => array::at, "boolean_and" => array::boolean_and, @@ -455,12 +536,17 @@ pub async fn idiom( "concat" => array::concat, "difference" => array::difference, "distinct" => array::distinct, + "every" => array::all((stk, ctx, Some(opt), doc)).await, "fill" => array::fill, - "filter_index" => array::filter_index, - "find_index" => array::find_index, + "filter" => array::filter((stk, ctx, Some(opt), doc)).await, + "filter_index" => array::filter_index((stk, ctx, Some(opt), doc)).await, + "find" => array::find((stk, ctx, Some(opt), doc)).await, + "find_index" => array::find_index((stk, ctx, Some(opt), doc)).await, "first" => array::first, "flatten" => array::flatten, "group" => array::group, + "includes" => array::any((stk, ctx, Some(opt), doc)).await, + "index_of" => array::find_index((stk, ctx, Some(opt), doc)).await, "insert" => array::insert, "intersect" => array::intersect, "is_empty" => array::is_empty, @@ -471,7 +557,7 @@ pub async fn idiom( "logical_or" => array::logical_or, "logical_xor" => array::logical_xor, "matches" => array::matches, - "map" => array::map((stk, ctx, opt, doc)).await, + "map" => array::map((stk, ctx, Some(opt), doc)).await, "max" => array::max, "min" => array::min, "pop" => array::pop, @@ -481,6 +567,7 @@ pub async fn idiom( "reverse" => array::reverse, "shuffle" => array::shuffle, "slice" => array::slice, + "some" => array::any((stk, ctx, Some(opt), doc)).await, "sort" => array::sort, "swap" => array::swap, "transpose" => array::transpose, @@ -518,6 +605,7 @@ pub async fn idiom( name, args.clone(), "no such method found for the bytes type", + // "len" => bytes::len, ) } @@ -526,6 +614,7 @@ pub async fn idiom( name, args.clone(), "no such method found for the duration type", + // "days" => duration::days, "hours" => duration::hours, "micros" => duration::micros, @@ -542,6 +631,7 @@ pub async fn idiom( name, args.clone(), "no such method found for the geometry type", + // "area" => geo::area, "bearing" => geo::bearing, "centroid" => geo::centroid, @@ -555,6 +645,7 @@ pub async fn idiom( name, args.clone(), "no such method found for the record type", + // "exists" => record::exists((stk, ctx, Some(opt), doc)).await, "id" => record::id, "table" => record::tb, @@ -566,6 +657,7 @@ pub async fn idiom( name, args.clone(), "no such method found for the object type", + // "entries" => object::entries, "keys" => object::keys, "len" => object::len, @@ -577,9 +669,10 @@ pub async fn idiom( name, args.clone(), "no such method found for the string type", + // "concat" => string::concat, "contains" => string::contains, - "endsWith" => string::ends_with, + "ends_with" => string::ends_with, "join" => string::join, "len" => string::len, "lowercase" => string::lowercase, @@ -590,7 +683,7 @@ pub async fn idiom( "slice" => string::slice, "slug" => string::slug, "split" => string::split, - "startsWith" => string::starts_with, + "starts_with" => string::starts_with, "trim" => string::trim, "uppercase" => string::uppercase, "words" => string::words, @@ -635,6 +728,7 @@ pub async fn idiom( name, args.clone(), "no such method found for the datetime type", + // "ceil" => time::ceil, "day" => time::day, "floor" => time::floor, @@ -711,79 +805,17 @@ pub async fn idiom( "to_string" => r#type::string, "to_uuid" => r#type::uuid, // - "diff" => value::diff, - "patch" => value::patch, + "chain" => value::chain((stk, ctx, Some(opt), doc)).await, + "diff" => value::diff((stk, ctx, Some(opt), doc)).await, + "patch" => value::patch((stk, ctx, Some(opt), doc)).await, // "repeat" => array::repeat, - // - "chain" => shared::chain((stk, ctx, opt, doc)).await, ) } v => v, } } -/// Attempts to run any asynchronous function. -pub async fn asynchronous( - stk: &mut Stk, - ctx: &Context, - opt: &Options, - doc: Option<&CursorDoc>, - name: &str, - args: Vec, -) -> Result { - // Wrappers return a function as opposed to a value so that the dispatch! method can always - // perform a function call. - #[cfg(not(target_arch = "wasm32"))] - fn cpu_intensive( - function: impl FnOnce() -> R + Send + 'static, - ) -> impl FnOnce() -> executor::Task { - || crate::exe::spawn(async move { function() }) - } - - #[cfg(target_arch = "wasm32")] - fn cpu_intensive( - function: impl FnOnce() -> R + Send + 'static, - ) -> impl FnOnce() -> std::future::Ready { - || std::future::ready(function()) - } - - dispatch!( - 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, - "crypto::bcrypt::generate" => (cpu_intensive) crypto::bcrypt::gen.await, - "crypto::pbkdf2::compare" => (cpu_intensive) crypto::pbkdf2::cmp.await, - "crypto::pbkdf2::generate" => (cpu_intensive) crypto::pbkdf2::gen.await, - "crypto::scrypt::compare" => (cpu_intensive) crypto::scrypt::cmp.await, - "crypto::scrypt::generate" => (cpu_intensive) crypto::scrypt::gen.await, - // - "http::head" => http::head(ctx).await, - "http::get" => http::get(ctx).await, - "http::put" => http::put(ctx).await, - "http::post" => http::post(ctx).await, - "http::patch" => http::patch(ctx).await, - "http::delete" => http::delete(ctx).await, - // - "record::exists" => record::exists((stk, ctx, Some(opt), doc)).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, Some(opt), doc)).await, - "type::fields" => r#type::fields((stk, ctx, Some(opt), doc)).await, - ) -} - fn get_execution_context<'a>( ctx: &'a Context, doc: Option<&'a CursorDoc>, @@ -810,9 +842,6 @@ 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(); @@ -820,7 +849,7 @@ mod tests { let fnc_mod = include_str!("mod.rs"); // Patch out idiom methods - let re = Regex::new(r"(?ms)pub async fn idiom\(.*}\n+///").unwrap(); + let re = Regex::new(r"(?ms)pub async fn idiom\(.*}").unwrap(); let fnc_no_idiom = re.replace(fnc_mod, ""); for line in fnc_no_idiom.lines() { @@ -863,10 +892,6 @@ 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 1f240dd3..550917b7 100644 --- a/core/src/fnc/script/modules/surrealdb/functions/array.rs +++ b/core/src/fnc/script/modules/surrealdb/functions/array.rs @@ -1,5 +1,7 @@ +use super::fut; use super::run; use crate::fnc::script::modules::impl_module_def; +use js::prelude::Async; mod sort; #[non_exhaustive] @@ -9,8 +11,8 @@ impl_module_def!( Package, "array", "add" => run, - "all" => run, - "any" => run, + "all" => fut Async, + "any" => fut Async, "at" => run, "append" => run, "boolean_and" => run, @@ -23,12 +25,17 @@ impl_module_def!( "concat" => run, "difference" => run, "distinct" => run, + "every" => fut Async, "fill" => run, - "filter_index" => run, - "find_index" => run, + "filter" => fut Async, + "filter_index" => fut Async, + "find" => fut Async, + "find_index" => fut Async, "first" => run, "flatten" => run, "group" => run, + "includes" => fut Async, + "index_of" => fut Async, "insert" => run, "intersect" => run, "is_empty" => run, @@ -39,6 +46,7 @@ impl_module_def!( "logical_and" => run, "logical_or" => run, "logical_xor" => run, + "map" => fut Async, "matches" => run, "max" => run, "min" => run, @@ -51,6 +59,7 @@ impl_module_def!( "reverse" => run, "shuffle" => run, "slice" => run, + "some" => fut Async, "sort" => (sort::Package), "swap" => run, "transpose" => run, diff --git a/core/src/fnc/script/modules/surrealdb/functions/string.rs b/core/src/fnc/script/modules/surrealdb/functions/string.rs index 0ff32143..e6d79daf 100644 --- a/core/src/fnc/script/modules/surrealdb/functions/string.rs +++ b/core/src/fnc/script/modules/surrealdb/functions/string.rs @@ -16,7 +16,7 @@ impl_module_def!( "concat" => run, "contains" => run, "distance" => (distance::Package), - "endsWith" => run, + "ends_with" => run, "html" => (html::Package), "is" => (is::Package), "join" => run, @@ -30,7 +30,7 @@ impl_module_def!( "slice" => run, "slug" => run, "split" => run, - "startsWith" => run, + "starts_with" => run, "trim" => run, "uppercase" => run, "words" => run, diff --git a/core/src/fnc/script/modules/surrealdb/functions/value.rs b/core/src/fnc/script/modules/surrealdb/functions/value.rs index a6b38085..7156a681 100644 --- a/core/src/fnc/script/modules/surrealdb/functions/value.rs +++ b/core/src/fnc/script/modules/surrealdb/functions/value.rs @@ -1,5 +1,6 @@ -use super::run; +use super::fut; use crate::fnc::script::modules::impl_module_def; +use js::prelude::Async; #[non_exhaustive] pub struct Package; @@ -7,6 +8,6 @@ pub struct Package; impl_module_def!( Package, "value", - "diff" => run, - "patch" => run + "diff" => fut Async, + "patch" => fut Async ); diff --git a/core/src/fnc/shared.rs b/core/src/fnc/shared.rs deleted file mode 100644 index e8e4177c..00000000 --- a/core/src/fnc/shared.rs +++ /dev/null @@ -1,16 +0,0 @@ -use reblessive::tree::Stk; - -use crate::ctx::Context; -use crate::dbs::Options; -use crate::doc::CursorDoc; -use crate::err::Error; -use crate::sql::value::Value; -use crate::sql::{Closure, Function}; - -pub async fn chain( - (stk, ctx, opt, doc): (&mut Stk, &Context, &Options, Option<&CursorDoc>), - (value, worker): (Value, Closure), -) -> Result { - let fnc = Function::Anonymous(worker.into(), vec![value]); - fnc.compute(stk, ctx, opt, doc).await -} diff --git a/core/src/fnc/string.rs b/core/src/fnc/string.rs index 208f15b3..be870b7d 100644 --- a/core/src/fnc/string.rs +++ b/core/src/fnc/string.rs @@ -69,24 +69,25 @@ pub fn matches((val, regex): (String, Regex)) -> Result { Ok(regex.0.is_match(&val).into()) } -pub fn replace((val, old_or_regexp, new): (String, Value, String)) -> Result { - match old_or_regexp { - Value::Strand(old) => { - if new.len() > old.len() { - let increase = new.len() - old.len(); +pub fn replace((val, search, replace): (String, Value, String)) -> Result { + match search { + Value::Strand(search) => { + if replace.len() > search.len() { + let increase = replace.len() - search.len(); limit( "string::replace", - val.len().saturating_add(val.matches(&old.0).count().saturating_mul(increase)), + val.len() + .saturating_add(val.matches(&search.0).count().saturating_mul(increase)), )?; } - Ok(val.replace(&old.0, &new).into()) + Ok(val.replace(&search.0, &replace).into()) } - Value::Regex(r) => Ok(r.0.replace_all(&val, new).into_owned().into()), + Value::Regex(search) => Ok(search.0.replace_all(&val, replace).into_owned().into()), _ => Err(Error::InvalidArguments { name: "string::replace".to_string(), message: format!( "Argument 2 was the wrong type. Expected a string but found {}", - old_or_regexp + search ), }), } diff --git a/core/src/fnc/value.rs b/core/src/fnc/value.rs index 72060dad..e615dd9d 100644 --- a/core/src/fnc/value.rs +++ b/core/src/fnc/value.rs @@ -1,11 +1,46 @@ +use crate::ctx::Context; +use crate::dbs::Options; +use crate::doc::CursorDoc; use crate::err::Error; -use crate::sql::{Idiom, Value}; +use crate::sql::idiom::Idiom; +use crate::sql::value::Value; +use crate::sql::{Closure, Function}; +use reblessive::tree::Stk; -pub fn diff((val1, val2): (Value, Value)) -> Result { - Ok(val1.diff(&val2, Idiom::default()).into()) +pub async fn chain( + (stk, ctx, opt, doc): (&mut Stk, &Context, Option<&Options>, Option<&CursorDoc>), + (value, worker): (Value, Closure), +) -> Result { + if let Some(opt) = opt { + let fnc = Function::Anonymous(worker.into(), vec![value]); + fnc.compute(stk, ctx, opt, doc).await + } else { + Ok(Value::None) + } } -pub fn patch((mut val, diff): (Value, Value)) -> Result { - val.patch(diff)?; - Ok(val) +pub async fn diff( + (stk, ctx, opt, doc): (&mut Stk, &Context, Option<&Options>, Option<&CursorDoc>), + (val1, val2): (Value, Value), +) -> Result { + if let Some(opt) = opt { + let val1 = val1.compute(stk, ctx, opt, doc).await?; + let val2 = val2.compute(stk, ctx, opt, doc).await?; + Ok(val1.diff(&val2, Idiom::default()).into()) + } else { + Ok(Value::None) + } +} + +pub async fn patch( + (stk, ctx, opt, doc): (&mut Stk, &Context, Option<&Options>, Option<&CursorDoc>), + (val, diff): (Value, Value), +) -> Result { + if let Some(opt) = opt { + let mut val = val.compute(stk, ctx, opt, doc).await?; + val.patch(diff)?; + Ok(val) + } else { + Ok(Value::None) + } } diff --git a/core/src/sql/array.rs b/core/src/sql/array.rs index 9431690e..ae13f50a 100644 --- a/core/src/sql/array.rs +++ b/core/src/sql/array.rs @@ -54,6 +54,12 @@ impl From> for Array { } } +impl From> for Array { + fn from(v: Vec) -> Self { + Self(v.into_iter().map(Value::from).collect()) + } +} + impl From> for Array { fn from(v: Vec<&str>) -> Self { Self(v.into_iter().map(Value::from).collect()) diff --git a/core/src/sql/value/value.rs b/core/src/sql/value/value.rs index e0b900f8..40f0b773 100644 --- a/core/src/sql/value/value.rs +++ b/core/src/sql/value/value.rs @@ -500,6 +500,12 @@ impl From> for Value { } } +impl From> for Value { + fn from(v: Vec) -> Self { + Value::Array(Array::from(v)) + } +} + impl From> for Value { fn from(v: Vec) -> Self { Value::Array(Array::from(v)) @@ -1057,6 +1063,11 @@ impl Value { matches!(self, Value::Thing(_)) } + /// Check if this Value is a Closure + pub fn is_closure(&self) -> bool { + matches!(self, Value::Closure(_)) + } + /// Check if this Value is a Thing, and belongs to a certain table pub fn is_record_of_table(&self, table: String) -> bool { match self { diff --git a/core/src/syn/parser/builtin.rs b/core/src/syn/parser/builtin.rs index 92433cb3..76290a3f 100644 --- a/core/src/syn/parser/builtin.rs +++ b/core/src/syn/parser/builtin.rs @@ -97,12 +97,17 @@ 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::every") => PathKind::Function, UniCase::ascii("array::fill") => PathKind::Function, + UniCase::ascii("array::filter") => PathKind::Function, UniCase::ascii("array::filter_index") => PathKind::Function, + UniCase::ascii("array::find") => PathKind::Function, UniCase::ascii("array::find_index") => PathKind::Function, UniCase::ascii("array::first") => PathKind::Function, UniCase::ascii("array::flatten") => PathKind::Function, UniCase::ascii("array::group") => PathKind::Function, + UniCase::ascii("array::includes") => PathKind::Function, + UniCase::ascii("array::index_of") => PathKind::Function, UniCase::ascii("array::insert") => PathKind::Function, UniCase::ascii("array::intersect") => PathKind::Function, UniCase::ascii("array::is_empty") => PathKind::Function, @@ -125,13 +130,14 @@ pub(crate) static PATHS: phf::Map, PathKind> = phf_map! { UniCase::ascii("array::reverse") => PathKind::Function, UniCase::ascii("array::shuffle") => PathKind::Function, UniCase::ascii("array::slice") => PathKind::Function, - UniCase::ascii("array::sort::asc") => PathKind::Function, - UniCase::ascii("array::sort::desc") => PathKind::Function, + UniCase::ascii("array::some") => 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::windows") => PathKind::Function, + UniCase::ascii("array::sort::asc") => PathKind::Function, + UniCase::ascii("array::sort::desc") => PathKind::Function, // UniCase::ascii("bytes::len") => PathKind::Function, // @@ -284,7 +290,7 @@ pub(crate) static PATHS: phf::Map, PathKind> = phf_map! { // UniCase::ascii("string::concat") => PathKind::Function, UniCase::ascii("string::contains") => PathKind::Function, - UniCase::ascii("string::endsWith") => PathKind::Function, + UniCase::ascii("string::ends_with") => PathKind::Function, UniCase::ascii("string::join") => PathKind::Function, UniCase::ascii("string::len") => PathKind::Function, UniCase::ascii("string::lowercase") => PathKind::Function, @@ -294,7 +300,7 @@ pub(crate) static PATHS: phf::Map, PathKind> = phf_map! { UniCase::ascii("string::slice") => PathKind::Function, UniCase::ascii("string::slug") => PathKind::Function, UniCase::ascii("string::split") => PathKind::Function, - UniCase::ascii("string::startsWith") => PathKind::Function, + UniCase::ascii("string::starts_with") => PathKind::Function, UniCase::ascii("string::trim") => PathKind::Function, UniCase::ascii("string::uppercase") => PathKind::Function, UniCase::ascii("string::words") => PathKind::Function, diff --git a/sdk/fuzz/fuzz_targets/fuzz_executor.dict b/sdk/fuzz/fuzz_targets/fuzz_executor.dict index 442fdbd7..eb4105d1 100644 --- a/sdk/fuzz/fuzz_targets/fuzz_executor.dict +++ b/sdk/fuzz/fuzz_targets/fuzz_executor.dict @@ -166,12 +166,17 @@ "array::concat(" "array::difference(" "array::distinct(" +"array::every(" "array::fill(" +"array::filter(" "array::filter_index(" +"array::find(" "array::find_index(" "array::first(" "array::flatten(" "array::group(" +"array::includes(" +"array::index_of(" "array::insert(" "array::intersect(" "array::is_empty(" @@ -194,6 +199,7 @@ "array::reverse(" "array::shuffle(" "array::slice(" +"array::some(" "array::sort(" "array::sort::asc(" "array::sort::desc(" @@ -302,7 +308,7 @@ "string::contains(" "string::distance::hamming(" "string::distance::levenshtein(" -"string::endsWith(" +"string::ends_with(" "string::html::encode(" "string::html::sanitize(" "string::is::alphanum(" @@ -335,7 +341,7 @@ "string::slice(" "string::slug(" "string::split(" -"string::startsWith(" +"string::starts_with(" "string::trim(" "string::uppercase(" "string::words(" diff --git a/sdk/fuzz/fuzz_targets/fuzz_sql_parser.dict b/sdk/fuzz/fuzz_targets/fuzz_sql_parser.dict index f6627e6e..525287be 100644 --- a/sdk/fuzz/fuzz_targets/fuzz_sql_parser.dict +++ b/sdk/fuzz/fuzz_targets/fuzz_sql_parser.dict @@ -165,12 +165,17 @@ "array::concat(" "array::difference(" "array::distinct(" +"array::every(" "array::fill(" +"array::filter(" "array::filter_index(" +"array::find(" "array::find_index(" "array::first(" "array::flatten(" "array::group(" +"array::includes(" +"array::index_of(" "array::insert(" "array::intersect(" "array::is_empty(" @@ -193,6 +198,7 @@ "array::reverse(" "array::shuffle(" "array::slice(" +"array::some(" "array::sort(" "array::sort::asc(" "array::sort::desc(" @@ -300,7 +306,7 @@ "string::contains(" "string::distance::hamming(" "string::distance::levenshtein(" -"string::endsWith(" +"string::ends_with(" "string::html::encode(" "string::html::sanitize(" "string::is::alphanum(" @@ -333,7 +339,7 @@ "string::slice(" "string::slug(" "string::split(" -"string::startsWith(" +"string::starts_with(" "string::trim(" "string::uppercase(" "string::words(" diff --git a/sdk/tests/function.rs b/sdk/tests/function.rs index 5d37cd62..1f3bfd5d 100644 --- a/sdk/tests/function.rs +++ b/sdk/tests/function.rs @@ -305,23 +305,51 @@ async fn function_array_fill() -> Result<(), Error> { Ok(()) } +#[tokio::test] +async fn function_array_filter() -> Result<(), Error> { + let sql = r#" + RETURN array::filter([5, 7, 9], |$v| $v > 6); + RETURN array::filter(["hello_world", "goodbye world", "hello wombat", "goodbye world"], |$v| $v CONTAINS 'hello'); + RETURN array::filter(["nothing here"], |$v| $v == 3); + "#; + let desired_responses = ["[7, 9]", "['hello_world', 'hello wombat']", "[]"]; + test_queries(sql, &desired_responses).await?; + Ok(()) +} + #[tokio::test] async fn function_array_filter_index() -> Result<(), Error> { - let sql = r#"RETURN array::filter_index([0, 1, 2], 1); -RETURN array::filter_index([0, 0, 2], 0); -RETURN array::filter_index(["hello_world", "hello world", "hello wombat", "hello world"], "hello world"); -RETURN array::filter_index(["nothing here"], 0);"#; + let sql = r#" + RETURN array::filter_index([0, 1, 2], 1); + RETURN array::filter_index([0, 0, 2], 0); + RETURN array::filter_index(["hello_world", "hello world", "hello wombat", "hello world"], "hello world"); + RETURN array::filter_index(["nothing here"], 0); + "#; let desired_responses = ["[1]", "[0, 1]", "[1, 3]", "[]"]; test_queries(sql, &desired_responses).await?; Ok(()) } +#[tokio::test] +async fn function_array_find() -> Result<(), Error> { + let sql = r#" + RETURN array::find([5, 7, 9], |$v| $v >= 6); + RETURN array::find(["hello world", null, true], |$v| $v != NULL); + RETURN array::find([0, 1, 2], |$v| $v > 5); + "#; + let desired_responses = ["7", "'hello world'", "NONE"]; + test_queries(sql, &desired_responses).await?; + Ok(()) +} + #[tokio::test] async fn function_array_find_index() -> Result<(), Error> { - let sql = r#"RETURN array::find_index([5, 6, 7], 7); -RETURN array::find_index(["hello world", null, true], null); -RETURN array::find_index([0, 1, 2], 3);"#; - let desired_responses = ["2", "1", "null"]; + let sql = r#" + RETURN array::find_index([5, 6, 7], 7); + RETURN array::find_index(["hello world", null, true], null); + RETURN array::find_index([0, 1, 2], 3); + "#; + let desired_responses = ["2", "1", "NONE"]; test_queries(sql, &desired_responses).await?; Ok(()) } @@ -3375,9 +3403,9 @@ async fn function_string_contains() -> Result<(), Error> { #[tokio::test] async fn function_string_ends_with() -> Result<(), Error> { let sql = r#" - RETURN string::endsWith("", ""); - RETURN string::endsWith("", "test"); - RETURN string::endsWith("this is a test", "test"); + RETURN string::ends_with("", ""); + RETURN string::ends_with("", "test"); + RETURN string::ends_with("this is a test", "test"); "#; let mut test = Test::new(sql).await?; // @@ -4187,9 +4215,9 @@ async fn function_string_split() -> Result<(), Error> { #[tokio::test] async fn function_string_starts_with() -> Result<(), Error> { let sql = r#" - RETURN string::startsWith("", ""); - RETURN string::startsWith("", "test"); - RETURN string::startsWith("test this string", "test"); + RETURN string::starts_with("", ""); + RETURN string::starts_with("", "test"); + RETURN string::starts_with("test this string", "test"); "#; let mut test = Test::new(sql).await?; //