Skip to main content

what_core/database/
mod.rs

1//! Database Adapter — unified interface for SQLite, Cloudflare D1, and Supabase backends
2//!
3//! Configured via `[database]` in `what.toml`. Defaults to SQLite when no config is present.
4
5use serde_json::Value;
6use std::collections::HashMap;
7
8use crate::Result;
9
10mod sqlite;
11pub use sqlite::SqliteDatabase;
12
13pub mod d1;
14pub use d1::D1Database;
15
16pub mod supabase;
17pub use supabase::SupabaseDatabase;
18
19/// Query options for collection retrieval
20#[derive(Debug, Default, Clone)]
21pub struct CollectionQuery {
22    /// Sort expression: "field:asc" or "field:desc"
23    pub sort: Option<String>,
24    /// Filter expression: "field=value", "field>N", joined by "&" (AND) or "," (OR)
25    pub filter: Option<String>,
26    /// Full-text search term
27    pub search: Option<String>,
28    /// Fields to search in (comma-separated)
29    pub search_fields: Option<String>,
30    /// Max items to return
31    pub limit: Option<usize>,
32    /// Items to skip
33    pub offset: Option<usize>,
34    /// Mandatory scope filters AND-ed into every query regardless of the
35    /// user-supplied `filter`. Set by authorization policies (owner/tenant
36    /// scoping) — the user cannot widen past these. Each string uses the same
37    /// mini-language as `filter` (comma = OR, `&` = AND).
38    pub forced_filters: Vec<String>,
39}
40
41/// Unified database adapter — dispatches to SQLite, D1, or Supabase
42#[derive(Clone)]
43pub enum DatabaseAdapter {
44    Sqlite(SqliteDatabase),
45    D1(D1Database),
46    Supabase(SupabaseDatabase),
47}
48
49impl DatabaseAdapter {
50    /// Get all items from a collection (with optional query parameters)
51    pub async fn get_collection(&self, name: &str) -> Option<Vec<Value>> {
52        match self {
53            Self::Sqlite(db) => db
54                .get_collection(name)
55                .await
56                .map_err(|e| {
57                    tracing::warn!("SQLite get_collection '{}' error: {}", name, e);
58                    e
59                })
60                .ok(),
61            Self::D1(db) => db
62                .get_collection(name)
63                .await
64                .map_err(|e| {
65                    tracing::warn!("D1 get_collection '{}' error: {}", name, e);
66                    e
67                })
68                .ok(),
69            Self::Supabase(db) => db
70                .get_collection(name)
71                .await
72                .map_err(|e| {
73                    tracing::warn!("Supabase get_collection '{}' error: {}", name, e);
74                    e
75                })
76                .ok(),
77        }
78    }
79
80    /// Get items with query options (sort, filter, search, limit, offset)
81    pub async fn query_collection(
82        &self,
83        name: &str,
84        query: &CollectionQuery,
85    ) -> Option<Vec<Value>> {
86        match self {
87            Self::Sqlite(db) => db
88                .query_collection(name, query)
89                .await
90                .map_err(|e| {
91                    tracing::warn!("SQLite query_collection '{}' error: {}", name, e);
92                    e
93                })
94                .ok(),
95            Self::D1(db) => db
96                .query_collection(name, query)
97                .await
98                .map_err(|e| {
99                    tracing::warn!("D1 query_collection '{}' error: {}", name, e);
100                    e
101                })
102                .ok(),
103            Self::Supabase(db) => db
104                .query_collection(name, query)
105                .await
106                .map_err(|e| {
107                    tracing::warn!("Supabase query_collection '{}' error: {}", name, e);
108                    e
109                })
110                .ok(),
111        }
112    }
113
114    /// Find items by a field value
115    pub async fn find_by(&self, collection: &str, field: &str, value: &Value) -> Vec<Value> {
116        match self {
117            Self::Sqlite(db) => db
118                .find_by(collection, field, value)
119                .await
120                .unwrap_or_else(|e| {
121                    tracing::warn!("SQLite find_by '{}.{}' error: {}", collection, field, e);
122                    Vec::new()
123                }),
124            Self::D1(db) => db
125                .find_by(collection, field, value)
126                .await
127                .unwrap_or_else(|e| {
128                    tracing::warn!("D1 find_by '{}.{}' error: {}", collection, field, e);
129                    Vec::new()
130                }),
131            Self::Supabase(db) => db
132                .find_by(collection, field, value)
133                .await
134                .unwrap_or_else(|e| {
135                    tracing::warn!("Supabase find_by '{}.{}' error: {}", collection, field, e);
136                    Vec::new()
137                }),
138        }
139    }
140
141    /// Find a single item by field value
142    pub async fn find_one_by(&self, collection: &str, field: &str, value: &Value) -> Option<Value> {
143        match self {
144            Self::Sqlite(db) => db
145                .find_one_by(collection, field, value)
146                .await
147                .map_err(|e| {
148                    tracing::warn!("SQLite find_one_by '{}.{}' error: {}", collection, field, e);
149                    e
150                })
151                .ok()
152                .flatten(),
153            Self::D1(db) => db
154                .find_one_by(collection, field, value)
155                .await
156                .map_err(|e| {
157                    tracing::warn!("D1 find_one_by '{}.{}' error: {}", collection, field, e);
158                    e
159                })
160                .ok()
161                .flatten(),
162            Self::Supabase(db) => db
163                .find_one_by(collection, field, value)
164                .await
165                .map_err(|e| {
166                    tracing::warn!(
167                        "Supabase find_one_by '{}.{}' error: {}",
168                        collection,
169                        field,
170                        e
171                    );
172                    e
173                })
174                .ok()
175                .flatten(),
176        }
177    }
178
179    /// Create an item in a collection
180    pub async fn create(&self, collection: &str, item: Value) -> Result<Value> {
181        match self {
182            Self::Sqlite(db) => db.create(collection, item).await,
183            Self::D1(db) => db.create(collection, item).await,
184            Self::Supabase(db) => db.create(collection, item).await,
185        }
186    }
187
188    /// Update an item by ID
189    pub async fn update(
190        &self,
191        collection: &str,
192        id: &Value,
193        updates: Value,
194    ) -> Result<Option<Value>> {
195        match self {
196            Self::Sqlite(db) => db.update(collection, id, updates).await,
197            Self::D1(db) => db.update(collection, id, updates).await,
198            Self::Supabase(db) => db.update(collection, id, updates).await,
199        }
200    }
201
202    /// Delete an item by ID
203    pub async fn delete(&self, collection: &str, id: &Value) -> Result<bool> {
204        match self {
205            Self::Sqlite(db) => db.delete(collection, id).await,
206            Self::D1(db) => db.delete(collection, id).await,
207            Self::Supabase(db) => db.delete(collection, id).await,
208        }
209    }
210
211    /// Set a key-value pair
212    pub async fn set(&self, key: &str, value: Value) -> Result<()> {
213        match self {
214            Self::Sqlite(db) => db.set(key, value).await,
215            Self::D1(db) => db.set(key, value).await,
216            Self::Supabase(db) => db.set(key, value).await,
217        }
218    }
219
220    /// Get a value by key
221    pub async fn get(&self, key: &str) -> Option<Value> {
222        match self {
223            Self::Sqlite(db) => db
224                .get(key)
225                .await
226                .map_err(|e| {
227                    tracing::warn!("SQLite get '{}' error: {}", key, e);
228                    e
229                })
230                .ok()
231                .flatten(),
232            Self::D1(db) => db
233                .get(key)
234                .await
235                .map_err(|e| {
236                    tracing::warn!("D1 get '{}' error: {}", key, e);
237                    e
238                })
239                .ok()
240                .flatten(),
241            Self::Supabase(db) => db
242                .get(key)
243                .await
244                .map_err(|e| {
245                    tracing::warn!("Supabase get '{}' error: {}", key, e);
246                    e
247                })
248                .ok()
249                .flatten(),
250        }
251    }
252
253    /// Get all data as a template context
254    pub async fn as_context(&self) -> HashMap<String, Value> {
255        match self {
256            Self::Sqlite(db) => db.as_context().await.unwrap_or_else(|e| {
257                tracing::warn!("SQLite as_context error: {}", e);
258                HashMap::new()
259            }),
260            Self::D1(db) => db.as_context().await.unwrap_or_else(|e| {
261                tracing::warn!("D1 as_context error: {}", e);
262                HashMap::new()
263            }),
264            Self::Supabase(db) => db.as_context().await.unwrap_or_else(|e| {
265                tracing::warn!("Supabase as_context error: {}", e);
266                HashMap::new()
267            }),
268        }
269    }
270
271    /// Replace an entire collection
272    pub async fn set_collection(&self, name: &str, items: Vec<Value>) -> Result<()> {
273        match self {
274            Self::Sqlite(db) => db.set_collection(name, items).await,
275            Self::D1(db) => db.set_collection(name, items).await,
276            Self::Supabase(db) => db.set_collection(name, items).await,
277        }
278    }
279
280    /// Load collection from a file path (no-op for database backends)
281    pub async fn load_collection(
282        &self,
283        _name: &str,
284        _path: impl AsRef<std::path::Path>,
285    ) -> Result<()> {
286        Ok(())
287    }
288
289    /// Atomically modify a key-value
290    pub async fn atomic_modify<F>(&self, key: &str, f: F) -> Result<Value>
291    where
292        F: FnOnce(Option<&Value>) -> Value + Send + 'static,
293    {
294        match self {
295            Self::Sqlite(db) => db.atomic_modify(key, f).await,
296            Self::D1(db) => db.atomic_modify(key, f).await,
297            Self::Supabase(db) => db.atomic_modify(key, f).await,
298        }
299    }
300
301    /// Delete a key-value pair
302    pub async fn remove(&self, key: &str) -> Result<Option<Value>> {
303        match self {
304            Self::Sqlite(db) => db.remove(key).await,
305            Self::D1(db) => db.remove(key).await,
306            Self::Supabase(db) => db.remove(key).await,
307        }
308    }
309}
310
311// ---------------------------------------------------------------------------
312// In-memory query helpers (used by tests)
313// ---------------------------------------------------------------------------
314
315#[cfg(test)]
316fn apply_query_in_memory(mut items: Vec<Value>, query: &CollectionQuery) -> Vec<Value> {
317    // Filter
318    if let Some(ref filter_expr) = query.filter {
319        items = apply_filter(&items, filter_expr);
320    }
321
322    // Search
323    if let Some(ref search_term) = query.search {
324        if !search_term.is_empty() {
325            let fields: Vec<&str> = query
326                .search_fields
327                .as_deref()
328                .map(|s| s.split(',').collect())
329                .unwrap_or_default();
330            let term_lower = search_term.to_lowercase();
331            items.retain(|item| {
332                if fields.is_empty() {
333                    // Search all string fields
334                    if let Value::Object(map) = item {
335                        map.values().any(|v| {
336                            v.as_str()
337                                .map(|s| s.to_lowercase().contains(&term_lower))
338                                .unwrap_or(false)
339                        })
340                    } else {
341                        false
342                    }
343                } else {
344                    fields.iter().any(|field| {
345                        item.get(field.trim())
346                            .and_then(|v| v.as_str())
347                            .map(|s| s.to_lowercase().contains(&term_lower))
348                            .unwrap_or(false)
349                    })
350                }
351            });
352        }
353    }
354
355    // Sort
356    if let Some(ref sort_expr) = query.sort {
357        let (field, descending) = parse_sort_expr(sort_expr);
358        items.sort_by(|a, b| {
359            let va = a.get(&field);
360            let vb = b.get(&field);
361            let cmp = compare_json_values(va, vb);
362            if descending { cmp.reverse() } else { cmp }
363        });
364    }
365
366    // Offset
367    if let Some(offset) = query.offset {
368        if offset < items.len() {
369            items = items[offset..].to_vec();
370        } else {
371            items.clear();
372        }
373    }
374
375    // Limit
376    if let Some(limit) = query.limit {
377        items.truncate(limit);
378    }
379
380    items
381}
382
383#[cfg(test)]
384fn parse_sort_expr(expr: &str) -> (String, bool) {
385    if let Some((field, dir)) = expr.rsplit_once(':') {
386        (field.to_string(), dir.eq_ignore_ascii_case("desc"))
387    } else {
388        (expr.to_string(), false)
389    }
390}
391
392#[cfg(test)]
393fn compare_json_values(a: Option<&Value>, b: Option<&Value>) -> std::cmp::Ordering {
394    match (a, b) {
395        (None, None) => std::cmp::Ordering::Equal,
396        (None, Some(_)) => std::cmp::Ordering::Less,
397        (Some(_), None) => std::cmp::Ordering::Greater,
398        (Some(a), Some(b)) => {
399            // Try numeric comparison first
400            if let (Some(na), Some(nb)) = (a.as_f64(), b.as_f64()) {
401                return na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal);
402            }
403            // Fall back to string comparison
404            let sa = a.as_str().unwrap_or("");
405            let sb = b.as_str().unwrap_or("");
406            sa.cmp(sb)
407        }
408    }
409}
410
411#[cfg(test)]
412fn apply_filter(items: &[Value], filter_expr: &str) -> Vec<Value> {
413    // Support OR (comma) at top level, AND (&) within groups
414    let or_groups: Vec<&str> = filter_expr.split(',').collect();
415
416    items
417        .iter()
418        .filter(|item| {
419            or_groups.iter().any(|group| {
420                let and_conditions: Vec<&str> = group.split('&').collect();
421                and_conditions.iter().all(|cond| {
422                    let cond = cond.trim();
423                    evaluate_condition(item, cond)
424                })
425            })
426        })
427        .cloned()
428        .collect()
429}
430
431#[cfg(test)]
432fn evaluate_condition(item: &Value, cond: &str) -> bool {
433    if let Some((field, val)) = cond.split_once(">=") {
434        let field = field.trim();
435        let val = val.trim();
436        item.get(field)
437            .map(|v| {
438                if let (Some(n), Ok(target)) = (v.as_f64(), val.parse::<f64>()) {
439                    n >= target
440                } else {
441                    v.as_str().unwrap_or("") >= val
442                }
443            })
444            .unwrap_or(false)
445    } else if let Some((field, val)) = cond.split_once("<=") {
446        let field = field.trim();
447        let val = val.trim();
448        item.get(field)
449            .map(|v| {
450                if let (Some(n), Ok(target)) = (v.as_f64(), val.parse::<f64>()) {
451                    n <= target
452                } else {
453                    v.as_str().unwrap_or("") <= val
454                }
455            })
456            .unwrap_or(false)
457    } else if let Some((field, val)) = cond.split_once('>') {
458        let field = field.trim();
459        let val = val.trim();
460        item.get(field)
461            .map(|v| {
462                if let (Some(n), Ok(target)) = (v.as_f64(), val.parse::<f64>()) {
463                    n > target
464                } else {
465                    v.as_str().unwrap_or("") > val
466                }
467            })
468            .unwrap_or(false)
469    } else if let Some((field, val)) = cond.split_once('<') {
470        let field = field.trim();
471        let val = val.trim();
472        item.get(field)
473            .map(|v| {
474                if let (Some(n), Ok(target)) = (v.as_f64(), val.parse::<f64>()) {
475                    n < target
476                } else {
477                    v.as_str().unwrap_or("") < val
478                }
479            })
480            .unwrap_or(false)
481    } else if let Some((field, val)) = cond.split_once('=') {
482        let field = field.trim();
483        let val = val.trim();
484        item.get(field)
485            .map(|v| match v {
486                Value::String(s) => s == val,
487                Value::Number(n) => n.to_string() == val,
488                Value::Bool(b) => b.to_string() == val,
489                _ => false,
490            })
491            .unwrap_or(false)
492    } else {
493        true // Unknown condition format — don't filter
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use serde_json::json;
501
502    #[test]
503    fn test_sort_asc() {
504        let items = vec![
505            json!({"name": "Charlie", "age": 30}),
506            json!({"name": "Alice", "age": 25}),
507            json!({"name": "Bob", "age": 28}),
508        ];
509        let query = CollectionQuery {
510            sort: Some("name:asc".to_string()),
511            ..Default::default()
512        };
513        let result = apply_query_in_memory(items, &query);
514        assert_eq!(result[0]["name"], "Alice");
515        assert_eq!(result[1]["name"], "Bob");
516        assert_eq!(result[2]["name"], "Charlie");
517    }
518
519    #[test]
520    fn test_sort_desc() {
521        let items = vec![
522            json!({"name": "Alice", "age": 25}),
523            json!({"name": "Bob", "age": 28}),
524            json!({"name": "Charlie", "age": 30}),
525        ];
526        let query = CollectionQuery {
527            sort: Some("age:desc".to_string()),
528            ..Default::default()
529        };
530        let result = apply_query_in_memory(items, &query);
531        assert_eq!(result[0]["age"], 30);
532        assert_eq!(result[1]["age"], 28);
533        assert_eq!(result[2]["age"], 25);
534    }
535
536    #[test]
537    fn test_filter_exact() {
538        let items = vec![
539            json!({"status": "published", "title": "A"}),
540            json!({"status": "draft", "title": "B"}),
541            json!({"status": "published", "title": "C"}),
542        ];
543        let query = CollectionQuery {
544            filter: Some("status=published".to_string()),
545            ..Default::default()
546        };
547        let result = apply_query_in_memory(items, &query);
548        assert_eq!(result.len(), 2);
549    }
550
551    #[test]
552    fn test_filter_comparison() {
553        let items = vec![
554            json!({"price": 10}),
555            json!({"price": 25}),
556            json!({"price": 50}),
557        ];
558        let query = CollectionQuery {
559            filter: Some("price>20".to_string()),
560            ..Default::default()
561        };
562        let result = apply_query_in_memory(items, &query);
563        assert_eq!(result.len(), 2);
564    }
565
566    #[test]
567    fn test_filter_and() {
568        let items = vec![
569            json!({"status": "published", "category": "tech"}),
570            json!({"status": "published", "category": "food"}),
571            json!({"status": "draft", "category": "tech"}),
572        ];
573        let query = CollectionQuery {
574            filter: Some("status=published&category=tech".to_string()),
575            ..Default::default()
576        };
577        let result = apply_query_in_memory(items, &query);
578        assert_eq!(result.len(), 1);
579    }
580
581    #[test]
582    fn test_filter_or() {
583        let items = vec![
584            json!({"status": "published"}),
585            json!({"status": "draft"}),
586            json!({"status": "archived"}),
587        ];
588        let query = CollectionQuery {
589            filter: Some("status=published,status=draft".to_string()),
590            ..Default::default()
591        };
592        let result = apply_query_in_memory(items, &query);
593        assert_eq!(result.len(), 2);
594    }
595
596    #[test]
597    fn test_search() {
598        let items = vec![
599            json!({"title": "Rust Programming", "content": "Learn Rust"}),
600            json!({"title": "Python Basics", "content": "Learn Python"}),
601            json!({"title": "Rust Web Dev", "content": "Build web apps"}),
602        ];
603        let query = CollectionQuery {
604            search: Some("rust".to_string()),
605            search_fields: Some("title,content".to_string()),
606            ..Default::default()
607        };
608        let result = apply_query_in_memory(items, &query);
609        assert_eq!(result.len(), 2);
610    }
611
612    #[test]
613    fn test_limit_offset() {
614        let items: Vec<Value> = (1..=10).map(|i| json!({"n": i})).collect();
615        let query = CollectionQuery {
616            offset: Some(3),
617            limit: Some(2),
618            ..Default::default()
619        };
620        let result = apply_query_in_memory(items, &query);
621        assert_eq!(result.len(), 2);
622        assert_eq!(result[0]["n"], 4);
623        assert_eq!(result[1]["n"], 5);
624    }
625
626    #[test]
627    fn test_combined_query() {
628        let items = vec![
629            json!({"title": "Rust A", "views": 100, "status": "published"}),
630            json!({"title": "Rust B", "views": 50, "status": "draft"}),
631            json!({"title": "Python", "views": 200, "status": "published"}),
632            json!({"title": "Rust C", "views": 150, "status": "published"}),
633        ];
634        let query = CollectionQuery {
635            filter: Some("status=published".to_string()),
636            sort: Some("views:desc".to_string()),
637            limit: Some(2),
638            ..Default::default()
639        };
640        let result = apply_query_in_memory(items, &query);
641        assert_eq!(result.len(), 2);
642        assert_eq!(result[0]["title"], "Python");
643        assert_eq!(result[1]["title"], "Rust C");
644    }
645}