Skip to main content

lc_vector_stores/
sqlite_store.rs

1// lc-vector-stores/src/sqlite_store.rs
2//! SQLite document store implementation
3
4use async_trait::async_trait;
5use lc_shared::splitter::{RecursiveCharacterSplitter, TextSplitter};
6use rusqlite::Connection;
7use std::sync::Arc;
8use std::sync::Mutex;
9use uuid::Uuid;
10
11use crate::document_store::{ChunkDocument, ChunkedDocumentStoreTrait, DocumentStore};
12use crate::{Document, VectorStoreError};
13
14/// Parses the metadata column; on corruption, logs and falls back to an empty map instead of silently swallowing the error.
15fn parse_metadata_or_default(raw: &str) -> std::collections::HashMap<String, serde_json::Value> {
16    serde_json::from_str(raw).unwrap_or_else(|e| {
17        log::warn!(
18            "SQLite metadata in store is corrupted, falling back to empty map: {}",
19            e
20        );
21        std::collections::HashMap::new()
22    })
23}
24
25/// SQLite document store configuration
26#[derive(Debug, Clone)]
27pub struct SQLiteStoreConfig {
28    /// SQLite database file path
29    pub db_path: String,
30}
31
32impl Default for SQLiteStoreConfig {
33    fn default() -> Self {
34        Self {
35            db_path: "langchainrust.db".to_string(),
36        }
37    }
38}
39
40impl SQLiteStoreConfig {
41    /// Creates a config from a database file path.
42    pub fn new(path: impl Into<String>) -> Self {
43        Self {
44            db_path: path.into(),
45        }
46    }
47}
48
49/// SQLite document store implementation
50pub struct SQLiteDocumentStore {
51    conn: Arc<Mutex<Connection>>,
52}
53
54impl SQLiteDocumentStore {
55    /// Opens the SQLite database and creates the required tables and indexes.
56    pub fn new(config: SQLiteStoreConfig) -> Result<Self, VectorStoreError> {
57        let conn = Connection::open(&config.db_path)
58            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
59        conn.execute_batch(
60            "CREATE TABLE IF NOT EXISTS documents (
61                id TEXT PRIMARY KEY, content TEXT NOT NULL,
62                metadata TEXT NOT NULL DEFAULT '{}'
63            );
64            CREATE TABLE IF NOT EXISTS chunks (
65                chunk_id TEXT PRIMARY KEY, parent_id TEXT NOT NULL,
66                content TEXT NOT NULL, segment INTEGER NOT NULL,
67                metadata TEXT NOT NULL DEFAULT '{}'
68            );
69            CREATE INDEX IF NOT EXISTS idx_chunks_parent ON chunks(parent_id);
70            CREATE INDEX IF NOT EXISTS idx_chunks_segment ON chunks(parent_id, segment);",
71        )
72        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
73        Ok(Self {
74            conn: Arc::new(Mutex::new(conn)),
75        })
76    }
77
78    /// Explicitly triggers persistence to disk.
79    ///
80    /// C3: the fake default `ChunkedDocumentStoreTrait::save` has been removed from the trait;
81    /// SQLite persists automatically on every write, so this method is a compatibility no-op
82    /// (aligned with Redis's `save_to_disk`).
83    pub async fn save(&self) -> Result<(), VectorStoreError> {
84        Ok(())
85    }
86}
87
88#[async_trait]
89impl DocumentStore for SQLiteDocumentStore {
90    async fn add_document(&self, document: Document) -> Result<String, VectorStoreError> {
91        let id = document
92            .id
93            .clone()
94            .unwrap_or_else(|| Uuid::new_v4().to_string());
95        let meta = serde_json::to_string(&document.metadata).unwrap_or_else(|_| "{}".to_string());
96        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
97        conn.execute(
98            "INSERT OR REPLACE INTO documents (id, content, metadata) VALUES (?1, ?2, ?3)",
99            rusqlite::params![id, document.content, meta],
100        )
101        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
102        Ok(id)
103    }
104
105    async fn add_documents(
106        &self,
107        documents: Vec<Document>,
108    ) -> Result<Vec<String>, VectorStoreError> {
109        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
110        let mut ids = Vec::new();
111        for doc in documents {
112            let id = doc.id.clone().unwrap_or_else(|| Uuid::new_v4().to_string());
113            let meta = serde_json::to_string(&doc.metadata).unwrap_or_else(|_| "{}".to_string());
114            conn.execute(
115                "INSERT OR REPLACE INTO documents (id, content, metadata) VALUES (?1, ?2, ?3)",
116                rusqlite::params![id, doc.content, meta],
117            )
118            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
119            ids.push(id);
120        }
121        Ok(ids)
122    }
123
124    async fn get_document(&self, id: &str) -> Result<Option<Document>, VectorStoreError> {
125        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
126        let mut stmt = conn
127            .prepare("SELECT id, content, metadata FROM documents WHERE id = ?1")
128            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
129        let result = stmt.query_row(rusqlite::params![id], |row| {
130            let id: String = row.get(0)?;
131            let content: String = row.get(1)?;
132            let meta_str: String = row.get(2)?;
133            Ok(Document {
134                id: Some(id),
135                content,
136                metadata: parse_metadata_or_default(&meta_str),
137            })
138        });
139        match result {
140            Ok(doc) => Ok(Some(doc)),
141            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
142            Err(e) => Err(VectorStoreError::StorageError(e.to_string())),
143        }
144    }
145
146    async fn delete_document(&self, id: &str) -> Result<(), VectorStoreError> {
147        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
148        conn.execute(
149            "DELETE FROM chunks WHERE parent_id = ?1",
150            rusqlite::params![id],
151        )
152        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
153        conn.execute("DELETE FROM documents WHERE id = ?1", rusqlite::params![id])
154            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
155        Ok(())
156    }
157
158    async fn count(&self) -> usize {
159        // M-23: `count` returns usize (cannot `?`), so a poisoned sync lock
160        // surfaces as a logged error + 0, matching the error handling below.
161        let conn = match self.conn.lock() {
162            Ok(guard) => guard,
163            Err(e) => {
164                log::error!("SQLite lock poisoned in count(documents), returning 0: {}", e);
165                return 0;
166            }
167        };
168        match conn.query_row("SELECT COUNT(*) FROM documents", [], |r| r.get(0)) {
169            Ok(count) => count,
170            // M3: the trait returns usize so errors cannot propagate — stop silently swallowing
171            // errors and returning 0; log an error to surface storage failures, consistent with
172            // the "no silent row drops" pattern already used in this file.
173            Err(e) => {
174                log::error!("SQLite count(documents) query failed, returning 0: {}", e);
175                0
176            }
177        }
178    }
179
180    async fn clear(&self) -> Result<(), VectorStoreError> {
181        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
182        conn.execute_batch("DELETE FROM chunks; DELETE FROM documents;")
183            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
184        Ok(())
185    }
186}
187
188#[async_trait]
189impl ChunkedDocumentStoreTrait for SQLiteDocumentStore {
190    async fn add_parent_document(
191        &self,
192        document: Document,
193        chunk_size: usize,
194    ) -> Result<(String, Vec<String>), VectorStoreError> {
195        let splitter = RecursiveCharacterSplitter::new(chunk_size, chunk_size / 10);
196        let chunks_text = splitter.split_text(&document.content);
197        let parent_id = document
198            .id
199            .clone()
200            .unwrap_or_else(|| Uuid::new_v4().to_string());
201        let meta = serde_json::to_string(&document.metadata).unwrap_or_else(|_| "{}".to_string());
202
203        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
204        conn.execute(
205            "INSERT OR REPLACE INTO documents (id, content, metadata) VALUES (?1, ?2, ?3)",
206            rusqlite::params![parent_id, document.content, meta],
207        )
208        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
209
210        // H5: replace the parent's prior chunk set first so a re-ingest that shrank /
211        // changed segmentation leaves no stale `{parent}:chunk:{n}` rows behind
212        // (INSERT OR REPLACE only overwrites equal ids; higher segment ids from a
213        // previous, larger set would otherwise linger and stay retrievable).
214        conn.execute(
215            "DELETE FROM chunks WHERE parent_id = ?1",
216            rusqlite::params![parent_id],
217        )
218        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
219
220        let mut chunk_ids = Vec::new();
221        for (i, text) in chunks_text.iter().enumerate() {
222            let cid = format!("{}:chunk:{}", parent_id, i);
223            conn.execute("INSERT OR REPLACE INTO chunks (chunk_id, parent_id, content, segment, metadata) VALUES (?1, ?2, ?3, ?4, ?5)",
224                rusqlite::params![cid, parent_id, text, i, "{}"])
225                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
226            chunk_ids.push(cid);
227        }
228        Ok((parent_id, chunk_ids))
229    }
230
231    async fn add_parent_documents(
232        &self,
233        documents: Vec<Document>,
234        chunk_size: usize,
235    ) -> Result<Vec<(String, Vec<String>)>, VectorStoreError> {
236        let mut results = Vec::new();
237        for doc in documents {
238            results.push(self.add_parent_document(doc, chunk_size).await?);
239        }
240        Ok(results)
241    }
242
243    async fn get_parent_document(
244        &self,
245        parent_id: &str,
246    ) -> Result<Option<Document>, VectorStoreError> {
247        self.get_document(parent_id).await
248    }
249
250    async fn get_chunk(&self, chunk_id: &str) -> Result<Option<ChunkDocument>, VectorStoreError> {
251        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
252        let mut stmt = conn.prepare("SELECT chunk_id, parent_id, content, segment, metadata FROM chunks WHERE chunk_id = ?1")
253            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
254        let result = stmt.query_row(rusqlite::params![chunk_id], |row| {
255            Ok(ChunkDocument {
256                chunk_id: row.get(0)?,
257                parent_id: row.get(1)?,
258                content: row.get(2)?,
259                segment: row.get(3)?,
260                metadata: parse_metadata_or_default(&row.get::<_, String>(4)?),
261            })
262        });
263        match result {
264            Ok(chunk) => Ok(Some(chunk)),
265            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
266            Err(e) => Err(VectorStoreError::StorageError(e.to_string())),
267        }
268    }
269
270    async fn get_chunk_document(
271        &self,
272        chunk_id: &str,
273    ) -> Result<Option<Document>, VectorStoreError> {
274        Ok(self.get_chunk(chunk_id).await?.map(|c| c.to_document()))
275    }
276
277    async fn get_chunks_for_parent(
278        &self,
279        parent_id: &str,
280    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
281        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
282        let mut stmt = conn.prepare("SELECT chunk_id, parent_id, content, segment, metadata FROM chunks WHERE parent_id = ?1 ORDER BY segment")
283            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
284        let chunks = stmt
285            .query_map(rusqlite::params![parent_id], |row| {
286                Ok(ChunkDocument {
287                    chunk_id: row.get(0)?,
288                    parent_id: row.get(1)?,
289                    content: row.get(2)?,
290                    segment: row.get(3)?,
291                    metadata: parse_metadata_or_default(&row.get::<_, String>(4)?),
292                })
293            })
294            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
295            .filter_map(|r| match r {
296                Ok(chunk) => Some(chunk),
297                Err(e) => {
298                    // stop silently dropping rows: log to surface storage degradation
299                    log::error!(
300                        "SQLite store: a row failed to deserialize and was dropped from the query result: {}",
301                        e
302                    );
303                    None
304                }
305            })
306            .collect();
307        Ok(chunks)
308    }
309
310    async fn get_chunk_documents_for_parent(
311        &self,
312        parent_id: &str,
313    ) -> Result<Vec<Document>, VectorStoreError> {
314        Ok(self
315            .get_chunks_for_parent(parent_id)
316            .await?
317            .into_iter()
318            .map(|c| c.to_document())
319            .collect())
320    }
321
322    async fn delete_parent_document(&self, parent_id: &str) -> Result<(), VectorStoreError> {
323        self.delete_document(parent_id).await
324    }
325
326    async fn parent_count(&self) -> usize {
327        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
328        match conn.query_row("SELECT COUNT(*) FROM documents", [], |r| r.get(0)) {
329            Ok(count) => count,
330            // M3: do not silently swallow errors and return 0; log an error to surface storage failures.
331            Err(e) => {
332                log::error!("SQLite parent_count query failed, returning 0: {}", e);
333                0
334            }
335        }
336    }
337
338    async fn chunk_count(&self) -> usize {
339        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
340        match conn.query_row("SELECT COUNT(*) FROM chunks", [], |r| r.get(0)) {
341            Ok(count) => count,
342            // M3: do not silently swallow errors and return 0; log an error to surface storage failures.
343            Err(e) => {
344                log::error!("SQLite chunk_count query failed, returning 0: {}", e);
345                0
346            }
347        }
348    }
349
350    async fn get_all_chunks(&self) -> Result<Vec<ChunkDocument>, VectorStoreError> {
351        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
352        let mut stmt = conn
353            .prepare("SELECT chunk_id, parent_id, content, segment, metadata FROM chunks")
354            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
355        let chunks = stmt
356            .query_map([], |row| {
357                Ok(ChunkDocument {
358                    chunk_id: row.get(0)?,
359                    parent_id: row.get(1)?,
360                    content: row.get(2)?,
361                    segment: row.get(3)?,
362                    metadata: parse_metadata_or_default(&row.get::<_, String>(4)?),
363                })
364            })
365            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
366            .filter_map(|r| match r {
367                Ok(chunk) => Some(chunk),
368                Err(e) => {
369                    // stop silently dropping rows: log to surface storage degradation
370                    log::error!(
371                        "SQLite store: a row failed to deserialize and was dropped from the query result: {}",
372                        e
373                    );
374                    None
375                }
376            })
377            .collect();
378        Ok(chunks)
379    }
380
381    async fn clear(&self) -> Result<(), VectorStoreError> {
382        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
383        conn.execute_batch("DELETE FROM chunks; DELETE FROM documents;")
384            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
385        Ok(())
386    }
387
388    // Blocking methods
389    fn add_parent_document_blocking(
390        &self,
391        document: Document,
392        chunk_size: usize,
393    ) -> Result<(String, Vec<String>), VectorStoreError> {
394        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
395        let splitter = RecursiveCharacterSplitter::new(chunk_size, chunk_size / 10);
396        let chunks_text = splitter.split_text(&document.content);
397        let parent_id = document
398            .id
399            .clone()
400            .unwrap_or_else(|| Uuid::new_v4().to_string());
401        let meta = serde_json::to_string(&document.metadata).unwrap_or_else(|_| "{}".to_string());
402
403        conn.execute(
404            "INSERT OR REPLACE INTO documents (id, content, metadata) VALUES (?1, ?2, ?3)",
405            rusqlite::params![parent_id, document.content, meta],
406        )
407        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
408
409        // H5: drop the parent's prior chunk set so a shrunken re-ingest leaves none behind.
410        conn.execute(
411            "DELETE FROM chunks WHERE parent_id = ?1",
412            rusqlite::params![parent_id],
413        )
414        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
415
416        let mut chunk_ids = Vec::new();
417        for (i, text) in chunks_text.iter().enumerate() {
418            let cid = format!("{}:chunk:{}", parent_id, i);
419            conn.execute("INSERT OR REPLACE INTO chunks (chunk_id, parent_id, content, segment, metadata) VALUES (?1, ?2, ?3, ?4, ?5)",
420                rusqlite::params![cid, parent_id, text, i, "{}"])
421                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
422            chunk_ids.push(cid);
423        }
424        Ok((parent_id, chunk_ids))
425    }
426
427    fn get_parent_document_blocking(
428        &self,
429        parent_id: &str,
430    ) -> Result<Option<Document>, VectorStoreError> {
431        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
432        let mut stmt = conn
433            .prepare("SELECT id, content, metadata FROM documents WHERE id = ?1")
434            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
435        let result = stmt.query_row(rusqlite::params![parent_id], |row| {
436            Ok(Document {
437                id: Some(row.get(0)?),
438                content: row.get(1)?,
439                metadata: parse_metadata_or_default(&row.get::<_, String>(2)?),
440            })
441        });
442        match result {
443            Ok(doc) => Ok(Some(doc)),
444            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
445            Err(e) => Err(VectorStoreError::StorageError(e.to_string())),
446        }
447    }
448
449    fn get_chunk_blocking(
450        &self,
451        chunk_id: &str,
452    ) -> Result<Option<ChunkDocument>, VectorStoreError> {
453        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
454        let mut stmt = conn.prepare("SELECT chunk_id, parent_id, content, segment, metadata FROM chunks WHERE chunk_id = ?1")
455            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
456        let result = stmt.query_row(rusqlite::params![chunk_id], |row| {
457            Ok(ChunkDocument {
458                chunk_id: row.get(0)?,
459                parent_id: row.get(1)?,
460                content: row.get(2)?,
461                segment: row.get(3)?,
462                metadata: parse_metadata_or_default(&row.get::<_, String>(4)?),
463            })
464        });
465        match result {
466            Ok(chunk) => Ok(Some(chunk)),
467            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
468            Err(e) => Err(VectorStoreError::StorageError(e.to_string())),
469        }
470    }
471
472    fn blocking_get_chunks_for_parent(
473        &self,
474        parent_id: &str,
475    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
476        let conn = self.conn.lock().map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
477        let mut stmt = conn.prepare("SELECT chunk_id, parent_id, content, segment, metadata FROM chunks WHERE parent_id = ?1 ORDER BY segment")
478            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
479        let chunks = stmt
480            .query_map(rusqlite::params![parent_id], |row| {
481                Ok(ChunkDocument {
482                    chunk_id: row.get(0)?,
483                    parent_id: row.get(1)?,
484                    content: row.get(2)?,
485                    segment: row.get(3)?,
486                    metadata: parse_metadata_or_default(&row.get::<_, String>(4)?),
487                })
488            })
489            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
490            .filter_map(|r| match r {
491                Ok(chunk) => Some(chunk),
492                Err(e) => {
493                    // stop silently dropping rows: log to surface storage degradation
494                    log::error!(
495                        "SQLite store: a row failed to deserialize and was dropped from the query result: {}",
496                        e
497                    );
498                    None
499                }
500            })
501            .collect();
502        Ok(chunks)
503    }
504}