add meta ops to REPL

main
Ziyang Hu 2 years ago
parent 90c9268b16
commit 74dc326946

@ -24,6 +24,16 @@
在执行时加入 `-r``--repl` 参数可开启命令行界面REPL同时不会启动 web 服务。其它选择存储引擎的参数可一同使用。
在界面中可以使用以下特殊命令:
* `%set <键> <值>`:设置在查询中可用的参数值。
* `%unset <键>`:删除已设置的参数值。
* `%clear`:清空所有已设置的参数。
* `%params`:显示当前所有参数。
* `%save <文件>`:下一个成功查询的结果将会以 JSON 格式存储在指定的文件中。如果文件参数未给出,则清除上次的文件设置。
* `%backup <文件>`:备份全部数据至指定的文件。
* `%restore <文件>`:将指定的备份文件中的数据加载到当前数据库中。当前数据库必须为空。
## 查询 API
查询通过向 API 发送 POST 请求来完成。默认的请求地址是 `http://127.0.0.1:9070/text-query` 。请求必须包含 JSON 格式的正文,具体内容如下:

@ -32,6 +32,16 @@ If you start the server with the `-r` or `--repl` option, a web server will not
Instead, a terminal-based REPL is presented to you. The engine options can be used when
invoking the executable to choose the backend.
You can use the following meta ops in the REPL:
* `%set <KEY> <VALUE>`: set a parameter that can be used in queries.
* `%unset <KEY>`: unset a parameter.
* `%clear`: unset all parameters.
* `%params`: print all set parameters.
* `%save <FILE>`: the result of the next successful query will be saved in JSON format in a file instead of printed on screen. If `<FILE>` is omitted, then the effect of any previous `%save` command is nullified.
* `%backup <FILE>`: the current database will be backed up into the file.
* `%restore <FILE>`: restore the data in the backup to the current database. The current database must be empty.
## The query API
Queries are run by sending HTTP POST requests to the server.

@ -8,10 +8,13 @@
// This file is based on code contributed by https://github.com/rhn
use std::collections::BTreeMap;
use std::error::Error;
use std::io::Write;
use prettytable;
use rustyline;
use serde_json::{json, Value};
use cozo;
use cozo::DbInstance;
@ -56,45 +59,169 @@ impl rustyline::validate::Validator for Indented {
}
pub(crate) fn repl_main(db: DbInstance) -> Result<(), Box<dyn Error>> {
println!("Welcome to the Cozo REPL.");
println!("Type a space followed by newline to enter multiline mode.");
let mut exit = false;
let mut rl = rustyline::Editor::<Indented>::new()?;
let mut params = BTreeMap::new();
let mut save_next: Option<String> = None;
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::<Vec<_>>();
table.set_titles(prettytable::Row::new(headers));
let rows = out
.rows
.iter()
.map(|r| r.iter().map(|c| format!("{}", c)).collect::<Vec<_>>())
.collect::<Vec<_>>();
let rows = rows
.iter()
.map(|r| r.iter().map(prettytable::Cell::from).collect::<Vec<_>>());
for row in rows {
table.add_row(prettytable::Row::new(row));
if let Some(remaining) = line.strip_prefix("%") {
let remaining = remaining.trim();
let (op, payload) = remaining
.split_once(|c: char| c.is_whitespace())
.unwrap_or((remaining, ""));
match op {
"set" => {
if let Some((key, v_str)) =
payload.trim().split_once(|c: char| c.is_whitespace())
{
match serde_json::from_str(v_str) {
Ok(val) => {
params.insert(key.to_string(), val);
}
Err(e) => {
eprintln!("{:?}", e)
}
}
} else {
eprintln!("Bad set syntax. Should be '%set <KEY> <VALUE>'.")
}
}
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());
"unset" => {
let key = remaining.trim();
if params.remove(key).is_none() {
eprintln!("Key not found: '{}'", key)
}
}
"clear" => {
params.clear();
}
"params" => match serde_json::to_string_pretty(&json!(&params)) {
Ok(display) => {
println!("{}", display)
}
Err(err) => {
eprintln!("{:?}", err)
}
},
"backup" => {
let path = remaining.trim();
if path.is_empty() {
eprintln!("Backup requires a path");
} else {
match db.backup_db(path.to_string()) {
Ok(_) => {
println!("Backup written successfully to {}", path)
}
Err(err) => {
eprintln!("{:?}", err)
}
}
}
}
eprintln!("{:?}", err);
"restore" => {
let path = remaining.trim();
if path.is_empty() {
eprintln!("Restore requires a path");
} else {
match db.restore_backup(path) {
Ok(_) => {
println!("Backup successfully loaded from {}", path)
}
Err(err) => {
eprintln!("{:?}", err)
}
}
}
}
"save" => {
let next_path = remaining.trim();
if next_path.is_empty() {
eprintln!("Next result will NOT be saved to file");
} else {
eprintln!("Next result will be saved to file: {}", next_path);
save_next = Some(next_path.to_string())
}
}
op => eprintln!("Unknown op: {}", op),
}
};
} else {
match db.run_script(&line, params.clone()) {
Ok(out) => {
if let Some(path) = save_next.as_ref() {
println!(
"Query has returned {} rows, saving to file {}",
out.rows.len(),
path
);
let to_save = out
.rows
.iter()
.map(|row| -> Value {
row.iter()
.zip(out.headers.iter())
.map(|(v, k)| (k.to_string(), v.clone()))
.collect()
})
.collect();
let j_payload = Value::Array(to_save);
match std::fs::File::create(path) {
Ok(mut file) => {
match file.write_all(j_payload.to_string().as_bytes()) {
Ok(_) => {
save_next = None;
}
Err(err) => {
eprintln!("{:?}", err);
}
}
}
Err(err) => {
eprintln!("{:?}", err);
}
}
} else {
use prettytable::format;
let mut table = prettytable::Table::new();
let headers = out
.headers
.iter()
.map(prettytable::Cell::from)
.collect::<Vec<_>>();
table.set_titles(prettytable::Row::new(headers));
let rows = out
.rows
.iter()
.map(|r| r.iter().map(|c| format!("{}", c)).collect::<Vec<_>>())
.collect::<Vec<_>>();
let rows = rows.iter().map(|r| {
r.iter().map(prettytable::Cell::from).collect::<Vec<_>>()
});
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) => {

Loading…
Cancel
Save