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