vapor_cli/
lib.rs

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
10// Main entry points
11pub use repl::repl_mode;
12
13// Database management
14pub use db::{init_database, connect_database, create_table, list_tables};
15
16// SQL execution and display
17pub use display::{execute_sql, show_table_schema, show_all_schemas, show_database_info, OutputFormat, QueryOptions};
18
19// Data export functionality
20pub use export::export_to_csv;
21
22// Shell functionality
23pub use shell::Shell;
24
25// Bookmark management
26pub use bookmarks::{BookmarkManager, Bookmark};
27
28// Transaction management
29pub use transactions::{TransactionManager, TransactionState};
30
31// Data population and testing
32pub use populate::{populate_database, PopulationConfig, ColumnConfig, DataType, DataDistribution};
33
34// Re-export commonly used types for convenience
35pub use rusqlite::Connection;
36pub use anyhow::Result;
37
38/// A high-level API for interacting with SQLite databases through vapor-cli
39/// 
40/// This struct provides a simplified interface to vapor-cli's functionality,
41/// making it easy to use as a library in other Rust projects.
42pub 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    /// Create a new VaporDB instance with an existing database
51    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    /// Create a new database and return a VaporDB instance
67    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    /// Execute a SQL query and return the result
74    pub fn execute(&self, sql: &str) -> Result<()> {
75        let options = QueryOptions::default();
76        execute_sql(&self.connection, sql, &options)
77    }
78    
79    /// Execute a SQL query with custom options
80    pub fn execute_with_options(&self, sql: &str, options: &QueryOptions) -> Result<()> {
81        execute_sql(&self.connection, sql, options)
82    }
83    
84    /// List all tables in the database
85    pub fn list_tables(&self) -> Result<Vec<String>> {
86        list_tables(&self.db_path)
87    }
88    
89    /// Show schema for a specific table
90    pub fn show_table_schema(&self, table_name: &str) -> Result<()> {
91        show_table_schema(&self.connection, table_name)
92    }
93    
94    /// Show all table schemas
95    pub fn show_all_schemas(&self) -> Result<()> {
96        show_all_schemas(&self.connection)
97    }
98    
99    /// Show database information
100    pub fn show_database_info(&self) -> Result<()> {
101        show_database_info(&self.connection, &self.db_path)
102    }
103    
104    /// Export a table to CSV
105    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    /// Export query results to CSV
111    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    /// Start the interactive REPL
116    pub fn start_repl(&self) -> Result<()> {
117        repl_mode(&self.db_path)
118    }
119    
120    /// Start the interactive shell
121    pub fn start_shell(&self) -> Result<()> {
122        let mut shell = Shell::new();
123        shell.run();
124        Ok(())
125    }
126    
127    /// Populate database with test data
128    pub fn populate_with_test_data(&self, config: Option<PopulationConfig>) -> Result<()> {
129        populate_database(&self.db_path, config)
130    }
131    
132    /// Begin a transaction
133    pub fn begin_transaction(&self) -> Result<()> {
134        self.transaction_manager.begin_transaction(&self.connection)
135    }
136    
137    /// Commit the current transaction
138    pub fn commit_transaction(&self) -> Result<()> {
139        self.transaction_manager.commit_transaction(&self.connection)
140    }
141    
142    /// Rollback the current transaction
143    pub fn rollback_transaction(&self) -> Result<()> {
144        self.transaction_manager.rollback_transaction(&self.connection)
145    }
146    
147    /// Check if a transaction is active
148    pub fn is_transaction_active(&self) -> bool {
149        self.transaction_manager.is_active()
150    }
151    
152    /// Get access to the bookmark manager
153    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        // Create a test database with a table
169        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        // Test listing tables
176        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        // Test inserting data
192        execute_sql(&conn, "INSERT INTO test_table (name) VALUES ('test')", &QueryOptions::default()).unwrap();
193        
194        // Test selecting data with explicit column types
195        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        // Test showing schema
210        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        // Test showing all schemas
225        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        // Test showing database info
240        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        // Test creating a new database
249        let vapor_db = VaporDB::create(db_path).unwrap();
250        assert_eq!(vapor_db.db_path, db_path.to_string_lossy());
251        
252        // Test opening an existing database
253        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        // Test executing SQL
265        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        // Test listing tables
269        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        // Test transaction functionality
282        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        // Test bookmark manager access
299        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        // Test database initialization
319        init_database(db_path_str).unwrap();
320        assert!(db_path.exists());
321        
322        // Test that re-initializing existing database doesn't fail
323        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        // Initialize database first
332        init_database(db_path).unwrap();
333        
334        // Test table creation
335        create_table(db_path, "users", "id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT").unwrap();
336        
337        // Verify table was created
338        let tables = list_tables(db_path).unwrap();
339        assert!(tables.contains(&"users".to_string()));
340        
341        // Test creating table that already exists (should not fail)
342        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        // Test different output formats
361        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        // Test CSV export
396        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        // Verify the CSV file was created and has content
402        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        // Test table export method
418        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        // Test query export method
427        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")); // Should only contain method2
435    }
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        // Test execute with custom options
447        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        // Begin transaction and insert data
465        vapor_db.begin_transaction().unwrap();
466        vapor_db.execute("INSERT INTO rollback_test (name) VALUES ('transactional')").unwrap();
467        
468        // Rollback the transaction
469        vapor_db.rollback_transaction().unwrap();
470        assert!(!vapor_db.is_transaction_active());
471        
472        // Verify the transactional data was rolled back
473        // Note: This is a simplified test - in a real scenario you'd query to verify
474    }
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        // Test schema display functions
485        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        // Set up a temporary directory for bookmarks
496        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            // Test saving multiple bookmarks
504            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            // Test getting bookmarks
517            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            // Test deleting bookmark
526            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            // Test deleting non-existent bookmark
533            let not_deleted = bookmark_manager.delete_bookmark("non_existent").unwrap();
534            assert!(!not_deleted);
535        }
536        
537        // Restore original HOME environment variable
538        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        // Test default population config
548        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        // Test custom population config
555        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        // Test that data types can be created
584        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        // Test data distributions
593        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        // Test invalid SQL
608        let result = vapor_db.execute("INVALID SQL STATEMENT");
609        assert!(result.is_err());
610        
611        // Test querying non-existent table
612        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        // Set up a temporary directory for bookmarks
622        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        // Complete workflow test
627        let mut vapor_db = VaporDB::create(db_path).unwrap();
628        
629        // 1. Create schema
630        vapor_db.execute("CREATE TABLE workflow_test (id INTEGER PRIMARY KEY, name TEXT, score REAL)").unwrap();
631        
632        // 2. Insert data with transaction
633        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        // 3. Verify tables exist
639        let tables = vapor_db.list_tables().unwrap();
640        assert!(tables.contains(&"workflow_test".to_string()));
641        
642        // 4. Export data
643        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        // 5. Save bookmark
648        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        // 6. Verify workflow completed successfully
657        assert!(!vapor_db.is_transaction_active());
658        assert!(std::path::Path::new(csv_path).exists());
659        
660        // Restore original HOME environment variable
661        if let Some(home) = original_home {
662            std::env::set_var("HOME", home);
663        } else {
664            std::env::remove_var("HOME");
665        }
666    }
667}