Skip to main content

mtgjson_sdk/queries/
sealed.rs

1//! Sealed product queries against the DuckDB-backed parquet data.
2//!
3//! Sealed product data lives in the `sets` table's `sealedProduct` column. This module
4//! gracefully returns empty results if the column doesn't exist in the schema.
5
6use std::collections::HashMap;
7
8use serde_json::Value;
9
10use crate::error::Result;
11use crate::sql_builder::SqlBuilder;
12
13// ---------------------------------------------------------------------------
14// SealedQuery
15// ---------------------------------------------------------------------------
16
17/// Query interface for MTG sealed products derived from set data.
18pub struct SealedQuery<'a> {
19    conn: &'a crate::connection::Connection,
20}
21
22impl<'a> SealedQuery<'a> {
23    /// Create a new `SealedQuery` bound to the given connection.
24    pub fn new(conn: &'a crate::connection::Connection) -> Self {
25        Self { conn }
26    }
27
28    /// Check whether the `sealedProduct` column exists on the `sets` table.
29    fn has_sealed_column(&self) -> bool {
30        // Try a lightweight probe query; if it fails, the column doesn't exist.
31        let sql = "SELECT sealedProduct FROM sets LIMIT 0";
32        self.conn.execute(sql, &[]).is_ok()
33    }
34
35    /// List all sealed products, optionally filtered by set code and/or category.
36    ///
37    /// Returns an empty vector if the `sealedProduct` column is not present.
38    pub fn list(
39        &self,
40        set_code: Option<&str>,
41        category: Option<&str>,
42        limit: Option<usize>,
43    ) -> Result<Vec<Value>> {
44        self.conn.ensure_views(&["sets"])?;
45
46        if !self.has_sealed_column() {
47            return Ok(Vec::new());
48        }
49
50        let mut qb = SqlBuilder::new("sets");
51        qb.select(&["code", "name", "sealedProduct"]);
52
53        if let Some(sc) = set_code {
54            let upper = sc.to_uppercase();
55            qb.where_eq("code", &upper);
56        }
57
58        // Only include sets that actually have sealed product data
59        qb.where_clause("sealedProduct IS NOT NULL", &[]);
60
61        let (sql, params) = qb.build();
62        let rows = self.conn.execute(&sql, &params)?;
63
64        // Flatten: each row may contain a list of sealed products under the
65        // `sealedProduct` key. We extract and tag each product with the set code.
66        let mut results: Vec<Value> = Vec::new();
67        let limit = limit.unwrap_or(100);
68
69        for row in rows {
70            let code = row
71                .get("code")
72                .and_then(|v| v.as_str())
73                .unwrap_or("")
74                .to_string();
75            let set_name = row
76                .get("name")
77                .and_then(|v| v.as_str())
78                .unwrap_or("")
79                .to_string();
80
81            if let Some(Value::Array(products)) = row.get("sealedProduct") {
82                for product in products {
83                    let mut p = product.clone();
84                    if let Value::Object(ref mut map) = p {
85                        // Apply category filter if specified
86                        if let Some(cat) = category {
87                            let product_cat = map
88                                .get("category")
89                                .and_then(|v| v.as_str())
90                                .unwrap_or("");
91                            if product_cat != cat {
92                                continue;
93                            }
94                        }
95
96                        map.insert("setCode".to_string(), Value::String(code.clone()));
97                        map.insert("setName".to_string(), Value::String(set_name.clone()));
98                    }
99                    results.push(p);
100                    if results.len() >= limit {
101                        return Ok(results);
102                    }
103                }
104            }
105        }
106
107        Ok(results)
108    }
109
110    /// Get a single sealed product by its UUID.
111    ///
112    /// Returns `None` if the product is not found or the `sealedProduct` column
113    /// is not present.
114    pub fn get(&self, uuid: &str) -> Result<Option<Value>> {
115        self.conn.ensure_views(&["sets"])?;
116
117        if !self.has_sealed_column() {
118            return Ok(None);
119        }
120
121        // Search across all sets for a sealed product with the given UUID
122        let rows = self.conn.execute(
123            "SELECT code, name, sealedProduct FROM sets WHERE sealedProduct IS NOT NULL",
124            &[],
125        )?;
126
127        for row in rows {
128            let code = row
129                .get("code")
130                .and_then(|v| v.as_str())
131                .unwrap_or("")
132                .to_string();
133            let set_name = row
134                .get("name")
135                .and_then(|v| v.as_str())
136                .unwrap_or("")
137                .to_string();
138
139            if let Some(Value::Array(products)) = row.get("sealedProduct") {
140                for product in products {
141                    let product_uuid = product
142                        .get("uuid")
143                        .and_then(|v| v.as_str())
144                        .unwrap_or("");
145                    if product_uuid == uuid {
146                        let mut p = product.clone();
147                        if let Value::Object(ref mut map) = p {
148                            map.insert("setCode".to_string(), Value::String(code.clone()));
149                            map.insert(
150                                "setName".to_string(),
151                                Value::String(set_name.clone()),
152                            );
153                        }
154                        return Ok(Some(p));
155                    }
156                }
157            }
158        }
159
160        Ok(None)
161    }
162}
163
164// ---------------------------------------------------------------------------
165// Helpers
166// ---------------------------------------------------------------------------
167
168#[allow(dead_code)]
169fn rows_to_values(rows: Vec<HashMap<String, Value>>) -> Vec<Value> {
170    rows.into_iter()
171        .map(|r| serde_json::to_value(r).unwrap_or(Value::Null))
172        .collect()
173}