use graphitesql::{Connection, QueryResult, Value};
use std::io::{self, BufRead, IsTerminal, Write};
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let (path, script) = match args.split_first() {
None => (String::from(":memory:"), None),
Some((db, rest)) => {
let script = if rest.is_empty() {
None
} else {
Some(rest.join(" "))
};
(db.clone(), script)
}
};
let mut conn = match open(&path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error: unable to open {path:?}: {e}");
std::process::exit(1);
}
};
let mut shell = Shell { headers: false };
if let Some(sql) = script {
if let Err(e) = shell.run_sql_batch(&mut conn, &sql) {
eprintln!("Error: {e}");
std::process::exit(1);
}
return;
}
shell.repl(&mut conn, &path);
}
fn open(path: &str) -> graphitesql::Result<Connection> {
if path.is_empty() || path == ":memory:" {
Connection::open_memory()
} else if std::path::Path::new(path).exists() {
Connection::open(path)
} else {
Connection::create(path)
}
}
struct Shell {
headers: bool,
}
impl Shell {
fn repl(&mut self, conn: &mut Connection, path: &str) {
let interactive = io::stdin().is_terminal();
if interactive {
eprintln!("graphitesql shell — connected to {path}");
eprintln!(
"Enter SQL statements ending in ';'. \".help\" for commands, \".quit\" to exit."
);
}
let stdin = io::stdin();
let mut buffer = String::new();
loop {
if interactive {
let prompt = if buffer.is_empty() {
"graphitesql> "
} else {
" ...> "
};
print!("{prompt}");
let _ = io::stdout().flush();
}
let mut line = String::new();
match stdin.lock().read_line(&mut line) {
Ok(0) => break, Ok(_) => {}
Err(e) => {
eprintln!("Error reading input: {e}");
break;
}
}
let trimmed = line.trim();
if buffer.is_empty() && trimmed.starts_with('.') {
if self.dot_command(conn, trimmed) {
break;
}
continue;
}
buffer.push_str(&line);
if buffer.trim_end().ends_with(';') {
let sql = std::mem::take(&mut buffer);
if let Err(e) = self.run_sql_batch(conn, &sql) {
eprintln!("Error: {e}");
}
}
}
}
fn run_sql_batch(&mut self, conn: &mut Connection, sql: &str) -> graphitesql::Result<()> {
for stmt in split_statements(sql) {
let stmt = stmt.trim();
if stmt.is_empty() {
continue;
}
self.run_one(conn, stmt)?;
}
Ok(())
}
fn run_one(&mut self, conn: &mut Connection, sql: &str) -> graphitesql::Result<()> {
if returns_rows(sql) {
let result = conn.query(sql)?;
self.print_result(&result);
} else {
conn.execute(sql)?;
}
Ok(())
}
fn print_result(&self, result: &QueryResult) {
let out = io::stdout();
let mut out = out.lock();
if self.headers {
let _ = writeln!(out, "{}", result.columns.join("|"));
}
for row in &result.rows {
let cells: Vec<String> = row.iter().map(render_value).collect();
let _ = writeln!(out, "{}", cells.join("|"));
}
}
fn dot_command(&mut self, conn: &mut Connection, line: &str) -> bool {
let mut parts = line.split_whitespace();
let cmd = parts.next().unwrap_or("");
let arg = parts.next();
match cmd {
".quit" | ".exit" => return true,
".help" => print_help(),
".tables" => {
for obj in conn.schema().objects() {
if obj.obj_type == graphitesql::schema::ObjectType::Table {
println!("{}", obj.name);
}
}
}
".schema" => {
for obj in conn.schema().objects() {
if arg.is_none_or(|name| name == obj.name) {
if let Some(sql) = &obj.sql {
println!("{sql};");
}
}
}
}
".headers" => match arg {
Some("on") => self.headers = true,
Some("off") => self.headers = false,
_ => eprintln!("Usage: .headers on|off"),
},
".mode" => { }
other => eprintln!("Unknown command: {other}. Try \".help\"."),
}
false
}
}
fn print_help() {
eprintln!(".help Show this message");
eprintln!(".tables List table names");
eprintln!(".schema [TABLE] Show CREATE statements");
eprintln!(".headers on|off Toggle column headers (default off)");
eprintln!(".quit / .exit Exit the shell");
}
fn render_value(v: &Value) -> String {
match v {
Value::Null => String::new(),
Value::Integer(i) => i.to_string(),
Value::Real(r) => {
if *r == r.trunc() && r.is_finite() {
format!("{r:.1}")
} else {
format!("{r}")
}
}
Value::Text(s) => s.clone(),
Value::Blob(b) => {
let mut s = String::with_capacity(b.len() * 2);
for byte in b {
s.push_str(&format!("{byte:02x}"));
}
s
}
}
}
fn returns_rows(sql: &str) -> bool {
let word = sql
.trim_start()
.split(|c: char| !c.is_ascii_alphabetic())
.find(|w| !w.is_empty())
.unwrap_or("")
.to_ascii_uppercase();
matches!(word.as_str(), "SELECT" | "PRAGMA" | "WITH" | "VALUES")
}
fn split_statements(sql: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut in_str = false;
let mut chars = sql.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\'' => {
in_str = !in_str;
if !in_str && chars.peek() == Some(&'\'') {
cur.push('\'');
cur.push(chars.next().unwrap());
in_str = true;
continue;
}
cur.push(c);
}
';' if !in_str => {
out.push(std::mem::take(&mut cur));
}
_ => cur.push(c),
}
}
if !cur.trim().is_empty() {
out.push(cur);
}
out
}