Skip to content

Instantly share code, notes, and snippets.

@dblock
Created February 14, 2013 14:53
Show Gist options
  • Save dblock/4953303 to your computer and use it in GitHub Desktop.
Save dblock/4953303 to your computer and use it in GitHub Desktop.
A way to paginate "by cursor" with mongoid.
def cursor_and_tiebreak_id_from_params(options = {})
cursor, tiebreak_id = nil
if params[:cursor]
raw_cursor = params[:cursor].split(":")[0...-1].join(":")
cursor = (options[:field_type] == String ? raw_cursor : raw_cursor.to_i)
tiebreak_id = params[:cursor].split(":")[-1]
elsif options[:default_cursor]
cursor = (options[:field_type] == String ? options[:default_cursor] : options[:default_cursor].to_i)
end
[ cursor, tiebreak_id ]
end
# apply cursor-based pagination to a collection
# returns a hash:
# results: (paginated collection subset)
# next: (cursor to the next page)
def paginate_by_cursor(coll, options = {})
# Cursor-based pagination requires an ordering, on a single field
unless coll.is_a?(Mongoid::Criteria) &&
(coll.options[:sort] || {}).keys.size == 1
raise "paginate_by_cursor must be called on an ordered criterion"
end
ordering_field = coll.options[:sort].keys.first
# For now, require that the ordering field be a Time DateTime, or String
field_type = coll.klass.fields[ordering_field.to_s].type
unless [Time, DateTime, String].include?(field_type)
raise "paginate_by_cursor requires an ordering field of type Time or DateTime"
end
ordering_criteria = ([:asc, Mongo::ASCENDING].include?(coll.options[:sort][ordering_field]) ? '$gt' : '$lt')
cursor, tiebreak_id = cursor_and_tiebreak_id_from_params(options.merge(field_type: field_type))
size = (params[:size] || 5).to_i
# Inject secondary field sort by :_id
coll.options[:sort].merge!(:_id => Mongo::ASCENDING)
cursor_criteria = { ordering_field => { ordering_criteria => cursor } } if cursor
tiebreak_criteria = { ordering_field => cursor, :_id => { '$gt' => tiebreak_id } } if cursor && tiebreak_id
union_criteria = (cursor_criteria || tiebreak_criteria) ? { '$or' => [cursor_criteria, tiebreak_criteria].compact } : {}
results = coll.where(union_criteria).limit(size).to_a
annotate_results(results, ordering_field, field_type)
end
def union_and_paginate_by_cursor(*colls)
unless colls.map { |coll| coll.options[:sort].keys.first }.uniq.size == 1
raise "All collections must share the same ordering field"
end
ordering_field = colls[0].options[:sort].keys.first
unless colls.map { |coll| coll.klass.fields[ordering_field.to_s].type }.uniq.size == 1
raise "All collections must have ordering fields of the same type"
end
field_type = colls[0].klass.fields[ordering_field.to_s].type
unless colls.map { |coll| coll.options[:sort][ordering_field] }.uniq.size == 1
raise "All collections must share the same ordering direction"
end
ordering_direction = colls[0].options[:sort][ordering_field]
raw_results = colls.map { |coll| paginate_by_cursor(coll)[:results] }
results = raw_results.flatten.compact.sort { |x, y|
xpos = x.send(ordering_field)
ypos = y.send(ordering_field)
if x == y
x._id <=> y._id
elsif [:asc, Mongo::ASCENDING].include?(ordering_direction)
xpos <=> ypos
else
ypos <=> xpos
end
}.take((params[:size] || 5).to_i)
annotate_results(results, ordering_field, field_type)
end
private
def annotate_results(results, ordering_field, field_type)
if results.last
raw_cursor = results.last.send(ordering_field)
serialized_cursor = (field_type == String ? raw_cursor : raw_cursor.to_i)
next_cursor = "#{serialized_cursor}:#{results.last.id}"
else
next_cursor = nil
end
{ results: results, next: next_cursor }
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment