Class: Parse::Cursor

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/parse/query/cursor.rb

Overview

A cursor-based pagination iterator for efficiently traversing large datasets.

Unlike skip/offset pagination which becomes increasingly slow for large datasets, cursor-based pagination uses the last seen objectId to efficiently fetch the next page. This approach maintains consistent performance regardless of how deep into the dataset you paginate.

Examples:

Basic usage with each_page

cursor = Song.cursor(limit: 100, order: :created_at.desc)
cursor.each_page do |page|
  process(page)
end

Using each to iterate over individual items

Song.cursor(limit: 50).each do |song|
  puts song.title
end

With constraints

cursor = Song.cursor(artist: "Artist Name", limit: 25)
cursor.each_page { |page| process(page) }

Manual pagination control

cursor = User.cursor(limit: 100)
first_page = cursor.next_page
second_page = cursor.next_page
cursor.reset! # Start over from the beginning

Constant Summary collapse

MAX_PAGE_SIZE =

Maximum page size allowed (Parse Server limit)

1000
DEFAULT_PAGE_SIZE =

Default page size

100

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(query, limit: DEFAULT_PAGE_SIZE, order: nil) ⇒ Cursor

Create a new cursor-based paginator.

Parameters:

  • query (Parse::Query)

    the base query to paginate

  • limit (Integer) (defaults to: DEFAULT_PAGE_SIZE)

    the number of items per page (default: 100, max: 1000)

  • order (Parse::Order, Symbol) (defaults to: nil)

    the ordering for pagination. Defaults to :created_at.asc for stable ordering. Note: cursor pagination requires a stable sort order.

Raises:

  • (ArgumentError)

    if limit exceeds MAX_PAGE_SIZE



71
72
73
74
75
76
77
78
79
80
81
# File 'lib/parse/query/cursor.rb', line 71

def initialize(query, limit: DEFAULT_PAGE_SIZE, order: nil)
  @query = query.dup
  @page_size = validate_page_size(limit)
  @position = nil
  @pages_fetched = 0
  @items_fetched = 0
  @exhausted = false

  # Set up ordering - cursor pagination needs a stable order
  setup_ordering(order)
end

Instance Attribute Details

#items_fetchedInteger (readonly)

Returns the total number of items fetched so far.

Returns:

  • (Integer)

    the total number of items fetched so far



55
56
57
# File 'lib/parse/query/cursor.rb', line 55

def items_fetched
  @items_fetched
end

#order_directionSymbol (readonly)

Returns the order direction (:asc or :desc).

Returns:

  • (Symbol)

    the order direction (:asc or :desc)



61
62
63
# File 'lib/parse/query/cursor.rb', line 61

def order_direction
  @order_direction
end

#order_fieldSymbol (readonly)

Returns the field to order by for cursor positioning.

Returns:

  • (Symbol)

    the field to order by for cursor positioning



58
59
60
# File 'lib/parse/query/cursor.rb', line 58

def order_field
  @order_field
end

#page_sizeInteger (readonly)

Returns the number of items per page.

Returns:

  • (Integer)

    the number of items per page



46
47
48
# File 'lib/parse/query/cursor.rb', line 46

def page_size
  @page_size
end

#pages_fetchedInteger (readonly)

Returns the number of pages fetched so far.

Returns:

  • (Integer)

    the number of pages fetched so far



52
53
54
# File 'lib/parse/query/cursor.rb', line 52

def pages_fetched
  @pages_fetched
end

#positionString? (readonly)

Returns the current cursor position (objectId of last item).

Returns:

  • (String, nil)

    the current cursor position (objectId of last item)



49
50
51
# File 'lib/parse/query/cursor.rb', line 49

def position
  @position
end

#queryParse::Query (readonly)

Returns the base query for this cursor.

Returns:



43
44
45
# File 'lib/parse/query/cursor.rb', line 43

def query
  @query
end

Class Method Details

.deserialize(json_string) ⇒ Parse::Cursor

Deserialize a cursor from a previously serialized state.

Examples:

Resume a cursor

cursor = Parse::Cursor.deserialize(saved_state)
cursor.each_page { |page| process(page) }

Parameters:

  • json_string (String)

    the serialized cursor state

Returns:

Raises:

  • (ArgumentError)

    if the JSON is invalid or missing required fields



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/parse/query/cursor.rb', line 249

def self.deserialize(json_string)
  require "json"
  state = JSON.parse(json_string, symbolize_names: true)

  # Validate required fields
  required = [:class_name, :page_size, :order_field, :order_direction]
  missing = required.select { |f| state[f].nil? }
  unless missing.empty?
    raise ArgumentError, "Invalid cursor state: missing #{missing.join(", ")}"
  end

  # Get the model class
  klass = Parse::Model.find_class(state[:class_name])
  unless klass
    raise ArgumentError, "Unknown Parse class: #{state[:class_name]}"
  end

  # Rebuild the query
  query = klass.query(state[:constraints] || {})

  # Create the cursor with the original order
  order = state[:order_direction].to_sym == :desc ?
    state[:order_field].to_s.to_sym.desc :
    state[:order_field].to_s.to_sym.asc

  cursor = new(query, limit: state[:page_size], order: order)

  # Restore state
  cursor.instance_variable_set(:@position, state[:position])
  cursor.instance_variable_set(:@last_order_value, deserialize_value(state[:last_order_value]))
  cursor.instance_variable_set(:@last_object_id, state[:last_object_id])
  cursor.instance_variable_set(:@pages_fetched, state[:pages_fetched] || 0)
  cursor.instance_variable_set(:@items_fetched, state[:items_fetched] || 0)
  cursor.instance_variable_set(:@exhausted, state[:exhausted] || false)

  cursor
end

.deserialize_value(value) ⇒ Object

Deserialize a value from JSON storage



309
310
311
312
# File 'lib/parse/query/cursor.rb', line 309

def self.deserialize_value(value)
  return value unless value.is_a?(Hash) && value["__type"] == "Date"
  DateTime.parse(value["iso"])
end

.from_json(json_string) ⇒ Parse::Cursor

Alias for deserialize

Parameters:

  • json_string (String)

    the serialized cursor state

Returns:



290
291
292
# File 'lib/parse/query/cursor.rb', line 290

def self.from_json(json_string)
  deserialize(json_string)
end

Instance Method Details

#allArray<Parse::Object>

Fetch all results at once. Use with caution on large datasets.

Returns:



180
181
182
183
184
# File 'lib/parse/query/cursor.rb', line 180

def all
  results = []
  each_page { |page| results.concat(page) }
  results
end

#each {|Parse::Object| ... } ⇒ self

Iterate over each individual item. This is provided for Enumerable compatibility.

Yields:

Returns:

  • (self)


167
168
169
170
171
172
173
174
175
# File 'lib/parse/query/cursor.rb', line 167

def each(&block)
  return enum_for(:each) unless block_given?

  each_page do |page|
    page.each(&block)
  end

  self
end

#each_page {|Array<Parse::Object>| ... } ⇒ self

Iterate over each page of results.

Yields:

Returns:

  • (self)


151
152
153
154
155
156
157
158
159
160
161
# File 'lib/parse/query/cursor.rb', line 151

def each_page
  return enum_for(:each_page) unless block_given?

  while more_pages?
    page = next_page
    break if page.empty?
    yield page
  end

  self
end

#exhausted?Boolean

Check if the cursor has been exhausted (no more results).

Returns:

  • (Boolean)

    true if all results have been fetched



108
109
110
# File 'lib/parse/query/cursor.rb', line 108

def exhausted?
  @exhausted
end

#more_pages?Boolean

Check if more pages are available.

Returns:

  • (Boolean)

    true if more pages may be available



102
103
104
# File 'lib/parse/query/cursor.rb', line 102

def more_pages?
  !@exhausted
end

#next_pageArray<Parse::Object>, Array

Fetch the next page of results.

Returns:



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/parse/query/cursor.rb', line 115

def next_page
  return [] if @exhausted

  # Build the page query
  page_query = build_page_query

  # Execute the query
  results = page_query.results

  # Update state
  if results.empty? || results.size < @page_size
    @exhausted = true
  end

  unless results.empty?
    @pages_fetched += 1
    @items_fetched += results.size
    @position = extract_cursor_position(results.last)
  end

  results
end

#reset!self

Reset the cursor to the beginning.

Returns:

  • (self)


140
141
142
143
144
145
146
# File 'lib/parse/query/cursor.rb', line 140

def reset!
  @position = nil
  @pages_fetched = 0
  @items_fetched = 0
  @exhausted = false
  self
end

#serializeString

Serialize the cursor state to a JSON string for persistence. Useful for background jobs that may be interrupted and resumed.

Examples:

Save cursor state for later

cursor = Song.cursor(limit: 100)
cursor.next_page
state = cursor.serialize
# Store state in Redis, database, etc.

Resume in a background job

state = redis.get("cursor:#{job_id}")
cursor = Parse::Cursor.deserialize(state)
cursor.each_page { |page| process(page) }

Returns:

  • (String)

    JSON string containing cursor state



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/parse/query/cursor.rb', line 215

def serialize
  require "json"
  state = {
    class_name: @query.table,
    constraints: @query.constraints(true),
    page_size: @page_size,
    position: @position,
    last_order_value: serialize_value(@last_order_value),
    last_object_id: @last_object_id,
    pages_fetched: @pages_fetched,
    items_fetched: @items_fetched,
    exhausted: @exhausted,
    order_field: @order_field,
    order_direction: @order_direction,
    version: 1,  # For future compatibility
  }
  JSON.generate(state)
end

#statsHash

Get current cursor statistics.

Returns:

  • (Hash)

    statistics about the cursor pagination



188
189
190
191
192
193
194
195
196
197
198
# File 'lib/parse/query/cursor.rb', line 188

def stats
  {
    pages_fetched: @pages_fetched,
    items_fetched: @items_fetched,
    page_size: @page_size,
    exhausted: @exhausted,
    position: @position,
    order_field: @order_field,
    order_direction: @order_direction,
  }
end

#to_jsonString

Alias for serialize

Returns:

  • (String)

    JSON string containing cursor state



236
237
238
# File 'lib/parse/query/cursor.rb', line 236

def to_json
  serialize
end