use crate::commands::{CommandManager, LoopCondition};
use crate::executors::Result;
use crate::executors::command::CommandExecutor;
use crate::executors::sql::SqlExecutor;
use regex::Regex;
use rsql_core::{Configuration, EchoMode};
use rsql_drivers::Connection;
use rsql_formatters::writers::Output;
use rsql_formatters::{FormatterManager, FormatterOptions, Highlighter};
use rustyline::history::DefaultHistory;
use std::fmt;
use std::fmt::Debug;
pub struct Executor<'a> {
configuration: &'a mut Configuration,
command_manager: &'a CommandManager,
formatter_manager: &'a FormatterManager,
history: &'a DefaultHistory,
connection: &'a mut dyn Connection,
output: &'a mut Output,
}
impl<'a> Executor<'a> {
pub(crate) fn new(
configuration: &'a mut Configuration,
command_manager: &'a CommandManager,
formatter_manager: &'a FormatterManager,
history: &'a DefaultHistory,
connection: &'a mut dyn Connection,
output: &'a mut Output,
) -> Executor<'a> {
Self {
configuration,
command_manager,
formatter_manager,
history,
connection,
output,
}
}
fn parse_commands(&self, contents: &str) -> Result<Vec<String>> {
let command_identifier = regex::escape(&self.configuration.command_identifier);
let pattern = format!(r"(?ms)^\s*({command_identifier}.*?|.*?;|.*)\s*$");
let regex = Regex::new(pattern.as_str())?;
let commands: Vec<String> = regex
.find_iter(contents)
.map(|mat| mat.as_str().trim().to_string())
.collect();
Ok(commands)
}
pub async fn execute(&mut self, input: &str) -> Result<LoopCondition> {
let input = input.trim();
let commands = self.parse_commands(input)?;
for command in commands {
if let LoopCondition::Exit(exit_code) = self.execute_command(command.as_str()).await? {
return Ok(LoopCondition::Exit(exit_code));
}
}
Ok(LoopCondition::Continue)
}
async fn execute_command(&mut self, input: &str) -> Result<LoopCondition> {
let input = input.trim();
if input.is_empty() {
return Ok(LoopCondition::Continue);
}
let options = FormatterOptions {
color: self.configuration.color,
..Default::default()
};
let helper = Highlighter::new(&options, "sql");
let locale = self.configuration.locale.as_str();
let echo = t!("echo_command", locale = locale);
let off = t!("off", locale = locale);
let echo_off = format!("{}{} {}", self.configuration.command_identifier, echo, off);
if input != echo_off {
if self.configuration.echo == EchoMode::On {
let input = helper.highlight(input)?;
writeln!(&mut self.output, "{input}")?;
} else if self.configuration.echo == EchoMode::Prompt {
let prompt = t!(
"prompt",
locale = locale,
program_name = self.configuration.program_name,
);
let input = helper.highlight(input)?;
writeln!(&mut self.output, "{prompt}{input}")?;
}
}
let command_identifier = &self.configuration.command_identifier;
let loop_condition = if input.starts_with(command_identifier) {
let mut executor = CommandExecutor::new(
self.configuration,
self.command_manager,
self.formatter_manager,
self.history,
self.connection,
self.output,
);
executor.execute(input).await?
} else {
let mut executor = SqlExecutor::new(
self.configuration,
self.formatter_manager,
self.connection,
self.output,
);
executor.execute(input).await?
};
Ok(loop_condition)
}
}
impl Debug for Executor<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Executor")
.field("configuration", &self.configuration)
.field("command_manager", &self.command_manager)
.field("formatter_manager", &self.formatter_manager)
.field("connection", &self.connection)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
use rsql_drivers::MockConnection;
#[tokio::test]
async fn test_debug() {
let mut configuration = Configuration::default();
let command_manager = CommandManager::default();
let formatter_manager = FormatterManager::default();
let history = DefaultHistory::new();
let mut connection = MockConnection::new();
let output = &mut Output::default();
let executor = Executor::new(
&mut configuration,
&command_manager,
&formatter_manager,
&history,
&mut connection,
output,
);
let debug = format!("{executor:?}");
assert!(debug.contains("Executor"));
assert!(debug.contains("configuration"));
assert!(debug.contains("command_manager"));
assert!(debug.contains("formatter_manager"));
assert!(debug.contains("connection"));
}
#[tokio::test]
async fn test_parse_commands_default_command_identifier() -> anyhow::Result<()> {
let mut configuration = Configuration::default();
let command_manager = CommandManager::default();
let formatter_manager = FormatterManager::default();
let history = DefaultHistory::new();
let mut connection = MockConnection::new();
let mut output = Output::default();
let executor = Executor::new(
&mut configuration,
&command_manager,
&formatter_manager,
&history,
&mut connection,
&mut output,
);
let contents = indoc! {r"
.bail on
SELECT *
FROM table;
.timer on
INSERT INTO table ...;
.exit 1
SELECT 1"};
let commands = executor.parse_commands(contents)?;
assert_eq!(commands.len(), 6);
assert_eq!(commands[0], ".bail on");
assert_eq!(commands[1], "SELECT *\nFROM table;");
assert_eq!(commands[2], ".timer on");
assert_eq!(commands[3], "INSERT INTO table ...;");
assert_eq!(commands[4], ".exit 1");
assert_eq!(commands[5], "SELECT 1");
Ok(())
}
#[tokio::test]
async fn test_parse_commands_backslash_command_identifier() -> anyhow::Result<()> {
let mut configuration = Configuration {
command_identifier: "\\".to_string(),
..Default::default()
};
let command_manager = CommandManager::default();
let formatter_manager = FormatterManager::default();
let history = DefaultHistory::new();
let mut connection = MockConnection::new();
let mut output = Output::default();
let executor = Executor::new(
&mut configuration,
&command_manager,
&formatter_manager,
&history,
&mut connection,
&mut output,
);
let contents = indoc! {r"
\bail on
SELECT *
FROM table;
\timer on
INSERT INTO table ...;
\exit 1
"};
let commands = executor.parse_commands(contents)?;
assert_eq!(commands.len(), 5);
assert_eq!(commands[0], "\\bail on");
assert_eq!(commands[1], "SELECT *\nFROM table;");
assert_eq!(commands[2], "\\timer on");
assert_eq!(commands[3], "INSERT INTO table ...;");
assert_eq!(commands[4], "\\exit 1");
Ok(())
}
#[tokio::test]
async fn test_execute() -> anyhow::Result<()> {
let mut configuration = Configuration {
bail_on_error: false,
results_timer: false,
..Default::default()
};
let command_manager = CommandManager::default();
let formatter_manager = FormatterManager::default();
let history = DefaultHistory::new();
let mut connection = MockConnection::new();
let mut output = Output::default();
let mut executor = Executor::new(
&mut configuration,
&command_manager,
&formatter_manager,
&history,
&mut connection,
&mut output,
);
let input = indoc! {r"
.bail on
.timer on
"};
let _ = executor.execute(input).await?;
assert!(configuration.bail_on_error);
assert!(configuration.results_timer);
Ok(())
}
#[tokio::test]
async fn test_execute_empty_input() -> anyhow::Result<()> {
let mut configuration = Configuration::default();
let command_manager = CommandManager::default();
let formatter_manager = FormatterManager::default();
let history = DefaultHistory::new();
let mut connection = MockConnection::new();
let mut output = Output::default();
let mut executor = Executor::new(
&mut configuration,
&command_manager,
&formatter_manager,
&history,
&mut connection,
&mut output,
);
let result = executor.execute(" ").await?;
assert_eq!(result, LoopCondition::Continue);
Ok(())
}
#[tokio::test]
async fn test_execute_loop_exit() -> anyhow::Result<()> {
let mut configuration = Configuration::default();
let command_manager = CommandManager::default();
let formatter_manager = FormatterManager::default();
let history = DefaultHistory::new();
let mut connection = MockConnection::new();
connection.expect_close().returning(|| Ok(()));
let mut output = Output::default();
let mut executor = Executor::new(
&mut configuration,
&command_manager,
&formatter_manager,
&history,
&mut connection,
&mut output,
);
let result = executor.execute(".exit 42").await?;
assert_eq!(result, LoopCondition::Exit(42));
Ok(())
}
async fn test_execute_command(command_identifier: &str, echo: EchoMode) -> anyhow::Result<()> {
let mut configuration = Configuration {
bail_on_error: false,
color: false,
command_identifier: command_identifier.to_string(),
echo: echo.clone(),
..Default::default()
};
let command_manager = CommandManager::default();
let formatter_manager = FormatterManager::default();
let history = DefaultHistory::new();
let mut connection = MockConnection::new();
let mut output = Output::default();
let mut executor = Executor::new(
&mut configuration,
&command_manager,
&formatter_manager,
&history,
&mut connection,
&mut output,
);
let input = format!("{command_identifier}bail on");
let result = executor.execute_command(input.as_str()).await?;
assert_eq!(result, LoopCondition::Continue);
let execute_output = output.to_string();
match echo {
EchoMode::On => assert!(execute_output.contains(input.as_str())),
EchoMode::Prompt => {
let locale = configuration.locale.as_str();
let prompt = t!(
"prompt",
locale = locale,
program_name = configuration.program_name,
);
assert!(execute_output.contains(&*prompt));
assert!(execute_output.contains(input.as_str()));
}
EchoMode::Off => assert!(!execute_output.contains(input.as_str())),
}
assert!(configuration.bail_on_error);
Ok(())
}
#[tokio::test]
async fn test_execute_command_echo_on() -> anyhow::Result<()> {
test_execute_command("!", EchoMode::On).await
}
#[tokio::test]
async fn test_execute_command_echo_prompt() -> anyhow::Result<()> {
test_execute_command("!", EchoMode::Prompt).await
}
#[tokio::test]
async fn test_execute_command_echo_off() -> anyhow::Result<()> {
test_execute_command("\\", EchoMode::Off).await
}
async fn test_execute_command_sql(echo: EchoMode) -> anyhow::Result<()> {
let mut configuration = Configuration {
color: false,
echo: echo.clone(),
..Default::default()
};
let command_manager = CommandManager::default();
let formatter_manager = FormatterManager::default();
let history = DefaultHistory::new();
let mut connection = MockConnection::new();
let input = "INSERT INTO foo";
connection
.expect_execute()
.with(input, vec![])
.returning(|_, _| Ok(42));
connection
.expect_parse_sql()
.with(input)
.returning(|_| rsql_drivers::StatementMetadata::DML);
let mut output = Output::default();
let mut executor = Executor::new(
&mut configuration,
&command_manager,
&formatter_manager,
&history,
&mut connection,
&mut output,
);
let result = executor.execute_command(input).await?;
assert_eq!(result, LoopCondition::Continue);
let execute_output = output.to_string();
match echo {
EchoMode::On => assert!(execute_output.contains(input)),
EchoMode::Prompt => {
let locale = configuration.locale.as_str();
let prompt = t!(
"prompt",
locale = locale,
program_name = configuration.program_name,
);
assert!(execute_output.contains(&*prompt));
assert!(execute_output.contains(input));
}
EchoMode::Off => assert!(!execute_output.contains(input)),
}
assert!(execute_output.contains("42"));
Ok(())
}
#[tokio::test]
async fn test_execute_sql_echo_on() -> anyhow::Result<()> {
test_execute_command_sql(EchoMode::On).await
}
#[tokio::test]
async fn test_execute_sql_echo_prompt() -> anyhow::Result<()> {
test_execute_command_sql(EchoMode::Prompt).await
}
#[tokio::test]
async fn test_execute_sql_echo_off() -> anyhow::Result<()> {
test_execute_command_sql(EchoMode::Off).await
}
}