Method chaining ()

This commit is contained in:
Micha de Vries 2024-08-08 12:43:35 +01:00 committed by GitHub
parent 82349eb557
commit c5a6e08225
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 628 additions and 131 deletions

View file

@ -65,7 +65,7 @@ pub async fn run(
/// it is `async`. Finally, the path may be prefixed by a parenthesized wrapper function e.g.
/// `cpu_intensive`.
macro_rules! dispatch {
($name: ident, $args: ident, $($function_name: literal => $(($wrapper: tt))* $($function_path: ident)::+ $(($ctx_arg: expr))* $(.$await:tt)*,)+) => {
($name: ident, $args: expr, $message: expr, $($function_name: literal => $(($wrapper: tt))* $($function_path: ident)::+ $(($ctx_arg: expr))* $(.$await:tt)*,)+) => {
{
match $name {
$($function_name => {
@ -76,7 +76,7 @@ macro_rules! dispatch {
_ => {
return Err($crate::err::Error::InvalidFunction{
name: String::from($name),
message: "no such builtin function".to_string()
message: $message.to_string()
})
}
}
@ -94,6 +94,7 @@ pub fn synchronous(
dispatch!(
name,
args,
"no such builtin function found",
"array::add" => array::add,
"array::all" => array::all,
"array::any" => array::any,
@ -344,17 +345,21 @@ pub fn synchronous(
"time::from::unix" => time::from::unix,
//
"type::bool" => r#type::bool,
"type::bytes" => r#type::bytes,
"type::datetime" => r#type::datetime,
"type::decimal" => r#type::decimal,
"type::duration" => r#type::duration,
"type::float" => r#type::float,
"type::geometry" => r#type::geometry,
"type::int" => r#type::int,
"type::number" => r#type::number,
"type::point" => r#type::point,
"type::range" => r#type::range,
"type::record" => r#type::record,
"type::string" => r#type::string,
"type::table" => r#type::table,
"type::thing" => r#type::thing,
"type::range" => r#type::range,
"type::uuid" => r#type::uuid,
"type::is::array" => r#type::is::array,
"type::is::bool" => r#type::is::bool,
"type::is::bytes" => r#type::is::bytes,
@ -403,6 +408,288 @@ pub fn synchronous(
)
}
/// Attempts to run any synchronous function.
pub fn idiom(
ctx: &Context<'_>,
doc: Option<&CursorDoc<'_>>,
value: Value,
name: &str,
args: Vec<Value>,
) -> Result<Value, Error> {
let args = [vec![value.clone()], args].concat();
let specific = match value {
Value::Array(_) => {
dispatch!(
name,
args.clone(),
"no such method found for the array type",
"add" => array::add,
"all" => array::all,
"any" => array::any,
"append" => array::append,
"at" => array::at,
"boolean_and" => array::boolean_and,
"boolean_not" => array::boolean_not,
"boolean_or" => array::boolean_or,
"boolean_xor" => array::boolean_xor,
"clump" => array::clump,
"combine" => array::combine,
"complement" => array::complement,
"concat" => array::concat,
"difference" => array::difference,
"distinct" => array::distinct,
"filter_index" => array::filter_index,
"find_index" => array::find_index,
"first" => array::first,
"flatten" => array::flatten,
"group" => array::group,
"insert" => array::insert,
"intersect" => array::intersect,
"join" => array::join,
"last" => array::last,
"len" => array::len,
"logical_and" => array::logical_and,
"logical_or" => array::logical_or,
"logical_xor" => array::logical_xor,
"matches" => array::matches,
"max" => array::max,
"min" => array::min,
"pop" => array::pop,
"prepend" => array::prepend,
"push" => array::push,
"remove" => array::remove,
"reverse" => array::reverse,
"shuffle" => array::shuffle,
"slice" => array::slice,
"sort" => array::sort,
"transpose" => array::transpose,
"union" => array::union,
"sort_asc" => array::sort::asc,
"sort_desc" => array::sort::desc,
"windows" => array::windows,
//
"vector_add" => vector::add,
"vector_angle" => vector::angle,
"vector_cross" => vector::cross,
"vector_dot" => vector::dot,
"vector_divide" => vector::divide,
"vector_magnitude" => vector::magnitude,
"vector_multiply" => vector::multiply,
"vector_normalize" => vector::normalize,
"vector_project" => vector::project,
"vector_subtract" => vector::subtract,
"vector_distance_chebyshev" => vector::distance::chebyshev,
"vector_distance_euclidean" => vector::distance::euclidean,
"vector_distance_hamming" => vector::distance::hamming,
"vector_distance_knn" => vector::distance::knn((ctx, doc)),
"vector_distance_mahalanobis" => vector::distance::mahalanobis,
"vector_distance_manhattan" => vector::distance::manhattan,
"vector_distance_minkowski" => vector::distance::minkowski,
"vector_similarity_cosine" => vector::similarity::cosine,
"vector_similarity_jaccard" => vector::similarity::jaccard,
"vector_similarity_pearson" => vector::similarity::pearson,
"vector_similarity_spearman" => vector::similarity::spearman,
)
}
Value::Bytes(_) => {
dispatch!(
name,
args.clone(),
"no such method found for the bytes type",
"len" => bytes::len,
)
}
Value::Duration(_) => {
dispatch!(
name,
args.clone(),
"no such method found for the duration type",
"days" => duration::days,
"hours" => duration::hours,
"micros" => duration::micros,
"millis" => duration::millis,
"mins" => duration::mins,
"nanos" => duration::nanos,
"secs" => duration::secs,
"weeks" => duration::weeks,
"years" => duration::years,
)
}
Value::Geometry(_) => {
dispatch!(
name,
args.clone(),
"no such method found for the geometry type",
"area" => geo::area,
"bearing" => geo::bearing,
"centroid" => geo::centroid,
"distance" => geo::distance,
"hash::decode" => geo::hash::decode,
"hash::encode" => geo::hash::encode,
)
}
Value::Thing(_) => {
dispatch!(
name,
args.clone(),
"no such method found for the record type",
"id" => meta::id,
"table" => meta::tb,
"tb" => meta::tb,
)
}
Value::Object(_) => {
dispatch!(
name,
args.clone(),
"no such method found for the object type",
"entries" => object::entries,
"keys" => object::keys,
"len" => object::len,
"values" => object::values,
)
}
Value::Strand(_) => {
dispatch!(
name,
args.clone(),
"no such method found for the string type",
"concat" => string::concat,
"contains" => string::contains,
"endsWith" => string::ends_with,
"join" => string::join,
"len" => string::len,
"lowercase" => string::lowercase,
"matches" => string::matches,
"repeat" => string::repeat,
"replace" => string::replace,
"reverse" => string::reverse,
"slice" => string::slice,
"slug" => string::slug,
"split" => string::split,
"startsWith" => string::starts_with,
"trim" => string::trim,
"uppercase" => string::uppercase,
"words" => string::words,
"distance_hamming" => string::distance::hamming,
"distance_levenshtein" => string::distance::levenshtein,
"html_encode" => string::html::encode,
"html_sanitize" => string::html::sanitize,
"is_alphanum" => string::is::alphanum,
"is_alpha" => string::is::alpha,
"is_ascii" => string::is::ascii,
"is_datetime" => string::is::datetime,
"is_domain" => string::is::domain,
"is_email" => string::is::email,
"is_hexadecimal" => string::is::hexadecimal,
"is_ip" => string::is::ip,
"is_ipv4" => string::is::ipv4,
"is_ipv6" => string::is::ipv6,
"is_latitude" => string::is::latitude,
"is_longitude" => string::is::longitude,
"is_numeric" => string::is::numeric,
"is_semver" => string::is::semver,
"is_url" => string::is::url,
"is_uuid" => string::is::uuid,
"is_record" => string::is::record,
"similarity_fuzzy" => string::similarity::fuzzy,
"similarity_jaro" => string::similarity::jaro,
"similarity_smithwaterman" => string::similarity::smithwaterman,
"semver_compare" => string::semver::compare,
"semver_major" => string::semver::major,
"semver_minor" => string::semver::minor,
"semver_patch" => string::semver::patch,
"semver_inc::major" => string::semver::inc::major,
"semver_inc::minor" => string::semver::inc::minor,
"semver_inc::patch" => string::semver::inc::patch,
"semver_set::major" => string::semver::set::major,
"semver_set::minor" => string::semver::set::minor,
"semver_set::patch" => string::semver::set::patch,
)
}
Value::Datetime(_) => {
dispatch!(
name,
args.clone(),
"no such method found for the datetime type",
"time_ceil" => time::ceil,
"time_day" => time::day,
"time_floor" => time::floor,
"time_format" => time::format,
"time_group" => time::group,
"time_hour" => time::hour,
"time_minute" => time::minute,
"time_month" => time::month,
"time_nano" => time::nano,
"time_micros" => time::micros,
"time_millis" => time::millis,
"time_round" => time::round,
"time_second" => time::second,
"time_unix" => time::unix,
"time_wday" => time::wday,
"time_week" => time::week,
"time_yday" => time::yday,
"time_year" => time::year,
)
}
_ => Err(Error::InvalidFunction {
name: "".into(),
message: "".into(),
}),
};
match specific {
Err(Error::InvalidFunction {
..
}) => {
let message = format!("no such method found for the {} type", value.kindof());
dispatch!(
name,
args,
message,
"is_array" => r#type::is::array,
"is_bool" => r#type::is::bool,
"is_bytes" => r#type::is::bytes,
"is_collection" => r#type::is::collection,
"is_datetime" => r#type::is::datetime,
"is_decimal" => r#type::is::decimal,
"is_duration" => r#type::is::duration,
"is_float" => r#type::is::float,
"is_geometry" => r#type::is::geometry,
"is_int" => r#type::is::int,
"is_line" => r#type::is::line,
"is_none" => r#type::is::none,
"is_null" => r#type::is::null,
"is_multiline" => r#type::is::multiline,
"is_multipoint" => r#type::is::multipoint,
"is_multipolygon" => r#type::is::multipolygon,
"is_number" => r#type::is::number,
"is_object" => r#type::is::object,
"is_point" => r#type::is::point,
"is_polygon" => r#type::is::polygon,
"is_record" => r#type::is::record,
"is_string" => r#type::is::string,
"is_uuid" => r#type::is::uuid,
//
"to_bool" => r#type::bool,
"to_bytes" => r#type::bytes,
"to_datetime" => r#type::datetime,
"to_decimal" => r#type::decimal,
"to_duration" => r#type::duration,
"to_float" => r#type::float,
"to_geometry" => r#type::geometry,
"to_int" => r#type::int,
"to_number" => r#type::number,
"to_point" => r#type::point,
"to_record" => r#type::record,
"to_string" => r#type::string,
"to_uuid" => r#type::uuid,
)
}
v => v,
}
}
/// Attempts to run any asynchronous function.
pub async fn asynchronous(
stk: &mut Stk,
@ -431,6 +718,7 @@ pub async fn asynchronous(
dispatch!(
name,
args,
"no such builtin function found",
"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,
@ -477,6 +765,8 @@ fn get_execution_context<'a>(
#[cfg(test)]
mod tests {
use regex::Regex;
#[cfg(all(feature = "scripting", feature = "kv-mem"))]
use crate::dbs::Capabilities;
use crate::sql::{statements::OutputStatement, Function, Query, Statement, Value};
@ -488,7 +778,12 @@ mod tests {
// Read the source code of this file
let fnc_mod = include_str!("mod.rs");
for line in fnc_mod.lines() {
// Patch out idiom methods
let re = Regex::new(r"(?ms)pub fn idiom\(.*}\n+///").unwrap();
let fnc_no_idiom = re.replace(fnc_mod, "");
for line in fnc_no_idiom.lines() {
if !(line.contains("=>")
&& (line.trim().starts_with('"') || line.trim().ends_with(',')))
{

View file

@ -12,6 +12,7 @@ impl_module_def!(
Package,
"type",
"bool" => run,
"bytes" => run,
"datetime" => run,
"decimal" => run,
"duration" => run,
@ -26,5 +27,8 @@ impl_module_def!(
"string" => run,
"table" => run,
"thing" => run,
"range" => run
"range" => run,
"record" => run,
"uuid" => run,
"geometry" => run
);

View file

@ -7,7 +7,7 @@ use crate::err::Error;
use crate::sql::table::Table;
use crate::sql::thing::Thing;
use crate::sql::value::Value;
use crate::sql::{Id, Range, Strand};
use crate::sql::{Id, Kind, Range, Strand};
use crate::syn;
use reblessive::tree::Stk;
@ -15,6 +15,10 @@ pub fn bool((val,): (Value,)) -> Result<Value, Error> {
val.convert_to_bool().map(Value::from)
}
pub fn bytes((val,): (Value,)) -> Result<Value, Error> {
val.convert_to_bytes().map(Value::from)
}
pub fn datetime((val,): (Value,)) -> Result<Value, Error> {
val.convert_to_datetime().map(Value::from)
}
@ -71,6 +75,10 @@ pub fn float((val,): (Value,)) -> Result<Value, Error> {
val.convert_to_float().map(Value::from)
}
pub fn geometry((val,): (Value,)) -> Result<Value, Error> {
val.convert_to_geometry().map(Value::from)
}
pub fn int((val,): (Value,)) -> Result<Value, Error> {
val.convert_to_int().map(Value::from)
}
@ -83,57 +91,6 @@ pub fn point((val,): (Value,)) -> Result<Value, Error> {
val.convert_to_point().map(Value::from)
}
pub fn string((val,): (Value,)) -> Result<Value, Error> {
val.convert_to_strand().map(Value::from)
}
pub fn table((val,): (Value,)) -> Result<Value, Error> {
Ok(Value::Table(Table(match val {
Value::Thing(t) => t.tb,
v => v.as_string(),
})))
}
pub fn thing((arg1, arg2): (Value, Option<Value>)) -> Result<Value, Error> {
match (arg1, arg2) {
// Empty table name
(Value::Strand(arg1), _) if arg1.is_empty() => Err(Error::TbInvalid {
value: arg1.as_string(),
}),
// Empty ID part
(_, Some(Value::Strand(arg2))) if arg2.is_empty() => Err(Error::IdInvalid {
value: arg2.as_string(),
}),
// Handle second argument
(arg1, Some(arg2)) => Ok(Value::Thing(Thing {
tb: arg1.as_string(),
id: match arg2 {
Value::Thing(v) => v.id,
Value::Array(v) => v.into(),
Value::Object(v) => v.into(),
Value::Number(v) => v.into(),
v => v.as_string().into(),
},
})),
// No second argument passed
(arg1, _) => Ok(match arg1 {
Value::Thing(v) => Ok(v),
Value::Strand(v) => Thing::try_from(v.as_str()).map_err(move |_| Error::ConvertTo {
from: Value::Strand(v),
into: "record".into(),
}),
v => Err(Error::ConvertTo {
from: v,
into: "record".into(),
}),
}?
.into()),
}
}
pub fn range(args: Vec<Value>) -> Result<Value, Error> {
if args.len() > 4 || args.is_empty() {
return Err(Error::InvalidArguments {
@ -221,6 +178,79 @@ pub fn range(args: Vec<Value>) -> Result<Value, Error> {
.into())
}
pub fn record((rid, tb): (Value, Option<Value>)) -> Result<Value, Error> {
match tb {
Some(Value::Strand(Strand(tb)) | Value::Table(Table(tb))) if tb.is_empty() => {
Err(Error::TbInvalid {
value: tb,
})
}
Some(Value::Strand(Strand(tb)) | Value::Table(Table(tb))) => {
rid.convert_to(&Kind::Record(vec![tb.into()]))
}
Some(_) => Err(Error::InvalidArguments {
name: "type::record".into(),
message: "The second argument must be a table name or a string.".into(),
}),
None => rid.convert_to(&Kind::Record(vec![])),
}
}
pub fn string((val,): (Value,)) -> Result<Value, Error> {
val.convert_to_strand().map(Value::from)
}
pub fn table((val,): (Value,)) -> Result<Value, Error> {
Ok(Value::Table(Table(match val {
Value::Thing(t) => t.tb,
v => v.as_string(),
})))
}
pub fn thing((arg1, arg2): (Value, Option<Value>)) -> Result<Value, Error> {
match (arg1, arg2) {
// Empty table name
(Value::Strand(arg1), _) if arg1.is_empty() => Err(Error::TbInvalid {
value: arg1.as_string(),
}),
// Empty ID part
(_, Some(Value::Strand(arg2))) if arg2.is_empty() => Err(Error::IdInvalid {
value: arg2.as_string(),
}),
// Handle second argument
(arg1, Some(arg2)) => Ok(Value::Thing(Thing {
tb: arg1.as_string(),
id: match arg2 {
Value::Thing(v) => v.id,
Value::Array(v) => v.into(),
Value::Object(v) => v.into(),
Value::Number(v) => v.into(),
v => v.as_string().into(),
},
})),
// No second argument passed
(arg1, _) => Ok(match arg1 {
Value::Thing(v) => Ok(v),
Value::Strand(v) => Thing::try_from(v.as_str()).map_err(move |_| Error::ConvertTo {
from: Value::Strand(v),
into: "record".into(),
}),
v => Err(Error::ConvertTo {
from: v,
into: "record".into(),
}),
}?
.into()),
}
}
pub fn uuid((val,): (Value,)) -> Result<Value, Error> {
val.convert_to_uuid().map(Value::from)
}
pub mod is {
use crate::err::Error;
use crate::sql::table::Table;

View file

@ -5,7 +5,7 @@ use crate::err::Error;
use crate::sql::statements::info::InfoStructure;
use crate::sql::{
fmt::{fmt_separated_by, Fmt},
part::Next,
part::{Next, NextMethod},
paths::{ID, IN, META, OUT},
Part, Value,
};
@ -185,7 +185,13 @@ impl Idiom {
v.doc.get(stk, ctx, opt, doc, self).await?.compute(stk, ctx, opt, doc).await
}
// There isn't any document
None => Ok(Value::None),
None => {
Value::None
.get(stk, ctx, opt, doc, self.next_method())
.await?
.compute(stk, ctx, opt, doc)
.await
}
},
}
}

View file

@ -149,6 +149,30 @@ impl<'a> Next<'a> for &'a [Part] {
// ------------------------------
pub trait NextMethod<'a> {
fn next_method(&'a self) -> &[Part];
}
impl<'a> NextMethod<'a> for &'a [Part] {
fn next_method(&'a self) -> &'a [Part] {
match self.iter().position(|p| matches!(p, Part::Method(_, _))) {
None => &[],
Some(i) => &self[i..],
}
}
}
impl<'a> NextMethod<'a> for &'a Idiom {
fn next_method(&'a self) -> &'a [Part] {
match self.iter().position(|p| matches!(p, Part::Method(_, _))) {
None => &[],
Some(i) => &self[i..],
}
}
}
// ------------------------------
#[revisioned(revision = 1)]
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]

View file

@ -6,11 +6,12 @@ use crate::dbs::Options;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::exe::try_join_all_buffered;
use crate::fnc::idiom;
use crate::sql::edges::Edges;
use crate::sql::field::{Field, Fields};
use crate::sql::id::Id;
use crate::sql::part::Next;
use crate::sql::part::Part;
use crate::sql::part::{Next, NextMethod};
use crate::sql::paths::ID;
use crate::sql::statements::select::SelectStatement;
use crate::sql::thing::Thing;
@ -57,6 +58,10 @@ impl Value {
let obj = Value::Object(v.as_object());
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())?;
stk.run(|stk| v.get(stk, ctx, opt, doc, path.next())).await
}
// Otherwise return none
_ => Ok(Value::None),
},
@ -134,6 +139,10 @@ impl Value {
let obj = Value::from(obj);
stk.run(|stk| obj.get(stk, ctx, opt, doc, path.next())).await
}
Part::Method(name, args) => {
let v = idiom(ctx, doc, v.clone().into(), name, args.clone())?;
stk.run(|stk| v.get(stk, ctx, opt, doc, path.next())).await
}
_ => Ok(Value::None),
},
// Current value at path is an array
@ -183,6 +192,10 @@ impl Value {
},
_ => Ok(Value::None),
},
Part::Method(name, args) => {
let v = idiom(ctx, doc, v.clone().into(), name, args.clone())?;
stk.run(|stk| v.get(stk, ctx, opt, doc, path.next())).await
}
_ => stk
.scope(|scope| {
let futs =
@ -267,6 +280,10 @@ impl Value {
}
}
}
Part::Method(name, args) => {
let v = idiom(ctx, doc, v.clone().into(), name, args.clone())?;
stk.run(|stk| v.get(stk, ctx, opt, doc, path.next())).await
}
// This is a remote field expression
_ => {
let stm = SelectStatement {
@ -282,11 +299,19 @@ impl Value {
}
}
v => {
if matches!(p, Part::Flatten) {
stk.run(|stk| v.get(stk, ctx, opt, None, path.next())).await
} else {
// Ignore everything else
Ok(Value::None)
match p {
Part::Flatten => {
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())?;
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
_ => {
stk.run(|stk| Value::None.get(stk, ctx, opt, doc, path.next_method()))
.await
}
}
}
},

View file

@ -4,6 +4,8 @@ use crate::err::Error;
use crate::sql::value::serde::ser;
use crate::sql::Ident;
use crate::sql::Part;
use crate::sql::Value;
use ser::Serializer as _;
use serde::ser::Error as _;
use serde::ser::Impossible;
use serde::ser::Serialize;
@ -17,7 +19,7 @@ impl ser::Serializer for Serializer {
type SerializeSeq = Impossible<Part, Error>;
type SerializeTuple = Impossible<Part, Error>;
type SerializeTupleStruct = Impossible<Part, Error>;
type SerializeTupleVariant = Impossible<Part, Error>;
type SerializeTupleVariant = SerializePart;
type SerializeMap = Impossible<Part, Error>;
type SerializeStruct = Impossible<Part, Error>;
type SerializeStructVariant = Impossible<Part, Error>;
@ -62,6 +64,67 @@ impl ser::Serializer for Serializer {
}
}
}
fn serialize_tuple_variant(
self,
name: &'static str,
_variant_index: u32,
variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
let inner = match variant {
"Method" => Inner::Method(Default::default(), Default::default()),
variant => {
return Err(Error::custom(format!("unexpected tuple variant `{name}::{variant}`")));
}
};
Ok(SerializePart {
inner,
index: 0,
})
}
}
pub(super) struct SerializePart {
index: usize,
inner: Inner,
}
enum Inner {
Method(String, Vec<Value>),
}
impl serde::ser::SerializeTupleVariant for SerializePart {
type Ok = Part;
type Error = Error;
fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: Serialize + ?Sized,
{
match (self.index, &mut self.inner) {
(0, Inner::Method(ref mut var, _)) => {
*var = value.serialize(ser::string::Serializer.wrap())?;
}
(1, Inner::Method(_, ref mut var)) => {
*var = value.serialize(ser::value::vec::Serializer.wrap())?;
}
(index, inner) => {
let variant = match inner {
Inner::Method(..) => "Method",
};
return Err(Error::custom(format!("unexpected `Part::{variant}` index `{index}`")));
}
}
self.index += 1;
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
match self.inner {
Inner::Method(one, two) => Ok(Part::Method(one, two)),
}
}
}
#[cfg(test)]
@ -133,4 +196,11 @@ mod tests {
let serialized = part.serialize(Serializer.wrap()).unwrap();
assert_eq!(part, serialized);
}
#[test]
fn method() {
let part = Part::Method(Default::default(), Default::default());
let serialized = part.serialize(Serializer.wrap()).unwrap();
assert_eq!(part, serialized);
}
}

View file

@ -333,6 +333,7 @@ pub(crate) static PATHS: phf::Map<UniCase<&'static str>, PathKind> = phf_map! {
UniCase::ascii("time::from::unix") => PathKind::Function,
//
UniCase::ascii("type::bool") => PathKind::Function,
UniCase::ascii("type::bytes") => PathKind::Function,
UniCase::ascii("type::datetime") => PathKind::Function,
UniCase::ascii("type::decimal") => PathKind::Function,
UniCase::ascii("type::duration") => PathKind::Function,
@ -344,6 +345,9 @@ pub(crate) static PATHS: phf::Map<UniCase<&'static str>, PathKind> = phf_map! {
UniCase::ascii("type::table") => PathKind::Function,
UniCase::ascii("type::thing") => PathKind::Function,
UniCase::ascii("type::range") => PathKind::Function,
UniCase::ascii("type::record") => PathKind::Function,
UniCase::ascii("type::uuid") => PathKind::Function,
UniCase::ascii("type::geometry") => PathKind::Function,
UniCase::ascii("type::is::array") => PathKind::Function,
UniCase::ascii("type::is::bool") => PathKind::Function,
UniCase::ascii("type::is::bytes") => PathKind::Function,

View file

@ -1,7 +1,7 @@
use reblessive::Stk;
use crate::{
sql::{Function, Ident, Model},
sql::{Function, Ident, Model, Value},
syn::{
parser::{
mac::{expected, expected_whitespace, unexpected},
@ -24,7 +24,13 @@ impl Parser<'_> {
name.push_str("::");
name.push_str(&self.next_token_value::<Ident>()?.0)
}
let start = expected!(self, t!("(")).span;
expected!(self, t!("(")).span;
let args = self.parse_function_args(ctx).await?;
Ok(Function::Custom(name, args))
}
pub async fn parse_function_args(&mut self, ctx: &mut Stk) -> ParseResult<Vec<Value>> {
let start = self.last_span();
let mut args = Vec::new();
loop {
if self.eat(t!(")")) {
@ -39,8 +45,7 @@ impl Parser<'_> {
break;
}
}
Ok(Function::Custom(name, args))
Ok(args)
}
/// Parse a model invocation

View file

@ -83,7 +83,7 @@ impl Parser<'_> {
}
t!(".") => {
self.pop_peek();
res.push(self.parse_dot_part()?)
res.push(self.parse_dot_part(stk).await?)
}
t!("[") => {
let span = self.pop_peek().span;
@ -141,7 +141,7 @@ impl Parser<'_> {
}
t!(".") => {
self.pop_peek();
res.push(self.parse_dot_part()?)
res.push(self.parse_dot_part(ctx).await?)
}
t!("[") => {
let span = self.pop_peek().span;
@ -249,7 +249,7 @@ impl Parser<'_> {
}
/// Parse the part after the `.` in a idiom
pub fn parse_dot_part(&mut self) -> ParseResult<Part> {
pub async fn parse_dot_part(&mut self, ctx: &mut Stk) -> ParseResult<Part> {
let res = match self.peek_kind() {
t!("*") => {
self.pop_peek();
@ -257,14 +257,25 @@ impl Parser<'_> {
}
t!("{") => {
self.pop_peek();
self.parse_destructure_part()?
ctx.run(|ctx| self.parse_destructure_part(ctx)).await?
}
_ => {
let ident: Ident = self.next_token_value()?;
if self.eat(t!("(")) {
self.parse_function_part(ctx, ident).await?
} else {
Part::Field(ident)
}
}
_ => Part::Field(self.next_token_value()?),
};
Ok(res)
}
pub async fn parse_function_part(&mut self, ctx: &mut Stk, name: Ident) -> ParseResult<Part> {
let args = self.parse_function_args(ctx).await?;
Ok(Part::Method(name.0, args))
}
/// Parse the part after the `.{` in an idiom
pub fn parse_destructure_part(&mut self) -> ParseResult<Part> {
pub async fn parse_destructure_part(&mut self, ctx: &mut Stk) -> ParseResult<Part> {
let start = self.last_span();
let mut destructured: Vec<DestructurePart> = Vec::new();
loop {
@ -277,12 +288,12 @@ impl Parser<'_> {
let part = match self.peek_kind() {
t!(":") => {
self.pop_peek();
DestructurePart::Aliased(field, self.parse_local_idiom()?)
DestructurePart::Aliased(field, self.parse_local_idiom(ctx).await?)
}
t!(".") => {
self.pop_peek();
let found = self.peek_kind();
match self.parse_dot_part()? {
match self.parse_dot_part(ctx).await? {
Part::All => DestructurePart::All(field),
Part::Destructure(v) => DestructurePart::Destructure(field, v),
_ => {
@ -338,7 +349,7 @@ impl Parser<'_> {
t!("$param") => Part::Value(Value::Param(self.next_token_value()?)),
TokenKind::Qoute(_x) => Part::Value(Value::Strand(self.next_token_value()?)),
_ => {
let idiom = self.parse_basic_idiom()?;
let idiom = self.parse_basic_idiom(ctx).await?;
Part::Value(Value::Idiom(idiom))
}
};
@ -347,10 +358,10 @@ impl Parser<'_> {
}
/// Parse a list of basic idioms seperated by a ','
pub fn parse_basic_idiom_list(&mut self) -> ParseResult<Vec<Idiom>> {
let mut res = vec![self.parse_basic_idiom()?];
pub async fn parse_basic_idiom_list(&mut self, ctx: &mut Stk) -> ParseResult<Vec<Idiom>> {
let mut res = vec![self.parse_basic_idiom(ctx).await?];
while self.eat(t!(",")) {
res.push(self.parse_basic_idiom()?);
res.push(self.parse_basic_idiom(ctx).await?);
}
Ok(res)
}
@ -359,7 +370,7 @@ impl Parser<'_> {
///
/// Basic idioms differ from normal idioms in that they are more restrictive.
/// Flatten, graphs, conditions and indexing by param is not allowed.
pub fn parse_basic_idiom(&mut self) -> ParseResult<Idiom> {
pub async fn parse_basic_idiom(&mut self, ctx: &mut Stk) -> ParseResult<Idiom> {
let start = self.next_token_value::<Ident>()?;
let mut parts = vec![Part::Field(start)];
loop {
@ -367,7 +378,7 @@ impl Parser<'_> {
let part = match token.kind {
t!(".") => {
self.pop_peek();
self.parse_dot_part()?
self.parse_dot_part(ctx).await?
}
t!("[") => {
self.pop_peek();
@ -409,7 +420,7 @@ impl Parser<'_> {
/// Basic idioms differ from local idioms in that they are more restrictive.
/// Only field, all and number indexing is allowed. Flatten is also allowed but only at the
/// end.
pub fn parse_local_idiom(&mut self) -> ParseResult<Idiom> {
pub async fn parse_local_idiom(&mut self, ctx: &mut Stk) -> ParseResult<Idiom> {
let start = self.next_token_value()?;
let mut parts = vec![Part::Field(start)];
loop {
@ -417,7 +428,7 @@ impl Parser<'_> {
let part = match token.kind {
t!(".") => {
self.pop_peek();
self.parse_dot_part()?
self.parse_dot_part(ctx).await?
}
t!("[") => {
self.pop_peek();

View file

@ -132,19 +132,19 @@ impl Parser<'_> {
let value = match token.kind {
t!("NONE") => {
self.pop_peek();
return Ok(Value::None);
Value::None
}
t!("NULL") => {
self.pop_peek();
return Ok(Value::Null);
Value::Null
}
t!("true") => {
self.pop_peek();
return Ok(Value::Bool(true));
Value::Bool(true)
}
t!("false") => {
self.pop_peek();
return Ok(Value::Bool(false));
Value::Bool(false)
}
t!("<") => {
self.pop_peek();
@ -153,7 +153,7 @@ impl Parser<'_> {
self.expect_closing_delimiter(t!(">"), token.span)?;
let next = expected!(self, t!("{")).span;
let block = self.parse_block(ctx, next).await?;
return Ok(Value::Future(Box::new(crate::sql::Future(block))));
Value::Future(Box::new(crate::sql::Future(block)))
}
t!("r\"") => {
self.pop_peek();
@ -180,14 +180,14 @@ impl Parser<'_> {
return Ok(x);
}
}
return Ok(Value::Strand(s));
Value::Strand(s)
}
t!("+") | t!("-") | TokenKind::Number(_) | TokenKind::Digits | TokenKind::Duration => {
self.parse_number_like_prime()?
}
TokenKind::NaN => {
self.pop_peek();
return Ok(Value::Number(Number::Float(f64::NAN)));
Value::Number(Number::Float(f64::NAN))
}
t!("$param") => {
let param = self.next_token_value()?;
@ -285,11 +285,6 @@ impl Parser<'_> {
// Parse the rest of the idiom if it is being continued.
if Self::continues_idiom(self.peek_kind()) {
match value {
Value::None
| Value::Null
| Value::Bool(_)
| Value::Future(_)
| Value::Strand(_) => unreachable!(),
Value::Idiom(Idiom(x)) => self.parse_remaining_value_idiom(ctx, x).await,
Value::Table(Table(x)) => {
self.parse_remaining_value_idiom(ctx, vec![Part::Field(Ident(x))]).await
@ -420,7 +415,7 @@ impl Parser<'_> {
}
t!("REMOVE") => {
self.pop_peek();
let stmt = self.parse_remove_stmt()?;
let stmt = self.parse_remove_stmt(ctx).await?;
Subquery::Remove(stmt)
}
t!("REBUILD") => {
@ -553,7 +548,7 @@ impl Parser<'_> {
}
t!("REMOVE") => {
self.pop_peek();
let stmt = self.parse_remove_stmt()?;
let stmt = self.parse_remove_stmt(ctx).await?;
Subquery::Remove(stmt)
}
t!("REBUILD") => {
@ -648,7 +643,7 @@ impl Parser<'_> {
#[cfg(test)]
mod tests {
use super::*;
use crate::syn::{self, Parse};
use crate::syn::Parse;
#[test]
fn subquery_expression_statement() {
@ -657,12 +652,6 @@ mod tests {
assert_eq!("(1 + 2 + 3)", format!("{}", out))
}
#[test]
fn invalid_idiom() {
let sql = "'hello'.foo";
syn::parse(sql).unwrap_err();
}
#[test]
fn subquery_ifelse_statement() {
let sql = "IF true THEN false END";

View file

@ -47,7 +47,9 @@ impl Parser<'_> {
t!("FIELD") => {
ctx.run(|ctx| self.parse_define_field(ctx)).await.map(DefineStatement::Field)
}
t!("INDEX") => self.parse_define_index().map(DefineStatement::Index),
t!("INDEX") => {
ctx.run(|ctx| self.parse_define_index(ctx)).await.map(DefineStatement::Index)
}
t!("ANALYZER") => self.parse_define_analyzer().map(DefineStatement::Analyzer),
t!("ACCESS") => self.parse_define_access(ctx).await.map(DefineStatement::Access),
x => unexpected!(self, x, "a define statement keyword"),
@ -739,7 +741,7 @@ impl Parser<'_> {
} else {
(false, false)
};
let name = self.parse_local_idiom()?;
let name = self.parse_local_idiom(ctx).await?;
expected!(self, t!("ON"));
self.eat(t!("TABLE"));
let what = self.next_token_value()?;
@ -794,7 +796,7 @@ impl Parser<'_> {
Ok(res)
}
pub fn parse_define_index(&mut self) -> ParseResult<DefineIndexStatement> {
pub async fn parse_define_index(&mut self, ctx: &mut Stk) -> ParseResult<DefineIndexStatement> {
let (if_not_exists, overwrite) = if self.eat(t!("IF")) {
expected!(self, t!("NOT"));
expected!(self, t!("EXISTS"));
@ -823,9 +825,9 @@ impl Parser<'_> {
// COLUMNS and FIELDS are the same tokenkind
t!("FIELDS") => {
self.pop_peek();
res.cols = Idioms(vec![self.parse_local_idiom()?]);
res.cols = Idioms(vec![self.parse_local_idiom(ctx).await?]);
while self.eat(t!(",")) {
res.cols.0.push(self.parse_local_idiom()?);
res.cols.0.push(self.parse_local_idiom(ctx).await?);
}
}
t!("UNIQUE") => {

View file

@ -190,7 +190,7 @@ impl Parser<'_> {
}
t!("REMOVE") => {
self.pop_peek();
self.parse_remove_stmt().map(Statement::Remove)
ctx.run(|ctx| self.parse_remove_stmt(ctx)).await.map(Statement::Remove)
}
t!("SELECT") => {
self.pop_peek();
@ -291,7 +291,7 @@ impl Parser<'_> {
}
t!("REMOVE") => {
self.pop_peek();
self.parse_remove_stmt().map(Entry::Remove)
self.parse_remove_stmt(ctx).await.map(Entry::Remove)
}
t!("SELECT") => {
self.pop_peek();

View file

@ -205,8 +205,9 @@ impl Parser<'_> {
})
}
pub fn try_parse_group(
pub async fn try_parse_group(
&mut self,
ctx: &mut Stk,
fields: &Fields,
fields_span: Span,
) -> ParseResult<Option<Groups>> {
@ -223,7 +224,7 @@ impl Parser<'_> {
let has_all = fields.contains(&Field::All);
let before = self.peek().span;
let group = self.parse_basic_idiom()?;
let group = self.parse_basic_idiom(ctx).await?;
let group_span = before.covers(self.last_span());
if !has_all {
Self::check_idiom(MissingKind::Group, fields, fields_span, &group, group_span)?;
@ -232,7 +233,7 @@ impl Parser<'_> {
let mut groups = Groups(vec![Group(group)]);
while self.eat(t!(",")) {
let before = self.peek().span;
let group = self.parse_basic_idiom()?;
let group = self.parse_basic_idiom(ctx).await?;
let group_span = before.covers(self.last_span());
if !has_all {
Self::check_idiom(MissingKind::Group, fields, fields_span, &group, group_span)?;
@ -406,7 +407,7 @@ impl Parser<'_> {
}
let cond = self.try_parse_condition(stk).await?;
let group = self.try_parse_group(&fields, fields_span)?;
let group = self.try_parse_group(stk, &fields, fields_span).await?;
Ok(View {
expr: fields,

View file

@ -1,3 +1,5 @@
use reblessive::Stk;
use crate::{
sql::{
statements::{
@ -18,7 +20,7 @@ use crate::{
};
impl Parser<'_> {
pub fn parse_remove_stmt(&mut self) -> ParseResult<RemoveStatement> {
pub async fn parse_remove_stmt(&mut self, ctx: &mut Stk) -> ParseResult<RemoveStatement> {
let res = match self.next().kind {
t!("NAMESPACE") | t!("ns") => {
let if_exists = if self.eat(t!("IF")) {
@ -136,7 +138,7 @@ impl Parser<'_> {
} else {
false
};
let idiom = self.parse_local_idiom()?;
let idiom = self.parse_local_idiom(ctx).await?;
expected!(self, t!("ON"));
self.eat(t!("TABLE"));
let table = self.next_token_value()?;

View file

@ -42,9 +42,9 @@ impl Parser<'_> {
let with = self.try_parse_with()?;
let cond = self.try_parse_condition(stk).await?;
let split = self.try_parse_split(&expr, fields_span)?;
let group = self.try_parse_group(&expr, fields_span)?;
let order = self.try_parse_orders(&expr, fields_span)?;
let split = self.try_parse_split(stk, &expr, fields_span).await?;
let group = self.try_parse_group(stk, &expr, fields_span).await?;
let order = self.try_parse_orders(stk, &expr, fields_span).await?;
let (limit, start) = if let t!("START") = self.peek_kind() {
let start = self.try_parse_start(stk).await?;
let limit = self.try_parse_limit(stk).await?;
@ -104,8 +104,9 @@ impl Parser<'_> {
Ok(Some(with))
}
fn try_parse_split(
async fn try_parse_split(
&mut self,
ctx: &mut Stk,
fields: &Fields,
fields_span: Span,
) -> ParseResult<Option<Splits>> {
@ -118,7 +119,7 @@ impl Parser<'_> {
let has_all = fields.contains(&Field::All);
let before = self.peek().span;
let split = self.parse_basic_idiom()?;
let split = self.parse_basic_idiom(ctx).await?;
let split_span = before.covers(self.last_span());
if !has_all {
Self::check_idiom(MissingKind::Split, fields, fields_span, &split, split_span)?;
@ -127,7 +128,7 @@ impl Parser<'_> {
let mut res = vec![Split(split)];
while self.eat(t!(",")) {
let before = self.peek().span;
let split = self.parse_basic_idiom()?;
let split = self.parse_basic_idiom(ctx).await?;
let split_span = before.covers(self.last_span());
if !has_all {
Self::check_idiom(MissingKind::Split, fields, fields_span, &split, split_span)?;
@ -137,8 +138,9 @@ impl Parser<'_> {
Ok(Some(Splits(res)))
}
fn try_parse_orders(
async fn try_parse_orders(
&mut self,
ctx: &mut Stk,
fields: &Fields,
fields_span: Span,
) -> ParseResult<Option<Orders>> {
@ -164,7 +166,7 @@ impl Parser<'_> {
let has_all = fields.contains(&Field::All);
let before = self.recent_span();
let order = self.parse_order()?;
let order = self.parse_order(ctx).await?;
let order_span = before.covers(self.last_span());
if !has_all {
Self::check_idiom(MissingKind::Order, fields, fields_span, &order, order_span)?;
@ -173,7 +175,7 @@ impl Parser<'_> {
let mut orders = vec![order];
while self.eat(t!(",")) {
let before = self.recent_span();
let order = self.parse_order()?;
let order = self.parse_order(ctx).await?;
let order_span = before.covers(self.last_span());
if !has_all {
Self::check_idiom(MissingKind::Order, fields, fields_span, &order, order_span)?;
@ -184,8 +186,8 @@ impl Parser<'_> {
Ok(Some(Orders(orders)))
}
fn parse_order(&mut self) -> ParseResult<Order> {
let start = self.parse_basic_idiom()?;
async fn parse_order(&mut self, ctx: &mut Stk) -> ParseResult<Order> {
let start = self.parse_basic_idiom(ctx).await?;
let collate = self.eat(t!("COLLATE"));
let numeric = self.eat(t!("NUMERIC"));
let direction = match self.peek_kind() {

View file

@ -404,6 +404,9 @@
"type::string("
"type::table("
"type::thing("
"type::record("
"type::uuid("
"type::geometry("
"vector::add("
"vector::angle("
"vector::cross("

View file

@ -403,6 +403,9 @@
"type::table("
"type::thing("
"type::range("
"type::record("
"type::uuid("
"type::geometry("
"vector::add("
"vector::angle("
"vector::cross("

View file

@ -6227,3 +6227,24 @@ async fn function_outside_database() -> Result<(), Error> {
Ok(())
}
#[tokio::test]
async fn function_idiom_chaining() -> Result<(), Error> {
let sql = r#"
{ a: 1, b: 2 }.entries().flatten();
"ABC".lowercase();
true.is_number();
true.is_bool();
true.doesnt_exist();
field.bla.nested.is_none();
"#;
Test::new(sql)
.await?
.expect_val("['a', 1, 'b', 2]")?
.expect_val("'abc'")?
.expect_val("false")?
.expect_val("true")?
.expect_error("There was a problem running the doesnt_exist() function. no such method found for the bool type")?
.expect_val("true")?;
Ok(())
}