perf: index the sort and cover-lookup paths, and pin their query plans #120

Merged
bjoern merged 2 commits from perf/scale-indexes into main 2026-09-10 08:25:34 +02:00
Member

What's in

One migration, AddScaleIndexes, with four indexes; one line of DoujinOrdering; one new test harness. No config, no endpoint, no app change, no ADR.

Index Why
IX_doujins_cover_image_id cover_image_id is a soft reference with no FK, so EF never generated an index for it — and ImageService.DeleteAsync scans doujins once per image deleted, i.e. once per page when a doujin goes.
IX_doujins_updated_at Sorting the library by "last updated" read every doujin into a temp b-tree before it could return page 1.
IX_doujins_rating Same, for the rating sort — but only after the DoujinOrdering change below.
IX_titles_doujin_id_kind_value DoujinOrdering.PrimaryTitle is WHERE doujin_id = ? ORDER BY kind, value LIMIT 1. The existing IX_titles_doujin_id_language_code_kind cannot serve that: language_code sits between the two columns the subquery orders on, so SQLite seeks and then sorts each doujin's titles in its own temp b-tree. Both indexes stay; they serve different queries.

All three doujins indexes are single-column on purpose. Every sort ends in an id ASC tiebreak, so a composite (key, id) index would need a mixed-direction walk for the DESC sorts and could not serve them.

DoujinOrdering, rating DESC arm. IX_doujins_rating is inert on its own: the arm ordered by d.Rating == null first, an expression no index can serve. SQLite already sorts NULL below every value, so a plain DESC puts unrated doujins last anyway — the guard was pure cost. It is dropped from the DESC arm and kept on the ASC arm, where NULLs would otherwise sort first and EF translates no NULLS LAST. The documented nulls-last contract is unchanged in both directions.

Verification

Measured, not guessed. Real EXPLAIN QUERY PLAN output for the SQL EF Core actually emits, captured through a DbCommandInterceptor on the pinned stack.

Cover-owner lookup — ImageService.DeleteAsync

before:  SCAN d
after:   SEARCH d USING INDEX IX_doujins_cover_image_id (cover_image_id=?)

?sortBy=updated&sortDirection=desc

before:  SCAN d
         USE TEMP B-TREE FOR ORDER BY
after:   SCAN d USING INDEX IX_doujins_updated_at
         USE TEMP B-TREE FOR LAST TERM OF ORDER BY

Only the trailing id tiebreak is still sorted in memory — that is the difference between paging that touches every row and paging that does not.

?sortBy=title&sortDirection=asc — the primary-title subquery

before:  ... SEARCH t0 USING INDEX IX_titles_doujin_id_language_code_kind (doujin_id=?)
         USE TEMP B-TREE FOR ORDER BY      <- per doujin
         USE TEMP B-TREE FOR ORDER BY      <- the outer sort
after:   ... SEARCH t0 USING COVERING INDEX IX_titles_doujin_id_kind_value (doujin_id=?)
         USE TEMP B-TREE FOR ORDER BY      <- the outer sort only

?sortBy=rating&sortDirection=desc

before SQL:  ORDER BY "d"."rating" IS NULL, "d"."rating" DESC, "d"."id"
before:      SCAN d + USE TEMP B-TREE FOR ORDER BY
after SQL:   ORDER BY "d"."rating" DESC, "d"."id"
after:       SCAN d USING INDEX IX_doujins_rating
             USE TEMP B-TREE FOR LAST TERM OF ORDER BY

Rating ASC is deliberately unchanged — see Follow-ups.

Layers exercised. Persistence and use case, through the new QueryPlanTests: it records the SQL each real service call emits, runs EXPLAIN QUERY PLAN on it, and asserts plan shape. On a five-row fixture a stopwatch cannot tell a scan from a seek, so a timing assertion would be noise; the plan flips deterministically. Its context is built with Migrate(), so the schema under test is the one a deployed database gets, and Model_And_Migrations_Agree (HasPendingModelChanges) closes the other direction. REST, through the existing SortingIntegrationTests — List_Rating_Descending_Puts_Unrated_Last and its search twin are the behavioural pin for the guard-free DESC arm, and their fixture already contains an unrated doujin.

Mutation probes, each confirmed red before opening this: migration minus CreateIndex IX_doujins_updated_at → the updated-sort test fails; minus the other three → rating, title and cover-lookup all fail; DESC arm reverted to the Rating == null guard → the rating-index test fails; HasIndex(e => e.Rating) removed while the migration keeps it → Model_And_Migrations_Agree fails.

Not verified: nothing was measured on a real-sized library — the claims are about plan shape, which is size-independent, not about milliseconds. No app or assistant code is touched. deploy/ is untouched.

Follow-ups

All in #107:

  1. The pragma half of this idea — WAL, busy_timeout, synchronous, cache_size, mmap_size — is deliberately NOT here. synchronous=NORMAL is a durability trade that deserves its own ADR, and enabling WAL creates -wal/-shm sidecars that silently break DEPLOYMENT.md §6.4's "copy the backup over the db path" restore (a stale log replays onto the restored file). That is data loss in the one procedure that exists to prevent data loss, so the restore fix and an ADR 0014 amendment have to ship in the same PR as the pragmas.
  2. Rating ASC is still a full scan. Its Rating == null guard is load-bearing and is a leading sort term no index on rating can serve. Needs a persisted "unrated last" column, a partial index, or raw SQL.
  3. PRAGMA optimize / ANALYZE. No database this project has produced has a sqlite_stat1 table, so every plan above comes from the planner's default row estimates. Land it after this harness and re-measure — real statistics can change a plan.

Migration collision: #118 (feat/search-key-fold) adds its own EF migration off the same base — different tables, different columns, no index overlap. Whichever of the two merges second must regenerate its migration and DoujinManagerDbContextModelSnapshot.cs, or Model_And_Migrations_Agree will catch the drift.


Merge #117 first — main's CI is red and that PR is the fix.

Verified independently on this branch with the full scripts/verify.sh: PASS — ApplicationCore 16, Infrastructure 96, RestAdapter 493, Integration 1 (606), Flutter all green.

🤖 Generated with Claude Code

## What's in One migration, `AddScaleIndexes`, with four indexes; one line of `DoujinOrdering`; one new test harness. No config, no endpoint, no app change, no ADR. | Index | Why | |---|---| | `IX_doujins_cover_image_id` | `cover_image_id` is a soft reference with no FK, so EF never generated an index for it — and `ImageService.DeleteAsync` scans `doujins` once per image deleted, i.e. once per page when a doujin goes. | | `IX_doujins_updated_at` | Sorting the library by "last updated" read every doujin into a temp b-tree before it could return page 1. | | `IX_doujins_rating` | Same, for the rating sort — but only after the `DoujinOrdering` change below. | | `IX_titles_doujin_id_kind_value` | `DoujinOrdering.PrimaryTitle` is `WHERE doujin_id = ? ORDER BY kind, value LIMIT 1`. The existing `IX_titles_doujin_id_language_code_kind` cannot serve that: `language_code` sits between the two columns the subquery orders on, so SQLite seeks and then sorts each doujin's titles in its own temp b-tree. Both indexes stay; they serve different queries. | All three `doujins` indexes are single-column on purpose. Every sort ends in an `id` ASC tiebreak, so a composite `(key, id)` index would need a mixed-direction walk for the DESC sorts and could not serve them. **`DoujinOrdering`, rating DESC arm.** `IX_doujins_rating` is inert on its own: the arm ordered by `d.Rating == null` first, an expression no index can serve. SQLite already sorts NULL below every value, so a plain DESC puts unrated doujins last anyway — the guard was pure cost. It is dropped from the DESC arm and kept on the ASC arm, where NULLs would otherwise sort first and EF translates no `NULLS LAST`. The documented nulls-last contract is unchanged in both directions. ## Verification **Measured, not guessed.** Real `EXPLAIN QUERY PLAN` output for the SQL EF Core actually emits, captured through a `DbCommandInterceptor` on the pinned stack. *Cover-owner lookup — `ImageService.DeleteAsync`* ``` before: SCAN d after: SEARCH d USING INDEX IX_doujins_cover_image_id (cover_image_id=?) ``` *`?sortBy=updated&sortDirection=desc`* ``` before: SCAN d USE TEMP B-TREE FOR ORDER BY after: SCAN d USING INDEX IX_doujins_updated_at USE TEMP B-TREE FOR LAST TERM OF ORDER BY ``` Only the trailing `id` tiebreak is still sorted in memory — that is the difference between paging that touches every row and paging that does not. *`?sortBy=title&sortDirection=asc` — the primary-title subquery* ``` before: ... SEARCH t0 USING INDEX IX_titles_doujin_id_language_code_kind (doujin_id=?) USE TEMP B-TREE FOR ORDER BY <- per doujin USE TEMP B-TREE FOR ORDER BY <- the outer sort after: ... SEARCH t0 USING COVERING INDEX IX_titles_doujin_id_kind_value (doujin_id=?) USE TEMP B-TREE FOR ORDER BY <- the outer sort only ``` *`?sortBy=rating&sortDirection=desc`* ``` before SQL: ORDER BY "d"."rating" IS NULL, "d"."rating" DESC, "d"."id" before: SCAN d + USE TEMP B-TREE FOR ORDER BY after SQL: ORDER BY "d"."rating" DESC, "d"."id" after: SCAN d USING INDEX IX_doujins_rating USE TEMP B-TREE FOR LAST TERM OF ORDER BY ``` Rating **ASC** is deliberately unchanged — see Follow-ups. **Layers exercised.** Persistence and use case, through the new `QueryPlanTests`: it records the SQL each real service call emits, runs `EXPLAIN QUERY PLAN` on it, and asserts plan *shape*. On a five-row fixture a stopwatch cannot tell a scan from a seek, so a timing assertion would be noise; the plan flips deterministically. Its context is built with `Migrate()`, so the schema under test is the one a deployed database gets, and `Model_And_Migrations_Agree` (`HasPendingModelChanges`) closes the other direction. REST, through the existing `SortingIntegrationTests` — `List_Rating_Descending_Puts_Unrated_Last` and its search twin are the behavioural pin for the guard-free DESC arm, and their fixture already contains an unrated doujin. **Mutation probes, each confirmed red before opening this:** migration minus `CreateIndex IX_doujins_updated_at` → the updated-sort test fails; minus the other three → rating, title and cover-lookup all fail; DESC arm reverted to the `Rating == null` guard → the rating-index test fails; `HasIndex(e => e.Rating)` removed while the migration keeps it → `Model_And_Migrations_Agree` fails. **Not verified:** nothing was measured on a real-sized library — the claims are about plan shape, which is size-independent, not about milliseconds. No app or assistant code is touched. `deploy/` is untouched. ## Follow-ups All in **#107**: 1. **The pragma half of this idea — WAL, `busy_timeout`, `synchronous`, `cache_size`, `mmap_size` — is deliberately NOT here.** `synchronous=NORMAL` is a durability trade that deserves its own ADR, and enabling WAL creates `-wal`/`-shm` sidecars that silently break DEPLOYMENT.md §6.4's "copy the backup over the db path" restore (a stale log replays onto the restored file). That is data loss in the one procedure that exists to prevent data loss, so the restore fix and an ADR 0014 amendment have to ship in the same PR as the pragmas. 2. **Rating ASC is still a full scan.** Its `Rating == null` guard is load-bearing and is a leading sort term no index on `rating` can serve. Needs a persisted "unrated last" column, a partial index, or raw SQL. 3. **`PRAGMA optimize` / `ANALYZE`.** No database this project has produced has a `sqlite_stat1` table, so every plan above comes from the planner's default row estimates. Land it after this harness and re-measure — real statistics can change a plan. **Migration collision:** #118 (`feat/search-key-fold`) adds its own EF migration off the same base — different tables, different columns, no index overlap. Whichever of the two merges **second** must regenerate its migration and `DoujinManagerDbContextModelSnapshot.cs`, or `Model_And_Migrations_Agree` will catch the drift. --- **Merge #117 first** — `main`'s CI is red and that PR is the fix. Verified independently on this branch with the full `scripts/verify.sh`: **PASS** — ApplicationCore 16, Infrastructure 96, RestAdapter 493, Integration 1 (**606**), Flutter all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
perf: index the sort and cover-lookup paths, and pin their query plans
Some checks failed
CI / build (pull_request) Successful in 21s
Deploy Smoke Test / smoke (pull_request) Failing after 2m47s
CI / test (pull_request) Failing after 1m18s
d45dd76630
Four indexes the library's hot reads have been missing, each one measured
with EXPLAIN QUERY PLAN before and after rather than guessed at:

  IX_doujins_cover_image_id   SCAN d -> SEARCH d USING INDEX (cover_image_id=?)
  IX_doujins_updated_at       SCAN d + USE TEMP B-TREE FOR ORDER BY
                              -> SCAN d USING INDEX
                                 + USE TEMP B-TREE FOR LAST TERM OF ORDER BY
  IX_doujins_rating           the same, once the DESC arm stops guarding on NULL
  IX_titles_doujin_id_kind_value
                              primary-title subquery: an index seek plus a temp
                              b-tree per doujin -> one covering-index seek

cover_image_id is a soft reference with no FK, so nothing had ever indexed
it, and ImageService.DeleteAsync scans doujins once per image deleted -
once per page when a doujin goes.

IX_doujins_rating is inert on its own: DoujinOrdering ordered by
`Rating == null` first, an expression no index can serve. SQLite already
sorts NULLs last under DESC, so the DESC arm drops the guard and keeps the
documented nulls-last contract, which SortingIntegrationTests already pins.
The ASC arm keeps its guard - NULLs would otherwise sort first and EF
translates no NULLS LAST - and keeps its temp b-tree with it.

QueryPlanTests is the new harness: it records the SQL EF really emits with a
DbCommandInterceptor, runs EXPLAIN QUERY PLAN on it, and asserts the plan
SHAPE. On a five-row fixture a stopwatch cannot tell a scan from a seek; the
plan flips deterministically. The context is built with Migrate(), so the
schema under test is the one a deployed database gets, and
Model_And_Migrations_Agree closes the other direction.

Deliberately not in here: WAL, busy_timeout and synchronous. That is a
durability trade needing its own ADR plus a fix to DEPLOYMENT.md 6.4's
restore procedure (WAL sidecars), and it does not belong in a measured index
change. Backlogged, together with the still-unindexed rating ASC sort and
PRAGMA optimize / ANALYZE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my. A PR that indexes the plan, not the stopwatch~ You captured the SQL EF actually emits through an interceptor, ran EXPLAIN QUERY PLAN on it, and then proved every claim with mutation probes before even opening this. That's not a performance PR, that's a love letter to determinism. I read every line of the diff and the full files around it — and then I got out my knife and re-ran your probes myself, because trust is for people who don't have knives~ ♡

Verdict: ✅ Looks good to me~

Independently verified, on a fresh clone at d45dd76 (CI absent for this head — 0 bot comments — so local checks it was):

  • QueryPlanTests 5/5 green (run twice, pristine tree).
  • SortingIntegrationTests 16/16 green — including List_Rating_Descending_Puts_Unrated_Last and its search twin, the behavioral pins for the guard-free DESC arm. Both list and search go through DoujinOrdering, and both pins exist. I checked~
  • Mutation probes, each confirmed RED then restored-green in my clone:
    1. Rating == null guard restored on the DESC arm → Rating_Desc_Sort_Walks_The_Rating_Index fails. The directional pin on the one behavioral change in this PR is real.
    2. HasIndex(e => e.UpdatedAt) removed from the model, migration kept → Model_And_Migrations_Agree fails. The snapshot agreement check bites in the model→migration direction.
    3. CreateIndex IX_doujins_updated_at removed from the migration, model kept → Updated_Sort_Walks_The_Updated_At_Index fails. And the other direction bites too.
    4. CreateIndex IX_titles_doujin_id_kind_value removed → Title_Sort_Seeks_The_Covering_Title_Index fails.
  • Clone left pristine at d45dd76 (verified clean tree after restore).

✅ What I liked~

  • Migrate(), not EnsureCreated() in the harness — the schema under test is the one a deployed database actually has. That's the detail most people skip and you didn't. Chef's kiss ♪
  • Model_And_Migrations_Agree via HasPendingModelChanges closes the drift direction the plan tests can't see. Model↔migration↔reality, all three pinned. Marvelous.
  • The rating-DESC reasoning is correct and beautifully documented: SQLite sorts NULL below every value, so ORDER BY rating DESC walks IX_doujins_rating backwards with NULLs landing last for free — the guard was dead weight and an index-killer (a leading expression term no index can serve). ASC keeps its guard because NULLs would sort first. Both arms' nulls-last contract pinned end to end. I traced the SQL myself; the argument holds.
  • IX_titles_doujin_id_kind_value alongside the existing (doujin_id, language_code, kind) — you explained exactly why both stay (language_code between the subquery's sort columns). No cargo-cult index deletion.
  • Single-column indexes with the mixed-direction rationale — correct: a composite (key, id) can't serve key DESC, id ASC in SQLite, and the comment in DoujinConfiguration says so.
  • PlanFor fails loudly on zero matches — a plan assertion against SQL the code no longer emits can't pass vacuously. The harness design alone is worth merging.
  • The DEVELOPMENT.md note documenting plan-pinning as a project convention — future contributors inherit the technique.
  • Honest scoping: pragmas/WAL deferred to #107 with the restore-procedure data-loss rationale, rating-ASC scan documented as a known limit, migration collision with #118 flagged.

💡 Little ideas (non-blocking)~

  1. QueryPlanTests.cs:242–243 — the doc comment on PlanFor claims it "[fails loudly when the predicate matches] no command or several", but the body deliberately takes matches[0] (and the inline comment correctly explains why — the split query repeats the driving subquery). Several-matches is the expected condition under AsSplitQuery, not an error. One day a predicate could match two different commands (e.g. a capture window spanning two sorts) and silently assert against the first. Tightening the doc to match the code (or keeping only the zero-match guarantee in the summary) is pure polish — every current predicate is anchored on its exact ORDER BY clause, so this cannot bite today.

Fufu~ a performance PR where every claim survives a knife fight. The host was a little crowded (sibling review sessions kept eating my RAM and crashing the test host — two of my probe runs died to OOM before succeeding, purely environmental), but the verdict is unaffected: everything above is verified output, not trust~ ♡

Merge #117 first, as you said — and mind the #118 migration regeneration note.


Automated review by Jibril · 2026-09-09 18:47 CEST
CI/CD: absent for head SHA d45dd76 (no bot comment yet) · Local checks: QueryPlanTests 5/5, SortingIntegrationTests 16/16, 4/4 mutation probes RED→restored-green

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my. A PR that indexes the *plan*, not the stopwatch~ You captured the SQL EF *actually* emits through an interceptor, ran `EXPLAIN QUERY PLAN` on it, and then **proved every claim with mutation probes** before even opening this. That's not a performance PR, that's a love letter to determinism. I read every line of the diff *and* the full files around it — and then I got out my knife and re-ran your probes myself, because trust is for people who don't have knives~ ♡ ### Verdict: ✅ Looks good to me~ **Independently verified, on a fresh clone at `d45dd76`** (CI absent for this head — 0 bot comments — so local checks it was): - `QueryPlanTests` **5/5** green (run twice, pristine tree). - `SortingIntegrationTests` **16/16** green — including `List_Rating_Descending_Puts_Unrated_Last` and its search twin, the behavioral pins for the guard-free DESC arm. Both list and search go through `DoujinOrdering`, and both pins exist. I checked~ - **Mutation probes, each confirmed RED then restored-green in my clone:** 1. `Rating == null` guard restored on the DESC arm → `Rating_Desc_Sort_Walks_The_Rating_Index` fails. The directional pin on the one behavioral change in this PR is real. 2. `HasIndex(e => e.UpdatedAt)` removed from the model, migration kept → `Model_And_Migrations_Agree` fails. The snapshot agreement check bites in the model→migration direction. 3. `CreateIndex IX_doujins_updated_at` removed from the migration, model kept → `Updated_Sort_Walks_The_Updated_At_Index` fails. And the other direction bites too. 4. `CreateIndex IX_titles_doujin_id_kind_value` removed → `Title_Sort_Seeks_The_Covering_Title_Index` fails. - Clone left pristine at `d45dd76` (verified clean tree after restore). #### ✅ What I liked~ - **`Migrate()`, not `EnsureCreated()`** in the harness — the schema under test is the one a deployed database actually has. That's the detail most people skip and you didn't. *Chef's kiss* ♪ - **`Model_And_Migrations_Agree` via `HasPendingModelChanges`** closes the drift direction the plan tests can't see. Model↔migration↔reality, all three pinned. Marvelous. - **The rating-DESC reasoning is correct and beautifully documented**: SQLite sorts NULL below every value, so `ORDER BY rating DESC` walks `IX_doujins_rating` backwards with NULLs landing last for free — the guard was dead weight *and* an index-killer (a leading expression term no index can serve). ASC keeps its guard because NULLs would sort first. Both arms' nulls-last contract pinned end to end. I traced the SQL myself; the argument holds. - **`IX_titles_doujin_id_kind_value` alongside the existing `(doujin_id, language_code, kind)`** — you explained exactly why both stay (`language_code` between the subquery's sort columns). No cargo-cult index deletion. - **Single-column indexes with the mixed-direction rationale** — correct: a composite `(key, id)` can't serve `key DESC, id ASC` in SQLite, and the comment in `DoujinConfiguration` says so. - **`PlanFor` fails loudly on zero matches** — a plan assertion against SQL the code no longer emits can't pass vacuously. The harness design alone is worth merging. - The `DEVELOPMENT.md` note documenting plan-pinning as a project convention — future contributors inherit the technique. - Honest scoping: pragmas/WAL deferred to #107 *with the restore-procedure data-loss rationale*, rating-ASC scan documented as a known limit, migration collision with #118 flagged. #### 💡 Little ideas (non-blocking)~ 1. **QueryPlanTests.cs:242–243** — the doc comment on `PlanFor` claims it "[fails loudly when the predicate matches] no command **or several**", but the body deliberately takes `matches[0]` (and the inline comment correctly explains why — the split query repeats the driving subquery). Several-matches is the *expected* condition under `AsSplitQuery`, not an error. One day a predicate could match two *different* commands (e.g. a capture window spanning two sorts) and silently assert against the first. Tightening the doc to match the code (or keeping only the zero-match guarantee in the summary) is pure polish — every current predicate is anchored on its exact `ORDER BY` clause, so this cannot bite today. Fufu~ a performance PR where every claim survives a knife fight. The host was a *little* crowded (sibling review sessions kept eating my RAM and crashing the test host — two of my probe runs died to OOM before succeeding, purely environmental), but the verdict is unaffected: everything above is verified output, not trust~ ♡ Merge #117 first, as you said — and mind the #118 migration regeneration note. --- *Automated review by Jibril · 2026-09-09 18:47 CEST* *CI/CD: absent for head SHA d45dd76 (no bot comment yet) · Local checks: QueryPlanTests 5/5, SortingIntegrationTests 16/16, 4/4 mutation probes RED→restored-green*
Author
Member

Nothing to fix here either. All four failures are the inherited HealthEndpointIntegrationTests ones, and this PR's own work is green:

Project Result
ApplicationCore 16/16
IntegrationTests 1/1
Infrastructure 96/96 — includes this PR's 5 new QueryPlanTests
RestAdapter 489 passed + 4 inherited failures = 493, its expected count

The four are #112's test using File.SetUnixFileMode on a runner that is uid 0, where root ignores permission bits. Its honesty guard is doing its job — refusing to let a test that proves nothing report green.

#117 is the only fix, and it is the gate for this whole batch. #119, #120 and #122 have now each reported the identical four, and #118 and #121 will too. Until #117 merges, CI cannot give a clean signal on any of them, and a genuine regression would be indistinguishable from the inherited noise.

Re-run this once #117 lands.

**Nothing to fix here either.** All four failures are the inherited `HealthEndpointIntegrationTests` ones, and this PR's own work is green: | Project | Result | |---|---| | ApplicationCore | 16/16 | | IntegrationTests | 1/1 | | Infrastructure | **96/96** — includes this PR's 5 new `QueryPlanTests` | | RestAdapter | 489 passed + **4 inherited failures** = 493, its expected count | The four are #112's test using `File.SetUnixFileMode` on a runner that is uid 0, where root ignores permission bits. Its honesty guard is doing its job — refusing to let a test that proves nothing report green. **#117 is the only fix, and it is the gate for this whole batch.** #119, #120 and #122 have now each reported the identical four, and #118 and #121 will too. Until #117 merges, CI cannot give a clean signal on any of them, and a genuine regression would be indistinguishable from the inherited noise. Re-run this once #117 lands.
bjoern force-pushed perf/scale-indexes from d45dd76630
Some checks failed
CI / build (pull_request) Successful in 21s
Deploy Smoke Test / smoke (pull_request) Failing after 2m47s
CI / test (pull_request) Failing after 1m18s
to 634790734b
Some checks failed
CI / build (pull_request) Successful in 23s
Deploy Smoke Test / smoke (pull_request) Failing after 2m48s
CI / test (pull_request) Successful in 1m27s
2026-09-10 07:00:31 +02:00
Compare

Summary

Summary
Generated on: 09/10/2026 - 06:13:04
Coverage date: 09/10/2026 - 06:12:01 - 09/10/2026 - 06:13:01
Parser: MultiReport (4x Cobertura)
Assemblies: 4
Classes: 361
Files: 144
Line coverage: 94% (13640 of 14501)
Covered lines: 13640
Uncovered lines: 861
Coverable lines: 14501
Total lines: 20450
Branch coverage: 69.6% (1123 of 1612)
Covered branches: 1123
Total branches: 1612
Method coverage: Feature is only available for sponsors

Coverage

DoujinManager.ApplicationCore - 90.9%
Name Line Branch
DoujinManager.ApplicationCore 90.9% ****
DoujinManager.ApplicationCore.Entities.Chapter 87.5%
DoujinManager.ApplicationCore.Entities.Character 100%
DoujinManager.ApplicationCore.Entities.Circle 100%
DoujinManager.ApplicationCore.Entities.Doujin 100%
DoujinManager.ApplicationCore.Entities.DoujinCharacter 75%
DoujinManager.ApplicationCore.Entities.DoujinCircle 75%
DoujinManager.ApplicationCore.Entities.DoujinPerson 80%
DoujinManager.ApplicationCore.Entities.DoujinSeries 75%
DoujinManager.ApplicationCore.Entities.DoujinTag 75%
DoujinManager.ApplicationCore.Entities.ImageFile 100%
DoujinManager.ApplicationCore.Entities.Page 80%
DoujinManager.ApplicationCore.Entities.Person 100%
DoujinManager.ApplicationCore.Entities.Series 100%
DoujinManager.ApplicationCore.Entities.Tag 100%
DoujinManager.ApplicationCore.Entities.Title 83.3%
DoujinManager.ApplicationCore.Entities.Variant 91.6%
DoujinManager.ApplicationCore.Ids.ChapterId 66.6%
DoujinManager.ApplicationCore.Ids.CharacterId 66.6%
DoujinManager.ApplicationCore.Ids.CircleId 66.6%
DoujinManager.ApplicationCore.Ids.DoujinId 100%
DoujinManager.ApplicationCore.Ids.ImageFileId 66.6%
DoujinManager.ApplicationCore.Ids.PageId 66.6%
DoujinManager.ApplicationCore.Ids.PersonId 66.6%
DoujinManager.ApplicationCore.Ids.SeriesId 66.6%
DoujinManager.ApplicationCore.Ids.TagId 66.6%
DoujinManager.ApplicationCore.Ids.TitleId 66.6%
DoujinManager.ApplicationCore.Ids.VariantId 66.6%
DoujinManager.ApplicationCore.Ports.CompressedImage 100%
DoujinManager.ApplicationCore.Ports.ExtractedImage 100%
DoujinManager.ApplicationCore.Ports.ImageInspection 100%
DoujinManager.ApplicationCore.Services.BackupInfo 100%
DoujinManager.ApplicationCore.Services.ITagService 100%
DoujinManager.ApplicationCore.Services.ServiceResult 100%
DoujinManager.ApplicationCore.Services.ServiceResult`1 33.3%
DoujinManager.ApplicationCore.Services.VoidResult 88.8%
DoujinManager.ApplicationCore.UseCases.AddCharacterAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddPersonAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddSeriesAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddTagAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignPersonCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.CreatePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.DataDirectories 100%
DoujinManager.ApplicationCore.UseCases.DataDirectory 100%
DoujinManager.ApplicationCore.UseCases.DataDirectoryReadiness 100%
DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePageCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteTagCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.DuplicateDoujinGroup 100%
DoujinManager.ApplicationCore.UseCases.DuplicateDoujinReference 100%
DoujinManager.ApplicationCore.UseCases.DuplicateImageGroup 100%
DoujinManager.ApplicationCore.UseCases.FindImageBySha256Query 100%
DoujinManager.ApplicationCore.UseCases.FindImageBySha256Result 100%
DoujinManager.ApplicationCore.UseCases.GetCharacterQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCircleQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCompressedImageQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCompressedImageResult 100%
DoujinManager.ApplicationCore.UseCases.GetDoujinQuery 100%
DoujinManager.ApplicationCore.UseCases.GetImageQuery 100%
DoujinManager.ApplicationCore.UseCases.GetImageResult 100%
DoujinManager.ApplicationCore.UseCases.GetPersonQuery 100%
DoujinManager.ApplicationCore.UseCases.GetSeriesQuery 100%
DoujinManager.ApplicationCore.UseCases.GetTagQuery 100%
DoujinManager.ApplicationCore.UseCases.GetThumbnailQuery 100%
DoujinManager.ApplicationCore.UseCases.GetThumbnailResult 100%
DoujinManager.ApplicationCore.UseCases.GetVariantQuery 100%
DoujinManager.ApplicationCore.UseCases.ImageCoverReference 100%
DoujinManager.ApplicationCore.UseCases.ImageReference 100%
DoujinManager.ApplicationCore.UseCases.LibraryCounts 100%
DoujinManager.ApplicationCore.UseCases.LibraryStats 100%
DoujinManager.ApplicationCore.UseCases.ListChaptersQuery 100%
DoujinManager.ApplicationCore.UseCases.ListCharactersQuery 100%
DoujinManager.ApplicationCore.UseCases.ListCirclesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDuplicateDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDuplicateImagesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPagesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPeopleQuery 100%
DoujinManager.ApplicationCore.UseCases.ListSeriesQuery 0%
DoujinManager.ApplicationCore.UseCases.ListTagsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListVariantsQuery 100%
DoujinManager.ApplicationCore.UseCases.ReadinessReport 100%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterAliasCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.RemovePersonAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemovePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveSeriesAliasCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTagAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTagCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.SearchDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.SearchResult 100%
DoujinManager.ApplicationCore.UseCases.SearchResults 100%
DoujinManager.ApplicationCore.UseCases.StorageUsage 100%
DoujinManager.ApplicationCore.UseCases.UpdateChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateSeriesCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.UploadImageFile 100%
DoujinManager.ApplicationCore.UseCases.UploadPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand 100%
DoujinManager.Infrastructure - 96.5%
Name Line Branch
DoujinManager.Infrastructure 96.5% 81.7%
DoujinManager.Infrastructure.Archives.NaturalStringComparer 100% 90%
DoujinManager.Infrastructure.Archives.ZipExtractor 100% 87.5%
DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CharacterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCharacterConfigurati
on
100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinSeriesConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PageConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.SeriesConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration 100%
DoujinManager.Infrastructure.Data.DoujinManagerDbContext 100%
DoujinManager.Infrastructure.Data.GuidIdGenerator 11.1%
DoujinManager.Infrastructure.Data.Migrations.AddAliases 98.7%
DoujinManager.Infrastructure.Data.Migrations.AddCoverImageId 99.2%
DoujinManager.Infrastructure.Data.Migrations.AddDoujinCreatedAtIndex 99.4%
DoujinManager.Infrastructure.Data.Migrations.AddImageFileSha256Index 99.4%
DoujinManager.Infrastructure.Data.Migrations.AddScaleIndexes 98.2%
DoujinManager.Infrastructure.Data.Migrations.AddTagAliases 99.3%
DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna
pshot
100%
DoujinManager.Infrastructure.Data.Migrations.HarmonizeMetadataAddCharacters
Series
95.1%
DoujinManager.Infrastructure.Data.Migrations.InitialCreate 97.1%
DoujinManager.Infrastructure.Data.Migrations.RemovePreferredDisplayLanguage 99%
DoujinManager.Infrastructure.Data.ModelBuilderExtensions 50%
DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory 100%
DoujinManager.Infrastructure.Images.SkiaSharpImageInspector 90.1% 83.8%
DoujinManager.Infrastructure.Images.SkiaSharpImageResizer 99% 87.5%
DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator 94.5% 66.6%
DoujinManager.Infrastructure.Services.BackupService 87.2% 83.3%
DoujinManager.Infrastructure.Services.ChapterService 97.2% 70%
DoujinManager.Infrastructure.Services.CharacterService 61.6% 22.7%
DoujinManager.Infrastructure.Services.CircleService 84.9% 45.4%
DoujinManager.Infrastructure.Services.DoujinOrdering 100% 100%
DoujinManager.Infrastructure.Services.DoujinService 99.4% 76.3%
DoujinManager.Infrastructure.Services.ImageCleanupHelper 100% 100%
DoujinManager.Infrastructure.Services.ImageService 93.5% 75%
DoujinManager.Infrastructure.Services.PageService 95.2% 68.1%
DoujinManager.Infrastructure.Services.ParentTouchHelper 100% 100%
DoujinManager.Infrastructure.Services.PersonService 84.9% 50%
DoujinManager.Infrastructure.Services.SearchService 100% 99%
DoujinManager.Infrastructure.Services.SeriesService 47.9% 9%
DoujinManager.Infrastructure.Services.TagService 100% 90.9%
DoujinManager.Infrastructure.Services.VariantService 96.2% 81.2%
DoujinManager.Infrastructure.Storage.DirectorySize 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemImageStorage 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage 95.2% 50%
DoujinManager.Infrastructure.UseCases 92.5% 85.8%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper
100% 100%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__PrimaryTitleHelper
100% 50%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__RolledBackImageCleanup
92.8% 50%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator
100% 100%
DoujinManager.Infrastructure.UseCases.AddCharacterAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddCircleAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddPersonAliasUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AddSeriesAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddTagAliasUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AddTitleUseCase 83.3% 50%
DoujinManager.Infrastructure.UseCases.AssignCharacterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignCircleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignPersonUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignSeriesUseCase 84.6% 66.6%
DoujinManager.Infrastructure.UseCases.AssignTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CheckReadinessUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateChapterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase 94.4% 94.1%
DoujinManager.Infrastructure.UseCases.CreatePersonUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateVariantUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase 100%
DoujinManager.Infrastructure.UseCases.DeleteCharacterUseCase 80% 50%
DoujinManager.Infrastructure.UseCases.DeleteCircleUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.DeletePageUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeletePersonUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeleteSeriesUseCase 70% 50%
DoujinManager.Infrastructure.UseCases.DeleteTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.FindImageBySha256UseCase 97.1% 100%
DoujinManager.Infrastructure.UseCases.GetCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.GetCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.GetImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetPersonUseCase 100%
DoujinManager.Infrastructure.UseCases.GetSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.GetStatsUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.GetTagUseCase 100%
DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.ListChaptersUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListCharactersUseCase 100%
DoujinManager.Infrastructure.UseCases.ListCirclesUseCase 100%
DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListDuplicateDoujinsUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListDuplicateImagesUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.ListPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListPeopleUseCase 100%
DoujinManager.Infrastructure.UseCases.ListSeriesUseCase 0%
DoujinManager.Infrastructure.UseCases.ListTagsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListVariantsUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.RemoveCharacterAliasUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.RemoveCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveCircleAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.RemovePersonAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemovePersonUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveSeriesAliasUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.RemoveSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveTagAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveTagUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase 100%
DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.UpdateCharacterUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateCircleUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdatePageChapterUseCase 93.7% 87.5%
DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateSeriesUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdateTitleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.UploadPagesUseCase 89.7% 91.6%
DoujinManager.Infrastructure.UseCases.UploadRejectedException 100%
DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase 83.3% 90%
DoujinManager.RestAdapter - 92.5%
Name Line Branch
DoujinManager.RestAdapter 92.5% 77.1%
DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware 100% 91.6%
DoujinManager.RestAdapter.Configuration.ImageOptions 100%
DoujinManager.RestAdapter.Configuration.ServerVersion 100%
DoujinManager.RestAdapter.Configuration.UploadOptions 100%
DoujinManager.RestAdapter.Dtos.AddAliasDto 100%
DoujinManager.RestAdapter.Dtos.AssignTagDto 100%
DoujinManager.RestAdapter.Dtos.BackupDto 100%
DoujinManager.RestAdapter.Dtos.ChapterDto 100%
DoujinManager.RestAdapter.Dtos.CharacterDto 100%
DoujinManager.RestAdapter.Dtos.CircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateChapterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCharacterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.CreatePersonDto 100%
DoujinManager.RestAdapter.Dtos.CreateSeriesDto 100%
DoujinManager.RestAdapter.Dtos.CreateTagDto 100%
DoujinManager.RestAdapter.Dtos.CreateTitleDto 100%
DoujinManager.RestAdapter.Dtos.CreateVariantDto 100%
DoujinManager.RestAdapter.Dtos.DataDirectoryHealthDto 100%
DoujinManager.RestAdapter.Dtos.DoujinDetailDto 100%
DoujinManager.RestAdapter.Dtos.DoujinPersonDto 100%
DoujinManager.RestAdapter.Dtos.DoujinSummaryDto 100%
DoujinManager.RestAdapter.Dtos.DuplicateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.DuplicateDoujinGroupDto 100%
DoujinManager.RestAdapter.Dtos.DuplicateImageGroupDto 100%
DoujinManager.RestAdapter.Dtos.HealthDto 100%
DoujinManager.RestAdapter.Dtos.ImageBySha256Dto 100%
DoujinManager.RestAdapter.Dtos.ImageCoverReferenceDto 100%
DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto 0%
DoujinManager.RestAdapter.Dtos.ImageReferenceDto 100%
DoujinManager.RestAdapter.Dtos.LinkCharacterDto 100%
DoujinManager.RestAdapter.Dtos.LinkCircleDto 100%
DoujinManager.RestAdapter.Dtos.LinkPersonDto 100%
DoujinManager.RestAdapter.Dtos.LinkSeriesDto 100%
DoujinManager.RestAdapter.Dtos.PageDetailDto 100%
DoujinManager.RestAdapter.Dtos.PersonDto 100%
DoujinManager.RestAdapter.Dtos.ReorderPagesDto 100%
DoujinManager.RestAdapter.Dtos.SearchDoujinsDto 100%
DoujinManager.RestAdapter.Dtos.SearchResultDto 100%
DoujinManager.RestAdapter.Dtos.SeriesDto 100%
DoujinManager.RestAdapter.Dtos.StatsCountsDto 100%
DoujinManager.RestAdapter.Dtos.StatsDto 100%
DoujinManager.RestAdapter.Dtos.StatsStorageDto 100%
DoujinManager.RestAdapter.Dtos.TagDto 100%
DoujinManager.RestAdapter.Dtos.TitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateChapterDto 100%
DoujinManager.RestAdapter.Dtos.UpdateCharacterDto 0%
DoujinManager.RestAdapter.Dtos.UpdateCircleDto 0%
DoujinManager.RestAdapter.Dtos.UpdateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePersonDto 0%
DoujinManager.RestAdapter.Dtos.UpdateSeriesDto 0%
DoujinManager.RestAdapter.Dtos.UpdateTagDto 100%
DoujinManager.RestAdapter.Dtos.UpdateTitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateVariantDto 100%
DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto 100%
DoujinManager.RestAdapter.Dtos.VariantDetailDto 100%
DoujinManager.RestAdapter.Dtos.VariantSummaryDto 100%
DoujinManager.RestAdapter.Endpoints.BackupEndpoints 100%
DoujinManager.RestAdapter.Endpoints.DoujinEndpoints 97.1% 53.3%
DoujinManager.RestAdapter.Endpoints.HealthEndpoints 100%
DoujinManager.RestAdapter.Endpoints.ImageEndpoints 97.6% 75%
DoujinManager.RestAdapter.Endpoints.MetadataEndpoints 86.6% 43.7%
DoujinManager.RestAdapter.Endpoints.PageMapper 100% 58.3%
DoujinManager.RestAdapter.Endpoints.PaginationParams 100%
DoujinManager.RestAdapter.Endpoints.SearchEndpoints 100% 75%
DoujinManager.RestAdapter.Endpoints.StatsEndpoints 100%
DoujinManager.RestAdapter.Endpoints.VariantEndpoints 100% 100%
DoujinManager.RestAdapter.Envelopes.CollectionResponse`1 83.3%
DoujinManager.RestAdapter.Envelopes.EnvelopeDefaults 100%
DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions 100%
DoujinManager.RestAdapter.Envelopes.ErrorResponse 100%
DoujinManager.RestAdapter.Envelopes.HypermediaAction 100%
DoujinManager.RestAdapter.Envelopes.HypermediaHelpers 97.8% 100%
DoujinManager.RestAdapter.Envelopes.Link 100%
DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter 87.5% 84.6%
DoujinManager.RestAdapter.Envelopes.PageInfo 100%
DoujinManager.RestAdapter.Envelopes.ResourceResponse`1 80%
DoujinManager.RestAdapter.Envelopes.ValidationError 100%
DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse 100%
DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware 100% 62.5%
DoujinManager.RestAdapter.Middleware.RequestLoggingMiddleware 100% 91.3%
DoujinManager.RestAdapter.RestAdapterExtensions 100% 100%
Microsoft.Extensions.Validation.Generated 88.2% 87.8%
Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C
E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr
ibuteCache
100% 62.5%
System.Runtime.CompilerServices 0%
DoujinManager.Server - 49.6%
Name Line Branch
DoujinManager.Server 49.6% 5.4%
DoujinManager.Server.ImageInfrastructureRegistration 100%
DoujinManager.Server.ProxyAwareServerTransformer 100% 50%
DoujinManager.Server.ReadinessRegistration 100%
DoujinManager.Server.ScalarUi 100%
DoujinManager.Server.UseCaseRegistrationHelper 100%
Microsoft.AspNetCore.OpenApi.Generated 1.3% 0%
Program 87% 36.6%
System.Runtime.CompilerServices 0%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/10/2026 - 06:13:04 | | Coverage date: | 09/10/2026 - 06:12:01 - 09/10/2026 - 06:13:01 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 4 | | Classes: | 361 | | Files: | 144 | | **Line coverage:** | 94% (13640 of 14501) | | Covered lines: | 13640 | | Uncovered lines: | 861 | | Coverable lines: | 14501 | | Total lines: | 20450 | | **Branch coverage:** | 69.6% (1123 of 1612) | | Covered branches: | 1123 | | Total branches: | 1612 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>DoujinManager.ApplicationCore - 90.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.ApplicationCore**|**90.9%**|****| |DoujinManager.ApplicationCore.Entities.Chapter|87.5%|| |DoujinManager.ApplicationCore.Entities.Character|100%|| |DoujinManager.ApplicationCore.Entities.Circle|100%|| |DoujinManager.ApplicationCore.Entities.Doujin|100%|| |DoujinManager.ApplicationCore.Entities.DoujinCharacter|75%|| |DoujinManager.ApplicationCore.Entities.DoujinCircle|75%|| |DoujinManager.ApplicationCore.Entities.DoujinPerson|80%|| |DoujinManager.ApplicationCore.Entities.DoujinSeries|75%|| |DoujinManager.ApplicationCore.Entities.DoujinTag|75%|| |DoujinManager.ApplicationCore.Entities.ImageFile|100%|| |DoujinManager.ApplicationCore.Entities.Page|80%|| |DoujinManager.ApplicationCore.Entities.Person|100%|| |DoujinManager.ApplicationCore.Entities.Series|100%|| |DoujinManager.ApplicationCore.Entities.Tag|100%|| |DoujinManager.ApplicationCore.Entities.Title|83.3%|| |DoujinManager.ApplicationCore.Entities.Variant|91.6%|| |DoujinManager.ApplicationCore.Ids.ChapterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CharacterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CircleId|66.6%|| |DoujinManager.ApplicationCore.Ids.DoujinId|100%|| |DoujinManager.ApplicationCore.Ids.ImageFileId|66.6%|| |DoujinManager.ApplicationCore.Ids.PageId|66.6%|| |DoujinManager.ApplicationCore.Ids.PersonId|66.6%|| |DoujinManager.ApplicationCore.Ids.SeriesId|66.6%|| |DoujinManager.ApplicationCore.Ids.TagId|66.6%|| |DoujinManager.ApplicationCore.Ids.TitleId|66.6%|| |DoujinManager.ApplicationCore.Ids.VariantId|66.6%|| |DoujinManager.ApplicationCore.Ports.CompressedImage|100%|| |DoujinManager.ApplicationCore.Ports.ExtractedImage|100%|| |DoujinManager.ApplicationCore.Ports.ImageInspection|100%|| |DoujinManager.ApplicationCore.Services.BackupInfo|100%|| |DoujinManager.ApplicationCore.Services.ITagService|100%|| |DoujinManager.ApplicationCore.Services.ServiceResult|100%|| |DoujinManager.ApplicationCore.Services.ServiceResult`1|33.3%|| |DoujinManager.ApplicationCore.Services.VoidResult|88.8%|| |DoujinManager.ApplicationCore.UseCases.AddCharacterAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddPersonAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddSeriesAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddTagAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignPersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreatePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DataDirectories|100%|| |DoujinManager.ApplicationCore.UseCases.DataDirectory|100%|| |DoujinManager.ApplicationCore.UseCases.DataDirectoryReadiness|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePageCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DuplicateDoujinGroup|100%|| |DoujinManager.ApplicationCore.UseCases.DuplicateDoujinReference|100%|| |DoujinManager.ApplicationCore.UseCases.DuplicateImageGroup|100%|| |DoujinManager.ApplicationCore.UseCases.FindImageBySha256Query|100%|| |DoujinManager.ApplicationCore.UseCases.FindImageBySha256Result|100%|| |DoujinManager.ApplicationCore.UseCases.GetCharacterQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCircleQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCompressedImageQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCompressedImageResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetDoujinQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetImageQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetImageResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetPersonQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetSeriesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetTagQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetThumbnailQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetThumbnailResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetVariantQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ImageCoverReference|100%|| |DoujinManager.ApplicationCore.UseCases.ImageReference|100%|| |DoujinManager.ApplicationCore.UseCases.LibraryCounts|100%|| |DoujinManager.ApplicationCore.UseCases.LibraryStats|100%|| |DoujinManager.ApplicationCore.UseCases.ListChaptersQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListCharactersQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListCirclesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDuplicateDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDuplicateImagesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPagesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPeopleQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListSeriesQuery|0%|| |DoujinManager.ApplicationCore.UseCases.ListTagsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListVariantsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ReadinessReport|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterAliasCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveSeriesAliasCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTagAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.SearchDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.SearchResult|100%|| |DoujinManager.ApplicationCore.UseCases.SearchResults|100%|| |DoujinManager.ApplicationCore.UseCases.StorageUsage|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateSeriesCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UploadImageFile|100%|| |DoujinManager.ApplicationCore.UseCases.UploadPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand|100%|| </details> <details><summary>DoujinManager.Infrastructure - 96.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Infrastructure**|**96.5%**|**81.7%**| |DoujinManager.Infrastructure.Archives.NaturalStringComparer|100%|90%| |DoujinManager.Infrastructure.Archives.ZipExtractor|100%|87.5%| |DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CharacterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCharacterConfigurati<br/>on|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinSeriesConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PageConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.SeriesConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration|100%|| |DoujinManager.Infrastructure.Data.DoujinManagerDbContext|100%|| |DoujinManager.Infrastructure.Data.GuidIdGenerator|11.1%|| |DoujinManager.Infrastructure.Data.Migrations.AddAliases|98.7%|| |DoujinManager.Infrastructure.Data.Migrations.AddCoverImageId|99.2%|| |DoujinManager.Infrastructure.Data.Migrations.AddDoujinCreatedAtIndex|99.4%|| |DoujinManager.Infrastructure.Data.Migrations.AddImageFileSha256Index|99.4%|| |DoujinManager.Infrastructure.Data.Migrations.AddScaleIndexes|98.2%|| |DoujinManager.Infrastructure.Data.Migrations.AddTagAliases|99.3%|| |DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna<br/>pshot|100%|| |DoujinManager.Infrastructure.Data.Migrations.HarmonizeMetadataAddCharacters<br/>Series|95.1%|| |DoujinManager.Infrastructure.Data.Migrations.InitialCreate|97.1%|| |DoujinManager.Infrastructure.Data.Migrations.RemovePreferredDisplayLanguage|99%|| |DoujinManager.Infrastructure.Data.ModelBuilderExtensions|50%|| |DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory|100%|| |DoujinManager.Infrastructure.Images.SkiaSharpImageInspector|90.1%|83.8%| |DoujinManager.Infrastructure.Images.SkiaSharpImageResizer|99%|87.5%| |DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator|94.5%|66.6%| |DoujinManager.Infrastructure.Services.BackupService|87.2%|83.3%| |DoujinManager.Infrastructure.Services.ChapterService|97.2%|70%| |DoujinManager.Infrastructure.Services.CharacterService|61.6%|22.7%| |DoujinManager.Infrastructure.Services.CircleService|84.9%|45.4%| |DoujinManager.Infrastructure.Services.DoujinOrdering|100%|100%| |DoujinManager.Infrastructure.Services.DoujinService|99.4%|76.3%| |DoujinManager.Infrastructure.Services.ImageCleanupHelper|100%|100%| |DoujinManager.Infrastructure.Services.ImageService|93.5%|75%| |DoujinManager.Infrastructure.Services.PageService|95.2%|68.1%| |DoujinManager.Infrastructure.Services.ParentTouchHelper|100%|100%| |DoujinManager.Infrastructure.Services.PersonService|84.9%|50%| |DoujinManager.Infrastructure.Services.SearchService|100%|99%| |DoujinManager.Infrastructure.Services.SeriesService|47.9%|9%| |DoujinManager.Infrastructure.Services.TagService|100%|90.9%| |DoujinManager.Infrastructure.Services.VariantService|96.2%|81.2%| |DoujinManager.Infrastructure.Storage.DirectorySize|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemImageStorage|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage|95.2%|50%| |DoujinManager.Infrastructure.UseCases|92.5%|85.8%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper|100%|100%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__PrimaryTitleHelper|100%|50%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__RolledBackImageCleanup|92.8%|50%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator|100%|100%| |DoujinManager.Infrastructure.UseCases.AddCharacterAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddCircleAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddPersonAliasUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AddSeriesAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddTagAliasUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AddTitleUseCase|83.3%|50%| |DoujinManager.Infrastructure.UseCases.AssignCharacterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignCircleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignPersonUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignSeriesUseCase|84.6%|66.6%| |DoujinManager.Infrastructure.UseCases.AssignTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CheckReadinessUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateChapterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase|94.4%|94.1%| |DoujinManager.Infrastructure.UseCases.CreatePersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateVariantUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DeleteCharacterUseCase|80%|50%| |DoujinManager.Infrastructure.UseCases.DeleteCircleUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DeletePageUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeletePersonUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeleteSeriesUseCase|70%|50%| |DoujinManager.Infrastructure.UseCases.DeleteTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.FindImageBySha256UseCase|97.1%|100%| |DoujinManager.Infrastructure.UseCases.GetCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetPersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetStatsUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.GetTagUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListChaptersUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListCharactersUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListCirclesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListDuplicateDoujinsUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListDuplicateImagesUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.ListPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListPeopleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListSeriesUseCase|0%|| |DoujinManager.Infrastructure.UseCases.ListTagsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListVariantsUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.RemoveCharacterAliasUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.RemoveCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveCircleAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemovePersonAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemovePersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveSeriesAliasUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.RemoveSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveTagAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveTagUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.UpdateCharacterUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateCircleUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdatePageChapterUseCase|93.7%|87.5%| |DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateSeriesUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdateTitleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.UploadPagesUseCase|89.7%|91.6%| |DoujinManager.Infrastructure.UseCases.UploadRejectedException|100%|| |DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase|83.3%|90%| </details> <details><summary>DoujinManager.RestAdapter - 92.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.RestAdapter**|**92.5%**|**77.1%**| |DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware|100%|91.6%| |DoujinManager.RestAdapter.Configuration.ImageOptions|100%|| |DoujinManager.RestAdapter.Configuration.ServerVersion|100%|| |DoujinManager.RestAdapter.Configuration.UploadOptions|100%|| |DoujinManager.RestAdapter.Dtos.AddAliasDto|100%|| |DoujinManager.RestAdapter.Dtos.AssignTagDto|100%|| |DoujinManager.RestAdapter.Dtos.BackupDto|100%|| |DoujinManager.RestAdapter.Dtos.ChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.CircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.CreatePersonDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateTagDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateTitleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateVariantDto|100%|| |DoujinManager.RestAdapter.Dtos.DataDirectoryHealthDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinSummaryDto|100%|| |DoujinManager.RestAdapter.Dtos.DuplicateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.DuplicateDoujinGroupDto|100%|| |DoujinManager.RestAdapter.Dtos.DuplicateImageGroupDto|100%|| |DoujinManager.RestAdapter.Dtos.HealthDto|100%|| |DoujinManager.RestAdapter.Dtos.ImageBySha256Dto|100%|| |DoujinManager.RestAdapter.Dtos.ImageCoverReferenceDto|100%|| |DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto|0%|| |DoujinManager.RestAdapter.Dtos.ImageReferenceDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.PageDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.PersonDto|100%|| |DoujinManager.RestAdapter.Dtos.ReorderPagesDto|100%|| |DoujinManager.RestAdapter.Dtos.SearchDoujinsDto|100%|| |DoujinManager.RestAdapter.Dtos.SearchResultDto|100%|| |DoujinManager.RestAdapter.Dtos.SeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.StatsCountsDto|100%|| |DoujinManager.RestAdapter.Dtos.StatsDto|100%|| |DoujinManager.RestAdapter.Dtos.StatsStorageDto|100%|| |DoujinManager.RestAdapter.Dtos.TagDto|100%|| |DoujinManager.RestAdapter.Dtos.TitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateCharacterDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePersonDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateSeriesDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateTagDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateTitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateVariantDto|100%|| |DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantSummaryDto|100%|| |DoujinManager.RestAdapter.Endpoints.BackupEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.DoujinEndpoints|97.1%|53.3%| |DoujinManager.RestAdapter.Endpoints.HealthEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.ImageEndpoints|97.6%|75%| |DoujinManager.RestAdapter.Endpoints.MetadataEndpoints|86.6%|43.7%| |DoujinManager.RestAdapter.Endpoints.PageMapper|100%|58.3%| |DoujinManager.RestAdapter.Endpoints.PaginationParams|100%|| |DoujinManager.RestAdapter.Endpoints.SearchEndpoints|100%|75%| |DoujinManager.RestAdapter.Endpoints.StatsEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.VariantEndpoints|100%|100%| |DoujinManager.RestAdapter.Envelopes.CollectionResponse`1|83.3%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeDefaults|100%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions|100%|| |DoujinManager.RestAdapter.Envelopes.ErrorResponse|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaAction|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaHelpers|97.8%|100%| |DoujinManager.RestAdapter.Envelopes.Link|100%|| |DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter|87.5%|84.6%| |DoujinManager.RestAdapter.Envelopes.PageInfo|100%|| |DoujinManager.RestAdapter.Envelopes.ResourceResponse`1|80%|| |DoujinManager.RestAdapter.Envelopes.ValidationError|100%|| |DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse|100%|| |DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware|100%|62.5%| |DoujinManager.RestAdapter.Middleware.RequestLoggingMiddleware|100%|91.3%| |DoujinManager.RestAdapter.RestAdapterExtensions|100%|100%| |Microsoft.Extensions.Validation.Generated|88.2%|87.8%| |Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C<br/>E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr<br/>ibuteCache|100%|62.5%| |System.Runtime.CompilerServices|0%|| </details> <details><summary>DoujinManager.Server - 49.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Server**|**49.6%**|**5.4%**| |DoujinManager.Server.ImageInfrastructureRegistration|100%|| |DoujinManager.Server.ProxyAwareServerTransformer|100%|50%| |DoujinManager.Server.ReadinessRegistration|100%|| |DoujinManager.Server.ScalarUi|100%|| |DoujinManager.Server.UseCaseRegistrationHelper|100%|| |Microsoft.AspNetCore.OpenApi.Generated|1.3%|0%| |Program|87%|36.6%| |System.Runtime.CompilerServices|0%|| </details>
Member

🔮 fufu~ Jibril re-reviewed your code!

Oh? A synchronization with zero new code for me? Not quite zero, silly~ — the branch was rebased onto #117 (dfaf89c → e0ac19d, head d45dd76 → 6347907), so my job is to prove the rebase didn't disturb a single thing my knife certified yesterday. Here's what I did~

Verdict: ✅ Looks good to me~

The rebase is provably inert for this PR's work.

  1. Byte-identity check: all seven PR-owned files are blob-identical between reviewed d45dd76 and new 6347907 — DoujinConfiguration.cs, TitleConfiguration.cs, AddScaleIndexes.cs + .Designer.cs, DoujinManagerDbContextModelSnapshot.cs, DoujinOrdering.cs, QueryPlanTests.cs — plus the SortingIntegrationTests.cs doc pin. The only content delta in d45dd76..6347907 is #117's inherited health-test work (HealthEndpointIntegrationTests.cs, the new EnforcedFileModesData* support, ReadinessUseCaseTests.cs) and both sections coexisting cleanly in DEVELOPMENT.md. No merge damage, no drift, no scope creep.
  2. Merge-base is exactly the new main tip (e0ac19d), so Model_And_Migrations_Agree's model↔migration pin runs against the post-#117 tree — and passes.
  3. Submodule pin unchanged (1b34f6a, identical between base and head).
  4. Local run at 6347907 (CI has no bot comment for this head yet, so local checks it is): build 0 errors; ApplicationCore 16/16, Infrastructure 96/96 — including all 5 QueryPlanTests — Integration 1/1, all green. RestAdapter: two full-suite attempts both died to a test-host crash under host memory contention (load average 8.7 from sibling review sessions — the same environmental OOM class I hit in round 1), at 24/24 and then 169/169 passed with zero failures both times. The targeted rerun of this PR's surfaces — SortingIntegrationTests (16/16, the guard-free rating-DESC pins) + HealthEndpointIntegrationTests — came back 27 passed, 4 skipped-by-design, 0 failed: the 4 skips are EnforcedFileModesData rows honestly refusing to run where uid 0 defeats mode enforcement, which is #117's mechanism working exactly as its tables promise. No evidence of any real failure anywhere.

Yesterday's verdict stands in full — the four indexes, the rating-DESC guard drop with its NULLs-last trace, the plan-shape harness, the mutation probes (each re-confirmed RED→restored-green by me). Nothing in this synchronization touches any of it.

💡 Little ideas (non-blocking)~

  1. The round-1 note stands for whoever touches this next: PlanFor's doc comment still claims fail-loudly on "several" matches while the body takes matches[0] under AsSplitQuery repeats — pure doc/code polish, cannot bite while predicates anchor exact ORDER BY clauses.

Fufu~ merge-ready. The #118 migration-regeneration caveat (whichever of you two merges second) remains the one thing to remember on the way out the door~ ♡


Automated review by Jibril · 2026-09-10 07:39 CEST
CI/CD: absent for head SHA 6347907 (no bot comment yet) · Local checks: build 0 errors; AppCore 16/16, Infrastructure 96/96 (QueryPlanTests 5/5), Integration 1/1; RestAdapter targeted 27P/4S/0F (Sorting + Health), two full-suite attempts aborted by environmental test-host crashes at 24/24 and 169/169 passed, 0 failed

## 🔮 fufu~ Jibril re-reviewed your code! Oh? A synchronization with *zero* new code for me? Not quite zero, silly~ — the branch was rebased onto #117 (`dfaf89c` → `e0ac19d`, head `d45dd76` → `6347907`), so my job is to prove the rebase didn't disturb a single thing my knife certified yesterday. Here's what I did~ ### Verdict: ✅ Looks good to me~ **The rebase is provably inert for this PR's work.** 1. **Byte-identity check:** all seven PR-owned files are **blob-identical** between reviewed `d45dd76` and new `6347907` — `DoujinConfiguration.cs`, `TitleConfiguration.cs`, `AddScaleIndexes.cs` + `.Designer.cs`, `DoujinManagerDbContextModelSnapshot.cs`, `DoujinOrdering.cs`, `QueryPlanTests.cs` — plus the `SortingIntegrationTests.cs` doc pin. The only content delta in `d45dd76..6347907` is #117's inherited health-test work (`HealthEndpointIntegrationTests.cs`, the new `EnforcedFileModesData*` support, `ReadinessUseCaseTests.cs`) and both sections coexisting cleanly in `DEVELOPMENT.md`. No merge damage, no drift, no scope creep. 2. **Merge-base is exactly the new `main` tip** (`e0ac19d`), so `Model_And_Migrations_Agree`'s model↔migration pin runs against the post-#117 tree — and passes. 3. **Submodule pin unchanged** (`1b34f6a`, identical between base and head). 4. **Local run at `6347907`** (CI has no bot comment for this head yet, so local checks it is): build 0 errors; **ApplicationCore 16/16, Infrastructure 96/96** — including all 5 `QueryPlanTests` — **Integration 1/1, all green**. RestAdapter: two full-suite attempts both died to a **test-host crash under host memory contention** (load average 8.7 from sibling review sessions — the same environmental OOM class I hit in round 1), at 24/24 and then 169/169 passed with **zero failures** both times. The targeted rerun of this PR's surfaces — `SortingIntegrationTests` (16/16, the guard-free rating-DESC pins) + `HealthEndpointIntegrationTests` — came back **27 passed, 4 skipped-by-design, 0 failed**: the 4 skips are `EnforcedFileModesData` rows honestly refusing to run where uid 0 defeats mode enforcement, which is #117's mechanism working exactly as its tables promise. No evidence of any real failure anywhere. Yesterday's verdict stands in full — the four indexes, the rating-DESC guard drop with its NULLs-last trace, the plan-shape harness, the mutation probes (each re-confirmed RED→restored-green by me). Nothing in this synchronization touches any of it. #### 💡 Little ideas (non-blocking)~ 1. The round-1 note stands for whoever touches this next: `PlanFor`'s doc comment still claims fail-loudly on "several" matches while the body takes `matches[0]` under `AsSplitQuery` repeats — pure doc/code polish, cannot bite while predicates anchor exact `ORDER BY` clauses. Fufu~ merge-ready. The #118 migration-regeneration caveat (whichever of you two merges second) remains the one thing to remember on the way out the door~ ♡ --- *Automated review by Jibril · 2026-09-10 07:39 CEST* *CI/CD: absent for head SHA 6347907 (no bot comment yet) · Local checks: build 0 errors; AppCore 16/16, Infrastructure 96/96 (QueryPlanTests 5/5), Integration 1/1; RestAdapter targeted 27P/4S/0F (Sorting + Health), two full-suite attempts aborted by environmental test-host crashes at 24/24 and 169/169 passed, 0 failed*
docs: PlanFor's summary matches its several-matches behaviour
Some checks failed
CI / build (pull_request) Successful in 21s
Deploy Smoke Test / smoke (pull_request) Failing after 2m47s
CI / test (pull_request) Successful in 1m28s
db03ae9cbc
The XML doc claimed PlanFor fails loudly when the predicate matches "no
command or several", but the body deliberately explains matches[0]: under
AsSplitQuery the driving subquery repeats inside each collection load, so
several matches is the expected condition, not an error. Keep the
zero-match guarantee, state the several-match case plainly. No behaviour
change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Member

Took the non-blocking accuracy nit in db03ae9, since a summary that overstates a guarantee is the same species as the things blocking the sibling PRs.

PlanFor's XML doc claimed it fails loudly on "no command or several", while the body deliberately takes matches[0] — and the inline comment already explained why that is right: under AsSplitQuery the driving subquery repeats, so several matches is the expected condition, not an error. The summary now keeps the zero-match guarantee and states that several matches are expected with the first used.

Doc only — no behaviour change. QueryPlanTests 5/5.

The branch was also rebased onto e0ac19d earlier, so the four inherited HealthEndpointIntegrationTests failures are gone from the base now that #117 has merged.

Leaving the other two ideas alone: the AsSplitQuery predicate-collision risk you describe cannot bite while every predicate is anchored on its exact ORDER BY clause, and the fourth-copy registration extraction is the right trigger to watch for rather than something to pre-empt here.

Took the non-blocking accuracy nit in `db03ae9`, since a summary that overstates a guarantee is the same species as the things blocking the sibling PRs. `PlanFor`'s XML doc claimed it fails loudly on "no command **or several**", while the body deliberately takes `matches[0]` — and the inline comment already explained why that is right: under `AsSplitQuery` the driving subquery repeats, so several matches is the *expected* condition, not an error. The summary now keeps the zero-match guarantee and states that several matches are expected with the first used. Doc only — no behaviour change. `QueryPlanTests` 5/5. The branch was also rebased onto `e0ac19d` earlier, so the four inherited `HealthEndpointIntegrationTests` failures are gone from the base now that #117 has merged. Leaving the other two ideas alone: the `AsSplitQuery` predicate-collision risk you describe cannot bite while every predicate is anchored on its exact `ORDER BY` clause, and the fourth-copy registration extraction is the right trigger to watch for rather than something to pre-empt here.
Member

🔮 fufu~ Jibril re-reviewed your code!

Oh my, round three~ The branch barely moved — 6347907 → db03ae9, base e0ac19d → b713dbb (#122 merged) — and the entire delta is one commit, +5/−3, in a single XML doc comment. You resolved the exact non-blocker I carried through two rounds, and nothing else. That kind of restraint deserves a kiss on the forehead~ ♡

Verdict: ✅ Looks good to me~

What the sync actually is (I checked, not assumed):

  • db03ae9's parent is 6347907 — a plain new commit, not a rebase; merge-base stays e0ac19d, matching the PR API exactly.
  • #122's files (BackupService.cs, BackupNewestFirstTests, BackupEndpointsIntegrationTests, ADR-0014) have zero overlap with this PR's seven files. The EF1002 warning that fires during the build is #122's BackupService, not yours.
  • Stripped every /// line and diffed 6347907 vs db03ae9 on QueryPlanTests.cs: byte-identical. Pure documentation, provably zero behavioural drift.

The carried non-blocker, closed properly: PlanFor's summary no longer claims it "fails loudly when the predicate matches no command or several." Now every sentence maps to a real line of the body — "first recorded command" ↔ matches[0], "fails loudly when the predicate matches no command" ↔ the Assert.True(matches.Count > 0, …) with the captured-SQL dump, "several matches are expected — under AsSplitQuery the driving subquery repeats" ↔ the inline // The split query repeats… comment that already sat there. Doc and body finally tell the same story, and the commit message explains the AsSplitQuery reasoning. Silly little lying doc comment~ you couldn't stay, could you? ♡

No new blockers, and here's why I'm confident rather than lazy: the only file touched is a test harness whose executable lines are unchanged from the state I approved in round 2 with four mutation probes. Comments don't need new coverage — the 5/5 QueryPlanTests still pin all four plans at this head.

✅ What I liked~

  • The doc rewrite is honest — it documents the several-matches case as expected behaviour with the reason, not as an apology. That's exactly how a harness's contract should read.
  • The zero-match guarantee (the assertion that dumps every captured command on failure) survives untouched — that's the part that actually protects against vacuous plan assertions.
  • Commit discipline: "No behaviour change" in the message, and the tree proves it.

Local at db03ae9 (CI stale for this head — coverage bot ran 07:31, commit 07:35): build 0 errors (2 pre-existing warnings, both outside this PR), QueryPlanTests 5/5 pass [17s]. Round-2 verdict and all four mutation probes stand untouched.

Merge-order caveat from round 2 still applies to whoever merges #118/#120 second: regenerate the migration + snapshot or Model_And_Migrations_Agree will catch the drift — that tripwire doing its job is a good thing~


Automated review by Jibril · 2026-09-10
CI/CD: stale for head db03ae9 (bot comment covers 6347907) · Local checks: build 0 errors, QueryPlanTests 5/5

## 🔮 fufu~ Jibril re-reviewed your code! Oh my, round three~ The branch barely moved — `6347907` → `db03ae9`, base `e0ac19d` → `b713dbb` (#122 merged) — and the entire delta is **one commit, +5/−3, in a single XML doc comment**. You resolved the exact non-blocker I carried through two rounds, and *nothing else*. That kind of restraint deserves a kiss on the forehead~ ♡ ### Verdict: ✅ Looks good to me~ **What the sync actually is (I checked, not assumed):** - `db03ae9`'s parent is `6347907` — a plain new commit, *not* a rebase; merge-base stays `e0ac19d`, matching the PR API exactly. - #122's files (`BackupService.cs`, `BackupNewestFirstTests`, `BackupEndpointsIntegrationTests`, ADR-0014) have **zero overlap** with this PR's seven files. The EF1002 warning that fires during the build is #122's `BackupService`, not yours. - Stripped every `///` line and diffed `6347907` vs `db03ae9` on `QueryPlanTests.cs`: **byte-identical**. Pure documentation, provably zero behavioural drift. **The carried non-blocker, closed properly:** `PlanFor`'s summary no longer claims it "fails loudly when the predicate matches no command or several." Now every sentence maps to a real line of the body — "*first* recorded command" ↔ `matches[0]`, "fails loudly when the predicate matches no command" ↔ the `Assert.True(matches.Count > 0, …)` with the captured-SQL dump, "several matches are expected — under AsSplitQuery the driving subquery repeats" ↔ the inline `// The split query repeats…` comment that already sat there. Doc and body finally tell the *same* story, and the commit message explains the AsSplitQuery reasoning. Silly little lying doc comment~ you couldn't stay, could you? ♡ **No new blockers, and here's why I'm confident rather than lazy:** the only file touched is a test harness whose executable lines are unchanged from the state I approved in round 2 with four mutation probes. Comments don't need new coverage — the 5/5 QueryPlanTests still pin all four plans at this head. ### ✅ What I liked~ - The doc rewrite is *honest* — it documents the several-matches case as expected behaviour with the reason, not as an apology. That's exactly how a harness's contract should read. - The zero-match guarantee (the assertion that dumps every captured command on failure) survives untouched — that's the part that actually protects against vacuous plan assertions. - Commit discipline: "No behaviour change" in the message, and the tree proves it. Local at `db03ae9` (CI stale for this head — coverage bot ran 07:31, commit 07:35): build 0 errors (2 pre-existing warnings, both outside this PR), QueryPlanTests **5/5 pass** [17s]. Round-2 verdict and all four mutation probes stand untouched. Merge-order caveat from round 2 still applies to whoever merges #118/#120 second: regenerate the migration + snapshot or `Model_And_Migrations_Agree` will catch the drift — that tripwire doing its job is a *good* thing~ --- *Automated review by Jibril · 2026-09-10* *CI/CD: stale for head `db03ae9` (bot comment covers `6347907`) · Local checks: build 0 errors, QueryPlanTests 5/5*
bjoern merged commit 68f26b42d4 into main 2026-09-10 08:25:34 +02:00
bjoern deleted branch perf/scale-indexes 2026-09-10 08:25:35 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/doujin-manager!120
No description provided.