1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum SortDirection {
16 Asc,
17 Desc,
18}
19
20pub type InstalledPackage = Package;
22pub type NewInstalledPackage<'a> = NewPackage<'a>;
24
25#[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
81pub struct CoreRepository;
83
84impl CoreRepository {
85 pub fn list_all(conn: &mut SqliteConnection) -> QueryResult<Vec<Package>> {
87 packages::table.select(Package::as_select()).load(conn)
88 }
89
90 #[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 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 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 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 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 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 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 pub fn count(conn: &mut SqliteConnection) -> QueryResult<i64> {
228 packages::table.count().get_result(conn)
229 }
230
231 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}