vibesql_executor/
persistence.rs

1//! Database Persistence - Loading SQL Dumps
2//!
3//! Provides functionality for loading databases from SQL dump files.
4//! This mirrors the save functionality in vibesql-storage but requires
5//! the executor layer since it needs to parse and execute SQL statements.
6
7use std::path::Path;
8
9use vibesql_storage::Database;
10
11use crate::{
12    CreateIndexExecutor, CreateTableExecutor, ExecutorError, InsertExecutor, RoleExecutor,
13    SchemaExecutor, UpdateExecutor, ViewExecutor,
14};
15
16/// Load database from SQL dump file
17///
18/// Reads SQL dump, parses statements, and executes them to recreate database state.
19/// This is the shared implementation used by CLI, Python bindings, and other consumers.
20///
21/// # Arguments
22/// * `path` - Path to the SQL dump file
23///
24/// # Returns
25/// A new Database instance with the loaded state
26///
27/// # Errors
28/// Returns error if:
29/// - File cannot be read
30/// - File is not valid SQL dump format (e.g., binary SQLite file)
31/// - SQL parsing fails
32/// - Statement execution fails
33///
34/// # Example
35/// ```no_run
36/// # use vibesql_executor::load_sql_dump;
37/// let db = load_sql_dump("database.sql").unwrap();
38/// ```
39pub fn load_sql_dump<P: AsRef<Path>>(path: P) -> Result<Database, ExecutorError> {
40    // Read the SQL dump file using storage utility
41    let sql_content = vibesql_storage::read_sql_dump(&path).map_err(|e| {
42        ExecutorError::Other(format!("Failed to read database file {:?}: {}", path.as_ref(), e))
43    })?;
44
45    // Split into individual statements using storage utility
46    let statements = vibesql_storage::parse_sql_statements(&sql_content)
47        .map_err(|e| ExecutorError::Other(format!("Failed to parse SQL dump: {}", e)))?;
48
49    // Create a new database to populate
50    let mut db = Database::new();
51
52    // Execute each statement
53    for (idx, stmt_sql) in statements.iter().enumerate() {
54        // Skip empty statements and comments
55        let trimmed = stmt_sql.trim();
56        if trimmed.is_empty() || trimmed.starts_with("--") {
57            continue;
58        }
59
60        // Parse the statement
61        let statement = vibesql_parser::Parser::parse_sql(trimmed).map_err(|e| {
62            ExecutorError::Other(format!(
63                "Failed to parse statement {} in {:?}: {}\nStatement: {}",
64                idx + 1,
65                path.as_ref(),
66                e,
67                truncate_for_error(trimmed, 100)
68            ))
69        })?;
70
71        // Execute the statement
72        execute_statement_for_load(&mut db, statement).map_err(|e| {
73            ExecutorError::Other(format!(
74                "Failed to execute statement {} in {:?}: {}\nStatement: {}",
75                idx + 1,
76                path.as_ref(),
77                e,
78                truncate_for_error(trimmed, 100)
79            ))
80        })?;
81    }
82
83    Ok(db)
84}
85
86/// Execute a single statement during database load
87///
88/// Only DDL and INSERT statements are supported during load.
89/// Other statement types will return an error.
90fn execute_statement_for_load(
91    db: &mut Database,
92    statement: vibesql_ast::Statement,
93) -> Result<(), ExecutorError> {
94    match statement {
95        vibesql_ast::Statement::CreateSchema(schema_stmt) => {
96            SchemaExecutor::execute_create_schema(&schema_stmt, db)?;
97        }
98        vibesql_ast::Statement::CreateTable(create_stmt) => {
99            CreateTableExecutor::execute(&create_stmt, db)?;
100        }
101        vibesql_ast::Statement::CreateIndex(index_stmt) => {
102            CreateIndexExecutor::execute(&index_stmt, db)?;
103        }
104        vibesql_ast::Statement::CreateView(view_stmt) => {
105            ViewExecutor::execute_create_view(&view_stmt, db)?;
106        }
107        vibesql_ast::Statement::CreateRole(role_stmt) => {
108            RoleExecutor::execute_create_role(&role_stmt, db)?;
109        }
110        vibesql_ast::Statement::Insert(insert_stmt) => {
111            InsertExecutor::execute(db, &insert_stmt)?;
112        }
113        vibesql_ast::Statement::Update(update_stmt) => {
114            UpdateExecutor::execute(&update_stmt, db)?;
115        }
116        _ => {
117            return Err(ExecutorError::Other(format!(
118                "Statement type not supported in database load: {:?}",
119                statement
120            )));
121        }
122    }
123    Ok(())
124}
125
126/// Truncate a string for error messages
127fn truncate_for_error(s: &str, max_len: usize) -> String {
128    if s.len() <= max_len {
129        s.to_string()
130    } else {
131        format!("{}...", &s[..max_len])
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use std::fs;
139    use std::io::Write;
140
141    #[test]
142    fn test_load_simple_database() {
143        // Create a temporary SQL dump file
144        let temp_file = "/tmp/test_load_simple.sql";
145        let sql_dump = r#"
146-- Test database
147CREATE TABLE users (id INTEGER, name VARCHAR(50));
148INSERT INTO users VALUES (1, 'Alice');
149INSERT INTO users VALUES (2, 'Bob');
150"#;
151
152        fs::write(temp_file, sql_dump).unwrap();
153
154        // Load the database
155        let db = load_sql_dump(temp_file).unwrap();
156
157        // Verify the table exists (note: identifiers are uppercased)
158        assert!(db.get_table("USERS").is_some());
159
160        // Verify data was loaded
161        let table = db.get_table("USERS").unwrap();
162        assert_eq!(table.row_count(), 2);
163
164        // Clean up
165        fs::remove_file(temp_file).unwrap();
166    }
167
168    #[test]
169    fn test_load_with_schema() {
170        let temp_file = "/tmp/test_load_schema.sql";
171        // Note: Schema-qualified INSERT statements not yet supported by parser
172        // So we create the table in a schema but insert without schema qualification
173        let sql_dump = r#"
174CREATE SCHEMA test_schema;
175CREATE TABLE test_schema.products (id INTEGER, price REAL);
176"#;
177
178        fs::write(temp_file, sql_dump).unwrap();
179
180        let db = load_sql_dump(temp_file).unwrap();
181
182        // Verify table exists in schema (note: identifiers are uppercased)
183        assert!(db.get_table("TEST_SCHEMA.PRODUCTS").is_some());
184
185        fs::remove_file(temp_file).unwrap();
186    }
187
188    #[test]
189    fn test_load_nonexistent_file() {
190        let result = load_sql_dump("/tmp/nonexistent_file.sql");
191        assert!(result.is_err());
192        assert!(result.unwrap_err().to_string().contains("does not exist"));
193    }
194
195    #[test]
196    fn test_load_invalid_sql() {
197        let temp_file = "/tmp/test_load_invalid.sql";
198        fs::write(temp_file, "THIS IS NOT VALID SQL;").unwrap();
199
200        let result = load_sql_dump(temp_file);
201        assert!(result.is_err());
202        assert!(result.unwrap_err().to_string().contains("Failed to parse"));
203
204        fs::remove_file(temp_file).unwrap();
205    }
206
207    #[test]
208    fn test_load_binary_file_error() {
209        let temp_file = "/tmp/test_load_binary.db";
210        let mut file = fs::File::create(temp_file).unwrap();
211        file.write_all(b"SQLite format 3\0").unwrap();
212        file.write_all(&[0xFF, 0xFE, 0xFD]).unwrap();
213
214        let result = load_sql_dump(temp_file);
215        assert!(result.is_err());
216        assert!(result.unwrap_err().to_string().contains("binary SQLite database"));
217
218        fs::remove_file(temp_file).unwrap();
219    }
220
221    #[test]
222    fn test_load_with_indexes() {
223        let temp_file = "/tmp/test_load_indexes.sql";
224        let sql_dump = r#"
225CREATE TABLE employees (id INTEGER, name VARCHAR(100), dept VARCHAR(50));
226INSERT INTO employees VALUES (1, 'Alice', 'Engineering');
227INSERT INTO employees VALUES (2, 'Bob', 'Sales');
228CREATE INDEX idx_dept ON employees (dept Asc);
229"#;
230
231        fs::write(temp_file, sql_dump).unwrap();
232
233        let db = load_sql_dump(temp_file).unwrap();
234
235        // Verify table and data (note: identifiers are uppercased)
236        assert!(db.get_table("EMPLOYEES").is_some());
237        let table = db.get_table("EMPLOYEES").unwrap();
238        assert_eq!(table.row_count(), 2);
239
240        // Verify index exists (note: identifiers are uppercased)
241        assert!(db.get_index("IDX_DEPT").is_some());
242
243        fs::remove_file(temp_file).unwrap();
244    }
245
246    #[test]
247    fn test_load_with_roles() {
248        let temp_file = "/tmp/test_load_roles.sql";
249        let sql_dump = r#"
250CREATE ROLE admin;
251CREATE ROLE user;
252CREATE TABLE data (id INTEGER);
253"#;
254
255        fs::write(temp_file, sql_dump).unwrap();
256
257        let db = load_sql_dump(temp_file).unwrap();
258
259        // Verify table exists (note: identifiers are uppercased)
260        assert!(db.get_table("DATA").is_some());
261
262        fs::remove_file(temp_file).unwrap();
263    }
264}