diff --git a/core/dr-catalog/src/faces.rs b/core/dr-catalog/src/faces.rs index 184399e..bbcaf14 100644 --- a/core/dr-catalog/src/faces.rs +++ b/core/dr-catalog/src/faces.rs @@ -838,6 +838,22 @@ pub fn unassigned(conn: &Connection, model_id: &str) -> Result, Cata rows.collect::>().map_err(Into::into) } +/// How many faces have no identity yet — [`unassigned`] counted rather than +/// listed, for a screen that only shows the number. +pub fn count_unassigned(conn: &Connection, model_id: &str) -> Result { + let n: i64 = conn.query_row( + &format!( + "SELECT COUNT(*) FROM faces f + LEFT JOIN face_person fp ON fp.face_id = f.id + WHERE fp.face_id IS NULL AND {} = ?1", + embedder_sql("f.model_id") + ), + [embedder_of(model_id)], + |r| r.get(0), + )?; + Ok(n as u64) +} + /// One face's stored embedding, as the clustering pass consumes it. /// /// A struct rather than a tuple because it crosses a crate boundary and "the @@ -910,17 +926,46 @@ pub fn rename_person(conn: &Connection, person: PersonId, name: &str) -> Result< /// Merged-away people are excluded: they exist as redirects so a sync does not /// resurrect them, not as entries in a list. pub fn people(conn: &Connection) -> Result, CatalogError> { - let mut q = conn.prepare( + people_where(conn, false) +} + +/// Everyone who holds a face, carries a name, or was set aside — the people a +/// screen has a row for. +/// +/// The rest are the empty, unnamed groups a regrouping pass leaves behind +/// (`prune_empty_unnamed`), and on the reference library they were 17,000 of +/// 19,000 rows: read, counted, sorted by name and then thrown away by the +/// caller on every redraw. Filtered here, in the query, they are never +/// sorted. The filter is SQL's `trim`, which strips spaces and not every +/// whitespace character, so a name that is only a tab is listed rather than +/// hidden — the safe direction for a row the user typed something into. +pub fn people_in_use(conn: &Connection) -> Result, CatalogError> { + people_where(conn, true) +} + +/// [`people`], with face counts aggregated once per person *before* the join +/// rather than grouped after it: the face table is joined to the 2,000 +/// people it names, not the 19,000 rows of the people table. +fn people_where(conn: &Connection, in_use: bool) -> Result, CatalogError> { + let filter = if in_use { + "AND (c.person_id IS NOT NULL OR trim(p.name) <> '' OR p.ignored)" + } else { + "" + }; + let mut q = conn.prepare(&format!( "SELECT p.id, p.uuid, p.name, - COALESCE(SUM(fp.confirmed = 1), 0), - COALESCE(SUM(fp.confirmed = 0), 0), + COALESCE(c.confirmed, 0), + COALESCE(c.suggested, 0), p.ignored FROM people p - LEFT JOIN face_person fp ON fp.person_id = p.id - WHERE p.merged_into IS NULL - GROUP BY p.id - ORDER BY 4 DESC, 5 DESC, p.name", - )?; + LEFT JOIN (SELECT person_id, + SUM(confirmed = 1) AS confirmed, + SUM(confirmed = 0) AS suggested + FROM face_person + GROUP BY person_id) c ON c.person_id = p.id + WHERE p.merged_into IS NULL {filter} + ORDER BY 4 DESC, 5 DESC, p.name" + ))?; let rows = q.query_map([], |r| { Ok(Person { id: PersonId(r.get::<_, i64>(0)? as u64), diff --git a/ui/dr-ui/src/identity.rs b/ui/dr-ui/src/identity.rs index 20ac959..ff91e6b 100644 --- a/ui/dr-ui/src/identity.rs +++ b/ui/dr-ui/src/identity.rs @@ -216,7 +216,9 @@ pub struct IdentityView { /// Filtered rather than deleted, because this is a screen being drawn and not a /// catalog being repaired. A row is withheld; nothing is lost, a sync cannot /// resurrect what was never removed, and the prune stays the one place that -/// decides these are disposable. +/// decides these are disposable. The filter is the catalog's +/// (`faces::people_in_use`), in the query, so the rows withheld are never +/// read or sorted either. /// /// An empty group with a *name* still shows. That one is not debris, it is the /// symptom of a real failure — a named person whose faces were regrouped out @@ -228,11 +230,8 @@ pub fn load_people( model_id: &str, ) -> Result { let conn = catalog.connection(); - let people = faces::people(conn)? + let people = faces::people_in_use(conn)? .into_iter() - .filter(|p| { - p.confirmed_faces + p.suggested_faces > 0 || !p.name.trim().is_empty() || p.ignored - }) .map(|p| PersonRow { id: p.id, name: p.name, @@ -247,7 +246,7 @@ pub fn load_people( people, selected: None, faces: Vec::new(), - unassigned: faces::unassigned(conn, model_id)?.len(), + unassigned: faces::count_unassigned(conn, model_id)? as usize, calibrated: faces::calibration(conn, model_id)?.is_some_and(|(c, _)| c.valid), }) }