Use rust-peg for tx parsing. r=rnewman

This commit is contained in:
Nick Alexander 2018-05-10 10:32:27 -07:00
commit 9a4bd0de4f
24 changed files with 282 additions and 644 deletions

View file

@ -76,9 +76,6 @@ path = "query-translator"
[dependencies.mentat_tx]
path = "tx"
[dependencies.mentat_tx_parser]
path = "tx-parser"
[dependencies.mentat_tolstoy]
path = "tolstoy"

View file

@ -157,6 +157,8 @@ So what are they?
Our EDN parser. It uses `rust-peg` to parse [EDN](https://github.com/edn-format/edn), which is Clojure/Datomic's richer alternative to JSON. `edn`'s dependencies are all either for representing rich values (`chrono`, `uuid`, `ordered-float`) or for parsing (`serde`, `peg`).
In addition, this crate turns a stream of EDN values into a representation suitable to be transacted.
#### `mentat_core`
This is the lowest-level Mentat crate. It collects together the following things:
@ -185,12 +187,6 @@ Similarly, this crate defines an abstract representation of a SQL query as under
Mentat has two main inputs: reads (queries) and writes (transacts). Just as `mentat_query` defines the types produced by the query parser, `mentat_tx` defines the types produced by the tx parser.
### Transact processing
#### `mentat_tx_parser`
This is a `combine` parser that turns a stream of EDN values into a representation suitable to be transacted.
### Query processing
#### `mentat_query_parser`

View file

@ -29,9 +29,6 @@ path = "../sql"
[dependencies.mentat_tx]
path = "../tx"
[dependencies.mentat_tx_parser]
path = "../tx-parser"
# Should be dev-dependencies.
[dependencies.tabwriter]
version = "1.0.3"

View file

@ -17,7 +17,6 @@ use edn::symbols;
use entids;
use db::TypedSQLValue;
use mentat_tx::entities::Entity;
use mentat_tx_parser;
use mentat_core::{
IdentMap,
Schema,
@ -300,6 +299,6 @@ pub(crate) fn bootstrap_entities() -> Vec<Entity> {
// Failure here is a coding error (since the inputs are fixed), not a runtime error.
// TODO: represent these bootstrap data errors rather than just panicing.
let bootstrap_entities: Vec<Entity> = mentat_tx_parser::Tx::parse(&bootstrap_assertions.with_spans()).unwrap();
let bootstrap_entities: Vec<Entity> = edn::parse::entities(&bootstrap_assertions.to_string()).unwrap();
return bootstrap_entities;
}

View file

@ -1159,7 +1159,6 @@ mod tests {
Schema,
attribute,
};
use mentat_tx_parser;
use rusqlite;
use std::collections::{
BTreeMap,
@ -1217,8 +1216,7 @@ mod tests {
fn transact<I>(&mut self, transaction: I) -> Result<TxReport> where I: Borrow<str> {
// Failure to parse the transaction is a coding error, so we unwrap.
let assertions = edn::parse::value(transaction.borrow()).expect(format!("to be able to parse {} into EDN", transaction.borrow()).as_str());
let entities: Vec<_> = mentat_tx_parser::Tx::parse(&assertions).expect(format!("to be able to parse {} into entities", assertions).as_str());
let entities = edn::parse::entities(transaction.borrow()).expect(format!("to be able to parse {} into entities", transaction.borrow()).as_str());
let details = {
// The block scopes the borrow of self.sqlite.

View file

@ -21,7 +21,6 @@ use rusqlite;
use mentat_tx::entities::{
TempId,
};
use mentat_tx_parser;
use mentat_core::{
KnownEntid,
};
@ -60,6 +59,30 @@ impl ::std::fmt::Display for SchemaConstraintViolation {
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum InputError {
/// Map notation included a bad `:db/id` value.
BadDbId,
/// A value place cannot be interpreted as an entity place (for example, in nested map
/// notation).
BadEntityPlace,
}
impl ::std::fmt::Display for InputError {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
use self::InputError::*;
match self {
&BadDbId => {
writeln!(f, ":db/id in map notation must either not be present or be an entid, an ident, or a tempid")
},
&BadEntityPlace => {
writeln!(f, "cannot convert value place into entity place")
},
}
}
}
error_chain! {
types {
Error, ErrorKind, ResultExt, Result;
@ -69,10 +92,6 @@ error_chain! {
Rusqlite(rusqlite::Error);
}
links {
TxParseError(mentat_tx_parser::Error, mentat_tx_parser::ErrorKind);
}
errors {
/// We're just not done yet. Message that the feature is recognized but not yet
/// implemented.
@ -152,5 +171,12 @@ error_chain! {
description("schema constraint violation")
display("schema constraint violation: {}", violation)
}
/// The transaction was malformed in some way (that was not recognized at parse time; for
/// example, in a way that is schema-dependent).
InputError(error: InputError) {
description("transaction input error")
display("transaction input error: {}", error)
}
}
}

View file

@ -19,19 +19,101 @@ use mentat_core::KnownEntid;
use mentat_core::util::Either;
use edn;
use edn::{
SpannedValue,
ValueAndSpan,
};
use errors;
use errors::ErrorKind;
use errors::{
ErrorKind,
Result,
};
use schema::{
SchemaTypeChecking,
};
use types::{
AVMap,
AVPair,
Entid,
Schema,
TypedValue,
ValueType,
};
use mentat_tx::entities;
use mentat_tx::entities::{
EntidOrLookupRefOrTempId,
OpType,
TempId,
TxFunction,
};
/// The transactor is tied to `edn::ValueAndSpan` right now, but in the future we'd like to support
/// `TypedValue` directly for programmatic use. `TransactableValue` encapsulates the interface
/// value types (i.e., values in the value place) need to support to be transacted.
pub trait TransactableValue {
/// Coerce this value place into the given type. This is where we perform schema-aware
/// coercion, for example coercing an integral value into a ref where appropriate.
fn into_typed_value(self, schema: &Schema, value_type: ValueType) -> Result<TypedValue>;
/// Make an entity place out of this value place. This is where we limit values in nested maps
/// to valid entity places.
fn into_entity_place(self) -> Result<EntidOrLookupRefOrTempId>;
fn as_tempid(&self) -> Option<TempId>;
}
impl TransactableValue for ValueAndSpan {
fn into_typed_value(self, schema: &Schema, value_type: ValueType) -> Result<TypedValue> {
schema.to_typed_value(&self.without_spans(), value_type)
}
fn into_entity_place(self) -> Result<EntidOrLookupRefOrTempId> {
use self::SpannedValue::*;
match self.inner {
Integer(v) => Ok(EntidOrLookupRefOrTempId::Entid(entities::Entid::Entid(v))),
NamespacedKeyword(v) => Ok(EntidOrLookupRefOrTempId::Entid(entities::Entid::Ident(v))),
Text(v) => Ok(EntidOrLookupRefOrTempId::TempId(TempId::External(v))),
List(ls) => {
let mut it = ls.iter();
match (it.next().map(|x| &x.inner), it.next(), it.next(), it.next()) {
// Like "(transaction-id)".
(Some(&PlainSymbol(ref op)), None, None, None) => {
Ok(EntidOrLookupRefOrTempId::TxFunction(TxFunction { op: op.clone() }))
},
// Like "(lookup-ref)".
(Some(&PlainSymbol(edn::PlainSymbol(ref s))), Some(a), Some(v), None) if s == "lookup-ref" => {
match a.clone().into_entity_place()? {
EntidOrLookupRefOrTempId::Entid(a) => Ok(EntidOrLookupRefOrTempId::LookupRef(entities::LookupRef { a, v: v.clone().without_spans() })),
EntidOrLookupRefOrTempId::TempId(_) |
EntidOrLookupRefOrTempId::TxFunction(_) |
EntidOrLookupRefOrTempId::LookupRef(_) => bail!(ErrorKind::InputError(errors::InputError::BadEntityPlace)),
}
},
_ => bail!(ErrorKind::InputError(errors::InputError::BadEntityPlace)),
}
},
Nil |
Boolean(_) |
Instant(_) |
BigInteger(_) |
Float(_) |
Uuid(_) |
PlainSymbol(_) |
NamespacedSymbol(_) |
Keyword(_) |
Vector(_) |
Set(_) |
Map(_) => bail!(ErrorKind::InputError(errors::InputError::BadEntityPlace)),
}
}
fn as_tempid(&self) -> Option<TempId> {
self.inner.as_text().cloned().map(TempId::External)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)]
pub enum Term<E, V> {
AddOrRetract(OpType, E, Entid, V),

View file

@ -26,7 +26,6 @@ extern crate time;
#[macro_use] extern crate mentat_core;
extern crate mentat_sql;
extern crate mentat_tx;
extern crate mentat_tx_parser;
use std::iter::repeat;

View file

@ -82,6 +82,7 @@ use internal_types::{
TermWithTempIds,
TermWithTempIdsAndLookupRefs,
TermWithoutTempIds,
TransactableValue,
TypedValueOr,
replace_lookup_ref,
};
@ -105,7 +106,6 @@ use mentat_tx::entities::{
OpType,
TempId,
};
use mentat_tx_parser;
use metadata;
use rusqlite;
use schema::{
@ -161,6 +161,29 @@ pub struct Tx<'conn, 'a, W> where W: TransactWatcher {
tx_instant: Option<DateTime<Utc>>,
}
/// Remove any :db/id value from the given map notation, converting the returned value into
/// something suitable for the entity position rather than something suitable for a value position.
pub fn remove_db_id(map: &mut entmod::MapNotation) -> Result<Option<entmod::EntidOrLookupRefOrTempId>> {
// TODO: extract lazy defined constant.
let db_id_key = entmod::Entid::Ident(NamespacedKeyword::new("db", "id"));
let db_id: Option<entmod::EntidOrLookupRefOrTempId> = if let Some(id) = map.remove(&db_id_key) {
match id {
entmod::AtomOrLookupRefOrVectorOrMapNotation::Atom(v) => Some(v.into_entity_place()?),
entmod::AtomOrLookupRefOrVectorOrMapNotation::LookupRef(_) |
entmod::AtomOrLookupRefOrVectorOrMapNotation::TxFunction(_) |
entmod::AtomOrLookupRefOrVectorOrMapNotation::Vector(_) |
entmod::AtomOrLookupRefOrVectorOrMapNotation::MapNotation(_) => {
bail!(ErrorKind::InputError(errors::InputError::BadDbId))
},
}
} else {
None
};
Ok(db_id)
}
impl<'conn, 'a, W> Tx<'conn, 'a, W> where W: TransactWatcher {
pub fn new(
store: &'conn rusqlite::Connection,
@ -340,20 +363,21 @@ impl<'conn, 'a, W> Tx<'conn, 'a, W> where W: TransactWatcher {
}
match x {
entmod::AtomOrLookupRefOrVectorOrMapNotation::Atom(ref v) => {
entmod::AtomOrLookupRefOrVectorOrMapNotation::Atom(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)? {
match v.as_tempid() {
Some(tempid) => Ok(Either::Right(LookupRefOrTempId::TempId(self.intern_temp_id(tempid)))),
None => {
if let TypedValue::Ref(entid) = v.into_typed_value(&self.schema, ValueType::Ref)? {
Ok(Either::Left(KnownEntid(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) =>
@ -392,7 +416,7 @@ impl<'conn, 'a, W> Tx<'conn, 'a, W> where W: TransactWatcher {
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(|| in_process.allocate_mentat_id());
let db_id: entmod::EntidOrLookupRefOrTempId = 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.
@ -418,14 +442,16 @@ impl<'conn, 'a, W> Tx<'conn, 'a, W> where W: TransactWatcher {
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())))
// 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 attribute.value_type == ValueType::Ref {
match v.as_tempid() {
Some(tempid) => Either::Right(LookupRefOrTempId::TempId(in_process.intern_temp_id(tempid))),
None => v.into_typed_value(&self.schema, attribute.value_type).map(Either::Left)?,
}
} 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)
v.into_typed_value(&self.schema, attribute.value_type).map(Either::Left)?
}
},
@ -488,7 +514,7 @@ impl<'conn, 'a, W> Tx<'conn, 'a, W> where W: TransactWatcher {
// :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 db_id: Option<entmod::EntidOrLookupRefOrTempId> = 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());

View file

@ -23,6 +23,8 @@ use num::BigInt;
use ordered_float::OrderedFloat;
use uuid::Uuid;
use entities::*;
use symbols::*;
use types::{SpannedValue, Span, ValueAndSpan};
// Goal: Be able to parse https://github.com/edn-format/edn
@ -51,21 +53,25 @@ validbase = [3][0-6] / [12][0-9] / [2-9]
hex = [0-9a-fA-F]
sign = [+-]
pub bigint -> SpannedValue = b:$( sign? digit+ ) "N"
{ SpannedValue::BigInteger(b.parse::<BigInt>().unwrap()) }
pub octalinteger -> SpannedValue = "0" i:$( octaldigit+ )
{ SpannedValue::Integer(i64::from_str_radix(i, 8).unwrap()) }
pub hexinteger -> SpannedValue = "0x" i:$( hex+ )
{ SpannedValue::Integer(i64::from_str_radix(i, 16).unwrap()) }
pub raw_bigint -> BigInt = b:$( sign? digit+ ) "N"
{ b.parse::<BigInt>().unwrap() }
pub raw_octalinteger -> i64 = "0" i:$( octaldigit+ )
{ i64::from_str_radix(i, 8).unwrap() }
pub raw_hexinteger -> i64 = "0x" i:$( hex+ )
{ i64::from_str_radix(i, 16).unwrap() }
pub raw_basedinteger -> i64 = b:$( validbase ) "r" i:$( alphanumeric+ )
{ i64::from_str_radix(i, b.parse::<u32>().unwrap()).unwrap() }
pub raw_integer -> i64 = i:$( sign? digit+ ) !("." / ([eE]))
{ i.parse::<i64>().unwrap() }
pub raw_float -> OrderedFloat<f64> = f:$(sign? digit+ ("." digit+)? ([eE] sign? digit+)?)
{ OrderedFloat(f.parse::<f64>().unwrap()) }
pub basedinteger -> SpannedValue = b:$( validbase ) "r" i:$( alphanumeric+ )
{ SpannedValue::Integer(i64::from_str_radix(i, b.parse::<u32>().unwrap()).unwrap()) }
pub integer -> SpannedValue = i:$( sign? digit+ ) !("." / ([eE]))
{ SpannedValue::Integer(i.parse::<i64>().unwrap()) }
pub float -> SpannedValue = f:$(sign? digit+ ("." digit+)? ([eE] sign? digit+)?)
{ SpannedValue::Float(OrderedFloat(f.parse::<f64>().unwrap())) }
pub bigint -> SpannedValue = v:raw_bigint { SpannedValue::BigInteger(v) }
pub octalinteger -> SpannedValue = v:raw_octalinteger { SpannedValue::Integer(v) }
pub hexinteger -> SpannedValue = v:raw_hexinteger { SpannedValue::Integer(v) }
pub basedinteger -> SpannedValue = v:raw_basedinteger { SpannedValue::Integer(v) }
pub integer -> SpannedValue = v:raw_integer { SpannedValue::Integer(v) }
pub float -> SpannedValue = v:raw_float { SpannedValue::Float(v) }
number -> SpannedValue = ( bigint / basedinteger / hexinteger / octalinteger / integer / float )
@ -81,8 +87,11 @@ string_normal_chars -> &'input str = $([^"\\]+)
// output = [quote, "foo", backslash, "bar", quote]
// result = r#""foo\\bar""#
// For the typical case, string_normal_chars will match multiple, leading to a single-element vec.
pub text -> SpannedValue = "\"" t:((string_special_char / string_normal_chars)*) "\""
{ SpannedValue::Text(t.join(&"").to_string()) }
pub raw_text -> String = "\"" t:((string_special_char / string_normal_chars)*) "\""
{ t.join(&"").to_string() }
pub text -> SpannedValue
= v:raw_text { SpannedValue::Text(v) }
// RFC 3339 timestamps. #inst "1985-04-12T23:20:50.52Z"
// We accept an arbitrary depth of decimals.
@ -180,9 +189,69 @@ pub value -> ValueAndSpan =
}
}
atom -> ValueAndSpan
= v:value {? if v.is_atom() { Ok(v) } else { Err("expected atom") } }
// Clojure (and thus EDN) regards commas as whitespace, and thus the two-element vectors [1 2] and
// [1,,,,2] are equivalent, as are the maps {:a 1, :b 2} and {:a 1 :b 2}.
whitespace = [ \r\n\t,]
comment = ";" [^\r\n]* [\r\n]?
whitespace = #quiet<[ \r\n\t,]>
comment = #quiet<";" [^\r\n]* [\r\n]?>
__ = (whitespace / comment)*
pub op -> OpType
= ":db/add" { OpType::Add }
/ ":db/retract" { OpType::Retract }
raw_keyword -> NamespacedKeyword
= keyword_prefix ns:$(symbol_namespace) namespace_separator n:$(symbol_name) { NamespacedKeyword::new(ns, n) }
raw_forward_keyword -> NamespacedKeyword
= v:raw_keyword {? if v.is_forward() { Ok(v) } else { Err("expected :forward/keyword") } }
raw_backward_keyword -> NamespacedKeyword
= v:raw_keyword {? if v.is_backward() { Ok(v) } else { Err("expected :backward/_keyword") } }
entid -> Entid
= v:( raw_basedinteger / raw_hexinteger / raw_octalinteger / raw_integer ) { Entid::Entid(v) }
/ v:raw_keyword { Entid::Ident(v) }
forward_entid -> Entid
= v:( raw_basedinteger / raw_hexinteger / raw_octalinteger / raw_integer ) { Entid::Entid(v) }
/ v:raw_forward_keyword { Entid::Ident(v) }
backward_entid -> Entid
= v:raw_backward_keyword { Entid::Ident(v.to_reversed()) }
lookup_ref -> LookupRef
= "(" __ "lookup-ref" __ a:(entid) __ v:(value) __ ")" { LookupRef { a, v: v.without_spans() } }
tx_function -> TxFunction
= "(" __ n:$(symbol_name) __ ")" { TxFunction { op: PlainSymbol::new(n) } }
entity_place -> EntidOrLookupRefOrTempId
= v:raw_text { EntidOrLookupRefOrTempId::TempId(TempId::External(v)) }
/ v:entid { EntidOrLookupRefOrTempId::Entid(v) }
/ v:lookup_ref { EntidOrLookupRefOrTempId::LookupRef(v) }
/ v:tx_function { EntidOrLookupRefOrTempId::TxFunction(v) }
value_place_pair -> (Entid, AtomOrLookupRefOrVectorOrMapNotation)
= k:(entid) __ v:(value_place) { (k, v) }
map_notation -> MapNotation
= "{" __ kvs:(value_place_pair*) __ "}" { kvs.into_iter().collect() }
value_place -> AtomOrLookupRefOrVectorOrMapNotation
= __ v:lookup_ref __ { AtomOrLookupRefOrVectorOrMapNotation::LookupRef(v) }
/ __ v:tx_function __ { AtomOrLookupRefOrVectorOrMapNotation::TxFunction(v) }
/ __ "[" __ vs:(value_place*) __ "]" __ { AtomOrLookupRefOrVectorOrMapNotation::Vector(vs) }
/ __ v:map_notation __ { AtomOrLookupRefOrVectorOrMapNotation::MapNotation(v) }
/ __ v:atom __ { AtomOrLookupRefOrVectorOrMapNotation::Atom(v) }
pub entity -> Entity
= __ "[" __ op:(op) __ e:(entity_place) __ a:(forward_entid) __ v:(value_place) __ "]" __ { Entity::AddOrRetract { op, e: e, a, v: v } }
/ __ "[" __ op:(op) __ e:(value_place) __ a:(backward_entid) __ v:(entity_place) __ "]" __ { Entity::AddOrRetract { op, e: v, a, v: e } }
/ __ map:map_notation __ { Entity::MapNotation(map) }
pub entities -> Vec<Entity>
= __ "[" __ es:(entity*) __ "]" __ { es }

View file

@ -10,14 +10,19 @@
//! This module defines core types that support the transaction processor.
extern crate edn;
use std::collections::BTreeMap;
use std::fmt;
use self::edn::symbols::NamespacedKeyword;
use symbols::{
NamespacedKeyword,
PlainSymbol,
};
use types::{
Value,
ValueAndSpan,
};
/// A tempid, either an external tempid given in a transaction (usually as an `edn::Value::Text`),
/// A tempid, either an external tempid given in a transaction (usually as an `Value::Text`),
/// or an internal tempid allocated by Mentat itself.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)]
pub enum TempId {
@ -63,7 +68,7 @@ pub struct LookupRef {
pub a: Entid,
// In theory we could allow nested lookup-refs. In practice this would require us to process
// lookup-refs in multiple phases, like how we resolve tempids, which isn't worth the effort.
pub v: edn::Value, // An atom.
pub v: Value, // An atom.
}
/// A "transaction function" that exposes some value determined by the current transaction. The
@ -80,14 +85,14 @@ pub struct LookupRef {
/// generalization.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)]
pub struct TxFunction {
pub op: edn::PlainSymbol,
pub op: PlainSymbol,
}
pub type MapNotation = BTreeMap<Entid, AtomOrLookupRefOrVectorOrMapNotation>;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)]
pub enum AtomOrLookupRefOrVectorOrMapNotation {
Atom(edn::ValueAndSpan),
Atom(ValueAndSpan),
LookupRef(LookupRef),
TxFunction(TxFunction),
Vector(Vec<AtomOrLookupRefOrVectorOrMapNotation>),

View file

@ -22,6 +22,7 @@ extern crate serde;
#[macro_use]
extern crate serde_derive;
pub mod entities;
pub mod symbols;
pub mod types;
pub mod pretty_print;

View file

@ -110,6 +110,10 @@ impl ValueAndSpan {
}
}
pub fn is_atom(&self) -> bool {
self.inner.is_atom()
}
pub fn as_atom(&self) -> Option<&ValueAndSpan> {
if self.inner.is_atom() {
Some(self)

View file

@ -10,6 +10,10 @@
#![allow(dead_code)]
use std::borrow::{
Borrow,
};
use std::collections::{
BTreeMap,
};
@ -85,8 +89,6 @@ use mentat_tx::entities::{
OpType,
};
use mentat_tx_parser;
use mentat_tolstoy::Syncer;
use uuid::Uuid;
@ -494,9 +496,8 @@ impl<'a, 'c> InProgress<'a, 'c> {
Ok(report)
}
pub fn transact(&mut self, transaction: &str) -> Result<TxReport> {
let assertion_vector = edn::parse::value(transaction)?;
let entities = mentat_tx_parser::Tx::parse(&assertion_vector)?;
pub fn transact<B>(&mut self, transaction: B) -> Result<TxReport> where B: Borrow<str> {
let entities = edn::parse::entities(transaction.borrow())?;
self.transact_entities(entities)
}
@ -912,15 +913,14 @@ impl Conn {
/// Transact entities against the Mentat store, using the given connection and the current
/// metadata.
pub fn transact(&mut self,
pub fn transact<B>(&mut self,
sqlite: &mut rusqlite::Connection,
transaction: &str) -> Result<TxReport> {
transaction: B) -> Result<TxReport> where B: Borrow<str> {
// Parse outside the SQL transaction. This is a tradeoff: we are limiting the scope of the
// transaction, and indeed we don't even create a SQL transaction if the provided input is
// invalid, but it means SQLite errors won't be found until the parse is complete, and if
// there's a race for the database (don't do that!) we are less likely to win it.
let assertion_vector = edn::parse::value(transaction)?;
let entities = mentat_tx_parser::Tx::parse(&assertion_vector)?;
let entities = edn::parse::entities(transaction.borrow())?;
let mut in_progress = self.begin_transaction(sqlite)?;
let report = in_progress.transact_entities(entities)?;
@ -1248,7 +1248,7 @@ mod tests {
// Bad transaction data: missing leading :db/add.
let report = conn.transact(&mut sqlite, "[[\"t\" :db/ident :b/keyword]]");
match report.unwrap_err() {
Error(ErrorKind::TxParseError(::mentat_tx_parser::errors::ErrorKind::ParseError(_)), _) => { },
Error(ErrorKind::EdnParseError(_), _) => { },
x => panic!("expected EDN parse error, got {:?}", x),
}

View file

@ -30,7 +30,6 @@ use mentat_query_pull;
use mentat_query_translator;
use mentat_sql;
use mentat_tolstoy;
use mentat_tx_parser;
error_chain! {
types {
@ -52,7 +51,6 @@ error_chain! {
PullError(mentat_query_pull::errors::Error, mentat_query_pull::errors::ErrorKind);
TranslatorError(mentat_query_translator::Error, mentat_query_translator::ErrorKind);
SqlError(mentat_sql::Error, mentat_sql::ErrorKind);
TxParseError(mentat_tx_parser::Error, mentat_tx_parser::ErrorKind);
SyncError(mentat_tolstoy::Error, mentat_tolstoy::ErrorKind);
}

View file

@ -32,7 +32,6 @@ extern crate mentat_query_translator;
extern crate mentat_sql;
extern crate mentat_tolstoy;
extern crate mentat_tx;
extern crate mentat_tx_parser;
pub use mentat_core::{
Attribute,

View file

@ -742,7 +742,7 @@ fn test_type_reqs() {
{:db/ident :test/long2 :db/valueType :db.type/long :db/cardinality :db.cardinality/one}
]"#).unwrap();
conn.transact(&mut c, &format!("[[:db/add {} :test/long2 5]]", entid)).unwrap();
conn.transact(&mut c, format!("[[:db/add {} :test/long2 5]]", entid)).unwrap();
let longs_query = r#"[:find [?v ...]
:order (asc ?v)
:in ?e

View file

@ -511,7 +511,7 @@ impl Repl {
fn transact(&mut self, transaction: String) -> ::mentat::errors::Result<TxReport> {
let mut tx = self.store.begin_transaction()?;
let report = tx.transact(&transaction)?;
let report = tx.transact(transaction)?;
tx.commit()?;
Ok(report)
}

View file

@ -1,17 +0,0 @@
[package]
name = "mentat_tx_parser"
version = "0.0.1"
workspace = ".."
[dependencies]
combine = "2.3.2"
error-chain = { git = "https://github.com/rnewman/error-chain", branch = "rnewman/sync" }
[dependencies.edn]
path = "../edn"
[dependencies.mentat_tx]
path = "../tx"
[dependencies.mentat_parser_utils]
path = "../parser-utils"

View file

@ -1,51 +0,0 @@
#![feature(test)]
// These benchmarks can be run from the project root with:
// > cargo bench --package mentat_tx_parser
extern crate test;
extern crate edn;
extern crate mentat_tx_parser;
use test::Bencher;
use mentat_tx_parser::Tx;
#[bench]
fn bench_parse1(b: &mut Bencher) {
let input = r#"[[:db/add 1 :test/val "a"]]"#;
let parsed_edn = edn::parse::value(input).expect("to parse test input");
b.iter(|| Tx::parse(parsed_edn.clone()));
}
#[bench]
fn bench_parse2(b: &mut Bencher) {
let input = r#"
[[:db/add 1 :test/val "a"]
[:db/add 2 :test/val "b"]
[:db/add 3 :test/val "c"]
[:db/add 4 :test/val "d"]
[:db/add 5 :test/val "e"]
[:db/add 6 :test/val "f"]
[:db/add 7 :test/val "g"]
[:db/add 8 :test/val "h"]
[:db/add 9 :test/val "i"]
[:db/add 10 :test/val "j"]
[:db/add 11 :test/val "k"]
[:db/add 12 :test/val "l"]
[:db/add 13 :test/val "m"]
[:db/add 14 :test/val "n"]
[:db/add 15 :test/val "o"]
[:db/add 16 :test/val "p"]
[:db/add 17 :test/val "q"]
[:db/add 18 :test/val "r"]
[:db/add 19 :test/val "s"]
[:db/add 20 :test/val "t"]
[:db/add 21 :test/val "u"]
[:db/add 22 :test/val "v"]
[:db/add 23 :test/val "w"]
[:db/add 24 :test/val "x"]
[:db/add 25 :test/val "y"]
[:db/add 26 :test/val "z"]]"#;
let parsed_edn = edn::parse::value(input).expect("to parse test input");
b.iter(|| Tx::parse(parsed_edn.clone()));
}

View file

@ -1,31 +0,0 @@
// Copyright 2016 Mozilla
//
// Licensed 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.
#![allow(dead_code)]
use mentat_parser_utils::ValueParseError;
error_chain! {
types {
Error, ErrorKind, ResultExt, Result;
}
errors {
ParseError(parse_error: ValueParseError) {
description("error parsing edn values")
display("error parsing edn values:\n{}", parse_error)
}
DbIdError {
description("bad :db/id in map notation")
display("bad :db/id in map notation: must either be not present or be an entid, an ident, or a tempid")
}
}
}

View file

@ -1,355 +0,0 @@
// Copyright 2016 Mozilla
//
// Licensed 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.
#![allow(dead_code)]
extern crate combine;
#[macro_use]
extern crate error_chain;
extern crate edn;
extern crate mentat_tx;
#[macro_use]
extern crate mentat_parser_utils;
use combine::{
choice,
eof,
many,
parser,
satisfy,
satisfy_map,
try,
Parser,
ParseResult,
};
use mentat_tx::entities::{
AtomOrLookupRefOrVectorOrMapNotation,
Entid,
EntidOrLookupRefOrTempId,
Entity,
LookupRef,
MapNotation,
OpType,
TempId,
TxFunction,
};
use mentat_parser_utils::{ResultParser};
use mentat_parser_utils::value_and_span::{
Item,
OfExactlyParsing,
backward_keyword,
forward_keyword,
integer,
list,
map,
namespaced_keyword,
vector,
};
pub mod errors;
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, {
list().of_exactly(
Tx::literal_lookup_ref()
.with((Tx::entid(),
Tx::atom()))
.map(|(a, v)| LookupRef { a: a, v: v.clone().without_spans() }))
});
def_parser!(Tx, entid_or_lookup_ref_or_temp_id, EntidOrLookupRefOrTempId, {
Tx::temp_id().map(EntidOrLookupRefOrTempId::TempId)
.or(Tx::entid().map(EntidOrLookupRefOrTempId::Entid))
.or(try(Tx::lookup_ref().map(EntidOrLookupRefOrTempId::LookupRef)))
.or(try(Tx::tx_function().map(EntidOrLookupRefOrTempId::TxFunction)))
});
def_matches_plain_symbol!(Tx, literal_transaction_tx, "transaction-tx");
def_parser!(Tx, tx_function, TxFunction, {
list().of_exactly(
Tx::literal_transaction_tx().map(|_| edn::PlainSymbol::new("transaction-tx"))
.map(|op| TxFunction { op: op }))
});
def_parser!(Tx, temp_id, TempId, {
satisfy_map(|x: &'a edn::ValueAndSpan| x.as_text().cloned().map(TempId::External))
});
def_parser!(Tx, atom, &'a edn::ValueAndSpan, {
satisfy_map(|x: &'a edn::ValueAndSpan| x.as_atom())
});
def_parser!(Tx, nested_vector, Vec<AtomOrLookupRefOrVectorOrMapNotation>, {
vector().of_exactly(many(Tx::atom_or_lookup_ref_or_vector()))
});
def_parser!(Tx, atom_or_lookup_ref_or_vector, AtomOrLookupRefOrVectorOrMapNotation, {
choice::<[&mut Parser<Input = _, Output = AtomOrLookupRefOrVectorOrMapNotation>; 5], _>
([&mut try(Tx::lookup_ref().map(AtomOrLookupRefOrVectorOrMapNotation::LookupRef)),
&mut try(Tx::tx_function().map(AtomOrLookupRefOrVectorOrMapNotation::TxFunction)),
&mut Tx::nested_vector().map(AtomOrLookupRefOrVectorOrMapNotation::Vector),
&mut Tx::map_notation().map(AtomOrLookupRefOrVectorOrMapNotation::MapNotation),
&mut Tx::atom().map(|x| x.clone()).map(AtomOrLookupRefOrVectorOrMapNotation::Atom)
])
});
def_matches_namespaced_keyword!(Tx, literal_db_add, "db", "add");
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)),
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, {
map()
.of_exactly(many((Tx::entid(), Tx::atom_or_lookup_ref_or_vector())))
.map(|avs: Vec<(Entid, AtomOrLookupRefOrVectorOrMapNotation)>| -> MapNotation {
avs.into_iter().collect()
})
});
def_parser!(Tx, entity, Entity, {
Tx::add_or_retract()
.or(Tx::map_notation().map(Entity::MapNotation))
});
def_parser!(Tx, entities, Vec<Entity>, {
vector().of_exactly(many(Tx::entity()))
});
impl<'a> Tx<'a> {
pub fn parse(input: &'a edn::ValueAndSpan) -> std::result::Result<Vec<Entity>, errors::Error> {
Tx::entities()
.skip(eof())
.parse(input.atom_stream())
.map(|x| x.0)
.map_err(|e| Error::from_kind(ErrorKind::ParseError(e.into())))
}
fn parse_entid_or_lookup_ref_or_temp_id(input: edn::ValueAndSpan) -> std::result::Result<EntidOrLookupRefOrTempId, errors::Error> {
Tx::entid_or_lookup_ref_or_temp_id()
.skip(eof())
.parse(input.atom_stream())
.map(|x| x.0)
.map_err(|e| Error::from_kind(ErrorKind::ParseError(e.into())))
}
}
/// Remove any :db/id value from the given map notation, converting the returned value into
/// something suitable for the entity position rather than something suitable for a value position.
///
/// This is here simply to not expose some of the internal APIs of the tx-parser.
pub fn remove_db_id(map: &mut MapNotation) -> std::result::Result<Option<EntidOrLookupRefOrTempId>, errors::Error> {
// TODO: extract lazy defined constant.
let db_id_key = Entid::Ident(edn::NamespacedKeyword::new("db", "id"));
let db_id: Option<EntidOrLookupRefOrTempId> = if let Some(id) = map.remove(&db_id_key) {
match id {
AtomOrLookupRefOrVectorOrMapNotation::Atom(v) => {
let db_id = Tx::parse_entid_or_lookup_ref_or_temp_id(v)
.chain_err(|| Error::from(ErrorKind::DbIdError))?;
Some(db_id)
},
AtomOrLookupRefOrVectorOrMapNotation::LookupRef(_) |
AtomOrLookupRefOrVectorOrMapNotation::TxFunction(_) |
AtomOrLookupRefOrVectorOrMapNotation::Vector(_) |
AtomOrLookupRefOrVectorOrMapNotation::MapNotation(_) => {
bail!(ErrorKind::DbIdError)
},
}
} else {
None
};
Ok(db_id)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use combine::Parser;
use edn::{
NamespacedKeyword,
PlainSymbol,
Span,
SpannedValue,
Value,
ValueAndSpan,
};
use mentat_tx::entities::{
Entid,
EntidOrLookupRefOrTempId,
Entity,
OpType,
AtomOrLookupRefOrVectorOrMapNotation,
};
fn kw(namespace: &str, name: &str) -> Value {
Value::NamespacedKeyword(NamespacedKeyword::new(namespace, name))
}
#[test]
fn test_add() {
let input = Value::Vector(vec![kw("db", "add"),
kw("test", "entid"),
kw("test", "a"),
Value::Text("v".into())]);
let input = input.with_spans();
let stream = input.atom_stream();
let result = Tx::entity().parse(stream).map(|x| x.0);
assert_eq!(result,
Ok(Entity::AddOrRetract {
op: OpType::Add,
e: EntidOrLookupRefOrTempId::Entid(Entid::Ident(NamespacedKeyword::new("test",
"entid"))),
a: Entid::Ident(NamespacedKeyword::new("test", "a")),
v: AtomOrLookupRefOrVectorOrMapNotation::Atom(ValueAndSpan::new(SpannedValue::Text("v".into()), Span(29, 32))),
}));
}
#[test]
fn test_retract() {
let input = Value::Vector(vec![kw("db", "retract"),
Value::Integer(101),
kw("test", "a"),
Value::Text("v".into())]);
let input = input.with_spans();
let stream = input.atom_stream();
let result = Tx::entity().parse(stream).map(|x| x.0);
assert_eq!(result,
Ok(Entity::AddOrRetract {
op: OpType::Retract,
e: EntidOrLookupRefOrTempId::Entid(Entid::Entid(101)),
a: Entid::Ident(NamespacedKeyword::new("test", "a")),
v: AtomOrLookupRefOrVectorOrMapNotation::Atom(ValueAndSpan::new(SpannedValue::Text("v".into()), Span(25, 28))),
}));
}
#[test]
fn test_lookup_ref() {
let input = Value::Vector(vec![kw("db", "add"),
Value::List(vec![Value::PlainSymbol(PlainSymbol::new("lookup-ref")),
kw("test", "a1"),
Value::Text("v1".into())].into_iter().collect()),
kw("test", "a"),
Value::Text("v".into())]);
let input = input.with_spans();
let stream = input.atom_stream();
let result = Tx::entity().parse(stream).map(|x| x.0);
assert_eq!(result,
Ok(Entity::AddOrRetract {
op: OpType::Add,
e: EntidOrLookupRefOrTempId::LookupRef(LookupRef {
a: Entid::Ident(NamespacedKeyword::new("test", "a1")),
v: Value::Text("v1".into()),
}),
a: Entid::Ident(NamespacedKeyword::new("test", "a")),
v: AtomOrLookupRefOrVectorOrMapNotation::Atom(ValueAndSpan::new(SpannedValue::Text("v".into()), Span(44, 47))),
}));
}
#[test]
fn test_nested_vector() {
let input = Value::Vector(vec![kw("db", "add"),
Value::List(vec![Value::PlainSymbol(PlainSymbol::new("lookup-ref")),
kw("test", "a1"),
Value::Text("v1".into())].into_iter().collect()),
kw("test", "a"),
Value::Vector(vec![Value::Text("v1".into()), Value::Text("v2".into())])]);
let input = input.with_spans();
let stream = input.atom_stream();
let result = Tx::entity().parse(stream).map(|x| x.0);
assert_eq!(result,
Ok(Entity::AddOrRetract {
op: OpType::Add,
e: EntidOrLookupRefOrTempId::LookupRef(LookupRef {
a: Entid::Ident(NamespacedKeyword::new("test", "a1")),
v: Value::Text("v1".into()),
}),
a: Entid::Ident(NamespacedKeyword::new("test", "a")),
v: AtomOrLookupRefOrVectorOrMapNotation::Vector(vec![AtomOrLookupRefOrVectorOrMapNotation::Atom(ValueAndSpan::new(SpannedValue::Text("v1".into()), Span(45, 49))),
AtomOrLookupRefOrVectorOrMapNotation::Atom(ValueAndSpan::new(SpannedValue::Text("v2".into()), Span(50, 54)))]),
}));
}
#[test]
fn test_map_notation() {
let mut expected: MapNotation = BTreeMap::default();
expected.insert(Entid::Ident(NamespacedKeyword::new("db", "id")), AtomOrLookupRefOrVectorOrMapNotation::Atom(ValueAndSpan::new(SpannedValue::Text("t".to_string()), Span(8, 11))));
expected.insert(Entid::Ident(NamespacedKeyword::new("db", "ident")), AtomOrLookupRefOrVectorOrMapNotation::Atom(ValueAndSpan::new(SpannedValue::NamespacedKeyword(NamespacedKeyword::new("test", "attribute")), Span(22, 37))));
let mut map: BTreeMap<Value, Value> = BTreeMap::default();
map.insert(kw("db", "id"), Value::Text("t".to_string()));
map.insert(kw("db", "ident"), kw("test", "attribute"));
let input = Value::Map(map.clone());
let input = input.with_spans();
let stream = input.atom_stream();
let result = Tx::entity().parse(stream).map(|x| x.0);
assert_eq!(result,
Ok(Entity::MapNotation(expected)));
}
}

View file

@ -1,106 +0,0 @@
// Copyright 2016 Mozilla
//
// Licensed 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.
extern crate edn;
extern crate combine;
extern crate mentat_tx;
extern crate mentat_tx_parser;
use edn::parse;
use edn::symbols::NamespacedKeyword;
use mentat_tx::entities::{
AtomOrLookupRefOrVectorOrMapNotation,
Entid,
EntidOrLookupRefOrTempId,
Entity,
OpType,
TempId,
};
use mentat_tx_parser::Tx;
#[test]
fn test_float_and_uuid() {
let expected_uuid = edn::Uuid::parse_str("267bab92-ee39-4ca2-b7f0-1163a85af1fb").expect("valid uuid");
let input = r#"
[[:db/add 101 :test/a #uuid "267bab92-ee39-4ca2-b7f0-1163a85af1fb"]
[:db/add 102 :test/b #f NaN]]
"#;
let edn = parse::value(input).expect("to parse test input");
let result = Tx::parse(&edn);
assert_eq!(result.unwrap(),
vec![
Entity::AddOrRetract {
op: OpType::Add,
e: EntidOrLookupRefOrTempId::Entid(Entid::Entid(101)),
a: Entid::Ident(NamespacedKeyword::new("test", "a")),
v: AtomOrLookupRefOrVectorOrMapNotation::Atom(edn::ValueAndSpan::new(edn::SpannedValue::Uuid(expected_uuid), edn::Span(23, 67))),
},
Entity::AddOrRetract {
op: OpType::Add,
e: EntidOrLookupRefOrTempId::Entid(Entid::Entid(102)),
a: Entid::Ident(NamespacedKeyword::new("test", "b")),
v: AtomOrLookupRefOrVectorOrMapNotation::Atom(edn::ValueAndSpan::new(edn::SpannedValue::Float(edn::OrderedFloat(std::f64::NAN)), edn::Span(91, 97))),
},
]);
}
#[test]
fn test_entities() {
let input = r#"
[[:db/add 101 :test/a "v"]
[:db/add "tempid" :test/a "v"]
[:db/retract 102 :test/b "w"]]"#;
let edn = parse::value(input).expect("to parse test input");
let result = Tx::parse(&edn);
assert_eq!(result.unwrap(),
vec![
Entity::AddOrRetract {
op: OpType::Add,
e: EntidOrLookupRefOrTempId::Entid(Entid::Entid(101)),
a: Entid::Ident(NamespacedKeyword::new("test", "a")),
v: AtomOrLookupRefOrVectorOrMapNotation::Atom(edn::ValueAndSpan::new(edn::SpannedValue::Text("v".into()), edn::Span(23, 26))),
},
Entity::AddOrRetract {
op: OpType::Add,
e: EntidOrLookupRefOrTempId::TempId(TempId::External("tempid".into())),
a: Entid::Ident(NamespacedKeyword::new("test", "a")),
v: AtomOrLookupRefOrVectorOrMapNotation::Atom(edn::ValueAndSpan::new(edn::SpannedValue::Text("v".into()), edn::Span(55, 58))),
},
Entity::AddOrRetract {
op: OpType::Retract,
e: EntidOrLookupRefOrTempId::Entid(Entid::Entid(102)),
a: Entid::Ident(NamespacedKeyword::new("test", "b")),
v: AtomOrLookupRefOrVectorOrMapNotation::Atom(edn::ValueAndSpan::new(edn::SpannedValue::Text("w".into()), edn::Span(86, 89))),
},
]);
}
#[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.

View file

@ -8,4 +8,6 @@
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
pub mod entities;
#[allow(unused_imports)]
#[macro_use] extern crate edn;
pub use edn::entities;