Skip to content

Conversation

@QaidVoid
Copy link
Member

@QaidVoid QaidVoid commented Dec 22, 2025

Summary by CodeRabbit

  • New Features

    • System health checks to find/remove broken packages and broken symlinks
    • Nest management commands for adding/removing/listing repositories
    • New CLI listing/searching/inspect/use/remove/update/install flows with richer package metadata
  • Improvements

    • Configurable display: icons, spinners, and multiple progress styles
    • Enhanced error diagnostics with actionable messages and codes
    • Redesigned database backend for more reliable metadata sync and package operations

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 22, 2025

Warning

Rate limit exceeded

@QaidVoid has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 57 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 73fad60 and b2317b9.

📒 Files selected for processing (7)
  • README.md
  • crates/soar-cli/src/list.rs
  • crates/soar-config/src/config.rs
  • crates/soar-config/src/repository.rs
  • crates/soar-core/src/database/models.rs
  • crates/soar-registry/src/package.rs
  • crates/soar-utils/src/fs.rs

Walkthrough

Workspace-wide refactor: crates moved under crates/, rusqlite replaced by Diesel, a new soar-db crate with Diesel repositories (Core/Metadata/Nest) was added, CLI commands were adapted to use DieselDatabase/MetadataManager/AppState and error handling upgraded to miette diagnostics.

Changes

Cohort / File(s) Summary
Workspace & Manifests
Cargo.toml, crates/*/Cargo.toml
Reorganized workspace members to crates/ paths; added/updated workspace path dependencies; changed release opt-level; added serde_json to diesel features; bumped many crate dependency versions.
soar-db: core connection & manager
crates/soar-db/src/connection.rs, crates/soar-db/src/lib.rs, crates/soar-db/Cargo.toml
New DbConnection/DatabaseManager abstractions, migrations handling, and crate module surface; new DbError and Result alias.
soar-db: repositories & models
crates/soar-db/src/repository/*, crates/soar-db/src/models/*, crates/soar-db/src/schema/*, crates/soar-db/migrations/*
Added MetadataRepository, CoreRepository, NestRepository; models refactored for Diesel with custom Queryable impls; schema/migration changes (removed/renamed columns like pkg, rank, checksum->bsum); removed JsonValue wrapper.
soar-core: Diesel integration & metadata manager
crates/soar-core/src/database/connection.rs, crates/soar-core/src/database/mod.rs, crates/soar-core/src/database/models.rs
Added DieselDatabase, MetadataManager, new package/install models and conversions from soar-db types; removed prior rusqlite Database/migration/statement modules.
soar-core: package ops
crates/soar-core/src/package/*
Migrated install/remove/update flows to use DieselDatabase and CoreRepository; added remove_old_versions; adapted PackageInstaller/InstallTarget types and signatures.
soar-core: errors & utils
crates/soar-core/src/error.rs, crates/soar-core/src/utils.rs, crates/soar-core/src/constants.rs
Reworked SoarError to derive miette::Diagnostic with rich codes/helps; added ErrorContext trait; added/modified utility functions and constants.
soar-config: display settings
crates/soar-config/src/display.rs, crates/soar-config/src/config.rs, crates/soar-config/src/lib.rs
New DisplaySettings type, ProgressStyle enum, Config.display field and accessor; exported display module.
soar-cli: AppState & command refactors
crates/soar-cli/src/state.rs, crates/soar-cli/src/{download,inspect,install,run,update,remove,health,list,use,progress,utils,main}.rs
Introduced AppState (lazy DieselDatabase + MetadataManager); refactored CLI commands to use metadata/cores repositories, DieselDatabase, icons/display settings; added health, progress spinner, icons utilities; integrated miette error hook.
soar-utils: errors & fs
crates/soar-utils/src/error.rs, crates/soar-utils/src/fs.rs, crates/soar-utils/Cargo.toml
Converted error types to thiserror + miette diagnostics; IoContext/IoOperation refactor; safer remove handling; added workspace deps.
soar-registry: metadata changes
crates/soar-registry/src/{lib.rs,metadata.rs,package.rs}
Added optional existing_etag parameter to fetch functions; removed download_count fields from RemotePackage model.
Removed legacy rusqlite code
soar-core/src/database/*, soar-core/migrations/*, soar-cli/src/{state.rs,health.rs,list.rs,nest.rs,remove.rs,use.rs}, soar-core/src/database/*
Deleted old rusqlite-based database, migration manager, query builders, and many CLI modules replaced by new Diesel-backed implementations.
CI
.github/workflows/ci.yaml
Changed lint job toolchain from stable to nightly.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as soar-cli (command)
    participant App as AppState
    participant MetaMgr as MetadataManager
    participant MetaRepo as MetadataRepository
    participant CoreRepo as CoreRepository
    participant DB as DieselDatabase
    participant SQL as SQLite

    CLI->>App: AppState::new() / request action
    CLI->>App: metadata_manager()
    App->>DB: create/open metadata DBs
    DB->>SQL: open + migrations
    DB-->>App: DieselDatabase

    App->>MetaMgr: build MetadataManager (add repo DBs)
    CLI->>MetaMgr: query_repo / query_all
    MetaMgr->>MetaRepo: execute query (per-repo)
    MetaRepo->>DB: with_conn -> run SELECT
    DB->>SQL: SELECT packages...
    SQL-->>DB: rows
    DB-->>MetaRepo: rows -> deserialized
    MetaRepo-->>MetaMgr: Vec<Package>
    MetaMgr-->>CLI: results

    CLI->>CoreRepo: list_filtered / record_installation
    CoreRepo->>DB: with_conn -> run INSERT/SELECT
    DB->>SQL: write/read core DB
    SQL-->>DB: rows/ok
    DB-->>CoreRepo: result
    CoreRepo-->>CLI: installed package state
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • Areas needing extra attention:
    • Database migration strategy and schema changes (column renames/removals, JSON -> JSONB handling)
    • Repository implementations: correctness of SQL, transactions, and edge cases in CoreRepository/MetadataRepository
    • AppState lifecycle, lazy init, concurrency and OnceCell/AsyncOnceCell usage
    • Model conversions between soar-db and soar-core types (field mapping, optionality)
    • CLI command refactors (install/update/remove/inspect/download) to ensure behavior parity and user interaction flows
    • Error/diagnostic changes (miette integration and new error variants) and their propagation
    • Removal of legacy modules: search for dangling references to removed rusqlite-based APIs

Possibly related issues

Possibly related PRs

Poem

🐰 I hopped through crates and changed the burrow's map,

Diesel laid paths where rusqlite took a nap,
Repos bloom like carrots, metadata in rows,
AppState keeps watch while the new wind blows.
A tidy warren, tests ready — nibble on this patch! 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor(integration): integrate soar with modular crates' accurately describes the main architectural change—migrating from a monolithic structure to modular, crate-scoped organization with Diesel-based repositories.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/soar-cli/src/download.rs (1)

194-197: Use continue instead of break to process remaining links.

When packages.is_empty(), using break stops processing all subsequent links. This differs from the behavior in other branches (e.g., lines 117-119, 126-128) that log errors and continue. Use continue for consistent behavior.

🔎 Proposed fix
             if packages.is_empty() {
                 error!("Invalid download resource '{}'", link);
-                break;
+                continue;
             }
crates/soar-cli/src/inspect.rs (1)

189-196: Bug: {inspect_type} not interpolated in string literal.

The prompt on line 191 uses {inspect_type} inside a regular string literal, not a format string. This will display the literal text {inspect_type} to the user instead of "log" or "script".

Suggested fix
         if content_length > 1_048_576 {
             let response = interactive_ask(
-                "The {inspect_type} file is too large. Do you really want to view it (y/N)?",
+                &format!("The {inspect_type} file is too large. Do you really want to view it (y/N)?"),
             )?;
🧹 Nitpick comments (37)
crates/soar-db/src/error.rs (1)

59-69: Consider preserving context in the NotFound conversion.

The conversion from diesel::result::Error::NotFound uses a generic "Record not found" message, which loses context about what entity was being queried. Callers typically have this context and may prefer to construct DbError::NotFound directly with a meaningful message rather than relying on ? propagation.

This is acceptable as-is if callers handle NotFound explicitly, but worth noting for future debugging.

crates/soar-utils/src/fs.rs (1)

42-46: Correct use of metadata.is_dir() for symlink-safe behavior.

Using metadata.is_dir() (where metadata comes from symlink_metadata) ensures that symlinks are always removed with remove_file, even when pointing to directories. This is the correct behavior.

Consider enhancing the function's documentation to explicitly state that symlinks (regardless of their target type) are removed as files, not directories.

Optional: Add test coverage for symlink scenarios

Consider adding tests to verify correct symlink handling:

#[test]
fn test_safe_remove_symlink_to_file() {
    let dir = tempdir().unwrap();
    let target = dir.path().join("target_file");
    fs::write(&target, "content").unwrap();
    let link = dir.path().join("link");
    os::unix::fs::symlink(&target, &link).unwrap();
    
    safe_remove(&link).unwrap();
    assert!(!link.exists());
    assert!(target.exists()); // Target should still exist
}

#[test]
fn test_safe_remove_symlink_to_dir() {
    let dir = tempdir().unwrap();
    let target_dir = dir.path().join("target_dir");
    fs::create_dir(&target_dir).unwrap();
    let link = dir.path().join("link");
    os::unix::fs::symlink(&target_dir, &link).unwrap();
    
    safe_remove(&link).unwrap();
    assert!(!link.exists());
    assert!(target_dir.exists()); // Target should still exist
}

#[test]
fn test_safe_remove_broken_symlink() {
    let dir = tempdir().unwrap();
    let link = dir.path().join("broken_link");
    os::unix::fs::symlink("/nonexistent", &link).unwrap();
    
    safe_remove(&link).unwrap();
    assert!(!link.exists());
}
crates/soar-utils/src/error.rs (2)

149-159: Consider clarifying CreateSymlink error message wording.

The message "from '{from}' to '{target}'" reads slightly counter-intuitively since symlinks are typically described as "at path X pointing to Y". Additionally, the help message "target doesn't already exist" refers to the symlink location (from), not the target path, which could confuse users.

Suggested clarification
-    #[error("Failed to create symlink from '{from}' to '{target}'")]
+    #[error("Failed to create symlink at '{from}' pointing to '{target}'")]
     #[diagnostic(
         code(soar_utils::fs::create_symlink),
-        help("Check if you have write permissions and the target doesn't already exist")
+        help("Check if you have write permissions and the symlink path doesn't already exist")
     )]

205-209: Consider deriving Debug for IoContext.

IoOperation derives Debug and Clone, but IoContext doesn't derive any traits. Adding at least Debug would help with debugging and logging context when errors occur.

Suggested change
 /// Context for filesystem operations.
+#[derive(Debug)]
 pub struct IoContext {
     path: PathBuf,
     operation: IoOperation,
 }
crates/soar-db/src/migration.rs (1)

10-10: Consider consistent parameter passing for DbType.

DbType is Copy, so passing by value vs reference is functionally equivalent. However, there's inconsistency:

  • apply_migrations takes &DbType (line 27)
  • migrate_json_to_jsonb takes DbType by value (line 75)

For Copy types, Rust idiom typically prefers passing by value. Consider standardizing to either approach across all functions.

Suggested refactor: Pass DbType by value consistently
 pub fn apply_migrations(
     conn: &mut SqliteConnection,
-    db_type: &DbType,
+    db_type: DbType,
 ) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
     loop {
-        match conn.run_pending_migrations(get_migrations(db_type)) {
+        match conn.run_pending_migrations(get_migrations(&db_type)) {
             Ok(_) => break,
             Err(e) if e.to_string().contains("already exists") => {
-                mark_first_pending(conn, db_type)?;
+                mark_first_pending(conn, &db_type)?;
             }
             Err(e) => return Err(e),
         }
     }

Also applies to: 27-27, 75-75

crates/soar-db/src/repository/nest.rs (1)

44-49: Consider returning the inserted entity.

Since the workspace enables returning_clauses_for_sqlite_3_35, you could return the inserted Nest with its auto-generated ID, which can be convenient for callers.

🔎 Optional: Return inserted entity
-    pub fn insert(conn: &mut SqliteConnection, nest: &NewNest) -> QueryResult<usize> {
+    pub fn insert(conn: &mut SqliteConnection, nest: &NewNest) -> QueryResult<Nest> {
         diesel::insert_into(nests::table)
             .values(nest)
-            .execute(conn)
+            .returning(Nest::as_returning())
+            .get_result(conn)
     }
crates/soar-db/src/connection.rs (2)

58-62: Consider enabling WAL mode for consistency.

The open_without_migrations method skips setting WAL mode, unlike open(). If this connection will be used for writes, the inconsistent journal mode could cause issues when multiple connections access the same database.

🔎 Proposed fix
 pub fn open_without_migrations<P: AsRef<Path>>(path: P) -> Result<Self, ConnectionError> {
     let path_str = path.as_ref().to_string_lossy();
-    let conn = SqliteConnection::establish(&path_str)?;
+    let mut conn = SqliteConnection::establish(&path_str)?;
+
+    sql_query("PRAGMA journal_mode = WAL;")
+        .execute(&mut conn)
+        .map_err(|e| ConnectionError::BadConnection(e.to_string()))?;
+
     Ok(Self { conn })
 }

144-158: Consider ensuring the base directory exists before opening databases.

DatabaseManager::new assumes base_dir exists. If the directory doesn't exist, the database file creation will fail with a potentially unclear error. Consider creating the directory or documenting this precondition.

🔎 Proposed fix
 pub fn new<P: AsRef<Path>>(base_dir: P) -> Result<Self, ConnectionError> {
     let base = base_dir.as_ref();

+    std::fs::create_dir_all(base)
+        .map_err(|e| ConnectionError::BadConnection(format!("failed to create db directory: {e}")))?;
+
     let core_path = base.join("core.db");
     let nests_path = base.join("nests.db");
crates/soar-core/Cargo.toml (1)

14-14: Consider using workspace-managed version for chrono.

The chrono dependency uses a direct version spec ("0.4") while most other dependencies use workspace = true. If chrono is defined in the workspace manifest, consider using workspace management for consistency and to avoid version drift.

#!/bin/bash
# Check if chrono is defined in the workspace Cargo.toml
grep -A2 'chrono' Cargo.toml | head -5
crates/soar-core/src/package/update.rs (1)

15-37: Consider transactional consistency for cleanup operations.

The current implementation removes directories from disk in a loop before cleaning up the database. If directory removal fails partway through, you could end up with an inconsistent state where some old versions are removed from disk but all old version records remain in the database.

Consider one of these approaches:

  1. Collect all removal errors and only delete from DB if all disk removals succeed
  2. Use a defer/finally pattern to ensure DB cleanup happens even on partial disk removal
  3. Accept the current behavior but document that partial cleanup may occur
🔎 Option 1: Only clean DB if all disk removals succeed
 pub fn remove_old_versions(package: &Package, db: &DieselDatabase) -> SoarResult<()> {
     let Package {
         pkg_id,
         pkg_name,
         repo_name,
         ..
     } = package;
 
     let old_packages =
         db.with_conn(|conn| CoreRepository::get_old_package_paths(conn, pkg_id, pkg_name, repo_name))?;
 
+    let mut removal_errors = Vec::new();
     for (_id, installed_path) in &old_packages {
         let path = Path::new(installed_path);
         if path.exists() {
-            fs::remove_dir_all(path)
-                .with_context(|| format!("removing old package directory {}", path.display()))?;
+            if let Err(e) = fs::remove_dir_all(path)
+                .with_context(|| format!("removing old package directory {}", path.display())) {
+                removal_errors.push(e);
+            }
         }
     }
+    
+    if !removal_errors.is_empty() {
+        return Err(removal_errors.into_iter().next().unwrap());
+    }
 
     db.with_conn(|conn| CoreRepository::delete_old_packages(conn, pkg_id, pkg_name, repo_name))?;
 
     Ok(())
 }
crates/soar-cli/src/update.rs (2)

76-91: Consider extracting duplicate version-check logic into a helper function.

The logic for finding newer versions and converting the result is duplicated between the Some(packages) branch (lines 77-91) and the else branch (lines 125-139). This could be extracted to reduce duplication.

🔎 Proposed refactor
fn find_newer_package(
    metadata_mgr: &MetadataManager,
    pkg: &InstalledPackage,
) -> SoarResult<Option<Package>> {
    let new_pkg = metadata_mgr
        .query_repo(&pkg.repo_name, |conn| {
            MetadataRepository::find_newer_version(
                conn,
                &pkg.pkg_name,
                &pkg.pkg_id,
                &pkg.version,
            )
        })?
        .flatten()
        .map(|p| {
            let mut package: Package = p.into();
            package.repo_name = pkg.repo_name.clone();
            package
        });
    Ok(new_pkg)
}

Also applies to: 125-139


328-328: Silently ignoring remove_old_versions errors may hide cleanup failures.

Using let _ = remove_old_versions(...) discards any errors from the cleanup operation. While failing to remove old versions may not be critical, silently ignoring these errors could lead to disk space accumulation or stale files that users aren't aware of.

Consider logging a warning when cleanup fails:

🔎 Proposed improvement
 if !keep {
-    let _ = remove_old_versions(&target.package, &diesel_db);
+    if let Err(e) = remove_old_versions(&target.package, &diesel_db) {
+        warn!("Failed to remove old versions for {}: {}", target.package.pkg_name, e);
+    }
 }

Also applies to: 341-341

crates/soar-cli/src/nest.rs (2)

4-16: Consider handling duplicate nest name insertions gracefully.

The add_nest function doesn't check if a nest with the same name already exists before inserting. Depending on the database schema, this could either:

  1. Cause a unique constraint violation (which would return an error)
  2. Create duplicate entries

If there's a unique constraint on name, the current error message "Failed to add nest" may not clearly indicate a duplicate name issue.

🔎 Proposed improvement
 pub async fn add_nest(name: &str, url: &str) -> SoarResult<()> {
     let full_name = format!("nest-{name}");
     let mut conn = get_nests_db_conn()?;
 
     let nest = NewNest {
         name: &full_name,
         url,
     };
     NestRepository::insert(conn.conn(), &nest)
-        .map_err(|e| SoarError::Custom(format!("Failed to add nest: {}", e)))?;
+        .map_err(|e| {
+            if e.to_string().contains("UNIQUE constraint") {
+                SoarError::Custom(format!("Nest `{name}` already exists"))
+            } else {
+                SoarError::Custom(format!("Failed to add nest: {}", e))
+            }
+        })?;
     println!("Added nest: {}", name);
     Ok(())
 }

4-4: Functions marked async but contain no await points.

All three functions (add_nest, remove_nest, list_nests) are marked as async but perform only synchronous database operations. This adds unnecessary overhead from the async state machine.

If these need to be async for API consistency with other CLI commands, this is fine. Otherwise, consider removing the async keyword.

Also applies to: 18-18, 34-34

crates/soar-core/src/package/remove.rs (1)

26-28: Constructor is marked async but contains no await points.

The new method is marked as async but only performs synchronous struct initialization. Consider removing the async keyword unless it's required for API consistency.

🔎 Proposed fix
-pub async fn new(package: InstalledPackage, db: DieselDatabase) -> Self {
+pub fn new(package: InstalledPackage, db: DieselDatabase) -> Self {
     Self { package, db }
 }
crates/soar-cli/src/remove.rs (1)

76-88: Clarify the skip condition logic.

The condition query.name.is_none() && !installed_pkg.with_pkg_id determines whether to skip removal. Consider adding a comment explaining when this scenario occurs and why these packages should be skipped.

crates/soar-cli/src/use.rs (1)

145-147: Consider combining the two transactions.

The unlink (line 78-80) and link (line 145-147) operations are in separate transactions. If an error occurs between them (e.g., during symlink manipulation), the database could be left in an inconsistent state where packages are unlinked but the selected one isn't linked.

crates/soar-cli/src/download.rs (1)

263-292: Consider extracting retry logic into a helper.

The retry loop with backoff for OCI downloads (lines 263-292) is well-implemented but could be extracted into a reusable utility if similar patterns exist elsewhere in the codebase.

crates/soar-db/src/models/metadata.rs (1)

47-129: Consider using Diesel's #[diesel(deserialize_as)] attribute.

The manual Queryable implementation with 37-element tuple is verbose and error-prone when adding/removing fields. Consider if Diesel's deserialize_as attribute could simplify JSON field handling while maintaining the custom deserialization logic.

crates/soar-config/src/display.rs (1)

32-35: Avoid unnecessary clone.

self.progress_style is an Option<ProgressStyle>, and calling .clone() before .unwrap_or_default() creates an unnecessary copy when the value is Some. Use clone_or_default() or pattern match.

Suggested fix
     pub fn progress_style(&self) -> ProgressStyle {
-        self.progress_style.clone().unwrap_or_default()
+        self.progress_style.clone().unwrap_or_default()
     }

Actually, since ProgressStyle is a simple enum (cheap to clone), this is acceptable. However, a cleaner pattern would be:

     pub fn progress_style(&self) -> ProgressStyle {
-        self.progress_style.clone().unwrap_or_default()
+        self.progress_style.as_ref().cloned().unwrap_or_default()
     }

Or simply keep as-is since the clone cost is negligible for this enum.

crates/soar-cli/src/health.rs (2)

112-139: Simplify: Rc<RefCell<Vec>> is unnecessary here.

The closures passed to walk_dir don't escape the function scope, so a simple mutable reference would suffice. The Rc<RefCell> adds complexity without benefit.

Suggested simplification
 fn get_broken_symlinks() -> SoarResult<Vec<std::path::PathBuf>> {
-    let broken_symlinks = Rc::new(RefCell::new(Vec::new()));
-
-    let broken_symlinks_clone = Rc::clone(&broken_symlinks);
-    let mut collect_action = |path: &Path| -> FileSystemResult<()> {
+    let mut broken_symlinks = Vec::new();
+    
+    let mut collect_action = |path: &Path| -> FileSystemResult<()> {
         if !path.exists() {
-            broken_symlinks_clone.borrow_mut().push(path.to_path_buf());
+            broken_symlinks.push(path.to_path_buf());
         }
         Ok(())
     };
-
-    let mut soar_files_action = |path: &Path| -> FileSystemResult<()> {
-        if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
-            if filename.ends_with("-soar") && !path.exists() {
-                broken_symlinks_clone.borrow_mut().push(path.to_path_buf());
-            }
-        }
-        Ok(())
-    };
-
     walk_dir(&get_config().get_bin_path()?, &mut collect_action)?;
-    walk_dir(desktop_dir(), &mut soar_files_action)?;
-    walk_dir(icons_dir(), &mut soar_files_action)?;
+    
+    let mut soar_files_action = |path: &Path| -> FileSystemResult<()> {
+        if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
+            if filename.ends_with("-soar") && !path.exists() {
+                broken_symlinks.push(path.to_path_buf());
+            }
+        }
+        Ok(())
+    };
+    walk_dir(desktop_dir(), &mut soar_files_action)?;
+    walk_dir(icons_dir(), &mut soar_files_action)?;
 
-    Ok(Rc::try_unwrap(broken_symlinks)
-        .unwrap_or_else(|rc| rc.borrow().clone().into())
-        .into_inner())
+    Ok(broken_symlinks)
 }

100-110: Consider making get_broken_packages synchronous.

This function doesn't perform any async operations - diesel_db.with_conn() is synchronous. The async keyword is unnecessary overhead.

Suggested change
-async fn get_broken_packages() -> SoarResult<Vec<(String, String, String)>> {
+fn get_broken_packages() -> SoarResult<Vec<(String, String, String)>> {

Note: This would require updating the call site at line 22 to remove .await.

crates/soar-cli/src/list.rs (2)

536-594: Complex fold accumulator reduces readability.

The 5-tuple accumulator (installed_count, unique_count, broken_count, installed_size, broken_size) is hard to follow. Consider using a named struct or breaking this into a more explicit loop.

Suggested approach
struct PackageStats {
    installed_count: u32,
    unique_count: u32,
    broken_count: u32,
    installed_size: u64,
    broken_size: u64,
}

// Then use a simple for loop with mutable stats
let mut stats = PackageStats::default();
for package in &packages {
    // ... update stats
}

60-66: Consider extracting the installed packages lookup pattern.

The same pattern for building a HashMap of installed packages appears in both search_packages (lines 60-66) and list_packages (lines 407-413). Consider extracting this to a helper function to reduce duplication.

Example helper
fn get_installed_packages_map(
    diesel_db: &DieselDatabase,
) -> SoarResult<HashMap<(String, String, String), bool>> {
    diesel_db
        .with_conn(|conn| {
            CoreRepository::list_filtered(conn, None, None, None, None, None, None, None, None)
        })?
        .into_par_iter()
        .map(|pkg| ((pkg.repo_name, pkg.pkg_id, pkg.pkg_name), pkg.is_installed))
        .collect()
}

Also applies to: 407-413

crates/soar-core/src/error.rs (1)

152-170: Consider extending root_cause() to leverage the source() chain for more variants.

Currently root_cause() only provides special handling for UreqError and Config. Other variants with #[source] attributes (like IoError) would benefit from similar treatment to surface the underlying cause.

Optional: Generic source chain traversal
 pub fn root_cause(&self) -> String {
     match self {
         Self::UreqError(e) => {
             format!(
                 "Root cause: {}",
                 e.source()
                     .map_or_else(|| e.to_string(), |source| source.to_string())
             )
         }
         Self::Config(err) => err.to_string(),
-        _ => self.to_string(),
+        Self::IoError { source, .. } => format!("Root cause: {}", source),
+        other => {
+            // Generic fallback using Error::source()
+            if let Some(src) = other.source() {
+                format!("Root cause: {}", src)
+            } else {
+                other.to_string()
+            }
+        }
     }
 }
crates/soar-cli/src/state.rs (4)

32-50: Error handling is good, but consider preserving original error type.

The function maps database errors to SoarError::Custom, which loses the structured error information. Consider using a dedicated DatabaseError variant or propagating the DbError type if available.


81-135: Silent failure on individual nest sync may hide issues.

Lines 129-130 log errors but continue processing. This is likely intentional for resilience (one failed nest shouldn't block others), but consider whether failed syncs should be collected and reported as a summary warning at the end.

Additionally, line 130's empty _ => {} arm silently ignores Ok(None) (unchanged/cached). Consider adding a trace log for debugging purposes:

                 Err(err) => error!("Failed to sync nest {}: {err}", nest.name),
-                _ => {}
+                Ok(None) => tracing::trace!("[{}] Nest unchanged (cached)", nest.name),
             }

316-322: Potential performance concern: sync methods called on every metadata_manager() access.

Even with force=false, init_repo_dbs and sync_nests are called every time metadata_manager() is accessed. While OnceCell::get_or_try_init ensures create_metadata_manager runs only once, the sync methods run unconditionally before the check.

Consider moving the sync calls inside the initialization closure or caching the sync state:

     pub async fn metadata_manager(&self) -> SoarResult<&MetadataManager> {
-        self.init_repo_dbs(false).await?;
-        self.sync_nests(false).await?;
-        self.inner
+        self.inner
             .metadata_manager
-            .get_or_try_init(|| self.create_metadata_manager())
+            .get_or_try_init(|| async {
+                self.init_repo_dbs(false).await?;
+                self.sync_nests(false).await?;
+                self.create_metadata_manager()
+            }.await)
     }

Note: This would require changing OnceCell to tokio::sync::OnceCell for async initialization.


245-274: Silent error suppression in create_metadata_manager may hide configuration issues.

Lines 249, 257-259 use if let Ok(...) patterns that silently ignore errors when getting paths or listing nests. While this prevents failures from blocking startup, it could mask misconfigurations. Consider logging warnings when these paths fail.

         for repo in &self.inner.config.repositories {
-            if let Ok(repo_path) = repo.get_path() {
+            match repo.get_path() {
+                Ok(repo_path) => {
                     let metadata_db = repo_path.join("metadata.db");
                     if metadata_db.is_file() {
                         manager.add_repo(&repo.name, metadata_db)?;
                     }
+                }
+                Err(e) => tracing::warn!("Skipping repo {}: {}", repo.name, e),
             }
         }
crates/soar-core/src/database/connection.rs (1)

91-107: Consider using HashMap for O(1) repository lookups.

MetadataManager stores databases in a Vec<(String, DieselDatabase)>, which results in O(n) lookup time for query_repo and find_first. If the number of repositories grows, this could become a performance concern.

A HashMap<String, DieselDatabase> would provide O(1) lookups while still supporting iteration for query_all operations.

🔎 Proposed refactor
+use std::collections::HashMap;
+
 /// Manager for multiple metadata databases (one per repository).
 /// Replaces the ATTACH DATABASE pattern with separate connections.
 pub struct MetadataManager {
-    databases: Vec<(String, DieselDatabase)>,
+    databases: HashMap<String, DieselDatabase>,
+    // Preserve insertion order for deterministic iteration
+    order: Vec<String>,
 }
crates/soar-cli/src/install.rs (2)

366-373: Unused _state parameter.

The _state parameter is prefixed with underscore indicating it's intentionally unused. If it's not needed, consider removing it from the function signature to avoid confusion. If it's planned for future use, a brief comment would help.


160-167: Consider using a type alias for the MetadataManager path.

The full path soar_core::database::connection::MetadataManager is used inline. Since DieselDatabase is already imported from this module, consider importing MetadataManager as well for consistency.

🔎 Proposed fix
 use soar_core::{
-    database::{connection::DieselDatabase, models::Package},
+    database::{connection::{DieselDatabase, MetadataManager}, models::Package},
     error::{ErrorContext, SoarError},
     ...
 };
 ...
 fn resolve_packages(
     state: &AppState,
-    metadata_mgr: &soar_core::database::connection::MetadataManager,
+    metadata_mgr: &MetadataManager,
     diesel_db: &DieselDatabase,
crates/soar-db/src/repository/metadata.rs (1)

134-158: Consider using Diesel's ilike for case-insensitive search if available.

The current implementation uses raw SQL for case-insensitive LIKE queries. While functional, this reduces type safety. SQLite's LIKE is case-insensitive for ASCII by default, so the explicit LOWER() calls may be unnecessary.

crates/soar-db/src/repository/core.rs (4)

368-403: #[allow(clippy::too_many_arguments)] suggests potential for a builder pattern.

The function has 11 parameters, which is hard to use correctly. Consider introducing a builder or a struct to group related parameters.

🔎 Proposed approach
pub struct InstallationRecord<'a> {
    pub repo_name: &'a str,
    pub pkg_name: &'a str,
    pub pkg_id: &'a str,
    pub version: &'a str,
    pub new_version: &'a str,
    pub size: i64,
    pub provides: Option<Vec<PackageProvide>>,
    pub with_pkg_id: bool,
    pub checksum: Option<&'a str>,
    pub installed_date: &'a str,
}

impl CoreRepository {
    pub fn record_installation(
        conn: &mut SqliteConnection,
        record: &InstallationRecord,
    ) -> QueryResult<Option<i32>> {
        // ... implementation
    }
}

530-590: Code duplication between get_old_package_paths and delete_old_packages.

Both methods have nearly identical logic for finding the latest package ID and then filtering on the same criteria. Consider extracting the common query builder or reusing get_old_package_paths in delete_old_packages.

🔎 Proposed refactor
+    /// Helper to find the latest package ID for a given pkg_id, pkg_name, repo_name.
+    fn find_latest_id(
+        conn: &mut SqliteConnection,
+        pkg_id: &str,
+        pkg_name: &str,
+        repo_name: &str,
+    ) -> QueryResult<Option<i32>> {
+        packages::table
+            .filter(packages::pkg_id.eq(pkg_id))
+            .filter(packages::pkg_name.eq(pkg_name))
+            .filter(packages::repo_name.eq(repo_name))
+            .order(packages::id.desc())
+            .select(packages::id)
+            .first(conn)
+            .optional()
+    }
+
     /// Gets old package versions (all except the newest unpinned one) for cleanup.
     pub fn get_old_package_paths(
         conn: &mut SqliteConnection,
         pkg_id: &str,
         pkg_name: &str,
         repo_name: &str,
     ) -> QueryResult<Vec<(i32, String)>> {
-        let latest_id: Option<i32> = packages::table
-            .filter(packages::pkg_id.eq(pkg_id))
-            .filter(packages::pkg_name.eq(pkg_name))
-            .filter(packages::repo_name.eq(repo_name))
-            .order(packages::id.desc())
-            .select(packages::id)
-            .first(conn)
-            .optional()?;
-
-        let Some(latest_id) = latest_id else {
+        let Some(latest_id) = Self::find_latest_id(conn, pkg_id, pkg_name, repo_name)? else {
             return Ok(Vec::new());
         };
         // ... rest unchanged
     }

486-522: Upsert pattern is correct but could use on_conflict for atomicity.

The current update-then-insert pattern works but isn't atomic. If two concurrent calls occur, both might see updated == 0 and try to insert, causing a constraint violation.

Diesel supports on_conflict which would be more robust, though SQLite's ON CONFLICT behavior with DO UPDATE requires careful handling.


232-252: Raw SQL for COUNT(DISTINCT ...) reduces type safety.

While this works, the raw SQL string is error-prone. Consider using Diesel's distinct() method or documenting why raw SQL is necessary.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7915faf and 5779193.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (84)
  • Cargo.toml
  • crates/soar-cli/Cargo.toml
  • crates/soar-cli/build.rs
  • crates/soar-cli/src/cli.rs
  • crates/soar-cli/src/download.rs
  • crates/soar-cli/src/health.rs
  • crates/soar-cli/src/inspect.rs
  • crates/soar-cli/src/install.rs
  • crates/soar-cli/src/list.rs
  • crates/soar-cli/src/logging.rs
  • crates/soar-cli/src/main.rs
  • crates/soar-cli/src/nest.rs
  • crates/soar-cli/src/progress.rs
  • crates/soar-cli/src/remove.rs
  • crates/soar-cli/src/run.rs
  • crates/soar-cli/src/self_actions.rs
  • crates/soar-cli/src/state.rs
  • crates/soar-cli/src/update.rs
  • crates/soar-cli/src/use.rs
  • crates/soar-cli/src/utils.rs
  • crates/soar-config/src/config.rs
  • crates/soar-config/src/display.rs
  • crates/soar-config/src/lib.rs
  • crates/soar-core/CHANGELOG.md
  • crates/soar-core/Cargo.toml
  • crates/soar-core/src/constants.rs
  • crates/soar-core/src/database/connection.rs
  • crates/soar-core/src/database/mod.rs
  • crates/soar-core/src/database/models.rs
  • crates/soar-core/src/error.rs
  • crates/soar-core/src/lib.rs
  • crates/soar-core/src/package/install.rs
  • crates/soar-core/src/package/mod.rs
  • crates/soar-core/src/package/query.rs
  • crates/soar-core/src/package/remove.rs
  • crates/soar-core/src/package/update.rs
  • crates/soar-core/src/utils.rs
  • crates/soar-db/Cargo.toml
  • crates/soar-db/migrations/core/2025-10-19-024753-0000_create_packages/up.sql
  • crates/soar-db/migrations/metadata/2025-10-19-095216-0000_create_packages/up.sql
  • crates/soar-db/src/connection.rs
  • crates/soar-db/src/error.rs
  • crates/soar-db/src/lib.rs
  • crates/soar-db/src/migration.rs
  • crates/soar-db/src/models/core.rs
  • crates/soar-db/src/models/metadata.rs
  • crates/soar-db/src/models/types.rs
  • crates/soar-db/src/repository/core.rs
  • crates/soar-db/src/repository/metadata.rs
  • crates/soar-db/src/repository/mod.rs
  • crates/soar-db/src/repository/nest.rs
  • crates/soar-db/src/schema/core.rs
  • crates/soar-db/src/schema/metadata.rs
  • crates/soar-registry/src/lib.rs
  • crates/soar-registry/src/metadata.rs
  • crates/soar-registry/src/package.rs
  • crates/soar-utils/Cargo.toml
  • crates/soar-utils/src/error.rs
  • crates/soar-utils/src/fs.rs
  • soar-cli/src/health.rs
  • soar-cli/src/list.rs
  • soar-cli/src/nest.rs
  • soar-cli/src/remove.rs
  • soar-cli/src/state.rs
  • soar-cli/src/use.rs
  • soar-core/migrations/core/V5_baseline.sql
  • soar-core/migrations/core/V6_add_portable_cache.sql
  • soar-core/migrations/metadata/V1_initial.sql
  • soar-core/migrations/nests/V1_initial.sql
  • soar-core/src/constants.rs
  • soar-core/src/database/connection.rs
  • soar-core/src/database/migration.rs
  • soar-core/src/database/mod.rs
  • soar-core/src/database/models.rs
  • soar-core/src/database/nests/mod.rs
  • soar-core/src/database/nests/models.rs
  • soar-core/src/database/nests/repository.rs
  • soar-core/src/database/packages/mod.rs
  • soar-core/src/database/packages/models.rs
  • soar-core/src/database/packages/query.rs
  • soar-core/src/database/repository.rs
  • soar-core/src/database/statements.rs
  • soar-core/src/error.rs
  • soar-core/src/package/update.rs
💤 Files with no reviewable changes (28)
  • crates/soar-db/src/schema/core.rs
  • crates/soar-registry/src/package.rs
  • soar-core/src/constants.rs
  • soar-cli/src/remove.rs
  • soar-core/migrations/nests/V1_initial.sql
  • crates/soar-db/migrations/core/2025-10-19-024753-0000_create_packages/up.sql
  • soar-core/migrations/core/V6_add_portable_cache.sql
  • soar-cli/src/nest.rs
  • soar-cli/src/health.rs
  • soar-core/src/error.rs
  • crates/soar-db/src/models/types.rs
  • soar-core/src/database/nests/repository.rs
  • soar-core/src/database/repository.rs
  • soar-core/migrations/core/V5_baseline.sql
  • soar-core/src/database/nests/mod.rs
  • soar-core/src/database/packages/mod.rs
  • soar-core/src/database/packages/query.rs
  • soar-core/migrations/metadata/V1_initial.sql
  • soar-core/src/database/packages/models.rs
  • soar-cli/src/use.rs
  • soar-core/src/database/statements.rs
  • soar-core/src/database/connection.rs
  • soar-core/src/database/nests/models.rs
  • soar-core/src/database/migration.rs
  • soar-core/src/database/mod.rs
  • soar-cli/src/list.rs
  • soar-cli/src/state.rs
  • soar-core/src/database/models.rs
🧰 Additional context used
🧬 Code graph analysis (21)
crates/soar-db/src/repository/mod.rs (1)
crates/soar-db/src/connection.rs (2)
  • core (181-183)
  • metadata (188-190)
crates/soar-config/src/lib.rs (1)
crates/soar-config/src/config.rs (1)
  • display (414-416)
crates/soar-core/src/package/update.rs (1)
crates/soar-db/src/repository/core.rs (2)
  • get_old_package_paths (532-559)
  • delete_old_packages (562-590)
crates/soar-cli/src/utils.rs (4)
crates/soar-cli/src/state.rs (1)
  • config (277-279)
crates/soar-config/src/config.rs (2)
  • get_config (123-135)
  • display (414-416)
crates/soar-utils/src/fs.rs (1)
  • is_elf (301-306)
crates/soar-utils/src/system.rs (1)
  • platform (9-16)
crates/soar-cli/src/nest.rs (2)
crates/soar-core/src/utils.rs (1)
  • get_nests_db_conn (98-104)
crates/soar-db/src/repository/nest.rs (3)
  • insert (45-49)
  • delete_by_name (52-54)
  • list_all (13-15)
crates/soar-db/src/error.rs (2)
crates/soar-core/src/error.rs (1)
  • from (173-175)
crates/soar-utils/src/error.rs (1)
  • from (282-326)
crates/soar-config/src/display.rs (1)
crates/soar-core/src/database/connection.rs (1)
  • default (173-175)
crates/soar-cli/src/inspect.rs (5)
crates/soar-cli/src/utils.rs (3)
  • display_settings (49-51)
  • interactive_ask (61-74)
  • select_package_interactively (105-123)
crates/soar-cli/src/progress.rs (1)
  • create_spinner (51-68)
crates/soar-db/src/repository/core.rs (2)
  • find_exact (173-191)
  • from (54-80)
crates/soar-cli/src/state.rs (1)
  • new (64-74)
crates/soar-db/src/repository/metadata.rs (1)
  • find_filtered (296-330)
crates/soar-cli/src/progress.rs (2)
crates/soar-config/src/config.rs (2)
  • display (414-416)
  • new (238-256)
crates/soar-cli/src/utils.rs (1)
  • display_settings (49-51)
crates/soar-registry/src/metadata.rs (3)
crates/soar-config/src/config.rs (2)
  • get_config (123-135)
  • new (238-256)
crates/soar-config/src/repository.rs (1)
  • sync_interval (58-71)
crates/soar-registry/src/nest.rs (1)
  • new (37-43)
crates/soar-cli/src/download.rs (4)
crates/soar-db/src/connection.rs (2)
  • metadata (188-190)
  • conn (82-84)
crates/soar-core/src/package/query.rs (1)
  • try_from (20-67)
crates/soar-core/src/database/models.rs (2)
  • repo_name (93-95)
  • repo_name (139-141)
crates/soar-db/src/repository/metadata.rs (1)
  • find_filtered (296-330)
crates/soar-core/src/database/models.rs (2)
crates/soar-db/src/repository/core.rs (1)
  • from (54-80)
crates/soar-db/src/models/metadata.rs (1)
  • new (140-145)
crates/soar-core/src/database/connection.rs (1)
crates/soar-db/src/connection.rs (4)
  • conn (82-84)
  • open (33-53)
  • new (144-158)
  • open_metadata (70-79)
crates/soar-registry/src/lib.rs (1)
crates/soar-registry/src/metadata.rs (6)
  • fetch_metadata (329-407)
  • fetch_metadata_with_etag (427-506)
  • fetch_nest_metadata (88-166)
  • fetch_nest_metadata_with_etag (173-252)
  • fetch_public_key (268-284)
  • process_metadata_content (531-577)
crates/soar-core/src/package/remove.rs (1)
crates/soar-db/src/repository/core.rs (2)
  • delete_portable (525-528)
  • delete (460-462)
crates/soar-db/src/connection.rs (1)
crates/soar-db/src/migration.rs (3)
  • sql_query (48-49)
  • apply_migrations (25-40)
  • migrate_json_to_jsonb (73-147)
crates/soar-cli/src/health.rs (7)
crates/soar-cli/src/state.rs (2)
  • config (277-279)
  • new (64-74)
crates/soar-config/src/config.rs (3)
  • get_config (123-135)
  • new (238-256)
  • display (414-416)
crates/soar-core/src/package/remove.rs (2)
  • remove (30-101)
  • new (26-28)
crates/soar-utils/src/fs.rs (1)
  • walk_dir (167-201)
crates/soar-utils/src/path.rs (2)
  • desktop_dir (141-143)
  • icons_dir (146-148)
crates/soar-core/src/package/install.rs (1)
  • new (59-112)
crates/soar-db/src/repository/core.rs (1)
  • list_broken (146-156)
crates/soar-core/src/utils.rs (2)
crates/soar-config/src/config.rs (1)
  • get_config (123-135)
crates/soar-db/src/connection.rs (2)
  • conn (82-84)
  • open (33-53)
crates/soar-db/src/repository/metadata.rs (2)
crates/soar-db/src/models/metadata.rs (1)
  • new (140-145)
crates/soar-db/src/models/types.rs (1)
  • from_string (29-55)
crates/soar-cli/src/update.rs (3)
crates/soar-core/src/package/update.rs (1)
  • remove_old_versions (15-37)
crates/soar-db/src/repository/core.rs (3)
  • find_exact (173-191)
  • list_filtered (93-143)
  • list_updatable (159-170)
crates/soar-db/src/repository/metadata.rs (1)
  • find_newer_version (335-362)
crates/soar-db/src/repository/core.rs (3)
crates/soar-db/src/repository/metadata.rs (5)
  • diesel (194-196)
  • sql (145-148)
  • sql (211-214)
  • sql (229-229)
  • sql (352-355)
crates/soar-core/src/database/models.rs (12)
  • from (146-172)
  • from (177-203)
  • from (208-264)
  • from (269-273)
  • repo_name (93-95)
  • repo_name (139-141)
  • pkg_name (81-83)
  • pkg_name (127-129)
  • pkg_id (85-87)
  • pkg_id (131-133)
  • version (89-91)
  • version (135-137)
crates/soar-db/src/error.rs (2)
  • from (60-68)
  • from (72-74)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: coverage
  • GitHub Check: test

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
crates/soar-cli/src/install.rs (1)

218-228: The unwrap panic risk from the previous review remains unaddressed.

Lines 224-225 still unwrap the result from select_package_interactively, which can return None if the user cancels the selection. Line 227 also unwraps without guaranteeing the vector is non-empty at that point (though the is_empty() check at line 218 provides some protection for that specific path).

crates/soar-core/src/package/install.rs (1)

248-264: Critical: The version parameter bug and unsafe default from the previous review remain unfixed.

Line 255 still incorrectly passes version twice to record_installation. Based on the function signature, the second parameter should be the new version, but it's receiving the same version value again. This prevents the new version from being recorded correctly.

Additionally, line 264's unwrap_or(0) silently converts a failed insert into record_id = 0. The subsequent upsert_portable call at line 298 will then attempt to update records with package_id=0, which could corrupt data or silently fail.

These are the same issues flagged in the previous review and represent critical correctness bugs.

Proposed fix
 let record_id: Option<i32> = self.db.with_conn(|conn| {
     CoreRepository::record_installation(
         conn,
         repo_name,
         pkg_name,
         pkg_id,
         version,
-        version,
+        version,  // TODO: Should this be a different new_version parameter?
         size,
         provides,
         with_pkg_id,
         checksum,
         &installed_date,
     )
 })?;

-let record_id = record_id.unwrap_or(0);
+let record_id = record_id.ok_or_else(|| {
+    SoarError::Custom(format!(
+        "Failed to record installation for {}#{}",
+        pkg_name, pkg_id
+    ))
+})?;

Verify the intended signature of CoreRepository::record_installation to confirm the correct parameter:

#!/bin/bash
# Search for the record_installation function definition
rg "fn record_installation" -A 15 crates/soar-db/src/repository/
🧹 Nitpick comments (3)
crates/soar-core/Cargo.toml (1)

14-16: Unify dependency management approach for consistency.

chrono is pinned to "0.4" while diesel and miette use workspace = true. For easier maintenance and to match the codebase pattern, consider moving chrono to workspace management.

The Diesel workspace configuration is properly set up with the required sqlite backend feature, so no changes needed there.

crates/soar-cli/src/install.rs (1)

253-292: Consider extracting the repeated metadata query pattern.

Lines 253-292 duplicate the query logic from lines 177-216 (query single repo vs. all repos, map packages with repo_name). This pattern appears multiple times in the file. Consider extracting a helper function like query_packages_with_repo_name to reduce duplication and improve maintainability.

Example helper function
fn query_packages_with_repo_name(
    metadata_mgr: &MetadataManager,
    repo_name: Option<&str>,
    pkg_name: Option<&str>,
    pkg_id: Option<&str>,
    version: Option<&str>,
    sort: Option<SortDirection>,
) -> SoarResult<Vec<Package>> {
    if let Some(ref repo) = repo_name {
        Ok(metadata_mgr
            .query_repo(repo, |conn| {
                MetadataRepository::find_filtered(conn, pkg_name, pkg_id, version, None, sort)
            })?
            .unwrap_or_default()
            .into_iter()
            .map(|p| {
                let mut pkg: Package = p.into();
                pkg.repo_name = repo.to_string();
                pkg
            })
            .collect())
    } else {
        metadata_mgr.query_all_flat(|repo_name, conn| {
            let pkgs = MetadataRepository::find_filtered(conn, pkg_name, pkg_id, version, None, sort)?;
            Ok(pkgs
                .into_iter()
                .map(|p| {
                    let mut pkg: Package = p.into();
                    pkg.repo_name = repo_name.to_string();
                    pkg
                })
                .collect())
        })
    }
}
crates/soar-cli/src/update.rs (1)

320-345: Consider logging errors from remove_old_versions.

Lines 330 and 343 silently ignore errors from remove_old_versions using let _ =. While this prevents cleanup failures from blocking the update flow, it could mask issues like database corruption or filesystem problems. Consider at least logging these errors at the warning level so they're visible for troubleshooting.

Suggested improvement
 if !keep {
-    let _ = remove_old_versions(&target.package, &diesel_db);
+    if let Err(e) = remove_old_versions(&target.package, &diesel_db) {
+        warn!("Failed to remove old versions: {}", e);
+    }
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5779193 and 777381b.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • crates/soar-cli/src/install.rs
  • crates/soar-cli/src/update.rs
  • crates/soar-core/Cargo.toml
  • crates/soar-core/src/package/install.rs
🧰 Additional context used
🧬 Code graph analysis (1)
crates/soar-cli/src/install.rs (3)
crates/soar-core/src/package/query.rs (1)
  • try_from (20-67)
crates/soar-db/src/repository/metadata.rs (1)
  • find_filtered (296-330)
crates/soar-db/src/repository/core.rs (1)
  • list_filtered (93-143)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: coverage
  • GitHub Check: test
🔇 Additional comments (16)
crates/soar-core/Cargo.toml (3)

22-22: LGTM! Good architectural separation.

The addition of soar-db as a workspace dependency aligns well with the refactoring goals to introduce a dedicated database abstraction layer with repository patterns.


26-26: LGTM! Improved consistency.

Converting thiserror to workspace management improves dependency version consistency across the workspace.


14-29: Verify that chrono version resolves to a secure release.

The generic version spec chrono = "0.4" needs to be checked. Versions of chrono before 0.4.20 had a potential segfault vulnerability (RUSTSEC-2020-0159) in Unix-like systems due to dereferencing a dangling pointer. Ensure the Cargo.lock or resolution confirms a patched version is selected. Additionally, verify that workspace dependencies for diesel and miette are current and free from known advisories.

crates/soar-cli/src/install.rs (6)

82-92: Progress style setup looks good.

The conditional progress style based on icon settings is well-implemented, providing a graceful fallback to plain text when icons are disabled.


130-158: Clean refactoring to AppState-driven flow.

The function properly initializes the state and delegates to the resolution layer. The migration from direct DB connections to state-managed resources is well-structured.


234-250: Repository query pattern correctly implemented.

The use of CoreRepository::list_filtered through the Diesel connection is correct and follows the new architectural pattern.


368-447: Package selection logic is correct.

The function properly handles existing installs, repo-specific queries, and cross-repo queries. The metadata-driven approach is correctly implemented, though it shares the same duplication pattern noted elsewhere in the file.


480-520: Enhanced display output with icons looks great.

The per-package installation details with optional icon support provide a much better user experience. The graceful fallback to plain text characters when icons are disabled is well-implemented.


525-567: Excellent summary presentation with conditional rich formatting.

The use of tabled for icon-enabled mode provides a polished summary table, while maintaining a clean fallback for plain text mode. All edge cases (no installs, failures, mixed results) are properly handled.

crates/soar-cli/src/update.rs (4)

24-39: Clean helper function for finding existing packages.

The function correctly uses CoreRepository::find_exact through the Diesel connection wrapper. The signature and implementation are straightforward and correct.


54-117: Update resolution logic is sound.

The flow correctly identifies installed packages matching the query, checks for newer versions in the metadata repository, and builds update targets. The use of find_newer_version is appropriate for the update use case.


119-166: "Update all" logic properly implemented.

The use of CoreRepository::list_updatable correctly identifies packages eligible for updates, and the subsequent metadata queries follow the same sound pattern as the selective update path.


230-275: Update summary follows the same polished pattern as install.

The summary presentation is consistent with install.rs, providing clear feedback with optional rich formatting. All result scenarios are properly handled.

crates/soar-core/src/package/install.rs (3)

59-113: Constructor properly migrated to repository-based insertion.

The refactoring correctly uses NewInstalledPackage and CoreRepository::insert instead of direct SQL. The addition of timestamp tracking with chrono and the pinned field support are appropriate enhancements.


275-289: Path resolution logic is well-implemented.

The resolve_path closure properly handles empty strings, absolute paths, and relative paths, ensuring consistent path normalization for portable configurations. This is a good defensive programming practice.


314-361: Alternate package cleanup correctly migrated to repository pattern.

The use of CoreRepository::find_alternates and InstalledPackageWithPortable properly replaces the previous direct SQL approach while maintaining the same cleanup behavior for symlinks and provides.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/soar-registry/src/metadata.rs (2)

64-93: Fix misleading documentation about ETag source.

Line 73 states the existing_etag is "read from the database," but the function no longer reads it—the caller must provide it. Update the documentation to clarify that the caller is responsible for retrieving and passing the ETag.

🔎 Suggested documentation fix
-/// * `existing_etag` - Optional etag from a previous fetch, read from the database
+/// * `existing_etag` - Optional etag from a previous fetch to enable conditional requests

202-249: Fix misleading documentation about ETag source.

Line 212 states the existing_etag is "read from the database," but the caller must provide it. This is the same documentation issue as in fetch_nest_metadata. Update to clarify caller responsibility.

🔎 Suggested documentation fix
-/// * `existing_etag` - Optional etag from a previous fetch, read from the database
+/// * `existing_etag` - Optional etag from a previous fetch to enable conditional requests

However, the example code (lines 236-237) correctly demonstrates the new signature. Good update there!

♻️ Duplicate comments (3)
crates/soar-db/src/lib.rs (1)

47-53: Silent JSON deserialization errors remain unaddressed.

The json_vec! macro continues to use unwrap_or_default(), which silently swallows deserialization errors. This issue was flagged in previous reviews but has not been resolved. Silent failures can hide data corruption and make debugging difficult.

crates/soar-db/src/models/core.rs (1)

28-71: JSON deserialization errors remain silently swallowed.

Lines 68-69 use the json_vec! macro which silently converts deserialization failures to empty vectors via unwrap_or_default(). This inherits the issue flagged in previous reviews of the macro itself.

Additionally, the manual 18-field tuple Queryable implementation is fragile—column reordering would break silently. Consider adding a doc comment documenting the expected column order as suggested in previous reviews.

crates/soar-db/src/repository/metadata.rs (1)

337-367: Indexing mismatch between Rust and SQL remains unfixed.

The issue flagged in previous reviews persists: Rust's [14..] slice (line 348) and SQL's substr(version, 14) (line 359) extract different substrings due to 0-based vs 1-based indexing.

For "HEAD-20231215-abc123":

  • Rust [14..] extracts "abc123" (starts at 15th character, 1-based)
  • SQL substr(version, 14) extracts "-abc123" (starts at 14th character, 1-based)

Change SQL to substr(version, 15) to match Rust's offset.

🧹 Nitpick comments (7)
crates/soar-cli/src/use.rs (1)

85-90: Simplify error handling pattern.

The let _ = assignment followed immediately by ? is redundant and confusing. Either remove the assignment or handle the intermediate result if needed.

🔎 Proposed fix
-    let _ = mangle_package_symlinks(
+    mangle_package_symlinks(
         &install_dir,
         &bin_dir,
         selected_package.provides.as_deref(),
     )
-    .await?;
+    .await?;
crates/soar-cli/src/state.rs (2)

31-49: Consider verifying file removal success.

The function removes the existing metadata file but doesn't verify the removal succeeded before creating a new connection. While fs::remove_file will fail if the file doesn't exist (and we handle errors), race conditions could leave the file in an inconsistent state.

🔎 Suggested improvement
     if metadata_db.exists() {
         fs::remove_file(metadata_db)
             .with_context(|| format!("removing metadata file {}", metadata_db.display()))?;
     }
+    
+    // Ensure file is removed before proceeding
+    if metadata_db.exists() {
+        return Err(SoarError::Custom(format!(
+            "metadata file {} still exists after removal",
+            metadata_db.display()
+        )));
+    }

80-134: LGTM! Consider adding sync metrics.

The concurrent nest synchronization with per-nest error handling is well-structured. Errors are logged without blocking other nests from syncing.

Optional enhancement: track and report success/failure counts for better observability:

let mut synced = 0;
let mut failed = 0;
// ... in loop ...
Ok(Some(_)) => { synced += 1; /* ... */ }
Err(_) => { failed += 1; /* ... */ }
// ... after loop ...
info!("Nest sync complete: {} synced, {} failed", synced, failed);
crates/soar-cli/src/list.rs (1)

380-403: Consider pagination or warning for large repositories.

The function calls list_all() without any limit, which could return thousands of packages in large repositories. This may cause performance issues or overwhelming output.

Consider:

  1. Adding pagination support
  2. Implementing a configurable max limit with a warning
  3. Using streaming/chunked output for large result sets
crates/soar-db/src/repository/core.rs (3)

53-81: Consider extracting portable reference once.

The implementation correctly handles the conversion, but lines 74-78 repeatedly call portable.as_ref(). While this is functionally correct and the compiler likely optimizes it, you could extract the reference once for slightly cleaner code.

🔎 Optional refactor to extract portable reference once
 impl From<(Package, Option<PortablePackage>)> for InstalledPackageWithPortable {
     fn from((pkg, portable): (Package, Option<PortablePackage>)) -> Self {
+        let (portable_path, portable_home, portable_config, portable_share, portable_cache) =
+            if let Some(ref p) = portable {
+                (
+                    p.portable_path.clone(),
+                    p.portable_home.clone(),
+                    p.portable_config.clone(),
+                    p.portable_share.clone(),
+                    p.portable_cache.clone(),
+                )
+            } else {
+                (None, None, None, None, None)
+            };
+
         Self {
             id: pkg.id,
             repo_name: pkg.repo_name,
             pkg_id: pkg.pkg_id,
             pkg_name: pkg.pkg_name,
             pkg_type: pkg.pkg_type,
             version: pkg.version,
             size: pkg.size,
             checksum: pkg.checksum,
             installed_path: pkg.installed_path,
             installed_date: pkg.installed_date,
             profile: pkg.profile,
             pinned: pkg.pinned,
             is_installed: pkg.is_installed,
             with_pkg_id: pkg.with_pkg_id,
             detached: pkg.detached,
             unlinked: pkg.unlinked,
             provides: pkg.provides,
             install_patterns: pkg.install_patterns,
-            portable_path: portable.as_ref().and_then(|p| p.portable_path.clone()),
-            portable_home: portable.as_ref().and_then(|p| p.portable_home.clone()),
-            portable_config: portable.as_ref().and_then(|p| p.portable_config.clone()),
-            portable_share: portable.as_ref().and_then(|p| p.portable_share.clone()),
-            portable_cache: portable.as_ref().and_then(|p| p.portable_cache.clone()),
+            portable_path,
+            portable_home,
+            portable_config,
+            portable_share,
+            portable_cache,
         }
     }
 }

368-401: Consider using a parameter struct for record_installation.

The method has 10 parameters and requires #[allow(clippy::too_many_arguments)]. Consider introducing a parameter struct (e.g., InstallationRecord) to improve readability and maintainability.

🔎 Example parameter struct approach
pub struct InstallationRecord<'a> {
    pub repo_name: &'a str,
    pub pkg_name: &'a str,
    pub pkg_id: &'a str,
    pub version: &'a str,
    pub size: i64,
    pub provides: Option<Vec<PackageProvide>>,
    pub with_pkg_id: bool,
    pub checksum: Option<&'a str>,
    pub installed_date: &'a str,
}

pub fn record_installation(
    conn: &mut SqliteConnection,
    record: &InstallationRecord,
) -> QueryResult<Option<i32>> {
    // ... implementation using record.field_name
}

590-644: Consider extracting common checksum filter logic.

The unlink_others_by_checksum and link_by_checksum methods have very similar structure with conditional checksum filtering. While the code is clear, you could reduce duplication by extracting a helper method for building the query filters.

🔎 Optional refactor example
fn filter_by_checksum<'a>(
    query: diesel::query_builder::UpdateStatement<
        packages::table,
        diesel::query_builder::WhereClause</* filter type */>,
    >,
    pkg_name: &str,
    pkg_id: &str,
    checksum: Option<&str>,
    match_pkg_id: bool, // true to match pkg_id, false to exclude it
) -> /* return type */ {
    let query = query
        .filter(packages::pkg_name.eq(pkg_name))
        .filter(if match_pkg_id {
            packages::pkg_id.eq(pkg_id)
        } else {
            packages::pkg_id.ne(pkg_id)
        });
    
    if let Some(cs) = checksum {
        query.filter(if match_pkg_id {
            packages::checksum.eq(cs)
        } else {
            packages::checksum.ne(cs)
        })
    } else {
        query
    }
}

Note: Diesel's type system can make this refactoring complex. The current duplication is acceptable if extracting the common logic proves too difficult.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 777381b and 07c1620.

📒 Files selected for processing (13)
  • .github/workflows/ci.yaml
  • Cargo.toml
  • crates/soar-cli/src/list.rs
  • crates/soar-cli/src/state.rs
  • crates/soar-cli/src/use.rs
  • crates/soar-core/src/database/connection.rs
  • crates/soar-core/src/package/install.rs
  • crates/soar-db/src/lib.rs
  • crates/soar-db/src/models/core.rs
  • crates/soar-db/src/repository/core.rs
  • crates/soar-db/src/repository/metadata.rs
  • crates/soar-registry/src/lib.rs
  • crates/soar-registry/src/metadata.rs
🧰 Additional context used
🧬 Code graph analysis (4)
crates/soar-core/src/package/install.rs (2)
crates/soar-core/src/database/models.rs (12)
  • repo_name (93-95)
  • repo_name (139-141)
  • pkg_id (85-87)
  • pkg_id (131-133)
  • pkg_name (81-83)
  • pkg_name (127-129)
  • version (89-91)
  • version (135-137)
  • from (146-172)
  • from (177-203)
  • from (208-264)
  • from (269-273)
crates/soar-db/src/repository/core.rs (6)
  • insert (349-354)
  • record_installation (369-401)
  • from (54-80)
  • upsert_portable (485-520)
  • unlink_others (422-439)
  • find_alternates (301-316)
crates/soar-db/src/models/core.rs (1)
crates/soar-db/src/models/metadata.rs (1)
  • build (88-128)
crates/soar-db/src/repository/metadata.rs (2)
crates/soar-db/src/models/metadata.rs (1)
  • new (140-145)
crates/soar-db/src/models/types.rs (1)
  • from_string (29-55)
crates/soar-cli/src/state.rs (5)
crates/soar-core/src/utils.rs (1)
  • get_nests_db_conn (98-104)
crates/soar-db/src/connection.rs (7)
  • core (181-183)
  • metadata (188-190)
  • conn (82-84)
  • open (33-53)
  • new (144-158)
  • nests (198-200)
  • open_metadata (70-79)
crates/soar-registry/src/metadata.rs (3)
  • fetch_metadata (246-325)
  • fetch_nest_metadata (89-168)
  • write_metadata_db (421-430)
crates/soar-db/src/repository/metadata.rs (4)
  • import_packages (378-399)
  • update_repo_metadata (76-84)
  • exists_by_pkg_id (370-375)
  • get_repo_etag (68-73)
crates/soar-core/src/package/install.rs (1)
  • new (60-113)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: coverage
🔇 Additional comments (24)
Cargo.toml (2)

3-12: LGTM! Clean workspace restructure.

The workspace reorganization to a crates/ subdirectory follows best practices for multi-crate Rust projects, and the addition of soar-db aligns with the Diesel-based migration described in the PR summary.

Also applies to: 45-52


64-64: Verify the intentional change from speed to size optimization.

The opt-level has been changed from "3" (optimize for speed) to "z" (optimize for binary size). This trade-off prioritizes smaller binaries at the potential cost of runtime performance.

Please confirm this change is intentional and that any performance impact on CLI operations has been evaluated.

crates/soar-registry/src/metadata.rs (2)

112-129: LGTM: ETag handling logic is sound.

The use of unwrap_or_default() provides a clean fallback, and the cache validation logic correctly handles both empty and non-empty ETags in conjunction with the force flag and sync interval checks.


266-282: LGTM: Consistent ETag handling.

The implementation mirrors fetch_nest_metadata and correctly handles the optional ETag parameter with proper cache validation logic.

crates/soar-registry/src/lib.rs (1)

22-23: No action required—the documentation example is correct and all callers have been updated.

The existing_etag parameter was introduced in the initial crate creation with the matching call site already updated in crates/soar-cli/src/state.rs. There are no other callers to update. The function example in lib.rs correctly demonstrates usage with this parameter.

crates/soar-cli/src/use.rs (2)

16-37: LGTM!

The early return when no packages are found is appropriate and prevents unnecessary processing.


121-143: Past issue resolved: empty vector check added.

The condition now correctly checks !pkg.is_empty() before calling integrate_package, preventing the vacuous truth issue flagged in previous reviews.

crates/soar-cli/src/state.rs (2)

136-176: LGTM!

The repository initialization follows the same solid concurrent pattern as nest syncing, with appropriate error handling and separation of fetch/validate concerns.


178-232: LGTM! Package replacement logic is well-implemented.

The validation correctly identifies when installed packages have been replaced in the repository metadata and updates the core DB accordingly. The user-facing logging is informative.

crates/soar-cli/src/list.rs (3)

33-168: LGTM! Search limit is appropriately configurable.

The search limit uses a three-tier fallback: user-provided limit → config setting → default of 20. This is flexible and well-designed.


170-373: LGTM!

The query function provides comprehensive package details with good formatting. The conditional display of optional fields keeps output clean.


499-678: LGTM!

The fold operation efficiently computes installed/broken counts and sizes in a single pass. The display logic clearly distinguishes between healthy and broken installations.

crates/soar-core/src/package/install.rs (3)

59-113: LGTM!

The package installation initialization correctly uses the repository pattern with a clean transaction-based insert.


248-268: Past issue resolved: proper error handling for failed insert.

The ok_or_else pattern (lines 263-268) correctly replaces the previous unwrap_or(0) approach, ensuring the function fails explicitly when package record insertion fails rather than silently proceeding with package_id=0.


279-312: LGTM!

The path resolution closure elegantly handles empty strings and preserves the distinction between absolute and relative paths before persisting portable metadata.

crates/soar-core/src/database/connection.rs (3)

31-38: Past issue resolved: correct method used for metadata DB.

The code now correctly uses DbConnection::open_metadata(path) instead of the generic open() method, ensuring only the JSON-to-JSONB migration is applied rather than full migrations.


49-53: Past issue resolved: proper error propagation for mutex lock.

The conn() method now returns Result and properly propagates PoisonError instead of panicking on mutex lock failure.


89-179: LGTM!

The MetadataManager provides a clean abstraction for querying across multiple repository databases. The various query methods (query_all, query_all_flat, query_repo, find_first) cover common use cases well.

crates/soar-db/src/repository/metadata.rs (2)

485-496: Past issue resolved: regex compiled once using OnceLock.

The regex is now declared as a static OnceLock (lines 10-11) and initialized once via get_or_init() (line 487), eliminating the performance issue of compiling on every call.


406-420: Provides filtering logic is correct. The three conditions—exact name match, recurse_provides flag, and package name followed by a delimiter—correctly handle the intended filtering. Edge cases (e.g., foobar vs foo==bar vs foo-bar) are properly handled by the delimiter check.

crates/soar-db/src/repository/core.rs (4)

13-51: LGTM! Well-structured data models.

The data structures are clear and well-organized. The InstalledPackageWithPortable struct effectively represents the joined result of package and portable configuration data.


422-439: Verify unlink_others logic matches intent.

The filter on lines 432-434 unlinks packages where pkg_id != keep_pkg_id OR version != keep_version. This means only packages matching BOTH keep_pkg_id AND keep_version remain linked, which appears correct for keeping a single specific package version linked.

Confirm this matches the expected behavior.


463-526: LGTM! Portable configuration methods are well-implemented.

The portable package configuration management methods correctly handle CRUD operations, and the upsert_portable method uses the proper update-then-insert pattern for manual upserts.


528-588: LGTM! Cleanup methods correctly identify and remove old versions.

Both get_old_package_paths and delete_old_packages correctly identify the latest version by ID (descending order) and exclude pinned packages from cleanup. The logic is sound.

Comment on lines +369 to +401
pub fn record_installation(
conn: &mut SqliteConnection,
repo_name: &str,
pkg_name: &str,
pkg_id: &str,
version: &str,
size: i64,
provides: Option<Vec<PackageProvide>>,
with_pkg_id: bool,
checksum: Option<&str>,
installed_date: &str,
) -> QueryResult<Option<i32>> {
let provides = provides.map(|v| serde_json::to_value(v).unwrap_or_default());
diesel::update(
packages::table
.filter(packages::repo_name.eq(repo_name))
.filter(packages::pkg_name.eq(pkg_name))
.filter(packages::pkg_id.eq(pkg_id))
.filter(packages::pinned.eq(false))
.filter(packages::version.eq(version)),
)
.set((
packages::size.eq(size),
packages::installed_date.eq(installed_date),
packages::is_installed.eq(true),
packages::provides.eq(provides),
packages::with_pkg_id.eq(with_pkg_id),
packages::checksum.eq(checksum),
))
.returning(packages::id)
.get_result(conn)
.optional()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Handle JSON serialization failure explicitly.

Line 381 uses unwrap_or_default() on JSON serialization, which silently converts failures to null. This can result in data loss if PackageProvide fails to serialize. Consider returning a Result or logging the error.

🔎 Proposed fix to propagate serialization error
-    pub fn record_installation(
+    pub fn record_installation(
         conn: &mut SqliteConnection,
         repo_name: &str,
         pkg_name: &str,
         pkg_id: &str,
         version: &str,
         size: i64,
         provides: Option<Vec<PackageProvide>>,
         with_pkg_id: bool,
         checksum: Option<&str>,
         installed_date: &str,
-    ) -> QueryResult<Option<i32>> {
-        let provides = provides.map(|v| serde_json::to_value(v).unwrap_or_default());
+    ) -> Result<Option<i32>, Box<dyn std::error::Error>> {
+        let provides = provides
+            .map(|v| serde_json::to_value(v))
+            .transpose()?;
         diesel::update(
             packages::table
                 .filter(packages::repo_name.eq(repo_name))
                 .filter(packages::pkg_name.eq(pkg_name))
                 .filter(packages::pkg_id.eq(pkg_id))
                 .filter(packages::pinned.eq(false))
                 .filter(packages::version.eq(version)),
         )
         .set((
             packages::size.eq(size),
             packages::installed_date.eq(installed_date),
             packages::is_installed.eq(true),
             packages::provides.eq(provides),
             packages::with_pkg_id.eq(with_pkg_id),
             packages::checksum.eq(checksum),
         ))
         .returning(packages::id)
         .get_result(conn)
-        .optional()
+        .optional()
+        .map_err(Into::into)
     }

Alternatively, if you want to keep the current signature, at minimum log the serialization error before defaulting.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn record_installation(
conn: &mut SqliteConnection,
repo_name: &str,
pkg_name: &str,
pkg_id: &str,
version: &str,
size: i64,
provides: Option<Vec<PackageProvide>>,
with_pkg_id: bool,
checksum: Option<&str>,
installed_date: &str,
) -> QueryResult<Option<i32>> {
let provides = provides.map(|v| serde_json::to_value(v).unwrap_or_default());
diesel::update(
packages::table
.filter(packages::repo_name.eq(repo_name))
.filter(packages::pkg_name.eq(pkg_name))
.filter(packages::pkg_id.eq(pkg_id))
.filter(packages::pinned.eq(false))
.filter(packages::version.eq(version)),
)
.set((
packages::size.eq(size),
packages::installed_date.eq(installed_date),
packages::is_installed.eq(true),
packages::provides.eq(provides),
packages::with_pkg_id.eq(with_pkg_id),
packages::checksum.eq(checksum),
))
.returning(packages::id)
.get_result(conn)
.optional()
}
pub fn record_installation(
conn: &mut SqliteConnection,
repo_name: &str,
pkg_name: &str,
pkg_id: &str,
version: &str,
size: i64,
provides: Option<Vec<PackageProvide>>,
with_pkg_id: bool,
checksum: Option<&str>,
installed_date: &str,
) -> Result<Option<i32>, Box<dyn std::error::Error>> {
let provides = provides
.map(|v| serde_json::to_value(v))
.transpose()?;
diesel::update(
packages::table
.filter(packages::repo_name.eq(repo_name))
.filter(packages::pkg_name.eq(pkg_name))
.filter(packages::pkg_id.eq(pkg_id))
.filter(packages::pinned.eq(false))
.filter(packages::version.eq(version)),
)
.set((
packages::size.eq(size),
packages::installed_date.eq(installed_date),
packages::is_installed.eq(true),
packages::provides.eq(provides),
packages::with_pkg_id.eq(with_pkg_id),
packages::checksum.eq(checksum),
))
.returning(packages::id)
.get_result(conn)
.optional()
.map_err(Into::into)
}
🤖 Prompt for AI Agents
crates/soar-db/src/repository/core.rs lines 369-401: the call to
serde_json::to_value(...).unwrap_or_default() silently swallows serialization
errors; replace the unwrap_or_default with explicit error propagation by mapping
the serde_json::Error into the function's QueryResult error type (e.g. convert
to diesel::result::Error::SerializationError or otherwise return Err(...))
before performing the diesel::update so the call returns an Err on serialization
failure; alternatively, if you must keep the same signature, log the serde_json
error and return an appropriate diesel::result::Error instead of defaulting to
null.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/soar-cli/src/utils.rs (1)

244-250: Relative path "SOAR_SYMS" may resolve incorrectly.

Path::new("SOAR_SYMS") creates a relative path that depends on the current working directory. If the CWD differs from expectations, this could check the wrong location. Consider making it relative to install_dir:

🔎 Proposed fix
-        let soar_syms = Path::new("SOAR_SYMS");
+        let soar_syms = install_dir.join("SOAR_SYMS");
         let (is_syms, binaries_dir) = if soar_syms.is_dir() {
-            (true, soar_syms)
+            (true, soar_syms.as_path())
         } else {
             (false, install_dir)
         };
♻️ Duplicate comments (4)
crates/soar-core/src/package/remove.rs (1)

94-98: Potential integer truncation when casting package.id from u64 to i32.

The cast self.package.id as i32 silently truncates if the ID exceeds i32::MAX. This was flagged in a previous review and remains unaddressed.

🔎 Recommended fix using try_into()
-        let package_id = self.package.id as i32;
+        let package_id: i32 = self.package.id.try_into()
+            .map_err(|_| crate::SoarError::Custom(format!("Package ID {} exceeds i32 range", self.package.id)))?;
         self.db.transaction(|conn| {
crates/soar-cli/src/inspect.rs (1)

111-116: Potential panic on chained unwrap() calls.

The .unwrap() on line 113 will panic if select_package_interactively returns Ok(None) (e.g., user provides invalid input). This was flagged in a previous review and remains unaddressed.

🔎 Suggested fix with graceful handling
         let selected_pkg = if packages.len() > 1 {
-            &select_package_interactively(packages, &query.name.unwrap_or(package.to_string()))?
-                .unwrap()
+            match select_package_interactively(packages, &query.name.clone().unwrap_or(package.to_string()))? {
+                Some(pkg) => pkg,
+                None => {
+                    error!("No package selected");
+                    return Ok(());
+                }
+            }
         } else {
-            packages.first().unwrap()
+            packages.into_iter().next().unwrap() // safe: len checked above
         };

Note: The packages.first().unwrap() on line 115 is safe because we reach this branch only when packages.len() == 1.

crates/soar-cli/src/install.rs (1)

229-237: Potential panic if user cancels interactive selection.

The select_package_interactively returns an Option that is unwrapped on line 234. If the user cancels the selection, this will panic.

crates/soar-db/src/repository/core.rs (1)

381-382: JSON serialization error is silently converted to null.

The unwrap_or_default() on line 382 silently converts serialization failures to null, potentially causing data loss. While PackageProvide serialization is unlikely to fail, this masks any issues.

🧹 Nitpick comments (16)
crates/soar-db/src/connection.rs (4)

32-34: Consider stricter path validation.

Using to_string_lossy() may silently replace invalid UTF-8 sequences in the path with replacement characters, which could lead to attempting to open an incorrect database file. If database paths are expected to be valid UTF-8, consider using to_str() and returning an error for invalid UTF-8, or document this behavior explicitly.

🔎 Proposed alternative using stricter validation
-        let path_str = path.as_ref().to_string_lossy();
+        let path_str = path
+            .as_ref()
+            .to_str()
+            .ok_or_else(|| ConnectionError::BadConnection("Invalid UTF-8 in database path".into()))?;
         let mut conn = SqliteConnection::establish(&path_str)?;

36-49: Error context is lost in conversion to BadConnection.

Mapping all migration and query errors to ConnectionError::BadConnection(e.to_string()) loses the original error type information, making it harder to diagnose issues or handle specific error cases in calling code. While this approach provides a consistent error type, consider whether preserving more error context (e.g., via custom error types or error chaining) would be valuable for debugging migration or WAL pragma failures.


67-84: WAL mode not enabled for metadata databases.

Similar to open_without_migrations, this method doesn't enable WAL mode. If concurrent access to metadata databases is expected, WAL mode should be enabled here as well for consistency with the open method's "better concurrent access" goal.

🔎 Proposed fix to enable WAL mode
 pub fn open_metadata<P: AsRef<Path>>(path: P) -> Result<Self, ConnectionError> {
     let path_str = path.as_ref().to_string_lossy();
     let mut conn = SqliteConnection::establish(&path_str)?;

+    // WAL mode for better concurrent access
+    sql_query("PRAGMA journal_mode = WAL;")
+        .execute(&mut conn)
+        .map_err(|e| ConnectionError::BadConnection(e.to_string()))?;
+
     // Migrate text JSON to JSONB binary format
     migrate_json_to_jsonb(&mut conn, DbType::Metadata)
         .map_err(|e| ConnectionError::BadConnection(e.to_string()))?;

113-127: Documentation example may be misleading.

The example uses ignore and shows method calls like list_installed() and search() that don't appear to be defined on DbConnection. Consider using no_run instead of ignore and ensuring the example code accurately reflects the actual API, or simplify it to show only connection management operations.

crates/soar-db/src/repository/nest.rs (1)

61-65: Consider the check-then-act pattern implications for exists_by_name.

The exists_by_name method is correctly implemented. However, if it's used in a check-then-insert pattern (e.g., "if not exists, insert"), be aware that there's a potential TOCTOU (time-of-check to time-of-use) race condition without proper transaction isolation or unique constraints.

This is addressed if the database schema enforces uniqueness on the name field, which would cause the insert to fail with a constraint violation instead of silently creating duplicates.

crates/soar-cli/src/state.rs (2)

31-49: Consider extracting duplicated database file removal logic.

The handle_json_metadata function removes an existing metadata file before opening a new connection. This pattern is reasonable, but the error mapping uses different styles (with_context for fs operations vs manual map_err for db operations). Consider using consistent error handling patterns.


241-270: Consider logging errors instead of silently ignoring them.

Multiple if let Ok(...) patterns silently swallow errors when loading repository metadata (lines 245, 253-255). While graceful degradation is good, silent failures can make debugging difficult when repositories fail to load.

🔎 Proposed approach

Consider logging warnings when errors occur:

-        if let Ok(mut nests_db) = get_nests_db_conn() {
-            if let Ok(nests) = NestRepository::list_all(nests_db.conn()) {
+        match get_nests_db_conn() {
+            Ok(mut nests_db) => {
+                match NestRepository::list_all(nests_db.conn()) {
+                    Ok(nests) => {
+                        // existing logic
+                    }
+                    Err(e) => tracing::warn!("Failed to list nests: {e}"),
+                }
+            }
+            Err(e) => tracing::warn!("Failed to connect to nests database: {e}"),
+        }
crates/soar-db/src/error.rs (1)

56-66: Generic "Record not found" message may lose context.

The diesel::result::Error::NotFound is mapped to a generic message. Consider whether callers should wrap this error with more context about which entity was not found, or if this is acceptable as a low-level error.

crates/soar-cli/src/use.rs (2)

42-68: Consider simplifying the linked/unlinked status indicator.

The logic on lines 63-67 is a bit convoluted with the double negation pattern using .then() and .unwrap_or_else().

🔎 Suggested simplification
-            package
-                .unlinked
-                .then(String::new)
-                .unwrap_or_else(|| format!(" {}", Colored(Red, "*")))
+            if package.unlinked { String::new() } else { format!(" {}", Colored(Red, "*")) }

88-89: Discarding the result of mangle_package_symlinks may hide failures.

The let _ = pattern explicitly discards both the success value and any details about what was mangled. If symlink creation fails silently here, the user might not realize the package isn't fully linked.

Consider logging a warning if the operation succeeds but returns unexpected results, or remove let _ = if the return value isn't needed (the ? already propagates errors).

🔎 Proposed fix
-    let _ = mangle_package_symlinks(&install_dir, &bin_dir, selected_package.provides.as_deref())
-        .await?;
+    mangle_package_symlinks(&install_dir, &bin_dir, selected_package.provides.as_deref())
+        .await?;
crates/soar-cli/src/health.rs (1)

116-143: The Rc<RefCell<Vec>> pattern is overly complex for this use case.

Since walk_dir takes &mut F and the closures don't escape the function, you can use a simpler mutable Vec directly. The current approach with Rc::try_unwrap and fallback cloning adds unnecessary complexity.

🔎 Simplified implementation
 fn get_broken_symlinks() -> SoarResult<Vec<std::path::PathBuf>> {
-    let broken_symlinks = Rc::new(RefCell::new(Vec::new()));
-
-    let broken_symlinks_clone = Rc::clone(&broken_symlinks);
-    let mut collect_action = |path: &Path| -> FileSystemResult<()> {
-        if !path.exists() {
-            broken_symlinks_clone.borrow_mut().push(path.to_path_buf());
-        }
-        Ok(())
-    };
-
-    let mut soar_files_action = |path: &Path| -> FileSystemResult<()> {
-        if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
-            if filename.ends_with("-soar") && !path.exists() {
-                broken_symlinks_clone.borrow_mut().push(path.to_path_buf());
-            }
-        }
-        Ok(())
-    };
-
-    walk_dir(&get_config().get_bin_path()?, &mut collect_action)?;
-    walk_dir(desktop_dir(), &mut soar_files_action)?;
-    walk_dir(icons_dir(), &mut soar_files_action)?;
-
-    Ok(Rc::try_unwrap(broken_symlinks)
-        .unwrap_or_else(|rc| rc.borrow().clone().into())
-        .into_inner())
+    let mut broken_symlinks = Vec::new();
+
+    let mut collect_action = |path: &Path| -> FileSystemResult<()> {
+        if !path.exists() {
+            broken_symlinks.push(path.to_path_buf());
+        }
+        Ok(())
+    };
+    walk_dir(&get_config().get_bin_path()?, &mut collect_action)?;
+
+    let mut soar_files_action = |path: &Path| -> FileSystemResult<()> {
+        if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
+            if filename.ends_with("-soar") && !path.exists() {
+                broken_symlinks.push(path.to_path_buf());
+            }
+        }
+        Ok(())
+    };
+    walk_dir(desktop_dir(), &mut soar_files_action)?;
+    walk_dir(icons_dir(), &mut soar_files_action)?;
+
+    Ok(broken_symlinks)
 }

Note: The closures need to be defined before each walk_dir call since collect_action borrows broken_symlinks mutably and must end before soar_files_action is defined.

crates/soar-cli/src/update.rs (3)

82-122: Consider extracting duplicated update target creation logic.

The logic for finding newer versions and creating InstallTarget structs is nearly identical in both branches (lines 82-122 and 131-171). Consider extracting this into a helper function to reduce duplication.

🔎 Suggested helper function
fn create_update_target(
    pkg: &InstalledPackage,
    new_pkg: Package,
    diesel_db: &DieselDatabase,
) -> SoarResult<Option<InstallTarget>> {
    let existing_install = get_existing(&new_pkg, diesel_db)?;
    if let Some(ref existing) = existing_install {
        if existing.is_installed {
            return Ok(None);
        }
    }

    Ok(Some(InstallTarget {
        package: new_pkg,
        existing_install,
        with_pkg_id: pkg.with_pkg_id,
        pinned: pkg.pinned,
        profile: Some(pkg.profile.clone()),
        portable: pkg.portable_path.clone(),
        portable_home: pkg.portable_home.clone(),
        portable_config: pkg.portable_config.clone(),
        portable_share: pkg.portable_share.clone(),
        portable_cache: pkg.portable_cache.clone(),
    }))
}

Also applies to: 131-171


340-342: Consider logging errors from remove_old_versions instead of silently ignoring.

The let _ = pattern silently discards any errors from remove_old_versions. While failing to clean up old versions shouldn't block the update, logging these errors would aid debugging orphaned files.

🔎 Proposed fix
                     if !keep {
-                        let _ = remove_old_versions(&target.package, &diesel_db);
+                        if let Err(e) = remove_old_versions(&target.package, &diesel_db) {
+                            tracing::warn!("Failed to remove old versions: {e}");
+                        }
                     }

Apply similarly to line 354.

Also applies to: 353-355


299-299: Potential panic if semaphore is closed.

acquire_owned().await.unwrap() will panic if the semaphore is closed. While unlikely in normal operation, consider using expect with a descriptive message or handling the error gracefully.

🔎 Proposed fix
-    let permit = ctx.semaphore.clone().acquire_owned().await.unwrap();
+    let permit = ctx.semaphore.clone().acquire_owned().await
+        .expect("semaphore should not be closed during update");
crates/soar-cli/src/install.rs (2)

183-222: Repeated metadata query patterns could be consolidated.

The pattern for querying metadata (with repo-specific vs. all-repos logic) is repeated multiple times. Consider extracting a helper to reduce duplication and improve maintainability.

Also applies to: 262-301


593-593: Same semaphore unwrap concern as in update.rs.

Consider using expect with a descriptive message for consistency.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07c1620 and 603cdce.

📒 Files selected for processing (18)
  • crates/soar-cli/src/health.rs
  • crates/soar-cli/src/inspect.rs
  • crates/soar-cli/src/install.rs
  • crates/soar-cli/src/progress.rs
  • crates/soar-cli/src/state.rs
  • crates/soar-cli/src/update.rs
  • crates/soar-cli/src/use.rs
  • crates/soar-cli/src/utils.rs
  • crates/soar-core/src/error.rs
  • crates/soar-core/src/package/remove.rs
  • crates/soar-core/src/package/update.rs
  • crates/soar-db/src/connection.rs
  • crates/soar-db/src/error.rs
  • crates/soar-db/src/repository/core.rs
  • crates/soar-db/src/repository/metadata.rs
  • crates/soar-db/src/repository/nest.rs
  • crates/soar-utils/src/error.rs
  • crates/soar-utils/src/fs.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/soar-utils/src/fs.rs
  • crates/soar-core/src/package/update.rs
🧰 Additional context used
🧬 Code graph analysis (10)
crates/soar-db/src/error.rs (5)
crates/soar-db/src/repository/core.rs (1)
  • from (54-80)
crates/soar-core/src/error.rs (1)
  • from (161-163)
crates/soar-utils/src/error.rs (1)
  • from (273-339)
crates/soar-core/src/database/models.rs (4)
  • from (146-172)
  • from (177-203)
  • from (208-264)
  • from (269-273)
crates/soar-db/src/repository/metadata.rs (1)
  • diesel (199-201)
crates/soar-cli/src/utils.rs (2)
crates/soar-config/src/config.rs (2)
  • get_config (123-135)
  • display (414-416)
crates/soar-utils/src/fs.rs (1)
  • is_elf (306-311)
crates/soar-cli/src/health.rs (6)
crates/soar-config/src/config.rs (2)
  • get_config (123-135)
  • display (414-416)
crates/soar-core/src/package/remove.rs (1)
  • remove (30-101)
crates/soar-utils/src/fs.rs (1)
  • walk_dir (172-206)
crates/soar-utils/src/path.rs (2)
  • desktop_dir (141-143)
  • icons_dir (146-148)
crates/soar-db/src/repository/core.rs (1)
  • list_broken (147-157)
crates/soar-registry/src/package.rs (1)
  • s (32-32)
crates/soar-db/src/connection.rs (1)
crates/soar-db/src/migration.rs (2)
  • apply_migrations (25-40)
  • migrate_json_to_jsonb (73-147)
crates/soar-utils/src/error.rs (1)
crates/soar-utils/src/fs.rs (1)
  • create_symlink (121-142)
crates/soar-cli/src/progress.rs (2)
crates/soar-config/src/config.rs (1)
  • display (414-416)
crates/soar-cli/src/utils.rs (1)
  • display_settings (51-53)
crates/soar-core/src/package/remove.rs (2)
crates/soar-core/src/package/install.rs (1)
  • new (60-113)
crates/soar-db/src/repository/core.rs (2)
  • delete_portable (524-527)
  • delete (459-461)
crates/soar-db/src/repository/nest.rs (2)
crates/soar-db/src/connection.rs (2)
  • nests (203-205)
  • conn (87-89)
crates/soar-db/src/repository/core.rs (5)
  • list_all (88-90)
  • find_by_id (256-262)
  • find_by_name (280-285)
  • insert (350-355)
  • delete (459-461)
crates/soar-cli/src/inspect.rs (6)
crates/soar-cli/src/utils.rs (3)
  • display_settings (51-53)
  • interactive_ask (63-76)
  • select_package_interactively (107-125)
crates/soar-cli/src/progress.rs (1)
  • create_spinner (54-71)
crates/soar-db/src/repository/core.rs (2)
  • find_exact (174-192)
  • from (54-80)
crates/soar-cli/src/state.rs (1)
  • new (63-73)
crates/soar-core/src/package/query.rs (1)
  • try_from (20-67)
crates/soar-db/src/repository/metadata.rs (1)
  • find_filtered (301-335)
crates/soar-db/src/repository/metadata.rs (2)
crates/soar-db/src/models/metadata.rs (1)
  • new (140-145)
crates/soar-db/src/models/types.rs (1)
  • from_string (29-55)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: coverage
🔇 Additional comments (58)
crates/soar-db/src/connection.rs (4)

1-14: LGTM!

The module documentation clearly describes the database types and their purposes. Imports are appropriate for the functionality provided.


86-104: LGTM!

The accessor methods and Deref/DerefMut trait implementations provide flexible and ergonomic access to the underlying SqliteConnection. This is a standard Rust pattern for wrapper types.


137-163: LGTM!

The new method correctly initializes the core and nests database connections with proper migration support. The metadata HashMap starting empty is appropriate since repositories are added dynamically.


165-216: LGTM with a note on metadata DB replacement behavior.

The API design is clean and provides flexible access to database connections. Note that add_metadata_db will silently replace any existing metadata database with the same repository name due to HashMap::insert behavior. If this is intended, consider documenting it; otherwise, you might want to return the old connection or error on duplicate names.

crates/soar-db/src/repository/nest.rs (6)

1-8: LGTM!

The module documentation and imports are clear and appropriate for a Diesel-based repository.


10-11: LGTM!

Using a unit struct for namespacing repository methods is an idiomatic Rust pattern and consistent with the codebase.


14-26: LGTM!

Both methods follow standard Diesel query patterns and are consistent with the CoreRepository implementation.


51-59: LGTM!

Both delete methods follow standard Diesel patterns and are consistent with the CoreRepository implementation.


13-66: Add update methods to NestRepository for complete CRUD support, or document why nests are immutable.

Diesel supports CRUD operations which include "Create Read Update Delete". The repository currently lacks update functionality. If nests are intentionally immutable after creation, this should be explicitly documented. Otherwise, callers requiring modifications would need to delete and re-insert, which is inefficient and doesn't preserve the original ID.


28-44: The name and url fields have UNIQUE constraints defined at the column level in the database schema, making the Option<Nest> return type correct and the implementation safe.

crates/soar-cli/src/state.rs (4)

51-73: LGTM!

Clean use of Arc<AppStateInner> with OnceCell for lazy initialization of database connections. The pattern allows thread-safe sharing while deferring expensive operations.


136-175: LGTM!

The parallel metadata fetch with sequential result processing is appropriate. Error handling correctly distinguishes between task join failures and metadata fetch failures.


177-229: LGTM!

Good implementation of package replacement detection. The logic properly checks if an installed package still exists in metadata and attempts to find and apply replacements when needed.


89-99: The id cast from i32 to i64 is safe and requires no changes.

The nest.id field is an i32, and casting i32 to i64 is a safe widening conversion that cannot truncate or misrepresent values. No issues exist here.

Likely an incorrect or invalid review comment.

crates/soar-db/src/error.rs (2)

6-54: LGTM!

Well-designed error enum with comprehensive diagnostic annotations. Each variant has appropriate error codes and helpful user-facing guidance. The #[from] attribute on IoError simplifies conversions.


68-75: LGTM!

Standard pattern for connection error conversion and result type alias.

crates/soar-core/src/error.rs (3)

10-138: LGTM!

Excellent enhancement to error handling with diagnostic metadata. The #[diagnostic(transparent)] for wrapped errors and explicit codes/help for other variants provide good user-facing diagnostics. The pattern is consistent across all variants.


140-157: LGTM!

The root_cause method provides appropriate unwrapping for errors that have meaningful source chains.


166-185: LGTM!

Clean implementation of the ErrorContext trait. This provides an ergonomic way to add action context to IO errors, similar to patterns in anyhow/eyre.

crates/soar-utils/src/error.rs (5)

8-64: LGTM!

Well-structured error types with comprehensive diagnostic annotations. The BytesError, HashError, and PathError enums provide clear, actionable error messages with appropriate help text.


66-191: LGTM!

Comprehensive FileSystemError enum covering all common filesystem operations. Good distinction between IO errors (with #[source]) and validation errors (NotFound, NotADirectory, NotAFile) that don't wrap underlying errors.


193-270: LGTM!

Clean implementation of IoContext and IoOperation. The helper constructors provide an ergonomic API for creating context, and create_symlink correctly captures both source and target paths.


272-354: LGTM!

The From implementation correctly maps each IoOperation variant to the corresponding FileSystemError. The IoResultExt trait provides a clean extension method for adding path context to IO results.


378-443: LGTM!

Tests properly verify the error message formatting and have been updated to match the new single-quoted path format.

crates/soar-cli/src/utils.rs (2)

28-59: LGTM!

Clean utility additions. The Icons struct provides centralized icon constants, and the helper functions (icon_or, display_settings, term_width) are simple and focused. The fallback terminal width of 80 is a reasonable default.


291-309: LGTM!

Good validation logic with a clear, actionable error message that lists valid repository options for the current platform.

crates/soar-cli/src/progress.rs (4)

5-14: LGTM: Clean import organization and spinner constant.

The new imports for ConfigProgressStyle and display_settings are appropriately added, and the SPINNER_CHARS constant provides a nice set of Unicode spinner frames for visual feedback.


16-21: LGTM: Good refactor to delegate style creation.

Extracting the style selection logic to get_progress_style() improves separation of concerns and makes create_progress_bar() cleaner.


23-52: LGTM: Well-structured progress style selection.

The get_progress_style() function cleanly maps configuration options to distinct visual styles. Each variant is appropriately configured with its own template, progress characters, and the shared format_bytes key.


54-71: LGTM: Good spinner implementation with configuration awareness.

The create_spinner function respects the spinners() display setting, providing a graceful fallback to a plain message template when spinners are disabled. The tick duration of 80ms provides smooth animation.

crates/soar-cli/src/use.rs (3)

1-17: LGTM: Appropriate imports for the use-alternate flow.

The imports cover all necessary dependencies for database access, package models, integration utilities, and user interaction.


120-142: Good fix: Empty package check now guards integrate_package.

The condition !pkg.is_empty() && pkg.iter().all(has_desktop_integration) correctly prevents the vacuous truth issue from the previous review. The logic flow between desktop integration and portable setup is clear.


144-158: LGTM: Transaction-wrapped link operation with clear success logging.

The link operation is properly wrapped in a transaction, and the success message provides clear feedback about which package was activated.

crates/soar-core/src/package/remove.rs (2)

8-12: LGTM: Clean import updates for Diesel migration.

The imports correctly bring in DieselDatabase, CoreRepository, and ProvideStrategy to support the new repository-based deletion flow.


17-28: LGTM: Constructor updated for DieselDatabase.

The struct and constructor properly accept DieselDatabase instead of the previous Arc<Mutex<Connection>>, aligning with the Diesel migration.

crates/soar-cli/src/inspect.rs (4)

1-22: LGTM: Updated imports for Diesel-based access and spinner utilities.

The imports correctly bring in the new repository types, spinner utilities, and display settings for the refactored implementation.


38-58: LGTM: Clean migration to CoreRepository for installed path lookup.

The function properly uses DieselDatabase.with_conn() and CoreRepository::find_exact() to query the installed package, with appropriate handling of the optional result.


67-106: LGTM: Well-structured metadata query with repo-aware mapping.

The branching logic correctly handles both repo-specific and global queries, properly mapping the results to Package objects with enriched repo_name fields. The query_all_flat pattern for cross-repo queries is clean.


165-177: LGTM: Good spinner integration with configuration awareness.

The spinner is conditionally created based on display settings and properly cleared after the fetch completes. The Option pattern handles the disabled case cleanly.

crates/soar-cli/src/health.rs (3)

1-21: LGTM: Comprehensive imports for health monitoring.

The imports cover all necessary dependencies for table rendering, filesystem operations, configuration access, and styled output.


23-102: LGTM: Well-structured health display with clear remediation guidance.

The display_health() function provides a clean overview of system state with actionable commands for fixing issues. The table formatting is consistent and the conditional detail sections are helpful.


145-168: LGTM: Clean broken package removal flow.

The function properly iterates over broken packages, removes each one using PackageRemover, and provides clear logging. The .clone() on diesel_db is appropriate since it's passed to each PackageRemover instance.

crates/soar-cli/src/update.rs (3)

1-28: LGTM on import reorganization.

The imports are properly structured for the Diesel-based refactoring, correctly importing DieselDatabase, repository types, and UI utilities.


30-45: LGTM on get_existing function.

The function correctly uses CoreRepository::find_exact through the DieselDatabase connection wrapper and properly handles the optional result conversion.


200-289: LGTM on perform_update function.

The async orchestration, error collection, and summary display logic are well-structured. The icon-enabled table rendering provides good UX enhancement.

crates/soar-cli/src/install.rs (3)

377-456: LGTM on select_package function.

The function properly handles all cases: no packages found, single package (auto-select), and multiple packages (interactive or auto-select with yes flag).


458-584: LGTM on perform_installation function.

The installation orchestration, progress tracking, and summary display are well-implemented. The icon-enabled table output provides consistent UX with the update flow.


643-888: LGTM on install_single_package function.

The function properly handles existing installations, signature verification, checksum validation, and integration. Error handling is appropriate throughout.

crates/soar-db/src/repository/metadata.rs (5)

1-23: LGTM on module structure and imports.

The use of OnceLock for the MAINTAINER_RE regex is the correct approach for lazy static initialization without external dependencies.


340-367: LGTM on find_newer_version with corrected indexing.

The Rust slice [14..] and SQL substr(version, 15) now correctly align to extract the same substring from HEAD-prefixed versions. The version comparison logic is sound.


377-399: LGTM on import_packages transactional import.

The transaction ensures atomicity - either all packages are imported or none. The empty etag on upsert is appropriate as it will be updated separately after successful import.


461-479: LGTM on conflict handling and maintainer linking.

The early return on inserted == 0 correctly prevents calling last_insert_id() when no row was inserted due to conflict. Maintainer linking only occurs for newly inserted packages.


485-496: LGTM on extract_name_and_contact with cached regex.

The OnceLock pattern correctly initializes the regex once and reuses it for all subsequent calls, addressing the previous performance concern.

crates/soar-db/src/repository/core.rs (5)

248-253: Improved distinct count with null byte separator.

Using '\x00' (null byte) as a separator is a good improvement over simple concatenation. This character is extremely unlikely to appear in pkg_id or pkg_name, effectively eliminating collision risk.


25-81: LGTM on InstalledPackageWithPortable struct and conversion.

The struct properly combines package data with optional portable configuration. The From implementation correctly handles the optional portable fields.


86-171: LGTM on list functions with portable joins.

The list_filtered, list_broken, and list_updatable functions correctly use left_join to include optional portable configuration. The filtering and sorting logic is well-implemented.


485-521: LGTM on upsert_portable implementation.

The update-then-insert pattern is a valid approach for SQLite upsert when the conflict columns differ from the update columns. The logic correctly falls back to insert when no row is updated.


529-589: LGTM on old package cleanup functions.

Both get_old_package_paths and delete_old_packages correctly identify the latest package by ID and exclude it from cleanup. The pinned filter ensures user-pinned packages are preserved.

Comment on lines +46 to +49
/// Inserts a new nest.
pub fn insert(conn: &mut SqliteConnection, nest: &NewNest) -> QueryResult<usize> {
diesel::insert_into(nests::table).values(nest).execute(conn)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Align insert return type with CoreRepository for API consistency.

The insert method returns QueryResult<usize> (row count), while CoreRepository::insert returns QueryResult<i32> (the inserted ID). Returning the ID is generally more useful and maintains consistency across repository interfaces.

🔎 Proposed fix to return the inserted ID
-    /// Inserts a new nest.
-    pub fn insert(conn: &mut SqliteConnection, nest: &NewNest) -> QueryResult<usize> {
-        diesel::insert_into(nests::table).values(nest).execute(conn)
+    /// Inserts a new nest and returns its ID.
+    pub fn insert(conn: &mut SqliteConnection, nest: &NewNest) -> QueryResult<i32> {
+        diesel::insert_into(nests::table)
+            .values(nest)
+            .returning(nests::id)
+            .get_result(conn)
     }
🤖 Prompt for AI Agents
In crates/soar-db/src/repository/nest.rs around lines 46-49, change the insert
function to return the inserted row ID (QueryResult<i32>) instead of the
affected row count: update the signature to pub fn insert(conn: &mut
SqliteConnection, nest: &NewNest) -> QueryResult<i32>, perform the insert as
before, then fetch and return the last inserted row id using Diesel's
last_insert_rowid (e.g. run the insert, then
diesel::select(diesel::dsl::last_insert_rowid).get_result::<i64>(conn) and cast
to i32) — wrap both operations in a transaction or propagate errors so you
return QueryResult<i32> of the inserted ID.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
crates/soar-db/src/migration.rs (2)

141-144: Wrap migration queries in a transaction for atomicity.

The function executes multiple UPDATE queries sequentially without a transaction boundary. If a query fails mid-way (e.g., due to malformed JSON), earlier queries have already committed, leaving some columns migrated while others remain as text, creating an inconsistent database state.

🔎 Proposed fix: Add transaction boundary
+use diesel::connection::Connection;
+
 pub fn migrate_json_to_jsonb(
     conn: &mut SqliteConnection,
     db_type: DbType,
 ) -> Result<usize, Box<dyn Error + Send + Sync + 'static>> {
+    conn.transaction(|conn| {
         // Check for text type OR blob containing text JSON (starts with '[' or '{')
         // Use hex comparison for blobs: 5B = '[', 7B = '{'
         let json_condition = |col: &str| {
             format!(
                 "{col} IS NOT NULL AND (typeof({col}) = 'text' OR (typeof({col}) = 'blob' AND hex(substr({col}, 1, 1)) IN ('5B', '7B')))"
             )
         };
     
         // ... query building logic ...
     
         let mut total = 0;
         for query in queries {
             total += sql_query(&query).execute(conn)?;
         }
     
         Ok(total)
+    })
 }

141-144: Add context to migration errors for easier debugging.

If a query fails (e.g., due to invalid JSON), the error provides no indication of which column or query caused the failure. When migrating 9+ columns, this makes troubleshooting difficult.

🔎 Proposed improvement: Add error context
     let mut total = 0;
-    for query in queries {
-        total += sql_query(&query).execute(conn)?;
+    for (idx, query) in queries.iter().enumerate() {
+        total += sql_query(query)
+            .execute(conn)
+            .map_err(|e| format!("Migration query #{} failed: {}\nQuery: {}", idx, e, query))?;
     }
crates/soar-cli/src/state.rs (1)

316-328: Previous race condition concern is addressed.

The metadata_manager method now correctly uses tokio::sync::OnceCell with the initialization logic (init_repo_dbs, sync_nests, create_metadata_manager) inside the get_or_try_init async closure. This ensures only one task performs initialization while others await.

crates/soar-cli/src/install.rs (1)

230-238: Potential panic if user cancels package selection.

The code unwraps the Option returned by select_package_interactively, which could panic if the user cancels the selection. This was flagged in a previous review but remains unaddressed.

🔎 Proposed fix
                 let pkg = if repo_pkgs.len() > 1 {
-                    &select_package_interactively(
+                    match select_package_interactively(
                         repo_pkgs,
                         &query.name.unwrap_or(package.clone()),
-                    )?
-                    .unwrap()
+                    )? {
+                        Some(pkg) => pkg,
+                        None => continue, // User cancelled selection
+                    }
                 } else {
                     repo_pkgs.first().unwrap()
                 };
🧹 Nitpick comments (10)
crates/soar-utils/src/error.rs (2)

193-270: Clean abstraction for IO operation context.

The IoContext and IoOperation types provide excellent structure for capturing filesystem operation context. The comprehensive set of constructor methods makes the API ergonomic and type-safe.

💡 Optional: Consider adding a `path()` accessor

While the private fields provide good encapsulation, a public path() getter (similar to the existing operation() getter) could be useful for debugging or logging purposes:

 impl IoContext {
     // ... existing methods ...

     pub fn operation(&self) -> &IoOperation {
         &self.operation
     }
+
+    pub fn path(&self) -> &Path {
+        &self.path
+    }
 }

382-447: Tests verify core error message formatting.

The test suite covers key error variants and confirms the display messages are correct. While the derives handle most functionality automatically, the tests provide confidence in the expected user-facing messages.

💡 Optional: Consider expanding test coverage

For more comprehensive validation, you could add tests for:

  • Diagnostic codes using miette::Diagnostic trait methods
  • Help messages
  • Error source chaining (verify std::error::Error::source() returns the expected nested error)
  • UtilsError conversions via ? operator or .into()

However, since the derives handle most of this automatically, the current tests are adequate.

crates/soar-db/src/connection.rs (1)

55-70: Clarify documentation for open_without_migrations() regarding JSON migration.

The method docs mention skipping schema migrations but don't explicitly state that migrate_json_to_jsonb() is also skipped. Since the codebase provides open_metadata() specifically for handling JSON migration separately, consider updating the documentation to clarify that callers using open_without_migrations() must ensure the database is fully migrated (both schema and JSON conversion already applied). Alternatively, if this method is intended for databases where JSON columns may still be in text format, JSON migration should be performed as well.

crates/soar-cli/src/health.rs (2)

116-143: Simplify: Rc<RefCell> is unnecessary here.

The Rc<RefCell> pattern adds complexity without benefit since this function is synchronous and single-threaded. A simple mutable Vec would suffice.

Additionally, the symlink detection logic may be incorrect: !path.exists() returns false for a symlink whose target doesn't exist (broken symlink), but walk_dir iterates over directory entries, not symlink targets. Consider using path.symlink_metadata() to check if it's a symlink, then path.exists() to check if the target exists.

🔎 Proposed simplification
 fn get_broken_symlinks() -> SoarResult<Vec<std::path::PathBuf>> {
-    let broken_symlinks = Rc::new(RefCell::new(Vec::new()));
-
-    let broken_symlinks_clone = Rc::clone(&broken_symlinks);
-    let mut collect_action = |path: &Path| -> FileSystemResult<()> {
-        if !path.exists() {
-            broken_symlinks_clone.borrow_mut().push(path.to_path_buf());
+    let mut broken_symlinks = Vec::new();
+
+    let mut collect_action = |path: &Path| -> FileSystemResult<()> {
+        // Check if it's a symlink with a non-existent target
+        if path.symlink_metadata().map(|m| m.file_type().is_symlink()).unwrap_or(false)
+            && !path.exists()
+        {
+            broken_symlinks.push(path.to_path_buf());
         }
         Ok(())
     };

-    let mut soar_files_action = |path: &Path| -> FileSystemResult<()> {
+    let mut soar_files_action = |path: &Path| -> FileSystemResult<()> {
         if let Some(filename) = path.file_stem().and_then(|s| s.to_str()) {
-            if filename.ends_with("-soar") && !path.exists() {
-                broken_symlinks_clone.borrow_mut().push(path.to_path_buf());
+            if filename.ends_with("-soar")
+                && path.symlink_metadata().map(|m| m.file_type().is_symlink()).unwrap_or(false)
+                && !path.exists()
+            {
+                broken_symlinks.push(path.to_path_buf());
             }
         }
         Ok(())
     };

     walk_dir(&get_config().get_bin_path()?, &mut collect_action)?;
     walk_dir(desktop_dir(), &mut soar_files_action)?;
     walk_dir(icons_dir(), &mut soar_files_action)?;

-    Ok(Rc::try_unwrap(broken_symlinks)
-        .unwrap_or_else(|rc| rc.borrow().clone().into())
-        .into_inner())
+    Ok(broken_symlinks)
 }

104-114: Consider accepting AppState as a parameter to avoid redundant initialization.

Both get_broken_packages and remove_broken_packages create a new AppState instance. If these functions are called after display_health (which likely has its own AppState), this results in redundant initialization of database connections.

Consider refactoring to accept &AppState as a parameter for reuse:

-async fn get_broken_packages() -> SoarResult<Vec<(String, String, String)>> {
-    let state = AppState::new();
+async fn get_broken_packages(state: &AppState) -> SoarResult<Vec<(String, String, String)>> {
     let diesel_db = state.diesel_core_db()?;

Also applies to: 145-147

crates/soar-cli/src/state.rs (2)

254-268: Consider logging errors instead of silently ignoring them.

The nested if let Ok(...) patterns silently swallow errors from get_nests_db_conn(), NestRepository::list_all(), and get_repositories_path(). This can make debugging difficult when nest metadata fails to load.

Consider logging at debug/warn level to aid troubleshooting:

-        if let Ok(mut nests_db) = get_nests_db_conn() {
-            if let Ok(nests) = NestRepository::list_all(nests_db.conn()) {
+        match get_nests_db_conn() {
+            Ok(mut nests_db) => match NestRepository::list_all(nests_db.conn()) {
+                Ok(nests) => {
                     // ... existing logic ...
+                }
+                Err(e) => tracing::debug!("Failed to list nests: {e}"),
+            },
+            Err(e) => tracing::debug!("Failed to open nests DB: {e}"),
+        }

32-50: Consider atomic file replacement for metadata database.

The current pattern removes the existing file before creating the new one. If the process crashes between removal and successful write, the metadata file will be missing. For improved robustness, consider writing to a temporary file first, then renaming:

let temp_path = metadata_db.with_extension("db.tmp");
// ... write to temp_path ...
fs::rename(&temp_path, metadata_db)?;

This is a minor concern since metadata can be re-synced, but worth considering for reliability.

crates/soar-cli/src/list.rs (2)

60-66: Consider filtering installed packages query for better performance.

The code fetches all installed packages into a HashMap to check installation status. For users with many installed packages, this could be inefficient. Consider filtering by the package keys that are actually in the search results, or using a more targeted query.


534-592: Complex fold pattern is functional but could benefit from clarity.

The fold accumulates multiple counters and conditionally updates them based on installation status. While correct, consider extracting the per-package processing into a helper function for improved readability.

🔎 Suggested refactor for clarity
+    struct PackageStats {
+        installed_count: u32,
+        unique_count: u32,
+        broken_count: u32,
+        installed_size: u64,
+        broken_size: u64,
+    }
+
     let (installed_count, unique_count, broken_count, installed_size, broken_size) =
-        packages.iter().fold(
-            (0, 0, 0, 0, 0),
-            |(installed_count, unique_count, broken_count, installed_size, broken_size),
-             package| {
-                // ... complex logic
-            },
-        );
+        packages.iter().fold(
+            PackageStats::default(),
+            |mut stats, package| {
+                // Update stats...
+                stats
+            },
+        );
crates/soar-cli/src/update.rs (1)

340-355: Consider logging cleanup failures instead of silently ignoring.

Both occurrences of remove_old_versions use let _ = to discard the result, which silently ignores any cleanup failures. While this shouldn't block the update, logging a warning would aid debugging.

🔎 Proposed improvement
                     if !keep {
-                        let _ = remove_old_versions(&target.package, &diesel_db);
+                        if let Err(e) = remove_old_versions(&target.package, &diesel_db) {
+                            warn!("Failed to remove old versions: {}", e);
+                        }
                     }

Apply similarly to line 354.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 603cdce and 73fad60.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • Cargo.toml
  • crates/soar-cli/Cargo.toml
  • crates/soar-cli/src/health.rs
  • crates/soar-cli/src/install.rs
  • crates/soar-cli/src/list.rs
  • crates/soar-cli/src/state.rs
  • crates/soar-cli/src/update.rs
  • crates/soar-core/Cargo.toml
  • crates/soar-db/src/connection.rs
  • crates/soar-db/src/migration.rs
  • crates/soar-package/Cargo.toml
  • crates/soar-utils/src/error.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/soar-cli/Cargo.toml
🧰 Additional context used
🧬 Code graph analysis (5)
crates/soar-cli/src/list.rs (6)
crates/soar-cli/src/state.rs (2)
  • config (274-276)
  • new (64-74)
crates/soar-utils/src/fs.rs (1)
  • dir_size (263-284)
crates/soar-cli/src/utils.rs (4)
  • icon_or (43-49)
  • pretty_package_size (134-139)
  • term_width (55-59)
  • vec_string (93-95)
crates/soar-core/src/database/models.rs (12)
  • repo_name (93-95)
  • repo_name (139-141)
  • pkg_name (81-83)
  • pkg_name (127-129)
  • pkg_id (85-87)
  • pkg_id (131-133)
  • version (89-91)
  • version (135-137)
  • from (146-172)
  • from (177-203)
  • from (208-264)
  • from (269-273)
crates/soar-db/src/repository/metadata.rs (4)
  • search_case_sensitive (166-187)
  • search (141-163)
  • find_filtered (301-335)
  • count (206-208)
crates/soar-db/src/repository/core.rs (4)
  • list_filtered (94-144)
  • count (229-231)
  • count_distinct_installed (234-253)
  • from (54-80)
crates/soar-cli/src/update.rs (4)
crates/soar-core/src/package/update.rs (1)
  • remove_old_versions (15-38)
crates/soar-db/src/repository/core.rs (3)
  • find_exact (174-192)
  • list_filtered (94-144)
  • list_updatable (160-171)
crates/soar-core/src/package/remove.rs (1)
  • new (23-28)
crates/soar-db/src/repository/metadata.rs (1)
  • find_newer_version (340-367)
crates/soar-db/src/connection.rs (1)
crates/soar-db/src/migration.rs (3)
  • sql_query (48-49)
  • apply_migrations (25-40)
  • migrate_json_to_jsonb (73-147)
crates/soar-cli/src/health.rs (6)
crates/soar-cli/src/state.rs (2)
  • config (274-276)
  • new (64-74)
crates/soar-config/src/config.rs (3)
  • get_config (123-135)
  • new (238-256)
  • display (414-416)
crates/soar-utils/src/fs.rs (1)
  • walk_dir (172-206)
crates/soar-utils/src/path.rs (2)
  • desktop_dir (141-143)
  • icons_dir (146-148)
crates/soar-cli/src/utils.rs (2)
  • icon_or (43-49)
  • term_width (55-59)
crates/soar-db/src/repository/core.rs (1)
  • list_broken (147-157)
crates/soar-utils/src/error.rs (1)
crates/soar-utils/src/fs.rs (1)
  • create_symlink (121-142)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: coverage
  • GitHub Check: test
🔇 Additional comments (27)
crates/soar-utils/src/error.rs (7)

1-17: Excellent use of thiserror and miette for structured error handling.

The derive-based approach with diagnostic codes and contextual help messages significantly improves error reporting quality. The BytesError implementation is clean and provides actionable guidance.


19-32: LGTM! Proper error chaining with #[source].

The use of #[source] ensures the underlying std::io::Error is preserved in the error chain, enabling full diagnostic context.


34-64: Well-designed PathError with comprehensive variant coverage.

The four variants cover common path operation failures with appropriate diagnostic codes and helpful guidance. The distinction between validation errors (Empty, UnclosedVariable) and runtime errors (FailedToGetCurrentDir, MissingEnvVar) is clear.


66-191: Comprehensive FileSystemError with rich diagnostic metadata.

The enum covers all major filesystem operations with consistent error messages and helpful guidance. The special handling of CreateSymlink with separate from and target fields provides clear context when symlink creation fails.


272-340: LGTM! Exhaustive and correct error conversion.

The From implementation properly maps each IoOperation variant to its corresponding FileSystemError, preserving both the path context and the underlying std::io::Error. The special handling of CreateSymlink correctly captures both the source path and symlink target.


342-354: Excellent extension trait for ergonomic error handling.

The IoResultExt trait enables clean, chainable error context attachment: std::fs::operation().with_path(path, operation)?. This pattern significantly improves code readability at call sites.


356-380: Perfect! HashError variant properly integrated.

The UtilsError enum now correctly includes all error types (Bytes, Path, FileSystem, Hash) with transparent diagnostic forwarding. The #[from] attributes enable seamless error conversion throughout the utils crate. This addresses the concern from the previous review.

crates/soar-db/src/migration.rs (1)

10-10: LGTM: Appropriate derives for the enum.

The Clone and Copy derives are suitable for this simple enum, making it more ergonomic to pass around without borrowing.

crates/soar-db/src/connection.rs (4)

32-53: LGTM: Well-structured initialization with proper migration handling.

The method correctly:

  • Establishes the connection
  • Enables WAL mode for concurrency
  • Applies schema migrations
  • Selectively runs JSON-to-JSONB migration only for Core and Nest databases

The conditional migration (lines 43-48) appropriately excludes Metadata databases, which use the dedicated open_metadata method instead.


72-94: LGTM: Appropriate metadata database handling.

This method correctly:

  • Enables WAL mode for concurrent access
  • Skips schema migrations (as documented, since metadata schemas are externally managed)
  • Performs JSON-to-JSONB migration to handle externally-generated databases

The approach aligns well with the documented use case of metadata databases generated by rusqlite.


102-114: LGTM: Standard wrapper pattern.

The Deref and DerefMut implementations allow DbConnection to be used transparently as a SqliteConnection, which is a common and appropriate pattern for wrapper types.


116-226: LGTM: Well-designed database orchestration.

The DatabaseManager provides a clean API for managing multiple database connections:

  • Clear separation between core, metadata, and nests databases
  • Appropriate use of HashMap for per-repository metadata connections
  • Good ergonomics with iterator methods (all_metadata, metadata_names)
  • Comprehensive documentation with usage examples

The design makes it easy to manage the lifecycle of multiple database connections.

crates/soar-package/Cargo.toml (1)

14-14: LGTM - minor patch version bump.

The image crate update from 0.25.8 to 0.25.9 is a routine patch version bump. Features remain unchanged.

Cargo.toml (2)

63-67: Verify the opt-level change from 3 to "z" is intentional.

Changing opt-level from 3 (maximum speed) to "z" (minimum size) prioritizes binary size reduction over runtime performance. This can noticeably slow down hot paths, especially in CPU-bound operations like package processing or compression.

If binary size is a priority (e.g., for distribution), this is appropriate. Otherwise, consider keeping opt-level = 3 or using "s" for a balanced approach.


3-12: LGTM - clean workspace reorganization.

The workspace member paths and path dependencies are correctly updated to reflect the crates/ directory structure. This improves project organization.

Also applies to: 45-52

crates/soar-cli/src/health.rs (1)

23-102: LGTM - well-structured health check display.

The function provides clear, actionable output with color-coded status indicators and remediation guidance. The table rendering with tabled is appropriate for this use case.

crates/soar-core/Cargo.toml (1)

13-29: LGTM - clean migration to workspace-managed dependencies.

The shift to { workspace = true } for shared dependencies ensures version consistency across crates. The addition of soar-db aligns with the Diesel-based repository pattern introduced in this PR.

crates/soar-cli/src/state.rs (1)

76-176: LGTM - well-structured async sync with concurrent fetching.

The pattern of spawning tasks for parallel metadata fetches and then awaiting/processing results is appropriate. Error handling for join errors and fetch failures is properly logged.

crates/soar-cli/src/list.rs (2)

170-373: LGTM!

The query_package function is well-structured with proper handling of repo-specific vs. cross-repo queries, comprehensive metadata display, and good use of optional field handling. The structured logging captures all relevant package details.


380-403: Good improvement: Using list_all instead of hardcoded pagination.

The previous hardcoded pagination limit of 3000 has been replaced with MetadataRepository::list_all, which properly retrieves all packages without arbitrary limits.

crates/soar-cli/src/update.rs (3)

30-45: LGTM!

The get_existing function is cleanly migrated to use Diesel-based CoreRepository::find_exact. The parameter ordering matches the repository signature and the result is properly converted.


236-286: LGTM!

The update summary rendering properly handles both icon and text modes, with appropriate handling of the edge case where no packages were updated and none failed.


82-97: Verify find_newer_version correctly handles HEAD version comparisons.

The implementation in find_newer_version (lines 347-357 of crates/soar-db/src/repository/metadata.rs) correctly handles HEAD-prefixed versions by extracting a suffix starting at position 14 of the version string and using it for lexicographic comparison. The query filters for either regular versions greater than the current version, or HEAD versions with a greater suffix. The logic is sound and aligns with the expected versioning scheme.

crates/soar-cli/src/install.rs (4)

86-99: LGTM!

The create_install_context function properly initializes the progress bar with conditional styling based on display settings.


378-457: LGTM!

The select_package function properly handles all three lookup cases (existing install, repo-specific, all repos) and correctly returns Option<Package> with appropriate handling for 0, 1, or multiple results.


499-530: LGTM!

The per-package installation output is well-formatted with clear binary paths, notes display, and proper icon usage based on display settings.


644-889: LGTM!

The install_single_package function is comprehensive with proper error handling for:

  • Directory cleanup before installation
  • Signature verification with minisign
  • Checksum validation with appropriate warnings
  • Desktop integration and package recording

@QaidVoid QaidVoid merged commit 2d340e5 into main Dec 22, 2025
5 of 7 checks passed
This was referenced Dec 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants