diff --git a/cozoserver/README-zh.md b/cozoserver/README-zh.md index 1a258771..2e2d2c6a 100644 --- a/cozoserver/README-zh.md +++ b/cozoserver/README-zh.md @@ -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 格式的正文,具体内容如下: diff --git a/cozoserver/README.md b/cozoserver/README.md index 985f2cee..cae78ada 100644 --- a/cozoserver/README.md +++ b/cozoserver/README.md @@ -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 `: set a parameter that can be used in queries. +* `%unset `: unset a parameter. +* `%clear`: unset all parameters. +* `%params`: print all set parameters. +* `%save `: the result of the next successful query will be saved in JSON format in a file instead of printed on screen. If `` is omitted, then the effect of any previous `%save` command is nullified. +* `%backup `: the current database will be backed up into the file. +* `%restore `: 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. diff --git a/cozoserver/src/repl.rs b/cozoserver/src/repl.rs index 9b11da2d..d89045ba 100644 --- a/cozoserver/src/repl.rs +++ b/cozoserver/src/repl.rs @@ -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> { + 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::::new()?; + let mut params = BTreeMap::new(); + let mut save_next: Option = 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::>(); - 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)); + 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 '.") + } } - 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!(¶ms)) { + 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::>(); + 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) => {