1use std::path::Path;
8
9use vibesql_storage::Database;
10
11use crate::{
12 CreateIndexExecutor, CreateTableExecutor, ExecutorError, InsertExecutor, RoleExecutor,
13 SchemaExecutor, TriggerExecutor, 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_for_load_with_source(
121 &create_stmt,
122 db,
123 Some(original_sql),
124 )?;
125 }
126 vibesql_ast::Statement::CreateIndex(index_stmt) => {
127 CreateIndexExecutor::execute(&index_stmt, db)?;
128 }
129 vibesql_ast::Statement::CreateView(mut view_stmt) => {
130 view_stmt.sql_definition = Some(original_sql.to_string());
132 ViewExecutor::execute_create_view(&view_stmt, db)?;
133 }
134 vibesql_ast::Statement::CreateTrigger(trigger_stmt) => {
135 TriggerExecutor::create_trigger_with_sql(db, &trigger_stmt, Some(original_sql))?;
141 }
142 vibesql_ast::Statement::CreateRole(role_stmt) => {
143 RoleExecutor::execute_create_role(&role_stmt, db)?;
144 }
145 vibesql_ast::Statement::Insert(insert_stmt) => {
146 InsertExecutor::execute(db, &insert_stmt)?;
147 }
148 vibesql_ast::Statement::Update(update_stmt) => {
149 UpdateExecutor::execute(&update_stmt, db)?;
150 }
151 _ => {
152 return Err(ExecutorError::Other(format!(
153 "Statement type not supported in database load: {:?}",
154 statement
155 )));
156 }
157 }
158 Ok(())
159}
160
161fn truncate_for_error(s: &str, max_len: usize) -> String {
163 if s.len() <= max_len {
164 s.to_string()
165 } else {
166 format!("{}...", &s[..max_len])
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use std::{fs, io::Write};
173 use tempfile::NamedTempFile;
174
175 use super::*;
176
177 #[test]
178 fn test_load_simple_database() {
179 let temp_file = NamedTempFile::new().unwrap();
181 let sql_dump = r#"
182-- Test database
183CREATE TABLE users (id INTEGER, name VARCHAR(50));
184INSERT INTO users VALUES (1, 'Alice');
185INSERT INTO users VALUES (2, 'Bob');
186"#;
187
188 fs::write(temp_file.path(), sql_dump).unwrap();
189
190 let db = load_sql_dump(temp_file.path().to_str().unwrap()).unwrap();
192
193 assert!(db.get_table("USERS").is_some());
195
196 let table = db.get_table("USERS").unwrap();
198 assert_eq!(table.row_count(), 2);
199 }
201
202 #[test]
203 fn test_load_with_schema() {
204 let temp_file = NamedTempFile::new().unwrap();
205 let sql_dump = r#"
208CREATE SCHEMA test_schema;
209CREATE TABLE test_schema.products (id INTEGER, price REAL);
210"#;
211
212 fs::write(temp_file.path(), sql_dump).unwrap();
213
214 let db = load_sql_dump(temp_file.path().to_str().unwrap()).unwrap();
215
216 assert!(db.get_table("test_schema.products").is_some());
218 }
219
220 #[test]
221 fn test_load_nonexistent_file() {
222 let result = load_sql_dump("/tmp/nonexistent_file.sql");
223 assert!(result.is_err());
224 assert!(result.unwrap_err().to_string().contains("does not exist"));
225 }
226
227 #[test]
228 fn test_load_invalid_sql() {
229 let temp_file = NamedTempFile::new().unwrap();
230 fs::write(temp_file.path(), "THIS IS NOT VALID SQL;").unwrap();
231
232 let result = load_sql_dump(temp_file.path().to_str().unwrap());
233 assert!(result.is_err());
234 assert!(result.unwrap_err().to_string().contains("Failed to parse"));
235 }
236
237 #[test]
238 fn test_load_binary_file_error() {
239 let temp_file = NamedTempFile::new().unwrap();
240 let mut file = fs::File::create(temp_file.path()).unwrap();
241 file.write_all(b"SQLite format 3\0").unwrap();
242 file.write_all(&[0xFF, 0xFE, 0xFD]).unwrap();
243
244 let result = load_sql_dump(temp_file.path().to_str().unwrap());
245 assert!(result.is_err());
246 assert!(result.unwrap_err().to_string().contains("binary SQLite database"));
247 }
248
249 #[test]
250 fn test_load_with_indexes() {
251 let temp_file = NamedTempFile::new().unwrap();
252 let sql_dump = r#"
253CREATE TABLE employees (id INTEGER, name VARCHAR(100), dept VARCHAR(50));
254INSERT INTO employees VALUES (1, 'Alice', 'Engineering');
255INSERT INTO employees VALUES (2, 'Bob', 'Sales');
256CREATE INDEX idx_dept ON employees (dept Asc);
257"#;
258
259 fs::write(temp_file.path(), sql_dump).unwrap();
260
261 let db = load_sql_dump(temp_file.path().to_str().unwrap()).unwrap();
262
263 assert!(db.get_table("EMPLOYEES").is_some());
265 let table = db.get_table("EMPLOYEES").unwrap();
266 assert_eq!(table.row_count(), 2);
267
268 assert!(db.get_index("IDX_DEPT").is_some());
270 }
271
272 #[cfg(test)]
275 fn exec_one(db: &mut Database, sql: &str) {
276 use crate::{
277 index_ddl::IndexExecutor, BeginTransactionExecutor, CommitExecutor, DropTableExecutor,
278 };
279 let st = vibesql_parser::Parser::parse_sql(sql).unwrap();
280 match st {
281 vibesql_ast::Statement::CreateTable(s) => {
282 CreateTableExecutor::execute(&s, db).unwrap();
283 }
284 vibesql_ast::Statement::CreateIndex(s) => {
285 CreateIndexExecutor::execute(&s, db).unwrap();
286 }
287 vibesql_ast::Statement::DropIndex(s) => {
288 IndexExecutor::execute_drop(&s, db).unwrap();
289 }
290 vibesql_ast::Statement::DropTable(s) => {
291 DropTableExecutor::execute(&s, db).unwrap();
292 }
293 vibesql_ast::Statement::BeginTransaction(s) => {
294 BeginTransactionExecutor::execute(&s, db).unwrap();
295 }
296 vibesql_ast::Statement::Commit(s) => {
297 CommitExecutor::execute(&s, db).unwrap();
298 }
299 other => panic!("unexpected statement in test helper: {:?}", other),
300 }
301 }
302
303 #[test]
304 fn test_transactional_bulk_create_drop_round_trips_cleanly() {
305 let mut db = Database::new();
314
315 exec_one(&mut db, "BEGIN");
317 for i in 0..200 {
318 exec_one(&mut db, &format!("CREATE TABLE tbl{} (a, b, c)", i));
319 }
320 exec_one(&mut db, "COMMIT");
321
322 exec_one(&mut db, "BEGIN");
324 for i in 0..200 {
325 exec_one(&mut db, &format!("DROP TABLE tbl{}", i));
326 }
327 exec_one(&mut db, "COMMIT");
328
329 assert!(db.list_tables().is_empty(), "all tables should be dropped: {:?}", db.list_tables());
331
332 let temp_file = NamedTempFile::new().unwrap();
335 db.save_sql_dump(temp_file.path()).unwrap();
336 let reloaded = load_sql_dump(temp_file.path().to_str().unwrap())
337 .expect("dump of a committed bulk create+drop transaction must reload (issue #5624)");
338 assert!(
339 reloaded.list_tables().is_empty(),
340 "reloaded DB should have no tables, got: {:?}",
341 reloaded.list_tables()
342 );
343 }
344
345 #[test]
346 fn test_transactional_partial_drop_round_trips_cleanly() {
347 let mut db = Database::new();
352
353 exec_one(&mut db, "BEGIN");
354 for i in 0..10 {
355 exec_one(&mut db, &format!("CREATE TABLE t{} (a, b)", i));
356 }
357 for i in (0..10).step_by(2) {
359 exec_one(&mut db, &format!("DROP TABLE t{}", i));
360 }
361 exec_one(&mut db, "COMMIT");
362
363 let temp_file = NamedTempFile::new().unwrap();
364 db.save_sql_dump(temp_file.path()).unwrap();
365 let reloaded = load_sql_dump(temp_file.path().to_str().unwrap())
366 .expect("partial-drop transaction dump must reload (issue #5624)");
367
368 for i in 0..10 {
369 let exists = reloaded.get_table(&format!("t{}", i)).is_some();
370 if i % 2 == 0 {
371 assert!(!exists, "dropped table t{} must NOT survive reload", i);
372 } else {
373 assert!(exists, "kept table t{} must survive reload", i);
374 }
375 }
376 }
377
378 #[test]
379 fn test_table_replacing_dropped_index_round_trips_cleanly() {
380 let mut db = Database::new();
392 exec_one(&mut db, "CREATE TABLE test2(one text)");
393 exec_one(&mut db, "CREATE INDEX test3 ON test2(one)");
394 exec_one(&mut db, "DROP INDEX test3");
395 exec_one(&mut db, "CREATE TABLE test3(two text)");
396
397 assert!(
400 db.catalog.list_all_indexes().iter().all(|idx| idx.name.to_lowercase() != "test3"),
401 "stale `test3` index must not survive DROP INDEX: {:?}",
402 db.catalog.list_all_indexes().iter().map(|i| i.name.clone()).collect::<Vec<_>>()
403 );
404
405 let temp_file = NamedTempFile::new().unwrap();
406 db.save_sql_dump(temp_file.path()).unwrap();
407 let dump = fs::read_to_string(temp_file.path()).unwrap();
408 assert!(
411 !dump.to_uppercase().contains("CREATE INDEX TEST3"),
412 "dump must not re-emit the dropped `test3` index:\n{}",
413 dump
414 );
415
416 let reloaded = load_sql_dump(temp_file.path().to_str().unwrap()).expect(
417 "table replacing a dropped same-named index must reload without collision (issue #5624)",
418 );
419 assert!(reloaded.get_table("test2").is_some(), "test2 must survive reload");
420 assert!(reloaded.get_table("test3").is_some(), "test3 (table) must survive reload");
421 }
422
423 #[test]
424 fn test_table_index_namespace_collision_round_trips_cleanly() {
425 let mut db = Database::new();
436
437 let create_t2 = vibesql_parser::Parser::parse_sql("CREATE TABLE test2(one text)").unwrap();
438 if let vibesql_ast::Statement::CreateTable(s) = create_t2 {
439 CreateTableExecutor::execute(&s, &mut db).unwrap();
440 }
441 let create_ix =
442 vibesql_parser::Parser::parse_sql("CREATE INDEX test3 ON test2(one)").unwrap();
443 if let vibesql_ast::Statement::CreateIndex(s) = create_ix {
444 CreateIndexExecutor::execute(&s, &mut db).unwrap();
445 }
446
447 let collide = vibesql_parser::Parser::parse_sql("CREATE TABLE test3(two text)").unwrap();
449 if let vibesql_ast::Statement::CreateTable(s) = collide {
450 let err = CreateTableExecutor::execute(&s, &mut db).unwrap_err();
451 assert_eq!(err.to_string(), "there is already an index named test3");
452 }
453
454 let temp_file = NamedTempFile::new().unwrap();
456 db.save_sql_dump(temp_file.path()).unwrap();
457
458 let reloaded = load_sql_dump(temp_file.path().to_str().unwrap())
459 .expect("schema must reload without bricking (issue #5613)");
460
461 assert!(reloaded.get_table("test2").is_some());
463 assert!(reloaded.get_index("test3").is_some());
464 }
465
466 #[test]
467 fn test_dump_with_table_index_collision_is_rejected_on_load() {
468 let temp_file = NamedTempFile::new().unwrap();
473 let sql_dump = r#"
474CREATE TABLE test2 (one TEXT);
475CREATE INDEX test3 ON test2 (one);
476CREATE TABLE test3 (two TEXT);
477"#;
478 fs::write(temp_file.path(), sql_dump).unwrap();
479
480 let err = load_sql_dump(temp_file.path().to_str().unwrap())
481 .expect_err("colliding dump must be rejected, not bricked")
482 .to_string();
483 assert!(
484 err.contains("there is already an index named test3"),
485 "expected index-collision wording, got: {}",
486 err
487 );
488 }
489
490 #[test]
491 fn test_load_dump_with_sqlite_prefixed_table_reloads_cleanly() {
492 let temp_file = NamedTempFile::new().unwrap();
500 let sql_dump = r#"
501CREATE TABLE sqlite_t3 (a BLOB, b BLOB, c BLOB);
502INSERT INTO sqlite_t3 VALUES (1, 2, 3);
503"#;
504 fs::write(temp_file.path(), sql_dump).unwrap();
505
506 let db = load_sql_dump(temp_file.path().to_str().unwrap())
507 .expect("a persisted sqlite_-prefixed table must reload, not brick (issue #5614)");
508
509 let table = db.get_table("sqlite_t3").expect("sqlite_t3 must exist after reload");
511 assert_eq!(table.row_count(), 1);
512 }
513
514 #[test]
515 fn test_user_create_sqlite_prefixed_table_still_rejected() {
516 let mut db = Database::new();
520 let stmt = vibesql_parser::Parser::parse_sql("CREATE TABLE sqlite_foo (x INTEGER)").unwrap();
521 if let vibesql_ast::Statement::CreateTable(s) = stmt {
522 let err = CreateTableExecutor::execute(&s, &mut db).unwrap_err();
523 assert_eq!(err.to_string(), "object name reserved for internal use: sqlite_foo");
524 } else {
525 panic!("expected CreateTable");
526 }
527 }
528
529 #[test]
530 fn test_alter_rename_to_reserved_name_rejected() {
531 use crate::AlterTableExecutor;
537
538 let mut db = Database::new();
539 let create = vibesql_parser::Parser::parse_sql("CREATE TABLE t3(a, b, c)").unwrap();
540 if let vibesql_ast::Statement::CreateTable(s) = create {
541 CreateTableExecutor::execute(&s, &mut db).unwrap();
542 }
543
544 let alter =
545 vibesql_parser::Parser::parse_sql("ALTER TABLE t3 RENAME TO sqlite_t3").unwrap();
546 if let vibesql_ast::Statement::AlterTable(s) = alter {
547 let err = AlterTableExecutor::execute(&s, &mut db).unwrap_err();
548 assert_eq!(err.to_string(), "object name reserved for internal use: sqlite_t3");
549 } else {
550 panic!("expected AlterTable");
551 }
552
553 assert!(db.get_table("t3").is_some());
556 assert!(db.get_table("sqlite_t3").is_none());
557 }
558
559 #[test]
560 fn test_load_with_roles() {
561 let temp_file = NamedTempFile::new().unwrap();
562 let sql_dump = r#"
563CREATE ROLE admin;
564CREATE ROLE user;
565CREATE TABLE data (id INTEGER);
566"#;
567
568 fs::write(temp_file.path(), sql_dump).unwrap();
569
570 let db = load_sql_dump(temp_file.path().to_str().unwrap()).unwrap();
571
572 assert!(db.get_table("DATA").is_some());
574 }
575
576 #[test]
582 fn test_dump_reload_preserves_original_case_and_quoted_keywords() {
583 let mut db = Database::new();
584 for sql in [
585 "CREATE TABLE \"create\" (f1 INT)",
586 "CREATE TABLE big (a, b)",
587 "CREATE TABLE \"MixedCase\" (\"ColA\" INT, \"B\" INT)",
588 ] {
589 let stmt = vibesql_parser::Parser::parse_sql(sql).unwrap();
590 if let vibesql_ast::Statement::CreateTable(s) = stmt {
591 CreateTableExecutor::execute(&s, &mut db).unwrap();
592 } else {
593 panic!("expected CreateTable for: {sql}");
594 }
595 }
596
597 let temp_file = NamedTempFile::new().unwrap();
598 db.save_sql_dump(temp_file.path()).unwrap();
599 let reloaded = load_sql_dump(temp_file.path().to_str().unwrap()).unwrap();
600
601 let create_tbl = reloaded.get_table("create").expect("quoted-keyword table must survive");
605 assert_eq!(
606 create_tbl.schema.name, "create",
607 "quoted keyword table name must round-trip verbatim (not folded to CREATE)"
608 );
609
610 let mixed = reloaded.get_table("mixedcase").expect("mixed-case table must survive");
611 assert_eq!(mixed.schema.name, "MixedCase", "mixed-case table name must be preserved");
612 assert_eq!(mixed.schema.columns[0].name, "ColA");
614 assert_eq!(mixed.schema.columns[1].name, "B");
615
616 let big = reloaded.get_table("big").expect("table must survive");
617 assert_eq!(big.schema.name, "big");
618 }
619}