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