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