mongoid-scroll 0.3.2 → 0.3.7

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.
@@ -4,6 +4,14 @@ Bundler.setup(:default, :development)
4
4
  require 'mongoid-scroll'
5
5
  require 'faker'
6
6
 
7
+ if defined?(Moped)
8
+ Moped.logger = Logger.new($stdout)
9
+ Moped.logger.level = Logger::DEBUG
10
+ else
11
+ Mongoid.logger.level = Logger::INFO
12
+ Mongo::Logger.logger.level = Logger::INFO if Mongoid::Compatibility::Version.mongoid5?
13
+ end
14
+
7
15
  Mongoid.connect_to 'mongoid_scroll_demo'
8
16
  Mongoid.purge!
9
17
 
@@ -27,9 +35,6 @@ total_items.times do |_i|
27
35
  Feed::Item.create! title: Faker::Lorem.sentence, position: rands.pop
28
36
  end
29
37
 
30
- Moped.logger = Logger.new($stdout)
31
- Moped.logger.level = Logger::DEBUG
32
-
33
38
  Feed::Item.create_indexes
34
39
 
35
40
  total_shown = 0
@@ -7,6 +7,8 @@ require 'faker'
7
7
  Mongoid.connect_to 'mongoid_scroll_demo'
8
8
  Mongoid.purge!
9
9
 
10
+ raise 'No Moped' unless Object.const_defined?(:Moped)
11
+
10
12
  # total items to insert
11
13
  total_items = 20
12
14
  # a MongoDB query will be executed every scroll_by items
@@ -18,5 +18,5 @@ en:
18
18
  unsupported_field_type:
19
19
  message: "Unsupported field type."
20
20
  summary: "The type of the field '%{field}' is not supported: %{type}."
21
- resolution: "Please open a feature request in https://github.com/dblock/mongoid-scroll."
21
+ resolution: "Please open a feature request in https://github.com/mongoid/mongoid-scroll."
22
22
 
@@ -0,0 +1,34 @@
1
+ module Mongo
2
+ module Scrollable
3
+ def scroll(cursor = nil, options = nil, &_block)
4
+ view = self
5
+ # we don't support scrolling over a view with multiple fields
6
+ raise Mongoid::Scroll::Errors::MultipleSortFieldsError.new(sort: view.sort) if view.sort && view.sort.keys.size != 1
7
+ # scroll field and direction
8
+ scroll_field = view.sort ? view.sort.keys.first : :_id
9
+ scroll_direction = view.sort ? view.sort.values.first.to_i : 1
10
+ # scroll cursor from the parameter, with value and tiebreak_id
11
+ options = { field_type: BSON::ObjectId } unless options
12
+ cursor_options = { field_name: scroll_field, direction: scroll_direction }.merge(options)
13
+ cursor = cursor.is_a?(Mongoid::Scroll::Cursor) ? cursor : Mongoid::Scroll::Cursor.new(cursor, cursor_options)
14
+ # make a view
15
+ view = Mongo::Collection::View.new(
16
+ view.collection,
17
+ view.selector.merge(cursor.criteria),
18
+ sort: (view.sort || {}).merge(_id: scroll_direction),
19
+ skip: skip,
20
+ limit: limit
21
+ )
22
+ # scroll
23
+ if block_given?
24
+ view.each do |record|
25
+ yield record, Mongoid::Scroll::Cursor.from_record(record, cursor_options)
26
+ end
27
+ else
28
+ view
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ Mongo::Collection::View.send(:include, Mongo::Scrollable)
@@ -3,13 +3,10 @@ require 'i18n'
3
3
  I18n.load_path << File.join(File.dirname(__FILE__), 'config', 'locales', 'en.yml')
4
4
 
5
5
  require 'mongoid'
6
+ require 'mongoid-compatibility'
6
7
  require 'mongoid/scroll/version'
7
- require 'mongoid/scroll/mongoid'
8
8
  require 'mongoid/scroll/errors'
9
9
  require 'mongoid/scroll/cursor'
10
-
11
10
  require 'moped/scrollable' if Object.const_defined?(:Moped)
12
- require 'mongoid/criterion/scrollable'
13
-
14
- Moped::Query.send(:include, Moped::Scrollable) if Object.const_defined?(:Moped)
15
- Mongoid::Criteria.send(:include, Mongoid::Criterion::Scrollable)
11
+ require 'mongo/scrollable' if Object.const_defined?(:Mongo)
12
+ require 'mongoid/criteria/scrollable'
@@ -0,0 +1,81 @@
1
+ module Mongoid
2
+ class Criteria
3
+ module Scrollable
4
+ def scroll(cursor = nil, &_block)
5
+ raise_multiple_sort_fields_error if multiple_sort_fields?
6
+ criteria = dup
7
+ criteria.merge!(default_sort) if no_sort_option?
8
+ cursor_options = build_cursor_options(criteria)
9
+ cursor = cursor.is_a?(Mongoid::Scroll::Cursor) ? cursor : new_cursor(cursor, cursor_options)
10
+ cursor_criteria = build_cursor_criteria(criteria, cursor)
11
+ if block_given?
12
+ cursor_criteria.order_by(_id: scroll_direction(criteria)).each do |record|
13
+ yield record, cursor_from_record(record, cursor_options)
14
+ end
15
+ else
16
+ cursor_criteria
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def raise_multiple_sort_fields_error
23
+ raise Mongoid::Scroll::Errors::MultipleSortFieldsError.new(sort: criteria.options.sort)
24
+ end
25
+
26
+ def multiple_sort_fields?
27
+ options.sort && options.sort.keys.size != 1
28
+ end
29
+
30
+ def no_sort_option?
31
+ options.sort.blank? || options.sort.empty?
32
+ end
33
+
34
+ def default_sort
35
+ asc(:_id)
36
+ end
37
+
38
+ def scroll_field(criteria)
39
+ criteria.options.sort.keys.first
40
+ end
41
+
42
+ def scroll_direction(criteria)
43
+ criteria.options.sort.values.first.to_i
44
+ end
45
+
46
+ def build_cursor_options(criteria)
47
+ {
48
+ field_type: scroll_field_type(criteria),
49
+ field_name: scroll_field(criteria),
50
+ direction: scroll_direction(criteria)
51
+ }
52
+ end
53
+
54
+ def new_cursor(cursor, cursor_options)
55
+ Mongoid::Scroll::Cursor.new(cursor, cursor_options)
56
+ end
57
+
58
+ def build_cursor_criteria(criteria, cursor)
59
+ cursor_criteria = criteria.dup
60
+ cursor_criteria.selector = { '$and' => [criteria.selector, cursor.criteria] }
61
+ cursor_criteria
62
+ end
63
+
64
+ def cursor_from_record(record, cursor_options)
65
+ Mongoid::Scroll::Cursor.from_record(record, cursor_options)
66
+ end
67
+
68
+ def scroll_field_type(criteria)
69
+ scroll_field = scroll_field(criteria)
70
+ field = criteria.klass.fields[scroll_field.to_s]
71
+ field.foreign_key? && field.object_id_field? ? bson_type : field.type
72
+ end
73
+
74
+ def bson_type
75
+ Mongoid::Compatibility::Version.mongoid3? ? Moped::BSON::ObjectId : BSON::ObjectId
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ Mongoid::Criteria.send(:include, Mongoid::Criteria::Scrollable)
@@ -14,8 +14,12 @@ module Mongoid
14
14
  compare_direction = direction == 1 ? '$gt' : '$lt'
15
15
  cursor_criteria = { field_name => { compare_direction => mongo_value } } if mongo_value
16
16
  tiebreak_criteria = { field_name => mongo_value, :_id => { compare_direction => tiebreak_id } } if mongo_value && tiebreak_id
17
- cursor_selector = Origin::Selector.new
18
- cursor_selector.merge!('$or' => [cursor_criteria, tiebreak_criteria].compact) if cursor_criteria || tiebreak_criteria
17
+ cursor_selector = if Mongoid::Compatibility::Version.mongoid6? || Mongoid::Compatibility::Version.mongoid7?
18
+ Mongoid::Criteria::Queryable::Selector.new
19
+ else
20
+ Origin::Selector.new
21
+ end
22
+ cursor_selector['$or'] = [cursor_criteria, tiebreak_criteria].compact if cursor_criteria || tiebreak_criteria
19
23
  cursor_selector.__evolve_object_id__
20
24
  end
21
25
 
@@ -39,16 +43,16 @@ module Mongoid
39
43
  return unless value
40
44
  parts = value.split(':')
41
45
  unless parts.length >= 2
42
- fail Mongoid::Scroll::Errors::InvalidCursorError.new(cursor: value)
46
+ raise Mongoid::Scroll::Errors::InvalidCursorError.new(cursor: value)
43
47
  end
44
48
  id = parts[-1]
45
49
  value = parts[0...-1].join(':')
46
50
  @value = Mongoid::Scroll::Cursor.parse_field_value(field_type, field_name, value)
47
- if Mongoid::Scroll.mongoid3?
48
- @tiebreak_id = Moped::BSON::ObjectId(id)
49
- else
50
- @tiebreak_id = BSON::ObjectId.from_string(id)
51
- end
51
+ @tiebreak_id = if Mongoid::Compatibility::Version.mongoid3?
52
+ Moped::BSON::ObjectId(id)
53
+ else
54
+ BSON::ObjectId.from_string(id)
55
+ end
52
56
  end
53
57
 
54
58
  class << self
@@ -58,7 +62,7 @@ module Mongoid
58
62
  elsif options && (field = options[:field])
59
63
  [field.type.to_s, field.name.to_s]
60
64
  else
61
- fail ArgumentError.new 'Missing options[:field_name] and/or options[:field_type].'
65
+ raise ArgumentError.new 'Missing options[:field_name] and/or options[:field_type].'
62
66
  end
63
67
  end
64
68
 
@@ -72,7 +76,7 @@ module Mongoid
72
76
  when 'Float' then value.to_f
73
77
  when 'Integer' then value.to_i
74
78
  else
75
- fail Mongoid::Scroll::Errors::UnsupportedFieldTypeError.new(field: field_name, type: field_type)
79
+ raise Mongoid::Scroll::Errors::UnsupportedFieldTypeError.new(field: field_name, type: field_type)
76
80
  end
77
81
  end
78
82
 
@@ -85,7 +89,7 @@ module Mongoid
85
89
  when 'Float' then value.to_f
86
90
  when 'Integer' then value.to_i
87
91
  else
88
- fail Mongoid::Scroll::Errors::UnsupportedFieldTypeError.new(field: field_name, type: field_type)
92
+ raise Mongoid::Scroll::Errors::UnsupportedFieldTypeError.new(field: field_name, type: field_type)
89
93
  end
90
94
  end
91
95
  end
@@ -27,7 +27,7 @@ module Mongoid
27
27
 
28
28
  private
29
29
 
30
- BASE_KEY = 'mongoid.scroll.errors.messages' #:nodoc:
30
+ BASE_KEY = 'mongoid.scroll.errors.messages'.freeze #:nodoc:
31
31
 
32
32
  # Given the key of the specific error and the options hash, translate the
33
33
  # message.
@@ -1,5 +1,5 @@
1
1
  module Mongoid
2
2
  module Scroll
3
- VERSION = '0.3.2'
3
+ VERSION = '0.3.7'.freeze
4
4
  end
5
5
  end
@@ -1,17 +1,16 @@
1
1
  module Moped
2
2
  module Scrollable
3
3
  def scroll(cursor = nil, options = nil, &_block)
4
- options = if Mongoid::Scroll.mongoid3?
5
- { field_type: Moped::BSON::ObjectId }
6
- else
7
- { field_type: BSON::ObjectId }
8
- end unless options
4
+ unless options
5
+ bson_type = Mongoid::Compatibility::Version.mongoid3? ? Moped::BSON::ObjectId : BSON::ObjectId
6
+ options = { field_type: bson_type }
7
+ end
9
8
  query = Query.new(collection, operation.selector.dup)
10
9
  query.operation.skip = operation.skip
11
10
  query.operation.limit = operation.limit
12
11
  # we don't support scrolling over a criteria with multiple fields
13
12
  if query.operation.selector['$orderby'] && query.operation.selector['$orderby'].keys.size != 1
14
- fail Mongoid::Scroll::Errors::MultipleSortFieldsError.new(sort: query.operation.selector['$orderby'])
13
+ raise Mongoid::Scroll::Errors::MultipleSortFieldsError.new(sort: query.operation.selector['$orderby'])
15
14
  elsif !query.operation.selector.key?('$orderby') || query.operation.selector['$orderby'].empty?
16
15
  # introduce a default sort order if there's none
17
16
  query.sort(_id: 1)
@@ -35,3 +34,5 @@ module Moped
35
34
  end
36
35
  end
37
36
  end
37
+
38
+ Moped::Query.send(:include, Moped::Scrollable)
@@ -10,9 +10,10 @@ Gem::Specification.new do |s|
10
10
  s.required_rubygems_version = '>= 1.3.6'
11
11
  s.files = `git ls-files`.split("\n")
12
12
  s.require_paths = ['lib']
13
- s.homepage = 'http://github.com/dblock/mongoid-scroll'
13
+ s.homepage = 'http://github.com/mongoid/mongoid-scroll'
14
14
  s.licenses = ['MIT']
15
15
  s.summary = 'Mongoid extensions to enable infinite scroll.'
16
16
  s.add_dependency 'mongoid', '>= 3.0'
17
+ s.add_dependency 'mongoid-compatibility'
17
18
  s.add_dependency 'i18n'
18
19
  end
@@ -0,0 +1,126 @@
1
+ require 'spec_helper'
2
+
3
+ if Object.const_defined?(:Mongo)
4
+ describe Mongo::Collection::View do
5
+ context 'scrollable' do
6
+ subject do
7
+ Mongoid.default_client['feed_items'].find
8
+ end
9
+ it ':scroll' do
10
+ expect(subject).to respond_to(:scroll)
11
+ end
12
+ end
13
+ context 'with multiple sort fields' do
14
+ subject do
15
+ Mongoid.default_client['feed_items'].find.sort(name: 1, value: -1)
16
+ end
17
+ it 'raises Mongoid::Scroll::Errors::MultipleSortFieldsError' do
18
+ expect { subject.scroll }.to raise_error Mongoid::Scroll::Errors::MultipleSortFieldsError,
19
+ /You're attempting to scroll over data with a sort order that includes multiple fields: name, value./
20
+ end
21
+ end
22
+ context 'with no sort' do
23
+ subject do
24
+ Mongoid.default_client['feed_items'].find
25
+ end
26
+ it 'adds a default sort by _id' do
27
+ expect(subject.scroll.sort).to eq('_id' => 1)
28
+ end
29
+ end
30
+ context 'with data' do
31
+ before :each do
32
+ 10.times do |i|
33
+ Mongoid.default_client['feed_items'].insert_one(
34
+ a_string: i.to_s,
35
+ a_integer: i,
36
+ a_datetime: DateTime.mongoize(DateTime.new(2013, i + 1, 21, 1, 42, 3, 'UTC')),
37
+ a_date: Date.mongoize(Date.new(2013, i + 1, 21)),
38
+ a_time: Time.mongoize(Time.at(Time.now.to_i + i))
39
+ )
40
+ end
41
+ end
42
+ context 'default' do
43
+ it 'scrolls all' do
44
+ records = []
45
+ Mongoid.default_client['feed_items'].find.scroll do |record, _next_cursor|
46
+ records << record
47
+ end
48
+ expect(records.size).to eq 10
49
+ expect(records).to eq Mongoid.default_client['feed_items'].find.to_a
50
+ end
51
+ end
52
+ { a_string: String, a_integer: Integer, a_date: Date, a_datetime: DateTime }.each_pair do |field_name, field_type|
53
+ context field_type do
54
+ it 'scrolls all with a block' do
55
+ records = []
56
+ Mongoid.default_client['feed_items'].find.sort(field_name => 1).scroll(nil, field_type: field_type) do |record, _next_cursor|
57
+ records << record
58
+ end
59
+ expect(records.size).to eq 10
60
+ expect(records).to eq Mongoid.default_client['feed_items'].find.to_a
61
+ end
62
+ it 'scrolls all with a break' do
63
+ records = []
64
+ cursor = nil
65
+ Mongoid.default_client['feed_items'].find.sort(field_name => 1).limit(5).scroll(nil, field_type: field_type) do |record, next_cursor|
66
+ records << record
67
+ cursor = next_cursor
68
+ end
69
+ expect(records.size).to eq 5
70
+ Mongoid.default_client['feed_items'].find.sort(field_name => 1).scroll(cursor, field_type: field_type) do |record, next_cursor|
71
+ records << record
72
+ cursor = next_cursor
73
+ end
74
+ expect(records.size).to eq 10
75
+ expect(records).to eq Mongoid.default_client['feed_items'].find.to_a
76
+ end
77
+ it 'scrolls in descending order' do
78
+ records = []
79
+ Mongoid.default_client['feed_items'].find.sort(field_name => -1).limit(3).scroll(nil, field_type: field_type, field_name: field_name) do |record, _next_cursor|
80
+ records << record
81
+ end
82
+ expect(records.size).to eq 3
83
+ expect(records).to eq Mongoid.default_client['feed_items'].find.sort(field_name => -1).limit(3).to_a
84
+ end
85
+ it 'map' do
86
+ record = Mongoid.default_client['feed_items'].find.limit(3).scroll(nil, field_type: field_type, field_name: field_name).map { |r| r }.last
87
+ cursor = Mongoid::Scroll::Cursor.from_record(record, field_type: field_type, field_name: field_name)
88
+ expect(cursor).to_not be nil
89
+ expect(cursor.to_s.split(':')).to eq [
90
+ Mongoid::Scroll::Cursor.transform_field_value(field_type, field_name, record[field_name.to_s]).to_s,
91
+ record['_id'].to_s
92
+ ]
93
+ end
94
+ end
95
+ end
96
+ end
97
+ context 'with overlapping data', if: MongoDB.mmapv1? do
98
+ before :each do
99
+ 3.times { Feed::Item.create! a_integer: 5 }
100
+ Feed::Item.first.update_attributes!(name: Array(1000).join('a'))
101
+ end
102
+ it 'natural order is different from order by id' do
103
+ # natural order isn't necessarily going to be the same as _id order
104
+ # if a document is updated and grows in size, it may need to be relocated and
105
+ # thus cause the natural order to change
106
+ expect(Feed::Item.order_by('$natural' => 1).to_a).to_not eq Feed::Item.order_by(_id: 1).to_a
107
+ end
108
+ [{ a_integer: 1 }, { a_integer: -1 }].each do |sort_order|
109
+ it "scrolls by #{sort_order}" do
110
+ records = []
111
+ cursor = nil
112
+ Mongoid.default_client['feed_items'].find.sort(sort_order).limit(2).scroll do |record, next_cursor|
113
+ records << record
114
+ cursor = next_cursor
115
+ end
116
+ expect(records.size).to eq 2
117
+ Mongoid.default_client['feed_items'].find.sort(sort_order).scroll(cursor) do |record, _next_cursor|
118
+ records << record
119
+ end
120
+ expect(records.size).to eq 3
121
+ expect(records).to eq Mongoid.default_client['feed_items'].find.sort(sort_order.merge(_id: sort_order[:a_integer])).to_a
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -13,6 +13,7 @@ describe Mongoid::Criteria do
13
13
  /You're attempting to scroll over data with a sort order that includes multiple fields: name, value./
14
14
  end
15
15
  end
16
+
16
17
  context 'with no sort' do
17
18
  subject do
18
19
  Feed::Item.all
@@ -42,7 +43,30 @@ describe Mongoid::Criteria do
42
43
  expect(records.size).to eq 10
43
44
  expect(records).to eq Feed::Item.all.to_a
44
45
  end
46
+ it 'does not change original criteria' do
47
+ criteria = Feed::Item.where(:a_time.gt => Time.new(2013, 7, 22, 1, 2, 3))
48
+ original_criteria = criteria.dup
49
+ criteria.limit(2).scroll
50
+ expect(criteria).to eq original_criteria
51
+ cursor = nil
52
+ criteria.limit(2).scroll(cursor) do |_record, next_cursor|
53
+ cursor = next_cursor
54
+ end
55
+ criteria.scroll(cursor) do |_record, next_cursor|
56
+ cursor = next_cursor
57
+ end
58
+ expect(criteria).to eq original_criteria
59
+ end
45
60
  end
61
+
62
+ context 'with a foreign key' do
63
+ it 'sorts by object id' do
64
+ records = []
65
+ Feed::Item.asc('publisher_id').scroll { |r, _| records << r }
66
+ expect(records).not_to be_empty
67
+ end
68
+ end
69
+
46
70
  { a_string: String, a_integer: Integer, a_date: Date, a_datetime: DateTime }.each_pair do |field_name, field_type|
47
71
  context field_type do
48
72
  it 'scrolls all with a block' do
@@ -68,6 +92,17 @@ describe Mongoid::Criteria do
68
92
  expect(records.size).to eq 10
69
93
  expect(records).to eq Feed::Item.all.to_a
70
94
  end
95
+ it 'scrolls from a cursor' do
96
+ last_record = nil
97
+ cursor = nil
98
+ Feed::Item.asc(field_name).limit(5).scroll do |record, next_cursor|
99
+ last_record = record
100
+ cursor = next_cursor
101
+ end
102
+ sixth_item = Feed::Item.asc(field_name).to_a[5]
103
+ from_item = Feed::Item.asc(field_name).scroll(cursor).to_a.first
104
+ expect(from_item).to eq sixth_item
105
+ end
71
106
  it 'scrolls in descending order' do
72
107
  records = []
73
108
  Feed::Item.desc(field_name).limit(3).scroll do |record, _next_cursor|
@@ -132,9 +167,9 @@ describe Mongoid::Criteria do
132
167
  end
133
168
  it 'merges cursor criteria when no sort field is given' do
134
169
  criteria = Feed::Item.where(:a_time.gt => Time.new(2013, 7, 22, 1, 2, 3))
135
- feed_item_1 = Feed::Item.where(name: 'Feed Item 1').first
136
- cursor_input = "#{feed_item_1.id}:#{feed_item_1.id}"
137
- field_type = Mongoid::Scroll.mongoid3? ? Moped::BSON::ObjectId : BSON::ObjectId
170
+ feed_item = Feed::Item.where(name: 'Feed Item 1').first
171
+ cursor_input = "#{feed_item.id}:#{feed_item.id}"
172
+ field_type = Mongoid::Compatibility::Version.mongoid3? ? Moped::BSON::ObjectId : BSON::ObjectId
138
173
  cursor_options = { field_type: field_type, field_name: '_id', direction: 1 }
139
174
  cursor = Mongoid::Scroll::Cursor.new(cursor_input, cursor_options)
140
175
  records = []
@@ -160,7 +195,7 @@ describe Mongoid::Criteria do
160
195
  expect(records.map(&:name)).to eq ['embedded']
161
196
  end
162
197
  end
163
- context 'with overlapping data' do
198
+ context 'with overlapping data', if: MongoDB.mmapv1? do
164
199
  before :each do
165
200
  3.times { Feed::Item.create! a_integer: 5 }
166
201
  Feed::Item.first.update_attributes!(name: Array(1000).join('a'))