diff --git a/Cargo.toml b/Cargo.toml index 6cb47d11..ff694e36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ version = "0.4.0" build = "build/version.rs" [workspace] -members = [] +members = ["tools/cli"] [build-dependencies] rustc_version = "0.1.7" @@ -61,6 +61,3 @@ path = "query-translator" [dependencies.mentat_tx_parser] path = "tx-parser" - -[dependencies.mentat_cli] -path = "tools/cli" diff --git a/build/version.rs b/build/version.rs index 54f8b2a4..6ad73157 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 07f5bc5b..a67c6562 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 67554a93..cd74523b 100644 --- a/tools/cli/Cargo.toml +++ b/tools/cli/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "mentat_cli" version = "0.0.1" -workspace = "../.." [lib] name = "mentat_cli" @@ -18,3 +17,29 @@ env_logger = "0.3" linefeed = "0.1" log = "0.3" tempfile = "1.1" +combine = "2.2.2" +lazy_static = "0.2.2" +error-chain = "0.8.1" + +[dependencies.rusqlite] +version = "0.11" +# System sqlite might be very old. +features = ["bundled", "limits"] + +[dependencies.mentat] +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 new file mode 100644 index 00000000..aa81e4d7 --- /dev/null +++ b/tools/cli/src/mentat_cli/command_parser.rs @@ -0,0 +1,428 @@ +// 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 combine::{ + any, + eof, + look_ahead, + many1, + parser, + satisfy, + sep_end_by, + token, + Parser +}; +use combine::char::{ + space, + spaces, + string +}; +use combine::combinator::{ + choice, + 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(String), + Query(String), + Help(Vec), + Open(String), + Close, +} + +impl Command { + /// is_complete returns true if no more input is required for the command to be successfully executed. + /// false is returned if the command is not considered valid. + /// Defaults to true for all commands except Query and Transact. + /// 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(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 { + let arguments = || sep_end_by::, _, _>(many1(satisfy(|c: char| !c.is_whitespace())), many1::, _>(space())).expected("arguments"); + + let help_parser = string(HELP_COMMAND) + .with(spaces()) + .with(arguments()) + .map(|args| { + Ok(Command::Help(args.clone())) + }); + + let open_parser = string(OPEN_COMMAND) + .with(spaces()) + .with(arguments()) + .map(|args| { + if args.len() < 1 { + bail!(cli::ErrorKind::CommandParse("Missing required argument".to_string())); + } + if args.len() > 1 { + 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 { + 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>; 5], _> + ([&mut try(help_parser), + &mut try(open_parser), + &mut try(close_parser), + &mut try(query_parser), + &mut try(transact_parser)])) + .parse(s) + .unwrap_or((Err(cli::ErrorKind::CommandParse(format!("Invalid command {:?}", s)).into()), "")).0 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_help_parser_multiple_args() { + let input = ".help command1 command2"; + let cmd = command(&input).expect("Expected help command"); + match cmd { + Command::Help(args) => { + assert_eq!(args, vec!["command1", "command2"]); + }, + _ => assert!(false) + } + } + + #[test] + fn test_help_parser_dot_arg() { + let input = ".help .command1"; + let cmd = command(&input).expect("Expected help command"); + match cmd { + Command::Help(args) => { + assert_eq!(args, vec![".command1"]); + }, + _ => assert!(false) + } + } + + #[test] + fn test_help_parser_no_args() { + let input = ".help"; + let cmd = command(&input).expect("Expected help command"); + match cmd { + Command::Help(args) => { + let empty: Vec = vec![]; + assert_eq!(args, empty); + }, + _ => assert!(false) + } + } + + #[test] + fn test_help_parser_no_args_trailing_whitespace() { + let input = ".help "; + let cmd = command(&input).expect("Expected help command"); + match cmd { + Command::Help(args) => { + let empty: Vec = vec![]; + assert_eq!(args, empty); + }, + _ => assert!(false) + } + } + + #[test] + fn test_open_parser_multiple_args() { + let input = ".open database1 database2"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), "Unrecognized argument \"database2\""); + } + + #[test] + fn test_open_parser_single_arg() { + let input = ".open database1"; + let cmd = command(&input).expect("Expected open command"); + match cmd { + Command::Open(arg) => { + assert_eq!(arg, "database1".to_string()); + }, + _ => assert!(false) + } + } + + #[test] + fn test_open_parser_path_arg() { + let input = ".open /path/to/my.db"; + let cmd = command(&input).expect("Expected open command"); + match cmd { + Command::Open(arg) => { + assert_eq!(arg, "/path/to/my.db".to_string()); + }, + _ => assert!(false) + } + } + + #[test] + fn test_open_parser_file_arg() { + let input = ".open my.db"; + let cmd = command(&input).expect("Expected open command"); + match cmd { + Command::Open(arg) => { + assert_eq!(arg, "my.db".to_string()); + }, + _ => assert!(false) + } + } + + #[test] + fn test_open_parser_no_args() { + let input = ".open"; + let err = command(&input).expect_err("Expected an error"); + 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"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_close_parser_no_args() { + let input = ".close"; + let cmd = command(&input).expect("Expected close command"); + match cmd { + Command::Close => assert!(true), + _ => assert!(false) + } + } + + #[test] + fn test_close_parser_no_args_trailing_whitespace() { + let input = ".close "; + let cmd = command(&input).expect("Expected close command"); + match cmd { + Command::Close => assert!(true), + _ => assert!(false) + } + } + + #[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 "; + let cmd = command(&input).expect("Expected close command"); + match cmd { + Command::Close => assert!(true), + _ => assert!(false) + } + } + + #[test] + fn test_command_parser_no_dot() { + let input = "help command1 command2"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } + + #[test] + fn test_command_parser_invalid_cmd() { + let input = ".foo command1"; + let err = command(&input).expect_err("Expected an error"); + assert_eq!(err.to_string(), format!("Invalid command {:?}", input)); + } +} diff --git a/tools/cli/src/mentat_cli/errors.rs b/tools/cli/src/mentat_cli/errors.rs new file mode 100644 index 00000000..8e095ab3 --- /dev/null +++ b/tools/cli/src/mentat_cli/errors.rs @@ -0,0 +1,36 @@ +// Copyright 2016 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. + +#![allow(dead_code)] + +use rusqlite; + +use mentat::errors as mentat; + +error_chain! { + types { + Error, ErrorKind, ResultExt, Result; + } + + foreign_links { + Rusqlite(rusqlite::Error); + } + + links { + MentatError(mentat::Error, mentat::ErrorKind); + } + + errors { + CommandParse(message: String) { + description("An error occured parsing the entered command") + display("{}", message) + } + } +} diff --git a/tools/cli/src/mentat_cli/input.rs b/tools/cli/src/mentat_cli/input.rs index 267b3cf4..2a614e5a 100644 --- a/tools/cli/src/mentat_cli/input.rs +++ b/tools/cli/src/mentat_cli/input.rs @@ -8,21 +8,34 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. -use std::io::{self, stdin, BufRead, BufReader}; +use std::io::stdin; use linefeed::Reader; use linefeed::terminal::DefaultTerminal; use self::InputResult::*; +use command_parser::{ + Command, + command +}; + +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 { - /// rusti command as input; (name, rest of line) - Command(String, Option), + /// mentat command as input; (name, rest of line) + MetaCommand(Command), /// An empty line Empty, - /// Needs more input; i.e. there is an unclosed delimiter + /// Needs more input More, /// End of file reached Eof, @@ -32,6 +45,7 @@ pub enum InputResult { pub struct InputReader { buffer: String, reader: Option>, + in_process_cmd: Option, } impl InputReader { @@ -48,6 +62,7 @@ impl InputReader { InputReader{ buffer: String::new(), reader: r, + in_process_cmd: None, } } @@ -59,28 +74,53 @@ 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) -> InputResult { + 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 Eof, + None => return Ok(Eof), }; self.buffer.push_str(&line); if self.buffer.is_empty() { - return Empty; + return Ok(Empty); } self.add_history(&line); - let res = More; - - match res { - More => (), - _ => self.buffer.clear(), + // 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)) + } }; - res + match cmd { + Command::Query(_) | + Command::Transact(_) if !cmd.is_complete() => { + // 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)) + } + } } fn read_line(&mut self, prompt: &str) -> Option { @@ -88,7 +128,7 @@ impl InputReader { Some(ref mut r) => { r.set_prompt(prompt); r.read_line().ok().and_then(|line| line) - } + }, None => self.read_stdin() } } diff --git a/tools/cli/src/mentat_cli/lib.rs b/tools/cli/src/mentat_cli/lib.rs index 41d196fd..bcc7c71c 100644 --- a/tools/cli/src/mentat_cli/lib.rs +++ b/tools/cli/src/mentat_cli/lib.rs @@ -7,18 +7,32 @@ // 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. + #![crate_name = "mentat_cli"] #[macro_use] extern crate log; +#[macro_use] extern crate lazy_static; +#[macro_use] extern crate error_chain; +extern crate combine; extern crate env_logger; extern crate getopts; 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; +pub mod command_parser; +pub mod store; pub mod input; pub mod repl; +pub mod errors; pub fn run() -> i32 { env_logger::init().unwrap(); @@ -26,7 +40,10 @@ pub fn run() -> i32 { let args = std::env::args().collect::>(); let mut opts = Options::new(); + 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..]) { @@ -41,13 +58,42 @@ pub fn run() -> i32 { print_version(); return 0; } + if matches.opt_present("help") { print_usage(&args[0], &opts); return 0; } - let mut repl = repl::Repl::new(); - repl.run(); + 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(Some(cmds)); + + } else { + println!("{}", repl.err().unwrap()); + } 0 } diff --git a/tools/cli/src/mentat_cli/repl.rs b/tools/cli/src/mentat_cli/repl.rs index d7b95f58..9f84060c 100644 --- a/tools/cli/src/mentat_cli/repl.rs +++ b/tools/cli/src/mentat_cli/repl.rs @@ -7,63 +7,192 @@ // 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 input::{InputReader}; -use input::InputResult::{Command, Empty, More, Eof}; -/// Starting prompt -const DEFAULT_PROMPT: &'static str = "mentat=> "; -/// Prompt when further input is being read -const MORE_PROMPT: &'static str = "mentat.> "; -/// Prompt when a `.block` command is in effect -const BLOCK_PROMPT: &'static str = "mentat+> "; +use std::collections::HashMap; + +use mentat::query::QueryResults; +use mentat_core::TypedValue; + +use command_parser::{ + Command, + HELP_COMMAND, + OPEN_COMMAND, + LONG_QUERY_COMMAND, + SHORT_QUERY_COMMAND, + LONG_TRANSACT_COMMAND, + SHORT_TRANSACT_COMMAND, +}; +use input::InputReader; +use input::InputResult::{ + MetaCommand, + Empty, + More, + Eof +}; +use store::{ + Store, + db_output_name +}; + +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 } impl Repl { /// Constructs a new `Repl`. - pub fn new() -> Repl { - Repl{} + 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 = false; + 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 { MORE_PROMPT } else { DEFAULT_PROMPT }); - // let res = if self.read_block { - // self.read_block = false; - // input.read_block_input(BLOCK_PROMPT) - // } else { - // input.read_input(if more { MORE_PROMPT } else { DEFAULT_PROMPT }) - // }; + let res = input.read_input(); match res { - Command(name, args) => { - debug!("read command: {} {:?}", name, args); - - more = false; - self.handle_command(name, args); + Ok(MetaCommand(cmd)) => { + debug!("read command: {:?}", cmd); + self.handle_command(cmd); }, - Empty => (), - More => { more = true; }, - Eof => { + Ok(Empty) | + Ok(More) => (), + Ok(Eof) => { if input.is_tty() { println!(""); } break; - } - }; + }, + Err(e) => println!("{}", e.to_string()), + } } } /// Runs a single command input. - fn handle_command(&mut self, cmd: String, args: Option) { - println!("{:?} {:?}", cmd, args); + fn handle_command(&mut self, cmd: Command) { + 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()) + }; + }, + Command::Close => { + 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.to_string()) + }; + }, + Command::Query(query) => self.execute_query(query), + Command::Transact(transaction) => self.execute_transact(transaction), + } + } + + fn help_command(&self, args: Vec) { + if args.is_empty() { + for (cmd, msg) in COMMAND_HELP.iter() { + println!(".{} - {}", cmd, msg); + } + } else { + for mut arg in args { + if arg.chars().nth(0).unwrap() == '.' { + arg.remove(0); + } + let msg = COMMAND_HELP.get(arg.as_str()); + if msg.is_some() { + println!(".{} - {}", arg, msg.unwrap()); + } else { + println!("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), + }; + + 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), + } } } diff --git a/tools/cli/src/mentat_cli/store.rs b/tools/cli/src/mentat_cli/store.rs new file mode 100644 index 00000000..bc49443c --- /dev/null +++ b/tools/cli/src/mentat_cli/store.rs @@ -0,0 +1,61 @@ +// 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 errors as cli; + +use mentat::{ + new_connection, +}; +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(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))) + } +}