Hide folders holding only hidden versions from /photos dirs (#194)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-10-30 19:24:22 -04:00
1 parent aefc4bc411
commit 0e1fdd43e5
3 files changed
+122

No files matched your search

+76
View File
@@ -228,6 +228,17 @@ pub trait VersionDao: Sync + Send {
view_library_ids: &[i32],
) -> Result<HashSet<(i32, String)>, DbError>;
/// Of `dirs` (rel paths), those that hold indexed files and where every
/// indexed file anywhere underneath is a version hidden in the view.
/// Such a folder would open empty. A folder with no indexed rows at all
/// is not reported (nothing says it is safe to hide). One indexed range
/// scan per dir; no filesystem walk.
fn dirs_holding_only_hidden_versions(
&mut self,
view_library_ids: &[i32],
dirs: &[String],
) -> Result<HashSet<String>, DbError>;
/// Rows in a library whose file name follows the version convention
/// (`<name>.<kind>[.<n>].<ext>`), have a hash and aren't linked yet.
/// Returns `(id, rel_path, content_hash)` ordered by row id, starting
@@ -780,6 +791,71 @@ impl VersionDao for SqliteVersionDao {
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn dirs_holding_only_hidden_versions(
&mut self,
view_library_ids: &[i32],
dirs: &[String],
) -> Result<HashSet<String>, DbError> {
if dirs.is_empty() {
return Ok(HashSet::new());
}
trace_db_call("query", "dirs_holding_only_hidden_versions", |_span| {
#[derive(QueryableByName)]
struct Counts {
#[diesel(sql_type = BigInt)]
total: i64,
#[diesel(sql_type = BigInt)]
visible: i64,
}
#[derive(QueryableByName)]
struct Id {
#[diesel(sql_type = Integer)]
id: i32,
}
let mut conn = self.connection.lock().expect("Unable to get VersionDao");
// Scan the view's libraries (all of them when unscoped) so the
// (library_id, rel_path) index serves the prefix range.
let scan: Vec<i32> = if view_library_ids.is_empty() {
sql_query("SELECT id FROM libraries")
.load::<Id>(&mut *conn)?
.into_iter()
.map(|r| r.id)
.collect()
} else {
view_library_ids.to_vec()
};
let list = scan
.iter()
.map(i32::to_string)
.collect::<Vec<_>>()
.join(",");
let sql = format!(
"SELECT COUNT(*) AS total, \
COALESCE(SUM(CASE WHEN {} THEN 1 ELSE 0 END), 0) AS visible \
FROM image_exif \
WHERE library_id IN ({list}) AND rel_path >= ? AND rel_path < ?",
visible_version_clause(view_library_ids)
);
let mut out = HashSet::new();
for dir in dirs {
let dir = dir.trim_end_matches('/');
if dir.is_empty() {
continue;
}
// '0' is the character after '/', closing the prefix range.
let c = sql_query(&sql)
.bind::<Text, _>(format!("{dir}/"))
.bind::<Text, _>(format!("{dir}0"))
.get_result::<Counts>(&mut *conn)?;
if c.total > 0 && c.visible == 0 {
out.insert(dir.to_string());
}
}
Ok(out)
})
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn unlinked_name_candidates(
&mut self,
library_id: i32,