{"id":7457410,"url":"https://github.com/healthie/activerecord_cursor_paginate","last_synced_at":"2025-10-08T17:10:47.657Z","repository":{"id":226594282,"uuid":"767565698","full_name":"healthie/activerecord_cursor_paginate","owner":"healthie","description":"Cursor-based pagination for ActiveRecord","archived":false,"fork":false,"pushed_at":"2025-04-08T16:23:19.000Z","size":68,"stargazers_count":149,"open_issues_count":1,"forks_count":7,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-09-09T02:58:51.005Z","etag":null,"topics":["activerecord","gem","pagination","rails"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/healthie.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-03-05T14:23:40.000Z","updated_at":"2025-07-29T17:55:24.000Z","dependencies_parsed_at":null,"dependency_job_id":"4adf3ff2-a9d2-473c-8653-454677124e25","html_url":"https://github.com/healthie/activerecord_cursor_paginate","commit_stats":{"total_commits":20,"total_committers":6,"mean_commits":"3.3333333333333335","dds":0.25,"last_synced_commit":"a8d16e016c15c37de1be731ea606bb780f4f9b23"},"previous_names":["fatkodima/activerecord_cursor_paginate","healthie/activerecord_cursor_paginate"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/healthie/activerecord_cursor_paginate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/healthie%2Factiverecord_cursor_paginate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/healthie%2Factiverecord_cursor_paginate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/healthie%2Factiverecord_cursor_paginate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/healthie%2Factiverecord_cursor_paginate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/healthie","download_url":"https://codeload.github.com/healthie/activerecord_cursor_paginate/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/healthie%2Factiverecord_cursor_paginate/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278677156,"owners_count":26026872,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-06T02:00:05.630Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","gem","pagination","rails"],"created_at":"2024-04-06T09:06:04.529Z","updated_at":"2025-10-08T17:10:47.626Z","avatar_url":"https://github.com/healthie.png","language":"Ruby","funding_links":[],"categories":["Ruby","Pagination"],"sub_categories":[],"readme":"# ActiveRecordCursorPaginate\n\nThis library allows to paginate through an `ActiveRecord` relation using cursor pagination.\nIt also supports ordering by any column on the relation in either ascending or descending order.\n\nCursor pagination allows to paginate results and gracefully deal with deletions / additions on previous pages.\nWhere a regular limit / offset pagination would jump in results if a record on a previous page gets deleted or added while requesting the next page, cursor pagination just returns the records following the one identified in the request.\n\nTo learn more about cursor pagination, check out the _\"How does it work?\"_ section below.\n\n[![Build Status](https://github.com/healthie/activerecord_cursor_paginate/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/healthie/activerecord_cursor_paginate/actions/workflows/test.yml)\n\n## Requirements\n\n- Ruby 2.7+\n- Rails (ActiveRecord) 7.0+\n\nIf you need support for older ruby and rails, please open an issue.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"activerecord_cursor_paginate\"\n```\n\nAnd then run:\n\n```sh\n$ bundle install\n```\n\n## Usage\n\nLet's assume we have a `Post` model of which we want to fetch some data and then paginate through it.\nTherefore, we first apply our scopes, `where` clauses or other functionality as usual:\n\n```ruby\nposts = Post.where(author: \"Jane\")\n```\n\nAnd then we create our paginator to fetch the first response page:\n\n```ruby\npaginator = posts.cursor_paginate\n\n# Total number of records to iterate by this paginator\npaginator.total_count # =\u003e 145\n\npage = paginator.fetch\npage.records # =\u003e [#\u003cPost:0x00007fd7071b2ea8 @id=1\u003e, #\u003cPost:0x00007fd7071bb738 @id=2\u003e, ..., #\u003cPost:0x00007fd707238260 @id=10\u003e]\n\n# Number of records in this page\npage.count # =\u003e 10\n\npage.empty? # =\u003e false\npage.cursors            # =\u003e [\"MQ\", \"Mg\", ..., \"MTA\"]\n                               |                 |\n                               |                 |\npage.previous_cursor    # =\u003e  \"MQ\"               |\npage.next_cursor        # =\u003e  \"MTA\" -------------|\npage.has_previous? # =\u003e false\npage.has_next? # =\u003e true\n```\n\nNote that any ordering of the relation at this stage will be ignored by the gem.\nTake a look at the next section _\"Ordering\"_ to see how you can have an order different than ascending IDs.\n\nTo then get the next result page, you simply need to pass the last cursor of the returned page item via:\n\n```ruby\npaginator = posts.cursor_paginate(after: \"MTA\")\n```\n\nThis will then fetch the next result page.\nYou can also just as easily paginate to previous pages by using `before` instead of `after` and using the first cursor of the current page.\n\n```ruby\npaginator = posts.cursor_paginate(before: \"MQ\")\n```\n\nBy default, this will always return up to 10 results. But you can also specify how many records should be returned via `limit` parameter.\n\n```ruby\npaginator = posts.cursor_paginate(after: \"MTA\", limit: 2)\n```\n\n```ruby\npaginator = posts.cursor_paginate(before: \"MQ\", limit: 2)\n```\n\nYou can also easily iterate over the whole relation:\n\n```ruby\npaginator = posts.cursor_paginate\n\n# Will lazily iterate over the pages.\npaginator.pages.each do |page|\n  # do something with the page\nend\n```\n\n### Ordering\n\nAs said, this gem ignores any previous ordering added to the passed relation.\nBut you can still paginate through relations with an order different than by ascending IDs.\n\nYou can specify a different column and direction to order the results by via an `order` parameter.\n\n```ruby\n# Order records ascending by the `:author` field.\npaginator = posts.cursor_paginate(order: :author)\n\n# Order records descending by the `:author` field.\npaginator = posts.cursor_paginate(order: { author: :desc })\n\n# Order records ascending by the `:author` and `:title` fields.\npaginator = posts.cursor_paginate(order: [:author, :title])\n\n# Order records ascending by the `:author` and descending by the `:title` fields.\npaginator = posts.cursor_paginate(order: { author: :asc, title: :desc })\n```\n\nThe gem implicitly appends a primary key column to the list of sorting columns. It may be useful\nto disable it for the table with a UUID primary key or when the sorting is done by a combination\nof columns that are already unique.\n\n```ruby\npaginator = UserSettings.cursor_paginate(order: :user_id, append_primary_key: false)\n```\n\n**Important:**\nIf your app regularly orders by another column, you might want to add a database index for this.\nSay that your order column is `author` then you'll want to add a compound index on `(author, id)`.\nIf your table is called `posts` you can use a query like this in MySQL or PostgreSQL:\n\n```sql\nCREATE INDEX index_posts_on_author_and_id ON posts (author, id)\n```\n\nOr you can just do it via an `ActiveRecord::Migration`:\n\n```ruby\nclass AddAuthorAndIdIndexToPosts \u003c ActiveRecord::Migration[7.1]\n  def change\n    add_index :posts, [:author, :id]\n  end\nend\n```\n\nTake a look at the _\"How does it work?\"_ to find out more why this is necessary.\n\n#### Ordering and `JOIN`s\n\nTo order by a column from the `JOIN`ed table, you need to explicitly specify and fully qualify the column name for the `:order` parameter:\n\n```ruby\npaginator = User.joins(:projects).cursor_paginate(order: [Arel.sql(\"projects.id\"), :id])\npage = paginator.fetch\n```\n\n**Note**: Make sure to wrap custom SQL expressions by `Arel.sql`.\n\n#### Order by more complex logic\n\nSometimes you might not only want to order by a column ascending or descending, but need more complex logic.\nImagine you would also store the post's `category` on the `posts` table (as a plain string for simplicity's sake).\nAnd the category could be `pinned`, `announcement`, or `general`.\nThen you might want to show all `pinned` posts first, followed by the `announcement` ones and lastly show the `general` posts.\n\nIn MySQL you could e.g. use a `FIELD(category, 'pinned', 'announcement', 'general')` query in the `ORDER BY` clause to achieve this.\nHowever, you cannot add an index to such a statement. And therefore, the performance of this is pretty dismal.\n\nThe gem supports ordering by custom SQL expressions, but make sure the performance will not suffer.\n\nWhat is recommended if you _do_ need to order by more complex logic is to have a separate column that you only use for ordering.\nYou can use `ActiveRecord` hooks to automatically update this column whenever you change your data.\nOr, for example in MySQL, you can also use a [generated column](https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html) that is automatically being updated by the database based on some stored logic.\n\nFor example, if you want paginate `users` by a lowercased `email`, you can use the following:\n\n```ruby\npaginator = User.cursor_paginate(order: Arel.sql(\"lower(email)\"))\npage = paginator.fetch\n```\n\n**Note**: Make sure to wrap custom SQL expressions by `Arel.sql`.\n\n### Configuration\n\nYou can change the default page size to a value that better fits the needs of your application.\nSo if a user doesn't request a given `:limit` value, the default amount of records is being returned.\n\nTo change the default, simply add an initializer to your app that does the following:\n\n```ruby\n# config/initializers/activerecord_cursor_paginate.rb\nActiveRecordCursorPaginate.configure do |config|\n  config.default_page_size = 50\nend\n```\n\nThis would set the default page size to 50.\n\nYou can also set a global `max_page_size` to prevent a client from requesting too large pages.\n\n```ruby\nActiveRecordCursorPaginate.configure do |config|\n  config.max_page_size = 100\nend\n```\n\n## How does it work?\n\nThis library allows to paginate through a passed relation using a cursor\nfor before or after parameters. It also supports ordering by any column\non the relation in either ascending or descending order.\n\nCursor pagination allows to paginate results and gracefully deal with\ndeletions / additions on previous pages. Where a regular limit / offset\npagination would jump in results if a record on a previous page gets deleted\nor added while requesting the next page, cursor pagination just returns the\nrecords following the one identified in the request.\n\nHow this works is that it uses a \"cursor\", which is an encoded value that\nuniquely identifies a given row for the requested order. Then, based on\nthis cursor, you can request the \"n records AFTER the cursor\"\n(forward-pagination) or the \"n records BEFORE the cursor\" (backward-pagination).\n\nAs an example, assume we have a table called \"posts\" with this data:\n\n| id | author |\n|----|--------|\n| 1  | Jane   |\n| 2  | John   |\n| 3  | John   |\n| 4  | Jane   |\n| 5  | Jane   |\n| 6  | John   |\n| 7  | John   |\n\nNow if we make a basic request without any `before`, `after`, custom `order` column,\nthis will just request the first page of this relation.\n\n```ruby\npaginator = relation.cursor_paginate\npage = paginator.fetch\n```\n\nAssume that our default page size here is 2 and we would get a query like this:\n\n```sql\nSELECT *\nFROM posts\nORDER BY id ASC\nLIMIT 2\n```\n\nThis will return the first page of results, containing post #1 and #2. Since\nno custom order is defined, each item in the returned collection will have a\ncursor that only encodes the record's ID.\n\nIf we want to now request the next page, we can pass in the cursor of record\n#2 which would be `\"Mg\"` (can get via `page.cursor`). So now we can request\nthe next page by calling:\n\n```ruby\npaginator = relation.cursor_paginate(limit: 2, after: \"Mg\")\npage = paginator.fetch\n```\n\nAnd this will decode the given cursor and issue a query like:\n\n```sql\nSELECT *\nFROM posts\nWHERE id \u003e 2\nORDER BY id ASC\nLIMIT 2\n```\n\nWhich would return posts #3 and #4. If we now want to paginate back, we can\nrequest the posts that came before the first post, whose cursor would be\n`\"Mw\"` (can get via `page.previous_cursor`):\n\n```ruby\npaginator = relation.cursor_paginate(limit: 2, before: \"Mw\")\npage = paginator.fetch\n```\n\nSince we now paginate backward, the resulting SQL query needs to be flipped\naround to get the last two records that have an ID smaller than the given one:\n\n```sql\nSELECT *\nFROM posts\nWHERE id \u003c 3\nORDER BY id DESC\nLIMIT 2\n```\n\nThis would return posts #2 and #1. Since we still requested them in\nascending order, the result will be reversed before it is returned.\n\nNow, in case that the user wants to order by a column different than the ID,\nwe require this information in our cursor. Therefore, when requesting the\nfirst page like this:\n\n```ruby\npaginator = relation.cursor_paginate(order: :author)\npage = paginator.fetch\n```\n\nThis will issue the following SQL query:\n\n```sql\nSELECT *\nFROM posts\nORDER BY author ASC, id ASC\nLIMIT 2\n```\n\nAs you can see, this will now order by the author first, and if two records\nhave the same author it will order them by ID. Ordering only the author is not\nenough since we cannot know if the custom column only has unique values.\nAnd we need to guarantee the correct order of ambiguous records independent\nof the direction of ordering. This unique order is the basis of being able\nto paginate forward and backward repeatedly and getting the correct records.\n\nThe query will then return records #1 and #4. But the cursor for these\nrecords will also be different to the previous query where we ordered by ID\nonly. It is important that the cursor encodes all the data we need to\nuniquely identify a row and filter based upon it. Therefore, we need to\nencode the same information as we used for the ordering in our SQL query.\nHence, the cursor for pagination with a custom column contains a tuple of\ndata, the first record being the custom order column followed by the\nrecord's ID.\n\nTherefore, the cursor of record #4 will encode `['Jane', 4]`, which yields\nthis cursor: `\"WyJKYW5lIiw0XQ\"`.\n\nIf we now want to request the next page via:\n\n```ruby\npaginator = relation.cursor_paginate(order: :author, limit: 2, after: \"WyJKYW5lIiw0XQ\")\npage = paginator.fetch\n```\n\nWe get this SQL query:\n\n```sql\nSELECT *\nFROM posts\nWHERE (author \u003e 'Jane' OR (author = 'Jane') AND (id \u003e 4))\nORDER BY author ASC, id ASC\nLIMIT 2\n```\n\nYou can see how the cursor is being used by the WHERE clause to uniquely\nidentify the row and properly filter based on this. We only want to get\nrecords that either have a name that is alphabetically after `\"Jane\"` or\nanother `\"Jane\"` record with an ID that is higher than `4`. We will get the\nrecords #5 and #2 as response.\n\nWhen using a custom `order`, this affects both filtering as well as\nordering. Therefore, it is recommended to add an index for columns that are\nfrequently used for ordering. In our test case we would want to add a compound\nindex for the `(author, id)` column combination. Databases like MySQL and\nPostgreSQL are able to then use the leftmost part of the index, in our case\n`author`, by its own or can use it combined with the `id` index.\n\n## Credits\n\nThanks to [rails_cursor_pagination gem](https://github.com/xing/rails_cursor_pagination) for the original ideas.\n\n## Development\n\nAfter checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rake` to run the linter and tests. This project uses multiple Gemfiles to test against multiple versions of Active Record; you can run the tests against the specific version with `BUNDLE_GEMFILE=gemfiles/activerecord_71.gemfile bundle exec rake test`.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/healthie/activerecord_cursor_paginate.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhealthie%2Factiverecord_cursor_paginate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhealthie%2Factiverecord_cursor_paginate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhealthie%2Factiverecord_cursor_paginate/lists"}