Handle :attribute/_reverse in transactor. Fixes #187. r=rnewman
This commit is contained in:
commit
5c5818069f
10 changed files with 539 additions and 179 deletions
160
db/src/db.rs
160
db/src/db.rs
|
@ -2085,6 +2085,164 @@ mod tests {
|
|||
|
||||
// Verify that we can explode map notation with nested maps, even if the inner map would be
|
||||
// dangling, if we give a :db/id explicitly.
|
||||
assert_transact!(conn, "[{:test/dangling {:db/id \"t\" :test/many 11}}]");
|
||||
assert_transact!(conn, "[{:test/dangling {:db/id \"t\" :test/many 12}}]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_explode_reversed_notation() {
|
||||
let mut conn = TestConn::default();
|
||||
|
||||
// Start by installing a few attributes.
|
||||
assert_transact!(conn, "[[:db/add 111 :db/ident :test/many]
|
||||
[:db/add 111 :db/valueType :db.type/long]
|
||||
[:db/add 111 :db/cardinality :db.cardinality/many]
|
||||
[:db/add 222 :db/ident :test/component]
|
||||
[:db/add 222 :db/isComponent true]
|
||||
[:db/add 222 :db/valueType :db.type/ref]
|
||||
[:db/add 333 :db/ident :test/unique]
|
||||
[:db/add 333 :db/unique :db.unique/identity]
|
||||
[:db/add 333 :db/index true]
|
||||
[:db/add 333 :db/valueType :db.type/long]
|
||||
[:db/add 444 :db/ident :test/dangling]
|
||||
[:db/add 444 :db/valueType :db.type/ref]]");
|
||||
|
||||
// Check that we can explode direct reversed notation, entids.
|
||||
let report = assert_transact!(conn, "[[:db/add 100 :test/_dangling 200]]");
|
||||
assert_matches!(conn.last_transaction(),
|
||||
"[[200 :test/dangling 100 ?tx true]
|
||||
[?tx :db/txInstant ?ms ?tx true]]");
|
||||
assert_matches!(tempids(&report),
|
||||
"{}");
|
||||
|
||||
// Check that we can explode direct reversed notation, idents.
|
||||
let report = assert_transact!(conn, "[[:db/add :test/many :test/_dangling :test/unique]]");
|
||||
assert_matches!(conn.last_transaction(),
|
||||
"[[333 :test/dangling :test/many ?tx true]
|
||||
[?tx :db/txInstant ?ms ?tx true]]");
|
||||
assert_matches!(tempids(&report),
|
||||
"{}");
|
||||
|
||||
// Check that we can explode direct reversed notation, tempids.
|
||||
let report = assert_transact!(conn, "[[:db/add \"s\" :test/_dangling \"t\"]]");
|
||||
assert_matches!(conn.last_transaction(),
|
||||
"[[65537 :test/dangling 65536 ?tx true]
|
||||
[?tx :db/txInstant ?ms ?tx true]]");
|
||||
// This is implementation specific, but it should be deterministic.
|
||||
assert_matches!(tempids(&report),
|
||||
"{\"s\" 65536
|
||||
\"t\" 65537}");
|
||||
|
||||
// Check that we can explode reversed notation in map notation without :db/id.
|
||||
let report = assert_transact!(conn, "[{:test/_dangling 501}
|
||||
{:test/_dangling :test/many}
|
||||
{:test/_dangling \"t\"}]");
|
||||
assert_matches!(conn.last_transaction(),
|
||||
"[[111 :test/dangling ?e1 ?tx true]
|
||||
[501 :test/dangling ?e2 ?tx true]
|
||||
[65538 :test/dangling ?e3 ?tx true]
|
||||
[?tx :db/txInstant ?ms ?tx true]]");
|
||||
assert_matches!(tempids(&report),
|
||||
"{\"t\" 65538}");
|
||||
|
||||
// Check that we can explode reversed notation in map notation with :db/id, entid.
|
||||
let report = assert_transact!(conn, "[{:db/id 600 :test/_dangling 601}]");
|
||||
assert_matches!(conn.last_transaction(),
|
||||
"[[601 :test/dangling 600 ?tx true]
|
||||
[?tx :db/txInstant ?ms ?tx true]]");
|
||||
assert_matches!(tempids(&report),
|
||||
"{}");
|
||||
|
||||
// Check that we can explode reversed notation in map notation with :db/id, ident.
|
||||
let report = assert_transact!(conn, "[{:db/id :test/component :test/_dangling :test/component}]");
|
||||
assert_matches!(conn.last_transaction(),
|
||||
"[[222 :test/dangling :test/component ?tx true]
|
||||
[?tx :db/txInstant ?ms ?tx true]]");
|
||||
assert_matches!(tempids(&report),
|
||||
"{}");
|
||||
|
||||
// Check that we can explode reversed notation in map notation with :db/id, tempid.
|
||||
let report = assert_transact!(conn, "[{:db/id \"s\" :test/_dangling \"t\"}]");
|
||||
assert_matches!(conn.last_transaction(),
|
||||
"[[65543 :test/dangling 65542 ?tx true]
|
||||
[?tx :db/txInstant ?ms ?tx true]]");
|
||||
// This is implementation specific, but it should be deterministic.
|
||||
assert_matches!(tempids(&report),
|
||||
"{\"s\" 65542
|
||||
\"t\" 65543}");
|
||||
|
||||
// Check that we can use the same attribute in both forward and backward form in the same
|
||||
// transaction.
|
||||
let report = assert_transact!(conn, "[[:db/add 888 :test/dangling 889]
|
||||
[:db/add 888 :test/_dangling 889]]");
|
||||
assert_matches!(conn.last_transaction(),
|
||||
"[[888 :test/dangling 889 ?tx true]
|
||||
[889 :test/dangling 888 ?tx true]
|
||||
[?tx :db/txInstant ?ms ?tx true]]");
|
||||
assert_matches!(tempids(&report),
|
||||
"{}");
|
||||
|
||||
// Check that we can use the same attribute in both forward and backward form in the same
|
||||
// transaction in map notation.
|
||||
let report = assert_transact!(conn, "[{:db/id 998 :test/dangling 999 :test/_dangling 999}]");
|
||||
assert_matches!(conn.last_transaction(),
|
||||
"[[998 :test/dangling 999 ?tx true]
|
||||
[999 :test/dangling 998 ?tx true]
|
||||
[?tx :db/txInstant ?ms ?tx true]]");
|
||||
assert_matches!(tempids(&report),
|
||||
"{}");
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_explode_reversed_notation_errors() {
|
||||
let mut conn = TestConn::default();
|
||||
|
||||
// Start by installing a few attributes.
|
||||
assert_transact!(conn, "[[:db/add 111 :db/ident :test/many]
|
||||
[:db/add 111 :db/valueType :db.type/long]
|
||||
[:db/add 111 :db/cardinality :db.cardinality/many]
|
||||
[:db/add 222 :db/ident :test/component]
|
||||
[:db/add 222 :db/isComponent true]
|
||||
[:db/add 222 :db/valueType :db.type/ref]
|
||||
[:db/add 333 :db/ident :test/unique]
|
||||
[:db/add 333 :db/unique :db.unique/identity]
|
||||
[:db/add 333 :db/index true]
|
||||
[:db/add 333 :db/valueType :db.type/long]
|
||||
[:db/add 444 :db/ident :test/dangling]
|
||||
[:db/add 444 :db/valueType :db.type/ref]]");
|
||||
|
||||
// `tx-parser` should fail to parse direct reverse notation with nested value maps and
|
||||
// nested value vectors, so we only test things that "get through" to the map notation
|
||||
// dynamic processor here.
|
||||
|
||||
// Verify that we can't explode reverse notation in map notation with nested value maps.
|
||||
assert_transact!(conn,
|
||||
"[{:test/_dangling {:test/many 14}}]",
|
||||
Err("not yet implemented: Cannot explode map notation value in :attr/_reversed notation for attribute 444"));
|
||||
|
||||
// Verify that we can't explode reverse notation in map notation with nested value vectors.
|
||||
assert_transact!(conn,
|
||||
"[{:test/_dangling [:test/many]}]",
|
||||
Err("not yet implemented: Cannot explode vector value in :attr/_reversed notation for attribute 444"));
|
||||
|
||||
// Verify that we can't use reverse notation with non-:db.type/ref attributes.
|
||||
assert_transact!(conn,
|
||||
"[{:test/_unique 500}]",
|
||||
Err("not yet implemented: Cannot use :attr/_reversed notation for attribute 333 that is not :db/valueType :db.type/ref"));
|
||||
|
||||
// Verify that we can't use reverse notation with unrecognized attributes.
|
||||
assert_transact!(conn,
|
||||
"[{:test/_unknown 500}]",
|
||||
Err("no entid found for ident: :test/unknown")); // TODO: make this error reference the original :test/_unknown.
|
||||
|
||||
// Verify that we can't use reverse notation with bad value types: here, an unknown keyword
|
||||
// that can't be coerced to a ref.
|
||||
assert_transact!(conn,
|
||||
"[{:test/_dangling :test/unknown}]",
|
||||
Err("no entid found for ident: :test/unknown"));
|
||||
// And here, a float.
|
||||
assert_transact!(conn,
|
||||
"[{:test/_dangling 1.23}]",
|
||||
Err("EDN value \'1.23\' is not the expected Mentat value type Ref"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,27 @@ pub enum Either<L, R> {
|
|||
Right(R),
|
||||
}
|
||||
|
||||
// Cribbed from https://github.com/bluss/either/blob/f793721f3fdeb694f009e731b23a2858286bc0d6/src/lib.rs#L219-L259.
|
||||
impl<L, R> Either<L, R> {
|
||||
pub fn map_left<F, M>(self, f: F) -> Either<M, R>
|
||||
where F: FnOnce(L) -> M
|
||||
{
|
||||
match self {
|
||||
Left(l) => Left(f(l)),
|
||||
Right(r) => Right(r),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_right<F, S>(self, f: F) -> Either<L, S>
|
||||
where F: FnOnce(R) -> S
|
||||
{
|
||||
match self {
|
||||
Left(l) => Left(l),
|
||||
Right(r) => Right(f(r)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use self::Either::*;
|
||||
|
||||
pub type EntidOr<T> = Either<Entid, T>;
|
||||
|
|
|
@ -223,30 +223,32 @@ impl SchemaBuilding for Schema {
|
|||
pub trait SchemaTypeChecking {
|
||||
/// Do schema-aware typechecking and coercion.
|
||||
///
|
||||
/// Either assert that the given value is in the attribute's value set, or (in limited cases)
|
||||
/// coerce the given value into the attribute's value set.
|
||||
fn to_typed_value(&self, value: &edn::Value, attribute: &Attribute) -> Result<TypedValue>;
|
||||
/// Either assert that the given value is in the value type's value set, or (in limited cases)
|
||||
/// coerce the given value into the value type's value set.
|
||||
fn to_typed_value(&self, value: &edn::Value, value_type: ValueType) -> Result<TypedValue>;
|
||||
}
|
||||
|
||||
impl SchemaTypeChecking for Schema {
|
||||
fn to_typed_value(&self, value: &edn::Value, attribute: &Attribute) -> Result<TypedValue> {
|
||||
// TODO: encapsulate entid-ident-attribute for better error messages.
|
||||
fn to_typed_value(&self, value: &edn::Value, value_type: ValueType) -> Result<TypedValue> {
|
||||
// TODO: encapsulate entid-ident-attribute for better error messages, perhaps by including
|
||||
// the attribute (rather than just the attribute's value type) into this function or a
|
||||
// wrapper function.
|
||||
match TypedValue::from_edn_value(value) {
|
||||
// We don't recognize this EDN at all. Get out!
|
||||
None => bail!(ErrorKind::BadEDNValuePair(value.clone(), attribute.value_type.clone())),
|
||||
Some(typed_value) => match (&attribute.value_type, typed_value) {
|
||||
None => bail!(ErrorKind::BadEDNValuePair(value.clone(), value_type)),
|
||||
Some(typed_value) => match (value_type, typed_value) {
|
||||
// Most types don't coerce at all.
|
||||
(&ValueType::Boolean, tv @ TypedValue::Boolean(_)) => Ok(tv),
|
||||
(&ValueType::Long, tv @ TypedValue::Long(_)) => Ok(tv),
|
||||
(&ValueType::Double, tv @ TypedValue::Double(_)) => Ok(tv),
|
||||
(&ValueType::String, tv @ TypedValue::String(_)) => Ok(tv),
|
||||
(&ValueType::Uuid, tv @ TypedValue::Uuid(_)) => Ok(tv),
|
||||
(&ValueType::Keyword, tv @ TypedValue::Keyword(_)) => Ok(tv),
|
||||
(ValueType::Boolean, tv @ TypedValue::Boolean(_)) => Ok(tv),
|
||||
(ValueType::Long, tv @ TypedValue::Long(_)) => Ok(tv),
|
||||
(ValueType::Double, tv @ TypedValue::Double(_)) => Ok(tv),
|
||||
(ValueType::String, tv @ TypedValue::String(_)) => Ok(tv),
|
||||
(ValueType::Uuid, tv @ TypedValue::Uuid(_)) => Ok(tv),
|
||||
(ValueType::Keyword, tv @ TypedValue::Keyword(_)) => Ok(tv),
|
||||
// Ref coerces a little: we interpret some things depending on the schema as a Ref.
|
||||
(&ValueType::Ref, TypedValue::Long(x)) => Ok(TypedValue::Ref(x)),
|
||||
(&ValueType::Ref, TypedValue::Keyword(ref x)) => self.require_entid(&x).map(|entid| TypedValue::Ref(entid)),
|
||||
(ValueType::Ref, TypedValue::Long(x)) => Ok(TypedValue::Ref(x)),
|
||||
(ValueType::Ref, TypedValue::Keyword(ref x)) => self.require_entid(&x).map(|entid| TypedValue::Ref(entid)),
|
||||
// Otherwise, we have a type mismatch.
|
||||
(value_type, _) => bail!(ErrorKind::BadEDNValuePair(value.clone(), value_type.clone())),
|
||||
(value_type, _) => bail!(ErrorKind::BadEDNValuePair(value.clone(), value_type)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
375
db/src/tx.rs
375
db/src/tx.rs
|
@ -51,6 +51,7 @@ use std::collections::{
|
|||
BTreeSet,
|
||||
VecDeque,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
use db;
|
||||
use db::{
|
||||
|
@ -61,14 +62,16 @@ use entids;
|
|||
use errors::{ErrorKind, Result};
|
||||
use internal_types::{
|
||||
Either,
|
||||
EntidOr,
|
||||
LookupRef,
|
||||
LookupRefOrTempId,
|
||||
TempIdHandle,
|
||||
TempIdMap,
|
||||
Term,
|
||||
TermWithTempIdsAndLookupRefs,
|
||||
TermWithTempIds,
|
||||
TermWithTempIdsAndLookupRefs,
|
||||
TermWithoutTempIds,
|
||||
TypedValueOr,
|
||||
replace_lookup_ref};
|
||||
|
||||
use mentat_core::{
|
||||
|
@ -191,23 +194,125 @@ impl<'conn, 'a> Tx<'conn, 'a> {
|
|||
/// The `Term` instances produce share interned TempId and LookupRef handles, and we return the
|
||||
/// interned handle sets so that consumers can ensure all handles are used appropriately.
|
||||
fn entities_into_terms_with_temp_ids_and_lookup_refs<I>(&self, entities: I) -> Result<(Vec<TermWithTempIdsAndLookupRefs>, intern_set::InternSet<TempId>, intern_set::InternSet<AVPair>)> where I: IntoIterator<Item=Entity> {
|
||||
let mut temp_ids: intern_set::InternSet<TempId> = intern_set::InternSet::new();
|
||||
let mut lookup_refs: intern_set::InternSet<AVPair> = intern_set::InternSet::new();
|
||||
struct InProcess<'a> {
|
||||
schema: &'a Schema,
|
||||
mentat_id_count: i64,
|
||||
temp_ids: intern_set::InternSet<TempId>,
|
||||
lookup_refs: intern_set::InternSet<AVPair>,
|
||||
}
|
||||
|
||||
let intern_lookup_ref = |lookup_refs: &mut intern_set::InternSet<AVPair>, lookup_ref: entmod::LookupRef| -> Result<LookupRef> {
|
||||
let lr_a: i64 = match lookup_ref.a {
|
||||
entmod::Entid::Entid(ref a) => *a,
|
||||
entmod::Entid::Ident(ref a) => self.schema.require_entid(&a)?,
|
||||
};
|
||||
let lr_attribute: &Attribute = self.schema.require_attribute_for_entid(lr_a)?;
|
||||
|
||||
if lr_attribute.unique.is_none() {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot resolve (lookup-ref {} {}) with attribute that is not :db/unique", lr_a, lookup_ref.v)))
|
||||
impl<'a> InProcess<'a> {
|
||||
fn with_schema(schema: &'a Schema) -> InProcess<'a> {
|
||||
InProcess {
|
||||
schema: schema,
|
||||
mentat_id_count: 0,
|
||||
temp_ids: intern_set::InternSet::new(),
|
||||
lookup_refs: intern_set::InternSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
let lr_typed_value: TypedValue = self.schema.to_typed_value(&lookup_ref.v, &lr_attribute)?;
|
||||
Ok(lookup_refs.intern((lr_a, lr_typed_value)))
|
||||
};
|
||||
fn intern_lookup_ref(&mut self, lookup_ref: &entmod::LookupRef) -> Result<LookupRef> {
|
||||
let lr_a: i64 = match lookup_ref.a {
|
||||
entmod::Entid::Entid(ref a) => *a,
|
||||
entmod::Entid::Ident(ref a) => self.schema.require_entid(&a)?,
|
||||
};
|
||||
let lr_attribute: &Attribute = self.schema.require_attribute_for_entid(lr_a)?;
|
||||
|
||||
if lr_attribute.unique.is_none() {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot resolve (lookup-ref {} {}) with attribute that is not :db/unique", lr_a, lookup_ref.v)))
|
||||
}
|
||||
|
||||
let lr_typed_value: TypedValue = self.schema.to_typed_value(&lookup_ref.v, lr_attribute.value_type)?;
|
||||
Ok(self.lookup_refs.intern((lr_a, lr_typed_value)))
|
||||
}
|
||||
|
||||
fn intern_temp_id(&mut self, temp_id: TempId) -> Rc<TempId> {
|
||||
self.temp_ids.intern(temp_id)
|
||||
}
|
||||
|
||||
/// Allocate private internal tempids reserved for Mentat. Internal tempids just need to be
|
||||
/// unique within one transaction; they should never escape a transaction.
|
||||
fn allocate_mentat_id(&mut self) -> entmod::EntidOrLookupRefOrTempId {
|
||||
self.mentat_id_count += 1;
|
||||
entmod::EntidOrLookupRefOrTempId::TempId(TempId::Internal(self.mentat_id_count))
|
||||
}
|
||||
|
||||
fn entity_e_into_term_e(&mut self, x: entmod::EntidOrLookupRefOrTempId) -> Result<EntidOr<LookupRefOrTempId>> {
|
||||
match x {
|
||||
entmod::EntidOrLookupRefOrTempId::Entid(e) => {
|
||||
let e: i64 = match e {
|
||||
entmod::Entid::Entid(ref e) => *e,
|
||||
entmod::Entid::Ident(ref e) => self.schema.require_entid(&e)?,
|
||||
};
|
||||
Ok(Either::Left(e))
|
||||
},
|
||||
|
||||
entmod::EntidOrLookupRefOrTempId::TempId(e) => {
|
||||
Ok(Either::Right(LookupRefOrTempId::TempId(self.intern_temp_id(e))))
|
||||
},
|
||||
|
||||
entmod::EntidOrLookupRefOrTempId::LookupRef(ref lookup_ref) => {
|
||||
Ok(Either::Right(LookupRefOrTempId::LookupRef(self.intern_lookup_ref(lookup_ref)?)))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn entity_a_into_term_a(&mut self, x: entmod::Entid) -> Result<Entid> {
|
||||
let a = match x {
|
||||
entmod::Entid::Entid(ref a) => *a,
|
||||
entmod::Entid::Ident(ref a) => self.schema.require_entid(&a)?,
|
||||
};
|
||||
Ok(a)
|
||||
}
|
||||
|
||||
fn entity_e_into_term_v(&mut self, x: entmod::EntidOrLookupRefOrTempId) -> Result<TypedValueOr<LookupRefOrTempId>> {
|
||||
self.entity_e_into_term_e(x).map(|r| r.map_left(TypedValue::Ref))
|
||||
}
|
||||
|
||||
fn entity_v_into_term_e(&mut self, x: entmod::AtomOrLookupRefOrVectorOrMapNotation, backward_a: &entmod::Entid) -> Result<EntidOr<LookupRefOrTempId>> {
|
||||
match backward_a.unreversed() {
|
||||
None => {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot explode map notation value in :attr/_reversed notation for forward attribute")));
|
||||
},
|
||||
Some(forward_a) => {
|
||||
let forward_a = self.entity_a_into_term_a(forward_a)?;
|
||||
let forward_attribute = self.schema.require_attribute_for_entid(forward_a)?;
|
||||
if forward_attribute.value_type != ValueType::Ref {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot use :attr/_reversed notation for attribute {} that is not :db/valueType :db.type/ref", forward_a)))
|
||||
}
|
||||
|
||||
match x {
|
||||
entmod::AtomOrLookupRefOrVectorOrMapNotation::Atom(ref v) => {
|
||||
// Here is where we do schema-aware typechecking: we either assert
|
||||
// that the given value is in the attribute's value set, or (in
|
||||
// limited cases) coerce the value into the attribute's value set.
|
||||
if let Some(text) = v.inner.as_text() {
|
||||
Ok(Either::Right(LookupRefOrTempId::TempId(self.intern_temp_id(TempId::External(text.clone())))))
|
||||
} else {
|
||||
if let TypedValue::Ref(entid) = self.schema.to_typed_value(&v.clone().without_spans(), ValueType::Ref)? {
|
||||
Ok(Either::Left(entid))
|
||||
} else {
|
||||
// The given value is expected to be :db.type/ref, so this shouldn't happen.
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot use :attr/_reversed notation for attribute {} with value that is not :db.valueType :db.type/ref", forward_a)))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
entmod::AtomOrLookupRefOrVectorOrMapNotation::LookupRef(ref lookup_ref) =>
|
||||
Ok(Either::Right(LookupRefOrTempId::LookupRef(self.intern_lookup_ref(lookup_ref)?))),
|
||||
|
||||
entmod::AtomOrLookupRefOrVectorOrMapNotation::Vector(_) =>
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot explode vector value in :attr/_reversed notation for attribute {}", forward_a))),
|
||||
|
||||
entmod::AtomOrLookupRefOrVectorOrMapNotation::MapNotation(_) =>
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot explode map notation value in :attr/_reversed notation for attribute {}", forward_a))),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut in_process = InProcess::with_schema(&self.schema);
|
||||
|
||||
// We want to handle entities in the order they're given to us, while also "exploding" some
|
||||
// entities into many. We therefore push the initial entities onto the back of the deque,
|
||||
|
@ -215,14 +320,6 @@ impl<'conn, 'a> Tx<'conn, 'a> {
|
|||
let mut deque: VecDeque<Entity> = VecDeque::default();
|
||||
deque.extend(entities);
|
||||
|
||||
// Allocate private internal tempids reserved for Mentat. Internal tempids just need to be
|
||||
// unique within one transaction; they should never escape a transaction.
|
||||
let mut mentat_id_count = 0;
|
||||
let mut allocate_mentat_id = move || {
|
||||
mentat_id_count += 1;
|
||||
entmod::EntidOrLookupRefOrTempId::TempId(TempId::Internal(mentat_id_count))
|
||||
};
|
||||
|
||||
let mut terms: Vec<TermWithTempIdsAndLookupRefs> = Vec::with_capacity(deque.len());
|
||||
|
||||
while let Some(entity) = deque.pop_front() {
|
||||
|
@ -230,7 +327,7 @@ impl<'conn, 'a> Tx<'conn, 'a> {
|
|||
Entity::MapNotation(mut map_notation) => {
|
||||
// :db/id is optional; if it's not given, we generate a special internal tempid
|
||||
// to use for upserting. This tempid will not be reported in the TxReport.
|
||||
let db_id: entmod::EntidOrLookupRefOrTempId = mentat_tx_parser::remove_db_id(&mut map_notation)?.unwrap_or_else(&mut allocate_mentat_id);
|
||||
let db_id: entmod::EntidOrLookupRefOrTempId = mentat_tx_parser::remove_db_id(&mut map_notation)?.unwrap_or_else(|| in_process.allocate_mentat_id());
|
||||
|
||||
// We're not nested, so :db/isComponent is not relevant. We just explode the
|
||||
// map notation.
|
||||
|
@ -245,157 +342,135 @@ impl<'conn, 'a> Tx<'conn, 'a> {
|
|||
},
|
||||
|
||||
Entity::AddOrRetract { op, e, a, v } => {
|
||||
let a: i64 = match a {
|
||||
entmod::Entid::Entid(ref a) => *a,
|
||||
entmod::Entid::Ident(ref a) => self.schema.require_entid(&a)?,
|
||||
};
|
||||
if let Some(reversed_a) = a.unreversed() {
|
||||
let reversed_e = in_process.entity_v_into_term_e(v, &a)?;
|
||||
let reversed_a = in_process.entity_a_into_term_a(reversed_a)?;
|
||||
let reversed_v = in_process.entity_e_into_term_v(e)?;
|
||||
terms.push(Term::AddOrRetract(OpType::Add, reversed_e, reversed_a, reversed_v));
|
||||
} else {
|
||||
let a = in_process.entity_a_into_term_a(a)?;
|
||||
let attribute = self.schema.require_attribute_for_entid(a)?;
|
||||
|
||||
let attribute: &Attribute = self.schema.require_attribute_for_entid(a)?;
|
||||
let v = match v {
|
||||
entmod::AtomOrLookupRefOrVectorOrMapNotation::Atom(v) => {
|
||||
if attribute.value_type == ValueType::Ref && v.inner.is_text() {
|
||||
Either::Right(LookupRefOrTempId::TempId(in_process.intern_temp_id(v.inner.as_text().cloned().map(TempId::External).unwrap())))
|
||||
} else {
|
||||
// Here is where we do schema-aware typechecking: we either assert that
|
||||
// the given value is in the attribute's value set, or (in limited
|
||||
// cases) coerce the value into the attribute's value set.
|
||||
let typed_value: TypedValue = self.schema.to_typed_value(&v.without_spans(), attribute.value_type)?;
|
||||
Either::Left(typed_value)
|
||||
}
|
||||
},
|
||||
|
||||
let v = match v {
|
||||
entmod::AtomOrLookupRefOrVectorOrMapNotation::Atom(v) => {
|
||||
if attribute.value_type == ValueType::Ref && v.inner.is_text() {
|
||||
Either::Right(LookupRefOrTempId::TempId(temp_ids.intern(v.inner.as_text().cloned().map(TempId::External).unwrap())))
|
||||
} else {
|
||||
// Here is where we do schema-aware typechecking: we either assert that
|
||||
// the given value is in the attribute's value set, or (in limited
|
||||
// cases) coerce the value into the attribute's value set.
|
||||
let typed_value: TypedValue = self.schema.to_typed_value(&v.without_spans(), &attribute)?;
|
||||
Either::Left(typed_value)
|
||||
}
|
||||
},
|
||||
entmod::AtomOrLookupRefOrVectorOrMapNotation::LookupRef(ref lookup_ref) => {
|
||||
if attribute.value_type != ValueType::Ref {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot resolve value lookup ref for attribute {} that is not :db/valueType :db.type/ref", a)))
|
||||
}
|
||||
|
||||
entmod::AtomOrLookupRefOrVectorOrMapNotation::LookupRef(lookup_ref) => {
|
||||
if attribute.value_type != ValueType::Ref {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot resolve value lookup ref for attribute {} that is not :db/valueType :db.type/ref", a)))
|
||||
}
|
||||
Either::Right(LookupRefOrTempId::LookupRef(in_process.intern_lookup_ref(lookup_ref)?))
|
||||
},
|
||||
|
||||
Either::Right(LookupRefOrTempId::LookupRef(intern_lookup_ref(&mut lookup_refs, lookup_ref)?))
|
||||
},
|
||||
entmod::AtomOrLookupRefOrVectorOrMapNotation::Vector(vs) => {
|
||||
if !attribute.multival {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot explode vector value for attribute {} that is not :db.cardinality :db.cardinality/many", a)));
|
||||
}
|
||||
|
||||
entmod::AtomOrLookupRefOrVectorOrMapNotation::Vector(vs) => {
|
||||
if !attribute.multival {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot explode vector value for attribute {} that is not :db.cardinality :db.cardinality/many", a)));
|
||||
}
|
||||
for vv in vs {
|
||||
deque.push_front(Entity::AddOrRetract {
|
||||
op: op.clone(),
|
||||
e: e.clone(),
|
||||
a: entmod::Entid::Entid(a),
|
||||
v: vv,
|
||||
});
|
||||
}
|
||||
continue
|
||||
},
|
||||
|
||||
for vv in vs {
|
||||
deque.push_front(Entity::AddOrRetract {
|
||||
op: op.clone(),
|
||||
e: e.clone(),
|
||||
a: entmod::Entid::Entid(a),
|
||||
v: vv,
|
||||
});
|
||||
}
|
||||
continue
|
||||
},
|
||||
entmod::AtomOrLookupRefOrVectorOrMapNotation::MapNotation(mut map_notation) => {
|
||||
// TODO: consider handling this at the tx-parser level. That would be
|
||||
// more strict and expressive, but it would lead to splitting
|
||||
// AddOrRetract, which proliferates types and code, or only handling
|
||||
// nested maps rather than map values, like Datomic does.
|
||||
if op != OpType::Add {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot explode nested map value in :db/retract for attribute {}", a)));
|
||||
}
|
||||
|
||||
entmod::AtomOrLookupRefOrVectorOrMapNotation::MapNotation(mut map_notation) => {
|
||||
// TODO: consider handling this at the tx-parser level. That would be
|
||||
// more strict and expressive, but it would lead to splitting
|
||||
// AddOrRetract, which proliferates types and code, or only handling
|
||||
// nested maps rather than map values, like Datomic does.
|
||||
if op != OpType::Add {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot explode nested map value in :db/retract for attribute {}", a)));
|
||||
}
|
||||
if attribute.value_type != ValueType::Ref {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot explode nested map value for attribute {} that is not :db/valueType :db.type/ref", a)))
|
||||
}
|
||||
|
||||
if attribute.value_type != ValueType::Ref {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot explode nested map value for attribute {} that is not :db/valueType :db.type/ref", a)))
|
||||
}
|
||||
// :db/id is optional; if it's not given, we generate a special internal tempid
|
||||
// to use for upserting. This tempid will not be reported in the TxReport.
|
||||
let db_id: Option<entmod::EntidOrLookupRefOrTempId> = mentat_tx_parser::remove_db_id(&mut map_notation)?;
|
||||
let mut dangling = db_id.is_none();
|
||||
let db_id: entmod::EntidOrLookupRefOrTempId = db_id.unwrap_or_else(|| in_process.allocate_mentat_id());
|
||||
|
||||
// :db/id is optional; if it's not given, we generate a special internal tempid
|
||||
// to use for upserting. This tempid will not be reported in the TxReport.
|
||||
let db_id: Option<entmod::EntidOrLookupRefOrTempId> = mentat_tx_parser::remove_db_id(&mut map_notation)?;
|
||||
let mut dangling = db_id.is_none();
|
||||
let db_id: entmod::EntidOrLookupRefOrTempId = db_id.unwrap_or_else(&mut allocate_mentat_id);
|
||||
|
||||
// We're nested, so we want to ensure we're not creating "dangling"
|
||||
// entities that can't be reached. If we're :db/isComponent, then this
|
||||
// is not dangling. Otherwise, the resulting map needs to have a
|
||||
// :db/unique :db.unique/identity [a v] pair, so that it's reachable.
|
||||
// Per http://docs.datomic.com/transactions.html: "Either the reference
|
||||
// to the nested map must be a component attribute, or the nested map
|
||||
// must include a unique attribute. This constraint prevents the
|
||||
// accidental creation of easily-orphaned entities that have no identity
|
||||
// or relation to other entities."
|
||||
if attribute.component {
|
||||
dangling = false;
|
||||
}
|
||||
|
||||
for (inner_a, inner_v) in map_notation {
|
||||
let inner_entid: i64 = match inner_a {
|
||||
entmod::Entid::Entid(ref a) => *a,
|
||||
entmod::Entid::Ident(ref a) => self.schema.require_entid(&a)?,
|
||||
};
|
||||
|
||||
let inner_attribute: &Attribute = self.schema.require_attribute_for_entid(inner_entid)?;
|
||||
if inner_attribute.unique == Some(attribute::Unique::Identity) {
|
||||
// We're nested, so we want to ensure we're not creating "dangling"
|
||||
// entities that can't be reached. If we're :db/isComponent, then this
|
||||
// is not dangling. Otherwise, the resulting map needs to have a
|
||||
// :db/unique :db.unique/identity [a v] pair, so that it's reachable.
|
||||
// Per http://docs.datomic.com/transactions.html: "Either the reference
|
||||
// to the nested map must be a component attribute, or the nested map
|
||||
// must include a unique attribute. This constraint prevents the
|
||||
// accidental creation of easily-orphaned entities that have no identity
|
||||
// or relation to other entities."
|
||||
if attribute.component {
|
||||
dangling = false;
|
||||
}
|
||||
|
||||
deque.push_front(Entity::AddOrRetract {
|
||||
op: OpType::Add,
|
||||
e: db_id.clone(),
|
||||
a: entmod::Entid::Entid(inner_entid),
|
||||
v: inner_v,
|
||||
});
|
||||
}
|
||||
for (inner_a, inner_v) in map_notation {
|
||||
if let Some(reversed_a) = inner_a.unreversed() {
|
||||
// We definitely have a reference. The reference might be
|
||||
// dangling (a bare entid, for example), but we don't yet
|
||||
// support nested maps and reverse notation simultaneously
|
||||
// (i.e., we don't accept {:reverse/_attribute {:nested map}})
|
||||
// so we don't need to check that the nested map reference isn't
|
||||
// dangling.
|
||||
dangling = false;
|
||||
|
||||
if dangling {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot explode nested map value that would lead to dangling entity for attribute {}", a)));
|
||||
}
|
||||
let reversed_e = in_process.entity_v_into_term_e(inner_v, &inner_a)?;
|
||||
let reversed_a = in_process.entity_a_into_term_a(reversed_a)?;
|
||||
let reversed_v = in_process.entity_e_into_term_v(db_id.clone())?;
|
||||
terms.push(Term::AddOrRetract(OpType::Add, reversed_e, reversed_a, reversed_v));
|
||||
} else {
|
||||
let inner_a = in_process.entity_a_into_term_a(inner_a)?;
|
||||
let inner_attribute = self.schema.require_attribute_for_entid(inner_a)?;
|
||||
if inner_attribute.unique == Some(attribute::Unique::Identity) {
|
||||
dangling = false;
|
||||
}
|
||||
|
||||
// Similar, but not identical, to the expansion of the entity position e
|
||||
// below. This returns Either::Left(TypedValue) instances; that returns
|
||||
// Either::Left(i64) instances.
|
||||
match db_id {
|
||||
entmod::EntidOrLookupRefOrTempId::Entid(e) => {
|
||||
let e: i64 = match e {
|
||||
entmod::Entid::Entid(ref e) => *e,
|
||||
entmod::Entid::Ident(ref e) => self.schema.require_entid(&e)?,
|
||||
};
|
||||
Either::Left(TypedValue::Ref(e))
|
||||
},
|
||||
deque.push_front(Entity::AddOrRetract {
|
||||
op: OpType::Add,
|
||||
e: db_id.clone(),
|
||||
a: entmod::Entid::Entid(inner_a),
|
||||
v: inner_v,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
entmod::EntidOrLookupRefOrTempId::TempId(e) => {
|
||||
Either::Right(LookupRefOrTempId::TempId(temp_ids.intern(e)))
|
||||
},
|
||||
if dangling {
|
||||
bail!(ErrorKind::NotYetImplemented(format!("Cannot explode nested map value that would lead to dangling entity for attribute {}", a)));
|
||||
}
|
||||
|
||||
entmod::EntidOrLookupRefOrTempId::LookupRef(lookup_ref) => {
|
||||
Either::Right(LookupRefOrTempId::LookupRef(intern_lookup_ref(&mut lookup_refs, lookup_ref)?))
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
in_process.entity_e_into_term_v(db_id)?
|
||||
},
|
||||
};
|
||||
|
||||
let e = match e {
|
||||
entmod::EntidOrLookupRefOrTempId::Entid(e) => {
|
||||
let e: i64 = match e {
|
||||
entmod::Entid::Entid(ref e) => *e,
|
||||
entmod::Entid::Ident(ref e) => self.schema.require_entid(&e)?,
|
||||
};
|
||||
Either::Left(e)
|
||||
},
|
||||
|
||||
entmod::EntidOrLookupRefOrTempId::TempId(e) => {
|
||||
Either::Right(LookupRefOrTempId::TempId(temp_ids.intern(e)))
|
||||
},
|
||||
|
||||
entmod::EntidOrLookupRefOrTempId::LookupRef(lookup_ref) => {
|
||||
Either::Right(LookupRefOrTempId::LookupRef(intern_lookup_ref(&mut lookup_refs, lookup_ref)?))
|
||||
},
|
||||
};
|
||||
|
||||
terms.push(Term::AddOrRetract(op, e, a, v));
|
||||
let e = in_process.entity_e_into_term_e(e)?;
|
||||
terms.push(Term::AddOrRetract(op, e, a, v));
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
Ok((terms, temp_ids, lookup_refs))
|
||||
Ok((terms, in_process.temp_ids, in_process.lookup_refs))
|
||||
}
|
||||
|
||||
/// Pipeline stage 2: rewrite `Term` instances with lookup refs into `Term` instances without
|
||||
/// lookup refs.
|
||||
///
|
||||
/// The `Term` instances produce share interned TempId handles and have no LookupRef references.
|
||||
/// The `Term` instances produced share interned TempId handles and have no LookupRef references.
|
||||
fn resolve_lookup_refs<I>(&self, lookup_ref_map: &AVMap, terms: I) -> Result<Vec<TermWithTempIds>> where I: IntoIterator<Item=TermWithTempIdsAndLookupRefs> {
|
||||
terms.into_iter().map(|term: TermWithTempIdsAndLookupRefs| -> Result<TermWithTempIds> {
|
||||
match term {
|
||||
|
|
|
@ -214,6 +214,30 @@ impl NamespacedKeyword {
|
|||
namespace: self.namespace.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// If this `NamespacedKeyword` is 'backward' (see `symbols::NamespacedKeyword::is_backward`),
|
||||
/// return `Some('forward name')`; otherwise, return `None`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use edn::symbols::NamespacedKeyword;
|
||||
/// let nsk = NamespacedKeyword::new("foo", "bar");
|
||||
/// assert_eq!(None, nsk.unreversed());
|
||||
///
|
||||
/// let reversed = nsk.to_reversed();
|
||||
/// assert_eq!(Some(nsk), reversed.unreversed());
|
||||
/// ```
|
||||
pub fn unreversed(&self) -> Option<NamespacedKeyword> {
|
||||
if self.is_backward() {
|
||||
Some(NamespacedKeyword {
|
||||
name: self.name[1..].to_string(),
|
||||
namespace: self.namespace.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
@ -594,7 +594,6 @@ mod test {
|
|||
|
||||
use chrono::{
|
||||
DateTime,
|
||||
TimeZone,
|
||||
UTC,
|
||||
};
|
||||
use num::BigInt;
|
||||
|
|
|
@ -452,6 +452,26 @@ pub fn namespaced_keyword<'a>() -> Expected<FnParser<Stream<'a>, fn(Stream<'a>)
|
|||
parser(namespaced_keyword_ as fn(Stream<'a>) -> ParseResult<&'a edn::NamespacedKeyword, Stream<'a>>).expected("namespaced_keyword")
|
||||
}
|
||||
|
||||
pub fn forward_keyword_<'a>(input: Stream<'a>) -> ParseResult<&'a edn::NamespacedKeyword, Stream<'a>> {
|
||||
satisfy_map(|v: &'a edn::ValueAndSpan| v.inner.as_namespaced_keyword().and_then(|k| if k.is_forward() { Some(k) } else { None }))
|
||||
.parse_lazy(input)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn forward_keyword<'a>() -> Expected<FnParser<Stream<'a>, fn(Stream<'a>) -> ParseResult<&'a edn::NamespacedKeyword, Stream<'a>>>> {
|
||||
parser(forward_keyword_ as fn(Stream<'a>) -> ParseResult<&'a edn::NamespacedKeyword, Stream<'a>>).expected("forward_keyword")
|
||||
}
|
||||
|
||||
pub fn backward_keyword_<'a>(input: Stream<'a>) -> ParseResult<&'a edn::NamespacedKeyword, Stream<'a>> {
|
||||
satisfy_map(|v: &'a edn::ValueAndSpan| v.inner.as_namespaced_keyword().and_then(|k| if k.is_backward() { Some(k) } else { None }))
|
||||
.parse_lazy(input)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn backward_keyword<'a>() -> Expected<FnParser<Stream<'a>, fn(Stream<'a>) -> ParseResult<&'a edn::NamespacedKeyword, Stream<'a>>>> {
|
||||
parser(backward_keyword_ as fn(Stream<'a>) -> ParseResult<&'a edn::NamespacedKeyword, Stream<'a>>).expected("backward_keyword")
|
||||
}
|
||||
|
||||
/// Generate a `satisfy` expression that matches a `PlainSymbol` value with the given name.
|
||||
///
|
||||
/// We do this rather than using `combine::token` so that we don't need to allocate a new `String`
|
||||
|
|
|
@ -45,6 +45,8 @@ use mentat_parser_utils::{ResultParser};
|
|||
use mentat_parser_utils::value_and_span::{
|
||||
Item,
|
||||
OfExactlyParsing,
|
||||
backward_keyword,
|
||||
forward_keyword,
|
||||
integer,
|
||||
list,
|
||||
map,
|
||||
|
@ -57,12 +59,25 @@ pub use errors::*;
|
|||
|
||||
pub struct Tx<'a>(std::marker::PhantomData<&'a ()>);
|
||||
|
||||
// Accepts entid, :attribute/forward, and :attribute/_backward.
|
||||
def_parser!(Tx, entid, Entid, {
|
||||
integer()
|
||||
.map(|x| Entid::Entid(x))
|
||||
.or(namespaced_keyword().map(|x| Entid::Ident(x.clone())))
|
||||
});
|
||||
|
||||
// Accepts entid and :attribute/forward.
|
||||
def_parser!(Tx, forward_entid, Entid, {
|
||||
integer()
|
||||
.map(|x| Entid::Entid(x))
|
||||
.or(forward_keyword().map(|x| Entid::Ident(x.clone())))
|
||||
});
|
||||
|
||||
// Accepts only :attribute/_backward.
|
||||
def_parser!(Tx, backward_entid, Entid, {
|
||||
backward_keyword().map(|x| Entid::Ident(x.to_reversed()))
|
||||
});
|
||||
|
||||
def_matches_plain_symbol!(Tx, literal_lookup_ref, "lookup-ref");
|
||||
|
||||
def_parser!(Tx, lookup_ref, LookupRef, {
|
||||
|
@ -106,18 +121,23 @@ def_matches_namespaced_keyword!(Tx, literal_db_retract, "db", "retract");
|
|||
|
||||
def_parser!(Tx, add_or_retract, Entity, {
|
||||
vector().of_exactly(
|
||||
// TODO: This commits as soon as it sees :db/{add,retract}, but could use an improved error message.
|
||||
(Tx::literal_db_add().map(|_| OpType::Add).or(Tx::literal_db_retract().map(|_| OpType::Retract)),
|
||||
Tx::entid_or_lookup_ref_or_temp_id(),
|
||||
Tx::entid(),
|
||||
Tx::atom_or_lookup_ref_or_vector())
|
||||
.map(|(op, e, a, v)| {
|
||||
Entity::AddOrRetract {
|
||||
op: op,
|
||||
e: e,
|
||||
a: a,
|
||||
v: v,
|
||||
}
|
||||
}))
|
||||
try((Tx::entid_or_lookup_ref_or_temp_id(),
|
||||
Tx::forward_entid(),
|
||||
Tx::atom_or_lookup_ref_or_vector()))
|
||||
.or(try((Tx::atom_or_lookup_ref_or_vector(),
|
||||
Tx::backward_entid(),
|
||||
Tx::entid_or_lookup_ref_or_temp_id()))
|
||||
.map(|(v, a, e)| (e, a, v))))
|
||||
.map(|(op, (e, a, v))| {
|
||||
Entity::AddOrRetract {
|
||||
op: op,
|
||||
e: e,
|
||||
a: a,
|
||||
v: v,
|
||||
}
|
||||
}))
|
||||
});
|
||||
|
||||
def_parser!(Tx, map_notation, MapNotation, {
|
||||
|
|
|
@ -85,4 +85,22 @@ fn test_entities() {
|
|||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reverse_notation_illegal_nested_values() {
|
||||
// Verify that we refuse to parse direct reverse notation with nested value maps or vectors.
|
||||
|
||||
let input = "[[:db/add 100 :test/_dangling {:test/many 13}]]";
|
||||
let edn = parse::value(input).expect("to parse test input");
|
||||
let result = Tx::parse(&edn);
|
||||
// TODO: it would be much better to assert details about the error (here and below), but right
|
||||
// now the error message isn't clear that the given value isn't valid for the backward attribute
|
||||
// :test/_dangling.
|
||||
assert!(result.is_err());
|
||||
|
||||
let input = "[[:db/add 100 :test/_dangling [:test/many 13]]]";
|
||||
let edn = parse::value(input).expect("to parse test input");
|
||||
let result = Tx::parse(&edn);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// TODO: test error handling in select cases.
|
||||
|
|
|
@ -56,6 +56,29 @@ pub enum Entid {
|
|||
Ident(NamespacedKeyword),
|
||||
}
|
||||
|
||||
impl Entid {
|
||||
pub fn is_backward(&self) -> bool {
|
||||
match self {
|
||||
&Entid::Entid(_) => false,
|
||||
&Entid::Ident(ref a) => a.is_backward(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_reversed(&self) -> Option<Entid> {
|
||||
match self {
|
||||
&Entid::Entid(_) => None,
|
||||
&Entid::Ident(ref a) => Some(Entid::Ident(a.to_reversed())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unreversed(&self) -> Option<Entid> {
|
||||
match self {
|
||||
&Entid::Entid(_) => None,
|
||||
&Entid::Ident(ref a) => a.unreversed().map(Entid::Ident),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)]
|
||||
pub struct LookupRef {
|
||||
pub a: Entid,
|
||||
|
|
Loading…
Reference in a new issue