fix: ensure count is never null in GET /refractor/cards (#182) #184
No reviewers
Labels
No Label
ai-changes-requested
ai-failed
ai-merged
ai-pr-opened
ai-reviewed
ai-reviewing
ai-reviewing
ai-working
bug
enhancement
evolution
performance
phase-0
phase-1a
phase-1b
phase-1c
phase-1d
security
tech-debt
todo
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/paper-dynasty-database#184
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "issue/182-bug-get-refractor-cards-returns-count-null-with-ou"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #182
Summary
GET /api/v2/refractor/cardscould return{"count": null, "items": []}when a filter combination yielded 0 matching rows. Peewee 3.17.9's.count()on a complex multi-join query (three-table JOIN with LEFT OUTER) can returnNonewhen the cursor wrapper produces an empty result for a 0-row count query.Fix
app/routers_v2/refractor.py— one line:Guards the count result with
or 0so the response contract (countis always an integer) is always upheld.Additional context
The reported reproduction step (
tier=99) is already rejected upstream by FastAPI'sge=0, le=4constraint on thetierparameter, returning a 422 before the query runs. Theor 0guard covers the remaining cases where a valid filter combination (e.g.tier=4with no T4 cards) produces 0 results.Files changed
app/routers_v2/refractor.py—total = query.count() or 0Other observations
The
GET /api/v2/teams/{team_id}/refractorsendpoint inteams.py(line 1579) has the sametotal = query.count()pattern and noge=0, le=4constraint ontier— it would be susceptible to the same null-count bug for any 0-row result, includingtier=99. Out of scope for this issue but worth a follow-up.AI Code Review
Files Reviewed
app/routers_v2/refractor.py(modified)CLAUDE.md(modified)Findings
Correctness
query.count() or 0maps bothNoneand0to0while leaving positive integers unchanged. The response contract (countis always an integer) is upheld in all cases..count()on aModelSelectwith multi-model.select()wraps the original query as a subquery (SELECT COUNT(1) FROM (...) AS t1). If the cursor wrapper returns no rows rather than a(0,)row, the result isNonebefore Python coerces it. The guard is the right defensive fix.cc65dc5already onmain(merged 2026-04-06). The PR branch predates that merge. Depending on git's merge strategy this will either auto-resolve cleanly (same change on both sides) or produce a trivially resolvable conflict — worth a quick check before merging.Security
ge=0, le=4ontier,ge=1, le=100onlimit). No raw SQL or user-controlled expressions.Style & Conventions
Suggestions
teams.pyGET /{team_id}/refractorsendpoint (line 1579) has the sametotal = query.count()pattern and noge=0, le=4guard ontier, making it susceptible to the same null-count contract violation for any 0-row result (includingtier=99). The PR author already flagged this as a follow-up — tracking issue recommended.Verdict: APPROVED (posted as COMMENT — self-review restriction)
Clean, targeted, correct fix. The one-line change directly addresses the null-count contract violation with no side effects. The CLAUDE.md merge note is cosmetic and non-blocking.
Automated review by Claude PR Reviewer
Checkout
From your project repository, check out a new branch and test the changes.