Allow whitelisting certain migration changes

This commit is contained in:
Max Brunsfeld 2025-08-25 15:01:39 -07:00
parent c088c83f4d
commit 5abe0134ad
3 changed files with 62 additions and 18 deletions

View file

@ -3,6 +3,10 @@ use crate::connection::Connection;
pub trait Domain: 'static { pub trait Domain: 'static {
const NAME: &str; const NAME: &str;
const MIGRATIONS: &[&str]; const MIGRATIONS: &[&str];
fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool {
false
}
} }
pub trait Migrator: 'static { pub trait Migrator: 'static {
@ -17,7 +21,11 @@ impl Migrator for () {
impl<D: Domain> Migrator for D { impl<D: Domain> Migrator for D {
fn migrate(connection: &Connection) -> anyhow::Result<()> { fn migrate(connection: &Connection) -> anyhow::Result<()> {
connection.migrate(Self::NAME, Self::MIGRATIONS) connection.migrate(
Self::NAME,
Self::MIGRATIONS,
Self::should_allow_migration_change,
)
} }
} }

View file

@ -34,7 +34,12 @@ impl Connection {
/// Note: Unlike everything else in SQLez, migrations are run eagerly, without first /// Note: Unlike everything else in SQLez, migrations are run eagerly, without first
/// preparing the SQL statements. This makes it possible to do multi-statement schema /// preparing the SQL statements. This makes it possible to do multi-statement schema
/// updates in a single string without running into prepare errors. /// updates in a single string without running into prepare errors.
pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> { pub fn migrate(
&self,
domain: &'static str,
migrations: &[&'static str],
mut should_allow_migration_change: impl FnMut(usize, &str, &str) -> bool,
) -> Result<()> {
self.with_savepoint("migrating", || { self.with_savepoint("migrating", || {
// Setup the migrations table unconditionally // Setup the migrations table unconditionally
self.exec(indoc! {" self.exec(indoc! {"
@ -70,6 +75,9 @@ impl Connection {
{ {
// Migration already run. Continue // Migration already run. Continue
continue; continue;
} else if should_allow_migration_change(index, &completed_migration, &migration)
{
continue;
} else { } else {
anyhow::bail!(formatdoc! {" anyhow::bail!(formatdoc! {"
Migration changed for {domain} at step {index} Migration changed for {domain} at step {index}
@ -110,6 +118,7 @@ mod test {
a TEXT, a TEXT,
b TEXT b TEXT
)"}], )"}],
disallow_migration_change,
) )
.unwrap(); .unwrap();
@ -138,6 +147,7 @@ mod test {
d TEXT d TEXT
)"}, )"},
], ],
disallow_migration_change,
) )
.unwrap(); .unwrap();
@ -216,7 +226,11 @@ mod test {
// Run the migration verifying that the row got dropped // Run the migration verifying that the row got dropped
connection connection
.migrate("test", &["DELETE FROM test_table"]) .migrate(
"test",
&["DELETE FROM test_table"],
disallow_migration_change,
)
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
connection connection
@ -234,7 +248,11 @@ mod test {
// Run the same migration again and verify that the table was left unchanged // Run the same migration again and verify that the table was left unchanged
connection connection
.migrate("test", &["DELETE FROM test_table"]) .migrate(
"test",
&["DELETE FROM test_table"],
disallow_migration_change,
)
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
connection connection
@ -254,27 +272,28 @@ mod test {
.migrate( .migrate(
"test migration", "test migration",
&[ &[
indoc! {" "CREATE TABLE test (col INTEGER)",
CREATE TABLE test ( "INSERT INTO test (col) VALUES (1)",
col INTEGER
)"},
indoc! {"
INSERT INTO test (col) VALUES (1)"},
], ],
disallow_migration_change,
) )
.unwrap(); .unwrap();
let mut migration_changed = false;
// Create another migration with the same domain but different steps // Create another migration with the same domain but different steps
let second_migration_result = connection.migrate( let second_migration_result = connection.migrate(
"test migration", "test migration",
&[ &[
indoc! {" "CREATE TABLE test (color INTEGER )",
CREATE TABLE test ( "INSERT INTO test (color) VALUES (1)",
color INTEGER
)"},
indoc! {"
INSERT INTO test (color) VALUES (1)"},
], ],
|_, old, new| {
assert_eq!(old, "CREATE TABLE test (col INTEGER)");
assert_eq!(new, "CREATE TABLE test (color INTEGER)");
migration_changed = true;
false
},
); );
// Verify new migration returns error when run // Verify new migration returns error when run
@ -286,7 +305,11 @@ mod test {
let connection = Connection::open_memory(Some("test_create_alter_drop")); let connection = Connection::open_memory(Some("test_create_alter_drop"));
connection connection
.migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"]) .migrate(
"first_migration",
&["CREATE TABLE table1(a TEXT) STRICT;"],
disallow_migration_change,
)
.unwrap(); .unwrap();
connection connection
@ -307,6 +330,7 @@ mod test {
ALTER TABLE table2 RENAME TO table1; ALTER TABLE table2 RENAME TO table1;
"}], "}],
disallow_migration_change,
) )
.unwrap(); .unwrap();
@ -314,4 +338,8 @@ mod test {
assert_eq!(res, "test text"); assert_eq!(res, "test text");
} }
fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool {
false
}
} }

View file

@ -553,7 +553,7 @@ impl Domain for WorkspaceDb {
WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
NULL NULL
ELSE ELSE
json('[' || '"' || replace(workspaces.local_paths_array, ',', '"' || "," || '"') || '"' || ']') replace(workspaces.local_paths_array, ',', '\0')
END END
END as paths, END as paths,
@ -606,6 +606,12 @@ impl Domain for WorkspaceDb {
CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
), ),
]; ];
// Allow recovering from bad migration
fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool {
old.starts_with("CREATE TABLE ssh_connections")
&& new.starts_with("CREATE TABLE ssh_connections")
}
} }
db::static_connection!(DB, WorkspaceDb, []); db::static_connection!(DB, WorkspaceDb, []);
@ -1812,6 +1818,7 @@ mod tests {
ON DELETE CASCADE ON DELETE CASCADE
) STRICT; ) STRICT;
)], )],
|_, _, _| false,
) )
.unwrap(); .unwrap();
}) })
@ -1860,6 +1867,7 @@ mod tests {
REFERENCES workspaces(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE ON DELETE CASCADE
) STRICT;)], ) STRICT;)],
|_, _, _| false,
) )
}) })
.await .await