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, trimmed).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    original_sql: &str,
94) -> Result<(), ExecutorError> {
95    match statement {
96        vibesql_ast::Statement::CreateSchema(schema_stmt) => {
97            // Skip built-in schemas (main and all temp schemas) - they already exist
98            // This handles backward compatibility with old SQL dumps
99            let schema_name = schema_stmt.schema_name.to_lowercase();
100            if schema_name != vibesql_catalog::DEFAULT_SCHEMA
101                && !vibesql_catalog::Catalog::is_temp_schema(&schema_name)
102            {
103                SchemaExecutor::execute_create_schema(&schema_stmt, db)?;
104            }
105        }
106        vibesql_ast::Statement::CreateTable(create_stmt) => {
107            CreateTableExecutor::execute(&create_stmt, db)?;
108        }
109        vibesql_ast::Statement::CreateIndex(index_stmt) => {
110            CreateIndexExecutor::execute(&index_stmt, db)?;
111        }
112        vibesql_ast::Statement::CreateView(mut view_stmt) => {
113            // Store original SQL for sqlite_master compatibility
114            view_stmt.sql_definition = Some(original_sql.to_string());
115            ViewExecutor::execute_create_view(&view_stmt, db)?;
116        }
117        vibesql_ast::Statement::CreateRole(role_stmt) => {
118            RoleExecutor::execute_create_role(&role_stmt, db)?;
119        }
120        vibesql_ast::Statement::Insert(insert_stmt) => {
121            InsertExecutor::execute(db, &insert_stmt)?;
122        }
123        vibesql_ast::Statement::Update(update_stmt) => {
124            UpdateExecutor::execute(&update_stmt, db)?;
125        }
126        _ => {
127            return Err(ExecutorError::Other(format!(
128                "Statement type not supported in database load: {:?}",
129                statement
130            )));
131        }
132    }
133    Ok(())
134}
135
136/// Truncate a string for error messages
137fn truncate_for_error(s: &str, max_len: usize) -> String {
138    if s.len() <= max_len {
139        s.to_string()
140    } else {
141        format!("{}...", &s[..max_len])
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use std::{fs, io::Write};
148    use tempfile::NamedTempFile;
149
150    use super::*;
151
152    #[test]
153    fn test_load_simple_database() {
154        // Create a temporary SQL dump file
155        let temp_file = NamedTempFile::new().unwrap();
156        let sql_dump = r#"
157-- Test database
158CREATE TABLE users (id INTEGER, name VARCHAR(50));
159INSERT INTO users VALUES (1, 'Alice');
160INSERT INTO users VALUES (2, 'Bob');
161"#;
162
163        fs::write(temp_file.path(), sql_dump).unwrap();
164
165        // Load the database
166        let db = load_sql_dump(temp_file.path().to_str().unwrap()).unwrap();
167
168        // Verify the table exists (note: identifiers are uppercased)
169        assert!(db.get_table("USERS").is_some());
170
171        // Verify data was loaded
172        let table = db.get_table("USERS").unwrap();
173        assert_eq!(table.row_count(), 2);
174        // temp_file is automatically cleaned up on drop
175    }
176
177    #[test]
178    fn test_load_with_schema() {
179        let temp_file = NamedTempFile::new().unwrap();
180        // Note: Schema-qualified INSERT statements not yet supported by parser
181        // So we create the table in a schema but insert without schema qualification
182        let sql_dump = r#"
183CREATE SCHEMA test_schema;
184CREATE TABLE test_schema.products (id INTEGER, price REAL);
185"#;
186
187        fs::write(temp_file.path(), sql_dump).unwrap();
188
189        let db = load_sql_dump(temp_file.path().to_str().unwrap()).unwrap();
190
191        // Verify table exists in schema (case-insensitive lookup)
192        assert!(db.get_table("test_schema.products").is_some());
193    }
194
195    #[test]
196    fn test_load_nonexistent_file() {
197        let result = load_sql_dump("/tmp/nonexistent_file.sql");
198        assert!(result.is_err());
199        assert!(result.unwrap_err().to_string().contains("does not exist"));
200    }
201
202    #[test]
203    fn test_load_invalid_sql() {
204        let temp_file = NamedTempFile::new().unwrap();
205        fs::write(temp_file.path(), "THIS IS NOT VALID SQL;").unwrap();
206
207        let result = load_sql_dump(temp_file.path().to_str().unwrap());
208        assert!(result.is_err());
209        assert!(result.unwrap_err().to_string().contains("Failed to parse"));
210    }
211
212    #[test]
213    fn test_load_binary_file_error() {
214        let temp_file = NamedTempFile::new().unwrap();
215        let mut file = fs::File::create(temp_file.path()).unwrap();
216        file.write_all(b"SQLite format 3\0").unwrap();
217        file.write_all(&[0xFF, 0xFE, 0xFD]).unwrap();
218
219        let result = load_sql_dump(temp_file.path().to_str().unwrap());
220        assert!(result.is_err());
221        assert!(result.unwrap_err().to_string().contains("binary SQLite database"));
222    }
223
224    #[test]
225    fn test_load_with_indexes() {
226        let temp_file = NamedTempFile::new().unwrap();
227        let sql_dump = r#"
228CREATE TABLE employees (id INTEGER, name VARCHAR(100), dept VARCHAR(50));
229INSERT INTO employees VALUES (1, 'Alice', 'Engineering');
230INSERT INTO employees VALUES (2, 'Bob', 'Sales');
231CREATE INDEX idx_dept ON employees (dept Asc);
232"#;
233
234        fs::write(temp_file.path(), sql_dump).unwrap();
235
236        let db = load_sql_dump(temp_file.path().to_str().unwrap()).unwrap();
237
238        // Verify table and data (note: identifiers are uppercased)
239        assert!(db.get_table("EMPLOYEES").is_some());
240        let table = db.get_table("EMPLOYEES").unwrap();
241        assert_eq!(table.row_count(), 2);
242
243        // Verify index exists (note: identifiers are uppercased)
244        assert!(db.get_index("IDX_DEPT").is_some());
245    }
246
247    #[test]
248    fn test_load_with_roles() {
249        let temp_file = NamedTempFile::new().unwrap();
250        let sql_dump = r#"
251CREATE ROLE admin;
252CREATE ROLE user;
253CREATE TABLE data (id INTEGER);
254"#;
255
256        fs::write(temp_file.path(), sql_dump).unwrap();
257
258        let db = load_sql_dump(temp_file.path().to_str().unwrap()).unwrap();
259
260        // Verify table exists (note: identifiers are uppercased)
261        assert!(db.get_table("DATA").is_some());
262    }
263}