Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions internal/service/mod_audit/mod_audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/gofrs/uuid"
"github.com/stashapp/stash-box/internal/models"
"github.com/stashapp/stash-box/internal/queries"
queryhelper "github.com/stashapp/stash-box/internal/service/query"
)

// ModAuditService handles mod audit operations
Expand Down Expand Up @@ -61,14 +62,13 @@ func (s *ModAuditService) QueryModAudits(ctx context.Context, filter models.ModA
userID = uuid.NullUUID{UUID: *filter.UserID, Valid: true}
}

offset := (filter.Page - 1) * filter.PerPage
limit := filter.PerPage
p := queryhelper.Pagination(filter.Page, filter.PerPage)

dbAudits, err := s.queries.QueryModAudits(ctx, queries.QueryModAuditsParams{
Action: action,
UserID: userID,
Limit: int32(limit),
Offset: int32(offset),
Limit: p.Limit,
Offset: p.Offset,
})
if err != nil {
return nil, err
Expand Down
12 changes: 6 additions & 6 deletions internal/service/notification/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/stashapp/stash-box/internal/converter"
"github.com/stashapp/stash-box/internal/models"
"github.com/stashapp/stash-box/internal/queries"
queryhelper "github.com/stashapp/stash-box/internal/service/query"
"github.com/stashapp/stash-box/pkg/logger"
)

Expand Down Expand Up @@ -112,8 +113,7 @@ func (s *Notification) GetNotifications(ctx context.Context, userID uuid.UUID, u
var notifications []queries.Notification
var err error

offset := (page - 1) * perPage
limit := perPage
p := queryhelper.Pagination(page, perPage)

var typeParam queries.NullNotificationType
if notificationType != nil {
Expand All @@ -126,15 +126,15 @@ func (s *Notification) GetNotifications(ctx context.Context, userID uuid.UUID, u
if unreadOnly {
notifications, err = s.queries.FindUnreadNotificationsByUser(ctx, queries.FindUnreadNotificationsByUserParams{
UserID: userID,
Limit: int32(limit),
Offset: int32(offset),
Limit: p.Limit,
Offset: p.Offset,
Type: typeParam,
})
} else {
notifications, err = s.queries.FindNotificationsByUser(ctx, queries.FindNotificationsByUserParams{
UserID: userID,
Limit: int32(limit),
Offset: int32(offset),
Limit: p.Limit,
Offset: p.Offset,
Type: typeParam,
})
}
Expand Down
35 changes: 30 additions & 5 deletions internal/service/query/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,41 @@ import (
"github.com/stashapp/stash-box/internal/queries"
)

// ApplyPagination applies pagination to a query with default values
func ApplyPagination(query sq.SelectBuilder, page, perPage int) sq.SelectBuilder {
// DefaultPerPage is applied when a paginated query's per-page value is unset.
const DefaultPerPage = 25

// MaxPerPage is the maximum number of results a paginated query returns per page.
const MaxPerPage = 100

// PageParams holds the normalized limit and offset for a paginated query.
type PageParams struct {
Limit int32
Offset int32
}

// Pagination resolves a raw page and per-page value to normalized pagination
// parameters: the page is 1-based, the limit defaults to DefaultPerPage and is
// capped at MaxPerPage, and the offset is computed from the two.
func Pagination(page, perPage int) PageParams {
if page <= 0 {
page = 1
}
if perPage <= 0 {
perPage = 25
perPage = DefaultPerPage
}
offset := (page - 1) * perPage
return query.Limit(uint64(perPage)).Offset(uint64(offset))
if perPage > MaxPerPage {
perPage = MaxPerPage
}
return PageParams{
Limit: int32(perPage),
Offset: int32((page - 1) * perPage),
}
}

// ApplyPagination applies normalized pagination to a query with default values
func ApplyPagination(query sq.SelectBuilder, page, perPage int) sq.SelectBuilder {
p := Pagination(page, perPage)
return query.Limit(uint64(p.Limit)).Offset(uint64(p.Offset))
}

// ApplySortParams applies sorting to query with optional table prefix
Expand Down
60 changes: 60 additions & 0 deletions internal/service/query/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package query

import (
"testing"

sq "github.com/Masterminds/squirrel"
"github.com/stretchr/testify/assert"
)

func TestPagination(t *testing.T) {
tests := []struct {
name string
page int
perPage int
wantLimit int32
wantOffset int32
}{
{"negative page defaults", -1, 25, 25, 0},
{"zero page defaults", 0, 25, 25, 0},
{"page one offset zero", 1, 40, 40, 0},
{"page two offset", 2, 40, 40, 40},
{"negative per page defaults", 1, -1, 25, 0},
{"zero per page defaults", 1, 0, 25, 0},
{"per page at max", 1, 100, 100, 0},
{"per page above max", 1, 101, 100, 0},
{"per page far above max", 1, 1000, 100, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := Pagination(tt.page, tt.perPage)
assert.Equal(t, tt.wantLimit, p.Limit)
assert.Equal(t, tt.wantOffset, p.Offset)
})
}
}

func TestApplyPaginationNormalizes(t *testing.T) {
q := sq.Select("scenes.*").From("scenes")

sql, _, err := ApplyPagination(q, 1, 500).ToSql()
assert.NoError(t, err)
assert.Contains(t, sql, "LIMIT 100")
assert.Contains(t, sql, "OFFSET 0")

sql, _, err = ApplyPagination(q, 2, 40).ToSql()
assert.NoError(t, err)
assert.Contains(t, sql, "LIMIT 40")
assert.Contains(t, sql, "OFFSET 40")

// Unset per_page defaults to 25.
sql, _, err = ApplyPagination(q, 1, 0).ToSql()
assert.NoError(t, err)
assert.Contains(t, sql, "LIMIT 25")

// Unset page defaults to 1 (offset 0).
sql, _, err = ApplyPagination(q, 0, 40).ToSql()
assert.NoError(t, err)
assert.Contains(t, sql, "LIMIT 40")
assert.Contains(t, sql, "OFFSET 0")
}
12 changes: 2 additions & 10 deletions internal/service/scene/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,22 +228,14 @@ func (s *Scene) buildSceneQuery(psql sq.StatementBuilderType, input models.Scene
if !hasOtherFilters && !forCount {
// Optimize: limit the trending subquery directly
// Note: Use manual pagination here since we're limiting in the subquery
page := 1
perPage := 25
if input.Page > 0 {
page = input.Page
}
if input.PerPage > 0 {
perPage = input.PerPage
}
offset := (page - 1) * perPage
p := queryhelper.Pagination(input.Page, input.PerPage)

query = query.Join(fmt.Sprintf(`(
SELECT scene_id, trending_count AS count
FROM scene_popularity_trending
ORDER BY trending_count DESC, scene_id DESC
LIMIT %d OFFSET %d
) TRENDING ON scenes.id = TRENDING.scene_id`, perPage, offset))
) TRENDING ON scenes.id = TRENDING.scene_id`, p.Limit, p.Offset))
query = query.OrderBy("TRENDING.count DESC, TRENDING.scene_id DESC")
// Don't apply pagination again below since we already limited in the subquery
} else {
Expand Down
Loading