Skip to main content

synd_persistence/sqlite/feed_registry/journal/
mod.rs

1use 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;