Add DEFINE TABLE ... RELATION ()

Co-authored-by: Tobie Morgan Hitchcock <tobie@surrealdb.com>
This commit is contained in:
Raphael Darley 2024-03-19 11:20:58 +00:00 committed by GitHub
parent 7f6abc69bb
commit 50125cb2b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 602 additions and 47 deletions

View file

@ -13,6 +13,8 @@ impl<'a> Document<'a> {
txn: &Transaction,
stm: &Statement<'_>,
) -> Result<Value, Error> {
// Check if table has corrent relation status
self.relation(ctx, opt, txn, stm).await?;
// Alter record data
self.alter(ctx, opt, txn, stm).await?;
// Merge fields data

View file

@ -50,6 +50,8 @@ impl<'a> Document<'a> {
txn: &Transaction,
stm: &Statement<'_>,
) -> Result<Value, Error> {
// Check if table has correct relation status
self.relation(ctx, opt, txn, stm).await?;
// Merge record data
self.merge(ctx, opt, txn, stm).await?;
// Merge fields data

View file

@ -34,6 +34,7 @@ mod lives; // Processes any live queries relevant for this document
mod merge; // Merges any field changes for an INSERT statement
mod pluck; // Pulls the projected expressions from the document
mod purge; // Deletes this document, and any edges or indexes
mod relation; // Checks whether the record is the right kind for the table
mod reset; // Resets internal fields which were set for this document
mod store; // Writes the document content to the storage engine
mod table; // Processes any foreign tables relevant for this document

View file

@ -13,6 +13,8 @@ impl<'a> Document<'a> {
txn: &Transaction,
stm: &Statement<'_>,
) -> Result<Value, Error> {
// Check if table has correct relation status
self.relation(ctx, opt, txn, stm).await?;
// Check current record
match self.current.doc.is_some() {
// Create new edge

42
core/src/doc/relation.rs Normal file
View file

@ -0,0 +1,42 @@
use crate::ctx::Context;
use crate::dbs::Statement;
use crate::dbs::{Options, Transaction};
use crate::doc::Document;
use crate::err::Error;
impl<'a> Document<'a> {
pub async fn relation(
&mut self,
_ctx: &Context<'_>,
opt: &Options,
txn: &Transaction,
stm: &Statement<'_>,
) -> Result<(), Error> {
let tb = self.tb(opt, txn).await?;
let rid = self.id.as_ref().unwrap();
match stm {
Statement::Create(_) | Statement::Insert(_) => {
if !tb.allows_normal() {
return Err(Error::TableCheck {
thing: rid.to_string(),
relation: false,
target_type: tb.kind.clone(),
});
}
}
Statement::Relate(_) => {
if !tb.allows_relation() {
return Err(Error::TableCheck {
thing: rid.to_string(),
relation: true,
target_type: tb.kind.clone(),
});
}
}
_ => {}
}
// Carry on
Ok(())
}
}

View file

@ -6,6 +6,7 @@ use crate::sql::idiom::Idiom;
use crate::sql::index::Distance;
use crate::sql::thing::Thing;
use crate::sql::value::Value;
use crate::sql::TableType;
use crate::syn::error::RenderedError as RenderedParserError;
use crate::vs::Error as VersionstampError;
use base64_lib::DecodeError as Base64Error;
@ -525,6 +526,14 @@ pub enum Error {
value: String,
},
/// The specified table is not configured for the type of record being added
#[error("Found record: `{thing}` which is {}a relation, but expected a `target_type`", if *relation { "not " } else { "" })]
TableCheck {
thing: String,
relation: bool,
target_type: TableType,
},
/// The specified field did not conform to the field type check
#[error("Found {value} for field `{field}`, with record `{thing}`, but expected a {check}")]
FieldCheck {

View file

@ -2,6 +2,7 @@ use crate::key::database::tb;
use crate::key::database::tb::Tb;
use crate::kvs::ScanPage;
use crate::sql::statements::DefineTableStatement;
use crate::sql::TableType;
#[tokio::test]
#[serial]
@ -71,6 +72,7 @@ async fn table_definitions_can_be_deleted() {
changefeed: None,
comment: None,
if_not_exists: false,
kind: TableType::Any,
};
tx.set(&key, &value).await.unwrap();

View file

@ -293,8 +293,8 @@ mod tests {
#[test]
fn pretty_define_query() {
let query = parse("DEFINE TABLE test SCHEMAFULL PERMISSIONS FOR create, update, delete NONE FOR select WHERE public = true;").unwrap();
assert_eq!(format!("{}", query), "DEFINE TABLE test SCHEMAFULL PERMISSIONS FOR select WHERE public = true, FOR create, update, delete NONE;");
assert_eq!(format!("{:#}", query), "DEFINE TABLE test SCHEMAFULL\n\tPERMISSIONS\n\t\tFOR select\n\t\t\tWHERE public = true\n\t\tFOR create, update, delete NONE\n;");
assert_eq!(format!("{}", query), "DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS FOR select WHERE public = true, FOR create, update, delete NONE;");
assert_eq!(format!("{:#}", query), "DEFINE TABLE test TYPE ANY SCHEMAFULL\n\tPERMISSIONS\n\t\tFOR select\n\t\t\tWHERE public = true\n\t\tFOR create, update, delete NONE\n;");
}
#[test]

View file

@ -58,6 +58,7 @@ pub(crate) mod statement;
pub(crate) mod strand;
pub(crate) mod subquery;
pub(crate) mod table;
pub(crate) mod table_type;
pub(crate) mod thing;
pub(crate) mod timeout;
pub(crate) mod tokenizer;
@ -133,6 +134,7 @@ pub use self::strand::Strand;
pub use self::subquery::Subquery;
pub use self::table::Table;
pub use self::table::Tables;
pub use self::table_type::{Relation, TableType};
pub use self::thing::Thing;
pub use self::timeout::Timeout;
pub use self::tokenizer::Tokenizer;

View file

@ -3,10 +3,12 @@ use crate::dbs::{Options, Transaction};
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::iam::{Action, ResourceKind};
use crate::sql::statements::DefineTableStatement;
use crate::sql::Part;
use crate::sql::{
fmt::is_pretty, fmt::pretty_indent, Base, Ident, Idiom, Kind, Permissions, Strand, Value,
};
use crate::sql::{Relation, TableType};
use derive::Store;
use revision::revisioned;
use serde::{Deserialize, Serialize};
@ -51,20 +53,30 @@ impl DefineFieldStatement {
if self.if_not_exists && run.get_tb_field(opt.ns(), opt.db(), &self.what, &fd).await.is_ok()
{
return Err(Error::FdAlreadyExists {
value: self.name.to_string(),
value: fd,
});
}
// Process the statement
let key = crate::key::table::fd::new(opt.ns(), opt.db(), &self.what, &fd);
run.add_ns(opt.ns(), opt.strict).await?;
run.add_db(opt.ns(), opt.db(), opt.strict).await?;
run.add_tb(opt.ns(), opt.db(), &self.what, opt.strict).await?;
let tb = run.add_tb(opt.ns(), opt.db(), &self.what, opt.strict).await?;
let key = crate::key::table::fd::new(opt.ns(), opt.db(), &self.what, &fd);
run.set(
key,
DefineFieldStatement {
if_not_exists: false,
..self.clone()
},
)
.await?;
// find existing field definitions.
let fields = run.all_tb_fields(opt.ns(), opt.db(), &self.what).await.ok();
// Process possible recursive_definitions.
if let Some(mut cur_kind) = self.kind.as_ref().and_then(|x| x.inner_kind()) {
let mut name = self.name.clone();
// find existing field definitions.
let fields = run.all_tb_fields(opt.ns(), opt.db(), &self.what).await.ok();
loop {
let new_kind = cur_kind.inner_kind();
name.0.push(Part::All);
@ -103,14 +115,48 @@ impl DefineFieldStatement {
}
}
run.set(
key,
DefineFieldStatement {
if_not_exists: false,
..self.clone()
},
)
.await?;
let new_tb = match (fd.as_str(), tb.kind.clone(), self.kind.clone()) {
("in", TableType::Relation(rel), Some(dk)) => {
if !matches!(dk, Kind::Record(_)) {
return Err(Error::Thrown("in field on a relation must be a record".into()));
};
if rel.from.as_ref() != Some(&dk) {
Some(DefineTableStatement {
kind: TableType::Relation(Relation {
from: Some(dk),
..rel
}),
..tb
})
} else {
None
}
}
("out", TableType::Relation(rel), Some(dk)) => {
if !matches!(dk, Kind::Record(_)) {
return Err(Error::Thrown("out field on a relation must be a record".into()));
};
if rel.to.as_ref() != Some(&dk) {
Some(DefineTableStatement {
kind: TableType::Relation(Relation {
to: Some(dk),
..rel
}),
..tb
})
} else {
None
}
}
_ => None,
};
if let Some(tb) = new_tb {
let key = crate::key::database::tb::new(opt.ns(), opt.db(), &self.what);
run.set(key, &tb).await?;
let key = crate::key::table::ft::prefix(opt.ns(), opt.db(), &self.what);
run.clr(key).await?;
}
// Clear the cache
let key = crate::key::table::fd::prefix(opt.ns(), opt.db(), &self.what);
run.clr(key).await?;

View file

@ -1,9 +1,3 @@
use std::fmt::{self, Display, Write};
use derive::Store;
use revision::revisioned;
use serde::{Deserialize, Serialize};
use crate::ctx::Context;
use crate::dbs::{Force, Options, Transaction};
use crate::doc::CursorDoc;
@ -17,9 +11,17 @@ use crate::sql::{
};
use std::sync::Arc;
use crate::sql::{Idiom, Kind, Part, Table, TableType};
use derive::Store;
use revision::revisioned;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Write};
use super::DefineFieldStatement;
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[revisioned(revision = 2)]
#[revisioned(revision = 3)]
pub struct DefineTableStatement {
pub id: Option<u32>,
pub name: Ident,
@ -31,6 +33,8 @@ pub struct DefineTableStatement {
pub comment: Option<Strand>,
#[revision(start = 2)]
pub if_not_exists: bool,
#[revision(start = 3)]
pub kind: TableType,
}
impl DefineTableStatement {
@ -69,6 +73,36 @@ impl DefineTableStatement {
..self.clone()
}
};
if let TableType::Relation(rel) = &self.kind {
let tb: &str = &self.name;
let in_kind = rel.from.clone().unwrap_or(Kind::Record(vec![]));
let out_kind = rel.to.clone().unwrap_or(Kind::Record(vec![]));
let in_key = crate::key::table::fd::new(opt.ns(), opt.db(), tb, "in");
let out_key = crate::key::table::fd::new(opt.ns(), opt.db(), tb, "out");
run.set(
in_key,
DefineFieldStatement {
name: Idiom(vec![Part::from("in")]),
what: tb.into(),
kind: Some(in_kind),
..Default::default()
},
)
.await?;
run.set(
out_key,
DefineFieldStatement {
name: Idiom(vec![Part::from("out")]),
what: tb.into(),
kind: Some(out_kind),
..Default::default()
},
)
.await?;
}
let tb_key = crate::key::table::fd::prefix(opt.ns(), opt.db(), &self.name);
run.clr(tb_key).await?;
run.set(key, &dt).await?;
// Check if table is a view
if let Some(view) = &self.view {
@ -100,11 +134,30 @@ impl DefineTableStatement {
} else if dt.changefeed.is_some() {
run.record_table_change(opt.ns(), opt.db(), self.name.0.as_str(), &dt);
}
// Ok all good
Ok(Value::None)
}
}
impl DefineTableStatement {
pub fn is_relation(&self) -> bool {
matches!(self.kind, TableType::Relation(_))
}
pub fn allows_relation(&self) -> bool {
matches!(self.kind, TableType::Relation(_) | TableType::Any)
}
pub fn allows_normal(&self) -> bool {
matches!(self.kind, TableType::Normal | TableType::Any)
}
}
fn get_tables_from_kind(tables: &[Table]) -> String {
tables.iter().map(|t| t.0.as_str()).collect::<Vec<_>>().join(" | ")
}
impl Display for DefineTableStatement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DEFINE TABLE")?;
@ -112,6 +165,24 @@ impl Display for DefineTableStatement {
write!(f, " IF NOT EXISTS")?
}
write!(f, " {}", self.name)?;
write!(f, " TYPE")?;
match &self.kind {
TableType::Normal => {
f.write_str(" NORMAL")?;
}
TableType::Relation(rel) => {
f.write_str(" RELATION")?;
if let Some(Kind::Record(kind)) = &rel.from {
write!(f, " IN {}", get_tables_from_kind(kind))?;
}
if let Some(Kind::Record(kind)) = &rel.to {
write!(f, " OUT {}", get_tables_from_kind(kind))?;
}
}
TableType::Any => {
f.write_str(" ANY")?;
}
}
if self.drop {
f.write_str(" DROP")?;
}

View file

@ -0,0 +1,50 @@
use revision::revisioned;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fmt::Display;
use super::{Kind, Table};
/// The type of records stored by a table
#[derive(Debug, Default, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[revisioned(revision = 1)]
pub enum TableType {
#[default]
Any,
Normal,
Relation(Relation),
}
impl Display for TableType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
TableType::Normal => {
f.write_str(" NORMAL")?;
}
TableType::Relation(rel) => {
f.write_str(" RELATION")?;
if let Some(Kind::Record(kind)) = &rel.from {
write!(f, " IN {}", get_tables_from_kind(kind))?;
}
if let Some(Kind::Record(kind)) = &rel.to {
write!(f, " OUT {}", get_tables_from_kind(kind))?;
}
}
TableType::Any => {
f.write_str(" ANY")?;
}
}
Ok(())
}
}
fn get_tables_from_kind(tables: &[Table]) -> String {
tables.iter().map(|t| t.0.as_str()).collect::<Vec<_>>().join(" | ")
}
#[derive(Debug, Default, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[revisioned(revision = 1)]
pub struct Relation {
pub from: Option<Kind>,
pub to: Option<Kind>,
}

View file

@ -40,6 +40,7 @@ mod permission;
mod permissions;
mod primitive;
mod range;
mod relation;
mod scoring;
mod split;
mod start;
@ -48,6 +49,7 @@ mod strand;
mod string;
mod subquery;
mod table;
mod table_type;
mod thing;
mod timeout;
mod tokenizer;

View file

@ -0,0 +1,70 @@
use crate::err::Error;
use crate::sql::value::serde::ser;
use crate::sql::Kind;
use crate::sql::Relation;
use ser::Serializer as _;
use serde::ser::Error as _;
use serde::ser::Impossible;
use serde::ser::Serialize;
pub struct Serializer;
impl ser::Serializer for Serializer {
type Ok = Relation;
type Error = Error;
type SerializeSeq = Impossible<Relation, Error>;
type SerializeTuple = Impossible<Relation, Error>;
type SerializeTupleStruct = Impossible<Relation, Error>;
type SerializeTupleVariant = Impossible<Relation, Error>;
type SerializeMap = Impossible<Relation, Error>;
type SerializeStruct = SerializeRelation;
type SerializeStructVariant = Impossible<Relation, Error>;
const EXPECTED: &'static str = "a struct `Relation`";
#[inline]
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Error> {
Ok(SerializeRelation::default())
}
}
#[derive(Default)]
pub struct SerializeRelation {
from: Option<Kind>,
to: Option<Kind>,
}
impl serde::ser::SerializeStruct for SerializeRelation {
type Ok = Relation;
type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
match key {
"from" => {
self.from = value.serialize(ser::kind::opt::Serializer.wrap())?;
}
"to" => {
self.to = value.serialize(ser::kind::opt::Serializer.wrap())?;
}
key => {
return Err(Error::custom(format!("unexpected field `Relation::{key}`")));
}
}
Ok(())
}
fn end(self) -> Result<Self::Ok, Error> {
Ok(Relation {
from: self.from,
to: self.to,
})
}
}

View file

@ -5,6 +5,7 @@ use crate::sql::value::serde::ser;
use crate::sql::Ident;
use crate::sql::Permissions;
use crate::sql::Strand;
use crate::sql::TableType;
use crate::sql::View;
use ser::Serializer as _;
use serde::ser::Error as _;
@ -48,6 +49,7 @@ pub struct SerializeDefineTableStatement {
changefeed: Option<ChangeFeed>,
comment: Option<Strand>,
if_not_exists: bool,
kind: TableType,
}
impl serde::ser::SerializeStruct for SerializeDefineTableStatement {
@ -83,6 +85,9 @@ impl serde::ser::SerializeStruct for SerializeDefineTableStatement {
"comment" => {
self.comment = value.serialize(ser::strand::opt::Serializer.wrap())?;
}
"kind" => {
self.kind = value.serialize(ser::table_type::Serializer.wrap())?;
}
"if_not_exists" => {
self.if_not_exists = value.serialize(ser::primitive::bool::Serializer.wrap())?
}
@ -105,6 +110,7 @@ impl serde::ser::SerializeStruct for SerializeDefineTableStatement {
permissions: self.permissions,
changefeed: self.changefeed,
comment: self.comment,
kind: self.kind,
if_not_exists: self.if_not_exists,
})
}

View file

@ -0,0 +1,56 @@
use crate::err::Error;
use crate::sql::value::serde::ser;
use crate::sql::TableType;
use serde::ser::Error as _;
use serde::ser::Impossible;
use serde::ser::Serialize;
pub struct Serializer;
impl ser::Serializer for Serializer {
type Ok = TableType;
type Error = Error;
type SerializeSeq = Impossible<TableType, Error>;
type SerializeTuple = Impossible<TableType, Error>;
type SerializeTupleStruct = Impossible<TableType, Error>;
type SerializeTupleVariant = Impossible<TableType, Error>;
type SerializeMap = Impossible<TableType, Error>;
type SerializeStruct = Impossible<TableType, Error>;
type SerializeStructVariant = Impossible<TableType, Error>;
const EXPECTED: &'static str = "a `TableType`";
fn serialize_newtype_variant<T>(
self,
name: &'static str,
_variant_index: u32,
variant: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
match variant {
"Relation" => {
Ok(TableType::Relation(value.serialize(ser::relation::Serializer.wrap())?))
}
variant => {
Err(Error::custom(format!("unexpected newtype variant `{name}::{variant}`")))
}
}
}
fn serialize_unit_variant(
self,
name: &'static str,
_variant_index: u32,
variant: &'static str,
) -> Result<Self::Ok, Error> {
match variant {
"Normal" => Ok(TableType::Normal),
"Any" => Ok(TableType::Any),
variant => Err(Error::custom(format!("unknown variant `{name}::{variant}`"))),
}
}
}

View file

@ -9,8 +9,14 @@ use super::super::super::{
use crate::sql::{
statements::DefineTableStatement, ChangeFeed, Permission, Permissions, Strand, View,
};
use crate::{
sql::{Kind, Relation, TableType},
syn::v1::common::verbar,
syn::v1::ParseError,
};
use nom::{branch::alt, bytes::complete::tag_no_case, combinator::cut, multi::many0};
use nom::{combinator::opt, sequence::tuple};
use nom::{combinator::opt, multi::separated_list1, sequence::tuple, Err};
pub fn table(i: &str) -> IResult<&str, DefineTableStatement> {
let (i, _) = tag_no_case("TABLE")(i)?;
@ -23,7 +29,7 @@ pub fn table(i: &str) -> IResult<&str, DefineTableStatement> {
let (i, name) = cut(ident)(i)?;
let (i, opts) = many0(table_opts)(i)?;
let (i, _) = expected(
"DROP, SCHEMALESS, SCHEMAFUL(L), VIEW, CHANGEFEED, PERMISSIONS, or COMMENT",
"TYPE, RELATION, DROP, SCHEMALESS, SCHEMAFUL(L), VIEW, CHANGEFEED, PERMISSIONS, or COMMENT",
ending::query,
)(i)?;
// Create the base statement
@ -57,6 +63,9 @@ pub fn table(i: &str) -> IResult<&str, DefineTableStatement> {
DefineTableOption::Permissions(v) => {
res.permissions = v;
}
DefineTableOption::TableType(t) => {
res.kind = t;
}
}
}
// Return the statement
@ -72,6 +81,42 @@ enum DefineTableOption {
Comment(Strand),
Permissions(Permissions),
ChangeFeed(ChangeFeed),
TableType(TableType),
}
enum RelationDir {
From(Kind),
To(Kind),
}
impl Relation {
fn merge<'a>(&mut self, i: &'a str, other: RelationDir) -> IResult<&'a str, ()> {
//TODO: error if both self and other are some
match other {
RelationDir::From(f) => {
if self.from.is_some() {
Err(Err::Failure(ParseError::Expected {
tried: i,
expected: "only one IN clause",
}))
} else {
self.from = Some(f);
Ok((i, ()))
}
}
RelationDir::To(t) => {
if self.to.is_some() {
Err(Err::Failure(ParseError::Expected {
tried: i,
expected: "only one OUT clause",
}))
} else {
self.to = Some(t);
Ok((i, ()))
}
}
}
}
}
fn table_opts(i: &str) -> IResult<&str, DefineTableOption> {
@ -83,6 +128,8 @@ fn table_opts(i: &str) -> IResult<&str, DefineTableOption> {
table_schemafull,
table_permissions,
table_changefeed,
table_type,
table_relation,
))(i)
}
@ -130,6 +177,56 @@ fn table_permissions(i: &str) -> IResult<&str, DefineTableOption> {
Ok((i, DefineTableOption::Permissions(v)))
}
fn table_type(i: &str) -> IResult<&str, DefineTableOption> {
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("TYPE")(i)?;
alt((table_normal, table_any, table_relation))(i)
}
fn table_normal(i: &str) -> IResult<&str, DefineTableOption> {
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("NORMAL")(i)?;
Ok((i, DefineTableOption::TableType(TableType::Normal)))
}
fn table_any(i: &str) -> IResult<&str, DefineTableOption> {
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("ANY")(i)?;
Ok((i, DefineTableOption::TableType(TableType::Any)))
}
fn table_relation(i: &str) -> IResult<&str, DefineTableOption> {
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("RELATION")(i)?;
let (i, dirs) = many0(alt((relation_from, relation_to)))(i)?;
let mut relation: Relation = Default::default();
for dir in dirs {
relation.merge(i, dir)?;
}
Ok((i, DefineTableOption::TableType(TableType::Relation(relation))))
}
fn relation_from(i: &str) -> IResult<&str, RelationDir> {
let (i, _) = shouldbespace(i)?;
let (i, _) = alt((tag_no_case("FROM"), tag_no_case("IN")))(i)?;
let (i, _) = shouldbespace(i)?;
let (i, idents) = separated_list1(verbar, ident)(i)?;
Ok((i, RelationDir::From(Kind::Record(idents.into_iter().map(Into::into).collect()))))
}
fn relation_to(i: &str) -> IResult<&str, RelationDir> {
let (i, _) = shouldbespace(i)?;
let (i, _) = alt((tag_no_case("TO"), tag_no_case("OUT")))(i)?;
let (i, _) = shouldbespace(i)?;
let (i, idents) = separated_list1(verbar, ident)(i)?;
Ok((i, RelationDir::To(Kind::Record(idents.into_iter().map(Into::into).collect()))))
}
#[cfg(test)]
mod tests {
@ -137,7 +234,7 @@ mod tests {
#[test]
fn define_table_with_changefeed() {
let sql = "TABLE mytable SCHEMALESS CHANGEFEED 1h PERMISSIONS NONE";
let sql = "TABLE mytable TYPE ANY SCHEMALESS CHANGEFEED 1h PERMISSIONS NONE";
let res = table(sql);
let out = res.unwrap().1;
assert_eq!(format!("DEFINE {sql}"), format!("{}", out));

View file

@ -161,6 +161,7 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
UniCase::ascii("PUNCT") => TokenKind::Keyword(Keyword::Punct),
UniCase::ascii("READONLY") => TokenKind::Keyword(Keyword::Readonly),
UniCase::ascii("RELATE") => TokenKind::Keyword(Keyword::Relate),
UniCase::ascii("RELATION") => TokenKind::Keyword(Keyword::Relation),
UniCase::ascii("REMOVE") => TokenKind::Keyword(Keyword::Remove),
UniCase::ascii("REPLACE") => TokenKind::Keyword(Keyword::Replace),
UniCase::ascii("RETURN") => TokenKind::Keyword(Keyword::Return),
@ -191,6 +192,7 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
UniCase::ascii("THEN") => TokenKind::Keyword(Keyword::Then),
UniCase::ascii("THROW") => TokenKind::Keyword(Keyword::Throw),
UniCase::ascii("TIMEOUT") => TokenKind::Keyword(Keyword::Timeout),
UniCase::ascii("TO") => TokenKind::Keyword(Keyword::To),
UniCase::ascii("TOKENIZERS") => TokenKind::Keyword(Keyword::Tokenizers),
UniCase::ascii("TOKEN") => TokenKind::Keyword(Keyword::Token),
UniCase::ascii("TRANSACTION") => TokenKind::Keyword(Keyword::Transaction),
@ -227,6 +229,8 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
UniCase::ascii("CONTAINSNOT") => TokenKind::Keyword(Keyword::ContainsNot),
UniCase::ascii("CONTAINS") => TokenKind::Keyword(Keyword::Contains),
UniCase::ascii("IN") => TokenKind::Keyword(Keyword::In),
UniCase::ascii("OUT") => TokenKind::Keyword(Keyword::Out),
UniCase::ascii("NORMAL") => TokenKind::Keyword(Keyword::Normal),
UniCase::ascii("ANY") => TokenKind::Keyword(Keyword::Any),
UniCase::ascii("ARRAY") => TokenKind::Keyword(Keyword::Array),

View file

@ -1,3 +1,4 @@
use crate::sql::{table_type, TableType};
use crate::{
sql::{
filter::Filter,
@ -9,7 +10,7 @@ use crate::{
DefineTableStatement, DefineTokenStatement, DefineUserStatement,
},
tokenizer::Tokenizer,
Ident, Idioms, Index, Param, Permissions, Scoring, Strand, Values,
Ident, Idioms, Index, Kind, Param, Permissions, Scoring, Strand, Values,
},
syn::v2::{
parser::{
@ -353,6 +354,28 @@ impl Parser<'_> {
self.pop_peek();
res.drop = true;
}
t!("RELATION") => {
self.pop_peek();
res.kind = TableType::Relation(self.parse_relation_schema()?);
}
t!("TYPE") => {
self.pop_peek();
match self.peek_kind() {
t!("NORMAL") => {
self.pop_peek();
res.kind = TableType::Normal;
}
t!("RELATION") => {
self.pop_peek();
res.kind = TableType::Relation(self.parse_relation_schema()?);
}
t!("ANY") => {
self.pop_peek();
res.kind = TableType::Any;
}
x => unexpected!(self, x, "`NORMAL`, `RELATION`, or `ANY`"),
}
}
t!("SCHEMALESS") => {
self.pop_peek();
res.full = false;
@ -765,4 +788,35 @@ impl Parser<'_> {
}
Ok(res)
}
pub fn parse_relation_schema(&mut self) -> ParseResult<table_type::Relation> {
let mut res = table_type::Relation {
from: None,
to: None,
};
loop {
match self.peek_kind() {
t!("FROM") => {
self.pop_peek();
let from = self.parse_tables()?;
res.from = Some(from);
}
t!("TO") => {
self.pop_peek();
let to = self.parse_tables()?;
res.to = Some(to);
}
_ => break,
}
}
Ok(res)
}
pub fn parse_tables(&mut self) -> ParseResult<Kind> {
let mut names = vec![self.next_token_value()?];
while self.eat(t!("|")) {
names.push(self.next_token_value()?);
}
Ok(Kind::Record(names))
}
}

View file

@ -25,7 +25,7 @@ use crate::{
Expression, Fetch, Fetchs, Field, Fields, Future, Graph, Group, Groups, Id, Ident, Idiom,
Idioms, Index, Kind, Limit, Number, Object, Operator, Order, Orders, Output, Param, Part,
Permission, Permissions, Scoring, Split, Splits, Start, Statement, Strand, Subquery, Table,
Tables, Thing, Timeout, Uuid, Value, Values, Version, With,
TableType, Tables, Thing, Timeout, Uuid, Value, Values, Version, With,
},
syn::v2::parser::mac::test_parse,
};
@ -327,6 +327,7 @@ fn parse_define_table() {
}),
comment: None,
if_not_exists: false,
kind: TableType::Any,
}))
);
}

View file

@ -21,7 +21,7 @@ use crate::{
Expression, Fetch, Fetchs, Field, Fields, Future, Graph, Group, Groups, Id, Ident, Idiom,
Idioms, Index, Kind, Limit, Number, Object, Operator, Order, Orders, Output, Param, Part,
Permission, Permissions, Scoring, Split, Splits, Start, Statement, Strand, Subquery, Table,
Tables, Thing, Timeout, Uuid, Value, Values, Version, With,
TableType, Tables, Thing, Timeout, Uuid, Value, Values, Version, With,
},
syn::v2::parser::{Parser, PartialResult},
};
@ -247,6 +247,7 @@ fn statements() -> Vec<Statement> {
}),
comment: None,
if_not_exists: false,
kind: TableType::Any,
})),
Statement::Define(DefineStatement::Event(DefineEventStatement {
name: Ident("event".to_owned()),

View file

@ -123,6 +123,7 @@ keyword! {
Punct => "PUNCT",
Readonly => "READONLY",
Relate => "RELATE",
Relation => "RELATION",
Remove => "REMOVE",
Replace => "REPLACE",
Return => "RETURN",
@ -151,6 +152,7 @@ keyword! {
Timeout => "TIMEOUT",
Tokenizers => "TOKENIZERS",
Token => "TOKEN",
To => "TO",
Transaction => "TRANSACTION",
True => "true",
Type => "TYPE",
@ -185,6 +187,8 @@ keyword! {
ContainsNot => "CONTAINSNOT",
Contains => "CONTAINS",
In => "IN",
Out => "OUT",
Normal => "NORMAL",
Any => "ANY",
Array => "ARRAY",

View file

@ -122,7 +122,7 @@ async fn define_statement_table_drop() -> Result<(), Error> {
models: {},
params: {},
scopes: {},
tables: { test: 'DEFINE TABLE test DROP SCHEMALESS PERMISSIONS NONE' },
tables: { test: 'DEFINE TABLE test TYPE ANY DROP SCHEMALESS PERMISSIONS NONE' },
users: {},
}",
);
@ -154,7 +154,7 @@ async fn define_statement_table_schemaless() -> Result<(), Error> {
models: {},
params: {},
scopes: {},
tables: { test: 'DEFINE TABLE test SCHEMALESS PERMISSIONS NONE' },
tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMALESS PERMISSIONS NONE' },
users: {},
}",
);
@ -190,7 +190,7 @@ async fn define_statement_table_schemafull() -> Result<(), Error> {
models: {},
params: {},
scopes: {},
tables: { test: 'DEFINE TABLE test SCHEMAFULL PERMISSIONS NONE' },
tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE' },
users: {},
}",
);
@ -222,7 +222,7 @@ async fn define_statement_table_schemaful() -> Result<(), Error> {
models: {},
params: {},
scopes: {},
tables: { test: 'DEFINE TABLE test SCHEMAFULL PERMISSIONS NONE' },
tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE' },
users: {},
}",
);
@ -263,8 +263,8 @@ async fn define_statement_table_foreigntable() -> Result<(), Error> {
params: {},
scopes: {},
tables: {
test: 'DEFINE TABLE test SCHEMAFULL PERMISSIONS NONE',
view: 'DEFINE TABLE view SCHEMALESS AS SELECT count() FROM test GROUP ALL PERMISSIONS NONE',
test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE',
view: 'DEFINE TABLE view TYPE ANY SCHEMALESS AS SELECT count() FROM test GROUP ALL PERMISSIONS NONE',
},
users: {},
}",
@ -276,7 +276,7 @@ async fn define_statement_table_foreigntable() -> Result<(), Error> {
"{
events: {},
fields: {},
tables: { view: 'DEFINE TABLE view SCHEMALESS AS SELECT count() FROM test GROUP ALL PERMISSIONS NONE' },
tables: { view: 'DEFINE TABLE view TYPE ANY SCHEMALESS AS SELECT count() FROM test GROUP ALL PERMISSIONS NONE' },
indexes: {},
lives: {},
}",
@ -296,7 +296,7 @@ async fn define_statement_table_foreigntable() -> Result<(), Error> {
params: {},
scopes: {},
tables: {
test: 'DEFINE TABLE test SCHEMAFULL PERMISSIONS NONE',
test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE',
},
users: {},
}",
@ -1948,7 +1948,7 @@ async fn permissions_checks_define_table() {
// Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { TB: 'DEFINE TABLE TB SCHEMALESS PERMISSIONS NONE' }, tokens: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { TB: 'DEFINE TABLE TB TYPE ANY SCHEMALESS PERMISSIONS NONE' }, tokens: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"]
];
@ -2139,9 +2139,9 @@ async fn define_statement_table_permissions() -> Result<(), Error> {
params: {},
scopes: {},
tables: {
default: 'DEFINE TABLE default SCHEMALESS PERMISSIONS NONE',
full: 'DEFINE TABLE full SCHEMALESS PERMISSIONS FULL',
select_full: 'DEFINE TABLE select_full SCHEMALESS PERMISSIONS FOR select FULL, FOR create, update, delete NONE'
default: 'DEFINE TABLE default TYPE ANY SCHEMALESS PERMISSIONS NONE',
full: 'DEFINE TABLE full TYPE ANY SCHEMALESS PERMISSIONS FULL',
select_full: 'DEFINE TABLE select_full TYPE ANY SCHEMALESS PERMISSIONS FOR select FULL, FOR create, update, delete NONE'
},
tokens: {},
users: {}
@ -2643,3 +2643,32 @@ async fn redefining_existing_user_with_if_not_exists_should_error() -> Result<()
//
Ok(())
}
#[tokio::test]
#[cfg(feature = "sql2")]
async fn define_table_relation() -> Result<(), Error> {
let sql = "
DEFINE TABLE likes RELATION;
CREATE person:raphael, person:tobie;
RELATE person:raphael->likes->person:tobie;
CREATE likes:1;
";
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(), 4);
//
let tmp = res.remove(0).result;
assert!(tmp.is_ok());
//
let tmp = res.remove(0).result;
assert!(tmp.is_ok());
//
let tmp = res.remove(0).result;
assert!(tmp.is_ok());
//
let tmp = res.remove(0).result;
assert!(tmp.is_err());
//
Ok(())
}

View file

@ -789,7 +789,7 @@ async fn field_definition_edge_permissions() -> Result<(), Error> {
DEFINE TABLE user SCHEMAFULL;
DEFINE TABLE business SCHEMAFULL;
DEFINE FIELD owner ON TABLE business TYPE record<user>;
DEFINE TABLE contact SCHEMAFULL PERMISSIONS FOR create WHERE in.owner.id = $auth.id;
DEFINE TABLE contact RELATION SCHEMAFULL PERMISSIONS FOR create WHERE in.owner.id = $auth.id;
INSERT INTO user (id, name) VALUES (user:one, 'John'), (user:two, 'Lucy');
INSERT INTO business (id, owner) VALUES (business:one, user:one), (business:two, user:two);
";

View file

@ -1015,7 +1015,7 @@ async fn permissions_checks_remove_table() {
// Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { TB: 'DEFINE TABLE TB SCHEMALESS PERMISSIONS NONE' }, tokens: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { TB: 'DEFINE TABLE TB TYPE ANY SCHEMALESS PERMISSIONS NONE' }, tokens: { }, users: { } }"],
];
let test_cases = [

View file

@ -258,7 +258,7 @@ async fn loose_mode_all_ok() -> Result<(), Error> {
models: {},
params: {},
scopes: {},
tables: { test: 'DEFINE TABLE test SCHEMALESS PERMISSIONS NONE' },
tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMALESS PERMISSIONS NONE' },
users: {},
}",
);

View file

@ -43,7 +43,7 @@ async fn define_foreign_table() -> Result<(), Error> {
"{
events: {},
fields: {},
tables: { person_by_age: 'DEFINE TABLE person_by_age SCHEMALESS AS SELECT count(), age, math::sum(age) AS total, math::mean(score) AS average FROM person GROUP BY age PERMISSIONS NONE' },
tables: { person_by_age: 'DEFINE TABLE person_by_age TYPE ANY SCHEMALESS AS SELECT count(), age, math::sum(age) AS total, math::mean(score) AS average FROM person GROUP BY age PERMISSIONS NONE' },
indexes: {},
lives: {},
}",

View file

@ -99,7 +99,7 @@ mod cli_integration {
{
let args = format!("export --conn http://{addr} {creds} --ns {ns} --db {db} -");
let output = common::run(&args).output().expect("failed to run stdout export: {args}");
assert!(output.contains("DEFINE TABLE thing SCHEMALESS PERMISSIONS NONE;"));
assert!(output.contains("DEFINE TABLE thing TYPE ANY SCHEMALESS PERMISSIONS NONE;"));
assert!(output.contains("UPDATE thing:one CONTENT { id: thing:one };"));
}
@ -632,8 +632,10 @@ mod cli_integration {
let args = format!(
"sql --conn http://{addr} {creds} --ns {ns} --db {db} --multi --hide-welcome"
);
let output =
common::run(&args).input("DEFINE TABLE thing CHANGEFEED 1s;\n").output().unwrap();
let output = common::run(&args)
.input("DEFINE TABLE thing TYPE ANY CHANGEFEED 1s;\n")
.output()
.unwrap();
let output = remove_debug_info(output);
assert_eq!(output, "[NONE]\n\n".to_owned(), "failed to send sql: {args}");
}