fix(db): bump sqlx to 0.9, honour no-transaction
sqlx-sqlite 0.8 ignored Migration.no_tx and wrapped every migration in a transaction, where PRAGMA foreign_keys = OFF is a no-op — so any table rebuild cascade-deleted children. 0.9 honours `-- no-transaction`, so 0014 drops its movie_releases_backup / episode_releases_backup workaround and runs the plain rebuild recipe with foreign keys off. A migration test rebuilds both parents from a pre-0014 database and asserts the child link rows survive. Closes #155 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
-- no-transaction
|
||||
-- #108: a vanished torrent must not re-grab. Overrides #86, which reopened
|
||||
-- the gap (state = 'missing', wanted untouched) so the next tick re-grabbed
|
||||
-- the same release. Adds 'parked' so the daemon can clear `wanted` and mark
|
||||
-- the title honestly instead — distinct from 'missing' (an open gap) and
|
||||
-- 'available' (satisfied). SQLite cannot alter a CHECK, so both tables are
|
||||
-- rebuilt (see 0007).
|
||||
--
|
||||
-- #155: runs outside a transaction so `PRAGMA foreign_keys = OFF` takes
|
||||
-- effect and dropping the old tables does not cascade into their children.
|
||||
|
||||
CREATE TABLE movie_releases_backup AS SELECT * FROM movie_releases;
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
CREATE TABLE movies_new (
|
||||
id INTEGER PRIMARY KEY,
|
||||
@@ -63,11 +67,6 @@ BEGIN
|
||||
SELECT RAISE(ABORT, 'movies require a movie root');
|
||||
END;
|
||||
|
||||
INSERT INTO movie_releases SELECT * FROM movie_releases_backup;
|
||||
DROP TABLE movie_releases_backup;
|
||||
|
||||
CREATE TABLE episode_releases_backup AS SELECT * FROM episode_releases;
|
||||
|
||||
CREATE TABLE episodes_new (
|
||||
id INTEGER PRIMARY KEY,
|
||||
season_id INTEGER NOT NULL REFERENCES seasons (id) ON DELETE CASCADE,
|
||||
@@ -102,5 +101,4 @@ CREATE INDEX episodes_pending_search
|
||||
|
||||
CREATE INDEX episodes_state ON episodes (state);
|
||||
|
||||
INSERT INTO episode_releases SELECT * FROM episode_releases_backup;
|
||||
DROP TABLE episode_releases_backup;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
+85
-10
@@ -297,16 +297,8 @@ mod tests {
|
||||
.await
|
||||
.expect("connect");
|
||||
|
||||
let pre_0007 = sqlx::migrate::Migrator {
|
||||
migrations: std::borrow::Cow::Owned(
|
||||
MIGRATOR.iter().filter(|m| m.version < 7).cloned().collect(),
|
||||
),
|
||||
ignore_missing: MIGRATOR.ignore_missing,
|
||||
locking: MIGRATOR.locking,
|
||||
no_tx: MIGRATOR.no_tx,
|
||||
};
|
||||
pre_0007
|
||||
.run(db.pool())
|
||||
MIGRATOR
|
||||
.run_to(6, db.pool())
|
||||
.await
|
||||
.expect("migrations before #73");
|
||||
|
||||
@@ -344,6 +336,89 @@ mod tests {
|
||||
assert_eq!(renamed_from_imported, "available");
|
||||
}
|
||||
|
||||
/// #155: migration 0014 rebuilds `movies` and `episodes` outside a
|
||||
/// transaction so `PRAGMA foreign_keys = OFF` holds and dropping the old
|
||||
/// tables does not cascade-delete `movie_releases`/`episode_releases`.
|
||||
#[tokio::test]
|
||||
async fn table_rebuild_preserves_child_rows() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let db = Db::connect(dir.path().join("arr.db"))
|
||||
.await
|
||||
.expect("connect");
|
||||
|
||||
MIGRATOR
|
||||
.run_to(13, db.pool())
|
||||
.await
|
||||
.expect("migrations before the 0014 rebuild");
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO movies (tmdb_id, title, root_id)
|
||||
SELECT 693134, 'Dune Part Two', id FROM roots WHERE kind = 'movie' LIMIT 1",
|
||||
)
|
||||
.execute(db.pool())
|
||||
.await
|
||||
.expect("movie");
|
||||
let series_id = sqlx::query(
|
||||
"INSERT INTO series (tmdb_id, title, root_id)
|
||||
SELECT 82728, 'Bluey', id FROM roots WHERE kind = 'tv' LIMIT 1",
|
||||
)
|
||||
.execute(db.pool())
|
||||
.await
|
||||
.expect("series")
|
||||
.last_insert_rowid();
|
||||
let season_id = sqlx::query("INSERT INTO seasons (series_id, number) VALUES (?, 1)")
|
||||
.bind(series_id)
|
||||
.execute(db.pool())
|
||||
.await
|
||||
.expect("season")
|
||||
.last_insert_rowid();
|
||||
let episode_id =
|
||||
sqlx::query("INSERT INTO episodes (season_id, number, title) VALUES (?, 1, 'x')")
|
||||
.bind(season_id)
|
||||
.execute(db.pool())
|
||||
.await
|
||||
.expect("episode")
|
||||
.last_insert_rowid();
|
||||
let release_id = sqlx::query(
|
||||
"INSERT INTO releases (indexer_id, guid, name, size, download_url, parsed)
|
||||
VALUES (1, 'guid-1', 'Some.Release', 1024, 'http://x', '{}')",
|
||||
)
|
||||
.execute(db.pool())
|
||||
.await
|
||||
.expect("release")
|
||||
.last_insert_rowid();
|
||||
sqlx::query(
|
||||
"INSERT INTO movie_releases (movie_id, release_id)
|
||||
SELECT id, ? FROM movies",
|
||||
)
|
||||
.bind(release_id)
|
||||
.execute(db.pool())
|
||||
.await
|
||||
.expect("movie link");
|
||||
sqlx::query("INSERT INTO episode_releases (episode_id, release_id) VALUES (?, ?)")
|
||||
.bind(episode_id)
|
||||
.bind(release_id)
|
||||
.execute(db.pool())
|
||||
.await
|
||||
.expect("episode link");
|
||||
|
||||
db.migrate().await.expect("remaining migrations");
|
||||
|
||||
let movie_links: i64 = sqlx::query_scalar("SELECT count(*) FROM movie_releases")
|
||||
.fetch_one(db.pool())
|
||||
.await
|
||||
.expect("movie_releases");
|
||||
let episode_links: i64 = sqlx::query_scalar("SELECT count(*) FROM episode_releases")
|
||||
.fetch_one(db.pool())
|
||||
.await
|
||||
.expect("episode_releases");
|
||||
assert_eq!(movie_links, 1, "movie_releases survive the movies rebuild");
|
||||
assert_eq!(
|
||||
episode_links, 1,
|
||||
"episode_releases survive the episodes rebuild"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn seeds_two_tv_roots_with_distinct_policies() {
|
||||
let (_dir, db) = fresh().await;
|
||||
|
||||
Reference in New Issue
Block a user