feat(storage): remember which server generation wrote the cached catalog

The cache was version-blind: nothing recorded which Jellyfin generation produced
a row, so a server upgraded underneath the app kept serving rows parsed under the
previous generation's assumptions.

Migration 026 adds servers.catalog_generation and deliberately does NOT clear
synced_at the way migration 025 did. The column starts NULL, which reads as "no
generation recorded yet" rather than "changed", so the first connection after
upgrading simply records what it finds. Invalidation happens only when the
recorded generation actually changes.

That distinction is the point. Treating absent information as a change would
charge every existing user a full catalog re-fetch to defend against a server
upgrade that has not happened — and at the time of writing, 12.0 is hours old, so
essentially no installed server is on the newer generation at all.

Capabilities are also wired at repository creation: the version storage already
holds is read once, resolved, and handed to the online repository. A missing or
unparseable version is not an error — it resolves to the older generation, whose
request shapes work on both.

TRACES: UR-085 | IR-035, DR-280, DR-284

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 20:09:53 +02:00
co-authored by Claude Opus 5
parent 33e1403981
commit 9e2278080d
2 changed files with 251 additions and 11 deletions
+20
View File
@@ -30,6 +30,7 @@ pub const MIGRATIONS: &[(&str, &str)] = &[
("023_downloads_expiry", MIGRATION_023),
("024_multi_user_profiles", MIGRATION_024),
("025_backfill_item_library_id", MIGRATION_025),
("026_server_catalog_generation", MIGRATION_026),
];
/// Initial schema migration
@@ -921,6 +922,25 @@ const MIGRATION_025: &str = r#"
UPDATE items SET synced_at = NULL;
"#;
/// Remember which server generation wrote the cached catalog.
///
/// The cache was version-blind: nothing recorded which Jellyfin generation
/// produced a row, so a server upgraded underneath the app kept serving rows
/// parsed under the previous generation's assumptions.
///
/// This deliberately does **not** clear `synced_at` the way MIGRATION_025 did.
/// The column starts NULL, which reads as "no generation recorded yet", and the
/// first connection after upgrading simply records what it finds. Invalidation
/// happens only when the recorded generation actually *changes* — punishing
/// every existing user with a full re-fetch for a server upgrade that has not
/// happened would cost real bandwidth to defend against nothing. At the time of
/// writing no installed server is on the newer generation at all.
///
/// TRACES: UR-085 | DR-284
const MIGRATION_026: &str = r#"
ALTER TABLE servers ADD COLUMN catalog_generation TEXT;
"#;
#[cfg(test)]
mod migration_024_tests {
use super::*;