fix(home,users): add unique tiebreakers to non-total ORDER BY clauses (bookshelf-cehm) #898
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-cehm"
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?
Summary
Three home-dashboard rails and one users query ordered by a non-unique timestamp column then applied LIMIT — ties produce arbitrary ordering, causing both UI inconsistency and potential test flakes (the review-standard non-total-order flake pattern).
continueReadingSQL(home/service.go:358):ORDER BY ubp.last_read_time DESC→+ ubp.book_id DESCrecentlyFinishedSQL(home/service.go:459):ORDER BY ubp.date_finished DESC→+ ubp.book_id DESConDeckSQL(home/service.go:893):ORDER BY us.last_series_read DESC→+ b.id DESCGetAdminUserContentRestrictions(users/content_restriction.go:70):ORDER BY restriction_type, mode, value→+ id ASCMirrors the existing pattern in
lastReadBookSQLwhich already uses, ubp.book_id DESCas a tiebreaker.Test plan
make testpassesmake coveragepasses (100% gate green)golangci-lint run ./internal/home/... ./internal/users/...— 0 issuesCloses bead bookshelf-cehm on merge.
Workflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)
fbe9b12dd530564da540Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)
Workflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
Security Review — bookshelf-cehm (PR #898)
Scope: ORDER BY tiebreaker additions in
internal/home/service.goandinternal/users/content_restriction.go.SQL Injection
All four tiebreakers (
ubp.book_id DESC,b.id DESC,id ASC) are static string literals baked directly into Govarorconstquery strings at compile time. No user-supplied value participates in the ORDER BY construction.buildRailQueryinjects only parameterized?placeholders for library IDs, not column names. No injection surface exists.Multi-user Scoping
WHERE clauses are untouched by the diff:
continueReadingSQL:WHERE ubp.user_id = ?— unchanged, scoped by session userID.recentlyFinishedSQL:WHERE ubp.user_id = ?— unchanged, scoped by session userID.onDeckSQL: CTE scoped byubp.user_id = ?; both anti-join subqueries useubp2.user_id = ?andubp3.user_id = ?— unchanged, all three bind to the session userID.GetAdminUserContentRestrictions:WHERE user_id = ?— unchanged; the function is wired ininternal/users/wire.gobehindd.AdminRequiredwith no change in this diff.Adding a tiebreaker column to ORDER BY cannot widen a result set or cross user boundaries.
Information Leak
Ordering affects row sequence only, never row membership. The tiebreaker columns (
ubp.book_id,b.id,id) are already part of the query's join graph and do not surface new data. No information leak is possible.Architecture Boundary
internal/home/service.goimports only stdlib +internal/books+internal/library.internal/users/content_restriction.goimports only stdlib. No workflow-engine package referenced.Test Hygiene
Both test files declare
package home_test/package users_test(black-box). The query-capture tests work via the curried-function pattern: the innerBeforeEachoverrideslistCR/queryFnwith a capturing stub; the outerJustBeforeEachcalls the function, populatingcapturedQuery; theItblock asserts on the captured string. No unexported symbols are accessed.REVIEW VERDICT: 0 blocker, 0 major, 0 minor
CODE REVIEW: APPROVED
Phase 0 — DEMO Verification
No DEMO block in the bead. This is a pure SQL-string tiebreaker fix with no interactive output to demonstrate. The captured-query unit tests added in this PR serve as the practical verification mechanism. Proceeding per "PARTIAL with valid reason" treatment.
Phase 1 — Spec Compliance
Bead description lists 4 targets:
continueReadingSQL,recentlyFinishedSQL,onDeckSQLininternal/home/service.go, andGetAdminUserContentRestrictionsininternal/users/content_restriction.go. All 4 are present and addressed. No extra unneeded changes found.Phase 2 — Code Quality
Total-order correctness
continueReadingSQL—internal/home/service.go:358Primary:
ubp.last_read_time DESC. Tiebreaker:ubp.book_id DESC.Query filters
WHERE ubp.user_id = ?; within a single user,(user_id, book_id)is unique inuser_book_progress. Tiebreaker is unique within the result set. Total order achieved. ✓recentlyFinishedSQL—internal/home/service.go:459Primary:
ubp.date_finished DESC. Tiebreaker:ubp.book_id DESC.Same reasoning:
ubp.book_idis unique per user inuser_book_progress. Total order achieved. ✓onDeckSQL—internal/home/service.go:893Primary:
us.last_series_read DESC. Tiebreaker:b.id DESC.b.idis thebooktable PK — unconditionally unique across the result set. Total order achieved. ✓GetAdminUserContentRestrictions—internal/users/content_restriction.go:70Primary keys:
restriction_type ASC, mode ASC, value ASC. Tiebreaker:id ASC.idis the PK ofuser_content_restriction, unconditionally unique. Total order achieved. ✓Other ORDER BY + LIMIT patterns in touched files verified:
service.go:50(lastReadBookSQL): already hasubp.book_id DESCtiebreaker — bead description noted this and left it alone. ✓service.go:676(ORDER BY b.id LIMIT ?):b.idis PK, already a total order. ✓service.go:562(authors batch query):ORDER BY bm.book_id, bm.sort_orderwith no LIMIT clause, scoped byWHERE bm.book_id IN (...). Not a flake risk for this pattern. ✓Behavior preservation
All four tiebreakers are appended as the final key in their respective ORDER BY clauses. Primary sort semantics (recency of last read, finish date, last series read, restriction type/mode/value) are completely unchanged. ✓
Multi-user scoping
Content restriction query retains
WHERE user_id = ?. Home rail queries retainWHERE ubp.user_id = ?. No scoping changes. Fail-closed. ✓Test quality
[MINOR]
internal/home/service_test.go(tiebreaker contexts for ListContinueReading, ListRecentlyFinished, ListOnDeck),internal/users/content_restriction_test.go(tiebreaker context) — Tests assert SQL string presence, not deterministic ordering behavior.The new
Itblocks capture the query string and assertContainSubstring("ubp.book_id DESC")/ContainSubstring(", id ASC"). This pins that the fix is textually in place, but does not demonstrate that two books with identicallast_read_timereturn inbook_idorder. Given the curried-function stub architecture (no real DB in unit tests) this is the practical limit at the unit tier. The deterministic ordering behavior would require an integration test with deliberately tied timestamps. This does not block.Ginkgo structure confirmed correct: outer
BeforeEachsets the list function, innerBeforeEachoverrides it with a capturing version, outerJustBeforeEachinvokes it —capturedQueryis set before theItassertion runs. ✓Other checks
package home_test,package users_testconfirmed. ✓Itblock has exactly oneExpect. ✓var capturedQuery stringdeclared at top of eachContext. ✓Verdict
Approved. The 1 minor (unit tests check SQL text rather than ordering behavior) is an inherent limitation of the stub-function test architecture, not a defect in the fix itself. All four tiebreakers are correct unique columns and all primary sort semantics are preserved.
30564da5407e11614a3bWorkflow Detail page screenshot (wf-detail-older-execution)
Older completed ContinueAsNew epoch detail — execution ID and state visible, Cancel absent.
Recompute Match Score — kebab open screenshot (recompute-match-score-kebab-open)