The board read is a split query, and says so #232

Merged
Grandiras merged 2 commits from board-read-split-query into main 2026-09-23 15:51:21 +00:00
Owner

The remark on CardService.GetColumnsAsync and the matching rule in docs/data.md said tags and assignees stay "in one query" because AsSplitQuery measured slower (25 ms against 18 ms). But Program.cs has made SplitQuery the app-wide default since June, and nothing in the method overrides it. So the documented single-query shape never ran.

What actually runs

I counted statements with a DbCommandInterceptor on a factory registered exactly as Program.cs registers it. GetColumnsAsync emits 5 statements: columns, cards, card tags + tag, card assignees + member, then the comment count. That's 724 rows on the documented board. With .AsSingleQuery() it's 2 statements and 600 rows, with identical results.

Measured again

Setup: the board docs/data.md describes (4 columns, 120 cards, 2 tags + 2 assignees + 5 comments a card, 780-character descriptions). The app's own CardService ran in one container and postgres:17-alpine in another, on the same podman network (the way production runs them). Figures are medians of 200–300 rounds, variants interleaved in random order.

board split (runs today) single
120 cards, 2 + 2 (the documented board), 4 runs 8.8–10.1 ms · 2.40 MiB 8.3–9.1 ms · 2.33 MiB
120 cards, 4 tags + 3 assignees 12.3 ms · 3.37 MiB 14.3 ms · 3.92 MiB
120 cards, 3,000-char descriptions 10.8 ms · 3.60 MiB 14.1 ms · 3.55 MiB
480 cards, 2 + 2 28.3 ms · 9.22 MiB 25.2 ms · 9.01 MiB
comments included, 120 cards 11.8 ms · 3.89 MiB 35.4 ms · 6.99 MiB (2,400 rows)

Single query wins by under a millisecond on the exact benchmark shape, and loses as soon as cards carry more tags and assignees or longer text. Its rows grow with tags × assignees; split's grow with tags + assignees. So split stays.

Changes

  • GetColumnsAsync now says .AsSplitQuery() in place, so its shape no longer depends on the global default. That hidden dependency is how the code and the docs came apart. Nothing changes at runtime: same 5 statements, same results.
  • Its remark, docs/data.md, docs/orientation.md §12, and the Program.cs comment (which still claimed "four collection navigations") now describe the split behaviour with the new numbers.
  • The "never a third collection include / comments counted, never loaded" rule is kept, with the cost it actually has. Under split, loading comments is 11.8 ms / 3.9 MiB against 8.8 ms / 2.4 MiB for counting them. The 2,400-row cartesian product only happens in a single query. Card.Comments, SearchService and docs/search.md repeated the "four rows a card / cartesian" reasoning, so they're corrected too.
  • docs/data.md now explains how to measure this without falling into either trap behind the old numbers:
    • Options built without the app's default. The previous figures (18 against 25 ms, and 2,400 rows / ~68 ms for loading comments) came from a harness that built its own DbContextOptions.
    • Timing through a rootless published port. Through pasta, single measured 46 ms against 9 ms even though Postgres ran it in 1.6 ms.

No changelog line: members see no difference.

Verification

  • tools/verify.sh: build clean, 560/560 unit tests pass (including DocumentationTests).
  • The benchmark was re-run against the edited code: still 5 statements, identical results.
  • E2E not run. Behaviour is unchanged, and the shared compose stack was left alone.

🤖 Generated with Claude Code

The remark on `CardService.GetColumnsAsync` and the matching rule in `docs/data.md` said tags and assignees stay "in one query" because `AsSplitQuery` measured slower (25 ms against 18 ms). But `Program.cs` has made `SplitQuery` the app-wide default since June, and nothing in the method overrides it. So the documented single-query shape never ran. ## What actually runs I counted statements with a `DbCommandInterceptor` on a factory registered exactly as `Program.cs` registers it. `GetColumnsAsync` emits **5 statements**: columns, cards, card tags + tag, card assignees + member, then the comment count. That's 724 rows on the documented board. With `.AsSingleQuery()` it's 2 statements and 600 rows, with identical results. ## Measured again Setup: the board `docs/data.md` describes (4 columns, 120 cards, 2 tags + 2 assignees + 5 comments a card, 780-character descriptions). The app's own `CardService` ran in one container and `postgres:17-alpine` in another, on the same podman network (the way production runs them). Figures are medians of 200–300 rounds, variants interleaved in random order. | board | split (runs today) | single | |---|---|---| | 120 cards, 2 + 2 (the documented board), 4 runs | 8.8–10.1 ms · 2.40 MiB | 8.3–9.1 ms · 2.33 MiB | | 120 cards, 4 tags + 3 assignees | **12.3 ms** · 3.37 MiB | 14.3 ms · 3.92 MiB | | 120 cards, 3,000-char descriptions | **10.8 ms** · 3.60 MiB | 14.1 ms · 3.55 MiB | | 480 cards, 2 + 2 | 28.3 ms · 9.22 MiB | **25.2 ms** · 9.01 MiB | | comments *included*, 120 cards | 11.8 ms · 3.89 MiB | 35.4 ms · 6.99 MiB (2,400 rows) | Single query wins by under a millisecond on the exact benchmark shape, and loses as soon as cards carry more tags and assignees or longer text. Its rows grow with tags × assignees; split's grow with tags + assignees. **So split stays.** ## Changes - `GetColumnsAsync` now says `.AsSplitQuery()` in place, so its shape no longer depends on the global default. That hidden dependency is how the code and the docs came apart. Nothing changes at runtime: same 5 statements, same results. - Its remark, `docs/data.md`, `docs/orientation.md` §12, and the `Program.cs` comment (which still claimed "four collection navigations") now describe the split behaviour with the new numbers. - The **"never a third collection include / comments counted, never loaded"** rule is kept, with the cost it actually has. Under split, loading comments is 11.8 ms / 3.9 MiB against 8.8 ms / 2.4 MiB for counting them. The 2,400-row cartesian product only happens in a single query. `Card.Comments`, `SearchService` and `docs/search.md` repeated the "four rows a card / cartesian" reasoning, so they're corrected too. - `docs/data.md` now explains how to measure this without falling into either trap behind the old numbers: - **Options built without the app's default.** The previous figures (18 against 25 ms, and 2,400 rows / ~68 ms for loading comments) came from a harness that built its own `DbContextOptions`. - **Timing through a rootless published port.** Through `pasta`, single measured 46 ms against 9 ms even though Postgres ran it in 1.6 ms. No changelog line: members see no difference. ## Verification - `tools/verify.sh`: build clean, **560/560** unit tests pass (including `DocumentationTests`). - The benchmark was re-run against the edited code: still 5 statements, identical results. - E2E not run. Behaviour is unchanged, and the shared compose stack was left alone. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The board read is a split query, and says so
All checks were successful
CI / build (pull_request) Successful in 42s
e92b19a25c
CardService.GetColumnsAsync's remark and docs/data.md said tags and
assignees stay "in one query" because AsSplitQuery measured slower
(25 ms against 18 ms). But Program.cs has made SplitQuery the default
since June, so the query has always run as four statements plus the
comment count: the documented shape never ran. The old figures came from
a harness that built its own DbContextOptions without that default.

Measured again through the app's own registration, container to
container as production runs it, on the documented board (120 cards,
2 tags + 2 assignees + 5 comments a card): single wins by under a
millisecond (~8.9 ms against ~9.6), and loses as soon as a card carries
more tags and assignees (14.3 against 12.3) or longer descriptions
(14.1 against 10.8), because its rows grow with their product. So split
stays, and GetColumnsAsync now says AsSplitQuery() in place so its shape
no longer depends on the global default.

The "never a third collection include / comments counted, never loaded"
rule stays, with its real cost: loading comments under split is
11.6 ms / 3.9 MiB against 8.8 ms / 2.4 MiB counted; the 2,400-row
cartesian product is what a single query would do (35 ms / 7.0 MiB).
docs/data.md records how to measure this without repeating either trap
(options built without the app's default; timing through a rootless
published port, which made single look 5x slower).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator

🤖 Reviewing this PR — the review will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the review runs.

🤖 Reviewing this PR — the review will link the full session transcript. <!-- opencode-run:review --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/opencode-bot/src/branch/opencode-logs/transcripts/Grandiras/Ponente/pr-232/run-2026-09-23T15-23-57-115Z.md) — updating every ~5 minutes while the review runs.
opencode-reviewer left a comment

✅ Verdict: approve — no blocking issues found.

Good to merge. The one runtime change — an explicit .AsSplitQuery() on GetColumnsAsync — is a behavioural no-op under the app-wide SplitQuery default in Program.cs, and its value is exactly what the PR says: the query's shape stops depending on a global setting that the docs had silently misdescribed. I verified the split default is indeed registered app-wide, that the documented statement layout (4 split statements plus the separate comment-count query) matches the query structure, and that the corrected "split vs single" and "comments counted, never loaded" reasoning is now consistent across CardService, Card.Comments, SearchService, docs/data.md, docs/search.md, docs/orientation.md, and the rewritten Program.cs comment. The new methodology section in data.md honestly records how the previous 18-vs-25 ms and 2,400-row/~68 ms figures went wrong (harness-built options bypassing the default; rootless published-port timing), which is the right lesson to leave behind. Keeping the rule with correct numbers rather than weakening it is the right call given the measurement table.


⏱ 4m 17s · 6 steps · $0.0146 · tokens: 69.8k in / 1.8k out (+36.1k cache-read) · Go: 5h 1% · wk 84% · mo 74%
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. Good to merge. The one runtime change — an explicit `.AsSplitQuery()` on `GetColumnsAsync` — is a behavioural no-op under the app-wide `SplitQuery` default in `Program.cs`, and its value is exactly what the PR says: the query's shape stops depending on a global setting that the docs had silently misdescribed. I verified the split default is indeed registered app-wide, that the documented statement layout (4 split statements plus the separate comment-count query) matches the query structure, and that the corrected "split vs single" and "comments counted, never loaded" reasoning is now consistent across `CardService`, `Card.Comments`, `SearchService`, `docs/data.md`, `docs/search.md`, `docs/orientation.md`, and the rewritten `Program.cs` comment. The new methodology section in `data.md` honestly records how the previous 18-vs-25 ms and 2,400-row/~68 ms figures went wrong (harness-built options bypassing the default; rootless published-port timing), which is the right lesson to leave behind. Keeping the rule with correct numbers rather than weakening it is the right call given the measurement table. --- ⏱ 4m 17s · 6 steps · $0.0146 · tokens: 69.8k in / 1.8k out (+36.1k cache-read) · Go: 5h 1% · wk 84% · mo 74% 📜 [Full session transcript](https://git.grandiras.net/Grandiras/opencode-bot/src/branch/opencode-logs/transcripts/Grandiras/Ponente/pr-232/run-2026-09-23T15-23-57-115Z.md) <!-- opencode-reviewed-head:e92b19a25c007215b58462644b1b85a70d57bf11 --> <!-- opencode-verdict:approve -->
Collaborator

🤖 Resolving the merge conflicts — the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the fix runs.

🤖 Resolving the merge conflicts — the reply will link the full session transcript. <!-- opencode-run:conflict --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/opencode-bot/src/branch/opencode-logs/transcripts/Grandiras/Ponente/pr-232/run-2026-09-23T15-28-14-726Z.md) — updating every ~5 minutes while the fix runs.
Merge remote-tracking branch 'origin/main' into board-read-split-query
All checks were successful
CI / build (pull_request) Successful in 45s
25e2836fda
# Conflicts:
#	docs/data.md
#	docs/orientation.md
#	docs/search.md
#	src/Ponente.Kanban/Data/Entities/Card.cs
#	src/Ponente.Kanban/Program.cs
#	src/Ponente.Kanban/Services/CardService.cs
#	src/Ponente.Kanban/Services/SearchService.cs
Collaborator

The merge is complete and clean. Here's the summary:

Merge resolution for #232 (board-read-split-query)

What conflicted: the branch's rewrite of the hot-read docs/comments collided head-on with #229's "debloat" rewrite of the same regions on main — and worse, main's debloated text carried two measured claims the branch's PR disproves (AsSplitQuery was slower (25 vs 18 ms), ~68 → ~18 ms), which would have contradicted the .AsSplitQuery() call the PR adds to GetColumnsAsync.

How I resolved it (7 files):

  • Kept main's debloated surfaces — the terse docs/data.md/docs/orientation.md/docs/search.md structure, #229's trimmed code comments, and all of #229's functional CardService/SearchService changes (constants, PlaceAt/BottomOfAsync, boardsById, deleted BoardOfCardAsync, etc.).
  • Kept the PR's substance — the explicit .AsSplitQuery() on GetColumnsAsync, the "it is a split query, and says so in place" rule with the PR's corrected figures (8.9 vs 9.6 ms on the measured board; 14.3 vs 12.3 / 14.1 vs 10.8 off it; 2,400 rows at 35 ms / 7.0 MiB; 11.6 vs 8.8 ms, 3.9 vs 2.4 MiB counted), orientation item 12 ("it is a split query… all three are measured"), and the search page's "split like every query here" clause.
  • Dropped the two disproved measurement claims, replacing them with the PR's trap note: the old figures came from a harness that built its own DbContextOptions without the split default already in Program.cs, and measuring through pasta's port forwarding inflates timings — so the corrected numbers and the "how it was measured" note live in docs/data.md, with the in-code comments carrying the terse version.

Verified: no conflict markers left; dotnet build Ponente.sln clean (0 warnings); full unit suite green (558 passed, including DocumentationTests); .AsSplitQuery() confirmed in the merged GetColumnsAsync; no stale "25 vs 18" claims outside the historical trap note. The final diff vs main is exactly the PR's change, now expressed in main's debloated style.

→ Merge resolved and pushed; a re-review follows.


⏱ 13m 45s · 48 steps · $0.2075 · tokens: 590.2k in / 32.6k out (+3112.3k cache-read) · Go: 5h 3% · wk 84% · mo 75%

📜 Full session transcript

The merge is complete and clean. Here's the summary: ## Merge resolution for #232 (board-read-split-query) **What conflicted:** the branch's rewrite of the hot-read docs/comments collided head-on with #229's "debloat" rewrite of the same regions on main — and worse, main's debloated text carried two measured claims the branch's PR disproves (`AsSplitQuery was slower (25 vs 18 ms)`, `~68 → ~18 ms`), which would have contradicted the `.AsSplitQuery()` call the PR adds to `GetColumnsAsync`. **How I resolved it (7 files):** - **Kept main's debloated surfaces** — the terse `docs/data.md`/`docs/orientation.md`/`docs/search.md` structure, #229's trimmed code comments, and all of #229's functional `CardService`/`SearchService` changes (constants, `PlaceAt`/`BottomOfAsync`, `boardsById`, deleted `BoardOfCardAsync`, etc.). - **Kept the PR's substance** — the explicit `.AsSplitQuery()` on `GetColumnsAsync`, the "it is a split query, and says so in place" rule with the PR's corrected figures (8.9 vs 9.6 ms on the measured board; 14.3 vs 12.3 / 14.1 vs 10.8 off it; 2,400 rows at 35 ms / 7.0 MiB; 11.6 vs 8.8 ms, 3.9 vs 2.4 MiB counted), orientation item 12 ("it is a split query… all three are measured"), and the search page's "split like every query here" clause. - **Dropped the two disproved measurement claims**, replacing them with the PR's trap note: the old figures came from a harness that built its own `DbContextOptions` without the split default already in `Program.cs`, and measuring through `pasta`'s port forwarding inflates timings — so the corrected numbers and the "how it was measured" note live in `docs/data.md`, with the in-code comments carrying the terse version. **Verified:** no conflict markers left; `dotnet build Ponente.sln` clean (0 warnings); full unit suite green (558 passed, including `DocumentationTests`); `.AsSplitQuery()` confirmed in the merged `GetColumnsAsync`; no stale "25 vs 18" claims outside the historical trap note. The final diff vs `main` is exactly the PR's change, now expressed in main's debloated style. → Merge resolved and pushed; a re-review follows. --- ⏱ 13m 45s · 48 steps · $0.2075 · tokens: 590.2k in / 32.6k out (+3112.3k cache-read) · Go: 5h 3% · wk 84% · mo 75% 📜 [Full session transcript](https://git.grandiras.net/Grandiras/opencode-bot/src/branch/opencode-logs/transcripts/Grandiras/Ponente/pr-232/run-2026-09-23T15-28-14-726Z.md) <!-- opencode-session:ses_f311e41e4ffeQfbFbZlLBjRebC -->
Collaborator

🤖 On it — the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — the reply will link the full session transcript. <!-- opencode-run:task --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/opencode-bot/src/branch/opencode-logs/transcripts/Grandiras/Ponente/issue-232/run-2026-09-23T15-50-17-701Z.md) — updating every ~5 minutes while the task runs.
Grandiras deleted branch board-read-split-query 2026-09-23 15:51:22 +00:00
Collaborator

No new comment asked anything of me — the thread was quiet. The one new requirement in the follow-up was to verify with the repo's own entry point, so I did that:

  • The fresh checkout turned out to be at task-232-muea4lt1 (the session's original directory no longer existed), and it carries the merge resolution as committed: 25e2836 Merge remote-tracking branch 'origin/main' into board-read-split-query on board-read-split-query, clean tree.
  • tools/verify.sh (default check mode — build + unit tests in Release, what CI runs): build succeeded, 0 warnings, 558/558 tests passed. This supersedes my earlier hand-rolled dotnet build/dotnet test run and confirms the same result through the sanctioned entry point.

The merge resolution itself is unchanged from my previous reply: main's debloated surfaces kept, the PR's .AsSplitQuery() and corrected measurements carried through, main's two disproved measurement claims replaced by the trap note in docs/data.md. Nothing further outstanding on #232.


⏱ 2m 56s · 60 steps · $0.2754 · tokens: 724.3k in / 33.9k out (+4526.7k cache-read) · Go: 5h 7% · wk 86% · mo 76%

📜 Full session transcript

No new comment asked anything of me — the thread was quiet. The one new requirement in the follow-up was to verify with the repo's own entry point, so I did that: - The fresh checkout turned out to be at `task-232-muea4lt1` (the session's original directory no longer existed), and it carries the merge resolution as committed: `25e2836 Merge remote-tracking branch 'origin/main' into board-read-split-query` on `board-read-split-query`, clean tree. - **`tools/verify.sh`** (default `check` mode — build + unit tests in Release, what CI runs): build succeeded, 0 warnings, **558/558 tests passed**. This supersedes my earlier hand-rolled `dotnet build`/`dotnet test` run and confirms the same result through the sanctioned entry point. The merge resolution itself is unchanged from my previous reply: main's debloated surfaces kept, the PR's `.AsSplitQuery()` and corrected measurements carried through, main's two disproved measurement claims replaced by the trap note in `docs/data.md`. Nothing further outstanding on #232. --- ⏱ 2m 56s · 60 steps · $0.2754 · tokens: 724.3k in / 33.9k out (+4526.7k cache-read) · Go: 5h 7% · wk 86% · mo 76% 📜 [Full session transcript](https://git.grandiras.net/Grandiras/opencode-bot/src/branch/opencode-logs/transcripts/Grandiras/Ponente/issue-232/run-2026-09-23T15-50-17-701Z.md)
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 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
Grandiras/Ponente!232
No description provided.