From 6fc5e725e68a18b46a9183f0644a4ca7bd9e7006 Mon Sep 17 00:00:00 2001 From: Maxime Delporte Date: Sat, 15 Nov 2025 16:34:40 +0100 Subject: [PATCH] Paginating Lists --- internal/data/filters.go | 8 ++++++++ internal/data/movies.go | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/internal/data/filters.go b/internal/data/filters.go index 33cbbca..19dd1b6 100644 --- a/internal/data/filters.go +++ b/internal/data/filters.go @@ -23,6 +23,14 @@ func ValidateFilters(v *validator.Validator, f Filters) { v.Check(validator.PermittedValue(f.Sort, f.SortSafelist...), "sort", "invalid sort value") } +func (f Filters) limit() int { + return f.PageSize +} + +func (f Filters) offset() int { + return (f.Page - 1) * f.PageSize +} + // sortColumn : Check that the client-provided Sort field matches one of the entries in our safelist and if it does, extract the column name from the Sort field by stripping the leading hyphen character (if one exists) func (f Filters) sortColumn() string { for _, safeValue := range f.SortSafelist { diff --git a/internal/data/movies.go b/internal/data/movies.go index be831ab..7b37b15 100644 --- a/internal/data/movies.go +++ b/internal/data/movies.go @@ -200,19 +200,25 @@ func (m MovieModel) GetAll(title string, genres []string, filters Filters) ([]*M // plainto_tsquery('simple', $1) takes a search value and turns it into a formatted query term that PostgreSQL full-text search can understand. As an example : "The Club" would result in the query term 'the' & 'club' // The @@ operator is the matches' operator. To continue the example, the query term 'the' & 'club' will match rows which contain both lexemes 'the' and 'club'. // Add an ORDER BY clause and interpolate the sort column and direction. Importantly, notice that we also include a secondary sort on the movie ID to ensure a consistent ordering. + // Update the SQL query to include the LIMIT and OFFSET clauses with placeholder parameter values query := fmt.Sprintf(` SELECT id, created_at, title, year, runtime, genres, version FROM movies WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1 = '') AND (genres @> $2 OR $2 = '{}') - ORDER BY %s %s, id ASC`, filters.sortColumn(), filters.sortDirection()) + ORDER BY %s %s, id ASC + LIMIT $3 OFFSET $4`, filters.sortColumn(), filters.sortDirection()) // Create a context with a 3-second timeout ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() + // As our SQL query now has quite a few placeholder parameters, let's collect the values for the placeholders in a slice. Notice here how we call the limit() and offset() methods on the Filters struct to get the appropriate values for the LIMIT and OFFSET clauses + args := []any{title, pq.Array(genres), filters.limit(), filters.offset()} + // Use QueryContext() to execute the query. This returns a sql.Rows resultset containing the result - rows, err := m.DB.QueryContext(ctx, query, title, pq.Array(genres)) + // And then pass the args slice to QueryContext() as a variadic parameter + rows, err := m.DB.QueryContext(ctx, query, args...) if err != nil { return nil, err }