Skip to main content

mtgjson_sdk/queries/
identifiers.rs

1//! Identifier queries that join `card_identifiers` with `cards`.
2//!
3//! Provides a generic `find_by` method plus 16 named convenience methods for every
4//! known identifier column (Scryfall, TCGplayer, MTGO, etc.).
5
6use std::collections::HashMap;
7
8use serde_json::Value;
9
10use crate::error::{MtgjsonError, Result};
11use crate::sql_builder::SqlBuilder;
12
13// ---------------------------------------------------------------------------
14// Known identifier columns
15// ---------------------------------------------------------------------------
16
17/// All identifier column names that exist in the `card_identifiers` parquet table.
18pub const KNOWN_ID_COLUMNS: &[&str] = &[
19    "cardKingdomEtchedId",
20    "cardKingdomFoilId",
21    "cardKingdomId",
22    "cardsphereId",
23    "cardsphereFoilId",
24    "mcmId",
25    "mcmMetaId",
26    "mtgArenaId",
27    "mtgjsonFoilVersionId",
28    "mtgjsonNonFoilVersionId",
29    "mtgjsonV4Id",
30    "mtgoFoilId",
31    "mtgoId",
32    "multiverseId",
33    "scryfallId",
34    "scryfallCardBackId",
35    "scryfallIllustrationId",
36    "scryfallOracleId",
37    "tcgplayerEtchedProductId",
38    "tcgplayerProductId",
39];
40
41// ---------------------------------------------------------------------------
42// IdentifierQuery
43// ---------------------------------------------------------------------------
44
45/// Query interface for looking up cards by external identifiers.
46pub struct IdentifierQuery<'a> {
47    conn: &'a crate::connection::Connection,
48}
49
50impl<'a> IdentifierQuery<'a> {
51    /// Create a new `IdentifierQuery` bound to the given connection.
52    pub fn new(conn: &'a crate::connection::Connection) -> Self {
53        Self { conn }
54    }
55
56    /// Generic find: look up cards whose `column` in `card_identifiers` matches `value`.
57    ///
58    /// Returns full card rows (joined from the `cards` view).
59    ///
60    /// Returns `Err(InvalidArgument)` if `column` is not in [`KNOWN_ID_COLUMNS`].
61    pub fn find_by(&self, column: &str, value: &str) -> Result<Vec<Value>> {
62        if !KNOWN_ID_COLUMNS.contains(&column) {
63            return Err(MtgjsonError::InvalidArgument(format!(
64                "Unknown identifier column: '{}'. Valid columns: {:?}",
65                column, KNOWN_ID_COLUMNS
66            )));
67        }
68
69        self.conn.ensure_views(&["cards", "card_identifiers"])?;
70
71        let condition = format!("ci.{} = ?", column);
72        let (sql, params) = SqlBuilder::new("cards c")
73            .join("JOIN card_identifiers ci ON c.uuid = ci.uuid")
74            .where_clause(&condition, &[value])
75            .build();
76
77        let rows = self.conn.execute(&sql, &params)?;
78        Ok(rows_to_values(rows))
79    }
80
81    /// Get all known identifiers for a card UUID.
82    ///
83    /// Returns the full `card_identifiers` row as a JSON object.
84    pub fn get_identifiers(&self, uuid: &str) -> Result<Option<Value>> {
85        self.conn.ensure_views(&["card_identifiers"])?;
86
87        let (sql, params) = SqlBuilder::new("card_identifiers")
88            .where_eq("uuid", uuid)
89            .limit(1)
90            .build();
91
92        let rows = self.conn.execute(&sql, &params)?;
93        Ok(rows
94            .into_iter()
95            .next()
96            .map(|r| serde_json::to_value(r).unwrap_or(Value::Null)))
97    }
98
99    // -- Convenience methods (one per known column) -------------------------
100
101    /// Find cards by Card Kingdom etched product ID.
102    pub fn find_by_card_kingdom_etched_id(&self, value: &str) -> Result<Vec<Value>> {
103        self.find_by("cardKingdomEtchedId", value)
104    }
105
106    /// Find cards by Card Kingdom foil product ID.
107    pub fn find_by_card_kingdom_foil_id(&self, value: &str) -> Result<Vec<Value>> {
108        self.find_by("cardKingdomFoilId", value)
109    }
110
111    /// Find cards by Card Kingdom product ID.
112    pub fn find_by_card_kingdom_id(&self, value: &str) -> Result<Vec<Value>> {
113        self.find_by("cardKingdomId", value)
114    }
115
116    /// Find cards by Cardsphere ID.
117    pub fn find_by_cardsphere_id(&self, value: &str) -> Result<Vec<Value>> {
118        self.find_by("cardsphereId", value)
119    }
120
121    /// Find cards by Cardsphere foil ID.
122    pub fn find_by_cardsphere_foil_id(&self, value: &str) -> Result<Vec<Value>> {
123        self.find_by("cardsphereFoilId", value)
124    }
125
126    /// Find cards by MCM (Cardmarket) ID.
127    pub fn find_by_mcm_id(&self, value: &str) -> Result<Vec<Value>> {
128        self.find_by("mcmId", value)
129    }
130
131    /// Find cards by MCM meta ID.
132    pub fn find_by_mcm_meta_id(&self, value: &str) -> Result<Vec<Value>> {
133        self.find_by("mcmMetaId", value)
134    }
135
136    /// Find cards by MTG Arena ID.
137    pub fn find_by_mtg_arena_id(&self, value: &str) -> Result<Vec<Value>> {
138        self.find_by("mtgArenaId", value)
139    }
140
141    /// Find cards by MTGJSON foil version ID.
142    pub fn find_by_mtgjson_foil_version_id(&self, value: &str) -> Result<Vec<Value>> {
143        self.find_by("mtgjsonFoilVersionId", value)
144    }
145
146    /// Find cards by MTGJSON non-foil version ID.
147    pub fn find_by_mtgjson_non_foil_version_id(&self, value: &str) -> Result<Vec<Value>> {
148        self.find_by("mtgjsonNonFoilVersionId", value)
149    }
150
151    /// Find cards by MTGJSON v4 ID.
152    pub fn find_by_mtgjson_v4_id(&self, value: &str) -> Result<Vec<Value>> {
153        self.find_by("mtgjsonV4Id", value)
154    }
155
156    /// Find cards by MTGO foil ID.
157    pub fn find_by_mtgo_foil_id(&self, value: &str) -> Result<Vec<Value>> {
158        self.find_by("mtgoFoilId", value)
159    }
160
161    /// Find cards by MTGO ID.
162    pub fn find_by_mtgo_id(&self, value: &str) -> Result<Vec<Value>> {
163        self.find_by("mtgoId", value)
164    }
165
166    /// Find cards by Multiverse ID.
167    pub fn find_by_multiverse_id(&self, value: &str) -> Result<Vec<Value>> {
168        self.find_by("multiverseId", value)
169    }
170
171    /// Find cards by Scryfall ID.
172    pub fn find_by_scryfall_id(&self, value: &str) -> Result<Vec<Value>> {
173        self.find_by("scryfallId", value)
174    }
175
176    /// Find cards by Scryfall card back ID.
177    pub fn find_by_scryfall_card_back_id(&self, value: &str) -> Result<Vec<Value>> {
178        self.find_by("scryfallCardBackId", value)
179    }
180
181    /// Find cards by Scryfall illustration ID.
182    pub fn find_by_scryfall_illustration_id(&self, value: &str) -> Result<Vec<Value>> {
183        self.find_by("scryfallIllustrationId", value)
184    }
185
186    /// Find cards by Scryfall Oracle ID.
187    pub fn find_by_scryfall_oracle_id(&self, value: &str) -> Result<Vec<Value>> {
188        self.find_by("scryfallOracleId", value)
189    }
190
191    /// Find cards by TCGplayer etched product ID.
192    pub fn find_by_tcgplayer_etched_product_id(&self, value: &str) -> Result<Vec<Value>> {
193        self.find_by("tcgplayerEtchedProductId", value)
194    }
195
196    /// Find cards by TCGplayer product ID.
197    pub fn find_by_tcgplayer_product_id(&self, value: &str) -> Result<Vec<Value>> {
198        self.find_by("tcgplayerProductId", value)
199    }
200}
201
202// ---------------------------------------------------------------------------
203// Helpers
204// ---------------------------------------------------------------------------
205
206fn rows_to_values(rows: Vec<HashMap<String, Value>>) -> Vec<Value> {
207    rows.into_iter()
208        .map(|r| serde_json::to_value(r).unwrap_or(Value::Null))
209        .collect()
210}