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;
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
52/// A high-level API for interacting with SQLite databases through vapor-cli
53///
54/// This struct provides a simplified interface to vapor-cli's functionality,
55/// making it easy to use as a library in other Rust projects.
56pub 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    /// Create a new VaporDB instance with an existing database
65    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    /// Create a new database and return a VaporDB instance
81    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    /// Execute a SQL query and return the result
88    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    /// Execute a SQL query with custom options
95    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    /// List all tables in the database
101    pub fn list_tables(&self) -> Result<Vec<String>> {
102        list_tables(&self.db_path)
103    }
104
105    /// Show schema for a specific table
106    pub fn show_table_schema(&self, table_name: &str) -> Result<()> {
107        show_table_schema(&self.connection, table_name)
108    }
109
110    /// Show all table schemas
111    pub fn show_all_schemas(&self) -> Result<()> {
112        show_all_schemas(&self.connection)
113    }
114
115    /// Show database information
116    pub fn show_database_info(&self) -> Result<()> {
117        show_database_info(&self.connection, &self.db_path)
118    }
119
120    /// Export a table to CSV
121    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    /// Export query results to CSV
127    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    /// Start the interactive REPL
132    pub fn start_repl(&self) -> Result<()> {
133        repl_mode(&self.db_path)
134    }
135
136    /// Start the interactive shell
137    pub fn start_shell(&self) -> Result<()> {
138        shell_mode(&self.db_path).map(|_| ())
139    }
140
141    /// Populate database with test data
142    pub fn populate_with_test_data(&self, config: Option<PopulationConfig>) -> Result<()> {
143        populate_database(&self.db_path, config)
144    }
145
146    /// Begin a transaction
147    pub fn begin_transaction(&self) -> Result<()> {
148        self.transaction_manager.begin_transaction(&self.connection)
149    }
150
151    /// Commit the current transaction
152    pub fn commit_transaction(&self) -> Result<()> {
153        self.transaction_manager
154            .commit_transaction(&self.connection)
155    }
156
157    /// Rollback the current transaction
158    pub fn rollback_transaction(&self) -> Result<()> {
159        self.transaction_manager
160            .rollback_transaction(&self.connection)
161    }
162
163    /// Check if a transaction is active
164    pub fn is_transaction_active(&self) -> bool {
165        self.transaction_manager.is_active()
166    }
167
168    /// Get access to the bookmark manager
169    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        // Create a test database with a table
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        )
190        .unwrap();
191
192        // Test listing tables
193        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        // Test inserting data
210        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        // Test selecting data with explicit column types
220        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        // Test showing schema
243        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        // Test showing all schemas
259        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        // Test showing database info
275        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        // Test creating a new database
284        let vapor_db = VaporDB::create(db_path).unwrap();
285        assert_eq!(vapor_db.db_path, db_path.to_string_lossy());
286
287        // Test opening an existing database
288        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        // Test executing SQL
300        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        // Test listing tables
308        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        // Test transaction functionality
323        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        // Test bookmark manager access
342        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        // Test database initialization
364        init_database(db_path_str).unwrap();
365        assert!(db_path.exists());
366
367        // Test that re-initializing existing database doesn't fail
368        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        // Initialize database first
377        init_database(db_path).unwrap();
378
379        // Test table creation
380        create_table(
381            db_path,
382            "users",
383            "id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT",
384        )
385        .unwrap();
386
387        // Verify table was created
388        let tables = list_tables(db_path).unwrap();
389        assert!(tables.contains(&"users".to_string()));
390
391        // Test creating table that already exists (should not fail)
392        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        // Test different output formats
418        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        // Test CSV export
458        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        // Verify the CSV file was created and has content
464        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        // Test table export method
484        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        // Test query export method
495        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")); // Should only contain method2
508    }
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        // Test execute with custom options
524        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        // Begin transaction and insert data
548        vapor_db.begin_transaction().unwrap();
549        vapor_db
550            .execute("INSERT INTO rollback_test (name) VALUES ('transactional')")
551            .unwrap();
552
553        // Rollback the transaction
554        vapor_db.rollback_transaction().unwrap();
555        assert!(!vapor_db.is_transaction_active());
556
557        // Verify the transactional data was rolled back
558        // Note: This is a simplified test - in a real scenario you'd query to verify
559    }
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        // Test schema display functions
570        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        // Set up a temporary directory for bookmarks
581        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            // Test saving multiple bookmarks
589            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            // Test getting bookmarks
606            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            // Test deleting bookmark
615            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            // Test deleting non-existent bookmark
622            let not_deleted = bookmark_manager.delete_bookmark("non_existent").unwrap();
623            assert!(!not_deleted);
624        }
625
626        // Restore original HOME environment variable
627        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        // Test default population config
637        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        // Test custom population config
644        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        // Test that data types can be created
673        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        // Test data distributions
682        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        // Test invalid SQL
700        let result = vapor_db.execute("INVALID SQL STATEMENT");
701        assert!(result.is_err());
702
703        // Test querying non-existent table
704        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        // Set up a temporary directory for bookmarks
714        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        // Complete workflow test
719        let mut vapor_db = VaporDB::create(db_path).unwrap();
720
721        // 1. Create schema
722        vapor_db
723            .execute("CREATE TABLE workflow_test (id INTEGER PRIMARY KEY, name TEXT, score REAL)")
724            .unwrap();
725
726        // 2. Insert data with transaction
727        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        // 3. Verify tables exist
737        let tables = vapor_db.list_tables().unwrap();
738        assert!(tables.contains(&"workflow_test".to_string()));
739
740        // 4. Export data
741        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        // 5. Save bookmark
746        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        // 6. Verify workflow completed successfully
756        assert!(!vapor_db.is_transaction_active());
757        assert!(std::path::Path::new(csv_path).exists());
758
759        // Restore original HOME environment variable
760        if let Some(home) = original_home {
761            std::env::set_var("HOME", home);
762        } else {
763            std::env::remove_var("HOME");
764        }
765    }
766}