Make SQL subquery behaviour understandable and consistent

Closes #1408
Closes #1441
This commit is contained in:
Tobie Morgan Hitchcock 2023-02-19 15:45:50 +00:00
parent 5e2157a0a2
commit f7dd73212d
9 changed files with 298 additions and 51 deletions

View file

@ -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<'_>,

View file

@ -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<'_>,

View file

@ -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<'_>,

View file

@ -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<'_>,

View file

@ -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<'_>,

View file

@ -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<'_>,

View file

@ -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),
}
}

View file

@ -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
View 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(())
}