Skip to main content

inline_client/
store.rs

1//! Store boundary for durable client state.
2//!
3//! The built-in SQLite implementation persists sessions, sync cursors and
4//! journals, dialogs, users, messages, reconciliation state, and transactions.
5//! The trait keeps storage pluggable for hosts with platform-specific keychain
6//! or database requirements.
7
8use std::{
9    collections::{HashMap, HashSet},
10    fmt,
11    fs::OpenOptions,
12    path::Path,
13    sync::{Arc, Mutex},
14    time::{SystemTime, UNIX_EPOCH},
15};
16
17use futures_util::future::BoxFuture;
18use rusqlite::{Connection, OptionalExtension, Row, params, types::Type};
19use serde::{Deserialize, Serialize};
20
21use crate::{
22    AuthCredential, ChatParticipantRecord, ClientErrorCategory, ClientFailure, DialogFollowMode,
23    DialogNotificationMode, DialogRecord, DialogsPage, DialogsRequest, HistoryPage, HistoryRequest,
24    InlineId, MessageContent, MessageRecord, SpaceMemberRecord, SpaceRecord, TransactionEvent,
25    TransactionId, TransactionIdentity, TransactionState, UserRecord, UserSettingsRecord,
26};
27
28/// Result type returned by client stores.
29pub type StoreResult<T> = Result<T, StoreError>;
30
31/// Redacted store error.
32#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
33#[error("{category:?}: {message}")]
34pub struct StoreError {
35    /// Stable error category.
36    pub category: ClientErrorCategory,
37    /// Redacted message suitable for hosts.
38    pub message: String,
39}
40
41impl StoreError {
42    /// Creates a store error.
43    pub fn new(category: ClientErrorCategory, message: impl Into<String>) -> Self {
44        Self {
45            category,
46            message: message.into(),
47        }
48    }
49
50    fn internal(message: impl Into<String>) -> Self {
51        Self::new(ClientErrorCategory::Internal, message)
52    }
53}
54
55/// Stored session metadata.
56#[derive(Clone, PartialEq, Eq)]
57pub struct StoredSession {
58    /// Auth credential needed by the runtime.
59    pub auth: AuthCredential,
60    /// Optional account/store namespace chosen by the host.
61    pub account_namespace: Option<String>,
62}
63
64/// Durable global synchronization state.
65///
66/// `last_sync_date` is the newest server update date that the client has
67/// coherently applied. It is used as the discovery cursor for
68/// `getUpdatesState` after startup and reconnect.
69#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
70pub struct SyncState {
71    /// Latest coherently applied server update date, in Unix seconds.
72    pub last_sync_date: i64,
73}
74
75/// Peer identity for an Inline chat update bucket.
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
77pub enum SyncBucketPeer {
78    /// Direct-message bucket addressed by user ID.
79    User {
80        /// Inline user ID.
81        user_id: InlineId,
82    },
83    /// Group or thread bucket addressed by chat ID.
84    Chat {
85        /// Inline chat ID.
86        chat_id: InlineId,
87    },
88}
89
90/// Stable identity of an Inline update bucket.
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
92pub enum SyncBucketKey {
93    /// Updates scoped to the authenticated user.
94    User,
95    /// Updates scoped to an Inline space.
96    Space {
97        /// Inline space ID.
98        space_id: InlineId,
99    },
100    /// Updates scoped to a direct-message or chat peer.
101    Chat {
102        /// Peer that owns the chat bucket.
103        peer: SyncBucketPeer,
104    },
105}
106
107/// Durable cursor for one Inline update bucket.
108#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
109pub struct SyncBucketState {
110    /// Last coherently applied sequence for the bucket.
111    pub seq: i64,
112    /// Newest coherently applied update date for the bucket, in Unix seconds.
113    pub date: i64,
114}
115
116/// Durable write-ahead record for one fetched update batch. The opaque payload
117/// is owned by the sync engine and retained until reducer writes and the bucket
118/// cursor have both committed.
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct PendingSyncBatch {
121    /// Bucket receiving the batch.
122    pub key: SyncBucketKey,
123    /// Cursor to commit after the batch is reapplied successfully.
124    pub committed_state: SyncBucketState,
125    /// Versioned opaque encoded updates and sidecars.
126    pub payload: Vec<u8>,
127}
128
129/// Durable current reaction state for one user and message.
130#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
131pub struct StoredReaction {
132    /// Inline chat ID.
133    pub chat_id: InlineId,
134    /// Inline message ID.
135    pub message_id: InlineId,
136    /// Inline user ID that owns the reaction.
137    pub user_id: InlineId,
138    /// Reaction emoji.
139    pub reaction: String,
140}
141
142/// Durable read state for one chat.
143#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
144pub struct StoredReadState {
145    /// Inline chat ID.
146    pub chat_id: InlineId,
147    /// Highest message ID known read, when supplied by the server.
148    pub read_max_id: Option<InlineId>,
149    /// Remaining unread count, when supplied by the server.
150    pub unread_count: Option<u32>,
151    /// Whether the dialog is explicitly marked unread.
152    pub marked_unread: bool,
153}
154
155/// Durable account-level state needed to reconcile a consumer after an event
156/// stream retention gap.
157#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
158pub struct AccountStateSnapshot {
159    /// Chats that were removed after being observed by this account store.
160    pub deleted_chat_ids: Vec<InlineId>,
161}
162
163/// Durable per-chat state that complements paged dialog/history queries during
164/// reconciliation.
165#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
166pub struct ChatStateSnapshot {
167    /// Inline chat ID represented by this snapshot.
168    pub chat_id: InlineId,
169    /// Current dialog metadata, or `None` if the chat is deleted or unknown.
170    pub dialog: Option<DialogRecord>,
171    /// Whether the store has a durable chat deletion tombstone.
172    pub deleted: bool,
173    /// Message IDs known to have been deleted from this chat.
174    pub deleted_message_ids: Vec<InlineId>,
175    /// Current reaction set known for the chat.
176    pub reactions: Vec<StoredReaction>,
177    /// Message IDs whose reaction set was observed as a complete snapshot,
178    /// including messages with zero reactions.
179    pub reaction_snapshot_message_ids: Vec<InlineId>,
180    /// Current read state known for the chat.
181    pub read_state: Option<StoredReadState>,
182    /// Current direct participant snapshot known for the chat.
183    pub participants: Vec<ChatParticipantRecord>,
184    /// Whether `participants` is a complete snapshot rather than only deltas.
185    pub participants_complete: bool,
186}
187
188impl fmt::Debug for StoredSession {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        f.debug_struct("StoredSession")
191            .field("auth", &self.auth)
192            .field(
193                "account_namespace",
194                &self.account_namespace.as_ref().map(|_| "<redacted>"),
195            )
196            .finish()
197    }
198}
199
200/// Durable store boundary for client state.
201pub trait ClientStore: fmt::Debug + Send + Sync + 'static {
202    /// Saves the current session.
203    fn save_session(&self, session: StoredSession) -> BoxFuture<'static, StoreResult<()>>;
204
205    /// Loads the current session.
206    fn load_session(&self) -> BoxFuture<'static, StoreResult<Option<StoredSession>>>;
207
208    /// Clears the current session.
209    fn clear_session(&self) -> BoxFuture<'static, StoreResult<()>>;
210
211    /// Clears all account-owned cache, sync, and transaction state while
212    /// leaving the session row under separate lifecycle control.
213    fn clear_account_data(&self) -> BoxFuture<'static, StoreResult<()>>;
214
215    /// Loads the global synchronization discovery cursor.
216    fn sync_state(&self) -> BoxFuture<'static, StoreResult<SyncState>>;
217
218    /// Saves the global synchronization discovery cursor.
219    fn save_sync_state(&self, state: SyncState) -> BoxFuture<'static, StoreResult<()>>;
220
221    /// Clears the global synchronization cursor and all bucket cursors.
222    fn clear_sync_state(&self) -> BoxFuture<'static, StoreResult<()>>;
223
224    /// Loads the cursor for one update bucket.
225    fn sync_bucket_state(
226        &self,
227        key: SyncBucketKey,
228    ) -> BoxFuture<'static, StoreResult<SyncBucketState>>;
229
230    /// Saves the cursor for one update bucket.
231    fn save_sync_bucket_state(
232        &self,
233        key: SyncBucketKey,
234        state: SyncBucketState,
235    ) -> BoxFuture<'static, StoreResult<()>>;
236
237    /// Saves multiple bucket cursors atomically when supported by the store.
238    fn save_sync_bucket_states(
239        &self,
240        states: Vec<(SyncBucketKey, SyncBucketState)>,
241    ) -> BoxFuture<'static, StoreResult<()>>;
242
243    /// Removes the cursor for one update bucket.
244    fn remove_sync_bucket_state(&self, key: SyncBucketKey) -> BoxFuture<'static, StoreResult<()>>;
245
246    /// Saves or replaces the write-ahead batch for one bucket before reducer
247    /// state is changed.
248    fn save_pending_sync_batch(
249        &self,
250        batch: PendingSyncBatch,
251    ) -> BoxFuture<'static, StoreResult<()>>;
252
253    /// Lists write-ahead batches that were not fully committed.
254    fn pending_sync_batches(&self) -> BoxFuture<'static, StoreResult<Vec<PendingSyncBatch>>>;
255
256    /// Atomically advances a bucket cursor and removes its write-ahead batch.
257    fn commit_pending_sync_batch(
258        &self,
259        key: SyncBucketKey,
260        state: SyncBucketState,
261    ) -> BoxFuture<'static, StoreResult<()>>;
262
263    /// Lists stored dialogs.
264    fn dialogs(&self, request: DialogsRequest) -> BoxFuture<'static, StoreResult<DialogsPage>>;
265
266    /// Loads one stored dialog by chat ID.
267    fn dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<Option<DialogRecord>>>;
268
269    /// Records a dialog in local state.
270    fn record_dialog(&self, dialog: DialogRecord) -> BoxFuture<'static, StoreResult<()>>;
271
272    /// Removes a dialog and its client-owned per-chat state.
273    fn remove_dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<()>>;
274
275    /// Lists durable chat deletion tombstones.
276    fn deleted_chat_ids(&self) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
277
278    /// Records user summaries in local state.
279    fn record_users(&self, users: Vec<UserRecord>) -> BoxFuture<'static, StoreResult<()>>;
280
281    /// Inserts or replaces a durable space summary.
282    fn record_space(&self, space: SpaceRecord) -> BoxFuture<'static, StoreResult<()>>;
283
284    /// Loads a durable space summary.
285    fn space(&self, space_id: InlineId) -> BoxFuture<'static, StoreResult<Option<SpaceRecord>>>;
286
287    /// Inserts or replaces one durable space member.
288    fn record_space_member(&self, member: SpaceMemberRecord)
289    -> BoxFuture<'static, StoreResult<()>>;
290
291    /// Replaces the complete durable membership snapshot for one space.
292    fn record_space_members(
293        &self,
294        space_id: InlineId,
295        members: Vec<SpaceMemberRecord>,
296    ) -> BoxFuture<'static, StoreResult<()>>;
297
298    /// Removes one durable space member.
299    fn remove_space_member(
300        &self,
301        space_id: InlineId,
302        user_id: InlineId,
303    ) -> BoxFuture<'static, StoreResult<()>>;
304
305    /// Lists durable members for a space.
306    fn space_members(
307        &self,
308        space_id: InlineId,
309    ) -> BoxFuture<'static, StoreResult<Vec<SpaceMemberRecord>>>;
310
311    /// Replaces durable global user settings.
312    fn record_user_settings(
313        &self,
314        settings: UserSettingsRecord,
315    ) -> BoxFuture<'static, StoreResult<()>>;
316
317    /// Loads durable global user settings.
318    fn user_settings(&self) -> BoxFuture<'static, StoreResult<Option<UserSettingsRecord>>>;
319
320    /// Replaces the durable participant snapshot for a chat.
321    fn record_chat_participants(
322        &self,
323        chat_id: InlineId,
324        participants: Vec<ChatParticipantRecord>,
325    ) -> BoxFuture<'static, StoreResult<()>>;
326
327    /// Inserts or replaces one durable chat participant.
328    fn record_chat_participant(
329        &self,
330        chat_id: InlineId,
331        participant: ChatParticipantRecord,
332    ) -> BoxFuture<'static, StoreResult<()>>;
333
334    /// Removes one durable chat participant.
335    fn remove_chat_participant(
336        &self,
337        chat_id: InlineId,
338        user_id: InlineId,
339    ) -> BoxFuture<'static, StoreResult<()>>;
340
341    /// Loads the durable participant snapshot for a chat.
342    fn chat_participants(
343        &self,
344        chat_id: InlineId,
345    ) -> BoxFuture<'static, StoreResult<Vec<ChatParticipantRecord>>>;
346
347    /// Returns whether the participant list was observed as a complete snapshot.
348    fn chat_participants_complete(
349        &self,
350        chat_id: InlineId,
351    ) -> BoxFuture<'static, StoreResult<bool>>;
352
353    /// Fetches stored history.
354    fn history(&self, request: HistoryRequest) -> BoxFuture<'static, StoreResult<HistoryPage>>;
355
356    /// Records a message in stored history.
357    fn record_message(&self, message: MessageRecord) -> BoxFuture<'static, StoreResult<()>>;
358
359    /// Records a durable message tombstone and removes the live message row.
360    fn record_message_deleted(
361        &self,
362        chat_id: InlineId,
363        message_id: InlineId,
364    ) -> BoxFuture<'static, StoreResult<()>>;
365
366    /// Returns whether a message has a durable deletion tombstone.
367    fn message_deleted(
368        &self,
369        chat_id: InlineId,
370        message_id: InlineId,
371    ) -> BoxFuture<'static, StoreResult<bool>>;
372
373    /// Lists durable message deletion tombstones for one chat.
374    fn deleted_message_ids(
375        &self,
376        chat_id: InlineId,
377    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
378
379    /// Clears stored messages for one chat and returns the removed IDs.
380    fn clear_chat_messages(
381        &self,
382        chat_id: InlineId,
383        before_date: Option<i64>,
384    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
385
386    /// Lists chat IDs currently associated with a space.
387    fn chat_ids_in_space(
388        &self,
389        space_id: InlineId,
390    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
391
392    /// Inserts or replaces one current reaction.
393    fn record_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>>;
394
395    /// Removes one current reaction.
396    fn remove_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>>;
397
398    /// Replaces the complete reaction set for one message.
399    fn replace_message_reactions(
400        &self,
401        chat_id: InlineId,
402        message_id: InlineId,
403        reactions: Vec<StoredReaction>,
404    ) -> BoxFuture<'static, StoreResult<()>>;
405
406    /// Lists current reactions for a message.
407    fn reactions(
408        &self,
409        chat_id: InlineId,
410        message_id: InlineId,
411    ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>>;
412
413    /// Lists the current reaction set for all known messages in one chat.
414    fn reactions_for_chat(
415        &self,
416        chat_id: InlineId,
417    ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>>;
418
419    /// Lists message IDs whose reaction set is known to be complete.
420    fn reaction_snapshot_message_ids(
421        &self,
422        chat_id: InlineId,
423    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>>;
424
425    /// Inserts or replaces read state for a chat.
426    fn record_read_state(&self, state: StoredReadState) -> BoxFuture<'static, StoreResult<()>>;
427
428    /// Loads read state for a chat.
429    fn read_state(
430        &self,
431        chat_id: InlineId,
432    ) -> BoxFuture<'static, StoreResult<Option<StoredReadState>>>;
433
434    /// Records a transaction state change.
435    fn record_transaction(
436        &self,
437        transaction: StoredTransaction,
438    ) -> BoxFuture<'static, StoreResult<()>>;
439
440    /// Finds a transaction by transaction ID.
441    fn transaction(
442        &self,
443        transaction_id: TransactionId,
444    ) -> BoxFuture<'static, StoreResult<Option<StoredTransaction>>>;
445}
446
447/// Stored durable transaction state.
448#[derive(Clone, Debug, PartialEq, Eq)]
449pub struct StoredTransaction {
450    /// Mutation identity and reconciliation IDs.
451    pub identity: TransactionIdentity,
452    /// Current transaction state.
453    pub state: TransactionState,
454    /// Chat containing the transaction, when known.
455    pub chat_id: Option<InlineId>,
456    /// Final message ID, when known.
457    pub message_id: Option<InlineId>,
458    /// Redacted failure for failed transactions.
459    pub failure: Option<ClientFailure>,
460    /// Unix timestamp in seconds when the transaction row was created.
461    pub created_at: i64,
462    /// Unix timestamp in seconds when the transaction row was last updated.
463    pub updated_at: i64,
464}
465
466impl StoredTransaction {
467    /// Creates a stored transaction in the provided state.
468    pub fn new(identity: TransactionIdentity, state: TransactionState) -> Self {
469        let now = now_seconds();
470        Self {
471            identity,
472            state,
473            chat_id: None,
474            message_id: None,
475            failure: None,
476            created_at: now,
477            updated_at: now,
478        }
479    }
480
481    /// Sets the chat ID.
482    pub fn with_chat_id(mut self, chat_id: InlineId) -> Self {
483        self.chat_id = Some(chat_id);
484        self
485    }
486
487    /// Sets the final message ID.
488    pub fn with_message_id(mut self, message_id: InlineId) -> Self {
489        self.message_id = Some(message_id);
490        self.identity.final_message_id = Some(message_id);
491        self
492    }
493
494    /// Sets the redacted failure.
495    pub fn with_failure(mut self, failure: ClientFailure) -> Self {
496        self.failure = Some(failure);
497        self
498    }
499
500    /// Converts to a client event payload.
501    pub fn event(&self) -> TransactionEvent {
502        TransactionEvent {
503            identity: self.identity.clone(),
504            state: self.state,
505            failure: self.failure.clone(),
506        }
507    }
508}
509
510/// In-memory store for tests and early client development.
511#[derive(Clone, Debug, Default)]
512pub struct InMemoryStore {
513    state: Arc<Mutex<InMemoryStoreState>>,
514}
515
516#[derive(Debug, Default)]
517struct InMemoryStoreState {
518    session: Option<StoredSession>,
519    sync_state: SyncState,
520    sync_buckets: HashMap<SyncBucketKey, SyncBucketState>,
521    pending_sync_batches: HashMap<SyncBucketKey, PendingSyncBatch>,
522    dialogs: Vec<DialogRecord>,
523    chat_tombstones: HashSet<i64>,
524    users: HashMap<i64, UserRecord>,
525    spaces: HashMap<i64, SpaceRecord>,
526    space_members: HashMap<(i64, i64), SpaceMemberRecord>,
527    user_settings: Option<UserSettingsRecord>,
528    participants: HashMap<i64, HashMap<i64, ChatParticipantRecord>>,
529    participant_snapshots: HashSet<i64>,
530    messages: HashMap<i64, Vec<MessageRecord>>,
531    message_tombstones: HashSet<(i64, i64)>,
532    reactions: HashMap<(i64, i64, i64, String), StoredReaction>,
533    reaction_snapshots: HashSet<(i64, i64)>,
534    read_states: HashMap<i64, StoredReadState>,
535    transactions: HashMap<String, StoredTransaction>,
536}
537
538impl InMemoryStore {
539    /// Creates an empty in-memory store.
540    pub fn new() -> Self {
541        Self::default()
542    }
543
544    /// Inserts or replaces a dialog.
545    pub fn upsert_dialog(&self, dialog: DialogRecord) {
546        let mut state = self.state.lock().expect("in-memory store poisoned");
547        state.chat_tombstones.remove(&dialog.chat_id.get());
548        upsert_dialog(&mut state.dialogs, dialog);
549    }
550
551    /// Inserts or replaces a user summary.
552    pub fn upsert_user(&self, user: UserRecord) {
553        let mut state = self.state.lock().expect("in-memory store poisoned");
554        state.users.insert(user.user_id.get(), user);
555    }
556
557    /// Inserts a message into history.
558    pub fn insert_message(&self, message: MessageRecord) {
559        let mut state = self.state.lock().expect("in-memory store poisoned");
560        state
561            .message_tombstones
562            .remove(&(message.chat_id.get(), message.message_id.get()));
563        insert_message(&mut state.messages, message);
564    }
565}
566
567impl ClientStore for InMemoryStore {
568    fn save_session(&self, session: StoredSession) -> BoxFuture<'static, StoreResult<()>> {
569        let store = self.clone();
570        Box::pin(async move {
571            store
572                .state
573                .lock()
574                .expect("in-memory store poisoned")
575                .session = Some(session);
576            Ok(())
577        })
578    }
579
580    fn load_session(&self) -> BoxFuture<'static, StoreResult<Option<StoredSession>>> {
581        let store = self.clone();
582        Box::pin(async move {
583            Ok(store
584                .state
585                .lock()
586                .expect("in-memory store poisoned")
587                .session
588                .clone())
589        })
590    }
591
592    fn clear_session(&self) -> BoxFuture<'static, StoreResult<()>> {
593        let store = self.clone();
594        Box::pin(async move {
595            store
596                .state
597                .lock()
598                .expect("in-memory store poisoned")
599                .session = None;
600            Ok(())
601        })
602    }
603
604    fn clear_account_data(&self) -> BoxFuture<'static, StoreResult<()>> {
605        let store = self.clone();
606        Box::pin(async move {
607            let mut state = store.state.lock().expect("in-memory store poisoned");
608            let session = state.session.take();
609            *state = InMemoryStoreState::default();
610            state.session = session;
611            Ok(())
612        })
613    }
614
615    fn sync_state(&self) -> BoxFuture<'static, StoreResult<SyncState>> {
616        let store = self.clone();
617        Box::pin(async move {
618            Ok(store
619                .state
620                .lock()
621                .expect("in-memory store poisoned")
622                .sync_state)
623        })
624    }
625
626    fn save_sync_state(&self, state: SyncState) -> BoxFuture<'static, StoreResult<()>> {
627        let store = self.clone();
628        Box::pin(async move {
629            store
630                .state
631                .lock()
632                .expect("in-memory store poisoned")
633                .sync_state = state;
634            Ok(())
635        })
636    }
637
638    fn clear_sync_state(&self) -> BoxFuture<'static, StoreResult<()>> {
639        let store = self.clone();
640        Box::pin(async move {
641            let mut state = store.state.lock().expect("in-memory store poisoned");
642            state.sync_state = SyncState::default();
643            state.sync_buckets.clear();
644            Ok(())
645        })
646    }
647
648    fn sync_bucket_state(
649        &self,
650        key: SyncBucketKey,
651    ) -> BoxFuture<'static, StoreResult<SyncBucketState>> {
652        let store = self.clone();
653        Box::pin(async move {
654            Ok(store
655                .state
656                .lock()
657                .expect("in-memory store poisoned")
658                .sync_buckets
659                .get(&key)
660                .copied()
661                .unwrap_or_default())
662        })
663    }
664
665    fn save_sync_bucket_state(
666        &self,
667        key: SyncBucketKey,
668        state: SyncBucketState,
669    ) -> BoxFuture<'static, StoreResult<()>> {
670        let store = self.clone();
671        Box::pin(async move {
672            store
673                .state
674                .lock()
675                .expect("in-memory store poisoned")
676                .sync_buckets
677                .insert(key, state);
678            Ok(())
679        })
680    }
681
682    fn save_sync_bucket_states(
683        &self,
684        states: Vec<(SyncBucketKey, SyncBucketState)>,
685    ) -> BoxFuture<'static, StoreResult<()>> {
686        let store = self.clone();
687        Box::pin(async move {
688            let mut stored = store.state.lock().expect("in-memory store poisoned");
689            stored.sync_buckets.extend(states);
690            Ok(())
691        })
692    }
693
694    fn remove_sync_bucket_state(&self, key: SyncBucketKey) -> BoxFuture<'static, StoreResult<()>> {
695        let store = self.clone();
696        Box::pin(async move {
697            store
698                .state
699                .lock()
700                .expect("in-memory store poisoned")
701                .sync_buckets
702                .remove(&key);
703            Ok(())
704        })
705    }
706
707    fn save_pending_sync_batch(
708        &self,
709        batch: PendingSyncBatch,
710    ) -> BoxFuture<'static, StoreResult<()>> {
711        let store = self.clone();
712        Box::pin(async move {
713            store
714                .state
715                .lock()
716                .expect("in-memory store poisoned")
717                .pending_sync_batches
718                .insert(batch.key, batch);
719            Ok(())
720        })
721    }
722
723    fn pending_sync_batches(&self) -> BoxFuture<'static, StoreResult<Vec<PendingSyncBatch>>> {
724        let store = self.clone();
725        Box::pin(async move {
726            let mut batches = store
727                .state
728                .lock()
729                .expect("in-memory store poisoned")
730                .pending_sync_batches
731                .values()
732                .cloned()
733                .collect::<Vec<_>>();
734            batches.sort_by_key(|batch| sync_bucket_sort_key(batch.key));
735            Ok(batches)
736        })
737    }
738
739    fn commit_pending_sync_batch(
740        &self,
741        key: SyncBucketKey,
742        state: SyncBucketState,
743    ) -> BoxFuture<'static, StoreResult<()>> {
744        let store = self.clone();
745        Box::pin(async move {
746            let mut stored = store.state.lock().expect("in-memory store poisoned");
747            stored.sync_buckets.insert(key, state);
748            stored.pending_sync_batches.remove(&key);
749            Ok(())
750        })
751    }
752
753    fn dialogs(&self, request: DialogsRequest) -> BoxFuture<'static, StoreResult<DialogsPage>> {
754        let store = self.clone();
755        Box::pin(async move {
756            let state = store.state.lock().expect("in-memory store poisoned");
757            let start = parse_cursor(request.cursor.as_deref())?;
758            let limit = request.limit.unwrap_or(50).max(1) as usize;
759            let dialogs = state
760                .dialogs
761                .iter()
762                .skip(start)
763                .take(limit)
764                .map(|dialog| {
765                    dialog_with_synced_through(
766                        dialog.clone(),
767                        max_message_id_from_memory(&state.messages, dialog.chat_id),
768                    )
769                })
770                .collect::<Vec<_>>();
771            let next = start + dialogs.len();
772            let users = all_users_from_memory(&state.users);
773            Ok(DialogsPage {
774                dialogs,
775                users,
776                next_cursor: (next < state.dialogs.len()).then(|| next.to_string()),
777            })
778        })
779    }
780
781    fn dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<Option<DialogRecord>>> {
782        let store = self.clone();
783        Box::pin(async move {
784            let state = store.state.lock().expect("in-memory store poisoned");
785            Ok(state
786                .dialogs
787                .iter()
788                .find(|dialog| dialog.chat_id == chat_id)
789                .cloned()
790                .map(|dialog| {
791                    dialog_with_synced_through(
792                        dialog,
793                        max_message_id_from_memory(&state.messages, chat_id),
794                    )
795                }))
796        })
797    }
798
799    fn record_dialog(&self, dialog: DialogRecord) -> BoxFuture<'static, StoreResult<()>> {
800        let store = self.clone();
801        Box::pin(async move {
802            store.upsert_dialog(dialog);
803            Ok(())
804        })
805    }
806
807    fn remove_dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<()>> {
808        let store = self.clone();
809        Box::pin(async move {
810            let mut state = store.state.lock().expect("in-memory store poisoned");
811            state.dialogs.retain(|dialog| dialog.chat_id != chat_id);
812            if let Some(messages) = state.messages.remove(&chat_id.get()) {
813                state.message_tombstones.extend(
814                    messages
815                        .into_iter()
816                        .map(|message| (chat_id.get(), message.message_id.get())),
817                );
818            }
819            state.participants.remove(&chat_id.get());
820            state.participant_snapshots.remove(&chat_id.get());
821            state
822                .reactions
823                .retain(|_, reaction| reaction.chat_id != chat_id);
824            state
825                .reaction_snapshots
826                .retain(|(stored_chat_id, _)| *stored_chat_id != chat_id.get());
827            state.read_states.remove(&chat_id.get());
828            state.chat_tombstones.insert(chat_id.get());
829            Ok(())
830        })
831    }
832
833    fn deleted_chat_ids(&self) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
834        let store = self.clone();
835        Box::pin(async move {
836            let mut ids = store
837                .state
838                .lock()
839                .expect("in-memory store poisoned")
840                .chat_tombstones
841                .iter()
842                .copied()
843                .map(InlineId::new)
844                .collect::<Vec<_>>();
845            ids.sort_by_key(|id| id.get());
846            Ok(ids)
847        })
848    }
849
850    fn record_users(&self, users: Vec<UserRecord>) -> BoxFuture<'static, StoreResult<()>> {
851        let store = self.clone();
852        Box::pin(async move {
853            let mut state = store.state.lock().expect("in-memory store poisoned");
854            for user in users {
855                state.users.insert(user.user_id.get(), user);
856            }
857            Ok(())
858        })
859    }
860
861    fn record_space(&self, space: SpaceRecord) -> BoxFuture<'static, StoreResult<()>> {
862        let store = self.clone();
863        Box::pin(async move {
864            store
865                .state
866                .lock()
867                .expect("in-memory store poisoned")
868                .spaces
869                .insert(space.space_id.get(), space);
870            Ok(())
871        })
872    }
873
874    fn space(&self, space_id: InlineId) -> BoxFuture<'static, StoreResult<Option<SpaceRecord>>> {
875        let store = self.clone();
876        Box::pin(async move {
877            Ok(store
878                .state
879                .lock()
880                .expect("in-memory store poisoned")
881                .spaces
882                .get(&space_id.get())
883                .cloned())
884        })
885    }
886
887    fn record_space_member(
888        &self,
889        member: SpaceMemberRecord,
890    ) -> BoxFuture<'static, StoreResult<()>> {
891        let store = self.clone();
892        Box::pin(async move {
893            store
894                .state
895                .lock()
896                .expect("in-memory store poisoned")
897                .space_members
898                .insert((member.space_id.get(), member.user_id.get()), member);
899            Ok(())
900        })
901    }
902
903    fn record_space_members(
904        &self,
905        space_id: InlineId,
906        members: Vec<SpaceMemberRecord>,
907    ) -> BoxFuture<'static, StoreResult<()>> {
908        let store = self.clone();
909        Box::pin(async move {
910            if members.iter().any(|member| member.space_id != space_id) {
911                return Err(StoreError::new(
912                    ClientErrorCategory::InvalidInput,
913                    "space member snapshot contains a different space id",
914                ));
915            }
916            let mut state = store.state.lock().expect("in-memory store poisoned");
917            state
918                .space_members
919                .retain(|(stored_space_id, _), _| *stored_space_id != space_id.get());
920            for member in members {
921                state
922                    .space_members
923                    .insert((space_id.get(), member.user_id.get()), member);
924            }
925            Ok(())
926        })
927    }
928
929    fn remove_space_member(
930        &self,
931        space_id: InlineId,
932        user_id: InlineId,
933    ) -> BoxFuture<'static, StoreResult<()>> {
934        let store = self.clone();
935        Box::pin(async move {
936            store
937                .state
938                .lock()
939                .expect("in-memory store poisoned")
940                .space_members
941                .remove(&(space_id.get(), user_id.get()));
942            Ok(())
943        })
944    }
945
946    fn space_members(
947        &self,
948        space_id: InlineId,
949    ) -> BoxFuture<'static, StoreResult<Vec<SpaceMemberRecord>>> {
950        let store = self.clone();
951        Box::pin(async move {
952            let mut members = store
953                .state
954                .lock()
955                .expect("in-memory store poisoned")
956                .space_members
957                .iter()
958                .filter(|((stored_space_id, _), _)| *stored_space_id == space_id.get())
959                .map(|(_, member)| member.clone())
960                .collect::<Vec<_>>();
961            members.sort_by_key(|member| member.user_id.get());
962            Ok(members)
963        })
964    }
965
966    fn record_user_settings(
967        &self,
968        settings: UserSettingsRecord,
969    ) -> BoxFuture<'static, StoreResult<()>> {
970        let store = self.clone();
971        Box::pin(async move {
972            store
973                .state
974                .lock()
975                .expect("in-memory store poisoned")
976                .user_settings = Some(settings);
977            Ok(())
978        })
979    }
980
981    fn user_settings(&self) -> BoxFuture<'static, StoreResult<Option<UserSettingsRecord>>> {
982        let store = self.clone();
983        Box::pin(async move {
984            Ok(store
985                .state
986                .lock()
987                .expect("in-memory store poisoned")
988                .user_settings
989                .clone())
990        })
991    }
992
993    fn record_chat_participants(
994        &self,
995        chat_id: InlineId,
996        participants: Vec<ChatParticipantRecord>,
997    ) -> BoxFuture<'static, StoreResult<()>> {
998        let store = self.clone();
999        Box::pin(async move {
1000            let snapshot = participants
1001                .into_iter()
1002                .map(|participant| (participant.user_id.get(), participant))
1003                .collect();
1004            let mut state = store.state.lock().expect("in-memory store poisoned");
1005            state.participants.insert(chat_id.get(), snapshot);
1006            state.participant_snapshots.insert(chat_id.get());
1007            Ok(())
1008        })
1009    }
1010
1011    fn record_chat_participant(
1012        &self,
1013        chat_id: InlineId,
1014        participant: ChatParticipantRecord,
1015    ) -> BoxFuture<'static, StoreResult<()>> {
1016        let store = self.clone();
1017        Box::pin(async move {
1018            store
1019                .state
1020                .lock()
1021                .expect("in-memory store poisoned")
1022                .participants
1023                .entry(chat_id.get())
1024                .or_default()
1025                .insert(participant.user_id.get(), participant);
1026            Ok(())
1027        })
1028    }
1029
1030    fn remove_chat_participant(
1031        &self,
1032        chat_id: InlineId,
1033        user_id: InlineId,
1034    ) -> BoxFuture<'static, StoreResult<()>> {
1035        let store = self.clone();
1036        Box::pin(async move {
1037            if let Some(participants) = store
1038                .state
1039                .lock()
1040                .expect("in-memory store poisoned")
1041                .participants
1042                .get_mut(&chat_id.get())
1043            {
1044                participants.remove(&user_id.get());
1045            }
1046            Ok(())
1047        })
1048    }
1049
1050    fn chat_participants(
1051        &self,
1052        chat_id: InlineId,
1053    ) -> BoxFuture<'static, StoreResult<Vec<ChatParticipantRecord>>> {
1054        let store = self.clone();
1055        Box::pin(async move {
1056            let mut participants = store
1057                .state
1058                .lock()
1059                .expect("in-memory store poisoned")
1060                .participants
1061                .get(&chat_id.get())
1062                .map(|participants| participants.values().cloned().collect::<Vec<_>>())
1063                .unwrap_or_default();
1064            sort_participants(&mut participants);
1065            Ok(participants)
1066        })
1067    }
1068
1069    fn chat_participants_complete(
1070        &self,
1071        chat_id: InlineId,
1072    ) -> BoxFuture<'static, StoreResult<bool>> {
1073        let store = self.clone();
1074        Box::pin(async move {
1075            Ok(store
1076                .state
1077                .lock()
1078                .expect("in-memory store poisoned")
1079                .participant_snapshots
1080                .contains(&chat_id.get()))
1081        })
1082    }
1083
1084    fn history(&self, request: HistoryRequest) -> BoxFuture<'static, StoreResult<HistoryPage>> {
1085        let store = self.clone();
1086        Box::pin(async move {
1087            let state = store.state.lock().expect("in-memory store poisoned");
1088            let mut messages = state
1089                .messages
1090                .get(&request.chat_id.get())
1091                .cloned()
1092                .unwrap_or_default();
1093            messages.sort_by_key(|message| (message.timestamp, message.message_id.get()));
1094            if request.before_message_id.is_some() && request.after_message_id.is_some() {
1095                return Err(StoreError::new(
1096                    ClientErrorCategory::InvalidInput,
1097                    "history request cannot specify both before_message_id and after_message_id",
1098                ));
1099            }
1100            if let Some(before) = request.before_message_id {
1101                messages.retain(|message| message.message_id.get() < before.get());
1102            }
1103            if let Some(after) = request.after_message_id {
1104                messages.retain(|message| message.message_id.get() > after.get());
1105            }
1106            let limit = request.limit.unwrap_or(50).max(1) as usize;
1107            let has_more = messages.len() > limit;
1108            if has_more {
1109                if request.after_message_id.is_some() {
1110                    messages.truncate(limit);
1111                } else {
1112                    let start = messages.len() - limit;
1113                    messages = messages[start..].to_vec();
1114                }
1115            }
1116            let users = users_for_messages_from_memory(&state.users, &messages);
1117            Ok(HistoryPage {
1118                messages,
1119                users,
1120                has_more,
1121                next_cursor: None,
1122            })
1123        })
1124    }
1125
1126    fn record_message(&self, message: MessageRecord) -> BoxFuture<'static, StoreResult<()>> {
1127        let store = self.clone();
1128        Box::pin(async move {
1129            let mut state = store.state.lock().expect("in-memory store poisoned");
1130            state
1131                .message_tombstones
1132                .remove(&(message.chat_id.get(), message.message_id.get()));
1133            insert_message(&mut state.messages, message);
1134            Ok(())
1135        })
1136    }
1137
1138    fn record_message_deleted(
1139        &self,
1140        chat_id: InlineId,
1141        message_id: InlineId,
1142    ) -> BoxFuture<'static, StoreResult<()>> {
1143        let store = self.clone();
1144        Box::pin(async move {
1145            let mut state = store.state.lock().expect("in-memory store poisoned");
1146            if let Some(messages) = state.messages.get_mut(&chat_id.get()) {
1147                messages.retain(|message| message.message_id != message_id);
1148            }
1149            state.reactions.retain(|_, reaction| {
1150                reaction.chat_id != chat_id || reaction.message_id != message_id
1151            });
1152            state
1153                .reaction_snapshots
1154                .remove(&(chat_id.get(), message_id.get()));
1155            state
1156                .message_tombstones
1157                .insert((chat_id.get(), message_id.get()));
1158            Ok(())
1159        })
1160    }
1161
1162    fn message_deleted(
1163        &self,
1164        chat_id: InlineId,
1165        message_id: InlineId,
1166    ) -> BoxFuture<'static, StoreResult<bool>> {
1167        let store = self.clone();
1168        Box::pin(async move {
1169            Ok(store
1170                .state
1171                .lock()
1172                .expect("in-memory store poisoned")
1173                .message_tombstones
1174                .contains(&(chat_id.get(), message_id.get())))
1175        })
1176    }
1177
1178    fn deleted_message_ids(
1179        &self,
1180        chat_id: InlineId,
1181    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1182        let store = self.clone();
1183        Box::pin(async move {
1184            let mut ids = store
1185                .state
1186                .lock()
1187                .expect("in-memory store poisoned")
1188                .message_tombstones
1189                .iter()
1190                .filter(|(stored_chat_id, _)| *stored_chat_id == chat_id.get())
1191                .map(|(_, message_id)| InlineId::new(*message_id))
1192                .collect::<Vec<_>>();
1193            ids.sort_by_key(|id| id.get());
1194            Ok(ids)
1195        })
1196    }
1197
1198    fn clear_chat_messages(
1199        &self,
1200        chat_id: InlineId,
1201        before_date: Option<i64>,
1202    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1203        let store = self.clone();
1204        Box::pin(async move {
1205            let mut state = store.state.lock().expect("in-memory store poisoned");
1206            let mut removed = Vec::new();
1207            if let Some(messages) = state.messages.get_mut(&chat_id.get()) {
1208                messages.retain(|message| {
1209                    let should_remove = before_date.is_none_or(|date| message.timestamp < date);
1210                    if should_remove {
1211                        removed.push(message.message_id);
1212                    }
1213                    !should_remove
1214                });
1215            }
1216            let removed_ids = removed.iter().map(|id| id.get()).collect::<HashSet<_>>();
1217            state.reactions.retain(|_, reaction| {
1218                reaction.chat_id != chat_id || !removed_ids.contains(&reaction.message_id.get())
1219            });
1220            state
1221                .reaction_snapshots
1222                .retain(|(stored_chat_id, message_id)| {
1223                    *stored_chat_id != chat_id.get() || !removed_ids.contains(message_id)
1224                });
1225            state.message_tombstones.extend(
1226                removed_ids
1227                    .iter()
1228                    .map(|message_id| (chat_id.get(), *message_id)),
1229            );
1230            let last_message_id = state.messages.get(&chat_id.get()).and_then(|messages| {
1231                messages
1232                    .iter()
1233                    .map(|message| message.message_id)
1234                    .max_by_key(|id| id.get())
1235            });
1236            if let Some(dialog) = state
1237                .dialogs
1238                .iter_mut()
1239                .find(|dialog| dialog.chat_id == chat_id)
1240            {
1241                dialog.last_message_id = last_message_id;
1242            }
1243            removed.sort_by_key(|id| id.get());
1244            Ok(removed)
1245        })
1246    }
1247
1248    fn chat_ids_in_space(
1249        &self,
1250        space_id: InlineId,
1251    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1252        let store = self.clone();
1253        Box::pin(async move {
1254            let mut chat_ids = store
1255                .state
1256                .lock()
1257                .expect("in-memory store poisoned")
1258                .dialogs
1259                .iter()
1260                .filter(|dialog| dialog.space_id == Some(space_id))
1261                .map(|dialog| dialog.chat_id)
1262                .collect::<Vec<_>>();
1263            chat_ids.sort_by_key(|id| id.get());
1264            Ok(chat_ids)
1265        })
1266    }
1267
1268    fn record_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>> {
1269        let store = self.clone();
1270        Box::pin(async move {
1271            let key = reaction_key(&reaction);
1272            store
1273                .state
1274                .lock()
1275                .expect("in-memory store poisoned")
1276                .reactions
1277                .insert(key, reaction);
1278            Ok(())
1279        })
1280    }
1281
1282    fn remove_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>> {
1283        let store = self.clone();
1284        Box::pin(async move {
1285            store
1286                .state
1287                .lock()
1288                .expect("in-memory store poisoned")
1289                .reactions
1290                .remove(&reaction_key(&reaction));
1291            Ok(())
1292        })
1293    }
1294
1295    fn replace_message_reactions(
1296        &self,
1297        chat_id: InlineId,
1298        message_id: InlineId,
1299        reactions: Vec<StoredReaction>,
1300    ) -> BoxFuture<'static, StoreResult<()>> {
1301        let store = self.clone();
1302        Box::pin(async move {
1303            let mut state = store.state.lock().expect("in-memory store poisoned");
1304            state.reactions.retain(|_, reaction| {
1305                reaction.chat_id != chat_id || reaction.message_id != message_id
1306            });
1307            for reaction in reactions {
1308                state.reactions.insert(reaction_key(&reaction), reaction);
1309            }
1310            state
1311                .reaction_snapshots
1312                .insert((chat_id.get(), message_id.get()));
1313            Ok(())
1314        })
1315    }
1316
1317    fn reactions(
1318        &self,
1319        chat_id: InlineId,
1320        message_id: InlineId,
1321    ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>> {
1322        let store = self.clone();
1323        Box::pin(async move {
1324            let mut reactions = store
1325                .state
1326                .lock()
1327                .expect("in-memory store poisoned")
1328                .reactions
1329                .values()
1330                .filter(|reaction| reaction.chat_id == chat_id && reaction.message_id == message_id)
1331                .cloned()
1332                .collect::<Vec<_>>();
1333            sort_reactions(&mut reactions);
1334            Ok(reactions)
1335        })
1336    }
1337
1338    fn reactions_for_chat(
1339        &self,
1340        chat_id: InlineId,
1341    ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>> {
1342        let store = self.clone();
1343        Box::pin(async move {
1344            let mut reactions = store
1345                .state
1346                .lock()
1347                .expect("in-memory store poisoned")
1348                .reactions
1349                .values()
1350                .filter(|reaction| reaction.chat_id == chat_id)
1351                .cloned()
1352                .collect::<Vec<_>>();
1353            reactions.sort_by(|left, right| {
1354                (
1355                    left.message_id.get(),
1356                    left.user_id.get(),
1357                    left.reaction.as_str(),
1358                )
1359                    .cmp(&(
1360                        right.message_id.get(),
1361                        right.user_id.get(),
1362                        right.reaction.as_str(),
1363                    ))
1364            });
1365            Ok(reactions)
1366        })
1367    }
1368
1369    fn reaction_snapshot_message_ids(
1370        &self,
1371        chat_id: InlineId,
1372    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
1373        let store = self.clone();
1374        Box::pin(async move {
1375            let mut ids = store
1376                .state
1377                .lock()
1378                .expect("in-memory store poisoned")
1379                .reaction_snapshots
1380                .iter()
1381                .filter(|(stored_chat_id, _)| *stored_chat_id == chat_id.get())
1382                .map(|(_, message_id)| InlineId::new(*message_id))
1383                .collect::<Vec<_>>();
1384            ids.sort_by_key(|id| id.get());
1385            Ok(ids)
1386        })
1387    }
1388
1389    fn record_read_state(&self, state: StoredReadState) -> BoxFuture<'static, StoreResult<()>> {
1390        let store = self.clone();
1391        Box::pin(async move {
1392            store
1393                .state
1394                .lock()
1395                .expect("in-memory store poisoned")
1396                .read_states
1397                .insert(state.chat_id.get(), state);
1398            Ok(())
1399        })
1400    }
1401
1402    fn read_state(
1403        &self,
1404        chat_id: InlineId,
1405    ) -> BoxFuture<'static, StoreResult<Option<StoredReadState>>> {
1406        let store = self.clone();
1407        Box::pin(async move {
1408            Ok(store
1409                .state
1410                .lock()
1411                .expect("in-memory store poisoned")
1412                .read_states
1413                .get(&chat_id.get())
1414                .copied())
1415        })
1416    }
1417
1418    fn record_transaction(
1419        &self,
1420        transaction: StoredTransaction,
1421    ) -> BoxFuture<'static, StoreResult<()>> {
1422        let store = self.clone();
1423        Box::pin(async move {
1424            let mut state = store.state.lock().expect("in-memory store poisoned");
1425            state.transactions.insert(
1426                transaction.identity.transaction_id.as_str().to_owned(),
1427                transaction,
1428            );
1429            Ok(())
1430        })
1431    }
1432
1433    fn transaction(
1434        &self,
1435        transaction_id: TransactionId,
1436    ) -> BoxFuture<'static, StoreResult<Option<StoredTransaction>>> {
1437        let store = self.clone();
1438        Box::pin(async move {
1439            Ok(store
1440                .state
1441                .lock()
1442                .expect("in-memory store poisoned")
1443                .transactions
1444                .get(transaction_id.as_str())
1445                .cloned())
1446        })
1447    }
1448}
1449
1450/// SQLite-backed durable store.
1451#[derive(Clone)]
1452pub struct SqliteStore {
1453    connection: Arc<Mutex<Connection>>,
1454}
1455
1456impl fmt::Debug for SqliteStore {
1457    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1458        f.debug_struct("SqliteStore")
1459            .field("connection", &"<sqlite>")
1460            .finish()
1461    }
1462}
1463
1464impl SqliteStore {
1465    /// Opens a SQLite store at `path`, creating parent directories and schema.
1466    pub fn open(path: impl AsRef<Path>) -> StoreResult<Self> {
1467        let path = path.as_ref();
1468        if let Some(parent) = path
1469            .parent()
1470            .filter(|parent| !parent.as_os_str().is_empty())
1471        {
1472            std::fs::create_dir_all(parent).map_err(|error| {
1473                StoreError::internal(format!("create store directory: {error}"))
1474            })?;
1475        }
1476        prepare_private_sqlite_file(path)?;
1477        let connection = Connection::open(path).map_err(sqlite_error)?;
1478        Self::from_connection(connection)
1479    }
1480
1481    /// Opens an in-memory SQLite store.
1482    pub fn open_in_memory() -> StoreResult<Self> {
1483        let connection = Connection::open_in_memory().map_err(sqlite_error)?;
1484        Self::from_connection(connection)
1485    }
1486
1487    fn from_connection(connection: Connection) -> StoreResult<Self> {
1488        migrate_sqlite(&connection)?;
1489        Ok(Self {
1490            connection: Arc::new(Mutex::new(connection)),
1491        })
1492    }
1493
1494    /// Inserts or replaces a dialog.
1495    pub fn upsert_dialog(&self, dialog: DialogRecord) -> StoreResult<()> {
1496        let connection = self.connection.lock().expect("sqlite store poisoned");
1497        upsert_sqlite_dialog(&connection, dialog)
1498    }
1499
1500    /// Inserts or replaces a user summary.
1501    pub fn upsert_user(&self, user: UserRecord) -> StoreResult<()> {
1502        let connection = self.connection.lock().expect("sqlite store poisoned");
1503        upsert_sqlite_user(&connection, user)
1504    }
1505
1506    /// Inserts a message into history.
1507    pub fn insert_message(&self, message: MessageRecord) -> StoreResult<()> {
1508        self.record_message_sync(message)
1509    }
1510
1511    fn save_session_sync(&self, session: StoredSession) -> StoreResult<()> {
1512        let auth_json = serde_json::to_string(&session.auth)
1513            .map_err(|error| StoreError::internal(format!("encode session auth: {error}")))?;
1514        let connection = self.connection.lock().expect("sqlite store poisoned");
1515        connection
1516            .execute(
1517                "INSERT INTO sessions (id, auth_json, account_namespace, updated_at)
1518                 VALUES (1, ?1, ?2, ?3)
1519                 ON CONFLICT(id) DO UPDATE SET
1520                   auth_json = excluded.auth_json,
1521                   account_namespace = excluded.account_namespace,
1522                   updated_at = excluded.updated_at",
1523                params![auth_json, session.account_namespace, now_seconds()],
1524            )
1525            .map_err(sqlite_error)?;
1526        Ok(())
1527    }
1528
1529    fn load_session_sync(&self) -> StoreResult<Option<StoredSession>> {
1530        let connection = self.connection.lock().expect("sqlite store poisoned");
1531        let row = connection
1532            .query_row(
1533                "SELECT auth_json, account_namespace FROM sessions WHERE id = 1",
1534                [],
1535                |row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<String>>(1)?)),
1536            )
1537            .optional()
1538            .map_err(sqlite_error)?;
1539
1540        row.map(|(auth_json, account_namespace)| {
1541            let auth = serde_json::from_str::<AuthCredential>(&auth_json)
1542                .map_err(|error| StoreError::internal(format!("decode session auth: {error}")))?;
1543            Ok(StoredSession {
1544                auth,
1545                account_namespace,
1546            })
1547        })
1548        .transpose()
1549    }
1550
1551    fn clear_session_sync(&self) -> StoreResult<()> {
1552        let connection = self.connection.lock().expect("sqlite store poisoned");
1553        connection
1554            .execute("DELETE FROM sessions WHERE id = 1", [])
1555            .map_err(sqlite_error)?;
1556        Ok(())
1557    }
1558
1559    fn clear_account_data_sync(&self) -> StoreResult<()> {
1560        let mut connection = self.connection.lock().expect("sqlite store poisoned");
1561        let transaction = connection.transaction().map_err(sqlite_error)?;
1562        for table in [
1563            "sync_state",
1564            "sync_buckets",
1565            "pending_sync_batches",
1566            "dialogs",
1567            "chat_tombstones",
1568            "users",
1569            "spaces",
1570            "space_members",
1571            "user_settings",
1572            "chat_participants",
1573            "chat_participant_snapshots",
1574            "messages",
1575            "message_tombstones",
1576            "reactions",
1577            "reaction_snapshots",
1578            "read_states",
1579            "transactions",
1580        ] {
1581            transaction
1582                .execute(&format!("DELETE FROM {table}"), [])
1583                .map_err(sqlite_error)?;
1584        }
1585        transaction.commit().map_err(sqlite_error)?;
1586        Ok(())
1587    }
1588
1589    fn sync_state_sync(&self) -> StoreResult<SyncState> {
1590        let connection = self.connection.lock().expect("sqlite store poisoned");
1591        let last_sync_date = connection
1592            .query_row(
1593                "SELECT last_sync_date FROM sync_state WHERE id = 1",
1594                [],
1595                |row| row.get::<_, i64>(0),
1596            )
1597            .optional()
1598            .map_err(sqlite_error)?
1599            .unwrap_or_default();
1600        Ok(SyncState { last_sync_date })
1601    }
1602
1603    fn save_sync_state_sync(&self, state: SyncState) -> StoreResult<()> {
1604        let connection = self.connection.lock().expect("sqlite store poisoned");
1605        connection
1606            .execute(
1607                "INSERT INTO sync_state (id, last_sync_date, updated_at)
1608                 VALUES (1, ?1, ?2)
1609                 ON CONFLICT(id) DO UPDATE SET
1610                   last_sync_date = excluded.last_sync_date,
1611                   updated_at = excluded.updated_at",
1612                params![state.last_sync_date, now_seconds()],
1613            )
1614            .map_err(sqlite_error)?;
1615        Ok(())
1616    }
1617
1618    fn clear_sync_state_sync(&self) -> StoreResult<()> {
1619        let mut connection = self.connection.lock().expect("sqlite store poisoned");
1620        let transaction = connection.transaction().map_err(sqlite_error)?;
1621        transaction
1622            .execute("DELETE FROM sync_state", [])
1623            .map_err(sqlite_error)?;
1624        transaction
1625            .execute("DELETE FROM sync_buckets", [])
1626            .map_err(sqlite_error)?;
1627        transaction.commit().map_err(sqlite_error)?;
1628        Ok(())
1629    }
1630
1631    fn sync_bucket_state_sync(&self, key: SyncBucketKey) -> StoreResult<SyncBucketState> {
1632        let (kind, entity_id) = sync_bucket_key_parts(key);
1633        let connection = self.connection.lock().expect("sqlite store poisoned");
1634        let state = connection
1635            .query_row(
1636                "SELECT seq, date FROM sync_buckets
1637                 WHERE bucket_kind = ?1 AND entity_id = ?2",
1638                params![kind, entity_id],
1639                |row| {
1640                    Ok(SyncBucketState {
1641                        seq: row.get(0)?,
1642                        date: row.get(1)?,
1643                    })
1644                },
1645            )
1646            .optional()
1647            .map_err(sqlite_error)?
1648            .unwrap_or_default();
1649        Ok(state)
1650    }
1651
1652    fn save_sync_bucket_state_sync(
1653        &self,
1654        key: SyncBucketKey,
1655        state: SyncBucketState,
1656    ) -> StoreResult<()> {
1657        let (kind, entity_id) = sync_bucket_key_parts(key);
1658        let connection = self.connection.lock().expect("sqlite store poisoned");
1659        upsert_sync_bucket(&connection, kind, entity_id, state)
1660    }
1661
1662    fn save_sync_bucket_states_sync(
1663        &self,
1664        states: Vec<(SyncBucketKey, SyncBucketState)>,
1665    ) -> StoreResult<()> {
1666        let mut connection = self.connection.lock().expect("sqlite store poisoned");
1667        let transaction = connection.transaction().map_err(sqlite_error)?;
1668        for (key, state) in states {
1669            let (kind, entity_id) = sync_bucket_key_parts(key);
1670            upsert_sync_bucket(&transaction, kind, entity_id, state)?;
1671        }
1672        transaction.commit().map_err(sqlite_error)?;
1673        Ok(())
1674    }
1675
1676    fn remove_sync_bucket_state_sync(&self, key: SyncBucketKey) -> StoreResult<()> {
1677        let (kind, entity_id) = sync_bucket_key_parts(key);
1678        let connection = self.connection.lock().expect("sqlite store poisoned");
1679        connection
1680            .execute(
1681                "DELETE FROM sync_buckets WHERE bucket_kind = ?1 AND entity_id = ?2",
1682                params![kind, entity_id],
1683            )
1684            .map_err(sqlite_error)?;
1685        Ok(())
1686    }
1687
1688    fn save_pending_sync_batch_sync(&self, batch: PendingSyncBatch) -> StoreResult<()> {
1689        let (kind, entity_id) = sync_bucket_key_parts(batch.key);
1690        let connection = self.connection.lock().expect("sqlite store poisoned");
1691        connection
1692            .execute(
1693                "INSERT INTO pending_sync_batches (
1694                   bucket_kind, entity_id, seq, date, payload, updated_at
1695                 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
1696                 ON CONFLICT(bucket_kind, entity_id) DO UPDATE SET
1697                   seq = excluded.seq,
1698                   date = excluded.date,
1699                   payload = excluded.payload,
1700                   updated_at = excluded.updated_at",
1701                params![
1702                    kind,
1703                    entity_id,
1704                    batch.committed_state.seq,
1705                    batch.committed_state.date,
1706                    batch.payload,
1707                    now_seconds(),
1708                ],
1709            )
1710            .map_err(sqlite_error)?;
1711        Ok(())
1712    }
1713
1714    fn pending_sync_batches_sync(&self) -> StoreResult<Vec<PendingSyncBatch>> {
1715        let connection = self.connection.lock().expect("sqlite store poisoned");
1716        let mut statement = connection
1717            .prepare(
1718                "SELECT bucket_kind, entity_id, seq, date, payload
1719                 FROM pending_sync_batches
1720                 ORDER BY bucket_kind ASC, entity_id ASC",
1721            )
1722            .map_err(sqlite_error)?;
1723        statement
1724            .query_map([], |row| {
1725                let kind = row.get::<_, String>(0)?;
1726                let entity_id = row.get::<_, i64>(1)?;
1727                let key = sync_bucket_key_from_parts(&kind, entity_id).map_err(|error| {
1728                    rusqlite::Error::FromSqlConversionFailure(0, Type::Text, Box::new(error))
1729                })?;
1730                Ok(PendingSyncBatch {
1731                    key,
1732                    committed_state: SyncBucketState {
1733                        seq: row.get(2)?,
1734                        date: row.get(3)?,
1735                    },
1736                    payload: row.get(4)?,
1737                })
1738            })
1739            .map_err(sqlite_error)?
1740            .collect::<Result<Vec<_>, _>>()
1741            .map_err(sqlite_error)
1742    }
1743
1744    fn commit_pending_sync_batch_sync(
1745        &self,
1746        key: SyncBucketKey,
1747        state: SyncBucketState,
1748    ) -> StoreResult<()> {
1749        let (kind, entity_id) = sync_bucket_key_parts(key);
1750        let mut connection = self.connection.lock().expect("sqlite store poisoned");
1751        let transaction = connection.transaction().map_err(sqlite_error)?;
1752        upsert_sync_bucket(&transaction, kind, entity_id, state)?;
1753        transaction
1754            .execute(
1755                "DELETE FROM pending_sync_batches
1756                 WHERE bucket_kind = ?1 AND entity_id = ?2",
1757                params![kind, entity_id],
1758            )
1759            .map_err(sqlite_error)?;
1760        transaction.commit().map_err(sqlite_error)?;
1761        Ok(())
1762    }
1763
1764    fn dialogs_sync(&self, request: DialogsRequest) -> StoreResult<DialogsPage> {
1765        let start = parse_cursor(request.cursor.as_deref())?;
1766        let limit = request.limit.unwrap_or(50).max(1) as usize;
1767        let connection = self.connection.lock().expect("sqlite store poisoned");
1768        let rows = {
1769            let mut stmt = connection
1770                .prepare(
1771                    "SELECT d.chat_id, d.peer_user_id, d.title, d.emoji, d.last_message_id,
1772                            (SELECT MAX(m.message_id) FROM messages m WHERE m.chat_id = d.chat_id) AS synced_through_message_id,
1773                            d.unread_count, d.space_id, d.is_public, d.archived, d.pinned,
1774                            d.open, d.chat_list_hidden, d.list_order, d.pinned_order,
1775                            d.notification_mode, d.follow_mode, d.pinned_message_ids_json
1776                     FROM dialogs d
1777                     ORDER BY COALESCE(last_message_id, 0) DESC, chat_id ASC
1778                     LIMIT ?1 OFFSET ?2",
1779                )
1780                .map_err(sqlite_error)?;
1781            stmt.query_map(
1782                params![(limit + 1) as i64, start as i64],
1783                sqlite_dialog_from_row,
1784            )
1785            .map_err(sqlite_error)?
1786            .collect::<Result<Vec<_>, _>>()
1787            .map_err(sqlite_error)?
1788        };
1789
1790        let has_more = rows.len() > limit;
1791        let dialogs = rows.into_iter().take(limit).collect::<Vec<_>>();
1792        let users = all_sqlite_users(&connection)?;
1793        Ok(DialogsPage {
1794            next_cursor: has_more.then(|| (start + dialogs.len()).to_string()),
1795            dialogs,
1796            users,
1797        })
1798    }
1799
1800    fn dialog_sync(&self, chat_id: InlineId) -> StoreResult<Option<DialogRecord>> {
1801        let connection = self.connection.lock().expect("sqlite store poisoned");
1802        connection
1803            .query_row(
1804                "SELECT d.chat_id, d.peer_user_id, d.title, d.emoji, d.last_message_id,
1805                        (SELECT MAX(m.message_id) FROM messages m WHERE m.chat_id = d.chat_id),
1806                        d.unread_count, d.space_id, d.is_public, d.archived, d.pinned,
1807                        d.open, d.chat_list_hidden, d.list_order, d.pinned_order,
1808                        d.notification_mode, d.follow_mode, d.pinned_message_ids_json
1809                 FROM dialogs d WHERE d.chat_id = ?1",
1810                params![chat_id.get()],
1811                sqlite_dialog_from_row,
1812            )
1813            .optional()
1814            .map_err(sqlite_error)
1815    }
1816
1817    fn remove_dialog_sync(&self, chat_id: InlineId) -> StoreResult<()> {
1818        let mut connection = self.connection.lock().expect("sqlite store poisoned");
1819        let transaction = connection.transaction().map_err(sqlite_error)?;
1820        transaction
1821            .execute(
1822                "INSERT INTO chat_tombstones (chat_id, deleted_at)
1823                 VALUES (?1, ?2)
1824                 ON CONFLICT(chat_id) DO UPDATE SET deleted_at = excluded.deleted_at",
1825                params![chat_id.get(), now_seconds()],
1826            )
1827            .map_err(sqlite_error)?;
1828        transaction
1829            .execute(
1830                "INSERT INTO message_tombstones (chat_id, message_id, deleted_at)
1831                 SELECT chat_id, message_id, ?2 FROM messages WHERE chat_id = ?1
1832                 ON CONFLICT(chat_id, message_id) DO UPDATE SET deleted_at = excluded.deleted_at",
1833                params![chat_id.get(), now_seconds()],
1834            )
1835            .map_err(sqlite_error)?;
1836        for table in [
1837            "dialogs",
1838            "messages",
1839            "chat_participants",
1840            "chat_participant_snapshots",
1841            "reactions",
1842            "reaction_snapshots",
1843            "read_states",
1844        ] {
1845            transaction
1846                .execute(
1847                    &format!("DELETE FROM {table} WHERE chat_id = ?1"),
1848                    params![chat_id.get()],
1849                )
1850                .map_err(sqlite_error)?;
1851        }
1852        transaction.commit().map_err(sqlite_error)?;
1853        Ok(())
1854    }
1855
1856    fn deleted_chat_ids_sync(&self) -> StoreResult<Vec<InlineId>> {
1857        let connection = self.connection.lock().expect("sqlite store poisoned");
1858        let mut statement = connection
1859            .prepare("SELECT chat_id FROM chat_tombstones ORDER BY chat_id ASC")
1860            .map_err(sqlite_error)?;
1861        statement
1862            .query_map([], |row| row.get::<_, i64>(0).map(InlineId::new))
1863            .map_err(sqlite_error)?
1864            .collect::<Result<Vec<_>, _>>()
1865            .map_err(sqlite_error)
1866    }
1867
1868    fn history_sync(&self, request: HistoryRequest) -> StoreResult<HistoryPage> {
1869        let limit = request.limit.unwrap_or(50).max(1) as usize;
1870        let connection = self.connection.lock().expect("sqlite store poisoned");
1871        let rows = if request.before_message_id.is_some() && request.after_message_id.is_some() {
1872            return Err(StoreError::new(
1873                ClientErrorCategory::InvalidInput,
1874                "history request cannot specify both before_message_id and after_message_id",
1875            ));
1876        } else if let Some(before) = request.before_message_id {
1877            let mut stmt = connection
1878                .prepare(
1879                    "SELECT chat_id, message_id, sender_id, timestamp, is_outgoing,
1880                            content_json, reply_to_message_id, transaction_json
1881                     FROM messages
1882                     WHERE chat_id = ?1 AND message_id < ?2
1883                     ORDER BY message_id DESC
1884                     LIMIT ?3",
1885                )
1886                .map_err(sqlite_error)?;
1887            query_message_rows(
1888                &mut stmt,
1889                params![request.chat_id.get(), before.get(), (limit + 1) as i64],
1890            )?
1891        } else if let Some(after) = request.after_message_id {
1892            let mut stmt = connection
1893                .prepare(
1894                    "SELECT chat_id, message_id, sender_id, timestamp, is_outgoing,
1895                            content_json, reply_to_message_id, transaction_json
1896                     FROM messages
1897                     WHERE chat_id = ?1 AND message_id > ?2
1898                     ORDER BY message_id ASC
1899                     LIMIT ?3",
1900                )
1901                .map_err(sqlite_error)?;
1902            query_message_rows(
1903                &mut stmt,
1904                params![request.chat_id.get(), after.get(), (limit + 1) as i64],
1905            )?
1906        } else {
1907            let mut stmt = connection
1908                .prepare(
1909                    "SELECT chat_id, message_id, sender_id, timestamp, is_outgoing,
1910                            content_json, reply_to_message_id, transaction_json
1911                     FROM messages
1912                     WHERE chat_id = ?1
1913                     ORDER BY message_id DESC
1914                     LIMIT ?2",
1915                )
1916                .map_err(sqlite_error)?;
1917            query_message_rows(
1918                &mut stmt,
1919                params![request.chat_id.get(), (limit + 1) as i64],
1920            )?
1921        };
1922
1923        let has_more = rows.len() > limit;
1924        let mut messages = rows
1925            .into_iter()
1926            .take(limit)
1927            .map(raw_sqlite_message_to_record)
1928            .collect::<StoreResult<Vec<_>>>()?;
1929        messages.sort_by_key(|message| (message.timestamp, message.message_id.get()));
1930        let users = sqlite_users_for_messages(&connection, &messages)?;
1931        Ok(HistoryPage {
1932            messages,
1933            users,
1934            has_more,
1935            next_cursor: None,
1936        })
1937    }
1938
1939    fn record_users_sync(&self, users: Vec<UserRecord>) -> StoreResult<()> {
1940        if users.is_empty() {
1941            return Ok(());
1942        }
1943        let connection = self.connection.lock().expect("sqlite store poisoned");
1944        for user in users {
1945            upsert_sqlite_user(&connection, user)?;
1946        }
1947        Ok(())
1948    }
1949
1950    fn record_space_sync(&self, space: SpaceRecord) -> StoreResult<()> {
1951        let payload = serde_json::to_string(&space)
1952            .map_err(|error| StoreError::internal(format!("encode space: {error}")))?;
1953        let connection = self.connection.lock().expect("sqlite store poisoned");
1954        connection
1955            .execute(
1956                "INSERT INTO spaces (space_id, record_json, updated_at)
1957                 VALUES (?1, ?2, ?3)
1958                 ON CONFLICT(space_id) DO UPDATE SET
1959                   record_json = excluded.record_json,
1960                   updated_at = excluded.updated_at",
1961                params![space.space_id.get(), payload, now_seconds()],
1962            )
1963            .map_err(sqlite_error)?;
1964        Ok(())
1965    }
1966
1967    fn space_sync(&self, space_id: InlineId) -> StoreResult<Option<SpaceRecord>> {
1968        let connection = self.connection.lock().expect("sqlite store poisoned");
1969        connection
1970            .query_row(
1971                "SELECT record_json FROM spaces WHERE space_id = ?1",
1972                params![space_id.get()],
1973                |row| row.get::<_, String>(0),
1974            )
1975            .optional()
1976            .map_err(sqlite_error)?
1977            .map(|payload| {
1978                serde_json::from_str(&payload)
1979                    .map_err(|error| StoreError::internal(format!("decode space: {error}")))
1980            })
1981            .transpose()
1982    }
1983
1984    fn record_space_member_sync(&self, member: SpaceMemberRecord) -> StoreResult<()> {
1985        let payload = serde_json::to_string(&member)
1986            .map_err(|error| StoreError::internal(format!("encode space member: {error}")))?;
1987        let connection = self.connection.lock().expect("sqlite store poisoned");
1988        connection
1989            .execute(
1990                "INSERT INTO space_members (space_id, user_id, record_json, updated_at)
1991                 VALUES (?1, ?2, ?3, ?4)
1992                 ON CONFLICT(space_id, user_id) DO UPDATE SET
1993                   record_json = excluded.record_json,
1994                   updated_at = excluded.updated_at",
1995                params![
1996                    member.space_id.get(),
1997                    member.user_id.get(),
1998                    payload,
1999                    now_seconds()
2000                ],
2001            )
2002            .map_err(sqlite_error)?;
2003        Ok(())
2004    }
2005
2006    fn record_space_members_sync(
2007        &self,
2008        space_id: InlineId,
2009        members: Vec<SpaceMemberRecord>,
2010    ) -> StoreResult<()> {
2011        if members.iter().any(|member| member.space_id != space_id) {
2012            return Err(StoreError::new(
2013                ClientErrorCategory::InvalidInput,
2014                "space member snapshot contains a different space id",
2015            ));
2016        }
2017        let mut encoded = Vec::with_capacity(members.len());
2018        for member in members {
2019            let payload = serde_json::to_string(&member)
2020                .map_err(|error| StoreError::internal(format!("encode space member: {error}")))?;
2021            encoded.push((member, payload));
2022        }
2023        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2024        let transaction = connection.transaction().map_err(sqlite_error)?;
2025        transaction
2026            .execute(
2027                "DELETE FROM space_members WHERE space_id = ?1",
2028                params![space_id.get()],
2029            )
2030            .map_err(sqlite_error)?;
2031        for (member, payload) in encoded {
2032            transaction
2033                .execute(
2034                    "INSERT INTO space_members (space_id, user_id, record_json, updated_at)
2035                     VALUES (?1, ?2, ?3, ?4)",
2036                    params![
2037                        member.space_id.get(),
2038                        member.user_id.get(),
2039                        payload,
2040                        now_seconds()
2041                    ],
2042                )
2043                .map_err(sqlite_error)?;
2044        }
2045        transaction.commit().map_err(sqlite_error)?;
2046        Ok(())
2047    }
2048
2049    fn remove_space_member_sync(&self, space_id: InlineId, user_id: InlineId) -> StoreResult<()> {
2050        let connection = self.connection.lock().expect("sqlite store poisoned");
2051        connection
2052            .execute(
2053                "DELETE FROM space_members WHERE space_id = ?1 AND user_id = ?2",
2054                params![space_id.get(), user_id.get()],
2055            )
2056            .map_err(sqlite_error)?;
2057        Ok(())
2058    }
2059
2060    fn space_members_sync(&self, space_id: InlineId) -> StoreResult<Vec<SpaceMemberRecord>> {
2061        let connection = self.connection.lock().expect("sqlite store poisoned");
2062        let mut statement = connection
2063            .prepare(
2064                "SELECT record_json FROM space_members
2065                 WHERE space_id = ?1 ORDER BY user_id ASC",
2066            )
2067            .map_err(sqlite_error)?;
2068        let payloads = statement
2069            .query_map(params![space_id.get()], |row| row.get::<_, String>(0))
2070            .map_err(sqlite_error)?
2071            .collect::<Result<Vec<_>, _>>()
2072            .map_err(sqlite_error)?;
2073        payloads
2074            .into_iter()
2075            .map(|payload| {
2076                serde_json::from_str(&payload)
2077                    .map_err(|error| StoreError::internal(format!("decode space member: {error}")))
2078            })
2079            .collect()
2080    }
2081
2082    fn record_user_settings_sync(&self, settings: UserSettingsRecord) -> StoreResult<()> {
2083        let payload = serde_json::to_string(&settings)
2084            .map_err(|error| StoreError::internal(format!("encode user settings: {error}")))?;
2085        let connection = self.connection.lock().expect("sqlite store poisoned");
2086        connection
2087            .execute(
2088                "INSERT INTO user_settings (id, settings_json, updated_at)
2089                 VALUES (1, ?1, ?2)
2090                 ON CONFLICT(id) DO UPDATE SET
2091                   settings_json = excluded.settings_json,
2092                   updated_at = excluded.updated_at",
2093                params![payload, now_seconds()],
2094            )
2095            .map_err(sqlite_error)?;
2096        Ok(())
2097    }
2098
2099    fn user_settings_sync(&self) -> StoreResult<Option<UserSettingsRecord>> {
2100        let connection = self.connection.lock().expect("sqlite store poisoned");
2101        connection
2102            .query_row(
2103                "SELECT settings_json FROM user_settings WHERE id = 1",
2104                [],
2105                |row| row.get::<_, String>(0),
2106            )
2107            .optional()
2108            .map_err(sqlite_error)?
2109            .map(|payload| {
2110                serde_json::from_str(&payload)
2111                    .map_err(|error| StoreError::internal(format!("decode user settings: {error}")))
2112            })
2113            .transpose()
2114    }
2115
2116    fn record_chat_participants_sync(
2117        &self,
2118        chat_id: InlineId,
2119        participants: Vec<ChatParticipantRecord>,
2120    ) -> StoreResult<()> {
2121        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2122        let transaction = connection.transaction().map_err(sqlite_error)?;
2123        transaction
2124            .execute(
2125                "DELETE FROM chat_participants WHERE chat_id = ?1",
2126                params![chat_id.get()],
2127            )
2128            .map_err(sqlite_error)?;
2129        for participant in participants {
2130            upsert_sqlite_participant(&transaction, chat_id, participant)?;
2131        }
2132        transaction
2133            .execute(
2134                "INSERT INTO chat_participant_snapshots (chat_id, updated_at)
2135                 VALUES (?1, ?2)
2136                 ON CONFLICT(chat_id) DO UPDATE SET updated_at = excluded.updated_at",
2137                params![chat_id.get(), now_seconds()],
2138            )
2139            .map_err(sqlite_error)?;
2140        transaction.commit().map_err(sqlite_error)?;
2141        Ok(())
2142    }
2143
2144    fn record_chat_participant_sync(
2145        &self,
2146        chat_id: InlineId,
2147        participant: ChatParticipantRecord,
2148    ) -> StoreResult<()> {
2149        let connection = self.connection.lock().expect("sqlite store poisoned");
2150        upsert_sqlite_participant(&connection, chat_id, participant)
2151    }
2152
2153    fn remove_chat_participant_sync(
2154        &self,
2155        chat_id: InlineId,
2156        user_id: InlineId,
2157    ) -> StoreResult<()> {
2158        let connection = self.connection.lock().expect("sqlite store poisoned");
2159        connection
2160            .execute(
2161                "DELETE FROM chat_participants WHERE chat_id = ?1 AND user_id = ?2",
2162                params![chat_id.get(), user_id.get()],
2163            )
2164            .map_err(sqlite_error)?;
2165        Ok(())
2166    }
2167
2168    fn chat_participants_sync(&self, chat_id: InlineId) -> StoreResult<Vec<ChatParticipantRecord>> {
2169        let connection = self.connection.lock().expect("sqlite store poisoned");
2170        let mut statement = connection
2171            .prepare(
2172                "SELECT user_id, date FROM chat_participants
2173                 WHERE chat_id = ?1 ORDER BY user_id ASC",
2174            )
2175            .map_err(sqlite_error)?;
2176        statement
2177            .query_map(params![chat_id.get()], |row| {
2178                Ok(ChatParticipantRecord {
2179                    user_id: InlineId::new(row.get(0)?),
2180                    date: row.get(1)?,
2181                })
2182            })
2183            .map_err(sqlite_error)?
2184            .collect::<Result<Vec<_>, _>>()
2185            .map_err(sqlite_error)
2186    }
2187
2188    fn chat_participants_complete_sync(&self, chat_id: InlineId) -> StoreResult<bool> {
2189        let connection = self.connection.lock().expect("sqlite store poisoned");
2190        connection
2191            .query_row(
2192                "SELECT 1 FROM chat_participant_snapshots WHERE chat_id = ?1",
2193                params![chat_id.get()],
2194                |_| Ok(()),
2195            )
2196            .optional()
2197            .map(|row| row.is_some())
2198            .map_err(sqlite_error)
2199    }
2200
2201    fn record_message_sync(&self, message: MessageRecord) -> StoreResult<()> {
2202        let content_json = serde_json::to_string(&message.content)
2203            .map_err(|error| StoreError::internal(format!("encode message content: {error}")))?;
2204        let transaction_json = message
2205            .transaction
2206            .as_ref()
2207            .map(serde_json::to_string)
2208            .transpose()
2209            .map_err(|error| {
2210                StoreError::internal(format!("encode transaction identity: {error}"))
2211            })?;
2212        let connection = self.connection.lock().expect("sqlite store poisoned");
2213        connection
2214            .execute(
2215                "INSERT INTO messages (
2216                   chat_id, message_id, sender_id, timestamp, is_outgoing,
2217                   content_json, reply_to_message_id, transaction_json
2218                 )
2219                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
2220                 ON CONFLICT(chat_id, message_id) DO UPDATE SET
2221                   sender_id = excluded.sender_id,
2222                   timestamp = excluded.timestamp,
2223                   is_outgoing = excluded.is_outgoing,
2224                   content_json = excluded.content_json,
2225                   reply_to_message_id = excluded.reply_to_message_id,
2226                   transaction_json = excluded.transaction_json",
2227                params![
2228                    message.chat_id.get(),
2229                    message.message_id.get(),
2230                    message.sender_id.get(),
2231                    message.timestamp,
2232                    i64::from(message.is_outgoing),
2233                    content_json,
2234                    message.reply_to_message_id.map(InlineId::get),
2235                    transaction_json,
2236                ],
2237            )
2238            .map_err(sqlite_error)?;
2239        connection
2240            .execute(
2241                "DELETE FROM message_tombstones WHERE chat_id = ?1 AND message_id = ?2",
2242                params![message.chat_id.get(), message.message_id.get()],
2243            )
2244            .map_err(sqlite_error)?;
2245        upsert_sqlite_dialog_last_message(&connection, message.chat_id, message.message_id)?;
2246        Ok(())
2247    }
2248
2249    fn record_message_deleted_sync(
2250        &self,
2251        chat_id: InlineId,
2252        message_id: InlineId,
2253    ) -> StoreResult<()> {
2254        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2255        let transaction = connection.transaction().map_err(sqlite_error)?;
2256        transaction
2257            .execute(
2258                "DELETE FROM messages WHERE chat_id = ?1 AND message_id = ?2",
2259                params![chat_id.get(), message_id.get()],
2260            )
2261            .map_err(sqlite_error)?;
2262        transaction
2263            .execute(
2264                "DELETE FROM reactions WHERE chat_id = ?1 AND message_id = ?2",
2265                params![chat_id.get(), message_id.get()],
2266            )
2267            .map_err(sqlite_error)?;
2268        transaction
2269            .execute(
2270                "DELETE FROM reaction_snapshots WHERE chat_id = ?1 AND message_id = ?2",
2271                params![chat_id.get(), message_id.get()],
2272            )
2273            .map_err(sqlite_error)?;
2274        transaction
2275            .execute(
2276                "INSERT INTO message_tombstones (chat_id, message_id, deleted_at)
2277                 VALUES (?1, ?2, ?3)
2278                 ON CONFLICT(chat_id, message_id) DO UPDATE SET
2279                   deleted_at = excluded.deleted_at",
2280                params![chat_id.get(), message_id.get(), now_seconds()],
2281            )
2282            .map_err(sqlite_error)?;
2283        transaction.commit().map_err(sqlite_error)?;
2284        Ok(())
2285    }
2286
2287    fn message_deleted_sync(&self, chat_id: InlineId, message_id: InlineId) -> StoreResult<bool> {
2288        let connection = self.connection.lock().expect("sqlite store poisoned");
2289        connection
2290            .query_row(
2291                "SELECT 1 FROM message_tombstones WHERE chat_id = ?1 AND message_id = ?2",
2292                params![chat_id.get(), message_id.get()],
2293                |_| Ok(()),
2294            )
2295            .optional()
2296            .map(|row| row.is_some())
2297            .map_err(sqlite_error)
2298    }
2299
2300    fn deleted_message_ids_sync(&self, chat_id: InlineId) -> StoreResult<Vec<InlineId>> {
2301        let connection = self.connection.lock().expect("sqlite store poisoned");
2302        let mut statement = connection
2303            .prepare(
2304                "SELECT message_id FROM message_tombstones
2305                 WHERE chat_id = ?1 ORDER BY message_id ASC",
2306            )
2307            .map_err(sqlite_error)?;
2308        statement
2309            .query_map(params![chat_id.get()], |row| {
2310                row.get::<_, i64>(0).map(InlineId::new)
2311            })
2312            .map_err(sqlite_error)?
2313            .collect::<Result<Vec<_>, _>>()
2314            .map_err(sqlite_error)
2315    }
2316
2317    fn clear_chat_messages_sync(
2318        &self,
2319        chat_id: InlineId,
2320        before_date: Option<i64>,
2321    ) -> StoreResult<Vec<InlineId>> {
2322        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2323        let transaction = connection.transaction().map_err(sqlite_error)?;
2324        let removed = {
2325            let (query, parameters): (&str, Vec<i64>) = match before_date {
2326                Some(date) => (
2327                    "SELECT message_id FROM messages
2328                     WHERE chat_id = ?1 AND timestamp < ?2 ORDER BY message_id ASC",
2329                    vec![chat_id.get(), date],
2330                ),
2331                None => (
2332                    "SELECT message_id FROM messages
2333                     WHERE chat_id = ?1 ORDER BY message_id ASC",
2334                    vec![chat_id.get()],
2335                ),
2336            };
2337            let mut statement = transaction.prepare(query).map_err(sqlite_error)?;
2338            statement
2339                .query_map(rusqlite::params_from_iter(parameters), |row| {
2340                    row.get::<_, i64>(0).map(InlineId::new)
2341                })
2342                .map_err(sqlite_error)?
2343                .collect::<Result<Vec<_>, _>>()
2344                .map_err(sqlite_error)?
2345        };
2346        for message_id in &removed {
2347            transaction
2348                .execute(
2349                    "DELETE FROM reactions WHERE chat_id = ?1 AND message_id = ?2",
2350                    params![chat_id.get(), message_id.get()],
2351                )
2352                .map_err(sqlite_error)?;
2353            transaction
2354                .execute(
2355                    "DELETE FROM reaction_snapshots WHERE chat_id = ?1 AND message_id = ?2",
2356                    params![chat_id.get(), message_id.get()],
2357                )
2358                .map_err(sqlite_error)?;
2359            transaction
2360                .execute(
2361                    "INSERT INTO message_tombstones (chat_id, message_id, deleted_at)
2362                     VALUES (?1, ?2, ?3)
2363                     ON CONFLICT(chat_id, message_id) DO UPDATE SET deleted_at = excluded.deleted_at",
2364                    params![chat_id.get(), message_id.get(), now_seconds()],
2365                )
2366                .map_err(sqlite_error)?;
2367        }
2368        match before_date {
2369            Some(date) => transaction
2370                .execute(
2371                    "DELETE FROM messages WHERE chat_id = ?1 AND timestamp < ?2",
2372                    params![chat_id.get(), date],
2373                )
2374                .map_err(sqlite_error)?,
2375            None => transaction
2376                .execute(
2377                    "DELETE FROM messages WHERE chat_id = ?1",
2378                    params![chat_id.get()],
2379                )
2380                .map_err(sqlite_error)?,
2381        };
2382        transaction
2383            .execute(
2384                "UPDATE dialogs SET
2385                   last_message_id = (SELECT MAX(message_id) FROM messages WHERE chat_id = ?1),
2386                   updated_at = ?2
2387                 WHERE chat_id = ?1",
2388                params![chat_id.get(), now_seconds()],
2389            )
2390            .map_err(sqlite_error)?;
2391        transaction.commit().map_err(sqlite_error)?;
2392        Ok(removed)
2393    }
2394
2395    fn chat_ids_in_space_sync(&self, space_id: InlineId) -> StoreResult<Vec<InlineId>> {
2396        let connection = self.connection.lock().expect("sqlite store poisoned");
2397        let mut statement = connection
2398            .prepare("SELECT chat_id FROM dialogs WHERE space_id = ?1 ORDER BY chat_id ASC")
2399            .map_err(sqlite_error)?;
2400        statement
2401            .query_map(params![space_id.get()], |row| {
2402                row.get::<_, i64>(0).map(InlineId::new)
2403            })
2404            .map_err(sqlite_error)?
2405            .collect::<Result<Vec<_>, _>>()
2406            .map_err(sqlite_error)
2407    }
2408
2409    fn record_reaction_sync(&self, reaction: StoredReaction) -> StoreResult<()> {
2410        let connection = self.connection.lock().expect("sqlite store poisoned");
2411        connection
2412            .execute(
2413                "INSERT INTO reactions (chat_id, message_id, user_id, reaction, updated_at)
2414                 VALUES (?1, ?2, ?3, ?4, ?5)
2415                 ON CONFLICT(chat_id, message_id, user_id, reaction) DO UPDATE SET
2416                   updated_at = excluded.updated_at",
2417                params![
2418                    reaction.chat_id.get(),
2419                    reaction.message_id.get(),
2420                    reaction.user_id.get(),
2421                    reaction.reaction,
2422                    now_seconds(),
2423                ],
2424            )
2425            .map_err(sqlite_error)?;
2426        Ok(())
2427    }
2428
2429    fn remove_reaction_sync(&self, reaction: StoredReaction) -> StoreResult<()> {
2430        let connection = self.connection.lock().expect("sqlite store poisoned");
2431        connection
2432            .execute(
2433                "DELETE FROM reactions
2434                 WHERE chat_id = ?1 AND message_id = ?2 AND user_id = ?3 AND reaction = ?4",
2435                params![
2436                    reaction.chat_id.get(),
2437                    reaction.message_id.get(),
2438                    reaction.user_id.get(),
2439                    reaction.reaction,
2440                ],
2441            )
2442            .map_err(sqlite_error)?;
2443        Ok(())
2444    }
2445
2446    fn replace_message_reactions_sync(
2447        &self,
2448        chat_id: InlineId,
2449        message_id: InlineId,
2450        reactions: Vec<StoredReaction>,
2451    ) -> StoreResult<()> {
2452        let mut connection = self.connection.lock().expect("sqlite store poisoned");
2453        let transaction = connection.transaction().map_err(sqlite_error)?;
2454        transaction
2455            .execute(
2456                "DELETE FROM reactions WHERE chat_id = ?1 AND message_id = ?2",
2457                params![chat_id.get(), message_id.get()],
2458            )
2459            .map_err(sqlite_error)?;
2460        for reaction in reactions {
2461            transaction
2462                .execute(
2463                    "INSERT INTO reactions (chat_id, message_id, user_id, reaction, updated_at)
2464                     VALUES (?1, ?2, ?3, ?4, ?5)",
2465                    params![
2466                        chat_id.get(),
2467                        message_id.get(),
2468                        reaction.user_id.get(),
2469                        reaction.reaction,
2470                        now_seconds(),
2471                    ],
2472                )
2473                .map_err(sqlite_error)?;
2474        }
2475        transaction
2476            .execute(
2477                "INSERT INTO reaction_snapshots (chat_id, message_id, updated_at)
2478                 VALUES (?1, ?2, ?3)
2479                 ON CONFLICT(chat_id, message_id) DO UPDATE SET updated_at = excluded.updated_at",
2480                params![chat_id.get(), message_id.get(), now_seconds()],
2481            )
2482            .map_err(sqlite_error)?;
2483        transaction.commit().map_err(sqlite_error)?;
2484        Ok(())
2485    }
2486
2487    fn reactions_sync(
2488        &self,
2489        chat_id: InlineId,
2490        message_id: InlineId,
2491    ) -> StoreResult<Vec<StoredReaction>> {
2492        let connection = self.connection.lock().expect("sqlite store poisoned");
2493        let mut statement = connection
2494            .prepare(
2495                "SELECT user_id, reaction FROM reactions
2496                 WHERE chat_id = ?1 AND message_id = ?2
2497                 ORDER BY user_id ASC, reaction ASC",
2498            )
2499            .map_err(sqlite_error)?;
2500        statement
2501            .query_map(params![chat_id.get(), message_id.get()], |row| {
2502                Ok(StoredReaction {
2503                    chat_id,
2504                    message_id,
2505                    user_id: InlineId::new(row.get(0)?),
2506                    reaction: row.get(1)?,
2507                })
2508            })
2509            .map_err(sqlite_error)?
2510            .collect::<Result<Vec<_>, _>>()
2511            .map_err(sqlite_error)
2512    }
2513
2514    fn reactions_for_chat_sync(&self, chat_id: InlineId) -> StoreResult<Vec<StoredReaction>> {
2515        let connection = self.connection.lock().expect("sqlite store poisoned");
2516        let mut statement = connection
2517            .prepare(
2518                "SELECT message_id, user_id, reaction FROM reactions
2519                 WHERE chat_id = ?1
2520                 ORDER BY message_id ASC, user_id ASC, reaction ASC",
2521            )
2522            .map_err(sqlite_error)?;
2523        statement
2524            .query_map(params![chat_id.get()], |row| {
2525                Ok(StoredReaction {
2526                    chat_id,
2527                    message_id: InlineId::new(row.get(0)?),
2528                    user_id: InlineId::new(row.get(1)?),
2529                    reaction: row.get(2)?,
2530                })
2531            })
2532            .map_err(sqlite_error)?
2533            .collect::<Result<Vec<_>, _>>()
2534            .map_err(sqlite_error)
2535    }
2536
2537    fn reaction_snapshot_message_ids_sync(&self, chat_id: InlineId) -> StoreResult<Vec<InlineId>> {
2538        let connection = self.connection.lock().expect("sqlite store poisoned");
2539        let mut statement = connection
2540            .prepare(
2541                "SELECT message_id FROM reaction_snapshots
2542                 WHERE chat_id = ?1 ORDER BY message_id ASC",
2543            )
2544            .map_err(sqlite_error)?;
2545        statement
2546            .query_map(params![chat_id.get()], |row| {
2547                row.get::<_, i64>(0).map(InlineId::new)
2548            })
2549            .map_err(sqlite_error)?
2550            .collect::<Result<Vec<_>, _>>()
2551            .map_err(sqlite_error)
2552    }
2553
2554    fn record_read_state_sync(&self, state: StoredReadState) -> StoreResult<()> {
2555        let connection = self.connection.lock().expect("sqlite store poisoned");
2556        connection
2557            .execute(
2558                "INSERT INTO read_states (
2559                   chat_id, read_max_id, unread_count, marked_unread, updated_at
2560                 ) VALUES (?1, ?2, ?3, ?4, ?5)
2561                 ON CONFLICT(chat_id) DO UPDATE SET
2562                   read_max_id = excluded.read_max_id,
2563                   unread_count = excluded.unread_count,
2564                   marked_unread = excluded.marked_unread,
2565                   updated_at = excluded.updated_at",
2566                params![
2567                    state.chat_id.get(),
2568                    state.read_max_id.map(InlineId::get),
2569                    state.unread_count.map(i64::from),
2570                    i64::from(state.marked_unread),
2571                    now_seconds(),
2572                ],
2573            )
2574            .map_err(sqlite_error)?;
2575        Ok(())
2576    }
2577
2578    fn read_state_sync(&self, chat_id: InlineId) -> StoreResult<Option<StoredReadState>> {
2579        let connection = self.connection.lock().expect("sqlite store poisoned");
2580        connection
2581            .query_row(
2582                "SELECT read_max_id, unread_count, marked_unread
2583                 FROM read_states WHERE chat_id = ?1",
2584                params![chat_id.get()],
2585                |row| {
2586                    Ok(StoredReadState {
2587                        chat_id,
2588                        read_max_id: row.get::<_, Option<i64>>(0)?.map(InlineId::new),
2589                        unread_count: row
2590                            .get::<_, Option<i64>>(1)?
2591                            .and_then(|value| u32::try_from(value).ok()),
2592                        marked_unread: row.get::<_, i64>(2)? != 0,
2593                    })
2594                },
2595            )
2596            .optional()
2597            .map_err(sqlite_error)
2598    }
2599
2600    fn record_transaction_sync(&self, transaction: StoredTransaction) -> StoreResult<()> {
2601        let identity_json = serde_json::to_string(&transaction.identity).map_err(|error| {
2602            StoreError::internal(format!("encode transaction identity: {error}"))
2603        })?;
2604        let state_json = serde_json::to_string(&transaction.state)
2605            .map_err(|error| StoreError::internal(format!("encode transaction state: {error}")))?;
2606        let failure_json = transaction
2607            .failure
2608            .as_ref()
2609            .map(serde_json::to_string)
2610            .transpose()
2611            .map_err(|error| {
2612                StoreError::internal(format!("encode transaction failure: {error}"))
2613            })?;
2614        let connection = self.connection.lock().expect("sqlite store poisoned");
2615        connection
2616            .execute(
2617                "INSERT INTO transactions (
2618                   transaction_id, identity_json, state_json, chat_id, message_id,
2619                   random_id, external_source, external_id, failure_json, created_at, updated_at
2620                 )
2621                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
2622                 ON CONFLICT(transaction_id) DO UPDATE SET
2623                   identity_json = excluded.identity_json,
2624                   state_json = excluded.state_json,
2625                   chat_id = excluded.chat_id,
2626                   message_id = excluded.message_id,
2627                   random_id = excluded.random_id,
2628                   external_source = excluded.external_source,
2629                   external_id = excluded.external_id,
2630                   failure_json = excluded.failure_json,
2631                   updated_at = excluded.updated_at",
2632                params![
2633                    transaction.identity.transaction_id.as_str(),
2634                    identity_json,
2635                    state_json,
2636                    transaction.chat_id.map(InlineId::get),
2637                    transaction.message_id.map(InlineId::get),
2638                    transaction.identity.random_id.get(),
2639                    transaction
2640                        .identity
2641                        .external_id
2642                        .as_ref()
2643                        .map(|external| external.source()),
2644                    transaction
2645                        .identity
2646                        .external_id
2647                        .as_ref()
2648                        .map(|external| external.id()),
2649                    failure_json,
2650                    transaction.created_at,
2651                    transaction.updated_at,
2652                ],
2653            )
2654            .map_err(sqlite_error)?;
2655        Ok(())
2656    }
2657
2658    fn transaction_sync(
2659        &self,
2660        transaction_id: TransactionId,
2661    ) -> StoreResult<Option<StoredTransaction>> {
2662        let connection = self.connection.lock().expect("sqlite store poisoned");
2663        let row = connection
2664            .query_row(
2665                "SELECT identity_json, state_json, chat_id, message_id,
2666                        failure_json, created_at, updated_at
2667                 FROM transactions
2668                 WHERE transaction_id = ?1",
2669                params![transaction_id.as_str()],
2670                |row| {
2671                    Ok(RawSqliteTransaction {
2672                        identity_json: row.get(0)?,
2673                        state_json: row.get(1)?,
2674                        chat_id: row.get(2)?,
2675                        message_id: row.get(3)?,
2676                        failure_json: row.get(4)?,
2677                        created_at: row.get(5)?,
2678                        updated_at: row.get(6)?,
2679                    })
2680                },
2681            )
2682            .optional()
2683            .map_err(sqlite_error)?;
2684
2685        row.map(raw_sqlite_transaction_to_record).transpose()
2686    }
2687}
2688
2689fn prepare_private_sqlite_file(path: &Path) -> StoreResult<()> {
2690    let mut options = OpenOptions::new();
2691    options.create(true).truncate(false).read(true).write(true);
2692    #[cfg(unix)]
2693    {
2694        use std::os::unix::fs::OpenOptionsExt;
2695        options.mode(0o600);
2696    }
2697    let _file = options
2698        .open(path)
2699        .map_err(|error| StoreError::internal(format!("prepare SQLite store file: {error}")))?;
2700    #[cfg(unix)]
2701    {
2702        use std::os::unix::fs::PermissionsExt;
2703        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).map_err(
2704            |error| StoreError::internal(format!("secure SQLite store permissions: {error}")),
2705        )?;
2706    }
2707    Ok(())
2708}
2709
2710impl ClientStore for SqliteStore {
2711    fn save_session(&self, session: StoredSession) -> BoxFuture<'static, StoreResult<()>> {
2712        let store = self.clone();
2713        Box::pin(async move { store.save_session_sync(session) })
2714    }
2715
2716    fn load_session(&self) -> BoxFuture<'static, StoreResult<Option<StoredSession>>> {
2717        let store = self.clone();
2718        Box::pin(async move { store.load_session_sync() })
2719    }
2720
2721    fn clear_session(&self) -> BoxFuture<'static, StoreResult<()>> {
2722        let store = self.clone();
2723        Box::pin(async move { store.clear_session_sync() })
2724    }
2725
2726    fn clear_account_data(&self) -> BoxFuture<'static, StoreResult<()>> {
2727        let store = self.clone();
2728        Box::pin(async move { store.clear_account_data_sync() })
2729    }
2730
2731    fn sync_state(&self) -> BoxFuture<'static, StoreResult<SyncState>> {
2732        let store = self.clone();
2733        Box::pin(async move { store.sync_state_sync() })
2734    }
2735
2736    fn save_sync_state(&self, state: SyncState) -> BoxFuture<'static, StoreResult<()>> {
2737        let store = self.clone();
2738        Box::pin(async move { store.save_sync_state_sync(state) })
2739    }
2740
2741    fn clear_sync_state(&self) -> BoxFuture<'static, StoreResult<()>> {
2742        let store = self.clone();
2743        Box::pin(async move { store.clear_sync_state_sync() })
2744    }
2745
2746    fn sync_bucket_state(
2747        &self,
2748        key: SyncBucketKey,
2749    ) -> BoxFuture<'static, StoreResult<SyncBucketState>> {
2750        let store = self.clone();
2751        Box::pin(async move { store.sync_bucket_state_sync(key) })
2752    }
2753
2754    fn save_sync_bucket_state(
2755        &self,
2756        key: SyncBucketKey,
2757        state: SyncBucketState,
2758    ) -> BoxFuture<'static, StoreResult<()>> {
2759        let store = self.clone();
2760        Box::pin(async move { store.save_sync_bucket_state_sync(key, state) })
2761    }
2762
2763    fn save_sync_bucket_states(
2764        &self,
2765        states: Vec<(SyncBucketKey, SyncBucketState)>,
2766    ) -> BoxFuture<'static, StoreResult<()>> {
2767        let store = self.clone();
2768        Box::pin(async move { store.save_sync_bucket_states_sync(states) })
2769    }
2770
2771    fn remove_sync_bucket_state(&self, key: SyncBucketKey) -> BoxFuture<'static, StoreResult<()>> {
2772        let store = self.clone();
2773        Box::pin(async move { store.remove_sync_bucket_state_sync(key) })
2774    }
2775
2776    fn save_pending_sync_batch(
2777        &self,
2778        batch: PendingSyncBatch,
2779    ) -> BoxFuture<'static, StoreResult<()>> {
2780        let store = self.clone();
2781        Box::pin(async move { store.save_pending_sync_batch_sync(batch) })
2782    }
2783
2784    fn pending_sync_batches(&self) -> BoxFuture<'static, StoreResult<Vec<PendingSyncBatch>>> {
2785        let store = self.clone();
2786        Box::pin(async move { store.pending_sync_batches_sync() })
2787    }
2788
2789    fn commit_pending_sync_batch(
2790        &self,
2791        key: SyncBucketKey,
2792        state: SyncBucketState,
2793    ) -> BoxFuture<'static, StoreResult<()>> {
2794        let store = self.clone();
2795        Box::pin(async move { store.commit_pending_sync_batch_sync(key, state) })
2796    }
2797
2798    fn dialogs(&self, request: DialogsRequest) -> BoxFuture<'static, StoreResult<DialogsPage>> {
2799        let store = self.clone();
2800        Box::pin(async move { store.dialogs_sync(request) })
2801    }
2802
2803    fn dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<Option<DialogRecord>>> {
2804        let store = self.clone();
2805        Box::pin(async move { store.dialog_sync(chat_id) })
2806    }
2807
2808    fn record_dialog(&self, dialog: DialogRecord) -> BoxFuture<'static, StoreResult<()>> {
2809        let store = self.clone();
2810        Box::pin(async move { store.upsert_dialog(dialog) })
2811    }
2812
2813    fn remove_dialog(&self, chat_id: InlineId) -> BoxFuture<'static, StoreResult<()>> {
2814        let store = self.clone();
2815        Box::pin(async move { store.remove_dialog_sync(chat_id) })
2816    }
2817
2818    fn deleted_chat_ids(&self) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
2819        let store = self.clone();
2820        Box::pin(async move { store.deleted_chat_ids_sync() })
2821    }
2822
2823    fn record_users(&self, users: Vec<UserRecord>) -> BoxFuture<'static, StoreResult<()>> {
2824        let store = self.clone();
2825        Box::pin(async move { store.record_users_sync(users) })
2826    }
2827
2828    fn record_space(&self, space: SpaceRecord) -> BoxFuture<'static, StoreResult<()>> {
2829        let store = self.clone();
2830        Box::pin(async move { store.record_space_sync(space) })
2831    }
2832
2833    fn space(&self, space_id: InlineId) -> BoxFuture<'static, StoreResult<Option<SpaceRecord>>> {
2834        let store = self.clone();
2835        Box::pin(async move { store.space_sync(space_id) })
2836    }
2837
2838    fn record_space_member(
2839        &self,
2840        member: SpaceMemberRecord,
2841    ) -> BoxFuture<'static, StoreResult<()>> {
2842        let store = self.clone();
2843        Box::pin(async move { store.record_space_member_sync(member) })
2844    }
2845
2846    fn record_space_members(
2847        &self,
2848        space_id: InlineId,
2849        members: Vec<SpaceMemberRecord>,
2850    ) -> BoxFuture<'static, StoreResult<()>> {
2851        let store = self.clone();
2852        Box::pin(async move { store.record_space_members_sync(space_id, members) })
2853    }
2854
2855    fn remove_space_member(
2856        &self,
2857        space_id: InlineId,
2858        user_id: InlineId,
2859    ) -> BoxFuture<'static, StoreResult<()>> {
2860        let store = self.clone();
2861        Box::pin(async move { store.remove_space_member_sync(space_id, user_id) })
2862    }
2863
2864    fn space_members(
2865        &self,
2866        space_id: InlineId,
2867    ) -> BoxFuture<'static, StoreResult<Vec<SpaceMemberRecord>>> {
2868        let store = self.clone();
2869        Box::pin(async move { store.space_members_sync(space_id) })
2870    }
2871
2872    fn record_user_settings(
2873        &self,
2874        settings: UserSettingsRecord,
2875    ) -> BoxFuture<'static, StoreResult<()>> {
2876        let store = self.clone();
2877        Box::pin(async move { store.record_user_settings_sync(settings) })
2878    }
2879
2880    fn user_settings(&self) -> BoxFuture<'static, StoreResult<Option<UserSettingsRecord>>> {
2881        let store = self.clone();
2882        Box::pin(async move { store.user_settings_sync() })
2883    }
2884
2885    fn record_chat_participants(
2886        &self,
2887        chat_id: InlineId,
2888        participants: Vec<ChatParticipantRecord>,
2889    ) -> BoxFuture<'static, StoreResult<()>> {
2890        let store = self.clone();
2891        Box::pin(async move { store.record_chat_participants_sync(chat_id, participants) })
2892    }
2893
2894    fn record_chat_participant(
2895        &self,
2896        chat_id: InlineId,
2897        participant: ChatParticipantRecord,
2898    ) -> BoxFuture<'static, StoreResult<()>> {
2899        let store = self.clone();
2900        Box::pin(async move { store.record_chat_participant_sync(chat_id, participant) })
2901    }
2902
2903    fn remove_chat_participant(
2904        &self,
2905        chat_id: InlineId,
2906        user_id: InlineId,
2907    ) -> BoxFuture<'static, StoreResult<()>> {
2908        let store = self.clone();
2909        Box::pin(async move { store.remove_chat_participant_sync(chat_id, user_id) })
2910    }
2911
2912    fn chat_participants(
2913        &self,
2914        chat_id: InlineId,
2915    ) -> BoxFuture<'static, StoreResult<Vec<ChatParticipantRecord>>> {
2916        let store = self.clone();
2917        Box::pin(async move { store.chat_participants_sync(chat_id) })
2918    }
2919
2920    fn chat_participants_complete(
2921        &self,
2922        chat_id: InlineId,
2923    ) -> BoxFuture<'static, StoreResult<bool>> {
2924        let store = self.clone();
2925        Box::pin(async move { store.chat_participants_complete_sync(chat_id) })
2926    }
2927
2928    fn history(&self, request: HistoryRequest) -> BoxFuture<'static, StoreResult<HistoryPage>> {
2929        let store = self.clone();
2930        Box::pin(async move { store.history_sync(request) })
2931    }
2932
2933    fn record_message(&self, message: MessageRecord) -> BoxFuture<'static, StoreResult<()>> {
2934        let store = self.clone();
2935        Box::pin(async move { store.record_message_sync(message) })
2936    }
2937
2938    fn record_message_deleted(
2939        &self,
2940        chat_id: InlineId,
2941        message_id: InlineId,
2942    ) -> BoxFuture<'static, StoreResult<()>> {
2943        let store = self.clone();
2944        Box::pin(async move { store.record_message_deleted_sync(chat_id, message_id) })
2945    }
2946
2947    fn message_deleted(
2948        &self,
2949        chat_id: InlineId,
2950        message_id: InlineId,
2951    ) -> BoxFuture<'static, StoreResult<bool>> {
2952        let store = self.clone();
2953        Box::pin(async move { store.message_deleted_sync(chat_id, message_id) })
2954    }
2955
2956    fn deleted_message_ids(
2957        &self,
2958        chat_id: InlineId,
2959    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
2960        let store = self.clone();
2961        Box::pin(async move { store.deleted_message_ids_sync(chat_id) })
2962    }
2963
2964    fn clear_chat_messages(
2965        &self,
2966        chat_id: InlineId,
2967        before_date: Option<i64>,
2968    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
2969        let store = self.clone();
2970        Box::pin(async move { store.clear_chat_messages_sync(chat_id, before_date) })
2971    }
2972
2973    fn chat_ids_in_space(
2974        &self,
2975        space_id: InlineId,
2976    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
2977        let store = self.clone();
2978        Box::pin(async move { store.chat_ids_in_space_sync(space_id) })
2979    }
2980
2981    fn record_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>> {
2982        let store = self.clone();
2983        Box::pin(async move { store.record_reaction_sync(reaction) })
2984    }
2985
2986    fn remove_reaction(&self, reaction: StoredReaction) -> BoxFuture<'static, StoreResult<()>> {
2987        let store = self.clone();
2988        Box::pin(async move { store.remove_reaction_sync(reaction) })
2989    }
2990
2991    fn replace_message_reactions(
2992        &self,
2993        chat_id: InlineId,
2994        message_id: InlineId,
2995        reactions: Vec<StoredReaction>,
2996    ) -> BoxFuture<'static, StoreResult<()>> {
2997        let store = self.clone();
2998        Box::pin(
2999            async move { store.replace_message_reactions_sync(chat_id, message_id, reactions) },
3000        )
3001    }
3002
3003    fn reactions(
3004        &self,
3005        chat_id: InlineId,
3006        message_id: InlineId,
3007    ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>> {
3008        let store = self.clone();
3009        Box::pin(async move { store.reactions_sync(chat_id, message_id) })
3010    }
3011
3012    fn reactions_for_chat(
3013        &self,
3014        chat_id: InlineId,
3015    ) -> BoxFuture<'static, StoreResult<Vec<StoredReaction>>> {
3016        let store = self.clone();
3017        Box::pin(async move { store.reactions_for_chat_sync(chat_id) })
3018    }
3019
3020    fn reaction_snapshot_message_ids(
3021        &self,
3022        chat_id: InlineId,
3023    ) -> BoxFuture<'static, StoreResult<Vec<InlineId>>> {
3024        let store = self.clone();
3025        Box::pin(async move { store.reaction_snapshot_message_ids_sync(chat_id) })
3026    }
3027
3028    fn record_read_state(&self, state: StoredReadState) -> BoxFuture<'static, StoreResult<()>> {
3029        let store = self.clone();
3030        Box::pin(async move { store.record_read_state_sync(state) })
3031    }
3032
3033    fn read_state(
3034        &self,
3035        chat_id: InlineId,
3036    ) -> BoxFuture<'static, StoreResult<Option<StoredReadState>>> {
3037        let store = self.clone();
3038        Box::pin(async move { store.read_state_sync(chat_id) })
3039    }
3040
3041    fn record_transaction(
3042        &self,
3043        transaction: StoredTransaction,
3044    ) -> BoxFuture<'static, StoreResult<()>> {
3045        let store = self.clone();
3046        Box::pin(async move { store.record_transaction_sync(transaction) })
3047    }
3048
3049    fn transaction(
3050        &self,
3051        transaction_id: TransactionId,
3052    ) -> BoxFuture<'static, StoreResult<Option<StoredTransaction>>> {
3053        let store = self.clone();
3054        Box::pin(async move { store.transaction_sync(transaction_id) })
3055    }
3056}
3057
3058pub(crate) fn parse_cursor(cursor: Option<&str>) -> StoreResult<usize> {
3059    match cursor {
3060        Some(cursor) if !cursor.trim().is_empty() => cursor.parse::<usize>().map_err(|_| {
3061            StoreError::new(
3062                ClientErrorCategory::InvalidInput,
3063                "invalid pagination cursor",
3064            )
3065        }),
3066        _ => Ok(0),
3067    }
3068}
3069
3070fn migrate_sqlite(connection: &Connection) -> StoreResult<()> {
3071    connection
3072        .execute_batch(
3073            "
3074            PRAGMA foreign_keys = ON;
3075
3076            CREATE TABLE IF NOT EXISTS sessions (
3077                id INTEGER PRIMARY KEY CHECK (id = 1),
3078                auth_json TEXT NOT NULL,
3079                account_namespace TEXT,
3080                updated_at INTEGER NOT NULL
3081            );
3082
3083            CREATE TABLE IF NOT EXISTS sync_state (
3084                id INTEGER PRIMARY KEY CHECK (id = 1),
3085                last_sync_date INTEGER NOT NULL,
3086                updated_at INTEGER NOT NULL
3087            );
3088
3089            CREATE TABLE IF NOT EXISTS sync_buckets (
3090                bucket_kind TEXT NOT NULL,
3091                entity_id INTEGER NOT NULL,
3092                seq INTEGER NOT NULL,
3093                date INTEGER NOT NULL,
3094                updated_at INTEGER NOT NULL,
3095                PRIMARY KEY (bucket_kind, entity_id)
3096            );
3097
3098            CREATE TABLE IF NOT EXISTS pending_sync_batches (
3099                bucket_kind TEXT NOT NULL,
3100                entity_id INTEGER NOT NULL,
3101                seq INTEGER NOT NULL,
3102                date INTEGER NOT NULL,
3103                payload BLOB NOT NULL,
3104                updated_at INTEGER NOT NULL,
3105                PRIMARY KEY (bucket_kind, entity_id)
3106            );
3107
3108            CREATE TABLE IF NOT EXISTS dialogs (
3109                chat_id INTEGER PRIMARY KEY,
3110                peer_user_id INTEGER,
3111                title TEXT,
3112                emoji TEXT,
3113                last_message_id INTEGER,
3114                unread_count INTEGER,
3115                space_id INTEGER,
3116                is_public INTEGER,
3117                archived INTEGER,
3118                pinned INTEGER,
3119                open INTEGER,
3120                chat_list_hidden INTEGER,
3121                list_order TEXT,
3122                pinned_order TEXT,
3123                notification_mode TEXT,
3124                follow_mode TEXT,
3125                pinned_message_ids_json TEXT NOT NULL DEFAULT '[]',
3126                updated_at INTEGER NOT NULL
3127            );
3128
3129            CREATE TABLE IF NOT EXISTS chat_tombstones (
3130                chat_id INTEGER PRIMARY KEY,
3131                deleted_at INTEGER NOT NULL
3132            );
3133
3134            CREATE TABLE IF NOT EXISTS users (
3135                user_id INTEGER PRIMARY KEY,
3136                display_name TEXT,
3137                username TEXT,
3138                first_name TEXT,
3139                last_name TEXT,
3140                avatar_url TEXT,
3141                is_bot INTEGER,
3142                updated_at INTEGER NOT NULL
3143            );
3144
3145            CREATE TABLE IF NOT EXISTS spaces (
3146                space_id INTEGER PRIMARY KEY,
3147                record_json TEXT NOT NULL,
3148                updated_at INTEGER NOT NULL
3149            );
3150
3151            CREATE TABLE IF NOT EXISTS space_members (
3152                space_id INTEGER NOT NULL,
3153                user_id INTEGER NOT NULL,
3154                record_json TEXT NOT NULL,
3155                updated_at INTEGER NOT NULL,
3156                PRIMARY KEY (space_id, user_id)
3157            );
3158
3159            CREATE INDEX IF NOT EXISTS idx_space_members_space
3160                ON space_members (space_id, user_id);
3161
3162            CREATE TABLE IF NOT EXISTS user_settings (
3163                id INTEGER PRIMARY KEY CHECK (id = 1),
3164                settings_json TEXT NOT NULL,
3165                updated_at INTEGER NOT NULL
3166            );
3167
3168            CREATE TABLE IF NOT EXISTS chat_participants (
3169                chat_id INTEGER NOT NULL,
3170                user_id INTEGER NOT NULL,
3171                date INTEGER,
3172                updated_at INTEGER NOT NULL,
3173                PRIMARY KEY (chat_id, user_id)
3174            );
3175
3176            CREATE TABLE IF NOT EXISTS chat_participant_snapshots (
3177                chat_id INTEGER PRIMARY KEY,
3178                updated_at INTEGER NOT NULL
3179            );
3180
3181            CREATE TABLE IF NOT EXISTS messages (
3182                chat_id INTEGER NOT NULL,
3183                message_id INTEGER NOT NULL,
3184                sender_id INTEGER NOT NULL,
3185                timestamp INTEGER NOT NULL,
3186                is_outgoing INTEGER NOT NULL,
3187                content_json TEXT NOT NULL,
3188                reply_to_message_id INTEGER,
3189                transaction_json TEXT,
3190                PRIMARY KEY (chat_id, message_id)
3191            );
3192
3193            CREATE INDEX IF NOT EXISTS idx_messages_chat_message
3194                ON messages (chat_id, message_id DESC);
3195
3196            CREATE TABLE IF NOT EXISTS message_tombstones (
3197                chat_id INTEGER NOT NULL,
3198                message_id INTEGER NOT NULL,
3199                deleted_at INTEGER NOT NULL,
3200                PRIMARY KEY (chat_id, message_id)
3201            );
3202
3203            CREATE TABLE IF NOT EXISTS reactions (
3204                chat_id INTEGER NOT NULL,
3205                message_id INTEGER NOT NULL,
3206                user_id INTEGER NOT NULL,
3207                reaction TEXT NOT NULL,
3208                updated_at INTEGER NOT NULL,
3209                PRIMARY KEY (chat_id, message_id, user_id, reaction)
3210            );
3211
3212            CREATE INDEX IF NOT EXISTS idx_reactions_message
3213                ON reactions (chat_id, message_id);
3214
3215            CREATE TABLE IF NOT EXISTS reaction_snapshots (
3216                chat_id INTEGER NOT NULL,
3217                message_id INTEGER NOT NULL,
3218                updated_at INTEGER NOT NULL,
3219                PRIMARY KEY (chat_id, message_id)
3220            );
3221
3222            CREATE TABLE IF NOT EXISTS read_states (
3223                chat_id INTEGER PRIMARY KEY,
3224                read_max_id INTEGER,
3225                unread_count INTEGER,
3226                marked_unread INTEGER NOT NULL,
3227                updated_at INTEGER NOT NULL
3228            );
3229
3230            CREATE TABLE IF NOT EXISTS transactions (
3231                transaction_id TEXT PRIMARY KEY,
3232                identity_json TEXT NOT NULL,
3233                state_json TEXT NOT NULL,
3234                chat_id INTEGER,
3235                message_id INTEGER,
3236                random_id INTEGER NOT NULL,
3237                external_source TEXT,
3238                external_id TEXT,
3239                failure_json TEXT,
3240                created_at INTEGER NOT NULL,
3241                updated_at INTEGER NOT NULL
3242            );
3243
3244            CREATE INDEX IF NOT EXISTS idx_transactions_external
3245                ON transactions (external_source, external_id);
3246            CREATE INDEX IF NOT EXISTS idx_transactions_random
3247                ON transactions (random_id);
3248            ",
3249        )
3250        .map_err(sqlite_error)?;
3251    ensure_sqlite_column(connection, "sessions", "account_namespace", "TEXT")?;
3252    ensure_sqlite_column(connection, "dialogs", "peer_user_id", "INTEGER")?;
3253    ensure_sqlite_column(connection, "dialogs", "emoji", "TEXT")?;
3254    ensure_sqlite_column(connection, "dialogs", "space_id", "INTEGER")?;
3255    ensure_sqlite_column(connection, "dialogs", "is_public", "INTEGER")?;
3256    ensure_sqlite_column(connection, "dialogs", "archived", "INTEGER")?;
3257    ensure_sqlite_column(connection, "dialogs", "pinned", "INTEGER")?;
3258    ensure_sqlite_column(connection, "dialogs", "open", "INTEGER")?;
3259    ensure_sqlite_column(connection, "dialogs", "chat_list_hidden", "INTEGER")?;
3260    ensure_sqlite_column(connection, "dialogs", "list_order", "TEXT")?;
3261    ensure_sqlite_column(connection, "dialogs", "pinned_order", "TEXT")?;
3262    ensure_sqlite_column(connection, "dialogs", "notification_mode", "TEXT")?;
3263    ensure_sqlite_column(connection, "dialogs", "follow_mode", "TEXT")?;
3264    ensure_sqlite_column(
3265        connection,
3266        "dialogs",
3267        "pinned_message_ids_json",
3268        "TEXT NOT NULL DEFAULT '[]'",
3269    )?;
3270    Ok(())
3271}
3272
3273fn ensure_sqlite_column(
3274    connection: &Connection,
3275    table: &str,
3276    column: &str,
3277    definition: &str,
3278) -> StoreResult<()> {
3279    let pragma = format!("PRAGMA table_info({table})");
3280    let mut stmt = connection.prepare(&pragma).map_err(sqlite_error)?;
3281    let columns = stmt
3282        .query_map([], |row| row.get::<_, String>(1))
3283        .map_err(sqlite_error)?
3284        .collect::<Result<Vec<_>, _>>()
3285        .map_err(sqlite_error)?;
3286    if columns.iter().any(|name| name == column) {
3287        return Ok(());
3288    }
3289    connection
3290        .execute(
3291            &format!("ALTER TABLE {table} ADD COLUMN {column} {definition}"),
3292            [],
3293        )
3294        .map_err(sqlite_error)?;
3295    Ok(())
3296}
3297
3298fn sync_bucket_key_parts(key: SyncBucketKey) -> (&'static str, i64) {
3299    match key {
3300        SyncBucketKey::User => ("user", 0),
3301        SyncBucketKey::Space { space_id } => ("space", space_id.get()),
3302        SyncBucketKey::Chat {
3303            peer: SyncBucketPeer::User { user_id },
3304        } => ("chat_user", user_id.get()),
3305        SyncBucketKey::Chat {
3306            peer: SyncBucketPeer::Chat { chat_id },
3307        } => ("chat_chat", chat_id.get()),
3308    }
3309}
3310
3311fn sync_bucket_key_from_parts(kind: &str, entity_id: i64) -> StoreResult<SyncBucketKey> {
3312    match kind {
3313        "user" if entity_id == 0 => Ok(SyncBucketKey::User),
3314        "space" => Ok(SyncBucketKey::Space {
3315            space_id: InlineId::new(entity_id),
3316        }),
3317        "chat_user" => Ok(SyncBucketKey::Chat {
3318            peer: SyncBucketPeer::User {
3319                user_id: InlineId::new(entity_id),
3320            },
3321        }),
3322        "chat_chat" => Ok(SyncBucketKey::Chat {
3323            peer: SyncBucketPeer::Chat {
3324                chat_id: InlineId::new(entity_id),
3325            },
3326        }),
3327        _ => Err(StoreError::internal(format!(
3328            "invalid stored sync bucket {kind:?}/{entity_id}"
3329        ))),
3330    }
3331}
3332
3333fn sync_bucket_sort_key(key: SyncBucketKey) -> (u8, i64) {
3334    match key {
3335        SyncBucketKey::User => (0, 0),
3336        SyncBucketKey::Space { space_id } => (1, space_id.get()),
3337        SyncBucketKey::Chat {
3338            peer: SyncBucketPeer::User { user_id },
3339        } => (2, user_id.get()),
3340        SyncBucketKey::Chat {
3341            peer: SyncBucketPeer::Chat { chat_id },
3342        } => (3, chat_id.get()),
3343    }
3344}
3345
3346fn upsert_sync_bucket(
3347    connection: &Connection,
3348    kind: &str,
3349    entity_id: i64,
3350    state: SyncBucketState,
3351) -> StoreResult<()> {
3352    connection
3353        .execute(
3354            "INSERT INTO sync_buckets (bucket_kind, entity_id, seq, date, updated_at)
3355             VALUES (?1, ?2, ?3, ?4, ?5)
3356             ON CONFLICT(bucket_kind, entity_id) DO UPDATE SET
3357               seq = excluded.seq,
3358               date = excluded.date,
3359               updated_at = excluded.updated_at",
3360            params![kind, entity_id, state.seq, state.date, now_seconds()],
3361        )
3362        .map_err(sqlite_error)?;
3363    Ok(())
3364}
3365
3366fn upsert_sqlite_user(connection: &Connection, user: UserRecord) -> StoreResult<()> {
3367    connection
3368        .execute(
3369            "INSERT INTO users (
3370               user_id, display_name, username, first_name, last_name,
3371               avatar_url, is_bot, updated_at
3372             )
3373             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
3374             ON CONFLICT(user_id) DO UPDATE SET
3375               display_name = excluded.display_name,
3376               username = excluded.username,
3377               first_name = excluded.first_name,
3378               last_name = excluded.last_name,
3379               avatar_url = excluded.avatar_url,
3380               is_bot = excluded.is_bot,
3381               updated_at = excluded.updated_at",
3382            params![
3383                user.user_id.get(),
3384                user.display_name,
3385                user.username,
3386                user.first_name,
3387                user.last_name,
3388                user.avatar_url,
3389                user.is_bot.map(|is_bot| if is_bot { 1_i64 } else { 0_i64 }),
3390                now_seconds(),
3391            ],
3392        )
3393        .map_err(sqlite_error)?;
3394    Ok(())
3395}
3396
3397fn upsert_sqlite_participant(
3398    connection: &Connection,
3399    chat_id: InlineId,
3400    participant: ChatParticipantRecord,
3401) -> StoreResult<()> {
3402    connection
3403        .execute(
3404            "INSERT INTO chat_participants (chat_id, user_id, date, updated_at)
3405             VALUES (?1, ?2, ?3, ?4)
3406             ON CONFLICT(chat_id, user_id) DO UPDATE SET
3407               date = excluded.date,
3408               updated_at = excluded.updated_at",
3409            params![
3410                chat_id.get(),
3411                participant.user_id.get(),
3412                participant.date,
3413                now_seconds(),
3414            ],
3415        )
3416        .map_err(sqlite_error)?;
3417    Ok(())
3418}
3419
3420fn sqlite_dialog_from_row(row: &Row<'_>) -> rusqlite::Result<DialogRecord> {
3421    let pinned_json = row.get::<_, String>(17)?;
3422    let pinned_message_ids = serde_json::from_str(&pinned_json).map_err(|error| {
3423        rusqlite::Error::FromSqlConversionFailure(17, Type::Text, Box::new(error))
3424    })?;
3425    Ok(DialogRecord {
3426        chat_id: InlineId::new(row.get(0)?),
3427        peer_user_id: row.get::<_, Option<i64>>(1)?.map(InlineId::new),
3428        title: row.get(2)?,
3429        emoji: row.get(3)?,
3430        last_message_id: row.get::<_, Option<i64>>(4)?.map(InlineId::new),
3431        synced_through_message_id: row.get::<_, Option<i64>>(5)?.map(InlineId::new),
3432        unread_count: row
3433            .get::<_, Option<i64>>(6)?
3434            .and_then(|value| u32::try_from(value).ok()),
3435        space_id: row.get::<_, Option<i64>>(7)?.map(InlineId::new),
3436        is_public: row.get::<_, Option<bool>>(8)?,
3437        archived: row.get::<_, Option<bool>>(9)?,
3438        pinned: row.get::<_, Option<bool>>(10)?,
3439        open: row.get::<_, Option<bool>>(11)?,
3440        chat_list_hidden: row.get::<_, Option<bool>>(12)?,
3441        order: row.get(13)?,
3442        pinned_order: row.get(14)?,
3443        notification_mode: row
3444            .get::<_, Option<String>>(15)?
3445            .as_deref()
3446            .and_then(dialog_notification_mode_from_store),
3447        follow_mode: row
3448            .get::<_, Option<String>>(16)?
3449            .as_deref()
3450            .and_then(dialog_follow_mode_from_store),
3451        pinned_message_ids,
3452    })
3453}
3454
3455fn upsert_sqlite_dialog(connection: &Connection, dialog: DialogRecord) -> StoreResult<()> {
3456    let chat_id = dialog.chat_id;
3457    let pinned_message_ids_json = serde_json::to_string(&dialog.pinned_message_ids)
3458        .map_err(|error| StoreError::internal(format!("encode pinned message IDs: {error}")))?;
3459    connection
3460        .execute(
3461            "INSERT INTO dialogs (
3462               chat_id, peer_user_id, title, emoji, last_message_id, unread_count,
3463               space_id, is_public, archived, pinned, open, chat_list_hidden,
3464               list_order, pinned_order, notification_mode, follow_mode,
3465               pinned_message_ids_json, updated_at
3466             ) VALUES (
3467               ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13,
3468               ?14, ?15, ?16, ?17, ?18
3469             )
3470             ON CONFLICT(chat_id) DO UPDATE SET
3471               peer_user_id = excluded.peer_user_id,
3472               title = excluded.title,
3473               emoji = excluded.emoji,
3474               last_message_id = excluded.last_message_id,
3475               unread_count = excluded.unread_count,
3476               space_id = excluded.space_id,
3477               is_public = excluded.is_public,
3478               archived = excluded.archived,
3479               pinned = excluded.pinned,
3480               open = excluded.open,
3481               chat_list_hidden = excluded.chat_list_hidden,
3482               list_order = excluded.list_order,
3483               pinned_order = excluded.pinned_order,
3484               notification_mode = excluded.notification_mode,
3485               follow_mode = excluded.follow_mode,
3486               pinned_message_ids_json = excluded.pinned_message_ids_json,
3487               updated_at = excluded.updated_at",
3488            params![
3489                dialog.chat_id.get(),
3490                dialog.peer_user_id.map(InlineId::get),
3491                dialog.title,
3492                dialog.emoji,
3493                dialog.last_message_id.map(InlineId::get),
3494                dialog.unread_count.map(i64::from),
3495                dialog.space_id.map(InlineId::get),
3496                dialog.is_public.map(i64::from),
3497                dialog.archived.map(i64::from),
3498                dialog.pinned.map(i64::from),
3499                dialog.open.map(i64::from),
3500                dialog.chat_list_hidden.map(i64::from),
3501                dialog.order,
3502                dialog.pinned_order,
3503                dialog
3504                    .notification_mode
3505                    .map(dialog_notification_mode_for_store),
3506                dialog.follow_mode.map(dialog_follow_mode_for_store),
3507                pinned_message_ids_json,
3508                now_seconds(),
3509            ],
3510        )
3511        .map_err(sqlite_error)?;
3512    connection
3513        .execute(
3514            "DELETE FROM chat_tombstones WHERE chat_id = ?1",
3515            params![chat_id.get()],
3516        )
3517        .map_err(sqlite_error)?;
3518    Ok(())
3519}
3520
3521const fn dialog_notification_mode_for_store(mode: DialogNotificationMode) -> &'static str {
3522    match mode {
3523        DialogNotificationMode::All => "all",
3524        DialogNotificationMode::Mentions => "mentions",
3525        DialogNotificationMode::None => "none",
3526    }
3527}
3528
3529fn dialog_notification_mode_from_store(value: &str) -> Option<DialogNotificationMode> {
3530    match value {
3531        "all" => Some(DialogNotificationMode::All),
3532        "mentions" => Some(DialogNotificationMode::Mentions),
3533        "none" => Some(DialogNotificationMode::None),
3534        _ => None,
3535    }
3536}
3537
3538const fn dialog_follow_mode_for_store(mode: DialogFollowMode) -> &'static str {
3539    match mode {
3540        DialogFollowMode::Following => "following",
3541    }
3542}
3543
3544fn dialog_follow_mode_from_store(value: &str) -> Option<DialogFollowMode> {
3545    match value {
3546        "following" => Some(DialogFollowMode::Following),
3547        _ => None,
3548    }
3549}
3550
3551fn upsert_sqlite_dialog_last_message(
3552    connection: &Connection,
3553    chat_id: InlineId,
3554    message_id: InlineId,
3555) -> StoreResult<()> {
3556    let updated = connection
3557        .execute(
3558            "UPDATE dialogs
3559             SET last_message_id = ?1, updated_at = ?2
3560             WHERE chat_id = ?3",
3561            params![message_id.get(), now_seconds(), chat_id.get()],
3562        )
3563        .map_err(sqlite_error)?;
3564    if updated == 0 {
3565        connection
3566            .execute(
3567                "INSERT INTO dialogs (chat_id, title, last_message_id, unread_count, updated_at)
3568                 VALUES (?1, NULL, ?2, 0, ?3)",
3569                params![chat_id.get(), message_id.get(), now_seconds()],
3570            )
3571            .map_err(sqlite_error)?;
3572    }
3573    connection
3574        .execute(
3575            "DELETE FROM chat_tombstones WHERE chat_id = ?1",
3576            params![chat_id.get()],
3577        )
3578        .map_err(sqlite_error)?;
3579    Ok(())
3580}
3581
3582fn all_sqlite_users(connection: &Connection) -> StoreResult<Vec<UserRecord>> {
3583    let mut stmt = connection
3584        .prepare(
3585            "SELECT user_id, display_name, username, first_name, last_name, avatar_url, is_bot
3586             FROM users
3587             ORDER BY user_id ASC",
3588        )
3589        .map_err(sqlite_error)?;
3590    query_user_rows(&mut stmt, [])
3591}
3592
3593fn sqlite_users_for_messages(
3594    connection: &Connection,
3595    messages: &[MessageRecord],
3596) -> StoreResult<Vec<UserRecord>> {
3597    let mut user_ids = messages
3598        .iter()
3599        .map(|message| message.sender_id.get())
3600        .collect::<Vec<_>>();
3601    user_ids.sort_unstable();
3602    user_ids.dedup();
3603
3604    let mut users = Vec::new();
3605    let mut stmt = connection
3606        .prepare(
3607            "SELECT user_id, display_name, username, first_name, last_name, avatar_url, is_bot
3608             FROM users
3609             WHERE user_id = ?1",
3610        )
3611        .map_err(sqlite_error)?;
3612    for user_id in user_ids {
3613        if let Some(user) = stmt
3614            .query_row(params![user_id], sqlite_user_from_row)
3615            .optional()
3616            .map_err(sqlite_error)?
3617        {
3618            users.push(user);
3619        }
3620    }
3621    Ok(users)
3622}
3623
3624fn query_user_rows<P>(stmt: &mut rusqlite::Statement<'_>, params: P) -> StoreResult<Vec<UserRecord>>
3625where
3626    P: rusqlite::Params,
3627{
3628    stmt.query_map(params, sqlite_user_from_row)
3629        .map_err(sqlite_error)?
3630        .collect::<Result<Vec<_>, _>>()
3631        .map_err(sqlite_error)
3632}
3633
3634fn sqlite_user_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<UserRecord> {
3635    Ok(UserRecord {
3636        user_id: InlineId::new(row.get::<_, i64>(0)?),
3637        display_name: row.get::<_, Option<String>>(1)?,
3638        username: row.get::<_, Option<String>>(2)?,
3639        first_name: row.get::<_, Option<String>>(3)?,
3640        last_name: row.get::<_, Option<String>>(4)?,
3641        avatar_url: row.get::<_, Option<String>>(5)?,
3642        is_bot: row.get::<_, Option<i64>>(6)?.map(|value| value != 0),
3643    })
3644}
3645
3646#[derive(Debug)]
3647struct RawSqliteMessage {
3648    chat_id: i64,
3649    message_id: i64,
3650    sender_id: i64,
3651    timestamp: i64,
3652    is_outgoing: bool,
3653    content_json: String,
3654    reply_to_message_id: Option<i64>,
3655    transaction_json: Option<String>,
3656}
3657
3658fn query_message_rows<P>(
3659    stmt: &mut rusqlite::Statement<'_>,
3660    params: P,
3661) -> StoreResult<Vec<RawSqliteMessage>>
3662where
3663    P: rusqlite::Params,
3664{
3665    stmt.query_map(params, |row| {
3666        Ok(RawSqliteMessage {
3667            chat_id: row.get(0)?,
3668            message_id: row.get(1)?,
3669            sender_id: row.get(2)?,
3670            timestamp: row.get(3)?,
3671            is_outgoing: row.get::<_, i64>(4)? != 0,
3672            content_json: row.get(5)?,
3673            reply_to_message_id: row.get(6)?,
3674            transaction_json: row.get(7)?,
3675        })
3676    })
3677    .map_err(sqlite_error)?
3678    .collect::<Result<Vec<_>, _>>()
3679    .map_err(sqlite_error)
3680}
3681
3682fn raw_sqlite_message_to_record(raw: RawSqliteMessage) -> StoreResult<MessageRecord> {
3683    let content = serde_json::from_str::<MessageContent>(&raw.content_json)
3684        .map_err(|error| StoreError::internal(format!("decode message content: {error}")))?;
3685    let transaction = raw
3686        .transaction_json
3687        .as_deref()
3688        .map(serde_json::from_str::<TransactionIdentity>)
3689        .transpose()
3690        .map_err(|error| StoreError::internal(format!("decode transaction identity: {error}")))?;
3691    Ok(MessageRecord {
3692        chat_id: InlineId::new(raw.chat_id),
3693        message_id: InlineId::new(raw.message_id),
3694        sender_id: InlineId::new(raw.sender_id),
3695        timestamp: raw.timestamp,
3696        is_outgoing: raw.is_outgoing,
3697        content,
3698        reply_to_message_id: raw.reply_to_message_id.map(InlineId::new),
3699        transaction,
3700    })
3701}
3702
3703#[derive(Debug)]
3704struct RawSqliteTransaction {
3705    identity_json: String,
3706    state_json: String,
3707    chat_id: Option<i64>,
3708    message_id: Option<i64>,
3709    failure_json: Option<String>,
3710    created_at: i64,
3711    updated_at: i64,
3712}
3713
3714fn raw_sqlite_transaction_to_record(raw: RawSqliteTransaction) -> StoreResult<StoredTransaction> {
3715    let identity = serde_json::from_str::<TransactionIdentity>(&raw.identity_json)
3716        .map_err(|error| StoreError::internal(format!("decode transaction identity: {error}")))?;
3717    let state = serde_json::from_str::<TransactionState>(&raw.state_json)
3718        .map_err(|error| StoreError::internal(format!("decode transaction state: {error}")))?;
3719    let failure = raw
3720        .failure_json
3721        .as_deref()
3722        .map(serde_json::from_str::<ClientFailure>)
3723        .transpose()
3724        .map_err(|error| StoreError::internal(format!("decode transaction failure: {error}")))?;
3725    Ok(StoredTransaction {
3726        identity,
3727        state,
3728        chat_id: raw.chat_id.map(InlineId::new),
3729        message_id: raw.message_id.map(InlineId::new),
3730        failure,
3731        created_at: raw.created_at,
3732        updated_at: raw.updated_at,
3733    })
3734}
3735
3736fn sqlite_error(error: rusqlite::Error) -> StoreError {
3737    StoreError::internal(format!("sqlite store error: {error}"))
3738}
3739
3740fn now_seconds() -> i64 {
3741    SystemTime::now()
3742        .duration_since(UNIX_EPOCH)
3743        .map(|duration| duration.as_secs() as i64)
3744        .unwrap_or_default()
3745}
3746
3747fn all_users_from_memory(users: &HashMap<i64, UserRecord>) -> Vec<UserRecord> {
3748    let mut users = users.values().cloned().collect::<Vec<_>>();
3749    users.sort_by_key(|user| user.user_id.get());
3750    users
3751}
3752
3753fn max_message_id_from_memory(
3754    messages: &HashMap<i64, Vec<MessageRecord>>,
3755    chat_id: InlineId,
3756) -> Option<InlineId> {
3757    messages
3758        .get(&chat_id.get())?
3759        .iter()
3760        .map(|message| message.message_id.get())
3761        .max()
3762        .map(InlineId::new)
3763}
3764
3765fn dialog_with_synced_through(
3766    mut dialog: DialogRecord,
3767    synced_through_message_id: Option<InlineId>,
3768) -> DialogRecord {
3769    dialog.synced_through_message_id = synced_through_message_id;
3770    dialog
3771}
3772
3773fn users_for_messages_from_memory(
3774    users: &HashMap<i64, UserRecord>,
3775    messages: &[MessageRecord],
3776) -> Vec<UserRecord> {
3777    let mut user_ids = messages
3778        .iter()
3779        .map(|message| message.sender_id.get())
3780        .collect::<Vec<_>>();
3781    user_ids.sort_unstable();
3782    user_ids.dedup();
3783    user_ids
3784        .into_iter()
3785        .filter_map(|user_id| users.get(&user_id).cloned())
3786        .collect()
3787}
3788
3789fn upsert_dialog(dialogs: &mut Vec<DialogRecord>, dialog: DialogRecord) {
3790    if let Some(existing) = dialogs
3791        .iter_mut()
3792        .find(|existing| existing.chat_id == dialog.chat_id)
3793    {
3794        *existing = dialog;
3795        return;
3796    }
3797    dialogs.push(dialog);
3798}
3799
3800fn insert_message(messages: &mut HashMap<i64, Vec<MessageRecord>>, message: MessageRecord) {
3801    let chat_id = message.chat_id.get();
3802    messages.entry(chat_id).or_default().push(message);
3803    if let Some(messages) = messages.get_mut(&chat_id) {
3804        messages.sort_by_key(|message| (message.timestamp, message.message_id.get()));
3805    }
3806}
3807
3808fn reaction_key(reaction: &StoredReaction) -> (i64, i64, i64, String) {
3809    (
3810        reaction.chat_id.get(),
3811        reaction.message_id.get(),
3812        reaction.user_id.get(),
3813        reaction.reaction.clone(),
3814    )
3815}
3816
3817fn sort_reactions(reactions: &mut [StoredReaction]) {
3818    reactions.sort_by(|left, right| {
3819        (left.user_id.get(), left.reaction.as_str())
3820            .cmp(&(right.user_id.get(), right.reaction.as_str()))
3821    });
3822}
3823
3824fn sort_participants(participants: &mut [ChatParticipantRecord]) {
3825    participants.sort_by_key(|participant| participant.user_id.get());
3826}
3827
3828/// Updates or inserts a dialog's last message pointer.
3829pub(crate) fn upsert_dialog_last_message(
3830    dialogs: &mut Vec<DialogRecord>,
3831    chat_id: InlineId,
3832    message_id: InlineId,
3833) {
3834    if let Some(dialog) = dialogs.iter_mut().find(|dialog| dialog.chat_id == chat_id) {
3835        dialog.last_message_id = Some(message_id);
3836        return;
3837    }
3838    dialogs.push(DialogRecord {
3839        chat_id,
3840        peer_user_id: None,
3841        title: None,
3842        last_message_id: Some(message_id),
3843        synced_through_message_id: None,
3844        unread_count: Some(0),
3845        ..DialogRecord::new(chat_id)
3846    });
3847}
3848
3849#[cfg(test)]
3850mod tests {
3851    use crate::{AuthToken, ExternalId, RandomId};
3852
3853    use super::*;
3854
3855    fn session() -> StoredSession {
3856        StoredSession {
3857            auth: AuthCredential::AccessToken {
3858                token: AuthToken::try_new("secret-token").unwrap(),
3859            },
3860            account_namespace: Some("team".to_owned()),
3861        }
3862    }
3863
3864    fn test_message(message_id: i64) -> MessageRecord {
3865        MessageRecord {
3866            chat_id: InlineId::new(7),
3867            message_id: InlineId::new(message_id),
3868            sender_id: InlineId::new(2),
3869            timestamp: message_id,
3870            is_outgoing: false,
3871            content: MessageContent::Text {
3872                text: format!("message {message_id}"),
3873            },
3874            reply_to_message_id: None,
3875            transaction: None,
3876        }
3877    }
3878
3879    #[tokio::test]
3880    async fn stored_session_debug_redacts_token() {
3881        let rendered = format!("{:?}", session());
3882
3883        assert!(rendered.contains("[redacted]"));
3884        assert!(rendered.contains("<redacted>"));
3885        assert!(!rendered.contains("secret-token"));
3886        assert!(!rendered.contains("team"));
3887    }
3888
3889    #[tokio::test]
3890    async fn in_memory_store_saves_and_clears_session() {
3891        let store = InMemoryStore::new();
3892        store.save_session(session()).await.unwrap();
3893
3894        assert!(store.load_session().await.unwrap().is_some());
3895
3896        store.clear_session().await.unwrap();
3897        assert!(store.load_session().await.unwrap().is_none());
3898    }
3899
3900    #[tokio::test]
3901    async fn in_memory_store_round_trips_sync_cursors() {
3902        let store = InMemoryStore::new();
3903        let user = SyncBucketKey::User;
3904        let direct = SyncBucketKey::Chat {
3905            peer: SyncBucketPeer::User {
3906                user_id: InlineId::new(2),
3907            },
3908        };
3909        let chat = SyncBucketKey::Chat {
3910            peer: SyncBucketPeer::Chat {
3911                chat_id: InlineId::new(7),
3912            },
3913        };
3914        let space = SyncBucketKey::Space {
3915            space_id: InlineId::new(9),
3916        };
3917
3918        assert_eq!(store.sync_state().await.unwrap(), SyncState::default());
3919        assert_eq!(
3920            store.sync_bucket_state(user).await.unwrap(),
3921            SyncBucketState::default()
3922        );
3923
3924        store
3925            .save_sync_state(SyncState { last_sync_date: 40 })
3926            .await
3927            .unwrap();
3928        store
3929            .save_sync_bucket_state(user, SyncBucketState { seq: 3, date: 30 })
3930            .await
3931            .unwrap();
3932        store
3933            .save_sync_bucket_states(vec![
3934                (direct, SyncBucketState { seq: 4, date: 31 }),
3935                (chat, SyncBucketState { seq: 5, date: 32 }),
3936                (space, SyncBucketState { seq: 6, date: 33 }),
3937            ])
3938            .await
3939            .unwrap();
3940        store
3941            .save_sync_bucket_state(user, SyncBucketState { seq: 7, date: 41 })
3942            .await
3943            .unwrap();
3944
3945        assert_eq!(
3946            store.sync_state().await.unwrap(),
3947            SyncState { last_sync_date: 40 }
3948        );
3949        assert_eq!(
3950            store.sync_bucket_state(user).await.unwrap(),
3951            SyncBucketState { seq: 7, date: 41 }
3952        );
3953        assert_eq!(
3954            store.sync_bucket_state(direct).await.unwrap(),
3955            SyncBucketState { seq: 4, date: 31 }
3956        );
3957        assert_eq!(
3958            store.sync_bucket_state(chat).await.unwrap(),
3959            SyncBucketState { seq: 5, date: 32 }
3960        );
3961        assert_eq!(
3962            store.sync_bucket_state(space).await.unwrap(),
3963            SyncBucketState { seq: 6, date: 33 }
3964        );
3965
3966        store.remove_sync_bucket_state(direct).await.unwrap();
3967        assert_eq!(
3968            store.sync_bucket_state(direct).await.unwrap(),
3969            SyncBucketState::default()
3970        );
3971
3972        store.clear_sync_state().await.unwrap();
3973        assert_eq!(store.sync_state().await.unwrap(), SyncState::default());
3974        assert_eq!(
3975            store.sync_bucket_state(user).await.unwrap(),
3976            SyncBucketState::default()
3977        );
3978        assert_eq!(
3979            store.sync_bucket_state(space).await.unwrap(),
3980            SyncBucketState::default()
3981        );
3982    }
3983
3984    #[tokio::test]
3985    async fn in_memory_store_lists_dialogs_and_history() {
3986        let store = InMemoryStore::new();
3987        store.upsert_dialog(DialogRecord {
3988            chat_id: InlineId::new(7),
3989            peer_user_id: None,
3990            title: Some("general".to_owned()),
3991            last_message_id: None,
3992            synced_through_message_id: None,
3993            unread_count: Some(0),
3994            ..DialogRecord::new(InlineId::new(7))
3995        });
3996        store
3997            .record_users(vec![UserRecord {
3998                user_id: InlineId::new(2),
3999                display_name: Some("Ada Lovelace".to_owned()),
4000                username: Some("ada".to_owned()),
4001                first_name: Some("Ada".to_owned()),
4002                last_name: Some("Lovelace".to_owned()),
4003                avatar_url: None,
4004                is_bot: Some(false),
4005            }])
4006            .await
4007            .unwrap();
4008        store
4009            .record_message(MessageRecord {
4010                chat_id: InlineId::new(7),
4011                message_id: InlineId::new(1),
4012                sender_id: InlineId::new(2),
4013                timestamp: 1,
4014                is_outgoing: false,
4015                content: MessageContent::Text {
4016                    text: "hello".to_owned(),
4017                },
4018                reply_to_message_id: None,
4019                transaction: None,
4020            })
4021            .await
4022            .unwrap();
4023
4024        let dialogs = store.dialogs(DialogsRequest::default()).await.unwrap();
4025        assert_eq!(dialogs.dialogs.len(), 1);
4026        assert_eq!(
4027            dialogs.dialogs[0].synced_through_message_id,
4028            Some(InlineId::new(1))
4029        );
4030        assert_eq!(dialogs.users.len(), 1);
4031        assert_eq!(
4032            dialogs.users[0].display_name.as_deref(),
4033            Some("Ada Lovelace")
4034        );
4035
4036        let history = store
4037            .history(HistoryRequest {
4038                chat_id: InlineId::new(7),
4039                limit: Some(10),
4040                before_message_id: None,
4041                after_message_id: None,
4042            })
4043            .await
4044            .unwrap();
4045        assert_eq!(history.messages.len(), 1);
4046        assert_eq!(history.users.len(), 1);
4047        assert_eq!(history.users[0].user_id, InlineId::new(2));
4048    }
4049
4050    #[tokio::test]
4051    async fn in_memory_store_records_transactions() {
4052        let store = InMemoryStore::new();
4053        let transaction = stored_transaction("txn-mem", TransactionState::Queued);
4054
4055        store.record_transaction(transaction.clone()).await.unwrap();
4056
4057        let loaded = store
4058            .transaction(transaction.identity.transaction_id.clone())
4059            .await
4060            .unwrap()
4061            .unwrap();
4062        assert_eq!(
4063            loaded.identity.transaction_id,
4064            transaction.identity.transaction_id
4065        );
4066        assert_eq!(loaded.state, TransactionState::Queued);
4067    }
4068
4069    #[tokio::test]
4070    async fn in_memory_store_persists_message_reaction_and_read_mutations() {
4071        let store = InMemoryStore::new();
4072        let message = test_message(10);
4073        let reaction = StoredReaction {
4074            chat_id: message.chat_id,
4075            message_id: message.message_id,
4076            user_id: InlineId::new(2),
4077            reaction: "👍".to_owned(),
4078        };
4079        let read_state = StoredReadState {
4080            chat_id: message.chat_id,
4081            read_max_id: Some(message.message_id),
4082            unread_count: Some(3),
4083            marked_unread: false,
4084        };
4085
4086        store.record_message(message.clone()).await.unwrap();
4087        store.record_reaction(reaction.clone()).await.unwrap();
4088        store.record_read_state(read_state).await.unwrap();
4089        assert_eq!(
4090            store
4091                .reactions(message.chat_id, message.message_id)
4092                .await
4093                .unwrap(),
4094            vec![reaction.clone()]
4095        );
4096        assert_eq!(
4097            store.read_state(message.chat_id).await.unwrap(),
4098            Some(read_state)
4099        );
4100
4101        store
4102            .record_message_deleted(message.chat_id, message.message_id)
4103            .await
4104            .unwrap();
4105        assert!(
4106            store
4107                .message_deleted(message.chat_id, message.message_id)
4108                .await
4109                .unwrap()
4110        );
4111        assert!(
4112            store
4113                .reactions(message.chat_id, message.message_id)
4114                .await
4115                .unwrap()
4116                .is_empty()
4117        );
4118
4119        store.record_message(message.clone()).await.unwrap();
4120        assert!(
4121            !store
4122                .message_deleted(message.chat_id, message.message_id)
4123                .await
4124                .unwrap()
4125        );
4126    }
4127
4128    #[tokio::test]
4129    async fn in_memory_store_fetches_history_after_checkpoint() {
4130        let store = InMemoryStore::new();
4131        for message_id in 1..=3 {
4132            store
4133                .record_message(test_message(message_id))
4134                .await
4135                .unwrap();
4136        }
4137
4138        let history = store
4139            .history(HistoryRequest {
4140                chat_id: InlineId::new(7),
4141                limit: Some(1),
4142                before_message_id: None,
4143                after_message_id: Some(InlineId::new(1)),
4144            })
4145            .await
4146            .unwrap();
4147
4148        assert_eq!(history.messages.len(), 1);
4149        assert_eq!(history.messages[0].message_id, InlineId::new(2));
4150        assert!(history.has_more);
4151    }
4152
4153    #[tokio::test]
4154    async fn in_memory_store_latest_history_keeps_newest_window() {
4155        let store = InMemoryStore::new();
4156        for message_id in 1..=4 {
4157            store
4158                .record_message(test_message(message_id))
4159                .await
4160                .unwrap();
4161        }
4162
4163        let history = store
4164            .history(HistoryRequest {
4165                chat_id: InlineId::new(7),
4166                limit: Some(2),
4167                before_message_id: None,
4168                after_message_id: None,
4169            })
4170            .await
4171            .unwrap();
4172
4173        assert!(history.has_more);
4174        assert_eq!(
4175            history
4176                .messages
4177                .iter()
4178                .map(|message| message.message_id)
4179                .collect::<Vec<_>>(),
4180            vec![InlineId::new(3), InlineId::new(4)]
4181        );
4182    }
4183
4184    #[tokio::test]
4185    async fn sqlite_store_persists_session_dialogs_and_history() {
4186        let path = sqlite_temp_path("persist");
4187        let store = SqliteStore::open(&path).unwrap();
4188
4189        store.save_session(session()).await.unwrap();
4190        store
4191            .upsert_dialog(DialogRecord {
4192                chat_id: InlineId::new(7),
4193                peer_user_id: Some(InlineId::new(2)),
4194                title: Some("general".to_owned()),
4195                emoji: Some("🚀".to_owned()),
4196                last_message_id: None,
4197                synced_through_message_id: None,
4198                unread_count: Some(3),
4199                space_id: Some(InlineId::new(5)),
4200                is_public: Some(true),
4201                archived: Some(false),
4202                pinned: Some(true),
4203                open: Some(true),
4204                chat_list_hidden: Some(false),
4205                order: Some("a0".to_owned()),
4206                pinned_order: Some("p0".to_owned()),
4207                notification_mode: Some(DialogNotificationMode::Mentions),
4208                follow_mode: Some(DialogFollowMode::Following),
4209                pinned_message_ids: vec![InlineId::new(99), InlineId::new(98)],
4210            })
4211            .unwrap();
4212        store
4213            .record_users(vec![UserRecord {
4214                user_id: InlineId::new(2),
4215                display_name: Some("Grace Hopper".to_owned()),
4216                username: Some("grace".to_owned()),
4217                first_name: Some("Grace".to_owned()),
4218                last_name: Some("Hopper".to_owned()),
4219                avatar_url: Some("https://cdn.inline.test/grace.jpg".to_owned()),
4220                is_bot: Some(false),
4221            }])
4222            .await
4223            .unwrap();
4224        store
4225            .record_message(MessageRecord {
4226                chat_id: InlineId::new(7),
4227                message_id: InlineId::new(10),
4228                sender_id: InlineId::new(2),
4229                timestamp: 100,
4230                is_outgoing: false,
4231                content: MessageContent::Text {
4232                    text: "persisted".to_owned(),
4233                },
4234                reply_to_message_id: None,
4235                transaction: None,
4236            })
4237            .await
4238            .unwrap();
4239        drop(store);
4240
4241        let reopened = SqliteStore::open(&path).unwrap();
4242        let session = reopened.load_session().await.unwrap().unwrap();
4243        assert_eq!(session.account_namespace.as_deref(), Some("team"));
4244        assert!(!format!("{reopened:?}").contains(path.to_string_lossy().as_ref()));
4245
4246        let dialogs = reopened.dialogs(DialogsRequest::default()).await.unwrap();
4247        assert_eq!(dialogs.dialogs.len(), 1);
4248        assert_eq!(dialogs.dialogs[0].chat_id, InlineId::new(7));
4249        assert_eq!(dialogs.dialogs[0].peer_user_id, Some(InlineId::new(2)));
4250        assert_eq!(dialogs.dialogs[0].title.as_deref(), Some("general"));
4251        assert_eq!(dialogs.dialogs[0].emoji.as_deref(), Some("🚀"));
4252        assert_eq!(dialogs.dialogs[0].last_message_id, Some(InlineId::new(10)));
4253        assert_eq!(dialogs.dialogs[0].space_id, Some(InlineId::new(5)));
4254        assert_eq!(dialogs.dialogs[0].is_public, Some(true));
4255        assert_eq!(dialogs.dialogs[0].archived, Some(false));
4256        assert_eq!(dialogs.dialogs[0].pinned, Some(true));
4257        assert_eq!(dialogs.dialogs[0].open, Some(true));
4258        assert_eq!(dialogs.dialogs[0].chat_list_hidden, Some(false));
4259        assert_eq!(dialogs.dialogs[0].order.as_deref(), Some("a0"));
4260        assert_eq!(dialogs.dialogs[0].pinned_order.as_deref(), Some("p0"));
4261        assert_eq!(
4262            dialogs.dialogs[0].notification_mode,
4263            Some(DialogNotificationMode::Mentions)
4264        );
4265        assert_eq!(
4266            dialogs.dialogs[0].follow_mode,
4267            Some(DialogFollowMode::Following)
4268        );
4269        assert_eq!(
4270            dialogs.dialogs[0].pinned_message_ids,
4271            vec![InlineId::new(99), InlineId::new(98)]
4272        );
4273        assert_eq!(
4274            dialogs.dialogs[0].synced_through_message_id,
4275            Some(InlineId::new(10))
4276        );
4277        assert_eq!(dialogs.users.len(), 1);
4278        assert_eq!(
4279            dialogs.users[0].display_name.as_deref(),
4280            Some("Grace Hopper")
4281        );
4282
4283        let history = reopened
4284            .history(HistoryRequest {
4285                chat_id: InlineId::new(7),
4286                limit: Some(10),
4287                before_message_id: None,
4288                after_message_id: None,
4289            })
4290            .await
4291            .unwrap();
4292        assert_eq!(history.messages.len(), 1);
4293        assert_eq!(history.messages[0].message_id, InlineId::new(10));
4294        assert!(matches!(
4295            history.messages[0].content,
4296            MessageContent::Text { ref text } if text == "persisted"
4297        ));
4298        assert_eq!(history.users.len(), 1);
4299        assert_eq!(
4300            history.users[0].avatar_url.as_deref(),
4301            Some("https://cdn.inline.test/grace.jpg")
4302        );
4303
4304        let _ = std::fs::remove_file(path);
4305    }
4306
4307    #[cfg(unix)]
4308    #[tokio::test]
4309    async fn sqlite_store_file_permissions_are_private() {
4310        use std::os::unix::fs::PermissionsExt;
4311
4312        let path = sqlite_temp_path("permissions");
4313        let store = SqliteStore::open(&path).unwrap();
4314        store.save_session(session()).await.unwrap();
4315
4316        let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
4317        assert_eq!(mode, 0o600);
4318
4319        drop(store);
4320        let _ = std::fs::remove_file(path);
4321    }
4322
4323    async fn assert_clear_history_store(store: &dyn ClientStore) {
4324        let mut dialog = DialogRecord::new(InlineId::new(7));
4325        dialog.space_id = Some(InlineId::new(5));
4326        store.record_dialog(dialog).await.unwrap();
4327        for (message_id, timestamp) in [(10, 100), (11, 200)] {
4328            store
4329                .record_message(MessageRecord {
4330                    chat_id: InlineId::new(7),
4331                    message_id: InlineId::new(message_id),
4332                    sender_id: InlineId::new(2),
4333                    timestamp,
4334                    is_outgoing: false,
4335                    content: MessageContent::Text {
4336                        text: format!("message {message_id}"),
4337                    },
4338                    reply_to_message_id: None,
4339                    transaction: None,
4340                })
4341                .await
4342                .unwrap();
4343        }
4344        store
4345            .record_reaction(StoredReaction {
4346                chat_id: InlineId::new(7),
4347                message_id: InlineId::new(10),
4348                user_id: InlineId::new(2),
4349                reaction: "ok".to_owned(),
4350            })
4351            .await
4352            .unwrap();
4353
4354        assert_eq!(
4355            store.chat_ids_in_space(InlineId::new(5)).await.unwrap(),
4356            vec![InlineId::new(7)]
4357        );
4358        assert_eq!(
4359            store
4360                .clear_chat_messages(InlineId::new(7), Some(150))
4361                .await
4362                .unwrap(),
4363            vec![InlineId::new(10)]
4364        );
4365        assert!(
4366            store
4367                .message_deleted(InlineId::new(7), InlineId::new(10))
4368                .await
4369                .unwrap()
4370        );
4371        assert!(
4372            store
4373                .reactions(InlineId::new(7), InlineId::new(10))
4374                .await
4375                .unwrap()
4376                .is_empty()
4377        );
4378        let history = store
4379            .history(HistoryRequest {
4380                chat_id: InlineId::new(7),
4381                limit: Some(10),
4382                before_message_id: None,
4383                after_message_id: None,
4384            })
4385            .await
4386            .unwrap();
4387        assert_eq!(
4388            history
4389                .messages
4390                .iter()
4391                .map(|message| message.message_id)
4392                .collect::<Vec<_>>(),
4393            vec![InlineId::new(11)]
4394        );
4395        assert_eq!(
4396            store
4397                .dialog(InlineId::new(7))
4398                .await
4399                .unwrap()
4400                .unwrap()
4401                .last_message_id,
4402            Some(InlineId::new(11))
4403        );
4404    }
4405
4406    #[tokio::test]
4407    async fn stores_clear_history_with_tombstones_and_space_lookup() {
4408        let memory = InMemoryStore::new();
4409        assert_clear_history_store(&memory).await;
4410        let sqlite = SqliteStore::open_in_memory().unwrap();
4411        assert_clear_history_store(&sqlite).await;
4412    }
4413
4414    async fn assert_space_and_settings_store(store: &dyn ClientStore) {
4415        let space = SpaceRecord {
4416            space_id: InlineId::new(5),
4417            name: "Engineering".to_owned(),
4418            creator: true,
4419            date: 100,
4420            is_public: Some(false),
4421        };
4422        let member = SpaceMemberRecord {
4423            space_id: InlineId::new(5),
4424            user_id: InlineId::new(2),
4425            role: Some(crate::SpaceMemberRole::Admin),
4426            date: 101,
4427            can_access_public_chats: true,
4428        };
4429        let settings = UserSettingsRecord {
4430            notification_mode: Some(crate::NotificationMode::Mentions),
4431            silent: Some(true),
4432            zen_mode_requires_mention: Some(true),
4433            zen_mode_uses_default_rules: Some(false),
4434            zen_mode_custom_rules: Some("important".to_owned()),
4435            disable_dm_notifications: Some(false),
4436        };
4437
4438        store.record_space(space.clone()).await.unwrap();
4439        store.record_space_member(member.clone()).await.unwrap();
4440        store.record_user_settings(settings.clone()).await.unwrap();
4441
4442        assert_eq!(store.space(InlineId::new(5)).await.unwrap(), Some(space));
4443        assert_eq!(
4444            store.space_members(InlineId::new(5)).await.unwrap(),
4445            vec![member]
4446        );
4447        assert_eq!(store.user_settings().await.unwrap(), Some(settings));
4448        store
4449            .remove_space_member(InlineId::new(5), InlineId::new(2))
4450            .await
4451            .unwrap();
4452        assert!(
4453            store
4454                .space_members(InlineId::new(5))
4455                .await
4456                .unwrap()
4457                .is_empty()
4458        );
4459    }
4460
4461    #[tokio::test]
4462    async fn stores_persist_spaces_members_and_user_settings() {
4463        let memory = InMemoryStore::new();
4464        assert_space_and_settings_store(&memory).await;
4465        let sqlite = SqliteStore::open_in_memory().unwrap();
4466        assert_space_and_settings_store(&sqlite).await;
4467    }
4468
4469    #[tokio::test]
4470    async fn sqlite_store_persists_sync_cursors_across_reopen() {
4471        let path = sqlite_temp_path("sync-cursors");
4472        let store = SqliteStore::open(&path).unwrap();
4473        let user = SyncBucketKey::User;
4474        let direct = SyncBucketKey::Chat {
4475            peer: SyncBucketPeer::User {
4476                user_id: InlineId::new(2),
4477            },
4478        };
4479        let chat = SyncBucketKey::Chat {
4480            peer: SyncBucketPeer::Chat {
4481                chat_id: InlineId::new(7),
4482            },
4483        };
4484        let space = SyncBucketKey::Space {
4485            space_id: InlineId::new(9),
4486        };
4487
4488        store
4489            .save_sync_state(SyncState { last_sync_date: 80 })
4490            .await
4491            .unwrap();
4492        store
4493            .save_sync_bucket_states(vec![
4494                (user, SyncBucketState { seq: 10, date: 70 }),
4495                (direct, SyncBucketState { seq: 11, date: 71 }),
4496                (chat, SyncBucketState { seq: 12, date: 72 }),
4497                (space, SyncBucketState { seq: 13, date: 73 }),
4498            ])
4499            .await
4500            .unwrap();
4501        store
4502            .save_sync_bucket_state(direct, SyncBucketState { seq: 14, date: 74 })
4503            .await
4504            .unwrap();
4505        drop(store);
4506
4507        let reopened = SqliteStore::open(&path).unwrap();
4508        assert_eq!(
4509            reopened.sync_state().await.unwrap(),
4510            SyncState { last_sync_date: 80 }
4511        );
4512        assert_eq!(
4513            reopened.sync_bucket_state(user).await.unwrap(),
4514            SyncBucketState { seq: 10, date: 70 }
4515        );
4516        assert_eq!(
4517            reopened.sync_bucket_state(direct).await.unwrap(),
4518            SyncBucketState { seq: 14, date: 74 }
4519        );
4520        assert_eq!(
4521            reopened.sync_bucket_state(chat).await.unwrap(),
4522            SyncBucketState { seq: 12, date: 72 }
4523        );
4524        assert_eq!(
4525            reopened.sync_bucket_state(space).await.unwrap(),
4526            SyncBucketState { seq: 13, date: 73 }
4527        );
4528
4529        reopened.remove_sync_bucket_state(chat).await.unwrap();
4530        assert_eq!(
4531            reopened.sync_bucket_state(chat).await.unwrap(),
4532            SyncBucketState::default()
4533        );
4534        reopened.clear_sync_state().await.unwrap();
4535        assert_eq!(reopened.sync_state().await.unwrap(), SyncState::default());
4536        assert_eq!(
4537            reopened.sync_bucket_state(space).await.unwrap(),
4538            SyncBucketState::default()
4539        );
4540
4541        drop(reopened);
4542        let _ = std::fs::remove_file(path);
4543    }
4544
4545    #[tokio::test]
4546    async fn sqlite_store_upgrades_initial_client_schema_in_place() {
4547        let path = sqlite_temp_path("initial-client-upgrade");
4548        let connection = Connection::open(&path).unwrap();
4549        connection
4550            .execute_batch(
4551                "
4552                CREATE TABLE sessions (
4553                    id INTEGER PRIMARY KEY CHECK (id = 1),
4554                    auth_json TEXT NOT NULL,
4555                    account_namespace TEXT,
4556                    updated_at INTEGER NOT NULL
4557                );
4558                CREATE TABLE dialogs (
4559                    chat_id INTEGER PRIMARY KEY,
4560                    peer_user_id INTEGER,
4561                    title TEXT,
4562                    last_message_id INTEGER,
4563                    unread_count INTEGER,
4564                    updated_at INTEGER NOT NULL
4565                );
4566                CREATE TABLE users (
4567                    user_id INTEGER PRIMARY KEY,
4568                    display_name TEXT,
4569                    username TEXT,
4570                    first_name TEXT,
4571                    last_name TEXT,
4572                    avatar_url TEXT,
4573                    is_bot INTEGER,
4574                    updated_at INTEGER NOT NULL
4575                );
4576                CREATE TABLE messages (
4577                    chat_id INTEGER NOT NULL,
4578                    message_id INTEGER NOT NULL,
4579                    sender_id INTEGER NOT NULL,
4580                    timestamp INTEGER NOT NULL,
4581                    is_outgoing INTEGER NOT NULL,
4582                    content_json TEXT NOT NULL,
4583                    reply_to_message_id INTEGER,
4584                    transaction_json TEXT,
4585                    PRIMARY KEY (chat_id, message_id)
4586                );
4587                CREATE TABLE transactions (
4588                    transaction_id TEXT PRIMARY KEY,
4589                    identity_json TEXT NOT NULL,
4590                    state_json TEXT NOT NULL,
4591                    chat_id INTEGER,
4592                    message_id INTEGER,
4593                    random_id INTEGER NOT NULL,
4594                    external_source TEXT,
4595                    external_id TEXT,
4596                    failure_json TEXT,
4597                    created_at INTEGER NOT NULL,
4598                    updated_at INTEGER NOT NULL
4599                );
4600                ",
4601            )
4602            .unwrap();
4603        let initial_session = session();
4604        connection
4605            .execute(
4606                "INSERT INTO sessions (id, auth_json, account_namespace, updated_at)
4607                 VALUES (1, ?1, ?2, 1)",
4608                params![
4609                    serde_json::to_string(&initial_session.auth).unwrap(),
4610                    initial_session.account_namespace
4611                ],
4612            )
4613            .unwrap();
4614        connection
4615            .execute(
4616                "INSERT INTO dialogs (
4617                   chat_id, peer_user_id, title, last_message_id, unread_count, updated_at
4618                 ) VALUES (7, 2, 'General', 10, 1, 1)",
4619                [],
4620            )
4621            .unwrap();
4622        connection
4623            .execute(
4624                "INSERT INTO messages (
4625                   chat_id, message_id, sender_id, timestamp, is_outgoing,
4626                   content_json, reply_to_message_id, transaction_json
4627                 ) VALUES (7, 10, 2, 10, 0, ?1, NULL, NULL)",
4628                params![
4629                    serde_json::to_string(&MessageContent::Text {
4630                        text: "from the initial client".to_owned(),
4631                    })
4632                    .unwrap()
4633                ],
4634            )
4635            .unwrap();
4636        drop(connection);
4637
4638        let upgraded = SqliteStore::open(&path).unwrap();
4639        assert_eq!(
4640            upgraded
4641                .load_session()
4642                .await
4643                .unwrap()
4644                .unwrap()
4645                .account_namespace
4646                .as_deref(),
4647            Some("team")
4648        );
4649        let dialogs = upgraded.dialogs(DialogsRequest::default()).await.unwrap();
4650        assert_eq!(dialogs.dialogs.len(), 1);
4651        assert_eq!(dialogs.dialogs[0].chat_id, InlineId::new(7));
4652        assert_eq!(dialogs.dialogs[0].peer_user_id, Some(InlineId::new(2)));
4653        assert_eq!(dialogs.dialogs[0].title.as_deref(), Some("General"));
4654        let history = upgraded
4655            .history(HistoryRequest {
4656                chat_id: InlineId::new(7),
4657                limit: Some(10),
4658                before_message_id: None,
4659                after_message_id: None,
4660            })
4661            .await
4662            .unwrap();
4663        assert_eq!(history.messages.len(), 1);
4664        assert!(matches!(
4665            &history.messages[0].content,
4666            MessageContent::Text { text } if text == "from the initial client"
4667        ));
4668        assert_eq!(upgraded.sync_state().await.unwrap(), SyncState::default());
4669        upgraded
4670            .save_sync_bucket_state(SyncBucketKey::User, SyncBucketState { seq: 3, date: 30 })
4671            .await
4672            .unwrap();
4673        assert_eq!(
4674            upgraded
4675                .sync_bucket_state(SyncBucketKey::User)
4676                .await
4677                .unwrap(),
4678            SyncBucketState { seq: 3, date: 30 }
4679        );
4680
4681        drop(upgraded);
4682        let _ = std::fs::remove_file(path);
4683    }
4684
4685    #[tokio::test]
4686    async fn sqlite_store_persists_message_tombstones_reactions_and_read_state() {
4687        let path = sqlite_temp_path("message-state");
4688        let store = SqliteStore::open(&path).unwrap();
4689        let message = test_message(10);
4690        let reaction = StoredReaction {
4691            chat_id: message.chat_id,
4692            message_id: message.message_id,
4693            user_id: InlineId::new(2),
4694            reaction: "🔥".to_owned(),
4695        };
4696        let read_state = StoredReadState {
4697            chat_id: message.chat_id,
4698            read_max_id: Some(message.message_id),
4699            unread_count: Some(1),
4700            marked_unread: true,
4701        };
4702
4703        store.record_message(message.clone()).await.unwrap();
4704        store.record_reaction(reaction).await.unwrap();
4705        store.record_read_state(read_state).await.unwrap();
4706        store
4707            .record_message_deleted(message.chat_id, message.message_id)
4708            .await
4709            .unwrap();
4710        drop(store);
4711
4712        let reopened = SqliteStore::open(&path).unwrap();
4713        assert!(
4714            reopened
4715                .message_deleted(message.chat_id, message.message_id)
4716                .await
4717                .unwrap()
4718        );
4719        assert!(
4720            reopened
4721                .reactions(message.chat_id, message.message_id)
4722                .await
4723                .unwrap()
4724                .is_empty()
4725        );
4726        assert_eq!(
4727            reopened.read_state(message.chat_id).await.unwrap(),
4728            Some(read_state)
4729        );
4730
4731        reopened.record_message(message.clone()).await.unwrap();
4732        assert!(
4733            !reopened
4734                .message_deleted(message.chat_id, message.message_id)
4735                .await
4736                .unwrap()
4737        );
4738
4739        drop(reopened);
4740        let _ = std::fs::remove_file(path);
4741    }
4742
4743    #[tokio::test]
4744    async fn sqlite_store_preserves_complete_reconciliation_markers_and_chat_tombstones() {
4745        let path = sqlite_temp_path("reconciliation-state");
4746        let store = SqliteStore::open(&path).unwrap();
4747        let message = test_message(10);
4748        store
4749            .record_dialog(DialogRecord::new(message.chat_id))
4750            .await
4751            .unwrap();
4752        store.record_message(message.clone()).await.unwrap();
4753        store
4754            .record_chat_participants(message.chat_id, Vec::new())
4755            .await
4756            .unwrap();
4757        store
4758            .replace_message_reactions(
4759                message.chat_id,
4760                message.message_id,
4761                vec![StoredReaction {
4762                    chat_id: message.chat_id,
4763                    message_id: message.message_id,
4764                    user_id: InlineId::new(2),
4765                    reaction: "👍".to_owned(),
4766                }],
4767            )
4768            .await
4769            .unwrap();
4770
4771        assert!(
4772            store
4773                .chat_participants_complete(message.chat_id)
4774                .await
4775                .unwrap()
4776        );
4777        assert_eq!(
4778            store
4779                .reaction_snapshot_message_ids(message.chat_id)
4780                .await
4781                .unwrap(),
4782            vec![message.message_id]
4783        );
4784
4785        store.remove_dialog(message.chat_id).await.unwrap();
4786        drop(store);
4787        let reopened = SqliteStore::open(&path).unwrap();
4788        assert_eq!(
4789            reopened.deleted_chat_ids().await.unwrap(),
4790            vec![message.chat_id]
4791        );
4792        assert_eq!(
4793            reopened.deleted_message_ids(message.chat_id).await.unwrap(),
4794            vec![message.message_id]
4795        );
4796        assert!(
4797            !reopened
4798                .chat_participants_complete(message.chat_id)
4799                .await
4800                .unwrap()
4801        );
4802
4803        drop(reopened);
4804        let _ = std::fs::remove_file(path);
4805    }
4806
4807    #[tokio::test]
4808    async fn sqlite_store_commits_sync_cursor_and_journal_removal_atomically() {
4809        let path = sqlite_temp_path("sync-journal");
4810        let key = SyncBucketKey::Chat {
4811            peer: SyncBucketPeer::Chat {
4812                chat_id: InlineId::new(7),
4813            },
4814        };
4815        let state = SyncBucketState { seq: 9, date: 90 };
4816        let store = SqliteStore::open(&path).unwrap();
4817        store
4818            .save_pending_sync_batch(PendingSyncBatch {
4819                key,
4820                committed_state: state,
4821                payload: vec![1, 2, 3],
4822            })
4823            .await
4824            .unwrap();
4825        drop(store);
4826
4827        let reopened = SqliteStore::open(&path).unwrap();
4828        assert_eq!(
4829            reopened.pending_sync_batches().await.unwrap(),
4830            vec![PendingSyncBatch {
4831                key,
4832                committed_state: state,
4833                payload: vec![1, 2, 3],
4834            }]
4835        );
4836        reopened
4837            .commit_pending_sync_batch(key, state)
4838            .await
4839            .unwrap();
4840        assert_eq!(reopened.sync_bucket_state(key).await.unwrap(), state);
4841        assert!(reopened.pending_sync_batches().await.unwrap().is_empty());
4842
4843        drop(reopened);
4844        let _ = std::fs::remove_file(path);
4845    }
4846
4847    #[tokio::test]
4848    async fn sqlite_store_persists_transactions() {
4849        let path = sqlite_temp_path("transactions");
4850        let store = SqliteStore::open(&path).unwrap();
4851        let transaction = stored_transaction("txn-sqlite", TransactionState::Sent)
4852            .with_chat_id(InlineId::new(7))
4853            .with_message_id(InlineId::new(11));
4854
4855        store.record_transaction(transaction.clone()).await.unwrap();
4856        drop(store);
4857
4858        let reopened = SqliteStore::open(&path).unwrap();
4859        let loaded = reopened
4860            .transaction(transaction.identity.transaction_id.clone())
4861            .await
4862            .unwrap()
4863            .unwrap();
4864
4865        assert_eq!(
4866            loaded.identity.transaction_id,
4867            transaction.identity.transaction_id
4868        );
4869        assert_eq!(
4870            loaded.identity.external_id,
4871            transaction.identity.external_id
4872        );
4873        assert_eq!(loaded.state, TransactionState::Sent);
4874        assert_eq!(loaded.chat_id, Some(InlineId::new(7)));
4875        assert_eq!(loaded.message_id, Some(InlineId::new(11)));
4876
4877        let _ = std::fs::remove_file(path);
4878    }
4879
4880    #[tokio::test]
4881    async fn sqlite_store_replaces_messages_by_chat_and_message_id() {
4882        let store = SqliteStore::open_in_memory().unwrap();
4883
4884        store
4885            .record_message(MessageRecord {
4886                chat_id: InlineId::new(7),
4887                message_id: InlineId::new(10),
4888                sender_id: InlineId::new(2),
4889                timestamp: 100,
4890                is_outgoing: false,
4891                content: MessageContent::Text {
4892                    text: "first".to_owned(),
4893                },
4894                reply_to_message_id: None,
4895                transaction: None,
4896            })
4897            .await
4898            .unwrap();
4899        store
4900            .record_message(MessageRecord {
4901                chat_id: InlineId::new(7),
4902                message_id: InlineId::new(10),
4903                sender_id: InlineId::new(2),
4904                timestamp: 101,
4905                is_outgoing: false,
4906                content: MessageContent::Text {
4907                    text: "updated".to_owned(),
4908                },
4909                reply_to_message_id: None,
4910                transaction: None,
4911            })
4912            .await
4913            .unwrap();
4914
4915        let history = store
4916            .history(HistoryRequest {
4917                chat_id: InlineId::new(7),
4918                limit: Some(10),
4919                before_message_id: None,
4920                after_message_id: None,
4921            })
4922            .await
4923            .unwrap();
4924        assert_eq!(history.messages.len(), 1);
4925        assert!(matches!(
4926            history.messages[0].content,
4927            MessageContent::Text { ref text } if text == "updated"
4928        ));
4929    }
4930
4931    #[tokio::test]
4932    async fn sqlite_store_history_windows_match_client_cursors() {
4933        let store = SqliteStore::open_in_memory().unwrap();
4934        for message_id in 1..=4 {
4935            store
4936                .record_message(test_message(message_id))
4937                .await
4938                .unwrap();
4939        }
4940
4941        let latest = store
4942            .history(HistoryRequest {
4943                chat_id: InlineId::new(7),
4944                limit: Some(2),
4945                before_message_id: None,
4946                after_message_id: None,
4947            })
4948            .await
4949            .unwrap();
4950        assert!(latest.has_more);
4951        assert_eq!(
4952            latest
4953                .messages
4954                .iter()
4955                .map(|message| message.message_id)
4956                .collect::<Vec<_>>(),
4957            vec![InlineId::new(3), InlineId::new(4)]
4958        );
4959
4960        let newer = store
4961            .history(HistoryRequest {
4962                chat_id: InlineId::new(7),
4963                limit: Some(2),
4964                before_message_id: None,
4965                after_message_id: Some(InlineId::new(1)),
4966            })
4967            .await
4968            .unwrap();
4969        assert!(newer.has_more);
4970        assert_eq!(
4971            newer
4972                .messages
4973                .iter()
4974                .map(|message| message.message_id)
4975                .collect::<Vec<_>>(),
4976            vec![InlineId::new(2), InlineId::new(3)]
4977        );
4978
4979        let older = store
4980            .history(HistoryRequest {
4981                chat_id: InlineId::new(7),
4982                limit: Some(2),
4983                before_message_id: Some(InlineId::new(4)),
4984                after_message_id: None,
4985            })
4986            .await
4987            .unwrap();
4988        assert!(older.has_more);
4989        assert_eq!(
4990            older
4991                .messages
4992                .iter()
4993                .map(|message| message.message_id)
4994                .collect::<Vec<_>>(),
4995            vec![InlineId::new(2), InlineId::new(3)]
4996        );
4997    }
4998
4999    fn sqlite_temp_path(label: &str) -> std::path::PathBuf {
5000        let unique = SystemTime::now()
5001            .duration_since(UNIX_EPOCH)
5002            .unwrap()
5003            .as_nanos();
5004        std::env::temp_dir().join(format!(
5005            "inline-client-{label}-{}-{unique}.db",
5006            std::process::id()
5007        ))
5008    }
5009
5010    fn stored_transaction(id: &str, state: TransactionState) -> StoredTransaction {
5011        StoredTransaction::new(
5012            TransactionIdentity::new(
5013                TransactionId::try_new(id).unwrap(),
5014                Some(ExternalId::try_new("host-event", "event-1").unwrap()),
5015                RandomId::new(42),
5016            ),
5017            state,
5018        )
5019    }
5020}