Reputation: 12521
I have a database table reviews
with id
, rating
where id is auto-incrementing and rating is an integer between 0 and 100.
I'm attempting to create cursor based pagination in a GraphQL API but am struggling to create the necessary queries for hasPreviousPage
and hasNextPage
.
Here is my data:
ID: 1, RATING: 50
ID: 2, RATING: 80
ID: 3, RATING: 20
ID: 4, RATING: 40
ID: 5, RATING: 60
Here's an example of a GQL query:
reviews(first: 3)
Which returns
ID: 1, RATING: 50
ID: 2, RATING: 80
ID: 3, RATING: 20
With pageInfo
hasPreviousPage: false
hasNextPage: true
The queries for pageInfo would be
hasPreviousPage = SELECT COUNT(*) > 0 FROM reviews WHERE id < 0;
hasNextPage = SELECT COUNT(*) > 0 FROM reviews WHERE id > 3;
My issue comes when sorting by rating. Making a query similar to before:
reviews(sort: "rating", first: 3)
Which returns
ID: 3, RATING: 20
ID: 4, RATING: 40
ID: 1, RATING: 50
With pageInfo
hasPreviousPage: false
hasNextPage: true
But how can I create the queries for hasPreviousPage
and hasNextPage
like I did before?
hasPreviousPage = SELECT COUNT(*) > 0 FROM reviews WHERE ???
hasNextPage = SELECT COUNT(*) > 0 FROM reviews WHERE ???
What should the WHERE clause be in this case? Does the query need to be a lot more complex with a sub query? I'm not sure what I'm missing.
Upvotes: 2
Views: 2034
Reputation: 800
You don't actually need any DB queries for hasPreviousPage
and hasNextPage
.
You need to apply the +2 trick to achieve this (given that you want to implement hasPreviousPage
for both before and after cases).
Let's say you have this query:
// `after` should be URL-safe encoded
// `id` must have a monotonic sort order, a ULID is a fine choice for an id
// if ULID is not an option for some reason, chose a different column that has a monotonicity to it, e.g. `created_at`
reviews(first: 3, after: "id:12345;sort_cols:user_id")
What you want to do is to query for the first 5 reviews:
SELECT *
FROM reviews
WHERE id >= ?
ORDER BY ? ASC
LIMIT 5;
-- result: 12345, 12346, 12347, 12348, 12349
-- from the app return (after computing `pageInfo`): 12346, 12347, 12348
If the id
of the first result matches the id in the cursor, i.e. 12345
it means there is a previous page.
If there is a previous page and the number of returned rows is 4 or less, hasNextPage: false
.
If there is not a previous page and the number of returned rows is 4 or more, hasNextPage: true
.
Before you return the results, make sure to filter out the item matching the cursor id (12345
) and the one extra item if there is a next page.
Beware that the SQL has to be generated correctly. The query would change depending on the direction of the pagination (before
vs. after
). It also becomes more complex if you want to support range requests, i.e. ones where before
and after
are supplied at the same time.
Example:
reviews(first: 3, before: "id:12345;sort_col:user_id")
Here you want to use a descending order. Also, you need to filter on ids <= the id from your cursor.
SELECT *
FROM reviews
WHERE id <= ?
ORDER BY ? DESC
LIMIT 5;
-- result: 12345, 12344, 12343, 12342, 12341
-- from the app return (after computing `pageInfo`): 12344, 12343, 12342
Upvotes: 4