diff --git a/Cargo.lock b/Cargo.lock index 048cf39..48c320b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,18 +17,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -473,6 +461,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -626,28 +620,22 @@ dependencies = [ "tracing", ] -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] [[package]] name = "hashlink" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown", ] [[package]] @@ -936,7 +924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown", ] [[package]] @@ -1003,9 +991,9 @@ checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4" dependencies = [ "cc", "pkg-config", @@ -1507,9 +1495,9 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.32.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110" dependencies = [ "bitflags 2.6.0", "fallible-iterator", @@ -1522,9 +1510,9 @@ dependencies = [ [[package]] name = "rusqlite-macros" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecdc5e5d64f172916dfc8a0b0f7876de19b899e7a5f1d5b2c04c722cc78e0e45" +checksum = "4ba708b8c51e7bdf79141f230241ac69b2244a12b2b8ff53c8a68949532416b4" dependencies = [ "fallible-iterator", "litrs", @@ -1768,9 +1756,9 @@ dependencies = [ [[package]] name = "soar-dl" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191f1217bc08fed8ba515e9165d6677560836a0ec201cc2343e2f6e2930bddc0" +checksum = "c32b89f370b21c04fa23c1320b32b4f1df385b22d6419f37df8edcf530ebfd82" dependencies = [ "futures", "regex", @@ -1798,9 +1786,9 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "sqlite3-parser" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5307dad6cb84730ce8bdefde56ff4cf95fe516972d52e2bbdc8a8cd8f2520b" +checksum = "b3a9d9c3080a2da890fff8c0a8b764da31460877be04f7ef9a4894a09bfdffb8" dependencies = [ "bitflags 2.6.0", "cc", diff --git a/Cargo.toml b/Cargo.toml index 0a32866..2f4050c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ futures = "0.3.31" rayon = "1.10.0" regex = { version = "1.11.1", default-features = false, features = ["unicode-case", "unicode-perl", "std"] } reqwest = { version = "0.12.9", default-features = false, features = ["rustls-tls", "blocking", "http2", "json", "stream"] } -rusqlite = { version = "0.32.1", features = ["bundled", "rusqlite-macros"] } +rusqlite = { version = "0.33.0", features = ["bundled", "rusqlite-macros"] } serde = { version = "1.0.217", features = ["derive"] } serde_json = { version = "1.0.135", features = ["indexmap"] } -soar-dl = "0.3.2" +soar-dl = "0.3.3" diff --git a/soar-cli/src/inspect.rs b/soar-cli/src/inspect.rs index 004e6e1..d7156b7 100644 --- a/soar-cli/src/inspect.rs +++ b/soar-cli/src/inspect.rs @@ -3,7 +3,12 @@ use std::fmt::Display; use futures::StreamExt; use indicatif::HumanBytes; use soar_core::{ - database::packages::PackageQueryBuilder, package::query::PackageQuery, SoarResult, + database::{ + models::Package, + packages::{PackageQueryBuilder, PaginatedResponse}, + }, + package::query::PackageQuery, + SoarResult, }; use tracing::{error, info}; @@ -31,7 +36,7 @@ pub async fn inspect_log(package: &str, inspect_type: InspectType) -> SoarResult let builder = PackageQueryBuilder::new(repo_db).limit(1); let builder = query.apply_filters(builder); - let packages = builder.load()?; + let packages: PaginatedResponse = builder.load()?; if packages.items.is_empty() { error!("Package {} not found", package); diff --git a/soar-cli/src/install.rs b/soar-cli/src/install.rs index 808f034..a490b5f 100644 --- a/soar-cli/src/install.rs +++ b/soar-cli/src/install.rs @@ -14,7 +14,7 @@ use soar_core::{ config::get_config, database::{ models::{InstalledPackage, Package, PackageExt}, - packages::{FilterCondition, PackageQueryBuilder, ProvideStrategy}, + packages::{FilterCondition, PackageQueryBuilder, PaginatedResponse, ProvideStrategy}, }, error::SoarError, package::{ @@ -144,7 +144,7 @@ fn resolve_packages( .items; if query.name.is_none() && query.pkg_id.is_some() { - let packages = builder.load()?; + let packages: PaginatedResponse = builder.load()?; for pkg in packages.items { let existing_install = installed_packages .iter() @@ -415,10 +415,10 @@ async fn install_single_package( if let Some(provides) = &target.package.provides { for provide in provides { - if let Some(ref target) = provide.target_name { + if let Some(ref target) = provide.target { let real_path = install_dir.join(provide.name.clone()); let is_symlink = match provide.strategy { - ProvideStrategy::KeepTargetOnly | ProvideStrategy::KeepBoth => true, + Some(ProvideStrategy::KeepTargetOnly) | Some(ProvideStrategy::KeepBoth) => true, _ => false, }; if is_symlink { diff --git a/soar-cli/src/list.rs b/soar-cli/src/list.rs index d4cc3ff..ff01021 100644 --- a/soar-cli/src/list.rs +++ b/soar-cli/src/list.rs @@ -6,8 +6,8 @@ use rusqlite::Connection; use soar_core::{ config::get_config, database::{ - models::Package, - packages::{FilterCondition, PackageQueryBuilder, SortDirection}, + models::{FromRow, Package}, + packages::{FilterCondition, PackageQueryBuilder, PaginatedResponse, SortDirection}, }, SoarResult, }; @@ -113,7 +113,7 @@ pub async fn query_package(query: String) -> SoarResult<()> { let state = AppState::new().await?; let repo_db = state.repo_db().clone(); - let packages = PackageQueryBuilder::new(repo_db) + let packages: Vec = PackageQueryBuilder::new(repo_db) .where_and("pkg_name", FilterCondition::Eq(query)) .load()? .items; @@ -346,6 +346,29 @@ pub async fn query_package(query: String) -> SoarResult<()> { Ok(()) } +#[derive(Debug, Clone)] +pub struct PackageList { + pkg_id: String, + pkg_name: String, + repo_name: String, + pkg_type: String, + version: String, + version_upstream: Option, +} + +impl FromRow for PackageList { + fn from_row(row: &rusqlite::Row) -> rusqlite::Result { + Ok(PackageList { + pkg_id: row.get("pkg_id")?, + pkg_name: row.get("pkg_name")?, + repo_name: row.get("repo_name")?, + pkg_type: row.get("pkg_type")?, + version: row.get("version")?, + version_upstream: row.get("version_upstream")?, + }) + } +} + pub async fn list_packages(repo_name: Option) -> SoarResult<()> { let state = AppState::new().await?; let repo_db = state.repo_db().clone(); @@ -359,10 +382,30 @@ pub async fn list_packages(repo_name: Option) -> SoarResult<()> { builder = builder.where_and("repo_name", FilterCondition::Eq(repo_name)); } + builder = builder.select(&[ + "pkg_id", + "pkg_name", + "pkg_type", + "version", + "version_upstream", + ]); + loop { - let packages = builder.load()?; + let packages: PaginatedResponse = builder.load()?; for package in &packages.items { - let install_state = get_package_install_state(&core_db, &package)?; + let installed_pkgs = PackageQueryBuilder::new(core_db.clone()) + .where_and("repo_name", FilterCondition::Eq(package.repo_name.clone())) + .where_and("pkg_id", FilterCondition::Eq(package.pkg_id.clone())) + .where_and("pkg_name", FilterCondition::Eq(package.pkg_name.clone())) + .limit(1) + .load_installed()? + .items; + + let install_state = match installed_pkgs { + _ if installed_pkgs.is_empty() => "-", + _ if installed_pkgs.first().unwrap().is_installed => "+", + _ => "?", + }; info!( pkg_name = package.pkg_name, diff --git a/soar-cli/src/update.rs b/soar-cli/src/update.rs index 3731280..9a8e988 100644 --- a/soar-cli/src/update.rs +++ b/soar-cli/src/update.rs @@ -1,6 +1,9 @@ use soar_core::{ config::get_config, - database::packages::{FilterCondition, PackageQueryBuilder}, + database::{ + models::Package, + packages::{FilterCondition, PackageQueryBuilder}, + }, package::{install::InstallTarget, query::PackageQuery}, SoarResult, }; @@ -26,7 +29,7 @@ pub async fn update_packages(packages: Option>) -> SoarResult<()> { let installed_pkgs = builder.load_installed()?.items; for pkg in installed_pkgs { - let updated = builder + let updated: Vec = builder .clone() .database(repo_db.clone()) .where_and("version", FilterCondition::Gt(pkg.version.clone())) @@ -50,7 +53,7 @@ pub async fn update_packages(packages: Option>) -> SoarResult<()> { .items; for pkg in installed_packages { - let updated = PackageQueryBuilder::new(repo_db.clone()) + let updated: Vec = PackageQueryBuilder::new(repo_db.clone()) .where_and("repo_name", FilterCondition::Eq(pkg.repo_name.clone())) .where_and("pkg_name", FilterCondition::Eq(pkg.pkg_name.clone())) .where_and("pkg_id", FilterCondition::Eq(pkg.pkg_id.clone())) diff --git a/soar-cli/src/use.rs b/soar-cli/src/use.rs index 980b276..c1eed19 100644 --- a/soar-cli/src/use.rs +++ b/soar-cli/src/use.rs @@ -111,10 +111,10 @@ pub async fn use_alternate_package(name: &str) -> SoarResult<()> { if let Some(provides) = &selected_package.provides { for provide in provides { - if let Some(ref target) = provide.target_name { + if let Some(ref target) = provide.target { let real_path = install_dir.join(provide.name.clone()); let is_symlink = match provide.strategy { - ProvideStrategy::KeepTargetOnly | ProvideStrategy::KeepBoth => true, + Some(ProvideStrategy::KeepTargetOnly) | Some(ProvideStrategy::KeepBoth) => true, _ => false, }; if is_symlink { diff --git a/soar-core/src/database/models.rs b/soar-core/src/database/models.rs index 02db2df..b264d80 100644 --- a/soar-core/src/database/models.rs +++ b/soar-core/src/database/models.rs @@ -27,6 +27,10 @@ pub trait PackageExt { fn should_create_original_symlink(&self) -> bool; } +pub trait FromRow: Sized { + fn from_row(row: &rusqlite::Row) -> rusqlite::Result; +} + #[derive(Debug, Clone)] pub struct Package { pub id: u64, @@ -76,8 +80,8 @@ pub struct Package { pub maintainers: Vec, } -impl Package { - pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result { +impl FromRow for Package { + fn from_row(row: &rusqlite::Row) -> rusqlite::Result { let parse_json_vec = |idx: &str| -> rusqlite::Result>> { Ok(row .get::<_, Option>(idx)? @@ -182,8 +186,8 @@ pub struct InstalledPackage { pub provides: Option>, } -impl InstalledPackage { - pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result { +impl FromRow for InstalledPackage { + fn from_row(row: &rusqlite::Row) -> rusqlite::Result { let parse_provides = |idx: &str| -> rusqlite::Result>> { let value: Option = row.get(idx)?; Ok(value.and_then(|s| serde_json::from_str(&s).ok())) @@ -380,7 +384,7 @@ fn should_create_original_symlink_impl(provides: Option<&Vec>) - .map(|links| { !links .iter() - .any(|link| matches!(link.strategy, ProvideStrategy::KeepTargetOnly)) + .any(|link| matches!(link.strategy, Some(ProvideStrategy::KeepTargetOnly))) }) .unwrap_or(true) } diff --git a/soar-core/src/database/packages/models.rs b/soar-core/src/database/packages/models.rs index 3a211d7..6951a12 100644 --- a/soar-core/src/database/packages/models.rs +++ b/soar-core/src/database/packages/models.rs @@ -48,13 +48,11 @@ pub struct PaginatedResponse { pub has_next: bool, } -#[derive(Debug, Default, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub enum ProvideStrategy { KeepTargetOnly, KeepBoth, Alias, - #[default] - None, } impl Display for ProvideStrategy { @@ -63,7 +61,6 @@ impl Display for ProvideStrategy { ProvideStrategy::KeepTargetOnly => "=>", ProvideStrategy::KeepBoth => "==", ProvideStrategy::Alias => ":", - _ => "", }; write!(f, "{}", msg) } @@ -72,8 +69,8 @@ impl Display for ProvideStrategy { #[derive(Debug, Default, Deserialize, Serialize, Clone)] pub struct PackageProvide { pub name: String, - pub target_name: Option, - pub strategy: ProvideStrategy, + pub target: Option, + pub strategy: Option, } impl PackageProvide { @@ -81,26 +78,26 @@ impl PackageProvide { if let Some((name, target_name)) = provide.split_once("==") { Self { name: name.to_string(), - target_name: Some(target_name.to_string()), - strategy: ProvideStrategy::KeepBoth, + target: Some(target_name.to_string()), + strategy: Some(ProvideStrategy::KeepBoth), } } else if let Some((name, target_name)) = provide.split_once("=>") { Self { name: name.to_string(), - target_name: Some(target_name.to_string()), - strategy: ProvideStrategy::KeepTargetOnly, + target: Some(target_name.to_string()), + strategy: Some(ProvideStrategy::KeepTargetOnly), } } else if let Some((name, target_name)) = provide.split_once(":") { Self { name: name.to_string(), - target_name: Some(target_name.to_string()), - strategy: ProvideStrategy::Alias, + target: Some(target_name.to_string()), + strategy: Some(ProvideStrategy::Alias), } } else { Self { name: provide.to_string(), - target_name: None, - strategy: ProvideStrategy::None, + target: None, + strategy: None, } } } diff --git a/soar-core/src/database/packages/query.rs b/soar-core/src/database/packages/query.rs index 4d0d206..608d02f 100644 --- a/soar-core/src/database/packages/query.rs +++ b/soar-core/src/database/packages/query.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex}; use rusqlite::{Connection, ToSql}; use crate::{ - database::models::{InstalledPackage, Package}, + database::models::{FromRow, InstalledPackage}, error::SoarError, SoarResult, }; @@ -18,6 +18,7 @@ pub struct PackageQueryBuilder { limit: Option, shards: Option>, page: u32, + select_columns: Vec, } impl PackageQueryBuilder { @@ -29,9 +30,16 @@ impl PackageQueryBuilder { limit: None, shards: None, page: 1, + select_columns: Vec::new(), } } + pub fn select(mut self, columns: &[&str]) -> Self { + self.select_columns + .extend(columns.iter().map(|&col| col.to_string())); + self + } + pub fn clear_filters(mut self) -> Self { self.filters = Vec::new(); self @@ -125,7 +133,7 @@ impl PackageQueryBuilder { self } - pub fn load(&self) -> SoarResult> { + pub fn load(&self) -> SoarResult> { let conn = self.db.lock().map_err(|_| SoarError::PoisonError)?; let shards = self.get_shards(&conn)?; @@ -138,7 +146,7 @@ impl PackageQueryBuilder { .collect(); let items = stmt - .query_map(params_ref.as_slice(), Package::from_row)? + .query_map(params_ref.as_slice(), T::from_row)? .filter_map(|r| match r { Ok(pkg) => Some(pkg), Err(err) => { @@ -190,9 +198,59 @@ impl PackageQueryBuilder { let shard_queries: Vec = shards .iter() .map(|shard| { + let cols = if self.select_columns.is_empty() { + vec![ + "p.id", + "disabled", + "json(disabled_reason) AS disabled_reason", + "rank", + "pkg", + "pkg_id", + "pkg_name", + "pkg_family", + "pkg_type", + "pkg_webpage", + "app_id", + "description", + "version", + "version_upstream", + "json(licenses) AS licenses", + "download_url", + "size", + "ghcr_pkg", + "ghcr_size", + "json(ghcr_files) AS ghcr_files", + "ghcr_blob", + "ghcr_url", + "bsum", + "shasum", + "icon", + "desktop", + "appstream", + "json(homepages) AS homepages", + "json(notes) AS notes", + "json(source_urls) AS source_urls", + "json(tags) AS tags", + "json(categories) AS categories", + "build_id", + "build_date", + "build_action", + "build_script", + "build_log", + "json(provides) AS provides", + "json(snapshots) AS snapshots", + "json(repology) AS repology", + "download_count", + "download_count_week", + "download_count_month", + ] + .join(",") + } else { + self.select_columns.join(",") + }; let select_clause = format!( "SELECT - p.*, r.name AS repo_name, + {cols}, r.name AS repo_name, json_group_array( json_object( 'name', m.name, @@ -259,7 +317,7 @@ impl PackageQueryBuilder { .iter() .map(|shard| { let select_clause = format!( - "SELECT COUNT(*) as cnt, r.name as repo_name FROM {0}.packages p JOIN {0}.repository r", + "SELECT COUNT(1) as cnt, r.name as repo_name FROM {0}.packages p JOIN {0}.repository r", shard ); @@ -298,7 +356,7 @@ impl PackageQueryBuilder { let (count_query, count_params) = { let mut params: Vec> = Vec::new(); - let select_clause = "SELECT COUNT(*) FROM packages p"; + let select_clause = "SELECT COUNT(1) FROM packages p"; let where_clause = self.build_where_clause(&mut params); let query = format!("{} {}", select_clause, where_clause); (query, params) diff --git a/soar-core/src/database/repository.rs b/soar-core/src/database/repository.rs index dde8e86..04931c6 100644 --- a/soar-core/src/database/repository.rs +++ b/soar-core/src/database/repository.rs @@ -89,7 +89,7 @@ impl<'a> PackageRepository<'a> { .collect::>() }); let provides = serde_json::to_string(&provides).unwrap(); - self.statements.package_insert.execute(params![ + let inserted = self.statements.package_insert.execute(params![ package.disabled, disabled_reason, package.rank, @@ -133,6 +133,9 @@ impl<'a> PackageRepository<'a> { package.download_count_week, package.download_count_month ])?; + if inserted == 0 { + return Ok(()); + } let package_id = self.tx.last_insert_rowid(); for maintainer in &package.maintainers { let typed = self.extract_name_and_contact(&maintainer); diff --git a/soar-core/src/database/statements.rs b/soar-core/src/database/statements.rs index e45a12c..bb091bb 100644 --- a/soar-core/src/database/statements.rs +++ b/soar-core/src/database/statements.rs @@ -37,10 +37,10 @@ impl<'a> DbStatements<'a> { ) VALUES ( - ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, - ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, - ?26, ?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35, ?36, ?37, - ?38, ?39, ?40, ?41, ?42 + ?1, jsonb(?2), ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, + jsonb(?14), ?15, ?16, ?17, ?18, jsonb(?19), ?20, ?21, ?22, ?23, ?24, ?25, + ?26, jsonb(?27), jsonb(?28), jsonb(?29), jsonb(?30), jsonb(?31), ?32, ?33, ?34, ?35, ?36, + jsonb(?37), jsonb(?38), jsonb(?39), ?40, ?41, ?42 ) ON CONFLICT (pkg_id, pkg_name) DO NOTHING", )?, diff --git a/soar-core/src/package/install.rs b/soar-core/src/package/install.rs index cf7d0d4..1b205be 100644 --- a/soar-core/src/package/install.rs +++ b/soar-core/src/package/install.rs @@ -224,9 +224,10 @@ impl PackageInstaller { if let Some(provides) = package.provides { for provide in provides { - if let Some(ref target) = provide.target_name { + if let Some(ref target) = provide.target { let is_symlink = match provide.strategy { - ProvideStrategy::KeepTargetOnly | ProvideStrategy::KeepBoth => true, + Some(ProvideStrategy::KeepTargetOnly) + | Some(ProvideStrategy::KeepBoth) => true, _ => false, }; if is_symlink { diff --git a/soar-core/src/package/remove.rs b/soar-core/src/package/remove.rs index 85f8075..e2c6e26 100644 --- a/soar-core/src/package/remove.rs +++ b/soar-core/src/package/remove.rs @@ -38,9 +38,11 @@ impl PackageRemover { if let Some(provides) = &self.package.provides { for provide in provides { - if let Some(ref target) = provide.target_name { + if let Some(ref target) = provide.target { let is_symlink = match provide.strategy { - ProvideStrategy::KeepTargetOnly | ProvideStrategy::KeepBoth => true, + Some(ProvideStrategy::KeepTargetOnly) | Some(ProvideStrategy::KeepBoth) => { + true + } _ => false, }; if is_symlink {