fix(pagination): offset slice math returns wrong rows; add keyset cursors - #10
fix(pagination): offset slice math returns wrong rows; add keyset cursors#10frankstupak wants to merge 1 commit into
Conversation
|
CI has now run on this branch for the first time — the earlier runs sat unapproved (fork PR, first-time-contributor policy) and expired, so nothing was ever reported here. Result: failure on Node 20, in this PR's own README. Node 18 and 22 were cancelled once 20 failed. Line 64 is the row you added:
Worth knowing before you re-push: |
|
Status update. Two things had been masking the real verdicts:
This one is now the only thing standing between the branch and a merge — the failure is in your own change, detailed in the comment above. Once it is fixed, rebase or use "Update branch" so CI runs against current Also filed #15 for a flake you may hit on a re-run: |
…strict query validation - NEW cursor-based strategy (Stripe/Slack/GitHub pattern): opaque base64url cursor encoding the (sortBy value, id) anchor tuple; id tiebreaker makes non-unique sort fields safe; forward/backward via nextCursor/prevCursor; cursors scoped to sort+filter and rejected with InvalidCursorError when replayed against a different scope; anchor deletion tolerated. Binary-search seek: 1M-row full sweep 112ms vs 17.9s naive linear seek (~160x), single page at 90% depth ~430x (bench/cursor-bench.ts, npm run bench). - Fixed silent wrong-rows bug: fractional page/offset/limit flowed raw into Array.slice (page=1.5, limit=2 returned rows 2-3); NaN limit produced NaN totalPages + empty page with a 200. All numeric inputs now integer-normalized (src/normalize.ts). - Fixed lax server parsing: parseInt accepted "5abc" as 5 and truncated "1.5" to 1; numeric query params now strictly validated (400 on garbage). - /employees/cursor-based endpoint + cursor_based support in /employees/paginate. - require.main guard on startServer: importing the module no longer binds :3001, enabling a new HTTP inject() suite (server.test.ts). - /employees/stats single-pass counting (was O(departments x employees)). - fastify + @fastify/swagger added to subproject deps (standalone install). - +45 tests (81 total), incl. offset dup/skip failure demos vs cursor immunity.
ba4d42f to
b85f6fe
Compare
|
Rebased onto main, both checks green now. The failure was a markdown-lint table-alignment error in Ready for review whenever you get a chance. |
|
Sorry for the delay — you cleared this on Aug 30 and it sat. Confirmed: Before I review the substance, one scope question. This PR is
That's 23,989 lines of brand-new lockfiles for three sub-packages this PR doesn't otherwise touch — none of them exist on Was that deliberate, or did a Two reasons beyond review ergonomics:
Strip those three and I'll review the cursor/keyset work properly — that part I'm interested in, and the offset slice math bug is worth having fixed. |
What was wrong
1. No cursor pagination at all. The README correctly warns that offset result sets "can shift during pagination" — and then ships no fix. Insert one row at the head of the list between two requests and offset page 2 re-serves the last row of page 1; delete three rows and it silently skips rows 6–8. Both failure modes are now demonstrated in tests against the shipped implementation.
2. Silent wrong-rows math.
page/offset/limitflowed raw intoArray.slice.page=1.5, limit=2→slice(1, 3)→ returns rows 2 and 3 with a 200 and no error.limit=NaN→totalPages: NaNand an empty page, also a 200.3. Lax query parsing.
parseIntacceptedpage=5abcas 5 and truncatedpage=1.5to 1 — client typos got wrong data instead of a 400.4. Untestable server.
startServer()ran at module load, so importingcreateAppfrom a test bound port 3001. There were zero HTTP-level tests.5.
/employees/statsrescanned the full dataset once per department (O(d×n)).What's better
Cursor-based (keyset) pagination — the Stripe/Slack/GitHub pattern:
(sortBy value, id)anchor tuple. Theidtiebreaker makes non-unique sort fields safe: sweeping 101 rows sorted by a 3-value department field returns every row exactly once (tested).nextCursor/prevCursor;nextCursor: nullterminates.InvalidCursorError→ clean 400, never silently wrong rows.findIndexa first-pass implementation uses.Benchmarks (
npm run bench, 1,000,000 rows, 100/page, committed atbench/cursor-bench.ts):Fixes:
src/normalize.ts); fractional/NaN/Infinity can no longer reach slice math. Regression tests pin the old wrong-rows behavior.page=5abc,page=1.5,offset=2.5,limit=10x→ 400.GET /employees/cursor-based+type=cursor_basedon the unified/employees/paginate.require.mainguard → new HTTPinject()suite (server.test.ts) covering cursor sweeps, scope rejection, and the strict-parsing regressions./employees/statssingle pass.fastify+@fastify/swaggeradded to the subproject's own deps so a standalonenpm install && tscworks.Compatibility: everything additive. Existing exports, endpoints, and response shapes unchanged; all 36 original tests pass untouched.
Numbers
npm run test:all: 784 passed, 0 failedtsc --noEmitclean,eslintclean— Lumen Industries