From 57b7bad086d59cdbbc6bd2f346b7e54092c73349 Mon Sep 17 00:00:00 2001 From: Cameron Cordes Date: Sun, 3 May 2026 18:24:07 -0400 Subject: [PATCH] =?UTF-8?q?duplicates:=20library-aware=20visibility=20?= =?UTF-8?q?=E2=80=94=20only=20hide=20a=20demoted=20row=20when=20its=20surv?= =?UTF-8?q?ivor=20is=20reachable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Soft-marked rows used to disappear from /photos globally, including from a library-scoped view that didn't contain the survivor at all. A user browsing lib A who'd promoted a file from lib B as the survivor would silently lose visibility on their own copy in lib A, even though lib B's file isn't reachable from lib A's view. Library-scoped queries now keep a demoted row visible when its survivor lives in a library outside the current scope. Implemented as a NOT EXISTS subquery against the same image_exif table aliased as `survivor`. The unscoped (all-libraries) view is unchanged — every survivor is reachable, so demoted rows stay hidden as before. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/database/mod.rs | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/database/mod.rs b/src/database/mod.rs index 8dfaa5c..696ffbc 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1222,7 +1222,38 @@ impl ExifDao for SqliteExifDao { } if !include_duplicates { - query = query.filter(duplicate_of_hash.is_null()); + if library_ids.is_empty() { + // Unscoped (all-libraries) view — every survivor is + // reachable somewhere, so a soft-marked row is + // genuinely a duplicate from the user's perspective. + // Hide it. + query = query.filter(duplicate_of_hash.is_null()); + } else { + // Scoped to specific libraries: only hide a + // soft-marked row when the survivor is reachable + // *in this view*. If the survivor lives in a + // library the user can't see right now, the + // demoted file is the only copy of those bytes + // they have access to — keep it visible. + // + // Implemented as a correlated NOT EXISTS subquery + // over an aliased image_exif. Library ids are i32 + // so format!-inlining the integer list is safe. + use diesel::sql_types::Bool; + let lib_list = library_ids + .iter() + .map(i32::to_string) + .collect::>() + .join(","); + let raw = format!( + "(image_exif.duplicate_of_hash IS NULL OR NOT EXISTS \ + (SELECT 1 FROM image_exif AS survivor \ + WHERE survivor.content_hash = image_exif.duplicate_of_hash \ + AND survivor.library_id IN ({})))", + lib_list + ); + query = query.filter(diesel::dsl::sql::(&raw)); + } } query