chug_cli/db/
models.rs

1use std::path::Path;
2
3use anyhow::Context;
4use diesel::{prelude::*, sqlite::Sqlite};
5
6use crate::db::{
7    connection,
8    schema::{dependencies, downloaded_bottles, linked_files},
9};
10
11#[derive(Clone, Debug, Queryable, Selectable)]
12#[diesel(table_name = downloaded_bottles)]
13#[diesel(check_for_backend(Sqlite))]
14pub struct DownloadedBottle {
15    id: i32,
16    name: String,
17    version: String,
18    path: String,
19}
20
21#[derive(Debug, Insertable)]
22#[diesel(table_name = downloaded_bottles)]
23#[diesel(check_for_backend(Sqlite))]
24struct NewDownloadedBottle<'a> {
25    name: &'a str,
26    version: &'a str,
27    path: &'a str,
28}
29
30#[derive(Debug, Queryable, Selectable)]
31#[diesel(table_name = linked_files)]
32#[diesel(check_for_backend(Sqlite))]
33pub struct LinkedFile {
34    id: i32,
35    path: String,
36}
37
38#[derive(Debug, Insertable)]
39#[diesel(table_name = linked_files)]
40#[diesel(check_for_backend(Sqlite))]
41struct NewLinkedFile<'a> {
42    path: &'a str,
43    bottle_id: i32,
44}
45
46#[derive(Debug, Queryable, Selectable, Insertable)]
47#[diesel(table_name = dependencies)]
48#[diesel(check_for_backend(Sqlite))]
49pub struct Dependency {
50    dependent_id: Option<i32>,
51    dependency_id: i32,
52}
53
54impl DownloadedBottle {
55    pub fn create(name: &str, version: &str, path: &Path) -> anyhow::Result<DownloadedBottle> {
56        let mut db = connection()?;
57
58        let result = diesel::insert_into(downloaded_bottles::table)
59            .values(NewDownloadedBottle {
60                name,
61                version,
62                path: path.to_str().context("Installed bottle path is non-utf8")?,
63            })
64            .returning(DownloadedBottle::as_returning())
65            .get_result(&mut *db)?;
66
67        Ok(result)
68    }
69
70    pub fn get(name: &str, version: &str) -> anyhow::Result<Option<DownloadedBottle>> {
71        use downloaded_bottles::dsl;
72
73        let mut db = connection()?;
74        let name_value = name;
75        let version_value = version;
76
77        let mut results = dsl::downloaded_bottles
78            .filter(dsl::name.eq(name_value))
79            .filter(dsl::version.eq(version_value))
80            .select(DownloadedBottle::as_select())
81            .load_iter(&mut *db)?;
82
83        if let Some(bottle) = results.next() {
84            Ok(Some(bottle?))
85        } else {
86            Ok(None)
87        }
88    }
89
90    fn get_by_id(id: i32) -> anyhow::Result<DownloadedBottle> {
91        use downloaded_bottles::dsl;
92
93        let mut db = connection()?;
94
95        let result = dsl::downloaded_bottles
96            .filter(dsl::id.eq(id))
97            .select(Self::as_select())
98            .first(&mut *db)?;
99
100        Ok(result)
101    }
102
103    pub fn get_all() -> anyhow::Result<Vec<DownloadedBottle>> {
104        use downloaded_bottles::dsl;
105
106        let mut db = connection()?;
107
108        let results = dsl::downloaded_bottles
109            .order((dsl::name, dsl::version))
110            .select(DownloadedBottle::as_select())
111            .load(&mut *db)?;
112
113        Ok(results)
114    }
115
116    pub fn delete(&self) -> anyhow::Result<()> {
117        use downloaded_bottles::dsl;
118
119        let mut db = connection()?;
120
121        diesel::delete(downloaded_bottles::table)
122            .filter(dsl::id.eq(self.id))
123            .execute(&mut *db)?;
124
125        Ok(())
126    }
127
128    pub fn id(&self) -> i32 {
129        self.id
130    }
131
132    pub fn name(&self) -> &str {
133        &self.name
134    }
135
136    pub fn version(&self) -> &str {
137        &self.version
138    }
139
140    pub fn path(&self) -> &Path {
141        self.path.as_ref()
142    }
143
144    pub fn linked_files(&self) -> anyhow::Result<Vec<LinkedFile>> {
145        use linked_files::dsl;
146
147        let mut db = connection()?;
148
149        let results = dsl::linked_files
150            .filter(dsl::bottle_id.eq(self.id))
151            .select(LinkedFile::as_select())
152            .load(&mut *db)?;
153
154        Ok(results)
155    }
156}
157
158impl LinkedFile {
159    pub fn create(path: &Path, bottle: &DownloadedBottle) -> anyhow::Result<()> {
160        let mut db = connection()?;
161
162        diesel::insert_into(linked_files::table)
163            .values(NewLinkedFile {
164                path: path.to_str().context("Linked file path is non-utf8")?,
165                bottle_id: bottle.id,
166            })
167            .on_conflict(linked_files::dsl::path)
168            .do_nothing()
169            .execute(&mut *db)?;
170
171        Ok(())
172    }
173
174    pub fn delete(&self) -> anyhow::Result<()> {
175        use linked_files::dsl;
176
177        let mut db = connection()?;
178
179        diesel::delete(linked_files::table)
180            .filter(dsl::id.eq(self.id))
181            .execute(&mut *db)?;
182
183        Ok(())
184    }
185
186    pub fn path(&self) -> &Path {
187        self.path.as_ref()
188    }
189}
190
191impl Dependency {
192    pub fn get_all() -> anyhow::Result<Vec<Dependency>> {
193        use dependencies::dsl;
194
195        let mut db = connection()?;
196
197        let result = dsl::dependencies
198            .select(Dependency::as_select())
199            .load(&mut *db)?;
200
201        Ok(result)
202    }
203
204    pub fn replace_all<'a>(
205        dependencies: impl Iterator<Item = (Option<&'a DownloadedBottle>, &'a DownloadedBottle)>,
206    ) -> anyhow::Result<()> {
207        use dependencies::dsl;
208
209        let mut db = connection()?;
210
211        db.transaction::<(), anyhow::Error, _>(|db| {
212            diesel::delete(dsl::dependencies).execute(db)?;
213
214            let new_dependencies = dependencies
215                .map(|(dependent, dependency)| Dependency {
216                    dependent_id: dependent.map(|b| b.id),
217                    dependency_id: dependency.id,
218                })
219                .collect::<Vec<_>>();
220            diesel::insert_into(dsl::dependencies)
221                .values(&new_dependencies)
222                .execute(db)?;
223
224            Ok(())
225        })?;
226
227        Ok(())
228    }
229
230    /// Returns `None` if the bottle was manually added, i.e. if it is a root.
231    pub fn dependent(&self) -> anyhow::Result<Option<DownloadedBottle>> {
232        if let Some(id) = self.dependent_id {
233            Ok(Some(DownloadedBottle::get_by_id(id)?))
234        } else {
235            Ok(None)
236        }
237    }
238
239    pub fn dependent_id(&self) -> Option<i32> {
240        self.dependent_id
241    }
242
243    pub fn dependency(&self) -> anyhow::Result<DownloadedBottle> {
244        DownloadedBottle::get_by_id(self.dependency_id)
245    }
246
247    pub fn dependency_id(&self) -> i32 {
248        self.dependency_id
249    }
250}