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 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 21:21:33 -05:00
parent e734ef650b
commit 3e41c9d055
3 changed files with 87 additions and 2 deletions

View File

@@ -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<LibraryStats> {
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();