Skip to main content

what_core/database/
sqlite.rs

1//! SQLite database backend for the What framework.
2//!
3//! Each collection is stored as a table with:
4//! - `id` INTEGER PRIMARY KEY AUTOINCREMENT
5//! - `data` TEXT (JSON object of all fields)
6//!
7//! Key-value pairs use a special `_kv_store` table.
8//!
9//! Uses r2d2 connection pool + spawn_blocking to avoid blocking the async runtime.
10
11use r2d2::Pool;
12use r2d2_sqlite::SqliteConnectionManager;
13use rusqlite::{Connection, params};
14use serde_json::{Map, Value, json};
15use std::collections::HashMap;
16use std::path::Path;
17
18use super::CollectionQuery;
19use crate::Result;
20
21/// SQLite-backed database for collections and key-value storage.
22/// Uses a connection pool for concurrent reads and spawn_blocking
23/// to keep the async runtime responsive.
24#[derive(Clone)]
25pub struct SqliteDatabase {
26    pool: Pool<SqliteConnectionManager>,
27}
28
29/// Connection customizer that sets WAL mode and busy timeout on each new connection.
30#[derive(Debug)]
31struct WhatCustomizer;
32
33impl r2d2::CustomizeConnection<Connection, rusqlite::Error> for WhatCustomizer {
34    fn on_acquire(&self, conn: &mut Connection) -> std::result::Result<(), rusqlite::Error> {
35        conn.execute_batch("PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;")?;
36        Ok(())
37    }
38}
39
40impl SqliteDatabase {
41    /// Open or create a SQLite database at the given path.
42    /// Creates a connection pool with WAL mode for concurrent reads.
43    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
44        let manager = SqliteConnectionManager::file(path);
45        let pool = Pool::builder()
46            .max_size(8)
47            .connection_customizer(Box::new(WhatCustomizer))
48            .build(manager)
49            .map_err(|e| crate::Error::Data(format!("Pool creation failed: {}", e)))?;
50        let db = Self { pool };
51        db.init()?;
52        Ok(db)
53    }
54
55    /// Create an in-memory database (for testing).
56    /// Uses pool_size=1 since in-memory DBs are per-connection.
57    pub fn in_memory() -> Result<Self> {
58        let manager = SqliteConnectionManager::memory();
59        let pool = Pool::builder()
60            .max_size(1)
61            .connection_customizer(Box::new(WhatCustomizer))
62            .build(manager)
63            .map_err(|e| crate::Error::Data(format!("Pool creation failed: {}", e)))?;
64        let db = Self { pool };
65        db.init()?;
66        Ok(db)
67    }
68
69    fn init(&self) -> Result<()> {
70        let conn = self
71            .pool
72            .get()
73            .map_err(|e| crate::Error::Data(format!("Pool get failed: {}", e)))?;
74        // Set WAL mode once (requires exclusive lock, so do it before pool fills)
75        conn.execute_batch("PRAGMA journal_mode=WAL;")?;
76        conn.execute(
77            "CREATE TABLE IF NOT EXISTS _kv_store (
78                key TEXT PRIMARY KEY,
79                value TEXT NOT NULL
80            )",
81            [],
82        )?;
83        conn.execute(
84            "CREATE TABLE IF NOT EXISTS _collections (
85                name TEXT PRIMARY KEY
86            )",
87            [],
88        )?;
89        Ok(())
90    }
91
92    /// Synchronous import of JSON items into a collection (for store.json migration).
93    /// Only inserts if the collection table is currently empty.
94    pub fn import_json_collection(&self, name: &str, items: &[Value]) {
95        let conn = match self.pool.get() {
96            Ok(c) => c,
97            Err(_) => return,
98        };
99        let safe_name = sanitize_table_name(name);
100        if ensure_table_sync(&conn, name).is_err() {
101            return;
102        }
103
104        let count: i64 = conn
105            .query_row(
106                &format!("SELECT COUNT(*) FROM \"{}\"", safe_name),
107                [],
108                |r| r.get(0),
109            )
110            .unwrap_or(0);
111        if count > 0 {
112            return;
113        }
114
115        for item in items {
116            let mut data_map = match item {
117                Value::Object(map) => map.clone(),
118                _ => continue,
119            };
120            data_map.remove("id");
121            let data_str = serde_json::to_string(&Value::Object(data_map)).unwrap_or_default();
122            conn.execute(
123                &format!("INSERT INTO \"{}\" (data) VALUES (?1)", safe_name),
124                params![data_str],
125            )
126            .ok();
127        }
128        tracing::info!(
129            "Imported {} items from store.json into '{}'",
130            items.len(),
131            name
132        );
133    }
134
135    /// Get all items from a collection
136    pub async fn get_collection(&self, name: &str) -> Result<Vec<Value>> {
137        let pool = self.pool.clone();
138        let name = name.to_string();
139        tokio::task::spawn_blocking(move || {
140            let conn = pool.get().map_err(|e| crate::Error::Data(e.to_string()))?;
141            ensure_table_sync(&conn, &name)?;
142            let safe_name = sanitize_table_name(&name);
143            let mut stmt = conn.prepare(&format!(
144                "SELECT id, data FROM \"{}\" ORDER BY id",
145                safe_name
146            ))?;
147            let rows = stmt.query_map([], |row| {
148                let id: i64 = row.get(0)?;
149                let data_str: String = row.get(1)?;
150                Ok((id, data_str))
151            })?;
152            let mut items = Vec::new();
153            for row in rows {
154                let (id, data_str) = row?;
155                let mut item: Value = serde_json::from_str(&data_str).unwrap_or(json!({}));
156                if let Value::Object(ref mut map) = item {
157                    map.insert("id".to_string(), json!(id));
158                }
159                items.push(item);
160            }
161            Ok(items)
162        })
163        .await
164        .unwrap()
165    }
166
167    /// Query a collection with sort, filter, search, limit, offset
168    pub async fn query_collection(
169        &self,
170        name: &str,
171        query: &CollectionQuery,
172    ) -> Result<Vec<Value>> {
173        let pool = self.pool.clone();
174        let name = name.to_string();
175        let query = query.clone();
176        tokio::task::spawn_blocking(move || {
177            let conn = pool.get().map_err(|e| crate::Error::Data(e.to_string()))?;
178            ensure_table_sync(&conn, &name)?;
179            let safe_name = sanitize_table_name(&name);
180
181            let mut sql = format!("SELECT id, data FROM \"{}\"", safe_name);
182            let mut where_clauses = Vec::new();
183            let mut bind_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
184
185            if let Some(ref filter_expr) = query.filter {
186                let (clause, values) = build_filter_sql(filter_expr);
187                if !clause.is_empty() {
188                    where_clauses.push(clause);
189                    bind_values.extend(values);
190                }
191            }
192
193            // Policy-forced scope filters — AND-ed in, cannot be widened by the user.
194            for forced in &query.forced_filters {
195                let (clause, values) = build_filter_sql(forced);
196                if !clause.is_empty() {
197                    where_clauses.push(clause);
198                    bind_values.extend(values);
199                }
200            }
201
202            if let Some(ref search_term) = query.search {
203                if !search_term.is_empty() {
204                    let fields: Vec<&str> = query
205                        .search_fields
206                        .as_deref()
207                        .map(|s| s.split(',').map(|f| f.trim()).collect())
208                        .unwrap_or_default();
209
210                    if fields.is_empty() {
211                        where_clauses.push("data LIKE ?".to_string());
212                        bind_values.push(Box::new(format!("%{}%", search_term)));
213                    } else {
214                        let field_conditions: Vec<String> = fields
215                            .iter()
216                            .map(|f| {
217                                bind_values.push(Box::new(format!("%{}%", search_term)));
218                                format!("json_extract(data, '$.{}') LIKE ?", sanitize_field_name(f))
219                            })
220                            .collect();
221                        where_clauses.push(format!("({})", field_conditions.join(" OR ")));
222                    }
223                }
224            }
225
226            if !where_clauses.is_empty() {
227                sql.push_str(" WHERE ");
228                sql.push_str(&where_clauses.join(" AND "));
229            }
230
231            if let Some(ref sort_expr) = query.sort {
232                let (field, desc) = parse_sort(sort_expr);
233                let safe_field = sanitize_field_name(&field);
234                let dir = if desc { "DESC" } else { "ASC" };
235                sql.push_str(&format!(
236                    " ORDER BY json_extract(data, '$.{}') {}",
237                    safe_field, dir
238                ));
239            } else {
240                sql.push_str(" ORDER BY id");
241            }
242
243            if let Some(limit) = query.limit {
244                sql.push_str(&format!(" LIMIT {}", limit));
245            }
246            if let Some(offset) = query.offset {
247                sql.push_str(&format!(" OFFSET {}", offset));
248            }
249
250            let mut stmt = conn.prepare(&sql)?;
251            let params_ref: Vec<&dyn rusqlite::types::ToSql> =
252                bind_values.iter().map(|b| b.as_ref()).collect();
253            let rows = stmt.query_map(params_ref.as_slice(), |row| {
254                let id: i64 = row.get(0)?;
255                let data_str: String = row.get(1)?;
256                Ok((id, data_str))
257            })?;
258
259            let mut items = Vec::new();
260            for row in rows {
261                let (id, data_str) = row?;
262                let mut item: Value = serde_json::from_str(&data_str).unwrap_or(json!({}));
263                if let Value::Object(ref mut map) = item {
264                    map.insert("id".to_string(), json!(id));
265                }
266                items.push(item);
267            }
268            Ok(items)
269        })
270        .await
271        .unwrap()
272    }
273
274    /// Find items by a field value
275    pub async fn find_by(
276        &self,
277        collection: &str,
278        field: &str,
279        value: &Value,
280    ) -> Result<Vec<Value>> {
281        let pool = self.pool.clone();
282        let collection = collection.to_string();
283        let field = field.to_string();
284        let value = value.clone();
285        tokio::task::spawn_blocking(move || {
286            let conn = pool.get().map_err(|e| crate::Error::Data(e.to_string()))?;
287            ensure_table_sync(&conn, &collection)?;
288            let safe_name = sanitize_table_name(&collection);
289            let safe_field = sanitize_field_name(&field);
290
291            let value_str = match &value {
292                Value::String(s) => s.clone(),
293                Value::Number(n) => n.to_string(),
294                Value::Bool(b) => b.to_string(),
295                _ => serde_json::to_string(&value).unwrap_or_default(),
296            };
297
298            let sql = if field == "id" {
299                format!("SELECT id, data FROM \"{}\" WHERE id = ?1", safe_name)
300            } else {
301                format!(
302                    "SELECT id, data FROM \"{}\" WHERE json_extract(data, '$.{}') = ?1",
303                    safe_name, safe_field
304                )
305            };
306
307            let mut stmt = conn.prepare(&sql)?;
308            let rows = stmt.query_map(params![value_str], |row| {
309                let id: i64 = row.get(0)?;
310                let data_str: String = row.get(1)?;
311                Ok((id, data_str))
312            })?;
313
314            let mut items = Vec::new();
315            for row in rows {
316                let (id, data_str) = row?;
317                let mut item: Value = serde_json::from_str(&data_str).unwrap_or(json!({}));
318                if let Value::Object(ref mut map) = item {
319                    map.insert("id".to_string(), json!(id));
320                }
321                items.push(item);
322            }
323            Ok(items)
324        })
325        .await
326        .unwrap()
327    }
328
329    /// Find a single item by field value
330    pub async fn find_one_by(
331        &self,
332        collection: &str,
333        field: &str,
334        value: &Value,
335    ) -> Result<Option<Value>> {
336        let items = self.find_by(collection, field, value).await?;
337        Ok(items.into_iter().next())
338    }
339
340    /// Create an item in a collection
341    pub async fn create(&self, collection: &str, item: Value) -> Result<Value> {
342        let pool = self.pool.clone();
343        let collection = collection.to_string();
344        tokio::task::spawn_blocking(move || {
345            let conn = pool.get().map_err(|e| crate::Error::Data(e.to_string()))?;
346            ensure_table_sync(&conn, &collection)?;
347            let safe_name = sanitize_table_name(&collection);
348
349            let mut data_map = match item {
350                Value::Object(map) => map,
351                _ => Map::new(),
352            };
353            data_map.remove("id");
354            // Auto-stamp a sortable creation timestamp so `sort=created_at:desc`
355            // works out of the box (millisecond precision avoids ties).
356            if !data_map.contains_key("created_at") {
357                data_map.insert(
358                    "created_at".to_string(),
359                    json!(chrono::Local::now()
360                        .format("%Y-%m-%dT%H:%M:%S%.3f")
361                        .to_string()),
362                );
363            }
364            let data_str = serde_json::to_string(&Value::Object(data_map.clone()))?;
365
366            conn.execute(
367                &format!("INSERT INTO \"{}\" (data) VALUES (?1)", safe_name),
368                params![data_str],
369            )?;
370
371            let id = conn.last_insert_rowid();
372            data_map.insert("id".to_string(), json!(id));
373            Ok(Value::Object(data_map))
374        })
375        .await
376        .unwrap()
377    }
378
379    /// Update an item by ID
380    pub async fn update(
381        &self,
382        collection: &str,
383        id: &Value,
384        updates: Value,
385    ) -> Result<Option<Value>> {
386        let pool = self.pool.clone();
387        let collection = collection.to_string();
388        let id = id.clone();
389        tokio::task::spawn_blocking(move || {
390            let conn = pool.get().map_err(|e| crate::Error::Data(e.to_string()))?;
391            ensure_table_sync(&conn, &collection)?;
392            let safe_name = sanitize_table_name(&collection);
393
394            let id_num = match &id {
395                Value::Number(n) => n.as_i64().unwrap_or(0),
396                Value::String(s) => s.parse::<i64>().unwrap_or(0),
397                _ => 0,
398            };
399
400            let current: Option<String> = conn
401                .query_row(
402                    &format!("SELECT data FROM \"{}\" WHERE id = ?1", safe_name),
403                    params![id_num],
404                    |row| row.get(0),
405                )
406                .ok();
407
408            let Some(current_str) = current else {
409                return Ok(None);
410            };
411
412            let mut current_data: Map<String, Value> =
413                serde_json::from_str(&current_str).unwrap_or_default();
414
415            if let Value::Object(update_map) = updates {
416                for (k, v) in update_map {
417                    if k != "id" {
418                        current_data.insert(k, v);
419                    }
420                }
421            }
422
423            let updated_str = serde_json::to_string(&Value::Object(current_data.clone()))?;
424            conn.execute(
425                &format!("UPDATE \"{}\" SET data = ?1 WHERE id = ?2", safe_name),
426                params![updated_str, id_num],
427            )?;
428
429            current_data.insert("id".to_string(), json!(id_num));
430            Ok(Some(Value::Object(current_data)))
431        })
432        .await
433        .unwrap()
434    }
435
436    /// Delete an item by ID
437    pub async fn delete(&self, collection: &str, id: &Value) -> Result<bool> {
438        let pool = self.pool.clone();
439        let collection = collection.to_string();
440        let id = id.clone();
441        tokio::task::spawn_blocking(move || {
442            let conn = pool.get().map_err(|e| crate::Error::Data(e.to_string()))?;
443            ensure_table_sync(&conn, &collection)?;
444            let safe_name = sanitize_table_name(&collection);
445
446            let id_num = match &id {
447                Value::Number(n) => n.as_i64().unwrap_or(0),
448                Value::String(s) => s.parse::<i64>().unwrap_or(0),
449                _ => 0,
450            };
451
452            let rows = conn.execute(
453                &format!("DELETE FROM \"{}\" WHERE id = ?1", safe_name),
454                params![id_num],
455            )?;
456
457            Ok(rows > 0)
458        })
459        .await
460        .unwrap()
461    }
462
463    /// Set a key-value pair
464    pub async fn set(&self, key: &str, value: Value) -> Result<()> {
465        let pool = self.pool.clone();
466        let key = key.to_string();
467        tokio::task::spawn_blocking(move || {
468            let conn = pool.get().map_err(|e| crate::Error::Data(e.to_string()))?;
469            let value_str = serde_json::to_string(&value)?;
470            conn.execute(
471                "INSERT OR REPLACE INTO _kv_store (key, value) VALUES (?1, ?2)",
472                params![key, value_str],
473            )?;
474            Ok(())
475        })
476        .await
477        .unwrap()
478    }
479
480    /// Get a value by key
481    pub async fn get(&self, key: &str) -> Result<Option<Value>> {
482        let pool = self.pool.clone();
483        let key = key.to_string();
484        tokio::task::spawn_blocking(move || {
485            let conn = pool.get().map_err(|e| crate::Error::Data(e.to_string()))?;
486            let result: Option<String> = conn
487                .query_row(
488                    "SELECT value FROM _kv_store WHERE key = ?1",
489                    params![key],
490                    |row| row.get(0),
491                )
492                .ok();
493            match result {
494                Some(s) => Ok(Some(serde_json::from_str(&s)?)),
495                None => Ok(None),
496            }
497        })
498        .await
499        .unwrap()
500    }
501
502    /// Get all data as template context
503    pub async fn as_context(&self) -> Result<HashMap<String, Value>> {
504        let pool = self.pool.clone();
505        tokio::task::spawn_blocking(move || {
506            let conn = pool.get().map_err(|e| crate::Error::Data(e.to_string()))?;
507            let mut context = HashMap::new();
508
509            let mut stmt = conn.prepare("SELECT name FROM _collections")?;
510            let names: Vec<String> = stmt
511                .query_map([], |row| row.get(0))?
512                .filter_map(|r| r.ok())
513                .collect();
514            drop(stmt);
515
516            for name in names {
517                let safe_name = sanitize_table_name(&name);
518                let mut stmt = conn.prepare(&format!(
519                    "SELECT id, data FROM \"{}\" ORDER BY id",
520                    safe_name
521                ))?;
522                let items: Vec<Value> = stmt
523                    .query_map([], |row| {
524                        let id: i64 = row.get(0)?;
525                        let data_str: String = row.get(1)?;
526                        Ok((id, data_str))
527                    })?
528                    .filter_map(|r| r.ok())
529                    .map(|(id, data_str)| {
530                        let mut item: Value = serde_json::from_str(&data_str).unwrap_or(json!({}));
531                        if let Value::Object(ref mut map) = item {
532                            map.insert("id".to_string(), json!(id));
533                        }
534                        item
535                    })
536                    .collect();
537                drop(stmt);
538                context.insert(name, Value::Array(items));
539            }
540
541            let mut stmt = conn.prepare("SELECT key, value FROM _kv_store")?;
542            let kvs: Vec<(String, String)> = stmt
543                .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
544                .filter_map(|r| r.ok())
545                .collect();
546            drop(stmt);
547
548            for (key, value_str) in kvs {
549                if let Ok(value) = serde_json::from_str(&value_str) {
550                    context.insert(key, value);
551                }
552            }
553
554            Ok(context)
555        })
556        .await
557        .unwrap()
558    }
559
560    /// Replace an entire collection
561    pub async fn set_collection(&self, name: &str, items: Vec<Value>) -> Result<()> {
562        let pool = self.pool.clone();
563        let name = name.to_string();
564        tokio::task::spawn_blocking(move || {
565            let conn = pool.get().map_err(|e| crate::Error::Data(e.to_string()))?;
566            ensure_table_sync(&conn, &name)?;
567            let safe_name = sanitize_table_name(&name);
568
569            conn.execute(&format!("DELETE FROM \"{}\"", safe_name), [])?;
570
571            let mut stmt =
572                conn.prepare(&format!("INSERT INTO \"{}\" (data) VALUES (?1)", safe_name))?;
573            for item in items {
574                let mut data = match item {
575                    Value::Object(map) => map,
576                    _ => Map::new(),
577                };
578                data.remove("id");
579                let data_str = serde_json::to_string(&Value::Object(data))?;
580                stmt.execute(params![data_str])?;
581            }
582
583            Ok(())
584        })
585        .await
586        .unwrap()
587    }
588
589    /// Atomically modify a key-value pair
590    pub async fn atomic_modify<F>(&self, key: &str, f: F) -> Result<Value>
591    where
592        F: FnOnce(Option<&Value>) -> Value + Send + 'static,
593    {
594        let pool = self.pool.clone();
595        let key = key.to_string();
596        tokio::task::spawn_blocking(move || {
597            let conn = pool.get().map_err(|e| crate::Error::Data(e.to_string()))?;
598            let current: Option<Value> = conn
599                .query_row(
600                    "SELECT value FROM _kv_store WHERE key = ?1",
601                    params![key],
602                    |row| {
603                        let s: String = row.get(0)?;
604                        Ok(serde_json::from_str(&s).ok())
605                    },
606                )
607                .ok()
608                .flatten();
609
610            let new_value = f(current.as_ref());
611            let value_str = serde_json::to_string(&new_value)?;
612            conn.execute(
613                "INSERT OR REPLACE INTO _kv_store (key, value) VALUES (?1, ?2)",
614                params![key, value_str],
615            )?;
616            Ok(new_value)
617        })
618        .await
619        .unwrap()
620    }
621
622    /// Delete a key-value pair
623    pub async fn remove(&self, key: &str) -> Result<Option<Value>> {
624        let pool = self.pool.clone();
625        let key = key.to_string();
626        tokio::task::spawn_blocking(move || {
627            let conn = pool.get().map_err(|e| crate::Error::Data(e.to_string()))?;
628            let current: Option<String> = conn
629                .query_row(
630                    "SELECT value FROM _kv_store WHERE key = ?1",
631                    params![key],
632                    |row| row.get(0),
633                )
634                .ok();
635            let result = current.and_then(|s| serde_json::from_str(&s).ok());
636            conn.execute("DELETE FROM _kv_store WHERE key = ?1", params![key])?;
637            Ok(result)
638        })
639        .await
640        .unwrap()
641    }
642}
643
644// ---------------------------------------------------------------------------
645// SQL Helpers
646// ---------------------------------------------------------------------------
647
648/// Ensure a collection table exists (called inside spawn_blocking with a pooled connection)
649fn ensure_table_sync(conn: &Connection, name: &str) -> Result<()> {
650    let safe_name = sanitize_table_name(name);
651    conn.execute(
652        &format!(
653            "CREATE TABLE IF NOT EXISTS \"{}\" (
654                id INTEGER PRIMARY KEY AUTOINCREMENT,
655                data TEXT NOT NULL DEFAULT '{{}}'
656            )",
657            safe_name
658        ),
659        [],
660    )?;
661    conn.execute(
662        "INSERT OR IGNORE INTO _collections (name) VALUES (?1)",
663        params![name],
664    )?;
665    Ok(())
666}
667
668fn sanitize_table_name(name: &str) -> String {
669    name.chars()
670        .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
671        .collect()
672}
673
674fn sanitize_field_name(name: &str) -> String {
675    name.chars()
676        .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
677        .collect()
678}
679
680fn build_filter_sql(filter_expr: &str) -> (String, Vec<Box<dyn rusqlite::types::ToSql>>) {
681    let mut clauses = Vec::new();
682    let mut values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
683
684    let or_groups: Vec<&str> = filter_expr.split(',').collect();
685    let mut or_parts = Vec::new();
686
687    for group in or_groups {
688        let and_conditions: Vec<&str> = group.split('&').collect();
689        let mut and_parts = Vec::new();
690
691        for cond in and_conditions {
692            let cond = cond.trim();
693            if let Some((field, val)) = cond.split_once(">=") {
694                let field = sanitize_field_name(field.trim());
695                values.push(Box::new(val.trim().to_string()));
696                and_parts.push(format!("json_extract(data, '$.{}') >= ?", field));
697            } else if let Some((field, val)) = cond.split_once("<=") {
698                let field = sanitize_field_name(field.trim());
699                values.push(Box::new(val.trim().to_string()));
700                and_parts.push(format!("json_extract(data, '$.{}') <= ?", field));
701            } else if let Some((field, val)) = cond.split_once('>') {
702                let field = sanitize_field_name(field.trim());
703                values.push(Box::new(val.trim().to_string()));
704                and_parts.push(format!("json_extract(data, '$.{}') > ?", field));
705            } else if let Some((field, val)) = cond.split_once('<') {
706                let field = sanitize_field_name(field.trim());
707                values.push(Box::new(val.trim().to_string()));
708                and_parts.push(format!("json_extract(data, '$.{}') < ?", field));
709            } else if let Some((field, val)) = cond.split_once('=') {
710                let field = sanitize_field_name(field.trim());
711                values.push(Box::new(val.trim().to_string()));
712                and_parts.push(format!("json_extract(data, '$.{}') = ?", field));
713            }
714        }
715
716        if !and_parts.is_empty() {
717            or_parts.push(format!("({})", and_parts.join(" AND ")));
718        }
719    }
720
721    if !or_parts.is_empty() {
722        clauses.push(format!("({})", or_parts.join(" OR ")));
723    }
724
725    (clauses.join(" AND "), values)
726}
727
728fn parse_sort(expr: &str) -> (String, bool) {
729    if let Some((field, dir)) = expr.rsplit_once(':') {
730        (field.to_string(), dir.eq_ignore_ascii_case("desc"))
731    } else {
732        (expr.to_string(), false)
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use super::*;
739    use serde_json::json;
740
741    #[tokio::test]
742    async fn test_create_and_get() {
743        let db = SqliteDatabase::in_memory().unwrap();
744        let item = json!({"title": "Hello", "content": "World"});
745        let created = db.create("posts", item).await.unwrap();
746        assert_eq!(created.get("id"), Some(&json!(1)));
747        assert_eq!(created.get("title"), Some(&json!("Hello")));
748
749        let items = db.get_collection("posts").await.unwrap();
750        assert_eq!(items.len(), 1);
751        assert_eq!(items[0].get("title"), Some(&json!("Hello")));
752    }
753
754    #[tokio::test]
755    async fn test_find_by() {
756        let db = SqliteDatabase::in_memory().unwrap();
757        db.create("users", json!({"name": "Alice", "role": "admin"}))
758            .await
759            .unwrap();
760        db.create("users", json!({"name": "Bob", "role": "user"}))
761            .await
762            .unwrap();
763        db.create("users", json!({"name": "Charlie", "role": "admin"}))
764            .await
765            .unwrap();
766
767        let admins = db.find_by("users", "role", &json!("admin")).await.unwrap();
768        assert_eq!(admins.len(), 2);
769    }
770
771    #[tokio::test]
772    async fn test_find_by_id() {
773        let db = SqliteDatabase::in_memory().unwrap();
774        db.create("posts", json!({"title": "First"})).await.unwrap();
775        db.create("posts", json!({"title": "Second"}))
776            .await
777            .unwrap();
778
779        let found = db.find_one_by("posts", "id", &json!(2)).await.unwrap();
780        assert!(found.is_some());
781        assert_eq!(found.unwrap().get("title"), Some(&json!("Second")));
782    }
783
784    #[tokio::test]
785    async fn test_update() {
786        let db = SqliteDatabase::in_memory().unwrap();
787        db.create("posts", json!({"title": "Draft"})).await.unwrap();
788
789        let updated = db
790            .update(
791                "posts",
792                &json!(1),
793                json!({"title": "Published", "status": "live"}),
794            )
795            .await
796            .unwrap();
797        assert!(updated.is_some());
798        let u = updated.unwrap();
799        assert_eq!(u.get("title"), Some(&json!("Published")));
800        assert_eq!(u.get("status"), Some(&json!("live")));
801    }
802
803    #[tokio::test]
804    async fn test_delete() {
805        let db = SqliteDatabase::in_memory().unwrap();
806        db.create("posts", json!({"title": "To Delete"}))
807            .await
808            .unwrap();
809
810        let deleted = db.delete("posts", &json!(1)).await.unwrap();
811        assert!(deleted);
812
813        let items = db.get_collection("posts").await.unwrap();
814        assert!(items.is_empty());
815    }
816
817    #[tokio::test]
818    async fn test_kv_store() {
819        let db = SqliteDatabase::in_memory().unwrap();
820        db.set("counter", json!(42)).await.unwrap();
821
822        let val = db.get("counter").await.unwrap();
823        assert_eq!(val, Some(json!(42)));
824
825        let removed = db.remove("counter").await.unwrap();
826        assert_eq!(removed, Some(json!(42)));
827
828        let val = db.get("counter").await.unwrap();
829        assert_eq!(val, None);
830    }
831
832    #[tokio::test]
833    async fn test_query_with_filter() {
834        let db = SqliteDatabase::in_memory().unwrap();
835        db.create("posts", json!({"title": "A", "status": "published"}))
836            .await
837            .unwrap();
838        db.create("posts", json!({"title": "B", "status": "draft"}))
839            .await
840            .unwrap();
841        db.create("posts", json!({"title": "C", "status": "published"}))
842            .await
843            .unwrap();
844
845        let query = CollectionQuery {
846            filter: Some("status=published".to_string()),
847            ..Default::default()
848        };
849        let items = db.query_collection("posts", &query).await.unwrap();
850        assert_eq!(items.len(), 2);
851    }
852
853    #[tokio::test]
854    async fn test_query_with_sort_and_limit() {
855        let db = SqliteDatabase::in_memory().unwrap();
856        for i in 1..=5 {
857            db.create("items", json!({"n": i})).await.unwrap();
858        }
859
860        let query = CollectionQuery {
861            sort: Some("n:desc".to_string()),
862            limit: Some(3),
863            ..Default::default()
864        };
865        let items = db.query_collection("items", &query).await.unwrap();
866        assert_eq!(items.len(), 3);
867        assert_eq!(items[0]["n"], 5);
868        assert_eq!(items[1]["n"], 4);
869        assert_eq!(items[2]["n"], 3);
870    }
871
872    #[tokio::test]
873    async fn test_query_with_search() {
874        let db = SqliteDatabase::in_memory().unwrap();
875        db.create(
876            "posts",
877            json!({"title": "Rust Programming", "content": "Learn Rust"}),
878        )
879        .await
880        .unwrap();
881        db.create(
882            "posts",
883            json!({"title": "Python Basics", "content": "Learn Python"}),
884        )
885        .await
886        .unwrap();
887
888        let query = CollectionQuery {
889            search: Some("rust".to_string()),
890            search_fields: Some("title".to_string()),
891            ..Default::default()
892        };
893        let items = db.query_collection("posts", &query).await.unwrap();
894        assert_eq!(items.len(), 1);
895        assert_eq!(items[0]["title"], "Rust Programming");
896    }
897
898    #[tokio::test]
899    async fn test_as_context() {
900        let db = SqliteDatabase::in_memory().unwrap();
901        db.create("posts", json!({"title": "Hello"})).await.unwrap();
902        db.set("site_name", json!("My Site")).await.unwrap();
903
904        let ctx = db.as_context().await.unwrap();
905        assert!(ctx.contains_key("posts"));
906        assert!(ctx.contains_key("site_name"));
907        assert_eq!(ctx["site_name"], json!("My Site"));
908    }
909
910    #[tokio::test]
911    async fn test_set_collection() {
912        let db = SqliteDatabase::in_memory().unwrap();
913        let items = vec![json!({"name": "A"}), json!({"name": "B"})];
914        db.set_collection("letters", items).await.unwrap();
915
916        let result = db.get_collection("letters").await.unwrap();
917        assert_eq!(result.len(), 2);
918    }
919}