From 47e1375434da71b8e5a2632ee0dabad43187f09c Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Sat, 22 Aug 2026 21:26:40 +0100 Subject: [PATCH] fix(db): enforce root media kinds --- crates/arr-db/migrations/0005_series.sql | 28 ++++++++++++++++++++++++ crates/arr-db/src/lib.rs | 25 +++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/crates/arr-db/migrations/0005_series.sql b/crates/arr-db/migrations/0005_series.sql index 3785274..bdd4583 100644 --- a/crates/arr-db/migrations/0005_series.sql +++ b/crates/arr-db/migrations/0005_series.sql @@ -14,6 +14,34 @@ CREATE TABLE series ( CREATE INDEX series_root ON series (root_id); +CREATE TRIGGER movies_require_movie_root +BEFORE INSERT ON movies +WHEN NOT EXISTS (SELECT 1 FROM roots WHERE id = NEW.root_id AND kind = 'movie') +BEGIN + SELECT RAISE(ABORT, 'movies require a movie root'); +END; + +CREATE TRIGGER movies_require_movie_root_on_update +BEFORE UPDATE OF root_id ON movies +WHEN NOT EXISTS (SELECT 1 FROM roots WHERE id = NEW.root_id AND kind = 'movie') +BEGIN + SELECT RAISE(ABORT, 'movies require a movie root'); +END; + +CREATE TRIGGER series_require_tv_root +BEFORE INSERT ON series +WHEN NOT EXISTS (SELECT 1 FROM roots WHERE id = NEW.root_id AND kind = 'tv') +BEGIN + SELECT RAISE(ABORT, 'series require a TV root'); +END; + +CREATE TRIGGER series_require_tv_root_on_update +BEFORE UPDATE OF root_id ON series +WHEN NOT EXISTS (SELECT 1 FROM roots WHERE id = NEW.root_id AND kind = 'tv') +BEGIN + SELECT RAISE(ABORT, 'series require a TV root'); +END; + CREATE TABLE seasons ( id INTEGER PRIMARY KEY, series_id INTEGER NOT NULL REFERENCES series (id) ON DELETE CASCADE, diff --git a/crates/arr-db/src/lib.rs b/crates/arr-db/src/lib.rs index 69784f8..a718a50 100644 --- a/crates/arr-db/src/lib.rs +++ b/crates/arr-db/src/lib.rs @@ -304,4 +304,29 @@ mod tests { .expect("movie schema remains available"); assert_eq!(movie_count, 0); } + + #[tokio::test] + async fn aggregates_require_roots_of_their_own_kind() { + let (_dir, db) = fresh().await; + let movie_root: i64 = + sqlx::query_scalar("SELECT id FROM roots WHERE kind = 'movie' LIMIT 1") + .fetch_one(db.pool()) + .await + .expect("movie root"); + let tv_root: i64 = sqlx::query_scalar("SELECT id FROM roots WHERE kind = 'tv' LIMIT 1") + .fetch_one(db.pool()) + .await + .expect("TV root"); + + sqlx::query("INSERT INTO movies (tmdb_id, title, root_id) VALUES (1, 'x', ?)") + .bind(tv_root) + .execute(db.pool()) + .await + .expect_err("movie cannot use TV root"); + sqlx::query("INSERT INTO series (tmdb_id, title, root_id) VALUES (2, 'x', ?)") + .bind(movie_root) + .execute(db.pool()) + .await + .expect_err("series cannot use movie root"); + } }