Skip to main content

mtgjson_sdk/queries/
tokens.rs

1//! Token queries against the DuckDB-backed parquet data.
2
3use std::collections::HashMap;
4
5use serde_json::Value;
6
7use crate::error::Result;
8use crate::sql_builder::SqlBuilder;
9
10// ---------------------------------------------------------------------------
11// SearchTokensParams
12// ---------------------------------------------------------------------------
13
14/// Parameters for the advanced token search.
15#[derive(Debug, Clone, Default)]
16pub struct SearchTokensParams {
17    pub name: Option<String>,
18    pub set_code: Option<String>,
19    pub colors: Option<Vec<String>>,
20    pub types: Option<String>,
21    pub artist: Option<String>,
22    pub limit: Option<usize>,
23    pub offset: Option<usize>,
24}
25
26// ---------------------------------------------------------------------------
27// TokenQuery
28// ---------------------------------------------------------------------------
29
30/// Query interface for MTG tokens backed by the `tokens` parquet view.
31pub struct TokenQuery<'a> {
32    conn: &'a crate::connection::Connection,
33}
34
35impl<'a> TokenQuery<'a> {
36    /// Create a new `TokenQuery` bound to the given connection.
37    pub fn new(conn: &'a crate::connection::Connection) -> Self {
38        Self { conn }
39    }
40
41    /// Retrieve a single token by its UUID.
42    pub fn get_by_uuid(&self, uuid: &str) -> Result<Option<Value>> {
43        self.conn.ensure_views(&["tokens"])?;
44
45        let (sql, params) = SqlBuilder::new("tokens")
46            .where_eq("uuid", uuid)
47            .limit(1)
48            .build();
49
50        let rows = self.conn.execute(&sql, &params)?;
51        Ok(rows.into_iter().next().map(|r| serde_json::to_value(r).unwrap_or(Value::Null)))
52    }
53
54    /// Retrieve multiple tokens by their UUIDs.
55    pub fn get_by_uuids(&self, uuids: &[&str]) -> Result<Vec<Value>> {
56        self.conn.ensure_views(&["tokens"])?;
57
58        let (sql, params) = SqlBuilder::new("tokens")
59            .where_in("uuid", uuids)
60            .build();
61
62        let rows = self.conn.execute(&sql, &params)?;
63        Ok(rows_to_values(rows))
64    }
65
66    /// Get all tokens with the given name, optionally filtered by set code.
67    pub fn get_by_name(&self, name: &str, set_code: Option<&str>) -> Result<Vec<Value>> {
68        self.conn.ensure_views(&["tokens"])?;
69
70        let mut qb = SqlBuilder::new("tokens");
71        qb.where_eq("name", name);
72
73        if let Some(sc) = set_code {
74            qb.where_eq("setCode", sc);
75        }
76
77        let (sql, params) = qb.build();
78        let rows = self.conn.execute(&sql, &params)?;
79        Ok(rows_to_values(rows))
80    }
81
82    /// Search tokens using a combination of filters.
83    pub fn search(&self, params: &SearchTokensParams) -> Result<Vec<Value>> {
84        self.conn.ensure_views(&["tokens"])?;
85
86        let mut qb = SqlBuilder::new("tokens");
87
88        if let Some(ref name) = params.name {
89            if name.contains('%') {
90                qb.where_like("tokens.name", name);
91            } else {
92                qb.where_eq("tokens.name", name);
93            }
94        }
95
96        if let Some(ref sc) = params.set_code {
97            qb.where_eq("tokens.setCode", sc);
98        }
99
100        if let Some(ref colors) = params.colors {
101            for color in colors {
102                qb.where_clause(
103                    "list_contains(tokens.colors, ?)",
104                    &[color.as_str()],
105                );
106            }
107        }
108
109        if let Some(ref types) = params.types {
110            qb.where_like("tokens.type", &format!("%{}%", types));
111        }
112
113        if let Some(ref artist) = params.artist {
114            qb.where_like("tokens.artist", &format!("%{}%", artist));
115        }
116
117        let limit = params.limit.unwrap_or(100);
118        let offset = params.offset.unwrap_or(0);
119        qb.limit(limit);
120        qb.offset(offset);
121
122        let (sql, sql_params) = qb.build();
123        let rows = self.conn.execute(&sql, &sql_params)?;
124        Ok(rows_to_values(rows))
125    }
126
127    /// Get all tokens for a specific set code.
128    pub fn for_set(&self, set_code: &str) -> Result<Vec<Value>> {
129        self.conn.ensure_views(&["tokens"])?;
130
131        let (sql, params) = SqlBuilder::new("tokens")
132            .where_eq("setCode", set_code)
133            .build();
134
135        let rows = self.conn.execute(&sql, &params)?;
136        Ok(rows_to_values(rows))
137    }
138
139    /// Count tokens, optionally filtered by the supplied column/value pairs.
140    pub fn count(&self, filters: &HashMap<String, String>) -> Result<i64> {
141        self.conn.ensure_views(&["tokens"])?;
142
143        let mut qb = SqlBuilder::new("tokens");
144        qb.select(&["COUNT(*) AS cnt"]);
145
146        for (col, val) in filters {
147            qb.where_eq(col, val);
148        }
149
150        let (sql, params) = qb.build();
151        let rows = self.conn.execute(&sql, &params)?;
152
153        let cnt = rows
154            .first()
155            .and_then(|r| r.get("cnt"))
156            .and_then(|v| v.as_i64())
157            .unwrap_or(0);
158
159        Ok(cnt)
160    }
161}
162
163// ---------------------------------------------------------------------------
164// Helpers
165// ---------------------------------------------------------------------------
166
167fn rows_to_values(rows: Vec<HashMap<String, Value>>) -> Vec<Value> {
168    rows.into_iter()
169        .map(|r| serde_json::to_value(r).unwrap_or(Value::Null))
170        .collect()
171}