1use anyhow::{Context, Result};
18use atty::Stream;
19use rusqlite::Connection;
20use rustyline::DefaultEditor;
21use std::io::{Read, Write};
22use std::path::Path;
23use std::sync::{Arc, Mutex};
24
25use crate::bookmarks::BookmarkManager;
26use crate::config;
27use crate::db::list_tables;
28use crate::display::{
29 execute_sql, show_all_schemas, show_database_info, show_table_schema, OutputFormat,
30 QueryOptions,
31};
32use crate::export::{export_to_csv, import_csv_to_table};
33use crate::transactions::TransactionManager;
34
35pub fn repl_mode(db_path: &str) -> Result<()> {
51 let db_path = std::fs::canonicalize(db_path)
53 .with_context(|| format!("Failed to resolve absolute path for database '{}'", db_path))?
54 .to_str()
55 .ok_or_else(|| anyhow::anyhow!("Database path contains invalid UTF-8 characters"))?
56 .to_string();
57
58 if !Path::new(&db_path).exists() {
60 anyhow::bail!(
61 "Database '{}' does not exist. Use 'vapor-cli init --name {}' to create it.",
62 db_path,
63 db_path.trim_end_matches(".db")
64 );
65 }
66
67 verify_database_file(&db_path)?;
69
70 let mut conn = create_robust_connection(&db_path)?;
72
73 if !atty::is(Stream::Stdin) {
75 return handle_non_interactive_mode(&conn);
76 }
77
78 println!("Connected to database: {}", db_path);
79 println!("REPL with timing, bookmarks, and transaction support");
80 print_help_summary();
81
82 let mut rl = match DefaultEditor::new() {
84 Ok(editor) => editor,
85 Err(e) => {
86 eprintln!("Warning: Could not initialize readline editor: {}", e);
87 eprintln!(" Falling back to basic input mode.");
88 return handle_basic_repl_mode(&conn);
89 }
90 };
91
92 let history_path = config::get_repl_history_path()?;
94 if rl.load_history(&history_path).is_err() {
95 }
97
98 let mut multi_line_input = String::new();
99 let last_select_query = Arc::new(Mutex::new(String::new()));
100 let bookmarks = Arc::new(Mutex::new(
101 BookmarkManager::new().with_context(|| "Failed to initialize bookmarks")?,
102 ));
103 let transaction_manager = TransactionManager::new();
104 let mut query_options = QueryOptions::default();
105
106 loop {
107 let prompt = get_prompt(&multi_line_input, &transaction_manager);
108
109 let readline = rl.readline(prompt);
110 match readline {
111 Ok(line) => {
112 let line = line.trim();
113 if line.is_empty() && multi_line_input.is_empty() {
114 continue;
115 }
116
117 let command_to_execute = handle_multi_line_input(&mut multi_line_input, line);
119
120 if let Some(command) = command_to_execute {
121 let command_trimmed = command.trim();
122 let result = if command_trimmed.starts_with('.') {
123 handle_special_commands(
124 command_trimmed,
125 &mut conn,
126 &db_path,
127 &bookmarks,
128 &last_select_query,
129 &transaction_manager,
130 &mut query_options,
131 )
132 } else {
133 match transaction_manager.handle_sql_command(&conn, command_trimmed) {
134 Ok(true) => Ok(()), Ok(false) => {
136 handle_single_line_command(
138 command_trimmed,
139 &mut conn,
140 &transaction_manager,
141 &mut query_options,
142 )
143 }
144 Err(e) => Err(e), }
146 };
147
148 if let Err(e) = result {
149 print_command_error(&command, &e);
150 if is_critical_error(&e) {
151 if !offer_reconnection(&db_path) {
152 break; }
154 }
155 }
156 }
157 }
158 Err(rustyline::error::ReadlineError::Interrupted) => {
159 println!("^C");
160 continue;
161 }
162 Err(rustyline::error::ReadlineError::Eof) => {
163 println!("EOF");
164 break;
165 }
166 Err(err) => {
167 eprintln!("Input error: {}", err);
168 eprintln!("Try typing your command again or type 'help' for assistance.");
169 continue;
170 }
171 }
172 }
173
174 cleanup_repl_session(&conn, &transaction_manager, &mut rl, &history_path)?;
176 println!("Goodbye!");
177 Ok(())
178}
179
180fn verify_database_file(db_path: &str) -> Result<()> {
181 let metadata = std::fs::metadata(db_path)
182 .with_context(|| format!("Cannot read database file '{}'", db_path))?;
183
184 if metadata.is_dir() {
185 anyhow::bail!("'{}' is a directory, not a database file", db_path);
186 }
187
188 if metadata.len() == 0 {
189 eprintln!("Warning: Database file '{}' is empty", db_path);
190 }
191
192 Ok(())
193}
194
195fn create_robust_connection(db_path: &str) -> Result<Connection> {
196 let mut last_error = None;
197 let max_retries = 3;
198
199 for attempt in 1..=max_retries {
200 match Connection::open(db_path) {
201 Ok(conn) => {
202 if attempt > 1 {
203 println!("Connection succeeded on attempt {}", attempt);
204 }
205 return Ok(conn);
206 }
207 Err(e) => {
208 last_error = Some(e);
209 if attempt < max_retries {
210 println!("Connection attempt {} failed, retrying...", attempt);
211 std::thread::sleep(std::time::Duration::from_millis(100 * attempt as u64));
212 }
213 }
214 }
215 }
216
217 Err(last_error.unwrap())
218 .with_context(|| format!(
219 "Failed to connect to database '{}' after {} attempts. Database may be locked or corrupted.",
220 db_path, max_retries
221 ))
222}
223
224fn handle_non_interactive_mode(conn: &Connection) -> Result<()> {
225 let mut input = String::new();
226 std::io::stdin().read_to_string(&mut input)?;
227
228 let options = QueryOptions::default();
229 execute_sql(conn, &input, &options)
230}
231
232fn handle_basic_repl_mode(conn: &Connection) -> Result<()> {
233 let mut buffer = String::with_capacity(1024); let options = QueryOptions::default();
235 let stdout = std::io::stdout();
236 let mut stdout_handle = stdout.lock(); loop {
239 stdout_handle.write_all(b"vapor> ")?;
240 stdout_handle.flush()?;
241
242 buffer.clear(); if std::io::stdin().read_line(&mut buffer)? == 0 {
244 break;
245 }
246
247 let line = buffer.trim();
248 if line.is_empty() {
249 continue;
250 }
251
252 if let Err(e) = execute_sql(conn, line, &options) {
253 writeln!(stdout_handle, "Error: {}", e)?;
254 }
255 }
256
257 Ok(())
258}
259
260fn get_prompt(multi_line_input: &str, transaction_manager: &TransactionManager) -> &'static str {
261 if multi_line_input.is_empty() {
262 if transaction_manager.is_active() {
263 "*> "
264 } else {
265 "> "
266 }
267 } else {
268 "... "
269 }
270}
271
272fn handle_multi_line_input(multi_line_input: &mut String, line: &str) -> Option<String> {
273 if !multi_line_input.is_empty() {
274 multi_line_input.push_str(" ");
275 multi_line_input.push_str(line);
276 if line.ends_with(';') {
277 let command = multi_line_input.trim().to_string();
278 multi_line_input.clear();
279 Some(command)
280 } else {
281 None
282 }
283 } else if line.ends_with(';') || is_complete_command(line) {
284 Some(line.to_string())
285 } else {
286 multi_line_input.push_str(line);
287 None
288 }
289}
290
291fn is_complete_command(line: &str) -> bool {
292 let line_lower = line.to_lowercase();
293 matches!(
295 line_lower.as_str(),
296 "exit" | "quit" | "help" | "tables" | "clear" | "info"
297 ) || line_lower.starts_with("schema")
298 || line_lower.starts_with(".")
299 || line_lower.starts_with("begin")
300 || line_lower.starts_with("commit")
301 || line_lower.starts_with("rollback")
302 || line_lower.starts_with("drop")
303}
304
305fn print_help_summary() {
306 println!("Vapor CLI - SQLite Database Management");
307 println!("\nSpecial Commands:");
308 println!(" .help Show this help message");
309 println!(" .tables List all tables");
310 println!(" .schema [table] Show schema for all tables or specific table");
311 println!(" .info Show database information");
312 println!(" .format [type] Set output format (table, json, csv)");
313 println!(" .limit [n] Set row limit (0 for no limit)");
314 println!(" .timing Enable query timing");
315 println!(" .notiming Disable query timing");
316 println!(" .clear Clear screen");
317 println!(" .exit/.quit Exit REPL");
318 println!("\nSQL Commands:");
319 println!(" Enter any valid SQL command ending with semicolon");
320 println!(" Example: SELECT * FROM users;");
321}
322
323fn print_command_error(command: &str, error: &anyhow::Error) {
324 eprintln!("Error executing command '{}':", command);
325 eprintln!("{}", error);
326}
327
328fn is_critical_error(error: &anyhow::Error) -> bool {
329 let error_msg = error.to_string().to_lowercase();
330 error_msg.contains("database is locked")
331 || error_msg.contains("connection")
332 || error_msg.contains("i/o error")
333 || error_msg.contains("disk")
334}
335
336fn offer_reconnection(db_path: &str) -> bool {
337 print!(
338 "Would you like to try reconnecting to '{}'? (y/N): ",
339 db_path
340 );
341 std::io::stdout().flush().unwrap_or(());
342
343 let mut input = String::new();
344 if std::io::stdin().read_line(&mut input).is_ok() {
345 input.trim().to_lowercase().starts_with('y')
346 } else {
347 false
348 }
349}
350
351fn cleanup_repl_session(
352 conn: &Connection,
353 transaction_manager: &TransactionManager,
354 rl: &mut DefaultEditor,
355 history_path: &Path,
356) -> Result<()> {
357 if transaction_manager.is_active() {
359 println!("Rolling back active transaction...");
360 transaction_manager.rollback_transaction(conn)?;
361 }
362
363 if let Err(e) = rl.save_history(history_path) {
365 eprintln!("Warning: Could not save command history: {}", e);
366 }
367
368 Ok(())
369}
370
371fn handle_special_commands(
372 command: &str,
373 conn: &mut Connection,
374 db_path: &str,
375 bookmarks: &Arc<Mutex<BookmarkManager>>,
376 last_select_query: &Arc<Mutex<String>>,
377 transaction_manager: &TransactionManager,
378 query_options: &mut QueryOptions,
379) -> Result<()> {
380 let command = command.trim();
381 let parts: Vec<&str> = command.split_whitespace().collect();
382 let base_command = parts.get(0).cloned().unwrap_or("");
383
384 match base_command {
385 ".help" => show_help(),
386 ".shell" => {
387 println!("Switching to shell mode...");
388 crate::shell::shell_mode(db_path)?;
389 println!("\nReturning to REPL mode.");
390 print_help_summary();
391 }
392 ".exit" | ".quit" => std::process::exit(0),
393 ".tables" => {
394 let tables = list_tables(db_path)?;
395 for table in tables {
396 println!("{}", table);
397 }
398 }
399 ".clear" => {
400 print!("\x1B[2J\x1B[1;1H");
401 std::io::stdout()
402 .flush()
403 .context("Failed to flush stdout")?;
404 }
405 ".info" => show_database_info(conn, db_path)?,
406 ".format" => {
407 if parts.len() > 1 {
408 match parts[1] {
409 "table" => query_options.format = OutputFormat::Table,
410 "json" => query_options.format = OutputFormat::Json,
411 "csv" => query_options.format = OutputFormat::Csv,
412 _ => println!("Invalid format. Available: table, json, csv"),
413 }
414 } else {
415 println!("Current format: {:?}", query_options.format);
416 println!("Usage: .format [table|json|csv]");
417 }
418 }
419 ".limit" => {
420 if parts.len() > 1 {
421 if let Ok(n) = parts[1].parse::<usize>() {
422 if n == 0 {
423 query_options.max_rows = None;
424 println!("Row limit removed");
425 } else {
426 query_options.max_rows = Some(n);
427 println!("Row limit set to {}", n);
428 }
429 } else {
430 println!("Invalid limit value. Use a positive number or 0 for no limit.");
431 }
432 } else {
433 match query_options.max_rows {
434 None => println!("No row limit set"),
435 Some(n) => println!("Current row limit: {}", n),
436 }
437 }
438 }
439 ".timing" => {
440 query_options.show_timing = true;
441 println!("Query timing enabled");
442 }
443 ".notiming" => {
444 query_options.show_timing = false;
445 println!("Query timing disabled");
446 }
447 ".export" => {
448 if parts.len() > 1 {
449 let filename = parts[1];
450 let query = last_select_query.lock().unwrap().clone();
451 if query.is_empty() {
452 println!("No SELECT query has been executed yet.");
453 } else {
454 export_to_csv(conn, &query, filename)?;
455 }
456 } else {
457 println!("Usage: .export FILENAME");
458 }
459 }
460 ".import" => {
461 if parts.len() >= 3 {
462 import_csv_to_table(conn, parts[1], parts[2])?;
463 } else {
464 println!("Usage: .import CSV_FILENAME TABLE_NAME");
465 }
466 }
467 ".bookmark" => {
468 return handle_bookmark_command(
469 command,
470 bookmarks,
471 last_select_query,
472 conn,
473 query_options,
474 );
475 }
476 ".schema" => {
477 if parts.len() > 1 {
478 show_table_schema(conn, parts[1])?;
479 } else {
480 show_all_schemas(conn)?;
481 }
482 }
483 ".status" => {
484 transaction_manager.show_status();
485 }
486 _ => {
487 println!(
488 "Unknown command: '{}'. Type '.help' for a list of commands.",
489 command
490 );
491 }
492 }
493 Ok(())
494}
495
496fn handle_single_line_command(
497 line: &str,
498 conn: &mut Connection,
499 transaction_manager: &TransactionManager,
500 query_options: &mut QueryOptions,
501) -> Result<()> {
502 let line = line.trim();
503 match line.to_lowercase().as_str() {
504 "begin" | "begin transaction" => transaction_manager.begin_transaction(conn),
505 "commit" | "commit transaction" => transaction_manager.commit_transaction(conn),
506 "rollback" | "rollback transaction" => transaction_manager.rollback_transaction(conn),
507 _ => {
508 execute_sql(conn, line, query_options)
510 }
511 }
512}
513
514fn handle_bookmark_command(
515 line: &str,
516 bookmarks: &Arc<Mutex<BookmarkManager>>,
517 last_select_query: &Arc<Mutex<String>>,
518 conn: &mut Connection,
519 query_options: &QueryOptions,
520) -> Result<()> {
521 let parts: Vec<&str> = line.split_whitespace().collect();
522 if parts.len() < 2 {
523 println!("Usage: .bookmark [save|list|run|show|delete] [args...]");
524 return Ok(());
525 }
526
527 let mut bookmarks = bookmarks.lock().unwrap();
528
529 match parts[1] {
530 "save" => {
531 if parts.len() < 3 {
532 println!("Usage: .bookmark save NAME [DESCRIPTION]");
533 return Ok(());
534 }
535 let name = parts[2].to_string();
536 let description = if parts.len() > 3 {
537 Some(parts[3..].join(" "))
538 } else {
539 None
540 };
541 let query = last_select_query.lock().unwrap().clone();
542 if query.is_empty() {
543 println!("No query to save. Execute a query first.");
544 } else {
545 bookmarks.save_bookmark(name.clone(), query, description)?;
546 println!("Bookmark '{}' saved.", name);
547 }
548 }
549 "list" => {
550 bookmarks.list_bookmarks();
551 }
552 "run" => {
553 if parts.len() < 3 {
554 println!("Usage: .bookmark run NAME");
555 return Ok(());
556 }
557 let name = parts[2];
558 if let Some(bookmark) = bookmarks.get_bookmark(name) {
559 println!("Executing bookmark '{}': {}", name, bookmark.query);
560 execute_sql(conn, &bookmark.query, query_options)?;
561 } else {
562 println!("Bookmark '{}' not found.", name);
563 }
564 }
565 "show" => {
566 if parts.len() < 3 {
567 println!("Usage: .bookmark show NAME");
568 return Ok(());
569 }
570 let name = parts[2];
571 if bookmarks.show_bookmark(name).is_none() {
572 println!("Bookmark '{}' not found.", name);
573 }
574 }
575 "delete" => {
576 if parts.len() < 3 {
577 println!("Usage: .bookmark delete NAME");
578 return Ok(());
579 }
580 let name = parts[2];
581 if bookmarks.delete_bookmark(name)? {
582 println!("Bookmark '{}' deleted.", name);
583 } else {
584 println!("Bookmark '{}' not found.", name);
585 }
586 }
587 _ => {
588 println!("Unknown bookmark command. Use: save, list, run, show, or delete");
589 }
590 }
591 Ok(())
592}
593
594pub fn show_help() {
600 println!("Enhanced REPL Commands:");
601 println!();
602 println!("SQL Operations:");
603 println!(" SQL statements - Any valid SQL statement ending with semicolon");
604 println!(" begin/commit/rollback - Transaction control");
605 println!();
606 println!("Database Information:");
607 println!(" tables - List all tables in the database");
608 println!(" schema [table_name] - Show schema for a table or all tables");
609 println!(" info - Show database information and statistics");
610 println!();
611 println!("Output Control:");
612 println!(" .format [table|json|csv] - Set output format (default: table)");
613 println!(" .limit [N] - Set row limit, 0 for no limit (default: 1000)");
614 println!(" .timing [on|off] - Toggle query timing (default: on)");
615 println!(" .export FILENAME - Export last SELECT query to CSV file");
616 println!(" .import CSV_FILENAME TABLE_NAME - Import CSV file into table");
617 println!();
618 println!("Bookmarks:");
619 println!(" .bookmark save NAME [DESC] - Save current query as bookmark");
620 println!(" .bookmark list - List all saved bookmarks");
621 println!(" .bookmark run NAME - Execute a saved bookmark");
622 println!(" .bookmark show NAME - Show bookmark details");
623 println!(" .bookmark delete NAME - Delete a bookmark");
624 println!();
625 println!("Session Management:");
626 println!(" .status - Show transaction status");
627 println!(" clear - Clear the screen");
628 println!(" help - Show this help message");
629 println!(" exit/quit - Exit the REPL");
630 println!();
631 println!("Features:");
632 println!(" • Multi-line input support (continue until semicolon)");
633 println!(" • Command history with arrow keys");
634 println!(" • Query timing and result pagination");
635 println!(" • Transaction status in prompt (* indicates active transaction)");
636 println!(" • Multiple output formats (table, JSON, CSV)");
637 println!(" • Query bookmarking system");
638}