diff --git a/build/version.rs b/build/version.rs index 54f8b2a40..6ad731574 100644 --- a/build/version.rs +++ b/build/version.rs @@ -16,7 +16,7 @@ use rustc_version::version_matches; /// MIN_VERSION should be changed when there's a new minimum version of rustc required /// to build the project. -static MIN_VERSION: &'static str = ">= 1.15.1"; +static MIN_VERSION: &'static str = ">= 1.17.0"; fn main() { if !version_matches(MIN_VERSION) { diff --git a/db/src/lib.rs b/db/src/lib.rs index 07f5bc5bb..a67c6562b 100644 --- a/db/src/lib.rs +++ b/db/src/lib.rs @@ -43,7 +43,7 @@ mod entids; pub mod errors; mod metadata; mod schema; -mod types; +pub mod types; mod internal_types; mod upsert_resolution; mod tx; diff --git a/tools/cli/Cargo.toml b/tools/cli/Cargo.toml index 58cbc09d1..cd74523b5 100644 --- a/tools/cli/Cargo.toml +++ b/tools/cli/Cargo.toml @@ -31,3 +31,15 @@ path = "../.." [dependencies.mentat_parser_utils] path = "../../parser-utils" + +[dependencies.edn] +path = "../../edn" + +[dependencies.mentat_query] +path = "../../query" + +[dependencies.mentat_core] +path = "../../core" + +[dependencies.mentat_db] +path = "../../db" diff --git a/tools/cli/src/mentat_cli/command_parser.rs b/tools/cli/src/mentat_cli/command_parser.rs index 27d8e27f9..aa81e4d71 100644 --- a/tools/cli/src/mentat_cli/command_parser.rs +++ b/tools/cli/src/mentat_cli/command_parser.rs @@ -9,8 +9,11 @@ // specific language governing permissions and limitations under the License. use combine::{ + any, eof, + look_ahead, many1, + parser, satisfy, sep_end_by, token, @@ -26,16 +29,24 @@ use combine::combinator::{ try }; +use combine::primitives::Consumed; + use errors as cli; +use edn; + pub static HELP_COMMAND: &'static str = &"help"; pub static OPEN_COMMAND: &'static str = &"open"; pub static CLOSE_COMMAND: &'static str = &"close"; +pub static LONG_QUERY_COMMAND: &'static str = &"query"; +pub static SHORT_QUERY_COMMAND: &'static str = &"q"; +pub static LONG_TRANSACT_COMMAND: &'static str = &"transact"; +pub static SHORT_TRANSACT_COMMAND: &'static str = &"t"; #[derive(Clone, Debug, Eq, PartialEq)] pub enum Command { - Transact(Vec), - Query(Vec), + Transact(String), + Query(String), Help(Vec), Open(String), Close, @@ -48,13 +59,35 @@ impl Command { /// TODO: for query and transact commands, they will be considered complete if a parsable EDN has been entered as an argument pub fn is_complete(&self) -> bool { match self { - &Command::Query(_) | - &Command::Transact(_) => false, + &Command::Query(ref args) | + &Command::Transact(ref args) => { + edn::parse::value(&args).is_ok() + }, &Command::Help(_) | &Command::Open(_) | &Command::Close => true } } + + pub fn output(&self) -> String { + match self { + &Command::Query(ref args) => { + format!(".{} {}", LONG_QUERY_COMMAND, args) + }, + &Command::Transact(ref args) => { + format!(".{} {}", LONG_TRANSACT_COMMAND, args) + }, + &Command::Help(ref args) => { + format!(".{} {:?}", HELP_COMMAND, args) + }, + &Command::Open(ref args) => { + format!(".{} {}", OPEN_COMMAND, args) + } + &Command::Close => { + format!(".{}", CLOSE_COMMAND) + }, + } + } } pub fn command(s: &str) -> Result { @@ -72,34 +105,55 @@ pub fn command(s: &str) -> Result { .with(arguments()) .map(|args| { if args.len() < 1 { - return Err(cli::ErrorKind::CommandParse("Missing required argument".to_string()).into()); + bail!(cli::ErrorKind::CommandParse("Missing required argument".to_string())); } if args.len() > 1 { - return Err(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[1])).into()); + bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[1]))); } Ok(Command::Open(args[0].clone())) }); let close_parser = string(CLOSE_COMMAND) .with(arguments()) + .skip(spaces()) + .skip(eof()) .map(|args| { if args.len() > 0 { - return Err(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])).into()); + bail!(cli::ErrorKind::CommandParse(format!("Unrecognized argument {:?}", args[0])) ); } Ok(Command::Close) }); + let edn_arg_parser = || spaces() + .with(look_ahead(string("[").or(string("{"))) + .with(many1::, _>(try(any()))) + .and_then(|args| -> Result { + Ok(args.iter().collect()) + }) + ); + + let query_parser = try(string(LONG_QUERY_COMMAND)).or(try(string(SHORT_QUERY_COMMAND))) + .with(edn_arg_parser()) + .map(|x| { + Ok(Command::Query(x)) + }); + + let transact_parser = try(string(LONG_TRANSACT_COMMAND)).or(try(string(SHORT_TRANSACT_COMMAND))) + .with(edn_arg_parser()) + .map( |x| { + Ok(Command::Transact(x)) + }); + spaces() .skip(token('.')) - .with(choice::<[&mut Parser>; 3], _> + .with(choice::<[&mut Parser>; 5], _> ([&mut try(help_parser), &mut try(open_parser), - &mut try(close_parser),])) - .skip(spaces()) - .skip(eof()) + &mut try(close_parser), + &mut try(query_parser), + &mut try(transact_parser)])) .parse(s) - .map(|x| x.0) - .unwrap_or(Err(cli::ErrorKind::CommandParse(format!("Invalid command {:?}", s)).into())) + .unwrap_or((Err(cli::ErrorKind::CommandParse(format!("Invalid command {:?}", s)).into()), "")).0 } #[cfg(test)] @@ -198,7 +252,7 @@ mod tests { _ => assert!(false) } } - + #[test] fn test_open_parser_no_args() { let input = ".open"; @@ -206,6 +260,13 @@ mod tests { assert_eq!(err.to_string(), "Missing required argument"); } + #[test] + fn test_open_parser_no_args_trailing_whitespace() { + let input = ".open "; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), "Missing required argument"); + } + #[test] fn test_close_parser_with_args() { let input = ".close arg1"; @@ -233,6 +294,114 @@ mod tests { } } + #[test] + fn test_query_parser_complete_edn() { + let input = ".q [:find ?x :where [?x foo/bar ?y]]"; + let cmd = command(&input).expect("Expected query command"); + match cmd { + Command::Query(edn) => assert_eq!(edn, "[:find ?x :where [?x foo/bar ?y]]"), + _ => assert!(false) + } + } + + #[test] + fn test_query_parser_alt_query_command() { + let input = ".query [:find ?x :where [?x foo/bar ?y]]"; + let cmd = command(&input).expect("Expected query command"); + match cmd { + Command::Query(edn) => assert_eq!(edn, "[:find ?x :where [?x foo/bar ?y]]"), + _ => assert!(false) + } + } + + #[test] + fn test_query_parser_incomplete_edn() { + let input = ".q [:find ?x\r\n"; + let cmd = command(&input).expect("Expected query command"); + match cmd { + Command::Query(edn) => assert_eq!(edn, "[:find ?x\r\n"), + _ => assert!(false) + } + } + + #[test] + fn test_query_parser_empty_edn() { + let input = ".q {}"; + let cmd = command(&input).expect("Expected query command"); + match cmd { + Command::Query(edn) => assert_eq!(edn, "{}"), + _ => assert!(false) + } + } + + #[test] + fn test_query_parser_no_edn() { + let input = ".q "; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_query_parser_invalid_start_char() { + let input = ".q :find ?x"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_transact_parser_complete_edn() { + let input = ".t [[:db/add \"s\" :db/ident :foo/uuid] [:db/add \"r\" :db/ident :bar/uuid]]"; + let cmd = command(&input).expect("Expected transact command"); + match cmd { + Command::Transact(edn) => assert_eq!(edn, "[[:db/add \"s\" :db/ident :foo/uuid] [:db/add \"r\" :db/ident :bar/uuid]]"), + _ => assert!(false) + } + } + + #[test] + fn test_transact_parser_alt_command() { + let input = ".transact [[:db/add \"s\" :db/ident :foo/uuid] [:db/add \"r\" :db/ident :bar/uuid]]"; + let cmd = command(&input).expect("Expected transact command"); + match cmd { + Command::Transact(edn) => assert_eq!(edn, "[[:db/add \"s\" :db/ident :foo/uuid] [:db/add \"r\" :db/ident :bar/uuid]]"), + _ => assert!(false) + } + } + + #[test] + fn test_transact_parser_incomplete_edn() { + let input = ".t {\r\n"; + let cmd = command(&input).expect("Expected transact command"); + match cmd { + Command::Transact(edn) => assert_eq!(edn, "{\r\n"), + _ => assert!(false) + } + } + + #[test] + fn test_transact_parser_empty_edn() { + let input = ".t {}"; + let cmd = command(&input).expect("Expected transact command"); + match cmd { + Command::Transact(edn) => assert_eq!(edn, "{}"), + _ => assert!(false) + } + } + + #[test] + fn test_transact_parser_no_edn() { + let input = ".t "; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_transact_parser_invalid_start_char() { + let input = ".t :db/add \"s\" :db/ident :foo/uuid"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + #[test] fn test_parser_preceeding_trailing_whitespace() { let input = " .close "; diff --git a/tools/cli/src/mentat_cli/input.rs b/tools/cli/src/mentat_cli/input.rs index 0679b3b6a..2a614e5ad 100644 --- a/tools/cli/src/mentat_cli/input.rs +++ b/tools/cli/src/mentat_cli/input.rs @@ -22,6 +22,12 @@ use command_parser::{ use errors as cli; +/// Starting prompt +const DEFAULT_PROMPT: &'static str = "mentat=> "; +/// Prompt when further input is being read +// TODO: Should this actually reflect the current open brace? +const MORE_PROMPT: &'static str = "mentat.> "; + /// Possible results from reading input from `InputReader` #[derive(Clone, Debug)] pub enum InputResult { @@ -29,8 +35,8 @@ pub enum InputResult { MetaCommand(Command), /// An empty line Empty, - /// Needs more input; i.e. there is an unclosed delimiter - More(Command), + /// Needs more input + More, /// End of file reached Eof, } @@ -39,6 +45,7 @@ pub enum InputResult { pub struct InputReader { buffer: String, reader: Option>, + in_process_cmd: Option, } impl InputReader { @@ -55,6 +62,7 @@ impl InputReader { InputReader{ buffer: String::new(), reader: r, + in_process_cmd: None, } } @@ -66,7 +74,8 @@ impl InputReader { /// Reads a single command, item, or statement from `stdin`. /// Returns `More` if further input is required for a complete result. /// In this case, the input received so far is buffered internally. - pub fn read_input(&mut self, prompt: &str) -> Result { + pub fn read_input(&mut self) -> Result { + let prompt = if self.in_process_cmd.is_some() { MORE_PROMPT } else { DEFAULT_PROMPT }; let line = match self.read_line(prompt) { Some(s) => s, None => return Ok(Eof), @@ -80,15 +89,35 @@ impl InputReader { self.add_history(&line); - let cmd = try!(command(&self.buffer)); + // if we have a command in process (i.e. in incomplete query or transaction), + // then we already know which type of command it is and so we don't need to parse the + // command again, only the content, which we do later. + // Therefore, we add the newly read in line to the existing command args. + // If there is no in process command, we parse the read in line as a new command. + let cmd = match &self.in_process_cmd { + &Some(Command::Query(ref args)) => { + Command::Query(args.clone() + " " + &line) + }, + &Some(Command::Transact(ref args)) => { + Command::Transact(args.clone() + " " + &line) + }, + _ => { + try!(command(&self.buffer)) + } + }; match cmd { Command::Query(_) | Command::Transact(_) if !cmd.is_complete() => { - Ok(More(cmd)) + // a query or transact is complete if it contains a valid edn. + // if the command is not complete, ask for more from the repl and remember + // which type of command we've found here. + self.in_process_cmd = Some(cmd); + Ok(More) }, _ => { self.buffer.clear(); + self.in_process_cmd = None; Ok(InputResult::MetaCommand(cmd)) } } diff --git a/tools/cli/src/mentat_cli/lib.rs b/tools/cli/src/mentat_cli/lib.rs index 76f7d602d..bcc7c71c7 100644 --- a/tools/cli/src/mentat_cli/lib.rs +++ b/tools/cli/src/mentat_cli/lib.rs @@ -21,6 +21,10 @@ extern crate linefeed; extern crate rusqlite; extern crate mentat; +extern crate edn; +extern crate mentat_query; +extern crate mentat_core; +extern crate mentat_db; use getopts::Options; @@ -38,6 +42,8 @@ pub fn run() -> i32 { opts.optopt("d", "", "The path to a database to open", "DATABASE"); opts.optflag("h", "help", "Print this help message and exit"); + opts.optmulti("q", "query", "Execute a query on startup. Queries are executed after any transacts.", "QUERY"); + opts.optmulti("t", "transact", "Execute a transact on startup. Transacts are executed before queries.", "TRANSACT"); opts.optflag("v", "version", "Print version and exit"); let matches = match opts.parse(&args[1..]) { @@ -58,11 +64,33 @@ pub fn run() -> i32 { return 0; } - let db_name = matches.opt_str("d"); - - let repl = repl::Repl::new(db_name); + let mut last_arg: Option<&str> = None; + let cmds:Vec = args.iter().filter_map(|arg| { + match last_arg { + Some("-d") => { + last_arg = None; + Some(command_parser::Command::Open(arg.clone())) + }, + Some("-q") => { + last_arg = None; + Some(command_parser::Command::Query(arg.clone())) + }, + Some("-t") => { + last_arg = None; + Some(command_parser::Command::Transact(arg.clone())) + }, + Some(_) | + None => { + last_arg = Some(&arg); + None + }, + } + }).collect(); + + let repl = repl::Repl::new(); if repl.is_ok() { - repl.unwrap().run(); + repl.unwrap().run(Some(cmds)); + } else { println!("{}", repl.err().unwrap()); } diff --git a/tools/cli/src/mentat_cli/repl.rs b/tools/cli/src/mentat_cli/repl.rs index 0ad0b99ef..9f84060c3 100644 --- a/tools/cli/src/mentat_cli/repl.rs +++ b/tools/cli/src/mentat_cli/repl.rs @@ -10,10 +10,17 @@ use std::collections::HashMap; +use mentat::query::QueryResults; +use mentat_core::TypedValue; + use command_parser::{ Command, HELP_COMMAND, - OPEN_COMMAND + OPEN_COMMAND, + LONG_QUERY_COMMAND, + SHORT_QUERY_COMMAND, + LONG_TRANSACT_COMMAND, + SHORT_TRANSACT_COMMAND, }; use input::InputReader; use input::InputResult::{ @@ -27,50 +34,54 @@ use store::{ db_output_name }; -/// Starting prompt -const DEFAULT_PROMPT: &'static str = "mentat=> "; -/// Prompt when further input is being read -// TODO: Should this actually reflect the current open brace? -const MORE_PROMPT: &'static str = "mentat.> "; - lazy_static! { static ref COMMAND_HELP: HashMap<&'static str, &'static str> = { let mut map = HashMap::new(); map.insert(HELP_COMMAND, "Show help for commands."); map.insert(OPEN_COMMAND, "Open a database at path."); + map.insert(LONG_QUERY_COMMAND, "Execute a query against the current open database."); + map.insert(SHORT_QUERY_COMMAND, "Shortcut for `.query`. Execute a query against the current open database."); + map.insert(LONG_TRANSACT_COMMAND, "Execute a transact against the current open database."); + map.insert(SHORT_TRANSACT_COMMAND, "Shortcut for `.transact`. Execute a transact against the current open database."); map }; } /// Executes input and maintains state of persistent items. pub struct Repl { - store: Store, + store: Store } impl Repl { /// Constructs a new `Repl`. - pub fn new(db_name: Option) -> Result { - let store = try!(Store::new(db_name.clone()).map_err(|e| e.to_string())); + pub fn new() -> Result { + let store = Store::new(None).map_err(|e| e.to_string())?; Ok(Repl{ store: store, }) } /// Runs the REPL interactively. - pub fn run(&mut self) { - let mut more: Option = None; + pub fn run(&mut self, startup_commands: Option>) { let mut input = InputReader::new(); + if let Some(cmds) = startup_commands { + for command in cmds.iter() { + println!("{}", command.output()); + self.handle_command(command.clone()); + } + } + loop { - let res = input.read_input(if more.is_some() { MORE_PROMPT } else { DEFAULT_PROMPT }); + let res = input.read_input(); + match res { Ok(MetaCommand(cmd)) => { debug!("read command: {:?}", cmd); - more = None; self.handle_command(cmd); }, - Ok(Empty) => (), - Ok(More(cmd)) => { more = Some(cmd); }, + Ok(Empty) | + Ok(More) => (), Ok(Eof) => { if input.is_tty() { println!(""); @@ -99,7 +110,8 @@ impl Repl { Err(e) => println!("{}", e.to_string()) }; }, - _ => unimplemented!(), + Command::Query(query) => self.execute_query(query), + Command::Transact(transaction) => self.execute_transact(transaction), } } @@ -122,6 +134,66 @@ impl Repl { } } } + + pub fn execute_query(&self, query: String) { + let results = match self.store.query(query){ + Result::Ok(vals) => { + vals + }, + Result::Err(err) => return println!("{:?}.", err), + }; + + if results.is_empty() { + println!("No results found.") + } + + 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))); + } + }, + QueryResults::Coll(vv) => { + for val in vv { + output.push_str(&format!("{}\n", 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))); + } + output.push_str("\n"); + } + }, + _ => output.push_str(&format!("No results found.")) + } + println!("\n{}", output); + } + + pub fn execute_transact(&mut self, transaction: String) { + match self.store.transact(transaction) { + Result::Ok(report) => println!("{:?}", report), + Result::Err(err) => println!("{:?}.", err), + } + } + + fn typed_value_as_string(&self, value: TypedValue) -> String { + match value { + TypedValue::Boolean(b) => if b { "true".to_string() } else { "false".to_string() }, + TypedValue::Double(d) => format!("{}", d), + TypedValue::Instant(i) => format!("{}", i), + TypedValue::Keyword(k) => format!("{}", k), + TypedValue::Long(l) => format!("{}", l), + TypedValue::Ref(r) => format!("{}", r), + TypedValue::String(s) => format!("{:?}", s.to_string()), + TypedValue::Uuid(u) => format!("{}", u), + } + } } #[cfg(test)] diff --git a/tools/cli/src/mentat_cli/store.rs b/tools/cli/src/mentat_cli/store.rs index a1689f63f..bc49443c6 100644 --- a/tools/cli/src/mentat_cli/store.rs +++ b/tools/cli/src/mentat_cli/store.rs @@ -9,13 +9,16 @@ // specific language governing permissions and limitations under the License. use rusqlite; + +use errors as cli; + use mentat::{ new_connection, }; +use mentat::query::QueryResults; use mentat::conn::Conn; - -use errors as cli; +use mentat_db::types::TxReport; pub struct Store { handle: rusqlite::Connection, @@ -48,4 +51,11 @@ impl Store { self.open(None) } + pub fn query(&self, query: String) -> Result { + Ok(try!(self.conn.q_once(&self.handle, &query, None))) + } + + pub fn transact(&mut self, transaction: String) -> Result { + Ok(try!(self.conn.transact(&mut self.handle, &transaction))) + } }