Skip to main content

png_db/
lib.rs

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        // Read zTXt chunks
83        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                // Extract coordinates from keyword (row_x_y format)
92                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        // Add schema as zTXt chunk
135        let schema_json = serde_json::to_string(&self.schema)?;
136        encoder.add_ztxt_chunk("schema".to_string(), schema_json)?;
137        
138        // Add each row as a zTXt chunk
139        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        // Create a simple RGB image with black pixels
148        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    // Simple parser for WHERE clauses
192    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(); // Remove "WHERE"
199    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            // Check if it's a coordinate field
230            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            // Parse JSON value
241            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