vapor_cli/
db.rs

1//! # Database Management
2//! 
3//! This module provides all the core functionalities for interacting with the SQLite database.
4//! It handles database initialization, connection, table creation, and listing tables.
5//! The functions in this module are designed to be robust, with features like retry logic
6//! for connections and integrity checks to ensure database validity.
7
8use anyhow::{Context, Result};
9use prettytable::{row, Table};
10use rusqlite::{params, Connection};
11use std::fs;
12use std::path::Path;
13use std::time::Duration;
14
15/// Initializes a new SQLite database file.
16///
17/// This function creates a new database file at the specified path. It includes logic to:
18/// - Append `.db` if not present.
19/// - Check if the database already exists to prevent overwriting.
20/// - Create parent directories if they don't exist.
21/// - Use a retry mechanism for creating the connection.
22/// - Verify the integrity of the newly created database.
23///
24/// # Arguments
25///
26/// * `name` - The name of the database file to create.
27///
28/// # Returns
29///
30/// A `Result` which is `Ok(())` on successful creation, or an `Err` with context if it fails.
31pub fn init_database(name: &str) -> Result<()> {
32    let db_path = if name.ends_with(".db") {
33        name.to_string()
34    } else {
35        format!("{}.db", name)
36    };
37
38    // Check if the database already exists
39    if Path::new(&db_path).exists() {
40        println!("Database '{}' already exists.", db_path);
41        // Verify it's a valid SQLite database
42        verify_database_integrity(&db_path)?;
43        return Ok(());
44    }
45
46    // Create the database directory if it doesn't exist
47    if let Some(parent) = Path::new(&db_path).parent() {
48        if !parent.exists() {
49            println!("Creating directory: {:?}", parent);
50            fs::create_dir_all(parent).with_context(|| {
51                format!(
52                    "Failed to create directory: {:?}. Check permissions and disk space.",
53                    parent
54                )
55            })?;
56        }
57    }
58
59    // Create a new SQLite database with retry logic
60    let _conn = create_connection_with_retry(&db_path, 3)?;
61
62    // Verify the database was created successfully
63    verify_database_integrity(&db_path)?;
64
65    println!("Successfully created database: {}", db_path);
66    // Connection will be automatically dropped when it goes out of scope
67
68    Ok(())
69}
70
71/// Connects to an existing SQLite database.
72///
73/// This function establishes a connection to a given database file. It performs several checks:
74/// - Ensures the database file exists.
75/// - Verifies that the path points to a file, not a directory.
76/// - Uses a retry mechanism for the connection.
77/// - Performs an integrity check on the database upon successful connection.
78///
79/// # Arguments
80///
81/// * `path` - The file path to the SQLite database.
82///
83/// # Returns
84///
85/// A `Result` which is `Ok(())` on successful connection, or an `Err` with context if it fails.
86pub fn connect_database(path: &str) -> Result<()> {
87    // Check if the database exists
88    if !Path::new(path).exists() {
89        anyhow::bail!(
90            "Database '{}' does not exist. Use 'vapor-cli init --name {}' to create it.",
91            path,
92            path.trim_end_matches(".db")
93        );
94    }
95
96    // Check if it's a file (not a directory)
97    let metadata =
98        fs::metadata(path).with_context(|| format!("Cannot read file metadata for '{}'", path))?;
99
100    if metadata.is_dir() {
101        anyhow::bail!("'{}' is a directory, not a database file", path);
102    }
103
104    // Try to connect to the database with retry logic
105    let _conn = create_connection_with_retry(path, 3)?;
106
107    // Verify database integrity
108    verify_database_integrity(path)?;
109
110    println!("Successfully connected to database: {}", path);
111    // Connection will be automatically dropped when it goes out of scope
112
113    Ok(())
114}
115
116/// Creates a new table in the specified database.
117///
118/// This function adds a new table to the database with the given name and column definitions.
119/// It includes validation to:
120/// - Ensure the database file exists.
121/// - Check if a table with the same name already exists.
122/// - Perform basic validation on the column definition syntax.
123/// - Verify that the table was actually created after execution.
124///
125/// # Arguments
126///
127/// * `db_path` - The path to the database file.
128/// * `table_name` - The name of the table to create.
129/// * `columns` - A string defining the table's columns (e.g., "id INTEGER PRIMARY KEY, name TEXT").
130///
131/// # Returns
132///
133/// A `Result` which is `Ok(())` on successful table creation, or an `Err` with context if it fails.
134pub fn create_table(db_path: &str, table_name: &str, columns: &str) -> Result<()> {
135    // Validate database exists and is accessible
136    if !Path::new(db_path).exists() {
137        anyhow::bail!(
138            "Database '{}' does not exist. Use 'vapor-cli init --name {}' to create it.",
139            db_path,
140            db_path.trim_end_matches(".db")
141        );
142    }
143
144    // Connect to the database with retry logic
145    let conn = create_connection_with_retry(db_path, 3)?;
146
147    // Check if table already exists
148    let table_exists = check_table_exists(&conn, table_name)?;
149    if table_exists {
150        println!(
151            "Table '{}' already exists in database: {}",
152            table_name, db_path
153        );
154        return Ok(());
155    }
156
157    // Validate column definition syntax
158    validate_column_syntax(columns)?;
159
160    // Create the table with proper error handling
161    let create_table_sql = format!("CREATE TABLE {} ({})", table_name, columns);
162
163    conn.execute(&create_table_sql, params![])
164        .with_context(|| {
165            format!(
166                "Failed to create table '{}'. Check column syntax: {}",
167                table_name, columns
168            )
169        })?;
170
171    // Verify table was created successfully
172    let table_exists_after = check_table_exists(&conn, table_name)?;
173    if !table_exists_after {
174        anyhow::bail!(
175            "Table creation appeared to succeed but table '{}' is not found",
176            table_name
177        );
178    }
179
180    println!(
181        "Successfully created table '{}' in database: {}",
182        table_name, db_path
183    );
184    // Connection will be automatically dropped when it goes out of scope
185
186    Ok(())
187}
188
189/// Lists all user-created tables in the specified database.
190///
191/// This function queries the `sqlite_master` table to find all tables, excluding the internal
192/// `sqlite_` tables. It then prints the list of tables in a formatted table to the console.
193///
194/// # Arguments
195///
196/// * `db_path` - The path to the database file.
197///
198/// # Returns
199///
200/// A `Result` containing a `Vec<String>` of table names on success, or an `Err` with context if it fails.
201pub fn list_tables(db_path: &str) -> Result<Vec<String>> {
202    // Validate database exists and is accessible
203    if !Path::new(db_path).exists() {
204        anyhow::bail!(
205            "Database '{}' does not exist. Use 'vapor-cli init --name {}' to create it.",
206            db_path,
207            db_path.trim_end_matches(".db")
208        );
209    }
210
211    // Connect to the database with retry logic
212    let conn = create_connection_with_retry(db_path, 3)?;
213
214    // Query for all tables with error handling
215    let mut stmt = conn
216        .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
217        .context("Failed to prepare statement for listing tables. Database may be corrupted.")?;
218
219    let table_names = stmt
220        .query_map(params![], |row| row.get::<_, String>(0))
221        .context("Failed to execute query for listing tables")?;
222
223    // Create a pretty table for display
224    let mut table = Table::new();
225    table.add_row(row!["Table Name"]);
226
227    let mut has_tables = false;
228    let mut table_count = 0;
229    let mut tables = Vec::new();
230
231    for table_name_result in table_names {
232        let name =
233            table_name_result.with_context(|| "Failed to read table name from database result")?;
234        table.add_row(row![&name]);
235        tables.push(name);
236        has_tables = true;
237        table_count += 1;
238    }
239
240    if has_tables {
241        println!("Tables in database '{}':", db_path);
242        table.printstd();
243        println!("Total: {} table(s)", table_count);
244    } else {
245        println!("No tables found in database: {}", db_path);
246        println!("Use 'create-table' command to create your first table.");
247    }
248
249    // Connection will be automatically dropped when it goes out of scope
250    Ok(tables)
251}
252
253/// Create a database connection with retry logic for handling temporary issues
254fn create_connection_with_retry(db_path: &str, max_retries: u32) -> Result<Connection> {
255    let mut last_error = None;
256
257    for attempt in 1..=max_retries {
258        match Connection::open(db_path) {
259            Ok(conn) => {
260                if attempt > 1 {
261                    println!("Connection succeeded on attempt {}", attempt);
262                }
263                return Ok(conn);
264            }
265            Err(e) => {
266                last_error = Some(e);
267                if attempt < max_retries {
268                    println!("Connection attempt {} failed, retrying...", attempt);
269                    std::thread::sleep(Duration::from_millis(100 * attempt as u64));
270                }
271            }
272        }
273    }
274
275    Err(last_error.unwrap())
276        .with_context(|| format!(
277            "Failed to connect to database '{}' after {} attempts. Database may be locked or corrupted.",
278            db_path, max_retries
279        ))
280}
281
282/// Verify database integrity
283fn verify_database_integrity(db_path: &str) -> Result<()> {
284    let conn = Connection::open(db_path)
285        .with_context(|| format!("Cannot open database '{}' for integrity check", db_path))?;
286
287    // Run a simple integrity check
288    let integrity_result: String = conn
289        .query_row("PRAGMA integrity_check", [], |row| row.get(0))
290        .with_context(|| format!("Database '{}' failed integrity check", db_path))?;
291
292    if integrity_result != "ok" {
293        anyhow::bail!("Database integrity check failed: {}", integrity_result);
294    }
295
296    // Test basic functionality
297    let test_result: i32 = conn
298        .query_row("SELECT 1", [], |row| row.get(0))
299        .with_context(|| format!("Database '{}' failed basic functionality test", db_path))?;
300
301    if test_result != 1 {
302        anyhow::bail!(
303            "Basic functionality test failed: expected 1, got {}",
304            test_result
305        );
306    }
307
308    Ok(())
309}
310
311/// Check if a table exists in the database
312fn check_table_exists(conn: &Connection, table_name: &str) -> Result<bool> {
313    let mut stmt = conn
314        .prepare("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?1")
315        .context("Failed to prepare table existence check query")?;
316
317    let count: i64 = stmt
318        .query_row(params![table_name], |row| row.get(0))
319        .with_context(|| format!("Failed to check if table '{}' exists", table_name))?;
320
321    Ok(count > 0)
322}
323
324/// Validate column definition syntax
325fn validate_column_syntax(columns: &str) -> Result<()> {
326    let columns = columns.trim();
327
328    // Check for basic SQL injection patterns
329    let dangerous_patterns = ["DROP", "DELETE", "INSERT", "UPDATE", "EXEC"];
330    let columns_upper = columns.to_uppercase();
331
332    for pattern in &dangerous_patterns {
333        if columns_upper.contains(pattern) {
334            anyhow::bail!(
335                "Column definition contains potentially dangerous SQL keyword: {}",
336                pattern
337            );
338        }
339    }
340
341    // Check for balanced parentheses
342    let open_parens = columns.chars().filter(|&c| c == '(').count();
343    let close_parens = columns.chars().filter(|&c| c == ')').count();
344
345    if open_parens != close_parens {
346        anyhow::bail!("Column definition has unbalanced parentheses");
347    }
348
349    // Check for at least one column definition
350    if !columns.contains(' ') && !columns.contains(',') {
351        anyhow::bail!(
352            "Column definition appears incomplete. Example: 'id INTEGER PRIMARY KEY, name TEXT'"
353        );
354    }
355
356    Ok(())
357}