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:)