| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
use std::path::{Path, PathBuf}; |
| 11 |
|
| 12 |
use rusqlite::Connection; |
| 13 |
use sha2::{Digest, Sha384}; |
| 14 |
|
| 15 |
|
| 16 |
const WITHHELD: usize = 3; |
| 17 |
|
| 18 |
fn migrations_dir() -> PathBuf { |
| 19 |
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../migrations/sqlite") |
| 20 |
} |
| 21 |
|
| 22 |
|
| 23 |
fn migration_files() -> Vec<(i64, String, String)> { |
| 24 |
let mut out: Vec<(i64, String, String)> = std::fs::read_dir(migrations_dir()) |
| 25 |
.expect("read the migrations directory") |
| 26 |
.filter_map(|entry| { |
| 27 |
let path = entry.expect("read a directory entry").path(); |
| 28 |
if path.extension().is_none_or(|ext| ext != "sql") { |
| 29 |
return None; |
| 30 |
} |
| 31 |
let stem = path.file_stem()?.to_str()?.to_owned(); |
| 32 |
let (version, description) = stem.split_once('_')?; |
| 33 |
Some(( |
| 34 |
version.parse::<i64>().expect("a numeric migration prefix"), |
| 35 |
description.to_owned(), |
| 36 |
std::fs::read_to_string(&path).expect("read a migration"), |
| 37 |
)) |
| 38 |
}) |
| 39 |
.collect(); |
| 40 |
out.sort_by_key(|(version, _, _)| *version); |
| 41 |
assert!( |
| 42 |
out.len() > WITHHELD, |
| 43 |
"not enough migrations to withhold any" |
| 44 |
); |
| 45 |
out |
| 46 |
} |
| 47 |
|
| 48 |
|
| 49 |
fn applied(conn: &Connection) -> Vec<i64> { |
| 50 |
let mut stmt = conn |
| 51 |
.prepare("SELECT version FROM _sqlx_migrations ORDER BY version") |
| 52 |
.expect("prepare the ledger read"); |
| 53 |
let rows = stmt |
| 54 |
.query_map([], |row| row.get::<_, i64>(0)) |
| 55 |
.expect("query the ledger"); |
| 56 |
rows.collect::<Result<_, _>>().expect("collect the ledger") |
| 57 |
} |
| 58 |
|
| 59 |
|
| 60 |
fn seed_old_schema(path: &Path, files: &[(i64, String, String)]) { |
| 61 |
let conn = Connection::open(path).expect("open the seed database"); |
| 62 |
conn.execute_batch( |
| 63 |
"CREATE TABLE IF NOT EXISTS _sqlx_migrations ( |
| 64 |
version BIGINT PRIMARY KEY, |
| 65 |
description TEXT NOT NULL, |
| 66 |
installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| 67 |
success BOOLEAN NOT NULL, |
| 68 |
checksum BLOB NOT NULL, |
| 69 |
execution_time BIGINT NOT NULL |
| 70 |
);", |
| 71 |
) |
| 72 |
.expect("create the ledger"); |
| 73 |
|
| 74 |
for (version, description, sql) in &files[..files.len() - WITHHELD] { |
| 75 |
conn.execute_batch(sql) |
| 76 |
.unwrap_or_else(|e| panic!("apply migration {version}: {e}")); |
| 77 |
conn.execute( |
| 78 |
"INSERT INTO _sqlx_migrations \ |
| 79 |
(version, description, success, checksum, execution_time) \ |
| 80 |
VALUES (?1, ?2, TRUE, ?3, 0)", |
| 81 |
rusqlite::params![ |
| 82 |
version, |
| 83 |
description, |
| 84 |
Sha384::digest(sql.as_bytes()).to_vec() |
| 85 |
], |
| 86 |
) |
| 87 |
.expect("record the migration"); |
| 88 |
} |
| 89 |
} |
| 90 |
|
| 91 |
#[test] |
| 92 |
fn startup_migrates_a_database_left_at_an_older_schema() { |
| 93 |
let files = migration_files(); |
| 94 |
let dir = std::env::temp_dir().join(format!("go-mcp-startup-{}", uuid::Uuid::new_v4())); |
| 95 |
std::fs::create_dir_all(&dir).expect("make a scratch directory"); |
| 96 |
let path = dir.join("goingson.db"); |
| 97 |
seed_old_schema(&path, &files); |
| 98 |
|
| 99 |
|
| 100 |
let seeded = applied(&Connection::open(&path).expect("reopen the seed")); |
| 101 |
assert_eq!(seeded.len(), files.len() - WITHHELD); |
| 102 |
|
| 103 |
let db = goingson_db_sqlite::init_pool(Some(&path.to_string_lossy())).expect("open the pool"); |
| 104 |
go_mcp::startup::migrate(&db, &path).expect("startup migrates the gap away"); |
| 105 |
|
| 106 |
let after = applied(&db.conn().expect("check out a connection")); |
| 107 |
let expected: Vec<i64> = files.iter().map(|(version, _, _)| *version).collect(); |
| 108 |
assert_eq!(after, expected, "startup left migrations unapplied"); |
| 109 |
|
| 110 |
|
| 111 |
go_mcp::startup::migrate(&db, &path).expect("second startup is clean"); |
| 112 |
assert_eq!( |
| 113 |
applied(&db.conn().expect("check out a connection")), |
| 114 |
expected |
| 115 |
); |
| 116 |
|
| 117 |
drop(db); |
| 118 |
let _ = std::fs::remove_dir_all(&dir); |
| 119 |
} |
| 120 |
|