forked from mongodb/mongoid
/
enumerable.rb
312 lines (284 loc) · 8.85 KB
/
enumerable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# encoding: utf-8
require 'mongoid/contexts/enumerable/sort'
module Mongoid #:nodoc:
module Contexts #:nodoc:
class Enumerable
include Relations::Embedded::Atomic
attr_accessor :collection, :criteria
delegate :blank?, :empty?, :first, :last, :to => :execute
delegate :klass, :documents, :options, :field_list, :selector, :to => :criteria
# Return aggregation counts of the grouped documents. This will count by
# the first field provided in the fields array.
#
# @example Aggregate on a field.
# person.addresses.only(:street).aggregate
#
# @return [ Hash ] Field values as keys, count as values
def aggregate
{}.tap do |counts|
group.each_pair { |key, value| counts[key] = value.size }
end
end
# Get the average value for the supplied field.
#
# @example Get the average.
# context.avg(:age)
#
# @return [ Numeric ] A numeric value that is the average.
def avg(field)
total = sum(field)
total ? (total.to_f / count) : nil
end
# Gets the number of documents in the array. Delegates to size.
#
# @example Get the count.
# context.count
#
# @return [ Integer ] The count of documents.
def count
@count ||= execute.size
end
alias :length :count
alias :size :count
# Delete all the documents in the database matching the selector.
#
# @example Delete the documents.
# context.delete_all
#
# @return [ Integer ] The number of documents deleted.
#
# @since 2.0.0.rc.1
def delete_all
atomically(:$pull) do
set_collection
count.tap do
filter.each { |doc| doc.delete }
end
end
end
alias :delete :delete_all
# Destroy all the documents in the database matching the selector.
#
# @example Destroy the documents.
# context.destroy_all
#
# @return [ Integer ] The number of documents destroyed.
#
# @since 2.0.0.rc.1
def destroy_all
atomically(:$pull) do
set_collection
count.tap do
filter.each { |doc| doc.destroy }
end
end
end
alias :destroy :destroy_all
# Gets an array of distinct values for the supplied field across the
# entire array or the susbset given the criteria.
#
# @example Get the list of distinct values.
# context.distinct(:title)
#
# @return [ Array<String> ] The distinct values.
def distinct(field)
execute.collect { |doc| doc.send(field) }.uniq
end
# Enumerable implementation of execute. Returns matching documents for
# the selector, and adds options if supplied.
#
# @example Execute the context.
# context.execute
#
# @return [ Array<Document> ] Documents that matched the selector.
def execute
limit(sort(filter)) || []
end
# Groups the documents by the first field supplied in the field options.
#
# @example Group the context.
# context.group
#
# @return [ Hash ] Field values as keys, arrays of documents as values.
def group
field = field_list.first
execute.group_by { |doc| doc.send(field) }
end
# Create the new enumerable context. This will need the selector and
# options from a +Criteria+ and a documents array that is the underlying
# array of embedded documents from a has many association.
#
# @example Create a new context.
# Mongoid::Contexts::Enumerable.new(criteria)
#
# @param [ Criteria ] criteria The criteria for the context.
def initialize(criteria)
@criteria = criteria
end
# Iterate over each +Document+ in the results. This can take an optional
# block to pass to each argument in the results.
#
# @example Iterate over the documents.
# context.iterate { |doc| p doc }
def iterate(&block)
execute.each(&block)
end
# Get the largest value for the field in all the documents.
#
# @example Get the max value.
# context.max(:age)
#
# @return [ Numeric ] The numerical largest value.
def max(field)
determine(field, :>=)
end
# Get the smallest value for the field in all the documents.
#
# @example Get the minimum value.
# context.min(:age)
#
# @return [ Numeric ] The numerical smallest value.
def min(field)
determine(field, :<=)
end
# Get one document.
#
# @example Get one document.
# context.one
#
# @return [ Document ] The first document in the array.
alias :one :first
# Get one document and tell the criteria to skip this record on
# successive calls.
#
# @example Shift the documents.
# context.shift
#
# @return [ Document ] The first document in the array.
def shift
first.tap do |document|
self.criteria = criteria.skip((options[:skip] || 0) + 1)
end
end
# Get the sum of the field values for all the documents.
#
# @example Get the sum of the field.
# context.sum(:cost)
#
# @return [ Numeric ] The numerical sum of all the document field values.
def sum(field)
sum = execute.inject(nil) do |memo, doc|
value = doc.send(field) || 0
memo ? memo += value : value
end
end
# Very basic update that will perform a simple atomic $set of the
# attributes provided in the hash. Can be expanded to later for more
# robust functionality.
#
# @example Update all matching documents.
# context.update_all(:title => "Sir")
#
# @param [ Hash ] attributes The sets to perform.
#
# @since 2.0.0.rc.6
def update_all(attributes = nil)
iterate do |doc|
doc.update_attributes(attributes || {})
end
end
alias :update :update_all
protected
# Get the root class collection name.
#
# @example Get the root class collection name.
# context.collection_name
#
# @return [ String ] The name of the collection.
#
# @since 2.4.3
def collection_name
root ? root.collection_name : nil
end
# Filters the documents against the criteria's selector
#
# @example Filter the documents.
# context.filter
#
# @return [ Array ] The documents filtered.
def filter
documents.select { |document| document.matches?(selector) }
end
# If the field exists, perform the comparison and set if true.
#
# @example Compare.
# context.determine
#
# @return [ Array<Document> ] The matching documents.
def determine(field, operator)
matching = documents.inject(nil) do |memo, doc|
value = doc.send(field) || 0
(memo && memo.send(operator, value)) ? memo : value
end
end
# Limits the result set if skip and limit options.
#
# @example Limit the results.
# context.limit(documents)
#
# @return [ Array<Document> ] The limited documents.
def limit(documents)
skip, limit = options[:skip], options[:limit]
if skip && limit
return documents.slice(skip, limit)
elsif limit
return documents.first(limit)
elsif skip
return documents.slice(skip..-1)
end
documents
end
# Get the root document for the enumerable.
#
# @example Get the root document.
# context.root
#
# @return [ Document ] The root.
def root
@root ||= documents.first.try(:_root)
end
# Get the root class for the enumerable.
#
# @example Get the root class.
# context.root_class
#
# @return [ Class ] The root class.
def root_class
@root_class ||= root ? root.class : nil
end
# Set the collection to the collection of the root document.
#
# @example Set the collection.
# context.set_collection
#
# @return [ Collection ] The root collection.
def set_collection
@collection = root.collection if root && !root.embedded?
end
# Sorts the result set if sort options have been set.
#
# @example Sort the documents.
# context.sort(documents)
#
# @return [ Array<Document> ] The sorted documents.
def sort(documents)
return documents if options[:sort].blank?
documents.sort_by do |document|
options[:sort].map do |key, direction|
Sort.new(document.read_attribute(key), direction)
end
end
end
end
end
end