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 #[error("internal error: {0}")]
20 Internal(#[from] sql::Error),
21 #[error("invalid timestamp: {0}")]
23 Timestamp(#[from] TryFromIntError),
24 #[error("repository error: {0}")]
26 Repository(#[from] RepositoryError),
27 #[error("storage error: {0}")]
29 Storage(#[from] storage::Error),
30 #[error("storage refs error: {0}")]
32 Refs(#[from] storage::refs::Error),
33 #[error("no rows returned")]
35 NoRows,
36}
37
38pub trait Store {
42 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 fn get(
53 &self,
54 repo: &RepoId,
55 namespace: &NodeId,
56 refname: &Qualified,
57 ) -> Result<Option<(Oid, LocalTime)>, Error>;
58 fn delete(
60 &mut self,
61 repo: &RepoId,
62 namespace: &NodeId,
63 refname: &Qualified,
64 ) -> Result<bool, Error>;
65 fn populate<S: ReadStorage>(&mut self, storage: &S) -> Result<(), Error>;
67 fn count(&self) -> Result<usize, Error>;
69 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}