class Pagy::Keyset
Implement wicked-fast keyset pagination for big data
def self.new(set, **vars)
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
def filter_newest_query
with a set like Pet.order(:animal, :name, :id) it returns the following string:
When :tuple_comparison is enabled, and if the order is all :asc or all :desc,
( "pets"."animal" > :animal )
( "pets"."animal" = :animal AND "pets"."name" < :name ) OR
( "pets"."animal" = :animal AND "pets"."name" = :name AND "pets"."id" > :id ) OR
With a set like Pet.order(animal: :asc, name: :desc, id: :asc) it returns the following string:
For example:
used to filter the newest records.
Prepare the literal query string (complete with the placeholders for value interpolation)
def filter_newest_query operator = { asc: '>', desc: '<' } directions = @keyset.values table = @set.model.table_name name = @keyset.to_h { |column| [column, %("#{table}"."#{column}")] } if @vars[:tuple_comparison] && (directions.all?(:asc) || directions.all?(:desc)) placeholders = @keyset.keys.map { |column| ":#{column}" }.join(', ') "( #{name.values.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| "#{name[column]} = :#{column}" } \ << "#{name[last_column]} #{operator[last_direction]} :#{last_column}").join(' AND ') query << ' )' where << query end where.join(' OR ') end end
def initialize(set, **vars)
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 = typecast_latest(latest) raise InternalError, 'page and keyset are not consistent' \ unless @latest.keys == @keyset.keys end
def next
def next records return unless @more @next ||= begin hash = keyset_attributes_from(@records.last) json = @vars[:jsonify_keyset_attributes]&.(hash) || hash.to_json B64.urlsafe_encode(json) end end
def records
def records @records ||= begin @set = apply_select if select? if @latest # :nocov: @set = @vars[:after_latest]&.(@set, @latest) || # deprecated # :nocov: @vars[:filter_newest]&.(@set, @latest, @keyset) || filter_newest end records = @set.limit(@limit + 1).to_a @more = records.size > @limit && !records.pop.nil? records end end