lib/pagy/keyset.rb
# See Pagy API documentation: https://ddnexus.github.io/pagy/docs/api/keyset # frozen_string_literal: true require 'json' require_relative 'b64' class Pagy # Implement wicked-fast keyset pagination for big data class Keyset class TypeError < ::TypeError; end include SharedMethods # Pick the right adapter for the set def self.new(set, **vars) if self == Pagy::Keyset if defined?(::ActiveRecord) && set.is_a?(::ActiveRecord::Relation) ActiveRecord elsif defined?(::Sequel) && set.is_a?(::Sequel::Dataset) Sequel else raise TypeError, "expected set to be an instance of ActiveRecord::Relation or Sequel::Dataset; got #{set.class}" end.new(set, **vars) else allocate.tap { |instance| instance.send(:initialize, set, **vars) } end end attr_reader :latest # Other readers from SharedMethods def initialize(set, **vars) default = DEFAULT.slice(:limit, :page_param, # from pagy :headers, # from headers extra :jsonapi, # from jsonapi extra :limit_param, :limit_max, :limit_extra) # from limit_extra assign_vars({ **default, page: nil }, vars) assign_limit @set = set @page = @vars[:page] @keyset = extract_keyset raise InternalError, 'the set must be ordered' if @keyset.empty? return unless @page latest = JSON.parse(B64.urlsafe_decode(@page)).transform_keys(&:to_sym) @latest = @vars[:typecast_latest]&.(latest) || typecast_latest(latest) raise InternalError, 'page and keyset are not consistent' \ unless @latest.keys == @keyset.keys end # Return the next page def next records return unless @more @next ||= B64.urlsafe_encode(latest_from(@records.last).to_json) end # Retrieve the array of records for the current page def records @records ||= begin @set = apply_select if select? @set = @vars[:after_latest]&.(@set, @latest) || after_latest if @latest records = @set.limit(@limit + 1).to_a @more = records.size > @limit && !records.pop.nil? records end end protected # Prepare the literal query to filter out the already fetched records def after_latest_query operator = { asc: '>', desc: '<' } directions = @keyset.values if @vars[:tuple_comparison] && (directions.all?(:asc) || directions.all?(:desc)) columns = @keyset.keys placeholders = columns.map { |column| ":#{column}" }.join(', ') "( #{columns.join(', ')} ) #{operator[directions.first]} ( #{placeholders} )" else keyset = @keyset.to_a where = [] until keyset.empty? last_column, last_direction = keyset.pop query = +'( ' query << (keyset.map { |column, _d| "#{column} = :#{column}" } \ << "#{last_column} #{operator[last_direction]} :#{last_column}").join(' AND ') query << ' )' where << query end where.join(' OR ') end end end end require_relative 'keyset/active_record' require_relative 'keyset/sequel'