diff --git a/Cargo.toml b/Cargo.toml index 1cf68462..7adde7de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ authors = [ "Thom Chiovoloni ", ] name = "mentat" -version = "0.6.2" +version = "0.7.0" build = "build/version.rs" [features] diff --git a/core/Cargo.toml b/core/Cargo.toml index 96a217bd..7b75353e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -6,6 +6,7 @@ workspace = ".." [dependencies] chrono = { version = "0.4", features = ["serde"] } enum-set = { git = "https://github.com/rnewman/enum-set" } +indexmap = "1" lazy_static = "0.2" num = "0.1" ordered-float = { version = "0.5", features = ["serde"] } diff --git a/core/src/lib.rs b/core/src/lib.rs index 9d381797..a2095964 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -10,6 +10,7 @@ extern crate chrono; extern crate enum_set; +extern crate indexmap; extern crate ordered_float; extern crate uuid; extern crate serde; @@ -57,6 +58,7 @@ pub use types::{ Entid, KnownEntid, TypedValue, + Binding, ValueType, ValueTypeTag, now, diff --git a/core/src/types.rs b/core/src/types.rs index 799e947b..f6121bd0 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -22,6 +22,10 @@ use ::chrono::{ Timelike, // For truncation. }; +use ::indexmap::{ + IndexMap, +}; + use ::edn::{ self, FromMicros, @@ -49,6 +53,12 @@ impl From for Entid { } } +impl From for Binding { + fn from(k: KnownEntid) -> Binding { + Binding::Scalar(TypedValue::Ref(k.0)) + } +} + impl From for TypedValue { fn from(k: KnownEntid) -> TypedValue { TypedValue::Ref(k.0) @@ -172,6 +182,90 @@ pub enum TypedValue { Uuid(Uuid), // It's only 128 bits, so this should be acceptable to clone. } +/// The values bound in a query specification can be: +/// +/// * Vecs of structured values, for multi-valued component attributes or nested expressions. +/// * Single structured values, for single-valued component attributes or nested expressions. +/// * Single typed values, for simple attributes. +/// +/// The `Binding` enum defines these three options. +/// +/// Datomic also supports structured inputs; at present Mentat does not, but this type +/// would also serve that purpose. +/// +/// Note that maps are not ordered, and so `Binding` is neither `Ord` nor `PartialOrd`. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Binding { + Scalar(TypedValue), + Vec(Rc>), + Map(Rc), +} + +impl From for Binding { + fn from(src: TypedValue) -> Self { + Binding::Scalar(src) + } +} + + +impl<'a> From<&'a str> for Binding { + fn from(value: &'a str) -> Binding { + Binding::Scalar(TypedValue::String(Rc::new(value.to_string()))) + } +} + +impl From for Binding { + fn from(value: String) -> Binding { + Binding::Scalar(TypedValue::String(Rc::new(value))) + } +} + +impl Binding { + pub fn val(self) -> Option { + match self { + Binding::Scalar(v) => Some(v), + _ => None, + } + } +} + +/// A pull expression expands a binding into a structure. The returned structure +/// associates attributes named in the input or retrieved from the store with values. +/// This association is a `StructuredMap`. +/// +/// Note that 'attributes' in Datomic's case can mean: +/// - Reversed attribute keywords (:artist/_country). +/// - An alias using `:as` (:artist/name :as "Band name"). +/// +/// We entirely support the former, and partially support the latter -- you can alias +/// using a different keyword only. +#[derive(Debug, Eq, PartialEq)] +pub struct StructuredMap(IndexMap, Binding>); + +impl Binding { + /// Returns true if the provided type is `Some` and matches this value's type, or if the + /// provided type is `None`. + #[inline] + pub fn is_congruent_with>>(&self, t: T) -> bool { + t.into().map_or(true, |x| self.matches_type(x)) + } + + #[inline] + pub fn matches_type(&self, t: ValueType) -> bool { + self.value_type() == Some(t) + } + + pub fn value_type(&self) -> Option { + match self { + &Binding::Scalar(ref v) => Some(v.value_type()), + + &Binding::Map(_) => None, + &Binding::Vec(_) => None, + } + } +} + + impl TypedValue { /// Returns true if the provided type is `Some` and matches this value's type, or if the /// provided type is `None`. @@ -382,6 +476,85 @@ impl TypedValue { } } +impl Binding { + pub fn into_known_entid(self) -> Option { + match self { + Binding::Scalar(TypedValue::Ref(v)) => Some(KnownEntid(v)), + _ => None, + } + } + + pub fn into_entid(self) -> Option { + match self { + Binding::Scalar(TypedValue::Ref(v)) => Some(v), + _ => None, + } + } + + pub fn into_kw(self) -> Option> { + match self { + Binding::Scalar(TypedValue::Keyword(v)) => Some(v), + _ => None, + } + } + + pub fn into_boolean(self) -> Option { + match self { + Binding::Scalar(TypedValue::Boolean(v)) => Some(v), + _ => None, + } + } + + pub fn into_long(self) -> Option { + match self { + Binding::Scalar(TypedValue::Long(v)) => Some(v), + _ => None, + } + } + + pub fn into_double(self) -> Option { + match self { + Binding::Scalar(TypedValue::Double(v)) => Some(v.into_inner()), + _ => None, + } + } + + pub fn into_instant(self) -> Option> { + match self { + Binding::Scalar(TypedValue::Instant(v)) => Some(v), + _ => None, + } + } + + pub fn into_timestamp(self) -> Option { + match self { + Binding::Scalar(TypedValue::Instant(v)) => Some(v.timestamp()), + _ => None, + } + } + + pub fn into_string(self) -> Option> { + match self { + Binding::Scalar(TypedValue::String(v)) => Some(v), + _ => None, + } + } + + pub fn into_uuid(self) -> Option { + match self { + Binding::Scalar(TypedValue::Uuid(v)) => Some(v), + _ => None, + } + } + + pub fn into_uuid_string(self) -> Option { + match self { + Binding::Scalar(TypedValue::Uuid(v)) => Some(v.hyphenated().to_string()), + _ => None, + } + } +} + #[test] fn test_typed_value() { assert!(TypedValue::Boolean(false).is_congruent_with(None)); diff --git a/query-projector/src/lib.rs b/query-projector/src/lib.rs index 7ebfb642..b2a279c5 100644 --- a/query-projector/src/lib.rs +++ b/query-projector/src/lib.rs @@ -34,6 +34,7 @@ use rusqlite::{ }; use mentat_core::{ + Binding, TypedValue, ValueType, ValueTypeTag, @@ -84,6 +85,7 @@ pub use project::{ pub use relresult::{ RelResult, + StructuredRelResult, }; use errors::{ @@ -99,10 +101,10 @@ pub struct QueryOutput { #[derive(Clone, Debug, PartialEq, Eq)] pub enum QueryResults { - Scalar(Option), - Tuple(Option>), - Coll(Vec), - Rel(RelResult), + Scalar(Option), + Tuple(Option>), + Coll(Vec), + Rel(RelResult), } impl From for QueryResults { @@ -153,7 +155,9 @@ impl QueryOutput { match &**spec { &FindScalar(Element::Variable(ref var)) | &FindScalar(Element::Corresponding(ref var)) => { - let val = bindings.get(var).cloned(); + let val = bindings.get(var) + .cloned() + .map(|v| v.into()); QueryResults::Scalar(val) }, &FindScalar(Element::Aggregate(ref _agg)) => { @@ -165,7 +169,10 @@ impl QueryOutput { .map(|e| match e { &Element::Variable(ref var) | &Element::Corresponding(ref var) => { - bindings.get(var).cloned().expect("every var to have a binding") + bindings.get(var) + .cloned() + .expect("every var to have a binding") + .into() }, &Element::Aggregate(ref _agg) => { // TODO: static computation of aggregates, then @@ -178,7 +185,10 @@ impl QueryOutput { }, &FindColl(Element::Variable(ref var)) | &FindColl(Element::Corresponding(ref var)) => { - let val = bindings.get(var).cloned().expect("every var to have a binding"); + let val = bindings.get(var) + .cloned() + .expect("every var to have a binding") + .into(); QueryResults::Coll(vec![val]) }, &FindColl(Element::Aggregate(ref _agg)) => { @@ -193,7 +203,10 @@ impl QueryOutput { let values = elements.iter().map(|e| match e { &Element::Variable(ref var) | &Element::Corresponding(ref var) => { - bindings.get(var).cloned().expect("every var to have a binding") + bindings.get(var) + .cloned() + .expect("every var to have a binding") + .into() }, &Element::Aggregate(ref _agg) => { // TODO: static computation of aggregates, then @@ -206,19 +219,19 @@ impl QueryOutput { } } - pub fn into_scalar(self) -> Result> { + pub fn into_scalar(self) -> Result> { self.results.into_scalar() } - pub fn into_coll(self) -> Result> { + pub fn into_coll(self) -> Result> { self.results.into_coll() } - pub fn into_tuple(self) -> Result>> { + pub fn into_tuple(self) -> Result>> { self.results.into_tuple() } - pub fn into_rel(self) -> Result { + pub fn into_rel(self) -> Result> { self.results.into_rel() } } @@ -244,7 +257,7 @@ impl QueryResults { } } - pub fn into_scalar(self) -> Result> { + pub fn into_scalar(self) -> Result> { match self { QueryResults::Scalar(o) => Ok(o), QueryResults::Coll(_) => bail!(ErrorKind::UnexpectedResultsType("coll", "scalar")), @@ -253,7 +266,7 @@ impl QueryResults { } } - pub fn into_coll(self) -> Result> { + pub fn into_coll(self) -> Result> { match self { QueryResults::Scalar(_) => bail!(ErrorKind::UnexpectedResultsType("scalar", "coll")), QueryResults::Coll(c) => Ok(c), @@ -262,7 +275,7 @@ impl QueryResults { } } - pub fn into_tuple(self) -> Result>> { + pub fn into_tuple(self) -> Result>> { match self { QueryResults::Scalar(_) => bail!(ErrorKind::UnexpectedResultsType("scalar", "tuple")), QueryResults::Coll(_) => bail!(ErrorKind::UnexpectedResultsType("coll", "tuple")), @@ -271,7 +284,7 @@ impl QueryResults { } } - pub fn into_rel(self) -> Result { + pub fn into_rel(self) -> Result> { match self { QueryResults::Scalar(_) => bail!(ErrorKind::UnexpectedResultsType("scalar", "rel")), QueryResults::Coll(_) => bail!(ErrorKind::UnexpectedResultsType("coll", "rel")), @@ -302,18 +315,22 @@ impl TypedIndex { /// /// This function will return a runtime error if the type tag is unknown, or the value is /// otherwise not convertible by the DB layer. - fn lookup<'a, 'stmt>(&self, row: &Row<'a, 'stmt>) -> Result { + fn lookup<'a, 'stmt>(&self, row: &Row<'a, 'stmt>) -> Result { use TypedIndex::*; match self { &Known(value_index, value_type) => { let v: rusqlite::types::Value = row.get(value_index); - TypedValue::from_sql_value_pair(v, value_type).map_err(|e| e.into()) + TypedValue::from_sql_value_pair(v, value_type) + .map(|v| v.into()) + .map_err(|e| e.into()) }, &Unknown(value_index, type_index) => { let v: rusqlite::types::Value = row.get(value_index); let value_type_tag: i32 = row.get(type_index); - TypedValue::from_sql_value_pair(v, value_type_tag).map_err(|e| e.into()) + TypedValue::from_sql_value_pair(v, value_type_tag) + .map(|v| v.into()) + .map_err(|e| e.into()) }, } } @@ -422,7 +439,7 @@ impl TupleProjector { } // This is exactly the same as for rel. - fn collect_bindings<'a, 'stmt>(&self, row: Row<'a, 'stmt>) -> Result> { + fn collect_bindings<'a, 'stmt>(&self, row: Row<'a, 'stmt>) -> Result> { // There will be at least as many SQL columns as Datalog columns. // gte 'cos we might be querying extra columns for ordering. // The templates will take care of ignoring columns. @@ -430,7 +447,7 @@ impl TupleProjector { self.templates .iter() .map(|ti| ti.lookup(&row)) - .collect::>>() + .collect::>>() } fn combine(spec: Rc, column_count: usize, elements: ProjectedElements) -> Result { @@ -485,7 +502,7 @@ impl RelProjector { } } - fn collect_bindings_into<'a, 'stmt, 'out>(&self, row: Row<'a, 'stmt>, out: &mut Vec) -> Result<()> { + fn collect_bindings_into<'a, 'stmt, 'out>(&self, row: Row<'a, 'stmt>, out: &mut Vec) -> Result<()> { // There will be at least as many SQL columns as Datalog columns. // gte 'cos we might be querying extra columns for ordering. // The templates will take care of ignoring columns. @@ -580,7 +597,7 @@ impl CollProjector { impl Projector for CollProjector { fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result { - let mut out: Vec = vec![]; + let mut out: Vec<_> = vec![]; while let Some(r) = rows.next() { let row = r?; let binding = self.template.lookup(&row)?; @@ -664,7 +681,7 @@ pub fn query_projection(query: &AlgebraicQuery) -> Result { pub width: usize, - pub values: Vec, + pub values: Vec, } -impl RelResult { - pub fn empty(width: usize) -> RelResult { +pub type StructuredRelResult = RelResult; + +impl RelResult { + pub fn empty(width: usize) -> RelResult { RelResult { width: width, values: Vec::new(), @@ -52,12 +55,12 @@ impl RelResult { self.values.len() / self.width } - pub fn rows(&self) -> ::std::slice::Chunks { + pub fn rows(&self) -> ::std::slice::Chunks { // TODO: Nightly-only API `exact_chunks`. #47115. self.values.chunks(self.width) } - pub fn row(&self, index: usize) -> Option<&[TypedValue]> { + pub fn row(&self, index: usize) -> Option<&[T]> { let end = self.width * (index + 1); if end > self.values.len() { None @@ -70,15 +73,15 @@ impl RelResult { #[test] fn test_rel_result() { - let empty = RelResult::empty(3); - let unit = RelResult { + let empty = StructuredRelResult::empty(3); + let unit = StructuredRelResult { width: 1, - values: vec![TypedValue::Long(5)], + values: vec![TypedValue::Long(5).into()], }; - let two_by_two = RelResult { + let two_by_two = StructuredRelResult { width: 2, - values: vec![TypedValue::Long(5), TypedValue::Boolean(true), - TypedValue::Long(-2), TypedValue::Boolean(false)], + values: vec![TypedValue::Long(5).into(), TypedValue::Boolean(true).into(), + TypedValue::Long(-2).into(), TypedValue::Boolean(false).into()], }; assert!(empty.is_empty()); @@ -93,18 +96,18 @@ fn test_rel_result() { assert_eq!(unit.row(1), None); assert_eq!(two_by_two.row(2), None); - assert_eq!(unit.row(0), Some(vec![TypedValue::Long(5)].as_slice())); - assert_eq!(two_by_two.row(0), Some(vec![TypedValue::Long(5), TypedValue::Boolean(true)].as_slice())); - assert_eq!(two_by_two.row(1), Some(vec![TypedValue::Long(-2), TypedValue::Boolean(false)].as_slice())); + assert_eq!(unit.row(0), Some(vec![TypedValue::Long(5).into()].as_slice())); + assert_eq!(two_by_two.row(0), Some(vec![TypedValue::Long(5).into(), TypedValue::Boolean(true).into()].as_slice())); + assert_eq!(two_by_two.row(1), Some(vec![TypedValue::Long(-2).into(), TypedValue::Boolean(false).into()].as_slice())); let mut rr = two_by_two.rows(); - assert_eq!(rr.next(), Some(vec![TypedValue::Long(5), TypedValue::Boolean(true)].as_slice())); - assert_eq!(rr.next(), Some(vec![TypedValue::Long(-2), TypedValue::Boolean(false)].as_slice())); + assert_eq!(rr.next(), Some(vec![TypedValue::Long(5).into(), TypedValue::Boolean(true).into()].as_slice())); + assert_eq!(rr.next(), Some(vec![TypedValue::Long(-2).into(), TypedValue::Boolean(false).into()].as_slice())); assert_eq!(rr.next(), None); } // Primarily for testing. -impl From>> for RelResult { +impl From>> for RelResult { fn from(src: Vec>) -> Self { if src.is_empty() { RelResult::empty(0) @@ -112,23 +115,23 @@ impl From>> for RelResult { let width = src.get(0).map(|r| r.len()).unwrap_or(0); RelResult { width: width, - values: src.into_iter().flat_map(|r| r.into_iter()).collect(), + values: src.into_iter().flat_map(|r| r.into_iter().map(|v| v.into())).collect(), } } } } -pub struct SubvecIntoIterator { +pub struct SubvecIntoIterator { width: usize, - values: ::std::vec::IntoIter, + values: ::std::vec::IntoIter, } -impl Iterator for SubvecIntoIterator { +impl Iterator for SubvecIntoIterator { // TODO: this is a good opportunity to use `SmallVec` instead: most queries // return a handful of columns. - type Item = Vec; - fn next(&mut self) -> Option> { - let result: Vec = (&mut self.values).take(self.width).collect(); + type Item = Vec; + fn next(&mut self) -> Option { + let result: Vec<_> = (&mut self.values).take(self.width).collect(); if result.is_empty() { None } else { @@ -137,9 +140,9 @@ impl Iterator for SubvecIntoIterator { } } -impl IntoIterator for RelResult { - type Item = Vec; - type IntoIter = SubvecIntoIterator; +impl IntoIterator for RelResult { + type Item = Vec; + type IntoIter = SubvecIntoIterator; fn into_iter(self) -> Self::IntoIter { SubvecIntoIterator { diff --git a/query-translator/tests/translate.rs b/query-translator/tests/translate.rs index 90a36850..e0530745 100644 --- a/query-translator/tests/translate.rs +++ b/query-translator/tests/translate.rs @@ -735,7 +735,7 @@ fn test_ground_scalar() { let constant = translate_to_constant(&schema, query); assert_eq!(constant.project_without_rows().unwrap() .into_scalar().unwrap(), - Some(TypedValue::typed_string("yyy"))); + Some(TypedValue::typed_string("yyy").into())); // Verify that we accept bound input constants. let query = r#"[:find ?x . :in ?v :where [(ground ?v) ?x]]"#; @@ -743,7 +743,7 @@ fn test_ground_scalar() { let constant = translate_with_inputs_to_constant(&schema, query, inputs); assert_eq!(constant.project_without_rows().unwrap() .into_scalar().unwrap(), - Some(TypedValue::typed_string("aaa"))); + Some(TypedValue::typed_string("aaa").into())); } #[test] @@ -765,7 +765,7 @@ fn test_ground_tuple() { let constant = translate_with_inputs_to_constant(&schema, query, inputs); assert_eq!(constant.project_without_rows().unwrap() .into_tuple().unwrap(), - Some(vec![TypedValue::Long(2), TypedValue::typed_string("aaa")])); + Some(vec![TypedValue::Long(2).into(), TypedValue::typed_string("aaa").into()])); // TODO: treat 2 as an input variable that could be bound late, rather than eagerly binding it. // In that case the query wouldn't be constant, and would look more like: diff --git a/src/conn.rs b/src/conn.rs index f9f1187b..3ef0df5d 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -923,6 +923,7 @@ mod tests { use mentat_core::{ CachedAttributes, + Binding, TypedValue, }; @@ -1038,7 +1039,7 @@ mod tests { let during = in_progress.q_once("[:find ?x . :where [?x :db/ident :a/keyword1]]", None) .expect("query succeeded"); - assert_eq!(during.results, QueryResults::Scalar(Some(TypedValue::Ref(one)))); + assert_eq!(during.results, QueryResults::Scalar(Some(TypedValue::Ref(one).into()))); let report = in_progress.transact(t2).expect("t2 succeeded"); in_progress.commit().expect("commit succeeded"); @@ -1083,10 +1084,10 @@ mod tests { values).expect("prepare succeeded"); let yeses = prepared.run(None).expect("result"); - assert_eq!(yeses.results, QueryResults::Coll(vec![TypedValue::Ref(yes)])); + assert_eq!(yeses.results, QueryResults::Coll(vec![TypedValue::Ref(yes).into()])); let yeses_again = prepared.run(None).expect("result"); - assert_eq!(yeses_again.results, QueryResults::Coll(vec![TypedValue::Ref(yes)])); + assert_eq!(yeses_again.results, QueryResults::Coll(vec![TypedValue::Ref(yes).into()])); } #[test] @@ -1119,7 +1120,7 @@ mod tests { let during = in_progress.q_once("[:find ?x . :where [?x :db/ident :a/keyword1]]", None) .expect("query succeeded"); - assert_eq!(during.results, QueryResults::Scalar(Some(TypedValue::Ref(one)))); + assert_eq!(during.results, QueryResults::Scalar(Some(TypedValue::Ref(one).into()))); // And we can do direct lookup, too. let kw = in_progress.lookup_value_for_attribute(one, &edn::NamespacedKeyword::new("db", "ident")) @@ -1233,7 +1234,7 @@ mod tests { let entities = conn.q_once(&sqlite, r#"[:find ?e . :where [?e :foo/bar 400]]"#, None).expect("Expected query to work").into_scalar().expect("expected rel results"); let first = entities.expect("expected a result"); let entid = match first { - TypedValue::Ref(entid) => entid, + Binding::Scalar(TypedValue::Ref(entid)) => entid, x => panic!("expected Some(Ref), got {:?}", x), }; @@ -1279,7 +1280,7 @@ mod tests { let mut ip = conn.begin_transaction(&mut sqlite).expect("began"); let ident = ip.q_once(query.as_str(), None).into_scalar_result().expect("query"); - assert_eq!(ident, Some(TypedValue::typed_ns_keyword("db.type", "string"))); + assert_eq!(ident, Some(TypedValue::typed_ns_keyword("db.type", "string").into())); let start = time::PreciseTime::now(); ip.q_once(query.as_str(), None).into_scalar_result().expect("query"); @@ -1292,7 +1293,7 @@ mod tests { assert!(ip.cache.is_attribute_cached_forward(db_ident)); let ident = ip.q_once(query.as_str(), None).into_scalar_result().expect("query"); - assert_eq!(ident, Some(TypedValue::typed_ns_keyword("db.type", "string"))); + assert_eq!(ident, Some(TypedValue::typed_ns_keyword("db.type", "string").into())); let start = time::PreciseTime::now(); ip.q_once(query.as_str(), None).into_scalar_result().expect("query"); @@ -1309,7 +1310,7 @@ mod tests { let mut ip = conn.begin_transaction(&mut sqlite).expect("began"); let ident = ip.q_once(query.as_str(), None).into_scalar_result().expect("query"); - assert_eq!(ident, Some(TypedValue::typed_ns_keyword("db.type", "string"))); + assert_eq!(ident, Some(TypedValue::typed_ns_keyword("db.type", "string").into())); ip.cache(&kw!(:db/ident), CacheDirection::Forward, CacheAction::Register).expect("registered"); ip.cache(&kw!(:db/valueType), CacheDirection::Forward, CacheAction::Register).expect("registered"); @@ -1344,7 +1345,7 @@ mod tests { [?neighborhood :neighborhood/district ?d] [?d :district/name ?district]]"#; let hood = "Beacon Hill"; - let inputs = QueryInputs::with_value_sequence(vec![(var!(?hood), TypedValue::typed_string(hood))]); + let inputs = QueryInputs::with_value_sequence(vec![(var!(?hood), TypedValue::typed_string(hood).into())]); let mut prepared = in_progress.q_prepare(query, inputs) .expect("prepared"); match &prepared { diff --git a/src/lib.rs b/src/lib.rs index fb46e26b..d9facc6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,6 +41,7 @@ pub use mentat_core::{ KnownEntid, NamespacedKeyword, Schema, + Binding, TypedValue, Uuid, Utc, diff --git a/src/query.rs b/src/query.rs index d270654e..207b5502 100644 --- a/src/query.rs +++ b/src/query.rs @@ -18,6 +18,7 @@ use mentat_core::{ HasSchema, KnownEntid, Schema, + Binding, TypedValue, }; @@ -117,26 +118,26 @@ impl<'sqlite> PreparedQuery<'sqlite> { } pub trait IntoResult { - fn into_scalar_result(self) -> Result>; - fn into_coll_result(self) -> Result>; - fn into_tuple_result(self) -> Result>>; - fn into_rel_result(self) -> Result; + fn into_scalar_result(self) -> Result>; + fn into_coll_result(self) -> Result>; + fn into_tuple_result(self) -> Result>>; + fn into_rel_result(self) -> Result>; } impl IntoResult for QueryExecutionResult { - fn into_scalar_result(self) -> Result> { + fn into_scalar_result(self) -> Result> { self?.into_scalar().map_err(|e| e.into()) } - fn into_coll_result(self) -> Result> { + fn into_coll_result(self) -> Result> { self?.into_coll().map_err(|e| e.into()) } - fn into_tuple_result(self) -> Result>> { + fn into_tuple_result(self) -> Result>> { self?.into_tuple().map_err(|e| e.into()) } - fn into_rel_result(self) -> Result { + fn into_rel_result(self) -> Result> { self?.into_rel().map_err(|e| e.into()) } } @@ -232,7 +233,10 @@ pub fn lookup_value<'sqlite, 'schema, 'cache, E, A> if known.is_attribute_cached_forward(attrid) { Ok(known.get_value_for_entid(known.schema, attrid, entid).cloned()) } else { - fetch_values(sqlite, known, entid, attrid, true).into_scalar_result() + fetch_values(sqlite, known, entid, attrid, true) + .into_scalar_result() + // Safe to unwrap: we never retrieve structure. + .map(|r| r.map(|v| v.val().unwrap())) } } @@ -251,7 +255,10 @@ pub fn lookup_values<'sqlite, E, A> .cloned() .unwrap_or_else(|| vec![])) } else { - fetch_values(sqlite, known, entid, attrid, false).into_coll_result() + fetch_values(sqlite, known, entid, attrid, false) + .into_coll_result() + // Safe to unwrap: we never retrieve structure. + .map(|v| v.into_iter().map(|x| x.val().unwrap()).collect()) } } diff --git a/src/query_builder.rs b/src/query_builder.rs index 87c34c41..40142b4b 100644 --- a/src/query_builder.rs +++ b/src/query_builder.rs @@ -16,6 +16,7 @@ use std::collections::{ use mentat_core::{ Entid, NamespacedKeyword, + Binding, TypedValue, ValueType, }; @@ -86,22 +87,22 @@ impl<'a> QueryBuilder<'a> { read.q_once(&self.sql, query_inputs) } - pub fn execute_scalar(&mut self) -> Result> { + pub fn execute_scalar(&mut self) -> Result> { let results = self.execute()?; results.into_scalar().map_err(|e| e.into()) } - pub fn execute_coll(&mut self) -> Result> { + pub fn execute_coll(&mut self) -> Result> { let results = self.execute()?; results.into_coll().map_err(|e| e.into()) } - pub fn execute_tuple(&mut self) -> Result>> { + pub fn execute_tuple(&mut self) -> Result>> { let results = self.execute()?; results.into_tuple().map_err(|e| e.into()) } - pub fn execute_rel(&mut self) -> Result { + pub fn execute_rel(&mut self) -> Result> { let results = self.execute()?; results.into_rel().map_err(|e| e.into()) } @@ -286,15 +287,15 @@ mod test { let n_yes = report.tempids.get("n").expect("found it").clone(); - let results: Vec = QueryBuilder::new(&mut store, r#"[:find [?x, ?i] - :in ?v ?i - :where [?x :foo/boolean ?v] - [?x :foo/long ?i]]"#) + let results: Vec<_> = QueryBuilder::new(&mut store, r#"[:find [?x, ?i] + :in ?v ?i + :where [?x :foo/boolean ?v] + [?x :foo/long ?i]]"#) .bind_value("?v", true) .bind_long("?i", 27) .execute_tuple().expect("TupleResult").unwrap_or(vec![]); - let entid = TypedValue::Ref(n_yes.clone()); - let long_val = TypedValue::Long(27); + let entid = TypedValue::Ref(n_yes.clone()).into(); + let long_val = TypedValue::Long(27).into(); assert_eq!(results, vec![entid, long_val]); } diff --git a/src/vocabulary.rs b/src/vocabulary.rs index 2e88239f..883649f5 100644 --- a/src/vocabulary.rs +++ b/src/vocabulary.rs @@ -110,6 +110,7 @@ use ::{ HasSchema, IntoResult, NamespacedKeyword, + Binding, TypedValue, ValueType, }; @@ -172,6 +173,7 @@ pub struct Definition { /// IntoResult, /// Queryable, /// Store, +/// TypedValue, /// ValueType, /// }; /// @@ -225,7 +227,7 @@ pub struct Definition { /// for row in results.into_iter() { /// let mut r = row.into_iter(); /// let e = r.next().and_then(|e| e.into_known_entid()).expect("entity"); -/// let obsolete = r.next().expect("value"); +/// let obsolete = r.next().expect("value").val().expect("typed value"); /// builder.retract(e, link_title, obsolete)?; /// } /// ip.transact_builder(builder)?; @@ -882,7 +884,8 @@ impl HasVocabularies for T where T: HasSchema + Queryable { .into_iter() .filter_map(|v| match (&v[0], &v[1]) { - (&TypedValue::Ref(vocab), &TypedValue::Long(version)) + (&Binding::Scalar(TypedValue::Ref(vocab)), + &Binding::Scalar(TypedValue::Long(version))) if version > 0 && (version < u32::max_value() as i64) => Some((vocab, version as u32)), (_, _) => None, }) @@ -895,7 +898,8 @@ impl HasVocabularies for T where T: HasSchema + Queryable { .into_iter() .filter_map(|v| { match (&v[0], &v[1]) { - (&TypedValue::Ref(vocab), &TypedValue::Ref(attr)) => Some((vocab, attr)), + (&Binding::Scalar(TypedValue::Ref(vocab)), + &Binding::Scalar(TypedValue::Ref(attr))) => Some((vocab, attr)), (_, _) => None, } }); diff --git a/tests/cache.rs b/tests/cache.rs index e2adbc62..fe64cb36 100644 --- a/tests/cache.rs +++ b/tests/cache.rs @@ -27,6 +27,7 @@ use mentat::{ Queryable, Schema, Store, + Binding, TypedValue, }; @@ -187,7 +188,7 @@ fn test_fetch_attribute_value_for_entid() { let entities = store.q_once(r#"[:find ?e . :where [?e :foo/bar 100]]"#, None).expect("Expected query to work").into_scalar().expect("expected scalar results"); let entid = match entities { - Some(TypedValue::Ref(entid)) => entid, + Some(Binding::Scalar(TypedValue::Ref(entid))) => entid, x => panic!("expected Some(Ref), got {:?}", x), }; @@ -208,7 +209,7 @@ fn test_fetch_attribute_values_for_entid() { let entities = store.q_once(r#"[:find ?e . :where [?e :foo/bar 100]]"#, None).expect("Expected query to work").into_scalar().expect("expected scalar results"); let entid = match entities { - Some(TypedValue::Ref(entid)) => entid, + Some(Binding::Scalar(TypedValue::Ref(entid))) => entid, x => panic!("expected Some(Ref), got {:?}", x), }; diff --git a/tests/query.rs b/tests/query.rs index d192d9c5..2d10d557 100644 --- a/tests/query.rs +++ b/tests/query.rs @@ -30,7 +30,6 @@ use mentat_core::{ Entid, HasSchema, KnownEntid, - TypedValue, Utc, Uuid, ValueType, @@ -50,7 +49,9 @@ use mentat::{ QueryResults, RelResult, Store, + Binding, TxReport, + TypedValue, Variable, new_connection, }; @@ -132,7 +133,7 @@ fn test_scalar() { assert_eq!(1, results.len()); - if let QueryResults::Scalar(Some(TypedValue::Keyword(ref rc))) = results { + if let QueryResults::Scalar(Some(Binding::Scalar(TypedValue::Keyword(ref rc)))) = results { // Should be '24'. assert_eq!(&NamespacedKeyword::new("db.type", "keyword"), rc.as_ref()); assert_eq!(KnownEntid(24), @@ -166,7 +167,7 @@ fn test_tuple() { if let QueryResults::Tuple(Some(ref tuple)) = results { let cardinality_one = NamespacedKeyword::new("db.cardinality", "one"); assert_eq!(tuple.len(), 2); - assert_eq!(tuple[0], TypedValue::Boolean(true)); + assert_eq!(tuple[0], TypedValue::Boolean(true).into()); assert_eq!(tuple[1], db.schema.get_entid(&cardinality_one).expect("c1").into()); } else { panic!("Expected tuple."); @@ -214,7 +215,7 @@ fn test_inputs() { .expect("query to succeed") .results; - if let QueryResults::Scalar(Some(TypedValue::Keyword(value))) = results { + if let QueryResults::Scalar(Some(Binding::Scalar(TypedValue::Keyword(value)))) = results { assert_eq!(value.as_ref(), &NamespacedKeyword::new("db.install", "valueType")); } else { panic!("Expected scalar."); @@ -267,9 +268,9 @@ fn test_instants_and_uuids() { QueryResults::Tuple(Some(vals)) => { let mut vals = vals.into_iter(); match (vals.next(), vals.next(), vals.next(), vals.next()) { - (Some(TypedValue::Ref(e)), - Some(TypedValue::Uuid(u)), - Some(TypedValue::Instant(t)), + (Some(Binding::Scalar(TypedValue::Ref(e))), + Some(Binding::Scalar(TypedValue::Uuid(u))), + Some(Binding::Scalar(TypedValue::Instant(t))), None) => { assert!(e > 40); // There are at least this many entities in the store. assert_eq!(Ok(u), Uuid::from_str("cf62d552-6569-4d1b-b667-04703041dfc4")); @@ -384,9 +385,9 @@ fn test_fulltext() { QueryResults::Tuple(Some(vals)) => { let mut vals = vals.into_iter(); match (vals.next(), vals.next(), vals.next(), vals.next()) { - (Some(TypedValue::Ref(x)), - Some(TypedValue::String(text)), - Some(TypedValue::Double(score)), + (Some(Binding::Scalar(TypedValue::Ref(x))), + Some(Binding::Scalar(TypedValue::String(text))), + Some(Binding::Scalar(TypedValue::Double(score))), None) => { assert_eq!(x, v); assert_eq!(text.as_str(), "hello darkness my old friend"); @@ -447,11 +448,12 @@ fn test_fulltext() { .into(); match r { QueryResults::Rel(rels) => { - assert_eq!(rels, vec![ - vec![TypedValue::Ref(v), - TypedValue::String("I've come to talk with you again".to_string().into()), + let values: Vec> = rels.into_iter().collect(); + assert_eq!(values, vec![ + vec![Binding::Scalar(TypedValue::Ref(v)), + "I've come to talk with you again".into(), ] - ].into()); + ]); }, _ => panic!("Expected query to work."), } @@ -486,8 +488,8 @@ fn test_instant_range_query() { match r { QueryResults::Coll(vals) => { assert_eq!(vals, - vec![TypedValue::Ref(*ids.get("b").unwrap()), - TypedValue::Ref(*ids.get("c").unwrap())]); + vec![Binding::Scalar(TypedValue::Ref(*ids.get("b").unwrap())), + Binding::Scalar(TypedValue::Ref(*ids.get("c").unwrap()))]); }, _ => panic!("Expected query to work."), } @@ -628,7 +630,7 @@ fn test_aggregates_type_handling() { .unwrap(); // Our two transactions, the bootstrap transaction, plus the three values. - assert_eq!(TypedValue::Long(6), r); + assert_eq!(Binding::Scalar(TypedValue::Long(6)), r); // And you can min them, which returns an instant. let r = store.q_once(r#"[:find (min ?v) . @@ -639,7 +641,7 @@ fn test_aggregates_type_handling() { .unwrap(); let earliest = DateTime::parse_from_rfc3339("2017-01-01T11:00:00.000Z").unwrap().with_timezone(&Utc); - assert_eq!(TypedValue::Instant(earliest), r); + assert_eq!(Binding::Scalar(TypedValue::Instant(earliest)), r); let r = store.q_once(r#"[:find (sum ?v) . :where [_ _ ?v] [(long ?v)]]"#, @@ -650,7 +652,7 @@ fn test_aggregates_type_handling() { // Yes, the current version is in the store as a Long! let total = 30i64 + 20i64 + 10i64 + ::mentat_db::db::CURRENT_VERSION as i64; - assert_eq!(TypedValue::Long(total), r); + assert_eq!(Binding::Scalar(TypedValue::Long(total)), r); let r = store.q_once(r#"[:find (avg ?v) . :where [_ _ ?v] [(double ?v)]]"#, @@ -660,7 +662,7 @@ fn test_aggregates_type_handling() { .unwrap(); let avg = (6.4f64 / 3f64) + (4.4f64 / 3f64) + (2.4f64 / 3f64); - assert_eq!(TypedValue::Double(avg.into()), r); + assert_eq!(Binding::Scalar(TypedValue::Double(avg.into())), r); } #[test] @@ -700,7 +702,7 @@ fn test_type_reqs() { assert_eq!(res.width, 1); let entid = match res.into_iter().next().unwrap().into_iter().next().unwrap() { - TypedValue::Ref(eid) => { + Binding::Scalar(TypedValue::Ref(eid)) => { eid }, unexpected => { @@ -753,7 +755,7 @@ fn test_type_reqs() { .into(); match res { QueryResults::Coll(vals) => { - assert_eq!(vals, vec![TypedValue::Long(5), TypedValue::Long(33)]) + assert_eq!(vals, vec![Binding::Scalar(TypedValue::Long(5)), Binding::Scalar(TypedValue::Long(33))]) }, v => { panic!("Query returned unexpected type: {:?}", v); @@ -800,7 +802,7 @@ fn test_monster_head_aggregates() { .expect("results") .into(); match res { - QueryResults::Scalar(Some(TypedValue::Long(count))) => { + QueryResults::Scalar(Some(Binding::Scalar(TypedValue::Long(count)))) => { assert_eq!(count, 4); }, r => panic!("Unexpected result {:?}", r), @@ -811,7 +813,7 @@ fn test_monster_head_aggregates() { .expect("results") .into(); match res { - QueryResults::Scalar(Some(TypedValue::Long(count))) => { + QueryResults::Scalar(Some(Binding::Scalar(TypedValue::Long(count)))) => { assert_eq!(count, 6); }, r => panic!("Unexpected result {:?}", r), @@ -921,7 +923,7 @@ fn test_basic_aggregates() { .into(); match r { QueryResults::Scalar(Some(val)) => { - assert_eq!(val, TypedValue::Long(2)); + assert_eq!(val, Binding::Scalar(TypedValue::Long(2))); }, _ => panic!("Expected scalar."), } @@ -935,8 +937,8 @@ fn test_basic_aggregates() { match r { QueryResults::Tuple(Some(vals)) => { assert_eq!(vals, - vec![TypedValue::Long(14), - TypedValue::Long(42)]); + vec![Binding::Scalar(TypedValue::Long(14)), + Binding::Scalar(TypedValue::Long(42))]); }, _ => panic!("Expected tuple."), } @@ -952,8 +954,8 @@ fn test_basic_aggregates() { match r { QueryResults::Tuple(Some(vals)) => { assert_eq!(vals, - vec![TypedValue::String("Alice".to_string().into()), - TypedValue::Long(14)]); + vec!["Alice".into(), + Binding::Scalar(TypedValue::Long(14))]); }, r => panic!("Unexpected results {:?}", r), } @@ -969,8 +971,8 @@ fn test_basic_aggregates() { match r { QueryResults::Tuple(Some(vals)) => { assert_eq!(vals, - vec![TypedValue::String("Carlos".to_string().into()), - TypedValue::Long(42)]); + vec!["Carlos".into(), + Binding::Scalar(TypedValue::Long(42))]); }, _ => panic!("Expected tuple."), } @@ -1057,7 +1059,7 @@ fn test_combinatorial() { // How many different pairings of dancers were there? // If we just use `!=` (or `differ`), the number is doubled because of symmetry! - assert_eq!(TypedValue::Long(6), + assert_eq!(Binding::Scalar(TypedValue::Long(6)), store.q_once(r#"[:find (count ?right) . :with ?left :where @@ -1074,7 +1076,7 @@ fn test_combinatorial() { // will come to rely on it. Instead we expose a specific operator: `unpermute`. // When used in a query that generates permuted pairs of references, this // ensures that only one permutation is returned for a given pair. - assert_eq!(TypedValue::Long(3), + assert_eq!(Binding::Scalar(TypedValue::Long(3)), store.q_once(r#"[:find (count ?right) . :with ?left :where @@ -1240,7 +1242,7 @@ fn test_aggregation_implicit_grouping() { ]"#).unwrap().tempids; // How many different scores were there? - assert_eq!(TypedValue::Long(7), + assert_eq!(Binding::Scalar(TypedValue::Long(7)), store.q_once(r#"[:find (count ?score) . :where [?game :foo/score ?score]]"#, None) @@ -1249,7 +1251,7 @@ fn test_aggregation_implicit_grouping() { // How many different games resulted in scores? // '14' appears twice. - assert_eq!(TypedValue::Long(8), + assert_eq!(Binding::Scalar(TypedValue::Long(8)), store.q_once(r#"[:find (count ?score) . :with ?game :where @@ -1258,7 +1260,7 @@ fn test_aggregation_implicit_grouping() { .expect("scalar results").unwrap()); // Who's the highest-scoring vegetarian? - assert_eq!(vec!["Alice".into(), TypedValue::Long(99)], + assert_eq!(vec!["Alice".into(), Binding::Scalar(TypedValue::Long(99))], store.q_once(r#"[:find [(the ?name) (max ?score)] :where [?game :foo/score ?score] @@ -1270,11 +1272,11 @@ fn test_aggregation_implicit_grouping() { // We can't run an ambiguous correspondence. let res = store.q_once(r#"[:find [(the ?name) (min ?score) (max ?score)] - :where - [?game :foo/score ?score] - [?person :foo/play ?game] - [?person :foo/is-vegetarian true] - [?person :foo/name ?name]]"#, None); + :where + [?game :foo/score ?score] + [?person :foo/play ?game] + [?person :foo/is-vegetarian true] + [?person :foo/name ?name]]"#, None); match res { Result::Err( Error( @@ -1292,7 +1294,7 @@ fn test_aggregation_implicit_grouping() { } // Max scores for vegetarians. - let expected: RelResult = + let expected: RelResult = vec![vec!["Alice".into(), TypedValue::Long(99)], vec!["Beli".into(), TypedValue::Long(22)]].into(); assert_eq!(expected, @@ -1382,6 +1384,7 @@ fn test_tx_ids() { .into(); match r { QueryResults::Coll(txs) => { + let expected: Vec = expected.into_iter().map(|tv| tv.into()).collect(); assert_eq!(txs, expected); }, x => panic!("Got unexpected results {:?}", x), diff --git a/tests/vocabulary.rs b/tests/vocabulary.rs index a8de8f02..a0ecc538 100644 --- a/tests/vocabulary.rs +++ b/tests/vocabulary.rs @@ -49,6 +49,7 @@ use mentat::{ Queryable, RelResult, Store, + Binding, TypedValue, ValueType, }; @@ -212,8 +213,8 @@ fn test_add_vocab() { .into_tuple_result() .expect("query returns") .expect("a result"); - assert_eq!(ver_attr[0], TypedValue::Long(1)); - assert_eq!(ver_attr[1], TypedValue::typed_ns_keyword("foo", "bar")); + assert_eq!(ver_attr[0], TypedValue::Long(1).into()); + assert_eq!(ver_attr[1], TypedValue::typed_ns_keyword("foo", "bar").into()); // If we commit, it'll stick around. in_progress.commit().expect("commit succeeded"); @@ -227,8 +228,8 @@ fn test_add_vocab() { .into_tuple_result() .expect("query returns") .expect("a result"); - assert_eq!(ver_attr[0], TypedValue::Long(1)); - assert_eq!(ver_attr[1], TypedValue::typed_ns_keyword("foo", "bar")); + assert_eq!(ver_attr[0], TypedValue::Long(1).into()); + assert_eq!(ver_attr[1], TypedValue::typed_ns_keyword("foo", "bar").into()); // Scoped borrow of `conn`. { @@ -264,8 +265,8 @@ fn test_add_vocab() { .expect("query returns"); assert_eq!(actual_attributes, vec![ - TypedValue::typed_ns_keyword("foo", "bar"), - TypedValue::typed_ns_keyword("foo", "baz"), + TypedValue::typed_ns_keyword("foo", "bar").into(), + TypedValue::typed_ns_keyword("foo", "baz").into(), ]); // Now let's modify our vocabulary without bumping the version. This is invalid and will result @@ -336,10 +337,10 @@ fn test_add_vocab() { } /// A helper to turn rows from `[:find ?e ?a :where [?e ?a ?v]]` into a tuple. -fn ea(row: Vec) -> (KnownEntid, KnownEntid) { +fn ea(row: Vec) -> (KnownEntid, KnownEntid) { let mut row = row.into_iter(); match (row.next(), row.next()) { - (Some(TypedValue::Ref(e)), Some(TypedValue::Ref(a))) => { + (Some(Binding::Scalar(TypedValue::Ref(e))), Some(Binding::Scalar(TypedValue::Ref(a)))) => { (KnownEntid(e), KnownEntid(a)) }, _ => panic!("Incorrect query shape for 'ea' helper."), @@ -347,18 +348,20 @@ fn ea(row: Vec) -> (KnownEntid, KnownEntid) { } /// A helper to turn rows from `[:find ?a ?v :where [?e ?a ?v]]` into a tuple. -fn av(row: Vec) -> (KnownEntid, TypedValue) { +/// Panics if any of the values are maps or vecs. +fn av(row: Vec) -> (KnownEntid, TypedValue) { let mut row = row.into_iter(); match (row.next(), row.next()) { - (Some(TypedValue::Ref(a)), Some(v)) => { - (KnownEntid(a), v) + (Some(Binding::Scalar(TypedValue::Ref(a))), Some(v)) => { + (KnownEntid(a), v.val().unwrap()) }, _ => panic!("Incorrect query shape for 'av' helper."), } } /// A helper to turn rows from `[:find ?e ?v :where [?e ?a ?v]]` into a tuple. -fn ev(row: Vec) -> (KnownEntid, TypedValue) { +/// Panics if any of the values are maps or vecs. +fn ev(row: Vec) -> (KnownEntid, TypedValue) { // This happens to be the same as `av`. av(row) } @@ -382,7 +385,7 @@ fn height_of_person(in_progress: &InProgress, name: &str) -> Option { .into_scalar_result() .expect("result"); match h { - Some(TypedValue::Long(v)) => Some(v), + Some(Binding::Scalar(TypedValue::Long(v))) => Some(v), _ => None, } } @@ -585,7 +588,7 @@ fn test_upgrade_with_functions() { :order (asc ?year)]"#, None) .into_coll_result() .expect("coll"); - assert_eq!(years, vec![TypedValue::Long(1984), TypedValue::Long(2019)]); + assert_eq!(years, vec![Binding::Scalar(TypedValue::Long(1984)), Binding::Scalar(TypedValue::Long(2019))]); in_progress.commit().expect("commit succeeded"); } @@ -605,7 +608,7 @@ fn test_upgrade_with_functions() { .into_iter() { let mut row = row.into_iter(); match (row.next(), row.next()) { - (Some(TypedValue::Ref(person)), Some(TypedValue::Long(height))) => { + (Some(Binding::Scalar(TypedValue::Ref(person))), Some(Binding::Scalar(TypedValue::Long(height)))) => { // No need to explicitly retract: cardinality-one. builder.add(KnownEntid(person), person_height, TypedValue::Long(inches_to_cm(height)))?; }, @@ -674,7 +677,7 @@ fn test_upgrade_with_functions() { .into_iter() { let mut row = row.into_iter(); match (row.next(), row.next()) { - (Some(TypedValue::Ref(food)), Some(TypedValue::String(name))) => { + (Some(Binding::Scalar(TypedValue::Ref(food))), Some(Binding::Scalar(TypedValue::String(name)))) => { if name.chars().any(|c| !c.is_lowercase()) { let lowercased = name.to_lowercase(); println!("Need to rename {} from '{}' to '{}'", food, name, lowercased); @@ -707,7 +710,7 @@ fn test_upgrade_with_functions() { .into_iter() { let mut row = row.into_iter(); match (row.next(), row.next()) { - (Some(TypedValue::Ref(left)), Some(TypedValue::Ref(right))) => { + (Some(Binding::Scalar(TypedValue::Ref(left))), Some(Binding::Scalar(TypedValue::Ref(right)))) => { let keep = KnownEntid(left); let replace = KnownEntid(right); @@ -831,7 +834,7 @@ fn test_upgrade_with_functions() { [?f :food/name ?food]]"#, None) .into_coll_result() .expect("success"), - vec![TypedValue::typed_string("spice")]); + vec![TypedValue::typed_string("spice").into()]); // // Migration 4: multi-definition migration. @@ -1032,7 +1035,7 @@ fn test_upgrade_with_functions() { [?p :person/height ?height] [?p :person/name ?name]]"#; let r = store.q_once(q, None).into_rel_result().unwrap(); - let expected: RelResult = + let expected: RelResult = vec![vec![TypedValue::typed_string("Sam"), TypedValue::Long(162)], vec![TypedValue::typed_string("Beth"), TypedValue::Long(172)]].into(); assert_eq!(expected, r); @@ -1045,6 +1048,8 @@ fn test_upgrade_with_functions() { [?p :food/likes ?f] [?f :food/name ?food]]"#; let r = store.q_once(q, None).into_coll_result().unwrap(); - assert_eq!(vec![TypedValue::typed_string("spice"), TypedValue::typed_string("weird blue worms")], - r); + let expected: Vec = + vec![TypedValue::typed_string("spice").into(), + TypedValue::typed_string("weird blue worms").into()]; + assert_eq!(expected, r); } diff --git a/tools/cli/src/mentat_cli/repl.rs b/tools/cli/src/mentat_cli/repl.rs index 77fccccc..5ed13c6e 100644 --- a/tools/cli/src/mentat_cli/repl.rs +++ b/tools/cli/src/mentat_cli/repl.rs @@ -31,6 +31,7 @@ use mentat::{ QueryOutput, QueryResults, Store, + Binding, Syncable, TxReport, TypedValue, @@ -422,14 +423,14 @@ impl Repl { match query_output.results { QueryResults::Scalar(v) => { if let Some(val) = v { - writeln!(output, "| {}\t |", &self.typed_value_as_string(val))?; + writeln!(output, "| {}\t |", &self.binding_as_string(val))?; } }, QueryResults::Tuple(vv) => { if let Some(vals) = vv { for val in vals { - write!(output, "| {}\t", self.typed_value_as_string(val))?; + write!(output, "| {}\t", self.binding_as_string(val))?; } writeln!(output, "|")?; } @@ -437,14 +438,14 @@ impl Repl { QueryResults::Coll(vv) => { for val in vv { - writeln!(output, "| {}\t|", self.typed_value_as_string(val))?; + writeln!(output, "| {}\t|", self.binding_as_string(val))?; } }, QueryResults::Rel(vvv) => { for vv in vvv { for v in vv { - write!(output, "| {}\t", self.typed_value_as_string(v))?; + write!(output, "| {}\t", self.binding_as_string(v))?; } writeln!(output, "|")?; } @@ -511,16 +512,26 @@ impl Repl { Ok(report) } - fn typed_value_as_string(&self, value: TypedValue) -> String { + fn binding_as_string(&self, value: Binding) -> String { + use self::Binding::*; match value { - TypedValue::Boolean(b) => if b { "true".to_string() } else { "false".to_string() }, - TypedValue::Double(d) => format!("{}", d), - TypedValue::Instant(i) => format!("{}", i), - TypedValue::Keyword(k) => format!("{}", k), - TypedValue::Long(l) => format!("{}", l), - TypedValue::Ref(r) => format!("{}", r), - TypedValue::String(s) => format!("{:?}", s.to_string()), - TypedValue::Uuid(u) => format!("{}", u), + Scalar(v) => self.value_as_string(v), + Map(_) => format!("TODO"), + Vec(_) => format!("TODO"), + } + } + + fn value_as_string(&self, value: TypedValue) -> String { + use self::TypedValue::*; + match value { + Boolean(b) => if b { "true".to_string() } else { "false".to_string() }, + Double(d) => format!("{}", d), + Instant(i) => format!("{}", i), + Keyword(k) => format!("{}", k), + Long(l) => format!("{}", l), + Ref(r) => format!("{}", r), + String(s) => format!("{:?}", s.to_string()), + Uuid(u) => format!("{}", u), } } }