Skip to main content

soar_db/repository/
core.rs

1//! Core database repository for installed packages.
2
3use diesel::{prelude::*, sql_types::Bool, sqlite::Sqlite};
4
5use crate::{
6    models::{
7        core::{NewPackage, NewPortablePackage, Package, PortablePackage},
8        types::PackageProvide,
9    },
10    schema::core::{packages, portable_package},
11};
12
13/// Sort direction for queries.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum SortDirection {
16    Asc,
17    Desc,
18}
19
20/// Type alias for installed package (for clarity).
21pub type InstalledPackage = Package;
22/// Type alias for new installed package (for clarity).
23pub type NewInstalledPackage<'a> = NewPackage<'a>;
24
25/// Installed package with portable configuration joined.
26#[derive(Debug, Clone)]
27pub struct InstalledPackageWithPortable {
28    pub id: i32,
29    pub repo_name: String,
30    pub pkg_id: String,
31    pub pkg_name: String,
32    pub pkg_type: Option<String>,
33    pub version: String,
34    pub size: i64,
35    pub checksum: Option<String>,
36    pub installed_path: String,
37    pub installed_date: String,
38    pub profile: String,
39    pub pinned: bool,
40    pub is_installed: bool,
41    pub detached: bool,
42    pub unlinked: bool,
43    pub provides: Option<Vec<PackageProvide>>,
44    pub install_patterns: Option<Vec<String>>,
45    pub portable_path: Option<String>,
46    pub portable_home: Option<String>,
47    pub portable_config: Option<String>,
48    pub portable_share: Option<String>,
49    pub portable_cache: Option<String>,
50}
51
52impl From<(Package, Option<PortablePackage>)> for InstalledPackageWithPortable {
53    fn from((pkg, portable): (Package, Option<PortablePackage>)) -> Self {
54        Self {
55            id: pkg.id,
56            repo_name: pkg.repo_name,
57            pkg_id: pkg.pkg_id,
58            pkg_name: pkg.pkg_name,
59            pkg_type: pkg.pkg_type,
60            version: pkg.version,
61            size: pkg.size,
62            checksum: pkg.checksum,
63            installed_path: pkg.installed_path,
64            installed_date: pkg.installed_date,
65            profile: pkg.profile,
66            pinned: pkg.pinned,
67            is_installed: pkg.is_installed,
68            detached: pkg.detached,
69            unlinked: pkg.unlinked,
70            provides: pkg.provides,
71            install_patterns: pkg.install_patterns,
72            portable_path: portable.as_ref().and_then(|p| p.portable_path.clone()),
73            portable_home: portable.as_ref().and_then(|p| p.portable_home.clone()),
74            portable_config: portable.as_ref().and_then(|p| p.portable_config.clone()),
75            portable_share: portable.as_ref().and_then(|p| p.portable_share.clone()),
76            portable_cache: portable.as_ref().and_then(|p| p.portable_cache.clone()),
77        }
78    }
79}
80
81/// Repository for installed package operations.
82pub struct CoreRepository;
83
84impl CoreRepository {
85    /// Lists all installed packages.
86    pub fn list_all(conn: &mut SqliteConnection) -> QueryResult<Vec<Package>> {
87        packages::table.select(Package::as_select()).load(conn)
88    }
89
90    /// Lists installed packages with flexible filtering.
91    #[allow(clippy::too_many_arguments)]
92    pub fn list_filtered(
93        conn: &mut SqliteConnection,
94        repo_name: Option<&str>,
95        pkg_name: Option<&str>,
96        pkg_id: Option<&str>,
97        version: Option<&str>,
98        is_installed: Option<bool>,
99        pinned: Option<bool>,
100        limit: Option<i64>,
101        sort_by_id: Option<SortDirection>,
102    ) -> QueryResult<Vec<InstalledPackageWithPortable>> {
103        let mut query = packages::table
104            .left_join(portable_package::table)
105            .into_boxed();
106
107        if let Some(repo) = repo_name {
108            query = query.filter(packages::repo_name.eq(repo));
109        }
110        if let Some(name) = pkg_name {
111            query = query.filter(packages::pkg_name.eq(name));
112        }
113        if let Some(id) = pkg_id {
114            query = query.filter(packages::pkg_id.eq(id));
115        }
116        if let Some(ver) = version {
117            query = query.filter(packages::version.eq(ver));
118        }
119        if let Some(installed) = is_installed {
120            query = query.filter(packages::is_installed.eq(installed));
121        }
122        if let Some(pin) = pinned {
123            query = query.filter(packages::pinned.eq(pin));
124        }
125
126        if let Some(direction) = sort_by_id {
127            query = match direction {
128                SortDirection::Asc => query.order(packages::id.asc()),
129                SortDirection::Desc => query.order(packages::id.desc()),
130            };
131        }
132
133        if let Some(lim) = limit {
134            query = query.limit(lim);
135        }
136
137        let results: Vec<(Package, Option<PortablePackage>)> = query
138            .select((Package::as_select(), Option::<PortablePackage>::as_select()))
139            .load(conn)?;
140
141        Ok(results.into_iter().map(Into::into).collect())
142    }
143
144    /// Lists broken packages (is_installed = false).
145    pub fn list_broken(
146        conn: &mut SqliteConnection,
147    ) -> QueryResult<Vec<InstalledPackageWithPortable>> {
148        let results: Vec<(Package, Option<PortablePackage>)> = packages::table
149            .left_join(portable_package::table)
150            .filter(packages::is_installed.eq(false))
151            .select((Package::as_select(), Option::<PortablePackage>::as_select()))
152            .load(conn)?;
153
154        Ok(results.into_iter().map(Into::into).collect())
155    }
156
157    /// Lists installed packages that are not pinned (for updates).
158    pub fn list_updatable(
159        conn: &mut SqliteConnection,
160    ) -> QueryResult<Vec<InstalledPackageWithPortable>> {
161        let results: Vec<(Package, Option<PortablePackage>)> = packages::table
162            .left_join(portable_package::table)
163            .filter(packages::is_installed.eq(true))
164            .filter(packages::pinned.eq(false))
165            .select((Package::as_select(), Option::<PortablePackage>::as_select()))
166            .load(conn)?;
167
168        Ok(results.into_iter().map(Into::into).collect())
169    }
170
171    /// Finds an installed package by exact match on repo_name, pkg_name, pkg_id, and version.
172    pub fn find_exact(
173        conn: &mut SqliteConnection,
174        repo_name: &str,
175        pkg_name: &str,
176        pkg_id: &str,
177        version: &str,
178    ) -> QueryResult<Option<InstalledPackageWithPortable>> {
179        let result: Option<(Package, Option<PortablePackage>)> = packages::table
180            .left_join(portable_package::table)
181            .filter(packages::repo_name.eq(repo_name))
182            .filter(packages::pkg_name.eq(pkg_name))
183            .filter(packages::pkg_id.eq(pkg_id))
184            .filter(packages::version.eq(version))
185            .select((Package::as_select(), Option::<PortablePackage>::as_select()))
186            .first(conn)
187            .optional()?;
188
189        Ok(result.map(Into::into))
190    }
191
192    /// Lists all installed packages with portable configuration.
193    pub fn list_all_with_portable(
194        conn: &mut SqliteConnection,
195    ) -> QueryResult<Vec<InstalledPackageWithPortable>> {
196        let results: Vec<(Package, Option<PortablePackage>)> = packages::table
197            .left_join(portable_package::table)
198            .select((Package::as_select(), Option::<PortablePackage>::as_select()))
199            .load(conn)?;
200
201        Ok(results.into_iter().map(Into::into).collect())
202    }
203
204    /// Lists installed packages filtered by repo_name.
205    pub fn list_by_repo(conn: &mut SqliteConnection, repo_name: &str) -> QueryResult<Vec<Package>> {
206        packages::table
207            .filter(packages::repo_name.eq(repo_name))
208            .select(Package::as_select())
209            .load(conn)
210    }
211
212    /// Lists installed packages filtered by repo_name with portable configuration.
213    pub fn list_by_repo_with_portable(
214        conn: &mut SqliteConnection,
215        repo_name: &str,
216    ) -> QueryResult<Vec<InstalledPackageWithPortable>> {
217        let results: Vec<(Package, Option<PortablePackage>)> = packages::table
218            .left_join(portable_package::table)
219            .filter(packages::repo_name.eq(repo_name))
220            .select((Package::as_select(), Option::<PortablePackage>::as_select()))
221            .load(conn)?;
222
223        Ok(results.into_iter().map(Into::into).collect())
224    }
225
226    /// Counts installed packages.
227    pub fn count(conn: &mut SqliteConnection) -> QueryResult<i64> {
228        packages::table.count().get_result(conn)
229    }
230
231    /// Counts distinct installed packages.
232    pub fn count_distinct_installed(
233        conn: &mut SqliteConnection,
234        repo_name: Option<&str>,
235    ) -> QueryResult<i64> {
236        use diesel::dsl::sql;
237
238        let mut query = packages::table
239            .filter(packages::is_installed.eq(true))
240            .into_boxed();
241
242        if let Some(repo) = repo_name {
243            query = query.filter(packages::repo_name.eq(repo));
244        }
245
246        query
247            .select(sql::<diesel::sql_types::BigInt>(
248                "COUNT(DISTINCT pkg_id || '\x00' || pkg_name)",
249            ))
250            .first(conn)
251    }
252
253    /// Finds an installed package by ID.
254    pub fn find_by_id(conn: &mut SqliteConnection, id: i32) -> QueryResult<Option<Package>> {
255        packages::table
256            .filter(packages::id.eq(id))
257            .select(Package::as_select())
258            .first(conn)
259            .optional()
260    }
261
262    /// Finds an installed package by ID with portable configuration.
263    pub fn find_by_id_with_portable(
264        conn: &mut SqliteConnection,
265        id: i32,
266    ) -> QueryResult<Option<InstalledPackageWithPortable>> {
267        let result: Option<(Package, Option<PortablePackage>)> = packages::table
268            .left_join(portable_package::table)
269            .filter(packages::id.eq(id))
270            .select((Package::as_select(), Option::<PortablePackage>::as_select()))
271            .first(conn)
272            .optional()?;
273
274        Ok(result.map(Into::into))
275    }
276
277    /// Finds installed packages by name.
278    pub fn find_by_name(conn: &mut SqliteConnection, name: &str) -> QueryResult<Vec<Package>> {
279        packages::table
280            .filter(packages::pkg_name.eq(name))
281            .select(Package::as_select())
282            .load(conn)
283    }
284
285    /// Finds installed packages by name with portable configuration.
286    pub fn find_by_name_with_portable(
287        conn: &mut SqliteConnection,
288        name: &str,
289    ) -> QueryResult<Vec<InstalledPackageWithPortable>> {
290        let results: Vec<(Package, Option<PortablePackage>)> = packages::table
291            .left_join(portable_package::table)
292            .filter(packages::pkg_name.eq(name))
293            .select((Package::as_select(), Option::<PortablePackage>::as_select()))
294            .load(conn)?;
295
296        Ok(results.into_iter().map(Into::into).collect())
297    }
298
299    /// Finds installed packages by name, excluding specific pkg_id and version.
300    pub fn find_alternates(
301        conn: &mut SqliteConnection,
302        pkg_name: &str,
303        exclude_pkg_id: &str,
304        exclude_version: &str,
305    ) -> QueryResult<Vec<InstalledPackageWithPortable>> {
306        let results: Vec<(Package, Option<PortablePackage>)> = packages::table
307            .left_join(portable_package::table)
308            .filter(packages::pkg_name.eq(pkg_name))
309            .filter(packages::pkg_id.ne(exclude_pkg_id))
310            .filter(packages::version.ne(exclude_version))
311            .select((Package::as_select(), Option::<PortablePackage>::as_select()))
312            .load(conn)?;
313
314        Ok(results.into_iter().map(Into::into).collect())
315    }
316
317    /// Finds an installed package by pkg_id and repo_name.
318    pub fn find_by_pkg_id_and_repo(
319        conn: &mut SqliteConnection,
320        pkg_id: &str,
321        repo_name: &str,
322    ) -> QueryResult<Option<Package>> {
323        packages::table
324            .filter(packages::pkg_id.eq(pkg_id))
325            .filter(packages::repo_name.eq(repo_name))
326            .select(Package::as_select())
327            .first(conn)
328            .optional()
329    }
330
331    /// Finds an installed package by pkg_id, pkg_name, and repo_name.
332    pub fn find_by_pkg_id_name_and_repo(
333        conn: &mut SqliteConnection,
334        pkg_id: &str,
335        pkg_name: &str,
336        repo_name: &str,
337    ) -> QueryResult<Option<Package>> {
338        packages::table
339            .filter(packages::pkg_id.eq(pkg_id))
340            .filter(packages::pkg_name.eq(pkg_name))
341            .filter(packages::repo_name.eq(repo_name))
342            .select(Package::as_select())
343            .first(conn)
344            .optional()
345    }
346
347    /// Inserts a new installed package and returns the inserted ID.
348    pub fn insert(conn: &mut SqliteConnection, package: &NewPackage) -> QueryResult<i32> {
349        diesel::insert_into(packages::table)
350            .values(package)
351            .returning(packages::id)
352            .get_result(conn)
353    }
354
355    /// Updates an installed package's version.
356    pub fn update_version(
357        conn: &mut SqliteConnection,
358        id: i32,
359        new_version: &str,
360    ) -> QueryResult<usize> {
361        diesel::update(packages::table.filter(packages::id.eq(id)))
362            .set(packages::version.eq(new_version))
363            .execute(conn)
364    }
365
366    /// Updates an installed package after successful installation.
367    /// Only updates the record with is_installed=false (the newly created one).
368    #[allow(clippy::too_many_arguments)]
369    pub fn record_installation(
370        conn: &mut SqliteConnection,
371        repo_name: &str,
372        pkg_name: &str,
373        pkg_id: &str,
374        version: &str,
375        size: i64,
376        provides: Option<Vec<PackageProvide>>,
377        checksum: Option<&str>,
378        installed_date: &str,
379        installed_path: &str,
380    ) -> QueryResult<Option<i32>> {
381        let provides = provides.map(|v| serde_json::to_value(v).unwrap_or_default());
382        diesel::update(
383            packages::table
384                .filter(packages::repo_name.eq(repo_name))
385                .filter(packages::pkg_name.eq(pkg_name))
386                .filter(packages::pkg_id.eq(pkg_id))
387                .filter(packages::version.eq(version))
388                .filter(packages::is_installed.eq(false)),
389        )
390        .set((
391            packages::size.eq(size),
392            packages::installed_date.eq(installed_date),
393            packages::is_installed.eq(true),
394            packages::provides.eq(provides),
395            packages::checksum.eq(checksum),
396            packages::installed_path.eq(installed_path),
397        ))
398        .returning(packages::id)
399        .get_result(conn)
400        .optional()
401    }
402
403    /// Sets the pinned status of a package.
404    pub fn set_pinned(conn: &mut SqliteConnection, id: i32, pinned: bool) -> QueryResult<usize> {
405        diesel::update(packages::table.filter(packages::id.eq(id)))
406            .set(packages::pinned.eq(pinned))
407            .execute(conn)
408    }
409
410    /// Sets the unlinked status of a package.
411    pub fn set_unlinked(
412        conn: &mut SqliteConnection,
413        id: i32,
414        unlinked: bool,
415    ) -> QueryResult<usize> {
416        diesel::update(packages::table.filter(packages::id.eq(id)))
417            .set(packages::unlinked.eq(unlinked))
418            .execute(conn)
419    }
420
421    /// Unlinks all packages with a given name except those matching pkg_id and version.
422    pub fn unlink_others(
423        conn: &mut SqliteConnection,
424        pkg_name: &str,
425        keep_pkg_id: &str,
426        keep_version: &str,
427    ) -> QueryResult<usize> {
428        diesel::update(
429            packages::table
430                .filter(packages::pkg_name.eq(pkg_name))
431                .filter(
432                    packages::pkg_id
433                        .ne(keep_pkg_id)
434                        .or(packages::version.ne(keep_version)),
435                ),
436        )
437        .set(packages::unlinked.eq(true))
438        .execute(conn)
439    }
440
441    /// Updates the pkg_id for packages matching repo_name and old pkg_id.
442    pub fn update_pkg_id(
443        conn: &mut SqliteConnection,
444        repo_name: &str,
445        old_pkg_id: &str,
446        new_pkg_id: &str,
447    ) -> QueryResult<usize> {
448        diesel::update(
449            packages::table
450                .filter(packages::repo_name.eq(repo_name))
451                .filter(packages::pkg_id.eq(old_pkg_id)),
452        )
453        .set(packages::pkg_id.eq(new_pkg_id))
454        .execute(conn)
455    }
456
457    /// Deletes an installed package by ID.
458    pub fn delete(conn: &mut SqliteConnection, id: i32) -> QueryResult<usize> {
459        diesel::delete(packages::table.filter(packages::id.eq(id))).execute(conn)
460    }
461
462    /// Checks if a pending install (is_installed=false) exists for a specific package version.
463    /// Used to check if we can resume a partial install.
464    pub fn has_pending_install(
465        conn: &mut SqliteConnection,
466        pkg_id: &str,
467        pkg_name: &str,
468        repo_name: &str,
469        version: &str,
470    ) -> QueryResult<bool> {
471        let count: i64 = packages::table
472            .filter(packages::pkg_id.eq(pkg_id))
473            .filter(packages::pkg_name.eq(pkg_name))
474            .filter(packages::repo_name.eq(repo_name))
475            .filter(packages::version.eq(version))
476            .filter(packages::is_installed.eq(false))
477            .count()
478            .get_result(conn)?;
479        Ok(count > 0)
480    }
481
482    /// Deletes pending (is_installed=false) records for a package and returns their paths.
483    /// Used to clean up orphaned partial installs before starting a new install.
484    pub fn delete_pending_installs(
485        conn: &mut SqliteConnection,
486        pkg_id: &str,
487        pkg_name: &str,
488        repo_name: &str,
489    ) -> QueryResult<Vec<String>> {
490        let paths: Vec<String> = packages::table
491            .filter(packages::pkg_id.eq(pkg_id))
492            .filter(packages::pkg_name.eq(pkg_name))
493            .filter(packages::repo_name.eq(repo_name))
494            .filter(packages::is_installed.eq(false))
495            .select(packages::installed_path)
496            .load(conn)?;
497
498        diesel::delete(
499            packages::table
500                .filter(packages::pkg_id.eq(pkg_id))
501                .filter(packages::pkg_name.eq(pkg_name))
502                .filter(packages::repo_name.eq(repo_name))
503                .filter(packages::is_installed.eq(false)),
504        )
505        .execute(conn)?;
506
507        Ok(paths)
508    }
509
510    /// Gets the portable package configuration for a package.
511    pub fn get_portable(
512        conn: &mut SqliteConnection,
513        package_id: i32,
514    ) -> QueryResult<Option<PortablePackage>> {
515        portable_package::table
516            .filter(portable_package::package_id.eq(package_id))
517            .select(PortablePackage::as_select())
518            .first(conn)
519            .optional()
520    }
521
522    /// Inserts portable package configuration.
523    pub fn insert_portable(
524        conn: &mut SqliteConnection,
525        portable: &NewPortablePackage,
526    ) -> QueryResult<usize> {
527        diesel::insert_into(portable_package::table)
528            .values(portable)
529            .execute(conn)
530    }
531
532    /// Updates or inserts portable package configuration.
533    pub fn upsert_portable(
534        conn: &mut SqliteConnection,
535        package_id: i32,
536        portable_path: Option<&str>,
537        portable_home: Option<&str>,
538        portable_config: Option<&str>,
539        portable_share: Option<&str>,
540        portable_cache: Option<&str>,
541    ) -> QueryResult<usize> {
542        let updated = diesel::update(
543            portable_package::table.filter(portable_package::package_id.eq(package_id)),
544        )
545        .set((
546            portable_package::portable_path.eq(portable_path),
547            portable_package::portable_home.eq(portable_home),
548            portable_package::portable_config.eq(portable_config),
549            portable_package::portable_share.eq(portable_share),
550            portable_package::portable_cache.eq(portable_cache),
551        ))
552        .execute(conn)?;
553
554        if updated == 0 {
555            diesel::insert_into(portable_package::table)
556                .values(&NewPortablePackage {
557                    package_id,
558                    portable_path,
559                    portable_home,
560                    portable_config,
561                    portable_share,
562                    portable_cache,
563                })
564                .execute(conn)
565        } else {
566            Ok(updated)
567        }
568    }
569
570    /// Deletes portable package configuration.
571    pub fn delete_portable(conn: &mut SqliteConnection, package_id: i32) -> QueryResult<usize> {
572        diesel::delete(portable_package::table.filter(portable_package::package_id.eq(package_id)))
573            .execute(conn)
574    }
575
576    /// Gets old package versions (all except the newest one) for cleanup.
577    /// Returns the installed paths of packages to remove.
578    /// If `force` is true, includes pinned packages. Otherwise only unpinned packages.
579    pub fn get_old_package_paths(
580        conn: &mut SqliteConnection,
581        pkg_id: &str,
582        pkg_name: &str,
583        repo_name: &str,
584        force: bool,
585    ) -> QueryResult<Vec<(i32, String)>> {
586        let latest: Option<(i32, String)> = packages::table
587            .filter(packages::pkg_id.eq(pkg_id))
588            .filter(packages::pkg_name.eq(pkg_name))
589            .filter(packages::repo_name.eq(repo_name))
590            .order(packages::id.desc())
591            .select((packages::id, packages::installed_path))
592            .first(conn)
593            .optional()?;
594
595        let Some((latest_id, latest_path)) = latest else {
596            return Ok(Vec::new());
597        };
598
599        let query = packages::table
600            .filter(packages::pkg_id.eq(pkg_id))
601            .filter(packages::pkg_name.eq(pkg_name))
602            .filter(packages::repo_name.eq(repo_name))
603            .filter(packages::id.ne(latest_id))
604            .filter(packages::installed_path.ne(&latest_path))
605            .into_boxed();
606
607        let query = if force {
608            query
609        } else {
610            query.filter(packages::pinned.eq(false))
611        };
612
613        query
614            .select((packages::id, packages::installed_path))
615            .load(conn)
616    }
617
618    /// Deletes old package versions (all except the newest one).
619    /// If `force` is true, deletes pinned packages too. Otherwise only unpinned packages.
620    pub fn delete_old_packages(
621        conn: &mut SqliteConnection,
622        pkg_id: &str,
623        pkg_name: &str,
624        repo_name: &str,
625        force: bool,
626    ) -> QueryResult<usize> {
627        let latest_id: Option<i32> = packages::table
628            .filter(packages::pkg_id.eq(pkg_id))
629            .filter(packages::pkg_name.eq(pkg_name))
630            .filter(packages::repo_name.eq(repo_name))
631            .order(packages::id.desc())
632            .select(packages::id)
633            .first(conn)
634            .optional()?;
635
636        let Some(latest_id) = latest_id else {
637            return Ok(0);
638        };
639
640        let pinned_filter: Box<dyn BoxableExpression<packages::table, Sqlite, SqlType = Bool>> =
641            if force {
642                Box::new(diesel::dsl::sql::<Bool>("TRUE"))
643            } else {
644                Box::new(packages::pinned.eq(false))
645            };
646
647        let query = packages::table
648            .filter(packages::pkg_id.eq(pkg_id))
649            .filter(packages::pkg_name.eq(pkg_name))
650            .filter(packages::repo_name.eq(repo_name))
651            .filter(packages::id.ne(latest_id))
652            .filter(pinned_filter);
653
654        diesel::delete(query).execute(conn)
655    }
656
657    /// Unlinks all packages with a given name except those matching pkg_id and checksum.
658    /// Used when switching between alternate package versions.
659    pub fn unlink_others_by_checksum(
660        conn: &mut SqliteConnection,
661        pkg_name: &str,
662        keep_pkg_id: &str,
663        keep_checksum: Option<&str>,
664    ) -> QueryResult<usize> {
665        if let Some(checksum) = keep_checksum {
666            diesel::update(
667                packages::table
668                    .filter(packages::pkg_name.eq(pkg_name))
669                    .filter(packages::pkg_id.ne(keep_pkg_id))
670                    .filter(packages::checksum.ne(checksum)),
671            )
672            .set(packages::unlinked.eq(true))
673            .execute(conn)
674        } else {
675            diesel::update(
676                packages::table
677                    .filter(packages::pkg_name.eq(pkg_name))
678                    .filter(packages::pkg_id.ne(keep_pkg_id)),
679            )
680            .set(packages::unlinked.eq(true))
681            .execute(conn)
682        }
683    }
684
685    /// Links a package by pkg_name, pkg_id, and checksum.
686    /// Used when switching to an alternate package version.
687    pub fn link_by_checksum(
688        conn: &mut SqliteConnection,
689        pkg_name: &str,
690        pkg_id: &str,
691        checksum: Option<&str>,
692    ) -> QueryResult<usize> {
693        if let Some(checksum) = checksum {
694            diesel::update(
695                packages::table
696                    .filter(packages::pkg_name.eq(pkg_name))
697                    .filter(packages::pkg_id.eq(pkg_id))
698                    .filter(packages::checksum.eq(checksum)),
699            )
700            .set(packages::unlinked.eq(false))
701            .execute(conn)
702        } else {
703            diesel::update(
704                packages::table
705                    .filter(packages::pkg_name.eq(pkg_name))
706                    .filter(packages::pkg_id.eq(pkg_id)),
707            )
708            .set(packages::unlinked.eq(false))
709            .execute(conn)
710        }
711    }
712}