Skip to content

Commit 8a9ae6f

Browse files
committed
Add the :jsonify_keyset_attributes variable to override the encoding (#749)
1 parent aa1f502 commit 8a9ae6f

File tree

7 files changed

+78
-19
lines changed

7 files changed

+78
-19
lines changed

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Layout/LineLength:
2222
Exclude:
2323
- test/**/*
2424

25+
Layout/BeginEndAlignment:
26+
EnforcedStyleAlignWith: begin
27+
2528
Layout/ExtraSpacing:
2629
AllowForAlignment:
2730
Enabled: true

docs/api/keyset.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ If you need a specific order:
114114
### How Pagy::Keyset works
115115

116116
- You pass an `uniquely ordered` `set` and `Pagy::Keyset` queries the page of records.
117-
- It keeps track of the `latest` fetched record by encoding its `keyset` attributes into the `page` query string param of the
117+
- It keeps track of the `latest` fetched record by encoding its `keyset` attributes into the `page` query string param of the
118118
`next` URL.
119119
- At each request, the `:page` is decoded and used to prepare a `when` clause that filters the newest records, and
120120
the `:limit` of records is pulled.
@@ -196,6 +196,37 @@ end
196196
Pagy::Keyset(set, typecast_latest:)
197197
```
198198

199+
==- `:jsonify_keyset_attributes`
200+
201+
A lambda to override the generic json encoding of the `keyset` attributes. Use it when the generic `to_json` method would lose
202+
some information when decoded.
203+
204+
For example: `Time` objects may lose or round the fractional seconds through the
205+
encoding/decoding cycle, causing the ordering to fail and thus creating all sort of unexpected behaviors (e.g. skipping or
206+
repeating the same page, missing or duplicated records, etc.). Here is what you can do:
207+
208+
```ruby
209+
# Match the microsecods with the strings stored into the time columns of SQLite
210+
jsonify_keyset_attributes = lambda do |attributes|
211+
# Convert it to a string matching the stored value/format in SQLite DB
212+
attributes[:created_at] = attributes[:created_at].strftime('%F %T.%6N')
213+
attributes.to_json
214+
end
215+
216+
Pagy::Keyset(set, jsonify_keyset_attributes:)
217+
```
218+
219+
!!! ActiveRecord alternative for time_precision
220+
221+
With `ActiveRecord::Relation` set, you can fix the fractional seconds issue by just setting the `time_precision`:
222+
223+
```ruby
224+
ActiveSupport::JSON::Encoding.time_precision = 6
225+
```
226+
!!!
227+
228+
_(Notice that it doesn't work with `Sequel::Dataset` sets)_
229+
199230
===
200231

201232
## Attribute Readers
@@ -220,8 +251,17 @@ Product.order(:name, :production_date)
220251
Product.order(:name, :production_date, :id)
221252
```
222253
!!!
254+
255+
!!!danger You may have an encoding problem
256+
The generic `to_json` method used to encode the `page` loses some information when decoded.
257+
258+
!!!success
259+
- Check the actual executed DB query and the actual stored value
260+
- Identify the column that have a format that doesn't match with the keyset
261+
- Use your custom encoding with the [:jsonify_keyset_attributes](#jsonify-keyset-attributes) variable
262+
!!!
223263

224-
!!!danger ... or you have a typecasting problem
264+
!!!danger You may have a typecasting problem
225265
Your ORM and the storage formats don't match for one or more columns. It's a common case with `SQLite` and Time columns.
226266
They may have been stored as strings formatted differently than the default format used by your current ORM.
227267

gem/apps/keyset_ar.ru

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ end
138138

139139
# ActiveRecord setup
140140
require 'active_record'
141+
142+
# Match the microsecods with the strings stored into the time columns of SQLite
143+
# ActiveSupport::JSON::Encoding.time_precision = 6
144+
141145
# Log
142146
output = ENV['APP_ENV'].equal?('showcase') ? IO::NULL : $stdout
143147
ActiveRecord::Base.logger = Logger.new(output)

gem/lib/pagy/keyset.rb

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,28 @@ def next
5353
records
5454
return unless @more
5555

56-
@next ||= B64.urlsafe_encode(latest_from(@records.last).to_json)
56+
@next ||= begin
57+
hash = keyset_attributes_from(@records.last)
58+
json = @vars[:jsonify_keyset_attributes]&.(hash) || hash.to_json
59+
B64.urlsafe_encode(json)
60+
end
5761
end
5862

5963
# Fetch the array of records for the current page
6064
def records
6165
@records ||= begin
62-
@set = apply_select if select?
63-
if @latest
64-
# :nocov:
65-
@set = @vars[:after_latest]&.(@set, @latest) || # deprecated
66-
# :nocov:
67-
@vars[:filter_newest]&.(@set, @latest, @keyset) ||
68-
filter_newest
69-
end
70-
records = @set.limit(@limit + 1).to_a
71-
@more = records.size > @limit && !records.pop.nil?
72-
records
73-
end
66+
@set = apply_select if select?
67+
if @latest
68+
# :nocov:
69+
@set = @vars[:after_latest]&.(@set, @latest) || # deprecated
70+
# :nocov:
71+
@vars[:filter_newest]&.(@set, @latest, @keyset) ||
72+
filter_newest
73+
end
74+
records = @set.limit(@limit + 1).to_a
75+
@more = records.size > @limit && !records.pop.nil?
76+
records
77+
end
7478
end
7579

7680
protected

gem/lib/pagy/keyset/active_record.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ class Keyset
77
class ActiveRecord < Keyset
88
protected
99

10-
# Get the keyset attributes of the record
11-
def latest_from(latest_record) = latest_record.slice(*@keyset.keys)
10+
# Get the keyset attributes from the record
11+
def keyset_attributes_from(record) = record.slice(*@keyset.keys)
1212

1313
# Extract the keyset from the set
1414
def extract_keyset

gem/lib/pagy/keyset/sequel.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ class Keyset
77
class Sequel < Keyset
88
protected
99

10-
# Get the keyset attributes of the latest record
11-
def latest_from(latest_record) = latest_record.to_hash.slice(*@keyset.keys)
10+
# Get the keyset attributes from the record
11+
def keyset_attributes_from(record) = record.to_hash.slice(*@keyset.keys)
1212

1313
# Extract the keyset from the set
1414
def extract_keyset

test/pagy/keyset_test.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@
4949
_ = pagy.records
5050
_(pagy.latest).must_equal({id: 10})
5151
end
52+
it 'uses :jsonify_keyset_attributes' do
53+
pagy = Pagy::Keyset.new(model.order(:id),
54+
page: "eyJpZCI6MTB9",
55+
limit: 10,
56+
jsonify_keyset_attributes: lambda(&:to_json))
57+
_(pagy.next).must_equal("eyJpZCI6MjB9")
58+
_(pagy.latest).must_equal({id: 10})
59+
end
5260
it 'uses :filter_newest' do
5361
filter_newest = if model == Pet
5462
->(set, latest, _keyset) { set.where('id > :id', **latest) }

0 commit comments

Comments
 (0)