1use anyhow::{Context, Result};
9use prettytable::{row, Table};
10use rusqlite::{params, Connection};
11use std::fs;
12use std::path::Path;
13use std::time::Duration;
14
15pub 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 if Path::new(&db_path).exists() {
40 println!("Database '{}' already exists.", db_path);
41 verify_database_integrity(&db_path)?;
43 return Ok(());
44 }
45
46 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 let _conn = create_connection_with_retry(&db_path, 3)?;
61
62 verify_database_integrity(&db_path)?;
64
65 println!("Successfully created database: {}", db_path);
66 Ok(())
69}
70
71pub fn connect_database(path: &str) -> Result<()> {
87 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 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 let _conn = create_connection_with_retry(path, 3)?;
106
107 verify_database_integrity(path)?;
109
110 println!("Successfully connected to database: {}", path);
111 Ok(())
114}
115
116pub fn create_table(db_path: &str, table_name: &str, columns: &str) -> Result<()> {
135 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 let conn = create_connection_with_retry(db_path, 3)?;
146
147 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_syntax(columns)?;
159
160 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 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 Ok(())
187}
188
189pub fn list_tables(db_path: &str) -> Result<Vec<String>> {
202 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 let conn = create_connection_with_retry(db_path, 3)?;
213
214 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 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 Ok(tables)
251}
252
253fn 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
282fn 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 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 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
311fn 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
324fn validate_column_syntax(columns: &str) -> Result<()> {
326 let columns = columns.trim();
327
328 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 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 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}