use std::path::PathBuf;
use crate::{
parser::{DebugWhat, parse_multi_input},
repl::ReplState,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ReplAction {
Execute {
input: String,
sql: String,
modifiers: crate::parser::QueryModifiers,
},
Exit,
ToggleExpanded,
ToggleWriteMode,
ToggleRedaction,
Edit,
Copy,
IncludeFile {
file_path: PathBuf,
vars: Vec<(String, String)>,
},
RunSnippet {
name: String,
vars: Vec<(String, String)>,
},
SetOutputFile {
file_path: PathBuf,
},
UnsetOutputFile,
Debug {
what: DebugWhat,
},
Help,
SetVar {
name: String,
value: String,
},
DefaultVar {
name: String,
value: String,
},
UnsetVar {
name: String,
},
LookupVar {
pattern: Option<String>,
},
GetVar {
name: String,
},
SnippetSave {
name: String,
},
SnippetEdit {
name: String,
},
SnippetList,
SnippetRefetch,
List {
item: crate::parser::ListItem,
pattern: String,
detail: bool,
sameconn: bool,
},
Describe {
item: String,
detail: bool,
sameconn: bool,
},
Result {
subcommand: crate::parser::ResultSubcommand,
},
}
pub(crate) fn handle_input(
buffer: &str,
new_line: &str,
state: &ReplState,
) -> (String, Vec<ReplAction>) {
let mut new_buffer = buffer.to_string();
if !new_buffer.is_empty() {
new_buffer.push('\n');
}
new_buffer.push_str(new_line);
let user_input = new_buffer.trim().to_string();
if buffer.is_empty() && user_input.eq_ignore_ascii_case("quit") {
return (String::new(), vec![ReplAction::Exit]);
}
let (actions, remaining) = parse_multi_input(&user_input, state);
if actions.is_empty() {
if remaining.is_empty() {
(String::new(), vec![])
} else {
(new_buffer, vec![])
}
} else {
(remaining, actions)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handle_input_empty_line() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 0);
}
#[test]
fn test_handle_input_incomplete_query() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "SELECT * FROM users", &state);
assert_eq!(buffer, "SELECT * FROM users");
assert_eq!(actions.len(), 0);
}
#[test]
fn test_handle_input_complete_query_semicolon() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "SELECT * FROM users;", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
match &actions[0] {
ReplAction::Execute {
input,
sql,
modifiers,
} => {
assert_eq!(input, "SELECT * FROM users");
assert_eq!(sql, "SELECT * FROM users");
assert!(modifiers.is_empty());
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_handle_input_complete_query_backslash_g() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "SELECT * FROM users\\g", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
match &actions[0] {
ReplAction::Execute {
input,
sql,
modifiers,
} => {
assert_eq!(input, "SELECT * FROM users");
assert_eq!(sql, "SELECT * FROM users");
assert!(modifiers.is_empty());
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_handle_input_multiline_query() {
let state = ReplState::new();
let (buffer1, actions1) = handle_input("", "SELECT *", &state);
assert_eq!(buffer1, "SELECT *");
assert_eq!(actions1.len(), 0);
let (buffer2, actions2) = handle_input(&buffer1, "FROM users;", &state);
assert_eq!(buffer2, "");
assert_eq!(actions2.len(), 1);
match &actions2[0] {
ReplAction::Execute { input, sql, .. } => {
assert_eq!(input, "SELECT *\nFROM users");
assert_eq!(sql, "SELECT *\nFROM users");
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_handle_input_quit_command() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "\\q", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
assert!(matches!(actions[0], ReplAction::Exit));
}
#[test]
fn test_handle_input_quit_command_case_insensitive() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "QUIT", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
assert!(matches!(actions[0], ReplAction::Exit));
}
#[test]
fn test_handle_input_quit_after_incomplete() {
let state = ReplState::new();
let (buffer1, actions1) = handle_input("", "SELECT *", &state);
assert_eq!(buffer1, "SELECT *");
assert_eq!(actions1.len(), 0);
let (buffer2, actions2) = handle_input(&buffer1, "\\q", &state);
assert_eq!(buffer2, "");
assert_eq!(actions2.len(), 1);
assert!(matches!(actions2[0], ReplAction::Exit));
}
#[test]
fn test_handle_input_expanded_metacommand() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "\\x", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
assert!(matches!(actions[0], ReplAction::ToggleExpanded));
}
#[test]
fn test_handle_input_toggle_redaction_metacommand() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "\\R", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
assert!(matches!(actions[0], ReplAction::ToggleRedaction));
}
#[test]
fn test_ctrl_c_clears_buffer() {
let state = ReplState::new();
let (buffer, _) = handle_input("", "SELECT *", &state);
assert_eq!(buffer, "SELECT *");
let cleared_buffer = "";
assert_eq!(cleared_buffer, "");
let (new_buffer, actions) = handle_input(cleared_buffer, "SELECT 1;", &state);
assert_eq!(new_buffer, "");
assert_eq!(actions.len(), 1);
match &actions[0] {
ReplAction::Execute { input, sql, .. } => {
assert_eq!(input, "SELECT 1");
assert_eq!(sql, "SELECT 1");
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_ctrl_c_on_empty_buffer() {
let _buffer = "";
let cleared_buffer = "";
assert_eq!(cleared_buffer, "");
}
#[test]
fn test_ctrl_d_exits() {
}
#[test]
fn test_handle_input_preserves_modifiers() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "select 1+1 \\gx", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
match &actions[0] {
ReplAction::Execute {
input,
sql,
modifiers,
} => {
assert_eq!(input, "select 1+1");
assert_eq!(sql, "select 1+1");
assert!(modifiers.contains(&crate::parser::QueryModifier::Expanded));
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_expanded_mode_applied_to_query() {
let state = ReplState {
expanded_mode: true,
write_mode: false,
ots: None,
..ReplState::new()
};
let (buffer, actions) = handle_input("", "SELECT 1;", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
match &actions[0] {
ReplAction::Execute { modifiers, .. } => {
assert!(modifiers.contains(&crate::parser::QueryModifier::Expanded));
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_expanded_mode_not_applied_when_off() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "SELECT 1;", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
match &actions[0] {
ReplAction::Execute { modifiers, .. } => {
assert!(!modifiers.contains(&crate::parser::QueryModifier::Expanded));
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_expanded_mode_with_explicit_gx() {
let state = ReplState {
expanded_mode: true,
write_mode: false,
ots: None,
..ReplState::new()
};
let (buffer, actions) = handle_input("", "SELECT 1\\gx", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
match &actions[0] {
ReplAction::Execute { modifiers, .. } => {
assert!(modifiers.contains(&crate::parser::QueryModifier::Expanded));
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_comment_only_input() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "-- foo", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 0);
}
#[test]
fn test_metacommand_with_comment() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "\\vars -- foo", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
match &actions[0] {
ReplAction::LookupVar { pattern } => {
assert_eq!(pattern, &None);
}
_ => panic!("Expected LookupVar action"),
}
}
#[test]
fn test_query_with_inline_comment() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "SELECT 1 + 1; -- foo", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
match &actions[0] {
ReplAction::Execute { sql, .. } => {
assert_eq!(sql, "SELECT 1 + 1");
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_copy_metacommand() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "\\copy", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
assert!(matches!(actions[0], ReplAction::Copy));
}
#[test]
fn test_copy_metacommand_with_args() {
let state = ReplState::new();
let (buffer, actions) = handle_input("", "\\copy (select from blah) with headers", &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 1);
assert!(matches!(actions[0], ReplAction::Copy));
}
#[test]
fn test_multiple_statements() {
let state = ReplState::new();
let input = "select 1 + 2 \\gx\nselect 2 + 3;\n\\re list";
let (buffer, actions) = handle_input("", input, &state);
assert_eq!(buffer, "");
assert_eq!(actions.len(), 3);
match &actions[0] {
ReplAction::Execute { sql, modifiers, .. } => {
assert_eq!(sql, "select 1 + 2");
assert!(modifiers.contains(&crate::parser::QueryModifier::Expanded));
}
_ => panic!("Expected Execute for first action"),
}
match &actions[1] {
ReplAction::Execute { sql, modifiers, .. } => {
assert_eq!(sql, "select 2 + 3");
assert!(!modifiers.contains(&crate::parser::QueryModifier::Expanded));
}
_ => panic!("Expected Execute for second action"),
}
assert!(matches!(actions[2], ReplAction::Result { .. }));
}
#[test]
fn test_multiline_query_with_comments() {
let state = ReplState::new();
let (buffer1, actions1) = handle_input("", "select 1 + -- adding", &state);
assert_eq!(buffer1, "select 1 + -- adding");
assert_eq!(actions1.len(), 0);
let (buffer2, actions2) = handle_input(&buffer1, "1; -- result is 2", &state);
assert_eq!(buffer2, "");
assert_eq!(actions2.len(), 1);
match &actions2[0] {
ReplAction::Execute { sql, .. } => {
assert!(sql.contains("select 1 +"));
assert!(sql.contains("1"));
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_comment_between_incomplete_query_lines() {
let state = ReplState::new();
let (buffer1, actions1) = handle_input("", "select", &state);
assert_eq!(buffer1, "select");
assert_eq!(actions1.len(), 0);
let (buffer2, actions2) = handle_input(&buffer1, "-- this is a comment", &state);
assert_eq!(buffer2, "select\n-- this is a comment");
assert_eq!(actions2.len(), 0);
let (buffer3, actions3) = handle_input(&buffer2, "1;", &state);
assert_eq!(buffer3, "");
assert_eq!(actions3.len(), 1);
match &actions3[0] {
ReplAction::Execute { sql, .. } => {
assert!(sql.contains("select"));
assert!(sql.contains("-- this is a comment"));
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_auto_execute_by_appending_semicolon() {
let state = ReplState::new();
let incomplete_query = "SELECT 1 + 1";
let (remaining, actions) = handle_input("", incomplete_query, &state);
assert_eq!(actions.len(), 0);
assert_eq!(remaining, incomplete_query);
let completed = format!("{};", remaining);
let (remaining2, actions2) = handle_input("", &completed, &state);
assert_eq!(remaining2, "");
assert_eq!(actions2.len(), 1);
match &actions2[0] {
ReplAction::Execute { sql, .. } => {
assert_eq!(sql, "SELECT 1 + 1");
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_auto_execute_with_comments() {
let state = ReplState::new();
let incomplete_query = "-- Comment\nSELECT 1 + 1";
let (remaining, actions) = handle_input("", incomplete_query, &state);
assert_eq!(actions.len(), 0);
assert_eq!(remaining, incomplete_query);
let completed = format!("{};", remaining);
let (remaining2, actions2) = handle_input("", &completed, &state);
assert_eq!(remaining2, "");
assert_eq!(actions2.len(), 1);
match &actions2[0] {
ReplAction::Execute { sql, .. } => {
assert!(sql.contains("SELECT 1 + 1"));
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_auto_execute_mixed_complete_and_incomplete() {
let state = ReplState::new();
let mixed_query = "SELECT 1 + 1;\nSELECT 2 + 3";
let (remaining, actions) = handle_input("", mixed_query, &state);
assert_eq!(actions.len(), 1);
assert_eq!(remaining.trim(), "SELECT 2 + 3");
let completed = format!("{};", remaining);
let (remaining2, actions2) = handle_input("", &completed, &state);
assert_eq!(remaining2, "");
assert_eq!(actions2.len(), 1);
match &actions2[0] {
ReplAction::Execute { sql, .. } => {
assert_eq!(sql, "SELECT 2 + 3");
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_auto_execute_multiple_complete_then_incomplete() {
let state = ReplState::new();
let query = "SELECT 1;\nSELECT 2;\nSELECT 3";
let (remaining, actions) = handle_input("", query, &state);
assert_eq!(actions.len(), 2);
assert_eq!(remaining.trim(), "SELECT 3");
let completed = format!("{};", remaining);
let (remaining2, actions2) = handle_input("", &completed, &state);
assert_eq!(remaining2, "");
assert_eq!(actions2.len(), 1);
}
#[test]
fn test_already_complete_query_not_affected() {
let state = ReplState::new();
let complete_query = "SELECT 1 + 1;";
let (remaining, actions) = handle_input("", complete_query, &state);
assert_eq!(remaining, "");
assert_eq!(actions.len(), 1);
match &actions[0] {
ReplAction::Execute { sql, .. } => {
assert_eq!(sql, "SELECT 1 + 1");
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_auto_execute_with_backslash_g() {
let state = ReplState::new();
let complete_query = "SELECT 1 + 1 \\g";
let (remaining, actions) = handle_input("", complete_query, &state);
assert_eq!(remaining, "");
assert_eq!(actions.len(), 1);
match &actions[0] {
ReplAction::Execute { sql, .. } => {
assert_eq!(sql, "SELECT 1 + 1");
}
_ => panic!("Expected Execute action"),
}
}
}