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
|
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(
|
pub(crate) async fn compute(
|
||||||
&self,
|
&self,
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
|
|
|
@ -34,6 +34,14 @@ impl DeleteStatement {
|
||||||
true
|
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(
|
pub(crate) async fn compute(
|
||||||
&self,
|
&self,
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
|
|
|
@ -37,6 +37,14 @@ impl InsertStatement {
|
||||||
true
|
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(
|
pub(crate) async fn compute(
|
||||||
&self,
|
&self,
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
|
|
|
@ -45,6 +45,16 @@ impl RelateStatement {
|
||||||
true
|
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(
|
pub(crate) async fn compute(
|
||||||
&self,
|
&self,
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
|
|
|
@ -46,19 +46,6 @@ pub struct SelectStatement {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub(crate) fn writeable(&self) -> bool {
|
||||||
if self.expr.iter().any(|v| match v {
|
if self.expr.iter().any(|v| match v {
|
||||||
Field::All => false,
|
Field::All => false,
|
||||||
|
@ -73,6 +60,14 @@ impl SelectStatement {
|
||||||
self.cond.as_ref().map_or(false, |v| v.writeable())
|
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(
|
pub(crate) async fn compute(
|
||||||
&self,
|
&self,
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
|
|
|
@ -35,6 +35,14 @@ impl UpdateStatement {
|
||||||
true
|
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(
|
pub(crate) async fn compute(
|
||||||
&self,
|
&self,
|
||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
|
|
|
@ -4,7 +4,6 @@ use crate::dbs::Transaction;
|
||||||
use crate::err::Error;
|
use crate::err::Error;
|
||||||
use crate::sql::comment::mightbespace;
|
use crate::sql::comment::mightbespace;
|
||||||
use crate::sql::error::IResult;
|
use crate::sql::error::IResult;
|
||||||
use crate::sql::paths::ID;
|
|
||||||
use crate::sql::statements::create::{create, CreateStatement};
|
use crate::sql::statements::create::{create, CreateStatement};
|
||||||
use crate::sql::statements::delete::{delete, DeleteStatement};
|
use crate::sql::statements::delete::{delete, DeleteStatement};
|
||||||
use crate::sql::statements::ifelse::{ifelse, IfelseStatement};
|
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::Value(ref v) => v.compute(ctx, opt, txn, doc).await,
|
||||||
Self::Ifelse(ref v) => v.compute(ctx, opt, txn, doc).await,
|
Self::Ifelse(ref v) => v.compute(ctx, opt, txn, doc).await,
|
||||||
Self::Select(ref v) => {
|
Self::Select(ref v) => {
|
||||||
// Duplicate context
|
// Is this a single output?
|
||||||
let mut ctx = Context::new(ctx);
|
let one = v.single();
|
||||||
// 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) => {
|
|
||||||
// Duplicate context
|
// Duplicate context
|
||||||
let mut ctx = Context::new(ctx);
|
let mut ctx = Context::new(ctx);
|
||||||
// Add parent document
|
// Add parent document
|
||||||
|
@ -96,14 +76,42 @@ impl Subquery {
|
||||||
}
|
}
|
||||||
// Process subquery
|
// Process subquery
|
||||||
match v.compute(&ctx, opt, txn, doc).await? {
|
match v.compute(&ctx, opt, txn, doc).await? {
|
||||||
Value::Array(mut v) => match v.len() {
|
// This is a single record result
|
||||||
1 => Ok(v.remove(0).pick(ID.as_ref())),
|
Value::Array(mut a) if one => match a.len() {
|
||||||
_ => Ok(Value::from(v).pick(ID.as_ref())),
|
// 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),
|
v => Ok(v),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Update(ref v) => {
|
Self::Update(ref v) => {
|
||||||
|
// Is this a single output?
|
||||||
|
let one = v.single();
|
||||||
// Duplicate context
|
// Duplicate context
|
||||||
let mut ctx = Context::new(ctx);
|
let mut ctx = Context::new(ctx);
|
||||||
// Add parent document
|
// Add parent document
|
||||||
|
@ -112,14 +120,20 @@ impl Subquery {
|
||||||
}
|
}
|
||||||
// Process subquery
|
// Process subquery
|
||||||
match v.compute(&ctx, opt, txn, doc).await? {
|
match v.compute(&ctx, opt, txn, doc).await? {
|
||||||
Value::Array(mut v) => match v.len() {
|
// This is a single record result
|
||||||
1 => Ok(v.remove(0).pick(ID.as_ref())),
|
Value::Array(mut a) if one => match a.len() {
|
||||||
_ => Ok(Value::from(v).pick(ID.as_ref())),
|
// 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),
|
v => Ok(v),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Delete(ref v) => {
|
Self::Delete(ref v) => {
|
||||||
|
// Is this a single output?
|
||||||
|
let one = v.single();
|
||||||
// Duplicate context
|
// Duplicate context
|
||||||
let mut ctx = Context::new(ctx);
|
let mut ctx = Context::new(ctx);
|
||||||
// Add parent document
|
// Add parent document
|
||||||
|
@ -128,14 +142,20 @@ impl Subquery {
|
||||||
}
|
}
|
||||||
// Process subquery
|
// Process subquery
|
||||||
match v.compute(&ctx, opt, txn, doc).await? {
|
match v.compute(&ctx, opt, txn, doc).await? {
|
||||||
Value::Array(mut v) => match v.len() {
|
// This is a single record result
|
||||||
1 => Ok(v.remove(0).pick(ID.as_ref())),
|
Value::Array(mut a) if one => match a.len() {
|
||||||
_ => Ok(Value::from(v).pick(ID.as_ref())),
|
// 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),
|
v => Ok(v),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Relate(ref v) => {
|
Self::Relate(ref v) => {
|
||||||
|
// Is this a single output?
|
||||||
|
let one = v.single();
|
||||||
// Duplicate context
|
// Duplicate context
|
||||||
let mut ctx = Context::new(ctx);
|
let mut ctx = Context::new(ctx);
|
||||||
// Add parent document
|
// Add parent document
|
||||||
|
@ -144,14 +164,20 @@ impl Subquery {
|
||||||
}
|
}
|
||||||
// Process subquery
|
// Process subquery
|
||||||
match v.compute(&ctx, opt, txn, doc).await? {
|
match v.compute(&ctx, opt, txn, doc).await? {
|
||||||
Value::Array(mut v) => match v.len() {
|
// This is a single record result
|
||||||
1 => Ok(v.remove(0).pick(ID.as_ref())),
|
Value::Array(mut a) if one => match a.len() {
|
||||||
_ => Ok(Value::from(v).pick(ID.as_ref())),
|
// 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),
|
v => Ok(v),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::Insert(ref v) => {
|
Self::Insert(ref v) => {
|
||||||
|
// Is this a single output?
|
||||||
|
let one = v.single();
|
||||||
// Duplicate context
|
// Duplicate context
|
||||||
let mut ctx = Context::new(ctx);
|
let mut ctx = Context::new(ctx);
|
||||||
// Add parent document
|
// Add parent document
|
||||||
|
@ -160,10 +186,14 @@ impl Subquery {
|
||||||
}
|
}
|
||||||
// Process subquery
|
// Process subquery
|
||||||
match v.compute(&ctx, opt, txn, doc).await? {
|
match v.compute(&ctx, opt, txn, doc).await? {
|
||||||
Value::Array(mut v) => match v.len() {
|
// This is a single record result
|
||||||
1 => Ok(v.remove(0).pick(ID.as_ref())),
|
Value::Array(mut a) if one => match a.len() {
|
||||||
_ => Ok(Value::from(v).pick(ID.as_ref())),
|
// 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),
|
v => Ok(v),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ fn ok_future_graph_subquery_recursion_depth() -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
let tmp = res.next().unwrap()?;
|
let tmp = res.next().unwrap()?;
|
||||||
let val = Value::parse("[ [42] ]");
|
let val = Value::parse("[ { fut: [42] } ]");
|
||||||
assert_eq!(tmp, val);
|
assert_eq!(tmp, val);
|
||||||
//
|
//
|
||||||
Ok(())
|
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