fix: stop swallowing DB errors across the entire DAO layer

Every map_err(|_| DbError::new(...)) and map_err(|_| anyhow!("..."))
in the database layer was discarding the actual Diesel/SQLite error,
making failures impossible to diagnose from logs.

- Add DbError::log() that logs the source error before converting
- Replace all ~130 swallowed outer map_err closures with DbError::log
- Replace all ~47 swallowed inner anyhow closures to include the
  source error in the message

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 4.7 committed 2026-05-27 13:56:48 -04:00
1 parent 39ad83f55b
commit dad0220587
10 files changed
+174 -169

No files matched your search

+84 -79
View File
@@ -203,6 +203,11 @@ impl DbError {
DbError { kind }
}
fn log(kind: DbErrorKind, source: impl std::fmt::Display) -> Self {
log::error!("DB {:?}: {}", kind, source);
DbError { kind }
}
fn exists() -> Self {
DbError::new(DbErrorKind::AlreadyExists)
}
@@ -262,7 +267,7 @@ impl FavoriteDao for SqliteFavoriteDao {
path: favorite_path,
})
.execute(connection.deref_mut())
.map_err(|_| DbError::new(DbErrorKind::InsertError))
.map_err(|e| DbError::log(DbErrorKind::InsertError, e))
} else {
Err(DbError::exists())
}
@@ -283,7 +288,7 @@ impl FavoriteDao for SqliteFavoriteDao {
favorites
.filter(userid.eq(user_id))
.load::<Favorite>(self.connection.lock().unwrap().deref_mut())
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn update_path(&mut self, old_path: &str, new_path: &str) -> Result<(), DbError> {
@@ -292,7 +297,7 @@ impl FavoriteDao for SqliteFavoriteDao {
diesel::update(favorites.filter(rel_path.eq(old_path)))
.set(rel_path.eq(new_path))
.execute(self.connection.lock().unwrap().deref_mut())
.map_err(|_| DbError::new(DbErrorKind::UpdateError))?;
.map_err(|e| DbError::log(DbErrorKind::UpdateError, e))?;
Ok(())
}
@@ -303,7 +308,7 @@ impl FavoriteDao for SqliteFavoriteDao {
.select(rel_path)
.distinct()
.load(self.connection.lock().unwrap().deref_mut())
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
}
@@ -923,7 +928,7 @@ impl ExifDao for SqliteExifDao {
.first::<ImageExif>(connection.deref_mut())
.map_err(|e| anyhow::anyhow!("Post-insert lookup failed: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::InsertError))
.map_err(|e| DbError::log(DbErrorKind::InsertError, e))
}
fn get_exif(
@@ -950,7 +955,7 @@ impl ExifDao for SqliteExifDao {
Err(_) => Err(anyhow::anyhow!("Query error")),
}
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn update_exif(
@@ -987,15 +992,15 @@ impl ExifDao for SqliteExifDao {
last_modified.eq(&exif_data.last_modified),
))
.execute(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Update error"))?;
.map_err(|e| anyhow::anyhow!("Update error: {}", e))?;
image_exif
.filter(library_id.eq(exif_data.library_id))
.filter(rel_path.eq(&exif_data.file_path))
.first::<ImageExif>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::UpdateError))
.map_err(|e| DbError::log(DbErrorKind::UpdateError, e))
}
fn delete_exif(&mut self, context: &opentelemetry::Context, path: &str) -> Result<(), DbError> {
@@ -1005,9 +1010,9 @@ impl ExifDao for SqliteExifDao {
diesel::delete(image_exif.filter(rel_path.eq(path)))
.execute(self.connection.lock().unwrap().deref_mut())
.map(|_| ())
.map_err(|_| anyhow::anyhow!("Delete error"))
.map_err(|e| anyhow::anyhow!("Delete error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn get_all_with_date_taken(
@@ -1038,9 +1043,9 @@ impl ExifDao for SqliteExifDao {
.filter_map(|(path, dt)| dt.map(|ts| (path, ts)))
.collect()
})
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn get_exif_batch(
@@ -1064,9 +1069,9 @@ impl ExifDao for SqliteExifDao {
query
.filter(rel_path.eq_any(file_paths))
.load::<ImageExif>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn query_by_exif(
@@ -1125,9 +1130,9 @@ impl ExifDao for SqliteExifDao {
query
.load::<ImageExif>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn get_camera_makes(
@@ -1152,9 +1157,9 @@ impl ExifDao for SqliteExifDao {
.filter_map(|(make, cnt)| make.map(|m| (m, cnt)))
.collect()
})
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn update_file_path(
@@ -1171,10 +1176,10 @@ impl ExifDao for SqliteExifDao {
diesel::update(image_exif.filter(rel_path.eq(old_path)))
.set(rel_path.eq(new_path))
.execute(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Update error"))?;
.map_err(|e| anyhow::anyhow!("Update error: {}", e))?;
Ok(())
})
.map_err(|_| DbError::new(DbErrorKind::UpdateError))
.map_err(|e| DbError::log(DbErrorKind::UpdateError, e))
}
fn get_all_file_paths(
@@ -1189,9 +1194,9 @@ impl ExifDao for SqliteExifDao {
image_exif
.select(rel_path)
.load(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn get_all_with_gps(
@@ -1259,7 +1264,7 @@ impl ExifDao for SqliteExifDao {
Ok(filtered)
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn get_rows_missing_hash(
@@ -1278,9 +1283,9 @@ impl ExifDao for SqliteExifDao {
.order(id.asc())
.limit(limit)
.load::<(i32, String)>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn backfill_content_hash(
@@ -1304,9 +1309,9 @@ impl ExifDao for SqliteExifDao {
.set((content_hash.eq(hash), size_bytes.eq(size_val)))
.execute(connection.deref_mut())
.map(|_| ())
.map_err(|_| anyhow::anyhow!("Update error"))
.map_err(|e| anyhow::anyhow!("Update error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::UpdateError))
.map_err(|e| DbError::log(DbErrorKind::UpdateError, e))
}
fn list_distinct_content_hashes(
@@ -1324,9 +1329,9 @@ impl ExifDao for SqliteExifDao {
.distinct()
.load::<Option<String>>(connection.deref_mut())
.map(|rows| rows.into_iter().flatten().collect())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn list_paths_and_hashes_for_library(
@@ -1347,10 +1352,10 @@ impl ExifDao for SqliteExifDao {
.filter(library_id.eq(lib_id))
.select((rel_path, content_hash))
.load::<(String, Option<String>)>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
},
)
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn get_rows_needing_date_backfill(
@@ -1377,10 +1382,10 @@ impl ExifDao for SqliteExifDao {
.order(id.asc())
.limit(limit)
.load::<(i32, String)>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
},
)
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn backfill_date_taken(
@@ -1471,10 +1476,10 @@ impl ExifDao for SqliteExifDao {
.order(id.asc())
.limit(limit)
.load::<(String, String)>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
},
)
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn backfill_clip_embedding(
@@ -1567,7 +1572,7 @@ impl ExifDao for SqliteExifDao {
))
.order(id.asc())
.load::<(String, Vec<u8>)>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))?;
.map_err(|e| anyhow::anyhow!("Query error: {}", e))?;
// Dedupe by hash, keeping the first occurrence. Cheap; sized
// to ~14k entries on this library.
@@ -1581,7 +1586,7 @@ impl ExifDao for SqliteExifDao {
}
Ok(out)
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn set_manual_date_taken(
@@ -1741,7 +1746,7 @@ impl ExifDao for SqliteExifDao {
})
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn find_by_content_hash(
@@ -1758,9 +1763,9 @@ impl ExifDao for SqliteExifDao {
.filter(content_hash.eq(hash))
.first::<ImageExif>(connection.deref_mut())
.optional()
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn get_rel_paths_sharing_content(
@@ -1783,7 +1788,7 @@ impl ExifDao for SqliteExifDao {
.select(content_hash)
.first::<Option<String>>(connection.deref_mut())
.optional()
.map_err(|_| anyhow::anyhow!("Query error"))?
.map_err(|e| anyhow::anyhow!("Query error: {}", e))?
.flatten();
let paths = match hash {
@@ -1792,13 +1797,13 @@ impl ExifDao for SqliteExifDao {
.select(rel_path)
.distinct()
.load::<String>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))?,
.map_err(|e| anyhow::anyhow!("Query error: {}", e))?,
None => vec![rel_path_val.to_string()],
};
Ok(paths)
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn get_rel_paths_for_library(
@@ -1815,9 +1820,9 @@ impl ExifDao for SqliteExifDao {
.filter(library_id.eq(library_id_val))
.select(rel_path)
.load::<String>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn find_content_hash_anywhere(
@@ -1837,9 +1842,9 @@ impl ExifDao for SqliteExifDao {
.first::<Option<String>>(connection.deref_mut())
.optional()
.map(|opt| opt.flatten())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn get_rel_paths_by_hash(
@@ -1857,9 +1862,9 @@ impl ExifDao for SqliteExifDao {
.select(rel_path)
.distinct()
.load::<String>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn get_rel_paths_for_hashes(
@@ -1886,14 +1891,14 @@ impl ExifDao for SqliteExifDao {
.select((content_hash.assume_not_null(), rel_path))
.distinct()
.load::<(String, String)>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))?;
.map_err(|e| anyhow::anyhow!("Query error: {}", e))?;
for (hash, path) in rows {
out.entry(hash).or_default().push(path);
}
}
Ok(out)
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn list_rel_paths_for_libraries(
@@ -1959,9 +1964,9 @@ impl ExifDao for SqliteExifDao {
query
.load::<(i32, String)>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn delete_exif_by_library(
@@ -1980,9 +1985,9 @@ impl ExifDao for SqliteExifDao {
)
.execute(self.connection.lock().unwrap().deref_mut())
.map(|_| ())
.map_err(|_| anyhow::anyhow!("Delete error"))
.map_err(|e| anyhow::anyhow!("Delete error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn count_for_library(
@@ -1997,9 +2002,9 @@ impl ExifDao for SqliteExifDao {
.filter(library_id.eq(library_id_val))
.count()
.get_result::<i64>(self.connection.lock().unwrap().deref_mut())
.map_err(|_| anyhow::anyhow!("Count error"))
.map_err(|e| anyhow::anyhow!("Count error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn list_rel_paths_for_library_page(
@@ -2023,10 +2028,10 @@ impl ExifDao for SqliteExifDao {
.limit(limit)
.offset(offset)
.load::<(i32, String)>(self.connection.lock().unwrap().deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
},
)
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn get_rows_missing_perceptual_hash(
@@ -2071,10 +2076,10 @@ impl ExifDao for SqliteExifDao {
.order(id.asc())
.limit(limit)
.load::<(i32, String)>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
},
)
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn backfill_perceptual_hash(
@@ -2098,9 +2103,9 @@ impl ExifDao for SqliteExifDao {
.set((phash_64.eq(phash_val), dhash_64.eq(dhash_val)))
.execute(connection.deref_mut())
.map(|_| ())
.map_err(|_| anyhow::anyhow!("Update error"))
.map_err(|e| anyhow::anyhow!("Update error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::UpdateError))
.map_err(|e| DbError::log(DbErrorKind::UpdateError, e))
}
#[allow(clippy::type_complexity)]
@@ -2130,7 +2135,7 @@ impl ExifDao for SqliteExifDao {
q = q.filter(library_id.eq(lib));
}
q.load::<String>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))?
.map_err(|e| anyhow::anyhow!("Query error: {}", e))?
};
if dup_hashes.is_empty() {
@@ -2177,7 +2182,7 @@ impl ExifDao for SqliteExifDao {
Option<i64>,
)> = q
.load(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))?;
.map_err(|e| anyhow::anyhow!("Query error: {}", e))?;
Ok(rows
.into_iter()
@@ -2196,7 +2201,7 @@ impl ExifDao for SqliteExifDao {
})
.collect())
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
#[allow(clippy::type_complexity)]
@@ -2259,7 +2264,7 @@ impl ExifDao for SqliteExifDao {
Option<i64>,
)> = q
.load(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))?;
.map_err(|e| anyhow::anyhow!("Query error: {}", e))?;
// Dedup keyed on content_hash, keeping the first occurrence
// (deterministic by the SQL ORDER BY: lowest library_id,
@@ -2285,7 +2290,7 @@ impl ExifDao for SqliteExifDao {
}
Ok(out)
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn list_image_paths(
@@ -2310,9 +2315,9 @@ impl ExifDao for SqliteExifDao {
q = q.filter(duplicate_of_hash.is_null());
}
q.load::<(i32, String)>(connection.deref_mut())
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn lookup_duplicate_row(
@@ -2372,9 +2377,9 @@ impl ExifDao for SqliteExifDao {
duplicate_decided_at: r.10,
})
})
.map_err(|_| anyhow::anyhow!("Query error"))
.map_err(|e| anyhow::anyhow!("Query error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::QueryError))
.map_err(|e| DbError::log(DbErrorKind::QueryError, e))
}
fn set_duplicate_of(
@@ -2401,9 +2406,9 @@ impl ExifDao for SqliteExifDao {
))
.execute(connection.deref_mut())
.map(|_| ())
.map_err(|_| anyhow::anyhow!("Update error"))
.map_err(|e| anyhow::anyhow!("Update error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::UpdateError))
.map_err(|e| DbError::log(DbErrorKind::UpdateError, e))
}
fn clear_duplicate_of(
@@ -2428,9 +2433,9 @@ impl ExifDao for SqliteExifDao {
))
.execute(connection.deref_mut())
.map(|_| ())
.map_err(|_| anyhow::anyhow!("Update error"))
.map_err(|e| anyhow::anyhow!("Update error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::UpdateError))
.map_err(|e| DbError::log(DbErrorKind::UpdateError, e))
}
fn union_perceptual_tags(
@@ -2468,9 +2473,9 @@ impl ExifDao for SqliteExifDao {
.bind::<diesel::sql_types::Text, _>(survivor_hash)
.execute(connection.deref_mut())
.map(|_| ())
.map_err(|_| anyhow::anyhow!("Tag union error"))
.map_err(|e| anyhow::anyhow!("Tag union error: {}", e))
})
.map_err(|_| DbError::new(DbErrorKind::UpdateError))
.map_err(|e| DbError::log(DbErrorKind::UpdateError, e))
}
}