From 3e41c9d055802ca39d9b443ba0a4b5e9d2acee4d Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 7 Jul 2026 21:21:33 -0500 Subject: [PATCH] Add library_stats aggregate query for the sidebar summary New LibraryStats (unique cards owned, total copies, deck count, and per-color copy counts including colorless) via Catalog::library_stats, tested against the fixture catalog. Co-Authored-By: Claude Sonnet 5 --- crates/mtgc-core/src/lib.rs | 2 +- crates/mtgc-core/src/library.rs | 71 ++++++++++++++++++++++++++++++++- crates/mtgc-core/src/model.rs | 16 ++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/crates/mtgc-core/src/lib.rs b/crates/mtgc-core/src/lib.rs index 9828f60..2eb337f 100644 --- a/crates/mtgc-core/src/lib.rs +++ b/crates/mtgc-core/src/lib.rs @@ -19,7 +19,7 @@ use std::path::Path; use rusqlite::Connection; pub use error::{CoreError, Result}; -pub use model::{CardRef, ColorSet, Deck, DeckEntry, LibraryCard, LibraryFilter}; +pub use model::{CardRef, ColorSet, Deck, DeckEntry, LibraryCard, LibraryFilter, LibraryStats}; pub use search::NameIndex; /// A handle to an open catalog: the library db with the reference db attached. diff --git a/crates/mtgc-core/src/library.rs b/crates/mtgc-core/src/library.rs index a795880..c2b1528 100644 --- a/crates/mtgc-core/src/library.rs +++ b/crates/mtgc-core/src/library.rs @@ -6,7 +6,7 @@ use rusqlite::{params, OptionalExtension}; use crate::db::REF_DB; use crate::error::Result; -use crate::model::{CardRef, ColorSet, LibraryCard}; +use crate::model::{CardRef, ColorSet, LibraryCard, LibraryStats, WUBRG}; use crate::Catalog; /// Map a row shaped `(oracle_id, quantity_owned, location, name, mana_value, @@ -94,6 +94,56 @@ impl Catalog { .collect()) } + /// Aggregate stats about the owned library, for the sidebar summary. + pub fn library_stats(&self) -> Result { + let unique_cards: i64 = self.conn.query_row( + "SELECT COUNT(*) FROM library_cards WHERE quantity_owned > 0", + [], + |r| r.get(0), + )?; + let total_copies: i64 = self.conn.query_row( + "SELECT COALESCE(SUM(quantity_owned), 0) FROM library_cards", + [], + |r| r.get(0), + )?; + let deck_count: i64 = self + .conn + .query_row("SELECT COUNT(*) FROM decks", [], |r| r.get(0))?; + + let mut color_counts = [0i64; 5]; + let mut colorless_count = 0i64; + { + let mut stmt = self.conn.prepare(&format!( + "SELECT oc.colors, lc.quantity_owned + FROM library_cards lc + JOIN {REF_DB}.oracle_cards oc ON oc.oracle_id = lc.oracle_id + WHERE lc.quantity_owned > 0" + ))?; + let rows = + stmt.query_map([], |r| Ok((r.get::<_, i64>(0)? as u8, r.get::<_, i64>(1)?)))?; + for row in rows { + let (colors, qty) = row?; + if colors == 0 { + colorless_count += qty; + } else { + for (i, (bit, _)) in WUBRG.iter().enumerate() { + if colors & bit != 0 { + color_counts[i] += qty; + } + } + } + } + } + + Ok(LibraryStats { + unique_cards, + total_copies, + deck_count, + color_counts, + colorless_count, + }) + } + /// Set the free-text storage location for a card (creates the library row if /// absent). Empty string clears it. pub fn set_location(&self, oracle_id: &str, location: &str) -> Result<()> { @@ -157,6 +207,25 @@ mod tests { assert_eq!(sol.card.image_uri, "http://img/sol-ring"); } + #[test] + fn library_stats_aggregates_counts_and_colors() { + let fx = test_support::make(); + let cat = &fx.catalog; + cat.add_owned("llanowar-elves", 2).unwrap(); // G + cat.add_owned("deathrite-shaman", 1).unwrap(); // BG + cat.add_owned("sol-ring", 3).unwrap(); // colorless + cat.create_deck("Deck A", None).unwrap(); + cat.create_deck("Deck B", None).unwrap(); + + let stats = cat.library_stats().unwrap(); + assert_eq!(stats.unique_cards, 3); + assert_eq!(stats.total_copies, 6); + assert_eq!(stats.deck_count, 2); + assert_eq!(stats.colorless_count, 3); + // WUBRG order: W, U, B, R, G. + assert_eq!(stats.color_counts, [0, 0, 1, 0, 3]); + } + #[test] fn back_image_uri_is_plumbed_through() { let fx = test_support::make(); diff --git a/crates/mtgc-core/src/model.rs b/crates/mtgc-core/src/model.rs index fef5f95..8d5588c 100644 --- a/crates/mtgc-core/src/model.rs +++ b/crates/mtgc-core/src/model.rs @@ -139,6 +139,22 @@ pub struct Deck { pub notes: String, } +/// Aggregate stats about the owned library, for a dashboard-style summary. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct LibraryStats { + /// Distinct oracle cards owned (quantity_owned > 0). + pub unique_cards: i64, + /// Total physical copies owned, summed across all cards. + pub total_copies: i64, + pub deck_count: i64, + /// Copies owned in each color, WUBRG order. A multicolor card's copies + /// count toward every color it contains, so these can sum to more than + /// `total_copies`. + pub color_counts: [i64; 5], + /// Copies owned with no color (colors == 0). + pub colorless_count: i64, +} + /// Criteria for searching the owned library. All fields are optional; an empty /// filter matches every owned card. #[derive(Debug, Clone, Default, Serialize, Deserialize)]