mongoid-scroll 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.md +9 -3
- data/Gemfile +1 -0
- data/README.md +87 -16
- data/examples/mongoid_scroll_feed.rb +51 -0
- data/examples/moped_scroll_feed.rb +43 -0
- data/lib/mongoid/criterion/scrollable.rb +12 -12
- data/lib/mongoid/scroll/cursor.rb +43 -30
- data/lib/mongoid/scroll/errors/multiple_sort_fields_error.rb +3 -0
- data/lib/mongoid/scroll/version.rb +1 -1
- data/lib/mongoid-scroll.rb +3 -0
- data/lib/moped/scrollable.rb +30 -0
- data/spec/mongoid/criteria_spec.rb +46 -29
- data/spec/mongoid/scroll_cursor_spec.rb +22 -7
- data/spec/moped/query_spec.rb +117 -0
- data/spec/support/feed/item.rb +0 -1
- metadata +7 -3
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,11 @@
|
|
1
|
-
0.
|
2
|
-
|
1
|
+
0.2.0 (3/14/2013)
|
2
|
+
=================
|
3
3
|
|
4
|
-
*
|
4
|
+
* Extended `Moped::Query` with `scroll` - [@dblock](https://github.com/dblock).
|
5
|
+
* `Mongoid::Scroll::Cursor.from_record` can now be called with either a Mongoid field or `field_type` and `field_name` in the `options` hash - [@dblock](https://github.com/dblock).
|
6
|
+
|
7
|
+
0.1.0 (2/14/2013)
|
8
|
+
=================
|
9
|
+
|
10
|
+
* Initial public release, extends `Mongoid::Criteria` with `scroll` - [@dblock](https://github.com/dblock).
|
5
11
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,44 +1,57 @@
|
|
1
1
|
Mongoid::Scroll [](https://travis-ci.org/dblock/mongoid-scroll)
|
2
2
|
===============
|
3
3
|
|
4
|
-
Mongoid extension that
|
4
|
+
Mongoid extension that enables infinite scrolling for `Mongoid::Criteria` and `Moped::Query`.
|
5
|
+
|
6
|
+
Demo
|
7
|
+
----
|
8
|
+
|
9
|
+
Check out [artsy.net](http://artsy.net) homepage. Scroll down.
|
10
|
+
|
11
|
+
There're also two code samples for Mongoid and Moped in [examples](examples). Run `bundle exec ruby examples/mongoid_scroll_feed.rb`.
|
5
12
|
|
6
13
|
The Problem
|
7
14
|
-----------
|
8
15
|
|
9
16
|
Traditional pagination does not work when data changes between paginated requests, which makes it unsuitable for infinite scroll behaviors.
|
10
17
|
|
11
|
-
* If a record is inserted before the current page limit,
|
12
|
-
* If a record is removed before the current page limit,
|
18
|
+
* If a record is inserted before the current page limit, items will shift right, and the next page will include a duplicate.
|
19
|
+
* If a record is removed before the current page limit, items will shift left, and the next page will be missing a record.
|
13
20
|
|
14
21
|
The solution implemented by the `scroll` extension paginates data using a cursor, giving you the ability to restart pagination where you left it off. This is a non-trivial problem when combined with sorting over non-unique record fields, such as timestamps.
|
15
22
|
|
16
|
-
|
17
|
-
|
23
|
+
Installation
|
24
|
+
------------
|
18
25
|
|
19
|
-
Add
|
26
|
+
Add the gem to your Gemfile and run `bundle install`.
|
20
27
|
|
21
28
|
```ruby
|
22
29
|
gem 'mongoid-scroll'
|
23
30
|
```
|
24
31
|
|
32
|
+
Usage
|
33
|
+
-----
|
34
|
+
|
35
|
+
### Mongoid
|
36
|
+
|
25
37
|
A sample model.
|
26
38
|
|
27
39
|
```ruby
|
28
40
|
module Feed
|
29
41
|
class Item
|
30
42
|
include Mongoid::Document
|
31
|
-
field :
|
32
|
-
field :
|
43
|
+
field :title, type: String
|
44
|
+
field :position, type: Integer
|
45
|
+
index({ position: 1, _id: 1 })
|
33
46
|
end
|
34
47
|
end
|
35
48
|
```
|
36
49
|
|
37
|
-
Scroll and save a cursor to the last item.
|
50
|
+
Scroll by `:position` and save a cursor to the last item.
|
38
51
|
|
39
52
|
```ruby
|
40
53
|
saved_cursor = nil
|
41
|
-
Feed::Item.desc(:
|
54
|
+
Feed::Item.desc(:position).limit(5).scroll do |record, next_cursor|
|
42
55
|
# each record, one-by-one
|
43
56
|
saved_cursor = next_cursor
|
44
57
|
end
|
@@ -47,7 +60,7 @@ end
|
|
47
60
|
Resume iterating using the previously saved cursor.
|
48
61
|
|
49
62
|
```ruby
|
50
|
-
Feed::Item.desc(:
|
63
|
+
Feed::Item.desc(:position).limit(5).scroll(saved_cursor) do |record, next_cursor|
|
51
64
|
# each record, one-by-one
|
52
65
|
saved_cursor = next_cursor
|
53
66
|
end
|
@@ -56,23 +69,81 @@ end
|
|
56
69
|
The iteration finishes when no more records are available. You can also finish iterating over the remaining records by omitting the query limit.
|
57
70
|
|
58
71
|
```ruby
|
59
|
-
Feed::Item.desc(:
|
72
|
+
Feed::Item.desc(:position).scroll(saved_cursor) do |record, next_cursor|
|
60
73
|
# each record, one-by-one
|
61
74
|
end
|
62
75
|
```
|
63
76
|
|
77
|
+
### Moped
|
78
|
+
|
79
|
+
Scroll and save a cursor to the last item. You must also supply a `field_type` of the sort criteria.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
saved_cursor = nil
|
83
|
+
session[:feed_items].find.sort(position: -1).limit(5).scroll(nil, { field_type: DateTime }) do |record, next_cursor|
|
84
|
+
# each record, one-by-one
|
85
|
+
saved_cursor = next_cursor
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
Resume iterating using the previously saved cursor.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
session[:feed_items].find.sort(position: -1).limit(5).scroll(saved_cursor, { field_type: DateTime }) do |record, next_cursor|
|
93
|
+
# each record, one-by-one
|
94
|
+
saved_cursor = next_cursor
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
Indexes and Performance
|
99
|
+
-----------------------
|
100
|
+
|
101
|
+
A query without a cursor is identical to a query without a scroll.
|
102
|
+
|
103
|
+
``` ruby
|
104
|
+
# db.feed_items.find().sort({ position: 1 }).limit(5)
|
105
|
+
Feed::Item.desc(:position).limit(5).scroll
|
106
|
+
```
|
107
|
+
|
108
|
+
Subsequent queries use an `$or` to avoid skipping items with the same value as the one at the current cursor position.
|
109
|
+
|
110
|
+
``` ruby
|
111
|
+
# db.feed_items.find({ "$or" : [
|
112
|
+
# { "position" : { "$gt" : 13 }},
|
113
|
+
# { "position" : 13, "_id": { "$gt" : ObjectId("511d7c7c3b5552c92400000e") }}
|
114
|
+
# ]}).sort({ position: 1 })
|
115
|
+
Feed:Item.desc(:position).limit(5).scroll(cursor)
|
116
|
+
```
|
117
|
+
|
118
|
+
This means you need to hit an index on `position` and `_id`.
|
119
|
+
|
120
|
+
``` ruby
|
121
|
+
# db.feed_items.ensureIndex({ position: 1, _id: 1 })
|
122
|
+
|
123
|
+
module Feed
|
124
|
+
class Item
|
125
|
+
...
|
126
|
+
index({ position: 1, _id: 1 })
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
64
131
|
Cursors
|
65
132
|
-------
|
66
133
|
|
67
|
-
You can use `Mongoid::Scroll::Cursor.from_record` to generate a cursor.
|
134
|
+
You can use `Mongoid::Scroll::Cursor.from_record` to generate a cursor. A cursor points at the last record of the previous iteration and unlike MongoDB cursors will not expire.
|
68
135
|
|
69
136
|
```ruby
|
70
|
-
record = Feed::Item.desc(:
|
71
|
-
cursor = Mongoid::Scroll::Cursor.from_record(record, { field: Feed::Item.fields["
|
137
|
+
record = Feed::Item.desc(:position).limit(3).last
|
138
|
+
cursor = Mongoid::Scroll::Cursor.from_record(record, { field: Feed::Item.fields["position"] })
|
72
139
|
# cursor or cursor.to_s can be returned to a client and passed into .scroll(cursor)
|
73
140
|
```
|
74
141
|
|
75
|
-
|
142
|
+
You can also a `field_name` and `field_type` instead of a Mongoid field.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
cursor = Mongoid::Scroll::Cursor.from_record(record, { field_type: DateTime, field_name: "position" })
|
146
|
+
```
|
76
147
|
|
77
148
|
Contributing
|
78
149
|
------------
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.setup(:default, :development)
|
3
|
+
|
4
|
+
require 'mongoid-scroll'
|
5
|
+
require 'faker'
|
6
|
+
|
7
|
+
Mongoid.connect_to "mongoid_scroll_demo"
|
8
|
+
Mongoid.purge!
|
9
|
+
|
10
|
+
module Feed
|
11
|
+
class Item
|
12
|
+
include Mongoid::Document
|
13
|
+
field :title, type: String
|
14
|
+
field :position, type: Integer
|
15
|
+
index({ position: 1, _id: 1 })
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# total items to insert
|
20
|
+
total_items = 20
|
21
|
+
# a MongoDB query will be executed every scroll_by items
|
22
|
+
scroll_by = 7
|
23
|
+
|
24
|
+
# insert items with a position out-of-order
|
25
|
+
rands = (0..total_items).to_a.sort { rand }[0..total_items]
|
26
|
+
total_items.times do |i|
|
27
|
+
Feed::Item.create! title: Faker::Lorem.sentence, position: rands.pop
|
28
|
+
end
|
29
|
+
|
30
|
+
Moped.logger = Logger.new($stdout)
|
31
|
+
Moped.logger.level = Logger::DEBUG
|
32
|
+
|
33
|
+
Feed::Item.create_indexes
|
34
|
+
|
35
|
+
total_shown = 0
|
36
|
+
next_cursor = nil
|
37
|
+
while true
|
38
|
+
current_cursor = next_cursor
|
39
|
+
next_cursor = nil
|
40
|
+
Feed::Item.asc(:position).limit(scroll_by).scroll(current_cursor) do |item, cursor|
|
41
|
+
puts "#{item.position}: #{item.title}"
|
42
|
+
next_cursor = cursor
|
43
|
+
total_shown += 1
|
44
|
+
end
|
45
|
+
break unless next_cursor
|
46
|
+
# destroy an item just for the heck of it, scroll is not affected
|
47
|
+
Feed::Item.asc(:position).first.destroy
|
48
|
+
end
|
49
|
+
|
50
|
+
# this will be 20
|
51
|
+
puts "Shown #{total_shown} items."
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.setup(:default, :development)
|
3
|
+
|
4
|
+
require 'mongoid-scroll'
|
5
|
+
require 'faker'
|
6
|
+
|
7
|
+
Mongoid.connect_to "mongoid_scroll_demo"
|
8
|
+
Mongoid.purge!
|
9
|
+
|
10
|
+
# total items to insert
|
11
|
+
total_items = 20
|
12
|
+
# a MongoDB query will be executed every scroll_by items
|
13
|
+
scroll_by = 7
|
14
|
+
|
15
|
+
# insert items with a position out-of-order
|
16
|
+
rands = (0..total_items).to_a.sort { rand }[0..total_items]
|
17
|
+
total_items.times do |i|
|
18
|
+
Mongoid.default_session['feed_items'].insert(title: Faker::Lorem.sentence, position: rands.pop)
|
19
|
+
end
|
20
|
+
|
21
|
+
Mongoid.default_session['feed_items'].indexes.create(position: 1, _id: 1)
|
22
|
+
|
23
|
+
Moped.logger = Logger.new($stdout)
|
24
|
+
Moped.logger.level = Logger::DEBUG
|
25
|
+
|
26
|
+
total_shown = 0
|
27
|
+
next_cursor = nil
|
28
|
+
while true
|
29
|
+
current_cursor = next_cursor
|
30
|
+
next_cursor = nil
|
31
|
+
Mongoid.default_session['feed_items'].find.limit(scroll_by).sort(position: 1).scroll(current_cursor, { field_type: Integer, field_name: 'position' }) do |item, cursor|
|
32
|
+
puts "#{item['position']}: #{item['title']}"
|
33
|
+
next_cursor = cursor
|
34
|
+
total_shown += 1
|
35
|
+
end
|
36
|
+
break unless next_cursor
|
37
|
+
# destroy an item just for the heck of it, scroll is not affected
|
38
|
+
item = Mongoid.default_session['feed_items'].find.sort(position: 1).first
|
39
|
+
Mongoid.default_session['feed_items'].find(_id: item["_id"]).remove
|
40
|
+
end
|
41
|
+
|
42
|
+
# this will be 20
|
43
|
+
puts "Shown #{total_shown} items."
|
@@ -3,28 +3,28 @@ module Mongoid
|
|
3
3
|
module Scrollable
|
4
4
|
|
5
5
|
def scroll(cursor = nil, &block)
|
6
|
-
|
6
|
+
criteria = self
|
7
7
|
# we don't support scrolling over a criteria with multiple fields
|
8
|
-
if
|
9
|
-
sort
|
10
|
-
|
8
|
+
if criteria.options[:sort] && criteria.options[:sort].keys.size != 1
|
9
|
+
raise Mongoid::Scroll::Errors::MultipleSortFieldsError.new(sort: criteria.options[:sort])
|
10
|
+
elsif ! criteria.options.has_key?(:sort) || criteria.options[:sort].empty?
|
11
|
+
# introduce a default sort order if there's none
|
12
|
+
criteria = criteria.asc(:_id)
|
11
13
|
end
|
12
|
-
# introduce a default sort order if there's none
|
13
|
-
c = c.asc(:_id) if (! c.options[:sort]) || c.options[:sort].empty?
|
14
14
|
# scroll field and direction
|
15
|
-
scroll_field =
|
16
|
-
scroll_direction =
|
15
|
+
scroll_field = criteria.options[:sort].keys.first
|
16
|
+
scroll_direction = criteria.options[:sort].values.first.to_i == 1 ? '$gt' : '$lt'
|
17
17
|
# scroll cursor from the parameter, with value and tiebreak_id
|
18
|
-
field =
|
19
|
-
cursor_options = {
|
18
|
+
field = criteria.klass.fields[scroll_field.to_s]
|
19
|
+
cursor_options = { field_type: field.type, field_name: scroll_field, direction: scroll_direction }
|
20
20
|
cursor = cursor.is_a?(Mongoid::Scroll::Cursor) ? cursor : Mongoid::Scroll::Cursor.new(cursor, cursor_options)
|
21
21
|
# scroll
|
22
22
|
if block_given?
|
23
|
-
|
23
|
+
criteria.where(cursor.criteria).each do |record|
|
24
24
|
yield record, Mongoid::Scroll::Cursor.from_record(record, cursor_options)
|
25
25
|
end
|
26
26
|
else
|
27
|
-
|
27
|
+
criteria
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -2,72 +2,85 @@ module Mongoid
|
|
2
2
|
module Scroll
|
3
3
|
class Cursor
|
4
4
|
|
5
|
-
attr_accessor :value, :tiebreak_id, :
|
5
|
+
attr_accessor :value, :tiebreak_id, :field_type, :field_name, :direction
|
6
6
|
|
7
7
|
def initialize(value = nil, options = {})
|
8
|
-
|
9
|
-
raise ArgumentError.new "Missing options[:field]."
|
10
|
-
end
|
8
|
+
@field_type, @field_name = Mongoid::Scroll::Cursor.extract_field_options(options)
|
11
9
|
@direction = options[:direction] || '$gt'
|
12
|
-
|
10
|
+
parse(value)
|
13
11
|
end
|
14
12
|
|
15
13
|
def criteria
|
16
|
-
|
17
|
-
|
14
|
+
mongo_value = value.class.mongoize(value) if value
|
15
|
+
cursor_criteria = { field_name => { direction => mongo_value } } if mongo_value
|
16
|
+
tiebreak_criteria = { field_name => mongo_value, :_id => { '$gt' => tiebreak_id } } if mongo_value && tiebreak_id
|
18
17
|
(cursor_criteria || tiebreak_criteria) ? { '$or' => [ cursor_criteria, tiebreak_criteria].compact } : {}
|
19
18
|
end
|
20
19
|
|
21
20
|
class << self
|
22
21
|
def from_record(record, options)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
22
|
+
cursor = Mongoid::Scroll::Cursor.new(nil, options)
|
23
|
+
value = record.respond_to?(cursor.field_name) ? record.send(cursor.field_name) : record[cursor.field_name]
|
24
|
+
cursor.value = Mongoid::Scroll::Cursor.parse_field_value(cursor.field_type, cursor.field_name, value)
|
25
|
+
cursor.tiebreak_id = record["_id"]
|
26
|
+
cursor
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
30
|
def to_s
|
31
|
-
tiebreak_id ? [ Mongoid::Scroll::Cursor.transform_field_value(
|
31
|
+
tiebreak_id ? [ Mongoid::Scroll::Cursor.transform_field_value(field_type, field_name, value), tiebreak_id ].join(":") : nil
|
32
32
|
end
|
33
33
|
|
34
34
|
private
|
35
35
|
|
36
|
+
def parse(value)
|
37
|
+
return unless value
|
38
|
+
parts = value.split(":")
|
39
|
+
unless parts.length >= 2
|
40
|
+
raise Mongoid::Scroll::Errors::InvalidCursorError.new({ cursor: value })
|
41
|
+
end
|
42
|
+
id = parts[-1]
|
43
|
+
value = parts[0...-1].join(":")
|
44
|
+
@value = Mongoid::Scroll::Cursor.parse_field_value(field_type, field_name, value)
|
45
|
+
@tiebreak_id = Moped::BSON::ObjectId(id)
|
46
|
+
end
|
47
|
+
|
36
48
|
class << self
|
37
49
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
50
|
+
def extract_field_options(options)
|
51
|
+
if options && (field_name = options[:field_name]) && (field_type = options[:field_type])
|
52
|
+
[ field_type.to_s, field_name.to_s ]
|
53
|
+
elsif options && (field = options[:field])
|
54
|
+
[ field.type.to_s, field.name.to_s ]
|
55
|
+
else
|
56
|
+
raise ArgumentError.new "Missing options[:field_name] and/or options[:field_type]."
|
43
57
|
end
|
44
|
-
id = parts[-1]
|
45
|
-
value = parts[0...-1].join(":")
|
46
|
-
[ parse_field_value(options[:field], value), Moped::BSON::ObjectId(id) ]
|
47
58
|
end
|
48
59
|
|
49
|
-
def parse_field_value(
|
50
|
-
case
|
60
|
+
def parse_field_value(field_type, field_name, value)
|
61
|
+
case field_type.to_s
|
62
|
+
when "Moped::BSON::ObjectId" then value
|
51
63
|
when "String" then value.to_s
|
52
|
-
when "DateTime" then Time.at(value.to_i).to_datetime
|
53
|
-
when "Time" then Time.at(value.to_i)
|
54
|
-
when "Date" then Time.at(value.to_i).utc.to_date
|
64
|
+
when "DateTime" then value.is_a?(DateTime) ? value : Time.at(value.to_i).to_datetime
|
65
|
+
when "Time" then value.is_a?(Time) ? value : Time.at(value.to_i)
|
66
|
+
when "Date" then value.is_a?(Date) ? value : Time.at(value.to_i).utc.to_date
|
55
67
|
when "Float" then value.to_f
|
56
68
|
when "Integer" then value.to_i
|
57
69
|
else
|
58
|
-
raise Mongoid::Scroll::Errors::UnsupportedFieldTypeError.new(field:
|
70
|
+
raise Mongoid::Scroll::Errors::UnsupportedFieldTypeError.new(field: field_name, type: field_type)
|
59
71
|
end
|
60
72
|
end
|
61
73
|
|
62
|
-
def transform_field_value(
|
63
|
-
case
|
74
|
+
def transform_field_value(field_type, field_name, value)
|
75
|
+
case field_type.to_s
|
76
|
+
when "Moped::BSON::ObjectId" then value
|
64
77
|
when "String" then value.to_s
|
65
|
-
when "Date" then value.
|
78
|
+
when "Date" then Time.utc_time(value.year, value.month, value.day).to_i
|
66
79
|
when "DateTime", "Time" then value.to_i
|
67
80
|
when "Float" then value.to_f
|
68
81
|
when "Integer" then value.to_i
|
69
82
|
else
|
70
|
-
raise Mongoid::Scroll::Errors::UnsupportedFieldTypeError.new(field:
|
83
|
+
raise Mongoid::Scroll::Errors::UnsupportedFieldTypeError.new(field: field_name, type: field_type)
|
71
84
|
end
|
72
85
|
end
|
73
86
|
|
@@ -4,6 +4,9 @@ module Mongoid
|
|
4
4
|
class MultipleSortFieldsError < Mongoid::Scroll::Errors::Base
|
5
5
|
|
6
6
|
def initialize(opts = {})
|
7
|
+
if opts[:sort] && opts[:sort].is_a?(Hash)
|
8
|
+
opts = opts.merge(sort: opts[:sort].keys.join(", "))
|
9
|
+
end
|
7
10
|
super(compose_message("multiple_sort_fields", opts))
|
8
11
|
end
|
9
12
|
|
data/lib/mongoid-scroll.rb
CHANGED
@@ -6,6 +6,9 @@ require 'mongoid'
|
|
6
6
|
require 'mongoid/scroll/version'
|
7
7
|
require 'mongoid/scroll/errors'
|
8
8
|
require 'mongoid/scroll/cursor'
|
9
|
+
|
10
|
+
require 'moped/scrollable'
|
9
11
|
require 'mongoid/criterion/scrollable'
|
10
12
|
|
13
|
+
Moped::Query.send(:include, Moped::Scrollable)
|
11
14
|
Mongoid::Criteria.send(:include, Mongoid::Criterion::Scrollable)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Moped
|
2
|
+
module Scrollable
|
3
|
+
|
4
|
+
def scroll(cursor = nil, options = { field_type: Moped::BSON::ObjectId }, &block)
|
5
|
+
# we don't support scrolling over a criteria with multiple fields
|
6
|
+
if operation.selector["$orderby"] && operation.selector["$orderby"].keys.size != 1
|
7
|
+
raise Mongoid::Scroll::Errors::MultipleSortFieldsError.new(sort: operation.selector["$orderby"])
|
8
|
+
elsif ! operation.selector.has_key?("$orderby") || operation.selector["$orderby"].empty?
|
9
|
+
# introduce a default sort order if there's none
|
10
|
+
sort("_id" => 1)
|
11
|
+
end
|
12
|
+
# scroll field and direction
|
13
|
+
scroll_field = operation.selector["$orderby"].keys.first
|
14
|
+
scroll_direction = operation.selector["$orderby"].values.first.to_i == 1 ? '$gt' : '$lt'
|
15
|
+
# scroll cursor from the parameter, with value and tiebreak_id
|
16
|
+
cursor_options = { field_name: scroll_field, field_type: options[:field_type], direction: scroll_direction }
|
17
|
+
cursor = cursor.is_a?(Mongoid::Scroll::Cursor) ? cursor : Mongoid::Scroll::Cursor.new(cursor, cursor_options)
|
18
|
+
operation.selector["$query"].merge!(cursor.criteria)
|
19
|
+
# scroll
|
20
|
+
if block_given?
|
21
|
+
each do |record|
|
22
|
+
yield record, Mongoid::Scroll::Cursor.from_record(record, cursor_options)
|
23
|
+
end
|
24
|
+
else
|
25
|
+
self
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -32,47 +32,64 @@ describe Mongoid::Criteria do
|
|
32
32
|
Feed::Item.create!(
|
33
33
|
a_string: i.to_s,
|
34
34
|
a_integer: i,
|
35
|
-
a_datetime: DateTime.new(2013, i + 1, 21, 1, 42, 3)
|
35
|
+
a_datetime: DateTime.new(2013, i + 1, 21, 1, 42, 3),
|
36
|
+
a_date: Date.new(2013, i + 1, 21),
|
37
|
+
a_time: Time.at(Time.now.to_i + i)
|
36
38
|
)
|
37
39
|
end
|
38
40
|
end
|
39
|
-
context "
|
40
|
-
it "scrolls all
|
41
|
+
context "default" do
|
42
|
+
it "scrolls all" do
|
41
43
|
records = []
|
42
|
-
Feed::Item.
|
44
|
+
Feed::Item.all.scroll do |record, next_cursor|
|
43
45
|
records << record
|
44
46
|
end
|
45
47
|
records.size.should == 10
|
46
48
|
records.should eq Feed::Item.all.to_a
|
47
49
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
records
|
53
|
-
|
50
|
+
end
|
51
|
+
{ a_string: String, a_integer: Integer, a_date: Date, a_datetime: DateTime }.each_pair do |field_name, field_type|
|
52
|
+
context field_type do
|
53
|
+
it "scrolls all with a block" do
|
54
|
+
records = []
|
55
|
+
Feed::Item.asc(field_name).scroll do |record, next_cursor|
|
56
|
+
records << record
|
57
|
+
end
|
58
|
+
records.size.should == 10
|
59
|
+
records.should eq Feed::Item.all.to_a
|
54
60
|
end
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
61
|
+
it "scrolls all with a break" do
|
62
|
+
records = []
|
63
|
+
cursor = nil
|
64
|
+
Feed::Item.asc(field_name).limit(5).scroll do |record, next_cursor|
|
65
|
+
records << record
|
66
|
+
cursor = next_cursor
|
67
|
+
end
|
68
|
+
records.size.should == 5
|
69
|
+
Feed::Item.asc(field_name).scroll(cursor) do |record, next_cursor|
|
70
|
+
records << record
|
71
|
+
cursor = next_cursor
|
72
|
+
end
|
73
|
+
records.size.should == 10
|
74
|
+
records.should eq Feed::Item.all.to_a
|
59
75
|
end
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
records
|
76
|
+
it "scrolls in descending order" do
|
77
|
+
records = []
|
78
|
+
Feed::Item.desc(field_name).limit(3).scroll do |record, next_cursor|
|
79
|
+
records << record
|
80
|
+
end
|
81
|
+
records.size.should == 3
|
82
|
+
records.should eq Feed::Item.desc(field_name).limit(3).to_a
|
83
|
+
end
|
84
|
+
it "map" do
|
85
|
+
record = Feed::Item.desc(field_name).limit(3).scroll.map { |record, cursor| record }.last
|
86
|
+
cursor = Mongoid::Scroll::Cursor.from_record(record, { field_type: field_type, field_name: field_name })
|
87
|
+
cursor.should_not be_nil
|
88
|
+
cursor.to_s.split(":").should == [
|
89
|
+
Mongoid::Scroll::Cursor.transform_field_value(field_type, field_name, record.send(field_name)).to_s,
|
90
|
+
record.id.to_s
|
91
|
+
]
|
67
92
|
end
|
68
|
-
records.size.should == 3
|
69
|
-
records.should eq Feed::Item.desc(:a_integer).limit(3).to_a
|
70
|
-
end
|
71
|
-
it "map" do
|
72
|
-
record = Feed::Item.desc(:a_integer).limit(3).scroll.map { |record, cursor| record }.last
|
73
|
-
cursor = Mongoid::Scroll::Cursor.from_record(record, { field: Feed::Item.fields["a_integer"] })
|
74
|
-
cursor.should_not be_nil
|
75
|
-
cursor.to_s.split(":").should == [ record.a_integer.to_s, record.id.to_s ]
|
76
93
|
end
|
77
94
|
end
|
78
95
|
end
|
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
describe Mongoid::Scroll::Cursor do
|
4
4
|
context "an empty cursor" do
|
5
5
|
subject do
|
6
|
-
Mongoid::Scroll::Cursor.new
|
6
|
+
Mongoid::Scroll::Cursor.new nil, field_name: "a_string", field_type: String
|
7
7
|
end
|
8
8
|
its(:tiebreak_id) { should be_nil }
|
9
9
|
its(:value) { should be_nil }
|
@@ -11,14 +11,14 @@ describe Mongoid::Scroll::Cursor do
|
|
11
11
|
end
|
12
12
|
context "an invalid cursor" do
|
13
13
|
it "raises InvalidCursorError" do
|
14
|
-
expect { Mongoid::Scroll::Cursor.new "invalid",
|
14
|
+
expect { Mongoid::Scroll::Cursor.new "invalid", field_name: "a_string", field_type: String }.to raise_error Mongoid::Scroll::Errors::InvalidCursorError,
|
15
15
|
/The cursor supplied is invalid: invalid./
|
16
16
|
end
|
17
17
|
end
|
18
18
|
context "a string field cursor" do
|
19
19
|
let(:feed_item) { Feed::Item.create!(a_string: "astring") }
|
20
20
|
subject do
|
21
|
-
Mongoid::Scroll::Cursor.new "#{feed_item.a_string}:#{feed_item.id}",
|
21
|
+
Mongoid::Scroll::Cursor.new "#{feed_item.a_string}:#{feed_item.id}", field_name: "a_string", field_type: String
|
22
22
|
end
|
23
23
|
its(:value) { should eq feed_item.a_string }
|
24
24
|
its(:tiebreak_id) { should eq feed_item.id }
|
@@ -32,7 +32,7 @@ describe Mongoid::Scroll::Cursor do
|
|
32
32
|
context "an integer field cursor" do
|
33
33
|
let(:feed_item) { Feed::Item.create!(a_integer: 10) }
|
34
34
|
subject do
|
35
|
-
Mongoid::Scroll::Cursor.new "#{feed_item.a_integer}:#{feed_item.id}",
|
35
|
+
Mongoid::Scroll::Cursor.new "#{feed_item.a_integer}:#{feed_item.id}", field_name: "a_integer", field_type: Integer
|
36
36
|
end
|
37
37
|
its(:value) { should eq feed_item.a_integer }
|
38
38
|
its(:tiebreak_id) { should eq feed_item.id }
|
@@ -46,7 +46,7 @@ describe Mongoid::Scroll::Cursor do
|
|
46
46
|
context "a date/time field cursor" do
|
47
47
|
let(:feed_item) { Feed::Item.create!(a_datetime: DateTime.new(2013, 12, 21, 1, 42, 3)) }
|
48
48
|
subject do
|
49
|
-
Mongoid::Scroll::Cursor.new "#{feed_item.a_datetime.to_i}:#{feed_item.id}",
|
49
|
+
Mongoid::Scroll::Cursor.new "#{feed_item.a_datetime.to_i}:#{feed_item.id}", field_name: "a_datetime", field_type: DateTime
|
50
50
|
end
|
51
51
|
its(:value) { should eq feed_item.a_datetime }
|
52
52
|
its(:tiebreak_id) { should eq feed_item.id }
|
@@ -61,7 +61,7 @@ describe Mongoid::Scroll::Cursor do
|
|
61
61
|
context "a date field cursor" do
|
62
62
|
let(:feed_item) { Feed::Item.create!(a_date: Date.new(2013, 12, 21)) }
|
63
63
|
subject do
|
64
|
-
Mongoid::Scroll::Cursor.new "#{feed_item.a_date.to_datetime.to_i}:#{feed_item.id}",
|
64
|
+
Mongoid::Scroll::Cursor.new "#{feed_item.a_date.to_datetime.to_i}:#{feed_item.id}", field_name: "a_date", field_type: Date
|
65
65
|
end
|
66
66
|
its(:value) { should eq feed_item.a_date }
|
67
67
|
its(:tiebreak_id) { should eq feed_item.id }
|
@@ -74,6 +74,21 @@ describe Mongoid::Scroll::Cursor do
|
|
74
74
|
}
|
75
75
|
end
|
76
76
|
context "a time field cursor" do
|
77
|
+
let(:feed_item) { Feed::Item.create!(a_time: Time.new(2013, 12, 21, 1, 2, 3)) }
|
78
|
+
subject do
|
79
|
+
Mongoid::Scroll::Cursor.new "#{feed_item.a_time.to_i}:#{feed_item.id}", field_name: "a_time", field_type: Time
|
80
|
+
end
|
81
|
+
its(:value) { should eq feed_item.a_time }
|
82
|
+
its(:tiebreak_id) { should eq feed_item.id }
|
83
|
+
its(:to_s) { should eq "#{feed_item.a_time.to_i}:#{feed_item.id}" }
|
84
|
+
its(:criteria) {
|
85
|
+
should eq({ "$or" => [
|
86
|
+
{ "a_time" => { "$gt" => feed_item.a_time }},
|
87
|
+
{ "a_time" => feed_item.a_time, :_id => { "$gt" => feed_item.id }}
|
88
|
+
]})
|
89
|
+
}
|
90
|
+
end
|
91
|
+
context "a time field cursor with a field option" do
|
77
92
|
let(:feed_item) { Feed::Item.create!(a_time: Time.new(2013, 12, 21, 1, 2, 3)) }
|
78
93
|
subject do
|
79
94
|
Mongoid::Scroll::Cursor.new "#{feed_item.a_time.to_i}:#{feed_item.id}", field: Feed::Item.fields["a_time"]
|
@@ -92,7 +107,7 @@ describe Mongoid::Scroll::Cursor do
|
|
92
107
|
let(:feed_item) { Feed::Item.create!(a_array: [ "x", "y" ]) }
|
93
108
|
it "is not supported" do
|
94
109
|
expect {
|
95
|
-
Mongoid::Scroll::Cursor.from_record
|
110
|
+
Mongoid::Scroll::Cursor.from_record feed_item, field_name: "a_array", field_type: Array
|
96
111
|
}.to raise_error Mongoid::Scroll::Errors::UnsupportedFieldTypeError, /The type of the field 'a_array' is not supported: Array./
|
97
112
|
end
|
98
113
|
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Moped::Query do
|
4
|
+
context "scrollable" do
|
5
|
+
subject do
|
6
|
+
Mongoid.default_session['feed_items'].find
|
7
|
+
end
|
8
|
+
it ":scroll" do
|
9
|
+
subject.should.respond_to? :scroll
|
10
|
+
end
|
11
|
+
end
|
12
|
+
context "with multiple sort fields" do
|
13
|
+
subject do
|
14
|
+
Mongoid.default_session['feed_items'].find.sort(name: 1, value: -1)
|
15
|
+
end
|
16
|
+
it "raises Mongoid::Scroll::Errors::MultipleSortFieldsError" do
|
17
|
+
expect { subject.scroll }.to raise_error Mongoid::Scroll::Errors::MultipleSortFieldsError,
|
18
|
+
/You're attempting to scroll over data with a sort order that includes multiple fields: name, value./
|
19
|
+
end
|
20
|
+
end
|
21
|
+
context "with no sort" do
|
22
|
+
subject do
|
23
|
+
Mongoid.default_session['feed_items'].find
|
24
|
+
end
|
25
|
+
it "adds a default sort by _id" do
|
26
|
+
subject.scroll.operation.selector["$orderby"].should == { "_id" => 1 }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
context "with data" do
|
30
|
+
before :each do
|
31
|
+
10.times do |i|
|
32
|
+
Mongoid.default_session['feed_items'].insert(
|
33
|
+
a_string: i.to_s,
|
34
|
+
a_integer: i,
|
35
|
+
a_datetime: DateTime.mongoize(DateTime.new(2013, i + 1, 21, 1, 42, 3)),
|
36
|
+
a_date: Date.mongoize(Date.new(2013, i + 1, 21)),
|
37
|
+
a_time: Time.mongoize(Time.at(Time.now.to_i + i))
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
context "default" do
|
42
|
+
it "scrolls all" do
|
43
|
+
records = []
|
44
|
+
Mongoid.default_session['feed_items'].find.scroll do |record, next_cursor|
|
45
|
+
records << record
|
46
|
+
end
|
47
|
+
records.size.should == 10
|
48
|
+
records.should eq Mongoid.default_session['feed_items'].find.to_a
|
49
|
+
end
|
50
|
+
end
|
51
|
+
{ a_string: String, a_integer: Integer, a_date: Date, a_datetime: DateTime }.each_pair do |field_name, field_type|
|
52
|
+
context field_type do
|
53
|
+
it "scrolls all with a block" do
|
54
|
+
records = []
|
55
|
+
Mongoid.default_session['feed_items'].find.sort(field_name => 1).scroll(nil, { field_type: field_type }) do |record, next_cursor|
|
56
|
+
records << record
|
57
|
+
end
|
58
|
+
records.size.should == 10
|
59
|
+
records.should eq Mongoid.default_session['feed_items'].find.to_a
|
60
|
+
end
|
61
|
+
it "scrolls all with a break" do
|
62
|
+
records = []
|
63
|
+
cursor = nil
|
64
|
+
Mongoid.default_session['feed_items'].find.sort(field_name => 1).limit(5).scroll(nil, { field_type: field_type }) do |record, next_cursor|
|
65
|
+
records << record
|
66
|
+
cursor = next_cursor
|
67
|
+
end
|
68
|
+
records.size.should == 5
|
69
|
+
Mongoid.default_session['feed_items'].find.sort(field_name => 1).scroll(cursor, { field_type: field_type }) do |record, next_cursor|
|
70
|
+
records << record
|
71
|
+
cursor = next_cursor
|
72
|
+
end
|
73
|
+
records.size.should == 10
|
74
|
+
records.should eq Mongoid.default_session['feed_items'].find.to_a
|
75
|
+
end
|
76
|
+
it "scrolls in descending order" do
|
77
|
+
records = []
|
78
|
+
Mongoid.default_session['feed_items'].find.sort(field_name => -1).limit(3).scroll(nil, { field_type: field_type, field_name: field_name }) do |record, next_cursor|
|
79
|
+
records << record
|
80
|
+
end
|
81
|
+
records.size.should == 3
|
82
|
+
records.should eq Mongoid.default_session['feed_items'].find.sort(field_name => -1).limit(3).to_a
|
83
|
+
end
|
84
|
+
it "map" do
|
85
|
+
record = Mongoid.default_session['feed_items'].find.limit(3).scroll(nil, { field_type: field_type, field_name: field_name }).map {
|
86
|
+
|record, cursor| record
|
87
|
+
}.last
|
88
|
+
cursor = Mongoid::Scroll::Cursor.from_record(record, { field_type: field_type, field_name: field_name })
|
89
|
+
cursor.should_not be_nil
|
90
|
+
cursor.to_s.split(":").should == [
|
91
|
+
Mongoid::Scroll::Cursor.transform_field_value(field_type, field_name, record[field_name.to_s]).to_s,
|
92
|
+
record["_id"].to_s
|
93
|
+
]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
context "with overlapping data" do
|
99
|
+
before :each do
|
100
|
+
3.times { Feed::Item.create! a_integer: 5 }
|
101
|
+
end
|
102
|
+
it "scrolls" do
|
103
|
+
records = []
|
104
|
+
cursor = nil
|
105
|
+
Mongoid.default_session['feed_items'].find.sort(a_integer: -1).limit(2).scroll do |record, next_cursor|
|
106
|
+
records << record
|
107
|
+
cursor = next_cursor
|
108
|
+
end
|
109
|
+
records.size.should == 2
|
110
|
+
Mongoid.default_session['feed_items'].find.sort(a_integer: -1).scroll(cursor) do |record, next_cursor|
|
111
|
+
records << record
|
112
|
+
end
|
113
|
+
records.size.should == 3
|
114
|
+
records.should eq Mongoid.default_session['feed_items'].find.to_a
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/spec/support/feed/item.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mongoid-scroll
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2013-02-
|
13
|
+
date: 2013-02-15 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: mongoid
|
@@ -58,6 +58,8 @@ files:
|
|
58
58
|
- LICENSE.md
|
59
59
|
- README.md
|
60
60
|
- Rakefile
|
61
|
+
- examples/mongoid_scroll_feed.rb
|
62
|
+
- examples/moped_scroll_feed.rb
|
61
63
|
- lib/config/locales/en.yml
|
62
64
|
- lib/mongoid-scroll.rb
|
63
65
|
- lib/mongoid/criterion/scrollable.rb
|
@@ -70,10 +72,12 @@ files:
|
|
70
72
|
- lib/mongoid/scroll/errors/unsupported_field_type_error.rb
|
71
73
|
- lib/mongoid/scroll/version.rb
|
72
74
|
- lib/mongoid_scroll.rb
|
75
|
+
- lib/moped/scrollable.rb
|
73
76
|
- mongoid-scroll.gemspec
|
74
77
|
- spec/mongoid/criteria_spec.rb
|
75
78
|
- spec/mongoid/scroll_cursor_spec.rb
|
76
79
|
- spec/mongoid/scroll_spec.rb
|
80
|
+
- spec/moped/query_spec.rb
|
77
81
|
- spec/spec_helper.rb
|
78
82
|
- spec/support/feed/item.rb
|
79
83
|
homepage: http://github.com/dblock/mongoid-scroll
|
@@ -91,7 +95,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
95
|
version: '0'
|
92
96
|
segments:
|
93
97
|
- 0
|
94
|
-
hash:
|
98
|
+
hash: 4314128693864897181
|
95
99
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
100
|
none: false
|
97
101
|
requirements:
|