Skip to content

api/handlers: validators listing reports total: 0 for pages past the end of the set #750

Description

@nikw9944

Summary

/api/solana/validators reports total: 0 and on_dz_count: 0 for any page past the end of the result set, instead of the real whole-set counts. The web pager reads response.total (web/src/components/validators-page.tsx:244), so a client that lands past the end sees an empty table and a collapsed pager rather than a way back.

Cause

fetchValidatorsPage (api/handlers/validators.go) gets its whole-set aggregates from window functions in the same SELECT as the page:

count() OVER () as _total,
countIf(on_dz = true) OVER () as _on_dz_count
...
LIMIT ? OFFSET ?

total and onDZCount are only assigned inside the rows.Next() scan loop, so a page that returns zero rows leaves both at their zero value. The counts are correct for every page that returns at least one row.

This affects all shapes that go through the live path — filtered, non-stake-sorted, and any deep page — not just the unfiltered stake-desc listing.

Why it's filed separately

#746 made this visible rather than introducing it. That PR's cached path returns the true whole-set counts (it slices a complete cached set, so the aggregates don't depend on the page), which means the two paths now disagree for offset >= total:

path total at offset >= total
cached (sliceCachedValidators) real count
live (fetchValidatorsPage) 0

Both sides are pinned by TestGetValidators_CachedPageMatchesLive so neither can drift silently, and the reasoning is commented at both sites. The cached path is the more correct of the two.

It wasn't fixed in #746 because a correct fix needs the whole-set count for a query that returns no rows — i.e. a second aggregate query on that path — and the fix belongs at the live path for every shape, which is well outside a CPU-reduction PR. Per review discussion: a comment on the cached path is the one place a future reader of the live path won't look.

Fix sketch

In fetchValidatorsPage, when the row set is empty and offset > 0, run the aggregate alone (same CTE chain, LIMIT 1 OFFSET 0, selecting only the two window columns) to populate Total/OnDZCount. Worth confirming first whether the extra query is acceptable on that path or whether clamping the offset server-side is the better product behavior — a page past the end arguably ought to return the last page or a 404 rather than an empty success.

Either way, the fix should assert live == cached at offset >= total by extending the existing test, which currently pins the divergence.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions