diff --git a/dependencies/vaticle/repositories.bzl b/dependencies/vaticle/repositories.bzl index 744ec502..24e23b08 100644 --- a/dependencies/vaticle/repositories.bzl +++ b/dependencies/vaticle/repositories.bzl @@ -46,7 +46,7 @@ def vaticle_typedb_behaviour(): git_repository( name = "vaticle_typedb_behaviour", remote = "https://github.com/vaticle/typedb-behaviour", - commit = "d4947828f570853b71f0eb8aefd34af7b5a485fb", + commit = "d4947828f570853b71f0eb8aefd34af7b5a485fb", # sync-marker: do not remove this comment, this is used for sync-dependencies by @vaticle_typedb_behaviour ) def vaticle_typeql(): diff --git a/src/answer/concept_map.rs b/src/answer/concept_map.rs index 8d665ba7..5545b930 100644 --- a/src/answer/concept_map.rs +++ b/src/answer/concept_map.rs @@ -26,9 +26,10 @@ use std::{ use crate::concept::Concept; -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct ConceptMap { pub map: HashMap, + pub explainables: Explainables, } impl ConceptMap { @@ -39,20 +40,6 @@ impl ConceptMap { pub fn concepts(&self) -> impl Iterator { self.map.values() } - - pub fn concepts_to_vec(&self) -> Vec<&Concept> { - self.concepts().collect::>() - } -} - -impl Clone for ConceptMap { - fn clone(&self) -> Self { - let mut map = HashMap::with_capacity(self.map.len()); - for (k, v) in &self.map { - map.insert(k.clone(), v.clone()); - } - Self { map } - } } impl From for HashMap { @@ -77,3 +64,28 @@ impl IntoIterator for ConceptMap { self.map.into_iter() } } + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Explainables { + pub relations: HashMap, + pub attributes: HashMap, + pub ownerships: HashMap<(String, String), Explainable>, +} + +impl Explainables { + pub fn is_empty(&self) -> bool { + self.relations.is_empty() && self.attributes.is_empty() && self.ownerships.is_empty() + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Explainable { + pub conjunction: String, + pub id: i64, +} + +impl Explainable { + pub(crate) fn new(conjunction: String, id: i64) -> Self { + Self { conjunction, id } + } +} diff --git a/src/answer/mod.rs b/src/answer/mod.rs index 0060eab3..ad9c9a1c 100644 --- a/src/answer/mod.rs +++ b/src/answer/mod.rs @@ -19,11 +19,14 @@ * under the License. */ -mod concept_map; +pub mod concept_map; mod concept_map_group; mod numeric; mod numeric_group; pub use self::{ - concept_map::ConceptMap, concept_map_group::ConceptMapGroup, numeric::Numeric, numeric_group::NumericGroup, + concept_map::{ConceptMap, Explainable, Explainables}, + concept_map_group::ConceptMapGroup, + numeric::Numeric, + numeric_group::NumericGroup, }; diff --git a/src/common/error.rs b/src/common/error.rs index 813f7f57..3bd26862 100644 --- a/src/common/error.rs +++ b/src/common/error.rs @@ -44,6 +44,8 @@ error_messages! { ConnectionError 9: "Missing field in message received from server: '{}'.", UnknownRequestId(RequestID) = 10: "Received a response with unknown request id '{}'", + InvalidResponseField(&'static str) = + 11: "Invalid field in message received from server: '{}'.", ClusterUnableToConnect(String) = 12: "Unable to connect to TypeDB Cluster. Attempted connecting to the cluster members, but none are available: '{}'.", ClusterReplicaNotPrimary() = diff --git a/src/concept/mod.rs b/src/concept/mod.rs index 6e6931b8..f1f5b97e 100644 --- a/src/concept/mod.rs +++ b/src/concept/mod.rs @@ -29,7 +29,7 @@ pub use self::{ }, }; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum Concept { RootThingType(RootThingType), diff --git a/src/concept/thing.rs b/src/concept/thing.rs index 2bfadd4d..60df8349 100644 --- a/src/concept/thing.rs +++ b/src/concept/thing.rs @@ -47,12 +47,14 @@ impl Thing { pub struct Entity { pub iid: IID, pub type_: EntityType, + pub is_inferred: bool, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Relation { pub iid: IID, pub type_: RelationType, + pub is_inferred: bool, } #[derive(Clone, Debug, PartialEq)] @@ -60,6 +62,7 @@ pub struct Attribute { pub iid: IID, pub type_: AttributeType, pub value: Value, + pub is_inferred: bool, } #[derive(Clone, Debug, PartialEq)] diff --git a/src/connection/message.rs b/src/connection/message.rs index 631d2064..30da85d7 100644 --- a/src/connection/message.rs +++ b/src/connection/message.rs @@ -24,6 +24,7 @@ use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; use tonic::Streaming; use typedb_protocol::transaction; +use typeql_lang::pattern::{Conjunction, Variable}; use crate::{ answer::{ConceptMap, ConceptMapGroup, Numeric, NumericGroup}, @@ -32,6 +33,7 @@ use crate::{ Annotation, Attribute, AttributeType, Entity, EntityType, Relation, RelationType, RoleType, SchemaException, Thing, ThingType, Transitivity, Value, ValueType, }, + logic::{Explanation, Rule}, Options, SessionType, TransactionType, }; @@ -107,6 +109,8 @@ pub(super) enum TransactionRequest { ThingType(ThingTypeRequest), RoleType(RoleTypeRequest), Thing(ThingRequest), + Rule(RuleRequest), + Logic(LogicRequest), Stream { request_id: RequestID }, } @@ -120,6 +124,8 @@ pub(super) enum TransactionResponse { ThingType(ThingTypeResponse), RoleType(RoleTypeResponse), Thing(ThingResponse), + Rule(RuleResponse), + Logic(LogicResponse), } #[derive(Debug)] @@ -152,7 +158,7 @@ pub(super) enum QueryResponse { MatchAggregate { answer: Numeric }, - Explain {}, // TODO: explanations + Explain { answers: Vec }, MatchGroup { answers: Vec }, MatchGroupAggregate { answers: Vec }, @@ -457,3 +463,29 @@ pub(super) enum ThingResponse { AttributeGetOwners { owners: Vec }, } + +#[derive(Debug)] +pub(super) enum RuleRequest { + Delete { label: String }, + SetLabel { current_label: String, new_label: String }, +} + +#[derive(Debug)] +pub(super) enum RuleResponse { + Delete, + SetLabel, +} + +#[derive(Debug)] +pub(super) enum LogicRequest { + PutRule { label: String, when: Conjunction, then: Variable }, + GetRule { label: String }, + GetRules, +} + +#[derive(Debug)] +pub(super) enum LogicResponse { + PutRule { rule: Rule }, + GetRule { rule: Rule }, + GetRules { rules: Vec }, +} diff --git a/src/connection/network/proto/concept.rs b/src/connection/network/proto/concept.rs index 104e74ae..a5f16420 100644 --- a/src/connection/network/proto/concept.rs +++ b/src/connection/network/proto/concept.rs @@ -31,19 +31,21 @@ use typedb_protocol::{ r#type::{annotation, Annotation as AnnotationProto, Transitivity as TransitivityProto}, thing, thing_type, Attribute as AttributeProto, AttributeType as AttributeTypeProto, Concept as ConceptProto, ConceptMap as ConceptMapProto, ConceptMapGroup as ConceptMapGroupProto, Entity as EntityProto, - EntityType as EntityTypeProto, Numeric as NumericProto, NumericGroup as NumericGroupProto, + EntityType as EntityTypeProto, Explainable as ExplainableProto, Explainables as ExplainablesProto, + Explanation as ExplanationProto, Numeric as NumericProto, NumericGroup as NumericGroupProto, Relation as RelationProto, RelationType as RelationTypeProto, RoleType as RoleTypeProto, Thing as ThingProto, ThingType as ThingTypeProto, }; use super::{FromProto, IntoProto, TryFromProto}; use crate::{ - answer::{ConceptMap, ConceptMapGroup, Numeric, NumericGroup}, + answer::{ConceptMap, ConceptMapGroup, Explainable, Explainables, Numeric, NumericGroup}, concept::{ Annotation, Attribute, AttributeType, Concept, Entity, EntityType, Relation, RelationType, RoleType, RootThingType, ScopedLabel, Thing, ThingType, Transitivity, Value, ValueType, }, error::{ConnectionError, InternalError}, + logic::{Explanation, Rule}, Result, }; @@ -97,11 +99,11 @@ impl TryFromProto for ConceptMapGroup { impl TryFromProto for ConceptMap { fn try_from_proto(proto: ConceptMapProto) -> Result { - let mut map = HashMap::with_capacity(proto.map.len()); - for (k, v) in proto.map { - map.insert(k, Concept::try_from_proto(v)?); - } - Ok(Self { map }) + let ConceptMapProto { map: map_proto, explainables: explainables_proto } = proto; + let map = map_proto.into_iter().map(|(k, v)| Concept::try_from_proto(v).map(|v| (k, v))).try_collect()?; + let explainables = + explainables_proto.ok_or::(ConnectionError::MissingResponseField("explainables"))?; + Ok(Self { map, explainables: Explainables::from_proto(explainables) }) } } @@ -277,66 +279,64 @@ impl IntoProto for Thing { impl TryFromProto for Entity { fn try_from_proto(proto: EntityProto) -> Result { - let EntityProto { iid, entity_type, inferred: _ } = proto; + let EntityProto { iid, entity_type, inferred } = proto; Ok(Self { iid: iid.into(), type_: EntityType::from_proto(entity_type.ok_or(ConnectionError::MissingResponseField("entity_type"))?), + is_inferred: inferred, }) } } impl IntoProto for Entity { fn into_proto(self) -> EntityProto { - EntityProto { - iid: self.iid.into(), - entity_type: Some(self.type_.into_proto()), - inferred: false, // FIXME - } + let Self { iid, type_, is_inferred } = self; + EntityProto { iid: iid.into(), entity_type: Some(type_.into_proto()), inferred: is_inferred } } } impl TryFromProto for Relation { fn try_from_proto(proto: RelationProto) -> Result { - let RelationProto { iid, relation_type, inferred: _ } = proto; + let RelationProto { iid, relation_type, inferred } = proto; Ok(Self { iid: iid.into(), type_: RelationType::from_proto( relation_type.ok_or(ConnectionError::MissingResponseField("relation_type"))?, ), + is_inferred: inferred, }) } } impl IntoProto for Relation { fn into_proto(self) -> RelationProto { - RelationProto { - iid: self.iid.into(), - relation_type: Some(self.type_.into_proto()), - inferred: false, // FIXME - } + let Self { iid, type_, is_inferred } = self; + RelationProto { iid: iid.into(), relation_type: Some(type_.into_proto()), inferred: is_inferred } } } impl TryFromProto for Attribute { fn try_from_proto(proto: AttributeProto) -> Result { - let AttributeProto { iid, attribute_type, value, inferred: _ } = proto; + let AttributeProto { iid, attribute_type, value, inferred } = proto; Ok(Self { iid: iid.into(), type_: AttributeType::try_from_proto( attribute_type.ok_or(ConnectionError::MissingResponseField("attribute_type"))?, )?, value: Value::try_from_proto(value.ok_or(ConnectionError::MissingResponseField("value"))?)?, + is_inferred: inferred, }) } } impl IntoProto for Attribute { fn into_proto(self) -> AttributeProto { + let Self { iid, type_, value, is_inferred } = self; AttributeProto { - iid: self.iid.into(), - attribute_type: Some(self.type_.into_proto()), - value: Some(self.value.into_proto()), - inferred: false, // FIXME + iid: iid.into(), + attribute_type: Some(type_.into_proto()), + value: Some(value.into_proto()), + inferred: is_inferred, } } } @@ -369,3 +369,47 @@ impl IntoProto for Value { } } } + +impl FromProto for Explainables { + fn from_proto(proto: ExplainablesProto) -> Self { + let ExplainablesProto { + relations: relations_proto, + attributes: attributes_proto, + ownerships: ownerships_proto, + } = proto; + let relations = relations_proto.into_iter().map(|(k, v)| (k, Explainable::from_proto(v))).collect(); + let attributes = attributes_proto.into_iter().map(|(k, v)| (k, Explainable::from_proto(v))).collect(); + let mut ownerships = HashMap::new(); + for (k1, owned) in ownerships_proto { + for (k2, v) in owned.owned { + ownerships.insert((k1.clone(), k2), Explainable::from_proto(v)); + } + } + + Self { relations, attributes, ownerships } + } +} + +impl FromProto for Explainable { + fn from_proto(proto: ExplainableProto) -> Self { + let ExplainableProto { conjunction, id } = proto; + Self::new(conjunction, id) + } +} + +impl TryFromProto for Explanation { + fn try_from_proto(proto: ExplanationProto) -> Result { + let ExplanationProto { rule, conclusion, condition, var_mapping } = proto; + let variable_mapping = var_mapping.iter().map(|(k, v)| (k.clone(), v.clone().vars)).collect(); + Ok(Self { + rule: Rule::try_from_proto(rule.ok_or(ConnectionError::MissingResponseField("rule"))?)?, + conclusion: ConceptMap::try_from_proto( + conclusion.ok_or(ConnectionError::MissingResponseField("conclusion"))?, + )?, + condition: ConceptMap::try_from_proto( + condition.ok_or(ConnectionError::MissingResponseField("condition"))?, + )?, + variable_mapping, + }) + } +} diff --git a/src/connection/network/proto/logic.rs b/src/connection/network/proto/logic.rs new file mode 100644 index 00000000..1bce3bb8 --- /dev/null +++ b/src/connection/network/proto/logic.rs @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use typedb_protocol::Rule as RuleProto; +use typeql_lang::{ + parse_pattern, parse_variable, + pattern::{Pattern, Variable}, +}; + +use super::{IntoProto, TryFromProto}; +use crate::{common::Result, error::ConnectionError, logic::Rule}; + +impl TryFromProto for Rule { + fn try_from_proto(proto: RuleProto) -> Result { + let RuleProto { label: label_proto, when: when_proto, then: then_proto } = proto; + let when = match parse_pattern(&when_proto)? { + Pattern::Conjunction(conjunction) => conjunction, + _ => return Err(ConnectionError::InvalidResponseField("when").into()), + }; + let then = match parse_variable(&then_proto) { + Ok(Variable::Thing(thing)) => thing, + Ok(_) => return Err(ConnectionError::InvalidResponseField("then").into()), + Err(error) => return Err(error.into()), + }; + Ok(Self::new(label_proto, when, then)) + } +} + +impl IntoProto for Rule { + fn into_proto(self) -> RuleProto { + let Self { label, when, then } = self; + RuleProto { label, when: when.to_string(), then: then.to_string() } + } +} diff --git a/src/connection/network/proto/message.rs b/src/connection/network/proto/message.rs index e758c812..493b94ff 100644 --- a/src/connection/network/proto/message.rs +++ b/src/connection/network/proto/message.rs @@ -23,8 +23,8 @@ use std::time::Duration; use itertools::Itertools; use typedb_protocol::{ - attribute, attribute_type, concept_manager, database, database_manager, entity_type, query_manager, r#type, - relation, relation_type, role_type, server_manager, session, thing, thing_type, transaction, + attribute, attribute_type, concept_manager, database, database_manager, entity_type, logic_manager, query_manager, + r#type, relation, relation_type, role_type, rule, server_manager, session, thing, thing_type, transaction, }; use super::{FromProto, IntoProto, TryFromProto, TryIntoProto}; @@ -36,11 +36,12 @@ use crate::{ Thing, ThingType, ValueType, }, connection::message::{ - ConceptRequest, ConceptResponse, QueryRequest, QueryResponse, Request, Response, RoleTypeRequest, - RoleTypeResponse, ThingRequest, ThingResponse, ThingTypeRequest, ThingTypeResponse, TransactionRequest, - TransactionResponse, + ConceptRequest, ConceptResponse, LogicRequest, LogicResponse, QueryRequest, QueryResponse, Request, Response, + RoleTypeRequest, RoleTypeResponse, RuleRequest, RuleResponse, ThingRequest, ThingResponse, ThingTypeRequest, + ThingTypeResponse, TransactionRequest, TransactionResponse, }, error::{ConnectionError, InternalError}, + logic::{Explanation, Rule}, }; impl TryIntoProto for Request { @@ -266,6 +267,8 @@ impl IntoProto for TransactionRequest { Self::ThingType(thing_type_request) => transaction::req::Req::TypeReq(thing_type_request.into_proto()), Self::RoleType(role_type_request) => transaction::req::Req::TypeReq(role_type_request.into_proto()), Self::Thing(thing_request) => transaction::req::Req::ThingReq(thing_request.into_proto()), + Self::Rule(rule_request) => transaction::req::Req::RuleReq(rule_request.into_proto()), + Self::Logic(logic_request) => transaction::req::Req::LogicManagerReq(logic_request.into_proto()), Self::Stream { request_id: req_id } => { request_id = Some(req_id); transaction::req::Req::StreamReq(transaction::stream::Req {}) @@ -296,8 +299,12 @@ impl TryFromProto for TransactionResponse { Some(transaction::res::Res::TypeRes(r#type::Res { res: Some(r#type::res::Res::RoleTypeRes(res)) })) => { Ok(Self::RoleType(RoleTypeResponse::try_from_proto(res)?)) } + Some(transaction::res::Res::TypeRes(r#type::Res { res: None })) => { + Err(ConnectionError::MissingResponseField("res").into()) + } Some(transaction::res::Res::ThingRes(res)) => Ok(Self::Thing(ThingResponse::try_from_proto(res)?)), - Some(_) => todo!(), + Some(transaction::res::Res::RuleRes(res)) => Ok(Self::Rule(RuleResponse::try_from_proto(res)?)), + Some(transaction::res::Res::LogicManagerRes(res)) => Ok(Self::Logic(LogicResponse::try_from_proto(res)?)), None => Err(ConnectionError::MissingResponseField("res").into()), } } @@ -315,8 +322,14 @@ impl TryFromProto for TransactionResponse { Some(transaction::res_part::Res::TypeResPart(r#type::ResPart { res: Some(r#type::res_part::Res::RoleTypeResPart(res)), })) => Ok(Self::RoleType(RoleTypeResponse::try_from_proto(res)?)), + Some(transaction::res_part::Res::TypeResPart(r#type::ResPart { res: None })) => { + Err(ConnectionError::MissingResponseField("res").into()) + } Some(transaction::res_part::Res::ThingResPart(res)) => Ok(Self::Thing(ThingResponse::try_from_proto(res)?)), - Some(_) => todo!(), + Some(transaction::res_part::Res::LogicManagerResPart(res)) => { + Ok(Self::Logic(LogicResponse::try_from_proto(res)?)) + } + Some(transaction::res_part::Res::StreamResPart(_)) => unreachable!(), None => Err(ConnectionError::MissingResponseField("res").into()), } } @@ -357,7 +370,9 @@ impl IntoProto for QueryRequest { options, ), - _ => todo!(), + Self::Explain { explainable_id, options } => { + (query_manager::req::Req::ExplainReq(query_manager::explain::Req { explainable_id }), options) + } }; query_manager::Req { req: Some(req), options: Some(options.into_proto()) } } @@ -395,7 +410,9 @@ impl TryFromProto for QueryResponse { Some(query_manager::res_part::Res::MatchGroupAggregateResPart(res)) => Ok(Self::MatchGroupAggregate { answers: res.answers.into_iter().map(NumericGroup::try_from_proto).try_collect()?, }), - Some(_) => todo!(), + Some(query_manager::res_part::Res::ExplainResPart(res)) => Ok(Self::Explain { + answers: res.explanations.into_iter().map(Explanation::try_from_proto).try_collect()?, + }), None => Err(ConnectionError::MissingResponseField("res").into()), } } @@ -1104,3 +1121,62 @@ impl TryFromProto for ThingResponse { } } } + +impl IntoProto for RuleRequest { + fn into_proto(self) -> rule::Req { + let (req, label) = match self { + Self::Delete { label } => (rule::req::Req::RuleDeleteReq(rule::delete::Req {}), label), + Self::SetLabel { current_label, new_label } => { + (rule::req::Req::RuleSetLabelReq(rule::set_label::Req { label: new_label }), current_label) + } + }; + rule::Req { label, req: Some(req) } + } +} + +impl TryFromProto for RuleResponse { + fn try_from_proto(proto: rule::Res) -> Result { + match proto.res { + Some(rule::res::Res::RuleDeleteRes(_)) => Ok(Self::Delete), + Some(rule::res::Res::RuleSetLabelRes(_)) => Ok(Self::SetLabel), + None => Err(ConnectionError::MissingResponseField("res").into()), + } + } +} + +impl IntoProto for LogicRequest { + fn into_proto(self) -> logic_manager::Req { + let req = match self { + Self::PutRule { label, when, then } => logic_manager::req::Req::PutRuleReq(logic_manager::put_rule::Req { + label, + when: when.to_string(), + then: then.to_string(), + }), + Self::GetRule { label } => logic_manager::req::Req::GetRuleReq(logic_manager::get_rule::Req { label }), + Self::GetRules => logic_manager::req::Req::GetRulesReq(logic_manager::get_rules::Req {}), + }; + logic_manager::Req { req: Some(req) } + } +} + +impl TryFromProto for LogicResponse { + fn try_from_proto(proto: logic_manager::Res) -> Result { + match proto.res { + Some(logic_manager::res::Res::PutRuleRes(logic_manager::put_rule::Res { rule })) => { + Ok(Self::PutRule { rule: Rule::try_from_proto(rule.unwrap()).unwrap() }) + } + Some(logic_manager::res::Res::GetRuleRes(logic_manager::get_rule::Res { rule })) => { + Ok(Self::GetRule { rule: Rule::try_from_proto(rule.unwrap()).unwrap() }) + } + None => Err(ConnectionError::MissingResponseField("res").into()), + } + } +} + +impl TryFromProto for LogicResponse { + fn try_from_proto(proto: logic_manager::ResPart) -> Result { + Ok(Self::GetRules { + rules: proto.get_rules_res_part.unwrap().rules.into_iter().map(Rule::try_from_proto).try_collect()?, + }) + } +} diff --git a/src/connection/network/proto/mod.rs b/src/connection/network/proto/mod.rs index fc805bb2..be389929 100644 --- a/src/connection/network/proto/mod.rs +++ b/src/connection/network/proto/mod.rs @@ -22,6 +22,7 @@ mod common; mod concept; mod database; +mod logic; mod message; use crate::Result; diff --git a/src/connection/transaction_stream.rs b/src/connection/transaction_stream.rs index 3d4d2d12..4fedc498 100644 --- a/src/connection/transaction_stream.rs +++ b/src/connection/transaction_stream.rs @@ -22,6 +22,7 @@ use std::{fmt, iter}; use futures::{stream, Stream, StreamExt}; +use typeql_lang::pattern::{Conjunction, Variable}; use super::{ message::{RoleTypeRequest, RoleTypeResponse, ThingRequest, ThingResponse}, @@ -35,10 +36,11 @@ use crate::{ Thing, ThingType, Transitivity, Value, ValueType, }, connection::message::{ - ConceptRequest, ConceptResponse, QueryRequest, QueryResponse, ThingTypeRequest, ThingTypeResponse, - TransactionRequest, TransactionResponse, + ConceptRequest, ConceptResponse, LogicRequest, LogicResponse, QueryRequest, QueryResponse, RuleRequest, + RuleResponse, ThingTypeRequest, ThingTypeResponse, TransactionRequest, TransactionResponse, }, error::InternalError, + logic::{Explanation, Rule}, Options, TransactionType, }; @@ -929,6 +931,56 @@ impl TransactionStream { })) } + pub(crate) async fn rule_delete(&self, rule: Rule) -> Result { + match self.rule_single(RuleRequest::Delete { label: rule.label }).await? { + RuleResponse::Delete => Ok(()), + other => Err(InternalError::UnexpectedResponseType(format!("{other:?}")).into()), + } + } + + pub(crate) async fn rule_set_label(&self, rule: Rule, new_label: String) -> Result { + match self.rule_single(RuleRequest::SetLabel { current_label: rule.label, new_label }).await? { + RuleResponse::SetLabel => Ok(()), + other => Err(InternalError::UnexpectedResponseType(format!("{other:?}")).into()), + } + } + + pub(crate) async fn put_rule(&self, label: String, when: Conjunction, then: Variable) -> Result { + match self.logic_single(LogicRequest::PutRule { label, when, then }).await? { + LogicResponse::PutRule { rule } => Ok(rule), + other => Err(InternalError::UnexpectedResponseType(format!("{other:?}")).into()), + } + } + + pub(crate) async fn get_rule(&self, label: String) -> Result { + match self.logic_single(LogicRequest::GetRule { label }).await? { + LogicResponse::GetRule { rule } => Ok(rule), + other => Err(InternalError::UnexpectedResponseType(format!("{other:?}")).into()), + } + } + + pub(crate) fn get_rules(&self) -> Result>> { + let stream = self.logic_stream(LogicRequest::GetRules {})?; + Ok(stream.flat_map(|result| match result { + Ok(LogicResponse::GetRules { rules }) => stream_iter(rules.into_iter().map(Ok)), + Ok(other) => stream_once(Err(InternalError::UnexpectedResponseType(format!("{other:?}")).into())), + Err(err) => stream_once(Err(err)), + })) + } + + pub(crate) fn explain( + &self, + explainable_id: i64, + options: Options, + ) -> Result>> { + let stream = self.query_stream(QueryRequest::Explain { explainable_id, options })?; + Ok(stream.flat_map(|result| match result { + Ok(QueryResponse::Explain { answers }) => stream_iter(answers.into_iter().map(Ok)), + Ok(other) => stream_once(Err(InternalError::UnexpectedResponseType(format!("{other:?}")).into())), + Err(err) => stream_once(Err(err)), + })) + } + async fn single(&self, req: TransactionRequest) -> Result { self.transaction_transmitter.single(req).await } @@ -968,6 +1020,20 @@ impl TransactionStream { } } + async fn rule_single(&self, req: RuleRequest) -> Result { + match self.single(TransactionRequest::Rule(req)).await? { + TransactionResponse::Rule(res) => Ok(res), + other => Err(InternalError::UnexpectedResponseType(format!("{other:?}")).into()), + } + } + + async fn logic_single(&self, req: LogicRequest) -> Result { + match self.single(TransactionRequest::Logic(req)).await? { + TransactionResponse::Logic(res) => Ok(res), + other => Err(InternalError::UnexpectedResponseType(format!("{other:?}")).into()), + } + } + fn stream(&self, req: TransactionRequest) -> Result>> { self.transaction_transmitter.stream(req) } @@ -1011,6 +1077,14 @@ impl TransactionStream { Err(err) => Err(err), })) } + + fn logic_stream(&self, req: LogicRequest) -> Result>> { + Ok(self.stream(TransactionRequest::Logic(req))?.map(|response| match response { + Ok(TransactionResponse::Logic(res)) => Ok(res), + Ok(other) => Err(InternalError::UnexpectedResponseType(format!("{other:?}")).into()), + Err(err) => Err(err), + })) + } } impl fmt::Debug for TransactionStream { diff --git a/src/lib.rs b/src/lib.rs index 8de9067a..e532f4c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,7 @@ mod common; pub mod concept; mod connection; mod database; +pub mod logic; pub mod transaction; pub use self::{ diff --git a/src/logic/explanation.rs b/src/logic/explanation.rs new file mode 100644 index 00000000..0137e673 --- /dev/null +++ b/src/logic/explanation.rs @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use std::collections::HashMap; + +use crate::{answer::ConceptMap, logic::Rule}; + +#[derive(Debug)] +pub struct Explanation { + pub rule: Rule, + pub conclusion: ConceptMap, + pub condition: ConceptMap, + pub variable_mapping: HashMap>, +} diff --git a/src/logic/mod.rs b/src/logic/mod.rs new file mode 100644 index 00000000..f466262c --- /dev/null +++ b/src/logic/mod.rs @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +pub mod explanation; +mod rule; + +pub use self::{explanation::Explanation, rule::Rule}; diff --git a/src/logic/rule.rs b/src/logic/rule.rs new file mode 100644 index 00000000..1bfe7ecc --- /dev/null +++ b/src/logic/rule.rs @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use typeql_lang::pattern::{Conjunction, ThingVariable}; + +#[derive(Clone, Debug)] +pub struct Rule { + pub label: String, + pub when: Conjunction, + pub then: ThingVariable, +} + +impl Rule { + pub(crate) fn new(label: String, when: Conjunction, then: ThingVariable) -> Self { + Self { label, when, then } + } +} diff --git a/src/transaction/logic/api/mod.rs b/src/transaction/logic/api/mod.rs new file mode 100644 index 00000000..a36834e0 --- /dev/null +++ b/src/transaction/logic/api/mod.rs @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +mod rule; + +pub use self::rule::RuleAPI; diff --git a/src/transaction/logic/api/rule.rs b/src/transaction/logic/api/rule.rs new file mode 100644 index 00000000..97ee678d --- /dev/null +++ b/src/transaction/logic/api/rule.rs @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use async_trait::async_trait; + +use crate::{logic::Rule, Result, Transaction}; + +#[async_trait] +pub trait RuleAPI: Clone + Sync + Send { + fn label(&self) -> &String; + async fn delete(&mut self, transaction: &Transaction<'_>) -> Result; + async fn set_label(&mut self, transaction: &Transaction<'_>, new_label: String) -> Result; +} + +#[async_trait] +impl RuleAPI for Rule { + fn label(&self) -> &String { + &self.label + } + + async fn delete(&mut self, transaction: &Transaction<'_>) -> Result { + transaction.logic().transaction_stream.rule_delete(self.clone()).await + } + + async fn set_label(&mut self, transaction: &Transaction<'_>, new_label: String) -> Result { + transaction.logic().transaction_stream.rule_set_label(self.clone(), new_label).await + } +} diff --git a/src/transaction/logic/manager.rs b/src/transaction/logic/manager.rs new file mode 100644 index 00000000..2d8f8f20 --- /dev/null +++ b/src/transaction/logic/manager.rs @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use std::sync::Arc; + +use futures::Stream; +use typeql_lang::pattern::{Conjunction, Variable}; + +use crate::{common::Result, connection::TransactionStream, logic::Rule}; + +#[derive(Clone, Debug)] +pub struct LogicManager { + pub(super) transaction_stream: Arc, +} + +impl LogicManager { + pub(crate) fn new(transaction_stream: Arc) -> Self { + Self { transaction_stream } + } + + pub async fn put_rule(&self, label: String, when: Conjunction, then: Variable) -> Result { + self.transaction_stream.put_rule(label, when, then).await + } + + pub async fn get_rule(&self, label: String) -> Result { + self.transaction_stream.get_rule(label).await + } + + pub fn get_rules(&self) -> Result>> { + self.transaction_stream.get_rules() + } +} diff --git a/src/transaction/logic/mod.rs b/src/transaction/logic/mod.rs new file mode 100644 index 00000000..6f76b3be --- /dev/null +++ b/src/transaction/logic/mod.rs @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +pub mod api; +mod manager; + +pub use self::manager::LogicManager; diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs index c10f5d7d..a37ca63e 100644 --- a/src/transaction/mod.rs +++ b/src/transaction/mod.rs @@ -20,11 +20,12 @@ */ pub mod concept; +pub mod logic; mod query; use std::{fmt, marker::PhantomData, sync::Arc}; -use self::{concept::ConceptManager, query::QueryManager}; +use self::{concept::ConceptManager, logic::LogicManager, query::QueryManager}; use crate::{ common::{Result, TransactionType}, connection::TransactionStream, @@ -37,6 +38,7 @@ pub struct Transaction<'a> { query: QueryManager, concept: ConceptManager, + logic: LogicManager, transaction_stream: Arc, _lifetime_guard: PhantomData<&'a ()>, @@ -50,6 +52,7 @@ impl Transaction<'_> { options: transaction_stream.options().clone(), query: QueryManager::new(transaction_stream.clone()), concept: ConceptManager::new(transaction_stream.clone()), + logic: LogicManager::new(transaction_stream.clone()), transaction_stream, _lifetime_guard: PhantomData::default(), } @@ -71,6 +74,10 @@ impl Transaction<'_> { &self.concept } + pub fn logic(&self) -> &LogicManager { + &self.logic + } + pub async fn commit(self) -> Result { self.transaction_stream.commit().await } diff --git a/src/transaction/query.rs b/src/transaction/query.rs index 36df93f5..e19378cd 100644 --- a/src/transaction/query.rs +++ b/src/transaction/query.rs @@ -27,6 +27,7 @@ use crate::{ answer::{ConceptMap, ConceptMapGroup, Numeric, NumericGroup}, common::Result, connection::TransactionStream, + logic::Explanation, Options, }; @@ -119,4 +120,16 @@ impl QueryManager { ) -> Result>> { self.transaction_stream.match_group_aggregate(query.to_string(), options) } + + pub fn explain(&self, explainable_id: i64) -> Result>> { + self.explain_with_options(explainable_id, Options::new()) + } + + pub fn explain_with_options( + &self, + explainable_id: i64, + options: Options, + ) -> Result>> { + self.transaction_stream.explain(explainable_id, options) + } } diff --git a/tests/BUILD b/tests/BUILD index 6c241689..90eaca2a 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -42,6 +42,7 @@ rust_test( "@crates//:tokio", ], data = [ + "@vaticle_typedb_behaviour//concept/rule:rule.feature", "@vaticle_typedb_behaviour//concept/thing:entity.feature", "@vaticle_typedb_behaviour//concept/thing:relation.feature", "@vaticle_typedb_behaviour//concept/thing:attribute.feature", @@ -59,6 +60,17 @@ rust_test( "@vaticle_typedb_behaviour//typeql/language:match.feature", "@vaticle_typedb_behaviour//typeql/language:get.feature", "@vaticle_typedb_behaviour//typeql/language:rule-validation.feature", + "@vaticle_typedb_behaviour//typeql/reasoner:attribute-attachment.feature", + "@vaticle_typedb_behaviour//typeql/reasoner:compound-queries.feature", + "@vaticle_typedb_behaviour//typeql/reasoner:concept-inequality.feature", + "@vaticle_typedb_behaviour//typeql/reasoner:negation.feature", + "@vaticle_typedb_behaviour//typeql/reasoner:recursion.feature", + "@vaticle_typedb_behaviour//typeql/reasoner:relation-inference.feature", + "@vaticle_typedb_behaviour//typeql/reasoner:rule-interaction.feature", + "@vaticle_typedb_behaviour//typeql/reasoner:schema-queries.feature", + "@vaticle_typedb_behaviour//typeql/reasoner:type-hierarchy.feature", + "@vaticle_typedb_behaviour//typeql/reasoner:value-predicate.feature", + "@vaticle_typedb_behaviour//typeql/reasoner:variable-roles.feature", ], ) diff --git a/tests/behaviour/concept/mod.rs b/tests/behaviour/concept/mod.rs index b9d24896..97bfa416 100644 --- a/tests/behaviour/concept/mod.rs +++ b/tests/behaviour/concept/mod.rs @@ -19,5 +19,6 @@ * under the License. */ +mod rule; mod thing; mod type_; diff --git a/tests/behaviour/concept/rule/mod.rs b/tests/behaviour/concept/rule/mod.rs new file mode 100644 index 00000000..03af19ff --- /dev/null +++ b/tests/behaviour/concept/rule/mod.rs @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +mod steps; + +use serial_test::serial; + +use crate::behaviour::Context; + +#[tokio::test] +#[serial] +async fn test() { + // Bazel specific path: when running the test in bazel, the external data from + // @vaticle_typedb_behaviour is stored in a directory that is a sibling to + // the working directory. + assert!(Context::test("../vaticle_typedb_behaviour/concept/rule/rule.feature").await); +} diff --git a/tests/behaviour/concept/rule/steps.rs b/tests/behaviour/concept/rule/steps.rs new file mode 100644 index 00000000..9511cb5d --- /dev/null +++ b/tests/behaviour/concept/rule/steps.rs @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use cucumber::{given, then, when}; +use typedb_client::{transaction::logic::api::RuleAPI, Result as TypeDBResult}; + +use crate::{ + assert_err, + behaviour::{parameter::LabelParam, Context}, + generic_step_impl, +}; + +generic_step_impl! { + #[step(expr = "delete rule: {label}")] + async fn delete_rule(context: &mut Context, label: LabelParam) -> TypeDBResult { + let tx = context.transaction(); + context.get_rule(label.name).await?.delete(tx).await + } + + #[step(expr = "delete rule: {label}; throws exception")] + async fn delete_rule_throws(context: &mut Context, type_label: LabelParam) { + assert_err!(delete_rule(context, type_label).await); + } + + #[step(expr = r"rule\(( ){label}( )\) set label: {label}")] + async fn rule_set_label( + context: &mut Context, + current_label: LabelParam, + new_label: LabelParam, + ) -> TypeDBResult { + let tx = context.transaction(); + context.get_rule(current_label.name).await?.set_label(tx, new_label.name).await + } +} diff --git a/tests/behaviour/connection/database/mod.rs b/tests/behaviour/connection/database/mod.rs index 5683bb7e..ac57625c 100644 --- a/tests/behaviour/connection/database/mod.rs +++ b/tests/behaviour/connection/database/mod.rs @@ -19,7 +19,7 @@ * under the License. */ -mod steps; +pub(crate) mod steps; use serial_test::serial; diff --git a/tests/behaviour/connection/database/steps.rs b/tests/behaviour/connection/database/steps.rs index 2cfc560f..ce39534a 100644 --- a/tests/behaviour/connection/database/steps.rs +++ b/tests/behaviour/connection/database/steps.rs @@ -32,7 +32,7 @@ use crate::{ generic_step_impl! { #[step(expr = "connection create database: {word}")] - async fn connection_create_database(context: &mut Context, name: String) { + pub async fn connection_create_database(context: &mut Context, name: String) { context.databases.create(name).await.unwrap(); } @@ -49,7 +49,7 @@ generic_step_impl! { } #[step(expr = "connection delete database: {word}")] - async fn connection_delete_database(context: &mut Context, name: String) { + pub async fn connection_delete_database(context: &mut Context, name: String) { context.databases.get(name).and_then(Database::delete).await.unwrap(); } diff --git a/tests/behaviour/connection/mod.rs b/tests/behaviour/connection/mod.rs index f343d1d1..2ab68731 100644 --- a/tests/behaviour/connection/mod.rs +++ b/tests/behaviour/connection/mod.rs @@ -19,7 +19,7 @@ * under the License. */ -mod database; -mod session; -mod steps; -mod transaction; +pub(crate) mod database; +pub(crate) mod session; +pub(crate) mod steps; +pub(crate) mod transaction; diff --git a/tests/behaviour/connection/session/mod.rs b/tests/behaviour/connection/session/mod.rs index aa456e65..5c37d274 100644 --- a/tests/behaviour/connection/session/mod.rs +++ b/tests/behaviour/connection/session/mod.rs @@ -19,7 +19,7 @@ * under the License. */ -mod steps; +pub(crate) mod steps; use serial_test::serial; diff --git a/tests/behaviour/connection/session/steps.rs b/tests/behaviour/connection/session/steps.rs index 77fcf5a9..358b9759 100644 --- a/tests/behaviour/connection/session/steps.rs +++ b/tests/behaviour/connection/session/steps.rs @@ -30,14 +30,14 @@ use crate::{ generic_step_impl! { #[step(expr = "connection open schema session for database: {word}")] - async fn connection_open_schema_session_for_database(context: &mut Context, name: String) { + pub async fn connection_open_schema_session_for_database(context: &mut Context, name: String) { context .session_trackers .push(Session::new(context.databases.get(name).await.unwrap(), SessionType::Schema).await.unwrap().into()); } #[step(expr = "connection open (data )session for database: {word}")] - async fn connection_open_data_session_for_database(context: &mut Context, name: String) { + pub async fn connection_open_data_session_for_database(context: &mut Context, name: String) { context .session_trackers .push(Session::new(context.databases.get(name).await.unwrap(), SessionType::Data).await.unwrap().into()); @@ -73,7 +73,7 @@ generic_step_impl! { } #[step("connection close all sessions")] - async fn connection_close_all_sessions(context: &mut Context) { + pub async fn connection_close_all_sessions(context: &mut Context) { context.session_trackers.clear(); } diff --git a/tests/behaviour/connection/transaction/mod.rs b/tests/behaviour/connection/transaction/mod.rs index 391652e1..87789a05 100644 --- a/tests/behaviour/connection/transaction/mod.rs +++ b/tests/behaviour/connection/transaction/mod.rs @@ -19,7 +19,7 @@ * under the License. */ -mod steps; +pub(crate) mod steps; use serial_test::serial; diff --git a/tests/behaviour/connection/transaction/steps.rs b/tests/behaviour/connection/transaction/steps.rs index 928d08ca..e32e569c 100644 --- a/tests/behaviour/connection/transaction/steps.rs +++ b/tests/behaviour/connection/transaction/steps.rs @@ -33,7 +33,7 @@ generic_step_impl! { // =============================================// #[step(expr = "(for each )session(,) open(s) transaction(s) of type: {transaction_type}")] - async fn session_opens_transaction_of_type(context: &mut Context, type_: TransactionTypeParam) { + pub async fn session_opens_transaction_of_type(context: &mut Context, type_: TransactionTypeParam) { for session_tracker in &mut context.session_trackers { session_tracker.open_transaction(type_.transaction_type).await.unwrap(); } @@ -88,7 +88,7 @@ generic_step_impl! { } #[step(expr = "transaction commits")] - async fn transaction_commits(context: &mut Context) { + pub async fn transaction_commits(context: &mut Context) { context.take_transaction().commit().await.unwrap(); } diff --git a/tests/behaviour/mod.rs b/tests/behaviour/mod.rs index d9d52618..e87c6607 100644 --- a/tests/behaviour/mod.rs +++ b/tests/behaviour/mod.rs @@ -33,6 +33,7 @@ use futures::future::try_join_all; use typedb_client::{ answer::{ConceptMap, ConceptMapGroup, Numeric, NumericGroup}, concept::{Attribute, AttributeType, Entity, EntityType, Relation, RelationType, Thing}, + logic::Rule, Connection, Database, DatabaseManager, Result as TypeDBResult, Transaction, }; @@ -53,6 +54,7 @@ pub struct Context { impl Context { const GROUP_COLUMN_NAME: &'static str = "owner"; const VALUE_COLUMN_NAME: &'static str = "value"; + const DEFAULT_DATABASE: &'static str = "test"; async fn test(glob: &'static str) -> bool { let default_panic = std::panic::take_hook(); @@ -155,6 +157,10 @@ impl Context { pub fn insert_attribute(&mut self, var_name: String, attribute: Option) { self.insert_thing(var_name, attribute.map(Thing::Attribute)); } + + pub async fn get_rule(&self, label: String) -> TypeDBResult { + self.transaction().logic().get_rule(label).await + } } impl Default for Context { @@ -176,13 +182,13 @@ impl Default for Context { #[macro_export] macro_rules! generic_step_impl { - {$($(#[step($pattern:expr)])+ $async:ident fn $fn_name:ident $args:tt $(-> $res:ty)? $body:block)+} => { + {$($(#[step($pattern:expr)])+ $vis:vis $async:ident fn $fn_name:ident $args:tt $(-> $res:ty)? $body:block)+} => { $($( #[given($pattern)] #[when($pattern)] #[then($pattern)] )* - $async fn $fn_name $args $(-> $res)? $body + $vis $async fn $fn_name $args $(-> $res)? $body )* }; } diff --git a/tests/behaviour/typeql/define/mod.rs b/tests/behaviour/typeql/language/define/mod.rs similarity index 100% rename from tests/behaviour/typeql/define/mod.rs rename to tests/behaviour/typeql/language/define/mod.rs diff --git a/tests/behaviour/typeql/delete/mod.rs b/tests/behaviour/typeql/language/delete/mod.rs similarity index 100% rename from tests/behaviour/typeql/delete/mod.rs rename to tests/behaviour/typeql/language/delete/mod.rs diff --git a/tests/behaviour/typeql/get/mod.rs b/tests/behaviour/typeql/language/get/mod.rs similarity index 100% rename from tests/behaviour/typeql/get/mod.rs rename to tests/behaviour/typeql/language/get/mod.rs diff --git a/tests/behaviour/typeql/insert/mod.rs b/tests/behaviour/typeql/language/insert/mod.rs similarity index 100% rename from tests/behaviour/typeql/insert/mod.rs rename to tests/behaviour/typeql/language/insert/mod.rs diff --git a/tests/behaviour/typeql/match_/mod.rs b/tests/behaviour/typeql/language/match_/mod.rs similarity index 100% rename from tests/behaviour/typeql/match_/mod.rs rename to tests/behaviour/typeql/language/match_/mod.rs diff --git a/tests/behaviour/typeql/language/mod.rs b/tests/behaviour/typeql/language/mod.rs new file mode 100644 index 00000000..8bc83b90 --- /dev/null +++ b/tests/behaviour/typeql/language/mod.rs @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +mod define; +mod delete; +mod get; +mod insert; +mod match_; +mod rule_validation; +pub(crate) mod steps; +mod undefine; +mod update; diff --git a/tests/behaviour/typeql/rule_validation/mod.rs b/tests/behaviour/typeql/language/rule_validation/mod.rs similarity index 100% rename from tests/behaviour/typeql/rule_validation/mod.rs rename to tests/behaviour/typeql/language/rule_validation/mod.rs diff --git a/tests/behaviour/typeql/steps.rs b/tests/behaviour/typeql/language/steps.rs similarity index 85% rename from tests/behaviour/typeql/steps.rs rename to tests/behaviour/typeql/language/steps.rs index 66255573..67c68edb 100644 --- a/tests/behaviour/typeql/steps.rs +++ b/tests/behaviour/typeql/language/steps.rs @@ -24,18 +24,19 @@ use futures::TryStreamExt; use typedb_client::{answer::Numeric, Result as TypeDBResult}; use typeql_lang::parse_query; use util::{ - equals_approximate, iter_table_map, match_answer_concept, match_answer_concept_map, match_templated_answer, + equals_approximate, iter_table_map, match_answer_concept, match_answer_concept_map, match_answer_rule, + match_templated_answer, }; use crate::{ assert_err, - behaviour::{util, Context}, + behaviour::{parameter::LabelParam, util, Context}, generic_step_impl, }; generic_step_impl! { #[step(expr = "typeql define")] - async fn typeql_define(context: &mut Context, step: &Step) -> TypeDBResult { + pub async fn typeql_define(context: &mut Context, step: &Step) -> TypeDBResult { let parsed = parse_query(step.docstring().unwrap())?; context.transaction().query().define(&parsed.to_string()).await } @@ -62,7 +63,7 @@ generic_step_impl! { } #[step(expr = "typeql insert")] - async fn typeql_insert(context: &mut Context, step: &Step) -> TypeDBResult { + pub async fn typeql_insert(context: &mut Context, step: &Step) -> TypeDBResult { let parsed = parse_query(step.docstring().unwrap())?; context.transaction().query().insert(&parsed.to_string())?.try_collect::>().await?; Ok(()) @@ -102,7 +103,7 @@ generic_step_impl! { } #[step(expr = "get answers of typeql match")] - async fn get_answers_typeql_match(context: &mut Context, step: &Step) -> TypeDBResult { + pub async fn get_answers_typeql_match(context: &mut Context, step: &Step) -> TypeDBResult { let parsed = parse_query(step.docstring().unwrap())?; context.answer = context.transaction().query().match_(&parsed.to_string())?.try_collect::>().await?; Ok(()) @@ -116,7 +117,7 @@ generic_step_impl! { } #[step(expr = "answer size is: {int}")] - async fn answer_size(context: &mut Context, expected_answers: usize) { + pub async fn answer_size(context: &mut Context, expected_answers: usize) { let actual_answers = context.answer.len(); assert_eq!( actual_answers, expected_answers, @@ -286,13 +287,13 @@ generic_step_impl! { for table_row in &step_table { if match_answer_concept( context, - table_row.get(&Context::GROUP_COLUMN_NAME.to_string()).unwrap(), + table_row.get(Context::GROUP_COLUMN_NAME).unwrap(), &group.owner, ) .await { let mut table_row_wo_owner = table_row.clone(); - table_row_wo_owner.remove(&Context::GROUP_COLUMN_NAME.to_string()); + table_row_wo_owner.remove(Context::GROUP_COLUMN_NAME); if match_answer_concept_map(context, &table_row_wo_owner, &ans_row).await { matched_rows += 1; break; @@ -331,7 +332,7 @@ generic_step_impl! { for table_row in &step_table { if match_answer_concept( context, - table_row.get(&Context::GROUP_COLUMN_NAME.to_string()).unwrap(), + table_row.get(Context::GROUP_COLUMN_NAME).unwrap(), &group.owner, ) .await @@ -342,7 +343,7 @@ generic_step_impl! { Numeric::NaN => panic!("Last answer in NaN while expected answer is not."), }; let expected_value: f64 = - table_row.get(&Context::VALUE_COLUMN_NAME.to_string()).unwrap().parse().unwrap(); + table_row.get(Context::VALUE_COLUMN_NAME).unwrap().parse().unwrap(); if equals_approximate(answer, expected_value) { matched_rows += 1; break; @@ -356,4 +357,47 @@ generic_step_impl! { matched entries of given {actual_answers}." ); } + + #[step(expr = "rules contain: {label}")] + async fn rules_contain(context: &mut Context, rule_label: LabelParam) { + let res = context.transaction().logic().get_rule(rule_label.name).await; + assert!(res.is_ok(), "{res:?}"); + } + + #[step(expr = "rules do not contain: {label}")] + async fn rules_do_not_contain(context: &mut Context, rule_label: LabelParam) { + let res = context.transaction().logic().get_rule(rule_label.name).await; + assert!(res.is_err(), "{res:?}"); + } + + #[step(expr = "rules are")] + async fn rules_are(context: &mut Context, step: &Step) { + let stream = context.transaction().logic().get_rules(); + assert!(stream.is_ok(), "{:?}", stream.err()); + let res = stream.unwrap().try_collect::>().await; + assert!(res.is_ok(), "{:?}", res.err()); + let answers = res.unwrap(); + let step_table = iter_table_map(step).collect::>(); + let expected_answers = step_table.len(); + let actual_answers = answers.len(); + assert_eq!( + actual_answers, expected_answers, + "The number of identifier entries (rows) should match the number of answers, \ + but found {expected_answers} identifier entries and {actual_answers} answers." + ); + let mut matched_rows = 0; + for ans_row in &answers { + for table_row in &step_table { + if match_answer_rule(table_row, ans_row).await { + matched_rows += 1; + break; + } + } + } + assert_eq!( + matched_rows, actual_answers, + "An identifier entry (row) should match 1-to-1 to an answer, but there are only {matched_rows} \ + matched entries of given {actual_answers}." + ); + } } diff --git a/tests/behaviour/typeql/undefine/mod.rs b/tests/behaviour/typeql/language/undefine/mod.rs similarity index 100% rename from tests/behaviour/typeql/undefine/mod.rs rename to tests/behaviour/typeql/language/undefine/mod.rs diff --git a/tests/behaviour/typeql/update/mod.rs b/tests/behaviour/typeql/language/update/mod.rs similarity index 100% rename from tests/behaviour/typeql/update/mod.rs rename to tests/behaviour/typeql/language/update/mod.rs diff --git a/tests/behaviour/typeql/mod.rs b/tests/behaviour/typeql/mod.rs index c57da845..f4051a0e 100644 --- a/tests/behaviour/typeql/mod.rs +++ b/tests/behaviour/typeql/mod.rs @@ -19,12 +19,5 @@ * under the License. */ -mod define; -mod delete; -mod get; -mod insert; -mod match_; -mod rule_validation; -mod steps; -// mod undefine; -mod update; +mod language; +mod reasoner; diff --git a/tests/behaviour/typeql/reasoner/attribute_attachment/mod.rs b/tests/behaviour/typeql/reasoner/attribute_attachment/mod.rs new file mode 100644 index 00000000..183472e3 --- /dev/null +++ b/tests/behaviour/typeql/reasoner/attribute_attachment/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use serial_test::serial; + +use crate::behaviour::Context; + +#[tokio::test] +#[serial] +async fn test() { + // Bazel specific path: when running the test in bazel, the external data from + // @vaticle_typedb_behaviour is stored in a directory that is a sibling to + // the working directory. + assert!(Context::test("../vaticle_typedb_behaviour/typeql/reasoner/attribute-attachment.feature").await); +} diff --git a/tests/behaviour/typeql/reasoner/compound_queries/mod.rs b/tests/behaviour/typeql/reasoner/compound_queries/mod.rs new file mode 100644 index 00000000..657619d6 --- /dev/null +++ b/tests/behaviour/typeql/reasoner/compound_queries/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use serial_test::serial; + +use crate::behaviour::Context; + +#[tokio::test] +#[serial] +async fn test() { + // Bazel specific path: when running the test in bazel, the external data from + // @vaticle_typedb_behaviour is stored in a directory that is a sibling to + // the working directory. + assert!(Context::test("../vaticle_typedb_behaviour/typeql/reasoner/compound-queries.feature").await); +} diff --git a/tests/behaviour/typeql/reasoner/concept_inequality/mod.rs b/tests/behaviour/typeql/reasoner/concept_inequality/mod.rs new file mode 100644 index 00000000..001675b7 --- /dev/null +++ b/tests/behaviour/typeql/reasoner/concept_inequality/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use serial_test::serial; + +use crate::behaviour::Context; + +#[tokio::test] +#[serial] +async fn test() { + // Bazel specific path: when running the test in bazel, the external data from + // @vaticle_typedb_behaviour is stored in a directory that is a sibling to + // the working directory. + assert!(Context::test("../vaticle_typedb_behaviour/typeql/reasoner/concept-inequality.feature").await); +} diff --git a/tests/behaviour/typeql/reasoner/mod.rs b/tests/behaviour/typeql/reasoner/mod.rs new file mode 100644 index 00000000..c49cb72a --- /dev/null +++ b/tests/behaviour/typeql/reasoner/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +mod attribute_attachment; +mod compound_queries; +mod concept_inequality; +mod negation; +mod recursion; +mod relation_inference; +mod rule_interaction; +mod schema_queries; +mod steps; +mod type_hierarchy; +mod value_predicate; +mod variable_roles; diff --git a/tests/behaviour/typeql/reasoner/negation/mod.rs b/tests/behaviour/typeql/reasoner/negation/mod.rs new file mode 100644 index 00000000..bc5f2379 --- /dev/null +++ b/tests/behaviour/typeql/reasoner/negation/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use serial_test::serial; + +use crate::behaviour::Context; + +#[tokio::test] +#[serial] +async fn test() { + // Bazel specific path: when running the test in bazel, the external data from + // @vaticle_typedb_behaviour is stored in a directory that is a sibling to + // the working directory. + assert!(Context::test("../vaticle_typedb_behaviour/typeql/reasoner/negation.feature").await); +} diff --git a/tests/behaviour/typeql/reasoner/recursion/mod.rs b/tests/behaviour/typeql/reasoner/recursion/mod.rs new file mode 100644 index 00000000..5e80da69 --- /dev/null +++ b/tests/behaviour/typeql/reasoner/recursion/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use serial_test::serial; + +use crate::behaviour::Context; + +#[tokio::test] +#[serial] +async fn test() { + // Bazel specific path: when running the test in bazel, the external data from + // @vaticle_typedb_behaviour is stored in a directory that is a sibling to + // the working directory. + assert!(Context::test("../vaticle_typedb_behaviour/typeql/reasoner/recursion.feature").await); +} diff --git a/tests/behaviour/typeql/reasoner/relation_inference/mod.rs b/tests/behaviour/typeql/reasoner/relation_inference/mod.rs new file mode 100644 index 00000000..c8e24dd1 --- /dev/null +++ b/tests/behaviour/typeql/reasoner/relation_inference/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use serial_test::serial; + +use crate::behaviour::Context; + +#[tokio::test] +#[serial] +async fn test() { + // Bazel specific path: when running the test in bazel, the external data from + // @vaticle_typedb_behaviour is stored in a directory that is a sibling to + // the working directory. + assert!(Context::test("../vaticle_typedb_behaviour/typeql/reasoner/relation-inference.feature").await); +} diff --git a/tests/behaviour/typeql/reasoner/rule_interaction/mod.rs b/tests/behaviour/typeql/reasoner/rule_interaction/mod.rs new file mode 100644 index 00000000..f9734df3 --- /dev/null +++ b/tests/behaviour/typeql/reasoner/rule_interaction/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use serial_test::serial; + +use crate::behaviour::Context; + +#[tokio::test] +#[serial] +async fn test() { + // Bazel specific path: when running the test in bazel, the external data from + // @vaticle_typedb_behaviour is stored in a directory that is a sibling to + // the working directory. + assert!(Context::test("../vaticle_typedb_behaviour/typeql/reasoner/rule-interaction.feature").await); +} diff --git a/tests/behaviour/typeql/reasoner/schema_queries/mod.rs b/tests/behaviour/typeql/reasoner/schema_queries/mod.rs new file mode 100644 index 00000000..383d0f23 --- /dev/null +++ b/tests/behaviour/typeql/reasoner/schema_queries/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use serial_test::serial; + +use crate::behaviour::Context; + +#[tokio::test] +#[serial] +async fn test() { + // Bazel specific path: when running the test in bazel, the external data from + // @vaticle_typedb_behaviour is stored in a directory that is a sibling to + // the working directory. + assert!(Context::test("../vaticle_typedb_behaviour/typeql/reasoner/schema-queries.feature").await); +} diff --git a/tests/behaviour/typeql/reasoner/steps.rs b/tests/behaviour/typeql/reasoner/steps.rs new file mode 100644 index 00000000..eb74582e --- /dev/null +++ b/tests/behaviour/typeql/reasoner/steps.rs @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use cucumber::{gherkin::Step, given, then, when}; +use typedb_client::TransactionType; + +use crate::{ + behaviour::{ + connection::{ + database::steps::connection_create_database, + session::steps::{ + connection_close_all_sessions, connection_open_data_session_for_database, + connection_open_schema_session_for_database, + }, + transaction::steps::{session_opens_transaction_of_type, transaction_commits}, + }, + parameter::TransactionTypeParam, + typeql::language::steps::{answer_size, get_answers_typeql_match, typeql_define, typeql_insert}, + Context, + }, + generic_step_impl, +}; + +generic_step_impl! { + #[step(expr = "reasoning schema")] + async fn reasoning_schema(context: &mut Context, step: &Step) { + if !context.databases.contains(Context::DEFAULT_DATABASE).await.unwrap() { + connection_create_database(context, Context::DEFAULT_DATABASE.to_string()).await; + } + connection_open_schema_session_for_database(context, Context::DEFAULT_DATABASE.to_string()).await; + session_opens_transaction_of_type(context, TransactionTypeParam { transaction_type: TransactionType::Write }).await; + assert!(typeql_define(context, step).await.is_ok()); + transaction_commits(context).await; + connection_close_all_sessions(context).await; + } + + #[step(expr = "reasoning data")] + async fn reasoning_data(context: &mut Context, step: &Step) { + connection_open_data_session_for_database(context, Context::DEFAULT_DATABASE.to_string()).await; + session_opens_transaction_of_type(context, TransactionTypeParam { transaction_type: TransactionType::Write }).await; + assert!(typeql_insert(context, step).await.is_ok()); + transaction_commits(context).await; + connection_close_all_sessions(context).await; + } + + #[step(expr = "reasoning query")] + async fn reasoning_query(context: &mut Context, step: &Step) { + connection_open_data_session_for_database(context, Context::DEFAULT_DATABASE.to_string()).await; + session_opens_transaction_of_type(context, TransactionTypeParam { transaction_type: TransactionType::Read }).await; + assert!(get_answers_typeql_match(context, step).await.is_ok()); + connection_close_all_sessions(context).await; + } + + #[step(expr = "verify answer size is: {int}")] + async fn verify_answer_size(context: &mut Context, expected_answers: usize) { + answer_size(context, expected_answers).await; + } + + #[step(expr = "verify answer set is equivalent for query")] + async fn verify_answer_set_is_equivalent_for_query(context: &mut Context, step: &Step) { + let prev_answer = context.answer.clone(); + reasoning_query(context, step).await; + let total_rows = context.answer.len(); + let mut matched_rows = 0; + for row_curr in &context.answer { + for row_prev in &prev_answer { + if row_curr == row_prev { + matched_rows += 1; + break; + } + } + } + assert_eq!( + matched_rows, total_rows, + "There are only {matched_rows} matched entries of given {total_rows}." + ); + } + + #[step(expr = "verifier is initialised")] + #[step(expr = "verify answers are sound")] + #[step(expr = "verify answers are complete")] + async fn do_nothing(_context: &mut Context) { + // We don't have a verifier + } + + #[step(expr = "verify answers are consistent across {int} executions")] + async fn verify_answers_are_consistent_across_executions(_context: &mut Context, _executions: usize) { + // We can't execute previous query again because don't remember the query + } +} diff --git a/tests/behaviour/typeql/reasoner/type_hierarchy/mod.rs b/tests/behaviour/typeql/reasoner/type_hierarchy/mod.rs new file mode 100644 index 00000000..73edd887 --- /dev/null +++ b/tests/behaviour/typeql/reasoner/type_hierarchy/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use serial_test::serial; + +use crate::behaviour::Context; + +#[tokio::test] +#[serial] +async fn test() { + // Bazel specific path: when running the test in bazel, the external data from + // @vaticle_typedb_behaviour is stored in a directory that is a sibling to + // the working directory. + assert!(Context::test("../vaticle_typedb_behaviour/typeql/reasoner/type-hierarchy.feature").await); +} diff --git a/tests/behaviour/typeql/reasoner/value_predicate/mod.rs b/tests/behaviour/typeql/reasoner/value_predicate/mod.rs new file mode 100644 index 00000000..98671615 --- /dev/null +++ b/tests/behaviour/typeql/reasoner/value_predicate/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use serial_test::serial; + +use crate::behaviour::Context; + +#[tokio::test] +#[serial] +async fn test() { + // Bazel specific path: when running the test in bazel, the external data from + // @vaticle_typedb_behaviour is stored in a directory that is a sibling to + // the working directory. + assert!(Context::test("../vaticle_typedb_behaviour/typeql/reasoner/value-predicate.feature").await); +} diff --git a/tests/behaviour/typeql/reasoner/variable_roles/mod.rs b/tests/behaviour/typeql/reasoner/variable_roles/mod.rs new file mode 100644 index 00000000..1ba1abac --- /dev/null +++ b/tests/behaviour/typeql/reasoner/variable_roles/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use serial_test::serial; + +use crate::behaviour::Context; + +#[tokio::test] +#[serial] +async fn test() { + // Bazel specific path: when running the test in bazel, the external data from + // @vaticle_typedb_behaviour is stored in a directory that is a sibling to + // the working directory. + assert!(Context::test("../vaticle_typedb_behaviour/typeql/reasoner/variable-roles.feature").await); +} diff --git a/tests/behaviour/util.rs b/tests/behaviour/util.rs index 33adab9d..d046a963 100644 --- a/tests/behaviour/util.rs +++ b/tests/behaviour/util.rs @@ -31,10 +31,11 @@ use regex::{Captures, Regex}; use typedb_client::{ answer::ConceptMap, concept::{Annotation, Attribute, Concept, Entity, Relation, Value}, + logic::Rule, transaction::concept::api::ThingAPI, Result as TypeDBResult, }; -use typeql_lang::parse_query; +use typeql_lang::{parse_patterns, parse_query, pattern::Variable}; use crate::behaviour::Context; @@ -42,14 +43,14 @@ pub fn iter_table(step: &Step) -> impl Iterator { step.table().unwrap().rows.iter().flatten().map(String::as_str) } -pub fn iter_table_map(step: &Step) -> impl Iterator> { +pub fn iter_table_map(step: &Step) -> impl Iterator> { let (keys, rows) = step.table().unwrap().rows.split_first().unwrap(); - rows.iter().map(|row| keys.iter().zip(row).collect()) + rows.iter().map(|row| keys.iter().zip(row).map(|(k, v)| (k.as_str(), v.as_str())).collect()) } pub async fn match_answer_concept_map( context: &Context, - answer_identifiers: &HashMap<&String, &String>, + answer_identifiers: &HashMap<&str, &str>, answer: &ConceptMap, ) -> bool { stream::iter(answer_identifiers.keys()) @@ -60,7 +61,7 @@ pub async fn match_answer_concept_map( .await } -pub async fn match_answer_concept(context: &Context, answer_identifier: &String, answer: &Concept) -> bool { +pub async fn match_answer_concept(context: &Context, answer_identifier: &str, answer: &Concept) -> bool { let identifiers: Vec<&str> = answer_identifier.splitn(2, ":").collect(); match identifiers[0] { "key" => key_values_equal(context, identifiers[1], answer).await, @@ -174,3 +175,13 @@ fn get_iid(concept: &Concept) -> String { }; iid.to_string() } + +pub async fn match_answer_rule(answer_identifiers: &HashMap<&str, &str>, answer: &Rule) -> bool { + let when_clause = answer_identifiers.get("when").unwrap().to_string(); + let when = parse_patterns(when_clause.as_str()).unwrap()[0].clone().into_conjunction(); + let then_clause = answer_identifiers.get("then").unwrap().to_string(); + let then = parse_patterns(then_clause.as_str()).unwrap()[0].clone().into_variable(); + answer_identifiers.get("label").unwrap().to_string() == answer.label + && when == answer.when + && then == Variable::Thing(answer.then.clone()) +} diff --git a/tests/integration/common.rs b/tests/integration/common.rs index 44655c73..14011dc6 100644 --- a/tests/integration/common.rs +++ b/tests/integration/common.rs @@ -60,3 +60,39 @@ pub async fn create_test_database_with_schema(connection: Connection, schema: &s transaction.commit().await?; Ok(()) } + +#[macro_export] +macro_rules! test_for_each_arg { + { + $perm_args:tt + $( $( #[ $extra_anno:meta ] )* $async:ident fn $test:ident $args:tt -> $ret:ty $test_impl:block )+ + } => { + test_for_each_arg!{ @impl $( $async fn $test $args $ret $test_impl )+ } + test_for_each_arg!{ @impl_per $perm_args { $( $( #[ $extra_anno ] )* $async fn $test )+ } } + }; + + { @impl $( $async:ident fn $test:ident $args:tt $ret:ty $test_impl:block )+ } => { + mod _impl { + use super::*; + $( pub $async fn $test $args -> $ret $test_impl )+ + } + }; + + { @impl_per { $($mod:ident => $arg:expr),+ $(,)? } $fns:tt } => { + $(test_for_each_arg!{ @impl_mod { $mod => $arg } $fns })+ + }; + + { @impl_mod { $mod:ident => $arg:expr } { $( $( #[ $extra_anno:meta ] )* async fn $test:ident )+ } } => { + mod $mod { + use super::*; + $( + #[tokio::test] + #[serial($mod)] + $( #[ $extra_anno ] )* + pub async fn $test() { + _impl::$test($arg).await.unwrap(); + } + )+ + } + }; +} diff --git a/tests/integration/logic.rs b/tests/integration/logic.rs new file mode 100644 index 00000000..a29375d3 --- /dev/null +++ b/tests/integration/logic.rs @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2022 Vaticle + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +use std::{collections::HashMap, default::Default}; + +use futures::TryStreamExt; +use serial_test::serial; +use typedb_client::{ + answer::{ConceptMap, Explainable}, + concept::{Attribute, Concept, Value}, + logic::Explanation, + transaction::concept::api::ThingAPI, + Connection, DatabaseManager, Options, Result as TypeDBResult, Session, + SessionType::{Data, Schema}, + Transaction, + TransactionType::{Read, Write}, +}; + +use super::common; +use crate::test_for_each_arg; + +test_for_each_arg! { + { + core => common::new_core_connection().unwrap(), + cluster => common::new_cluster_connection().unwrap(), + } + + async fn test_disjunction_explainable(connection: Connection) -> TypeDBResult { + let schema = r#"define + person sub entity, + owns name, + plays friendship:friend, + plays marriage:husband, + plays marriage:wife; + name sub attribute, value string; + friendship sub relation, + relates friend; + marriage sub relation, + relates husband, relates wife;"#; + common::create_test_database_with_schema(connection.clone(), schema).await?; + + let databases = DatabaseManager::new(connection); + { + let session = Session::new(databases.get(common::TEST_DATABASE).await?, Schema).await?; + let transaction = session.transaction(Write).await?; + transaction.logic().put_rule( + "marriage-is-friendship".to_string(), + typeql_lang::parse_pattern("{ $x isa person; $y isa person; (husband: $x, wife: $y) isa marriage; }")? + .into_conjunction(), + typeql_lang::parse_variable("(friend: $x, friend: $y) isa friendship")?, + ).await?; + transaction.commit().await?; + } + + let session = Session::new(databases.get(common::TEST_DATABASE).await?, Data).await?; + let transaction = session.transaction(Write).await?; + let data = r#"insert $x isa person, has name 'Zack'; + $y isa person, has name 'Yasmin'; + (husband: $x, wife: $y) isa marriage;"#; + let _ = transaction.query().insert(data)?; + transaction.commit().await?; + + let with_inference_and_explanation = Options::new().infer(true).explain(true); + let transaction = session.transaction_with_options(Read, with_inference_and_explanation).await?; + let answer_stream = transaction.query().match_( + r#"match $p1 isa person; + { (friend: $p1, friend: $p2) isa friendship;} + or { $p1 has name 'Zack'; };"#, + )?; + let answers = answer_stream.try_collect::>().await?; + + assert_eq!(3, answers.len()); + + for answer in answers { + if answer.map.contains_key("p2") { + assert_eq!(3, answer.map.len()); + assert!(!answer.explainables.is_empty()); + assert_explanations_count_and_projection_match(&answer, 1, &transaction).await?; + } else { + assert_eq!(2, answer.map.len()); + assert!(answer.explainables.is_empty()); + } + } + + Ok(()) + } + + async fn test_relation_explainable(connection: Connection) -> TypeDBResult { + let schema = r#"define + person sub entity, + owns name, + plays friendship:friend, + plays marriage:husband, + plays marriage:wife; + name sub attribute, value string; + friendship sub relation, + relates friend; + marriage sub relation, + relates husband, relates wife;"#; + common::create_test_database_with_schema(connection.clone(), schema).await?; + + let databases = DatabaseManager::new(connection); + { + let session = Session::new(databases.get(common::TEST_DATABASE).await?, Schema).await?; + let transaction = session.transaction(Write).await?; + transaction.logic().put_rule( + "marriage-is-friendship".to_string(), + typeql_lang::parse_pattern("{ $x isa person; $y isa person; (husband: $x, wife: $y) isa marriage; }")? + .into_conjunction(), + typeql_lang::parse_variable("(friend: $x, friend: $y) isa friendship")?, + ).await?; + transaction.commit().await?; + } + + let session = Session::new(databases.get(common::TEST_DATABASE).await?, Data).await?; + let transaction = session.transaction(Write).await?; + let data = r#"insert $x isa person, has name 'Zack'; + $y isa person, has name 'Yasmin'; + (husband: $x, wife: $y) isa marriage;"#; + let _ = transaction.query().insert(data)?; + transaction.commit().await?; + + let with_inference_and_explanation = Options::new().infer(true).explain(true); + let transaction = session.transaction_with_options(Read, with_inference_and_explanation).await?; + let answer_stream = transaction.query().match_( + r#"match (friend: $p1, friend: $p2) isa friendship; $p1 has name $na;"#, + )?; + let answers = answer_stream.try_collect::>().await?; + + assert_eq!(2, answers.len()); + + for answer in answers { + assert!(!answer.explainables.is_empty()); + assert_explanations_count_and_projection_match(&answer, 1, &transaction).await?; + } + + Ok(()) + } + + async fn test_relation_explainable_multiple_ways(connection: Connection) -> TypeDBResult { + let schema = r#"define + person sub entity, + owns name, + plays friendship:friend, + plays marriage:husband, + plays marriage:wife; + name sub attribute, value string; + friendship sub relation, + relates friend; + marriage sub relation, + relates husband, relates wife;"#; + common::create_test_database_with_schema(connection.clone(), schema).await?; + + let databases = DatabaseManager::new(connection); + { + let session = Session::new(databases.get(common::TEST_DATABASE).await?, Schema).await?; + let transaction = session.transaction(Write).await?; + transaction.logic().put_rule( + "marriage-is-friendship".to_string(), + typeql_lang::parse_pattern("{ $x isa person; $y isa person; (husband: $x, wife: $y) isa marriage; }")? + .into_conjunction(), + typeql_lang::parse_variable("(friend: $x, friend: $y) isa friendship")?, + ).await?; + transaction.logic().put_rule( + "everyone-is-friends".to_string(), + typeql_lang::parse_pattern("{ $x isa person; $y isa person; not { $x is $y; }; }")? + .into_conjunction(), + typeql_lang::parse_variable("(friend: $x, friend: $y) isa friendship")?, + ).await?; + transaction.commit().await?; + } + + let session = Session::new(databases.get(common::TEST_DATABASE).await?, Data).await?; + let transaction = session.transaction(Write).await?; + let data = r#"insert $x isa person, has name 'Zack'; + $y isa person, has name 'Yasmin'; + (husband: $x, wife: $y) isa marriage;"#; + let _ = transaction.query().insert(data)?; + transaction.commit().await?; + + let with_inference_and_explanation = Options::new().infer(true).explain(true); + let transaction = session.transaction_with_options(Read, with_inference_and_explanation).await?; + let answer_stream = transaction.query().match_( + r#"match (friend: $p1, friend: $p2) isa friendship; $p1 has name $na;"#, + )?; + let answers = answer_stream.try_collect::>().await?; + + assert_eq!(2, answers.len()); + + for answer in answers { + assert!(!answer.explainables.is_empty()); + assert_explanations_count_and_projection_match(&answer, 3, &transaction).await?; + } + + Ok(()) + } + + async fn test_has_explicit_explainable_two_ways(connection: Connection) -> TypeDBResult { + let schema = r#"define + milk sub entity, + owns age-in-days, + owns is-still-good; + age-in-days sub attribute, value long; + is-still-good sub attribute, value boolean;"#; + common::create_test_database_with_schema(connection.clone(), schema).await?; + + let databases = DatabaseManager::new(connection); + { + let session = Session::new(databases.get(common::TEST_DATABASE).await?, Schema).await?; + let transaction = session.transaction(Write).await?; + transaction.logic().put_rule( + "old-milk-is-not-good".to_string(), + typeql_lang::parse_pattern("{ $x isa milk, has age-in-days <= 10; }")? + .into_conjunction(), + typeql_lang::parse_variable("$x has is-still-good true")?, + ).await?; + transaction.logic().put_rule( + "all-milk-is-good".to_string(), + typeql_lang::parse_pattern("{ $x isa milk; }")? + .into_conjunction(), + typeql_lang::parse_variable("$x has is-still-good true")?, + ).await?; + transaction.commit().await?; + } + + let session = Session::new(databases.get(common::TEST_DATABASE).await?, Data).await?; + let transaction = session.transaction(Write).await?; + let data = r#"insert $x isa milk, has age-in-days 5;"#; + let _ = transaction.query().insert(data)?; + let data = r#"insert $x isa milk, has age-in-days 10;"#; + let _ = transaction.query().insert(data)?; + let data = r#"insert $x isa milk, has age-in-days 15;"#; + let _ = transaction.query().insert(data)?; + transaction.commit().await?; + + let with_inference_and_explanation = Options::new().infer(true).explain(true); + let transaction = session.transaction_with_options(Read, with_inference_and_explanation).await?; + let answer_stream = transaction.query().match_( + r#"match $x has is-still-good $a;"#, + )?; + let answers = answer_stream.try_collect::>().await?; + + assert_eq!(3, answers.len()); + + let age_in_days = transaction.concept().get_attribute_type(String::from("age-in-days")).await?.unwrap(); + for answer in answers { + assert!(!answer.explainables.is_empty()); + match answer.map.get("x").unwrap() { + Concept::Entity(entity) => { + let attributes: Vec = entity.get_has(&transaction, vec![age_in_days.clone()], vec![])?.try_collect().await?; + if attributes.first().unwrap().value == Value::Long(15) { + assert_explanations_count_and_projection_match(&answer, 1, &transaction).await?; + } else { + assert_explanations_count_and_projection_match(&answer, 2, &transaction).await?; + } + }, + _ => panic!("Incorrect Concept type: {:?}", answer.map.get("x").unwrap()), + } + } + + Ok(()) + } +} + +async fn assert_explanations_count_and_projection_match( + ans: &ConceptMap, + explanations_count: usize, + transaction: &Transaction<'_>, +) -> TypeDBResult { + assert_explainables_in_concept_map(ans); + let explainables = all_explainables(ans); + assert_eq!(1, explainables.len()); + + let explanations = get_explanations(explainables[0], transaction).await?; + assert_eq!(explanations_count, explanations.len()); + + assert_explanation_concepts_match_projection(ans, explanations); + Ok(()) +} + +fn assert_explainables_in_concept_map(ans: &ConceptMap) { + ans.explainables.relations.keys().for_each(|k| assert!(ans.map.contains_key(k.as_str()))); + ans.explainables.attributes.keys().for_each(|k| assert!(ans.map.contains_key(k.as_str()))); + ans.explainables + .ownerships + .keys() + .for_each(|(k1, k2)| assert!(ans.map.contains_key(k1.as_str()) && ans.map.contains_key(k2.as_str()))); +} + +fn all_explainables(ans: &ConceptMap) -> Vec<&Explainable> { + let explainables = &ans.explainables; + explainables + .attributes + .values() + .chain(explainables.relations.values()) + .chain(explainables.ownerships.values()) + .collect::>() +} + +async fn get_explanations(explainable: &Explainable, transaction: &Transaction<'_>) -> TypeDBResult> { + transaction.query().explain(explainable.id)?.try_collect::>().await +} + +fn assert_explanation_concepts_match_projection(ans: &ConceptMap, explanations: Vec) { + for explanation in explanations { + let mapping = explanation.variable_mapping; + let projected = apply_mapping(&mapping, ans); + for var in projected.map.keys() { + assert!(explanation.conclusion.map.contains_key(var)); + assert_eq!(explanation.conclusion.map.get(var), projected.map.get(var)); + } + } +} + +fn apply_mapping(mapping: &HashMap>, complete_map: &ConceptMap) -> ConceptMap { + let mut concepts: HashMap = HashMap::new(); + for key in mapping.keys() { + assert!(complete_map.map.contains_key(key)); + let concept = complete_map.get(key).unwrap(); + for mapped in mapping.get(key).unwrap() { + assert!(!concepts.contains_key(mapped) || concepts.get(mapped).unwrap() == concept); + concepts.insert(mapped.to_string(), concept.clone()); + } + } + ConceptMap { map: concepts, explainables: Default::default() } +} diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index 036bff3c..5a87fc7d 100644 --- a/tests/integration/mod.rs +++ b/tests/integration/mod.rs @@ -20,5 +20,6 @@ */ mod common; +mod logic; mod queries; mod runtimes; diff --git a/tests/integration/queries.rs b/tests/integration/queries.rs index 5f5354f3..0966a270 100644 --- a/tests/integration/queries.rs +++ b/tests/integration/queries.rs @@ -34,41 +34,7 @@ use typedb_client::{ }; use super::common; - -macro_rules! test_for_each_arg { - { - $perm_args:tt - $( $( #[ $extra_anno:meta ] )* $async:ident fn $test:ident $args:tt -> $ret:ty $test_impl:block )+ - } => { - test_for_each_arg!{ @impl $( $async fn $test $args $ret $test_impl )+ } - test_for_each_arg!{ @impl_per $perm_args { $( $( #[ $extra_anno ] )* $async fn $test )+ } } - }; - - { @impl $( $async:ident fn $test:ident $args:tt $ret:ty $test_impl:block )+ } => { - mod _impl { - use super::*; - $( pub $async fn $test $args -> $ret $test_impl )+ - } - }; - - { @impl_per { $($mod:ident => $arg:expr),+ $(,)? } $fns:tt } => { - $(test_for_each_arg!{ @impl_mod { $mod => $arg } $fns })+ - }; - - { @impl_mod { $mod:ident => $arg:expr } { $( $( #[ $extra_anno:meta ] )* async fn $test:ident )+ } } => { - mod $mod { - use super::*; - $( - #[tokio::test] - #[serial($mod)] - $( #[ $extra_anno ] )* - pub async fn $test() { - _impl::$test($arg).await.unwrap(); - } - )+ - } - }; -} +use crate::test_for_each_arg; test_for_each_arg! { {