From 7018d3c429ed0df58b66b9a7a10bc6a40e4ab0fd Mon Sep 17 00:00:00 2001
From: Domizio Demichelis
Date: Sun, 15 Dec 2024 09:23:30 +0700
Subject: [PATCH] Improve keyset docs
---
docs/api/keyset.md | 64 +++++++++++++++++++++++++++++-----------------
1 file changed, 41 insertions(+), 23 deletions(-)
diff --git a/docs/api/keyset.md b/docs/api/keyset.md
index acdad8a11..19043fc83 100644
--- a/docs/api/keyset.md
+++ b/docs/api/keyset.md
@@ -52,7 +52,8 @@ If you want the best of the two worlds, check out the [keyset_for_ui extra](/doc
| `set` | The `uniquely ordered` `ActiveRecord::Relation` or `Sequel::Dataset` collection to paginate. |
| `keyset` | The hash of column/direction pairs. Pagy extracts it from the order of the `set`. |
| `keyset attributes` | The hash of keyset-column/record-value pairs of a record. |
-| `cutoff` | A point in the `set` where a `page` ended. Its value is a `Base64` encoded URL-safe string. |
+| `keyset attributes values` | The array of the values of the `keyset attributes`. |
+| `cutoff` | A point in the `set` where a `page` ends and the `next` begins. It is encoded as a `Base64` URL-safe string. |
| `page` | The current `page`, i.e. the page of records beginning after the `cutoff` of the previous page. Also the `:page` variable, which is set to the `cutoff` of the previous page |
| `next` | The next `page`, i.e. the page of records beginning after the `cutoff`. Also the `cutoff` value retured by the `next` method. |
@@ -164,41 +165,57 @@ If you need a specific order:
#### Understanding the Cutoffs
-A `cutoff` defines a point in the `set` where a `page` ended. All the records AFTER that point are or will be part of the `next` page.
+A `cutoff` defines a point in the `set` where a `page` ended. All the records AFTER that point are or will be part of the `next`
+page.
Let's consider an example of a simple `set`. In order to avoid confusion with numeric ids and number of records, let's assume that
-it has an `id` column populated by unique alphanumeric codes, and its order is: `order(:id)`.
+it has an `id` column populated by character keys, and its order is: `order(:id)`.
-Assuming a LIMIT of 6, the first page will include the first 6 records in the set: no `cutoff` required so far...
+Assuming a LIMIT of 10, the _"first page"_ will just include the first 10 records in the `set`: no `cutoff` required so far...
```
- | page | not yet paginated |
-beginning ->|. . . . . .|. . . . . . . . . . . . . . . . . . . . . . . . . . .|<- end of set
+ │ first page >│ rest >│
+beginning of set >[· · · · · · · · · ·]· · · · · · · · · · · · · · · · · ·]< end of set
```
-After we pull the first 6 records from the beginning of the `set`, we read the `id` of the last one, which is `F`. So our `cutoff` can be defined like: _"the point up to the value `F` in the `id` column"_.
+After we pull the first 10 records from the beginning of the `set`, we read the `id` of the last one, which is `X`. So our
+`cutoff` can be defined like: _"the point up to the value `X` in the `id` column"_.
-Notice that this is not like saying _"up to the record `F`"_. It's important to understand that a `cutoff` refers just to a value
+Notice that this is not like saying _"up to the record `X`"_. It's important to understand that a `cutoff` refers just to a value
in a column (or a combination of multiple column, in case of muti-columns keysets).
-Indeed, that very record could be deleted right after we read it, and our `cutoff` will still be the valid reference that _"we paginated the `set`, up to the "F" value"_...
+Indeed, that very record could be deleted right after we read it, and our `cutoff` will still be the valid reference that we
+paginated the `set`, up to the "X" value", cutting off the `page` any further record...
+
```
- | page | page | not yet paginated |
-beginning ->|. . . . . F]. . . . . .|. . . . . . . . . . . . . . . . . . . . .|<- end of set
- |
- cutoff-F
+ │ first page >│ second page >│ rest >│
+beginning of set >[· · · · · · · · · X]· · · · · · · · · ·]· · · · · · · ·]< end of set
+ ▲
+ cutoff-X
```
-For getting the `next` page of records - this time - we pull the `next` 6 records AFTER the `cutoff-F`. Again, we read the `id` of the last one, which is `L`: so we have our new `cutoff-L`, which is the end of the current `page`, and the `next` will go AFTER it...
+For getting the `next` page of records (i.e. the _"second page"_) we pull the `next` 10 records AFTER the `cutoff-X`. Again, we
+read the `id` of the last one, which is `Y`: so we have our new `cutoff-Y`, which is the end of the current `page`, and the `next`
+will go AFTER it...
```
- | page | page | page | not yet paginated |
-beginning ->|. . . . . F]. . . . . L]. . . . . .|. . . . . . . . . . . . . . .|<- end of set
- | |
- cutoff-F cutoff-L
+ │ first page >│ second page >│ last page >│
+beginning of set >[· · · · · · · · · X]· · · · · · · · · Y]· · · · · · · ·]< end of set
+ ▲ ▲
+ cutoff-X cutoff-Y
```
-
-Pagy encodes the values of the `cutoffs` in a `Base64` URL-safe string that is sent as a param in the `request`.
+
+When we pull the `next` page from the `cutoff-Y` we find only the remaining 8 records, which means that it's the _"last page"_,
+which doesn't have a `cutoff` because it ends with the end of the `set`.
+
+#### Keynotes
+
+- A `cutoff` identifies a "cutoff value", for a `page` in the `set`. It is not a record nor a reference to it.
+- Its value is derived from the `keyset attributes values` array of the last record of the `page`, converted to JSON and encoded
+ as a Base64 URL-safe string for easy use in URLs.
+ - `Pagy::Keyset` embeds it in the request URL; `Pagy::KeysetForUI` caches it on the server.
+- All the `page`s but the last, end with the `cutoff`.
+- All the `page`s but the first, begin AFTER the `cutoff` of the previous `page`.
## ORMs
@@ -248,8 +265,9 @@ Default `nil`.
==- `:jsonify_keyset_attributes`
-A lambda to override the generic json encoding of the `keyset` attributes. It receives the keyset attributes to jsonify, and it should return a JSON string of the `attributes.values` array. Use it when the generic `to_json` method would lose
-some information when decoded.
+A lambda to override the generic JSON encoding of the `keyset attributes`. It receives the `keyset attributes` as an arument, and
+it should return a JSON string of the `attributes.values` array. Use it when the generic `to_json` method would lose some
+information when decoded.
For example: `Time` objects may lose or round the fractional seconds through the encoding/decoding cycle, causing the ordering to
fail and thus creating all sort of unexpected behaviors (e.g. skipping or repeating the same page, missing or duplicated records,
@@ -260,7 +278,7 @@ etc.). Here is what you can do:
jsonify_keyset_attributes = lambda do |attributes|
# Convert it to a string matching the stored value/format in SQLite DB
attributes[:created_at] = attributes[:created_at].strftime('%F %T.%6N')
- attributes.values.to_json # remember to return an array of the values only
+ attributes.values.to_json # remember to return the array of values, not the attribute hash
end
Pagy::Keyset(set, jsonify_keyset_attributes:)