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 CHANGED
@@ -1,5 +1,11 @@
1
- 0.1.0 (TBD)
2
- ===========
1
+ 0.2.0 (3/14/2013)
2
+ =================
3
3
 
4
- * Initial public release - [@dblock](https://github.com/dblock).
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
@@ -6,4 +6,5 @@ group :development, :test do
6
6
  gem "rake"
7
7
  gem "bundler"
8
8
  gem "rspec"
9
+ gem "faker"
9
10
  end
data/README.md CHANGED
@@ -1,44 +1,57 @@
1
1
  Mongoid::Scroll [![Build Status](https://travis-ci.org/dblock/mongoid-scroll.png?branch=master)](https://travis-ci.org/dblock/mongoid-scroll)
2
2
  ===============
3
3
 
4
- Mongoid extension that enable infinite scrolling.
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, the collection will shift to the right, and the returned result will include a duplicate from a previous page.
12
- * If a record is removed before the current page limit, the collection will shift to the left, and the returned result will be missing a record.
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
- Usage
17
- -----
23
+ Installation
24
+ ------------
18
25
 
19
- Add `mongoid-scroll` to Gemfile.
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 :content, type: String
32
- field :created_at, type: DateTime
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(:created_at).limit(5).scroll do |record, next_cursor|
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(:created_at).limit(5).scroll(saved_cursor) do |record, next_cursor|
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(:created_at).scroll(saved_cursor) do |record, next_cursor|
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. This can be useful when you just want to return a collection of results and the cursor pointing to after the last item.
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(:created_at).limit(3).last
71
- cursor = Mongoid::Scroll::Cursor.from_record(record, { field: Feed::Item.fields["created_at"] })
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
- Note that unlike MongoDB cursors, `Mongoid::Scroll::Cursor` values don't expire.
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
- c = self
6
+ criteria = self
7
7
  # we don't support scrolling over a criteria with multiple fields
8
- if c.options[:sort] && c.options[:sort].keys.count != 1
9
- sort = c.options[:sort].keys.join(", ")
10
- raise Mongoid::Scroll::Errors::MultipleSortFieldsError.new(sort: sort)
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 = c.options[:sort].keys.first
16
- scroll_direction = c.options[:sort].values.first.to_i == 1 ? '$gt' : '$lt'
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 = c.klass.fields[scroll_field.to_s]
19
- cursor_options = { field: field, direction: scroll_direction }
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
- c.where(cursor.criteria).each do |record|
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
- c
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, :field, :direction
5
+ attr_accessor :value, :tiebreak_id, :field_type, :field_name, :direction
6
6
 
7
7
  def initialize(value = nil, options = {})
8
- unless options && (@field = options[:field])
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
- @value, @tiebreak_id = Mongoid::Scroll::Cursor.parse(value, options)
10
+ parse(value)
13
11
  end
14
12
 
15
13
  def criteria
16
- cursor_criteria = { field.name => { direction => value } } if value
17
- tiebreak_criteria = { field.name => value, :_id => { '$gt' => tiebreak_id } } if value && tiebreak_id
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
- unless options && (field = options[:field])
24
- raise ArgumentError.new "Missing options[:field]."
25
- end
26
- Mongoid::Scroll::Cursor.new("#{transform_field_value(field, record.send(field.name))}:#{record.id}", options)
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(field, value), tiebreak_id ].join(":") : nil
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 parse(value, options)
39
- return [ nil, nil ] unless value
40
- parts = value.split(":")
41
- unless parts.length >= 2
42
- raise Mongoid::Scroll::Errors::InvalidCursorError.new({ cursor: value })
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(field, value)
50
- case field.type.to_s
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: field.name, type: field.type)
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(field, value)
63
- case field.type.to_s
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.to_time(:utc).to_i
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: field.name, type: field.type)
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
 
@@ -1,5 +1,5 @@
1
1
  module Mongoid
2
2
  module Scroll
3
- VERSION = '0.1.0'
3
+ VERSION = '0.2.0'
4
4
  end
5
5
  end
@@ -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 "integer" do
40
- it "scrolls all with a block" do
41
+ context "default" do
42
+ it "scrolls all" do
41
43
  records = []
42
- Feed::Item.asc(:a_integer).scroll do |record, next_cursor|
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
- it "scrolls all with a break" do
49
- records = []
50
- cursor = nil
51
- Feed::Item.asc(:a_integer).limit(5).scroll do |record, next_cursor|
52
- records << record
53
- cursor = next_cursor
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
- records.size.should == 5
56
- Feed::Item.asc(:a_integer).scroll(cursor) do |record, next_cursor|
57
- records << record
58
- cursor = next_cursor
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
- records.size.should == 10
61
- records.should eq Feed::Item.all.to_a
62
- end
63
- it "scrolls in descending order" do
64
- records = []
65
- Feed::Item.desc(:a_integer).limit(3).scroll do |record, next_cursor|
66
- records << record
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(nil, { field: Feed::Item.fields["a_string"]})
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", field: Feed::Item.fields["a_string"] }.to raise_error Mongoid::Scroll::Errors::InvalidCursorError,
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}", field: Feed::Item.fields["a_string"]
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}", field: Feed::Item.fields["a_integer"]
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}", field: Feed::Item.fields["a_datetime"]
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}", field: Feed::Item.fields["a_date"]
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(feed_item, field: Feed::Item.fields["a_array"])
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
@@ -2,7 +2,6 @@ module Feed
2
2
  class Item
3
3
  include Mongoid::Document
4
4
 
5
- field :a_field
6
5
  field :a_integer, type: Integer
7
6
  field :a_string, type: String
8
7
  field :a_datetime, type: DateTime
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.1.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-14 00:00:00.000000000 Z
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: 1370092332583330753
98
+ hash: 4314128693864897181
95
99
  required_rubygems_version: !ruby/object:Gem::Requirement
96
100
  none: false
97
101
  requirements: