Use rust-peg for tx parsing. r=rnewman
This commit is contained in:
commit
9a4bd0de4f
24 changed files with 282 additions and 644 deletions
|
@ -76,9 +76,6 @@ path = "query-translator"
|
|||
[dependencies.mentat_tx]
|
||||
path = "tx"
|
||||
|
||||
[dependencies.mentat_tx_parser]
|
||||
path = "tx-parser"
|
||||
|
||||
[dependencies.mentat_tolstoy]
|
||||
path = "tolstoy"
|
||||
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
64
db/src/tx.rs
64
db/src/tx.rs
|
@ -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,18 +363,19 @@ 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)? {
|
||||
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)))
|
||||
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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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());
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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>),
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
20
src/conn.rs
20
src/conn.rs
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
|
@ -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()));
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue