coven/
db.rs

1//! Host database integration for sync bookkeeping.
2//!
3//! coven owns three bookkeeping tables — `sync_cursors`, `sync_state`,
4//! `cloud_outbox` — created by applying [`MIGRATION_SQL`] to the host's
5//! database. The host implements [`SyncBookkeeping`] (coven calls these during
6//! a sync cycle) and [`RawDbHandle`] (the session extension attaches to the
7//! host's write connection). coven imposes no SQLite driver this way.
8
9use std::collections::HashMap;
10
11use async_trait::async_trait;
12
13/// SQL that creates coven's bookkeeping tables. The host applies this alongside
14/// its own schema migration. Idempotent (`IF NOT EXISTS`).
15pub const MIGRATION_SQL: &str = "\
16CREATE TABLE IF NOT EXISTS sync_cursors (
17    device_id TEXT PRIMARY KEY,
18    last_seq INTEGER NOT NULL
19);
20
21CREATE TABLE IF NOT EXISTS sync_state (
22    key TEXT PRIMARY KEY,
23    value TEXT NOT NULL
24);
25
26CREATE TABLE IF NOT EXISTS cloud_outbox (
27    id INTEGER PRIMARY KEY AUTOINCREMENT,
28    operation TEXT NOT NULL CHECK (operation IN ('upload', 'delete')),
29    file_id TEXT NOT NULL,
30    cloud_key TEXT NOT NULL,
31    source_path TEXT,
32    created_at TEXT NOT NULL,
33    min_seq INTEGER,
34    UNIQUE(operation, cloud_key)
35);
36";
37
38/// An error from the host's bookkeeping implementation.
39#[derive(Debug, thiserror::Error)]
40#[error("sync bookkeeping error: {0}")]
41pub struct DbError(pub String);
42
43/// A pending cloud blob operation from the `cloud_outbox` table.
44#[derive(Debug, Clone)]
45pub struct OutboxEntry {
46    pub id: i64,
47    pub operation: OutboxOperation,
48    pub file_id: String,
49    pub cloud_key: String,
50    pub source_path: Option<String>,
51    pub created_at: String,
52    pub min_seq: Option<u64>,
53}
54
55/// Type of cloud blob operation.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum OutboxOperation {
58    Upload,
59    Delete,
60}
61
62impl OutboxOperation {
63    pub fn parse(s: &str) -> Option<Self> {
64        match s {
65            "upload" => Some(OutboxOperation::Upload),
66            "delete" => Some(OutboxOperation::Delete),
67            _ => None,
68        }
69    }
70}
71
72/// Bookkeeping the host's database performs against coven's tables. coven calls
73/// these during a sync cycle; the host's implementation runs the SQL.
74#[async_trait]
75pub trait SyncBookkeeping: Send + Sync {
76    /// Read a value from `sync_state` by key.
77    async fn get_sync_state(&self, key: &str) -> Result<Option<String>, DbError>;
78
79    /// Write a value to `sync_state`.
80    async fn set_sync_state(&self, key: &str, value: &str) -> Result<(), DbError>;
81
82    /// All per-device cursors from `sync_cursors` as `device_id -> last_seq`.
83    async fn get_all_sync_cursors(&self) -> Result<HashMap<String, u64>, DbError>;
84
85    /// Upsert a single device cursor.
86    async fn set_sync_cursor(&self, device_id: &str, seq: u64) -> Result<(), DbError>;
87
88    /// Pending `upload` entries from `cloud_outbox`, oldest first.
89    async fn get_pending_cloud_uploads(&self) -> Result<Vec<OutboxEntry>, DbError>;
90
91    /// Pending `delete` entries from `cloud_outbox`.
92    async fn get_pending_cloud_deletes(&self) -> Result<Vec<OutboxEntry>, DbError>;
93
94    /// Whether any `upload` entries remain (gates changeset push).
95    async fn has_pending_cloud_uploads(&self) -> Result<bool, DbError>;
96
97    /// Remove a `cloud_outbox` entry by id.
98    async fn remove_cloud_outbox_entry(&self, id: i64) -> Result<(), DbError>;
99}
100
101/// Access to the host's raw write connection, which the session extension
102/// attaches to. The same connection the host writes through.
103#[async_trait]
104pub trait RawDbHandle: Send + Sync {
105    /// Acquire the raw sqlite3 write connection pointer the session extension
106    /// attaches to. The same connection the host writes through.
107    ///
108    /// # Safety
109    /// The pointer must outlive all sync sessions; the caller serializes session
110    /// operations on it.
111    async fn raw_write_handle(&self) -> Result<*mut libsqlite3_sys::sqlite3, DbError>;
112}
113
114/// The full database surface coven needs from the host: bookkeeping plus the
115/// raw write handle. Blanket-implemented for any type providing both.
116pub trait SyncDb: SyncBookkeeping + RawDbHandle {}
117impl<T: SyncBookkeeping + RawDbHandle> SyncDb for T {}