1pub mod bookmarks;
2pub mod db;
3pub mod display;
4pub mod export;
5pub mod transactions;
6pub mod repl;
7pub mod shell;
8pub mod populate;
9
10pub use repl::repl_mode;
12
13pub use db::{init_database, connect_database, create_table, list_tables};
15
16pub use display::{execute_sql, show_table_schema, show_all_schemas, show_database_info, OutputFormat, QueryOptions};
18
19pub use export::export_to_csv;
21
22pub use shell::Shell;
24
25pub use bookmarks::{BookmarkManager, Bookmark};
27
28pub use transactions::{TransactionManager, TransactionState};
30
31pub use populate::{populate_database, PopulationConfig, ColumnConfig, DataType, DataDistribution};
33
34pub use rusqlite::Connection;
36pub use anyhow::Result;
37
38pub struct VaporDB {
43 pub connection: Connection,
44 pub db_path: String,
45 pub bookmark_manager: Option<BookmarkManager>,
46 pub transaction_manager: TransactionManager,
47}
48
49impl VaporDB {
50 pub fn open<P: AsRef<std::path::Path>>(db_path: P) -> Result<Self> {
52 let db_path_str = db_path.as_ref().to_string_lossy().to_string();
53 let connection = Connection::open(&db_path_str)?;
54
55 let bookmark_manager = BookmarkManager::new().ok();
56 let transaction_manager = TransactionManager::new();
57
58 Ok(VaporDB {
59 connection,
60 db_path: db_path_str,
61 bookmark_manager,
62 transaction_manager,
63 })
64 }
65
66 pub fn create<P: AsRef<std::path::Path>>(db_path: P) -> Result<Self> {
68 let db_path_str = db_path.as_ref().to_string_lossy().to_string();
69 init_database(&db_path_str)?;
70 Self::open(db_path)
71 }
72
73 pub fn execute(&self, sql: &str) -> Result<()> {
75 let options = QueryOptions::default();
76 execute_sql(&self.connection, sql, &options)
77 }
78
79 pub fn execute_with_options(&self, sql: &str, options: &QueryOptions) -> Result<()> {
81 execute_sql(&self.connection, sql, options)
82 }
83
84 pub fn list_tables(&self) -> Result<Vec<String>> {
86 list_tables(&self.db_path)
87 }
88
89 pub fn show_table_schema(&self, table_name: &str) -> Result<()> {
91 show_table_schema(&self.connection, table_name)
92 }
93
94 pub fn show_all_schemas(&self) -> Result<()> {
96 show_all_schemas(&self.connection)
97 }
98
99 pub fn show_database_info(&self) -> Result<()> {
101 show_database_info(&self.connection, &self.db_path)
102 }
103
104 pub fn export_to_csv(&self, table_name: &str, file_path: &str) -> Result<()> {
106 let query = format!("SELECT * FROM {}", table_name);
107 export_to_csv(&self.connection, &query, file_path)
108 }
109
110 pub fn export_query_to_csv(&self, query: &str, file_path: &str) -> Result<()> {
112 export_to_csv(&self.connection, query, file_path)
113 }
114
115 pub fn start_repl(&self) -> Result<()> {
117 repl_mode(&self.db_path)
118 }
119
120 pub fn start_shell(&self) -> Result<()> {
122 let mut shell = Shell::new();
123 shell.run();
124 Ok(())
125 }
126
127 pub fn populate_with_test_data(&self, config: Option<PopulationConfig>) -> Result<()> {
129 populate_database(&self.db_path, config)
130 }
131
132 pub fn begin_transaction(&self) -> Result<()> {
134 self.transaction_manager.begin_transaction(&self.connection)
135 }
136
137 pub fn commit_transaction(&self) -> Result<()> {
139 self.transaction_manager.commit_transaction(&self.connection)
140 }
141
142 pub fn rollback_transaction(&self) -> Result<()> {
144 self.transaction_manager.rollback_transaction(&self.connection)
145 }
146
147 pub fn is_transaction_active(&self) -> bool {
149 self.transaction_manager.is_active()
150 }
151
152 pub fn bookmark_manager(&mut self) -> Option<&mut BookmarkManager> {
154 self.bookmark_manager.as_mut()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use tempfile::NamedTempFile;
162
163 #[test]
164 fn test_list_tables() {
165 let temp_db = NamedTempFile::new().unwrap();
166 let db_path = temp_db.path().to_str().unwrap();
167
168 let conn = rusqlite::Connection::open(db_path).unwrap();
170 conn.execute(
171 "CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)",
172 [],
173 ).unwrap();
174
175 let tables = list_tables(db_path).unwrap();
177 assert!(tables.contains(&"test_table".to_string()));
178 }
179
180 #[test]
181 fn test_execute_sql() {
182 let temp_db = NamedTempFile::new().unwrap();
183 let db_path = temp_db.path().to_str().unwrap();
184
185 let conn = rusqlite::Connection::open(db_path).unwrap();
186 conn.execute(
187 "CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)",
188 [],
189 ).unwrap();
190
191 execute_sql(&conn, "INSERT INTO test_table (name) VALUES ('test')", &QueryOptions::default()).unwrap();
193
194 execute_sql(&conn, "SELECT id, name FROM test_table", &QueryOptions::default()).unwrap();
196 }
197
198 #[test]
199 fn test_show_table_schema() {
200 let temp_db = NamedTempFile::new().unwrap();
201 let db_path = temp_db.path().to_str().unwrap();
202
203 let conn = rusqlite::Connection::open(db_path).unwrap();
204 conn.execute(
205 "CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)",
206 [],
207 ).unwrap();
208
209 show_table_schema(&conn, "test_table").unwrap();
211 }
212
213 #[test]
214 fn test_show_all_schemas() {
215 let temp_db = NamedTempFile::new().unwrap();
216 let db_path = temp_db.path().to_str().unwrap();
217
218 let conn = rusqlite::Connection::open(db_path).unwrap();
219 conn.execute(
220 "CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)",
221 [],
222 ).unwrap();
223
224 show_all_schemas(&conn).unwrap();
226 }
227
228 #[test]
229 fn test_show_database_info() {
230 let temp_db = NamedTempFile::new().unwrap();
231 let db_path = temp_db.path().to_str().unwrap();
232
233 let conn = rusqlite::Connection::open(db_path).unwrap();
234 conn.execute(
235 "CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)",
236 [],
237 ).unwrap();
238
239 show_database_info(&conn, db_path).unwrap();
241 }
242
243 #[test]
244 fn test_vapor_db_create_and_open() {
245 let temp_db = NamedTempFile::new().unwrap();
246 let db_path = temp_db.path();
247
248 let vapor_db = VaporDB::create(db_path).unwrap();
250 assert_eq!(vapor_db.db_path, db_path.to_string_lossy());
251
252 let vapor_db2 = VaporDB::open(db_path).unwrap();
254 assert_eq!(vapor_db2.db_path, db_path.to_string_lossy());
255 }
256
257 #[test]
258 fn test_vapor_db_execute() {
259 let temp_db = NamedTempFile::new().unwrap();
260 let db_path = temp_db.path();
261
262 let vapor_db = VaporDB::create(db_path).unwrap();
263
264 vapor_db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)").unwrap();
266 vapor_db.execute("INSERT INTO test (name) VALUES ('test_value')").unwrap();
267
268 let tables = vapor_db.list_tables().unwrap();
270 assert!(tables.contains(&"test".to_string()));
271 }
272
273 #[test]
274 fn test_vapor_db_transactions() {
275 let temp_db = NamedTempFile::new().unwrap();
276 let db_path = temp_db.path();
277
278 let vapor_db = VaporDB::create(db_path).unwrap();
279 vapor_db.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)").unwrap();
280
281 assert!(!vapor_db.is_transaction_active());
283 vapor_db.begin_transaction().unwrap();
284 assert!(vapor_db.is_transaction_active());
285
286 vapor_db.execute("INSERT INTO test (name) VALUES ('test_transaction')").unwrap();
287 vapor_db.commit_transaction().unwrap();
288 assert!(!vapor_db.is_transaction_active());
289 }
290
291 #[test]
292 fn test_bookmark_manager() {
293 let temp_db = NamedTempFile::new().unwrap();
294 let db_path = temp_db.path();
295
296 let mut vapor_db = VaporDB::create(db_path).unwrap();
297
298 if let Some(bookmark_manager) = vapor_db.bookmark_manager() {
300 bookmark_manager.save_bookmark(
301 "test_bookmark".to_string(),
302 "SELECT * FROM test".to_string(),
303 Some("Test bookmark".to_string())
304 ).unwrap();
305
306 let bookmark = bookmark_manager.get_bookmark("test_bookmark");
307 assert!(bookmark.is_some());
308 assert_eq!(bookmark.unwrap().query, "SELECT * FROM test");
309 }
310 }
311
312 #[test]
313 fn test_init_database() {
314 let temp_dir = tempfile::tempdir().unwrap();
315 let db_path = temp_dir.path().join("new_test.db");
316 let db_path_str = db_path.to_str().unwrap();
317
318 init_database(db_path_str).unwrap();
320 assert!(db_path.exists());
321
322 init_database(db_path_str).unwrap();
324 }
325
326 #[test]
327 fn test_create_table_function() {
328 let temp_db = NamedTempFile::new().unwrap();
329 let db_path = temp_db.path().to_str().unwrap();
330
331 init_database(db_path).unwrap();
333
334 create_table(db_path, "users", "id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT").unwrap();
336
337 let tables = list_tables(db_path).unwrap();
339 assert!(tables.contains(&"users".to_string()));
340
341 create_table(db_path, "users", "id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT").unwrap();
343 }
344
345 #[test]
346 fn test_output_formats() {
347 let temp_db = NamedTempFile::new().unwrap();
348 let db_path = temp_db.path().to_str().unwrap();
349
350 let conn = rusqlite::Connection::open(db_path).unwrap();
351 conn.execute(
352 "CREATE TABLE test_output (id INTEGER PRIMARY KEY, name TEXT, value REAL)",
353 [],
354 ).unwrap();
355 conn.execute(
356 "INSERT INTO test_output (name, value) VALUES ('test1', 10.5), ('test2', 20.7)",
357 [],
358 ).unwrap();
359
360 let table_options = QueryOptions {
362 format: OutputFormat::Table,
363 ..Default::default()
364 };
365 execute_sql(&conn, "SELECT * FROM test_output", &table_options).unwrap();
366
367 let csv_options = QueryOptions {
368 format: OutputFormat::Csv,
369 ..Default::default()
370 };
371 execute_sql(&conn, "SELECT * FROM test_output", &csv_options).unwrap();
372
373 let json_options = QueryOptions {
374 format: OutputFormat::Json,
375 ..Default::default()
376 };
377 execute_sql(&conn, "SELECT * FROM test_output", &json_options).unwrap();
378 }
379
380 #[test]
381 fn test_export_functionality() {
382 let temp_db = NamedTempFile::new().unwrap();
383 let db_path = temp_db.path().to_str().unwrap();
384
385 let conn = rusqlite::Connection::open(db_path).unwrap();
386 conn.execute(
387 "CREATE TABLE export_test (id INTEGER PRIMARY KEY, name TEXT, value REAL)",
388 [],
389 ).unwrap();
390 conn.execute(
391 "INSERT INTO export_test (name, value) VALUES ('item1', 100.5), ('item2', 200.7)",
392 [],
393 ).unwrap();
394
395 let temp_csv = tempfile::NamedTempFile::new().unwrap();
397 let csv_path = temp_csv.path().to_str().unwrap();
398
399 export_to_csv(&conn, "SELECT * FROM export_test", csv_path).unwrap();
400
401 let csv_content = std::fs::read_to_string(csv_path).unwrap();
403 assert!(csv_content.contains("id,name,value"));
404 assert!(csv_content.contains("item1"));
405 assert!(csv_content.contains("item2"));
406 }
407
408 #[test]
409 fn test_vapor_db_export_methods() {
410 let temp_db = NamedTempFile::new().unwrap();
411 let db_path = temp_db.path();
412
413 let vapor_db = VaporDB::create(db_path).unwrap();
414 vapor_db.execute("CREATE TABLE export_methods_test (id INTEGER PRIMARY KEY, name TEXT)").unwrap();
415 vapor_db.execute("INSERT INTO export_methods_test (name) VALUES ('method1'), ('method2')").unwrap();
416
417 let temp_csv1 = tempfile::NamedTempFile::new().unwrap();
419 let csv_path1 = temp_csv1.path().to_str().unwrap();
420 vapor_db.export_to_csv("export_methods_test", csv_path1).unwrap();
421
422 let csv_content1 = std::fs::read_to_string(csv_path1).unwrap();
423 assert!(csv_content1.contains("id,name"));
424 assert!(csv_content1.contains("method1"));
425
426 let temp_csv2 = tempfile::NamedTempFile::new().unwrap();
428 let csv_path2 = temp_csv2.path().to_str().unwrap();
429 vapor_db.export_query_to_csv("SELECT name FROM export_methods_test WHERE name = 'method2'", csv_path2).unwrap();
430
431 let csv_content2 = std::fs::read_to_string(csv_path2).unwrap();
432 assert!(csv_content2.contains("name"));
433 assert!(csv_content2.contains("method2"));
434 assert!(!csv_content2.contains("method1")); }
436
437 #[test]
438 fn test_vapor_db_with_options() {
439 let temp_db = NamedTempFile::new().unwrap();
440 let db_path = temp_db.path();
441
442 let vapor_db = VaporDB::create(db_path).unwrap();
443 vapor_db.execute("CREATE TABLE options_test (id INTEGER PRIMARY KEY, data TEXT)").unwrap();
444 vapor_db.execute("INSERT INTO options_test (data) VALUES ('test1'), ('test2'), ('test3')").unwrap();
445
446 let options = QueryOptions {
448 format: OutputFormat::Json,
449 show_timing: true,
450 max_rows: Some(2),
451 };
452 vapor_db.execute_with_options("SELECT * FROM options_test", &options).unwrap();
453 }
454
455 #[test]
456 fn test_transaction_rollback() {
457 let temp_db = NamedTempFile::new().unwrap();
458 let db_path = temp_db.path();
459
460 let vapor_db = VaporDB::create(db_path).unwrap();
461 vapor_db.execute("CREATE TABLE rollback_test (id INTEGER PRIMARY KEY, name TEXT)").unwrap();
462 vapor_db.execute("INSERT INTO rollback_test (name) VALUES ('initial')").unwrap();
463
464 vapor_db.begin_transaction().unwrap();
466 vapor_db.execute("INSERT INTO rollback_test (name) VALUES ('transactional')").unwrap();
467
468 vapor_db.rollback_transaction().unwrap();
470 assert!(!vapor_db.is_transaction_active());
471
472 }
475
476 #[test]
477 fn test_schema_functions() {
478 let temp_db = NamedTempFile::new().unwrap();
479 let db_path = temp_db.path();
480
481 let vapor_db = VaporDB::create(db_path).unwrap();
482 vapor_db.execute("CREATE TABLE schema_test (id INTEGER PRIMARY KEY, name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)").unwrap();
483
484 vapor_db.show_table_schema("schema_test").unwrap();
486 vapor_db.show_all_schemas().unwrap();
487 vapor_db.show_database_info().unwrap();
488 }
489
490 #[test]
491 fn test_bookmark_operations() {
492 let temp_db = NamedTempFile::new().unwrap();
493 let db_path = temp_db.path();
494
495 let temp_dir = tempfile::tempdir().unwrap();
497 let original_home = std::env::var("HOME").ok();
498 std::env::set_var("HOME", temp_dir.path());
499
500 let mut vapor_db = VaporDB::create(db_path).unwrap();
501
502 if let Some(bookmark_manager) = vapor_db.bookmark_manager() {
503 bookmark_manager.save_bookmark(
505 "query1".to_string(),
506 "SELECT * FROM users".to_string(),
507 Some("Get all users".to_string())
508 ).unwrap();
509
510 bookmark_manager.save_bookmark(
511 "query2".to_string(),
512 "SELECT COUNT(*) FROM users".to_string(),
513 Some("Count users".to_string())
514 ).unwrap();
515
516 let bookmark1 = bookmark_manager.get_bookmark("query1");
518 assert!(bookmark1.is_some());
519 assert_eq!(bookmark1.unwrap().query, "SELECT * FROM users");
520
521 let bookmark2 = bookmark_manager.get_bookmark("query2");
522 assert!(bookmark2.is_some());
523 assert_eq!(bookmark2.unwrap().query, "SELECT COUNT(*) FROM users");
524
525 let deleted = bookmark_manager.delete_bookmark("query1").unwrap();
527 assert!(deleted);
528
529 let bookmark1_after_delete = bookmark_manager.get_bookmark("query1");
530 assert!(bookmark1_after_delete.is_none());
531
532 let not_deleted = bookmark_manager.delete_bookmark("non_existent").unwrap();
534 assert!(!not_deleted);
535 }
536
537 if let Some(home) = original_home {
539 std::env::set_var("HOME", home);
540 } else {
541 std::env::remove_var("HOME");
542 }
543 }
544
545 #[test]
546 fn test_population_config() {
547 let default_config = PopulationConfig::default();
549 assert_eq!(default_config.table_name, "large_table");
550 assert_eq!(default_config.row_count, 1_000_000);
551 assert_eq!(default_config.batch_size, 10_000);
552 assert_eq!(default_config.columns.len(), 3);
553
554 let custom_config = PopulationConfig {
556 table_name: "test_table".to_string(),
557 row_count: 1000,
558 batch_size: 100,
559 seed: Some(42),
560 columns: vec![
561 ColumnConfig {
562 name: "id".to_string(),
563 data_type: DataType::Integer,
564 distribution: DataDistribution::Sequential,
565 nullable: false,
566 },
567 ColumnConfig {
568 name: "name".to_string(),
569 data_type: DataType::Text,
570 distribution: DataDistribution::Random,
571 nullable: true,
572 },
573 ],
574 };
575
576 assert_eq!(custom_config.table_name, "test_table");
577 assert_eq!(custom_config.row_count, 1000);
578 assert_eq!(custom_config.seed, Some(42));
579 }
580
581 #[test]
582 fn test_data_types_and_distributions() {
583 let _int_type = DataType::Integer;
585 let _text_type = DataType::Text;
586 let _real_type = DataType::Real;
587 let _bool_type = DataType::Boolean;
588 let _date_type = DataType::Date;
589 let _timestamp_type = DataType::Timestamp;
590 let _uuid_type = DataType::UUID;
591
592 let _uniform = DataDistribution::Uniform;
594 let _normal = DataDistribution::Normal { mean: 50.0, std_dev: 10.0 };
595 let _sequential = DataDistribution::Sequential;
596 let _random = DataDistribution::Random;
597 let _custom = DataDistribution::Custom(vec!["value1".to_string(), "value2".to_string()]);
598 }
599
600 #[test]
601 fn test_error_handling() {
602 let temp_db = NamedTempFile::new().unwrap();
603 let db_path = temp_db.path();
604
605 let vapor_db = VaporDB::create(db_path).unwrap();
606
607 let result = vapor_db.execute("INVALID SQL STATEMENT");
609 assert!(result.is_err());
610
611 let result = vapor_db.execute("SELECT * FROM non_existent_table");
613 assert!(result.is_err());
614 }
615
616 #[test]
617 fn test_integration_workflow() {
618 let temp_db = NamedTempFile::new().unwrap();
619 let db_path = temp_db.path();
620
621 let temp_dir = tempfile::tempdir().unwrap();
623 let original_home = std::env::var("HOME").ok();
624 std::env::set_var("HOME", temp_dir.path());
625
626 let mut vapor_db = VaporDB::create(db_path).unwrap();
628
629 vapor_db.execute("CREATE TABLE workflow_test (id INTEGER PRIMARY KEY, name TEXT, score REAL)").unwrap();
631
632 vapor_db.begin_transaction().unwrap();
634 vapor_db.execute("INSERT INTO workflow_test (name, score) VALUES ('Alice', 95.5)").unwrap();
635 vapor_db.execute("INSERT INTO workflow_test (name, score) VALUES ('Bob', 87.2)").unwrap();
636 vapor_db.commit_transaction().unwrap();
637
638 let tables = vapor_db.list_tables().unwrap();
640 assert!(tables.contains(&"workflow_test".to_string()));
641
642 let temp_csv = tempfile::NamedTempFile::new().unwrap();
644 let csv_path = temp_csv.path().to_str().unwrap();
645 vapor_db.export_to_csv("workflow_test", csv_path).unwrap();
646
647 if let Some(bm) = vapor_db.bookmark_manager() {
649 bm.save_bookmark(
650 "high_scores".to_string(),
651 "SELECT * FROM workflow_test WHERE score > 90".to_string(),
652 Some("Students with high scores".to_string())
653 ).unwrap();
654 }
655
656 assert!(!vapor_db.is_transaction_active());
658 assert!(std::path::Path::new(csv_path).exists());
659
660 if let Some(home) = original_home {
662 std::env::set_var("HOME", home);
663 } else {
664 std::env::remove_var("HOME");
665 }
666 }
667}