Skip to content

refactor to use service and repository pattern - #493

Merged
awoimbee merged 4 commits into
mainfrom
aw/service-repo-pattern
Jul 9, 2026
Merged

refactor to use service and repository pattern#493
awoimbee merged 4 commits into
mainfrom
aw/service-repo-pattern

Conversation

@awoimbee

@awoimbee awoimbee commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

This adds a lot of boilerplate but makes working with the project way easier and nicer.

Summary by CodeRabbit

  • New Features

    • Added support for blob, manifest, tag, catalog, referrers, health, readiness, admission, and upload operations through a new service-backed architecture.
    • Improved registry proxy handling, including remote image fetching and caching.
  • Bug Fixes

    • Fixed health and readiness responses to reflect current storage status.
    • Improved manifest and upload workflows for more consistent behavior and error handling.
    • Updated catalog, blob, and referrers endpoints to return results from the new backend flow.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The registry module, init_db, and TrowStorageBackend are removed and replaced by FileStorage, a new repositories module wrapping read/write SQLite pools with domain models, and a services module (admission, health, catalog, blob, blob-upload, manifest, referrers, proxy, gc) that route handlers now delegate to via TrowServerState.services.

Changes

Service-Oriented Architecture Refactor

Layer / File(s) Summary
FileStorage backend rename
src/file_storage.rs
TrowStorageBackend is renamed to FileStorage, Stored is now defined locally, and tests are renamed accordingly.
Repositories data layer
src/repositories/*, src/test_utilities.rs
New domain models and repository types (blob, blob_upload, manifest, tag, repo_blob_assoc) wrap read/write SQLite pools via Repositories, plus in-memory test helpers.
Core services
src/services/error.rs, src/services/mod.rs, src/services/admission_service.rs, src/services/health_service.rs, src/services/catalog_service.rs
A shared Error enum and Services struct assemble AdmissionService, HealthService, and CatalogService over repositories/storage.
Blob and blob-upload services
src/services/blob_service.rs, src/services/blob_upload_service.rs
BlobService streams blob reads and BlobUploadService manages chunked upload lifecycle against repositories and storage.
Manifest, referrers, and proxy services
src/services/manifest_service.rs, src/services/referrers_service.rs, src/services/proxy_service/*
ManifestService handles get/put/delete manifest logic, ReferrersService lists OCI referrers, and ProxyService/oci_client implement remote image downloads and OCI/ECR auth.
Garbage collection service
src/services/gc_service.rs
GcService runs a watchdog loop reclaiming space by deleting stale uploads, orphan blobs, and old proxied images.
Server state and app wiring
src/lib.rs
TrowServerState now holds Arc<Services>; build_server_state constructs FileStorage, Repositories, and Services; build_app spawns GC via services.gc.watchdog().
Route handlers delegate to services
src/routes/*, src/utils/*, tests/smoke_test_proxy.rs
Blob, blob-upload, catalog, health, manifest, referrers, readiness, and admission routes delegate to state.services; response error conversions and tests are updated accordingly.
SQLx metadata updates
.sqlx/*.json
SQLite query cache entries are added, removed, or re-specified to match the new repository/service queries.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant BlobUploadRoute
  participant BlobUploadService
  participant Repositories
  participant FileStorage
  Client->>BlobUploadRoute: PATCH /blob_upload
  BlobUploadRoute->>BlobUploadService: patch_upload(repo, uuid, range, data)
  BlobUploadService->>FileStorage: write_blob_stream(part)
  BlobUploadService->>Repositories: update_offset(uuid, offset)
  BlobUploadService-->>BlobUploadRoute: UploadInfo
  BlobUploadRoute-->>Client: 202 Accepted
Loading
sequenceDiagram
  participant ManifestRoute
  participant ManifestService
  participant ProxyService
  participant Repositories
  ManifestRoute->>ManifestService: get_manifest(repo, reference, namespace)
  alt non-localhost registry
    ManifestService->>ProxyService: download_image(reference, proxy_config)
    ProxyService->>Repositories: insert manifest and associations
    ProxyService-->>ManifestService: digest
  else localhost
    ManifestService->>Repositories: resolve tag or digest
  end
  ManifestService-->>ManifestRoute: ManifestPayload
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main refactor to a service and repository architecture.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch aw/service-repo-pattern

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a major architectural refactoring to separate concerns by establishing a dedicated Repository layer for database metadata access and a Service layer for business logic orchestration. Direct database queries are moved out of route handlers into specialized repositories, and services are introduced to manage operations like blob uploads, manifest handling, and garbage collection. The feedback highlights several critical issues: first, multiple read-only database queries incorrectly use the write connection pool (which is limited to a single connection), risking connection starvation; second, a panic risk exists in the upload service due to an assertion on user-provided repository names; and third, leaky database abstractions returning nested options should be simplified to return booleans, which would significantly clean up conditional checks in the service layer.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/repositories/repo_blob_assoc_repository.rs
Comment thread src/repositories/blob_repository.rs
Comment thread src/repositories/manifest_repository.rs
Comment thread src/services/blob_upload_service.rs Outdated
Comment thread src/repositories/repo_blob_assoc_repository.rs Outdated
Comment thread src/repositories/repo_blob_assoc_repository.rs Outdated
Comment thread src/services/proxy_service/mod.rs
Comment thread src/services/manifest_service.rs Outdated
Comment thread src/services/manifest_service.rs Outdated
Comment thread src/services/manifest_service.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (4)
src/routes/manifest.rs (1)

51-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant Ok(... ?) wrapping.

Both handlers wrap Ok(expr.await?) while the function already returns Result<_, Error>. This trips Clippy's needless_question_mark, which fails the build under -D warnings. You can return the future's result directly.

♻️ Proposed simplification
-    Ok(state
-        .services
-        .manifest
-        .put_manifest(repo_name, reference, host, body)
-        .await?)
+    state
+        .services
+        .manifest
+        .put_manifest(repo_name, reference, host, body)
+        .await
-    Ok(state
-        .services
-        .manifest
-        .delete_manifest(repo, reference)
-        .await?)
+    state
+        .services
+        .manifest
+        .delete_manifest(repo, reference)
+        .await

Also applies to: 72-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/manifest.rs` around lines 51 - 55, The manifest route handlers are
redundantly wrapping awaited results in Ok(...?), which triggers Clippy’s
needless_question_mark in the handler functions. Update the manifest endpoint
returns in the handler(s) around put_manifest and the other matching route to
return the awaited service result directly from state.services.manifest instead
of wrapping it in Ok, keeping the Result<_, Error> signature intact.
src/test_utilities.rs (1)

36-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deduplicate repos_in_memory by reusing pool_in_memory.

Both helpers construct identical SqliteConnectOptions, create a pool with max_connections(1), and run migrations. repos_in_memory can delegate to pool_in_memory and wrap the result, eliminating the duplicated setup logic.

♻️ Proposed refactor
 pub async fn repos_in_memory() -> Arc<Repositories> {
-    let options = SqliteConnectOptions::new()
-        .filename(":memory:")
-        .synchronous(SqliteSynchronous::Normal)
-        .journal_mode(SqliteJournalMode::Wal)
-        .foreign_keys(true);
-
-    let pool = SqlitePoolOptions::new()
-        .max_connections(1)
-        .connect_with(options)
-        .await
-        .unwrap();
-
-    sqlx::migrate!().run(&pool).await.unwrap();
+    let pool = pool_in_memory().await;
     Arc::new(Repositories::from_pools(pool.clone(), pool))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test_utilities.rs` around lines 36 - 51, The repos_in_memory helper
duplicates the same SQLite setup and migration logic already handled by
pool_in_memory. Refactor repos_in_memory to call pool_in_memory() instead of
rebuilding SqliteConnectOptions, creating the pool, and running sqlx::migrate!
again, then wrap the returned pool in Repositories::from_pools so the behavior
stays the same while removing duplicated setup code.
src/repositories/tag_repository.rs (1)

66-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Specify column names explicitly in the INSERT statement.

INSERT INTO tag VALUES ($1, $2, $3) relies on the table's implicit column order. If a column is added or reordered in a future migration, this could silently insert values into wrong columns. The inline comment already acknowledges the non-obvious parameter mapping, which is a sign this should be more explicit.

♻️ Proposed refactor
             INSERT INTO tag
+                (tag, repo, manifest_digest)
             VALUES ($1, $2, $3)
             ON CONFLICT (repo, tag) DO UPDATE
                 SET manifest_digest = EXCLUDED.manifest_digest
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/repositories/tag_repository.rs` around lines 66 - 76, The INSERT in
tag_repository::insert_tag relies on implicit table column order, so update the
sqlx::query! statement to name the target columns explicitly and keep the
existing repo/tag/manifest_digest mapping clear. Use the same tag_repository
code path and the ON CONFLICT (repo, tag) handling, but make the INSERT clause
list the columns so future schema changes do not break the parameter ordering.
src/repositories/models.rs (1)

39-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider deriving Clone for ManifestReferrer.

All other domain models in this file derive Clone, but ManifestReferrer only derives Debug, FromRow. If any service or route handler needs to clone referrer results (e.g., for retry logic or parallel processing), the missing Clone will surface as a compile error. Adding it now keeps the models consistent.

♻️ Proposed refactor
-#[derive(Debug, FromRow)]
+#[derive(Debug, Clone, FromRow)]
 pub struct ManifestReferrer {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/repositories/models.rs` around lines 39 - 44, ManifestReferrer is missing
Clone while the other models in models.rs already derive it, so update the
ManifestReferrer definition to include Clone alongside Debug and FromRow. Keep
the change consistent with the surrounding domain structs in this file so any
code using ManifestReferrer for retries, parallel processing, or duplication can
clone it without compile errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/repositories/blob_repository.rs`:
- Around line 70-78: The blob existence check in the `BlobRepository::exists`
method is using the write pool instead of the read pool. Update this read-only
query to fetch from `db_ro` so concurrent `exists` calls don’t contend with
writes, matching the other read methods in `BlobRepository` and keeping the
`exists` logic otherwise unchanged.
- Around line 81-85: The sum_size method in BlobRepository should handle empty
tables because SQLite returns NULL for SUM on no rows, which breaks callers like
compute_space_to_reclaim. Update the query in sum_size to use COALESCE around
SUM(b.size) so it always returns 0 when there are no blob rows, while keeping
the existing async return type and query_scalar! usage.

In `@src/repositories/blob_upload_repository.rs`:
- Around line 113-117: The `sum_offset` query in `BlobUploadRepository` will
fail on an empty table because `SUM(u.offset)` can return NULL while `as
"size!"` forces a non-null `i64` decode. Update `sum_offset` to handle the
empty-set case the same way as `BlobRepository::sum_size`, so it returns 0
instead of raising a `DecodeError`; keep the fix localized in `sum_offset` and
its `sqlx::query_scalar!` mapping.

In `@src/repositories/manifest_repository.rs`:
- Around line 87-97: The `list_manifests_using_blob` read path is incorrectly
using `self.db_rw` for a SELECT-only query, which adds avoidable contention on
the write pool. Update `ManifestRepository::list_manifests_using_blob` to fetch
from `self.db_ro` instead, matching the repository’s other read methods like
`find` and `list_referrers`, and keep the rest of the query unchanged.

In `@src/repositories/repo_blob_assoc_repository.rs`:
- Around line 56-70: The read-only EXISTS lookup in manifest_exists_in_repo is
using db_rw, which can serialize proxy downloads and contend with writes. Update
this method to query through db_ro instead, matching the other read-only
repository methods, and keep the existing manifest_digest/repo_name parameters
and SQL unchanged.

In `@src/repositories/tag_repository.rs`:
- Around line 42-53: The cursor filter in tag pagination uses a different
collation than the sort, which can skip mixed-case tags between pages. Update
the query in the tag_repository logic that fetches tags so the predicate on
t.tag uses COLLATE NOCASE to match the ORDER BY t.tag COLLATE NOCASE, keeping
the cursor comparison consistent with the sort order.

In `@src/services/blob_upload_service.rs`:
- Around line 62-90: `patch_upload` only checks `exists(&uuid_str)` and can
accept a UUID that belongs to a different repo, so the upload must be validated
against `repo_name` before writing. Update `BlobUploadService::patch_upload` to
fetch the upload using a repo-aware lookup like `find_offset_in_repo` (or `find`
plus explicit repo comparison if needed), and return an error when the UUID does
not belong to the supplied repo instead of proceeding. Keep the
`UploadInfo::new` path unchanged once the repo ownership check passes.
- Around line 92-133: In complete_upload, replace the
assert_eq!(upload_row.repo, repo_name) check with an explicit error return so a
mismatched repo does not panic; use the existing Error type and keep the
validation near the upload_row lookup before proceeding to
write_blob_part_stream, complete_blob_write, and the repo/blob association
inserts. Refer to complete_upload and upload_row.repo/repo_name when updating
the control flow so the function fails gracefully on wrong-repo UUID
submissions.

In `@src/services/catalog_service.rs`:
- Around line 18-30: The pagination limit in list_repositories currently casts
an Option<u64> to i64 with as, which can wrap for values above i64::MAX and
produce invalid SQLite LIMIT behavior. Update list_repositories to validate or
clamp the limit before calling repo_blob_assoc.list_repos, and handle oversized
values explicitly rather than relying on the cast. Keep the fix localized to
list_repositories and its use of limit.unwrap_or(...) so the repository query
always receives a safe nonnegative i64.

In `@src/services/health_service.rs`:
- Around line 36-41: The readiness check in `HealthService::readiness` always
returns an empty `ReadyStatus.message`, even when `storage.is_ready()` fails.
Update `readiness()` to capture the result from `self.storage.is_ready().await`,
and when it returns `Err`, populate `ReadyStatus.message` with the error’s
message while keeping `is_ready` false. Use the existing `ReadyStatus` and
`storage.is_ready()` flow so the success path stays unchanged.

In `@src/services/manifest_service.rs`:
- Around line 145-163: The OCIManifest::V2 validation only checks distributable
layers and misses the config blob, so extend the manifest validation in the V2
branch to also verify the config digest is present in repo_blob_assoc before
accepting the manifest. Update the existing asset-checking logic in
manifest_service::ManifestService so it iterates over both layers and the config
blob, and return the same ManifestInvalid error if the config blob is missing.
Keep the change localized to the OCIManifest::V2 handling and reuse the existing
blob_belongs_to_repo check for consistency.

In `@src/services/proxy_service/oci_client.rs`:
- Around line 59-67: The ECR token parsing in the `get_authorization_token` flow
is using chained unwraps on `authorization_data`, the first entry, and
`authorization_token`, which can panic when the response is incomplete. Update
the token extraction in the OCI client code to handle each missing case
explicitly and return an `EcrPasswordError` instead of unwrapping; add a
missing-token variant if `EcrPasswordError` does not already cover this path.

In `@src/services/referrers_service.rs`:
- Around line 37-41: The manifest digest parsing in referrers_service’s
descriptor creation currently uses Digest::from_str(...).unwrap(), which can
panic on malformed stored data; replace this with explicit parsing error
handling in the referrers lookup path. Update the logic around Descriptor::new
so it maps a failed digest parse to Error::Invalid and propagates that error
instead of unwrapping, keeping the request path safe for bad rows.

---

Nitpick comments:
In `@src/repositories/models.rs`:
- Around line 39-44: ManifestReferrer is missing Clone while the other models in
models.rs already derive it, so update the ManifestReferrer definition to
include Clone alongside Debug and FromRow. Keep the change consistent with the
surrounding domain structs in this file so any code using ManifestReferrer for
retries, parallel processing, or duplication can clone it without compile
errors.

In `@src/repositories/tag_repository.rs`:
- Around line 66-76: The INSERT in tag_repository::insert_tag relies on implicit
table column order, so update the sqlx::query! statement to name the target
columns explicitly and keep the existing repo/tag/manifest_digest mapping clear.
Use the same tag_repository code path and the ON CONFLICT (repo, tag) handling,
but make the INSERT clause list the columns so future schema changes do not
break the parameter ordering.

In `@src/routes/manifest.rs`:
- Around line 51-55: The manifest route handlers are redundantly wrapping
awaited results in Ok(...?), which triggers Clippy’s needless_question_mark in
the handler functions. Update the manifest endpoint returns in the handler(s)
around put_manifest and the other matching route to return the awaited service
result directly from state.services.manifest instead of wrapping it in Ok,
keeping the Result<_, Error> signature intact.

In `@src/test_utilities.rs`:
- Around line 36-51: The repos_in_memory helper duplicates the same SQLite setup
and migration logic already handled by pool_in_memory. Refactor repos_in_memory
to call pool_in_memory() instead of rebuilding SqliteConnectOptions, creating
the pool, and running sqlx::migrate! again, then wrap the returned pool in
Repositories::from_pools so the behavior stays the same while removing
duplicated setup code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 380d88ef-3928-42b9-9d5e-b6c9c6511e04

📥 Commits

Reviewing files that changed from the base of the PR and between 508d624 and 3cfdec5.

📒 Files selected for processing (45)
  • src/file_storage.rs
  • src/init_db.rs
  • src/lib.rs
  • src/registry/api_types.rs
  • src/registry/garbage_collect.rs
  • src/registry/mod.rs
  • src/registry/proxy/download.rs
  • src/repositories/blob_repository.rs
  • src/repositories/blob_upload_repository.rs
  • src/repositories/manifest_repository.rs
  • src/repositories/mod.rs
  • src/repositories/models.rs
  • src/repositories/repo_blob_assoc_repository.rs
  • src/repositories/tag_repository.rs
  • src/routes/admission.rs
  • src/routes/blob.rs
  • src/routes/blob_upload.rs
  • src/routes/catalog.rs
  • src/routes/health.rs
  • src/routes/manifest.rs
  • src/routes/manifest_referrers.rs
  • src/routes/readiness.rs
  • src/routes/response/blob_reader.rs
  • src/routes/response/content_info.rs
  • src/routes/response/errors.rs
  • src/routes/response/health.rs
  • src/routes/response/readiness.rs
  • src/services/admission_service.rs
  • src/services/blob_service.rs
  • src/services/blob_upload_service.rs
  • src/services/catalog_service.rs
  • src/services/error.rs
  • src/services/gc_service.rs
  • src/services/health_service.rs
  • src/services/manifest_service.rs
  • src/services/mod.rs
  • src/services/proxy_service/errors.rs
  • src/services/proxy_service/mod.rs
  • src/services/proxy_service/oci_client.rs
  • src/services/referrers_service.rs
  • src/test_utilities.rs
  • src/utils/manifest.rs
  • src/utils/mod.rs
  • src/utils/resolve_reference.rs
  • tests/smoke_test_proxy.rs
💤 Files with no reviewable changes (6)
  • src/init_db.rs
  • src/registry/proxy/download.rs
  • src/registry/garbage_collect.rs
  • src/utils/mod.rs
  • src/registry/api_types.rs
  • src/registry/mod.rs

Comment thread src/repositories/blob_repository.rs
Comment thread src/repositories/blob_repository.rs Outdated
Comment thread src/repositories/blob_upload_repository.rs Outdated
Comment thread src/repositories/manifest_repository.rs
Comment thread src/repositories/repo_blob_assoc_repository.rs
Comment thread src/services/catalog_service.rs
Comment thread src/services/health_service.rs
Comment thread src/services/manifest_service.rs
Comment thread src/services/proxy_service/oci_client.rs
Comment thread src/services/referrers_service.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/repositories/blob_repository.rs (1)

80-86: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

sum_size still fails on empty blob table — unwrap_or(0) doesn't fix the NULL issue.

SUM(b.size) returns NULL in SQLite when the table has no rows. The "size!" annotation tells sqlx the column is NOT NULL, so query_scalar! generates i64 (not Option<i64>). When sqlx tries to decode the NULL result as i64, it returns a Decode error that propagates via ?unwrap_or(0) is never reached. This causes compute_space_to_reclaim to fail on fresh installs or after GC removes all blobs.

Add COALESCE to make the column truly NOT NULL:

🔧 Proposed fix
 pub async fn sum_size(&self) -> Result<usize, sqlx::Error> {
-    let res = sqlx::query_scalar!(r#"SELECT SUM(b.size) as "size!" FROM blob b"#)
+    let res = sqlx::query_scalar!(r#"SELECT COALESCE(SUM(b.size), 0) as "size!" FROM blob b"#)
         .fetch_one(&self.db_ro)
         .await?;
     Ok(usize::try_from(res).unwrap_or(0))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/repositories/blob_repository.rs` around lines 80 - 86, `sum_size` is
still decoding a NULL aggregate result as a non-null scalar, so `unwrap_or(0)`
never runs on an empty blob table. Update `BlobRepository::sum_size` to make the
SQL itself return a non-null value by using `COALESCE` in the
`sqlx::query_scalar!` query, keeping the return type and
`compute_space_to_reclaim` path working on fresh or fully-garbage-collected
databases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/repositories/blob_repository.rs`:
- Around line 80-86: `sum_size` is still decoding a NULL aggregate result as a
non-null scalar, so `unwrap_or(0)` never runs on an empty blob table. Update
`BlobRepository::sum_size` to make the SQL itself return a non-null value by
using `COALESCE` in the `sqlx::query_scalar!` query, keeping the return type and
`compute_space_to_reclaim` path working on fresh or fully-garbage-collected
databases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f3341a82-57b6-49bf-a7a7-9c3ac9fc86de

📥 Commits

Reviewing files that changed from the base of the PR and between 3cfdec5 and 1b85c8d.

📒 Files selected for processing (42)
  • .sqlx/query-0267ad1f011779abdd531fe5a332b7b636fb017c5e935b462a00d89e12ea4fd7.json
  • .sqlx/query-04380a0bd91d3405cfd45eff6b6f2b33b001f0c5e8c3065674ddf046a51bccee.json
  • .sqlx/query-0be73f5f867ae4ec655854a33aceb202ddf91c2eeff83c630672154f309ba1a9.json
  • .sqlx/query-0d94fec6d4492cf2c8e1be688a6e31d1d3389809b63ee2b7636fa345dea58138.json
  • .sqlx/query-11ef1d69ef684e5a1f1207bea26e541640bee4cbe6785441cb86aa25141ddb9a.json
  • .sqlx/query-16ad8411febff0e01175536aa208bcbaa8e2e17a2762a21e59e9539fc13e18b5.json
  • .sqlx/query-38275e6a8bc246702e74bd27deee17d41f0be846b5dee0b4d4b1c215afe89c80.json
  • .sqlx/query-40620d77dbcd8a1543cdaefb7ef02dd372e2c18e8a2bf774a9f4d133f4b3b751.json
  • .sqlx/query-459995dfcba9b3dc20c52e1709534bc260c313951fda246dfee7fdb64be31429.json
  • .sqlx/query-552852975d7cbaf61439d8ad1b2004d8e34ff55915ebd7f418986c68792fbb91.json
  • .sqlx/query-5b2946fbbd9c1f20e7f3e1e531b9036960dbec8eab7f419c3a27ef4ca58b7382.json
  • .sqlx/query-614b5d0c6ad0e1bcef68abe4d49aada513bbd702d6074ce266eb24c5d06f427d.json
  • .sqlx/query-64ff141cd6bfaaca29cfebfe70dd061723e8925b31e6e0d2a6dbc904aa970d7a.json
  • .sqlx/query-666ee67eace7ba2e0a858716c3bd364cd1f15a15bb79652b75c252bd76fe2d79.json
  • .sqlx/query-6a3b3dd4b9aacf35b47cc6441605b16a5a24b6e314c68596d4ba52101d3efddb.json
  • .sqlx/query-70c6f6aa2d8311ad45ddd5cf4961d6d4b89f7e743809dec6bc4319a28bcfd651.json
  • .sqlx/query-77fa684dc88b571e55a7b2f309a86eacb1ca987bd0d8b0b10c995131361c49b2.json
  • .sqlx/query-7b84ccfb9acb6ee086d49e0546c3cbcfe1f9bc8a4fc0d56a0de83d0de20fde03.json
  • .sqlx/query-897f4b82a419c8da99cb707f816f25785784959f03e4b38ade987fa3903920b9.json
  • .sqlx/query-8b5c40aca05a2657eae27a4b37019ce0ff6d9f0c22da8c5f04a9abb2a0103b8c.json
  • .sqlx/query-93c30a9805ea69749c4ee8954273ff4e9fc039053c2dd6303f797e9b6d6e3464.json
  • .sqlx/query-a19a4be40736f440dc27b24cc0f76860cee67616e343d06d3704bfef8fe12328.json
  • .sqlx/query-ae5cbd2998c86a6e412cac73aac2921e117a5f39e8608133401438e32d5240af.json
  • .sqlx/query-b93961bcf07284bc32e2ad7e6fcb1ab2b08bfff393cc6116422a5afa6164e414.json
  • .sqlx/query-c18c3fdfb91cbe3245a5423c3ce21c9c155236ea902db1425b396f786547fb98.json
  • .sqlx/query-c6d231b319e1386d6d16aaaf79a1b177e27564c80cc29ac55c6cc962fb6f753b.json
  • .sqlx/query-c729e8969ca0751f30ec8738b2ef207413399cbc6a9a7b2e4ea65afe22c32547.json
  • .sqlx/query-cc2eee2c57c61963207391adad6e7f1a0f29290843161e2176f157097f91b0b6.json
  • .sqlx/query-d373abe42ea596c62d555c2ccca627381da6e2347b5a9d2590bf97dab4ee3671.json
  • .sqlx/query-dd06315416372c82898ef0e279056575d43271e5bf7895a809de88415b174da4.json
  • .sqlx/query-f53d056f0d6784eccc62730fddd8169cb79bc5c582d070fa182b49fcdc8ceecc.json
  • .sqlx/query-f5d12cf96720c56798f47641f38f39ab24a6b027a5bb919f12af3836ea6a49da.json
  • .sqlx/query-f6f2371924284a1a70e53dae12eaf1c19a372fdf38274b04f9e9f6d7929ed72a.json
  • src/repositories/blob_repository.rs
  • src/repositories/blob_upload_repository.rs
  • src/repositories/manifest_repository.rs
  • src/repositories/repo_blob_assoc_repository.rs
  • src/services/blob_upload_service.rs
  • src/services/gc_service.rs
  • src/services/health_service.rs
  • src/services/manifest_service.rs
  • src/services/proxy_service/mod.rs
💤 Files with no reviewable changes (18)
  • .sqlx/query-ae5cbd2998c86a6e412cac73aac2921e117a5f39e8608133401438e32d5240af.json
  • .sqlx/query-a19a4be40736f440dc27b24cc0f76860cee67616e343d06d3704bfef8fe12328.json
  • .sqlx/query-c6d231b319e1386d6d16aaaf79a1b177e27564c80cc29ac55c6cc962fb6f753b.json
  • .sqlx/query-16ad8411febff0e01175536aa208bcbaa8e2e17a2762a21e59e9539fc13e18b5.json
  • .sqlx/query-93c30a9805ea69749c4ee8954273ff4e9fc039053c2dd6303f797e9b6d6e3464.json
  • .sqlx/query-c18c3fdfb91cbe3245a5423c3ce21c9c155236ea902db1425b396f786547fb98.json
  • .sqlx/query-5b2946fbbd9c1f20e7f3e1e531b9036960dbec8eab7f419c3a27ef4ca58b7382.json
  • .sqlx/query-666ee67eace7ba2e0a858716c3bd364cd1f15a15bb79652b75c252bd76fe2d79.json
  • .sqlx/query-dd06315416372c82898ef0e279056575d43271e5bf7895a809de88415b174da4.json
  • .sqlx/query-04380a0bd91d3405cfd45eff6b6f2b33b001f0c5e8c3065674ddf046a51bccee.json
  • .sqlx/query-c729e8969ca0751f30ec8738b2ef207413399cbc6a9a7b2e4ea65afe22c32547.json
  • .sqlx/query-0267ad1f011779abdd531fe5a332b7b636fb017c5e935b462a00d89e12ea4fd7.json
  • .sqlx/query-552852975d7cbaf61439d8ad1b2004d8e34ff55915ebd7f418986c68792fbb91.json
  • .sqlx/query-459995dfcba9b3dc20c52e1709534bc260c313951fda246dfee7fdb64be31429.json
  • .sqlx/query-6a3b3dd4b9aacf35b47cc6441605b16a5a24b6e314c68596d4ba52101d3efddb.json
  • .sqlx/query-0d94fec6d4492cf2c8e1be688a6e31d1d3389809b63ee2b7636fa345dea58138.json
  • .sqlx/query-f5d12cf96720c56798f47641f38f39ab24a6b027a5bb919f12af3836ea6a49da.json
  • .sqlx/query-7b84ccfb9acb6ee086d49e0546c3cbcfe1f9bc8a4fc0d56a0de83d0de20fde03.json
✅ Files skipped from review due to trivial changes (7)
  • .sqlx/query-77fa684dc88b571e55a7b2f309a86eacb1ca987bd0d8b0b10c995131361c49b2.json
  • .sqlx/query-b93961bcf07284bc32e2ad7e6fcb1ab2b08bfff393cc6116422a5afa6164e414.json
  • .sqlx/query-70c6f6aa2d8311ad45ddd5cf4961d6d4b89f7e743809dec6bc4319a28bcfd651.json
  • .sqlx/query-f6f2371924284a1a70e53dae12eaf1c19a372fdf38274b04f9e9f6d7929ed72a.json
  • .sqlx/query-614b5d0c6ad0e1bcef68abe4d49aada513bbd702d6074ce266eb24c5d06f427d.json
  • .sqlx/query-f53d056f0d6784eccc62730fddd8169cb79bc5c582d070fa182b49fcdc8ceecc.json
  • .sqlx/query-64ff141cd6bfaaca29cfebfe70dd061723e8925b31e6e0d2a6dbc904aa970d7a.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/services/health_service.rs
  • src/repositories/manifest_repository.rs
  • src/services/proxy_service/mod.rs
  • src/repositories/blob_upload_repository.rs
  • src/services/blob_upload_service.rs
  • src/services/manifest_service.rs
  • src/services/gc_service.rs

@awoimbee
awoimbee merged commit ee7027f into main Jul 9, 2026
5 of 6 checks passed
@awoimbee
awoimbee deleted the aw/service-repo-pattern branch July 9, 2026 14:20
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.

1 participant