From 7c41aef3ba03a8edd5ea44c94eba053fd98139a1 Mon Sep 17 00:00:00 2001 From: Micha de Vries Date: Thu, 22 Aug 2024 16:25:20 +0100 Subject: [PATCH] Enforced relations (#4579) --- core/src/doc/edges.rs | 28 +++++++++++++++++++++++++++- core/src/doc/relation.rs | 10 +++++----- core/src/err/mod.rs | 3 +-- core/src/sql/table_type.rs | 4 +++- core/src/syn/lexer/keywords.rs | 1 + core/src/syn/parser/stmt/define.rs | 4 ++++ core/src/syn/token/keyword.rs | 1 + sdk/tests/relate.rs | 22 ++++++++++++++++++++++ 8 files changed, 64 insertions(+), 9 deletions(-) diff --git a/core/src/doc/edges.rs b/core/src/doc/edges.rs index 57159d33..73e49abe 100644 --- a/core/src/doc/edges.rs +++ b/core/src/doc/edges.rs @@ -9,6 +9,8 @@ use crate::sql::paths::IN; use crate::sql::paths::OUT; use crate::sql::value::Value; use crate::sql::Dir; +use crate::sql::Relation; +use crate::sql::TableType; impl Document { pub async fn edges( @@ -17,8 +19,10 @@ impl Document { opt: &Options, _stm: &Statement<'_>, ) -> Result<(), Error> { + // Get the table + let tb = self.tb(ctx, opt).await?; // Check if the table is a view - if self.tb(ctx, opt).await?.drop { + if tb.drop { return Ok(()); } // Get the transaction @@ -29,6 +33,28 @@ impl Document { let rid = self.id.as_ref().unwrap(); // Store the record edges if let Workable::Relate(l, r, _) = &self.extras { + // For enforced relations, ensure that the edges exist + if matches!( + tb.kind, + TableType::Relation(Relation { + enforced: true, + .. + }) + ) { + let key = crate::key::thing::new(opt.ns()?, opt.db()?, &l.tb, &l.id); + if !txn.exists(key).await? { + return Err(Error::IdNotFound { + value: l.to_string(), + }); + } + + let key = crate::key::thing::new(opt.ns()?, opt.db()?, &r.tb, &r.id); + if !txn.exists(key).await? { + return Err(Error::IdNotFound { + value: r.to_string(), + }); + } + } // Get temporary edge references let (ref o, ref i) = (Dir::Out, Dir::In); // Store the left pointer edge diff --git a/core/src/doc/relation.rs b/core/src/doc/relation.rs index 72eaf4fe..20fd72ab 100644 --- a/core/src/doc/relation.rs +++ b/core/src/doc/relation.rs @@ -20,7 +20,7 @@ impl Document { return Err(Error::TableCheck { thing: rid.to_string(), relation: false, - target_type: tb.kind.clone(), + target_type: tb.kind.to_string(), }); } } @@ -29,7 +29,7 @@ impl Document { return Err(Error::TableCheck { thing: rid.to_string(), relation: false, - target_type: tb.kind.clone(), + target_type: tb.kind.to_string(), }); } } @@ -38,7 +38,7 @@ impl Document { return Err(Error::TableCheck { thing: rid.to_string(), relation: true, - target_type: tb.kind.clone(), + target_type: tb.kind.to_string(), }); } } @@ -48,7 +48,7 @@ impl Document { return Err(Error::TableCheck { thing: rid.to_string(), relation: true, - target_type: tb.kind.clone(), + target_type: tb.kind.to_string(), }); } } @@ -57,7 +57,7 @@ impl Document { return Err(Error::TableCheck { thing: rid.to_string(), relation: false, - target_type: tb.kind.clone(), + target_type: tb.kind.to_string(), }); } } diff --git a/core/src/err/mod.rs b/core/src/err/mod.rs index dfa2a20f..5d28fdf5 100644 --- a/core/src/err/mod.rs +++ b/core/src/err/mod.rs @@ -5,7 +5,6 @@ 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::DecodeError as Base64Error; @@ -575,7 +574,7 @@ pub enum Error { TableCheck { thing: String, relation: bool, - target_type: TableType, + target_type: String, }, /// The specified field did not conform to the field type check diff --git a/core/src/sql/table_type.rs b/core/src/sql/table_type.rs index 53a3721e..2901578d 100644 --- a/core/src/sql/table_type.rs +++ b/core/src/sql/table_type.rs @@ -60,11 +60,13 @@ impl InfoStructure for TableType { } } -#[revisioned(revision = 1)] +#[revisioned(revision = 2)] #[derive(Debug, Default, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] pub struct Relation { pub from: Option, pub to: Option, + #[revision(start = 2)] + pub enforced: bool, } diff --git a/core/src/syn/lexer/keywords.rs b/core/src/syn/lexer/keywords.rs index 82e3341d..2ee5768e 100644 --- a/core/src/syn/lexer/keywords.rs +++ b/core/src/syn/lexer/keywords.rs @@ -111,6 +111,7 @@ pub(crate) static KEYWORDS: phf::Map, TokenKind> = phf_map UniCase::ascii("EVENT") => TokenKind::Keyword(Keyword::Event), UniCase::ascii("ELSE") => TokenKind::Keyword(Keyword::Else), UniCase::ascii("END") => TokenKind::Keyword(Keyword::End), + UniCase::ascii("ENFORCED") => TokenKind::Keyword(Keyword::Enforced), UniCase::ascii("EXISTS") => TokenKind::Keyword(Keyword::Exists), UniCase::ascii("EXPLAIN") => TokenKind::Keyword(Keyword::Explain), UniCase::ascii("EXTEND_CANDIDATES") => TokenKind::Keyword(Keyword::ExtendCandidates), diff --git a/core/src/syn/parser/stmt/define.rs b/core/src/syn/parser/stmt/define.rs index 12aae05f..1b0effd6 100644 --- a/core/src/syn/parser/stmt/define.rs +++ b/core/src/syn/parser/stmt/define.rs @@ -1208,6 +1208,7 @@ impl Parser<'_> { let mut res = table_type::Relation { from: None, to: None, + enforced: false, }; loop { match self.peek_kind() { @@ -1224,6 +1225,9 @@ impl Parser<'_> { _ => break, } } + if self.eat(t!("ENFORCED")) { + res.enforced = true; + } Ok(res) } diff --git a/core/src/syn/token/keyword.rs b/core/src/syn/token/keyword.rs index c8e00ff2..f8f484ff 100644 --- a/core/src/syn/token/keyword.rs +++ b/core/src/syn/token/keyword.rs @@ -75,6 +75,7 @@ keyword! { Event => "EVENT", Else => "ELSE", End => "END", + Enforced => "ENFORCED", Exists => "EXISTS", Explain => "EXPLAIN", ExtendCandidates => "EXTEND_CANDIDATES", diff --git a/sdk/tests/relate.rs b/sdk/tests/relate.rs index f5adeb6d..67d3bf17 100644 --- a/sdk/tests/relate.rs +++ b/sdk/tests/relate.rs @@ -257,3 +257,25 @@ async fn schemafull_relate() -> Result<(), Error> { Ok(()) } + +#[tokio::test] +async fn relate_enforced() -> Result<(), Error> { + let sql = " + DEFINE TABLE edge TYPE RELATION ENFORCED; + RELATE a:1->edge:1->a:2; + CREATE a:1, a:2; + RELATE a:1->edge:1->a:2; + "; + + let mut t = Test::new(sql).await?; + // + t.skip_ok(1)?; + // + t.expect_error_func(|e| matches!(e, Error::IdNotFound { .. }))?; + // + t.expect_val("[{ id: a:1 }, { id: a:2 }]")?; + // + t.expect_val("[{ id: edge:1, in: a:1, out: a:2 }]")?; + // + Ok(()) +}