Skip to content

Commit 91490cb

Browse files
committed
fix(sqlite): preserve channel references during migration
SQLite executes ON DELETE actions when the migration drops the channel table with foreign keys enabled. Pause enforcement around an explicit transactional rebuild, preserve channel rows on rollback, and cover both directions with the real embedded migration.
1 parent c5485c0 commit 91490cb

4 files changed

Lines changed: 97 additions & 4 deletions

File tree

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
DROP TABLE IF EXISTS channel;
2-
CREATE TABLE channel
1+
PRAGMA foreign_keys = OFF;
2+
BEGIN;
3+
4+
CREATE TABLE channel_temp
35
(
46
id VARCHAR(24) PRIMARY KEY NOT NULL,
57
name VARCHAR NOT NULL,
68
avatar VARCHAR NOT NULL,
79
verified BOOLEAN NOT NULL
810
);
11+
INSERT INTO channel_temp
12+
SELECT id, name, COALESCE(avatar, ''), verified FROM channel;
13+
DROP TABLE channel;
14+
ALTER TABLE channel_temp RENAME TO channel;
15+
16+
COMMIT;
17+
PRAGMA foreign_keys = ON;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
run_in_transaction = false

migrations/sqlite/2026-08-18-132346-0000_channel/up.sql

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
-- make avatar url nullable
2-
-- https://stackoverflow.com/questions/4007014/alter-column-in-sqlite
1+
-- Rebuilding the parent table while foreign keys are enabled would execute the
2+
-- ON DELETE actions and erase rows that reference channels. This migration runs
3+
-- outside Diesel's transaction so the first PRAGMA takes effect, then wraps the
4+
-- rebuild in its own transaction.
5+
PRAGMA foreign_keys = OFF;
6+
BEGIN;
7+
8+
-- Make avatar nullable.
39
CREATE TABLE channel_temp
410
(
511
id VARCHAR(24) PRIMARY KEY NOT NULL,
@@ -10,3 +16,6 @@ CREATE TABLE channel_temp
1016
INSERT INTO channel_temp SELECT * FROM channel;
1117
DROP TABLE channel;
1218
ALTER TABLE channel_temp RENAME TO channel;
19+
20+
COMMIT;
21+
PRAGMA foreign_keys = ON;

src/main.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,36 @@ async fn run_migrations(pool: &DbPool, database_url: &str, approval: Option<&str
278278
mod tests {
279279
use super::require_migration_approval;
280280

281+
#[cfg(feature = "sqlite")]
282+
#[derive(diesel::QueryableByName)]
283+
struct RowCount {
284+
#[diesel(sql_type = diesel::sql_types::BigInt)]
285+
count: i64,
286+
}
287+
288+
#[cfg(feature = "sqlite")]
289+
fn table_count(conn: &mut diesel::SqliteConnection, table: &str) -> i64 {
290+
use diesel::RunQueryDsl;
291+
292+
diesel::sql_query(format!("SELECT COUNT(*) AS count FROM {table}"))
293+
.get_result::<RowCount>(conn)
294+
.unwrap()
295+
.count
296+
}
297+
298+
#[cfg(feature = "sqlite")]
299+
fn foreign_keys_enabled(conn: &mut diesel::SqliteConnection) -> bool {
300+
use diesel::RunQueryDsl;
301+
302+
diesel::sql_query(
303+
"SELECT COUNT(*) AS count FROM pragma_foreign_keys WHERE foreign_keys = 1",
304+
)
305+
.get_result::<RowCount>(conn)
306+
.unwrap()
307+
.count
308+
== 1
309+
}
310+
281311
#[test]
282312
fn fresh_database_does_not_require_approval() {
283313
assert!(require_migration_approval(false, &["20260721".to_owned()], None).is_ok());
@@ -290,4 +320,48 @@ mod tests {
290320
assert!(require_migration_approval(true, &pending, Some("20260721")).is_err());
291321
assert!(require_migration_approval(true, &pending, Some("20260721,20260722")).is_ok());
292322
}
323+
324+
#[cfg(feature = "sqlite")]
325+
#[test]
326+
fn channel_migration_preserves_referencing_rows() {
327+
use diesel::Connection;
328+
use diesel::connection::SimpleConnection;
329+
use diesel_migrations::MigrationHarness;
330+
331+
let mut conn = diesel::SqliteConnection::establish(":memory:").unwrap();
332+
conn.batch_execute("PRAGMA foreign_keys = ON;").unwrap();
333+
334+
let migrations = conn.pending_migrations(super::MIGRATIONS).unwrap();
335+
let channel_migration = migrations
336+
.iter()
337+
.position(|migration| migration.name().version().to_string() == "202608181323460000")
338+
.expect("channel migration must be embedded");
339+
340+
conn.run_migrations(&migrations[..channel_migration])
341+
.unwrap();
342+
conn.batch_execute(
343+
"INSERT INTO channel (id, name, avatar, verified) \
344+
VALUES ('channel-1', 'Channel', 'https://example.test/avatar', false); \
345+
INSERT INTO video (id, title, upload_date, uploader_id, thumbnail_url, duration) \
346+
VALUES ('video-1', 'Video', 0, 'channel-1', \
347+
'https://example.test/thumbnail', 60);",
348+
)
349+
.unwrap();
350+
351+
assert_eq!(table_count(&mut conn, "video"), 1);
352+
conn.run_migration(migrations[channel_migration].as_ref())
353+
.unwrap();
354+
assert!(foreign_keys_enabled(&mut conn));
355+
assert_eq!(table_count(&mut conn, "channel"), 1);
356+
assert_eq!(table_count(&mut conn, "video"), 1);
357+
358+
conn.batch_execute("UPDATE channel SET avatar = NULL;")
359+
.unwrap();
360+
conn.revert_migration(migrations[channel_migration].as_ref())
361+
.unwrap();
362+
assert!(foreign_keys_enabled(&mut conn));
363+
assert_eq!(table_count(&mut conn, "channel"), 1);
364+
assert_eq!(table_count(&mut conn, "video"), 1);
365+
assert_eq!(table_count(&mut conn, "pragma_foreign_key_check"), 0);
366+
}
293367
}

0 commit comments

Comments
 (0)