diff --git a/migrations/sqlite/2026-08-18-132346-0000_channel/down.sql b/migrations/sqlite/2026-08-18-132346-0000_channel/down.sql index c57599d..f387fe3 100644 --- a/migrations/sqlite/2026-08-18-132346-0000_channel/down.sql +++ b/migrations/sqlite/2026-08-18-132346-0000_channel/down.sql @@ -1,8 +1,17 @@ -DROP TABLE IF EXISTS channel; -CREATE TABLE channel +PRAGMA foreign_keys = OFF; +BEGIN; + +CREATE TABLE channel_temp ( id VARCHAR(24) PRIMARY KEY NOT NULL, name VARCHAR NOT NULL, avatar VARCHAR NOT NULL, verified BOOLEAN NOT NULL ); +INSERT INTO channel_temp +SELECT id, name, COALESCE(avatar, ''), verified FROM channel; +DROP TABLE channel; +ALTER TABLE channel_temp RENAME TO channel; + +COMMIT; +PRAGMA foreign_keys = ON; diff --git a/migrations/sqlite/2026-08-18-132346-0000_channel/metadata.toml b/migrations/sqlite/2026-08-18-132346-0000_channel/metadata.toml new file mode 100644 index 0000000..79e9221 --- /dev/null +++ b/migrations/sqlite/2026-08-18-132346-0000_channel/metadata.toml @@ -0,0 +1 @@ +run_in_transaction = false diff --git a/migrations/sqlite/2026-08-18-132346-0000_channel/up.sql b/migrations/sqlite/2026-08-18-132346-0000_channel/up.sql index a0836be..e1c0261 100644 --- a/migrations/sqlite/2026-08-18-132346-0000_channel/up.sql +++ b/migrations/sqlite/2026-08-18-132346-0000_channel/up.sql @@ -1,5 +1,11 @@ --- make avatar url nullable --- https://stackoverflow.com/questions/4007014/alter-column-in-sqlite +-- Rebuilding the parent table while foreign keys are enabled would execute the +-- ON DELETE actions and erase rows that reference channels. This migration runs +-- outside Diesel's transaction so the first PRAGMA takes effect, then wraps the +-- rebuild in its own transaction. +PRAGMA foreign_keys = OFF; +BEGIN; + +-- Make avatar nullable. CREATE TABLE channel_temp ( id VARCHAR(24) PRIMARY KEY NOT NULL, @@ -10,3 +16,6 @@ CREATE TABLE channel_temp INSERT INTO channel_temp SELECT * FROM channel; DROP TABLE channel; ALTER TABLE channel_temp RENAME TO channel; + +COMMIT; +PRAGMA foreign_keys = ON; diff --git a/src/main.rs b/src/main.rs index e3ac07a..c7f7ab1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -278,6 +278,36 @@ async fn run_migrations(pool: &DbPool, database_url: &str, approval: Option<&str mod tests { use super::require_migration_approval; + #[cfg(feature = "sqlite")] + #[derive(diesel::QueryableByName)] + struct RowCount { + #[diesel(sql_type = diesel::sql_types::BigInt)] + count: i64, + } + + #[cfg(feature = "sqlite")] + fn table_count(conn: &mut diesel::SqliteConnection, table: &str) -> i64 { + use diesel::RunQueryDsl; + + diesel::sql_query(format!("SELECT COUNT(*) AS count FROM {table}")) + .get_result::(conn) + .unwrap() + .count + } + + #[cfg(feature = "sqlite")] + fn foreign_keys_enabled(conn: &mut diesel::SqliteConnection) -> bool { + use diesel::RunQueryDsl; + + diesel::sql_query( + "SELECT COUNT(*) AS count FROM pragma_foreign_keys WHERE foreign_keys = 1", + ) + .get_result::(conn) + .unwrap() + .count + == 1 + } + #[test] fn fresh_database_does_not_require_approval() { assert!(require_migration_approval(false, &["20260721".to_owned()], None).is_ok()); @@ -290,4 +320,48 @@ mod tests { assert!(require_migration_approval(true, &pending, Some("20260721")).is_err()); assert!(require_migration_approval(true, &pending, Some("20260721,20260722")).is_ok()); } + + #[cfg(feature = "sqlite")] + #[test] + fn channel_migration_preserves_referencing_rows() { + use diesel::Connection; + use diesel::connection::SimpleConnection; + use diesel_migrations::MigrationHarness; + + let mut conn = diesel::SqliteConnection::establish(":memory:").unwrap(); + conn.batch_execute("PRAGMA foreign_keys = ON;").unwrap(); + + let migrations = conn.pending_migrations(super::MIGRATIONS).unwrap(); + let channel_migration = migrations + .iter() + .position(|migration| migration.name().version().to_string() == "202608181323460000") + .expect("channel migration must be embedded"); + + conn.run_migrations(&migrations[..channel_migration]) + .unwrap(); + conn.batch_execute( + "INSERT INTO channel (id, name, avatar, verified) \ + VALUES ('channel-1', 'Channel', 'https://example.test/avatar', false); \ + INSERT INTO video (id, title, upload_date, uploader_id, thumbnail_url, duration) \ + VALUES ('video-1', 'Video', 0, 'channel-1', \ + 'https://example.test/thumbnail', 60);", + ) + .unwrap(); + + assert_eq!(table_count(&mut conn, "video"), 1); + conn.run_migration(migrations[channel_migration].as_ref()) + .unwrap(); + assert!(foreign_keys_enabled(&mut conn)); + assert_eq!(table_count(&mut conn, "channel"), 1); + assert_eq!(table_count(&mut conn, "video"), 1); + + conn.batch_execute("UPDATE channel SET avatar = NULL;") + .unwrap(); + conn.revert_migration(migrations[channel_migration].as_ref()) + .unwrap(); + assert!(foreign_keys_enabled(&mut conn)); + assert_eq!(table_count(&mut conn, "channel"), 1); + assert_eq!(table_count(&mut conn, "video"), 1); + assert_eq!(table_count(&mut conn, "pragma_foreign_key_check"), 0); + } }