diff --git a/Cargo.toml b/Cargo.toml index f55e3ba0..4442d0e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ authors = [ "Emily Toop ", ] name = "mentat" -version = "0.5.1" +version = "0.6.0" build = "build/version.rs" [workspace] diff --git a/query-algebrizer/src/lib.rs b/query-algebrizer/src/lib.rs index 3bf22535..a10899cc 100644 --- a/query-algebrizer/src/lib.rs +++ b/query-algebrizer/src/lib.rs @@ -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, has_aggregates: bool, pub with: BTreeSet, pub order: Option>, @@ -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, diff --git a/query-projector/src/lib.rs b/query-projector/src/lib.rs index 37b0e1b7..6a0b8637 100644 --- a/query-projector/src/lib.rs +++ b/query-projector/src/lib.rs @@ -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, + pub results: QueryResults, +} + #[derive(Debug, PartialEq, Eq)] pub enum QueryResults { Scalar(Option), @@ -86,6 +94,63 @@ pub enum QueryResults { Rel(Vec>), } +impl From for QueryResults { + fn from(o: QueryOutput) -> QueryResults { + o.results + } +} + +impl QueryOutput { + pub fn empty_factory(spec: &FindSpec) -> Box 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) -> 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> { + self.results.into_scalar() + } + + pub fn into_coll(self) -> Result> { + self.results.into_coll() + } + + pub fn into_tuple(self) -> Result>> { + self.results.into_tuple() + } + + pub fn into_rel(self) -> Result>> { + 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 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> { match self { QueryResults::Scalar(o) => Ok(o), @@ -304,69 +349,87 @@ fn project_elements<'a, I: IntoIterator>( } pub trait Projector { - fn project<'stmt>(&self, rows: Rows<'stmt>) -> Result; + fn project<'stmt>(&self, rows: Rows<'stmt>) -> Result; } /// 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, results_factory: Box QueryResults>, } impl ConstantProjector { - fn new(results_factory: Box QueryResults>) -> ConstantProjector { - ConstantProjector { results_factory: results_factory } + fn new(spec: Rc, results_factory: Box QueryResults>) -> ConstantProjector { + ConstantProjector { + spec: spec, + results_factory: results_factory, + } } } impl Projector for ConstantProjector { - fn project<'stmt>(&self, _: Rows<'stmt>) -> Result { - Ok((self.results_factory)()) + fn project<'stmt>(&self, _: Rows<'stmt>) -> Result { + let results = (self.results_factory)(); + let spec = self.spec.clone(); + Ok(QueryOutput { + spec: spec, + results: results, + }) } } struct ScalarProjector { + spec: Rc, template: TypedIndex, } impl ScalarProjector { - fn with_template(template: TypedIndex) -> ScalarProjector { + fn with_template(spec: Rc, template: TypedIndex) -> ScalarProjector { ScalarProjector { + spec: spec, template: template, } } - fn combine(sql: Projection, mut templates: Vec) -> Result { + fn combine(spec: Rc, sql: Projection, mut templates: Vec) -> Result { 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 { - 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 { + 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, len: usize, templates: Vec, } impl TupleProjector { - fn with_templates(len: usize, templates: Vec) -> TupleProjector { + fn with_templates(spec: Rc, len: usize, templates: Vec) -> TupleProjector { TupleProjector { + spec: spec, len: len, templates: templates, } @@ -382,8 +445,8 @@ impl TupleProjector { .collect::>>() } - fn combine(column_count: usize, sql: Projection, templates: Vec) -> Result { - let p = TupleProjector::with_templates(column_count, templates); + fn combine(spec: Rc, column_count: usize, sql: Projection, templates: Vec) -> Result { + 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 { - 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 { + 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, len: usize, templates: Vec, } impl RelProjector { - fn with_templates(len: usize, templates: Vec) -> RelProjector { + fn with_templates(spec: Rc, len: usize, templates: Vec) -> RelProjector { RelProjector { + spec: spec, len: len, templates: templates, } @@ -431,8 +501,8 @@ impl RelProjector { .collect::>>() } - fn combine(column_count: usize, sql: Projection, templates: Vec) -> Result { - let p = RelProjector::with_templates(column_count, templates); + fn combine(spec: Rc, column_count: usize, sql: Projection, templates: Vec) -> Result { + 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 { + fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result { let mut out: Vec> = 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, template: TypedIndex, } impl CollProjector { - fn with_template(template: TypedIndex) -> CollProjector { + fn with_template(spec: Rc, template: TypedIndex) -> CollProjector { CollProjector { + spec: spec, template: template, } } - fn combine(sql: Projection, mut templates: Vec) -> Result { + fn combine(spec: Rc, sql: Projection, mut templates: Vec) -> Result { 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 { + fn project<'stmt>(&self, mut rows: Rows<'stmt>) -> Result { let mut out: Vec = 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 { 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) }, } } diff --git a/src/conn.rs b/src/conn.rs index 23848270..86c7c347 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -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 { + let mut connection = ::new_connection(path)?; + let conn = Conn::connect(&mut connection)?; + Ok(Store { + conn: conn, + sqlite: connection, + }) + } +} + pub trait Queryable { fn q_explain(&self, query: &str, inputs: T) -> Result where T: Into>; - fn q_once(&self, query: &str, inputs: T) -> Result + fn q_once(&self, query: &str, inputs: T) -> Result where T: Into>; fn lookup_values_for_attribute(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result> where E: Into; @@ -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(&self, query: &str, inputs: T) -> Result + fn q_once(&self, query: &str, inputs: T) -> Result where T: Into> { 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(&self, query: &str, inputs: T) -> Result + fn q_once(&self, query: &str, inputs: T) -> Result where T: Into> { 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> { + self.conn.begin_read(&mut self.sqlite) + } + + pub fn begin_transaction<'m>(&'m mut self) -> Result> { + self.conn.begin_transaction(&mut self.sqlite) + } +} + +impl Queryable for Store { + fn q_once(&self, query: &str, inputs: T) -> Result + where T: Into> { + self.conn.q_once(&self.sqlite, query, inputs) + } + + fn q_explain(&self, query: &str, inputs: T) -> Result + where T: Into> { + self.conn.q_explain(&self.sqlite, query, inputs) + } + + fn lookup_values_for_attribute(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result> + where E: Into { + self.conn.lookup_values_for_attribute(&self.sqlite, entity.into(), attribute) + } + + fn lookup_value_for_attribute(&self, entity: E, attribute: &edn::NamespacedKeyword) -> Result> + where E: Into { + 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(&self, sqlite: &rusqlite::Connection, query: &str, - inputs: T) -> Result + inputs: T) -> Result where T: Into> { 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); diff --git a/src/lib.rs b/src/lib.rs index bd2aba18..8d7edb58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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)] diff --git a/src/query.rs b/src/query.rs index 3a05b2a3..65e2e475 100644 --- a/src/query.rs +++ b/src/query.rs @@ -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; +pub type QueryExecutionResult = Result; pub trait IntoResult { fn into_scalar_result(self) -> Result>; @@ -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)?; diff --git a/src/vocabulary.rs b/src/vocabulary.rs index 39c7837f..cf94aa38 100644 --- a/src/vocabulary.rs +++ b/src/vocabulary.rs @@ -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 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); diff --git a/tests/query.rs b/tests/query.rs index cc075c8c..320550c2 100644 --- a/tests/query.rs +++ b/tests/query.rs @@ -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)]) diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml index 3e5104de..7bfe5168 100644 --- a/tools/cli/Cargo.toml +++ b/tools/cli/Cargo.toml @@ -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] diff --git a/tools/cli/src/mentat_cli/errors.rs b/tools/cli/src/mentat_cli/errors.rs index 8e095ab3..0012c319 100644 --- a/tools/cli/src/mentat_cli/errors.rs +++ b/tools/cli/src/mentat_cli/errors.rs @@ -21,6 +21,7 @@ error_chain! { foreign_links { Rusqlite(rusqlite::Error); + IoError(::std::io::Error); } links { diff --git a/tools/cli/src/mentat_cli/lib.rs b/tools/cli/src/mentat_cli/lib.rs index ad16e266..f15b273c 100644 --- a/tools/cli/src/mentat_cli/lib.rs +++ b/tools/cli/src/mentat_cli/lib.rs @@ -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; diff --git a/tools/cli/src/mentat_cli/repl.rs b/tools/cli/src/mentat_cli/repl.rs index b590f514..68bce64e 100644 --- a/tools/cli/src/mentat_cli/repl.rs +++ b/tools/cli/src/mentat_cli/repl.rs @@ -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 { - 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(&mut self, path: T) -> ::mentat::errors::Result<()> + where T: Into { + 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 { + 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() { - } -} diff --git a/tools/cli/src/mentat_cli/store.rs b/tools/cli/src/mentat_cli/store.rs deleted file mode 100644 index c4bc0731..00000000 --- a/tools/cli/src/mentat_cli/store.rs +++ /dev/null @@ -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) -> Result { - 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) -> 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 { - Ok(self.conn.q_once(&self.handle, &query, None)?) - } - - pub fn explain_query(&self, query: String) -> Result { - Ok(self.conn.q_explain(&self.handle, &query, None)?) - } - - pub fn transact(&mut self, transaction: String) -> Result { - Ok(self.conn.transact(&mut self.handle, &transaction)?) - } - - pub fn fetch_schema(&self) -> edn::Value { - self.conn.current_schema().to_edn_value() - } -}