1use std::collections::HashMap;
4
5use serde_json::Value;
6
7use crate::error::Result;
8use crate::sql_builder::SqlBuilder;
9
10#[derive(Debug, Clone, Default)]
18pub struct SearchCardsParams {
19 pub name: Option<String>,
20 pub fuzzy_name: Option<String>,
21 pub localized_name: Option<String>,
22 pub set_code: Option<String>,
23 pub colors: Option<Vec<String>>,
24 pub color_identity: Option<Vec<String>>,
25 pub types: Option<String>,
26 pub rarity: Option<String>,
27 pub legal_in: Option<String>,
28 pub mana_value: Option<f64>,
29 pub mana_value_lte: Option<f64>,
30 pub mana_value_gte: Option<f64>,
31 pub text: Option<String>,
32 pub text_regex: Option<String>,
33 pub power: Option<String>,
34 pub toughness: Option<String>,
35 pub artist: Option<String>,
36 pub keyword: Option<String>,
37 pub is_promo: Option<bool>,
38 pub availability: Option<String>,
39 pub language: Option<String>,
40 pub layout: Option<String>,
41 pub set_type: Option<String>,
42 pub limit: Option<usize>,
43 pub offset: Option<usize>,
44}
45
46pub struct CardQuery<'a> {
52 conn: &'a crate::connection::Connection,
53}
54
55impl<'a> CardQuery<'a> {
56 pub fn new(conn: &'a crate::connection::Connection) -> Self {
58 Self { conn }
59 }
60
61 pub fn get_by_uuid(&self, uuid: &str) -> Result<Option<Value>> {
65 self.conn.ensure_views(&["cards"])?;
66
67 let (sql, params) = SqlBuilder::new("cards")
68 .where_eq("uuid", uuid)
69 .limit(1)
70 .build();
71
72 let rows = self.conn.execute(&sql, ¶ms)?;
73 Ok(rows.into_iter().next().map(|r| serde_json::to_value(r).unwrap_or(Value::Null)))
74 }
75
76 pub fn get_by_uuids(&self, uuids: &[&str]) -> Result<Vec<Value>> {
80 self.conn.ensure_views(&["cards"])?;
81
82 let (sql, params) = SqlBuilder::new("cards")
83 .where_in("uuid", uuids)
84 .build();
85
86 let rows = self.conn.execute(&sql, ¶ms)?;
87 Ok(rows_to_values(rows))
88 }
89
90 pub fn get_by_name(&self, name: &str, set_code: Option<&str>) -> Result<Vec<Value>> {
94 self.conn.ensure_views(&["cards"])?;
95
96 let mut qb = SqlBuilder::new("cards");
97 qb.where_eq("name", name);
98
99 if let Some(sc) = set_code {
100 qb.where_eq("setCode", sc);
101 }
102
103 let (sql, params) = qb.build();
104 let rows = self.conn.execute(&sql, ¶ms)?;
105 Ok(rows_to_values(rows))
106 }
107
108 pub fn get_printings(&self, name: &str) -> Result<Vec<Value>> {
110 self.get_by_name(name, None)
111 }
112
113 pub fn get_atomic(&self, name: &str) -> Result<Vec<Value>> {
120 self.conn.ensure_views(&["cards"])?;
121
122 let (sql, params) = SqlBuilder::new("cards")
124 .select(&["DISTINCT ON (name, faceName) *"])
125 .where_eq("name", name)
126 .build();
127
128 let rows = self.conn.execute(&sql, ¶ms)?;
129 if !rows.is_empty() {
130 return Ok(rows_to_values(rows));
131 }
132
133 let (sql2, params2) = SqlBuilder::new("cards")
135 .select(&["DISTINCT ON (name, faceName) *"])
136 .where_eq("faceName", name)
137 .build();
138
139 let rows2 = self.conn.execute(&sql2, ¶ms2)?;
140 Ok(rows_to_values(rows2))
141 }
142
143 pub fn find_by_scryfall_id(&self, scryfall_id: &str) -> Result<Vec<Value>> {
147 self.conn.ensure_views(&["cards", "card_identifiers"])?;
148
149 let (sql, params) = SqlBuilder::new("cards c")
150 .join("JOIN card_identifiers ci ON c.uuid = ci.uuid")
151 .where_eq("ci.scryfallId", scryfall_id)
152 .build();
153
154 let rows = self.conn.execute(&sql, ¶ms)?;
155 Ok(rows_to_values(rows))
156 }
157
158 pub fn random(&self, count: usize) -> Result<Vec<Value>> {
162 self.conn.ensure_views(&["cards"])?;
163
164 let sql = format!("SELECT * FROM cards USING SAMPLE {}", count);
165 let rows = self.conn.execute(&sql, &[])?;
166 Ok(rows_to_values(rows))
167 }
168
169 pub fn count(&self, filters: &HashMap<String, String>) -> Result<i64> {
173 self.conn.ensure_views(&["cards"])?;
174
175 let mut qb = SqlBuilder::new("cards");
176 qb.select(&["COUNT(*) AS cnt"]);
177
178 for (col, val) in filters {
179 qb.where_eq(col, val);
180 }
181
182 let (sql, params) = qb.build();
183 let rows = self.conn.execute(&sql, ¶ms)?;
184
185 let cnt = rows
186 .first()
187 .and_then(|r| r.get("cnt"))
188 .and_then(|v| v.as_i64())
189 .unwrap_or(0);
190
191 Ok(cnt)
192 }
193
194 pub fn search(&self, params: &SearchCardsParams) -> Result<Vec<Value>> {
201 let mut views: Vec<&str> = vec!["cards"];
203 if params.legal_in.is_some() {
204 views.push("card_legalities");
205 }
206 if params.localized_name.is_some() {
207 views.push("card_foreign_data");
208 }
209 if params.set_type.is_some() {
210 views.push("sets");
211 }
212 self.conn.ensure_views(&views)?;
213
214 let mut qb = SqlBuilder::new("cards");
215
216 if let Some(ref name) = params.name {
218 if name.contains('%') {
219 qb.where_like("cards.name", name);
220 } else {
221 qb.where_eq("cards.name", name);
222 }
223 }
224
225 if let Some(ref fuzzy) = params.fuzzy_name {
227 qb.where_fuzzy("cards.name", fuzzy, 0.8);
228 qb.order_by(&[&format!(
229 "jaro_winkler_similarity(cards.name, '{}') DESC",
230 fuzzy.replace('\'', "''")
231 )]);
232 }
233
234 if let Some(ref loc_name) = params.localized_name {
236 qb.join("JOIN card_foreign_data cfd ON cards.uuid = cfd.uuid");
237 qb.where_like("cfd.name", &format!("%{}%", loc_name));
238 }
239
240 if let Some(ref sc) = params.set_code {
242 qb.where_eq("cards.setCode", sc);
243 }
244
245 if let Some(ref colors) = params.colors {
247 for color in colors {
248 qb.where_clause(
249 "list_contains(cards.colors, ?)",
250 &[color.as_str()],
251 );
252 }
253 }
254
255 if let Some(ref ci) = params.color_identity {
257 for color in ci {
258 qb.where_clause(
259 "list_contains(cards.colorIdentity, ?)",
260 &[color.as_str()],
261 );
262 }
263 }
264
265 if let Some(ref types) = params.types {
267 qb.where_like("cards.type", &format!("%{}%", types));
268 }
269
270 if let Some(ref rarity) = params.rarity {
272 qb.where_eq("cards.rarity", rarity);
273 }
274
275 if let Some(ref format_name) = params.legal_in {
277 qb.join("JOIN card_legalities cl ON cards.uuid = cl.uuid");
278 qb.where_eq("cl.format", format_name);
279 qb.where_eq("cl.status", "Legal");
280 }
281
282 if let Some(mv) = params.mana_value {
284 qb.where_eq("cards.manaValue", &mv.to_string());
285 }
286
287 if let Some(mv) = params.mana_value_lte {
289 qb.where_lte("cards.manaValue", &mv.to_string());
290 }
291
292 if let Some(mv) = params.mana_value_gte {
294 qb.where_gte("cards.manaValue", &mv.to_string());
295 }
296
297 if let Some(ref text) = params.text {
299 qb.where_like("cards.text", &format!("%{}%", text));
300 }
301
302 if let Some(ref regex) = params.text_regex {
304 qb.where_regex("cards.text", regex);
305 }
306
307 if let Some(ref power) = params.power {
309 qb.where_eq("cards.power", power);
310 }
311
312 if let Some(ref toughness) = params.toughness {
314 qb.where_eq("cards.toughness", toughness);
315 }
316
317 if let Some(ref artist) = params.artist {
319 qb.where_like("cards.artist", &format!("%{}%", artist));
320 }
321
322 if let Some(ref kw) = params.keyword {
324 qb.where_clause(
325 "list_contains(cards.keywords, ?)",
326 &[kw.as_str()],
327 );
328 }
329
330 if let Some(promo) = params.is_promo {
334 if promo {
335 qb.where_clause("cards.isPromo IS TRUE", &[]);
336 } else {
337 qb.where_clause("cards.isPromo IS NOT TRUE", &[]);
338 }
339 }
340
341 if let Some(ref avail) = params.availability {
343 qb.where_clause(
344 "list_contains(cards.availability, ?)",
345 &[avail.as_str()],
346 );
347 }
348
349 if let Some(ref lang) = params.language {
351 qb.where_eq("cards.language", lang);
352 }
353
354 if let Some(ref layout) = params.layout {
356 qb.where_eq("cards.layout", layout);
357 }
358
359 if let Some(ref st) = params.set_type {
361 qb.join("JOIN sets s ON cards.setCode = s.code");
362 qb.where_eq("s.type", st);
363 }
364
365 let limit = params.limit.unwrap_or(100);
367 let offset = params.offset.unwrap_or(0);
368 qb.limit(limit);
369 qb.offset(offset);
370
371 let (sql, sql_params) = qb.build();
372 let rows = self.conn.execute(&sql, &sql_params)?;
373 Ok(rows_to_values(rows))
374 }
375}
376
377fn rows_to_values(rows: Vec<HashMap<String, Value>>) -> Vec<Value> {
383 rows.into_iter()
384 .map(|r| serde_json::to_value(r).unwrap_or(Value::Null))
385 .collect()
386}