1use std::path::PathBuf;
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use rusqlite::{params, TransactionBehavior};
12use r2d2::Pool;
13use r2d2_sqlite::SqliteConnectionManager;
14use crate::api::{CommitMode, EventStore, PersistedEvent, Snapshot};
15
16pub struct SqliteEventStore {
18 _db_path: PathBuf,
19 pool: Pool<SqliteConnectionManager>,
20 commit_mode: CommitMode,
21}
22
23impl SqliteEventStore {
24 #[cfg_attr(feature = "dev-tracing", tracing::instrument(skip(db_path), fields(
26 crate_name = "persistence",
27 commit_mode = ?commit_mode
28 )))]
29 pub fn open(
30 db_path: impl Into<PathBuf>,
31 commit_mode: CommitMode,
32 ) -> anyhow::Result<Arc<Self>> {
33 let db_path = db_path.into();
34 if let Some(parent) = db_path.parent() {
36 if !parent.as_os_str().is_empty() {
37 std::fs::create_dir_all(parent)?;
38 }
39 }
40 let manager = SqliteConnectionManager::file(&db_path);
41 let pool = Pool::new(manager)?;
42 pool.get()?.execute_batch(
43 r#"
44 CREATE TABLE IF NOT EXISTS events (
45 lsn INTEGER PRIMARY KEY AUTOINCREMENT,
46 tr_id TEXT NOT NULL,
47 doc_id TEXT NOT NULL,
48 ts INTEGER NOT NULL,
49 actor TEXT,
50 idempotency_key TEXT NOT NULL UNIQUE,
51 meta TEXT NOT NULL,
52 payload BLOB NOT NULL,
53 checksum INTEGER NOT NULL
54 );
55 CREATE INDEX IF NOT EXISTS ix_events_doc_lsn ON events(doc_id, lsn);
56
57 CREATE TABLE IF NOT EXISTS snapshots (
58 doc_id TEXT NOT NULL,
59 upto_lsn INTEGER NOT NULL,
60 created_at INTEGER NOT NULL,
61 state_blob BLOB NOT NULL,
62 version INTEGER NOT NULL,
63 PRIMARY KEY (doc_id, upto_lsn)
64 );
65 CREATE INDEX IF NOT EXISTS ix_snapshots_doc_created ON snapshots(doc_id, created_at DESC);
66 "#,
67 )?;
68
69 Ok(Arc::new(Self { _db_path: db_path, pool, commit_mode }))
70 }
71}
72
73#[async_trait]
74impl EventStore for SqliteEventStore {
75 #[cfg_attr(feature = "dev-tracing", tracing::instrument(skip(self, ev), fields(
77 crate_name = "persistence",
78 tr_id = %ev.tr_id,
79 doc_id = %ev.doc_id
80 )))]
81 async fn append(
82 &self,
83 ev: PersistedEvent,
84 ) -> anyhow::Result<i64> {
85 let mut conn = self.pool.get()?;
86 let tx =
87 conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
88 tx.execute(
89 "INSERT INTO events (tr_id, doc_id, ts, actor, idempotency_key, meta, payload, checksum) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
90 params![
91 ev.tr_id.to_string(), ev.doc_id,
93 { ev.ts },
94 ev.actor,
95 ev.idempotency_key,
96 ev.meta.to_string(),
97 ev.payload,
98 ev.checksum as i64
99 ],
100 )?;
101 tx.commit()?;
102 let lsn = conn.last_insert_rowid();
103 if let CommitMode::SyncDurable = self.commit_mode {
104 conn.pragma_update(None, "wal_checkpoint", "TRUNCATE").ok();
105 }
106 Ok(lsn)
107 }
108
109 #[cfg_attr(feature = "dev-tracing", tracing::instrument(skip(self, evs), fields(
111 crate_name = "persistence",
112 batch_size = evs.len()
113 )))]
114 async fn append_batch(
115 &self,
116 evs: Vec<PersistedEvent>,
117 ) -> anyhow::Result<i64> {
118 if evs.is_empty() {
119 return Ok(0);
120 }
121 let mut conn = self.pool.get()?;
122 let tx =
123 conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
124 {
125 let mut stmt = tx.prepare("INSERT INTO events (tr_id, doc_id, ts, actor, idempotency_key, meta, payload, checksum) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)")?;
126 for ev in evs.into_iter() {
127 stmt.execute(params![
128 ev.tr_id.to_string(), ev.doc_id,
130 { ev.ts },
131 ev.actor,
132 ev.idempotency_key,
133 ev.meta.to_string(),
134 ev.payload,
135 ev.checksum as i64
136 ])?;
137 }
138 }
139 tx.commit()?;
140 let lsn = conn.last_insert_rowid();
141 Ok(lsn)
142 }
143
144 #[cfg_attr(feature = "dev-tracing", tracing::instrument(skip(self), fields(
146 crate_name = "persistence",
147 doc_id = %doc_id,
148 from_lsn = from_lsn,
149 limit = limit
150 )))]
151 async fn load_since(
152 &self,
153 doc_id: &str,
154 from_lsn: i64,
155 limit: u32,
156 ) -> anyhow::Result<Vec<PersistedEvent>> {
157 let conn = self.pool.get()?;
158 let mut stmt = conn.prepare("SELECT lsn, tr_id, doc_id, ts, actor, idempotency_key, meta, payload, checksum FROM events WHERE doc_id = ?1 AND lsn > ?2 ORDER BY lsn ASC LIMIT ?3")?;
159 let rows =
160 stmt.query_map(params![doc_id, from_lsn, limit as i64], |row| {
161 let meta_str: String = row.get(6)?;
162 let meta: serde_json::Value = serde_json::from_str(&meta_str)
163 .unwrap_or(serde_json::json!({}));
164 let tr_id_str: String = row.get(1)?;
165 let tr_id = uuid::Uuid::parse_str(&tr_id_str).map_err(|e| {
166 rusqlite::Error::FromSqlConversionFailure(
167 1,
168 rusqlite::types::Type::Text,
169 Box::new(e),
170 )
171 })?;
172 Ok(PersistedEvent {
173 lsn: row.get(0)?,
174 tr_id, doc_id: row.get(2)?,
176 ts: row.get(3)?,
177 actor: row.get(4).ok(),
178 idempotency_key: row.get(5)?,
179 meta,
180 payload: row.get(7)?,
181 checksum: row.get::<_, i64>(8)? as u32,
182 })
183 })?;
184 Ok(rows.filter_map(|r| r.ok()).collect())
185 }
186
187 #[cfg_attr(feature = "dev-tracing", tracing::instrument(skip(self), fields(
189 crate_name = "persistence",
190 doc_id = %doc_id
191 )))]
192 async fn latest_snapshot(
193 &self,
194 doc_id: &str,
195 ) -> anyhow::Result<Option<Snapshot>> {
196 let conn = self.pool.get()?;
197 let mut stmt = conn.prepare("SELECT doc_id, upto_lsn, created_at, state_blob, version FROM snapshots WHERE doc_id = ?1 ORDER BY created_at DESC LIMIT 1")?;
198 let mut rows = stmt.query(params![doc_id])?;
199 if let Some(row) = rows.next()? {
200 Ok(Some(Snapshot {
201 doc_id: row.get(0)?,
202 upto_lsn: row.get(1)?,
203 created_at: row.get(2)?,
204 state_blob: row.get(3)?,
205 version: row.get(4)?,
206 }))
207 } else {
208 Ok(None)
209 }
210 }
211
212 #[cfg_attr(feature = "dev-tracing", tracing::instrument(skip(self, snap), fields(
214 crate_name = "persistence",
215 doc_id = %snap.doc_id,
216 upto_lsn = snap.upto_lsn
217 )))]
218 async fn write_snapshot(
219 &self,
220 snap: Snapshot,
221 ) -> anyhow::Result<()> {
222 let mut conn = self.pool.get()?;
223 let tx =
224 conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
225 tx.execute(
226 "INSERT OR REPLACE INTO snapshots (doc_id, upto_lsn, created_at, state_blob, version) VALUES (?1, ?2, ?3, ?4, ?5)",
227 params![snap.doc_id, snap.upto_lsn, snap.created_at, snap.state_blob, snap.version],
228 )?;
229 tx.commit()?;
230 Ok(())
231 }
232
233 async fn compact(
235 &self,
236 doc_id: &str,
237 ) -> anyhow::Result<()> {
238 let conn = self.pool.get()?;
239 let upto: Option<i64> = conn.query_row(
241 "SELECT upto_lsn FROM snapshots WHERE doc_id = ?1 ORDER BY upto_lsn DESC LIMIT 1",
242 params![doc_id],
243 |r| r.get(0),
244 ).ok();
245 if let Some(upto_lsn) = upto {
246 conn.execute(
247 "DELETE FROM events WHERE doc_id = ?1 AND lsn <= ?2",
248 params![doc_id, upto_lsn],
249 )?;
250 }
251 Ok(())
252 }
253}