1#[cfg(feature = "cli")]
2use color_eyre::Result;
3
4#[cfg(not(feature = "cli"))]
5type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
6
7#[cfg(feature = "wasm")]
8pub mod web;
9use serde::{Deserialize, Serialize};
10use png::{Decoder, Encoder, ColorType, BitDepth};
11use serde_json::Value;
12use std::collections::HashMap;
13#[cfg(not(target_arch = "wasm32"))]
14use std::fs::File;
15use std::io::{BufReader, BufWriter};
16use thiserror::Error;
17
18#[derive(Error, Debug)]
19pub enum PngDbError {
20 #[error("PNG format error: {0}")]
21 PngError(#[from] png::DecodingError),
22 #[error("JSON parsing error: {0}")]
23 JsonError(#[from] serde_json::Error),
24 #[error("IO error: {0}")]
25 IoError(#[from] std::io::Error),
26 #[error("Database error: {0}")]
27 DatabaseError(String),
28 #[error("Query error: {0}")]
29 QueryError(String),
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Schema {
34 pub fields: HashMap<String, String>,
35}
36
37#[derive(Debug, Clone)]
38pub struct DataRow {
39 pub x: u32,
40 pub y: u32,
41 pub data: Value,
42}
43
44pub struct PngDatabase {
45 pub width: u32,
46 pub height: u32,
47 pub schema: Schema,
48 pub rows: Vec<DataRow>,
49}
50
51impl PngDatabase {
52 pub fn new(width: u32, height: u32, schema: Schema) -> Self {
53 Self {
54 width,
55 height,
56 schema,
57 rows: Vec::new(),
58 }
59 }
60
61 #[cfg(not(target_arch = "wasm32"))]
62 pub fn create_empty_png(width: u32, height: u32, schema: Schema, filename: &str) -> Result<Self> {
63 let db = Self::new(width, height, schema);
64 db.save_to_png(filename)?;
65 Ok(db)
66 }
67
68 #[cfg(not(target_arch = "wasm32"))]
69 pub fn load_from_png(filename: &str) -> Result<Self> {
70 let file = File::open(filename)?;
71 let reader = BufReader::new(file);
72 let decoder = Decoder::new(reader);
73 let reader = decoder.read_info()?;
74
75 let info = reader.info();
76 let width = info.width;
77 let height = info.height;
78
79 let mut schema = Schema { fields: HashMap::new() };
80 let mut rows = Vec::new();
81
82 for chunk in reader.info().compressed_latin1_text.iter() {
84 if chunk.keyword == "schema" {
85 let decompressed_text = chunk.get_text()?;
86 schema = serde_json::from_str(&decompressed_text)?;
87 } else if chunk.keyword.starts_with("row_") {
88 let decompressed_text = chunk.get_text()?;
89 let row_data: Value = serde_json::from_str(&decompressed_text)?;
90
91 let coords: Vec<&str> = chunk.keyword.split('_').collect();
93 if coords.len() == 3 {
94 let x = coords[1].parse::<u32>().unwrap_or(0);
95 let y = coords[2].parse::<u32>().unwrap_or(0);
96
97 rows.push(DataRow {
98 x,
99 y,
100 data: row_data,
101 });
102 }
103 }
104 }
105
106 Ok(Self {
107 width,
108 height,
109 schema,
110 rows,
111 })
112 }
113
114 pub fn insert(&mut self, x: u32, y: u32, data: Value) -> Result<()> {
115 if x >= self.width || y >= self.height {
116 return Err(PngDbError::DatabaseError(
117 format!("Coordinates ({}, {}) out of bounds", x, y)
118 ).into());
119 }
120
121 self.rows.push(DataRow { x, y, data });
122 Ok(())
123 }
124
125 #[cfg(not(target_arch = "wasm32"))]
126 pub fn save_to_png(&self, filename: &str) -> Result<()> {
127 let file = File::create(filename)?;
128 let w = &mut BufWriter::new(file);
129
130 let mut encoder = Encoder::new(w, self.width, self.height);
131 encoder.set_color(ColorType::Rgb);
132 encoder.set_depth(BitDepth::Eight);
133
134 let schema_json = serde_json::to_string(&self.schema)?;
136 encoder.add_ztxt_chunk("schema".to_string(), schema_json)?;
137
138 for row in &self.rows {
140 let row_json = serde_json::to_string(&row.data)?;
141 let keyword = format!("row_{}_{}", row.x, row.y);
142 encoder.add_ztxt_chunk(keyword, row_json)?;
143 }
144
145 let mut writer = encoder.write_header()?;
146
147 let image_data = vec![0u8; (self.width * self.height * 3) as usize];
149 writer.write_image_data(&image_data)?;
150 writer.finish()?;
151
152 Ok(())
153 }
154
155 pub fn query(&self, query_str: &str) -> Result<Vec<&DataRow>> {
156 let query = parse_query(query_str)?;
157 let mut results = Vec::new();
158
159 for row in &self.rows {
160 if matches_query(row, &query)? {
161 results.push(row);
162 }
163 }
164
165 Ok(results)
166 }
167}
168
169#[derive(Debug)]
170pub struct Query {
171 pub conditions: Vec<Condition>,
172}
173
174#[derive(Debug)]
175pub enum Condition {
176 Coordinate { field: String, op: ComparisonOp, value: u32 },
177 JsonField { field: String, op: ComparisonOp, value: Value },
178}
179
180#[derive(Debug)]
181pub enum ComparisonOp {
182 Equal,
183 NotEqual,
184 GreaterThan,
185 LessThan,
186 GreaterThanOrEqual,
187 LessThanOrEqual,
188}
189
190pub fn parse_query(query_str: &str) -> Result<Query> {
191 let query_str = query_str.trim();
193
194 if !query_str.to_lowercase().starts_with("where") {
195 return Err(PngDbError::QueryError("Query must start with WHERE".to_string()).into());
196 }
197
198 let conditions_str = &query_str[5..].trim(); let condition_parts: Vec<&str> = conditions_str.split(" AND ").collect();
200
201 let mut conditions = Vec::new();
202
203 for part in condition_parts {
204 let condition = parse_condition(part.trim())?;
205 conditions.push(condition);
206 }
207
208 Ok(Query { conditions })
209}
210
211fn parse_condition(condition_str: &str) -> Result<Condition> {
212 let operators = [">=", "<=", "!=", "=", ">", "<"];
213
214 for op_str in &operators {
215 if let Some(pos) = condition_str.find(op_str) {
216 let field = condition_str[..pos].trim();
217 let value_str = condition_str[pos + op_str.len()..].trim();
218
219 let op = match *op_str {
220 "=" => ComparisonOp::Equal,
221 "!=" => ComparisonOp::NotEqual,
222 ">" => ComparisonOp::GreaterThan,
223 "<" => ComparisonOp::LessThan,
224 ">=" => ComparisonOp::GreaterThanOrEqual,
225 "<=" => ComparisonOp::LessThanOrEqual,
226 _ => unreachable!(),
227 };
228
229 if field == "x" || field == "y" {
231 let value = value_str.parse::<u32>()
232 .map_err(|_| PngDbError::QueryError(format!("Invalid coordinate value: {}", value_str)))?;
233 return Ok(Condition::Coordinate {
234 field: field.to_string(),
235 op,
236 value,
237 });
238 }
239
240 let value = if value_str.starts_with('"') && value_str.ends_with('"') {
242 Value::String(value_str[1..value_str.len()-1].to_string())
243 } else if let Ok(num) = value_str.parse::<i64>() {
244 Value::Number(serde_json::Number::from(num))
245 } else if let Ok(float) = value_str.parse::<f64>() {
246 Value::Number(serde_json::Number::from_f64(float).unwrap())
247 } else if value_str == "true" {
248 Value::Bool(true)
249 } else if value_str == "false" {
250 Value::Bool(false)
251 } else {
252 Value::String(value_str.to_string())
253 };
254
255 return Ok(Condition::JsonField {
256 field: field.to_string(),
257 op,
258 value,
259 });
260 }
261 }
262
263 Err(PngDbError::QueryError(format!("Invalid condition: {}", condition_str)).into())
264}
265
266fn matches_query(row: &DataRow, query: &Query) -> Result<bool> {
267 for condition in &query.conditions {
268 if !matches_condition(row, condition)? {
269 return Ok(false);
270 }
271 }
272 Ok(true)
273}
274
275fn matches_condition(row: &DataRow, condition: &Condition) -> Result<bool> {
276 match condition {
277 Condition::Coordinate { field, op, value } => {
278 let coord_value = if field == "x" { row.x } else { row.y };
279 Ok(compare_numbers(coord_value as i64, *value as i64, op))
280 }
281 Condition::JsonField { field, op, value } => {
282 if let Some(field_value) = row.data.get(field) {
283 compare_json_values(field_value, value, op)
284 } else {
285 Ok(false)
286 }
287 }
288 }
289}
290
291fn compare_numbers(left: i64, right: i64, op: &ComparisonOp) -> bool {
292 match op {
293 ComparisonOp::Equal => left == right,
294 ComparisonOp::NotEqual => left != right,
295 ComparisonOp::GreaterThan => left > right,
296 ComparisonOp::LessThan => left < right,
297 ComparisonOp::GreaterThanOrEqual => left >= right,
298 ComparisonOp::LessThanOrEqual => left <= right,
299 }
300}
301
302fn compare_json_values(left: &Value, right: &Value, op: &ComparisonOp) -> Result<bool> {
303 use Value::*;
304
305 match (left, right) {
306 (String(l), String(r)) => Ok(match op {
307 ComparisonOp::Equal => l == r,
308 ComparisonOp::NotEqual => l != r,
309 _ => return Err(PngDbError::QueryError("String comparison only supports = and !=".to_string()).into()),
310 }),
311 (Number(l), Number(r)) => {
312 let l_val = l.as_f64().unwrap_or(0.0);
313 let r_val = r.as_f64().unwrap_or(0.0);
314 Ok(match op {
315 ComparisonOp::Equal => (l_val - r_val).abs() < f64::EPSILON,
316 ComparisonOp::NotEqual => (l_val - r_val).abs() >= f64::EPSILON,
317 ComparisonOp::GreaterThan => l_val > r_val,
318 ComparisonOp::LessThan => l_val < r_val,
319 ComparisonOp::GreaterThanOrEqual => l_val >= r_val,
320 ComparisonOp::LessThanOrEqual => l_val <= r_val,
321 })
322 }
323 (Bool(l), Bool(r)) => Ok(match op {
324 ComparisonOp::Equal => l == r,
325 ComparisonOp::NotEqual => l != r,
326 _ => return Err(PngDbError::QueryError("Boolean comparison only supports = and !=".to_string()).into()),
327 }),
328 _ => Err(PngDbError::QueryError("Cannot compare different value types".to_string()).into()),
329 }
330}
331