use std::collections::VecDeque;
use rustyline::{DefaultEditor, error::ReadlineError};
const SHELL_PROMPT: &str = "icydb> ";
const SHELL_CONTINUATION_PROMPT: &str = " -> ";
pub(super) enum ShellInput {
Sql(String),
Help,
Exit,
}
enum ShellTopLevelInput {
Blank,
Help,
Exit,
Sql,
}
pub(super) fn is_shell_help_command(input: &str) -> bool {
matches!(
input.trim().trim_end_matches(';').trim(),
"?" | "help" | "\\?" | "\\help"
)
}
pub(super) const fn shell_help_text() -> &'static str {
"meta commands:
? / help show this help
\\q / quit / exit quit the interactive shell
perf footer legend:
c = compile parse, lower, and compile the SQL surface
p = planner resolve visible indexes and build the structural access plan
s = store physical data/index-store traversal and physical payload decode
e = executor residual filter, order, group, aggregate, and projection logic
d = decode package the public SQL result payload for the shell
{pc=.../...} pure covering decode / pure covering row assembly
{er=...} remaining executor work outside the explicit pure covering subpath
{r=...} local shell render time for table/footer formatting
examples:
SELECT name FROM character;
EXPLAIN EXECUTION SELECT name FROM character;
CREATE INDEX character_level_idx ON character (level);
SHOW INDEXES FROM character;
DESCRIBE character;
DROP INDEX character_level_idx ON character;"
}
pub(super) fn read_statement(
editor: &mut DefaultEditor,
pending_sql: &mut VecDeque<String>,
partial_statement: &mut String,
) -> Result<ShellInput, String> {
if let Some(sql) = pending_sql.pop_front() {
return Ok(ShellInput::Sql(sql));
}
let mut prompt = shell_prompt(partial_statement);
loop {
match editor.readline(prompt) {
Ok(line) => {
let normalized_line = normalize_shell_statement_line(line.as_str());
if partial_statement_is_empty(partial_statement) {
match top_level_shell_input(normalized_line.as_str()) {
ShellTopLevelInput::Blank => {
prompt = SHELL_PROMPT;
continue;
}
ShellTopLevelInput::Exit => return Ok(ShellInput::Exit),
ShellTopLevelInput::Help => return Ok(ShellInput::Help),
ShellTopLevelInput::Sql => {}
}
}
append_shell_statement_line(partial_statement, normalized_line.as_str());
pending_sql.extend(drain_complete_shell_statements(partial_statement));
if let Some(sql) = pending_sql.pop_front() {
return Ok(ShellInput::Sql(sql));
}
prompt = SHELL_CONTINUATION_PROMPT;
}
Err(ReadlineError::Interrupted) => {
clear_shell_input_state(pending_sql, partial_statement);
prompt = shell_prompt(partial_statement);
}
Err(ReadlineError::Eof) => return Ok(shell_input_at_eof(partial_statement)),
Err(err) => return Err(err.to_string()),
}
}
}
fn partial_statement_is_empty(partial_statement: &str) -> bool {
partial_statement.trim().is_empty()
}
fn shell_prompt(partial_statement: &str) -> &'static str {
if partial_statement_is_empty(partial_statement) {
SHELL_PROMPT
} else {
SHELL_CONTINUATION_PROMPT
}
}
fn append_shell_statement_line(partial_statement: &mut String, line: &str) {
if !partial_statement.is_empty() {
partial_statement.push('\n');
}
partial_statement.push_str(line);
}
fn clear_shell_input_state(pending_sql: &mut VecDeque<String>, partial_statement: &mut String) {
partial_statement.clear();
pending_sql.clear();
}
fn shell_input_at_eof(partial_statement: &mut String) -> ShellInput {
if partial_statement_is_empty(partial_statement) {
println!();
return ShellInput::Exit;
}
let sql = partial_statement.trim().to_string();
partial_statement.clear();
ShellInput::Sql(sql)
}
fn top_level_shell_input(line: &str) -> ShellTopLevelInput {
if line.is_empty() {
return ShellTopLevelInput::Blank;
}
if matches!(line, "\\q" | "quit" | "exit") {
return ShellTopLevelInput::Exit;
}
if is_shell_help_command(line) {
return ShellTopLevelInput::Help;
}
ShellTopLevelInput::Sql
}
pub(super) fn drain_complete_shell_statements(statement: &mut String) -> VecDeque<String> {
let mut complete = VecDeque::<String>::new();
let mut start = 0usize;
let mut in_single_quote = false;
let chars = statement.char_indices().collect::<Vec<_>>();
let mut index = 0usize;
while index < chars.len() {
let (offset, ch) = chars[index];
if in_single_quote && ch == '\\' {
index += 2;
continue;
}
if ch == '\'' {
let next_is_quote = chars.get(index + 1).is_some_and(|(_, next)| *next == '\'');
if in_single_quote && next_is_quote {
index += 2;
continue;
}
in_single_quote = !in_single_quote;
index += 1;
continue;
}
if ch == ';' && !in_single_quote {
let end = offset + ch.len_utf8();
let candidate = statement[start..end].trim();
if !candidate.is_empty() {
complete.push_back(candidate.to_string());
}
start = end;
}
index += 1;
}
let remainder = statement[start..].trim().to_string();
statement.clear();
statement.push_str(remainder.as_str());
complete
}
pub(super) fn normalize_shell_statement_line(line: &str) -> String {
let trimmed = line.trim();
let without_extra_semicolons = trimmed.trim_end_matches(';');
if without_extra_semicolons.len() == trimmed.len() {
return trimmed.to_string();
}
format!("{without_extra_semicolons};")
}