Small improvements accumulated while building the logins API on top of Mentat. (#779) r=grisha

These build on #778, and implement a variety of small fixes (related
parts are labelled as such), and one non-trivial part -- matching
tuple results with the `BindingTuple` trait. In practice, this is very
helpful, and greatly streamlined the logins API.
This commit is contained in:
Nick Alexander 2018-07-05 16:46:02 -07:00
commit 46f7db36c9
14 changed files with 407 additions and 28 deletions

View file

@ -8,9 +8,18 @@
// 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,
};
use ::std::ops::{
Deref,
};
use ::std::os::raw::c_char;
use ::std::rc::{
@ -282,12 +291,47 @@ impl From<Vec<Binding>> for Binding {
}
impl Binding {
pub fn val(self) -> Option<TypedValue> {
pub fn into_scalar(self) -> Option<TypedValue> {
match self {
Binding::Scalar(v) => Some(v),
_ => None,
}
}
pub fn into_vec(self) -> Option<ValueRc<Vec<Binding>>> {
match self {
Binding::Vec(v) => Some(v),
_ => None,
}
}
pub fn into_map(self) -> Option<ValueRc<StructuredMap>> {
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<Binding>> {
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
@ -303,6 +347,14 @@ impl Binding {
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct StructuredMap(pub IndexMap<ValueRc<Keyword>, Binding>);
impl Deref for StructuredMap {
type Target = IndexMap<ValueRc<Keyword>, Binding>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl StructuredMap {
pub fn insert<N, B>(&mut self, name: N, value: B) where N: Into<ValueRc<Keyword>>, B: Into<Binding> {
self.0.insert(name.into(), value.into());
@ -379,15 +431,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<S: AsRef<str>, T: AsRef<str>>(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: AsRef<str>>(s: S) -> TypedValue {
s.as_ref().into()
}
pub fn current_instant() -> TypedValue {
@ -736,6 +788,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<Keyword>> {
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<Utc>> {
match self {
&Binding::Scalar(TypedValue::Instant(ref v)) => Some(v),
_ => None,
}
}
pub fn as_string(&self) -> Option<&ValueRc<String>> {
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]

View file

@ -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,
};

View file

@ -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<PullAttributeSpec>,
}
#[derive(Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Aggregate {
pub func: QueryFunction,
pub args: Vec<FnArg>,
}
#[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<Element>),

View file

@ -649,6 +649,28 @@ impl ToMicros for DateTime<Utc> {
}
}
pub trait FromMillis {
fn from_millis(ts: i64) -> Self;
}
impl FromMillis for DateTime<Utc> {
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<Utc> {
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;

View file

@ -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<Vec<Binding>>) -> Result<Option<Self>>;
}
// This is a no-op, essentially: we can always produce a vector representation of a tuple result.
impl BindingTuple for Vec<Binding> {
fn from_binding_vec(expected: usize, vec: Option<Vec<Binding>>) -> Result<Option<Self>> {
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<Vec<Binding>>) -> Result<Option<Self>> {
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<Vec<Binding>>) -> Result<Option<Self>> {
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<Vec<Binding>>) -> Result<Option<Self>> {
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<Vec<Binding>>) -> Result<Option<Self>> {
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<Vec<Binding>>) -> Result<Option<Self>> {
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<Vec<Binding>>) -> Result<Option<Self>> {
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())))
}
}
}
}
}

View file

@ -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),

View file

@ -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<FindSpec>,
pub results: QueryResults,
@ -267,8 +271,15 @@ impl QueryOutput {
self.results.into_coll()
}
pub fn into_tuple(self) -> Result<Option<Vec<Binding>>> {
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<B>(self) -> Result<Option<B>> 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<RelResult<Binding>> {
@ -515,3 +526,46 @@ pub fn query_projection(schema: &Schema, query: &AlgebraicQuery) -> Result<Eithe
}.map(Either::Right)
}
}
#[test]
fn test_into_tuple() {
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(Some(vec![Binding::Scalar(TypedValue::Long(0)),
Binding::Scalar(TypedValue::Long(2))])),
};
assert_eq!(query_output.clone().into_tuple().expect("into_tuple"),
Some((Binding::Scalar(TypedValue::Long(0)),
Binding::Scalar(TypedValue::Long(2)))));
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"),
}
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"),
}
}

View file

@ -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<T>(&self,
sqlite: &rusqlite::Connection,

View file

@ -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,10 +111,19 @@ 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;
pub use mentat_query_projector::{
BindingTuple,
ProjectorError,
};
pub use mentat_query_pull::PullError;
pub use mentat_sql::SQLError;

View file

@ -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())
}
}

View file

@ -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 {
@ -609,7 +613,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 +682,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;

View file

@ -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)?;

View file

@ -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();

View file

@ -350,7 +350,7 @@ fn av(row: Vec<Binding>) -> (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."),
}