vv_agent/runtime/stores/
sqlite.rs1use std::io::{Error, Result};
2use std::path::Path;
3use std::sync::Mutex;
4
5use rusqlite::{params, Connection, OptionalExtension};
6
7use crate::runtime::checkpoint_codec::{
8 cycles_from_json, cycles_to_json, messages_from_json, messages_to_json,
9};
10use crate::runtime::state::{
11 checkpoint_status_from_value, checkpoint_status_value, Checkpoint, StateStore,
12};
13
14#[derive(Debug)]
15pub struct SqliteStateStore {
16 connection: Mutex<Connection>,
17}
18
19impl SqliteStateStore {
20 pub fn new(db_path: impl AsRef<Path>) -> Result<Self> {
21 let db_path = db_path.as_ref().to_string_lossy().to_string();
22 let connection = Connection::open(db_path).map_err(sqlite_to_io)?;
23 connection
24 .execute_batch(
25 r#"
26 PRAGMA journal_mode=WAL;
27 CREATE TABLE IF NOT EXISTS checkpoints (
28 task_id TEXT PRIMARY KEY,
29 cycle_index INTEGER NOT NULL,
30 status TEXT NOT NULL,
31 messages TEXT NOT NULL,
32 cycles TEXT NOT NULL,
33 shared_state TEXT NOT NULL
34 );
35 "#,
36 )
37 .map_err(sqlite_to_io)?;
38 Ok(Self {
39 connection: Mutex::new(connection),
40 })
41 }
42
43 pub fn close(self) -> Result<()> {
44 let connection = self
45 .connection
46 .into_inner()
47 .map_err(|_| Error::other("sqlite state store lock is poisoned"))?;
48 connection.close().map_err(|(_, error)| sqlite_to_io(error))
49 }
50}
51
52impl StateStore for SqliteStateStore {
53 fn save_checkpoint(&self, checkpoint: Checkpoint) -> Result<()> {
54 let messages_json = messages_to_json(&checkpoint.messages)?;
55 let cycles_json = cycles_to_json(&checkpoint.cycles)?;
56 let shared_state_json = serde_json::to_string(&checkpoint.shared_state)
57 .map_err(|error| Error::other(error.to_string()))?;
58 self.connection
59 .lock()
60 .map_err(|_| Error::other("sqlite state store lock is poisoned"))?
61 .execute(
62 r#"
63 INSERT OR REPLACE INTO checkpoints
64 (task_id, cycle_index, status, messages, cycles, shared_state)
65 VALUES (?1, ?2, ?3, ?4, ?5, ?6)
66 "#,
67 params![
68 checkpoint.task_id,
69 checkpoint.cycle_index,
70 checkpoint_status_value(checkpoint.status),
71 messages_json,
72 cycles_json,
73 shared_state_json,
74 ],
75 )
76 .map_err(sqlite_to_io)?;
77 Ok(())
78 }
79
80 fn load_checkpoint(&self, task_id: &str) -> Result<Option<Checkpoint>> {
81 let connection = self
82 .connection
83 .lock()
84 .map_err(|_| Error::other("sqlite state store lock is poisoned"))?;
85 let row = connection
86 .query_row(
87 "SELECT task_id, cycle_index, status, messages, cycles, shared_state FROM checkpoints WHERE task_id = ?1",
88 params![task_id],
89 |row| {
90 Ok((
91 row.get::<_, String>(0)?,
92 row.get::<_, u32>(1)?,
93 row.get::<_, String>(2)?,
94 row.get::<_, String>(3)?,
95 row.get::<_, String>(4)?,
96 row.get::<_, String>(5)?,
97 ))
98 },
99 )
100 .optional()
101 .map_err(sqlite_to_io)?;
102 let Some((task_id, cycle_index, status, messages, cycles, shared_state)) = row else {
103 return Ok(None);
104 };
105 Ok(Some(Checkpoint {
106 task_id,
107 cycle_index,
108 status: checkpoint_status_from_value(&status)?,
109 messages: messages_from_json(&messages)?,
110 cycles: cycles_from_json(&cycles)?,
111 shared_state: serde_json::from_str(&shared_state)
112 .map_err(|error| Error::other(error.to_string()))?,
113 }))
114 }
115
116 fn delete_checkpoint(&self, task_id: &str) -> Result<()> {
117 self.connection
118 .lock()
119 .map_err(|_| Error::other("sqlite state store lock is poisoned"))?
120 .execute(
121 "DELETE FROM checkpoints WHERE task_id = ?1",
122 params![task_id],
123 )
124 .map_err(sqlite_to_io)?;
125 Ok(())
126 }
127
128 fn list_checkpoints(&self) -> Result<Vec<String>> {
129 let connection = self
130 .connection
131 .lock()
132 .map_err(|_| Error::other("sqlite state store lock is poisoned"))?;
133 let mut statement = connection
134 .prepare("SELECT task_id FROM checkpoints ORDER BY task_id")
135 .map_err(sqlite_to_io)?;
136 let rows = statement
137 .query_map([], |row| row.get::<_, String>(0))
138 .map_err(sqlite_to_io)?;
139 rows.collect::<rusqlite::Result<Vec<_>>>()
140 .map_err(sqlite_to_io)
141 }
142}
143
144fn sqlite_to_io(error: rusqlite::Error) -> Error {
145 Error::other(error.to_string())
146}