Skip to main content

ai_model_vault/rag/
database.rs

1//! Database connectivity abstractions for RAG systems.
2
3use crate::error::{Result, VaultError};
4use crate::rag::documents::{ChunkInfo, Document};
5use std::collections::HashMap;
6
7/// Database abstraction for rule-based systems
8pub trait Database {
9    /// Query the database
10    fn query(&self, query: &str) -> Result<Vec<HashMap<String, String>>>;
11
12    /// Insert data
13    fn insert(&mut self, table: &str, data: HashMap<String, String>) -> Result<()>;
14
15    /// Update data
16    fn update(&mut self, table: &str, id: &str, data: HashMap<String, String>) -> Result<()>;
17
18    /// Delete data
19    fn delete(&mut self, table: &str, id: &str) -> Result<()>;
20}
21
22/// In-memory database implementation
23pub struct InMemoryDatabase {
24    tables: HashMap<String, Vec<HashMap<String, String>>>,
25}
26
27impl InMemoryDatabase {
28    /// Create a new in-memory database
29    pub fn new() -> Self {
30        Self {
31            tables: HashMap::new(),
32        }
33    }
34
35    /// Create a table
36    pub fn create_table(&mut self, name: String) {
37        self.tables.insert(name, Vec::new());
38    }
39}
40
41impl Default for InMemoryDatabase {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl Database for InMemoryDatabase {
48    fn query(&self, query: &str) -> Result<Vec<HashMap<String, String>>> {
49        // Simple query: "table_name WHERE key=value"
50        let parts: Vec<&str> = query.split_whitespace().collect();
51        if parts.is_empty() {
52            return Ok(Vec::new());
53        }
54
55        let table_name = parts[0];
56
57        if let Some(table) = self.tables.get(table_name) {
58            if parts.len() > 2 && parts[1] == "WHERE" {
59                // Simple WHERE clause
60                let condition = parts[2];
61                let cond_parts: Vec<&str> = condition.split('=').collect();
62                if cond_parts.len() == 2 {
63                    let key = cond_parts[0];
64                    let value = cond_parts[1];
65
66                    let results: Vec<HashMap<String, String>> = table
67                        .iter()
68                        .filter(|row| row.get(key).map(|v| v == value).unwrap_or(false))
69                        .cloned()
70                        .collect();
71
72                    return Ok(results);
73                }
74            }
75
76            // Return all rows
77            Ok(table.clone())
78        } else {
79            Ok(Vec::new())
80        }
81    }
82
83    fn insert(&mut self, table: &str, data: HashMap<String, String>) -> Result<()> {
84        if let Some(table_data) = self.tables.get_mut(table) {
85            table_data.push(data);
86            Ok(())
87        } else {
88            Err(VaultError::InvalidInput(format!(
89                "Table {} not found",
90                table
91            )))
92        }
93    }
94
95    fn update(&mut self, table: &str, id: &str, data: HashMap<String, String>) -> Result<()> {
96        if let Some(table_data) = self.tables.get_mut(table) {
97            if let Some(row) = table_data
98                .iter_mut()
99                .find(|r| r.get("id").map(|v| v == id).unwrap_or(false))
100            {
101                for (k, v) in data {
102                    row.insert(k, v);
103                }
104                Ok(())
105            } else {
106                Err(VaultError::InvalidInput(format!(
107                    "Row with id {} not found",
108                    id
109                )))
110            }
111        } else {
112            Err(VaultError::InvalidInput(format!(
113                "Table {} not found",
114                table
115            )))
116        }
117    }
118
119    fn delete(&mut self, table: &str, id: &str) -> Result<()> {
120        if let Some(table_data) = self.tables.get_mut(table) {
121            table_data.retain(|r| !r.get("id").map(|v| v == id).unwrap_or(false));
122            Ok(())
123        } else {
124            Err(VaultError::InvalidInput(format!(
125                "Table {} not found",
126                table
127            )))
128        }
129    }
130}
131
132/// SQLite database implementation for persistent storage
133#[cfg(feature = "sqlite")]
134pub struct SQLiteDatabase {
135    conn: std::sync::Arc<std::sync::Mutex<rusqlite::Connection>>,
136}
137
138/// Validate that a SQL identifier (table or column name) contains only safe characters.
139/// Prevents SQL injection through dynamic table/column names.
140#[cfg(feature = "sqlite")]
141fn validate_sql_identifier(name: &str) -> Result<()> {
142    if name.is_empty() {
143        return Err(VaultError::InvalidInput(
144            "SQL identifier cannot be empty".to_string(),
145        ));
146    }
147    if name.len() > 128 {
148        return Err(VaultError::InvalidInput(
149            "SQL identifier too long (max 128 chars)".to_string(),
150        ));
151    }
152    // Only allow alphanumeric characters and underscores; must start with letter or underscore
153    // Safety: empty case is handled above, so .expect() is unreachable
154    let first = name
155        .chars()
156        .next()
157        .expect("BUG: empty check above should have returned");
158    if !first.is_ascii_alphabetic() && first != '_' {
159        return Err(VaultError::InvalidInput(format!(
160            "SQL identifier '{}' must start with a letter or underscore",
161            name
162        )));
163    }
164    if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
165        return Err(VaultError::InvalidInput(format!(
166            "SQL identifier '{}' contains invalid characters (only alphanumeric and underscore allowed)",
167            name
168        )));
169    }
170    Ok(())
171}
172
173#[cfg(feature = "sqlite")]
174impl SQLiteDatabase {
175    /// Create a new SQLite database
176    pub fn new(path: &std::path::Path) -> Result<Self> {
177        let conn = rusqlite::Connection::open(path).map_err(|e| {
178            VaultError::StorageError(format!("Failed to open SQLite database: {}", e))
179        })?;
180
181        Ok(Self {
182            conn: std::sync::Arc::new(std::sync::Mutex::new(conn)),
183        })
184    }
185
186    /// Create in-memory SQLite database
187    pub fn in_memory() -> Result<Self> {
188        let conn = rusqlite::Connection::open_in_memory().map_err(|e| {
189            VaultError::StorageError(format!("Failed to create in-memory database: {}", e))
190        })?;
191
192        Ok(Self {
193            conn: std::sync::Arc::new(std::sync::Mutex::new(conn)),
194        })
195    }
196
197    /// Create a table with schema
198    pub fn create_table(&self, name: &str, columns: &[(&str, &str)]) -> Result<()> {
199        validate_sql_identifier(name)?;
200        for (col_name, col_type) in columns {
201            validate_sql_identifier(col_name)?;
202            validate_sql_identifier(col_type)?;
203        }
204
205        let conn = self
206            .conn
207            .lock()
208            .map_err(|e| VaultError::StorageError(format!("Lock poisoned: {}", e)))?;
209
210        let mut sql = format!("CREATE TABLE IF NOT EXISTS {} (", name);
211        sql.push_str("id TEXT PRIMARY KEY, ");
212        for (i, (col_name, col_type)) in columns.iter().enumerate() {
213            sql.push_str(&format!("{} {}", col_name, col_type));
214            if i < columns.len() - 1 {
215                sql.push_str(", ");
216            }
217        }
218        sql.push(')');
219
220        conn.execute(&sql, [])
221            .map_err(|e| VaultError::StorageError(format!("Failed to create table: {}", e)))?;
222
223        Ok(())
224    }
225
226    /// Store a document in the database
227    pub fn store_document(&self, doc: &Document) -> Result<()> {
228        let conn = self
229            .conn
230            .lock()
231            .map_err(|e| VaultError::StorageError(format!("Lock poisoned: {}", e)))?;
232
233        // Ensure documents table exists
234        conn.execute(
235            "CREATE TABLE IF NOT EXISTS documents (
236                id TEXT PRIMARY KEY,
237                content TEXT NOT NULL,
238                metadata TEXT,
239                embedding BLOB,
240                chunk_parent_id TEXT,
241                chunk_index INTEGER,
242                chunk_total INTEGER,
243                chunk_overlap INTEGER,
244                created_at TEXT DEFAULT CURRENT_TIMESTAMP
245            )",
246            [],
247        )
248        .map_err(|e| {
249            VaultError::StorageError(format!("Failed to create documents table: {}", e))
250        })?;
251
252        // Serialize metadata and embedding
253        let metadata_json = serde_json::to_string(&doc.metadata)
254            .map_err(|e| VaultError::SerializationError(e.to_string()))?;
255
256        let embedding_blob = doc.embedding.as_ref().map(|emb| {
257            emb.iter()
258                .flat_map(|f| f.to_le_bytes())
259                .collect::<Vec<u8>>()
260        });
261
262        let (chunk_parent, chunk_idx, chunk_total, chunk_overlap) =
263            if let Some(chunk_info) = &doc.chunk_info {
264                (
265                    chunk_info.parent_id.clone(),
266                    Some(chunk_info.chunk_index as i64),
267                    Some(chunk_info.total_chunks as i64),
268                    Some(chunk_info.overlap as i64),
269                )
270            } else {
271                (None, None, None, None)
272            };
273
274        conn.execute(
275            "INSERT OR REPLACE INTO documents 
276             (id, content, metadata, embedding, chunk_parent_id, chunk_index, chunk_total, chunk_overlap)
277             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
278            rusqlite::params![
279                &doc.id,
280                &doc.content,
281                &metadata_json,
282                &embedding_blob,
283                &chunk_parent,
284                &chunk_idx,
285                &chunk_total,
286                &chunk_overlap,
287            ],
288        ).map_err(|e| VaultError::StorageError(format!("Failed to insert document: {}", e)))?;
289
290        Ok(())
291    }
292
293    /// Retrieve a document by ID
294    pub fn get_document(&self, id: &str) -> Result<Option<Document>> {
295        let conn = self
296            .conn
297            .lock()
298            .map_err(|e| VaultError::StorageError(format!("Lock poisoned: {}", e)))?;
299
300        let mut stmt = conn.prepare(
301            "SELECT id, content, metadata, embedding, chunk_parent_id, chunk_index, chunk_total, chunk_overlap 
302             FROM documents WHERE id = ?1"
303        ).map_err(|e| VaultError::StorageError(format!("Failed to prepare statement: {}", e)))?;
304
305        let result = stmt.query_row([id], |row| {
306            let id: String = row.get(0)?;
307            let content: String = row.get(1)?;
308            let metadata_json: String = row.get(2)?;
309            let embedding_blob: Option<Vec<u8>> = row.get(3)?;
310            let chunk_parent: Option<String> = row.get(4)?;
311            let chunk_idx: Option<i64> = row.get(5)?;
312            let chunk_total: Option<i64> = row.get(6)?;
313            let chunk_overlap: Option<i64> = row.get(7)?;
314
315            let metadata: HashMap<String, String> =
316                serde_json::from_str(&metadata_json).unwrap_or_default();
317
318            let embedding = embedding_blob.map(|blob| {
319                // `as_chunks` yields `[u8; 4]` directly, so there is no manual
320                // indexing to get wrong and no per-element bounds check.
321                let (whole, _trailing) = blob.as_chunks::<4>();
322                whole.iter().copied().map(f32::from_le_bytes).collect()
323            });
324
325            let chunk_info = if let (Some(parent), Some(idx), Some(total), Some(overlap)) =
326                (chunk_parent, chunk_idx, chunk_total, chunk_overlap)
327            {
328                Some(ChunkInfo {
329                    parent_id: Some(parent),
330                    chunk_index: idx as usize,
331                    total_chunks: total as usize,
332                    overlap: overlap as usize,
333                })
334            } else {
335                None
336            };
337
338            Ok(Document {
339                id,
340                content,
341                metadata,
342                embedding,
343                chunk_info,
344            })
345        });
346
347        match result {
348            Ok(doc) => Ok(Some(doc)),
349            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
350            Err(e) => Err(VaultError::StorageError(format!(
351                "Failed to query document: {}",
352                e
353            ))),
354        }
355    }
356
357    /// Search for documents containing text
358    pub fn search_documents(&self, query: &str, limit: usize) -> Result<Vec<Document>> {
359        let conn = self
360            .conn
361            .lock()
362            .map_err(|e| VaultError::StorageError(format!("Lock poisoned: {}", e)))?;
363
364        let mut stmt = conn.prepare(
365            "SELECT id, content, metadata, embedding, chunk_parent_id, chunk_index, chunk_total, chunk_overlap 
366             FROM documents 
367             WHERE content LIKE ?1
368             LIMIT ?2"
369        ).map_err(|e| VaultError::StorageError(format!("Failed to prepare statement: {}", e)))?;
370
371        let search_pattern = format!("%{}%", query);
372        let mut rows = stmt
373            .query(rusqlite::params![&search_pattern, limit as i64])
374            .map_err(|e| VaultError::StorageError(format!("Failed to query: {}", e)))?;
375
376        let mut documents = Vec::new();
377        while let Some(row) = rows
378            .next()
379            .map_err(|e| VaultError::StorageError(format!("Failed to iterate rows: {}", e)))?
380        {
381            let id: String = row
382                .get(0)
383                .map_err(|e| VaultError::StorageError(format!("Failed to get id: {}", e)))?;
384            let content: String = row
385                .get(1)
386                .map_err(|e| VaultError::StorageError(format!("Failed to get content: {}", e)))?;
387            let metadata_json: String = row
388                .get(2)
389                .map_err(|e| VaultError::StorageError(format!("Failed to get metadata: {}", e)))?;
390            let embedding_blob: Option<Vec<u8>> = row
391                .get(3)
392                .map_err(|e| VaultError::StorageError(format!("Failed to get embedding: {}", e)))?;
393
394            let metadata: HashMap<String, String> =
395                serde_json::from_str(&metadata_json).unwrap_or_default();
396
397            let embedding = embedding_blob.map(|blob| {
398                // `as_chunks` yields `[u8; 4]` directly, so there is no manual
399                // indexing to get wrong and no per-element bounds check.
400                let (whole, _trailing) = blob.as_chunks::<4>();
401                whole.iter().copied().map(f32::from_le_bytes).collect()
402            });
403
404            documents.push(Document {
405                id,
406                content,
407                metadata,
408                embedding,
409                chunk_info: None, // Simplified for search results
410            });
411        }
412
413        Ok(documents)
414    }
415}
416
417#[cfg(feature = "sqlite")]
418impl Database for SQLiteDatabase {
419    fn query(&self, query: &str) -> Result<Vec<HashMap<String, String>>> {
420        let conn = self
421            .conn
422            .lock()
423            .map_err(|e| VaultError::StorageError(format!("Lock poisoned: {}", e)))?;
424
425        let mut stmt = conn
426            .prepare(query)
427            .map_err(|e| VaultError::StorageError(format!("Failed to prepare query: {}", e)))?;
428
429        let column_names: Vec<String> = stmt.column_names().iter().map(|s| s.to_string()).collect();
430
431        let mut rows = stmt
432            .query([])
433            .map_err(|e| VaultError::StorageError(format!("Failed to execute query: {}", e)))?;
434
435        let mut results = Vec::new();
436        while let Some(row) = rows
437            .next()
438            .map_err(|e| VaultError::StorageError(format!("Failed to iterate rows: {}", e)))?
439        {
440            let mut record = HashMap::new();
441            for (i, col_name) in column_names.iter().enumerate() {
442                let value: String = row.get(i).unwrap_or_else(|_| String::new());
443                record.insert(col_name.clone(), value);
444            }
445            results.push(record);
446        }
447
448        Ok(results)
449    }
450
451    fn insert(&mut self, table: &str, data: HashMap<String, String>) -> Result<()> {
452        validate_sql_identifier(table)?;
453        let conn = self
454            .conn
455            .lock()
456            .map_err(|e| VaultError::StorageError(format!("Lock poisoned: {}", e)))?;
457
458        let columns: Vec<String> = data.keys().cloned().collect();
459        for col in &columns {
460            validate_sql_identifier(col)?;
461        }
462        let placeholders: Vec<String> = (1..=columns.len()).map(|i| format!("?{}", i)).collect();
463
464        let sql = format!(
465            "INSERT INTO {} ({}) VALUES ({})",
466            table,
467            columns.join(", "),
468            placeholders.join(", ")
469        );
470
471        let values: Vec<&str> = columns
472            .iter()
473            .map(|k| data.get(k).map(|s| s.as_str()).unwrap_or(""))
474            .collect();
475
476        conn.execute(&sql, rusqlite::params_from_iter(values))
477            .map_err(|e| VaultError::StorageError(format!("Failed to insert: {}", e)))?;
478
479        Ok(())
480    }
481
482    fn update(&mut self, table: &str, id: &str, data: HashMap<String, String>) -> Result<()> {
483        validate_sql_identifier(table)?;
484        for col in data.keys() {
485            validate_sql_identifier(col)?;
486        }
487
488        let conn = self
489            .conn
490            .lock()
491            .map_err(|e| VaultError::StorageError(format!("Lock poisoned: {}", e)))?;
492
493        let set_clause: Vec<String> = data.keys().map(|k| format!("{} = ?", k)).collect();
494        let sql = format!(
495            "UPDATE {} SET {} WHERE id = ?",
496            table,
497            set_clause.join(", ")
498        );
499
500        let mut values: Vec<&str> = data.values().map(|s| s.as_str()).collect();
501        values.push(id);
502
503        conn.execute(&sql, rusqlite::params_from_iter(values))
504            .map_err(|e| VaultError::StorageError(format!("Failed to update: {}", e)))?;
505
506        Ok(())
507    }
508
509    fn delete(&mut self, table: &str, id: &str) -> Result<()> {
510        validate_sql_identifier(table)?;
511
512        let conn = self
513            .conn
514            .lock()
515            .map_err(|e| VaultError::StorageError(format!("Lock poisoned: {}", e)))?;
516
517        let sql = format!("DELETE FROM {} WHERE id = ?", table);
518        conn.execute(&sql, [id])
519            .map_err(|e| VaultError::StorageError(format!("Failed to delete: {}", e)))?;
520
521        Ok(())
522    }
523}
524
525/// Sled key-value database implementation
526#[cfg(feature = "kv-store")]
527pub struct SledDatabase {
528    db: sled::Db,
529}
530
531#[cfg(feature = "kv-store")]
532impl SledDatabase {
533    /// Create a new Sled database
534    pub fn new(path: &std::path::Path) -> Result<Self> {
535        let db = sled::open(path).map_err(|e| {
536            VaultError::StorageError(format!("Failed to open Sled database: {}", e))
537        })?;
538
539        Ok(Self { db })
540    }
541
542    /// Create temporary in-memory database
543    pub fn temporary() -> Result<Self> {
544        let config = sled::Config::new().temporary(true);
545        let db = config.open().map_err(|e| {
546            VaultError::StorageError(format!("Failed to create temporary database: {}", e))
547        })?;
548
549        Ok(Self { db })
550    }
551
552    /// Store a document
553    pub fn store_document(&self, doc: &Document) -> Result<()> {
554        let doc_json =
555            serde_json::to_vec(doc).map_err(|e| VaultError::SerializationError(e.to_string()))?;
556
557        self.db
558            .insert(doc.id.as_bytes(), doc_json)
559            .map_err(|e| VaultError::StorageError(format!("Failed to insert document: {}", e)))?;
560
561        self.db
562            .flush()
563            .map_err(|e| VaultError::StorageError(format!("Failed to flush: {}", e)))?;
564
565        Ok(())
566    }
567
568    /// Retrieve a document by ID
569    pub fn get_document(&self, id: &str) -> Result<Option<Document>> {
570        if let Some(doc_bytes) = self
571            .db
572            .get(id.as_bytes())
573            .map_err(|e| VaultError::StorageError(format!("Failed to get document: {}", e)))?
574        {
575            let doc: Document = serde_json::from_slice(&doc_bytes).map_err(|e| {
576                VaultError::SerializationError(format!("Failed to deserialize: {}", e))
577            })?;
578
579            Ok(Some(doc))
580        } else {
581            Ok(None)
582        }
583    }
584
585    /// List all document IDs
586    pub fn list_documents(&self) -> Result<Vec<String>> {
587        let mut ids = Vec::new();
588
589        for (key, _) in self.db.iter().flatten() {
590            if let Ok(id) = String::from_utf8(key.to_vec()) {
591                ids.push(id);
592            }
593        }
594
595        Ok(ids)
596    }
597
598    /// Search documents by prefix
599    pub fn search_prefix(&self, prefix: &str) -> Result<Vec<Document>> {
600        let mut documents = Vec::new();
601
602        for (_, value) in self.db.scan_prefix(prefix.as_bytes()).flatten() {
603            if let Ok(doc) = serde_json::from_slice::<Document>(&value) {
604                documents.push(doc);
605            }
606        }
607
608        Ok(documents)
609    }
610}
611
612#[cfg(feature = "kv-store")]
613impl Database for SledDatabase {
614    fn query(&self, query: &str) -> Result<Vec<HashMap<String, String>>> {
615        // Simple prefix-based query for Sled
616        let parts: Vec<&str> = query.split_whitespace().collect();
617        if parts.is_empty() {
618            return Ok(Vec::new());
619        }
620
621        let prefix = parts[0];
622        let mut results = Vec::new();
623
624        for (key, value) in self.db.scan_prefix(prefix.as_bytes()).flatten() {
625            let mut record = HashMap::new();
626            record.insert("key".to_string(), String::from_utf8_lossy(&key).to_string());
627            record.insert(
628                "value".to_string(),
629                String::from_utf8_lossy(&value).to_string(),
630            );
631            results.push(record);
632        }
633
634        Ok(results)
635    }
636
637    fn insert(&mut self, table: &str, data: HashMap<String, String>) -> Result<()> {
638        // For Sled, use table as prefix and id as key
639        if let Some(id) = data.get("id") {
640            let key = format!("{}:{}", table, id);
641            let value = serde_json::to_vec(&data)
642                .map_err(|e| VaultError::SerializationError(e.to_string()))?;
643
644            self.db
645                .insert(key.as_bytes(), value)
646                .map_err(|e| VaultError::StorageError(format!("Failed to insert: {}", e)))?;
647
648            self.db
649                .flush()
650                .map_err(|e| VaultError::StorageError(format!("Failed to flush: {}", e)))?;
651
652            Ok(())
653        } else {
654            Err(VaultError::InvalidInput(
655                "Data must contain 'id' field".to_string(),
656            ))
657        }
658    }
659
660    fn update(&mut self, table: &str, id: &str, data: HashMap<String, String>) -> Result<()> {
661        let key = format!("{}:{}", table, id);
662
663        // Get existing data, merge with new data
664        if let Some(existing) = self
665            .db
666            .get(key.as_bytes())
667            .map_err(|e| VaultError::StorageError(format!("Failed to get: {}", e)))?
668        {
669            let mut existing_data: HashMap<String, String> =
670                serde_json::from_slice(&existing).unwrap_or_default();
671
672            existing_data.extend(data);
673
674            let value = serde_json::to_vec(&existing_data)
675                .map_err(|e| VaultError::SerializationError(e.to_string()))?;
676
677            self.db
678                .insert(key.as_bytes(), value)
679                .map_err(|e| VaultError::StorageError(format!("Failed to update: {}", e)))?;
680
681            self.db
682                .flush()
683                .map_err(|e| VaultError::StorageError(format!("Failed to flush: {}", e)))?;
684
685            Ok(())
686        } else {
687            Err(VaultError::InvalidInput(format!(
688                "Record {}:{} not found",
689                table, id
690            )))
691        }
692    }
693
694    fn delete(&mut self, table: &str, id: &str) -> Result<()> {
695        let key = format!("{}:{}", table, id);
696        self.db
697            .remove(key.as_bytes())
698            .map_err(|e| VaultError::StorageError(format!("Failed to delete: {}", e)))?;
699
700        self.db
701            .flush()
702            .map_err(|e| VaultError::StorageError(format!("Failed to flush: {}", e)))?;
703
704        Ok(())
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711
712    // ── InMemoryDatabase ──
713
714    #[test]
715    fn test_in_memory_create_and_insert() {
716        let mut db = InMemoryDatabase::new();
717        db.create_table("models".to_string());
718        let mut data = HashMap::new();
719        data.insert("id".to_string(), "m1".to_string());
720        data.insert("name".to_string(), "gpt".to_string());
721        db.insert("models", data).unwrap();
722
723        let results = db.query("models").unwrap();
724        assert_eq!(results.len(), 1);
725        assert_eq!(results[0].get("name").unwrap(), "gpt");
726    }
727
728    #[test]
729    fn test_in_memory_update() {
730        let mut db = InMemoryDatabase::new();
731        db.create_table("t".to_string());
732        let mut data = HashMap::new();
733        data.insert("id".to_string(), "r1".to_string());
734        data.insert("val".to_string(), "old".to_string());
735        db.insert("t", data).unwrap();
736
737        let mut upd = HashMap::new();
738        upd.insert("val".to_string(), "new".to_string());
739        db.update("t", "r1", upd).unwrap();
740
741        let results = db.query("t").unwrap();
742        assert_eq!(results[0].get("val").unwrap(), "new");
743    }
744
745    #[test]
746    fn test_in_memory_delete() {
747        let mut db = InMemoryDatabase::new();
748        db.create_table("t".to_string());
749        let mut data = HashMap::new();
750        data.insert("id".to_string(), "r1".to_string());
751        db.insert("t", data).unwrap();
752
753        db.delete("t", "r1").unwrap();
754        let results = db.query("t").unwrap();
755        assert!(results.is_empty());
756    }
757
758    #[test]
759    fn test_in_memory_insert_missing_table() {
760        let mut db = InMemoryDatabase::new();
761        let data = HashMap::new();
762        let err = db.insert("nonexistent", data).unwrap_err();
763        assert!(format!("{err}").contains("not found"));
764    }
765
766    #[test]
767    fn test_in_memory_update_missing_table() {
768        let mut db = InMemoryDatabase::new();
769        let err = db.update("nonexistent", "x", HashMap::new()).unwrap_err();
770        assert!(format!("{err}").contains("not found"));
771    }
772
773    #[test]
774    fn test_in_memory_update_missing_row() {
775        let mut db = InMemoryDatabase::new();
776        db.create_table("t".to_string());
777        let err = db.update("t", "missing", HashMap::new()).unwrap_err();
778        assert!(format!("{err}").contains("not found"));
779    }
780
781    #[test]
782    fn test_in_memory_delete_missing_table() {
783        let mut db = InMemoryDatabase::new();
784        let err = db.delete("nonexistent", "x").unwrap_err();
785        assert!(format!("{err}").contains("not found"));
786    }
787
788    #[test]
789    fn test_in_memory_query_missing_table() {
790        let db = InMemoryDatabase::new();
791        let results = db.query("nonexistent").unwrap();
792        assert!(results.is_empty());
793    }
794
795    #[test]
796    fn test_in_memory_query_where() {
797        let mut db = InMemoryDatabase::new();
798        db.create_table("t".to_string());
799        let mut d1 = HashMap::new();
800        d1.insert("id".to_string(), "1".to_string());
801        d1.insert("color".to_string(), "red".to_string());
802        db.insert("t", d1).unwrap();
803
804        let mut d2 = HashMap::new();
805        d2.insert("id".to_string(), "2".to_string());
806        d2.insert("color".to_string(), "blue".to_string());
807        db.insert("t", d2).unwrap();
808
809        // WHERE color=red
810        let results = db.query("t WHERE color=red").unwrap();
811        assert_eq!(results.len(), 1);
812        assert_eq!(results[0].get("color").unwrap(), "red");
813    }
814
815    // ── validate_sql_identifier ──
816
817    #[cfg(feature = "sqlite")]
818    #[test]
819    fn test_validate_sql_identifier_empty() {
820        let err = validate_sql_identifier("").unwrap_err();
821        assert!(format!("{err}").contains("cannot be empty"));
822    }
823
824    #[cfg(feature = "sqlite")]
825    #[test]
826    fn test_validate_sql_identifier_too_long() {
827        let long = "a".repeat(129);
828        let err = validate_sql_identifier(&long).unwrap_err();
829        assert!(format!("{err}").contains("too long"));
830    }
831
832    #[cfg(feature = "sqlite")]
833    #[test]
834    fn test_validate_sql_identifier_bad_start() {
835        let err = validate_sql_identifier("1abc").unwrap_err();
836        assert!(format!("{err}").contains("must start with"));
837    }
838
839    #[cfg(feature = "sqlite")]
840    #[test]
841    fn test_validate_sql_identifier_invalid_chars() {
842        let err = validate_sql_identifier("abc def").unwrap_err();
843        assert!(format!("{err}").contains("invalid characters"));
844    }
845
846    #[cfg(feature = "sqlite")]
847    #[test]
848    fn test_validate_sql_identifier_valid() {
849        assert!(validate_sql_identifier("_my_table_1").is_ok());
850        assert!(validate_sql_identifier("users").is_ok());
851    }
852
853    // ── SQLiteDatabase ──
854
855    #[cfg(feature = "sqlite")]
856    #[test]
857    fn test_sqlite_new_on_disk() {
858        let dir = tempfile::tempdir().unwrap();
859        let path = dir.path().join("test.db");
860        let db = SQLiteDatabase::new(&path).unwrap();
861        assert!(path.exists());
862
863        // Verify we can create a table and insert
864        db.create_table("t", &[("name", "TEXT")]).unwrap();
865        let mut data = HashMap::new();
866        data.insert("id".to_string(), "1".to_string());
867        data.insert("name".to_string(), "alice".to_string());
868        let mut db = db;
869        db.insert("t", data).unwrap();
870    }
871
872    #[cfg(feature = "sqlite")]
873    #[test]
874    fn test_sqlite_in_memory() {
875        let db = SQLiteDatabase::in_memory().unwrap();
876        db.create_table("items", &[("val", "TEXT")]).unwrap();
877    }
878
879    #[cfg(feature = "sqlite")]
880    #[test]
881    fn test_sqlite_create_table_and_crud() {
882        let mut db = SQLiteDatabase::in_memory().unwrap();
883        db.create_table("users", &[("name", "TEXT"), ("age", "TEXT")])
884            .unwrap();
885
886        // Insert
887        let mut data = HashMap::new();
888        data.insert("id".to_string(), "u1".to_string());
889        data.insert("name".to_string(), "Alice".to_string());
890        data.insert("age".to_string(), "30".to_string());
891        db.insert("users", data).unwrap();
892
893        // Query
894        let rows = db.query("SELECT * FROM users").unwrap();
895        assert_eq!(rows.len(), 1);
896        assert_eq!(rows[0].get("name").unwrap(), "Alice");
897
898        // Update
899        let mut upd = HashMap::new();
900        upd.insert("name".to_string(), "Bob".to_string());
901        db.update("users", "u1", upd).unwrap();
902        let rows = db.query("SELECT name FROM users WHERE id='u1'").unwrap();
903        assert_eq!(rows[0].get("name").unwrap(), "Bob");
904
905        // Delete
906        db.delete("users", "u1").unwrap();
907        let rows = db.query("SELECT * FROM users").unwrap();
908        assert!(rows.is_empty());
909    }
910
911    #[cfg(feature = "sqlite")]
912    #[test]
913    fn test_sqlite_store_and_get_document() {
914        let db = SQLiteDatabase::in_memory().unwrap();
915        let doc = Document {
916            id: "d1".to_string(),
917            content: "Test document content".to_string(),
918            metadata: {
919                let mut m = HashMap::new();
920                m.insert("author".to_string(), "test".to_string());
921                m
922            },
923            embedding: Some(vec![1.0, 2.0, 3.0]),
924            chunk_info: Some(ChunkInfo {
925                parent_id: Some("parent1".to_string()),
926                chunk_index: 0,
927                total_chunks: 3,
928                overlap: 50,
929            }),
930        };
931        db.store_document(&doc).unwrap();
932
933        let retrieved = db.get_document("d1").unwrap().unwrap();
934        assert_eq!(retrieved.id, "d1");
935        assert_eq!(retrieved.content, "Test document content");
936        assert_eq!(retrieved.metadata.get("author").unwrap(), "test");
937        assert!(retrieved.embedding.is_some());
938        assert!(retrieved.chunk_info.is_some());
939        let ci = retrieved.chunk_info.unwrap();
940        assert_eq!(ci.parent_id.as_deref(), Some("parent1"));
941        assert_eq!(ci.chunk_index, 0);
942        assert_eq!(ci.total_chunks, 3);
943    }
944
945    #[cfg(feature = "sqlite")]
946    #[test]
947    fn test_sqlite_get_document_not_found() {
948        let db = SQLiteDatabase::in_memory().unwrap();
949        // Create documents table first
950        let dummy = Document {
951            id: "setup".to_string(),
952            content: "x".to_string(),
953            metadata: HashMap::new(),
954            embedding: None,
955            chunk_info: None,
956        };
957        db.store_document(&dummy).unwrap();
958
959        let result = db.get_document("nonexistent").unwrap();
960        assert!(result.is_none());
961    }
962
963    #[cfg(feature = "sqlite")]
964    #[test]
965    fn test_sqlite_search_documents() {
966        let db = SQLiteDatabase::in_memory().unwrap();
967        let doc1 = Document {
968            id: "s1".to_string(),
969            content: "Machine learning with neural networks".to_string(),
970            metadata: HashMap::new(),
971            embedding: None,
972            chunk_info: None,
973        };
974        let doc2 = Document {
975            id: "s2".to_string(),
976            content: "Database management systems".to_string(),
977            metadata: HashMap::new(),
978            embedding: None,
979            chunk_info: None,
980        };
981        db.store_document(&doc1).unwrap();
982        db.store_document(&doc2).unwrap();
983
984        let results = db.search_documents("neural", 10).unwrap();
985        assert!(!results.is_empty());
986        assert!(results.iter().any(|d| d.id == "s1"));
987    }
988
989    #[cfg(feature = "sqlite")]
990    #[test]
991    fn test_sqlite_store_document_no_embedding_no_chunk() {
992        let db = SQLiteDatabase::in_memory().unwrap();
993        let doc = Document {
994            id: "plain".to_string(),
995            content: "Plain text".to_string(),
996            metadata: HashMap::new(),
997            embedding: None,
998            chunk_info: None,
999        };
1000        db.store_document(&doc).unwrap();
1001
1002        let retrieved = db.get_document("plain").unwrap().unwrap();
1003        assert_eq!(retrieved.content, "Plain text");
1004        assert!(retrieved.embedding.is_none());
1005        assert!(retrieved.chunk_info.is_none());
1006    }
1007
1008    #[test]
1009    fn test_sqlite_get_document_nonexistent() {
1010        let db = SQLiteDatabase::in_memory().unwrap();
1011        let doc = Document {
1012            id: "existing".to_string(),
1013            content: "some content".to_string(),
1014            metadata: HashMap::new(),
1015            embedding: None,
1016            chunk_info: None,
1017        };
1018        db.store_document(&doc).unwrap();
1019        let result = db.get_document("does_not_exist").unwrap();
1020        assert!(result.is_none(), "Nonexistent document should return None");
1021    }
1022
1023    #[test]
1024    fn test_sqlite_database_crud() {
1025        let mut db = SQLiteDatabase::in_memory().unwrap();
1026        db.create_table("items", &[("name", "TEXT")]).unwrap();
1027        let mut data = std::collections::HashMap::new();
1028        data.insert("id".to_string(), "item1".to_string());
1029        data.insert("name".to_string(), "widget".to_string());
1030        db.insert("items", data).unwrap();
1031        let rows = db.query("SELECT * FROM items").unwrap();
1032        assert!(!rows.is_empty());
1033        let mut upd = std::collections::HashMap::new();
1034        upd.insert("name".to_string(), "gizmo".to_string());
1035        db.update("items", "item1", upd).unwrap();
1036        db.delete("items", "item1").unwrap();
1037        let rows2 = db.query("SELECT * FROM items").unwrap();
1038        assert!(rows2.is_empty());
1039    }
1040
1041    #[test]
1042    fn test_sqlite_search_documents_no_results() {
1043        let db = SQLiteDatabase::in_memory().unwrap();
1044        // Store a document first to create documents table
1045        let doc = Document {
1046            id: "seed".to_string(),
1047            content: "seed content".to_string(),
1048            metadata: HashMap::new(),
1049            embedding: None,
1050            chunk_info: None,
1051        };
1052        db.store_document(&doc).unwrap();
1053        let results = db.search_documents("nonexistent query", 10).unwrap();
1054        assert!(results.is_empty());
1055    }
1056
1057    #[test]
1058    fn test_in_memory_default() {
1059        let mut db = InMemoryDatabase::default();
1060        db.create_table("t".to_string());
1061        let mut data = std::collections::HashMap::new();
1062        data.insert("id".to_string(), "1".to_string());
1063        data.insert("col".to_string(), "v".to_string());
1064        db.insert("t", data).unwrap();
1065        let rows = db.query("t").unwrap();
1066        assert_eq!(rows.len(), 1);
1067    }
1068
1069    #[test]
1070    fn test_in_memory_empty_query() {
1071        let db = InMemoryDatabase::new();
1072        let rows = db.query("").unwrap();
1073        assert!(rows.is_empty());
1074    }
1075
1076    #[cfg(feature = "sqlite")]
1077    #[test]
1078    fn test_sqlite_search_documents_with_embedding() {
1079        let db = SQLiteDatabase::in_memory().unwrap();
1080        db.create_table("items", &[("name", "TEXT")]).unwrap();
1081        // Store document with embedding
1082        let doc = crate::rag::documents::Document {
1083            id: "doc_emb".to_string(),
1084            content: "embedding search test".to_string(),
1085            metadata: std::collections::HashMap::new(),
1086            embedding: Some(vec![0.1, 0.2, 0.3]),
1087            chunk_info: None,
1088        };
1089        db.store_document(&doc).unwrap();
1090        // Search should hit the document and parse embedding
1091        let results = db.search_documents("embedding", 10).unwrap();
1092        assert_eq!(results.len(), 1);
1093        assert_eq!(results[0].id, "doc_emb");
1094        // Embedding roundtrip through BLOB
1095        assert!(results[0].embedding.is_some());
1096        let emb = results[0].embedding.as_ref().unwrap();
1097        assert_eq!(emb.len(), 3);
1098        assert!((emb[0] - 0.1).abs() < 1e-6);
1099    }
1100}