synd_persistence/sqlite/feed_registry/journal/
mod.rs1use chrono::{DateTime, Utc};
2use sqlx::{QueryBuilder, Sqlite, Transaction};
3use synd_registry::{
4 RegistryDbResult,
5 event::{
6 Event, EventCursor, EventCursorPos, EventEncoding, EventInterests, EventJournal,
7 EventJournalAppend, EventReadBatch, EventType, JournaledEvent, ProcessorId,
8 },
9};
10
11use super::{
12 SqliteRegistryTx,
13 error::{DecodeResultExt, IntoDbResult, SqliteError, SqliteResult},
14};
15
16async fn append_event(
17 tx: &mut Transaction<'_, Sqlite>,
18 event: Event,
19 occurred_at: DateTime<Utc>,
20) -> SqliteResult<EventType> {
21 let encoded = event.encode()?;
22 let event_type = encoded.event_type;
23 sqlx::query(
24 r"
25 INSERT INTO event_journal (occurred_at, payload_json)
26 VALUES (?, ?)
27 ",
28 )
29 .bind(occurred_at)
30 .bind(encoded.payload_json)
31 .execute(&mut **tx)
32 .await?;
33
34 Ok(event_type)
35}
36
37async fn read_after(
38 tx: &mut Transaction<'_, Sqlite>,
39 cursor: &EventCursor,
40 interests: EventInterests,
41) -> SqliteResult<EventReadBatch> {
42 let position = decode_event_cursor_position(cursor.position())?;
43 let processor = cursor.processor();
44 let event_types = interests
45 .types()
46 .iter()
47 .copied()
48 .map(<EventType as Into<&'static str>>::into)
49 .collect::<Vec<_>>();
50
51 let scanned = sqlx::query_as::<_, ScannedPositionRow>(
52 r"
53 SELECT COALESCE(MAX(position), ?) AS scanned_position
54 FROM event_journal
55 WHERE position > ?
56 ",
57 )
58 .bind(position)
59 .bind(position)
60 .fetch_one(&mut **tx)
61 .await?;
62
63 let scanned_cursor = EventCursor::at(
64 processor,
65 EventCursorPos::position(scanned.scanned_position.to_string()),
66 );
67
68 if event_types.is_empty() || scanned.scanned_position <= position {
69 return Ok(EventReadBatch::empty(scanned_cursor));
70 }
71
72 let mut query = QueryBuilder::<Sqlite>::new(
73 r"
74 SELECT event_type, occurred_at, payload_json
75 FROM event_journal
76 WHERE position > ",
77 );
78 query.push_bind(position);
79 query.push(" AND position <= ");
80 query.push_bind(scanned.scanned_position);
81 query.push(" AND event_type IN (");
82 let mut separated = query.separated(", ");
83 for event_type in event_types {
84 separated.push_bind(event_type);
85 }
86 separated.push_unseparated(") ORDER BY position");
87
88 let rows = query
89 .build_query_as::<JournalRow>()
90 .fetch_all(&mut **tx)
91 .await?;
92 let events = rows
93 .into_iter()
94 .map(JournalRow::into_journaled_event)
95 .collect::<SqliteResult<Vec<_>>>()?;
96
97 Ok(EventReadBatch::new(events, scanned_cursor))
98}
99
100async fn load_cursor(
101 tx: &mut Transaction<'_, Sqlite>,
102 processor: ProcessorId,
103) -> SqliteResult<EventCursor> {
104 let row = sqlx::query_as::<_, CursorRow>(
105 r"
106 SELECT position
107 FROM event_cursor
108 WHERE processor = ?
109 ",
110 )
111 .bind(processor.as_str())
112 .fetch_optional(&mut **tx)
113 .await?;
114
115 let Some(row) = row else {
116 return Ok(EventCursor::initial(processor));
117 };
118 Ok(EventCursor::at(
119 processor,
120 EventCursorPos::position(row.position.to_string()),
121 ))
122}
123
124async fn advance_cursor(
125 tx: &mut Transaction<'_, Sqlite>,
126 cursor: &EventCursor,
127) -> SqliteResult<()> {
128 let position = decode_event_cursor_position(cursor.position())?;
129 sqlx::query(
130 r"
131 INSERT INTO event_cursor (processor, position)
132 VALUES (?, ?)
133 ON CONFLICT(processor) DO UPDATE SET
134 position = CASE
135 WHEN excluded.position > event_cursor.position
136 THEN excluded.position
137 ELSE event_cursor.position
138 END
139 ",
140 )
141 .bind(cursor.processor().as_str())
142 .bind(position)
143 .execute(&mut **tx)
144 .await?;
145
146 Ok(())
147}
148
149#[derive(sqlx::FromRow)]
150struct ScannedPositionRow {
151 scanned_position: i64,
152}
153
154#[derive(sqlx::FromRow)]
155struct JournalRow {
156 event_type: String,
157 occurred_at: DateTime<Utc>,
158 payload_json: String,
159}
160
161impl JournalRow {
162 fn into_journaled_event(self) -> SqliteResult<JournaledEvent> {
163 Ok(JournaledEvent::new(
164 Event::decode(&self.event_type, &self.payload_json)?,
165 self.occurred_at,
166 ))
167 }
168}
169
170#[derive(sqlx::FromRow)]
171struct CursorRow {
172 position: i64,
173}
174
175fn decode_event_cursor_position(position: &EventCursorPos) -> SqliteResult<i64> {
176 match position {
177 EventCursorPos::Initial => Ok(0),
178 EventCursorPos::Position(position) => {
179 let position = position.parse::<i64>().decode()?;
180 if position < 0 {
181 return Err(SqliteError::decode_message(format!(
182 "event cursor position must be non-negative: {position}"
183 )));
184 }
185 Ok(position)
186 }
187 }
188}
189
190impl EventJournalAppend for SqliteRegistryTx<'_> {
191 async fn append_event(
192 &mut self,
193 event: Event,
194 occurred_at: DateTime<Utc>,
195 ) -> RegistryDbResult<EventType> {
196 append_event(&mut self.tx, event, occurred_at).await.db()
197 }
198}
199
200impl EventJournal for SqliteRegistryTx<'_> {
201 async fn read_after(
202 &mut self,
203 cursor: &EventCursor,
204 interests: EventInterests,
205 ) -> RegistryDbResult<EventReadBatch> {
206 read_after(&mut self.tx, cursor, interests).await.db()
207 }
208
209 async fn load_cursor(&mut self, processor: ProcessorId) -> RegistryDbResult<EventCursor> {
210 load_cursor(&mut self.tx, processor).await.db()
211 }
212
213 async fn advance_cursor(&mut self, cursor: &EventCursor) -> RegistryDbResult<()> {
214 advance_cursor(&mut self.tx, cursor).await.db()
215 }
216}
217
218#[cfg(test)]
219mod tests;