mongoid-scroll 0.3.2 → 0.3.7

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