Define Store, use TabWriter in the CLI for aligning columnar output. (#540) r=emily

* Define Store, which is a simple container for a SQLite connection and a Conn.
  This is a breaking change.
* Return the FindSpec as part of QueryOutput, not just results.
* Switch to using stderr in appropriate places in CLI.
* Print columns in CLI output.
This commit is contained in:
Richard Newman 2018-01-30 14:11:41 -08:00
parent 37a7c9ea48
commit 66e6fef75e
13 changed files with 395 additions and 247 deletions

View file

@ -8,7 +8,7 @@ authors = [
"Emily Toop <etoop@mozilla.com>",
]
name = "mentat"
version = "0.5.1"
version = "0.6.0"
build = "build/version.rs"
[workspace]

View file

@ -20,6 +20,7 @@ extern crate mentat_query;
use std::collections::BTreeSet;
use std::ops::Sub;
use std::rc::Rc;
mod errors;
mod types;
@ -61,7 +62,7 @@ pub use types::{
#[derive(Debug)]
pub struct AlgebraicQuery {
default_source: SrcVar,
pub find_spec: FindSpec,
pub find_spec: Rc<FindSpec>,
has_aggregates: bool,
pub with: BTreeSet<Variable>,
pub order: Option<Vec<OrderBy>>,
@ -192,7 +193,7 @@ pub fn algebrize_with_inputs(schema: &Schema,
let limit = if parsed.find_spec.is_unit_limited() { Limit::Fixed(1) } else { parsed.limit };
let q = AlgebraicQuery {
default_source: parsed.default_source,
find_spec: parsed.find_spec,
find_spec: Rc::new(parsed.find_spec),
has_aggregates: false, // TODO: we don't parse them yet.
with: with,
order: order,

View file

@ -20,6 +20,8 @@ extern crate mentat_query_sql;
extern crate mentat_sql;
use std::iter;
use std::rc::Rc;
use rusqlite::{
Row,
Rows,
@ -78,6 +80,12 @@ error_chain! {
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct QueryOutput {
pub spec: Rc<FindSpec>,
pub results: QueryResults,
}
#[derive(Debug, PartialEq, Eq)]
pub enum QueryResults {
Scalar(Option<TypedValue>),
@ -86,6 +94,63 @@ pub enum QueryResults {
Rel(Vec<Vec<TypedValue>>),
}
impl From<QueryOutput> for QueryResults {
fn from(o: QueryOutput) -> QueryResults {
o.results
}
}
impl QueryOutput {
pub fn empty_factory(spec: &FindSpec) -> Box<Fn() -> QueryResults> {
use self::FindSpec::*;
match spec {
&FindScalar(_) => Box::new(|| QueryResults::Scalar(None)),
&FindTuple(_) => Box::new(|| QueryResults::Tuple(None)),
&FindColl(_) => Box::new(|| QueryResults::Coll(vec![])),
&FindRel(_) => Box::new(|| QueryResults::Rel(vec![])),
}
}
pub fn len(&self) -> usize {
self.results.len()
}
pub fn is_empty(&self) -> bool {
self.results.is_empty()
}
pub fn empty(spec: &Rc<FindSpec>) -> QueryOutput {
use self::FindSpec::*;
let results =
match &**spec {
&FindScalar(_) => QueryResults::Scalar(None),
&FindTuple(_) => QueryResults::Tuple(None),
&FindColl(_) => QueryResults::Coll(vec![]),
&FindRel(_) => QueryResults::Rel(vec![]),
};
QueryOutput {
spec: spec.clone(),
results: results,
}
}
pub fn into_scalar(self) -> Result<Option<TypedValue>> {
self.results.into_scalar()
}
pub fn into_coll(self) -> Result<Vec<TypedValue>> {
self.results.into_coll()
}
pub fn into_tuple(self) -> Result<Option<Vec<TypedValue>>> {
self.results.into_tuple()
}
pub fn into_rel(self) -> Result<Vec<Vec<TypedValue>>> {
self.results.into_rel()
}
}
impl QueryResults {
pub fn len(&self) -> usize {
use QueryResults::*;
@ -107,26 +172,6 @@ impl QueryResults {
}
}
pub fn empty(spec: &FindSpec) -> QueryResults {
use self::FindSpec::*;
match spec {
&FindScalar(_) => QueryResults::Scalar(None),
&FindTuple(_) => QueryResults::Tuple(None),
&FindColl(_) => QueryResults::Coll(vec![]),
&FindRel(_) => QueryResults::Rel(vec![]),
}
}
pub fn empty_factory(spec: &FindSpec) -> Box<Fn() -> QueryResults> {
use self::FindSpec::*;
match spec {
&FindScalar(_) => Box::new(|| QueryResults::Scalar(None)),
&FindTuple(_) => Box::new(|| QueryResults::Tuple(None)),
&FindColl(_) => Box::new(|| QueryResults::Coll(vec![])),
&FindRel(_) => Box::new(|| QueryResults::Rel(vec![])),
}
}
pub fn into_scalar(self) -> Result<Option<TypedValue>> {
match self {
QueryResults::Scalar(o) => Ok(o),
@ -304,69 +349,87 @@ fn project_elements<'a, I: IntoIterator<Item = &'a Element>>(
}
pub trait Projector {
fn project<'stmt>(&self, rows: Rows<'stmt>) -> Result<QueryResults>;
fn project<'stmt>(&self, rows: Rows<'stmt>) -> Result<QueryOutput>;
}
/// A projector that produces a `QueryResult` containing fixed data.
/// Takes a boxed function that should return an empty result set of the desired type.
struct ConstantProjector {
spec: Rc<FindSpec>,
results_factory: Box<Fn() -> QueryResults>,
}
impl ConstantProjector {
fn new(results_factory: Box<Fn() -> QueryResults>) -> ConstantProjector {
ConstantProjector { results_factory: results_factory }
fn new(spec: Rc<FindSpec>, results_factory: Box<Fn() -> QueryResults>) -> ConstantProjector {
ConstantProjector {
spec: spec,
results_factory: results_factory,
}
}
}
impl Projector for ConstantProjector {
fn project<'stmt>(&self, _: Rows<'stmt>) -> Result<QueryResults> {
Ok((self.results_factory)())
fn project<'stmt>(&self, _: Rows<'stmt>) -> Result<QueryOutput> {
let results = (self.results_factory)();
let spec = self.spec.clone();
Ok(QueryOutput {
spec: spec,
results: results,
})
}
}
struct ScalarProjector {
spec: Rc<FindSpec>,
template: TypedIndex,
}
impl ScalarProjector {
fn with_template(template: TypedIndex) -> ScalarProjector {
fn with_template(spec: Rc<FindSpec>, template: TypedIndex) -> ScalarProjector {
ScalarProjector {
spec: spec,
template: template,
}
}
fn combine(sql: Projection, mut templates: Vec<TypedIndex>) -> Result<CombinedProjection> {
fn combine(spec: Rc<FindSpec>, sql: Projection, mut templates: Vec<TypedIndex>) -> Result<CombinedProjection> {
let template = templates.pop().expect("Expected a single template");
Ok(CombinedProjection {
sql_projection: sql,
datalog_projector: Box::new(ScalarProjector::with_template(template)),
datalog_projector: Box::new(ScalarProjector::with_template(spec, template)),
distinct: false,
})
}
}
impl Projector for ScalarProjector {
fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result<QueryResults> {
if let Some(r) = rows.next() {
let row = r?;
let binding = self.template.lookup(&row)?;
Ok(QueryResults::Scalar(Some(binding)))
} else {
Ok(QueryResults::Scalar(None))
}
fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result<QueryOutput> {
let results =
if let Some(r) = rows.next() {
let row = r?;
let binding = self.template.lookup(&row)?;
QueryResults::Scalar(Some(binding))
} else {
QueryResults::Scalar(None)
};
Ok(QueryOutput {
spec: self.spec.clone(),
results: results,
})
}
}
/// A tuple projector produces a single vector. It's the single-result version of rel.
struct TupleProjector {
spec: Rc<FindSpec>,
len: usize,
templates: Vec<TypedIndex>,
}
impl TupleProjector {
fn with_templates(len: usize, templates: Vec<TypedIndex>) -> TupleProjector {
fn with_templates(spec: Rc<FindSpec>, len: usize, templates: Vec<TypedIndex>) -> TupleProjector {
TupleProjector {
spec: spec,
len: len,
templates: templates,
}
@ -382,8 +445,8 @@ impl TupleProjector {
.collect::<Result<Vec<TypedValue>>>()
}
fn combine(column_count: usize, sql: Projection, templates: Vec<TypedIndex>) -> Result<CombinedProjection> {
let p = TupleProjector::with_templates(column_count, templates);
fn combine(spec: Rc<FindSpec>, column_count: usize, sql: Projection, templates: Vec<TypedIndex>) -> Result<CombinedProjection> {
let p = TupleProjector::with_templates(spec, column_count, templates);
Ok(CombinedProjection {
sql_projection: sql,
datalog_projector: Box::new(p),
@ -393,14 +456,19 @@ impl TupleProjector {
}
impl Projector for TupleProjector {
fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result<QueryResults> {
if let Some(r) = rows.next() {
let row = r?;
let bindings = self.collect_bindings(row)?;
Ok(QueryResults::Tuple(Some(bindings)))
} else {
Ok(QueryResults::Tuple(None))
}
fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result<QueryOutput> {
let results =
if let Some(r) = rows.next() {
let row = r?;
let bindings = self.collect_bindings(row)?;
QueryResults::Tuple(Some(bindings))
} else {
QueryResults::Tuple(None)
};
Ok(QueryOutput {
spec: self.spec.clone(),
results: results,
})
}
}
@ -410,13 +478,15 @@ impl Projector for TupleProjector {
/// Each column in the inner vector is the result of taking one or two columns from
/// the `Row`: one for the value and optionally one for the type tag.
struct RelProjector {
spec: Rc<FindSpec>,
len: usize,
templates: Vec<TypedIndex>,
}
impl RelProjector {
fn with_templates(len: usize, templates: Vec<TypedIndex>) -> RelProjector {
fn with_templates(spec: Rc<FindSpec>, len: usize, templates: Vec<TypedIndex>) -> RelProjector {
RelProjector {
spec: spec,
len: len,
templates: templates,
}
@ -431,8 +501,8 @@ impl RelProjector {
.collect::<Result<Vec<TypedValue>>>()
}
fn combine(column_count: usize, sql: Projection, templates: Vec<TypedIndex>) -> Result<CombinedProjection> {
let p = RelProjector::with_templates(column_count, templates);
fn combine(spec: Rc<FindSpec>, column_count: usize, sql: Projection, templates: Vec<TypedIndex>) -> Result<CombinedProjection> {
let p = RelProjector::with_templates(spec, column_count, templates);
Ok(CombinedProjection {
sql_projection: sql,
datalog_projector: Box::new(p),
@ -442,49 +512,57 @@ impl RelProjector {
}
impl Projector for RelProjector {
fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result<QueryResults> {
fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result<QueryOutput> {
let mut out: Vec<Vec<TypedValue>> = vec![];
while let Some(r) = rows.next() {
let row = r?;
let bindings = self.collect_bindings(row)?;
out.push(bindings);
}
Ok(QueryResults::Rel(out))
Ok(QueryOutput {
spec: self.spec.clone(),
results: QueryResults::Rel(out),
})
}
}
/// A coll projector produces a vector of values.
/// Each value is sourced from the same column.
struct CollProjector {
spec: Rc<FindSpec>,
template: TypedIndex,
}
impl CollProjector {
fn with_template(template: TypedIndex) -> CollProjector {
fn with_template(spec: Rc<FindSpec>, template: TypedIndex) -> CollProjector {
CollProjector {
spec: spec,
template: template,
}
}
fn combine(sql: Projection, mut templates: Vec<TypedIndex>) -> Result<CombinedProjection> {
fn combine(spec: Rc<FindSpec>, sql: Projection, mut templates: Vec<TypedIndex>) -> Result<CombinedProjection> {
let template = templates.pop().expect("Expected a single template");
Ok(CombinedProjection {
sql_projection: sql,
datalog_projector: Box::new(CollProjector::with_template(template)),
datalog_projector: Box::new(CollProjector::with_template(spec, template)),
distinct: true,
})
}
}
impl Projector for CollProjector {
fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result<QueryResults> {
fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result<QueryOutput> {
let mut out: Vec<TypedValue> = vec![];
while let Some(r) = rows.next() {
let row = r?;
let binding = self.template.lookup(&row)?;
out.push(binding);
}
Ok(QueryResults::Coll(out))
Ok(QueryOutput {
spec: self.spec.clone(),
results: QueryResults::Coll(out),
})
}
}
@ -523,37 +601,38 @@ impl CombinedProjection {
pub fn query_projection(query: &AlgebraicQuery) -> Result<CombinedProjection> {
use self::FindSpec::*;
let spec = query.find_spec.clone();
if query.is_known_empty() {
// Do a few gyrations to produce empty results of the right kind for the query.
let empty = QueryResults::empty_factory(&query.find_spec);
let constant_projector = ConstantProjector::new(empty);
let empty = QueryOutput::empty_factory(&spec);
let constant_projector = ConstantProjector::new(spec, empty);
Ok(CombinedProjection {
sql_projection: Projection::One,
datalog_projector: Box::new(constant_projector),
distinct: false,
})
} else {
match query.find_spec {
match *query.find_spec {
FindColl(ref element) => {
let (cols, templates) = project_elements(1, iter::once(element), query)?;
CollProjector::combine(cols, templates).map(|p| p.flip_distinct_for_limit(&query.limit))
CollProjector::combine(spec, cols, templates).map(|p| p.flip_distinct_for_limit(&query.limit))
},
FindScalar(ref element) => {
let (cols, templates) = project_elements(1, iter::once(element), query)?;
ScalarProjector::combine(cols, templates)
ScalarProjector::combine(spec, cols, templates)
},
FindRel(ref elements) => {
let column_count = query.find_spec.expected_column_count();
let (cols, templates) = project_elements(column_count, elements, query)?;
RelProjector::combine(column_count, cols, templates).map(|p| p.flip_distinct_for_limit(&query.limit))
RelProjector::combine(spec, column_count, cols, templates).map(|p| p.flip_distinct_for_limit(&query.limit))
},
FindTuple(ref elements) => {
let column_count = query.find_spec.expected_column_count();
let (cols, templates) = project_elements(column_count, elements, query)?;
TupleProjector::combine(column_count, cols, templates)
TupleProjector::combine(spec, column_count, cols, templates)
},
}
}

View file

@ -56,7 +56,7 @@ use query::{
q_explain,
QueryExplanation,
QueryInputs,
QueryResults,
QueryOutput,
};
use entity_builder::{
@ -104,10 +104,28 @@ pub struct Conn {
// the schema changes. #315.
}
/// A convenience wrapper around a single SQLite connection and a Conn. This is suitable
/// for applications that don't require complex connection management.
pub struct Store {
conn: Conn,
sqlite: rusqlite::Connection,
}
impl Store {
pub fn open(path: &str) -> Result<Store> {
let mut connection = ::new_connection(path)?;
let conn = Conn::connect(&mut connection)?;
Ok(Store {
conn: conn,
sqlite: connection,
})
}
}
pub trait Queryable {
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
where T: Into<Option<QueryInputs>>;
fn q_once<T>(&self, query: &str, inputs: T) -> Result<QueryResults>
fn q_once<T>(&self, query: &str, inputs: T) -> Result<QueryOutput>
where T: Into<Option<QueryInputs>>;
fn lookup_values_for_attribute<E>(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result<Vec<TypedValue>>
where E: Into<Entid>;
@ -132,7 +150,7 @@ pub struct InProgress<'a, 'c> {
pub struct InProgressRead<'a, 'c>(InProgress<'a, 'c>);
impl<'a, 'c> Queryable for InProgressRead<'a, 'c> {
fn q_once<T>(&self, query: &str, inputs: T) -> Result<QueryResults>
fn q_once<T>(&self, query: &str, inputs: T) -> Result<QueryOutput>
where T: Into<Option<QueryInputs>> {
self.0.q_once(query, inputs)
}
@ -154,7 +172,7 @@ impl<'a, 'c> Queryable for InProgressRead<'a, 'c> {
}
impl<'a, 'c> Queryable for InProgress<'a, 'c> {
fn q_once<T>(&self, query: &str, inputs: T) -> Result<QueryResults>
fn q_once<T>(&self, query: &str, inputs: T) -> Result<QueryOutput>
where T: Into<Option<QueryInputs>> {
q_once(&*(self.transaction),
@ -323,6 +341,46 @@ impl<'a, 'c> InProgress<'a, 'c> {
}
}
impl Store {
pub fn dismantle(self) -> (rusqlite::Connection, Conn) {
(self.sqlite, self.conn)
}
pub fn conn(&self) -> &Conn {
&self.conn
}
pub fn begin_read<'m>(&'m mut self) -> Result<InProgressRead<'m, 'm>> {
self.conn.begin_read(&mut self.sqlite)
}
pub fn begin_transaction<'m>(&'m mut self) -> Result<InProgress<'m, 'm>> {
self.conn.begin_transaction(&mut self.sqlite)
}
}
impl Queryable for Store {
fn q_once<T>(&self, query: &str, inputs: T) -> Result<QueryOutput>
where T: Into<Option<QueryInputs>> {
self.conn.q_once(&self.sqlite, query, inputs)
}
fn q_explain<T>(&self, query: &str, inputs: T) -> Result<QueryExplanation>
where T: Into<Option<QueryInputs>> {
self.conn.q_explain(&self.sqlite, query, inputs)
}
fn lookup_values_for_attribute<E>(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result<Vec<TypedValue>>
where E: Into<Entid> {
self.conn.lookup_values_for_attribute(&self.sqlite, entity.into(), attribute)
}
fn lookup_value_for_attribute<E>(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result<Option<TypedValue>>
where E: Into<Entid> {
self.conn.lookup_value_for_attribute(&self.sqlite, entity.into(), attribute)
}
}
impl Conn {
// Intentionally not public.
fn new(partition_map: PartitionMap, schema: Schema) -> Conn {
@ -358,7 +416,7 @@ impl Conn {
pub fn q_once<T>(&self,
sqlite: &rusqlite::Connection,
query: &str,
inputs: T) -> Result<QueryResults>
inputs: T) -> Result<QueryOutput>
where T: Into<Option<QueryInputs>> {
let metadata = self.metadata.lock().unwrap();
@ -458,6 +516,8 @@ mod tests {
TypedValue,
};
use ::QueryResults;
use mentat_db::USER0;
#[test]
@ -545,7 +605,7 @@ mod tests {
let during = in_progress.q_once("[:find ?x . :where [?x :db/ident :a/keyword1]]", None)
.expect("query succeeded");
assert_eq!(during, QueryResults::Scalar(Some(TypedValue::Ref(one))));
assert_eq!(during.results, QueryResults::Scalar(Some(TypedValue::Ref(one))));
let report = in_progress.transact(t2).expect("t2 succeeded");
in_progress.commit().expect("commit succeeded");
@ -589,7 +649,7 @@ mod tests {
let during = in_progress.q_once("[:find ?x . :where [?x :db/ident :a/keyword1]]", None)
.expect("query succeeded");
assert_eq!(during, QueryResults::Scalar(Some(TypedValue::Ref(one))));
assert_eq!(during.results, QueryResults::Scalar(Some(TypedValue::Ref(one))));
// And we can do direct lookup, too.
let kw = in_progress.lookup_value_for_attribute(one, &edn::NamespacedKeyword::new("db", "ident"))
@ -602,7 +662,7 @@ mod tests {
let after = conn.q_once(&mut sqlite, "[:find ?x . :where [?x :db/ident :a/keyword1]]", None)
.expect("query succeeded");
assert_eq!(after, QueryResults::Scalar(None));
assert_eq!(after.results, QueryResults::Scalar(None));
// The DB part table is unchanged.
let tempid_offset_after = get_next_entid(&conn);

View file

@ -40,6 +40,10 @@ pub use mentat_core::{
ValueType,
};
pub use mentat_query::{
FindSpec,
};
pub use mentat_db::{
CORE_SCHEMA_VERSION,
DB_SCHEMA_CORE,
@ -80,19 +84,13 @@ pub fn get_name() -> String {
return String::from("mentat");
}
/// Open a Mentat store at the provided path.
pub fn open(path: &str) -> errors::Result<(rusqlite::Connection, Conn)> {
let mut connection = new_connection(path)?;
let conn = Conn::connect(&mut connection)?;
Ok((connection, conn))
}
pub use query::{
IntoResult,
PlainSymbol,
QueryExecutionResult,
QueryExplanation,
QueryInputs,
QueryOutput,
QueryPlanStep,
QueryResults,
Variable,
@ -104,6 +102,7 @@ pub use conn::{
InProgress,
Metadata,
Queryable,
Store,
};
#[cfg(test)]

View file

@ -60,7 +60,8 @@ use mentat_query_translator::{
};
pub use mentat_query_projector::{
QueryResults,
QueryOutput, // Includes the columns/find spec.
QueryResults, // The results themselves.
};
use errors::{
@ -68,7 +69,7 @@ use errors::{
Result,
};
pub type QueryExecutionResult = Result<QueryResults>;
pub type QueryExecutionResult = Result<QueryOutput>;
pub trait IntoResult {
fn into_scalar_result(self) -> Result<Option<TypedValue>>;
@ -256,7 +257,7 @@ fn run_algebrized_query<'sqlite>(sqlite: &'sqlite rusqlite::Connection, algebriz
"Unbound variables should be checked by now");
if algebrized.is_known_empty() {
// We don't need to do any SQL work at all.
return Ok(QueryResults::empty(&algebrized.find_spec));
return Ok(QueryOutput::empty(&algebrized.find_spec));
}
let select = query_to_select(algebrized)?;

View file

@ -27,7 +27,7 @@
//! extern crate mentat;
//!
//! use mentat::{
//! Conn,
//! Store,
//! ValueType,
//! };
//!
@ -40,11 +40,11 @@
//! };
//!
//! fn main() {
//! let (mut sqlite, mut conn) = mentat::open("").expect("connected");
//! let mut store = Store::open("").expect("connected");
//!
//! {
//! // Read the list of installed vocabularies.
//! let reader = conn.begin_read(&mut sqlite).expect("began read");
//! let reader = store.begin_read().expect("began read");
//! let vocabularies = reader.read_vocabularies().expect("read");
//! for (name, vocabulary) in vocabularies.iter() {
//! println!("Vocab {} is at version {}.", name, vocabulary.version);
@ -55,7 +55,7 @@
//! }
//!
//! {
//! let mut in_progress = conn.begin_transaction(&mut sqlite).expect("began transaction");
//! let mut in_progress = store.begin_transaction().expect("began transaction");
//!
//! // Make sure the core vocabulary exists.
//! in_progress.verify_core_schema().expect("verified");
@ -569,13 +569,19 @@ impl<T> HasVocabularies for T where T: HasSchema + Queryable {
#[cfg(test)]
mod tests {
use super::HasVocabularies;
use ::{
Store,
};
use super::{
HasVocabularies,
};
#[test]
fn test_read_vocabularies() {
let (mut sqlite, mut conn) = ::open("").expect("opened");
let vocabularies = conn.begin_read(&mut sqlite).expect("in progress")
.read_vocabularies().expect("OK");
let mut store = Store::open("").expect("opened");
let vocabularies = store.begin_read().expect("in progress")
.read_vocabularies().expect("OK");
assert_eq!(vocabularies.len(), 1);
let core = vocabularies.get(&kw!(:db.schema/core)).expect("exists");
assert_eq!(core.version, 1);
@ -583,8 +589,8 @@ mod tests {
#[test]
fn test_core_schema() {
let (mut sqlite, mut conn) = ::open("").expect("opened");
let in_progress = conn.begin_transaction(&mut sqlite).expect("in progress");
let mut store = Store::open("").expect("opened");
let in_progress = store.begin_transaction().expect("in progress");
let vocab = in_progress.read_vocabularies().expect("vocabulary");
assert_eq!(1, vocab.len());
assert_eq!(1, vocab.get(&kw!(:db.schema/core)).expect("core vocab").version);

View file

@ -57,7 +57,8 @@ fn test_rel() {
let start = time::PreciseTime::now();
let results = q_once(&c, &db.schema,
"[:find ?x ?ident :where [?x :db/ident ?ident]]", None)
.expect("Query failed");
.expect("Query failed")
.results;
let end = time::PreciseTime::now();
// This will need to change each time we add a default ident.
@ -87,7 +88,8 @@ fn test_failing_scalar() {
let start = time::PreciseTime::now();
let results = q_once(&c, &db.schema,
"[:find ?x . :where [?x :db/fulltext true]]", None)
.expect("Query failed");
.expect("Query failed")
.results;
let end = time::PreciseTime::now();
assert_eq!(0, results.len());
@ -109,7 +111,8 @@ fn test_scalar() {
let start = time::PreciseTime::now();
let results = q_once(&c, &db.schema,
"[:find ?ident . :where [24 :db/ident ?ident]]", None)
.expect("Query failed");
.expect("Query failed")
.results;
let end = time::PreciseTime::now();
assert_eq!(1, results.len());
@ -139,7 +142,8 @@ fn test_tuple() {
:where [:db/txInstant :db/index ?index]
[:db/txInstant :db/cardinality ?cardinality]]",
None)
.expect("Query failed");
.expect("Query failed")
.results;
let end = time::PreciseTime::now();
assert_eq!(1, results.len());
@ -166,7 +170,8 @@ fn test_coll() {
let start = time::PreciseTime::now();
let results = q_once(&c, &db.schema,
"[:find [?e ...] :where [?e :db/ident _]]", None)
.expect("Query failed");
.expect("Query failed")
.results;
let end = time::PreciseTime::now();
assert_eq!(40, results.len());
@ -191,7 +196,8 @@ fn test_inputs() {
let inputs = QueryInputs::with_value_sequence(vec![ee]);
let results = q_once(&c, &db.schema,
"[:find ?i . :in ?e :where [?e :db/ident ?i]]", inputs)
.expect("query to succeed");
.expect("query to succeed")
.results;
if let QueryResults::Scalar(Some(TypedValue::Keyword(value))) = results {
assert_eq!(value.as_ref(), &NamespacedKeyword::new("db.install", "valueType"));
@ -239,9 +245,11 @@ fn test_instants_and_uuids() {
let r = conn.q_once(&mut c,
r#"[:find [?x ?u ?when]
:where [?x :foo/uuid ?u ?tx]
[?tx :db/txInstant ?when]]"#, None);
[?tx :db/txInstant ?when]]"#, None)
.expect("results")
.into();
match r {
Result::Ok(QueryResults::Tuple(Some(vals))) => {
QueryResults::Tuple(Some(vals)) => {
let mut vals = vals.into_iter();
match (vals.next(), vals.next(), vals.next(), vals.next()) {
(Some(TypedValue::Ref(e)),
@ -279,9 +287,11 @@ fn test_tx() {
let r = conn.q_once(&mut c,
r#"[:find ?tx
:where [?x :foo/uuid #uuid "cf62d552-6569-4d1b-b667-04703041dfc4" ?tx]]"#, None);
:where [?x :foo/uuid #uuid "cf62d552-6569-4d1b-b667-04703041dfc4" ?tx]]"#, None)
.expect("results")
.into();
match r {
Result::Ok(QueryResults::Rel(ref v)) => {
QueryResults::Rel(ref v) => {
assert_eq!(*v, vec![
vec![TypedValue::Ref(t.tx_id),]
]);
@ -314,9 +324,11 @@ fn test_tx_as_input() {
let r = conn.q_once(&mut c,
r#"[:find ?uuid
:in ?tx
:where [?x :foo/uuid ?uuid ?tx]]"#, inputs);
:where [?x :foo/uuid ?uuid ?tx]]"#, inputs)
.expect("results")
.into();
match r {
Result::Ok(QueryResults::Rel(ref v)) => {
QueryResults::Rel(ref v) => {
assert_eq!(*v, vec![
vec![TypedValue::Uuid(Uuid::from_str("cf62d552-6569-4d1b-b667-04703041dfc4").expect("Valid UUID")),]
]);
@ -350,9 +362,11 @@ fn test_fulltext() {
let r = conn.q_once(&mut c,
r#"[:find [?x ?val ?score]
:where [(fulltext $ :foo/fts "darkness") [[?x ?val _ ?score]]]]"#, None);
:where [(fulltext $ :foo/fts "darkness") [[?x ?val _ ?score]]]]"#, None)
.expect("results")
.into();
match r {
Result::Ok(QueryResults::Tuple(Some(vals))) => {
QueryResults::Tuple(Some(vals)) => {
let mut vals = vals.into_iter();
match (vals.next(), vals.next(), vals.next(), vals.next()) {
(Some(TypedValue::Ref(x)),
@ -413,9 +427,11 @@ fn test_fulltext() {
[?a :foo/term ?term]
[(fulltext $ :foo/fts ?term) [[?x ?val]]]]"#;
let inputs = QueryInputs::with_value_sequence(vec![(Variable::from_valid_name("?a"), TypedValue::Ref(a))]);
let r = conn.q_once(&mut c, query, inputs);
let r = conn.q_once(&mut c, query, inputs)
.expect("results")
.into();
match r {
Result::Ok(QueryResults::Rel(rels)) => {
QueryResults::Rel(rels) => {
assert_eq!(rels, vec![
vec![TypedValue::Ref(v),
TypedValue::String("I've come to talk with you again".to_string().into()),
@ -449,9 +465,11 @@ fn test_instant_range_query() {
:order (asc ?date)
:where
[?x :foo/date ?date]
[(< ?date #inst "2017-01-01T11:00:02.000Z")]]"#, None);
[(< ?date #inst "2017-01-01T11:00:02.000Z")]]"#, None)
.expect("results")
.into();
match r {
Result::Ok(QueryResults::Coll(vals)) => {
QueryResults::Coll(vals) => {
assert_eq!(vals,
vec![TypedValue::Ref(*ids.get("b").unwrap()),
TypedValue::Ref(*ids.get("c").unwrap())]);
@ -533,7 +551,9 @@ fn test_type_reqs() {
let eid_query = r#"[:find ?eid :where [?eid :test/string "foo"]]"#;
let res = conn.q_once(&mut c, eid_query, None).unwrap();
let res = conn.q_once(&mut c, eid_query, None)
.expect("results")
.into();
let entid = match res {
QueryResults::Rel(ref vs) if vs.len() == 1 && vs[0].len() == 1 && vs[0][0].matches_type(ValueType::Ref) =>
@ -562,8 +582,10 @@ fn test_type_reqs() {
for name in type_names {
let q = format!("[:find [?v ...] :in ?e :where [?e _ ?v] [({} ?v)]]", name);
let results = conn.q_once(&mut c, &q, QueryInputs::with_value_sequence(vec![
(Variable::from_valid_name("?e"), TypedValue::Ref(entid)),
])).unwrap();
(Variable::from_valid_name("?e"), TypedValue::Ref(entid)),
]))
.expect("results")
.into();
match results {
QueryResults::Coll(vals) => {
assert_eq!(vals.len(), 1, "Query should find exactly 1 item");
@ -585,8 +607,10 @@ fn test_type_reqs() {
:where [?e _ ?v] [(long ?v)]]"#;
let res = conn.q_once(&mut c, longs_query, QueryInputs::with_value_sequence(vec![
(Variable::from_valid_name("?e"), TypedValue::Ref(entid)),
])).unwrap();
(Variable::from_valid_name("?e"), TypedValue::Ref(entid)),
]))
.expect("results")
.into();
match res {
QueryResults::Coll(vals) => {
assert_eq!(vals, vec![TypedValue::Long(5), TypedValue::Long(33)])

View file

@ -12,13 +12,14 @@ doc = false
test = false
[dependencies]
getopts = "0.2"
combine = "2.2.2"
env_logger = "0.3"
getopts = "0.2"
lazy_static = "0.2"
linefeed = "0.4"
log = "0.3"
tabwriter = "1"
tempfile = "1.1"
combine = "2.2.2"
lazy_static = "0.2"
error-chain = { git = "https://github.com/rnewman/error-chain", branch = "rnewman/sync" }
[dependencies.rusqlite]

View file

@ -21,6 +21,7 @@ error_chain! {
foreign_links {
Rusqlite(rusqlite::Error);
IoError(::std::io::Error);
}
links {

View file

@ -19,6 +19,7 @@ extern crate env_logger;
extern crate getopts;
extern crate linefeed;
extern crate rusqlite;
extern crate tabwriter;
extern crate mentat;
extern crate edn;
@ -29,7 +30,6 @@ extern crate mentat_db;
use getopts::Options;
pub mod command_parser;
pub mod store;
pub mod input;
pub mod repl;
pub mod errors;

View file

@ -9,13 +9,20 @@
// specific language governing permissions and limitations under the License.
use std::collections::HashMap;
use std::io::Write;
use std::process;
use mentat::query::{
use tabwriter::TabWriter;
use mentat::{
Queryable,
QueryExplanation,
QueryOutput,
QueryResults,
Store,
TxReport,
TypedValue,
};
use mentat_core::TypedValue;
use command_parser::{
Command,
@ -31,16 +38,13 @@ use command_parser::{
LONG_QUERY_EXPLAIN_COMMAND,
SHORT_QUERY_EXPLAIN_COMMAND,
};
use input::InputReader;
use input::InputResult::{
MetaCommand,
Empty,
More,
Eof
};
use store::{
Store,
db_output_name
Eof,
};
lazy_static! {
@ -64,14 +68,24 @@ lazy_static! {
/// Executes input and maintains state of persistent items.
pub struct Repl {
store: Store
path: String,
store: Store,
}
impl Repl {
pub fn db_name(&self) -> String {
if self.path.is_empty() {
"in-memory db".to_string()
} else {
self.path.clone()
}
}
/// Constructs a new `Repl`.
pub fn new() -> Result<Repl, String> {
let store = Store::new(None).map_err(|e| e.to_string())?;
let store = Store::open("").map_err(|e| e.to_string())?;
Ok(Repl{
path: "".to_string(),
store: store,
})
}
@ -103,7 +117,7 @@ impl Repl {
}
break;
},
Err(e) => println!("{}", e.to_string()),
Err(e) => eprintln!("{}", e.to_string()),
}
}
}
@ -113,36 +127,49 @@ impl Repl {
match cmd {
Command::Help(args) => self.help_command(args),
Command::Open(db) => {
match self.store.open(Some(db.clone())) {
Ok(_) => println!("Database {:?} opened", db_output_name(&db)),
Err(e) => println!("{}", e.to_string())
match self.open(db) {
Ok(_) => println!("Database {:?} opened", self.db_name()),
Err(e) => eprintln!("{}", e.to_string()),
};
},
Command::Close => self.close(),
Command::Query(query) => self.execute_query(query),
Command::QueryExplain(query) => self.explain_query(query),
Command::Schema => {
let edn = self.store.fetch_schema();
let edn = self.store.conn().current_schema().to_edn_value();
match edn.to_pretty(120) {
Ok(s) => println!("{}", s),
Err(e) => println!("{}", e)
Err(e) => eprintln!("{}", e)
};
}
Command::Transact(transaction) => self.execute_transact(transaction),
Command::Exit => {
self.close();
println!("Exiting...");
eprintln!("Exiting...");
process::exit(0);
}
}
}
fn open<T>(&mut self, path: T) -> ::mentat::errors::Result<()>
where T: Into<String> {
let path = path.into();
if self.path.is_empty() || path != self.path {
let next = Store::open(path.as_str())?;
self.path = path;
self.store = next;
}
Ok(())
}
// Close the current store by opening a new in-memory store in its place.
fn close(&mut self) {
let old_db_name = self.store.db_name.clone();
match self.store.close() {
Ok(_) => println!("Database {:?} closed", db_output_name(&old_db_name)),
Err(e) => println!("{}", e)
let old_db_name = self.db_name();
match self.open("") {
Ok(_) => println!("Database {:?} closed.", old_db_name),
Err(e) => eprintln!("{}", e),
};
}
@ -160,54 +187,76 @@ impl Repl {
if msg.is_some() {
println!(".{} - {}", arg, msg.unwrap());
} else {
println!("Unrecognised command {}", arg);
eprintln!("Unrecognised command {}", arg);
}
}
}
}
pub fn execute_query(&self, query: String) {
let results = match self.store.query(query){
Result::Ok(vals) => {
vals
},
Result::Err(err) => return println!("{:?}.", err),
};
self.store.q_once(query.as_str(), None)
.map_err(|e| e.into())
.and_then(|o| self.print_results(o))
.map_err(|err| {
eprintln!("{:?}.", err);
}).ok();
}
if results.is_empty() {
println!("No results found.")
fn print_results(&self, query_output: QueryOutput) -> Result<(), ::errors::Error> {
let stdout = ::std::io::stdout();
let mut output = TabWriter::new(stdout.lock());
// Print the column headers.
for e in query_output.spec.columns() {
write!(output, "| {}\t", e)?;
}
writeln!(output, "|")?;
for _ in 0..query_output.spec.expected_column_count() {
write!(output, "---\t")?;
}
writeln!(output, "")?;
let mut output:String = String::new();
match results {
QueryResults::Scalar(Some(val)) => {
output.push_str(&self.typed_value_as_string(val) );
},
QueryResults::Tuple(Some(vals)) => {
for val in vals {
output.push_str(&format!("{}\t", self.typed_value_as_string(val)));
match query_output.results {
QueryResults::Scalar(v) => {
if let Some(val) = v {
writeln!(output, "| {}\t |", &self.typed_value_as_string(val))?;
}
},
QueryResults::Tuple(vv) => {
if let Some(vals) = vv {
for val in vals {
write!(output, "| {}\t", self.typed_value_as_string(val))?;
}
writeln!(output, "|")?;
}
},
QueryResults::Coll(vv) => {
for val in vv {
output.push_str(&format!("{}\n", self.typed_value_as_string(val)));
writeln!(output, "| {}\t|", self.typed_value_as_string(val))?;
}
},
QueryResults::Rel(vvv) => {
for vv in vvv {
for v in vv {
output.push_str(&format!("{}\t", self.typed_value_as_string(v)));
write!(output, "| {}\t", self.typed_value_as_string(v))?;
}
output.push_str("\n");
writeln!(output, "|")?;
}
},
_ => output.push_str(&format!("No results found."))
}
println!("\n{}", output);
for _ in 0..query_output.spec.expected_column_count() {
write!(output, "---\t")?;
}
writeln!(output, "")?;
output.flush()?;
Ok(())
}
pub fn explain_query(&self, query: String) {
match self.store.explain_query(query) {
match self.store.q_explain(query.as_str(), None) {
Result::Err(err) =>
println!("{:?}.", err),
Result::Ok(QueryExplanation::KnownEmpty(empty_because)) =>
@ -244,12 +293,19 @@ impl Repl {
}
pub fn execute_transact(&mut self, transaction: String) {
match self.store.transact(transaction) {
match self.transact(transaction) {
Result::Ok(report) => println!("{:?}", report),
Result::Err(err) => println!("{:?}.", err),
Result::Err(err) => eprintln!("Error: {:?}.", err),
}
}
fn transact(&mut self, transaction: String) -> ::mentat::errors::Result<TxReport> {
let mut tx = self.store.begin_transaction()?;
let report = tx.transact(&transaction)?;
tx.commit()?;
Ok(report)
}
fn typed_value_as_string(&self, value: TypedValue) -> String {
match value {
TypedValue::Boolean(b) => if b { "true".to_string() } else { "false".to_string() },
@ -263,10 +319,3 @@ impl Repl {
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
}
}

View file

@ -1,73 +0,0 @@
// Copyright 2017 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 rusqlite;
use edn;
use errors as cli;
use mentat::{
new_connection,
QueryExplanation,
};
use mentat::query::QueryResults;
use mentat::conn::Conn;
use mentat_db::types::TxReport;
pub struct Store {
handle: rusqlite::Connection,
conn: Conn,
pub db_name: String,
}
pub fn db_output_name(db_name: &String) -> String {
if db_name.is_empty() { "in-memory db".to_string() } else { db_name.clone() }
}
impl Store {
pub fn new(database: Option<String>) -> Result<Store, cli::Error> {
let db_name = database.unwrap_or("".to_string());
let mut handle = try!(new_connection(&db_name));
let conn = try!(Conn::connect(&mut handle));
Ok(Store { handle, conn, db_name })
}
pub fn open(&mut self, database: Option<String>) -> Result<(), cli::Error> {
self.db_name = database.unwrap_or("".to_string());
self.handle = try!(new_connection(&self.db_name));
self.conn = try!(Conn::connect(&mut self.handle));
Ok(())
}
pub fn close(&mut self) -> Result<(), cli::Error> {
self.db_name = "".to_string();
self.open(None)
}
pub fn query(&self, query: String) -> Result<QueryResults, cli::Error> {
Ok(self.conn.q_once(&self.handle, &query, None)?)
}
pub fn explain_query(&self, query: String) -> Result<QueryExplanation, cli::Error> {
Ok(self.conn.q_explain(&self.handle, &query, None)?)
}
pub fn transact(&mut self, transaction: String) -> Result<TxReport, cli::Error> {
Ok(self.conn.transact(&mut self.handle, &transaction)?)
}
pub fn fetch_schema(&self) -> edn::Value {
self.conn.current_schema().to_edn_value()
}
}