diff --git a/core/dr-catalog/examples/catalog_bench.rs b/core/dr-catalog/examples/catalog_bench.rs index 3f94f21..fda9265 100644 --- a/core/dr-catalog/examples/catalog_bench.rs +++ b/core/dr-catalog/examples/catalog_bench.rs @@ -170,11 +170,12 @@ const RATED_AT_LEAST_ONE: &str = " AND coalesce((SELECT dv.rating FROM versions /// `dr_ui::library::total_images_filtered`, spelled as it is there. fn grid_count(conn: &rusqlite::Connection, rated: &str) -> i64 { - let folded = dr_catalog::bursts::not_collapsed_away("i"); + let visible = "i.shadowed_by IS NULL AND i.trashed_at IS NULL"; + let hidden = dr_catalog::bursts::collapsed_away_frames("i"); conn.query_row( &format!( - "SELECT count(*) FROM images i - WHERE i.shadowed_by IS NULL AND i.trashed_at IS NULL{rated} AND {folded}" + "SELECT (SELECT count(*) FROM images i WHERE {visible}{rated}) + - (SELECT count(*) FROM {hidden} AND {visible}{rated})" ), [], |r| r.get(0), diff --git a/core/dr-catalog/src/bursts.rs b/core/dr-catalog/src/bursts.rs index 3c6ce17..a0f85f4 100644 --- a/core/dr-catalog/src/bursts.rs +++ b/core/dr-catalog/src/bursts.rs @@ -722,6 +722,31 @@ pub fn not_collapsed_away(image: &str) -> String { ) } +/// SQL for the rows [`not_collapsed_away`] drops, as a `FROM ... WHERE` +/// joining each such frame to its image under the alias `image`. +/// +/// For counting. A count that applies [`not_collapsed_away`] to every row +/// pays two primary-key probes per image to find the handful a collapsed +/// burst hides; counting everything and subtracting what this lists walks +/// only `burst_members`, which is empty on a library without bursts. The +/// caller appends its own conditions on `image` with `AND`, the same ones +/// it counted the whole with, so the subtraction takes away only rows the +/// whole included. `image_id` is `burst_members`' key, so no image is +/// listed twice. +/// +/// The two must describe the same rows: change one, change both, and +/// `the_collapsed_frames_are_what_the_predicate_drops` will say if they drift. +/// +/// Never interpolate anything user-supplied as `image`. +pub fn collapsed_away_frames(image: &str) -> String { + format!( + "burst_members bm CROSS JOIN images {image} ON {image}.id = bm.image_id + WHERE bm.representative = 0 + AND NOT EXISTS (SELECT 1 FROM burst_expanded be + WHERE be.burst_id = bm.burst_id)" + ) +} + #[cfg(test)] mod tests { use super::*; @@ -1235,6 +1260,46 @@ mod tests { assert_eq!(visible(cat.connection()), vec![1, 2, 3, 4]); } + #[test] + fn the_collapsed_frames_are_what_the_predicate_drops() { + // `collapsed_away_frames` is `not_collapsed_away` turned inside out + // for counting; the two must name the same rows, open or closed. + let cat = seeded(&[ + (1, 1000, Some(0xFF00)), + (2, 1001, Some(0xFF00)), + (3, 1002, Some(0xFF00)), + (4, 9000, Some(0xAA00)), + (5, 9001, Some(0xAA00)), + (6, 20000, Some(0xFF00)), + ]); + regroup(cat.connection(), Rules::default()).unwrap(); + let ids = |sql: String| -> Vec { + let c = cat.connection(); + let mut stmt = c.prepare(&sql).unwrap(); + let rows = stmt.query_map([], |r| r.get::<_, i64>(0)).unwrap(); + rows.collect::, _>>().unwrap() + }; + let dropped = || { + ids(format!( + "SELECT id FROM images i WHERE NOT {} ORDER BY id", + not_collapsed_away("i") + )) + }; + let listed = || { + ids(format!( + "SELECT i.id FROM {} ORDER BY i.id", + collapsed_away_frames("i") + )) + }; + + assert_eq!(listed(), dropped()); + set_expanded(cat.connection(), ImageId(1), false).unwrap(); + assert_eq!(dropped(), vec![2, 3]); + assert_eq!(listed(), dropped()); + set_expanded(cat.connection(), ImageId(4), false).unwrap(); + assert_eq!(listed(), dropped()); + } + #[test] fn a_library_with_no_bursts_hides_nothing() { // The predicate is in every grid query, so its cost and its effect on a diff --git a/ui/dr-ui/src/library/spans.rs b/ui/dr-ui/src/library/spans.rs index c6e2aac..38e104e 100644 --- a/ui/dr-ui/src/library/spans.rs +++ b/ui/dr-ui/src/library/spans.rs @@ -349,9 +349,17 @@ pub(super) fn total_images_filtered( filter: &RatingFilter, ) -> Result { let rated = filter.sql(); - let folded = uncollapsed("i"); + // Everything the filter keeps, less the frames a collapsed burst stands + // in for -- rather than `uncollapsed` asked of every image. Paid on + // every scroll reload: the predicate probed `burst_members` and + // `burst_expanded` once per visible image, 19,000 times on the reference + // library, to hide none. The subtraction walks only the burst members. + let hidden = dr_catalog::bursts::collapsed_away_frames("i"); let n: i64 = catalog.connection().query_row( - &format!("SELECT count(*) FROM images i WHERE {VISIBLE}{rated}{folded}"), + &format!( + "SELECT (SELECT count(*) FROM images i WHERE {VISIBLE}{rated}) + - (SELECT count(*) FROM {hidden} AND {VISIBLE}{rated})" + ), [], |r| r.get(0), )?;