vapor_cli/
display.rs

1use anyhow::{Context, Result};
2use prettytable::{row, Table};
3use rusqlite::{Connection, params};
4use std::time::{Instant, Duration};
5use serde_json::{json, Value};
6use std::collections::HashMap;
7use std::error::Error;
8use std::fmt;
9
10#[derive(Debug, Clone)]
11pub enum OutputFormat {
12    Table,
13    Json,
14    Csv,
15}
16
17#[derive(Debug)]
18#[allow(dead_code)]
19pub enum DisplayError {
20    DatabaseError(String),
21    QueryError(String),
22}
23
24impl fmt::Display for DisplayError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            DisplayError::QueryError(msg) => write!(f, "Query error: {}", msg),
28            DisplayError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
29        }
30    }
31}
32
33impl Error for DisplayError {}
34
35pub struct QueryOptions {
36    pub format: OutputFormat,
37    pub max_rows: Option<usize>,
38    pub show_timing: bool,
39}
40
41impl Default for QueryOptions {
42    fn default() -> Self {
43        Self {
44            format: OutputFormat::Table,
45            max_rows: Some(1000),
46            show_timing: true,
47        }
48    }
49}
50
51#[allow(dead_code)]
52pub struct QueryCache {
53    results: HashMap<String, (Vec<Vec<String>>, Instant)>,
54    max_size: usize,
55    ttl: Duration,
56}
57
58#[allow(dead_code)]
59impl QueryCache {
60    pub fn new(max_size: usize, ttl: Duration) -> Self {
61        Self {
62            results: HashMap::new(),
63            max_size,
64            ttl,
65        }
66    }
67
68    pub fn get(&self, query: &str) -> Option<&Vec<Vec<String>>> {
69        if let Some((results, timestamp)) = self.results.get(query) {
70            if timestamp.elapsed() < self.ttl {
71                return Some(results);
72            }
73        }
74        None
75    }
76
77    pub fn insert(&mut self, query: String, results: Vec<Vec<String>>) {
78        // Remove oldest entries if cache is full
79        if self.results.len() >= self.max_size {
80            let oldest_key = self.results.iter()
81                .min_by_key(|(_, (_, time))| time)
82                .map(|(key, _)| key.clone());
83            
84            if let Some(key) = oldest_key {
85                self.results.remove(&key);
86            }
87        }
88        
89        self.results.insert(query, (results, Instant::now()));
90    }
91
92    pub fn clear(&mut self) {
93        self.results.clear();
94    }
95}
96
97#[allow(dead_code)]
98pub struct ProgressiveLoader {
99    batch_size: usize,
100    total_rows: usize,
101    loaded_rows: usize,
102    column_names: Vec<String>,
103    current_batch: Vec<Vec<String>>,
104}
105
106#[allow(dead_code)]
107impl ProgressiveLoader {
108    pub fn new(batch_size: usize, column_names: Vec<String>) -> Self {
109        Self {
110            batch_size,
111            total_rows: 0,
112            loaded_rows: 0,
113            column_names,
114            current_batch: Vec::new(),
115        }
116    }
117
118    pub fn add_row(&mut self, row: Vec<String>) {
119        self.current_batch.push(row);
120        self.loaded_rows += 1;
121        
122        if self.current_batch.len() >= self.batch_size {
123            self.flush_batch();
124        }
125    }
126
127    pub fn flush_batch(&mut self) {
128        if !self.current_batch.is_empty() {
129            display_as_table(&self.column_names, &self.current_batch);
130            println!("Loaded {}/{} rows...", self.loaded_rows, self.total_rows);
131            self.current_batch.clear();
132        }
133    }
134
135    pub fn set_total_rows(&mut self, total: usize) {
136        self.total_rows = total;
137    }
138
139    pub fn is_complete(&self) -> bool {
140        self.loaded_rows >= self.total_rows
141    }
142}
143
144/// Execute a SQL command and display the results with enhanced formatting and timing
145pub fn execute_sql(conn: &Connection, sql: &str, options: &QueryOptions) -> Result<()> {
146    let start_time = Instant::now();
147    
148    // Execute the query
149    let mut stmt = conn.prepare(sql)
150        .context("Failed to prepare SQL statement")?;
151    
152    // Check if it's a SELECT query
153    let is_select = sql.trim().to_uppercase().starts_with("SELECT");
154    
155    if is_select {
156        // Get column names before executing the query
157        let column_names: Vec<String> = stmt.column_names()
158            .iter()
159            .map(|name| name.to_string())
160            .collect();
161        
162        let mut rows = stmt.query([])
163            .context("Failed to execute SELECT query")?;
164        
165        // Collect all rows
166        let mut all_rows = Vec::new();
167        let mut row_count = 0;
168        
169        while let Some(row) = rows.next()? {
170            let mut row_values = Vec::new();
171            for i in 0..column_names.len() {
172                let value = match row.get_ref(i)? {
173                    rusqlite::types::ValueRef::Null => "NULL".to_string(),
174                    rusqlite::types::ValueRef::Integer(val) => val.to_string(),
175                    rusqlite::types::ValueRef::Real(val) => val.to_string(),
176                    rusqlite::types::ValueRef::Text(val) => String::from_utf8_lossy(val).to_string(),
177                    rusqlite::types::ValueRef::Blob(val) => format!("<binary data: {} bytes>", val.len()),
178                };
179                row_values.push(value);
180            }
181            all_rows.push(row_values);
182            row_count += 1;
183            
184            if let Some(limit) = options.max_rows {
185                if row_count >= limit {
186                    break;
187                }
188            }
189        }
190        
191        // Display results based on format
192        if !all_rows.is_empty() {
193            match options.format {
194                OutputFormat::Table => display_as_table(&column_names, &all_rows),
195                OutputFormat::Json => display_as_json(&column_names, &all_rows)?,
196                OutputFormat::Csv => display_as_csv(&column_names, &all_rows),
197            }
198        }
199        
200        println!("{} row(s) returned", row_count);
201        
202        if let Some(limit) = options.max_rows {
203            if row_count >= limit {
204                println!("(Limited to {} rows. Use '.limit 0' to show all rows)", limit);
205            }
206        }
207    } else {
208        // For non-SELECT queries
209        let affected = stmt.execute([])
210            .context("Failed to execute non-SELECT query")?;
211        
212        println!("{} row(s) affected", affected);
213    }
214    
215    if options.show_timing {
216        println!("Query executed in {:.3}ms", start_time.elapsed().as_secs_f64() * 1000.0);
217    }
218    
219    Ok(())
220}
221
222fn display_as_table(column_names: &[String], rows: &[Vec<String>]) {
223    let mut table = Table::new();
224    table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
225
226    // Add header row
227    let mut header_row = prettytable::Row::empty();
228    for col_name in column_names {
229        header_row.add_cell(prettytable::Cell::new(col_name).style_spec("b"));
230    }
231    table.add_row(header_row);
232
233    // Add data rows
234    for row_values in rows {
235        let mut data_row = prettytable::Row::empty();
236        for value in row_values {
237            data_row.add_cell(prettytable::Cell::new(value));
238        }
239        table.add_row(data_row);
240    }
241
242    table.printstd();
243}
244
245fn display_as_json(column_names: &[String], rows: &[Vec<String>]) -> Result<()> {
246    let mut json_rows = Vec::new();
247    
248    for row_values in rows {
249        let mut json_row = serde_json::Map::new();
250        for (i, value) in row_values.iter().enumerate() {
251            let json_value = if value == "NULL" {
252                Value::Null
253            } else if let Ok(int_val) = value.parse::<i64>() {
254                Value::Number(serde_json::Number::from(int_val))
255            } else if let Ok(float_val) = value.parse::<f64>() {
256                Value::Number(serde_json::Number::from_f64(float_val).unwrap_or_else(|| serde_json::Number::from(0)))
257            } else {
258                Value::String(value.clone())
259            };
260            json_row.insert(column_names[i].clone(), json_value);
261        }
262        json_rows.push(Value::Object(json_row));
263    }
264    
265    let output = json!({
266        "data": json_rows,
267        "columns": column_names,
268        "row_count": rows.len()
269    });
270    
271    println!("{}", serde_json::to_string_pretty(&output)?);
272    Ok(())
273}
274
275fn display_as_csv(column_names: &[String], rows: &[Vec<String>]) {
276    // Print header
277    println!("{}", column_names.join(","));
278    
279    // Print rows
280    for row_values in rows {
281        let escaped_values: Vec<String> = row_values.iter()
282            .map(|v| {
283                if v.contains(',') || v.contains('"') || v.contains('\n') {
284                    format!("\"{}\"", v.replace('"', "\"\""))
285                } else {
286                    v.clone()
287                }
288            })
289            .collect();
290        println!("{}", escaped_values.join(","));
291    }
292}
293
294/// Show the schema for a specific table
295pub fn show_table_schema(conn: &Connection, table_name: &str) -> Result<()> {
296    // Check if the table exists
297    let mut check_stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?")
298        .context("Failed to prepare statement for checking table existence")?;
299
300    let exists: bool = check_stmt.exists(params![table_name])
301        .context(format!("Failed to check if table '{}' exists", table_name))?;
302
303    if !exists {
304        println!("Table '{}' does not exist.", table_name);
305        return Ok(());
306    }
307
308    // Get the table schema
309    let pragma_sql = format!("PRAGMA table_info({})", table_name);
310    let mut stmt = conn.prepare(&pragma_sql)
311        .context(format!("Failed to prepare statement for table schema: {}", table_name))?;
312
313    let columns = stmt.query_map(params![], |row| {
314        Ok((
315            row.get::<_, i32>(0)?, // cid
316            row.get::<_, String>(1)?, // name
317            row.get::<_, String>(2)?, // type
318            row.get::<_, bool>(3)?, // notnull
319            row.get::<_, Option<String>>(4)?, // dflt_value
320            row.get::<_, i32>(5)?, // pk
321        ))
322    }).context(format!("Failed to query schema for table: {}", table_name))?;
323
324    // Create a pretty table for display
325    let mut table = Table::new();
326    table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
327    table.add_row(row!["ID", "Name", "Type", "Not Null", "Default Value", "Primary Key"]);
328
329    let mut has_columns = false;
330    for column_result in columns {
331        has_columns = true;
332        let (cid, name, type_name, not_null, default_value, pk) = column_result
333            .context(format!("Failed to read column info for table: {}", table_name))?;
334
335        let not_null_str = if not_null { "YES" } else { "NO" };
336        let pk_str = if pk > 0 { "YES" } else { "NO" };
337        let default_str = default_value.unwrap_or_else(|| "NULL".to_string());
338
339        table.add_row(row![cid, name, type_name, not_null_str, default_str, pk_str]);
340    }
341
342    if has_columns {
343        println!("Schema for table '{}':", table_name);
344        table.printstd();
345    } else {
346        println!("No columns found for table: {}", table_name);
347    }
348
349    Ok(())
350}
351
352/// Show the schema for all tables
353pub fn show_all_schemas(conn: &Connection) -> Result<()> {
354    // Get all table names
355    let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
356        .context("Failed to prepare statement for listing tables")?;
357
358    let table_names = stmt.query_map(params![], |row| row.get::<_, String>(0))
359        .context("Failed to query tables")?;
360
361    let mut has_tables = false;
362    for (i, table_name_result) in table_names.enumerate() {
363        has_tables = true;
364        let table_name = table_name_result.context("Failed to read table name")?;
365        if i > 0 {
366            println!(); 
367        }
368        show_table_schema(conn, &table_name)?;
369    }
370
371    if !has_tables {
372        println!("No tables found in the database.");
373    }
374
375    Ok(())
376}
377
378/// Show database information and statistics
379pub fn show_database_info(conn: &Connection, db_path: &str) -> Result<()> {
380    println!("Database Information:");
381    println!("  Path: {}", db_path);
382    
383    // Get database file size
384    if let Ok(metadata) = std::fs::metadata(db_path) {
385        let size_mb = metadata.len() as f64 / (1024.0 * 1024.0);
386        println!("  Size: {:.2} MB", size_mb);
387    }
388    
389    // Get SQLite version
390    let version: String = conn.query_row("SELECT sqlite_version()", [], |row| row.get(0))?;
391    println!("  SQLite Version: {}", version);
392    
393    // Get page size and page count
394    let page_size: i64 = conn.query_row("PRAGMA page_size", [], |row| row.get(0))?;
395    let page_count: i64 = conn.query_row("PRAGMA page_count", [], |row| row.get(0))?;
396    println!("  Page Size: {} bytes", page_size);
397    println!("  Page Count: {}", page_count);
398    
399    // Get table statistics
400    let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")?;
401    let table_names = stmt.query_map([], |row| row.get::<_, String>(0))?;
402    
403    println!("\nTable Statistics:");
404    let mut total_rows = 0;
405    
406    for table_name_result in table_names {
407        let table_name = table_name_result?;
408        let count_sql = format!("SELECT COUNT(*) FROM {}", table_name);
409        let row_count: i64 = conn.query_row(&count_sql, [], |row| row.get(0))?;
410        println!("  {}: {} rows", table_name, row_count);
411        total_rows += row_count;
412    }
413    
414    println!("  Total Rows: {}", total_rows);
415    
416    Ok(())
417}