vibesql_executor/
persistence.rs1use std::path::Path;
8
9use vibesql_storage::Database;
10
11use crate::{
12 CreateIndexExecutor, CreateTableExecutor, ExecutorError, InsertExecutor, RoleExecutor,
13 SchemaExecutor, UpdateExecutor, ViewExecutor,
14};
15
16pub fn load_sql_dump<P: AsRef<Path>>(path: P) -> Result<Database, ExecutorError> {
40 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 let statements = vibesql_storage::parse_sql_statements(&sql_content)
47 .map_err(|e| ExecutorError::Other(format!("Failed to parse SQL dump: {}", e)))?;
48
49 let mut db = Database::new();
51
52 for (idx, stmt_sql) in statements.iter().enumerate() {
54 let trimmed = stmt_sql.trim();
56 if trimmed.is_empty() || trimmed.starts_with("--") {
57 continue;
58 }
59
60 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_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
86fn 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 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 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
136fn 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 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 let db = load_sql_dump(temp_file.path().to_str().unwrap()).unwrap();
167
168 assert!(db.get_table("USERS").is_some());
170
171 let table = db.get_table("USERS").unwrap();
173 assert_eq!(table.row_count(), 2);
174 }
176
177 #[test]
178 fn test_load_with_schema() {
179 let temp_file = NamedTempFile::new().unwrap();
180 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 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 assert!(db.get_table("EMPLOYEES").is_some());
240 let table = db.get_table("EMPLOYEES").unwrap();
241 assert_eq!(table.row_count(), 2);
242
243 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 assert!(db.get_table("DATA").is_some());
262 }
263}