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 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
83pub struct CoreRepository;
85
86impl CoreRepository {
87 pub fn list_all(conn: &mut SqliteConnection) -> QueryResult<Vec<Package>> {
89 packages::table.select(Package::as_select()).load(conn)
90 }
91
92 #[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 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 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 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 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 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 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 pub fn count(conn: &mut SqliteConnection) -> QueryResult<i64> {
230 packages::table.count().get_result(conn)
231 }
232
233 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}