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

@@ -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.

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();

View File

@@ -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)]