Skip to content

Commit 6f84371

Browse files
authored
MONGOID-5442 implement missing update operators (#5521)
* allow BSON::Timestamp to be demongoized to a Time instance * Add support for $currentDate operation * $min implementation for Contextual::Atomic * Implement $min on documents * $max implementation for Contextual::Atomic * Implement $max operator for documents * $mul for documents * $mul operation for Contextual::Atomic * initial support for $setOnInsert note: this doesn't (yet) work for upserts with `replace: true` * update_min => set_min, with alias as set_upper_bound * update_max -> set_max (with clamp_lower_bound alias) * min -> set_min (with clamp_upper_bound alias) * max -> set_max (with clamp_lower_bound alias) * mul -> set_mul * mul -> set_mul * remove the $currentDate implementation see comment on 5442 for background * remove current_date * guard against empty argument (for earlier mongodb versions)
1 parent fa13bb8 commit 6f84371

File tree

13 files changed

+961
-5
lines changed

13 files changed

+961
-5
lines changed

lib/mongoid/contextual/atomic.rb

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ def inc(incs)
5252
view.update_many("$inc" => collect_operations(incs))
5353
end
5454

55+
# Perform an atomic $mul operation on the matching documents.
56+
#
57+
# @example Perform the atomic multiplication.
58+
# context.set_mul(likes: 10)
59+
#
60+
# @param [ Hash ] factors The operations.
61+
#
62+
# @return [ nil ] Nil.
63+
def set_mul(factors)
64+
view.update_many("$mul" => collect_operations(factors))
65+
end
66+
5567
# Perform an atomic $pop operation on the matching documents.
5668
#
5769
# @example Pop the first value on the matches.
@@ -163,10 +175,62 @@ def unset(*args)
163175
view.update_many("$unset" => Hash[fields])
164176
end
165177

178+
# Performs an atomic $min update operation on the given field or fields.
179+
# Each field will be set to the minimum of [current_value, given value].
180+
# This has the effect of making sure that each field is no
181+
# larger than the given value; in other words, the given value is the
182+
# effective *maximum* for that field.
183+
#
184+
# @note Because of the existence of
185+
# Mongoid::Contextual::Aggregable::Mongo#min, this method cannot be
186+
# named #min, and thus breaks that convention of other similar methods
187+
# of being named for the MongoDB operation they perform.
188+
#
189+
# @example Set "views" to be no more than 100.
190+
# context.set_min(views: 100)
191+
#
192+
# @param [ Hash ] fields The fields with the maximum value that each
193+
# may be set to.
194+
#
195+
# @return [ nil ] Nil.
196+
def set_min(fields)
197+
view.update_many("$min" => collect_operations(fields))
198+
end
199+
alias :clamp_upper_bound :set_min
200+
201+
# Performs an atomic $max update operation on the given field or fields.
202+
# Each field will be set to the maximum of [current_value, given value].
203+
# This has the effect of making sure that each field is no
204+
# smaller than the given value; in other words, the given value is the
205+
# effective *minimum* for that field.
206+
#
207+
# @note Because of the existence of
208+
# Mongoid::Contextual::Aggregable::Mongo#max, this method cannot be
209+
# named #max, and thus breaks that convention of other similar methods
210+
# of being named for the MongoDB operation they perform.
211+
#
212+
# @example Set "views" to be no less than 100.
213+
# context.set_max(views: 100)
214+
#
215+
# @param [ Hash ] fields The fields with the minimum value that each
216+
# may be set to.
217+
#
218+
# @return [ nil ] Nil.
219+
def set_max(fields)
220+
view.update_many("$max" => collect_operations(fields))
221+
end
222+
alias :clamp_lower_bound :set_max
223+
166224
private
167225

168-
def collect_operations(ops)
169-
ops.each_with_object({}) do |(field, value), operations|
226+
# Collects and aggregates operations by field.
227+
#
228+
# @param [ Array | Hash ] ops The operations to collect.
229+
# @param [ Hash ] aggregator The hash to use to aggregate the operations.
230+
#
231+
# @return [ Hash ] The aggregated operations, by field.
232+
def collect_operations(ops, aggregator = {})
233+
ops.each_with_object(aggregator) do |(field, value), operations|
170234
operations[database_field_name(field)] = value.mongoize
171235
end
172236
end
@@ -176,6 +240,10 @@ def collect_each_operations(ops)
176240
operations[database_field_name(field)] = { "$each" => Array.wrap(value).mongoize }
177241
end
178242
end
243+
244+
def translate_date_type(type)
245+
Mongoid::Persistable::Datable.translate_date_field_spec(type)
246+
end
179247
end
180248
end
181249
end

lib/mongoid/extensions/time.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ def demongoize(object)
5858
rescue ArgumentError
5959
nil
6060
end
61+
elsif object.is_a?(BSON::Timestamp)
62+
::Time.at(object.seconds)
6163
end
6264

6365
return if time.nil?

lib/mongoid/persistable.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
require "mongoid/persistable/destroyable"
66
require "mongoid/persistable/incrementable"
77
require "mongoid/persistable/logical"
8+
require "mongoid/persistable/maxable"
9+
require "mongoid/persistable/minable"
10+
require "mongoid/persistable/multipliable"
811
require "mongoid/persistable/poppable"
912
require "mongoid/persistable/pullable"
1013
require "mongoid/persistable/pushable"
@@ -25,6 +28,9 @@ module Persistable
2528
include Destroyable
2629
include Incrementable
2730
include Logical
31+
include Maxable
32+
include Minable
33+
include Multipliable
2834
include Poppable
2935
include Positional
3036
include Pullable

lib/mongoid/persistable/maxable.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Persistable
5+
6+
# Defines behavior for setting a field (or fields) to the larger of
7+
# either it's current value, or a given value.
8+
module Maxable
9+
extend ActiveSupport::Concern
10+
11+
# Set the given field or fields to the larger of either it's current
12+
# value, or a given value.
13+
#
14+
# @example Set a field to be no less than 100.
15+
# document.set_max(field: 100)
16+
#
17+
# @param [ Hash<Symbol | String, Comparable> ] fields The fields to
18+
# set, with corresponding minimum values.
19+
#
20+
# @return [ Document ] The document.
21+
def set_max(fields)
22+
prepare_atomic_operation do |ops|
23+
process_atomic_operations(fields) do |field, value|
24+
current_value = attributes[field]
25+
if value > current_value
26+
process_attribute field, value
27+
ops[atomic_attribute_name(field)] = value
28+
end
29+
end
30+
{ "$max" => ops } unless ops.empty?
31+
end
32+
end
33+
alias :clamp_lower_bound :set_max
34+
end
35+
end
36+
end

lib/mongoid/persistable/minable.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Persistable
5+
6+
# Defines behavior for setting a field (or fields) to the smaller of
7+
# either it's current value, or a given value.
8+
module Minable
9+
extend ActiveSupport::Concern
10+
11+
# Set the given field or fields to the smaller of either it's current
12+
# value, or a given value.
13+
#
14+
# @example Set a field to be no more than 100.
15+
# document.min(field: 100)
16+
#
17+
# @param [ Hash<Symbol | String, Comparable> ] fields The fields to
18+
# set, with corresponding maximum values.
19+
#
20+
# @return [ Document ] The document.
21+
def set_min(fields)
22+
prepare_atomic_operation do |ops|
23+
process_atomic_operations(fields) do |field, value|
24+
current_value = attributes[field]
25+
if value < current_value
26+
process_attribute field, value
27+
ops[atomic_attribute_name(field)] = value
28+
end
29+
end
30+
{ "$min" => ops } unless ops.empty?
31+
end
32+
end
33+
alias :clamp_upper_bound :set_min
34+
end
35+
end
36+
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Persistable
5+
6+
# Defines behavior for $mul operations.
7+
module Multipliable
8+
extend ActiveSupport::Concern
9+
10+
# Multiply the provided fields by the corresponding values. Values can
11+
# be positive or negative, and if no value exists for the field it will
12+
# be set to zero.
13+
#
14+
# @example Multiply the fields.
15+
# document.set_mul(score: 10, place: 1, lives: -10)
16+
#
17+
# @param [ Hash ] factors The field/factor multiplier pairs.
18+
#
19+
# @return [ Document ] The document.
20+
def set_mul(factors)
21+
prepare_atomic_operation do |ops|
22+
process_atomic_operations(factors) do |field, value|
23+
factor = value.__to_inc__
24+
current = attributes[field]
25+
new_value = (current || 0) * factor
26+
process_attribute field, new_value if executing_atomically?
27+
attributes[field] = new_value
28+
ops[atomic_attribute_name(field)] = factor
29+
end
30+
{ "$mul" => ops } unless ops.empty?
31+
end
32+
end
33+
end
34+
end
35+
end

lib/mongoid/persistable/upsertable.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,33 @@ module Upsertable
2020
# @example Upsert the document with replace.
2121
# document.upsert(replace: true)
2222
#
23+
# @example Upsert with extra attributes to use when inserting.
24+
# document.upsert(set_on_insert: { created_at: DateTime.now })
25+
#
2326
# @param [ Hash ] options The validation options.
2427
#
2528
# @option options [ true | false ] :validate Whether or not to validate.
26-
# @option options [ true | false ] :replace Whether or not to replace the document on upsert.
29+
# @option options [ true | false ] :replace Whether or not to replace
30+
# the document on upsert.
31+
# @option options [ Hash ] :set_on_insert The attributes to include if
32+
# the document does not already exist.
2733
#
2834
# @return [ true ] True.
2935
def upsert(options = {})
3036
prepare_upsert(options) do
3137
if options[:replace]
38+
if options[:set_on_insert]
39+
raise ArgumentError, "cannot specify :set_on_insert with `replace: true`"
40+
end
41+
3242
collection.find(atomic_selector).replace_one(
3343
as_attributes, upsert: true, session: _session)
3444
else
45+
attrs = { "$set" => as_attributes }
46+
attrs["$setOnInsert"] = options[:set_on_insert] if options[:set_on_insert]
47+
3548
collection.find(atomic_selector).update_one(
36-
{ "$set" => as_attributes }, upsert: true, session: _session)
49+
attrs, upsert: true, session: _session)
3750
end
3851
end
3952
end

0 commit comments

Comments
 (0)