Welcome to mirror list, hosted at ThFree Co, Russian Federation.

pagination.md « graphql_guide « development « doc - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: bf9eaa9915816037c33095c5d1b235c56d520427 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# GraphQL pagination

## Types of pagination

GitLab uses two primary types of pagination: **offset** and **keyset**
(sometimes called cursor-based) pagination.
The GraphQL API mainly uses keyset pagination, falling back to offset pagination when needed.

### Offset pagination

This is the traditional, page-by-page pagination, that is most common,
and used across much of GitLab. You can recognize it by
a list of page numbers near the bottom of a page, which, when clicked,
take you to that page of results.

For example, when you click **Page 100**, we send `100` to the
backend. For example, if each page has say 20 items, the
backend calculates `20 * 100 = 2000`,
and it queries the database by offsetting (skipping) the first 2000
records and pulls the next 20.

```plaintext
page number * page size = where to find my records
```

There are a couple of problems with this:

- Performance. When we query for page 100 (which gives an offset of
  2000), then the database has to scan through the table to that
  specific offset, and then pick up the next 20 records. As the offset
  increases, the performance degrades quickly.
  Read more in
  [The SQL I Love <3. Efficient pagination of a table with 100M records](http://allyouneedisbackend.com/blog/2017/09/24/the-sql-i-love-part-1-scanning-large-table/).

- Data stability. When you get the 20 items for page 100 (at
  offset 2000), GitLab shows those 20 items. If someone then
  deletes or adds records in page 99 or before, the items at
  offset 2000 become a different set of items. You can even get into a
  situation where, when paginating, you could skip over items,
  because the list keeps changing.
  Read more in
  [Pagination: You're (Probably) Doing It Wrong](https://coderwall.com/p/lkcaag/pagination-you-re-probably-doing-it-wrong).

### Keyset pagination

Given any specific record, if you know how to calculate what comes
after it, you can query the database for those specific records.

For example, suppose you have a list of issues sorted by creation date.
If you know the first item on a page has a specific date (say Jan 1), you can ask
for all records that were created after that date and take the first 20.
It no longer matters if many are deleted or added, as you always ask for
the ones after that date, and so get the correct items.

Unfortunately, there is no easy way to know if the issue created
on Jan 1 is on page 20 or page 100.

Some of the benefits and tradeoffs of keyset pagination are

- Performance is much better.

- Data stability is greater since you're not going to miss records due to
  deletions or insertions.

- It's the best way to do infinite scrolling.

- It's more difficult to program and maintain. Easy for `updated_at` and
  `sort_order`, complicated (or impossible) for complex sorting scenarios.

## Implementation

When pagination is supported for a query, GitLab defaults to using
keyset pagination. You can see where this is configured in
[`pagination/connections.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/graphql/pagination/connections.rb).
If a query returns `ActiveRecord::Relation`, keyset pagination is automatically used.

This was a conscious decision to support performance and data stability.

However, there are some cases where we have to use the offset
pagination connection, `OffsetActiveRecordRelationConnection`, such as when
sorting by label priority in issues, due to the complexity of the sort.

<!-- ### Keyset pagination -->

<!-- ### Offset pagination -->

<!-- ### External pagination -->

## Testing

Any GraphQL field that supports pagination and sorting should be tested
using the sorted paginated query shared example found in
[`graphql/sorted_paginated_query_shared_examples.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb).
It helps verify that your sort keys are compatible and that cursors
work properly.

This is particularly important when using keyset pagination, as some sort keys might not be supported.

Add a section to your request specs like this:

```ruby
describe 'sorting and pagination' do
  ...
end
```

You can then use
[`issues_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/requests/api/graphql/project/issues_spec.rb)
as an example to construct your tests.

[`graphql/sorted_paginated_query_shared_examples.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb)
also contains some documentation on how to use the shared examples.

The shared example requires certain `let` variables and methods to be set up:

```ruby
describe 'sorting and pagination' do
  let(:sort_project) { create(:project, :public) }
  let(:data_path)    { [:project, :issues] }

  def pagination_query(params, page_info)
    graphql_query_for(
      'project',
      { 'fullPath' => sort_project.full_path },
      query_graphql_field('issues', params, "#{page_info} edges { node { id } }")
    )
  end

  def pagination_results_data(data)
    data.map { |issue| issue.dig('node', 'iid').to_i }
  end

  context 'when sorting by weight' do
    ...
    context 'when ascending' do
      it_behaves_like 'sorted paginated query' do
        let(:sort_param)       { 'WEIGHT_ASC' }
        let(:first_param)      { 2 }
        let(:expected_results) { [weight_issue3.iid, weight_issue5.iid, weight_issue1.iid, weight_issue4.iid, weight_issue2.iid] }
      end
    end
```