mongoid-scroll 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: