فهرست منبع

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.
Fred 1 ماه پیش
والد
کامیت
4237f8b090
2فایلهای تغییر یافته به همراه32 افزوده شده و 3 حذف شده
  1. 17 0
      internal/api/api_integration_test.go
  2. 15 3
      internal/storage/entry_query_builder.go

+ 17 - 0
internal/api/api_integration_test.go

@@ -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
internal/storage/entry_query_builder.go

@@ -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.