Skip to main content

eventuary_sqlite/
reader.rs

1use std::collections::{HashMap, VecDeque};
2use std::sync::Arc;
3use std::time::Duration;
4
5use chrono::{DateTime, Utc};
6use rusqlite::types::Value;
7use tokio::sync::Mutex;
8use tokio::sync::Notify;
9use tokio::sync::mpsc;
10
11use eventuary_core::io::cursor::{CursorOrder, JsonCursorCodec};
12use eventuary_core::io::filter::{EventFilter, NamespacePattern, TopicPattern};
13use eventuary_core::io::stream::SpawnedStream;
14use eventuary_core::io::{Acker, Cursor, Filter, Message, Reader};
15use eventuary_core::partition::{PartitionGroup, PartitionSelection};
16use eventuary_core::{
17    Error, Partition, PartitionableSubscription, Result, SerializedEvent, SerializedPayload,
18    StartFrom, StartableSubscription, StopAt,
19};
20
21use crate::database::SqliteConn;
22use crate::event_log::{SqliteEventLogSchema, SqliteEventLogSchemaConfig};
23use crate::relation::SqliteRelationName;
24
25#[derive(
26    Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Serialize, serde::Deserialize,
27)]
28#[serde(transparent)]
29pub struct SqliteCursor {
30    pub sequence: i64,
31}
32
33impl SqliteCursor {
34    pub fn new(sequence: i64) -> Self {
35        Self { sequence }
36    }
37
38    pub fn sequence(&self) -> i64 {
39        self.sequence
40    }
41}
42
43impl Cursor for SqliteCursor {
44    fn order_key(&self) -> CursorOrder {
45        CursorOrder::from_i64(self.sequence)
46    }
47}
48
49impl SqliteCursor {
50    pub fn codec() -> Result<JsonCursorCodec<Self>> {
51        JsonCursorCodec::new("eventuary.sqlite.sqlite_cursor.v1")
52    }
53}
54
55#[derive(Debug, Clone)]
56pub struct SqliteSubscription {
57    pub start: StartFrom<SqliteCursor>,
58    pub stop_at: StopAt<SqliteCursor>,
59    pub filter: EventFilter,
60    pub partitions: PartitionSelection,
61    pub batch_size: Option<usize>,
62    pub limit: Option<usize>,
63}
64
65impl Default for SqliteSubscription {
66    fn default() -> Self {
67        Self {
68            start: StartFrom::Latest,
69            stop_at: StopAt::Never,
70            filter: EventFilter::default(),
71            batch_size: None,
72            limit: None,
73            partitions: PartitionSelection::default(),
74        }
75    }
76}
77
78impl StartableSubscription<SqliteCursor> for SqliteSubscription {
79    fn with_start(mut self, start: StartFrom<SqliteCursor>) -> Self {
80        self.start = start;
81        self
82    }
83}
84
85impl PartitionableSubscription<SqliteCursor> for SqliteSubscription {
86    fn with_partition(mut self, partition: Partition) -> Self {
87        self.partitions = PartitionSelection::One(partition);
88        self
89    }
90}
91
92impl SqliteSubscription {
93    /// Restrict this subscription to a validated group of partitions sharing
94    /// the same `partition_count`. The reader emits a single SQL query per
95    /// poll using `partition_id IN (?, ?, ...)` instead of one query per
96    /// partition.
97    pub fn with_partitions(mut self, group: PartitionGroup) -> Self {
98        self.partitions = PartitionSelection::Many(group);
99        self
100    }
101}
102
103#[derive(Debug, Clone)]
104pub struct SqliteReaderConfig {
105    pub events_relation: SqliteRelationName,
106    pub poll_interval: Duration,
107    pub default_batch_size: usize,
108}
109
110impl Default for SqliteReaderConfig {
111    fn default() -> Self {
112        Self {
113            events_relation: SqliteRelationName::new("events").expect("default events relation"),
114            poll_interval: Duration::from_millis(100),
115            default_batch_size: 100,
116        }
117    }
118}
119
120#[derive(Clone)]
121pub struct SqliteCursorAcker {
122    state: Arc<Mutex<CursorState>>,
123    notify: Arc<Notify>,
124    sequence: i64,
125}
126
127struct CursorState {
128    last_acked: i64,
129    pending_nack: bool,
130}
131
132impl Acker for SqliteCursorAcker {
133    async fn ack(&self) -> Result<()> {
134        let mut state = self.state.lock().await;
135        if self.sequence > state.last_acked {
136            state.last_acked = self.sequence;
137        }
138        state.pending_nack = false;
139        self.notify.notify_waiters();
140        Ok(())
141    }
142
143    async fn nack(&self) -> Result<()> {
144        let mut state = self.state.lock().await;
145        state.pending_nack = true;
146        self.notify.notify_waiters();
147        Ok(())
148    }
149}
150
151pub struct SqliteReader {
152    conn: SqliteConn,
153    config: SqliteReaderConfig,
154}
155
156impl Clone for SqliteReader {
157    fn clone(&self) -> Self {
158        Self {
159            conn: Arc::clone(&self.conn),
160            config: self.config.clone(),
161        }
162    }
163}
164
165impl SqliteReader {
166    pub fn connect(conn: SqliteConn, config: SqliteReaderConfig) -> Result<Self> {
167        Self::prepare_schema(&conn, &config)?;
168        Ok(Self::new(conn, config))
169    }
170
171    pub fn prepare_schema(conn: &SqliteConn, config: &SqliteReaderConfig) -> Result<()> {
172        let guard = conn.lock().map_err(|e| Error::Store(e.to_string()))?;
173        SqliteEventLogSchema::prepare(
174            &guard,
175            &SqliteEventLogSchemaConfig {
176                events_relation: config.events_relation.clone(),
177            },
178        )
179    }
180
181    pub fn schema_sql(config: &SqliteReaderConfig) -> String {
182        SqliteEventLogSchema::schema_sql(&SqliteEventLogSchemaConfig {
183            events_relation: config.events_relation.clone(),
184        })
185    }
186
187    pub fn new(conn: SqliteConn, config: SqliteReaderConfig) -> Self {
188        Self { conn, config }
189    }
190}
191
192impl Reader for SqliteReader {
193    type Subscription = SqliteSubscription;
194    type Acker = SqliteCursorAcker;
195    type Cursor = SqliteCursor;
196    type Stream = SpawnedStream<SqliteCursorAcker, SqliteCursor>;
197
198    async fn read(&self, subscription: Self::Subscription) -> Result<Self::Stream> {
199        let conn = Arc::clone(&self.conn);
200        let events_relation = self.config.events_relation.render();
201        let poll_interval = self.config.poll_interval;
202        let batch_size = subscription
203            .batch_size
204            .unwrap_or(self.config.default_batch_size)
205            .clamp(1, 1000);
206        let filter = subscription.filter.clone();
207        let limit = subscription.limit;
208        let partitions = subscription.partitions.clone();
209        let (tx, rx) = mpsc::channel(64);
210
211        let (mut after_seq, lower_bound_ts) =
212            match resolve_initial_position(&conn, &events_relation, &subscription).await {
213                Ok(pos) => pos,
214                Err(e) => {
215                    let _ = tx.send(Err(e)).await;
216                    return Ok(SpawnedStream::from_receiver(rx));
217                }
218            };
219
220        let stop_seq = match resolve_stop_position(&conn, &events_relation, &subscription).await {
221            Ok(pos) => pos,
222            Err(e) => {
223                let _ = tx.send(Err(e)).await;
224                return Ok(SpawnedStream::from_receiver(rx));
225            }
226        };
227
228        let state = Arc::new(Mutex::new(CursorState {
229            last_acked: after_seq,
230            pending_nack: false,
231        }));
232        let notify = Arc::new(Notify::new());
233
234        let handle = tokio::spawn(async move {
235            let mut delivered = 0usize;
236            let mut buffer: VecDeque<(SerializedEvent, i64)> = VecDeque::new();
237            loop {
238                if buffer.is_empty() {
239                    let fetched = match fetch_batch(
240                        &conn,
241                        FetchBatchParams {
242                            events_relation: &events_relation,
243                            after_seq,
244                            stop_seq,
245                            take: batch_size,
246                            lower_bound_ts,
247                            filter: &filter,
248                            partitions: &partitions,
249                        },
250                    )
251                    .await
252                    {
253                        Ok(b) => b,
254                        Err(e) => {
255                            let _ = tx.send(Err(e)).await;
256                            return;
257                        }
258                    };
259                    if fetched.is_empty() {
260                        if stop_seq.is_some() {
261                            return;
262                        }
263                        tokio::time::sleep(poll_interval).await;
264                        continue;
265                    }
266                    buffer.extend(fetched);
267                }
268
269                while let Some((serialized, sequence)) = buffer.front() {
270                    let sequence = *sequence;
271                    let event = match serialized.to_event() {
272                        Ok(e) => e,
273                        Err(e) => {
274                            let _ = tx
275                                .send(Err(Error::Serialization(format!(
276                                    "decode event at sequence {sequence}: {e}"
277                                ))))
278                                .await;
279                            return;
280                        }
281                    };
282                    if !filter.matches(&event) {
283                        buffer.pop_front();
284                        after_seq = sequence;
285                        continue;
286                    }
287                    if let Some(l) = limit
288                        && delivered >= l
289                    {
290                        return;
291                    }
292                    let acker = SqliteCursorAcker {
293                        state: Arc::clone(&state),
294                        notify: Arc::clone(&notify),
295                        sequence,
296                    };
297                    let cursor = SqliteCursor { sequence };
298                    if tx
299                        .send(Ok(Message::new(event, acker, cursor)))
300                        .await
301                        .is_err()
302                    {
303                        return;
304                    }
305                    delivered += 1;
306
307                    loop {
308                        {
309                            let guard = state.lock().await;
310                            if guard.last_acked >= sequence {
311                                after_seq = sequence;
312                                buffer.pop_front();
313                                break;
314                            }
315                            if guard.pending_nack {
316                                break;
317                            }
318                            if tx.is_closed() {
319                                return;
320                            }
321                        }
322                        notify.notified().await;
323                    }
324                }
325            }
326        });
327
328        Ok(SpawnedStream::new(rx, handle))
329    }
330}
331
332async fn resolve_initial_position(
333    conn: &SqliteConn,
334    events_relation: &str,
335    subscription: &SqliteSubscription,
336) -> Result<(i64, Option<DateTime<Utc>>)> {
337    match subscription.start.clone() {
338        StartFrom::After(cursor) => Ok((cursor.sequence, None)),
339        StartFrom::Earliest => Ok((0, None)),
340        StartFrom::Latest => {
341            let conn = Arc::clone(conn);
342            let org = subscription
343                .filter
344                .organization
345                .as_ref()
346                .map(|o| o.as_str().to_owned());
347            let relation = events_relation.to_owned();
348            tokio::task::spawn_blocking(move || {
349                let guard = conn.lock().map_err(|e| Error::Store(e.to_string()))?;
350                let seq: i64 = match org {
351                    Some(o) => guard
352                        .query_row(
353                            &format!(
354                                "SELECT COALESCE(MAX(sequence), 0) FROM {relation} WHERE organization = ?1"
355                            ),
356                            rusqlite::params![o],
357                            |r| r.get(0),
358                        )
359                        .map_err(|e| Error::Store(e.to_string()))?,
360                    None => guard
361                        .query_row(
362                            &format!("SELECT COALESCE(MAX(sequence), 0) FROM {relation}"),
363                            [],
364                            |r| r.get(0),
365                        )
366                        .map_err(|e| Error::Store(e.to_string()))?,
367                };
368                Ok::<(i64, Option<DateTime<Utc>>), Error>((seq, None))
369            })
370            .await
371            .map_err(|e| Error::Store(format!("blocking task panicked: {e}")))?
372        }
373        StartFrom::Timestamp(ts) => {
374            let conn = Arc::clone(conn);
375            let org = subscription
376                .filter
377                .organization
378                .as_ref()
379                .map(|o| o.as_str().to_owned());
380            let ts_str = ts.to_rfc3339();
381            let relation = events_relation.to_owned();
382            tokio::task::spawn_blocking(move || {
383                let guard = conn.lock().map_err(|e| Error::Store(e.to_string()))?;
384                let seq: i64 = match org {
385                    Some(o) => guard
386                        .query_row(
387                            &format!(
388                                "SELECT COALESCE(MIN(sequence), 1) - 1 FROM {relation} \
389                                 WHERE organization = ?1 AND timestamp >= ?2"
390                            ),
391                            rusqlite::params![o, ts_str],
392                            |r| r.get(0),
393                        )
394                        .map_err(|e| Error::Store(e.to_string()))?,
395                    None => guard
396                        .query_row(
397                            &format!(
398                                "SELECT COALESCE(MIN(sequence), 1) - 1 FROM {relation} \
399                                 WHERE timestamp >= ?1"
400                            ),
401                            rusqlite::params![ts_str],
402                            |r| r.get(0),
403                        )
404                        .map_err(|e| Error::Store(e.to_string()))?,
405                };
406                Ok::<(i64, Option<DateTime<Utc>>), Error>((seq.max(0), Some(ts)))
407            })
408            .await
409            .map_err(|e| Error::Store(format!("blocking task panicked: {e}")))?
410        }
411    }
412}
413
414async fn resolve_stop_position(
415    conn: &SqliteConn,
416    events_relation: &str,
417    subscription: &SqliteSubscription,
418) -> Result<Option<i64>> {
419    match subscription.stop_at {
420        StopAt::Never => Ok(None),
421        StopAt::Cursor(cursor) => Ok(Some(cursor.sequence)),
422        StopAt::CurrentEnd => {
423            let conn = Arc::clone(conn);
424            let org = subscription
425                .filter
426                .organization
427                .as_ref()
428                .map(|o| o.as_str().to_owned());
429            let relation = events_relation.to_owned();
430            tokio::task::spawn_blocking(move || {
431                let guard = conn.lock().map_err(|e| Error::Store(e.to_string()))?;
432                let seq: i64 = match org {
433                    Some(o) => guard
434                        .query_row(
435                            &format!(
436                                "SELECT COALESCE(MAX(sequence), 0) FROM {relation} WHERE organization = ?1"
437                            ),
438                            rusqlite::params![o],
439                            |r| r.get(0),
440                        )
441                        .map_err(|e| Error::Store(e.to_string()))?,
442                    None => guard
443                        .query_row(
444                            &format!("SELECT COALESCE(MAX(sequence), 0) FROM {relation}"),
445                            [],
446                            |r| r.get(0),
447                        )
448                        .map_err(|e| Error::Store(e.to_string()))?,
449                };
450                Ok::<Option<i64>, Error>(Some(seq))
451            })
452            .await
453            .map_err(|e| Error::Store(format!("blocking task panicked: {e}")))?
454        }
455    }
456}
457
458struct FetchBatchParams<'a> {
459    events_relation: &'a str,
460    after_seq: i64,
461    stop_seq: Option<i64>,
462    take: usize,
463    lower_bound_ts: Option<DateTime<Utc>>,
464    filter: &'a EventFilter,
465    partitions: &'a PartitionSelection,
466}
467
468async fn fetch_batch(
469    conn: &SqliteConn,
470    p: FetchBatchParams<'_>,
471) -> Result<Vec<(SerializedEvent, i64)>> {
472    let FetchBatchParams {
473        events_relation,
474        after_seq,
475        stop_seq,
476        take,
477        lower_bound_ts,
478        filter,
479        partitions,
480    } = p;
481    let conn = Arc::clone(conn);
482    let relation = events_relation.to_owned();
483    let org = filter.organization.as_ref().map(|o| o.as_str().to_owned());
484    let exact_topic: Option<String> = filter.topic.as_ref().map(|p| match p {
485        TopicPattern::Exact(t) => t.as_str().to_owned(),
486    });
487    let ns_prefix = filter.namespace.as_ref().and_then(|p| match p {
488        NamespacePattern::Prefix(ns) if !ns.is_root() => Some(ns.as_str().to_owned()),
489        _ => None,
490    });
491    let ts_str = lower_bound_ts.map(|t| t.to_rfc3339());
492    let partitions = partitions.clone();
493
494    tokio::task::spawn_blocking(move || {
495        let guard = conn.lock().map_err(|e| Error::Store(e.to_string()))?;
496
497        let mut sql = format!(
498            "SELECT sequence, id, organization, namespace, topic, event_key, payload, content_type, metadata, \
499             timestamp, version, parent_id, correlation_id, causation_id \
500             FROM {relation} WHERE sequence > ?1"
501        );
502        let mut params: Vec<Value> = vec![Value::Integer(after_seq)];
503        let mut idx = 2usize;
504
505        if let Some(stop) = stop_seq {
506            sql.push_str(&format!(" AND sequence <= ?{idx}"));
507            params.push(Value::Integer(stop));
508            idx += 1;
509        }
510
511        if let Some(o) = &org {
512            sql.push_str(&format!(" AND organization = ?{idx}"));
513            params.push(Value::Text(o.clone()));
514            idx += 1;
515        }
516        if let Some(t) = &exact_topic {
517            sql.push_str(&format!(" AND topic = ?{idx}"));
518            params.push(Value::Text(t.clone()));
519            idx += 1;
520        }
521        if let Some(prefix) = &ns_prefix {
522            sql.push_str(&format!(
523                " AND (namespace = ?{idx} OR namespace LIKE ?{} || '/%')",
524                idx
525            ));
526            params.push(Value::Text(prefix.clone()));
527            idx += 1;
528        }
529        if let Some(ts) = &ts_str {
530            sql.push_str(&format!(" AND timestamp >= ?{idx}"));
531            params.push(Value::Text(ts.clone()));
532            idx += 1;
533        }
534        match &partitions {
535            PartitionSelection::All => {}
536            PartitionSelection::One(partition) => {
537                sql.push_str(&format!(" AND partition_count = ?{idx}"));
538                params.push(Value::Integer(partition.count() as i64));
539                idx += 1;
540                sql.push_str(&format!(" AND partition_id = ?{idx}"));
541                params.push(Value::Integer(partition.id() as i64));
542                idx += 1;
543            }
544            PartitionSelection::Many(group) => {
545                sql.push_str(&format!(" AND partition_count = ?{idx}"));
546                params.push(Value::Integer(group.count() as i64));
547                idx += 1;
548                let placeholders: Vec<String> = (0..group.partitions().len())
549                    .map(|i| format!("?{}", idx + i))
550                    .collect();
551                sql.push_str(&format!(
552                    " AND partition_id IN ({})",
553                    placeholders.join(",")
554                ));
555                for partition in group.partitions() {
556                    params.push(Value::Integer(partition.id() as i64));
557                }
558                idx += group.partitions().len();
559            }
560        }
561        sql.push_str(&format!(" ORDER BY sequence ASC LIMIT ?{idx}"));
562        params.push(Value::Integer(take as i64));
563
564        let mut stmt = guard
565            .prepare(&sql)
566            .map_err(|e| Error::Store(e.to_string()))?;
567        let rows = stmt
568            .query_map(rusqlite::params_from_iter(params.iter()), |row| {
569                let sequence: i64 = row.get(0)?;
570                let id: String = row.get(1)?;
571                let organization: String = row.get(2)?;
572                let namespace: String = row.get(3)?;
573                let topic: String = row.get(4)?;
574                let key: String = row.get(5)?;
575                let payload_str: String = row.get(6)?;
576                let content_type: String = row.get(7)?;
577                let metadata_str: String = row.get(8)?;
578                let timestamp_str: String = row.get(9)?;
579                let version: i64 = row.get(10)?;
580                let parent_id: Option<String> = row.get(11)?;
581                let correlation_id: Option<String> = row.get(12)?;
582                let causation_id: Option<String> = row.get(13)?;
583                Ok((
584                    sequence,
585                    id,
586                    organization,
587                    namespace,
588                    topic,
589                    key,
590                    payload_str,
591                    content_type,
592                    metadata_str,
593                    timestamp_str,
594                    version,
595                    parent_id,
596                    correlation_id,
597                    causation_id,
598                ))
599            })
600            .map_err(|e| Error::Store(e.to_string()))?;
601
602        let mut out = Vec::new();
603        for row in rows {
604            let (
605                sequence,
606                id,
607                organization,
608                namespace,
609                topic,
610                key,
611                payload_str,
612                content_type,
613                metadata_str,
614                timestamp_str,
615                version,
616                parent_id,
617                correlation_id,
618                causation_id,
619            ) = row.map_err(|e| Error::Store(e.to_string()))?;
620
621            let payload: SerializedPayload = serde_json::from_str(&payload_str)
622                .map_err(|e| Error::Serialization(format!("decode payload: {e}")))?;
623            let _ = content_type;
624            let id = uuid::Uuid::parse_str(&id)
625                .map_err(|e| Error::Serialization(format!("decode id: {e}")))?;
626            let parent_id = parent_id
627                .as_deref()
628                .map(uuid::Uuid::parse_str)
629                .transpose()
630                .map_err(|e| Error::Serialization(format!("decode parent_id: {e}")))?;
631            let metadata: HashMap<String, String> = serde_json::from_str(&metadata_str)
632                .map_err(|e| Error::Serialization(format!("decode metadata: {e}")))?;
633            let timestamp = DateTime::parse_from_rfc3339(&timestamp_str)
634                .map(|d| d.with_timezone(&Utc))
635                .map_err(|e| Error::Serialization(format!("decode timestamp: {e}")))?;
636            out.push((
637                SerializedEvent {
638                    id,
639                    organization,
640                    namespace,
641                    topic,
642                    payload,
643                    metadata,
644                    timestamp,
645                    version: version as u64,
646                    key,
647                    parent_id,
648                    correlation_id,
649                    causation_id,
650                },
651                sequence,
652            ));
653        }
654        Ok(out)
655    })
656    .await
657    .map_err(|e| Error::Store(format!("blocking task panicked: {e}")))?
658}
659
660#[cfg(test)]
661mod tests {
662    use std::collections::HashMap;
663    use std::num::NonZeroU16;
664    use std::time::Duration;
665
666    use futures::StreamExt;
667    use tokio::time::timeout;
668
669    use super::*;
670    use crate::database::SqliteDatabase;
671    use crate::writer::{SqlitePartitioningConfig, SqliteWriter, SqliteWriterConfig};
672    use eventuary_core::io::cursor::{CursorCodec, CursorOrder};
673    use eventuary_core::io::{Cursor, CursorId, Reader, Writer};
674    use eventuary_core::partition::{
675        EventKeyPartitionKeyResolver, Fnv1a64PartitionHasher, Partition, PartitionGroup,
676        PartitionHasher, PartitionKey,
677    };
678    use eventuary_core::{Event, PartitionableSubscription, Payload, StartFrom, StopAt};
679
680    #[test]
681    fn sqlite_subscription_with_partition_sets_partition_selection_one() {
682        let count = NonZeroU16::new(8).unwrap();
683        let partition = Partition::new(3, count).unwrap();
684        let sub = SqliteSubscription::default().with_partition(partition);
685        match sub.partitions {
686            PartitionSelection::One(p) => {
687                assert_eq!(p.id(), 3);
688                assert_eq!(p.count(), 8);
689            }
690            _ => panic!("expected PartitionSelection::One"),
691        }
692    }
693
694    #[test]
695    fn sqlite_subscription_with_partitions_sets_partition_selection_many() {
696        let count = NonZeroU16::new(8).unwrap();
697        let group = PartitionGroup::new(vec![
698            Partition::new(2, count).unwrap(),
699            Partition::new(5, count).unwrap(),
700        ])
701        .unwrap();
702        let sub = SqliteSubscription::default().with_partitions(group);
703        match sub.partitions {
704            PartitionSelection::Many(g) => {
705                assert_eq!(g.len(), 2);
706                let ids: Vec<u16> = g.partitions().iter().map(|p| p.id()).collect();
707                assert_eq!(ids, vec![2, 5]);
708            }
709            _ => panic!("expected PartitionSelection::Many"),
710        }
711    }
712
713    #[test]
714    fn sqlite_cursor_id_is_global() {
715        assert_eq!(SqliteCursor::new(42).id(), CursorId::global());
716    }
717
718    #[test]
719    fn sqlite_cursor_order_key_from_sequence() {
720        assert_eq!(SqliteCursor::new(42).order_key(), CursorOrder::from_i64(42));
721        assert!(SqliteCursor::new(9).order_key() < SqliteCursor::new(10).order_key());
722    }
723
724    #[test]
725    fn sqlite_cursor_codec_roundtrips() {
726        let codec = SqliteCursor::codec().unwrap();
727        let cursor = SqliteCursor::new(42);
728        let encoded = codec.encode(&cursor).unwrap();
729        assert_eq!(encoded.kind().as_str(), "eventuary.sqlite.sqlite_cursor.v1");
730        assert_eq!(encoded.order(), &CursorOrder::from_i64(42));
731        assert_eq!(codec.decode(&encoded).unwrap(), cursor);
732    }
733
734    #[test]
735    fn sqlite_cursor_codec_preserves_typed_ord() {
736        let codec = SqliteCursor::codec().unwrap();
737        let lo = codec.encode(&SqliteCursor::new(9)).unwrap();
738        let hi = codec.encode(&SqliteCursor::new(10)).unwrap();
739        assert!(lo < hi);
740    }
741
742    const PARTITION_COUNT: u16 = 4;
743
744    fn event_with_key(key: &str) -> Event {
745        Event::builder(
746            "acme",
747            "/orders",
748            "order.placed",
749            key,
750            Payload::from_string("{}"),
751        )
752        .unwrap()
753        .build()
754        .unwrap()
755    }
756
757    fn partition_for_key(key: &str) -> u16 {
758        let k = PartitionKey::new(key).unwrap();
759        let hash = Fnv1a64PartitionHasher.hash(&k);
760        (hash.get() % PARTITION_COUNT as u64) as u16
761    }
762
763    fn fast_config() -> SqliteReaderConfig {
764        SqliteReaderConfig {
765            poll_interval: Duration::from_millis(10),
766            ..SqliteReaderConfig::default()
767        }
768    }
769
770    #[tokio::test]
771    async fn reader_default_all_returns_every_event() {
772        let db = SqliteDatabase::open_in_memory().unwrap();
773        let config = SqliteWriterConfig {
774            partitioning: SqlitePartitioningConfig::inline(
775                NonZeroU16::new(PARTITION_COUNT).unwrap(),
776                EventKeyPartitionKeyResolver::new(),
777                Fnv1a64PartitionHasher,
778            ),
779            ..SqliteWriterConfig::default()
780        };
781        SqliteWriter::prepare_schema(&db.conn(), &config).unwrap();
782        let writer = SqliteWriter::new_with_config(db.conn(), config);
783
784        let keys = ["k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7"];
785        for key in &keys {
786            writer.write(&event_with_key(key)).await.unwrap();
787        }
788
789        let reader = SqliteReader::new(db.conn(), fast_config());
790        let subscription = SqliteSubscription {
791            start: StartFrom::Earliest,
792            stop_at: StopAt::CurrentEnd,
793            ..SqliteSubscription::default()
794        };
795        let mut stream = reader.read(subscription).await.unwrap();
796
797        let mut count = 0usize;
798        while let Ok(Some(Ok(msg))) = timeout(Duration::from_secs(5), stream.next()).await {
799            msg.acker().ack().await.unwrap();
800            count += 1;
801        }
802
803        assert_eq!(count, keys.len());
804    }
805
806    #[tokio::test]
807    async fn reader_one_filters_to_single_partition() {
808        let db = SqliteDatabase::open_in_memory().unwrap();
809        let config = SqliteWriterConfig {
810            partitioning: SqlitePartitioningConfig::inline(
811                NonZeroU16::new(PARTITION_COUNT).unwrap(),
812                EventKeyPartitionKeyResolver::new(),
813                Fnv1a64PartitionHasher,
814            ),
815            ..SqliteWriterConfig::default()
816        };
817        SqliteWriter::prepare_schema(&db.conn(), &config).unwrap();
818        let writer = SqliteWriter::new_with_config(db.conn(), config);
819
820        let keys = ["k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7"];
821        for key in &keys {
822            writer.write(&event_with_key(key)).await.unwrap();
823        }
824
825        let partitions_by_id: HashMap<u16, Vec<&str>> =
826            keys.iter().fold(HashMap::new(), |mut acc, key| {
827                acc.entry(partition_for_key(key)).or_default().push(key);
828                acc
829            });
830
831        let (chosen_partition, expected_keys) = partitions_by_id
832            .iter()
833            .find(|(_, ks)| ks.len() >= 2)
834            .map(|(id, ks)| (*id, ks.clone()))
835            .expect("expected at least one partition with >=2 events");
836
837        let reader = SqliteReader::new(db.conn(), fast_config());
838        let subscription = SqliteSubscription {
839            start: StartFrom::Earliest,
840            stop_at: StopAt::CurrentEnd,
841            partitions: PartitionSelection::One(
842                Partition::new(chosen_partition, NonZeroU16::new(PARTITION_COUNT).unwrap())
843                    .unwrap(),
844            ),
845            ..SqliteSubscription::default()
846        };
847        let mut stream = reader.read(subscription).await.unwrap();
848
849        let mut received_keys: Vec<String> = Vec::new();
850        while let Ok(Some(Ok(msg))) = timeout(Duration::from_secs(5), stream.next()).await {
851            let key = msg.event().key().as_str().to_owned();
852            msg.acker().ack().await.unwrap();
853            received_keys.push(key);
854        }
855
856        assert_eq!(received_keys.len(), expected_keys.len());
857        for key in &received_keys {
858            assert_eq!(
859                partition_for_key(key),
860                chosen_partition,
861                "event key {key} maps to wrong partition"
862            );
863        }
864    }
865
866    #[tokio::test]
867    async fn reader_many_filters_to_selected_partitions() {
868        let db = SqliteDatabase::open_in_memory().unwrap();
869        let config = SqliteWriterConfig {
870            partitioning: SqlitePartitioningConfig::inline(
871                NonZeroU16::new(PARTITION_COUNT).unwrap(),
872                EventKeyPartitionKeyResolver::new(),
873                Fnv1a64PartitionHasher,
874            ),
875            ..SqliteWriterConfig::default()
876        };
877        SqliteWriter::prepare_schema(&db.conn(), &config).unwrap();
878        let writer = SqliteWriter::new_with_config(db.conn(), config);
879
880        let keys = ["k0", "k1", "k2", "k3", "k4", "k5", "k6", "k7"];
881        for key in &keys {
882            writer.write(&event_with_key(key)).await.unwrap();
883        }
884
885        let count = NonZeroU16::new(PARTITION_COUNT).unwrap();
886        let mut populated: Vec<u16> = keys
887            .iter()
888            .map(|k| partition_for_key(k))
889            .collect::<std::collections::BTreeSet<_>>()
890            .into_iter()
891            .collect();
892        populated.truncate(2);
893        assert!(
894            populated.len() >= 2,
895            "fixture must populate at least 2 distinct partitions"
896        );
897
898        let selected: std::collections::HashSet<u16> = populated.iter().copied().collect();
899        let expected_len = keys
900            .iter()
901            .copied()
902            .filter(|k| selected.contains(&partition_for_key(k)))
903            .count();
904
905        let group = PartitionGroup::new(
906            populated
907                .iter()
908                .map(|id| Partition::new(*id, count).unwrap())
909                .collect(),
910        )
911        .unwrap();
912
913        let reader = SqliteReader::new(db.conn(), fast_config());
914        let subscription = SqliteSubscription {
915            start: StartFrom::Earliest,
916            stop_at: StopAt::CurrentEnd,
917            ..SqliteSubscription::default()
918        }
919        .with_partitions(group);
920        let mut stream = reader.read(subscription).await.unwrap();
921
922        let mut received: Vec<String> = Vec::new();
923        while let Ok(Some(Ok(msg))) = timeout(Duration::from_secs(5), stream.next()).await {
924            let key = msg.event().key().as_str().to_owned();
925            msg.acker().ack().await.unwrap();
926            received.push(key);
927        }
928
929        assert_eq!(received.len(), expected_len);
930        for key in &received {
931            assert!(
932                selected.contains(&partition_for_key(key)),
933                "event key {key} maps to partition outside selected group"
934            );
935        }
936    }
937}