mongoid 3.0.23 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. data/CHANGELOG.md +253 -9
  2. data/LICENSE +1 -1
  3. data/README.md +4 -1
  4. data/lib/config/locales/en.yml +7 -6
  5. data/lib/mongoid.rb +18 -1
  6. data/lib/mongoid/atomic.rb +22 -20
  7. data/lib/mongoid/atomic/paths/embedded.rb +19 -5
  8. data/lib/mongoid/atomic/paths/root.rb +1 -1
  9. data/lib/mongoid/atomic/positionable.rb +73 -0
  10. data/lib/mongoid/attributes.rb +63 -1
  11. data/lib/mongoid/callbacks.rb +58 -4
  12. data/lib/mongoid/components.rb +8 -3
  13. data/lib/mongoid/config.rb +71 -23
  14. data/lib/mongoid/contextual.rb +2 -1
  15. data/lib/mongoid/contextual/aggregable/mongo.rb +27 -63
  16. data/lib/mongoid/contextual/atomic.rb +4 -3
  17. data/lib/mongoid/contextual/find_and_modify.rb +1 -1
  18. data/lib/mongoid/contextual/geo_near.rb +238 -0
  19. data/lib/mongoid/contextual/map_reduce.rb +12 -1
  20. data/lib/mongoid/contextual/memory.rb +36 -31
  21. data/lib/mongoid/contextual/mongo.rb +147 -91
  22. data/lib/mongoid/contextual/queryable.rb +25 -0
  23. data/lib/mongoid/copyable.rb +4 -1
  24. data/lib/mongoid/criteria.rb +23 -275
  25. data/lib/mongoid/criterion/findable.rb +179 -0
  26. data/lib/mongoid/criterion/modifiable.rb +191 -0
  27. data/lib/mongoid/criterion/scoping.rb +11 -6
  28. data/lib/mongoid/document.rb +7 -56
  29. data/lib/mongoid/equality.rb +66 -0
  30. data/lib/mongoid/errors/mongoid_error.rb +7 -3
  31. data/lib/mongoid/extensions/array.rb +13 -1
  32. data/lib/mongoid/extensions/date.rb +9 -2
  33. data/lib/mongoid/extensions/hash.rb +38 -2
  34. data/lib/mongoid/extensions/nil_class.rb +12 -0
  35. data/lib/mongoid/extensions/object.rb +24 -0
  36. data/lib/mongoid/extensions/string.rb +14 -2
  37. data/lib/mongoid/extensions/time.rb +4 -1
  38. data/lib/mongoid/fields.rb +49 -5
  39. data/lib/mongoid/fields/foreign_key.rb +12 -0
  40. data/lib/mongoid/fields/standard.rb +12 -0
  41. data/lib/mongoid/finders.rb +8 -0
  42. data/lib/mongoid/hierarchy.rb +19 -1
  43. data/lib/mongoid/indexes.rb +30 -4
  44. data/lib/mongoid/indexes/validators/options.rb +12 -2
  45. data/lib/mongoid/inspection.rb +2 -1
  46. data/lib/mongoid/matchers/strategies.rb +5 -5
  47. data/lib/mongoid/observer.rb +27 -36
  48. data/lib/mongoid/persistence.rb +42 -17
  49. data/lib/mongoid/persistence/atomic.rb +10 -5
  50. data/lib/mongoid/persistence/atomic/operation.rb +26 -9
  51. data/lib/mongoid/persistence/atomic/unset.rb +1 -1
  52. data/lib/mongoid/persistence/operations/embedded/insert.rb +5 -2
  53. data/lib/mongoid/persistence/operations/embedded/remove.rb +5 -2
  54. data/lib/mongoid/persistence/operations/update.rb +7 -3
  55. data/lib/mongoid/railties/database.rake +12 -19
  56. data/lib/mongoid/relations.rb +2 -0
  57. data/lib/mongoid/relations/accessors.rb +30 -8
  58. data/lib/mongoid/relations/binding.rb +5 -1
  59. data/lib/mongoid/relations/bindings/referenced/in.rb +1 -1
  60. data/lib/mongoid/relations/bindings/referenced/many_to_many.rb +3 -3
  61. data/lib/mongoid/relations/counter_cache.rb +107 -0
  62. data/lib/mongoid/relations/embedded/batchable.rb +13 -4
  63. data/lib/mongoid/relations/embedded/many.rb +30 -1
  64. data/lib/mongoid/relations/macros.rb +2 -0
  65. data/lib/mongoid/relations/marshalable.rb +0 -1
  66. data/lib/mongoid/relations/metadata.rb +63 -11
  67. data/lib/mongoid/relations/options.rb +1 -0
  68. data/lib/mongoid/relations/proxy.rb +45 -2
  69. data/lib/mongoid/relations/referenced/in.rb +11 -2
  70. data/lib/mongoid/relations/referenced/many.rb +31 -3
  71. data/lib/mongoid/relations/referenced/many_to_many.rb +31 -3
  72. data/lib/mongoid/relations/referenced/one.rb +1 -1
  73. data/lib/mongoid/relations/targets/enumerable.rb +5 -1
  74. data/lib/mongoid/relations/touchable.rb +35 -6
  75. data/lib/mongoid/reloading.rb +5 -3
  76. data/lib/mongoid/scoping.rb +2 -2
  77. data/lib/mongoid/sessions.rb +57 -7
  78. data/lib/mongoid/sessions/factory.rb +22 -1
  79. data/lib/mongoid/threaded.rb +4 -30
  80. data/lib/mongoid/threaded/lifecycle.rb +12 -12
  81. data/lib/mongoid/timestamps.rb +1 -0
  82. data/lib/mongoid/timestamps/created.rb +2 -0
  83. data/lib/mongoid/timestamps/created/short.rb +19 -0
  84. data/lib/mongoid/timestamps/short.rb +10 -0
  85. data/lib/mongoid/timestamps/updated.rb +2 -0
  86. data/lib/mongoid/timestamps/updated/short.rb +19 -0
  87. data/lib/mongoid/validations.rb +2 -0
  88. data/lib/mongoid/validations/queryable.rb +2 -2
  89. data/lib/mongoid/validations/uniqueness.rb +1 -18
  90. data/lib/mongoid/version.rb +1 -1
  91. data/lib/rails/generators/mongoid/model/model_generator.rb +1 -0
  92. data/lib/rails/generators/mongoid/model/templates/model.rb.tt +3 -0
  93. data/lib/rails/mongoid.rb +53 -29
  94. data/lib/support/ruby_version.rb +26 -0
  95. metadata +18 -7
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
- require "mongoid/contextual/memory"
2
+ require "mongoid/contextual/queryable"
3
3
  require "mongoid/contextual/mongo"
4
+ require "mongoid/contextual/memory"
4
5
 
5
6
  module Mongoid
6
7
  module Contextual
@@ -24,8 +24,12 @@ module Mongoid
24
24
  # @since 3.0.0
25
25
  def aggregates(field)
26
26
  if query.count > 0
27
- map_reduce(mapper(field), reducer).
28
- out(inline: 1).finalize(finalizer).first["value"]
27
+ result = collection.aggregate(pipeline(field)).to_a
28
+ if result.empty?
29
+ { "count" => query.count, "avg" => 0, "sum" => 0 }
30
+ else
31
+ result.first
32
+ end
29
33
  else
30
34
  { "count" => 0 }
31
35
  end
@@ -109,73 +113,33 @@ module Mongoid
109
113
 
110
114
  private
111
115
 
112
- # Get the finalize function.
116
+ # Get the aggregation pipeline for provided field.
113
117
  #
114
118
  # @api private
115
119
  #
116
- # @example Get the finalize function.
117
- # aggregable.finalizer
118
- #
119
- # @return [ String ] The finalize JS function.
120
- #
121
- # @since 3.0.0
122
- def finalizer
123
- %Q{
124
- function(key, agg) {
125
- agg.avg = agg.sum / agg.count;
126
- return agg;
127
- }}
128
- end
129
-
130
- # Get the map function for the provided field.
131
- #
132
- # @api private
133
- #
134
- # @example Get the map function.
135
- # aggregable.mapper(:likes)
120
+ # @example Get the pipeline.
121
+ # aggregable.pipeline(:likes)
136
122
  #
137
123
  # @param [ String, Symbol ] field The name of the field.
138
124
  #
139
- # @return [ String ] The map JS function.
140
- #
141
- # @since 3.0.0
142
- def mapper(field)
143
- %Q{
144
- function() {
145
- var agg = {
146
- count: 1,
147
- max: this.#{field},
148
- min: this.#{field},
149
- sum: this.#{field}
150
- };
151
- emit("#{field}", agg);
152
- }}
153
- end
154
-
155
- # Get the reduce function for the provided field.
156
- #
157
- # @api private
158
- #
159
- # @example Get the reduce function.
160
- # aggregable.reducer(:likes)
161
- #
162
- # @return [ String ] The reduce JS function.
163
- #
164
- # @since 3.0.0
165
- def reducer
166
- %Q{
167
- function(key, values) {
168
- var agg = { count: 0, max: null, min: null, sum: 0 };
169
- values.forEach(function(val) {
170
- if (val.max !== null) {
171
- if (agg.max == null || val.max > agg.max) agg.max = val.max;
172
- if (agg.min == null || val.max < agg.min) agg.min = val.max;
173
- agg.sum += val.sum;
174
- }
175
- agg.count += val.count;
176
- });
177
- return agg;
178
- }}
125
+ # @return [ Array ] The array of pipeline operators.
126
+ #
127
+ # @since 3.1.0
128
+ def pipeline(field)
129
+ db_field = "$#{database_field_name(field)}"
130
+ pipeline = []
131
+ pipeline << { "$match" => criteria.nin(field => nil).selector }
132
+ pipeline << { "$limit" => criteria.options[:limit] } if criteria.options[:limit]
133
+ pipeline << {
134
+ "$group" => {
135
+ "_id" => field.to_s,
136
+ "count" => { "$sum" => 1 },
137
+ "max" => { "$max" => db_field },
138
+ "min" => { "$min" => db_field },
139
+ "sum" => { "$sum" => db_field },
140
+ "avg" => { "$avg" => db_field }
141
+ }
142
+ }
179
143
  end
180
144
  end
181
145
  end
@@ -166,13 +166,14 @@ module Mongoid
166
166
  # @example Unset the field on the matches.
167
167
  # context.unset(:name)
168
168
  #
169
- # @param [ String, Symbol ] field The name of the field.
169
+ # @param [ String, Symbol, Array ] fields The name of the fields.
170
170
  #
171
171
  # @return [ nil ] Nil.
172
172
  #
173
173
  # @since 3.0.0
174
- def unset(field)
175
- query.update_all("$unset" => { database_field_name(field) => true })
174
+ def unset(*args)
175
+ fields = args.__find_args__.collect { |f| [database_field_name(f), true] }
176
+ query.update_all("$unset" => Hash[fields])
176
177
  end
177
178
  end
178
179
  end
@@ -51,7 +51,7 @@ module Mongoid
51
51
  # @api private
52
52
  #
53
53
  # @example Apply the criteria options
54
- # map_reduce.apply_criteria_options
54
+ # find_and_modify.apply_criteria_options
55
55
  #
56
56
  # @return [ nil ] Nothing.
57
57
  #
@@ -0,0 +1,238 @@
1
+ # encoding: utf-8
2
+ module Mongoid
3
+ module Contextual
4
+ class GeoNear
5
+ include Enumerable
6
+ include Command
7
+
8
+ delegate :[], to: :results
9
+ delegate :==, :empty?, to: :entries
10
+
11
+ # Get the average distance for all documents from the point in the
12
+ # command.
13
+ #
14
+ # @example Get the average distance.
15
+ # geo_near.average_distance
16
+ #
17
+ # @return [ Float, nil ] The average distance.
18
+ #
19
+ # @since 3.1.0
20
+ def average_distance
21
+ average = stats["avgDistance"]
22
+ average.nan? ? nil : average
23
+ end
24
+
25
+ # Iterates over each of the documents in the $geoNear, excluding the
26
+ # extra information that was passed back from the database.
27
+ #
28
+ # @example Iterate over the results.
29
+ # geo_near.each do |doc|
30
+ # p doc
31
+ # end
32
+ #
33
+ # @return [ Enumerator ] The enumerator.
34
+ #
35
+ # @since 3.1.0
36
+ def each
37
+ if block_given?
38
+ documents.each do |doc|
39
+ yield doc
40
+ end
41
+ else
42
+ to_enum
43
+ end
44
+ end
45
+
46
+ # Provide a distance multiplier to be used for each returned distance.
47
+ #
48
+ # @example Provide the distance multiplier.
49
+ # geo_near.distance_multiplier(13113.1)
50
+ #
51
+ # @param [ Integer, Float ] value The distance multiplier.
52
+ #
53
+ # @return [ GeoNear ] The GeoNear wrapper.
54
+ #
55
+ # @since 3.1.0
56
+ def distance_multiplier(value)
57
+ command[:distanceMultiplier] = value
58
+ self
59
+ end
60
+
61
+ # Initialize the new map/reduce directive.
62
+ #
63
+ # @example Initialize the new map/reduce.
64
+ # MapReduce.new(criteria, map, reduce)
65
+ #
66
+ # @param [ Criteria ] criteria The Mongoid criteria.
67
+ # @param [ String ] map The map js function.
68
+ # @param [ String ] reduce The reduce js function.
69
+ #
70
+ # @since 3.0.0
71
+ def initialize(collection, criteria, near)
72
+ @collection, @criteria = collection, criteria
73
+ command[:geoNear] = collection.name.to_s
74
+ command[:near] = near
75
+ apply_criteria_options
76
+ end
77
+
78
+ # Get a pretty string representation of the command.
79
+ #
80
+ # @example Inspect the geoNear.
81
+ # geo_near.inspect
82
+ #
83
+ # @return [ String ] The inspection string.
84
+ #
85
+ # @since 3.1.0
86
+ def inspect
87
+ %Q{#<Mongoid::Contextual::GeoNear
88
+ selector: #{criteria.selector.inspect}
89
+ class: #{criteria.klass}
90
+ near: #{command[:near]}
91
+ multiplier: #{command[:distanceMultiplier] || "N/A"}
92
+ max: #{command[:maxDistance] || "N/A"}
93
+ unique: #{command[:unique].nil? ? true : command[:unique]}
94
+ spherical: #{command[:spherical] || false}>
95
+ }
96
+ end
97
+
98
+ # Specify the maximum distance to find documents for, or get the value of
99
+ # the document with the furthest distance.
100
+ #
101
+ # @example Set the max distance.
102
+ # geo_near.max_distance(0.5)
103
+ #
104
+ # @example Get the max distance.
105
+ # geo_near.max_distance
106
+ #
107
+ # @param [ Integer, Float ] value The maximum distance.
108
+ #
109
+ # @return [ GeoNear, Float ] The GeoNear command or the value.
110
+ #
111
+ # @since 3.1.0
112
+ def max_distance(value = nil)
113
+ if value
114
+ command[:maxDistance] = value
115
+ self
116
+ else
117
+ stats["maxDistance"]
118
+ end
119
+ end
120
+
121
+ # Tell the command to calculate based on spherical distances.
122
+ #
123
+ # @example Add the spherical flag.
124
+ # geo_near.spherical
125
+ #
126
+ # @return [ GeoNear ] The command.
127
+ #
128
+ # @since 3.1.0
129
+ def spherical
130
+ command[:spherical] = true
131
+ self
132
+ end
133
+
134
+ # Tell the command whether or not the retured results should be unique.
135
+ #
136
+ # @example Set the unique flag.
137
+ # geo_near.unique(false)
138
+ #
139
+ # @param [ true, false ] value Whether to return unique documents.
140
+ #
141
+ # @return [ GeoNear ] The command.
142
+ #
143
+ # @since 3.1.0
144
+ def unique(value = true)
145
+ command[:unique] = value
146
+ self
147
+ end
148
+
149
+ # Execute the $geoNear, returning the raw output.
150
+ #
151
+ # @example Run the $geoNear
152
+ # geo_near.execute
153
+ #
154
+ # @return [ Hash ] The raw output
155
+ #
156
+ # @since 3.1.0
157
+ def execute
158
+ results
159
+ end
160
+
161
+ # Get the stats for the command run.
162
+ #
163
+ # @example Get the stats.
164
+ # geo_near.stats
165
+ #
166
+ # @return [ Hash ] The stats from the command run.
167
+ #
168
+ # @since 3.1.0
169
+ def stats
170
+ results["stats"]
171
+ end
172
+
173
+ # Get the execution time of the command.
174
+ #
175
+ # @example Get the execution time.
176
+ # geo_near.time
177
+ #
178
+ # @return [ Float ] The execution time.
179
+ #
180
+ # @since 3.1.0
181
+ def time
182
+ stats["time"]
183
+ end
184
+
185
+ private
186
+
187
+ # Apply criteria specific options - query, limit.
188
+ #
189
+ # @api private
190
+ #
191
+ # @example Apply the criteria options
192
+ # geo_near.apply_criteria_options
193
+ #
194
+ # @return [ nil ] Nothing.
195
+ #
196
+ # @since 3.0.0
197
+ def apply_criteria_options
198
+ command[:query] = criteria.selector
199
+ if limit = criteria.options[:limit]
200
+ command[:num] = limit
201
+ end
202
+ end
203
+
204
+ # Get the result documents from the $geoNear.
205
+ #
206
+ # @api private
207
+ #
208
+ # @example Get the documents.
209
+ # geo_near.documents
210
+ #
211
+ # @return [ Array, Cursor ] The documents.
212
+ #
213
+ # @since 3.0.0
214
+ def documents
215
+ results["results"].map do |attributes|
216
+ doc = Factory.from_db(criteria.klass, attributes["obj"], criteria.object_id)
217
+ doc.attributes["geo_near_distance"] = attributes["dis"]
218
+ doc
219
+ end
220
+ end
221
+
222
+ # Execute the $geoNear command and get the results.
223
+ #
224
+ # @api private
225
+ #
226
+ # @example Get the results.
227
+ # geo_near.results
228
+ #
229
+ # @return [ Hash ] The results of the command.
230
+ #
231
+ # @since 3.0.0
232
+ def results
233
+ @results ||= session.command(command)
234
+ end
235
+ end
236
+ end
237
+ end
238
+
@@ -6,7 +6,7 @@ module Mongoid
6
6
  include Command
7
7
 
8
8
  delegate :[], to: :results
9
- delegate :==, :empty?, :inspect, to: :entries
9
+ delegate :==, :empty?, to: :entries
10
10
 
11
11
  # Get all the counts returned by the map/reduce.
12
12
  #
@@ -162,6 +162,17 @@ module Mongoid
162
162
  results
163
163
  end
164
164
 
165
+ # Execute the map/reduce, returning the raw output.
166
+ # Useful when you don't care about map/reduce's ouptut.
167
+ #
168
+ # @example Run the map reduce
169
+ # map_reduce.execute
170
+ #
171
+ # @return [ Hash ] The raw output
172
+ #
173
+ # @since 3.1.0
174
+ alias :execute :raw
175
+
165
176
  # Get the number of documents reduced by the map/reduce.
166
177
  #
167
178
  # @example Get the reduced document count.
@@ -6,22 +6,15 @@ module Mongoid
6
6
  class Memory
7
7
  include Enumerable
8
8
  include Aggregable::Memory
9
+ include Eager
10
+ include Queryable
11
+ include Mongoid::Atomic::Positionable
9
12
 
10
- # @attribute [r] collection The root collection.
11
- # @attribute [r] criteria The criteria for the context.
12
- # @attribute [r] klass The criteria class.
13
13
  # @attribute [r] root The root document.
14
14
  # @attribute [r] path The atomic path.
15
15
  # @attribute [r] selector The root document selector.
16
16
  # @attribute [r] matching The in memory documents that match the selector.
17
- attr_reader \
18
- :collection,
19
- :criteria,
20
- :documents,
21
- :klass,
22
- :path,
23
- :root,
24
- :selector
17
+ attr_reader :documents, :path, :root, :selector
25
18
 
26
19
  # Check if the context is equal to the other object.
27
20
  #
@@ -38,19 +31,6 @@ module Mongoid
38
31
  entries == other.entries
39
32
  end
40
33
 
41
- # Is the enumerable of matching documents empty?
42
- #
43
- # @example Is the context empty?
44
- # context.blank?
45
- #
46
- # @return [ true, false ] If the context is empty.
47
- #
48
- # @since 3.0.0
49
- def blank?
50
- count == 0
51
- end
52
- alias :empty? :blank?
53
-
54
34
  # Delete all documents in the database that match the selector.
55
35
  #
56
36
  # @example Delete all the documents.
@@ -66,7 +46,9 @@ module Mongoid
66
46
  doc.as_document
67
47
  end
68
48
  unless removed.empty?
69
- collection.find(selector).update("$pullAll" => { path => removed })
49
+ collection.find(selector).update(
50
+ positionally(selector, "$pullAll" => { path => removed })
51
+ )
70
52
  end
71
53
  deleted
72
54
  end
@@ -117,9 +99,10 @@ module Mongoid
117
99
  # @since 3.0.0
118
100
  def each
119
101
  if block_given?
120
- (documents[skipping || 0, limiting || documents.length] || []).each do |doc|
121
- yield doc
102
+ documents_for_iteration.each do |doc|
103
+ yield(doc)
122
104
  end
105
+ # eager_loadable? ? docs : self
123
106
  else
124
107
  to_enum
125
108
  end
@@ -146,7 +129,9 @@ module Mongoid
146
129
  #
147
130
  # @since 3.0.0
148
131
  def first
149
- documents.first
132
+ doc = documents.first
133
+ eager_load_one(doc) if eager_loadable?(doc)
134
+ doc
150
135
  end
151
136
  alias :one :first
152
137
 
@@ -178,7 +163,9 @@ module Mongoid
178
163
  #
179
164
  # @since 3.0.0
180
165
  def last
181
- documents.last
166
+ doc = documents.last
167
+ eager_load_one(doc) if eager_loadable?(doc)
168
+ doc
182
169
  end
183
170
 
184
171
  # Get the length of matching documents in the context.
@@ -269,6 +256,24 @@ module Mongoid
269
256
 
270
257
  private
271
258
 
259
+ # Get the documents the context should iterate. This follows 3 rules:
260
+ #
261
+ # @api private
262
+ #
263
+ # @example Get the documents for iteration.
264
+ # context.documents_for_iteration
265
+ #
266
+ # @return [ Array<Document> ] The docs to iterate.
267
+ #
268
+ # @since 3.1.0
269
+ def documents_for_iteration
270
+ docs = documents[skipping || 0, limiting || documents.length] || []
271
+ if eager_loadable?
272
+ eager_load(docs)
273
+ end
274
+ docs
275
+ end
276
+
272
277
  # Update the provided documents with the attributes.
273
278
  #
274
279
  # @api private
@@ -404,11 +409,11 @@ module Mongoid
404
409
  #
405
410
  # @since 3.0.0
406
411
  def in_place_sort(values)
407
- values.each_pair do |field, dir|
412
+ values.keys.reverse.each do |field|
408
413
  documents.sort! do |a, b|
409
414
  a_value, b_value = a[field], b[field]
410
415
  value = compare(a_value.__sortable__, b_value.__sortable__)
411
- dir < 0 ? value * -1 : value
416
+ values[field] < 0 ? value * -1 : value
412
417
  end
413
418
  end
414
419
  end