diff --git a/Cargo.lock b/Cargo.lock index 295c3a83c52e3b355a8e43e9d36c09149fdc694f..f4c94f8078b1ab392ed1a50e15c71dab1921f0a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15347,6 +15347,7 @@ dependencies = [ "futures 0.3.31", "indoc", "libsqlite3-sys", + "log", "parking_lot", "smol", "sqlformat", diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml index 16a3adebae24e0573f9e7ada18bc9259ff588ad1..6eb75aa171979283325d22300f95d584cee2cffb 100644 --- a/crates/sqlez/Cargo.toml +++ b/crates/sqlez/Cargo.toml @@ -14,6 +14,7 @@ collections.workspace = true futures.workspace = true indoc.workspace = true libsqlite3-sys.workspace = true +log.workspace = true parking_lot.workspace = true smol.workspace = true sqlformat.workspace = true diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index 2429ddeb4127591b56fb74a9c84884d9dc5f378f..567d82f9afe22ea4ab126c0989891c5d603879fd 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -59,6 +59,7 @@ impl Connection { let mut store_completed_migration = self .exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?; + let mut did_migrate = false; for (index, migration) in migrations.iter().enumerate() { let migration = sqlformat::format(migration, &sqlformat::QueryParams::None, Default::default()); @@ -70,9 +71,7 @@ impl Connection { &sqlformat::QueryParams::None, Default::default(), ); - if completed_migration == migration - || migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE") - { + if completed_migration == migration { // Migration already run. Continue continue; } else if should_allow_migration_change(index, &completed_migration, &migration) @@ -91,12 +90,58 @@ impl Connection { } self.eager_exec(&migration)?; + did_migrate = true; store_completed_migration((domain, index, migration))?; } + if did_migrate { + self.delete_rows_with_orphaned_foreign_key_references()?; + self.exec("PRAGMA foreign_key_check;")?()?; + } + Ok(()) }) } + + /// Delete any rows that were orphaned by a migration. This is needed + /// because we disable foreign key constraints during migrations, so + /// that it's possible to re-create a table with the same name, without + /// deleting all associated data. + fn delete_rows_with_orphaned_foreign_key_references(&self) -> Result<()> { + let foreign_key_info: Vec<(String, String, String, String)> = self.select( + r#" + SELECT DISTINCT + schema.name as child_table, + foreign_keys.[from] as child_key, + foreign_keys.[table] as parent_table, + foreign_keys.[to] as parent_key + FROM sqlite_schema schema + JOIN pragma_foreign_key_list(schema.name) foreign_keys + WHERE + schema.type = 'table' AND + schema.name NOT LIKE "sqlite_%" + "#, + )?()?; + + if !foreign_key_info.is_empty() { + log::info!( + "Found {} foreign key relationships to check", + foreign_key_info.len() + ); + } + + for (child_table, child_key, parent_table, parent_key) in foreign_key_info { + self.exec(&format!( + " + DELETE FROM {child_table} + WHERE {child_key} IS NOT NULL and {child_key} NOT IN + (SELECT {parent_key} FROM {parent_table}) + " + ))?()?; + } + + Ok(()) + } } #[cfg(test)] diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index 58d3afe78fb4d8b211c48c0ae1f9f72af74ad5c1..482905ac817bf94fcb64cb858b784c94283b686c 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -95,6 +95,14 @@ impl ThreadSafeConnectionBuilder { let mut migration_result = anyhow::Result::<()>::Err(anyhow::anyhow!("Migration never run")); + let foreign_keys_enabled: bool = + connection.select_row::("PRAGMA foreign_keys")?() + .unwrap_or(None) + .map(|enabled| enabled != 0) + .unwrap_or(false); + + connection.exec("PRAGMA foreign_keys = OFF;")?()?; + for _ in 0..MIGRATION_RETRIES { migration_result = connection .with_savepoint("thread_safe_multi_migration", || M::migrate(connection)); @@ -104,6 +112,9 @@ impl ThreadSafeConnectionBuilder { } } + if foreign_keys_enabled { + connection.exec("PRAGMA foreign_keys = ON;")?()?; + } migration_result }) .await?; diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index d674f6dd4d56ba95a664ac7d9e4ebf25969e2125..797c4796830ff767a0213058c417bb3a764c6bec 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -699,6 +699,9 @@ impl Domain for WorkspaceDb { PRIMARY KEY (workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) ) STRICT;), + sql!( + DROP TABLE ssh_connections; + ), ]; // Allow recovering from bad migration that was initially shipped to nightly