fix(storage): return correct total when offset is beyond the last entry

The total returned by GetEntriesWithCount comes from count(*) OVER(),
which is carried on the returned rows. When the requested offset lands
past the last matching row, the query returns no rows and the total was
reported as 0 even though matching entries exist, breaking clients that
paginate until offset >= total.

Fall back to a separate CountEntries() query when the page is empty and
the offset is greater than zero. With offset 0 an empty result genuinely
means zero matches, so the single-query fast path is unchanged for
normal requests.

Add an integration test requesting the page at offset == total, which
must return no entries while keeping the same total.
This commit is contained in:
Fred
2026-07-20 19:42:16 -07:00
committed by fguillot
parent c119273b89
commit 4237f8b090
2 changed files with 32 additions and 3 deletions
+17
View File
@@ -2517,6 +2517,23 @@ func TestGetAllEntriesEndpointWithFilter(t *testing.T) {
t.Fatalf(`Invalid title, got empty`)
}
emptyPage, err := regularUserClient.Entries(&miniflux.Filter{
FeedID: feedID,
Limit: 1,
Offset: feedEntries.Total,
})
if err != nil {
t.Fatal(err)
}
if len(emptyPage.Entries) != 0 {
t.Fatalf(`Expected no entries beyond the final page, got %d`, len(emptyPage.Entries))
}
if emptyPage.Total != feedEntries.Total {
t.Fatalf(`Expected total %d beyond the final page, got %d`, feedEntries.Total, emptyPage.Total)
}
recentEntries, err := regularUserClient.Entries(&miniflux.Filter{Order: "published_at", Direction: "desc"})
if err != nil {
t.Fatal(err)
+15 -3
View File
@@ -273,10 +273,22 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
}
// GetEntriesWithCount returns a list of entries and the total count of matching
// rows (ignoring limit/offset) in a single query using a window function.
// This avoids a separate CountEntries() round-trip.
// rows, ignoring limit and offset. It uses a window function for non-empty pages
// and falls back to a separate count when the requested offset returns no rows.
func (e *EntryQueryBuilder) GetEntriesWithCount() (model.Entries, int, error) {
return e.fetchEntries(true)
entries, total, err := e.fetchEntries(true)
if err != nil {
return nil, 0, err
}
if len(entries) == 0 && e.offset > 0 {
total, err = e.CountEntries()
if err != nil {
return nil, 0, err
}
}
return entries, total, nil
}
// fetchEntries is the shared implementation for GetEntries and GetEntriesWithCount.