fix(db): total ordering for series OFFSET pagination
GetDistinctSeries ordered by MAX(created_at) DESC alone. Bulk imports inserted in a single transaction share one NOW() timestamp, leaving large numbers of (library_id, series) groups tied on MAX(created_at); Postgres does not guarantee tie order between executions, so LIMIT/OFFSET pages could repeat or drop series across page loads. Add the GROUP BY keys (library_id, series) as tiebreakers so the sort is total and deterministic. Comment documents why the tiebreaker is required so it is not dropped as noise. sqlc regenerated (interface unchanged; the comment flows into querier.go docs).
This commit is contained in:
1 parent
f4414aa5b6
commit
38efbff7e1
3 files changed
+17
-2
No files matched your search
@@ -221,6 +221,11 @@ type Querier interface {
|
|||||||
// Series are grouped PER LIBRARY: the same series name in two libraries
|
// Series are grouped PER LIBRARY: the same series name in two libraries
|
||||||
// (novels vs manga adaptations, LN splits) stays two entries. Visible
|
// (novels vs manga adaptations, LN splits) stays two entries. Visible
|
||||||
// filter enforces per-user library visibility at the source.
|
// filter enforces per-user library visibility at the source.
|
||||||
|
// Tiebreaker REQUIRED for LIMIT/OFFSET pagination: bulk imports in one
|
||||||
|
// transaction share a single NOW() timestamp, leaving many groups tied on
|
||||||
|
// MAX(created_at). Postgres does not guarantee tie order between
|
||||||
|
// executions, so page boundaries could repeat or drop series. The GROUP BY
|
||||||
|
// keys make the sort total and deterministic.
|
||||||
GetDistinctSeries(ctx context.Context, arg GetDistinctSeriesParams) ([]GetDistinctSeriesRow, error)
|
GetDistinctSeries(ctx context.Context, arg GetDistinctSeriesParams) ([]GetDistinctSeriesRow, error)
|
||||||
GetDistinctSeriesCount(ctx context.Context, arg GetDistinctSeriesCountParams) (int32, error)
|
GetDistinctSeriesCount(ctx context.Context, arg GetDistinctSeriesCountParams) (int32, error)
|
||||||
GetFailedSyncQueueItems(ctx context.Context, limit int32) ([]SyncQueue, error)
|
GetFailedSyncQueueItems(ctx context.Context, limit int32) ([]SyncQueue, error)
|
||||||
|
|||||||
@@ -3840,7 +3840,7 @@ WHERE ($1::uuid IS NULL OR library_id = $1::uuid)
|
|||||||
AND missing_scan_count = 0
|
AND missing_scan_count = 0
|
||||||
AND series IS NOT NULL AND series != ''
|
AND series IS NOT NULL AND series != ''
|
||||||
GROUP BY library_id, series
|
GROUP BY library_id, series
|
||||||
ORDER BY MAX(created_at) DESC
|
ORDER BY MAX(created_at) DESC, library_id, series
|
||||||
LIMIT $4 OFFSET $3
|
LIMIT $4 OFFSET $3
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -3862,6 +3862,11 @@ type GetDistinctSeriesRow struct {
|
|||||||
// Series are grouped PER LIBRARY: the same series name in two libraries
|
// Series are grouped PER LIBRARY: the same series name in two libraries
|
||||||
// (novels vs manga adaptations, LN splits) stays two entries. Visible
|
// (novels vs manga adaptations, LN splits) stays two entries. Visible
|
||||||
// filter enforces per-user library visibility at the source.
|
// filter enforces per-user library visibility at the source.
|
||||||
|
// Tiebreaker REQUIRED for LIMIT/OFFSET pagination: bulk imports in one
|
||||||
|
// transaction share a single NOW() timestamp, leaving many groups tied on
|
||||||
|
// MAX(created_at). Postgres does not guarantee tie order between
|
||||||
|
// executions, so page boundaries could repeat or drop series. The GROUP BY
|
||||||
|
// keys make the sort total and deterministic.
|
||||||
func (q *Queries) GetDistinctSeries(ctx context.Context, arg GetDistinctSeriesParams) ([]GetDistinctSeriesRow, error) {
|
func (q *Queries) GetDistinctSeries(ctx context.Context, arg GetDistinctSeriesParams) ([]GetDistinctSeriesRow, error) {
|
||||||
rows, err := q.db.Query(ctx, GetDistinctSeries,
|
rows, err := q.db.Query(ctx, GetDistinctSeries,
|
||||||
arg.LibraryID,
|
arg.LibraryID,
|
||||||
|
|||||||
@@ -2880,7 +2880,12 @@ WHERE (sqlc.narg('library_id')::uuid IS NULL OR library_id = sqlc.narg('library_
|
|||||||
AND missing_scan_count = 0
|
AND missing_scan_count = 0
|
||||||
AND series IS NOT NULL AND series != ''
|
AND series IS NOT NULL AND series != ''
|
||||||
GROUP BY library_id, series
|
GROUP BY library_id, series
|
||||||
ORDER BY MAX(created_at) DESC
|
-- Tiebreaker REQUIRED for LIMIT/OFFSET pagination: bulk imports in one
|
||||||
|
-- transaction share a single NOW() timestamp, leaving many groups tied on
|
||||||
|
-- MAX(created_at). Postgres does not guarantee tie order between
|
||||||
|
-- executions, so page boundaries could repeat or drop series. The GROUP BY
|
||||||
|
-- keys make the sort total and deterministic.
|
||||||
|
ORDER BY MAX(created_at) DESC, library_id, series
|
||||||
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
|
||||||
|
|
||||||
-- name: GetDistinctSeriesCount :one
|
-- name: GetDistinctSeriesCount :one
|
||||||
|
|||||||
Reference in new issue
Block a user