radicle/node/refs/
store.rs

1#![allow(clippy::type_complexity)]
2use std::num::TryFromIntError;
3use std::str::FromStr;
4
5use localtime::LocalTime;
6use sqlite as sql;
7use thiserror::Error;
8
9use crate::git::{Oid, Qualified};
10use crate::node::Database;
11use crate::node::NodeId;
12use crate::prelude::RepoId;
13use crate::storage;
14use crate::storage::{ReadRepository, ReadStorage, RemoteRepository, RepositoryError};
15
16#[derive(Error, Debug)]
17pub enum Error {
18    /// An Internal error.
19    #[error("internal error: {0}")]
20    Internal(#[from] sql::Error),
21    /// Timestamp error.
22    #[error("invalid timestamp: {0}")]
23    Timestamp(#[from] TryFromIntError),
24    /// Repository error.
25    #[error("repository error: {0}")]
26    Repository(#[from] RepositoryError),
27    /// Storage error.
28    #[error("storage error: {0}")]
29    Storage(#[from] storage::Error),
30    /// Storage refs error.
31    #[error("storage refs error: {0}")]
32    Refs(#[from] storage::refs::Error),
33    /// No rows returned in query result.
34    #[error("no rows returned")]
35    NoRows,
36}
37
38/// Refs store.
39///
40/// Used to cache git references.
41pub trait Store {
42    /// Set a reference under a remote namespace to the given [`Oid`].
43    fn set(
44        &mut self,
45        repo: &RepoId,
46        namespace: &NodeId,
47        refname: &Qualified,
48        oid: Oid,
49        timestamp: LocalTime,
50    ) -> Result<bool, Error>;
51    /// Get a reference's [`Oid`] and timestamp.
52    fn get(
53        &self,
54        repo: &RepoId,
55        namespace: &NodeId,
56        refname: &Qualified,
57    ) -> Result<Option<(Oid, LocalTime)>, Error>;
58    /// Delete a reference.
59    fn delete(
60        &mut self,
61        repo: &RepoId,
62        namespace: &NodeId,
63        refname: &Qualified,
64    ) -> Result<bool, Error>;
65    /// Populate the database from storage.
66    fn populate<S: ReadStorage>(&mut self, storage: &S) -> Result<(), Error>;
67    /// Return the number of references.
68    fn count(&self) -> Result<usize, Error>;
69    /// Check if there are any references.
70    fn is_empty(&self) -> Result<bool, Error> {
71        self.count().map(|l| l == 0)
72    }
73}
74
75impl Store for Database {
76    fn set(
77        &mut self,
78        repo: &RepoId,
79        namespace: &NodeId,
80        refname: &Qualified,
81        oid: Oid,
82        timestamp: LocalTime,
83    ) -> Result<bool, Error> {
84        let mut stmt = self.db.prepare(
85            "INSERT INTO `refs` (repo, namespace, ref, oid, timestamp)
86             VALUES (?1, ?2, ?3, ?4, ?5)
87             ON CONFLICT DO UPDATE
88             SET oid = ?4, timestamp = ?5
89             WHERE timestamp < ?5 AND oid <> ?4",
90        )?;
91        stmt.bind((1, repo))?;
92        stmt.bind((2, namespace))?;
93        stmt.bind((3, refname.to_string().as_str()))?;
94        stmt.bind((4, oid.to_string().as_str()))?;
95        stmt.bind((5, i64::try_from(timestamp.as_millis())?))?;
96        stmt.next()?;
97
98        Ok(self.db.change_count() > 0)
99    }
100
101    fn get(
102        &self,
103        repo: &RepoId,
104        namespace: &NodeId,
105        refname: &Qualified,
106    ) -> Result<Option<(Oid, LocalTime)>, Error> {
107        let mut stmt = self.db.prepare(
108            "SELECT oid, timestamp FROM refs WHERE repo = ?1 AND namespace = ?2 AND ref = ?3",
109        )?;
110
111        stmt.bind((1, repo))?;
112        stmt.bind((2, namespace))?;
113        stmt.bind((3, refname.to_string().as_str()))?;
114
115        if let Some(Ok(row)) = stmt.into_iter().next() {
116            let oid = row.try_read::<&str, _>("oid")?;
117            let oid = Oid::from_str(oid).map_err(|e| {
118                Error::Internal(sql::Error {
119                    code: None,
120                    message: Some(format!("sql: invalid oid '{oid}': {e}")),
121                })
122            })?;
123            let timestamp = row.try_read::<i64, _>("timestamp")?;
124            let timestamp = LocalTime::from_millis(timestamp as u128);
125
126            Ok(Some((oid, timestamp)))
127        } else {
128            Ok(None)
129        }
130    }
131
132    fn delete(
133        &mut self,
134        repo: &RepoId,
135        namespace: &NodeId,
136        refname: &Qualified,
137    ) -> Result<bool, Error> {
138        let mut stmt = self
139            .db
140            .prepare("DELETE FROM refs WHERE repo = ?1 AND namespace = ?2 AND ref = ?3")?;
141
142        stmt.bind((1, repo))?;
143        stmt.bind((2, namespace))?;
144        stmt.bind((3, refname.to_string().as_str()))?;
145        stmt.next()?;
146
147        Ok(self.db.change_count() > 0)
148    }
149
150    fn count(&self) -> Result<usize, Error> {
151        let row = self
152            .db
153            .prepare("SELECT COUNT(*) FROM refs")?
154            .into_iter()
155            .next()
156            .ok_or(Error::NoRows)??;
157        let count = row.read::<i64, _>(0) as usize;
158
159        Ok(count)
160    }
161
162    fn populate<S: ReadStorage>(&mut self, storage: &S) -> Result<(), Error> {
163        let now = LocalTime::now();
164
165        for info in storage.repositories()? {
166            let repo = storage.repository(info.rid)?;
167            for refs_at in repo.remote_refs_at()? {
168                self.set(&repo.id(), &refs_at.remote, refs_at.path(), refs_at.at, now)?;
169            }
170        }
171        Ok(())
172    }
173}
174
175#[cfg(test)]
176#[allow(clippy::unwrap_used)]
177mod test {
178    use super::*;
179    use crate::git::qualified;
180    use crate::test::arbitrary;
181    use localtime::{LocalDuration, LocalTime};
182
183    #[test]
184    fn test_count() {
185        let mut db = Database::memory().unwrap();
186        let oid = arbitrary::oid();
187
188        let repo = arbitrary::gen::<RepoId>(1);
189        let namespace = arbitrary::gen::<NodeId>(1);
190        let refname1 = qualified!("refs/heads/master");
191        let refname2 = qualified!("refs/heads/main");
192        let timestamp = LocalTime::now();
193
194        assert!(db.is_empty().unwrap());
195        assert_eq!(db.count().unwrap(), 0);
196
197        assert!(db
198            .set(&repo, &namespace, &refname1, oid, timestamp)
199            .unwrap());
200        assert!(!db.is_empty().unwrap());
201        assert_eq!(db.count().unwrap(), 1);
202
203        assert!(db
204            .set(&repo, &namespace, &refname2, oid, timestamp)
205            .unwrap());
206        assert_eq!(db.count().unwrap(), 2);
207    }
208
209    #[test]
210    fn test_set_and_delete() {
211        let mut db = Database::memory().unwrap();
212        let oid = arbitrary::oid();
213
214        let repo = arbitrary::gen::<RepoId>(1);
215        let namespace = arbitrary::gen::<NodeId>(1);
216        let refname = qualified!("refs/heads/master");
217        let timestamp = LocalTime::now();
218
219        assert!(db.set(&repo, &namespace, &refname, oid, timestamp).unwrap());
220        assert!(db.get(&repo, &namespace, &refname).unwrap().is_some());
221        assert!(db.delete(&repo, &namespace, &refname).unwrap());
222        assert!(db.get(&repo, &namespace, &refname).unwrap().is_none());
223        assert!(!db.delete(&repo, &namespace, &refname).unwrap());
224    }
225
226    #[test]
227    fn test_set_and_get() {
228        let mut db = Database::memory().unwrap();
229        let oid1 = arbitrary::oid();
230        let oid2 = arbitrary::oid();
231
232        assert_ne!(oid1, oid2);
233
234        let repo = arbitrary::gen::<RepoId>(1);
235        let namespace = arbitrary::gen::<NodeId>(1);
236        let refname = qualified!("refs/heads/master");
237        let mut timestamp = LocalTime::now();
238
239        assert_eq!(db.get(&repo, &namespace, &refname).unwrap(), None);
240        assert!(db
241            .set(&repo, &namespace, &refname, oid1, timestamp)
242            .unwrap());
243        assert_eq!(
244            db.get(&repo, &namespace, &refname).unwrap(),
245            Some((oid1, timestamp))
246        );
247        assert!(!db
248            .set(&repo, &namespace, &refname, oid1, timestamp)
249            .unwrap());
250        timestamp.elapse(LocalDuration::from_millis(1));
251
252        assert!(db
253            .set(&repo, &namespace, &refname, oid2, timestamp)
254            .unwrap());
255        assert_eq!(
256            db.get(&repo, &namespace, &refname).unwrap(),
257            Some((oid2, timestamp))
258        );
259    }
260}