vapor_cli/
lib.rs

1//! # Vapor-CLI
2//! 
3//! Vapor-CLI is a command-line interface for managing SQLite databases. It provides a set of commands to initialize databases, create tables, and interact with the data through a REPL or shell mode.
4//! 
5//! ## Features
6//! 
7//! - **Database Initialization**: Create a new SQLite database with the `init` command.
8//! - **Table Management**: Define and create tables with the `create-table` command.
9//! - **Interactive REPL**: An interactive Read-Eval-Print Loop (REPL) for executing SQL queries directly.
10//! - **Shell Mode**: A shell mode with database context for more advanced operations.
11//! - **Data Population**: A `populate` command to insert large amounts of data for testing purposes.
12//! 
13//! ## Modules
14//! 
15//! The crate is organized into several modules, each responsible for a specific part of the functionality:
16//! 
17//! - `db`: Core database operations like connecting, creating tables, and listing tables.
18//! - `repl`: Implements the interactive REPL mode.
19//! - `shell`: Implements the shell mode.
20//! - `populate`: Provides functionality for populating the database with test data.
21//! - `bookmarks`: Manages SQL query bookmarks.
22//! - `config`: Handles application configuration.
23//! - `display`: Manages the display of query results.
24//! - `export`: Handles data exporting.
25//! - `transactions`: Manages database transactions.
26
27pub 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
37// Main entry points
38pub use crate::repl::repl_mode;
39use crate::shell::shell_mode;
40
41// Database management
42pub use db::{connect_database, create_table, init_database, list_tables};
43
44// SQL execution and display
45pub use display::{
46    execute_sql, show_all_schemas, show_database_info, show_table_schema, OutputFormat,
47    QueryOptions,
48};
49
50// Data export functionality
51pub use export::export_to_csv;
52
53// Shell functionality
54pub use shell::Shell;
55
56// Bookmark management
57pub use bookmarks::{Bookmark, BookmarkManager};
58
59// Transaction management
60pub use transactions::{TransactionManager, TransactionState};
61
62// Data population and testing
63pub use populate::{populate_database, ColumnConfig, DataDistribution, DataType, PopulationConfig};
64
65// Re-export commonly used types for convenience
66pub use anyhow::Result;
67pub use rusqlite::Connection;
68
69/// A high-level API for interacting with SQLite databases through vapor-cli
70///
71/// This struct provides a simplified interface to vapor-cli's functionality,
72/// making it easy to use as a library in other Rust projects.
73pub 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    /// Create a new VaporDB instance with an existing database
82    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    /// Create a new database and return a VaporDB instance
98    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    /// Execute a SQL query and return the result
105    pub fn execute(&self, sql: &str) -> Result<()> {
106        let options = QueryOptions::default();
107        execute_sql(&self.connection, sql, &options)
108    }
109
110    /// Execute a SQL query with custom options
111    pub fn execute_with_options(&self, sql: &str, options: &QueryOptions) -> Result<()> {
112        execute_sql(&self.connection, sql, options)
113    }
114
115    /// List all tables in the database
116    pub fn list_tables(&self) -> Result<Vec<String>> {
117        list_tables(&self.db_path)
118    }
119
120    /// Show schema for a specific table
121    pub fn show_table_schema(&self, table_name: &str) -> Result<()> {
122        show_table_schema(&self.connection, table_name)
123    }
124
125    /// Show all table schemas
126    pub fn show_all_schemas(&self) -> Result<()> {
127        show_all_schemas(&self.connection)
128    }
129
130    /// Show database information
131    pub fn show_database_info(&self) -> Result<()> {
132        show_database_info(&self.connection, &self.db_path)
133    }
134
135    /// Export a table to CSV
136    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    /// Export query results to CSV
142    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    /// Start the interactive REPL
147    pub fn start_repl(&self) -> Result<()> {
148        repl_mode(&self.db_path)
149    }
150
151    /// Start the interactive shell
152    pub fn start_shell(&self) -> Result<()> {
153        shell_mode(&self.db_path).map(|_| ())
154    }
155
156    /// Populate database with test data
157    pub fn populate_with_test_data(&self, config: Option<PopulationConfig>) -> Result<()> {
158        populate_database(&self.db_path, config)
159    }
160
161    /// Begin a transaction
162    pub fn begin_transaction(&self) -> Result<()> {
163        self.transaction_manager.begin_transaction(&self.connection)
164    }
165
166    /// Commit the current transaction
167    pub fn commit_transaction(&self) -> Result<()> {
168        self.transaction_manager
169            .commit_transaction(&self.connection)
170    }
171
172    /// Rollback the current transaction
173    pub fn rollback_transaction(&self) -> Result<()> {
174        self.transaction_manager
175            .rollback_transaction(&self.connection)
176    }
177
178    /// Check if a transaction is active
179    pub fn is_transaction_active(&self) -> bool {
180        self.transaction_manager.is_active()
181    }
182
183    /// Get access to the bookmark manager
184    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        // Create a test database with a table
200        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        // Test listing tables
208        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        // Test inserting data
225        execute_sql(
226            &conn,
227            "INSERT INTO test_table (name) VALUES ('test')",
228            &QueryOptions::default(),
229        )
230        .unwrap();
231
232        // Test selecting data with explicit column types
233        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        // Test showing schema
254        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        // Test showing all schemas
270        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        // Test showing database info
286        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        // Test creating a new database
295        let vapor_db = VaporDB::create(db_path).unwrap();
296        assert_eq!(vapor_db.db_path, db_path.to_string_lossy());
297
298        // Test opening an existing database
299        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        // Test executing SQL
311        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        // Test listing tables
319        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        // Test transaction functionality
334        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        // Test bookmark manager access
353        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        // Test database initialization
375        init_database(db_path_str).unwrap();
376        assert!(db_path.exists());
377
378        // Test that re-initializing existing database doesn't fail
379        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        // Initialize database first
388        init_database(db_path).unwrap();
389
390        // Test table creation
391        create_table(
392            db_path,
393            "users",
394            "id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT",
395        )
396        .unwrap();
397
398        // Verify table was created
399        let tables = list_tables(db_path).unwrap();
400        assert!(tables.contains(&"users".to_string()));
401
402        // Test creating table that already exists (should not fail)
403        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        // Test different output formats
429        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        // Test CSV export
466        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        // Verify the CSV file was created and has content
472        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        // Test table export method
492        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        // Test query export method
503        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")); // Should only contain method2
516    }
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        // Test execute with custom options
532        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        // Begin transaction and insert data
556        vapor_db.begin_transaction().unwrap();
557        vapor_db
558            .execute("INSERT INTO rollback_test (name) VALUES ('transactional')")
559            .unwrap();
560
561        // Rollback the transaction
562        vapor_db.rollback_transaction().unwrap();
563        assert!(!vapor_db.is_transaction_active());
564
565        // Verify the transactional data was rolled back
566        // Note: This is a simplified test - in a real scenario you'd query to verify
567    }
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        // Test schema display functions
578        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        // Set up a temporary directory for bookmarks
589        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            // Test saving multiple bookmarks
597            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            // Test getting bookmarks
614            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            // Test deleting bookmark
623            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            // Test deleting non-existent bookmark
630            let not_deleted = bookmark_manager.delete_bookmark("non_existent").unwrap();
631            assert!(!not_deleted);
632        }
633
634        // Restore original HOME environment variable
635        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        // Test default population config
645        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        // Test custom population config
652        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        // Test that data types can be created
681        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        // Test data distributions
690        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        // Test invalid SQL
708        let result = vapor_db.execute("INVALID SQL STATEMENT");
709        assert!(result.is_err());
710
711        // Test querying non-existent table
712        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        // Set up a temporary directory for bookmarks
722        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        // Complete workflow test
727        let mut vapor_db = VaporDB::create(db_path).unwrap();
728
729        // 1. Create schema
730        vapor_db
731            .execute("CREATE TABLE workflow_test (id INTEGER PRIMARY KEY, name TEXT, score REAL)")
732            .unwrap();
733
734        // 2. Insert data with transaction
735        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        // 3. Verify tables exist
745        let tables = vapor_db.list_tables().unwrap();
746        assert!(tables.contains(&"workflow_test".to_string()));
747
748        // 4. Export data
749        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        // 5. Save bookmark
754        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        // 6. Verify workflow completed successfully
764        assert!(!vapor_db.is_transaction_active());
765        assert!(std::path::Path::new(csv_path).exists());
766
767        // Restore original HOME environment variable
768        if let Some(home) = original_home {
769            std::env::set_var("HOME", home);
770        } else {
771            std::env::remove_var("HOME");
772        }
773    }
774}