1use std::path::Path;
2
3use anyhow::{Context, Result, bail, ensure};
4use rusqlite::{Connection, OpenFlags};
5
6use super::entry::{Migration, MigrationEntry, SqlMigration, apply_migration_and_verify_schema};
7use super::{MigratorBuilder, SchemaHash, SchemaHashes, schema};
8
9#[derive(Debug)]
31pub struct Migrator {
32 retired_migrations: Vec<SqlMigration>,
33 active_migrations: Vec<Migration>,
34 expected_schema_hashes: Vec<SchemaHash>,
35}
36
37impl Migrator {
38 pub(super) fn empty() -> Self {
43 Self {
44 retired_migrations: Vec::new(),
45 active_migrations: Vec::new(),
46 expected_schema_hashes: Vec::new(),
47 }
48 }
49
50 pub fn builder() -> Result<MigratorBuilder> {
52 MigratorBuilder::new()
53 }
54
55 pub(super) fn next_version(&self) -> usize {
60 self.expected_schema_hashes.len() + 1
61 }
62
63 pub(super) fn push_retired_unchecked(
70 &mut self,
71 migration: SqlMigration,
72 schema_hash: SchemaHash,
73 ) {
74 assert!(
75 self.active_migrations.is_empty(),
76 "cannot add retired migration after active migrations have started"
77 );
78 self.retired_migrations.push(migration);
79 self.expected_schema_hashes.push(schema_hash);
80 }
81
82 pub(super) fn push_active_unchecked(&mut self, migration: Migration, schema_hash: SchemaHash) {
88 self.active_migrations.push(migration);
89 self.expected_schema_hashes.push(schema_hash);
90 }
91
92 pub(super) fn validate(&self) -> Result<()> {
97 let migration_count = self.retired_migrations.len() + self.active_migrations.len();
98 ensure!(
99 !self.expected_schema_hashes.is_empty(),
100 "cannot build migrator without migrations"
101 );
102 ensure!(
103 self.expected_schema_hashes.len() == migration_count,
104 "migrator schema hash count {} must match migration count {migration_count}",
105 self.expected_schema_hashes.len()
106 );
107 Ok(())
108 }
109
110 pub fn schema_hashes(&self) -> SchemaHashes<'_> {
115 SchemaHashes(&self.expected_schema_hashes)
116 }
117
118 pub fn latest_version(&self) -> usize {
120 self.expected_schema_hashes.len()
121 }
122
123 pub fn create_database(
128 &self,
129 database_filepath: impl AsRef<Path>,
130 ) -> Result<MigrationDatabase<'_>> {
131 let database_filepath = database_filepath.as_ref();
132 ensure!(
133 !fs_err::exists(database_filepath).with_context(|| {
134 format!("failed to check database path {}", database_filepath.display())
135 })?,
136 "database already exists: {}",
137 database_filepath.display()
138 );
139
140 let conn = Connection::open_with_flags(
141 database_filepath,
142 OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
143 )
144 .with_context(|| format!("failed to create database {}", database_filepath.display()))?;
145
146 Ok(MigrationDatabase { migrator: self, conn })
147 }
148
149 pub fn open_database(
154 &self,
155 database_filepath: impl AsRef<Path>,
156 ) -> Result<MigrationDatabase<'_>> {
157 let database_filepath = existing_database_path(database_filepath.as_ref())?;
158 let conn =
159 Connection::open_with_flags(database_filepath, OpenFlags::SQLITE_OPEN_READ_WRITE)
160 .with_context(|| {
161 format!("failed to open database {}", database_filepath.display())
162 })?;
163 let current_version =
164 schema::get_version(&conn).context("failed to read database version")?;
165 ensure!(current_version > 0, "database has not been bootstrapped; run bootstrap first");
166
167 Ok(MigrationDatabase { migrator: self, conn })
168 }
169
170 pub fn verify_latest_schema(&self, database_filepath: impl AsRef<Path>) -> Result<()> {
179 let database_filepath = existing_database_path(database_filepath.as_ref())?;
180 let conn =
181 Connection::open_with_flags(database_filepath, OpenFlags::SQLITE_OPEN_READ_WRITE)
182 .with_context(|| {
183 format!("failed to open existing database {}", database_filepath.display())
184 })?;
185
186 self.verify_latest_connection_schema(&conn)
187 }
188
189 fn validate_target_version(&self, target_version: usize) -> Result<()> {
190 let latest_version = self.expected_schema_hashes.len();
191 ensure!(
192 (1..=latest_version).contains(&target_version),
193 "target version {target_version} must be between 1 and {latest_version}"
194 );
195 ensure!(
196 target_version >= self.retired_migrations.len(),
197 "target version {target_version} is inside the retired migration range"
198 );
199 Ok(())
200 }
201
202 fn apply_missing_migrations(
203 &self,
204 conn: &mut Connection,
205 current_version: usize,
206 target_version: usize,
207 ) -> Result<()> {
208 let retired_versions = self.retired_migrations.len();
209
210 let mut applied_version = current_version;
211 if applied_version == 0 {
212 for (idx, migration) in self.retired_migrations.iter().enumerate() {
213 let version = idx + 1;
214 self.apply_migration(conn, version, migration)?;
215 applied_version = version;
216 }
217 }
218
219 let active_start = applied_version.saturating_sub(retired_versions);
220 for (idx, migration) in self
221 .active_migrations
222 .iter()
223 .enumerate()
224 .take(target_version - retired_versions)
225 .skip(active_start)
226 {
227 let version = retired_versions + idx + 1;
228 self.apply_migration(conn, version, migration)?;
229 }
230
231 Ok(())
232 }
233
234 fn verify_latest_connection_schema(&self, conn: &Connection) -> Result<()> {
235 let current_version = self.version_check(conn)?;
236 let total_versions = self.expected_schema_hashes.len();
237
238 ensure!(
239 current_version == total_versions,
240 "database version {current_version} is older than migrator version {total_versions}; \
241 run the migrate command first"
242 );
243
244 Ok(())
245 }
246
247 fn version_check(&self, conn: &Connection) -> Result<usize> {
253 let current_version =
254 schema::get_version(conn).context("failed to read database version")?;
255 let total_versions = self.expected_schema_hashes.len();
256
257 ensure!(
258 current_version <= total_versions,
259 "database version {current_version} is newer than migrator version {total_versions}"
260 );
261
262 let retired_versions = self.retired_migrations.len();
263 if current_version > 0 && current_version < retired_versions {
264 let name = self.migration_name(current_version).unwrap_or("<unknown>");
265 bail!(
266 "database version {current_version} \"{name}\" is inside the retired migration \
267 range; retired migrations can only initialize new databases"
268 );
269 }
270
271 if current_version > 0 {
272 self.verify_current_schema(conn, current_version)?;
273 }
274
275 Ok(current_version)
276 }
277
278 fn apply_migration(
280 &self,
281 conn: &mut Connection,
282 version: usize,
283 migration: &impl MigrationEntry,
284 ) -> Result<()> {
285 let name = migration.name();
286 let expected = self.expected_schema_hashes[version - 1];
287 apply_migration_and_verify_schema(conn, version, migration, expected)
288 .with_context(|| format!("failed to apply migration {version} \"{name}\""))
289 }
290
291 fn verify_current_schema(&self, conn: &Connection, version: usize) -> Result<()> {
293 let name = self.migration_name(version).unwrap_or("<unknown>");
294 let expected = self.expected_schema_hashes[version - 1];
295 let actual = SchemaHash::new(conn).with_context(|| {
296 format!("failed to compute schema hash at database version {version} \"{name}\"")
297 })?;
298
299 ensure!(
300 actual == expected,
301 "schema hash mismatch at database version {version} \"{name}\": expected {expected}, \
302 got {actual}"
303 );
304 Ok(())
305 }
306
307 fn migration_name(&self, version: usize) -> Option<&'static str> {
309 if version == 0 {
310 return None;
311 }
312
313 if version <= self.retired_migrations.len() {
314 return Some(self.retired_migrations[version - 1].name());
315 }
316
317 self.active_migrations
318 .get(version - self.retired_migrations.len() - 1)
319 .map(MigrationEntry::name)
320 }
321}
322
323#[derive(Debug)]
328pub struct MigrationDatabase<'a> {
329 migrator: &'a Migrator,
330 conn: Connection,
331}
332
333impl MigrationDatabase<'_> {
334 pub fn migrate_to(&mut self, target_version: usize) -> Result<()> {
341 self.migrator.validate_target_version(target_version)?;
342 let current_version = self.migrator.version_check(&self.conn)?;
343 ensure!(
344 target_version >= current_version,
345 "target version {target_version} is older than database version {current_version}"
346 );
347 self.migrator
348 .apply_missing_migrations(&mut self.conn, current_version, target_version)
349 }
350}
351
352fn existing_database_path(database_filepath: &Path) -> Result<&Path> {
353 let metadata = fs_err::metadata(database_filepath)
354 .with_context(|| format!("failed to read database {}", database_filepath.display()))?;
355 ensure!(
356 metadata.is_file(),
357 "database path is not a file: {}",
358 database_filepath.display()
359 );
360 Ok(database_filepath)
361}
362
363#[cfg(test)]
364mod tests {
365 use std::path::{Path, PathBuf};
366
367 use anyhow::Result;
368 use rusqlite::{Connection, Transaction};
369
370 use super::super::{Migrator, schema};
371
372 fn add_items_index(tx: &Transaction<'_>) -> Result<()> {
373 tx.execute_batch("CREATE INDEX idx_items_value ON items(value);")?;
374 Ok(())
375 }
376
377 fn add_item_height(tx: &Transaction<'_>) -> Result<()> {
378 tx.execute_batch("ALTER TABLE items ADD COLUMN height INTEGER;")?;
379 Ok(())
380 }
381
382 fn create_extra_table_when_items_exist(tx: &Transaction<'_>) -> Result<()> {
383 let item_count: i64 = tx.query_row("SELECT COUNT(*) FROM items", [], |row| row.get(0))?;
384 if item_count > 0 {
385 tx.execute_batch("CREATE TABLE unexpected (id INTEGER PRIMARY KEY);")?;
386 }
387 Ok(())
388 }
389
390 fn create_items_table(tx: &Transaction<'_>) -> Result<()> {
391 tx.execute_batch("CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);")?;
392 Ok(())
393 }
394
395 fn object_exists(conn: &Connection, name: &str) -> Result<bool> {
396 let exists = conn.query_row(
397 "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE name = ?1)",
398 [name],
399 |row| row.get::<_, bool>(0),
400 )?;
401 Ok(exists)
402 }
403
404 struct TestDatabase {
405 path: PathBuf,
406 }
407
408 impl TestDatabase {
409 fn new(name: &str) -> Self {
410 let path = std::env::temp_dir()
411 .join(format!("miden-node-db-migrator-{name}-{}.sqlite3", std::process::id()));
412 let db = Self { path };
413 db.remove_files();
414 db
415 }
416
417 fn path(&self) -> &Path {
418 &self.path
419 }
420
421 fn open(&self) -> Result<Connection> {
422 Connection::open(&self.path).map_err(Into::into)
423 }
424
425 fn remove_files(&self) {
426 let _ = fs_err::remove_file(&self.path);
427 let _ = fs_err::remove_file(self.path.with_extension("sqlite3-wal"));
428 let _ = fs_err::remove_file(self.path.with_extension("sqlite3-shm"));
429 }
430 }
431
432 impl Drop for TestDatabase {
433 fn drop(&mut self) {
434 self.remove_files();
435 }
436 }
437
438 #[test]
439 fn applies_only_migrations_through_target_version() -> Result<()> {
440 let migrator = Migrator::builder()?
441 .push_retired(
442 "create items",
443 "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);",
444 )?
445 .push_code("index item values", add_items_index)?
446 .push_code("add item height", add_item_height)?
447 .build()?;
448 let db = TestDatabase::new("applies_only_migrations_through_target_version");
449
450 migrator.create_database(db.path())?.migrate_to(1)?;
451 let conn = db.open()?;
452 assert_eq!(schema::get_version(&conn)?, 1);
453 assert!(!object_exists(&conn, "idx_items_value")?);
454 conn.execute("INSERT INTO items (id, value) VALUES (1, 'preserved')", [])?;
455
456 let mut database = migrator.open_database(db.path())?;
457 assert_eq!(schema::get_version(&conn)?, 1);
458 assert!(!object_exists(&conn, "idx_items_value")?);
459 database.migrate_to(2)?;
460 database.migrate_to(2)?;
461 assert_eq!(schema::get_version(&conn)?, 2);
462 assert!(object_exists(&conn, "idx_items_value")?);
463 assert!(conn.prepare("SELECT height FROM items").is_err());
464 assert_eq!(
465 conn.query_row("SELECT value FROM items WHERE id = 1", [], |row| row
466 .get::<_, String>(0))?,
467 "preserved"
468 );
469
470 migrator.open_database(db.path())?.migrate_to(migrator.latest_version())?;
471 assert_eq!(schema::get_version(&conn)?, 3);
472 conn.execute("UPDATE items SET height = 10 WHERE id = 1", [])?;
473 migrator.verify_latest_schema(db.path())?;
474 Ok(())
475 }
476
477 #[test]
478 fn rejects_invalid_targets_without_changing_database() -> Result<()> {
479 let migrator = Migrator::builder()?
480 .push_retired(
481 "create items",
482 "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);",
483 )?
484 .push_retired("create notes", "CREATE TABLE notes (id INTEGER PRIMARY KEY);")?
485 .push_code("index item values", add_items_index)?
486 .push_code("add item height", add_item_height)?
487 .build()?;
488 let db = TestDatabase::new("rejects_invalid_targets_without_changing_database");
489
490 let mut database = migrator.create_database(db.path())?;
491 let conn = db.open()?;
492 assert_eq!(schema::get_version(&conn)?, 0);
493 assert!(!object_exists(&conn, "items")?);
494 for target in [0, 1, 5] {
495 assert!(database.migrate_to(target).is_err());
496 assert_eq!(schema::get_version(&conn)?, 0);
497 assert!(!object_exists(&conn, "items")?);
498 }
499
500 database.migrate_to(3)?;
501 for target in [0, 1, 2, 5] {
502 assert!(migrator.open_database(db.path())?.migrate_to(target).is_err());
503 assert_eq!(schema::get_version(&conn)?, 3);
504 assert!(object_exists(&conn, "idx_items_value")?);
505 assert!(conn.prepare("SELECT height FROM items").is_err());
506 }
507 Ok(())
508 }
509
510 #[test]
511 fn bounded_migration_rejects_schema_drift_even_at_target() -> Result<()> {
512 let migrator = Migrator::builder()?
513 .push_code("create items", create_items_table)?
514 .push_code("index item values", add_items_index)?
515 .build()?;
516 let db = TestDatabase::new("bounded_migration_rejects_schema_drift_even_at_target");
517 migrator.create_database(db.path())?.migrate_to(1)?;
518 let conn = db.open()?;
519 conn.execute_batch("CREATE TABLE unexpected (id INTEGER PRIMARY KEY);")?;
520
521 for target in [1, 2] {
522 let err = migrator
523 .open_database(db.path())?
524 .migrate_to(target)
525 .expect_err("schema drift should fail");
526 assert!(err.to_string().contains("schema hash mismatch"));
527 assert_eq!(schema::get_version(&conn)?, 1);
528 assert!(!object_exists(&conn, "idx_items_value")?);
529 }
530 Ok(())
531 }
532
533 #[test]
534 fn bootstraps_new_database_through_retired_and_code() -> Result<()> {
535 let migrator = Migrator::builder()?
536 .push_retired(
537 "create items",
538 "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);",
539 )?
540 .push_code("add item height", add_item_height)?
541 .build()?;
542
543 let db = TestDatabase::new("bootstraps_new_database_through_retired_and_code");
544 migrator.create_database(db.path())?.migrate_to(migrator.latest_version())?;
545
546 let conn = db.open()?;
547 assert_eq!(schema::get_version(&conn)?, 2);
548 conn.execute("INSERT INTO items (id, value, height) VALUES (1, 'a', 10)", [])?;
549 Ok(())
550 }
551
552 #[test]
553 fn bootstraps_new_database_with_code_only_migration() -> Result<()> {
554 let migrator =
555 Migrator::builder()?.push_code("create items", create_items_table)?.build()?;
556
557 let db = TestDatabase::new("bootstraps_new_database_with_code_only_migration");
558 migrator.create_database(db.path())?.migrate_to(migrator.latest_version())?;
559
560 let conn = db.open()?;
561 assert_eq!(schema::get_version(&conn)?, 1);
562 conn.execute("INSERT INTO items (id, value) VALUES (1, 'a')", [])?;
563 Ok(())
564 }
565
566 #[test]
567 fn bootstraps_new_database_with_sql_only_migration() -> Result<()> {
568 let migrator = Migrator::builder()?
569 .push_sql("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);")?
570 .build()?;
571
572 let db = TestDatabase::new("bootstraps_new_database_with_sql_only_migration");
573 migrator.create_database(db.path())?.migrate_to(migrator.latest_version())?;
574
575 let conn = db.open()?;
576 assert_eq!(schema::get_version(&conn)?, 1);
577 conn.execute("INSERT INTO items (id, value) VALUES (1, 'a')", [])?;
578 Ok(())
579 }
580
581 #[test]
582 fn applies_missing_code_migrations_to_existing_database() -> Result<()> {
583 let migrator = Migrator::builder()?
584 .push_retired(
585 "create items",
586 "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);",
587 )?
588 .push_code("index item values", add_items_index)?
589 .build()?;
590
591 let db = TestDatabase::new("applies_missing_code_migrations_to_existing_database");
592 {
593 let conn = db.open()?;
594 conn.execute_batch(
595 "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);
596 PRAGMA user_version = 1;",
597 )?;
598 }
599
600 migrator.open_database(db.path())?.migrate_to(migrator.latest_version())?;
601
602 let conn = db.open()?;
603 assert_eq!(schema::get_version(&conn)?, 2);
604 assert!(object_exists(&conn, "idx_items_value")?);
605 Ok(())
606 }
607
608 #[test]
609 fn create_database_rejects_existing_database_file() -> Result<()> {
610 let migrator = Migrator::builder()?
611 .push_sql("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")?
612 .build()?;
613
614 let db = TestDatabase::new("create_database_rejects_existing_database_file");
615 {
616 let _conn = db.open()?;
617 }
618
619 let err = migrator.create_database(db.path()).expect_err("existing database should fail");
620 assert!(err.to_string().contains("database already exists"));
621 Ok(())
622 }
623
624 #[test]
625 fn open_database_rejects_missing_database() -> Result<()> {
626 let migrator = Migrator::builder()?
627 .push_sql("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")?
628 .build()?;
629
630 let db = TestDatabase::new("migrate_rejects_missing_database");
631
632 let err = migrator.open_database(db.path()).expect_err("missing database should fail");
633 assert!(err.to_string().contains("failed to read database"));
634 assert!(!db.path().exists());
635 Ok(())
636 }
637
638 #[test]
639 fn open_database_rejects_unbootstrapped_database() -> Result<()> {
640 let migrator = Migrator::builder()?
641 .push_sql("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")?
642 .build()?;
643
644 let db = TestDatabase::new("migrate_rejects_unbootstrapped_database");
645 {
646 let _conn = db.open()?;
647 }
648
649 let err = migrator
650 .open_database(db.path())
651 .expect_err("unbootstrapped database should fail");
652 assert!(err.to_string().contains("database has not been bootstrapped"));
653 Ok(())
654 }
655
656 #[test]
657 fn rejects_existing_database_inside_retired_migration_range() -> Result<()> {
658 let migrator = Migrator::builder()?
659 .push_retired("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")?
660 .push_retired("create notes", "CREATE TABLE notes (id INTEGER PRIMARY KEY);")?
661 .build()?;
662
663 let db = TestDatabase::new("rejects_existing_database_inside_retired_migration_range");
664 {
665 let conn = db.open()?;
666 conn.execute_batch(
667 "CREATE TABLE items (id INTEGER PRIMARY KEY);
668 PRAGMA user_version = 1;",
669 )?;
670 }
671
672 let err = migrator
673 .open_database(db.path())?
674 .migrate_to(migrator.latest_version())
675 .expect_err("migration should fail");
676 assert!(err.to_string().contains("inside the retired migration range"));
677 Ok(())
678 }
679
680 #[test]
681 fn verifies_current_schema_before_applying_missing_migrations() -> Result<()> {
682 let migrator = Migrator::builder()?
683 .push_retired("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")?
684 .build()?;
685
686 let db = TestDatabase::new("verifies_current_schema_before_applying_missing_migrations");
687 migrator.create_database(db.path())?.migrate_to(migrator.latest_version())?;
688 {
689 let conn = db.open()?;
690 conn.execute_batch("CREATE TABLE tampered (id INTEGER PRIMARY KEY);")?;
691 }
692
693 let err = migrator
694 .open_database(db.path())?
695 .migrate_to(migrator.latest_version())
696 .expect_err("migration should fail");
697 assert!(err.to_string().contains("schema hash mismatch at database version 1"));
698 Ok(())
699 }
700
701 #[test]
702 fn rolls_back_code_migration_when_schema_hash_mismatches() -> Result<()> {
703 let migrator = Migrator::builder()?
704 .push_retired("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")?
705 .push_code("conditionally create extra", create_extra_table_when_items_exist)?
706 .build()?;
707
708 let db = TestDatabase::new("rolls_back_code_migration_when_schema_hash_mismatches");
709 {
710 let conn = db.open()?;
711 conn.execute_batch(
712 "CREATE TABLE items (id INTEGER PRIMARY KEY);
713 INSERT INTO items (id) VALUES (1);
714 PRAGMA user_version = 1;",
715 )?;
716 }
717
718 let err = migrator
719 .open_database(db.path())?
720 .migrate_to(migrator.latest_version())
721 .expect_err("migration should fail");
722 assert!(err.to_string().contains("failed to apply migration 2"));
723 assert!(err.chain().any(|cause| cause.to_string().contains("schema hash mismatch")));
724
725 let conn = db.open()?;
726 assert_eq!(schema::get_version(&conn)?, 1);
727 assert!(!object_exists(&conn, "unexpected")?);
728 Ok(())
729 }
730
731 #[test]
732 fn verify_latest_schema_accepts_current_database() -> Result<()> {
733 let migrator = Migrator::builder()?
734 .push_sql("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")?
735 .build()?;
736
737 let db = TestDatabase::new("verify_latest_schema_accepts_current_database");
738 migrator.create_database(db.path())?.migrate_to(migrator.latest_version())?;
739
740 migrator.verify_latest_schema(db.path())?;
741 Ok(())
742 }
743
744 #[test]
745 fn verify_latest_schema_rejects_schema_hash_mismatch() -> Result<()> {
746 let migrator = Migrator::builder()?
747 .push_sql("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")?
748 .build()?;
749
750 let db = TestDatabase::new("verify_latest_schema_rejects_schema_hash_mismatch");
751 {
752 let conn = db.open()?;
753 conn.execute_batch(
754 "CREATE TABLE different (id INTEGER PRIMARY KEY);
755 PRAGMA user_version = 1;",
756 )?;
757 }
758
759 let err = migrator.verify_latest_schema(db.path()).expect_err("schema drift should fail");
760 assert!(err.to_string().contains("schema hash mismatch"));
761 Ok(())
762 }
763
764 #[test]
765 fn verify_latest_schema_rejects_missing_migrations_without_applying_them() -> Result<()> {
766 let migrator = Migrator::builder()?
767 .push_sql("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);")?
768 .push_code("index item values", add_items_index)?
769 .build()?;
770
771 let db = TestDatabase::new("verify_latest_schema_rejects_missing_migrations");
772 {
773 let conn = db.open()?;
774 conn.execute_batch(
775 "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);
776 PRAGMA user_version = 1;",
777 )?;
778 }
779
780 let err = migrator.verify_latest_schema(db.path()).expect_err("old database should fail");
781 assert!(err.to_string().contains("run the migrate command first"));
782
783 let conn = db.open()?;
784 assert_eq!(schema::get_version(&conn)?, 1);
785 assert!(!object_exists(&conn, "idx_items_value")?);
786 Ok(())
787 }
788
789 #[test]
790 fn verify_latest_schema_rejects_missing_database_without_creating_it() -> Result<()> {
791 let migrator = Migrator::builder()?
792 .push_sql("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")?
793 .build()?;
794
795 let db = TestDatabase::new("verify_latest_schema_rejects_missing_database");
796
797 let err = migrator
798 .verify_latest_schema(db.path())
799 .expect_err("missing database should fail");
800 assert!(err.to_string().contains("failed to read database"));
801 assert!(!db.path().exists());
802 Ok(())
803 }
804}