Skip to content

Commit 56f04f5

Browse files
GCorbeldblock
andauthored
Gives the previous cursor in the scroll block (#38)
* Gives the previous cursor in the scroll block * apply changes for mongo * encrypt with the previous option * take values before the first page * keep the ordering when fetching previous records * keep the ordering when fetching previous records * add entry to the change log * Fix with Ruby 2.6 * Update the README and CHANGELOG * minor changes * change to use type * change to use an iterator object * refactor * Fix typos Co-authored-by: Daniel (dB.) Doubrovkine <[email protected]> * throw an error when type is unsupported --------- Co-authored-by: Daniel (dB.) Doubrovkine <[email protected]>
1 parent ddedd8c commit 56f04f5

File tree

17 files changed

+270
-89
lines changed

17 files changed

+270
-89
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
### 1.0.2 (Next)
1+
### 2.0.0 (Next)
22

3+
* [#38](https://github.com/mongoid/mongoid-scroll/pull/38): Allow to reverse the scroll - [@GCorbel](https://github.com/GCorbel).
34
* Your contribution here.
45

56
### 1.0.1 (2023/03/15)

README.md

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -69,27 +69,38 @@ end
6969
Scroll by `:position` and save a cursor to the last item.
7070

7171
```ruby
72-
saved_cursor = nil
73-
Feed::Item.desc(:position).limit(5).scroll do |record, next_cursor|
72+
saved_iterator = nil
73+
74+
Feed::Item.desc(:position).limit(5).scroll do |record, iterator|
7475
# each record, one-by-one
75-
saved_cursor = next_cursor
76+
saved_iterator = iterator
7677
end
7778
```
7879

79-
Resume iterating using the previously saved cursor.
80+
Resume iterating using saved cursor and save the cursor to go backward.
81+
82+
```ruby
83+
Feed::Item.desc(:position).limit(5).scroll(saved_iterator.next_cursor) do |record, iterator|
84+
# each record, one-by-one
85+
saved_iterator = iterator
86+
end
87+
```
88+
89+
Loop over the first records again.
8090

8191
```ruby
82-
Feed::Item.desc(:position).limit(5).scroll(saved_cursor) do |record, next_cursor|
92+
Feed::Item.desc(:position).limit(5).scroll(saved_iterator.previous_cursor) do |record, iterator|
8393
# each record, one-by-one
84-
saved_cursor = next_cursor
94+
saved_iterator = iterator
8595
end
8696
```
8797

8898
The iteration finishes when no more records are available. You can also finish iterating over the remaining records by omitting the query limit.
8999

90100
```ruby
91-
Feed::Item.desc(:position).scroll(saved_cursor) do |record, next_cursor|
101+
Feed::Item.desc(:position).limit(5).scroll(saved_iterator.next_cursor) do |record, iterator|
92102
# each record, one-by-one
103+
saved_iterator = iterator
93104
end
94105
```
95106

@@ -98,19 +109,19 @@ end
98109
Scroll a `Mongo::Collection::View` and save a cursor to the last item. You must also supply a `field_type` of the sort criteria.
99110

100111
```ruby
101-
saved_cursor = nil
102-
client[:feed_items].find.sort(position: -1).limit(5).scroll(nil, { field_type: DateTime }) do |record, next_cursor|
112+
saved_iterator = nil
113+
client[:feed_items].find.sort(position: -1).limit(5).scroll(nil, { field_type: DateTime }) do |record, iterator|
103114
# each record, one-by-one
104-
saved_cursor = next_cursor
115+
saved_iterator = iterator
105116
end
106117
```
107118

108119
Resume iterating using the previously saved cursor.
109120

110121
```ruby
111-
session[:feed_items].find.sort(position: -1).limit(5).scroll(saved_cursor, { field_type: DateTime }) do |record, next_cursor|
122+
session[:feed_items].find.sort(position: -1).limit(5).scroll(saved_iterator.next_cursor, { field_type: DateTime }) do |record, iterator|
112123
# each record, one-by-one
113-
saved_cursor = next_cursor
124+
saved_iterator = iterator
114125
end
115126
```
116127

@@ -179,15 +190,15 @@ Feed::Item.desc(:created_at).scroll(cursor) # Raises a Mongoid::Scroll::Errors::
179190

180191
### Standard Cursor
181192

182-
The `Mongoid::Scroll::Cursor` encodes a value and a tiebreak ID separated by `:`, and does not include other options, such as scroll direction. Take extra care not to pass a cursor into a scroll with different options.
193+
The `Mongoid::Scroll::Cursor` encodes a value and a tiebreak ID separated by `:`, and does not include other options, such as scroll direction. Take extra care not to pass a cursor into a scroll with different options.
183194

184195
### Base64 Encoded Cursor
185196

186197
The `Mongoid::Scroll::Base64EncodedCursor` can be used instead of `Mongoid::Scroll::Cursor` to generate a base64-encoded string (using RFC 4648) containing all the information needed to rebuild a cursor.
187198

188199
```ruby
189-
Feed::Item.desc(:position).limit(5).scroll(Mongoid::Scroll::Base64EncodedCursor) do |record, next_cursor|
190-
# next_cursor is of type Mongoid::Scroll::Base64EncodedCursor
200+
Feed::Item.desc(:position).limit(5).scroll(Mongoid::Scroll::Base64EncodedCursor) do |record, iterator|
201+
# iterator.next_cursor is of type Mongoid::Scroll::Base64EncodedCursor
191202
end
192203
```
193204

UPGRADING.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Upgrading
22

3+
## Upgrading to >= 2.0.0
4+
5+
The second argument yielded in the block in `Mongoid::Criteria::Scrollable#scroll` and `Mongo::Scrollable#scroll` has changed from a cursor to an instance of `Mongoid::Criteria::Scrollable` which provides `next_cursor` and `previous_cursor`. The `next_cursor` method returns the same cursor as in versions prior to 2.0.0.
6+
7+
For example, this code:
8+
9+
```ruby
10+
Feed::Item.asc(field_name).limit(2).scroll(cursor) do |_, next_cursor|
11+
cursor = next_cursor
12+
end
13+
```
14+
15+
Should be updated to:
16+
17+
```
18+
Feed::Item.asc(field_name).limit(2).scroll(cursor) do |_, iterator|
19+
cursor = iterator.next_cursor
20+
end
21+
```
22+
323
## Upgrading to >= 1.0.0
424

525
### Mismatched Sort Fields
@@ -9,6 +29,6 @@ Both `Mongoid::Criteria::Scrollable#scroll` and `Mongo::Scrollable` now raise a
929
For example, the following code will now raise a `MismatchedSortFieldsError` because we set a different field name (`position`) from the `created_at` field used to sort in `scroll`.
1030

1131
```ruby
12-
cursor.field_name = "position"
32+
cursor.field_name = "position"
1333
Feed::Item.desc(:created_at).scroll(cursor)
1434
```

lib/config/locales/en.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,7 @@ en:
2727
message: "Unsupported field type."
2828
summary: "The type of the field '%{field}' is not supported: %{type}."
2929
resolution: "Please open a feature request in https://github.com/mongoid/mongoid-scroll."
30-
30+
unsupported_type:
31+
message: "Unsupported type."
32+
summary: "The type supplied in the cursor is not supported: %{type}."
33+
resolution: "The cursor type can be either ':previous' or ':next'."

lib/mongo/scrollable.rb

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,41 @@ def scroll(cursor_or_type = nil, options = nil, &_block)
1616
cursor_options = { field_name: scroll_field, direction: scroll_direction }.merge(options)
1717
cursor = cursor && cursor.is_a?(cursor_type) ? cursor : cursor_type.new(cursor, cursor_options)
1818
raise_mismatched_sort_fields_error!(cursor, cursor_options) if different_sort_fields?(cursor, cursor_options)
19-
# make a view
20-
view = Mongo::Collection::View.new(
21-
view.collection,
22-
view.selector.merge(cursor.criteria),
23-
sort: (view.sort || {}).merge(_id: scroll_direction),
24-
skip: skip,
25-
limit: limit
26-
)
19+
20+
records = nil
21+
if cursor.type == :previous
22+
# scroll backwards by reversing the sort order, limit and then reverse again
23+
pipeline = [
24+
{ '$match' => view.selector.merge(cursor.criteria) },
25+
{ '$sort' => { scroll_field => -scroll_direction } },
26+
{ '$limit' => limit },
27+
{ '$sort' => { scroll_field => scroll_direction } }
28+
]
29+
aggregation_options = view.options.except(:sort)
30+
records = view.aggregate(pipeline, aggregation_options)
31+
else
32+
# make a view
33+
records = Mongo::Collection::View.new(
34+
view.collection,
35+
view.selector.merge(cursor.criteria),
36+
sort: (view.sort || {}).merge(_id: scroll_direction),
37+
skip: skip,
38+
limit: limit
39+
)
40+
end
2741
# scroll
2842
if block_given?
29-
view.each do |record|
30-
yield record, cursor_type.from_record(record, cursor_options)
43+
previous_cursor = nil
44+
records.each do |record|
45+
previous_cursor ||= cursor_type.from_record(record, cursor_options.merge(type: :previous))
46+
iterator = Mongoid::Criteria::Scrollable::Iterator.new(
47+
previous_cursor: previous_cursor,
48+
next_cursor: cursor_type.from_record(record, cursor_options)
49+
)
50+
yield record, iterator
3151
end
3252
else
33-
view
53+
records
3454
end
3555
end
3656
end

lib/mongoid-scroll.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
require 'mongoid/scroll/base64_encoded_cursor'
1212
require 'mongoid/criteria/scrollable/fields'
1313
require 'mongoid/criteria/scrollable/cursors'
14+
require 'mongoid/criteria/scrollable/iterator'
1415
require 'mongo/scrollable' if Object.const_defined?(:Mongo)
1516
require 'mongoid/criteria/scrollable'

lib/mongoid/criteria/scrollable.rb

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@ def scroll(cursor_or_type = nil, &_block)
1212
cursor_options = build_cursor_options(criteria)
1313
cursor = cursor.is_a?(cursor_type) ? cursor : new_cursor(cursor_type, cursor, cursor_options)
1414
raise_mismatched_sort_fields_error!(cursor, cursor_options) if different_sort_fields?(cursor, cursor_options)
15-
cursor_criteria = build_cursor_criteria(criteria, cursor)
15+
records = find_records(criteria, cursor)
1616
if block_given?
17-
cursor_criteria.order_by(_id: scroll_direction(criteria)).each do |record|
18-
yield record, cursor_from_record(cursor_type, record, cursor_options)
17+
previous_cursor = nil
18+
records.each do |record|
19+
previous_cursor ||= cursor_from_record(cursor_type, record, cursor_options.merge(type: :previous))
20+
iterator = Mongoid::Criteria::Scrollable::Iterator.new(
21+
previous_cursor: previous_cursor,
22+
next_cursor: cursor_from_record(cursor_type, record, cursor_options)
23+
)
24+
yield record, iterator
1925
end
2026
else
21-
cursor_criteria
27+
records
2228
end
2329
end
2430

@@ -60,10 +66,21 @@ def new_cursor(cursor_type, cursor, cursor_options)
6066
cursor_type.new(cursor, cursor_options)
6167
end
6268

63-
def build_cursor_criteria(criteria, cursor)
69+
def find_records(criteria, cursor)
6470
cursor_criteria = criteria.dup
6571
cursor_criteria.selector = { '$and' => [criteria.selector, cursor.criteria] }
66-
cursor_criteria
72+
if cursor.type == :previous
73+
pipeline = [
74+
{ '$match' => cursor_criteria.selector },
75+
{ '$sort' => { cursor.field_name => -cursor.direction } },
76+
{ '$limit' => criteria.options[:limit] },
77+
{ '$sort' => { cursor.field_name => cursor.direction } }
78+
]
79+
aggregation = cursor_criteria.view.aggregate(pipeline)
80+
aggregation.map { |record| Mongoid::Factory.from_db(cursor_criteria.klass, record) }
81+
else
82+
cursor_criteria.order_by(_id: scroll_direction(criteria))
83+
end
6784
end
6885

6986
def cursor_from_record(cursor_type, record, cursor_options)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module Mongoid
2+
class Criteria
3+
module Scrollable
4+
class Iterator
5+
attr_accessor :previous_cursor, :next_cursor
6+
7+
def initialize(previous_cursor:, next_cursor:)
8+
@previous_cursor = previous_cursor
9+
@next_cursor = next_cursor
10+
end
11+
end
12+
end
13+
end
14+
end

lib/mongoid/scroll/base64_encoded_cursor.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def initialize(value, options = {})
1818
field_name: parsed['field_name'],
1919
direction: parsed['direction'],
2020
include_current: parsed['include_current'],
21-
tiebreak_id: parsed['tiebreak_id'] && !parsed['tiebreak_id'].empty? ? BSON::ObjectId.from_string(parsed['tiebreak_id']) : nil
21+
tiebreak_id: parsed['tiebreak_id'] && !parsed['tiebreak_id'].empty? ? BSON::ObjectId.from_string(parsed['tiebreak_id']) : nil,
22+
type: parsed['type'].try(:to_sym)
2223
}
2324
else
2425
super nil, options
@@ -32,7 +33,8 @@ def to_s
3233
field_name: field_name,
3334
direction: direction,
3435
include_current: include_current,
35-
tiebreak_id: tiebreak_id && tiebreak_id.to_s
36+
tiebreak_id: tiebreak_id && tiebreak_id.to_s,
37+
type: type
3638
}.to_json)
3739
end
3840
end

lib/mongoid/scroll/base_cursor.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module Mongoid
22
module Scroll
33
class BaseCursor
4-
attr_accessor :value, :tiebreak_id, :field_type, :field_name, :direction, :include_current
4+
attr_accessor :value, :tiebreak_id, :field_type, :field_name, :direction, :include_current, :type
55

66
def initialize(value, options = {})
77
@value = value
@@ -10,6 +10,9 @@ def initialize(value, options = {})
1010
@field_name = options[:field_name]
1111
@direction = options[:direction] || 1
1212
@include_current = options[:include_current] || false
13+
@type = options[:type] || :next
14+
15+
raise Mongoid::Scroll::Errors::UnsupportedTypeError.new(type: @type) if ![:previous, :next].include?(@type)
1316
end
1417

1518
def criteria
@@ -86,20 +89,23 @@ def extract_field_options(options)
8689
field_type: field_type.to_s,
8790
field_name: field_name.to_s,
8891
direction: options[:direction] || 1,
89-
include_current: options[:include_current] || false
92+
include_current: options[:include_current] || false,
93+
type: options[:type].try(:to_sym) || :next
9094
}
9195
elsif options && (field = options[:field])
9296
{
9397
field_type: field.type.to_s,
9498
field_name: field.name.to_s,
9599
direction: options[:direction] || 1,
96-
include_current: options[:include_current] || false
100+
include_current: options[:include_current] || false,
101+
type: options[:type].try(:to_sym) || :next
97102
}
98103
end
99104
end
100105

101106
def compare_direction
102-
direction == 1 ? '$gt' : '$lt'
107+
dir = type == :previous ? -direction : direction
108+
dir == 1 ? '$gt' : '$lt'
103109
end
104110

105111
def tiebreak_compare_direction

0 commit comments

Comments
 (0)