1use std::pin::Pin;
4use std::sync::Arc;
5
6use async_stream::stream;
7use async_trait::async_trait;
8use chrono::Utc;
9use dashmap::DashMap;
10use futures::stream::Stream;
11use serde_json::Value;
12use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
13use sqlx::Row;
14use tokio::sync::broadcast;
15use uuid::Uuid;
16
17use photon_backend::{
18 topic_filter_matches, PhotonError, Result, StorageCapabilities, StoragePort, TransportCrypto,
19};
20use photon_backend::models::Event;
21
22use crate::config::sqlite_path_from_env;
23
24fn partition_key(topic_name: &str, topic_key: Option<&str>) -> String {
25 format!("{}:{}", topic_name, topic_key.unwrap_or("__null__"))
26}
27
28fn checkpoint_key(sub: &str, topic: &str, topic_key: Option<&str>) -> String {
29 format!("{sub}:{}:{}", topic, topic_key.unwrap_or("__null__"))
30}
31
32fn map_sqlx(err: &sqlx::Error) -> PhotonError {
33 PhotonError::PersistenceError(err.to_string())
34}
35
36pub struct SqliteStoragePort {
43 pool: SqlitePool,
44 crypto: TransportCrypto,
45 tx: broadcast::Sender<Event>,
46 events: Arc<DashMap<String, Event>>,
47 delivery_pins: Arc<DashMap<String, i64>>,
48}
49
50impl SqliteStoragePort {
51 pub async fn open(path: &str) -> Result<Self> {
57 let options = SqliteConnectOptions::new()
58 .filename(path)
59 .create_if_missing(true);
60 let pool = SqlitePoolOptions::new()
61 .max_connections(5)
62 .connect_with(options)
63 .await
64 .map_err(|e| map_sqlx(&e))?;
65 Self::with_pool(pool).await
66 }
67
68 pub async fn from_env() -> Result<Self> {
74 Self::open(&sqlite_path_from_env()).await
75 }
76
77 async fn with_pool(pool: SqlitePool) -> Result<Self> {
78 Self::migrate(&pool).await?;
79 let (tx, _) = broadcast::channel(1024);
80 Ok(Self {
81 pool,
82 crypto: TransportCrypto::from_env()?,
83 tx,
84 events: Arc::new(DashMap::new()),
85 delivery_pins: Arc::new(DashMap::new()),
86 })
87 }
88
89 async fn migrate(pool: &SqlitePool) -> Result<()> {
90 sqlx::query(
91 "CREATE TABLE IF NOT EXISTS events (
92 event_id TEXT PRIMARY KEY,
93 topic_name TEXT NOT NULL,
94 topic_key TEXT,
95 seq INTEGER NOT NULL,
96 actor_json TEXT NOT NULL,
97 payload_json TEXT NOT NULL,
98 created_at TEXT NOT NULL
99 )",
100 )
101 .execute(pool)
102 .await
103 .map_err(|e| map_sqlx(&e))?;
104
105 sqlx::query(
106 "CREATE INDEX IF NOT EXISTS idx_events_topic_seq
107 ON events(topic_name, topic_key, seq)",
108 )
109 .execute(pool)
110 .await
111 .map_err(|e| map_sqlx(&e))?;
112
113 sqlx::query(
114 "CREATE TABLE IF NOT EXISTS checkpoints (
115 checkpoint_key TEXT PRIMARY KEY,
116 last_seq INTEGER NOT NULL
117 )",
118 )
119 .execute(pool)
120 .await
121 .map_err(|e| map_sqlx(&e))?;
122
123 sqlx::query(
124 "CREATE TABLE IF NOT EXISTS seq_counters (
125 partition_key TEXT PRIMARY KEY,
126 next_seq INTEGER NOT NULL
127 )",
128 )
129 .execute(pool)
130 .await
131 .map_err(|e| map_sqlx(&e))?;
132
133 Ok(())
134 }
135
136 async fn next_seq(&self, topic_name: &str, topic_key: Option<&str>) -> Result<i64> {
137 let pk = partition_key(topic_name, topic_key);
138 let row = sqlx::query(
139 "INSERT INTO seq_counters (partition_key, next_seq) VALUES (?, 1)
140 ON CONFLICT(partition_key) DO UPDATE SET next_seq = seq_counters.next_seq + 1
141 RETURNING next_seq",
142 )
143 .bind(&pk)
144 .fetch_one(&self.pool)
145 .await
146 .map_err(|e| map_sqlx(&e))?;
147 Ok(row.get::<i64, _>(0))
148 }
149
150 async fn load_replay_events(
151 pool: &SqlitePool,
152 topic_name: &str,
153 topic_key_filter: Option<&str>,
154 after_seq: i64,
155 ) -> Result<Vec<Event>> {
156 let rows = if let Some(key) = topic_key_filter {
157 sqlx::query(
158 "SELECT event_id, topic_name, topic_key, seq, actor_json, payload_json, created_at
159 FROM events
160 WHERE topic_name = ? AND topic_key = ? AND seq > ?
161 ORDER BY seq ASC",
162 )
163 .bind(topic_name)
164 .bind(key)
165 .bind(after_seq)
166 .fetch_all(pool)
167 .await
168 } else {
169 sqlx::query(
170 "SELECT event_id, topic_name, topic_key, seq, actor_json, payload_json, created_at
171 FROM events
172 WHERE topic_name = ? AND seq > ?
173 ORDER BY seq ASC",
174 )
175 .bind(topic_name)
176 .bind(after_seq)
177 .fetch_all(pool)
178 .await
179 }
180 .map_err(|e| map_sqlx(&e))?;
181
182 rows.iter().map(row_to_event).collect()
183 }
184
185 async fn fetch_event_by_id(&self, event_id: &str) -> Result<Option<Event>> {
186 let row = sqlx::query(
187 "SELECT event_id, topic_name, topic_key, seq, actor_json, payload_json, created_at
188 FROM events WHERE event_id = ?",
189 )
190 .bind(event_id)
191 .fetch_optional(&self.pool)
192 .await
193 .map_err(|e| map_sqlx(&e))?;
194 row.as_ref().map(row_to_event).transpose()
195 }
196}
197
198fn row_to_event(row: &sqlx::sqlite::SqliteRow) -> Result<Event> {
199 let created_raw: String = row.get("created_at");
200 let created_at = chrono::DateTime::parse_from_rfc3339(&created_raw)
201 .map_err(|e| PhotonError::PersistenceError(e.to_string()))?
202 .with_timezone(&Utc);
203 let actor_json: String = row.get("actor_json");
204 let payload_json: String = row.get("payload_json");
205 Ok(Event {
206 event_id: row.get("event_id"),
207 topic_name: row.get("topic_name"),
208 topic_key: row.get("topic_key"),
209 seq: row.get("seq"),
210 actor_json: serde_json::from_str(&actor_json)?,
211 payload_json: serde_json::from_str(&payload_json)?,
212 created_at,
213 })
214}
215
216#[async_trait]
217impl StoragePort for SqliteStoragePort {
218 fn capabilities(&self) -> StorageCapabilities {
219 StorageCapabilities::sqlite()
220 }
221
222 async fn append(
223 &self,
224 topic_name: &str,
225 topic_key: Option<&str>,
226 actor_json: Value,
227 payload_json: Value,
228 ) -> Result<Event> {
229 let _ = self.crypto.encrypt(&actor_json, &payload_json)?;
230 let seq = self.next_seq(topic_name, topic_key).await?;
231 let event = Event {
232 event_id: Uuid::new_v4().to_string(),
233 topic_name: topic_name.to_string(),
234 topic_key: topic_key.map(String::from),
235 seq,
236 actor_json,
237 payload_json,
238 created_at: Utc::now(),
239 };
240
241 sqlx::query(
242 "INSERT INTO events
243 (event_id, topic_name, topic_key, seq, actor_json, payload_json, created_at)
244 VALUES (?, ?, ?, ?, ?, ?, ?)",
245 )
246 .bind(&event.event_id)
247 .bind(&event.topic_name)
248 .bind(&event.topic_key)
249 .bind(event.seq)
250 .bind(event.actor_json.to_string())
251 .bind(event.payload_json.to_string())
252 .bind(event.created_at.to_rfc3339())
253 .execute(&self.pool)
254 .await
255 .map_err(|e| map_sqlx(&e))?;
256
257 self.events
258 .insert(event.event_id.clone(), event.clone());
259 let _ = self.tx.send(event.clone());
260 Ok(event)
261 }
262
263 fn subscribe(
264 &self,
265 topic_name: String,
266 topic_key_filter: Option<String>,
267 after_seq: Option<i64>,
268 ) -> Pin<Box<dyn Stream<Item = Result<Event>> + Send>> {
269 let pool = self.pool.clone();
270 let mut live_rx = self.tx.subscribe();
271 let topic = topic_name.clone();
272 let filter = topic_key_filter;
273 let delivery_pins = Arc::clone(&self.delivery_pins);
274
275 Box::pin(stream! {
276 if let Some(seq) = after_seq {
277 match Self::load_replay_events(&pool, &topic_name, filter.as_deref(), seq)
278 .await
279 {
280 Ok(events) => {
281 for evt in events {
282 if topic_filter_matches(&evt, &topic, filter.as_ref()) {
283 yield Ok(evt);
284 }
285 }
286 }
287 Err(e) => yield Err(e),
288 }
289 }
290
291 loop {
292 match live_rx.recv().await {
293 Ok(ev) => {
294 if !topic_filter_matches(&ev, &topic, filter.as_ref()) {
295 continue;
296 }
297 if after_seq.is_some_and(|s| ev.seq <= s) {
298 continue;
299 }
300 let pin_key = partition_key(&ev.topic_name, ev.topic_key.as_deref());
301 delivery_pins.insert(pin_key, ev.seq);
302 yield Ok(ev);
303 }
304 Err(broadcast::error::RecvError::Lagged(_)) => {
305 let pin_key = partition_key(&topic, filter.as_deref());
307 let after = delivery_pins
308 .get(&pin_key)
309 .map(|v| *v)
310 .or(after_seq)
311 .unwrap_or(0);
312 match Self::load_replay_events(
313 &pool,
314 &topic_name,
315 filter.as_deref(),
316 after,
317 )
318 .await
319 {
320 Ok(events) => {
321 for evt in events {
322 if topic_filter_matches(&evt, &topic, filter.as_ref()) {
323 let pk = partition_key(
324 &evt.topic_name,
325 evt.topic_key.as_deref(),
326 );
327 delivery_pins.insert(pk, evt.seq);
328 yield Ok(evt);
329 }
330 }
331 }
332 Err(e) => yield Err(e),
333 }
334 }
335 Err(broadcast::error::RecvError::Closed) => break,
336 }
337 }
338 })
339 }
340
341 async fn get_event(&self, event_id: &str) -> Result<Option<Event>> {
342 if let Some(ev) = self.events.get(event_id) {
343 return Ok(Some(ev.clone()));
344 }
345 self.fetch_event_by_id(event_id).await
346 }
347
348 async fn load_checkpoint(
349 &self,
350 subscription_name: &str,
351 topic_name: &str,
352 topic_key: Option<&str>,
353 ) -> Result<Option<i64>> {
354 let key = checkpoint_key(subscription_name, topic_name, topic_key);
355 let row = sqlx::query("SELECT last_seq FROM checkpoints WHERE checkpoint_key = ?")
356 .bind(&key)
357 .fetch_optional(&self.pool)
358 .await
359 .map_err(|e| map_sqlx(&e))?;
360 Ok(row.map(|r| r.get::<i64, _>(0)))
361 }
362
363 async fn commit_checkpoint(
364 &self,
365 subscription_name: &str,
366 topic_name: &str,
367 topic_key: Option<&str>,
368 last_seq: i64,
369 ) -> Result<()> {
370 let key = checkpoint_key(subscription_name, topic_name, topic_key);
371 sqlx::query(
372 "INSERT INTO checkpoints (checkpoint_key, last_seq) VALUES (?, ?)
373 ON CONFLICT(checkpoint_key) DO UPDATE SET last_seq = excluded.last_seq",
374 )
375 .bind(&key)
376 .bind(last_seq)
377 .execute(&self.pool)
378 .await
379 .map_err(|e| map_sqlx(&e))?;
380 Ok(())
381 }
382
383 async fn delivery_seq_pin(&self, topic_name: &str, topic_key: Option<&str>) -> Option<i64> {
384 let key = partition_key(topic_name, topic_key);
385 self.delivery_pins.get(&key).map(|v| *v)
386 }
387}