Make SQL subquery behaviour understandable and consistent
Closes #1408 Closes #1441
This commit is contained in:
parent
5e2157a0a2
commit
f7dd73212d
9 changed files with 298 additions and 51 deletions
|
@ -33,6 +33,15 @@ impl CreateStatement {
|
|||
true
|
||||
}
|
||||
|
||||
pub(crate) fn single(&self) -> bool {
|
||||
match self.what.len() {
|
||||
1 if self.what[0].is_object() => true,
|
||||
1 if self.what[0].is_thing() => true,
|
||||
1 if self.what[0].is_table() => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn compute(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
|
|
|
@ -34,6 +34,14 @@ impl DeleteStatement {
|
|||
true
|
||||
}
|
||||
|
||||
pub(crate) fn single(&self) -> bool {
|
||||
match self.what.len() {
|
||||
1 if self.what[0].is_object() => true,
|
||||
1 if self.what[0].is_thing() => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn compute(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
|
|
|
@ -37,6 +37,14 @@ impl InsertStatement {
|
|||
true
|
||||
}
|
||||
|
||||
pub(crate) fn single(&self) -> bool {
|
||||
match &self.data {
|
||||
Data::SingleExpression(v) if v.is_object() => true,
|
||||
Data::ValuesExpression(v) if v.len() == 1 => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn compute(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
|
|
|
@ -45,6 +45,16 @@ impl RelateStatement {
|
|||
true
|
||||
}
|
||||
|
||||
pub(crate) fn single(&self) -> bool {
|
||||
match (&self.from, &self.with) {
|
||||
(v, w) if v.is_object() && w.is_object() => true,
|
||||
(v, w) if v.is_object() && w.is_thing() => true,
|
||||
(v, w) if v.is_thing() && w.is_object() => true,
|
||||
(v, w) if v.is_thing() && w.is_thing() => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn compute(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
|
|
|
@ -46,19 +46,6 @@ pub struct SelectStatement {
|
|||
}
|
||||
|
||||
impl SelectStatement {
|
||||
pub(crate) async fn limit(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
opt: &Options,
|
||||
txn: &Transaction,
|
||||
doc: Option<&Value>,
|
||||
) -> Result<usize, Error> {
|
||||
match &self.limit {
|
||||
Some(v) => v.process(ctx, opt, txn, doc).await,
|
||||
None => Ok(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn writeable(&self) -> bool {
|
||||
if self.expr.iter().any(|v| match v {
|
||||
Field::All => false,
|
||||
|
@ -73,6 +60,14 @@ impl SelectStatement {
|
|||
self.cond.as_ref().map_or(false, |v| v.writeable())
|
||||
}
|
||||
|
||||
pub(crate) fn single(&self) -> bool {
|
||||
match self.what.len() {
|
||||
1 if self.what[0].is_object() => true,
|
||||
1 if self.what[0].is_thing() => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn compute(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
|
|
|
@ -35,6 +35,14 @@ impl UpdateStatement {
|
|||
true
|
||||
}
|
||||
|
||||
pub(crate) fn single(&self) -> bool {
|
||||
match self.what.len() {
|
||||
1 if self.what[0].is_object() => true,
|
||||
1 if self.what[0].is_thing() => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn compute(
|
||||
&self,
|
||||
ctx: &Context<'_>,
|
||||
|
|
|
@ -4,7 +4,6 @@ use crate::dbs::Transaction;
|
|||
use crate::err::Error;
|
||||
use crate::sql::comment::mightbespace;
|
||||
use crate::sql::error::IResult;
|
||||
use crate::sql::paths::ID;
|
||||
use crate::sql::statements::create::{create, CreateStatement};
|
||||
use crate::sql::statements::delete::{delete, DeleteStatement};
|
||||
use crate::sql::statements::ifelse::{ifelse, IfelseStatement};
|
||||
|
@ -67,27 +66,8 @@ impl Subquery {
|
|||
Self::Value(ref v) => v.compute(ctx, opt, txn, doc).await,
|
||||
Self::Ifelse(ref v) => v.compute(ctx, opt, txn, doc).await,
|
||||
Self::Select(ref v) => {
|
||||
// Duplicate context
|
||||
let mut ctx = Context::new(ctx);
|
||||
// Add parent document
|
||||
if let Some(doc) = doc {
|
||||
ctx.add_value("parent".into(), doc);
|
||||
}
|
||||
// Process subquery
|
||||
let res = v.compute(&ctx, opt, txn, doc).await?;
|
||||
// Process result
|
||||
match v.limit(&ctx, opt, txn, doc).await? {
|
||||
1 => match v.expr.single() {
|
||||
Some(v) => res.first().get(&ctx, opt, txn, &v).await,
|
||||
None => res.first().ok(),
|
||||
},
|
||||
_ => match v.expr.single() {
|
||||
Some(v) => res.get(&ctx, opt, txn, &v).await,
|
||||
None => res.ok(),
|
||||
},
|
||||
}
|
||||
}
|
||||
Self::Create(ref v) => {
|
||||
// Is this a single output?
|
||||
let one = v.single();
|
||||
// Duplicate context
|
||||
let mut ctx = Context::new(ctx);
|
||||
// Add parent document
|
||||
|
@ -96,14 +76,42 @@ impl Subquery {
|
|||
}
|
||||
// Process subquery
|
||||
match v.compute(&ctx, opt, txn, doc).await? {
|
||||
Value::Array(mut v) => match v.len() {
|
||||
1 => Ok(v.remove(0).pick(ID.as_ref())),
|
||||
_ => Ok(Value::from(v).pick(ID.as_ref())),
|
||||
// This is a single record result
|
||||
Value::Array(mut a) if one => match a.len() {
|
||||
// There was at least one result
|
||||
v if v > 0 => Ok(a.remove(0)),
|
||||
// There were no results
|
||||
_ => Ok(Value::None),
|
||||
},
|
||||
// This is standard query result
|
||||
v => Ok(v),
|
||||
}
|
||||
}
|
||||
Self::Create(ref v) => {
|
||||
// Is this a single output?
|
||||
let one = v.single();
|
||||
// Duplicate context
|
||||
let mut ctx = Context::new(ctx);
|
||||
// Add parent document
|
||||
if let Some(doc) = doc {
|
||||
ctx.add_value("parent".into(), doc);
|
||||
}
|
||||
// Process subquery
|
||||
match v.compute(&ctx, opt, txn, doc).await? {
|
||||
// This is a single record result
|
||||
Value::Array(mut a) if one => match a.len() {
|
||||
// There was at least one result
|
||||
v if v > 0 => Ok(a.remove(0)),
|
||||
// There were no results
|
||||
_ => Ok(Value::None),
|
||||
},
|
||||
// This is standard query result
|
||||
v => Ok(v),
|
||||
}
|
||||
}
|
||||
Self::Update(ref v) => {
|
||||
// Is this a single output?
|
||||
let one = v.single();
|
||||
// Duplicate context
|
||||
let mut ctx = Context::new(ctx);
|
||||
// Add parent document
|
||||
|
@ -112,14 +120,20 @@ impl Subquery {
|
|||
}
|
||||
// Process subquery
|
||||
match v.compute(&ctx, opt, txn, doc).await? {
|
||||
Value::Array(mut v) => match v.len() {
|
||||
1 => Ok(v.remove(0).pick(ID.as_ref())),
|
||||
_ => Ok(Value::from(v).pick(ID.as_ref())),
|
||||
// This is a single record result
|
||||
Value::Array(mut a) if one => match a.len() {
|
||||
// There was at least one result
|
||||
v if v > 0 => Ok(a.remove(0)),
|
||||
// There were no results
|
||||
_ => Ok(Value::None),
|
||||
},
|
||||
// This is standard query result
|
||||
v => Ok(v),
|
||||
}
|
||||
}
|
||||
Self::Delete(ref v) => {
|
||||
// Is this a single output?
|
||||
let one = v.single();
|
||||
// Duplicate context
|
||||
let mut ctx = Context::new(ctx);
|
||||
// Add parent document
|
||||
|
@ -128,14 +142,20 @@ impl Subquery {
|
|||
}
|
||||
// Process subquery
|
||||
match v.compute(&ctx, opt, txn, doc).await? {
|
||||
Value::Array(mut v) => match v.len() {
|
||||
1 => Ok(v.remove(0).pick(ID.as_ref())),
|
||||
_ => Ok(Value::from(v).pick(ID.as_ref())),
|
||||
// This is a single record result
|
||||
Value::Array(mut a) if one => match a.len() {
|
||||
// There was at least one result
|
||||
v if v > 0 => Ok(a.remove(0)),
|
||||
// There were no results
|
||||
_ => Ok(Value::None),
|
||||
},
|
||||
// This is standard query result
|
||||
v => Ok(v),
|
||||
}
|
||||
}
|
||||
Self::Relate(ref v) => {
|
||||
// Is this a single output?
|
||||
let one = v.single();
|
||||
// Duplicate context
|
||||
let mut ctx = Context::new(ctx);
|
||||
// Add parent document
|
||||
|
@ -144,14 +164,20 @@ impl Subquery {
|
|||
}
|
||||
// Process subquery
|
||||
match v.compute(&ctx, opt, txn, doc).await? {
|
||||
Value::Array(mut v) => match v.len() {
|
||||
1 => Ok(v.remove(0).pick(ID.as_ref())),
|
||||
_ => Ok(Value::from(v).pick(ID.as_ref())),
|
||||
// This is a single record result
|
||||
Value::Array(mut a) if one => match a.len() {
|
||||
// There was at least one result
|
||||
v if v > 0 => Ok(a.remove(0)),
|
||||
// There were no results
|
||||
_ => Ok(Value::None),
|
||||
},
|
||||
// This is standard query result
|
||||
v => Ok(v),
|
||||
}
|
||||
}
|
||||
Self::Insert(ref v) => {
|
||||
// Is this a single output?
|
||||
let one = v.single();
|
||||
// Duplicate context
|
||||
let mut ctx = Context::new(ctx);
|
||||
// Add parent document
|
||||
|
@ -160,10 +186,14 @@ impl Subquery {
|
|||
}
|
||||
// Process subquery
|
||||
match v.compute(&ctx, opt, txn, doc).await? {
|
||||
Value::Array(mut v) => match v.len() {
|
||||
1 => Ok(v.remove(0).pick(ID.as_ref())),
|
||||
_ => Ok(Value::from(v).pick(ID.as_ref())),
|
||||
// This is a single record result
|
||||
Value::Array(mut a) if one => match a.len() {
|
||||
// There was at least one result
|
||||
v if v > 0 => Ok(a.remove(0)),
|
||||
// There were no results
|
||||
_ => Ok(Value::None),
|
||||
},
|
||||
// This is standard query result
|
||||
v => Ok(v),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ fn ok_future_graph_subquery_recursion_depth() -> Result<(), Error> {
|
|||
}
|
||||
//
|
||||
let tmp = res.next().unwrap()?;
|
||||
let val = Value::parse("[ [42] ]");
|
||||
let val = Value::parse("[ { fut: [42] } ]");
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
Ok(())
|
||||
|
|
179
lib/tests/subquery.rs
Normal file
179
lib/tests/subquery.rs
Normal file
|
@ -0,0 +1,179 @@
|
|||
mod parse;
|
||||
use parse::Parse;
|
||||
use surrealdb::dbs::Session;
|
||||
use surrealdb::err::Error;
|
||||
use surrealdb::kvs::Datastore;
|
||||
use surrealdb::sql::Value;
|
||||
|
||||
#[tokio::test]
|
||||
async fn subquery_select() -> Result<(), Error> {
|
||||
let sql = "
|
||||
-- Create a record
|
||||
CREATE person:test SET name = 'Tobie', age = 21;
|
||||
-- Select all records, returning an array
|
||||
SELECT age >= 18 as adult FROM person;
|
||||
-- Select a specific record, still returning an array
|
||||
SELECT age >= 18 as adult FROM person:test;
|
||||
-- Select all records in a subquery, returning an array
|
||||
RETURN (SELECT age >= 18 AS adult FROM person);
|
||||
-- Select a specific record in a subquery, returning an object
|
||||
RETURN (SELECT age >= 18 AS adult FROM person:test);
|
||||
-- Using an outer SELECT, select all records in a subquery, returning an array
|
||||
SELECT * FROM (SELECT age >= 18 AS adult FROM person) WHERE adult = true;
|
||||
-- Using an outer SELECT, select a specific record in a subquery, returning an array
|
||||
SELECT * FROM (SELECT age >= 18 AS adult FROM person:test) WHERE adult = true;
|
||||
";
|
||||
let dbs = Datastore::new("memory").await?;
|
||||
let ses = Session::for_kv().with_ns("test").with_db("test");
|
||||
let res = &mut dbs.execute(&sql, &ses, None, false).await?;
|
||||
assert_eq!(res.len(), 7);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::parse(
|
||||
"[
|
||||
{
|
||||
age: 21,
|
||||
id: person:test,
|
||||
name: 'Tobie'
|
||||
}
|
||||
]",
|
||||
);
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::parse(
|
||||
"[
|
||||
{
|
||||
adult: true
|
||||
}
|
||||
]",
|
||||
);
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::parse(
|
||||
"[
|
||||
{
|
||||
adult: true
|
||||
}
|
||||
]",
|
||||
);
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::parse(
|
||||
"[
|
||||
{
|
||||
adult: true
|
||||
}
|
||||
]",
|
||||
);
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::parse(
|
||||
"{
|
||||
adult: true
|
||||
}",
|
||||
);
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::parse(
|
||||
"[
|
||||
{
|
||||
adult: true
|
||||
}
|
||||
]",
|
||||
);
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::parse(
|
||||
"[
|
||||
{
|
||||
adult: true
|
||||
}
|
||||
]",
|
||||
);
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subquery_ifelse() -> Result<(), Error> {
|
||||
let sql = "
|
||||
-- Check if the record exists
|
||||
LET $record = (SELECT *, count() AS count FROM person:test);
|
||||
-- Return the specified record
|
||||
RETURN $record;
|
||||
-- Update the record field if it exists
|
||||
IF $record.count THEN
|
||||
( UPDATE person:test SET sport += 'football' RETURN sport )
|
||||
ELSE
|
||||
( UPDATE person:test SET sport = ['basketball'] RETURN sport )
|
||||
END;
|
||||
-- Check if the record exists
|
||||
LET $record = (SELECT *, count() AS count FROM person:test);
|
||||
-- Return the specified record
|
||||
RETURN $record;
|
||||
-- Update the record field if it exists
|
||||
IF $record.count THEN
|
||||
( UPDATE person:test SET sport += 'football' RETURN sport )
|
||||
ELSE
|
||||
( UPDATE person:test SET sport = ['basketball'] RETURN sport )
|
||||
END;
|
||||
";
|
||||
let dbs = Datastore::new("memory").await?;
|
||||
let ses = Session::for_kv().with_ns("test").with_db("test");
|
||||
let res = &mut dbs.execute(&sql, &ses, None, false).await?;
|
||||
assert_eq!(res.len(), 6);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::None;
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::None;
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::parse(
|
||||
"{
|
||||
sport: [
|
||||
'basketball'
|
||||
]
|
||||
}",
|
||||
);
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::None;
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::parse(
|
||||
"{
|
||||
count: 1,
|
||||
id: person:test,
|
||||
sport: [
|
||||
'basketball'
|
||||
]
|
||||
}",
|
||||
);
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
let tmp = res.remove(0).result?;
|
||||
let val = Value::parse(
|
||||
"{
|
||||
sport: [
|
||||
'basketball',
|
||||
'football'
|
||||
]
|
||||
}",
|
||||
);
|
||||
assert_eq!(tmp, val);
|
||||
//
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in a new issue