Cleanup Fetch resolution (#4312)

Co-authored-by: Gerard Guillemas Martos <gguillemas@users.noreply.github.com>
This commit is contained in:
Emmanuel Keller 2024-07-18 12:27:35 +01:00 committed by GitHub
parent 6a5dca9214
commit 968b1714dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 316 additions and 89 deletions

35
Cargo.lock generated
View file

@ -4785,12 +4785,24 @@ name = "revision"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4df61cfb2522f24fd6aa90ce3489c2c8660a181075e7bac3ae7bdf22287d238f"
dependencies = [
"bincode",
"revision-derive 0.7.0",
"serde",
"thiserror",
]
[[package]]
name = "revision"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98b99dba8f2787c9af2e46b17ff38437d213d46c8970b550e6b79b862bf7629"
dependencies = [
"bincode",
"chrono",
"geo 0.26.0",
"regex",
"revision-derive",
"revision-derive 0.8.0",
"roaring",
"rust_decimal",
"serde",
@ -4811,6 +4823,19 @@ dependencies = [
"syn 2.0.58",
]
[[package]]
name = "revision-derive"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3721b4a8e52f9e52c54f74f482a4f550601f5c44cb7876606a2ab79cb09469c1"
dependencies = [
"darling",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.58",
]
[[package]]
name = "rexie"
version = "0.4.2"
@ -5896,7 +5921,7 @@ dependencies = [
"rand 0.8.5",
"rcgen",
"reqwest",
"revision",
"revision 0.8.0",
"rmp-serde",
"rmpv",
"rustyline",
@ -5953,7 +5978,7 @@ dependencies = [
"reblessive",
"regex",
"reqwest",
"revision",
"revision 0.8.0",
"ring 0.17.8",
"rust_decimal",
"rustls 0.21.11",
@ -6042,7 +6067,7 @@ dependencies = [
"regex",
"regex-syntax 0.8.3",
"reqwest",
"revision",
"revision 0.8.0",
"ring 0.17.8",
"rmpv",
"roaring",
@ -6159,7 +6184,7 @@ dependencies = [
"lru",
"parking_lot",
"quick_cache",
"revision",
"revision 0.7.1",
"sha2",
"tokio",
"vart",

View file

@ -81,7 +81,7 @@ reqwest = { version = "0.11.22", default-features = false, features = [
"blocking",
"gzip",
] }
revision = { version = "0.7.0", features = [
revision = { version = "0.8.0", features = [
"chrono",
"geo",
"roaring",
@ -96,9 +96,9 @@ serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
serde_pack = { version = "1.1.2", package = "rmp-serde" }
surrealdb = { version = "2", path = "lib", features = [
"protocol-http",
"protocol-ws",
"rustls",
"protocol-http",
"protocol-ws",
"rustls",
] }
tempfile = "3.8.1"
thiserror = "1.0.50"

View file

@ -122,7 +122,7 @@ reqwest = { version = "0.11.22", default-features = false, features = [
"stream",
"multipart",
], optional = true }
revision = { version = "0.7.0", features = ["chrono", "geo", "roaring", "regex", "rust_decimal", "uuid"] }
revision = { version = "0.8.0", features = ["chrono", "geo", "roaring", "regex", "rust_decimal", "uuid"] }
rmpv = "1.0.1"
roaring = { version = "0.10.2", features = ["serde"] }
rocksdb = { version = "0.21.0", features = ["lz4", "snappy"], optional = true }

View file

@ -16,9 +16,6 @@ use crate::sql::range::Range;
use crate::sql::table::Table;
use crate::sql::thing::Thing;
use crate::sql::value::Value;
use crate::sql::Ident;
use crate::sql::Idiom;
use crate::sql::Part;
use reblessive::tree::Stk;
#[cfg(not(target_arch = "wasm32"))]
use reblessive::TreeStack;
@ -446,23 +443,11 @@ impl Iterator {
stm: &Statement<'_>,
) -> Result<(), Error> {
if let Some(fetchs) = stm.fetch() {
let mut idioms = Vec::with_capacity(fetchs.0.len());
for fetch in fetchs.iter() {
let i: &Idiom;
let new_idiom: Idiom;
if let Value::Idiom(idiom) = &fetch.0 {
i = idiom;
} else if let Value::Param(param) = &fetch.0 {
let p = param.compute(stk, ctx, opt, None).await?;
if let Value::Strand(s) = p {
let p: Part = Part::Field(Ident(s.0));
new_idiom = Idiom(vec![p]);
i = &new_idiom;
} else {
return Err(Error::Thrown("Parameter should be a string".to_string()));
}
} else {
return Err(Error::Thrown("Invalid field".to_string()));
}
fetch.compute(stk, ctx, opt, &mut idioms).await?;
}
for i in &idioms {
let mut values = self.results.take()?;
// Loop over each result value
for obj in &mut values {

View file

@ -166,6 +166,12 @@ pub enum Error {
field: String,
},
/// The FETCH clause accepts idioms, strings and fields.
#[error("Found {value} on FETCH CLAUSE, but FETCH expects an idiom, a string or fields")]
InvalidFetch {
value: Value,
},
#[error("Found '{field}' in SPLIT ON clause on line {line}, but field is not present in SELECT expression")]
InvalidSplit {
line: usize,

View file

@ -1,6 +1,11 @@
use crate::ctx::Context;
use crate::dbs::Options;
use crate::err::Error;
use crate::sql::fmt::Fmt;
use crate::sql::statements::info::InfoStructure;
use crate::sql::Value;
use crate::sql::{Idiom, Value};
use crate::syn;
use reblessive::tree::Stk;
use revision::revisioned;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter};
@ -39,22 +44,111 @@ impl InfoStructure for Fetchs {
}
}
#[revisioned(revision = 1)]
#[revisioned(revision = 2)]
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub struct Fetch(pub Value);
pub struct Fetch(
#[revision(end = 2, convert_fn = "convert_fetch_idiom")] pub Idiom,
#[revision(start = 2)] pub Value,
);
impl Fetch {
fn convert_fetch_idiom(&mut self, _revision: u16, old: Idiom) -> Result<(), revision::Error> {
self.1 = if old.is_empty() {
Value::None
} else {
Value::Idiom(old)
};
Ok(())
}
pub(crate) async fn compute(
&self,
stk: &mut Stk,
ctx: &Context<'_>,
opt: &Options,
idioms: &mut Vec<Idiom>,
) -> Result<(), Error> {
let strand_or_idiom = |v: Value| match v {
Value::Strand(s) => Ok(Idiom::from(s.0)),
Value::Idiom(i) => Ok(i.to_owned()),
v => Err(Error::InvalidFetch {
value: v,
}),
};
match &self.1 {
Value::Idiom(idiom) => {
idioms.push(idiom.to_owned());
Ok(())
}
Value::Param(param) => {
let v = param.compute(stk, ctx, opt, None).await?;
idioms.push(strand_or_idiom(v)?);
Ok(())
}
Value::Function(f) => {
if f.name() == Some("type::field") {
let v = match f.args().first().unwrap() {
Value::Param(v) => v.compute(stk, ctx, opt, None).await?,
v => v.to_owned(),
};
idioms.push(strand_or_idiom(v)?);
Ok(())
} else if f.name() == Some("type::fields") {
// Get the first argument which is guaranteed to exist
let args = match f.args().first().unwrap() {
Value::Param(v) => v.compute(stk, ctx, opt, None).await?,
v => v.to_owned(),
};
// This value is always an array, so we can convert it
let args: Vec<Value> = args.try_into()?;
// This value is always an array, so we can convert it
for v in args.into_iter() {
let i = match v {
Value::Param(v) => {
strand_or_idiom(v.compute(stk, ctx, opt, None).await?)?
}
Value::Strand(s) => syn::idiom(s.as_str())?,
Value::Idiom(i) => i,
v => {
return Err(Error::InvalidFetch {
value: v,
})
}
};
idioms.push(i);
}
Ok(())
} else {
Err(Error::InvalidFetch {
value: Value::Function(f.clone()),
})
}
}
v => Err(Error::InvalidFetch {
value: v.clone(),
}),
}
}
}
impl From<Value> for Fetch {
fn from(value: Value) -> Self {
Self(Idiom(vec![]), value)
}
}
impl Deref for Fetch {
type Target = Value;
fn deref(&self) -> &Self::Target {
&self.0
&self.1
}
}
impl Display for Fetch {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(&self.0, f)
Display::fmt(&self.1, f)
}
}

View file

@ -1,10 +1,9 @@
use crate::ctx::Context;
use crate::dbs::Options;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::sql::fetch::Fetchs;
use crate::sql::value::Value;
use crate::sql::{Ident, Idiom};
use crate::{ctx::Context, sql::Part};
use derive::Store;
use reblessive::tree::Stk;
use revision::revisioned;
@ -39,23 +38,11 @@ impl OutputStatement {
let mut value = self.what.compute(stk, ctx, opt, doc).await?;
// Fetch any
if let Some(fetchs) = &self.fetch {
let mut idioms = Vec::with_capacity(fetchs.0.len());
for fetch in fetchs.iter() {
let i: &Idiom;
let new_idiom: Idiom;
if let Value::Idiom(idiom) = &fetch.0 {
i = idiom;
} else if let Value::Param(param) = &fetch.0 {
let p = param.compute(stk, ctx, opt, None).await?;
if let Value::Strand(s) = p {
let p: Part = Part::Field(Ident(s.0));
new_idiom = Idiom(vec![p]);
i = &new_idiom;
} else {
return Err(Error::Thrown("Parameter should be a string".to_string()));
}
} else {
return Err(Error::Thrown("Invalid field".to_string()));
}
fetch.compute(stk, ctx, opt, &mut idioms).await?
}
for i in &idioms {
value.fetch(stk, ctx, opt, i).await?;
}
}

View file

@ -1 +1,84 @@
use crate::err::Error;
use crate::sql::value::serde::ser;
use crate::sql::{Fetch, Idiom, Value};
use ser::Serializer as _;
use serde::ser::Error as _;
use serde::ser::Impossible;
use serde::Serialize;
pub(super) mod vec;
struct FetchSerializer;
impl ser::Serializer for FetchSerializer {
type Ok = Fetch;
type Error = Error;
type SerializeSeq = Impossible<Fetch, Error>;
type SerializeTuple = Impossible<Fetch, Error>;
type SerializeTupleStruct = SerializeFetch;
type SerializeTupleVariant = Impossible<Fetch, Error>;
type SerializeMap = Impossible<Fetch, Error>;
type SerializeStruct = Impossible<Fetch, Error>;
type SerializeStructVariant = Impossible<Fetch, Error>;
const EXPECTED: &'static str = "a `Fetch`";
fn serialize_tuple_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleStruct, Error> {
Ok(SerializeFetch::default())
}
}
#[derive(Default)]
struct SerializeFetch {
index: usize,
idiom: Option<Idiom>,
value: Option<Value>,
}
impl serde::ser::SerializeTupleStruct for SerializeFetch {
type Ok = Fetch;
type Error = Error;
fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: Serialize + ?Sized,
{
match self.index {
0 => {
self.idiom = Some(Idiom(value.serialize(ser::part::vec::Serializer.wrap())?));
}
1 => {
self.value = Some(value.serialize(ser::value::Serializer.wrap())?);
}
index => {
return Err(Error::custom(format!("unexpected `Fetch` index `{index}`")));
}
}
self.index += 1;
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
match (self.idiom, self.value) {
(Some(idiom), Some(value)) => Ok(Fetch(idiom, value)),
_ => Err(ser::Error::custom("`Fetch` missing required value(s)")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fetch() {
let fetch = Fetch::default();
let serialized = fetch.serialize(FetchSerializer.wrap()).unwrap();
assert_eq!(fetch, serialized);
}
}

View file

@ -2,6 +2,7 @@ pub mod opt;
use crate::err::Error;
use crate::sql::value::serde::ser;
use crate::sql::value::serde::ser::fetch::FetchSerializer;
use crate::sql::Fetch;
use ser::Serializer as _;
use serde::ser::Impossible;
@ -52,7 +53,7 @@ impl serde::ser::SerializeSeq for SerializeFetchVec {
where
T: Serialize + ?Sized,
{
self.0.push(Fetch(value.serialize(ser::value::Serializer.wrap())?));
self.0.push(value.serialize(FetchSerializer.wrap())?);
Ok(())
}

View file

@ -53,7 +53,7 @@ impl Parser<'_> {
}
}
/// Parses a list of idioms seperated by a `,`
/// Parses a list of idioms separated by a `,`
pub async fn parse_idiom_list(&mut self, ctx: &mut Stk) -> ParseResult<Vec<Idiom>> {
let mut res = vec![self.parse_plain_idiom(ctx).await?];
while self.eat(t!(",")) {

View file

@ -2,12 +2,13 @@
use reblessive::Stk;
use crate::sql::Fetch;
use crate::{
sql::{
changefeed::ChangeFeed,
index::{Distance, VectorType},
Base, Cond, Data, Duration, Fetch, Fetchs, Field, Fields, Group, Groups, Ident, Idiom,
Output, Permission, Permissions, Tables, Timeout, Value, View,
Base, Cond, Data, Duration, Fetchs, Field, Fields, Group, Groups, Ident, Idiom, Output,
Permission, Permissions, Tables, Timeout, Value, View,
},
syn::{
parser::{
@ -108,18 +109,39 @@ impl Parser<'_> {
if !self.eat(t!("FETCH")) {
return Ok(None);
}
let mut fetchs = vec![Fetch(self.try_parse_param_or_idiom(ctx).await?)];
let mut fetchs = self.try_parse_param_or_idiom_or_fields(ctx).await?;
while self.eat(t!(",")) {
fetchs.push(Fetch(self.try_parse_param_or_idiom(ctx).await?));
fetchs.append(&mut self.try_parse_param_or_idiom_or_fields(ctx).await?);
}
Ok(Some(Fetchs(fetchs)))
}
pub async fn try_parse_param_or_idiom(&mut self, ctx: &mut Stk) -> ParseResult<Value> {
if self.peek().kind == t!("$param") {
Ok(Value::Param(self.next_token_value()?))
} else {
Ok(Value::Idiom(self.parse_plain_idiom(ctx).await?))
pub async fn try_parse_param_or_idiom_or_fields(
&mut self,
ctx: &mut Stk,
) -> ParseResult<Vec<Fetch>> {
match self.peek().kind {
t!("$param") => Ok(vec![Value::Param(self.next_token_value()?).into()]),
t!("TYPE") => {
let fields = self.parse_fields(ctx).await?;
let fetches = fields
.0
.into_iter()
.filter_map(|f| {
if let Field::Single {
expr,
..
} = f
{
Some(expr.into())
} else {
None
}
})
.collect();
Ok(fetches)
}
_ => Ok(vec![Value::Idiom(self.parse_plain_idiom(ctx).await?).into()]),
}
}

View file

@ -1771,9 +1771,10 @@ SELECT bar as foo,[1,2],bar OMIT bar FROM ONLY a,1
start: Some(Start(Value::Object(Object(
[("a".to_owned(), Value::Bool(true))].into_iter().collect()
)))),
fetch: Some(Fetchs(vec![Fetch(Value::Idiom(Idiom(vec![Part::Field(Ident(
"foo".to_owned()
))])))])),
fetch: Some(Fetchs(vec![Fetch(
Idiom(vec![]),
Value::Idiom(Idiom(vec![Part::Field(Ident("foo".to_owned()))]))
)])),
version: Some(Version(Datetime(expected_datetime))),
timeout: None,
parallel: false,
@ -2035,11 +2036,14 @@ fn parse_live() {
assert_eq!(
stmt.fetch,
Some(Fetchs(vec![
Fetch(Value::Idiom(Idiom(vec![
Part::Field(Ident("a".to_owned())),
Part::Where(Value::Idiom(Idiom(vec![Part::Field(Ident("foo".to_owned()))]))),
]))),
Fetch(Value::Idiom(Idiom(vec![Part::Field(Ident("b".to_owned()))]))),
Fetch(
Idiom(vec![]),
Value::Idiom(Idiom(vec![
Part::Field(Ident("a".to_owned())),
Part::Where(Value::Idiom(Idiom(vec![Part::Field(Ident("foo".to_owned()))]))),
]))
),
Fetch(Idiom(vec![]), Value::Idiom(Idiom(vec![Part::Field(Ident("b".to_owned()))]))),
])),
)
}
@ -2063,9 +2067,10 @@ fn parse_return() {
res,
Statement::Output(OutputStatement {
what: Value::Idiom(Idiom(vec![Part::Field(Ident("RETRUN".to_owned()))])),
fetch: Some(Fetchs(vec![Fetch(Value::Idiom(Idiom(vec![Part::Field(
Ident("RETURN".to_owned()).to_owned()
)])))])),
fetch: Some(Fetchs(vec![Fetch(
Idiom(vec![]),
Value::Idiom(Idiom(vec![Part::Field(Ident("RETURN".to_owned()).to_owned())]))
)])),
}),
)
}

View file

@ -503,9 +503,10 @@ fn statements() -> Vec<Statement> {
start: Some(Start(Value::Object(Object(
[("a".to_owned(), Value::Bool(true))].into_iter().collect(),
)))),
fetch: Some(Fetchs(vec![Fetch(Value::Idiom(Idiom(vec![Part::Field(Ident(
"foo".to_owned(),
))])))])),
fetch: Some(Fetchs(vec![Fetch(
Idiom(vec![]),
Value::Idiom(Idiom(vec![Part::Field(Ident("foo".to_owned()))])),
)])),
version: Some(Version(Datetime(expected_datetime))),
timeout: None,
parallel: false,
@ -593,9 +594,10 @@ fn statements() -> Vec<Statement> {
}),
Statement::Output(OutputStatement {
what: Value::Idiom(Idiom(vec![Part::Field(Ident("RETRUN".to_owned()))])),
fetch: Some(Fetchs(vec![Fetch(Value::Idiom(Idiom(vec![Part::Field(
Ident("RETURN".to_owned()).to_owned(),
)])))])),
fetch: Some(Fetchs(vec![Fetch(
Idiom(vec![]),
Value::Idiom(Idiom(vec![Part::Field(Ident("RETURN".to_owned()).to_owned())])),
)])),
}),
Statement::Relate(RelateStatement {
only: true,

View file

@ -82,7 +82,7 @@ reqwest = { version = "0.11.22", default-features = false, features = [
"stream",
"multipart",
], optional = true }
revision = { version = "0.7.0", features = [
revision = { version = "0.8.0", features = [
"chrono",
"geo",
"roaring",

View file

@ -1,10 +1,12 @@
mod parse;
use parse::Parse;
mod helpers;
use crate::helpers::skip_ok;
use helpers::new_ds;
use surrealdb::dbs::Session;
use surrealdb::err::Error;
use surrealdb::sql::Value;
use surrealdb_core::sql::Number;
#[tokio::test]
async fn create_relate_select() -> Result<(), Error> {
@ -21,13 +23,17 @@ async fn create_relate_select() -> Result<(), Error> {
SELECT *, ->bought->product.* AS products FROM user;
SELECT *, ->bought AS products FROM user FETCH products;
SELECT *, ->(bought AS purchases) FROM user FETCH purchases, purchases.out;
LET $param1 = \"purchases\";
LET $param1 = 'purchases';
SELECT *, ->(bought AS purchases) FROM user FETCH $param1, purchases.out;
SELECT *, ->(bought AS purchases) FROM user FETCH type::field('purchases'), purchases.out;
SELECT *, ->(bought AS purchases) FROM user FETCH type::fields([$param1, 'purchases.out']);
LET $faultyparam = 1.0f;
SELECT *, ->(bought AS purchases) FROM user FETCH $faultyparam, purchases.out;
";
let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 14);
assert_eq!(res.len(), 18);
//
let tmp = res.remove(0).result?;
let val = Value::parse(
@ -278,12 +284,13 @@ async fn create_relate_select() -> Result<(), Error> {
]",
);
assert_eq!(tmp, val);
// Skip the LET $param statements
skip_ok(res, 1)?;
//
// Remove LET statement result
res.remove(0);
let tmp = res.remove(0).result?;
let val = Value::parse(
"[
for i in 0..3 {
let tmp = res.remove(0).result.unwrap_or_else(|e| panic!("{i} {e}"));
let val = Value::parse(
"[
{
id: user:jaime,
name: 'Jaime',
@ -324,7 +331,17 @@ async fn create_relate_select() -> Result<(), Error> {
]
}
]",
);
);
assert_eq!(tmp, val, "{i}");
}
// Ignore LET statement result
res.remove(0);
match res.remove(0).result {
Err(Error::InvalidFetch {
value: Value::Number(Number::Float(1.0)),
}) => {}
found => panic!("Expected Err(Error::InvalidFetch), found '{found:?}'"),
};
assert_eq!(tmp, val);
//
Ok(())