From 2b4a0fc7a8d5a0a918e0cb72d3e441b0d9b78f91 Mon Sep 17 00:00:00 2001 From: Ziyang Hu Date: Tue, 3 Jan 2023 21:12:17 +0800 Subject: [PATCH] REPL implementation based on contribution: https://github.com/cozodb/cozo/issues/31 --- Cargo.lock | 175 +++++++++++++++++++++++++++++++++++++++++ cozoserver/Cargo.toml | 3 + cozoserver/README.md | 6 ++ cozoserver/src/main.rs | 20 +++++ cozoserver/src/repl.rs | 113 ++++++++++++++++++++++++++ 5 files changed, 317 insertions(+) create mode 100644 cozoserver/src/repl.rs diff --git a/Cargo.lock b/Cargo.lock index 3dae3010..266a8ec8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,6 +474,17 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clipboard-win" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + [[package]] name = "cmake" version = "0.1.49" @@ -658,8 +669,11 @@ dependencies = [ "cozo", "env_logger", "log", + "miette", + "prettytable", "rand 0.8.5", "rouille", + "rustyline", "serde", "serde_derive", "serde_json", @@ -844,6 +858,27 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "document-features" version = "0.2.7" @@ -859,6 +894,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.31" @@ -868,6 +909,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "env_logger" version = "0.10.0" @@ -902,6 +949,16 @@ dependencies = [ "libc", ] +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + [[package]] name = "fail" version = "0.4.0" @@ -928,6 +985,17 @@ dependencies = [ "instant", ] +[[package]] +name = "fd-lock" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb21c69b9fea5e15dbc1049e4b77145dd0ba1c84019c488102de0dc4ea4b0a27" +dependencies = [ + "cfg-if 1.0.0", + "rustix", + "windows-sys 0.42.0", +] + [[package]] name = "filetime" version = "0.2.19" @@ -1882,6 +1950,26 @@ dependencies = [ "smallvec", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "libc", +] + [[package]] name = "nom" version = "5.1.2" @@ -2309,6 +2397,20 @@ dependencies = [ "syn", ] +[[package]] +name = "prettytable" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46480520d1b77c9a3482d39939fcf96831537a250ec62d4fd8fbdf8e0302e781" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + [[package]] name = "priority-queue" version = "1.3.0" @@ -2585,6 +2687,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.7.3" @@ -2687,6 +2799,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.8", + "redox_syscall", + "thiserror", +] + [[package]] name = "regex" version = "1.7.0" @@ -2866,6 +2989,35 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + +[[package]] +name = "rustyline" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1cd5ae51d3f7bf65d7969d579d502168ef578f289452bd8ccc91de28fda20e" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "clipboard-win", + "dirs-next", + "fd-lock", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "scopeguard", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + [[package]] name = "ryu" version = "1.0.12" @@ -3142,6 +3294,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + [[package]] name = "strsim" version = "0.10.0" @@ -3263,6 +3421,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -3679,6 +3848,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "uuid" version = "1.2.2" diff --git a/cozoserver/Cargo.toml b/cozoserver/Cargo.toml index 4fc2bf17..47e31974 100644 --- a/cozoserver/Cargo.toml +++ b/cozoserver/Cargo.toml @@ -52,3 +52,6 @@ serde_derive = "1.0.137" serde = { version = "1.0.137" } chrono = "0.4.19" serde_json = "1.0.81" +prettytable = "0.10.0" +rustyline = "10.0.0" +miette = { version = "5.5.0", features = ["fancy"] } diff --git a/cozoserver/README.md b/cozoserver/README.md index 75bdcd9e..25cf4ff6 100644 --- a/cozoserver/README.md +++ b/cozoserver/README.md @@ -26,6 +26,12 @@ see `./cozoserver -h` To stop Cozo, press `CTRL-C`, or send `SIGTERM` to the process with e.g. `kill`. +## The REPL + +If you start the server with the `-r` or `--repl` option, a web server will not be started. +Instead, a terminal-based REPL is presented for you. The engine options can be used when +invoking the executable to choose the backend. + ## The query API Queries are run by sending HTTP POST requests to the server. diff --git a/cozoserver/src/main.rs b/cozoserver/src/main.rs index c1152ebc..bda0ce31 100644 --- a/cozoserver/src/main.rs +++ b/cozoserver/src/main.rs @@ -10,6 +10,7 @@ use std::collections::BTreeMap; use std::fmt::Debug; use std::fs; use std::net::Ipv6Addr; +use std::process::exit; use std::str::FromStr; use clap::Parser; @@ -21,6 +22,10 @@ use serde_json::json; use cozo::*; +use crate::repl::repl_main; + +mod repl; + #[derive(Parser, Debug)] #[clap(version, about, long_about = None)] struct Args { @@ -40,6 +45,10 @@ struct Args { #[clap(short, long, default_value_t = String::from("{}"))] config: String, + /// When on, start REPL instead of starting a webserver + #[clap(short, long)] + repl: bool, + /// Address to bind the service to #[clap(short, long, default_value_t = String::from("127.0.0.1"))] bind: String, @@ -80,6 +89,17 @@ fn main() { db.restore_backup(restore_path).unwrap(); } + if args.repl { + if let Err(e) = repl_main(db) { + eprintln!("{}", e); + exit(-1); + } + } else { + server_main(args, db) + } +} + +fn server_main(args: Args, db: DbInstance) { let conf_path = format!("{}.{}.cozo_auth", args.path, args.engine); let auth_guard = match fs::read_to_string(&conf_path) { Ok(s) => s.trim().to_string(), diff --git a/cozoserver/src/repl.rs b/cozoserver/src/repl.rs new file mode 100644 index 00000000..9b11da2d --- /dev/null +++ b/cozoserver/src/repl.rs @@ -0,0 +1,113 @@ +/* + * Copyright 2022, The Cozo Project Authors. + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// This file is based on code contributed by https://github.com/rhn + +use std::error::Error; + +use prettytable; +use rustyline; + +use cozo; +use cozo::DbInstance; + +struct Indented; + +impl rustyline::hint::Hinter for Indented { + type Hint = String; +} + +impl rustyline::highlight::Highlighter for Indented {} +impl rustyline::completion::Completer for Indented { + type Candidate = String; + + fn update( + &self, + _line: &mut rustyline::line_buffer::LineBuffer, + _start: usize, + _elected: &str, + ) { + unreachable!(); + } +} + +impl rustyline::Helper for Indented {} + +impl rustyline::validate::Validator for Indented { + fn validate( + &self, + ctx: &mut rustyline::validate::ValidationContext<'_>, + ) -> rustyline::Result { + Ok(if ctx.input().starts_with(" ") { + if ctx.input().ends_with("\n") { + rustyline::validate::ValidationResult::Valid(None) + } else { + rustyline::validate::ValidationResult::Incomplete + } + } else { + rustyline::validate::ValidationResult::Valid(None) + }) + } +} + +pub(crate) fn repl_main(db: DbInstance) -> Result<(), Box> { + let mut exit = false; + let mut rl = rustyline::Editor::::new()?; + rl.set_helper(Some(Indented)); + + loop { + let readline = rl.readline("=> "); + match readline { + Ok(line) => { + match db.run_script(&line, Default::default()) { + Ok(out) => { + use prettytable::format; + let mut table = prettytable::Table::new(); + let headers = out + .headers + .iter() + .map(prettytable::Cell::from) + .collect::>(); + table.set_titles(prettytable::Row::new(headers)); + let rows = out + .rows + .iter() + .map(|r| r.iter().map(|c| format!("{}", c)).collect::>()) + .collect::>(); + let rows = rows + .iter() + .map(|r| r.iter().map(prettytable::Cell::from).collect::>()); + for row in rows { + table.add_row(prettytable::Row::new(row)); + } + table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.printstd(); + } + Err(mut err) => { + if err.source_code().is_none() { + err = err.with_source_code(line.to_string()); + } + eprintln!("{:?}", err); + } + }; + rl.add_history_entry(line); + } + Err(rustyline::error::ReadlineError::Interrupted) => { + if exit { + break; + } else { + println!("Again to exit"); + exit = true; + } + } + Err(rustyline::error::ReadlineError::Eof) => break, + Err(e) => eprintln!("{:?}", e), + } + } + Ok(()) +}