Page the version name sweep with a resume cursor (#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:20:44 -04:00
1 parent 9191257fbb
commit 008dd0e7f4
3 files changed
+139 -25

No files matched your search

+25 -8
View File
@@ -230,12 +230,15 @@ pub trait VersionDao: Sync + Send {
/// Rows in a library whose file name follows the version convention
/// (`<name>.<kind>[.<n>].<ext>`), have a hash and aren't linked yet.
/// Returns `(rel_path, content_hash)`, oldest first.
/// Returns `(id, rel_path, content_hash)` ordered by row id, starting
/// after `after_id` (the caller's resume cursor, so rows that never
/// resolve can't starve the ones behind them).
fn unlinked_name_candidates(
&mut self,
library_id: i32,
after_id: i32,
limit: i64,
) -> Result<Vec<(String, String)>, DbError>;
) -> Result<Vec<(i32, String, String)>, DbError>;
/// Copy the original's date, GPS and camera fields onto every version
/// row whose copy is stale. Returns the number of rows updated.
@@ -723,11 +726,14 @@ impl VersionDao for SqliteVersionDao {
fn unlinked_name_candidates(
&mut self,
library_id: i32,
after_id: i32,
limit: i64,
) -> Result<Vec<(String, String)>, DbError> {
) -> Result<Vec<(i32, String, String)>, DbError> {
trace_db_call("query", "unlinked_name_candidates", |_span| {
#[derive(QueryableByName)]
struct Row {
#[diesel(sql_type = Integer)]
id: i32,
#[diesel(sql_type = Text)]
rel_path: String,
#[diesel(sql_type = Text)]
@@ -739,8 +745,8 @@ impl VersionDao for SqliteVersionDao {
.collect::<Vec<_>>()
.join(" OR ");
let sql = format!(
"SELECT rel_path, content_hash FROM image_exif \
WHERE library_id = ? AND content_hash IS NOT NULL AND ({kinds}) \
"SELECT id, rel_path, content_hash FROM image_exif \
WHERE library_id = ? AND id > ? AND content_hash IS NOT NULL AND ({kinds}) \
AND NOT EXISTS (SELECT 1 FROM photo_versions pv \
WHERE pv.version_hash = image_exif.content_hash) \
ORDER BY id LIMIT ?"
@@ -748,11 +754,12 @@ impl VersionDao for SqliteVersionDao {
let mut conn = self.connection.lock().expect("Unable to get VersionDao");
sql_query(sql)
.bind::<Integer, _>(library_id)
.bind::<Integer, _>(after_id)
.bind::<BigInt, _>(limit)
.load::<Row>(&mut *conn)
.map(|rows| {
rows.into_iter()
.map(|r| (r.rel_path, r.content_hash))
.map(|r| (r.id, r.rel_path, r.content_hash))
.collect()
})
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
@@ -1089,8 +1096,18 @@ mod tests {
insert_exif(&c, 2, "a/y.jpg.restored.png", "h4");
dao.link_version(&link("h2", "ox", "a/x.jpg.edit.2.png"))
.unwrap_or(LinkOutcome::Linked);
let got = dao.unlinked_name_candidates(1, 100).unwrap();
assert_eq!(got, vec![("a/x.jpg.restored.png".to_string(), "h1".into())]);
let got = dao.unlinked_name_candidates(1, 0, 100).unwrap();
assert_eq!(got.len(), 1);
assert_eq!(
(got[0].1.as_str(), got[0].2.as_str()),
("a/x.jpg.restored.png", "h1")
);
// The cursor skips rows at or before it.
assert!(
dao.unlinked_name_candidates(1, got[0].0, 100)
.unwrap()
.is_empty()
);
}
#[test]