2023-05-22 19:19:35 +00:00
|
|
|
use crate::cli::abstraction::{
|
2023-05-26 10:35:46 +00:00
|
|
|
AuthArguments, DatabaseConnectionArguments, DatabaseSelectionOptionalArguments,
|
2023-05-22 19:19:35 +00:00
|
|
|
};
|
2023-10-26 15:25:12 +00:00
|
|
|
use crate::cnf::PKG_VERSION;
|
2022-08-06 10:34:43 +00:00
|
|
|
use crate::err::Error;
|
2023-05-22 19:19:35 +00:00
|
|
|
use clap::Args;
|
2022-08-06 10:34:43 +00:00
|
|
|
use rustyline::error::ReadlineError;
|
2023-05-12 19:47:41 +00:00
|
|
|
use rustyline::validate::{ValidationContext, ValidationResult, Validator};
|
|
|
|
use rustyline::{Completer, Editor, Helper, Highlighter, Hinter};
|
2023-05-31 07:36:50 +00:00
|
|
|
use serde::Serialize;
|
|
|
|
use serde_json::ser::PrettyFormatter;
|
2023-08-30 20:34:46 +00:00
|
|
|
use surrealdb::dbs::Capabilities;
|
2023-01-07 08:32:18 +00:00
|
|
|
use surrealdb::engine::any::connect;
|
2022-12-30 21:27:19 +00:00
|
|
|
use surrealdb::opt::auth::Root;
|
2023-08-22 09:23:49 +00:00
|
|
|
use surrealdb::opt::Config;
|
2023-05-12 19:47:41 +00:00
|
|
|
use surrealdb::sql::{self, Statement, Value};
|
2023-06-09 13:45:07 +00:00
|
|
|
use surrealdb::Response;
|
2022-08-06 10:34:43 +00:00
|
|
|
|
2023-05-22 19:19:35 +00:00
|
|
|
#[derive(Args, Debug)]
|
|
|
|
pub struct SqlCommandArguments {
|
|
|
|
#[command(flatten)]
|
|
|
|
conn: DatabaseConnectionArguments,
|
|
|
|
#[command(flatten)]
|
|
|
|
auth: AuthArguments,
|
|
|
|
#[command(flatten)]
|
2023-05-26 10:35:46 +00:00
|
|
|
sel: Option<DatabaseSelectionOptionalArguments>,
|
2023-05-22 19:19:35 +00:00
|
|
|
/// Whether database responses should be pretty printed
|
|
|
|
#[arg(long)]
|
|
|
|
pretty: bool,
|
2023-05-31 07:36:50 +00:00
|
|
|
/// Whether to emit results in JSON
|
|
|
|
#[arg(long)]
|
|
|
|
json: bool,
|
2023-05-22 19:19:35 +00:00
|
|
|
/// Whether omitting semicolon causes a newline
|
|
|
|
#[arg(long)]
|
|
|
|
multi: bool,
|
2023-10-26 15:25:12 +00:00
|
|
|
/// Whether to show welcome message
|
|
|
|
#[arg(long, env = "SURREAL_HIDE_WELCOME")]
|
|
|
|
hide_welcome: bool,
|
2023-05-22 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn init(
|
|
|
|
SqlCommandArguments {
|
|
|
|
auth: AuthArguments {
|
|
|
|
username,
|
|
|
|
password,
|
|
|
|
},
|
|
|
|
conn: DatabaseConnectionArguments {
|
|
|
|
endpoint,
|
|
|
|
},
|
2023-05-26 10:35:46 +00:00
|
|
|
sel,
|
2023-05-22 19:19:35 +00:00
|
|
|
pretty,
|
2023-05-31 07:36:50 +00:00
|
|
|
json,
|
2023-05-22 19:19:35 +00:00
|
|
|
multi,
|
2023-10-26 15:25:12 +00:00
|
|
|
hide_welcome,
|
2023-05-22 19:19:35 +00:00
|
|
|
..
|
|
|
|
}: SqlCommandArguments,
|
|
|
|
) -> Result<(), Error> {
|
2023-03-29 18:16:18 +00:00
|
|
|
// Initialize opentelemetry and logging
|
2023-07-19 14:35:56 +00:00
|
|
|
crate::telemetry::builder().with_log_level("warn").init();
|
2023-08-30 20:34:46 +00:00
|
|
|
// Default datastore configuration for local engines
|
|
|
|
let config = Config::new().capabilities(Capabilities::all());
|
2023-05-22 19:19:35 +00:00
|
|
|
|
2023-07-29 18:47:25 +00:00
|
|
|
let client = if let Some((username, password)) = username.zip(password) {
|
|
|
|
let root = Root {
|
|
|
|
username: &username,
|
|
|
|
password: &password,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Connect to the database engine with authentication
|
|
|
|
//
|
|
|
|
// * For local engines, here we enable authentication and in the signin below we actually authenticate.
|
|
|
|
// * For remote engines, we connect to the endpoint and then signin.
|
|
|
|
#[cfg(feature = "has-storage")]
|
2023-08-30 20:34:46 +00:00
|
|
|
let address = (endpoint, config.user(root));
|
2023-07-29 18:47:25 +00:00
|
|
|
#[cfg(not(feature = "has-storage"))]
|
|
|
|
let address = endpoint;
|
|
|
|
let client = connect(address).await?;
|
|
|
|
|
|
|
|
// Sign in to the server
|
|
|
|
client.signin(root).await?;
|
|
|
|
client
|
|
|
|
} else {
|
2023-08-30 20:34:46 +00:00
|
|
|
connect((endpoint, config)).await?
|
2022-12-20 10:30:06 +00:00
|
|
|
};
|
2023-07-29 18:47:25 +00:00
|
|
|
|
2022-08-06 10:34:43 +00:00
|
|
|
// Create a new terminal REPL
|
2023-05-12 19:47:41 +00:00
|
|
|
let mut rl = Editor::new().unwrap();
|
|
|
|
// Set custom input validation
|
|
|
|
rl.set_helper(Some(InputValidator {
|
2023-05-22 19:19:35 +00:00
|
|
|
multi,
|
2023-05-12 19:47:41 +00:00
|
|
|
}));
|
2022-08-06 10:34:43 +00:00
|
|
|
// Load the command-line history
|
|
|
|
let _ = rl.load_history("history.txt");
|
2023-07-04 20:58:27 +00:00
|
|
|
// Configure the prompt
|
|
|
|
let mut prompt = "> ".to_owned();
|
2023-07-29 18:47:25 +00:00
|
|
|
// Keep track of current namespace/database.
|
2023-07-04 20:58:27 +00:00
|
|
|
if let Some(DatabaseSelectionOptionalArguments {
|
2023-05-26 10:35:46 +00:00
|
|
|
namespace,
|
|
|
|
database,
|
|
|
|
}) = sel
|
|
|
|
{
|
2023-07-04 20:58:27 +00:00
|
|
|
let is_not_empty = |s: &&str| !s.is_empty();
|
|
|
|
let namespace = namespace.as_deref().map(str::trim).filter(is_not_empty);
|
|
|
|
let database = database.as_deref().map(str::trim).filter(is_not_empty);
|
|
|
|
match (namespace, database) {
|
2023-05-05 18:12:19 +00:00
|
|
|
(Some(namespace), Some(database)) => {
|
2023-07-04 20:58:27 +00:00
|
|
|
client.use_ns(namespace).use_db(database).await?;
|
|
|
|
prompt = format!("{namespace}/{database}> ");
|
2023-05-05 18:12:19 +00:00
|
|
|
}
|
2023-07-04 20:58:27 +00:00
|
|
|
(Some(namespace), None) => {
|
|
|
|
client.use_ns(namespace).await?;
|
|
|
|
prompt = format!("{namespace}> ");
|
|
|
|
}
|
|
|
|
_ => {}
|
2022-12-30 21:27:19 +00:00
|
|
|
}
|
2023-07-29 18:47:25 +00:00
|
|
|
};
|
|
|
|
|
2023-10-26 15:25:12 +00:00
|
|
|
if !hide_welcome {
|
|
|
|
let hints = vec![
|
|
|
|
(true, "Different statements within a query should be separated by a (;) semicolon."),
|
|
|
|
(!multi, "To create a multi-line query, end your lines with a (\\) backslash, and press enter."),
|
|
|
|
(true, "To exit, send a SIGTERM or press CTRL+C")
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.filter(|(show, _)| *show)
|
|
|
|
.map(|(_, hint)| format!("# - {hint}"))
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join("\n");
|
|
|
|
|
|
|
|
eprintln!(
|
|
|
|
"
|
|
|
|
#
|
|
|
|
# Welcome to the SurrealDB SQL shell
|
|
|
|
#
|
|
|
|
# How to use this shell:
|
|
|
|
{hints}
|
|
|
|
#
|
|
|
|
# Consult https://surrealdb.com/docs/cli/sql for further instructions
|
|
|
|
#
|
|
|
|
# SurrealDB version: {}
|
|
|
|
#
|
|
|
|
",
|
|
|
|
*PKG_VERSION
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-07-04 20:58:27 +00:00
|
|
|
// Loop over each command-line input
|
|
|
|
loop {
|
2023-05-12 19:47:41 +00:00
|
|
|
// Prompt the user to input SQL and check the input.
|
|
|
|
let line = match rl.readline(&prompt) {
|
2022-08-06 10:34:43 +00:00
|
|
|
// The user typed a query
|
|
|
|
Ok(line) => {
|
2023-05-12 21:09:07 +00:00
|
|
|
// Filter out all new lines
|
2023-05-12 19:47:41 +00:00
|
|
|
let line = filter_line_continuations(&line);
|
2022-08-06 10:34:43 +00:00
|
|
|
// Add the entry to the history
|
2023-03-25 20:49:00 +00:00
|
|
|
if let Err(e) = rl.add_history_entry(line.as_str()) {
|
|
|
|
eprintln!("{e}");
|
|
|
|
}
|
2023-05-12 19:47:41 +00:00
|
|
|
line
|
|
|
|
}
|
|
|
|
// The user typed CTRL-C or CTRL-D
|
|
|
|
Err(ReadlineError::Interrupted | ReadlineError::Eof) => {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// There was en error
|
|
|
|
Err(e) => {
|
|
|
|
eprintln!("Error: {e:?}");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// Complete the request
|
|
|
|
match sql::parse(&line) {
|
|
|
|
Ok(query) => {
|
2023-07-04 20:58:27 +00:00
|
|
|
let mut namespace = None;
|
|
|
|
let mut database = None;
|
|
|
|
let mut vars = Vec::new();
|
|
|
|
// Capture `use` and `set/let` statements from the query
|
2023-05-12 19:47:41 +00:00
|
|
|
for statement in query.iter() {
|
|
|
|
match statement {
|
|
|
|
Statement::Use(stmt) => {
|
2023-07-04 20:58:27 +00:00
|
|
|
if let Some(ns) = &stmt.ns {
|
|
|
|
namespace = Some(ns.clone());
|
2022-12-30 21:27:19 +00:00
|
|
|
}
|
2023-07-04 20:58:27 +00:00
|
|
|
if let Some(db) = &stmt.db {
|
|
|
|
database = Some(db.clone());
|
2023-04-14 18:41:37 +00:00
|
|
|
}
|
2023-05-12 19:47:41 +00:00
|
|
|
}
|
|
|
|
Statement::Set(stmt) => {
|
2023-07-04 20:58:27 +00:00
|
|
|
vars.push((stmt.name.clone(), stmt.what.clone()));
|
2022-12-30 21:27:19 +00:00
|
|
|
}
|
2023-05-12 19:47:41 +00:00
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
2023-07-04 20:58:27 +00:00
|
|
|
// Extract the namespace and database from the current prompt
|
|
|
|
let (prompt_ns, prompt_db) = split_prompt(&prompt);
|
|
|
|
// The namespace should be set before the database can be set
|
|
|
|
if namespace.is_none() && prompt_ns.is_empty() && database.is_some() {
|
|
|
|
eprintln!(
|
|
|
|
"There was a problem with the database: Specify a namespace to use\n"
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Run the query provided
|
2023-05-12 19:47:41 +00:00
|
|
|
let res = client.query(query).await;
|
2023-05-31 07:36:50 +00:00
|
|
|
match process(pretty, json, res) {
|
2023-05-12 19:47:41 +00:00
|
|
|
Ok(v) => {
|
|
|
|
println!("{v}\n");
|
2022-12-30 21:27:19 +00:00
|
|
|
}
|
2023-04-14 18:41:37 +00:00
|
|
|
Err(e) => {
|
2023-04-20 18:20:50 +00:00
|
|
|
eprintln!("{e}\n");
|
2023-07-04 20:58:27 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Persist the variables extracted from the query
|
|
|
|
for (key, value) in vars {
|
|
|
|
let _ = client.set(key, value).await;
|
|
|
|
}
|
|
|
|
// Process the last `use` statements, if any
|
|
|
|
if namespace.is_some() || database.is_some() {
|
|
|
|
// Use the namespace provided in the query if any, otherwise use the one in the prompt
|
|
|
|
let namespace = namespace.as_deref().unwrap_or(prompt_ns);
|
|
|
|
// Use the database provided in the query if any, otherwise use the one in the prompt
|
|
|
|
let database = database.as_deref().unwrap_or(prompt_db);
|
|
|
|
// If the database is empty we should only use the namespace
|
|
|
|
if database.is_empty() {
|
|
|
|
if client.use_ns(namespace).await.is_ok() {
|
|
|
|
prompt = format!("{namespace}> ");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Otherwise we should use both the namespace and database
|
|
|
|
else if client.use_ns(namespace).use_db(database).await.is_ok() {
|
|
|
|
prompt = format!("{namespace}/{database}> ");
|
2023-04-14 18:41:37 +00:00
|
|
|
}
|
2022-08-06 10:34:43 +00:00
|
|
|
}
|
|
|
|
}
|
2023-03-25 20:49:00 +00:00
|
|
|
Err(e) => {
|
2023-05-12 19:47:41 +00:00
|
|
|
eprintln!("{e}\n");
|
2022-08-06 10:34:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Save the inputs to the history
|
|
|
|
let _ = rl.save_history("history.txt");
|
|
|
|
// Everything OK
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-05-31 07:36:50 +00:00
|
|
|
fn process(pretty: bool, json: bool, res: surrealdb::Result<Response>) -> Result<String, Error> {
|
2023-04-20 18:20:50 +00:00
|
|
|
// Check query response for an error
|
|
|
|
let mut response = res?;
|
|
|
|
// Get the number of statements the query contained
|
|
|
|
let num_statements = response.num_statements();
|
|
|
|
// Prepare a single value from the query response
|
2023-10-26 14:06:41 +00:00
|
|
|
let mut output = Vec::<Value>::with_capacity(num_statements);
|
|
|
|
for index in 0..num_statements {
|
|
|
|
let result = response.take(index).unwrap_or_else(|e| e.to_string().into());
|
|
|
|
output.push(result);
|
|
|
|
}
|
|
|
|
|
2023-05-31 07:36:50 +00:00
|
|
|
// Check if we should emit JSON and/or prettify
|
|
|
|
Ok(match (json, pretty) {
|
|
|
|
// Don't prettify the SurrealQL response
|
2023-10-26 14:06:41 +00:00
|
|
|
(false, false) => Value::from(output).to_string(),
|
2023-05-31 07:36:50 +00:00
|
|
|
// Yes prettify the SurrealQL response
|
2023-10-26 14:06:41 +00:00
|
|
|
(false, true) => output
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.map(|(i, v)| format!("-- Query {:?}\n{v:#}", i + 1))
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join("\n"),
|
2023-05-31 07:36:50 +00:00
|
|
|
// Don't pretty print the JSON response
|
2023-10-26 14:06:41 +00:00
|
|
|
(true, false) => serde_json::to_string(&Value::from(output).into_json()).unwrap(),
|
2023-05-31 07:36:50 +00:00
|
|
|
// Yes prettify the JSON response
|
2023-10-26 14:06:41 +00:00
|
|
|
(true, true) => output
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.map(|(i, v)| {
|
|
|
|
let mut buf = Vec::new();
|
|
|
|
let mut serializer = serde_json::Serializer::with_formatter(
|
|
|
|
&mut buf,
|
|
|
|
PrettyFormatter::with_indent(b"\t"),
|
|
|
|
);
|
|
|
|
|
|
|
|
v.clone().into_json().serialize(&mut serializer).unwrap();
|
|
|
|
let v = String::from_utf8(buf).unwrap();
|
|
|
|
format!("-- Query {:?}\n{v:#}", i + 1)
|
|
|
|
})
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join("\n"),
|
2023-04-14 18:41:37 +00:00
|
|
|
})
|
2022-08-06 10:34:43 +00:00
|
|
|
}
|
2023-05-12 19:47:41 +00:00
|
|
|
|
|
|
|
#[derive(Completer, Helper, Highlighter, Hinter)]
|
|
|
|
struct InputValidator {
|
|
|
|
/// If omitting semicolon causes newline.
|
|
|
|
multi: bool,
|
|
|
|
}
|
|
|
|
|
2023-05-12 21:09:07 +00:00
|
|
|
#[allow(clippy::if_same_then_else)]
|
2023-05-12 19:47:41 +00:00
|
|
|
impl Validator for InputValidator {
|
|
|
|
fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result<ValidationResult> {
|
|
|
|
use ValidationResult::{Incomplete, Invalid, Valid};
|
2023-05-12 21:09:07 +00:00
|
|
|
// Filter out all new line characters
|
2023-05-12 19:47:41 +00:00
|
|
|
let input = filter_line_continuations(ctx.input());
|
2023-05-12 21:09:07 +00:00
|
|
|
// Trim all whitespace from the user input
|
|
|
|
let input = input.trim();
|
|
|
|
// Process the input to check if we can send the query
|
|
|
|
let result = if self.multi && !input.ends_with(';') {
|
2023-05-23 22:10:37 +00:00
|
|
|
Incomplete // The line doesn't end with a ; and we are in multi mode
|
2023-05-12 21:09:07 +00:00
|
|
|
} else if self.multi && input.is_empty() {
|
|
|
|
Incomplete // The line was empty and we are in multi mode
|
|
|
|
} else if input.ends_with('\\') {
|
|
|
|
Incomplete // The line ends with a backslash
|
|
|
|
} else if let Err(e) = sql::parse(input) {
|
2023-05-12 19:47:41 +00:00
|
|
|
Invalid(Some(format!(" --< {e}")))
|
|
|
|
} else {
|
|
|
|
Valid(None)
|
|
|
|
};
|
2023-05-12 21:09:07 +00:00
|
|
|
// Validation complete
|
2023-05-12 19:47:41 +00:00
|
|
|
Ok(result)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn filter_line_continuations(line: &str) -> String {
|
|
|
|
line.replace("\\\n", "").replace("\\\r\n", "")
|
|
|
|
}
|
2023-07-04 20:58:27 +00:00
|
|
|
|
|
|
|
fn split_prompt(prompt: &str) -> (&str, &str) {
|
|
|
|
let selection = prompt.split_once('>').unwrap().0;
|
|
|
|
selection.split_once('/').unwrap_or((selection, ""))
|
|
|
|
}
|