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 tokio::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().await;
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().await;
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().await;
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().await;
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        let conn = self.conn.lock().await;
160        match conn.query_row("SELECT COUNT(*) FROM documents", [], |r| r.get(0)) {
161            Ok(count) => count,
162            // M3: the trait returns usize so errors cannot propagate — stop silently swallowing
163            // errors and returning 0; log an error to surface storage failures, consistent with
164            // the "no silent row drops" pattern already used in this file.
165            Err(e) => {
166                log::error!("SQLite count(documents) query failed, returning 0: {}", e);
167                0
168            }
169        }
170    }
171
172    async fn clear(&self) -> Result<(), VectorStoreError> {
173        let conn = self.conn.lock().await;
174        conn.execute_batch("DELETE FROM chunks; DELETE FROM documents;")
175            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
176        Ok(())
177    }
178}
179
180#[async_trait]
181impl ChunkedDocumentStoreTrait for SQLiteDocumentStore {
182    async fn add_parent_document(
183        &self,
184        document: Document,
185        chunk_size: usize,
186    ) -> Result<(String, Vec<String>), VectorStoreError> {
187        let splitter = RecursiveCharacterSplitter::new(chunk_size, chunk_size / 10);
188        let chunks_text = splitter.split_text(&document.content);
189        let parent_id = document
190            .id
191            .clone()
192            .unwrap_or_else(|| Uuid::new_v4().to_string());
193        let meta = serde_json::to_string(&document.metadata).unwrap_or_else(|_| "{}".to_string());
194
195        let conn = self.conn.lock().await;
196        conn.execute(
197            "INSERT OR REPLACE INTO documents (id, content, metadata) VALUES (?1, ?2, ?3)",
198            rusqlite::params![parent_id, document.content, meta],
199        )
200        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
201
202        let mut chunk_ids = Vec::new();
203        for (i, text) in chunks_text.iter().enumerate() {
204            let cid = format!("{}:chunk:{}", parent_id, i);
205            conn.execute("INSERT OR REPLACE INTO chunks (chunk_id, parent_id, content, segment, metadata) VALUES (?1, ?2, ?3, ?4, ?5)",
206                rusqlite::params![cid, parent_id, text, i, "{}"])
207                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
208            chunk_ids.push(cid);
209        }
210        Ok((parent_id, chunk_ids))
211    }
212
213    async fn add_parent_documents(
214        &self,
215        documents: Vec<Document>,
216        chunk_size: usize,
217    ) -> Result<Vec<(String, Vec<String>)>, VectorStoreError> {
218        let mut results = Vec::new();
219        for doc in documents {
220            results.push(self.add_parent_document(doc, chunk_size).await?);
221        }
222        Ok(results)
223    }
224
225    async fn get_parent_document(
226        &self,
227        parent_id: &str,
228    ) -> Result<Option<Document>, VectorStoreError> {
229        self.get_document(parent_id).await
230    }
231
232    async fn get_chunk(&self, chunk_id: &str) -> Result<Option<ChunkDocument>, VectorStoreError> {
233        let conn = self.conn.lock().await;
234        let mut stmt = conn.prepare("SELECT chunk_id, parent_id, content, segment, metadata FROM chunks WHERE chunk_id = ?1")
235            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
236        let result = stmt.query_row(rusqlite::params![chunk_id], |row| {
237            Ok(ChunkDocument {
238                chunk_id: row.get(0)?,
239                parent_id: row.get(1)?,
240                content: row.get(2)?,
241                segment: row.get(3)?,
242                metadata: parse_metadata_or_default(&row.get::<_, String>(4)?),
243            })
244        });
245        match result {
246            Ok(chunk) => Ok(Some(chunk)),
247            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
248            Err(e) => Err(VectorStoreError::StorageError(e.to_string())),
249        }
250    }
251
252    async fn get_chunk_document(
253        &self,
254        chunk_id: &str,
255    ) -> Result<Option<Document>, VectorStoreError> {
256        Ok(self.get_chunk(chunk_id).await?.map(|c| c.to_document()))
257    }
258
259    async fn get_chunks_for_parent(
260        &self,
261        parent_id: &str,
262    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
263        let conn = self.conn.lock().await;
264        let mut stmt = conn.prepare("SELECT chunk_id, parent_id, content, segment, metadata FROM chunks WHERE parent_id = ?1 ORDER BY segment")
265            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
266        let chunks = stmt
267            .query_map(rusqlite::params![parent_id], |row| {
268                Ok(ChunkDocument {
269                    chunk_id: row.get(0)?,
270                    parent_id: row.get(1)?,
271                    content: row.get(2)?,
272                    segment: row.get(3)?,
273                    metadata: parse_metadata_or_default(&row.get::<_, String>(4)?),
274                })
275            })
276            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
277            .filter_map(|r| match r {
278                Ok(chunk) => Some(chunk),
279                Err(e) => {
280                    // stop silently dropping rows: log to surface storage degradation
281                    log::error!(
282                        "SQLite store: a row failed to deserialize and was dropped from the query result: {}",
283                        e
284                    );
285                    None
286                }
287            })
288            .collect();
289        Ok(chunks)
290    }
291
292    async fn get_chunk_documents_for_parent(
293        &self,
294        parent_id: &str,
295    ) -> Result<Vec<Document>, VectorStoreError> {
296        Ok(self
297            .get_chunks_for_parent(parent_id)
298            .await?
299            .into_iter()
300            .map(|c| c.to_document())
301            .collect())
302    }
303
304    async fn delete_parent_document(&self, parent_id: &str) -> Result<(), VectorStoreError> {
305        self.delete_document(parent_id).await
306    }
307
308    async fn parent_count(&self) -> usize {
309        let conn = self.conn.lock().await;
310        match conn.query_row("SELECT COUNT(*) FROM documents", [], |r| r.get(0)) {
311            Ok(count) => count,
312            // M3: do not silently swallow errors and return 0; log an error to surface storage failures.
313            Err(e) => {
314                log::error!("SQLite parent_count query failed, returning 0: {}", e);
315                0
316            }
317        }
318    }
319
320    async fn chunk_count(&self) -> usize {
321        let conn = self.conn.lock().await;
322        match conn.query_row("SELECT COUNT(*) FROM chunks", [], |r| r.get(0)) {
323            Ok(count) => count,
324            // M3: do not silently swallow errors and return 0; log an error to surface storage failures.
325            Err(e) => {
326                log::error!("SQLite chunk_count query failed, returning 0: {}", e);
327                0
328            }
329        }
330    }
331
332    async fn get_all_chunks(&self) -> Result<Vec<ChunkDocument>, VectorStoreError> {
333        let conn = self.conn.lock().await;
334        let mut stmt = conn
335            .prepare("SELECT chunk_id, parent_id, content, segment, metadata FROM chunks")
336            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
337        let chunks = stmt
338            .query_map([], |row| {
339                Ok(ChunkDocument {
340                    chunk_id: row.get(0)?,
341                    parent_id: row.get(1)?,
342                    content: row.get(2)?,
343                    segment: row.get(3)?,
344                    metadata: parse_metadata_or_default(&row.get::<_, String>(4)?),
345                })
346            })
347            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
348            .filter_map(|r| match r {
349                Ok(chunk) => Some(chunk),
350                Err(e) => {
351                    // stop silently dropping rows: log to surface storage degradation
352                    log::error!(
353                        "SQLite store: a row failed to deserialize and was dropped from the query result: {}",
354                        e
355                    );
356                    None
357                }
358            })
359            .collect();
360        Ok(chunks)
361    }
362
363    async fn clear(&self) -> Result<(), VectorStoreError> {
364        let conn = self.conn.lock().await;
365        conn.execute_batch("DELETE FROM chunks; DELETE FROM documents;")
366            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
367        Ok(())
368    }
369
370    // Blocking methods
371    fn add_parent_document_blocking(
372        &self,
373        document: Document,
374        chunk_size: usize,
375    ) -> Result<(String, Vec<String>), VectorStoreError> {
376        let conn = self.conn.blocking_lock();
377        let splitter = RecursiveCharacterSplitter::new(chunk_size, chunk_size / 10);
378        let chunks_text = splitter.split_text(&document.content);
379        let parent_id = document
380            .id
381            .clone()
382            .unwrap_or_else(|| Uuid::new_v4().to_string());
383        let meta = serde_json::to_string(&document.metadata).unwrap_or_else(|_| "{}".to_string());
384
385        conn.execute(
386            "INSERT OR REPLACE INTO documents (id, content, metadata) VALUES (?1, ?2, ?3)",
387            rusqlite::params![parent_id, document.content, meta],
388        )
389        .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
390
391        let mut chunk_ids = Vec::new();
392        for (i, text) in chunks_text.iter().enumerate() {
393            let cid = format!("{}:chunk:{}", parent_id, i);
394            conn.execute("INSERT OR REPLACE INTO chunks (chunk_id, parent_id, content, segment, metadata) VALUES (?1, ?2, ?3, ?4, ?5)",
395                rusqlite::params![cid, parent_id, text, i, "{}"])
396                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
397            chunk_ids.push(cid);
398        }
399        Ok((parent_id, chunk_ids))
400    }
401
402    fn get_parent_document_blocking(
403        &self,
404        parent_id: &str,
405    ) -> Result<Option<Document>, VectorStoreError> {
406        let conn = self.conn.blocking_lock();
407        let mut stmt = conn
408            .prepare("SELECT id, content, metadata FROM documents WHERE id = ?1")
409            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
410        let result = stmt.query_row(rusqlite::params![parent_id], |row| {
411            Ok(Document {
412                id: Some(row.get(0)?),
413                content: row.get(1)?,
414                metadata: parse_metadata_or_default(&row.get::<_, String>(2)?),
415            })
416        });
417        match result {
418            Ok(doc) => Ok(Some(doc)),
419            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
420            Err(e) => Err(VectorStoreError::StorageError(e.to_string())),
421        }
422    }
423
424    fn get_chunk_blocking(
425        &self,
426        chunk_id: &str,
427    ) -> Result<Option<ChunkDocument>, VectorStoreError> {
428        let conn = self.conn.blocking_lock();
429        let mut stmt = conn.prepare("SELECT chunk_id, parent_id, content, segment, metadata FROM chunks WHERE chunk_id = ?1")
430            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
431        let result = stmt.query_row(rusqlite::params![chunk_id], |row| {
432            Ok(ChunkDocument {
433                chunk_id: row.get(0)?,
434                parent_id: row.get(1)?,
435                content: row.get(2)?,
436                segment: row.get(3)?,
437                metadata: parse_metadata_or_default(&row.get::<_, String>(4)?),
438            })
439        });
440        match result {
441            Ok(chunk) => Ok(Some(chunk)),
442            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
443            Err(e) => Err(VectorStoreError::StorageError(e.to_string())),
444        }
445    }
446
447    fn blocking_get_chunks_for_parent(
448        &self,
449        parent_id: &str,
450    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
451        let conn = self.conn.blocking_lock();
452        let mut stmt = conn.prepare("SELECT chunk_id, parent_id, content, segment, metadata FROM chunks WHERE parent_id = ?1 ORDER BY segment")
453            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
454        let chunks = stmt
455            .query_map(rusqlite::params![parent_id], |row| {
456                Ok(ChunkDocument {
457                    chunk_id: row.get(0)?,
458                    parent_id: row.get(1)?,
459                    content: row.get(2)?,
460                    segment: row.get(3)?,
461                    metadata: parse_metadata_or_default(&row.get::<_, String>(4)?),
462                })
463            })
464            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
465            .filter_map(|r| match r {
466                Ok(chunk) => Some(chunk),
467                Err(e) => {
468                    // stop silently dropping rows: log to surface storage degradation
469                    log::error!(
470                        "SQLite store: a row failed to deserialize and was dropped from the query result: {}",
471                        e
472                    );
473                    None
474                }
475            })
476            .collect();
477        Ok(chunks)
478    }
479}