Class: ActiveRecordCursorPaginate::Paginator

Inherits:
Object
  • Object
show all
Defined in:
lib/activerecord_cursor_paginate/paginator.rb

Overview

Use this Paginator class to effortlessly paginate through ActiveRecord relations using cursor pagination.

Examples:

Iterating one page at a time

ActiveRecordCursorPaginate::Paginator
  .new(relation, order: :author, limit: 2, after: "WyJKYW5lIiw0XQ")
  .fetch

Iterating over the whole relation

paginator = ActiveRecordCursorPaginate::Paginator
              .new(relation, order: :author, limit: 2, after: "WyJKYW5lIiw0XQ")

# Will lazily iterate over the pages.
paginator.pages.each do |page|
  # do something with the page
end

Instance Method Summary collapse

Constructor Details

#initialize(relation, before: nil, after: nil, limit: nil, order: nil, append_primary_key: true) ⇒ Paginator

Create a new instance of the ‘ActiveRecordCursorPaginate::Paginator`

Parameters:

  • relation (ActiveRecord::Relation)

    Relation that will be paginated.

  • before (String, nil) (defaults to: nil)

    Cursor to paginate upto (excluding).

  • after (String, nil) (defaults to: nil)

    Cursor to paginate forward from.

  • limit (Integer, nil) (defaults to: nil)

    Number of records to return in pagination.

  • order (Symbol, String, nil, Array<Symbol, String>, Hash) (defaults to: nil)

    Column(s) to order by, optionally with directions (either ‘:asc` or `:desc`, defaults to `:asc`). If none is provided, will default to ID column. NOTE: this will cause the query to filter on both the given column as well as the ID column. So you might want to add a compound index to your database similar to: “`sql

    CREATE INDEX <index_name> ON <table_name> (<order_fields>..., id)
    

    “‘

  • append_primary_key (Boolean) (defaults to: true)

    (true). Specifies whether the primary column(s) should be implicitly appended to the list of sorting columns. It may be useful to disable it for the table with a UUID primary key or when the sorting is done by a combination of columns that are already unique.

Raises:

  • (ArgumentError)

    If any parameter is not valid



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/activerecord_cursor_paginate/paginator.rb', line 43

def initialize(relation, before: nil, after: nil, limit: nil, order: nil, append_primary_key: true)
  unless relation.is_a?(ActiveRecord::Relation)
    raise ArgumentError, "relation is not an ActiveRecord::Relation"
  end

  if before.present? && after.present?
    raise ArgumentError, "Only one of :before and :after can be provided"
  end

  @relation = relation
  @primary_key = @relation.primary_key
  @cursor = before || after
  @is_forward_pagination = before.blank?

  config = ActiveRecordCursorPaginate.config
  @page_size = limit || config.default_page_size
  @page_size = [@page_size, config.max_page_size].min if config.max_page_size

  @append_primary_key = append_primary_key
  order = normalize_order(order)
  @columns = order.keys
  @directions = order.values
end

Instance Method Details

#fetchActiveRecordCursorPaginate::Page Also known as: page

Note:

Calling this method advances the paginator.

Get the paginated result.



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/activerecord_cursor_paginate/paginator.rb', line 72

def fetch
  relation = @relation

  # Non trivial columns (expressions or joined tables columns).
  if @columns.any?(/\W/)
    arel_columns = @columns.map.with_index do |column, i|
      arel_column(column).as("cursor_column_#{i + 1}")
    end
    cursor_column_names = arel_columns.map { |column| column.right.to_s }

    relation =
      if relation.select_values.empty?
        relation.select(relation.arel_table[Arel.star], arel_columns)
      else
        relation.select(arel_columns)
      end
  else
    cursor_column_names = @columns
  end

  pagination_directions = @directions.map { |direction| pagination_direction(direction) }
  relation = relation.reorder(cursor_column_names.zip(pagination_directions).to_h)

  if @cursor
    decoded_cursor = Cursor.decode(cursor_string: @cursor, columns: @columns)
    relation = apply_cursor(relation, decoded_cursor)
  end

  relation = relation.limit(@page_size + 1)
  records_plus_one = relation.to_a
  has_additional = records_plus_one.size > @page_size

  records = records_plus_one.take(@page_size)
  records.reverse! unless @is_forward_pagination

  if @is_forward_pagination
    has_next_page = has_additional
    has_previous_page = @cursor.present?
  else
    has_next_page = @cursor.present?
    has_previous_page = has_additional
  end

  page = Page.new(
    records,
    order_columns: cursor_column_names,
    has_next: has_next_page,
    has_previous: has_previous_page
  )

  advance_by_page(page) unless page.empty?

  page
end

#pagesEnumerator

Returns an enumerator that can be used to iterate over the whole relation.

Returns:

  • (Enumerator)


131
132
133
134
135
136
137
138
139
140
# File 'lib/activerecord_cursor_paginate/paginator.rb', line 131

def pages
  Enumerator.new do |yielder|
    loop do
      page = fetch
      break if page.empty?

      yielder.yield(page)
    end
  end
end