mod commands;
mod formatter;
mod helper;
mod repl;
#[cfg(not(target_arch = "wasm32"))]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use std::io::IsTerminal;
use std::path::PathBuf;
use std::process;
use clap::Parser;
use crate::formatter::OutputMode;
#[derive(Parser)]
#[command(
name = "citadel",
about = "Interactive SQL shell for Citadel encrypted database"
)]
#[command(version)]
struct Cli {
database: Option<PathBuf>,
sql: Option<String>,
#[arg(long)]
create: bool,
#[arg(long)]
passphrase: Option<String>,
#[arg(long, default_value = "box")]
mode: String,
#[arg(long, default_value = "on")]
header: String,
#[arg(long, default_value = "NULL")]
nullvalue: String,
#[arg(long)]
no_color: bool,
#[arg(long)]
init: Option<PathBuf>,
#[arg(long)]
cmd: Option<String>,
}
fn main() {
let cli = Cli::parse();
let db_path = match &cli.database {
Some(p) => p.clone(),
None => {
eprintln!("Error: database path is required");
eprintln!("Usage: citadel [OPTIONS] <DATABASE> [SQL]");
process::exit(1);
}
};
let passphrase = match &cli.passphrase {
Some(p) => p.clone(),
None => {
if !std::io::stdin().is_terminal() {
eprintln!("Error: passphrase required (use --passphrase in non-interactive mode)");
process::exit(1);
}
match rpassword::prompt_password("Enter passphrase: ") {
Ok(p) => p,
Err(e) => {
eprintln!("Error reading passphrase: {e}");
process::exit(1);
}
}
}
};
let db = if cli.create {
match citadel::DatabaseBuilder::new(&db_path)
.passphrase(passphrase.as_bytes())
.create()
{
Ok(db) => db,
Err(e) => {
eprintln!("Error creating database: {e}");
process::exit(1);
}
}
} else {
match citadel::DatabaseBuilder::new(&db_path)
.passphrase(passphrase.as_bytes())
.open()
{
Ok(db) => db,
Err(e) => {
eprintln!("Error opening database: {e}");
process::exit(1);
}
}
};
let output_mode = match cli.mode.as_str() {
"box" => OutputMode::Box,
"table" => OutputMode::Table,
"csv" => OutputMode::Csv,
"json" => OutputMode::Json,
"line" => OutputMode::Line,
other => {
eprintln!("Error: unknown output mode '{other}'. Use: box, table, csv, json, line");
process::exit(1);
}
};
let is_interactive = cli.sql.is_none() && std::io::stdin().is_terminal();
let use_color = is_interactive && !cli.no_color;
let mut settings = repl::Settings {
mode: output_mode,
show_headers: cli.header != "off",
null_display: cli.nullvalue.clone(),
timer: false,
show_changes: false,
use_color,
column_widths: Vec::new(),
output_file: None,
};
if let Some(ref sql) = cli.sql {
run_batch(&db, sql, &mut settings);
return;
}
if !is_interactive {
run_piped(&db, &mut settings);
return;
}
repl::run_interactive(db, db_path, passphrase, settings, cli.init, cli.cmd);
}
fn run_batch(db: &citadel::Database, sql: &str, settings: &mut repl::Settings) {
use std::time::Instant;
let conn = match citadel_sql::Connection::open(db) {
Ok(c) => c,
Err(e) => {
eprintln!("Error: {e}");
process::exit(1);
}
};
let start = Instant::now();
match conn.execute(sql) {
Ok(result) => {
let output = formatter::format_result(&result, settings);
if !output.is_empty() {
settings.write_output(&output);
}
if settings.timer {
settings.write_output(&format!("Run Time: {:.3}s", start.elapsed().as_secs_f64()));
}
}
Err(e) => {
eprintln!("Error: {e}");
process::exit(1);
}
}
}
fn run_piped(db: &citadel::Database, settings: &mut repl::Settings) {
use std::io::{self, BufRead};
let conn = match citadel_sql::Connection::open(db) {
Ok(c) => c,
Err(e) => {
eprintln!("Error: {e}");
process::exit(1);
}
};
let mut buf = String::new();
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = match line {
Ok(l) => l,
Err(e) => {
eprintln!("Error reading stdin: {e}");
process::exit(1);
}
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with('.') {
commands::execute_dot_command_mut(trimmed, db, &conn, settings, &mut io::stdout());
continue;
}
buf.push_str(&line);
buf.push(' ');
if has_complete_statement(&buf) {
let sql = buf.trim();
if !sql.is_empty() {
execute_and_display(&conn, sql, &mut *settings);
}
buf.clear();
}
}
if !buf.trim().is_empty() {
execute_and_display(&conn, buf.trim(), settings);
}
}
fn execute_and_display(
conn: &citadel_sql::Connection<'_>,
sql: &str,
settings: &mut repl::Settings,
) {
use std::time::Instant;
let start = Instant::now();
match conn.execute(sql) {
Ok(result) => {
let output = formatter::format_result(&result, settings);
if !output.is_empty() {
settings.write_output(&output);
}
if settings.timer {
settings.write_output(&format!("Run Time: {:.3}s", start.elapsed().as_secs_f64()));
}
}
Err(e) => {
eprintln!("Error: {e}");
}
}
}
fn has_complete_statement(s: &str) -> bool {
let trimmed = s.trim();
if trimmed.is_empty() {
return false;
}
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut last_char = '\0';
for ch in trimmed.chars() {
match ch {
'\'' if !in_double_quote && last_char != '\\' => in_single_quote = !in_single_quote,
'"' if !in_single_quote && last_char != '\\' => in_double_quote = !in_double_quote,
_ => {}
}
last_char = ch;
}
!in_single_quote && !in_double_quote && trimmed.ends_with(';')
}