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:
@@ -19,7 +19,7 @@ use std::path::Path;
|
|||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
|
|
||||||
pub use error::{CoreError, Result};
|
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;
|
pub use search::NameIndex;
|
||||||
|
|
||||||
/// A handle to an open catalog: the library db with the reference db attached.
|
/// A handle to an open catalog: the library db with the reference db attached.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use rusqlite::{params, OptionalExtension};
|
|||||||
|
|
||||||
use crate::db::REF_DB;
|
use crate::db::REF_DB;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::model::{CardRef, ColorSet, LibraryCard};
|
use crate::model::{CardRef, ColorSet, LibraryCard, LibraryStats, WUBRG};
|
||||||
use crate::Catalog;
|
use crate::Catalog;
|
||||||
|
|
||||||
/// Map a row shaped `(oracle_id, quantity_owned, location, name, mana_value,
|
/// Map a row shaped `(oracle_id, quantity_owned, location, name, mana_value,
|
||||||
@@ -94,6 +94,56 @@ impl Catalog {
|
|||||||
.collect())
|
.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
|
/// Set the free-text storage location for a card (creates the library row if
|
||||||
/// absent). Empty string clears it.
|
/// absent). Empty string clears it.
|
||||||
pub fn set_location(&self, oracle_id: &str, location: &str) -> Result<()> {
|
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");
|
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]
|
#[test]
|
||||||
fn back_image_uri_is_plumbed_through() {
|
fn back_image_uri_is_plumbed_through() {
|
||||||
let fx = test_support::make();
|
let fx = test_support::make();
|
||||||
|
|||||||
@@ -139,6 +139,22 @@ pub struct Deck {
|
|||||||
pub notes: String,
|
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
|
/// Criteria for searching the owned library. All fields are optional; an empty
|
||||||
/// filter matches every owned card.
|
/// filter matches every owned card.
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
|||||||
Reference in New Issue
Block a user