1use std::path::Path;
2use std::sync::Arc;
3use std::time::Duration;
4
5use async_trait::async_trait;
6
7use crate::entry::ContextEntry;
8use crate::storage::fts_index::FtsIndex;
9use crate::traits::ContextStorage;
10
11const TURSO_SCHEMA: &str = r"
12CREATE TABLE IF NOT EXISTS entries (
13 id TEXT PRIMARY KEY,
14 content TEXT NOT NULL,
15 timestamp INTEGER NOT NULL,
16 kind TEXT NOT NULL,
17 scope TEXT,
18 session_id TEXT,
19 token_count INTEGER CHECK(token_count >= 0),
20 metadata TEXT,
21 created_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', 'now') AS INTEGER))
22) STRICT;
23
24CREATE INDEX IF NOT EXISTS idx_entries_timestamp ON entries(timestamp);
25CREATE INDEX IF NOT EXISTS idx_entries_scope ON entries(scope);
26CREATE INDEX IF NOT EXISTS idx_entries_session_id ON entries(session_id);
27";
28
29pub struct TursoStorage {
31 pub(crate) db: Arc<turso::Database>,
32 pub(crate) fts: Arc<FtsIndex>,
33 max_entries: usize,
34}
35
36impl TursoStorage {
37 pub async fn open(db_path: &Path, max_entries: usize) -> crate::Result<Self> {
46 let path = db_path
47 .to_str()
48 .ok_or_else(|| crate::Error::Migration("non-UTF-8 database path".into()))?;
49 let db = turso::Builder::new_local(path)
50 .experimental_index_method(true)
51 .build()
52 .await?;
53 let conn = db.connect()?;
54 conn.busy_timeout(Duration::from_secs(5))?;
55 turso_migrate(&conn).await?;
56
57 let fts = FtsIndex::new()?;
59 let mut rows = conn.query("SELECT id, content FROM entries", ()).await?;
60 while let Some(row) = rows.next().await? {
61 let turso::Value::Text(id) = row.get_value(0)? else {
62 continue;
63 };
64 let turso::Value::Text(content) = row.get_value(1)? else {
65 continue;
66 };
67 fts.add(&id, &content)?;
68 }
69 fts.commit()?;
70
71 Ok(Self {
72 db: Arc::new(db),
73 fts,
74 max_entries,
75 })
76 }
77}
78
79pub(crate) async fn turso_migrate(conn: &turso::Connection) -> crate::Result<()> {
80 conn.execute_batch(TURSO_SCHEMA).await?;
81 conn.execute(
82 "CREATE INDEX IF NOT EXISTS idx_turso_fts ON entries USING fts (content)",
83 (),
84 )
85 .await?;
86 Ok(())
87}
88
89pub(crate) fn turso_row_to_entry(row: &turso::Row) -> crate::Result<ContextEntry> {
90 let id = get_text(row, 0, "id")?;
91 let content = get_text(row, 1, "content")?;
92 let timestamp = get_int(row, 2, "timestamp")?;
93 let kind = get_text(row, 3, "kind")?;
94 let scope = get_text_opt(row, 4, "scope")?;
95 let session_id = get_text_opt(row, 5, "session_id")?;
96 let token_count_raw = get_int_opt(row, 6, "token_count")?;
97 let metadata_str = get_text_opt(row, 7, "metadata")?;
98
99 let token_count = token_count_raw
100 .map(usize::try_from)
101 .transpose()
102 .map_err(|e| crate::Error::Migration(e.to_string()))?;
103
104 let metadata = metadata_str
105 .map(|s| serde_json::from_str::<serde_json::Value>(&s))
106 .transpose()
107 .map_err(|e| crate::Error::Migration(e.to_string()))?
108 .and_then(|v| {
110 if v.as_object().is_some_and(serde_json::Map::is_empty) {
111 None
112 } else {
113 Some(v)
114 }
115 });
116
117 Ok(ContextEntry {
118 id,
119 content,
120 timestamp,
121 kind,
122 scope,
123 session_id,
124 token_count,
125 metadata,
126 })
127}
128
129fn get_text(row: &turso::Row, idx: usize, field: &str) -> crate::Result<String> {
130 match row.get_value(idx)? {
131 turso::Value::Text(s) => Ok(s),
132 other => Err(crate::Error::Migration(format!(
133 "{field}: expected text, got {other:?}"
134 ))),
135 }
136}
137
138fn get_text_opt(row: &turso::Row, idx: usize, field: &str) -> crate::Result<Option<String>> {
139 match row.get_value(idx)? {
140 turso::Value::Text(s) => Ok(Some(s)),
141 turso::Value::Null => Ok(None),
142 other => Err(crate::Error::Migration(format!(
143 "{field}: expected text or null, got {other:?}"
144 ))),
145 }
146}
147
148fn get_int(row: &turso::Row, idx: usize, field: &str) -> crate::Result<i64> {
149 match row.get_value(idx)? {
150 turso::Value::Integer(i) => Ok(i),
151 other => Err(crate::Error::Migration(format!(
152 "{field}: expected integer, got {other:?}"
153 ))),
154 }
155}
156
157fn get_int_opt(row: &turso::Row, idx: usize, field: &str) -> crate::Result<Option<i64>> {
158 match row.get_value(idx)? {
159 turso::Value::Integer(i) => Ok(Some(i)),
160 turso::Value::Null => Ok(None),
161 other => Err(crate::Error::Migration(format!(
162 "{field}: expected integer or null, got {other:?}"
163 ))),
164 }
165}
166
167#[async_trait]
168impl ContextStorage for TursoStorage {
169 async fn save(&self, entry: &ContextEntry) -> crate::Result<()> {
170 let conn = self.db.connect()?;
171 conn.busy_timeout(Duration::from_secs(5))?;
172 let max_entries = self.max_entries;
173
174 let metadata_json = entry
175 .metadata
176 .as_ref()
177 .map(serde_json::to_string)
178 .transpose()
179 .map_err(|e| crate::Error::InvalidEntry(format!("metadata is not valid JSON: {e}")))?;
180
181 let result = async {
182 conn.execute("BEGIN IMMEDIATE", ()).await?;
183
184 let mut rows = conn
185 .query(
186 "SELECT EXISTS(SELECT 1 FROM entries WHERE id = ?1)",
187 (entry.id.clone(),),
188 )
189 .await?;
190 let exists: bool = match rows.next().await? {
191 Some(row) => matches!(row.get_value(0)?, turso::Value::Integer(1)),
192 None => false,
193 };
194
195 if !exists {
196 let mut count_rows = conn.query("SELECT COUNT(*) FROM entries", ()).await?;
197 let count: i64 = match count_rows.next().await? {
198 Some(row) => match row.get_value(0)? {
199 turso::Value::Integer(n) => n,
200 _ => 0,
201 },
202 None => 0,
203 };
204
205 if count >= i64::try_from(max_entries).unwrap_or(i64::MAX) {
206 conn.execute(
207 "DELETE FROM entries WHERE id = \
208 (SELECT id FROM entries ORDER BY timestamp ASC LIMIT 1)",
209 (),
210 )
211 .await?;
212 }
213 }
214
215 conn.execute(
216 "INSERT OR REPLACE INTO entries \
217 (id, content, timestamp, kind, scope, session_id, token_count, metadata) \
218 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
219 (
220 entry.id.clone(),
221 entry.content.clone(),
222 entry.timestamp,
223 entry.kind.clone(),
224 entry.scope.clone(),
225 entry.session_id.clone(),
226 entry
227 .token_count
228 .map(|n| i64::try_from(n).unwrap_or(i64::MAX)),
229 metadata_json,
230 ),
231 )
232 .await?;
233
234 conn.execute("COMMIT", ()).await?;
235 Ok(())
236 }
237 .await;
238
239 if result.is_err() {
240 let _ = conn.execute("ROLLBACK", ()).await;
241 return result;
242 }
243
244 self.fts.add(&entry.id, &entry.content)?;
247 self.fts.commit()?;
248
249 result
250 }
251
252 async fn get_top_k(&self, k: usize) -> crate::Result<Vec<ContextEntry>> {
253 let conn = self.db.connect()?;
254 conn.busy_timeout(Duration::from_secs(5))?;
255
256 let mut rows = conn
257 .query(
258 "SELECT id, content, timestamp, kind, scope, session_id, token_count, metadata \
259 FROM entries ORDER BY timestamp DESC LIMIT ?1",
260 (i64::try_from(k).unwrap_or(i64::MAX),),
261 )
262 .await?;
263
264 let mut result = Vec::new();
265 while let Some(row) = rows.next().await? {
266 result.push(turso_row_to_entry(&row)?);
267 }
268 Ok(result)
269 }
270
271 async fn get_all(&self) -> crate::Result<Vec<ContextEntry>> {
272 let conn = self.db.connect()?;
273 conn.busy_timeout(Duration::from_secs(5))?;
274
275 let mut rows = conn
276 .query(
277 "SELECT id, content, timestamp, kind, scope, session_id, token_count, metadata \
278 FROM entries ORDER BY timestamp DESC",
279 (),
280 )
281 .await?;
282
283 let mut result = Vec::new();
284 while let Some(row) = rows.next().await? {
285 result.push(turso_row_to_entry(&row)?);
286 }
287 Ok(result)
288 }
289
290 async fn delete(&self, id: &str) -> crate::Result<bool> {
291 let conn = self.db.connect()?;
292 conn.busy_timeout(Duration::from_secs(5))?;
293
294 let affected = conn
295 .execute("DELETE FROM entries WHERE id = ?1", (id.to_owned(),))
296 .await?;
297
298 if affected > 0 {
299 self.fts.remove(id)?;
300 }
301 Ok(affected > 0)
302 }
303
304 async fn clear(&self) -> crate::Result<usize> {
305 let conn = self.db.connect()?;
306 conn.busy_timeout(Duration::from_secs(5))?;
307
308 let affected = conn.execute("DELETE FROM entries", ()).await?;
309 self.fts.clear()?;
310 Ok(usize::try_from(affected).unwrap_or(usize::MAX))
311 }
312
313 async fn clear_scope(&self, scope: &str) -> crate::Result<usize> {
314 let conn = self.db.connect()?;
315 conn.busy_timeout(Duration::from_secs(5))?;
316
317 let mut id_rows = conn
319 .query(
320 "SELECT id FROM entries WHERE scope = ?1",
321 (scope.to_owned(),),
322 )
323 .await?;
324 let mut ids: Vec<String> = Vec::new();
325 while let Some(row) = id_rows.next().await? {
326 if let turso::Value::Text(id) = row.get_value(0)? {
327 ids.push(id);
328 }
329 }
330
331 let affected = conn
332 .execute("DELETE FROM entries WHERE scope = ?1", (scope.to_owned(),))
333 .await?;
334
335 for id in &ids {
336 self.fts.remove(id)?;
337 }
338 Ok(usize::try_from(affected).unwrap_or(usize::MAX))
339 }
340
341 async fn count(&self) -> crate::Result<usize> {
342 let conn = self.db.connect()?;
343 conn.busy_timeout(Duration::from_secs(5))?;
344
345 let mut rows = conn.query("SELECT COUNT(*) FROM entries", ()).await?;
346 match rows.next().await? {
347 Some(row) => match row.get_value(0)? {
348 turso::Value::Integer(n) => Ok(usize::try_from(n).unwrap_or(0)),
349 _ => Ok(0),
350 },
351 None => Ok(0),
352 }
353 }
354}