Forge

Listing & pagination

Sorted, limited, paginated reads with List.

GitHub

List reads many rows with filtering, sorting, a limit, and pagination (either cursor- or offset-based) — one method on every backend. It's part of the Store interface, so the same code works on PostgreSQL, MongoDB, and DynamoDB.

rows, err := db.List(ctx, "posts", dbswitch.ListOptions{
	Filter:  map[string]any{"status": "PUBLISHED"},
	SortBy:  "created_at",
	SortDir: dbswitch.Descending,
	Limit:   20,
})

ListOptions

Every field is optional — the zero value is "all rows, unordered, no limit" (same as Find).

FieldTypeMeaning
Filtermap[string]anyEquality conditions, ANDed (like Find's where)
SortBystringField to order by. Empty = no explicit ordering
SortDirSortDirectionAscending (default) or Descending
LimitintMax rows. 0 = no limit
OffsetintSkip N rows before returning results. 0 = no skip
AfteranyCursor — see below

Sorting & limiting

// Newest 10 published posts.
rows, err := db.List(ctx, "posts", dbswitch.ListOptions{
	Filter:  map[string]any{"status": "PUBLISHED"},
	SortBy:  "created_at",
	SortDir: dbswitch.Descending,
	Limit:   10,
})

This compiles to (Postgres):

SELECT * FROM "posts" WHERE "status" = $1 ORDER BY "created_at" DESC LIMIT 10

…and to an equivalent find + sort + limit on MongoDB. On DynamoDB, sorting and filtering on anything besides "id" are emulated in memory after a Scan — see Backends.

Offset (page-based) pagination

Set Offset to skip a fixed number of rows — the familiar page N = (N-1) * Limit model:

const pageSize = 20

page := 3
opts := dbswitch.ListOptions{
	SortBy:  "created_at",
	SortDir: dbswitch.Descending,
	Limit:   pageSize,
	Offset:  (page - 1) * pageSize,
}
rows, err := db.List(ctx, "posts", opts)

Offset makes the database (or, on DynamoDB, the in-memory emulation) walk and discard every skipped row, so it gets slower the deeper you page. Prefer After (cursor, below) for large or deeply-paged result sets.

Cursor pagination

Set After together with SortBy to fetch the next page: only rows past the cursor in the sort direction are returned — SortBy > After for Ascending, SortBy < After for Descending. Pass the SortBy value of the last row from the previous page.

opts := dbswitch.ListOptions{
	SortBy:  "created_at",
	SortDir: dbswitch.Descending,
	Limit:   20,
}

// Page 1
page, _ := db.List(ctx, "posts", opts)

// Page 2: continue after the last row of page 1
if len(page) > 0 {
	opts.After = page[len(page)-1]["created_at"]
	page, _ = db.List(ctx, "posts", opts)
}

On PostgreSQL and MongoDB, cursor pagination is keyset-based (a WHERE sort_col <> cursor comparison), not OFFSET — so it stays fast on large tables and doesn't skip/repeat rows when data changes between pages. Use a sort column that's unique (or unique-enough) to avoid ties at the boundary.

After only takes effect when SortBy is also set. Filters remain equality-only — for range filters (created_at < …) beyond the cursor, you still need your driver directly (see Limitations).

On this page