1pub mod bookmarks;
28pub mod config;
29pub mod db;
30pub mod display;
31pub mod export;
32pub mod populate;
33pub mod repl;
34pub mod shell;
35pub mod transactions;
36
37pub use crate::repl::repl_mode;
39use crate::shell::shell_mode;
40
41pub use db::{connect_database, create_table, init_database, list_tables};
43
44pub use display::{
46 execute_sql, show_all_schemas, show_database_info, show_table_schema, OutputFormat,
47 QueryOptions,
48};
49
50pub use export::export_to_csv;
52
53pub use shell::Shell;
55
56pub use bookmarks::{Bookmark, BookmarkManager};
58
59pub use transactions::{TransactionManager, TransactionState};
61
62pub use populate::{populate_database, ColumnConfig, DataDistribution, DataType, PopulationConfig};
64
65pub use anyhow::Result;
67pub use rusqlite::Connection;
68
69pub struct VaporDB {
74 pub connection: Connection,
75 pub db_path: String,
76 pub bookmark_manager: Option<BookmarkManager>,
77 pub transaction_manager: TransactionManager,
78}
79
80impl VaporDB {
81 pub fn open<P: AsRef<std::path::Path>>(db_path: P) -> Result<Self> {
83 let db_path_str = db_path.as_ref().to_string_lossy().to_string();
84 let connection = Connection::open(&db_path_str)?;
85
86 let bookmark_manager = BookmarkManager::new().ok();
87 let transaction_manager = TransactionManager::new();
88
89 Ok(VaporDB {
90 connection,
91 db_path: db_path_str,
92 bookmark_manager,
93 transaction_manager,
94 })
95 }
96
97 pub fn create<P: AsRef<std::path::Path>>(db_path: P) -> Result<Self> {
99 let db_path_str = db_path.as_ref().to_string_lossy().to_string();
100 init_database(&db_path_str)?;
101 Self::open(db_path)
102 }
103
104 pub fn execute(&self, sql: &str) -> Result<()> {
106 let options = QueryOptions::default();
107 execute_sql(&self.connection, sql, &options)
108 }
109
110 pub fn execute_with_options(&self, sql: &str, options: &QueryOptions) -> Result<()> {
112 execute_sql(&self.connection, sql, options)
113 }
114
115 pub fn list_tables(&self) -> Result<Vec<String>> {
117 list_tables(&self.db_path)
118 }
119
120 pub fn show_table_schema(&self, table_name: &str) -> Result<()> {
122 show_table_schema(&self.connection, table_name)
123 }
124
125 pub fn show_all_schemas(&self) -> Result<()> {
127 show_all_schemas(&self.connection)
128 }
129
130 pub fn show_database_info(&self) -> Result<()> {
132 show_database_info(&self.connection, &self.db_path)
133 }
134
135 pub fn export_to_csv(&self, table_name: &str, file_path: &str) -> Result<()> {
137 let query = format!("SELECT * FROM {}", table_name);
138 export_to_csv(&self.connection, &query, file_path)
139 }
140
141 pub fn export_query_to_csv(&self, query: &str, file_path: &str) -> Result<()> {
143 export_to_csv(&self.connection, query, file_path)
144 }
145
146 pub fn start_repl(&self) -> Result<()> {
148 repl_mode(&self.db_path)
149 }
150
151 pub fn start_shell(&self) -> Result<()> {
153 shell_mode(&self.db_path).map(|_| ())
154 }
155
156 pub fn populate_with_test_data(&self, config: Option<PopulationConfig>) -> Result<()> {
158 populate_database(&self.db_path, config)
159 }
160
161 pub fn begin_transaction(&self) -> Result<()> {
163 self.transaction_manager.begin_transaction(&self.connection)
164 }
165
166 pub fn commit_transaction(&self) -> Result<()> {
168 self.transaction_manager
169 .commit_transaction(&self.connection)
170 }
171
172 pub fn rollback_transaction(&self) -> Result<()> {
174 self.transaction_manager
175 .rollback_transaction(&self.connection)
176 }
177
178 pub fn is_transaction_active(&self) -> bool {
180 self.transaction_manager.is_active()
181 }
182
183 pub fn bookmark_manager(&mut self) -> Option<&mut BookmarkManager> {
185 self.bookmark_manager.as_mut()
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use tempfile::NamedTempFile;
193
194 #[test]
195 fn test_list_tables() {
196 let temp_db = NamedTempFile::new().unwrap();
197 let db_path = temp_db.path().to_str().unwrap();
198
199 let conn = rusqlite::Connection::open(db_path).unwrap();
201 conn.execute(
202 "CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)",
203 [],
204 )
205 .unwrap();
206
207 let tables = list_tables(db_path).unwrap();
209 assert!(tables.contains(&"test_table".to_string()));
210 }
211
212 #[test]
213 fn test_execute_sql() {
214 let temp_db = NamedTempFile::new().unwrap();
215 let db_path = temp_db.path().to_str().unwrap();
216
217 let conn = rusqlite::Connection::open(db_path).unwrap();
218 conn.execute(
219 "CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)",
220 [],
221 )
222 .unwrap();
223
224 execute_sql(
226 &conn,
227 "INSERT INTO test_table (name) VALUES ('test')",
228 &QueryOptions::default(),
229 )
230 .unwrap();
231
232 execute_sql(
234 &conn,
235 "SELECT id, name FROM test_table",
236 &QueryOptions::default(),
237 )
238 .unwrap();
239 }
240
241 #[test]
242 fn test_show_table_schema() {
243 let temp_db = NamedTempFile::new().unwrap();
244 let db_path = temp_db.path().to_str().unwrap();
245
246 let conn = rusqlite::Connection::open(db_path).unwrap();
247 conn.execute(
248 "CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)",
249 [],
250 )
251 .unwrap();
252
253 show_table_schema(&conn, "test_table").unwrap();
255 }
256
257 #[test]
258 fn test_show_all_schemas() {
259 let temp_db = NamedTempFile::new().unwrap();
260 let db_path = temp_db.path().to_str().unwrap();
261
262 let conn = rusqlite::Connection::open(db_path).unwrap();
263 conn.execute(
264 "CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)",
265 [],
266 )
267 .unwrap();
268
269 show_all_schemas(&conn).unwrap();
271 }
272
273 #[test]
274 fn test_show_database_info() {
275 let temp_db = NamedTempFile::new().unwrap();
276 let db_path = temp_db.path().to_str().unwrap();
277
278 let conn = rusqlite::Connection::open(db_path).unwrap();
279 conn.execute(
280 "CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)",
281 [],
282 )
283 .unwrap();
284
285 show_database_info(&conn, db_path).unwrap();
287 }
288
289 #[test]
290 fn test_vapor_db_create_and_open() {
291 let temp_db = NamedTempFile::new().unwrap();
292 let db_path = temp_db.path();
293
294 let vapor_db = VaporDB::create(db_path).unwrap();
296 assert_eq!(vapor_db.db_path, db_path.to_string_lossy());
297
298 let vapor_db2 = VaporDB::open(db_path).unwrap();
300 assert_eq!(vapor_db2.db_path, db_path.to_string_lossy());
301 }
302
303 #[test]
304 fn test_vapor_db_execute() {
305 let temp_db = NamedTempFile::new().unwrap();
306 let db_path = temp_db.path();
307
308 let vapor_db = VaporDB::create(db_path).unwrap();
309
310 vapor_db
312 .execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
313 .unwrap();
314 vapor_db
315 .execute("INSERT INTO test (name) VALUES ('test_value')")
316 .unwrap();
317
318 let tables = vapor_db.list_tables().unwrap();
320 assert!(tables.contains(&"test".to_string()));
321 }
322
323 #[test]
324 fn test_vapor_db_transactions() {
325 let temp_db = NamedTempFile::new().unwrap();
326 let db_path = temp_db.path();
327
328 let vapor_db = VaporDB::create(db_path).unwrap();
329 vapor_db
330 .execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
331 .unwrap();
332
333 assert!(!vapor_db.is_transaction_active());
335 vapor_db.begin_transaction().unwrap();
336 assert!(vapor_db.is_transaction_active());
337
338 vapor_db
339 .execute("INSERT INTO test (name) VALUES ('test_transaction')")
340 .unwrap();
341 vapor_db.commit_transaction().unwrap();
342 assert!(!vapor_db.is_transaction_active());
343 }
344
345 #[test]
346 fn test_bookmark_manager() {
347 let temp_db = NamedTempFile::new().unwrap();
348 let db_path = temp_db.path();
349
350 let mut vapor_db = VaporDB::create(db_path).unwrap();
351
352 if let Some(bookmark_manager) = vapor_db.bookmark_manager() {
354 bookmark_manager
355 .save_bookmark(
356 "test_bookmark".to_string(),
357 "SELECT * FROM test".to_string(),
358 Some("Test bookmark".to_string()),
359 )
360 .unwrap();
361
362 let bookmark = bookmark_manager.get_bookmark("test_bookmark");
363 assert!(bookmark.is_some());
364 assert_eq!(bookmark.unwrap().query, "SELECT * FROM test");
365 }
366 }
367
368 #[test]
369 fn test_init_database() {
370 let temp_dir = tempfile::tempdir().unwrap();
371 let db_path = temp_dir.path().join("new_test.db");
372 let db_path_str = db_path.to_str().unwrap();
373
374 init_database(db_path_str).unwrap();
376 assert!(db_path.exists());
377
378 init_database(db_path_str).unwrap();
380 }
381
382 #[test]
383 fn test_create_table_function() {
384 let temp_db = NamedTempFile::new().unwrap();
385 let db_path = temp_db.path().to_str().unwrap();
386
387 init_database(db_path).unwrap();
389
390 create_table(
392 db_path,
393 "users",
394 "id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT",
395 )
396 .unwrap();
397
398 let tables = list_tables(db_path).unwrap();
400 assert!(tables.contains(&"users".to_string()));
401
402 create_table(
404 db_path,
405 "users",
406 "id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT",
407 )
408 .unwrap();
409 }
410
411 #[test]
412 fn test_output_formats() {
413 let temp_db = NamedTempFile::new().unwrap();
414 let db_path = temp_db.path().to_str().unwrap();
415
416 let conn = rusqlite::Connection::open(db_path).unwrap();
417 conn.execute(
418 "CREATE TABLE test_output (id INTEGER PRIMARY KEY, name TEXT, value REAL)",
419 [],
420 )
421 .unwrap();
422 conn.execute(
423 "INSERT INTO test_output (name, value) VALUES ('test1', 10.5), ('test2', 20.7)",
424 [],
425 )
426 .unwrap();
427
428 let table_options = QueryOptions {
430 format: OutputFormat::Table,
431 ..Default::default()
432 };
433 execute_sql(&conn, "SELECT * FROM test_output", &table_options).unwrap();
434
435 let csv_options = QueryOptions {
436 format: OutputFormat::Csv,
437 ..Default::default()
438 };
439 execute_sql(&conn, "SELECT * FROM test_output", &csv_options).unwrap();
440
441 let json_options = QueryOptions {
442 format: OutputFormat::Json,
443 ..Default::default()
444 };
445 execute_sql(&conn, "SELECT * FROM test_output", &json_options).unwrap();
446 }
447
448 #[test]
449 fn test_export_functionality() {
450 let temp_db = NamedTempFile::new().unwrap();
451 let db_path = temp_db.path().to_str().unwrap();
452
453 let conn = rusqlite::Connection::open(db_path).unwrap();
454 conn.execute(
455 "CREATE TABLE export_test (id INTEGER PRIMARY KEY, name TEXT, value REAL)",
456 [],
457 )
458 .unwrap();
459 conn.execute(
460 "INSERT INTO export_test (name, value) VALUES ('item1', 100.5), ('item2', 200.7)",
461 [],
462 )
463 .unwrap();
464
465 let temp_csv = tempfile::NamedTempFile::new().unwrap();
467 let csv_path = temp_csv.path().to_str().unwrap();
468
469 export_to_csv(&conn, "SELECT * FROM export_test", csv_path).unwrap();
470
471 let csv_content = std::fs::read_to_string(csv_path).unwrap();
473 assert!(csv_content.contains("id,name,value"));
474 assert!(csv_content.contains("item1"));
475 assert!(csv_content.contains("item2"));
476 }
477
478 #[test]
479 fn test_vapor_db_export_methods() {
480 let temp_db = NamedTempFile::new().unwrap();
481 let db_path = temp_db.path();
482
483 let vapor_db = VaporDB::create(db_path).unwrap();
484 vapor_db
485 .execute("CREATE TABLE export_methods_test (id INTEGER PRIMARY KEY, name TEXT)")
486 .unwrap();
487 vapor_db
488 .execute("INSERT INTO export_methods_test (name) VALUES ('method1'), ('method2')")
489 .unwrap();
490
491 let temp_csv1 = tempfile::NamedTempFile::new().unwrap();
493 let csv_path1 = temp_csv1.path().to_str().unwrap();
494 vapor_db
495 .export_to_csv("export_methods_test", csv_path1)
496 .unwrap();
497
498 let csv_content1 = std::fs::read_to_string(csv_path1).unwrap();
499 assert!(csv_content1.contains("id,name"));
500 assert!(csv_content1.contains("method1"));
501
502 let temp_csv2 = tempfile::NamedTempFile::new().unwrap();
504 let csv_path2 = temp_csv2.path().to_str().unwrap();
505 vapor_db
506 .export_query_to_csv(
507 "SELECT name FROM export_methods_test WHERE name = 'method2'",
508 csv_path2,
509 )
510 .unwrap();
511
512 let csv_content2 = std::fs::read_to_string(csv_path2).unwrap();
513 assert!(csv_content2.contains("name"));
514 assert!(csv_content2.contains("method2"));
515 assert!(!csv_content2.contains("method1")); }
517
518 #[test]
519 fn test_vapor_db_with_options() {
520 let temp_db = NamedTempFile::new().unwrap();
521 let db_path = temp_db.path();
522
523 let vapor_db = VaporDB::create(db_path).unwrap();
524 vapor_db
525 .execute("CREATE TABLE options_test (id INTEGER PRIMARY KEY, data TEXT)")
526 .unwrap();
527 vapor_db
528 .execute("INSERT INTO options_test (data) VALUES ('test1'), ('test2'), ('test3')")
529 .unwrap();
530
531 let options = QueryOptions {
533 format: OutputFormat::Json,
534 show_timing: true,
535 max_rows: Some(2),
536 };
537 vapor_db
538 .execute_with_options("SELECT * FROM options_test", &options)
539 .unwrap();
540 }
541
542 #[test]
543 fn test_transaction_rollback() {
544 let temp_db = NamedTempFile::new().unwrap();
545 let db_path = temp_db.path();
546
547 let vapor_db = VaporDB::create(db_path).unwrap();
548 vapor_db
549 .execute("CREATE TABLE rollback_test (id INTEGER PRIMARY KEY, name TEXT)")
550 .unwrap();
551 vapor_db
552 .execute("INSERT INTO rollback_test (name) VALUES ('initial')")
553 .unwrap();
554
555 vapor_db.begin_transaction().unwrap();
557 vapor_db
558 .execute("INSERT INTO rollback_test (name) VALUES ('transactional')")
559 .unwrap();
560
561 vapor_db.rollback_transaction().unwrap();
563 assert!(!vapor_db.is_transaction_active());
564
565 }
568
569 #[test]
570 fn test_schema_functions() {
571 let temp_db = NamedTempFile::new().unwrap();
572 let db_path = temp_db.path();
573
574 let vapor_db = VaporDB::create(db_path).unwrap();
575 vapor_db.execute("CREATE TABLE schema_test (id INTEGER PRIMARY KEY, name TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)").unwrap();
576
577 vapor_db.show_table_schema("schema_test").unwrap();
579 vapor_db.show_all_schemas().unwrap();
580 vapor_db.show_database_info().unwrap();
581 }
582
583 #[test]
584 fn test_bookmark_operations() {
585 let temp_db = NamedTempFile::new().unwrap();
586 let db_path = temp_db.path();
587
588 let temp_dir = tempfile::tempdir().unwrap();
590 let original_home = std::env::var("HOME").ok();
591 std::env::set_var("HOME", temp_dir.path());
592
593 let mut vapor_db = VaporDB::create(db_path).unwrap();
594
595 if let Some(bookmark_manager) = vapor_db.bookmark_manager() {
596 bookmark_manager
598 .save_bookmark(
599 "query1".to_string(),
600 "SELECT * FROM users".to_string(),
601 Some("Get all users".to_string()),
602 )
603 .unwrap();
604
605 bookmark_manager
606 .save_bookmark(
607 "query2".to_string(),
608 "SELECT COUNT(*) FROM users".to_string(),
609 Some("Count users".to_string()),
610 )
611 .unwrap();
612
613 let bookmark1 = bookmark_manager.get_bookmark("query1");
615 assert!(bookmark1.is_some());
616 assert_eq!(bookmark1.unwrap().query, "SELECT * FROM users");
617
618 let bookmark2 = bookmark_manager.get_bookmark("query2");
619 assert!(bookmark2.is_some());
620 assert_eq!(bookmark2.unwrap().query, "SELECT COUNT(*) FROM users");
621
622 let deleted = bookmark_manager.delete_bookmark("query1").unwrap();
624 assert!(deleted);
625
626 let bookmark1_after_delete = bookmark_manager.get_bookmark("query1");
627 assert!(bookmark1_after_delete.is_none());
628
629 let not_deleted = bookmark_manager.delete_bookmark("non_existent").unwrap();
631 assert!(!not_deleted);
632 }
633
634 if let Some(home) = original_home {
636 std::env::set_var("HOME", home);
637 } else {
638 std::env::remove_var("HOME");
639 }
640 }
641
642 #[test]
643 fn test_population_config() {
644 let default_config = PopulationConfig::default();
646 assert_eq!(default_config.table_name, "large_table");
647 assert_eq!(default_config.row_count, 1_000_000);
648 assert_eq!(default_config.batch_size, 10_000);
649 assert_eq!(default_config.columns.len(), 3);
650
651 let custom_config = PopulationConfig {
653 table_name: "test_table".to_string(),
654 row_count: 1000,
655 batch_size: 100,
656 seed: Some(42),
657 columns: vec![
658 ColumnConfig {
659 name: "id".to_string(),
660 data_type: DataType::Integer,
661 distribution: DataDistribution::Sequential,
662 nullable: false,
663 },
664 ColumnConfig {
665 name: "name".to_string(),
666 data_type: DataType::Text,
667 distribution: DataDistribution::Random,
668 nullable: true,
669 },
670 ],
671 };
672
673 assert_eq!(custom_config.table_name, "test_table");
674 assert_eq!(custom_config.row_count, 1000);
675 assert_eq!(custom_config.seed, Some(42));
676 }
677
678 #[test]
679 fn test_data_types_and_distributions() {
680 let _int_type = DataType::Integer;
682 let _text_type = DataType::Text;
683 let _real_type = DataType::Real;
684 let _bool_type = DataType::Boolean;
685 let _date_type = DataType::Date;
686 let _timestamp_type = DataType::Timestamp;
687 let _uuid_type = DataType::UUID;
688
689 let _uniform = DataDistribution::Uniform;
691 let _normal = DataDistribution::Normal {
692 mean: 50.0,
693 std_dev: 10.0,
694 };
695 let _sequential = DataDistribution::Sequential;
696 let _random = DataDistribution::Random;
697 let _custom = DataDistribution::Custom(vec!["value1".to_string(), "value2".to_string()]);
698 }
699
700 #[test]
701 fn test_error_handling() {
702 let temp_db = NamedTempFile::new().unwrap();
703 let db_path = temp_db.path();
704
705 let vapor_db = VaporDB::create(db_path).unwrap();
706
707 let result = vapor_db.execute("INVALID SQL STATEMENT");
709 assert!(result.is_err());
710
711 let result = vapor_db.execute("SELECT * FROM non_existent_table");
713 assert!(result.is_err());
714 }
715
716 #[test]
717 fn test_integration_workflow() {
718 let temp_db = NamedTempFile::new().unwrap();
719 let db_path = temp_db.path();
720
721 let temp_dir = tempfile::tempdir().unwrap();
723 let original_home = std::env::var("HOME").ok();
724 std::env::set_var("HOME", temp_dir.path());
725
726 let mut vapor_db = VaporDB::create(db_path).unwrap();
728
729 vapor_db
731 .execute("CREATE TABLE workflow_test (id INTEGER PRIMARY KEY, name TEXT, score REAL)")
732 .unwrap();
733
734 vapor_db.begin_transaction().unwrap();
736 vapor_db
737 .execute("INSERT INTO workflow_test (name, score) VALUES ('Alice', 95.5)")
738 .unwrap();
739 vapor_db
740 .execute("INSERT INTO workflow_test (name, score) VALUES ('Bob', 87.2)")
741 .unwrap();
742 vapor_db.commit_transaction().unwrap();
743
744 let tables = vapor_db.list_tables().unwrap();
746 assert!(tables.contains(&"workflow_test".to_string()));
747
748 let temp_csv = tempfile::NamedTempFile::new().unwrap();
750 let csv_path = temp_csv.path().to_str().unwrap();
751 vapor_db.export_to_csv("workflow_test", csv_path).unwrap();
752
753 if let Some(bm) = vapor_db.bookmark_manager() {
755 bm.save_bookmark(
756 "high_scores".to_string(),
757 "SELECT * FROM workflow_test WHERE score > 90".to_string(),
758 Some("Students with high scores".to_string()),
759 )
760 .unwrap();
761 }
762
763 assert!(!vapor_db.is_transaction_active());
765 assert!(std::path::Path::new(csv_path).exists());
766
767 if let Some(home) = original_home {
769 std::env::set_var("HOME", home);
770 } else {
771 std::env::remove_var("HOME");
772 }
773 }
774}