Skip to content

Commit cf0cd56

Browse files
committed
Add pagination support to Table with ListPaged and WithPaginator methods
1 parent a3fdad7 commit cf0cd56

2 files changed

Lines changed: 127 additions & 4 deletions

File tree

table.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ type Record[T any, I ID] interface {
2525
// Table provides basic CRUD operations for database records.
2626
type Table[T any, P Record[T, I], I ID] struct {
2727
*DB
28-
Name string
29-
IDColumn string
28+
Name string
29+
IDColumn string
30+
Paginator Paginator[P]
3031
}
3132

3233
// helpers for setting timestamp fields
@@ -202,6 +203,18 @@ func (t *Table[T, P, I]) List(ctx context.Context, where sq.Sqlizer, orderBy []s
202203
return records, nil
203204
}
204205

206+
// ListPaged returns paginated records matching the condition.
207+
func (t *Table[T, P, I]) ListPaged(ctx context.Context, where sq.Sqlizer, page *Page) ([]P, *Page, error) {
208+
q := t.SQL.Select("*").From(t.Name).Where(where)
209+
210+
result, q := t.Paginator.PrepareQuery(q, page)
211+
if err := t.Query.GetAll(ctx, q, &result); err != nil {
212+
return nil, nil, err
213+
}
214+
result = t.Paginator.PrepareResult(result, page)
215+
return result, page, nil
216+
}
217+
205218
// Iter returns an iterator for records matching the condition.
206219
func (t *Table[T, P, I]) Iter(ctx context.Context, where sq.Sqlizer, orderBy []string) (iter.Seq2[P, error], error) {
207220
q := t.getListQuery(where, orderBy)
@@ -281,6 +294,16 @@ func (t *Table[T, P, I]) HardDeleteByID(ctx context.Context, id I) error {
281294
return nil
282295
}
283296

297+
// WithPaginator returns a table instance with the given paginator.
298+
func (t *Table[T, P, I]) WithPaginator(opts ...PaginatorOption) *Table[T, P, I] {
299+
return &Table[T, P, I]{
300+
DB: t.DB,
301+
Name: t.Name,
302+
IDColumn: t.IDColumn,
303+
Paginator: NewPaginator[P](opts...),
304+
}
305+
}
306+
284307
// WithTx returns a table instance bound to the given transaction.
285308
func (t *Table[T, P, I]) WithTx(tx pgx.Tx) *Table[T, P, I] {
286309
return &Table[T, P, I]{
@@ -289,8 +312,9 @@ func (t *Table[T, P, I]) WithTx(tx pgx.Tx) *Table[T, P, I] {
289312
SQL: t.DB.SQL,
290313
Query: t.DB.TxQuery(tx),
291314
},
292-
Name: t.Name,
293-
IDColumn: t.IDColumn,
315+
Name: t.Name,
316+
IDColumn: t.IDColumn,
317+
Paginator: t.Paginator,
294318
}
295319
}
296320

tests/table_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
sq "github.com/Masterminds/squirrel"
10+
"github.com/goware/pgkit/v2"
1011
"github.com/jackc/pgx/v5"
1112
"github.com/stretchr/testify/require"
1213
)
@@ -195,6 +196,104 @@ func TestTable(t *testing.T) {
195196
require.NoError(t, err, "SaveTx transaction failed")
196197
})
197198

199+
t.Run("ListPaged", func(t *testing.T) {
200+
ctx := t.Context()
201+
202+
account := &Account{Name: "ListPaged Account"}
203+
err := db.Accounts.Save(ctx, account)
204+
require.NoError(t, err)
205+
206+
// Create 15 articles.
207+
for i := range 15 {
208+
err := db.Articles.Save(ctx, &Article{
209+
AccountID: account.ID,
210+
Author: fmt.Sprintf("Author %02d", i),
211+
})
212+
require.NoError(t, err)
213+
}
214+
215+
// Default paginator (page size 10).
216+
page := pgkit.NewPage(0, 1)
217+
results, retPage, err := db.Articles.ListPaged(ctx, sq.Eq{"account_id": account.ID}, page)
218+
require.NoError(t, err)
219+
require.Len(t, results, 10)
220+
require.True(t, retPage.More, "should have more pages")
221+
222+
// Second page.
223+
page2 := pgkit.NewPage(0, 2)
224+
results2, retPage2, err := db.Articles.ListPaged(ctx, sq.Eq{"account_id": account.ID}, page2)
225+
require.NoError(t, err)
226+
require.Len(t, results2, 5)
227+
require.False(t, retPage2.More, "should not have more pages")
228+
229+
// No overlap between pages.
230+
for _, r1 := range results {
231+
for _, r2 := range results2 {
232+
require.NotEqual(t, r1.ID, r2.ID, "pages should not overlap")
233+
}
234+
}
235+
})
236+
237+
t.Run("WithPaginator", func(t *testing.T) {
238+
ctx := t.Context()
239+
240+
account := &Account{Name: "WithPaginator Account"}
241+
err := db.Accounts.Save(ctx, account)
242+
require.NoError(t, err)
243+
244+
for i := range 10 {
245+
err := db.Articles.Save(ctx, &Article{
246+
AccountID: account.ID,
247+
Author: fmt.Sprintf("PagAuthor %02d", i),
248+
})
249+
require.NoError(t, err)
250+
}
251+
252+
// Use a custom paginator with page size 3.
253+
pagedTable := db.Articles.Table.WithPaginator(pgkit.WithDefaultSize(3), pgkit.WithMaxSize(5))
254+
255+
page := pgkit.NewPage(0, 1)
256+
results, retPage, err := pagedTable.ListPaged(ctx, sq.Eq{"account_id": account.ID}, page)
257+
require.NoError(t, err)
258+
require.Len(t, results, 3, "should return 3 records with custom paginator")
259+
require.True(t, retPage.More)
260+
261+
// Request size larger than max should be capped.
262+
bigPage := pgkit.NewPage(100, 1)
263+
results, _, err = pagedTable.ListPaged(ctx, sq.Eq{"account_id": account.ID}, bigPage)
264+
require.NoError(t, err)
265+
require.Len(t, results, 5, "should be capped at max size 5")
266+
})
267+
268+
t.Run("WithTx preserves Paginator", func(t *testing.T) {
269+
ctx := t.Context()
270+
271+
account := &Account{Name: "WithTx Paginator Account"}
272+
err := db.Accounts.Save(ctx, account)
273+
require.NoError(t, err)
274+
275+
for i := range 5 {
276+
err := db.Articles.Save(ctx, &Article{
277+
AccountID: account.ID,
278+
Author: fmt.Sprintf("TxPag %02d", i),
279+
})
280+
require.NoError(t, err)
281+
}
282+
283+
pagedTable := db.Articles.Table.WithPaginator(pgkit.WithDefaultSize(2))
284+
285+
err = pgx.BeginFunc(ctx, db.Conn, func(pgTx pgx.Tx) error {
286+
txTable := pagedTable.WithTx(pgTx)
287+
page := pgkit.NewPage(0, 1)
288+
results, retPage, err := txTable.ListPaged(ctx, sq.Eq{"account_id": account.ID}, page)
289+
require.NoError(t, err)
290+
require.Len(t, results, 2, "paginator should be preserved through WithTx")
291+
require.True(t, retPage.More)
292+
return nil
293+
})
294+
require.NoError(t, err)
295+
})
296+
198297
t.Run("WithTx keeps IDColumn", func(t *testing.T) {
199298
ctx := t.Context()
200299

0 commit comments

Comments
 (0)