Skip to content

fix(go): offset pagination with offsetSemantics: item-index generates compiling SDKs - #17364

Open
devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1786375473-go-item-index-offset-pagination
Open

fix(go): offset pagination with offsetSemantics: item-index generates compiling SDKs#17364
devin-ai-integration[bot] wants to merge 5 commits into
mainfrom
devin/1786375473-go-item-index-offset-pagination

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Description

offsetSemantics: item-index + a pagination step produced Go SDKs that do not compile — reproduced against released fernapi/fern-go-sdk:1.55.0, so this is not a regression from any open PR:

things/client.go:96:19: undefined: results
next += int(len(results))              // uses results...
results := response.GetItems()         // ...declared on the next line

Three distinct defects, each in its own commit:

  1. Compile break (undefined: results)getReadPageResponseBodyForOffset wrote the increment before getNextResultsSetter emitted results := response.GetX(). Only the item-index branch is reordered, so page-index output (next += 1) is untouched. The increment is also now typed to the page property (int64(len(results)) / float64(len(results))), which previously could not compile for non-int offsets.
  2. Compile break on required query-parameter offsets — when the offset is a required (non-optional) query parameter, getPagePropertyInitializer's non-optional branch emitted request.Offset := fmt.Sprintf("%v", pageRequest.Cursor): invalid Go (non-name request.Offset on left side of :=), plus pageRequest out of scope. It now emits next := request.Offset. Independent of offsetSemantics — verified on released 1.55.0 with page-index and a required int query offset, which fails the same way. This does not apply to request-body offsets under feat(go): opt-in auto-pagination for request body cursors and offsets #17352, which take that PR's own getRequestBodyOffsetInitializer path and already emit next := request.Offset.
  3. Off-by-one (behavior change)getOffsetInitializer hardcoded next := 1 for every numeric page type, so item-index pagination skipped the first record of every collection. Item-index now starts at 0; page-index still starts at 1.

Changes Made

  • generators/go-v2/sdk/src/endpoint/utils/getPaginationInfo.ts: declare results before the item-index increment, type the increment to the page type, seed required offsets from the request, and start item-index offsets at 0.
  • New seed fixture go-pagination-offset-item-index (offsetSemantics: item-index) covering an optional query-param offset, a required query-param offset (the customer-facing shape), and a request-body offset. No fixture previously combined step with item-index semantics, which is why this shipped.
  • Changelog entries under generators/go/sdk/changes/unreleased/ (one per concern).

Generated output for the new fixture:

next := 0                              // was: next := 1
...
results := response.GetPlants()        // now declared first
next += int(len(results))

next := request.Offset                 // was: request.Offset := fmt.Sprintf(...)

Note on the request-body endpoint: on main, body-property offsets still generate a delegating (non-paginated) endpoint, so that endpoint's snapshot does not exercise the pager yet. It starts exercising this path — with no further generator changes needed — once #17352 (enableRequestBodyPagination) lands; this PR does not touch or depend on that branch.

Testing

  • Reproduced against released fern-go-sdk:1.55.0 with a minimal GET + query-param offset spec (undefined: results), and separately with a required query-param offset under page-index (non-name request.Offset on left side of :=).
  • Behavioral verification against a mock HTTP server (asserting on the offsets the server actually received, not on generated source) — item-index sends 0, 2, 4, honors a caller-supplied starting offset, terminates on the empty page, and concatenates pages with no gaps or duplicates; page-index still sends 1, 2, 3. Full observed sequences in the PR comment below.
  • seed test --generator go-sdk --fixture go-pagination-offset-item-index passes with build (go build, golangci-lint) and test scripts — the fixture failed to build before the fix with the same undefined: results error.
  • Regenerated pagination, pagination-custom, pagination-uri-path for go-sdk: zero diff, so page-index behavior is provably unchanged.
  • Manual testing completed

Link to Devin session: https://app.devin.ai/sessions/45f5319518da45f9bb66779546d36e9f


Open in Devin Review

devin-ai-integration Bot and others added 3 commits August 10, 2026 15:24
…d required offsets

Co-Authored-By: bot_apk <apk@cognition.ai>
Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review Summary

Reorders the item-index offset increment after the results declaration, types the increment to the page type, fixes the required-offset initializer, and starts item-index offsets at 0. Logic looks right for the int/int64/float64 paths; two gaps remain around non-numeric page types and item-index without a step.

  • 🟡 1 warning(s)
  • 🔵 1 suggestion(s)

Comment on lines +411 to +419
const underlying = pageType.underlying();
switch (underlying.internalType.type) {
case "int64":
return "next += int64(len(results))";
case "float64":
return "next += float64(len(results))";
default:
return "next += int(len(results))";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 warning

The default branch still emits next += int(len(results)), which won't compile when the page property is a string (var next string = "0") or uuid. Since getOffsetInitializer explicitly handles those cases, this function should too — either emit a string-safe increment or fall back to page-index behavior rather than generating broken Go. Same class of bug the PR is fixing, just one type away.

Also note int(len(results)) is a redundant conversion (len already returns int); next += len(results) reads better for the int case.

context: SdkGeneratorContext;
offset: FernIr.OffsetPagination;
}): boolean {
return offset.step != null && context.customConfig.offsetSemantics === "item-index";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

usesItemIndexOffset gates on offset.step != null, so an API configured with offsetSemantics: item-index but no step still generates page-index behavior (next := 1 / next += 1). This preserves prior behavior, but given the PR's framing (item-index offsets address records), it's worth confirming that's intentional rather than a second off-by-one waiting to be reported.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +410 to +420
function getOffsetIncrementByResultCount({ pageType }: { pageType: go.Type }): string {
const underlying = pageType.underlying();
switch (underlying.internalType.type) {
case "int64":
return "next += int64(len(results))";
case "float64":
return "next += float64(len(results))";
default:
return "next += int(len(results))";
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Generated pagination code still fails to compile when the page marker is text-based

The per-page advance amount is always produced as a whole number (getOffsetIncrementByResultCount at generators/go-v2/sdk/src/endpoint/utils/getPaginationInfo.ts:410-420) even when the page marker is text- or id-shaped, so the generated SDK does not build for those APIs.
Impact: Customers whose pagination marker is a string or UUID still get an SDK that fails to compile under item-index semantics.

Type switch omits the string/uuid page types that the initializer explicitly supports

getOffsetInitializer (generators/go-v2/sdk/src/endpoint/utils/getPaginationInfo.ts:615-632) explicitly supports string (var next string = "0") and uuid (var next uuid.UUID) page types. For those same types, getOffsetIncrementByResultCount falls through to the default branch and emits next += int(len(results)), which is invalid Go (invalid operation: next += int(...) (mismatched types string and int)).

This is pre-existing for the page-index branch as well (next += 1 on a string), but the PR's stated goal is that offsetSemantics: item-index generates compiling SDKs, and the newly added type switch covers only int/int64/float64.

A reasonable fix is to only apply item-index increment logic for numeric page types (and fall back to page-index behavior or raise a clear generator error otherwise).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed the behavior, but it's pre-existing and orthogonal to this PR, so I've left it alone.

A string/uuid offset never compiled under either semantics: page-index emits next += 1 against var next string = "1", which is just as invalid as next += int(len(results)). getOffsetInitializer handling those types doesn't mean the pager ever worked for them — nothing downstream can increment a string offset. So this PR neither introduces nor widens the breakage; it fixes the numeric cases that are reachable in practice.

The suggested remedies are behavior decisions rather than bug fixes: silently falling back to page-index would send the wrong offsets, and failing generation would break any spec that currently generates (broken) output. Both deserve their own change — happy to open a follow-up if you'd like, ideally erroring out at generation time with a clear message for non-numeric offset page types.

Co-Authored-By: bot_apk <apk@cognition.ai>
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-10T04:30:32Z).

Fixture main PR Delta
docs 260.2s (n=5) 263.9s (35 versions) +3.7s (+1.4%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-08-10T04:30:32Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-10 17:39 UTC

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-10T04:30:32Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 90s (n=5) N/A 64s -26s (-28.9%)
go-sdk square 145s (n=5) 303s (n=5) 125s -20s (-13.8%)
java-sdk square 233s (n=5) 281s (n=5) 222s -11s (-4.7%)
php-sdk square 80s (n=5) N/A 73s -7s (-8.8%)
python-sdk square 151s (n=5) 256s (n=5) 137s -14s (-9.3%)
ruby-sdk-v2 square 109s (n=5) 140s (n=5) 82s -27s (-24.8%)
rust-sdk square 228s (n=5) 236s (n=5) 196s -32s (-14.0%)
swift-sdk square 78s (n=5) 453s (n=5) 55s -23s (-29.5%)
ts-sdk square 176s (n=5) 189s (n=5) 124s -52s (-29.5%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-08-10T04:30:32Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-10 17:40 UTC

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author
Behavioral verification against a mock HTTP server — all scenarios passed

Rather than asserting on generated source, the generated pagers were run against httptest servers that log every inbound request's query string, asserting on what the server received.

item-index, no caller offset (corpus of 5, count=2):

offsets = [0 2 4 5]
queries = count=2&offset=0 | count=2&offset=2 | count=2&offset=4 | count=2&offset=5
results = [item-0 item-1 item-2 item-3 item-4]   (4 requests, terminated on empty page)

Starts at 0 (previously 1) and advances by len(results) — including the +1 step after the short final page.

item-index, caller passes offset=5 (corpus of 10, count=2):

offsets = [5 7 9 10]
results = [item-5 item-6 item-7 item-8 item-9]

The caller's starting offset is honored, not overwritten.

Seed fixture plants.ListWithRequiredOffset (required Offset: 3, Count: 2, corpus of 7):

offsets = [3 5 7]
results = [plant-3 plant-4 plant-5 plant-6]

Seed fixture plants.List (optional offset, none supplied, corpus of 7):

offsets = [0 2 4 6 7]
results = [plant-0 plant-1 plant-2 plant-3 plant-4 plant-5 plant-6]

Every loop was bounded at 200 iterations; all terminated naturally on the first empty page. Collected results matched the expected corpora exactly — no gaps, duplicates or reordering.

Regression: offsetSemantics: page-index (default) unchanged

Same OpenAPI spec regenerated with page-index:

offsets = [1 2 3]
queries = count=2&offset=1 | count=2&offset=2 | count=2&offset=3
results = [item-0 item-1 item-2 item-3]

Still 1-based and still next += 1.

Not covered

ListWithBodyOffset (not generated as a pager on main), non-int offset types (the string / int64 / float64 / uuid branches of getOffsetInitializer), and cursor pagination.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants