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