use std::env;
use std::path::PathBuf;
use datafusion_ducklake_provider::DuckLakeSessionContext;
use rustyline::DefaultEditor;
use rustyline::error::ReadlineError;
use crate::args::Cli;
use crate::error::{CliError, err};
use crate::runner::{RunOptions, run_statement};
use crate::split::split_complete_statements;
pub(crate) async fn run_repl(
ctx: &DuckLakeSessionContext,
cli: &Cli,
mut options: RunOptions,
) -> Result<(), CliError> {
let mut editor = DefaultEditor::new()?;
let history_file = history_file(cli);
if let Some(path) = &history_file {
let _ = editor.load_history(path);
}
let mut buffer = String::new();
loop {
let prompt = if buffer.trim().is_empty() {
"ducklake> "
} else {
"...> "
};
let line = match editor.readline(prompt) {
Ok(line) => line,
Err(ReadlineError::Interrupted) => {
buffer.clear();
eprintln!("^C");
continue;
}
Err(ReadlineError::Eof) => break,
Err(err) => return Err(err.into()),
};
let trimmed = line.trim();
if buffer.trim().is_empty() && trimmed.starts_with('.') {
match handle_dot_command(trimmed, &mut options) {
Ok(true) => break,
Ok(false) => {}
Err(err) => eprintln!("{err}"),
}
continue;
}
if trimmed.is_empty() && buffer.trim().is_empty() {
continue;
}
let _ = editor.add_history_entry(line.as_str());
buffer.push_str(&line);
buffer.push('\n');
let split = split_complete_statements(&buffer);
for statement in split.statements {
if let Err(err) = run_statement(ctx, &statement, options).await {
eprintln!("{err}");
}
}
buffer = split.trailing;
}
if let Some(path) = &history_file {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = editor.save_history(path);
}
Ok(())
}
fn handle_dot_command(command: &str, options: &mut RunOptions) -> Result<bool, CliError> {
match command {
".exit" | ".quit" => Ok(true),
".help" => {
print_help();
Ok(false)
}
".timing on" => {
options.timing = true;
eprintln!("Timing is on.");
Ok(false)
}
".timing off" => {
options.timing = false;
eprintln!("Timing is off.");
Ok(false)
}
".clear" => Ok(false),
other if other.starts_with(".timing") => Err(err("usage: .timing on|off")),
other => Err(err(format!(
"unknown command {other:?}; use .help for available commands"
))),
}
}
fn print_help() {
println!(
"\
DuckLake CLI commands:
.help Show this help.
.timing on Print elapsed time after each statement.
.timing off Disable elapsed time output.
.quit, .exit Exit the shell.
SQL statements are executed through DuckLakeSessionContext::sql.
Use ATTACH to connect a DuckLake catalog, for example:
ATTACH 'ducklake:sqlite:metadata.sqlite' AS lake (DATA_PATH 'data/');
"
);
}
fn history_file(cli: &Cli) -> Option<PathBuf> {
if cli.no_history {
return None;
}
cli.history_file
.clone()
.or_else(|| env::var_os("DUCKLAKE_CLI_HISTORY").map(PathBuf::from))
.or_else(|| {
env::var_os("HOME").map(|home| PathBuf::from(home).join(".ducklake-cli-history"))
})
}
#[cfg(test)]
mod tests {
use crate::runner::RunOptions;
use super::handle_dot_command;
#[test]
fn timing_command_toggles_option() {
let mut options = RunOptions {
quiet: false,
timing: false,
continue_on_error: false,
};
assert!(!handle_dot_command(".timing on", &mut options).unwrap());
assert!(options.timing);
assert!(!handle_dot_command(".timing off", &mut options).unwrap());
assert!(!options.timing);
}
#[test]
fn quit_commands_exit_repl() {
let mut options = RunOptions {
quiet: false,
timing: false,
continue_on_error: false,
};
assert!(handle_dot_command(".quit", &mut options).unwrap());
assert!(handle_dot_command(".exit", &mut options).unwrap());
}
}