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