vapor_cli/
display.rs

1//! # Query Result Display and Formatting
2//!
3//! This module is responsible for executing SQL queries and presenting the results in various
4//! formats. It handles the formatting of data into tables, JSON, and CSV, and also provides
5//! utility functions for displaying database metadata like table schemas and statistics.
6//!
7//! ## Core Components:
8//! - `execute_sql`: The main function that runs a SQL query and manages the display of its results.
9//! - `OutputFormat`: An enum to specify the desired output format (`Table`, `Json`, `Csv`).
10//! - `QueryOptions`: A struct to control display settings like row limits and timing information.
11//! - Schema Display: Functions like `show_table_schema` and `show_all_schemas` for inspecting the DB structure.
12//! - Database Info: `show_database_info` provides a summary of the database file and its contents.
13//!
14//! The module also includes experimental, currently unused features for result caching (`QueryCache`)
15//! and progressive data loading (`ProgressiveLoader`).
16
17use anyhow::{Context, Result};
18use prettytable::{row, Table};
19use rusqlite::{params, Connection};
20use serde_json::{json, Value};
21use std::collections::HashMap;
22use std::error::Error;
23use std::fmt;
24use std::time::{Duration, Instant};
25
26/// Specifies the output format for query results.
27#[derive(Debug, Clone)]
28pub enum OutputFormat {
29    Table,
30    Json,
31    Csv,
32}
33
34#[allow(dead_code)]
35/// Custom error types for display-related operations.
36///
37/// Note: This is currently not used extensively but is defined for future error handling enhancements.
38#[derive(Debug)]
39#[allow(dead_code)]
40pub enum DisplayError {
41    DatabaseError(String),
42    QueryError(String),
43}
44
45impl fmt::Display for DisplayError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            DisplayError::QueryError(msg) => write!(f, "Query error: {}", msg),
49            DisplayError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
50        }
51    }
52}
53
54impl Error for DisplayError {}
55
56/// Defines options for controlling how a query is executed and displayed.
57pub struct QueryOptions {
58    pub format: OutputFormat,
59    pub max_rows: Option<usize>,
60    pub show_timing: bool,
61}
62
63impl Default for QueryOptions {
64    fn default() -> Self {
65        Self {
66            format: OutputFormat::Table,
67            max_rows: Some(1000),
68            show_timing: true,
69        }
70    }
71}
72
73#[allow(dead_code)]
74/// A cache for storing and retrieving query results to improve performance for repeated queries.
75///
76/// Note: This feature is experimental and not currently integrated into the REPL or CLI.
77#[allow(dead_code)]
78pub struct QueryCache {
79    results: HashMap<String, (Vec<Vec<String>>, Instant)>,
80    max_size: usize,
81    ttl: Duration,
82}
83
84#[allow(dead_code)]
85impl QueryCache {
86    pub fn new(max_size: usize, ttl: Duration) -> Self {
87        Self {
88            results: HashMap::new(),
89            max_size,
90            ttl,
91        }
92    }
93
94    pub fn get(&self, query: &str) -> Option<&Vec<Vec<String>>> {
95        if let Some((results, timestamp)) = self.results.get(query) {
96            if timestamp.elapsed() < self.ttl {
97                return Some(results);
98            }
99        }
100        None
101    }
102
103    pub fn insert(&mut self, query: String, results: Vec<Vec<String>>) {
104        // Remove oldest entries if cache is full
105        if self.results.len() >= self.max_size {
106            let oldest_key = self
107                .results
108                .iter()
109                .min_by_key(|(_, (_, time))| time)
110                .map(|(key, _)| key.clone());
111
112            if let Some(key) = oldest_key {
113                self.results.remove(&key);
114            }
115        }
116
117        self.results.insert(query, (results, Instant::now()));
118    }
119
120    pub fn clear(&mut self) {
121        self.results.clear();
122    }
123}
124
125#[allow(dead_code)]
126/// A helper for loading and displaying large result sets in batches to avoid high memory usage.
127///
128/// Note: This feature is experimental and not currently integrated into the REPL or CLI.
129#[allow(dead_code)]
130pub struct ProgressiveLoader {
131    batch_size: usize,
132    total_rows: usize,
133    loaded_rows: usize,
134    column_names: Vec<String>,
135    current_batch: Vec<Vec<String>>,
136}
137
138#[allow(dead_code)]
139impl ProgressiveLoader {
140    pub fn new(batch_size: usize, column_names: Vec<String>) -> Self {
141        Self {
142            batch_size,
143            total_rows: 0,
144            loaded_rows: 0,
145            column_names,
146            current_batch: Vec::new(),
147        }
148    }
149
150    pub fn add_row(&mut self, row: Vec<String>) {
151        self.current_batch.push(row);
152        self.loaded_rows += 1;
153
154        if self.current_batch.len() >= self.batch_size {
155            self.flush_batch();
156        }
157    }
158
159    pub fn flush_batch(&mut self) {
160        if !self.current_batch.is_empty() {
161            display_as_table(&self.column_names, &self.current_batch);
162            println!("Loaded {}/{} rows...", self.loaded_rows, self.total_rows);
163            self.current_batch.clear();
164        }
165    }
166
167    pub fn set_total_rows(&mut self, total: usize) {
168        self.total_rows = total;
169    }
170
171    pub fn is_complete(&self) -> bool {
172        self.loaded_rows >= self.total_rows
173    }
174}
175
176/// Executes a SQL statement and displays the results according to the provided options.
177///
178/// This function handles both `SELECT` queries, which produce result sets, and other
179/// statements (e.g., `INSERT`, `UPDATE`, `CREATE`), which report the number of affected rows.
180///
181/// # Arguments
182///
183/// * `conn` - A reference to the active `rusqlite::Connection`.
184/// * `sql` - The SQL string to execute.
185/// * `options` - A `QueryOptions` struct specifying the format, row limit, and other settings.
186///
187/// # Returns
188///
189/// A `Result` which is `Ok(())` on success, or an `Err` if the query fails to prepare or execute.
190pub fn execute_sql(conn: &Connection, sql: &str, options: &QueryOptions, last_select_query: &std::sync::Arc<std::sync::Mutex<String>>) -> Result<()> {
191    let start_time = Instant::now();
192
193    // Execute the query
194    let mut stmt = conn
195        .prepare(sql)
196        .context("Failed to prepare SQL statement")?;
197
198    // Check if it's a SELECT query
199    let is_select = sql.trim().to_uppercase().starts_with("SELECT");
200
201    if is_select {
202        let mut last_query_guard = last_select_query.lock().unwrap();
203        last_query_guard.clear();
204        last_query_guard.push_str(sql);
205    }
206
207    if is_select {
208        // Get column names before executing the query
209        let column_names: Vec<String> = stmt
210            .column_names()
211            .iter()
212            .map(|name| name.to_string())
213            .collect();
214
215        let mut rows = stmt.query([]).context("Failed to execute SELECT query")?;
216
217        // Collect all rows
218        let mut all_rows = Vec::new();
219        let mut row_count = 0;
220
221        while let Some(row) = rows.next()? {
222            let mut row_values = Vec::new();
223            for i in 0..column_names.len() {
224                let value = match row.get_ref(i)? {
225                    rusqlite::types::ValueRef::Null => "NULL".to_string(),
226                    rusqlite::types::ValueRef::Integer(val) => val.to_string(),
227                    rusqlite::types::ValueRef::Real(val) => val.to_string(),
228                    rusqlite::types::ValueRef::Text(val) => {
229                        String::from_utf8_lossy(val).to_string()
230                    }
231                    rusqlite::types::ValueRef::Blob(val) => {
232                        format!("<binary data: {} bytes>", val.len())
233                    }
234                };
235                row_values.push(value);
236            }
237            all_rows.push(row_values);
238            row_count += 1;
239
240            if let Some(limit) = options.max_rows {
241                if row_count >= limit {
242                    break;
243                }
244            }
245        }
246
247        // Display results based on format
248        if !all_rows.is_empty() {
249            match options.format {
250                OutputFormat::Table => display_as_table(&column_names, &all_rows),
251                OutputFormat::Json => display_as_json(&column_names, &all_rows)?,
252                OutputFormat::Csv => display_as_csv(&column_names, &all_rows),
253            }
254        }
255
256        println!("{} row(s) returned", row_count);
257
258        if let Some(limit) = options.max_rows {
259            if row_count >= limit {
260                println!(
261                    "(Limited to {} rows. Use '.limit 0' to show all rows)",
262                    limit
263                );
264            }
265        }
266    } else {
267        // For non-SELECT queries
268        let affected = stmt
269            .execute([])
270            .context("Failed to execute non-SELECT query")?;
271
272        println!("{} row(s) affected", affected);
273    }
274
275    if options.show_timing {
276        println!(
277            "Query executed in {:.3}ms",
278            start_time.elapsed().as_secs_f64() * 1000.0
279        );
280    }
281
282    Ok(())
283}
284
285/// Formats and prints query results as a bordered table to the console.
286fn display_as_table(column_names: &[String], rows: &[Vec<String>]) {
287    let mut table = Table::new();
288    table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
289
290    // Add header row
291    let mut header_row = prettytable::Row::empty();
292    for col_name in column_names {
293        header_row.add_cell(prettytable::Cell::new(col_name).style_spec("b"));
294    }
295    table.add_row(header_row);
296
297    // Add data rows
298    for row_values in rows {
299        let mut data_row = prettytable::Row::empty();
300        for value in row_values {
301            data_row.add_cell(prettytable::Cell::new(value));
302        }
303        table.add_row(data_row);
304    }
305
306    table.printstd();
307}
308
309/// Formats and prints query results as a JSON object to the console.
310///
311/// The JSON output includes the column names, the number of rows, and the data itself.
312/// It attempts to infer numeric types from the string values.
313fn display_as_json(column_names: &[String], rows: &[Vec<String>]) -> Result<()> {
314    let mut json_rows = Vec::new();
315
316    for row_values in rows {
317        let mut json_row = serde_json::Map::new();
318        for (i, value) in row_values.iter().enumerate() {
319            let json_value = if value == "NULL" {
320                Value::Null
321            } else if let Ok(int_val) = value.parse::<i64>() {
322                Value::Number(serde_json::Number::from(int_val))
323            } else if let Ok(float_val) = value.parse::<f64>() {
324                Value::Number(
325                    serde_json::Number::from_f64(float_val)
326                        .unwrap_or_else(|| serde_json::Number::from(0)),
327                )
328            } else {
329                Value::String(value.clone())
330            };
331            json_row.insert(column_names[i].clone(), json_value);
332        }
333        json_rows.push(Value::Object(json_row));
334    }
335
336    let output = json!({
337        "data": json_rows,
338        "columns": column_names,
339        "row_count": rows.len()
340    });
341
342    println!("{}", serde_json::to_string_pretty(&output)?);
343    Ok(())
344}
345
346/// Formats and prints query results as CSV data to the console.
347///
348/// This function handles basic CSV escaping for values containing commas or quotes.
349fn display_as_csv(column_names: &[String], rows: &[Vec<String>]) {
350    // Print header
351    println!("{}", column_names.join(","));
352
353    // Print rows
354    for row_values in rows {
355        let escaped_values: Vec<String> = row_values
356            .iter()
357            .map(|v| {
358                if v.contains(',') || v.contains('"') || v.contains('\n') {
359                    format!("\"{}\"", v.replace('"', "\"\""))
360                } else {
361                    v.clone()
362                }
363            })
364            .collect();
365        println!("{}", escaped_values.join(","));
366    }
367}
368
369/// Displays the schema for a specific table, including column names, types, and constraints.
370///
371/// It uses `PRAGMA table_info` to retrieve the schema information from SQLite.
372///
373/// # Arguments
374///
375/// * `conn` - A reference to the active `rusqlite::Connection`.
376/// * `table_name` - The name of the table to inspect.
377///
378/// # Returns
379///
380/// A `Result` which is `Ok(())` on success, or an `Err` on failure.
381pub fn show_table_schema(conn: &Connection, table_name: &str) -> Result<()> {
382    // Check if the table exists
383    let mut check_stmt = conn
384        .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?")
385        .context("Failed to prepare statement for checking table existence")?;
386
387    let exists: bool = check_stmt
388        .exists(params![table_name])
389        .context(format!("Failed to check if table '{}' exists", table_name))?;
390
391    if !exists {
392        println!("Table '{}' does not exist.", table_name);
393        return Ok(());
394    }
395
396    // Get the table schema
397    let pragma_sql = format!("PRAGMA table_info({})", table_name);
398    let mut stmt = conn.prepare(&pragma_sql).context(format!(
399        "Failed to prepare statement for table schema: {}",
400        table_name
401    ))?;
402
403    let columns = stmt
404        .query_map(params![], |row| {
405            Ok((
406                row.get::<_, i32>(0)?,            // cid
407                row.get::<_, String>(1)?,         // name
408                row.get::<_, String>(2)?,         // type
409                row.get::<_, bool>(3)?,           // notnull
410                row.get::<_, Option<String>>(4)?, // dflt_value
411                row.get::<_, i32>(5)?,            // pk
412            ))
413        })
414        .context(format!("Failed to query schema for table: {}", table_name))?;
415
416    // Create a pretty table for display
417    let mut table = Table::new();
418    table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
419    table.add_row(row![
420        "ID",
421        "Name",
422        "Type",
423        "Not Null",
424        "Default Value",
425        "Primary Key"
426    ]);
427
428    let mut has_columns = false;
429    for column_result in columns {
430        has_columns = true;
431        let (cid, name, type_name, not_null, default_value, pk) = column_result.context(
432            format!("Failed to read column info for table: {}", table_name),
433        )?;
434
435        let not_null_str = if not_null { "YES" } else { "NO" };
436        let pk_str = if pk > 0 { "YES" } else { "NO" };
437        let default_str = default_value.unwrap_or_else(|| "NULL".to_string());
438
439        table.add_row(row![
440            cid,
441            name,
442            type_name,
443            not_null_str,
444            default_str,
445            pk_str
446        ]);
447    }
448
449    if has_columns {
450        println!("Schema for table '{}':", table_name);
451        table.printstd();
452    } else {
453        println!("No columns found for table: {}", table_name);
454    }
455
456    Ok(())
457}
458
459/// Iterates through all user-defined tables in the database and displays the schema for each one.
460///
461/// It queries the `sqlite_master` table to find all tables and then calls `show_table_schema` for each.
462///
463/// # Arguments
464///
465/// * `conn` - A reference to the active `rusqlite::Connection`.
466///
467/// # Returns
468///
469/// A `Result` which is `Ok(())` on success, or an `Err` on failure.
470pub fn show_all_schemas(conn: &Connection) -> Result<()> {
471    // Get all table names
472    let mut stmt = conn
473        .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
474        .context("Failed to prepare statement for listing tables")?;
475
476    let table_names = stmt
477        .query_map(params![], |row| row.get::<_, String>(0))
478        .context("Failed to query tables")?;
479
480    let mut has_tables = false;
481    for (i, table_name_result) in table_names.enumerate() {
482        has_tables = true;
483        let table_name = table_name_result.context("Failed to read table name")?;
484        if i > 0 {
485            println!();
486        }
487        show_table_schema(conn, &table_name)?;
488    }
489
490    if !has_tables {
491        println!("No tables found in the database.");
492    }
493
494    Ok(())
495}
496
497/// Displays general information and statistics about the connected database.
498///
499/// This includes the database file path, size, SQLite version, and row counts for each table.
500///
501/// # Arguments
502///
503/// * `conn` - A reference to the active `rusqlite::Connection`.
504/// * `db_path` - The file path of the database, used to calculate its size.
505///
506/// # Returns
507///
508/// A `Result` which is `Ok(())` on success, or an `Err` on failure.
509pub fn show_database_info(conn: &Connection, db_path: &str) -> Result<()> {
510    println!("Database Information:");
511    println!("  Path: {}", db_path);
512
513    // Get database file size
514    if let Ok(metadata) = std::fs::metadata(db_path) {
515        let size_mb = metadata.len() as f64 / (1024.0 * 1024.0);
516        println!("  Size: {:.2} MB", size_mb);
517    }
518
519    // Get SQLite version
520    let version: String = conn.query_row("SELECT sqlite_version()", [], |row| row.get(0))?;
521    println!("  SQLite Version: {}", version);
522
523    // Get page size and page count
524    let page_size: i64 = conn.query_row("PRAGMA page_size", [], |row| row.get(0))?;
525    let page_count: i64 = conn.query_row("PRAGMA page_count", [], |row| row.get(0))?;
526    println!("  Page Size: {} bytes", page_size);
527    println!("  Page Count: {}", page_count);
528
529    // Get table statistics
530    let mut stmt = conn.prepare(
531        "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
532    )?;
533    let table_names = stmt.query_map([], |row| row.get::<_, String>(0))?;
534
535    println!("\nTable Statistics:");
536    let mut total_rows = 0;
537
538    for table_name_result in table_names {
539        let table_name = table_name_result?;
540        let count_sql = format!("SELECT COUNT(*) FROM {}", table_name);
541        let row_count: i64 = conn.query_row(&count_sql, [], |row| row.get(0))?;
542        println!("  {}: {} rows", table_name, row_count);
543        total_rows += row_count;
544    }
545
546    println!("  Total Rows: {}", total_rows);
547
548    Ok(())
549}