upstream_rs/storage/database/
api.rs1use std::path::{Path, PathBuf};
2
3use anyhow::{Result, anyhow};
4
5use crate::models::upstream::Package;
6
7use super::packages::PackageConnection;
8
9#[derive(Debug, Clone)]
10pub struct PackageDatabase {
11 database_file: PathBuf,
12}
13
14impl PackageDatabase {
15 pub fn open(package_database_path: &Path) -> Result<Self> {
16 let database_file = Self::database_path_for(package_database_path);
17 PackageConnection::open(&database_file)?;
18 Ok(Self { database_file })
19 }
20
21 pub fn database_path_for(package_database_path: &Path) -> PathBuf {
22 match package_database_path
23 .extension()
24 .and_then(|extension| extension.to_str())
25 {
26 Some("db") => package_database_path.to_path_buf(),
27 _ => package_database_path.with_extension("db"),
28 }
29 }
30
31 pub fn schema_version(&self) -> Result<u32> {
32 self.connection()?.schema_version()
33 }
34
35 pub fn package_exists(&self, name: &str) -> Result<bool> {
36 self.connection()?.package_exists(name)
37 }
38
39 pub fn get_package(&self, name: &str) -> Result<Option<Package>> {
40 self.connection()?.get_package(name)
41 }
42
43 pub fn list_packages(&self) -> Result<Vec<Package>> {
44 self.connection()?.list_packages()
45 }
46
47 pub fn upsert_package(&mut self, package: &Package) -> Result<()> {
48 self.connection()?.upsert_package(package)
49 }
50
51 pub fn replace_all_packages(&mut self, packages: &[Package]) -> Result<()> {
52 self.connection()?.replace_all_packages(packages)
53 }
54
55 pub fn remove_package(&mut self, name: &str) -> Result<bool> {
56 self.connection()?.remove_package(name)
57 }
58
59 pub fn update_package<F>(&mut self, name: &str, update: F) -> Result<bool>
60 where
61 F: FnOnce(&mut Package) -> Result<bool>,
62 {
63 let mut package = self
64 .get_package(name)?
65 .ok_or_else(|| anyhow!("Package '{}' not found", name))?;
66 let changed = update(&mut package)?;
67 if !changed {
68 return Ok(false);
69 }
70 if package.name != name {
71 return Err(anyhow!(
72 "Package update changed '{}' to '{}'; use rename_package for package renames",
73 name,
74 package.name
75 ));
76 }
77
78 self.upsert_package(&package)?;
79 Ok(true)
80 }
81
82 pub fn rename_package(&mut self, old_name: &str, new_name: &str) -> Result<()> {
83 if self.package_exists(new_name)? {
84 return Err(anyhow!("Package '{}' already exists", new_name));
85 }
86
87 self.connection()?.update_package(old_name, |package| {
88 package.name = new_name.to_string();
89 Ok(())
90 })
91 }
92
93 fn connection(&self) -> Result<PackageConnection> {
94 PackageConnection::open(&self.database_file)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::PackageDatabase;
101 use crate::models::common::enums::{Channel, Filetype, Provider};
102 use crate::models::upstream::Package;
103 use std::path::{Path, PathBuf};
104 use std::time::{SystemTime, UNIX_EPOCH};
105 use std::{fs, io};
106
107 fn temp_database_path(name: &str) -> PathBuf {
108 let nanos = SystemTime::now()
109 .duration_since(UNIX_EPOCH)
110 .map(|d| d.as_nanos())
111 .unwrap_or(0);
112 std::env::temp_dir()
113 .join(format!("upstream-packages-test-{name}-{nanos}"))
114 .join("packages.db")
115 }
116
117 fn test_package(name: &str) -> Package {
118 Package::with_defaults(
119 name.to_string(),
120 format!("owner/{name}"),
121 Filetype::Binary,
122 None,
123 None,
124 Channel::Stable,
125 Provider::Github,
126 None,
127 )
128 }
129
130 fn legacy_packages_file(database_path: &Path) -> PathBuf {
131 database_path.with_extension("json")
132 }
133
134 fn cleanup(path: &Path) -> io::Result<()> {
135 if let Some(parent) = path.parent() {
136 fs::remove_dir_all(parent)?;
137 }
138 Ok(())
139 }
140
141 #[test]
142 fn open_starts_empty_when_file_missing() {
143 let path = temp_database_path("missing");
144 let db = PackageDatabase::open(&path).expect("open database");
145 assert!(db.list_packages().expect("list packages").is_empty());
146 assert!(path.exists());
147 cleanup(&path).expect("cleanup");
148 }
149
150 #[test]
151 fn open_ignores_adjacent_legacy_json() {
152 let path = temp_database_path("legacy-json-ignored");
153 let legacy_path = legacy_packages_file(&path);
154 if let Some(parent) = legacy_path.parent() {
155 fs::create_dir_all(parent).expect("create parent");
156 }
157 fs::write(&legacy_path, "{not-json").expect("write invalid legacy json");
158
159 let db = PackageDatabase::open(&path).expect("open database");
160
161 assert!(path.exists());
162 assert!(db.list_packages().expect("list packages").is_empty());
163
164 cleanup(&path).expect("cleanup");
165 }
166
167 #[test]
168 fn upsert_replaces_existing_package_name() {
169 let path = temp_database_path("update");
170 if let Some(parent) = path.parent() {
171 fs::create_dir_all(parent).expect("create parent");
172 }
173 let mut db = PackageDatabase::open(&path).expect("open database");
174
175 let mut first = test_package("tool");
176 first.version.major = 1;
177 db.upsert_package(&first).expect("store first");
178
179 let mut second = first.clone();
180 second.version.major = 2;
181 second.repo_slug = "owner/renamed-repo".to_string();
182 db.upsert_package(&second).expect("store update");
183
184 let package = db
185 .get_package("tool")
186 .expect("load package")
187 .expect("stored package");
188 assert_eq!(package.version.major, 2);
189 assert_eq!(package.repo_slug, "owner/renamed-repo");
190
191 cleanup(&path).expect("cleanup");
192 }
193
194 #[test]
195 fn update_package_upserts_changed_package() {
196 let path = temp_database_path("update-package");
197 if let Some(parent) = path.parent() {
198 fs::create_dir_all(parent).expect("create parent");
199 }
200 let mut db = PackageDatabase::open(&path).expect("open database");
201 db.upsert_package(&test_package("tool"))
202 .expect("store package");
203
204 let changed = db
205 .update_package("tool", |package| {
206 package.version.major = 3;
207 Ok(true)
208 })
209 .expect("update package");
210
211 assert!(changed);
212 let reloaded = PackageDatabase::open(&path).expect("reload database");
213 assert_eq!(
214 reloaded
215 .get_package("tool")
216 .expect("load package")
217 .expect("updated package")
218 .version
219 .major,
220 3
221 );
222
223 cleanup(&path).expect("cleanup");
224 }
225
226 #[test]
227 fn update_package_skips_unchanged_package() {
228 let path = temp_database_path("unchanged-package");
229 if let Some(parent) = path.parent() {
230 fs::create_dir_all(parent).expect("create parent");
231 }
232 let mut db = PackageDatabase::open(&path).expect("open database");
233 db.upsert_package(&test_package("tool"))
234 .expect("store package");
235
236 let changed = db
237 .update_package("tool", |_package| Ok(false))
238 .expect("update package");
239
240 assert!(!changed);
241 cleanup(&path).expect("cleanup");
242 }
243
244 #[test]
245 fn rename_package_updates_database_primary_key() {
246 let path = temp_database_path("rename-package");
247 if let Some(parent) = path.parent() {
248 fs::create_dir_all(parent).expect("create parent");
249 }
250 let mut db = PackageDatabase::open(&path).expect("open database");
251 db.upsert_package(&test_package("old"))
252 .expect("store package");
253
254 db.rename_package("old", "new").expect("rename package");
255
256 let reloaded = PackageDatabase::open(&path).expect("reload database");
257 assert!(reloaded.get_package("old").expect("load old").is_none());
258 assert!(reloaded.get_package("new").expect("load new").is_some());
259
260 cleanup(&path).expect("cleanup");
261 }
262
263 #[test]
264 fn remove_package_returns_expected_status() {
265 let path = temp_database_path("remove");
266 if let Some(parent) = path.parent() {
267 fs::create_dir_all(parent).expect("create parent");
268 }
269 let mut db = PackageDatabase::open(&path).expect("open database");
270 db.upsert_package(&test_package("one"))
271 .expect("store package");
272
273 assert!(db.remove_package("one").expect("remove"));
274 assert!(!db.remove_package("one").expect("second remove"));
275
276 cleanup(&path).expect("cleanup");
277 }
278
279 #[test]
280 fn upsert_creates_missing_parent_dirs() {
281 let path = temp_database_path("missing-parent");
282 let mut db = PackageDatabase::open(&path).expect("open database");
283 db.upsert_package(&test_package("tool"))
284 .expect("save package");
285
286 assert!(path.exists());
287 cleanup(&path).expect("cleanup");
288 }
289
290 #[test]
291 fn upsert_writes_database_and_can_reload() {
292 let path = temp_database_path("reload");
293 let mut db = PackageDatabase::open(&path).expect("open database");
294 db.upsert_package(&test_package("tool"))
295 .expect("save package");
296
297 let reloaded = PackageDatabase::open(&path).expect("reload database");
298 assert_eq!(reloaded.list_packages().expect("list packages").len(), 1);
299 assert!(
300 reloaded
301 .get_package("tool")
302 .expect("load package")
303 .is_some()
304 );
305
306 cleanup(&path).expect("cleanup");
307 }
308
309 #[test]
310 fn upsert_overwrites_visible_result() {
311 let path = temp_database_path("overwrite");
312 let mut db = PackageDatabase::open(&path).expect("open database");
313
314 let mut first = test_package("tool");
315 first.version.major = 1;
316 db.upsert_package(&first).expect("save first");
317
318 let mut second = test_package("tool");
319 second.version.major = 2;
320 db.upsert_package(&second).expect("save second");
321
322 let reloaded = PackageDatabase::open(&path).expect("reload database");
323 let packages = reloaded.list_packages().expect("list packages");
324 assert_eq!(packages.len(), 1);
325 assert_eq!(packages[0].version.major, 2);
326
327 cleanup(&path).expect("cleanup");
328 }
329}