From d49f7025125dd3f023e0fcd87ee90dd02d1df025 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Mon, 18 Jun 2018 11:05:35 -0700 Subject: [PATCH 1/7] Part 1: Expand Binding::val() into Binding::{into_*,as_*}. This is simply for completeness: we should provide fundamental conversion patterns even when they are mostly unused in our code base. --- core/src/types.rs | 105 +++++++++++++++++++++++++++++++++++++++++--- src/query.rs | 4 +- src/store.rs | 4 +- src/vocabulary.rs | 2 +- tests/pull.rs | 2 +- tests/vocabulary.rs | 2 +- 6 files changed, 107 insertions(+), 12 deletions(-) diff --git a/core/src/types.rs b/core/src/types.rs index 992ddb38..7efc26f2 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -8,6 +8,10 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. +use ::std::convert::{ + AsRef, +}; + use ::std::ffi::{ CString, }; @@ -282,12 +286,47 @@ impl From> for Binding { } impl Binding { - pub fn val(self) -> Option { + pub fn into_scalar(self) -> Option { match self { Binding::Scalar(v) => Some(v), _ => None, } } + + pub fn into_vec(self) -> Option>> { + match self { + Binding::Vec(v) => Some(v), + _ => None, + } + } + + pub fn into_map(self) -> Option> { + match self { + Binding::Map(v) => Some(v), + _ => None, + } + } + + pub fn as_scalar(&self) -> Option<&TypedValue> { + match self { + &Binding::Scalar(ref v) => Some(v), + _ => None, + } + } + + pub fn as_vec(&self) -> Option<&Vec> { + match self { + &Binding::Vec(ref v) => Some(v), + _ => None, + } + } + + pub fn as_map(&self) -> Option<&StructuredMap> { + match self { + &Binding::Map(ref v) => Some(v), + _ => None, + } + } } /// A pull expression expands a binding into a structure. The returned structure @@ -379,15 +418,15 @@ impl TypedValue { /// Construct a new `TypedValue::Keyword` instance by cloning the provided /// values and wrapping them in a new `ValueRc`. This is expensive, so this might /// be best limited to tests. - pub fn typed_ns_keyword(ns: &str, name: &str) -> TypedValue { - Keyword::namespaced(ns, name).into() + pub fn typed_ns_keyword, T: AsRef>(ns: S, name: T) -> TypedValue { + Keyword::namespaced(ns.as_ref(), name.as_ref()).into() } /// Construct a new `TypedValue::String` instance by cloning the provided /// value and wrapping it in a new `ValueRc`. This is expensive, so this might /// be best limited to tests. - pub fn typed_string(s: &str) -> TypedValue { - s.into() + pub fn typed_string>(s: S) -> TypedValue { + s.as_ref().into() } pub fn current_instant() -> TypedValue { @@ -736,6 +775,62 @@ impl Binding { _ => None, } } + + pub fn as_entid(&self) -> Option<&Entid> { + match self { + &Binding::Scalar(TypedValue::Ref(ref v)) => Some(v), + _ => None, + } + } + + pub fn as_kw(&self) -> Option<&ValueRc> { + match self { + &Binding::Scalar(TypedValue::Keyword(ref v)) => Some(v), + _ => None, + } + } + + pub fn as_boolean(&self) -> Option<&bool> { + match self { + &Binding::Scalar(TypedValue::Boolean(ref v)) => Some(v), + _ => None, + } + } + + pub fn as_long(&self) -> Option<&i64> { + match self { + &Binding::Scalar(TypedValue::Long(ref v)) => Some(v), + _ => None, + } + } + + pub fn as_double(&self) -> Option<&f64> { + match self { + &Binding::Scalar(TypedValue::Double(ref v)) => Some(&v.0), + _ => None, + } + } + + pub fn as_instant(&self) -> Option<&DateTime> { + match self { + &Binding::Scalar(TypedValue::Instant(ref v)) => Some(v), + _ => None, + } + } + + pub fn as_string(&self) -> Option<&ValueRc> { + match self { + &Binding::Scalar(TypedValue::String(ref v)) => Some(v), + _ => None, + } + } + + pub fn as_uuid(&self) -> Option<&Uuid> { + match self { + &Binding::Scalar(TypedValue::Uuid(ref v)) => Some(v), + _ => None, + } + } } #[test] diff --git a/src/query.rs b/src/query.rs index 16fa3e3f..a4f4a979 100644 --- a/src/query.rs +++ b/src/query.rs @@ -234,7 +234,7 @@ pub fn lookup_value<'sqlite, 'schema, 'cache, E, A> 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())) + .map(|r| r.map(|v| v.into_scalar().unwrap())) } } @@ -256,7 +256,7 @@ pub fn lookup_values<'sqlite, E, A> 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()) + .map(|v| v.into_iter().map(|x| x.into_scalar().unwrap()).collect()) } } diff --git a/src/store.rs b/src/store.rs index 1cfce8bc..1614d0f5 100644 --- a/src/store.rs +++ b/src/store.rs @@ -609,7 +609,7 @@ mod tests { let mut builder = in_progress.builder().describe_tempid(&name); builder.add(kw!(:todo/uuid), TypedValue::Uuid(uuid)).expect("Expected added uuid"); changeset.insert(uuid_entid.clone()); - builder.add(kw!(:todo/name), TypedValue::typed_string(&name)).expect("Expected added name"); + builder.add(kw!(:todo/name), TypedValue::typed_string(name)).expect("Expected added name"); changeset.insert(name_entid.clone()); if i % 2 == 0 { builder.add(kw!(:todo/completion_date), TypedValue::current_instant()).expect("Expected added date"); @@ -678,7 +678,7 @@ mod tests { for i in 0..3 { let name = format!("label{}", i); let mut builder = in_progress.builder().describe_tempid(&name); - builder.add(kw!(:label/name), TypedValue::typed_string(&name)).expect("Expected added name"); + builder.add(kw!(:label/name), TypedValue::typed_string(name)).expect("Expected added name"); builder.add(kw!(:label/color), TypedValue::typed_string("blue")).expect("Expected added color"); let (ip, _) = builder.transact(); in_progress = ip; diff --git a/src/vocabulary.rs b/src/vocabulary.rs index 42674b5b..b3cb6f24 100644 --- a/src/vocabulary.rs +++ b/src/vocabulary.rs @@ -227,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").val().expect("typed value"); +/// let obsolete = r.next().expect("value").into_scalar().expect("typed value"); /// builder.retract(e, link_title, obsolete)?; /// } /// ip.transact_builder(builder)?; diff --git a/tests/pull.rs b/tests/pull.rs index a2c21f91..ba8a7570 100644 --- a/tests/pull.rs +++ b/tests/pull.rs @@ -86,7 +86,7 @@ fn test_simple_pull() { .expect("hoods") .into_iter() .map(|b| { - b.val().and_then(|tv| tv.into_entid()).expect("scalar") + b.into_scalar().and_then(|tv| tv.into_entid()).expect("scalar") }) .collect(); diff --git a/tests/vocabulary.rs b/tests/vocabulary.rs index e8f7ce5d..f5905a94 100644 --- a/tests/vocabulary.rs +++ b/tests/vocabulary.rs @@ -350,7 +350,7 @@ fn av(row: Vec) -> (KnownEntid, TypedValue) { let mut row = row.into_iter(); match (row.next(), row.next()) { (Some(Binding::Scalar(TypedValue::Ref(a))), Some(v)) => { - (KnownEntid(a), v.val().unwrap()) + (KnownEntid(a), v.into_scalar().unwrap()) }, _ => panic!("Incorrect query shape for 'av' helper."), } From b9f3681728185d12265d0a35d327db00496b3ea7 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Mon, 18 Jun 2018 14:52:16 -0700 Subject: [PATCH 2/7] Part 2: Allow to Deref StructuredMap to the underlying IndexMap. Again, this is a fundamental Rust pattern for newtypes. It's awfully hard to actually use `StructuredMap` without it! --- core/src/types.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/src/types.rs b/core/src/types.rs index 7efc26f2..15aec5ee 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -15,6 +15,11 @@ use ::std::convert::{ use ::std::ffi::{ CString, }; + +use ::std::ops::{ + Deref, +}; + use ::std::os::raw::c_char; use ::std::rc::{ @@ -342,6 +347,14 @@ impl Binding { #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct StructuredMap(pub IndexMap, Binding>); +impl Deref for StructuredMap { + type Target = IndexMap, Binding>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl StructuredMap { pub fn insert(&mut self, name: N, value: B) where N: Into>, B: Into { self.0.insert(name.into(), value.into()); From 3744982cd9d53c71aec182e254d1d5b143188f62 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Wed, 2 May 2018 15:38:12 -0700 Subject: [PATCH 3/7] Add `last_tx_id`. --- src/conn.rs | 16 ++++++++++++++++ src/store.rs | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/src/conn.rs b/src/conn.rs index eb54dbbf..bc351244 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -387,6 +387,11 @@ impl<'a, 'c> HasSchema for InProgress<'a, 'c> { } } +impl<'a, 'c> InProgressRead<'a, 'c> { + pub fn last_tx_id(&self) -> Entid { + self.0.last_tx_id() + } +} impl<'a, 'c> InProgress<'a, 'c> { pub fn builder(self) -> InProgressBuilder<'a, 'c> { @@ -526,6 +531,10 @@ impl<'a, 'c> InProgress<'a, 'c> { }, } } + + pub fn last_tx_id(&self) -> Entid { + self.partition_map[":db.part/tx"].index - 1 + } } struct InProgressTransactWatcher<'a, 'o> { @@ -617,6 +626,13 @@ impl Conn { self.metadata.lock().unwrap().attribute_cache.clone() } + pub fn last_tx_id(&self) -> Entid { + // The mutex is taken during this entire method. + let metadata = self.metadata.lock().unwrap(); + + metadata.partition_map[":db.part/tx"].index - 1 + } + /// Query the Mentat store, using the given connection and the current metadata. pub fn q_once(&self, sqlite: &rusqlite::Connection, diff --git a/src/store.rs b/src/store.rs index 1614d0f5..e3b269e0 100644 --- a/src/store.rs +++ b/src/store.rs @@ -191,6 +191,10 @@ impl Store { pub fn unregister_observer(&mut self, key: &String) { self.conn.unregister_observer(key); } + + pub fn last_tx_id(&self) -> Entid { + self.conn.last_tx_id() + } } impl Queryable for Store { From 1c0602fa00b58299ba21740d1b76bf1312b5d5ee Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Tue, 19 Jun 2018 14:01:45 -0700 Subject: [PATCH 4/7] Part 1: Add {From,To}Millis. I think this is just oversight. Generally, we should anticipate what our consumers need to do to interact with Mentat, and producing milli- and micro-second timestamps is part of that need. --- edn/src/lib.rs | 2 ++ edn/src/types.rs | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/edn/src/lib.rs b/edn/src/lib.rs index f9c57c94..2d3d7037 100644 --- a/edn/src/lib.rs +++ b/edn/src/lib.rs @@ -57,9 +57,11 @@ pub use parse::ParseError; pub use uuid::ParseError as UuidParseError; pub use types::{ FromMicros, + FromMillis, Span, SpannedValue, ToMicros, + ToMillis, Value, ValueAndSpan, }; diff --git a/edn/src/types.rs b/edn/src/types.rs index 27b46e4c..51319db1 100644 --- a/edn/src/types.rs +++ b/edn/src/types.rs @@ -649,6 +649,28 @@ impl ToMicros for DateTime { } } +pub trait FromMillis { + fn from_millis(ts: i64) -> Self; +} + +impl FromMillis for DateTime { + fn from_millis(ts: i64) -> Self { + Utc.timestamp(ts / 1_000, ((ts % 1_000).abs() as u32) * 1_000) + } +} + +pub trait ToMillis { + fn to_millis(&self) -> i64; +} + +impl ToMillis for DateTime { + fn to_millis(&self) -> i64 { + let major: i64 = self.timestamp() * 1_000; + let minor: i64 = self.timestamp_subsec_millis() as i64; + major + minor + } +} + #[cfg(test)] mod test { extern crate chrono; From 2ab481f83ec0fe7b91a198f8185f8521d588a205 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Tue, 3 Jul 2018 14:18:06 -0700 Subject: [PATCH 5/7] Part 2: Expose time related things at top-level. Perhaps we actually want to subdivide the top-level namespace so that there is a `mentat::time` module, but I'd prefer to make part of the process of fixing the public API as we get ready to christen version 1.0. --- src/lib.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f1da47a9..35eafb05 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,18 +34,20 @@ extern crate mentat_tolstoy; pub use mentat_core::{ Attribute, - Entid, - DateTime, - HasSchema, - KnownEntid, - Keyword, - Schema, Binding, + DateTime, + Entid, + HasSchema, + Keyword, + KnownEntid, + Schema, + StructuredMap, TxReport, TypedValue, - Uuid, Utc, + Uuid, ValueType, + now, }; pub use mentat_query::{ @@ -109,7 +111,13 @@ pub use errors::{ Result, }; -pub use edn::ParseError; +pub use edn::{ + FromMicros, + FromMillis, + ParseError, + ToMicros, + ToMillis, +}; pub use mentat_db::DbError; pub use mentat_query_algebrizer::AlgebrizerError; pub use mentat_query_projector::ProjectorError; From e362ca621361a15e9fec2042960bdb122c801cbe Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Thu, 28 Jun 2018 12:21:41 -0700 Subject: [PATCH 6/7] Part 1: Allow to clone useful query structures. --- edn/src/query.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/edn/src/query.rs b/edn/src/query.rs index ca8ec76b..e4ba9f43 100644 --- a/edn/src/query.rs +++ b/edn/src/query.rs @@ -545,19 +545,19 @@ impl std::fmt::Display for PullAttributeSpec { } -#[derive(Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Pull { pub var: Variable, pub patterns: Vec, } -#[derive(Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Aggregate { pub func: QueryFunction, pub args: Vec, } -#[derive(Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Element { Variable(Variable), Aggregate(Aggregate), @@ -650,7 +650,7 @@ pub enum Limit { /// # } /// ``` /// -#[derive(Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum FindSpec { /// Returns an array of arrays, represented as a single array with length a multiple of width. FindRel(Vec), From 2cb7d441dca8e403a2c90823896cbc61b88c45b7 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Thu, 28 Jun 2018 12:15:26 -0700 Subject: [PATCH 7/7] Part 2: Make it easier to match tuple results. Right now, we write code like ```rust match q_once(q, inputs)?.into_tuple()? { Some(vs) => match (vs.len(), vs.get(0), vs.get(1)) { (2, &Some(Binding::Scalar(TypedValue::Long(a))), &Some(Binding::Scalar(TypedValue::Instant(ref b)))) => Some((a, b.clone())), _ => panic!(), }, None => None, } ``` to length-check tuples coming out of the database. It can also lead to a lot of cloning because references are the easiest thing to hand. This commit allows to write code like ```rust match q_once(q, inputs)?.into_tuple()? { Some((Binding::Scalar(TypedValue::Long(a)), Binding::Scalar(TypedValue::Instant(b)))) => Some((a, b)), Some(_) => panic!(), None => None, } ``` which is generally much easier to reason about. --- query-projector/src/binding_tuple.rs | 159 +++++++++++++++++++++++++++ query-projector/src/errors.rs | 3 + query-projector/src/lib.rs | 60 +++++++++- src/lib.rs | 5 +- 4 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 query-projector/src/binding_tuple.rs diff --git a/query-projector/src/binding_tuple.rs b/query-projector/src/binding_tuple.rs new file mode 100644 index 00000000..5af2abd6 --- /dev/null +++ b/query-projector/src/binding_tuple.rs @@ -0,0 +1,159 @@ +// Copyright 2018 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. + +use mentat_core::{ + Binding, +}; + +use errors::{ + ProjectorError, + Result, +}; + +/// A `BindingTuple` is any type that can accommodate a Mentat tuple query result of fixed length. +/// +/// Currently Rust tuples of length 1 through 6 (i.e., `(A)` through `(A, B, C, D, E, F)`) are +/// supported as are vectors (i.e., `Vec<>`). +pub trait BindingTuple: Sized { + fn from_binding_vec(expected: usize, vec: Option>) -> Result>; +} + +// This is a no-op, essentially: we can always produce a vector representation of a tuple result. +impl BindingTuple for Vec { + fn from_binding_vec(expected: usize, vec: Option>) -> Result> { + match vec { + None => Ok(None), + Some(vec) => { + if expected != vec.len() { + Err(ProjectorError::UnexpectedResultsTupleLength(expected, vec.len())) + } else { + Ok(Some(vec)) + } + }, + } + } +} + +// TODO: generate these repetitive implementations with a little macro. +impl BindingTuple for (Binding,) { + fn from_binding_vec(expected: usize, vec: Option>) -> Result> { + if expected != 1 { + return Err(ProjectorError::UnexpectedResultsTupleLength(1, expected)); + } + match vec { + None => Ok(None), + Some(vec) => { + if expected != vec.len() { + Err(ProjectorError::UnexpectedResultsTupleLength(expected, vec.len())) + } else { + let mut iter = vec.into_iter(); + Ok(Some((iter.next().unwrap(),))) + } + } + } + } +} + +impl BindingTuple for (Binding, Binding) { + fn from_binding_vec(expected: usize, vec: Option>) -> Result> { + if expected != 2 { + return Err(ProjectorError::UnexpectedResultsTupleLength(2, expected)); + } + match vec { + None => Ok(None), + Some(vec) => { + if expected != vec.len() { + Err(ProjectorError::UnexpectedResultsTupleLength(expected, vec.len())) + } else { + let mut iter = vec.into_iter(); + Ok(Some((iter.next().unwrap(), iter.next().unwrap()))) + } + } + } + } +} + +impl BindingTuple for (Binding, Binding, Binding) { + fn from_binding_vec(expected: usize, vec: Option>) -> Result> { + if expected != 3 { + return Err(ProjectorError::UnexpectedResultsTupleLength(3, expected)); + } + match vec { + None => Ok(None), + Some(vec) => { + if expected != vec.len() { + Err(ProjectorError::UnexpectedResultsTupleLength(expected, vec.len())) + } else { + let mut iter = vec.into_iter(); + Ok(Some((iter.next().unwrap(), iter.next().unwrap(), iter.next().unwrap()))) + } + } + } + } +} + +impl BindingTuple for (Binding, Binding, Binding, Binding) { + fn from_binding_vec(expected: usize, vec: Option>) -> Result> { + if expected != 4 { + return Err(ProjectorError::UnexpectedResultsTupleLength(4, expected)); + } + match vec { + None => Ok(None), + Some(vec) => { + if expected != vec.len() { + Err(ProjectorError::UnexpectedResultsTupleLength(expected, vec.len())) + } else { + let mut iter = vec.into_iter(); + Ok(Some((iter.next().unwrap(), iter.next().unwrap(), iter.next().unwrap(), iter.next().unwrap()))) + } + } + } + } +} + +impl BindingTuple for (Binding, Binding, Binding, Binding, Binding) { + fn from_binding_vec(expected: usize, vec: Option>) -> Result> { + if expected != 5 { + return Err(ProjectorError::UnexpectedResultsTupleLength(5, expected)); + } + match vec { + None => Ok(None), + Some(vec) => { + if expected != vec.len() { + Err(ProjectorError::UnexpectedResultsTupleLength(expected, vec.len())) + } else { + let mut iter = vec.into_iter(); + Ok(Some((iter.next().unwrap(), iter.next().unwrap(), iter.next().unwrap(), iter.next().unwrap(), iter.next().unwrap()))) + } + } + } + } +} + +// TODO: allow binding tuples of length more than 6. Folks who are binding such large tuples are +// probably doing something wrong -- they should investigate a pull expression. +impl BindingTuple for (Binding, Binding, Binding, Binding, Binding, Binding) { + fn from_binding_vec(expected: usize, vec: Option>) -> Result> { + if expected != 6 { + return Err(ProjectorError::UnexpectedResultsTupleLength(6, expected)); + } + match vec { + None => Ok(None), + Some(vec) => { + if expected != vec.len() { + Err(ProjectorError::UnexpectedResultsTupleLength(expected, vec.len())) + } else { + let mut iter = vec.into_iter(); + Ok(Some((iter.next().unwrap(), iter.next().unwrap(), iter.next().unwrap(), iter.next().unwrap(), iter.next().unwrap(), iter.next().unwrap()))) + } + } + } + } +} diff --git a/query-projector/src/errors.rs b/query-projector/src/errors.rs index c27a74d5..040a6f63 100644 --- a/query-projector/src/errors.rs +++ b/query-projector/src/errors.rs @@ -59,6 +59,9 @@ pub enum ProjectorError { #[fail(display = "expected {}, got {}", _0, _1)] UnexpectedResultsType(&'static str, &'static str), + #[fail(display = "expected tuple of length {}, got tuple of length {}", _0, _1)] + UnexpectedResultsTupleLength(usize, usize), + #[fail(display = "min/max expressions: {} (max 1), corresponding: {}", _0, _1)] AmbiguousAggregates(usize, usize), diff --git a/query-projector/src/lib.rs b/query-projector/src/lib.rs index de80ca8e..0e345fe7 100644 --- a/query-projector/src/lib.rs +++ b/query-projector/src/lib.rs @@ -73,6 +73,10 @@ use mentat_query_sql::{ pub mod errors; mod aggregates; +mod binding_tuple; +pub use binding_tuple::{ + BindingTuple, +}; mod project; mod projectors; mod pull; @@ -117,7 +121,7 @@ pub use errors::{ Result, }; -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct QueryOutput { pub spec: Rc, pub results: QueryResults, @@ -267,8 +271,15 @@ impl QueryOutput { self.results.into_coll() } - pub fn into_tuple(self) -> Result>> { - self.results.into_tuple() + /// Mentat tuple results can be expressed as multiple different data structures. Some + /// structures are generic (vectors) and some are easier for pattern matching (fixed length + /// tuples). + /// + /// This is the moral equivalent of `collect` (and `BindingTuple` of `FromIterator`), but + /// specialized to tuples of expected length. + pub fn into_tuple(self) -> Result> where B: BindingTuple { + let expected = self.spec.expected_column_count(); + self.results.into_tuple().and_then(|vec| B::from_binding_vec(expected, vec)) } pub fn into_rel(self) -> Result> { @@ -515,3 +526,46 @@ pub fn query_projection(schema: &Schema, query: &AlgebraicQuery) -> Result { + assert_eq!((expected, got), (3, 2)); + }, + // This forces the result type. + Ok(Some((_, _, _))) | _ => panic!("expected error"), + } + + let query_output = QueryOutput { + spec: Rc::new(FindSpec::FindTuple(vec![Element::Variable(Variable::from_valid_name("?x")), + Element::Variable(Variable::from_valid_name("?y"))])), + results: QueryResults::Tuple(None), + }; + + + match query_output.clone().into_tuple() { + Ok(None) => {}, + // This forces the result type. + Ok(Some((_, _))) | _ => panic!("expected error"), + } + + match query_output.clone().into_tuple() { + Err(ProjectorError::UnexpectedResultsTupleLength(expected, got)) => { + assert_eq!((expected, got), (3, 2)); + }, + // This forces the result type. + Ok(Some((_, _, _))) | _ => panic!("expected error"), + } +} diff --git a/src/lib.rs b/src/lib.rs index 35eafb05..78650a99 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,7 +120,10 @@ pub use edn::{ }; pub use mentat_db::DbError; pub use mentat_query_algebrizer::AlgebrizerError; -pub use mentat_query_projector::ProjectorError; +pub use mentat_query_projector::{ + BindingTuple, + ProjectorError, +}; pub use mentat_query_pull::PullError; pub use mentat_sql::SQLError;