activerecord_cursor_paginate 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +46 -38
- data/lib/activerecord_cursor_paginate/cursor.rb +4 -5
- data/lib/activerecord_cursor_paginate/extension.rb +4 -3
- data/lib/activerecord_cursor_paginate/page.rb +4 -2
- data/lib/activerecord_cursor_paginate/paginator.rb +17 -6
- data/lib/activerecord_cursor_paginate/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea74d80632e02c285a03b087219a635b30b678225dfbe7c17b949a83d74d2444
|
4
|
+
data.tar.gz: 06b461f0429a9527dd0180fc94ff5d984649aa412e78c641321cb463c63a40e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a671a9f299684a25cc158f38b0c7e23eb1eb03c36141bea92f435a0857dd6b35db371f4cfcf9e6a0ddb2ac10da0a88b7efb101e44a7da319a143e6da3320e3ed
|
7
|
+
data.tar.gz: 0f2e04a9dbb66a10253ea6f6309aacd1634a4a20cbde2f212ed1b3bf4d270931664cb6ad25e9d84c266b6bf9b815abda732a4fc4bacbe3d2f92cbb25dc7c1b40
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
1
|
## master (unreleased)
|
2
2
|
|
3
|
+
## 0.2.0 (2024-05-23)
|
4
|
+
|
5
|
+
- Fix prefixing selected columns when iterating over joined tables
|
6
|
+
- Change cursor encoding to url safe base64
|
7
|
+
- Fix `next_cursor`/`previous_cursor` for empty pages
|
8
|
+
- Fix iterating using only a timestamp column
|
9
|
+
|
10
|
+
- Add the ability to skip implicitly appending a primary key to the list of sorting columns.
|
11
|
+
|
12
|
+
It may be useful to disable it for the table with a UUID primary key or when the sorting
|
13
|
+
is done by a combination of columns that are already unique.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
paginator = UserSettings.cursor_paginate(order: :user_id, append_primary_key: false)
|
17
|
+
```
|
18
|
+
|
3
19
|
## 0.1.0 (2024-03-08)
|
4
20
|
|
5
21
|
- First release
|
data/README.md
CHANGED
@@ -48,11 +48,11 @@ page = paginator.fetch
|
|
48
48
|
page.records # => [#<Post:0x00007fd7071b2ea8 @id=1>, #<Post:0x00007fd7071bb738 @id=2>, ..., #<Post:0x00007fd707238260 @id=10>]
|
49
49
|
page.count # => 10
|
50
50
|
page.empty? # => false
|
51
|
-
page.cursors # => ["MQ
|
52
|
-
|
53
|
-
|
54
|
-
page.previous_cursor # =>
|
55
|
-
page.next_cursor # =>
|
51
|
+
page.cursors # => ["MQ", "Mg", ..., "MTA"]
|
52
|
+
| |
|
53
|
+
| |
|
54
|
+
page.previous_cursor # => "MQ" |
|
55
|
+
page.next_cursor # => "MTA" -------------|
|
56
56
|
page.has_previous? # => false
|
57
57
|
page.has_next? # => true
|
58
58
|
```
|
@@ -63,24 +63,24 @@ Take a look at the next section _"Ordering"_ to see how you can have an order di
|
|
63
63
|
To then get the next result page, you simply need to pass the last cursor of the returned page item via:
|
64
64
|
|
65
65
|
```ruby
|
66
|
-
paginator = posts.cursor_paginate(after: "MTA
|
66
|
+
paginator = posts.cursor_paginate(after: "MTA")
|
67
67
|
```
|
68
68
|
|
69
69
|
This will then fetch the next result page.
|
70
70
|
You can also just as easily paginate to previous pages by using `before` instead of `after` and using the first cursor of the current page.
|
71
71
|
|
72
72
|
```ruby
|
73
|
-
paginator = posts.cursor_paginate(before: "MQ
|
73
|
+
paginator = posts.cursor_paginate(before: "MQ")
|
74
74
|
```
|
75
75
|
|
76
76
|
By default, this will always return up to 10 results. But you can also specify how many records should be returned via `limit` parameter.
|
77
77
|
|
78
78
|
```ruby
|
79
|
-
paginator = posts.cursor_paginate(after: "MTA
|
79
|
+
paginator = posts.cursor_paginate(after: "MTA", limit: 2)
|
80
80
|
```
|
81
81
|
|
82
82
|
```ruby
|
83
|
-
paginator = posts.cursor_paginate(before: "MQ
|
83
|
+
paginator = posts.cursor_paginate(before: "MQ", limit: 2)
|
84
84
|
```
|
85
85
|
|
86
86
|
You can also easily iterate over the whole relation:
|
@@ -115,6 +115,14 @@ paginator = posts.cursor_paginate(order: [:author, :title])
|
|
115
115
|
paginator = posts.cursor_paginate(order: { author: :asc, title: :desc })
|
116
116
|
```
|
117
117
|
|
118
|
+
The gem implicitly appends a primary key column to the list of sorting columns. It may be useful
|
119
|
+
to disable it for the table with a UUID primary key or when the sorting is done by a combination
|
120
|
+
of columns that are already unique.
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
paginator = UserSettings.cursor_paginate(order: :user_id, append_primary_key: false)
|
124
|
+
```
|
125
|
+
|
118
126
|
**Important:**
|
119
127
|
If your app regularly orders by another column, you might want to add a database index for this.
|
120
128
|
Say that your order column is `author` then you'll want to add a compound index on `(author, id)`.
|
@@ -149,7 +157,7 @@ page = paginator.fetch
|
|
149
157
|
|
150
158
|
#### Order by more complex logic
|
151
159
|
|
152
|
-
Sometimes you might not only want to
|
160
|
+
Sometimes you might not only want to order by a column ascending or descending, but need more complex logic.
|
153
161
|
Imagine you would also store the post's `category` on the `posts` table (as a plain string for simplicity's sake).
|
154
162
|
And the category could be `pinned`, `announcement`, or `general`.
|
155
163
|
Then you might want to show all `pinned` posts first, followed by the `announcement` ones and lastly show the `general` posts.
|
@@ -215,15 +223,15 @@ this cursor, you can request the "n records AFTER the cursor"
|
|
215
223
|
|
216
224
|
As an example, assume we have a table called "posts" with this data:
|
217
225
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
226
|
+
| id | author |
|
227
|
+
|----|--------|
|
228
|
+
| 1 | Jane |
|
229
|
+
| 2 | John |
|
230
|
+
| 3 | John |
|
231
|
+
| 4 | Jane |
|
232
|
+
| 5 | Jane |
|
233
|
+
| 6 | John |
|
234
|
+
| 7 | John |
|
227
235
|
|
228
236
|
Now if we make a basic request without any `before`, `after`, custom `order` column,
|
229
237
|
this will just request the first page of this relation.
|
@@ -237,8 +245,8 @@ Assume that our default page size here is 2 and we would get a query like this:
|
|
237
245
|
|
238
246
|
```sql
|
239
247
|
SELECT *
|
240
|
-
FROM
|
241
|
-
ORDER BY
|
248
|
+
FROM posts
|
249
|
+
ORDER BY id ASC
|
242
250
|
LIMIT 2
|
243
251
|
```
|
244
252
|
|
@@ -247,11 +255,11 @@ no custom order is defined, each item in the returned collection will have a
|
|
247
255
|
cursor that only encodes the record's ID.
|
248
256
|
|
249
257
|
If we want to now request the next page, we can pass in the cursor of record
|
250
|
-
#2 which would be `"Mg
|
258
|
+
#2 which would be `"Mg"` (can get via `page.cursor`). So now we can request
|
251
259
|
the next page by calling:
|
252
260
|
|
253
261
|
```ruby
|
254
|
-
paginator = relation.cursor_paginate(limit: 2, after: "Mg
|
262
|
+
paginator = relation.cursor_paginate(limit: 2, after: "Mg")
|
255
263
|
page = paginator.fetch
|
256
264
|
```
|
257
265
|
|
@@ -259,18 +267,18 @@ And this will decode the given cursor and issue a query like:
|
|
259
267
|
|
260
268
|
```sql
|
261
269
|
SELECT *
|
262
|
-
FROM
|
263
|
-
WHERE
|
264
|
-
ORDER BY
|
270
|
+
FROM posts
|
271
|
+
WHERE id > 2
|
272
|
+
ORDER BY id ASC
|
265
273
|
LIMIT 2
|
266
274
|
```
|
267
275
|
|
268
276
|
Which would return posts #3 and #4. If we now want to paginate back, we can
|
269
277
|
request the posts that came before the first post, whose cursor would be
|
270
|
-
`"Mw
|
278
|
+
`"Mw"` (can get via `page.previous_cursor`):
|
271
279
|
|
272
280
|
```ruby
|
273
|
-
paginator = relation.cursor_paginate(limit: 2, before: "Mw
|
281
|
+
paginator = relation.cursor_paginate(limit: 2, before: "Mw")
|
274
282
|
page = paginator.fetch
|
275
283
|
```
|
276
284
|
|
@@ -279,9 +287,9 @@ around to get the last two records that have an ID smaller than the given one:
|
|
279
287
|
|
280
288
|
```sql
|
281
289
|
SELECT *
|
282
|
-
FROM
|
283
|
-
WHERE
|
284
|
-
ORDER BY
|
290
|
+
FROM posts
|
291
|
+
WHERE id < 3
|
292
|
+
ORDER BY id DESC
|
285
293
|
LIMIT 2
|
286
294
|
```
|
287
295
|
|
@@ -301,8 +309,8 @@ This will issue the following SQL query:
|
|
301
309
|
|
302
310
|
```sql
|
303
311
|
SELECT *
|
304
|
-
FROM
|
305
|
-
ORDER BY
|
312
|
+
FROM posts
|
313
|
+
ORDER BY author ASC, id ASC
|
306
314
|
LIMIT 2
|
307
315
|
```
|
308
316
|
|
@@ -323,12 +331,12 @@ data, the first record being the custom order column followed by the
|
|
323
331
|
record's ID.
|
324
332
|
|
325
333
|
Therefore, the cursor of record #4 will encode `['Jane', 4]`, which yields
|
326
|
-
this cursor: `"WyJKYW5lIiw0XQ
|
334
|
+
this cursor: `"WyJKYW5lIiw0XQ"`.
|
327
335
|
|
328
336
|
If we now want to request the next page via:
|
329
337
|
|
330
338
|
```ruby
|
331
|
-
paginator = relation.cursor_paginate(order: :author, limit: 2, after: "WyJKYW5lIiw0XQ
|
339
|
+
paginator = relation.cursor_paginate(order: :author, limit: 2, after: "WyJKYW5lIiw0XQ")
|
332
340
|
page = paginator.fetch
|
333
341
|
```
|
334
342
|
|
@@ -336,9 +344,9 @@ We get this SQL query:
|
|
336
344
|
|
337
345
|
```sql
|
338
346
|
SELECT *
|
339
|
-
FROM
|
340
|
-
WHERE (author > 'Jane' OR (author = 'Jane') AND (
|
341
|
-
ORDER BY
|
347
|
+
FROM posts
|
348
|
+
WHERE (author > 'Jane' OR (author = 'Jane') AND (id > 4))
|
349
|
+
ORDER BY author ASC, id ASC
|
342
350
|
LIMIT 2
|
343
351
|
```
|
344
352
|
|
@@ -8,13 +8,12 @@ module ActiveRecordCursorPaginate
|
|
8
8
|
class Cursor
|
9
9
|
class << self
|
10
10
|
def from_record(record, columns:)
|
11
|
-
columns = columns.map { |column| column.to_s.split(".").last }
|
12
11
|
values = columns.map { |column| record[column] }
|
13
12
|
new(columns: columns, values: values)
|
14
13
|
end
|
15
14
|
|
16
15
|
def decode(cursor_string:, columns:)
|
17
|
-
decoded = JSON.parse(Base64.
|
16
|
+
decoded = JSON.parse(Base64.urlsafe_decode64(cursor_string))
|
18
17
|
|
19
18
|
if (columns.size == 1 && decoded.is_a?(Array)) ||
|
20
19
|
(decoded.is_a?(Array) && decoded.size != columns.size)
|
@@ -48,8 +47,8 @@ module ActiveRecordCursorPaginate
|
|
48
47
|
attr_reader :columns, :values
|
49
48
|
|
50
49
|
def initialize(columns:, values:)
|
51
|
-
@columns = Array(columns)
|
52
|
-
@values = Array(values)
|
50
|
+
@columns = Array.wrap(columns)
|
51
|
+
@values = Array.wrap(values)
|
53
52
|
|
54
53
|
raise ArgumentError, "Cursor values can not be nil" if @values.any?(nil)
|
55
54
|
raise ArgumentError, ":columns and :values have different sizes" if @columns.size != @values.size
|
@@ -64,7 +63,7 @@ module ActiveRecordCursorPaginate
|
|
64
63
|
end
|
65
64
|
end
|
66
65
|
unencoded_cursor = (serialized_values.size == 1 ? serialized_values.first : serialized_values)
|
67
|
-
Base64.
|
66
|
+
Base64.urlsafe_encode64(unencoded_cursor.to_json, padding: false)
|
68
67
|
end
|
69
68
|
|
70
69
|
TIMESTAMP_PREFIX = "0aIX2_" # something random
|
@@ -4,14 +4,15 @@ module ActiveRecordCursorPaginate
|
|
4
4
|
module Extension
|
5
5
|
# Convenient method to use on ActiveRecord::Relation to get a paginator.
|
6
6
|
# @return [ActiveRecordCursorPaginate::Paginator]
|
7
|
+
# @see ActiveRecordCursorPaginate::Paginator#initialize
|
7
8
|
#
|
8
9
|
# @example
|
9
|
-
# paginator = Post.
|
10
|
+
# paginator = Post.cursor_paginate(limit: 2, after: "Mg")
|
10
11
|
# page = paginator.fetch
|
11
12
|
#
|
12
|
-
def cursor_paginate(after: nil, before: nil, limit: nil, order: nil)
|
13
|
+
def cursor_paginate(after: nil, before: nil, limit: nil, order: nil, append_primary_key: true)
|
13
14
|
relation = (is_a?(ActiveRecord::Relation) ? self : all)
|
14
|
-
Paginator.new(relation, after: after, before: before, limit: limit, order: order)
|
15
|
+
Paginator.new(relation, after: after, before: before, limit: limit, order: order, append_primary_key: append_primary_key)
|
15
16
|
end
|
16
17
|
alias cursor_pagination cursor_paginate
|
17
18
|
end
|
@@ -78,8 +78,10 @@ module ActiveRecordCursorPaginate
|
|
78
78
|
|
79
79
|
private
|
80
80
|
def cursor_for_record(record)
|
81
|
-
|
82
|
-
|
81
|
+
if record
|
82
|
+
cursor = Cursor.from_record(record, columns: @order_columns)
|
83
|
+
cursor.encode
|
84
|
+
end
|
83
85
|
end
|
84
86
|
end
|
85
87
|
end
|
@@ -6,12 +6,12 @@ module ActiveRecordCursorPaginate
|
|
6
6
|
#
|
7
7
|
# @example Iterating one page at a time
|
8
8
|
# ActiveRecordCursorPaginate::Paginator
|
9
|
-
# .new(relation, order: :author, limit: 2, after: "WyJKYW5lIiw0XQ
|
9
|
+
# .new(relation, order: :author, limit: 2, after: "WyJKYW5lIiw0XQ")
|
10
10
|
# .fetch
|
11
11
|
#
|
12
12
|
# @example Iterating over the whole relation
|
13
13
|
# paginator = ActiveRecordCursorPaginate::Paginator
|
14
|
-
# .new(relation, order: :author, limit: 2, after: "WyJKYW5lIiw0XQ
|
14
|
+
# .new(relation, order: :author, limit: 2, after: "WyJKYW5lIiw0XQ")
|
15
15
|
#
|
16
16
|
# # Will lazily iterate over the pages.
|
17
17
|
# paginator.pages.each do |page|
|
@@ -34,9 +34,13 @@ module ActiveRecordCursorPaginate
|
|
34
34
|
# ```sql
|
35
35
|
# CREATE INDEX <index_name> ON <table_name> (<order_fields>..., id)
|
36
36
|
# ```
|
37
|
+
# @param append_primary_key [Boolean] (true). Specifies whether the primary column(s)
|
38
|
+
# should be implicitly appended to the list of sorting columns. It may be useful
|
39
|
+
# to disable it for the table with a UUID primary key or when the sorting is done by a
|
40
|
+
# combination of columns that are already unique.
|
37
41
|
# @raise [ArgumentError] If any parameter is not valid
|
38
42
|
#
|
39
|
-
def initialize(relation, before: nil, after: nil, limit: nil, order: nil)
|
43
|
+
def initialize(relation, before: nil, after: nil, limit: nil, order: nil, append_primary_key: true)
|
40
44
|
unless relation.is_a?(ActiveRecord::Relation)
|
41
45
|
raise ArgumentError, "relation is not an ActiveRecord::Relation"
|
42
46
|
end
|
@@ -54,6 +58,7 @@ module ActiveRecordCursorPaginate
|
|
54
58
|
@page_size = limit || config.default_page_size
|
55
59
|
@page_size = [@page_size, config.max_page_size].min if config.max_page_size
|
56
60
|
|
61
|
+
@append_primary_key = append_primary_key
|
57
62
|
order = normalize_order(order)
|
58
63
|
@columns = order.keys
|
59
64
|
@directions = order.values
|
@@ -72,11 +77,11 @@ module ActiveRecordCursorPaginate
|
|
72
77
|
arel_columns = @columns.map.with_index do |column, i|
|
73
78
|
arel_column(column).as("cursor_column_#{i + 1}")
|
74
79
|
end
|
75
|
-
cursor_column_names =
|
80
|
+
cursor_column_names = arel_columns.map { |column| column.right.to_s }
|
76
81
|
|
77
82
|
relation =
|
78
83
|
if relation.select_values.empty?
|
79
|
-
relation.select(Arel.star, arel_columns)
|
84
|
+
relation.select(relation.arel_table[Arel.star], arel_columns)
|
80
85
|
else
|
81
86
|
relation.select(arel_columns)
|
82
87
|
end
|
@@ -153,7 +158,13 @@ module ActiveRecordCursorPaginate
|
|
153
158
|
|
154
159
|
result = result.with_indifferent_access
|
155
160
|
result.transform_values! { |direction| direction.downcase.to_sym }
|
156
|
-
|
161
|
+
|
162
|
+
if @append_primary_key
|
163
|
+
Array(@primary_key).each { |column| result[column] ||= default_direction }
|
164
|
+
end
|
165
|
+
|
166
|
+
raise ArgumentError, ":order must contain columns to order by" if result.blank?
|
167
|
+
|
157
168
|
result
|
158
169
|
end
|
159
170
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord_cursor_paginate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- fatkodima
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-05-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -63,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
63
|
- !ruby/object:Gem::Version
|
64
64
|
version: '0'
|
65
65
|
requirements: []
|
66
|
-
rubygems_version: 3.
|
66
|
+
rubygems_version: 3.4.19
|
67
67
|
signing_key:
|
68
68
|
specification_version: 4
|
69
69
|
summary: Cursor-based pagination for ActiveRecord.
|