Skip to main content

dovecote_sqlx_sqlite/
page.rs

1//! SQLite live and finite snapshot paging.
2
3use crate::{
4    begin_read, commit_transaction,
5    error::PageError,
6    hydrate::{DurableRow, hydrate_page},
7    install_foreign_keys,
8};
9use dovecote::{Limit, PagedEvent, RowId};
10use sqlx::{Sqlite, SqlitePool, Transaction, query_as, query_scalar};
11use std::marker::PhantomData;
12
13/// Reads one independent live page. Separate calls do not share a snapshot.
14pub async fn page(
15    pool: &SqlitePool,
16    after_row_id: Option<RowId>,
17    limit: Limit,
18) -> Result<Vec<PagedEvent>, PageError> {
19    let mut connection = pool
20        .acquire()
21        .await
22        .map_err(|source| PageError::sql("acquire live page connection", source))?;
23    install_foreign_keys(&mut connection)
24        .await
25        .map_err(|source| PageError::sql("enable live-page foreign keys", source))?;
26    read_page(
27        &mut *connection,
28        after_row_id.map_or(0, RowId::get),
29        None,
30        limit,
31    )
32    .await
33}
34
35/// Begins one finite read transaction and records its row-id ceiling only
36/// after the snapshot has been established.
37pub async fn begin_snapshot(pool: &SqlitePool) -> Result<SnapshotPager, PageError> {
38    let mut transaction = begin_read(pool)
39        .await
40        .map_err(|source| PageError::sql("begin snapshot transaction", source))?;
41    let upper_bound =
42        match query_scalar::<_, Option<i64>>("SELECT MAX(row_id) FROM dovecote_events")
43            .fetch_one(&mut *transaction)
44            .await
45        {
46            Ok(value) => value,
47            Err(source) => {
48                let _ = transaction.rollback().await;
49                return Err(PageError::sql("read snapshot upper row ID", source));
50            }
51        };
52    let upper_bound = match upper_bound
53        .map(|value| RowId::new(value).map_err(|error| PageError::serialization(error.to_string())))
54        .transpose()
55    {
56        Ok(value) => value,
57        Err(error) => {
58            let _ = transaction.rollback().await;
59            return Err(error);
60        }
61    };
62    Ok(SnapshotPager {
63        transaction: Some(transaction),
64        upper_bound,
65        cursor: None,
66        exhausted: upper_bound.is_none(),
67        _not_send: PhantomData,
68    })
69}
70
71/// A finite pager retaining one SQLite read transaction. The explicit marker
72/// makes accidental movement across unrelated executors a compile-time error.
73///
74/// ```compile_fail
75/// use dovecote_sqlx_sqlite::SnapshotPager;
76///
77/// fn requires_send<T: Send>() {}
78///
79/// fn main() {
80///     requires_send::<SnapshotPager>();
81/// }
82/// ```
83pub struct SnapshotPager {
84    transaction: Option<Transaction<'static, Sqlite>>,
85    upper_bound: Option<RowId>,
86    cursor: Option<RowId>,
87    exhausted: bool,
88    _not_send: PhantomData<*mut ()>,
89}
90
91impl SnapshotPager {
92    pub const fn cursor(&self) -> Option<RowId> {
93        self.cursor
94    }
95    pub const fn upper_bound(&self) -> Option<RowId> {
96        self.upper_bound
97    }
98    pub const fn is_exhausted(&self) -> bool {
99        self.exhausted
100    }
101
102    pub async fn next_page(&mut self, limit: Limit) -> Result<Vec<PagedEvent>, PageError> {
103        if self.exhausted {
104            return Ok(Vec::new());
105        }
106
107        let transaction = self.transaction.as_mut().ok_or(PageError::Closed)?;
108        let upper = self
109            .upper_bound
110            .expect("non-exhausted pager has an upper bound");
111        let result = read_page(
112            &mut **transaction,
113            self.cursor.map_or(0, RowId::get),
114            Some(upper.get()),
115            limit,
116        )
117        .await;
118        let rows = match result {
119            Ok(rows) => rows,
120            Err(error) => {
121                if let Some(transaction) = self.transaction.take() {
122                    let _ = transaction.rollback().await;
123                }
124
125                return Err(error);
126            }
127        };
128        if let Some(last) = rows.last() {
129            self.cursor = Some(last.row_id());
130
131            if rows.len() < limit.get() as usize || self.cursor == self.upper_bound {
132                self.exhausted = true;
133            }
134        } else {
135            self.exhausted = true;
136        }
137
138        Ok(rows)
139    }
140
141    pub async fn finish(mut self) -> Result<(), PageError> {
142        let Some(transaction) = self.transaction.take() else {
143            return Ok(());
144        };
145        commit_transaction(transaction)
146            .await
147            .map_err(|source| PageError::sql("finish snapshot transaction", source))
148    }
149    pub async fn rollback(mut self) -> Result<(), PageError> {
150        let Some(transaction) = self.transaction.take() else {
151            return Ok(());
152        };
153        transaction
154            .rollback()
155            .await
156            .map_err(|source| PageError::sql("rollback snapshot transaction", source))
157    }
158    pub async fn close(self) -> Result<(), PageError> {
159        self.rollback().await
160    }
161}
162
163async fn read_page<'c, E>(
164    executor: E,
165    after_row_id: i64,
166    upper_bound: Option<i64>,
167    limit: Limit,
168) -> Result<Vec<PagedEvent>, PageError>
169where
170    E: sqlx::Executor<'c, Database = Sqlite>,
171{
172    let rows = query_as::<_, DurableRow>(PAGE_SQL)
173        .bind(after_row_id)
174        .bind(upper_bound)
175        .bind(upper_bound)
176        .bind(i64::from(limit.get()))
177        .fetch_all(executor)
178        .await
179        .map_err(|source| PageError::sql("read event page", source))?;
180    rows.into_iter()
181        .map(hydrate_page)
182        .collect::<Result<Vec<_>, _>>()
183        .map_err(PageError::serialization)
184}
185
186const PAGE_SQL: &str = "SELECT e.row_id, e.stream, e.specversion, e.event_id, e.source, e.event_type, e.subject, e.occurred_at, e.enqueued_at, e.datacontenttype, e.dataschema, e.partitionkey, e.extensions, e.data_kind, e.data, d.state, d.available_at, d.attempts, d.claim_token, d.claimed_by, d.claim_expires_at, d.last_failure_code, d.last_failure_detail, d.delivered_at, d.quarantined_at, d.quarantine_reason FROM dovecote_events AS e LEFT JOIN dovecote_deliveries AS d ON d.event_row_id = e.row_id WHERE e.row_id > ? AND (? IS NULL OR e.row_id <= ?) ORDER BY e.row_id ASC LIMIT ?";