# File lib/pageturner.rb, line 135 def primary_key_selected?(ar_relation) ar_relation.select_values.empty? || ar_relation.select_values.include?(@ar_relation.primary_key.to_sym) end
class Pageturner
Constants
- ASC
- COMPARATOR_INTERNAL_BUG
- DESC
- GREATER_THAN_OPERATOR
- LESS_THAN_OPERATOR
- LOOKAHEAD
Public Class Methods
@param [String] anchor_column - Field to paginate on. @param [Anything, nil] anchor_value - Value of the anchor_column for the record to paginate from. @param [ActiveRecord::Relation] ar_relation - Resource collection to paginate. @param [String] sort_direction - Order of the pagination. Valid values: Pageturner::ASC
, Pageturner::DESC
. @param [Number] anchor_id - ID of the record to paginate from. @param [String, nil] anchor_column_active_record_identifier - Method chain that references the anchor column in the model you are paginating. @param [String, nil] anchor_column_sql_identifier - SQL identifier that references the anchor column in your database schema.
# File lib/pageturner.rb, line 16 def initialize(anchor_column:, anchor_value:, ar_relation:, sort_direction:, page_size:, anchor_id:, anchor_column_active_record_identifier: nil, anchor_column_sql_identifier: nil) @anchor_column = anchor_column @anchor_column_active_record_identifier = anchor_column_active_record_identifier || @anchor_column @anchor_column_sql_identifier = anchor_column_sql_identifier || @anchor_column @anchor_value = anchor_value @ar_relation = ar_relation # Always select id to use it as secondary sort to prevent non-deterministic ordering when primary sort is on a non-unique column. unless primary_key_selected?(@ar_relation) raise Exception::PrimaryKeyNotSelected, "The provided ActiveRecord::Relation does not have the primary key selected. Cursor pagination requires a primary key to paginate undeterministic columns." end @sort_direction = sort_direction @page_size = page_size @anchor_id = anchor_id unless [Pageturner::ASC, Pageturner::DESC].include?(@sort_direction) raise Exception::InvalidSortDirection, "The provided order is not supported. Supported order values are: '#{ASC}', '#{DESC}'" end end
Public Instance Methods
# File lib/pageturner.rb, line 40 def has_next_page? # Load the current page. self.resources @has_next_page end
# File lib/pageturner.rb, line 54 def next_cursor { anchor_column: @anchor_column, anchor_id: self.resources.last&.id, anchor_value: try_chain(self.resources.last, @anchor_column_active_record_identifier), page_size: @page_size, sort_direction: @sort_direction } end
# File lib/pageturner.rb, line 47 def resources # Memoize expensive querying. return @result if defined?(@result) @result = calculate_resources end
Private Instance Methods
# File lib/pageturner.rb, line 117 def apply_order_and_limit(ar_relation) ar_relation .order("#{@anchor_column_sql_identifier} #{@sort_direction}", @ar_relation.primary_key => @sort_direction) .limit(@page_size + LOOKAHEAD) end
# File lib/pageturner.rb, line 87 def calculate_first_page apply_order_and_limit(@ar_relation) end
# File lib/pageturner.rb, line 91 def calculate_next_page # Heuristic to check if the `anchor_column_sql_identifier` is a joined column. if @anchor_column_sql_identifier.include?(".") table, column = @anchor_column_sql_identifier.split(".") qualified_anchor_column = "`#{table}`.`#{column}`" else qualified_anchor_column = "#{@ar_relation.quoted_table_name}.#{ActiveRecord::Base.connection.quote_column_name(@anchor_column)}" end qualified_anchor_pk_column = "#{@ar_relation.quoted_table_name}.#{@ar_relation.quoted_primary_key}" where_clause = if nulls_listed_first? && !@anchor_value.nil? @ar_relation.where("(#{qualified_anchor_column}, #{qualified_anchor_pk_column}) #{comparator_for_fetching_resources} (?, ?)", @anchor_value, @anchor_id) elsif nulls_listed_first? && @anchor_value.nil? @ar_relation.where("(#{qualified_anchor_column} IS NULL AND #{qualified_anchor_pk_column} #{comparator_for_fetching_resources} ?) OR (#{qualified_anchor_column} IS NOT NULL)", @anchor_id) elsif nulls_listed_last? && !@anchor_value.nil? @ar_relation.where("(#{qualified_anchor_column} IS NULL) OR ((#{qualified_anchor_column}, #{qualified_anchor_pk_column}) #{comparator_for_fetching_resources} (?, ?))", @anchor_value, @anchor_id) elsif nulls_listed_last? && @anchor_value.nil? @ar_relation.where("#{qualified_anchor_column} IS NULL AND #{qualified_anchor_pk_column} #{comparator_for_fetching_resources} ?", @anchor_id) end apply_order_and_limit(where_clause) end
# File lib/pageturner.rb, line 66 def calculate_resources (can_calculate_nth_page? ? calculate_next_page : calculate_first_page).tap do |resources| @has_next_page = resources.size > @page_size end.take(@page_size) end
# File lib/pageturner.rb, line 83 def can_calculate_nth_page? @anchor_column && @anchor_id end
# File lib/pageturner.rb, line 72 def comparator_for_fetching_resources case @sort_direction when Pageturner::ASC GREATER_THAN_OPERATOR when Pageturner::DESC LESS_THAN_OPERATOR else raise COMPARATOR_INTERNAL_BUG end end
# File lib/pageturner.rb, line 123 def nulls_listed_first? # MySQL sorts nulls first for ascending order, and null last for descending order. # TODO: # The concept of null ordering should be database agnostic and modifiable. # For example, UX might decide that we need some other type of ordering. @sort_direction == Pageturner::ASC end
# File lib/pageturner.rb, line 131 def nulls_listed_last? !nulls_listed_first? end
Given attribute_chain = “association.attribute”, it will call `.association.attribute` on `object`.
# File lib/pageturner.rb, line 140 def try_chain(object, attribute_chain) attribute_chain.split(".").reduce(object) { |o, command| o.try!(command) } end