1use diesel::prelude::*;
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)]
370 pub fn record_installation(
371 conn: &mut SqliteConnection,
372 repo_name: &str,
373 pkg_name: &str,
374 pkg_id: &str,
375 version: &str,
376 size: i64,
377 provides: Option<Vec<PackageProvide>>,
378 with_pkg_id: bool,
379 checksum: Option<&str>,
380 installed_date: &str,
381 ) -> QueryResult<Option<i32>> {
382 let provides = provides.map(|v| serde_json::to_value(v).unwrap_or_default());
383 diesel::update(
384 packages::table
385 .filter(packages::repo_name.eq(repo_name))
386 .filter(packages::pkg_name.eq(pkg_name))
387 .filter(packages::pkg_id.eq(pkg_id))
388 .filter(packages::version.eq(version)),
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::with_pkg_id.eq(with_pkg_id),
396 packages::checksum.eq(checksum),
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 get_portable(
464 conn: &mut SqliteConnection,
465 package_id: i32,
466 ) -> QueryResult<Option<PortablePackage>> {
467 portable_package::table
468 .filter(portable_package::package_id.eq(package_id))
469 .select(PortablePackage::as_select())
470 .first(conn)
471 .optional()
472 }
473
474 pub fn insert_portable(
476 conn: &mut SqliteConnection,
477 portable: &NewPortablePackage,
478 ) -> QueryResult<usize> {
479 diesel::insert_into(portable_package::table)
480 .values(portable)
481 .execute(conn)
482 }
483
484 pub fn upsert_portable(
486 conn: &mut SqliteConnection,
487 package_id: i32,
488 portable_path: Option<&str>,
489 portable_home: Option<&str>,
490 portable_config: Option<&str>,
491 portable_share: Option<&str>,
492 portable_cache: Option<&str>,
493 ) -> QueryResult<usize> {
494 let updated = diesel::update(
495 portable_package::table.filter(portable_package::package_id.eq(package_id)),
496 )
497 .set((
498 portable_package::portable_path.eq(portable_path),
499 portable_package::portable_home.eq(portable_home),
500 portable_package::portable_config.eq(portable_config),
501 portable_package::portable_share.eq(portable_share),
502 portable_package::portable_cache.eq(portable_cache),
503 ))
504 .execute(conn)?;
505
506 if updated == 0 {
507 diesel::insert_into(portable_package::table)
508 .values(&NewPortablePackage {
509 package_id,
510 portable_path,
511 portable_home,
512 portable_config,
513 portable_share,
514 portable_cache,
515 })
516 .execute(conn)
517 } else {
518 Ok(updated)
519 }
520 }
521
522 pub fn delete_portable(conn: &mut SqliteConnection, package_id: i32) -> QueryResult<usize> {
524 diesel::delete(portable_package::table.filter(portable_package::package_id.eq(package_id)))
525 .execute(conn)
526 }
527
528 pub fn get_old_package_paths(
531 conn: &mut SqliteConnection,
532 pkg_id: &str,
533 pkg_name: &str,
534 repo_name: &str,
535 ) -> QueryResult<Vec<(i32, String)>> {
536 let latest: Option<(i32, String)> = packages::table
537 .filter(packages::pkg_id.eq(pkg_id))
538 .filter(packages::pkg_name.eq(pkg_name))
539 .filter(packages::repo_name.eq(repo_name))
540 .order(packages::id.desc())
541 .select((packages::id, packages::installed_path))
542 .first(conn)
543 .optional()?;
544
545 let Some((latest_id, latest_path)) = latest else {
546 return Ok(Vec::new());
547 };
548
549 packages::table
550 .filter(packages::pkg_id.eq(pkg_id))
551 .filter(packages::pkg_name.eq(pkg_name))
552 .filter(packages::repo_name.eq(repo_name))
553 .filter(packages::pinned.eq(false))
554 .filter(packages::id.ne(latest_id))
555 .filter(packages::installed_path.ne(&latest_path))
556 .select((packages::id, packages::installed_path))
557 .load(conn)
558 }
559
560 pub fn delete_old_packages(
562 conn: &mut SqliteConnection,
563 pkg_id: &str,
564 pkg_name: &str,
565 repo_name: &str,
566 ) -> QueryResult<usize> {
567 let latest_id: Option<i32> = packages::table
568 .filter(packages::pkg_id.eq(pkg_id))
569 .filter(packages::pkg_name.eq(pkg_name))
570 .filter(packages::repo_name.eq(repo_name))
571 .order(packages::id.desc())
572 .select(packages::id)
573 .first(conn)
574 .optional()?;
575
576 let Some(latest_id) = latest_id else {
577 return Ok(0);
578 };
579
580 diesel::delete(
581 packages::table
582 .filter(packages::pkg_id.eq(pkg_id))
583 .filter(packages::pkg_name.eq(pkg_name))
584 .filter(packages::repo_name.eq(repo_name))
585 .filter(packages::pinned.eq(false))
586 .filter(packages::id.ne(latest_id)),
587 )
588 .execute(conn)
589 }
590
591 pub fn unlink_others_by_checksum(
594 conn: &mut SqliteConnection,
595 pkg_name: &str,
596 keep_pkg_id: &str,
597 keep_checksum: Option<&str>,
598 ) -> QueryResult<usize> {
599 if let Some(checksum) = keep_checksum {
600 diesel::update(
601 packages::table
602 .filter(packages::pkg_name.eq(pkg_name))
603 .filter(packages::pkg_id.ne(keep_pkg_id))
604 .filter(packages::checksum.ne(checksum)),
605 )
606 .set(packages::unlinked.eq(true))
607 .execute(conn)
608 } else {
609 diesel::update(
610 packages::table
611 .filter(packages::pkg_name.eq(pkg_name))
612 .filter(packages::pkg_id.ne(keep_pkg_id)),
613 )
614 .set(packages::unlinked.eq(true))
615 .execute(conn)
616 }
617 }
618
619 pub fn link_by_checksum(
622 conn: &mut SqliteConnection,
623 pkg_name: &str,
624 pkg_id: &str,
625 checksum: Option<&str>,
626 ) -> QueryResult<usize> {
627 if let Some(checksum) = checksum {
628 diesel::update(
629 packages::table
630 .filter(packages::pkg_name.eq(pkg_name))
631 .filter(packages::pkg_id.eq(pkg_id))
632 .filter(packages::checksum.eq(checksum)),
633 )
634 .set(packages::unlinked.eq(false))
635 .execute(conn)
636 } else {
637 diesel::update(
638 packages::table
639 .filter(packages::pkg_name.eq(pkg_name))
640 .filter(packages::pkg_id.eq(pkg_id)),
641 )
642 .set(packages::unlinked.eq(false))
643 .execute(conn)
644 }
645 }
646}