From fbff08406dffdc954bc70268b8d8031254ba7ccc Mon Sep 17 00:00:00 2001 From: fatkodima Date: Sat, 18 Jan 2025 16:56:52 +0200 Subject: [PATCH] Add ability to paginate backward from the end of the collection --- CHANGELOG.md | 6 +++++ lib/activerecord_cursor_paginate/paginator.rb | 26 ++++++++++++------- test/paginator_test.rb | 10 +++++++ 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae3998..009475b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## master (unreleased) +- Add ability to paginate backward from the end of the collection + + ```ruby + paginator = users.cursor_paginate(forward_pagination: false) + ``` + ## 0.3.0 (2025-01-06) - Allow paginating over nullable columns diff --git a/lib/activerecord_cursor_paginate/paginator.rb b/lib/activerecord_cursor_paginate/paginator.rb index 8feabe0..8cba5f4 100644 --- a/lib/activerecord_cursor_paginate/paginator.rb +++ b/lib/activerecord_cursor_paginate/paginator.rb @@ -20,6 +20,7 @@ module ActiveRecordCursorPaginate # class Paginator attr_reader :relation, :before, :after, :limit, :order, :append_primary_key + attr_accessor :forward_pagination # Create a new instance of the `ActiveRecordCursorPaginate::Paginator` # @@ -45,10 +46,14 @@ class Paginator # It is not recommended to use this feature, because the complexity of produced SQL # queries can have a very negative impact on the database performance. It is better # to paginate using only non-nullable columns. + # @param forward_pagination [Boolean] Whether this is a forward or backward pagination. + # Optional, defaults to `true` if `:before` is not provided, `false` otherwise. + # Useful when paginating backward from the end of the collection. # # @raise [ArgumentError] If any parameter is not valid # - def initialize(relation, before: nil, after: nil, limit: nil, order: nil, append_primary_key: true, nullable_columns: nil) + def initialize(relation, before: nil, after: nil, limit: nil, order: nil, append_primary_key: true, + nullable_columns: nil, forward_pagination: before.nil?) unless relation.is_a?(ActiveRecord::Relation) raise ArgumentError, "relation is not an ActiveRecord::Relation" end @@ -58,7 +63,7 @@ def initialize(relation, before: nil, after: nil, limit: nil, order: nil, append @append_primary_key = append_primary_key @cursor = @current_cursor = nil - @is_forward_pagination = true + @forward_pagination = forward_pagination @before = @after = nil @page_size = nil @limit = nil @@ -80,17 +85,18 @@ def before=(value) @cursor = value || after @current_cursor = @cursor - @is_forward_pagination = value.blank? + @forward_pagination = false if value @before = value end def after=(value) - if before.present? && value.present? + if value.present? && before.present? raise ArgumentError, "Only one of :before and :after can be provided" end - @cursor = before || value + @cursor = value || before @current_cursor = @cursor + @forward_pagination = true if value @after = value end @@ -136,9 +142,9 @@ def fetch has_additional = records_plus_one.size > @page_size records = records_plus_one.take(@page_size) - records.reverse! unless @is_forward_pagination + records.reverse! unless @forward_pagination - if @is_forward_pagination + if @forward_pagination has_next_page = has_additional has_previous_page = @current_cursor.present? else @@ -301,7 +307,7 @@ def arel_column(column) end def pagination_direction(direction) - if @is_forward_pagination + if @forward_pagination direction else direction == :asc ? :desc : :asc @@ -309,7 +315,7 @@ def pagination_direction(direction) end def pagination_operator(direction) - if @is_forward_pagination + if @forward_pagination direction == :asc ? :gt : :lt else direction == :asc ? :lt : :gt @@ -318,7 +324,7 @@ def pagination_operator(direction) def advance_by_page(page) @current_cursor = - if @is_forward_pagination + if @forward_pagination page.next_cursor else page.previous_cursor diff --git a/test/paginator_test.rb b/test/paginator_test.rb index b56bda7..dc5c91c 100644 --- a/test/paginator_test.rb +++ b/test/paginator_test.rb @@ -80,6 +80,16 @@ def test_paginating_by_multiple_nullable_cursor_columns_in_desc_order assert_equal Project.order(name: :desc, organization_id: :asc, id: :asc).to_a, records end + def test_paginating_backward_from_the_end + p = User.cursor_paginate(limit: 3, forward_pagination: false) + + ids = p.pages.map do |page| + page.records.pluck(:id) + end + + assert_equal [[8, 9, 10], [5, 6, 7], [2, 3, 4], [1]], ids + end + def test_uses_default_limit ActiveRecordCursorPaginate.config.stub(:default_page_size, 4) do p = User.cursor_paginate