Pager.Take: take n rows without paying for a page it will not read - #17
Merged
Conversation
ExaltedTrou6
force-pushed
the
feat/pager-take
branch
from
August 6, 2026 18:21
3019928 to
2b1c14c
Compare
The obvious way to write "the first 45 deals" spends a request nobody reads. A break inside the range over Rows leaves the OUTER condition to be evaluated again, and that condition is a call to the portal, so the code compiles, looks right, returns the right rows and quietly spends a rate-limit token on a page it discards. The form that avoids it is the bound in the loop header, which one has to know. Take is that form, packaged. It keeps the rows it fetched but did not hand over, so stopping in the middle of a page costs neither those rows nor another request: the next Take or Next gets them without going to the portal. The cursor follows what was FETCHED rather than what was handed over, or a Scan stopped at row 45 of 50 would ask for the next page from 46 and deliver five rows twice. Count now reports rows handed over rather than rows fetched, which makes it honest for Take (45, not 50). For Next it is unchanged: Next hands over a whole page. The half that cannot be fixed — a break inside the caller's own loop is invisible from here — is documented rather than guessed at. Verified on a live portal of 54 deals with the HTTP requests counted on the wire: Take(ctx, 45) sends 1 request and reports Count 45, the hand-written loop sends 2 and reports 54, and Take followed by Next still sends 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ExaltedTrou6
force-pushed
the
feat/pager-take
branch
from
August 6, 2026 18:35
2b1c14c to
72feb49
Compare
Contributor
Author
|
Дополнение по ходу проверки: нашёлся и закрыт единственный цикл, которым Сервер, отвечающий «строк нет, но продолжение есть» с двигающимся курсором, заставлял Теперь такая страница заканчивает
Закреплено тестом |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
A live run of the SDK against a portal (50 deals in one batch + 45 of them listed through
Pages) exposed that the natural way to write "take N rows" silently spends an extra HTTP request:breakleaves the inner loop only. The outer condition is evaluated once more — and that is a request for a page nobody will read. The code compiles, it looks right, it hands over the right rows; the only thing wasted is a rate-limit token. The correct form is the bound in the loop header (for len(taken) < want && p.Next(ctx)), but one has to know it. This is exactly the class of mistakePageswas built for.Pager.Take(ctx, n)is that form, packaged.nrows only at the end of the list or on error; the error is returned alongside the rows it did read (a walk that failed on page three still read pages one and two).Take/Nexthands them over without going to the portal.TakeandNextmix freely.Scanstopped at the 45th row of a 50-row page would ask for the next page starting at the 46th and hand five rows over twice.n <= 0sends nothing.Count()It was lying when a walk stopped in the middle of a page:
Count() == 50with 45 rows taken. Renaming it is not an option, so:Count()now counts the rows handed over, not the rows fetched. ForNextthat is the same number (Nexthands over a whole page); forTakeit is the honest one: 45, not 50.Pagercannot fix, and does not pretend to: abreakinside the caller's loop never reaches it. The godoc ofCountsays so outright, together with the advice to take rows withTakewhen their number is what matters.Checks
go build ./...go vet ./...gofmt -l .— no outputgo test -race ./...Compatibility
Count()changes meaning only where there was no value before: for a walk withoutTakethe number is the same as it was.Tests
pager_take_test.go— everything goes through a counter of HTTP requests, because that is exactly where it shows whyTakeis needed: the rows the wrong form hands over are the very same ones.TestTakeSpendsNoRequestOnAPageItWillNotRead—Take(45)= 1 request, the same result through a loop withbreak= 2. The second half of the test pins the difference itself, so that aTakewhich quietly brings the extra request back fails here rather than on a customer's quota.TestTakeStopsExactlyOnAPageBoundary—Take(50)on a page of 50 rows does not reach for a second one.TestTakeCrossesPagesAndStops,TestTakeReturnsShortAtTheEndOfTheList,TestTakeZeroAsksThePortalForNothing.TestTakeThenNextContinuesInsideTheSamePage— the rest of the page is neither lost nor re-read.TestCountCountsRowsHandedOverNotRowsFetched.TestTakeReturnsWhatItReadWhenTheWalkFails— the rows arrive alongside the error.TestScanTakeKeepsTheCursorOnTheFetchedPage— the cursor follows what was fetched; otherwise the export holds duplicates, and duplicates are nowhere an error, they are simply wrong.The tests were checked with mutations: a
Takeimplemented on top ofNextwith truncation, and aNextthat ignores the rows not yet handed over — both fail the set.Verified on a live portal (54 deals, HTTP requests counted on the wire by a wrapper transport):