Skip to content

Commit

Permalink
MONGOID-5654: refactor and deprecate Hash#__consolidate__ (#5740)
Browse files Browse the repository at this point in the history
* refactor and deprecate Hash#__consolidate__

* fix linter complaints

* another minor refactoring for optimization

* remove pre-refactoring docs

* fix failing specs
  • Loading branch information
jamis authored Nov 7, 2023
1 parent 67c8035 commit 5dff6d6
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 121 deletions.
88 changes: 88 additions & 0 deletions lib/mongoid/atomic_update_preparer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true

module Mongoid
# A singleton class to assist with preparing attributes for atomic
# updates.
#
# Once the deprecated Hash#__consolidate__ method is removed entirely,
# these methods may be moved into Mongoid::Contextual::Mongo as private
# methods.
#
# @api private
class AtomicUpdatePreparer
class << self
# Convert the key/values in the attributes into a hash of atomic updates.
# Non-operator keys are assumed to use $set operation.
#
# @param [ Class ] klass The model class.
# @param [ Hash ] attributes The attributes to convert.
#
# @return [ Hash ] The prepared atomic updates.
def prepare(attributes, klass)
attributes.each_pair.with_object({}) do |(key, value), atomic_updates|
key = klass.database_field_name(key.to_s)

if key.to_s.start_with?('$')
(atomic_updates[key] ||= {}).update(prepare_operation(klass, key, value))
else
(atomic_updates['$set'] ||= {})[key] = mongoize_for(key, klass, key, value)
end
end
end

private

# Treats the key as if it were a MongoDB operator and prepares
# the value accordingly.
#
# @param [ Class ] klass the model class
# @param [ String | Symbol ] key the operator
# @param [ Hash ] value the operand
#
# @return [ Hash ] the prepared value.
def prepare_operation(klass, key, value)
value.each_with_object({}) do |(key2, value2), hash|
key2 = klass.database_field_name(key2)
hash[key2] = value_for(key, klass, value2)
end
end

# Get the value for the provided operator, klass, key and value.
#
# This is necessary for special cases like $rename, $addToSet and $push.
#
# @param [ String ] operator The operator.
# @param [ Class ] klass The model class.
# @param [ Object ] value The original value.
#
# @return [ Object ] Value prepared for the provided operator.
def value_for(operator, klass, value)
case operator
when '$rename' then value.to_s
when '$addToSet', '$push' then value.mongoize
else mongoize_for(operator, klass, operator, value)
end
end

# Mongoize for the klass, key and value.
#
# @param [ String ] operator The operator.
# @param [ Class ] klass The model class.
# @param [ String | Symbol ] key The field key.
# @param [ Object ] value The value to mongoize.
#
# @return [ Object ] The mongoized value.
def mongoize_for(operator, klass, key, value)
field = klass.fields[key.to_s]
return value unless field

mongoized = field.mongoize(value)
if Mongoid::Persistable::LIST_OPERATIONS.include?(operator) && field.resizable? && !value.is_a?(Array)
return mongoized.first
end

mongoized
end
end
end
end
5 changes: 3 additions & 2 deletions lib/mongoid/contextual/mongo.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true
# rubocop:todo all

require 'mongoid/atomic_update_preparer'
require "mongoid/contextual/mongo/documents_loader"
require "mongoid/contextual/atomic"
require "mongoid/contextual/aggregable/mongo"
Expand Down Expand Up @@ -817,8 +818,8 @@ def load_async
# @return [ true | false ] If the update succeeded.
def update_documents(attributes, method = :update_one, opts = {})
return false unless attributes
attributes = Hash[attributes.map { |k, v| [klass.database_field_name(k.to_s), v] }]
view.send(method, attributes.__consolidate__(klass), opts)

view.send(method, AtomicUpdatePreparer.prepare(attributes, klass), opts)
end

# Apply the field limitations.
Expand Down
69 changes: 7 additions & 62 deletions lib/mongoid/extensions/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,20 @@ def __mongoize_object_id__
end

# Consolidate the key/values in the hash under an atomic $set.
# DEPRECATED. This was never intended to be a public API and
# the functionality will no longer be exposed once this method
# is eventually removed.
#
# @example Consolidate the hash.
# { name: "Placebo" }.__consolidate__
#
# @return [ Hash ] A new consolidated hash.
#
# @deprecated
def __consolidate__(klass)
consolidated = {}
each_pair do |key, value|
if key =~ /\$/
value.keys.each do |key2|
value2 = value[key2]
real_key = klass.database_field_name(key2)

value.delete(key2) if real_key != key2
value[real_key] = value_for(key, klass, real_key, value2)
end
consolidated[key] ||= {}
consolidated[key].update(value)
else
consolidated["$set"] ||= {}
consolidated["$set"].update(key => mongoize_for(key, klass, key, value))
end
end
consolidated
Mongoid::AtomicUpdatePreparer.prepare(self, klass)
end
Mongoid.deprecate(self, :__consolidate__)

# Checks whether conditions given in this hash are known to be
# unsatisfiable, i.e., querying with this hash will always return no
Expand Down Expand Up @@ -166,50 +155,6 @@ def to_criteria

private

# Get the value for the provided operator, klass, key and value.
#
# This is necessary for special cases like $rename, $addToSet and $push.
#
# @param [ String ] operator The operator.
# @param [ Class ] klass The model class.
# @param [ String | Symbol ] key The field key.
# @param [ Object ] value The original value.
#
# @return [ Object ] Value prepared for the provided operator.
def value_for(operator, klass, key, value)
case operator
when "$rename" then value.to_s
when "$addToSet", "$push" then value.mongoize
else mongoize_for(operator, klass, operator, value)
end
end

# Mongoize for the klass, key and value.
#
# @api private
#
# @example Mongoize for the klass, field and value.
# {}.mongoize_for("$push", Band, "name", "test")
#
# @param [ String ] operator The operator.
# @param [ Class ] klass The model class.
# @param [ String | Symbol ] key The field key.
# @param [ Object ] value The value to mongoize.
#
# @return [ Object ] The mongoized value.
def mongoize_for(operator, klass, key, value)
field = klass.fields[key.to_s]
if field
val = field.mongoize(value)
if Mongoid::Persistable::LIST_OPERATIONS.include?(operator) && field.resizable?
val = val.first if !value.is_a?(Array)
end
val
else
value
end
end

module ClassMethods

# Turn the object from the ruby type we deal with to a Mongo friendly
Expand Down
83 changes: 83 additions & 0 deletions spec/mongoid/atomic_update_preparer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# frozen_string_literal: true

require 'spec_helper'

describe Mongoid::AtomicUpdatePreparer do
describe '#prepare' do
let(:prepared) { described_class.prepare(hash, Band) }

context 'when the hash already contains $set' do
context 'when the $set is first' do
let(:hash) do
{ '$set' => { name: 'Tool' }, likes: 10, '$inc' => { plays: 1 } }
end

it 'moves the non hash values under the provided key' do
expect(prepared).to eq(
'$set' => { 'name' => 'Tool', 'likes' => 10 },
'$inc' => { 'plays' => 1 }
)
end
end

context 'when the $set is not first' do
let(:hash) do
{ likes: 10, '$inc' => { plays: 1 }, '$set' => { name: 'Tool' } }
end

it 'moves the non hash values under the provided key' do
expect(prepared).to eq(
'$set' => { 'likes' => 10, 'name' => 'Tool' },
'$inc' => { 'plays' => 1 }
)
end
end
end

context 'when the hash does not contain $set' do
let(:hash) do
{ likes: 10, '$inc' => { plays: 1 }, name: 'Tool' }
end

it 'moves the non hash values under the provided key' do
expect(prepared).to eq(
'$set' => { 'likes' => 10, 'name' => 'Tool' },
'$inc' => { 'plays' => 1 }
)
end
end

context 'when the hash contains $rename' do
let(:hash) { { likes: 10, '$rename' => { old: 'new' } } }

it 'preserves the $rename operator' do
expect(prepared).to eq(
'$set' => { 'likes' => 10 },
'$rename' => { 'old' => 'new' }
)
end
end

context 'when the hash contains $addToSet' do
let(:hash) { { likes: 10, '$addToSet' => { list: 'new' } } }

it 'preserves the $addToSet operator' do
expect(prepared).to eq(
'$set' => { 'likes' => 10 },
'$addToSet' => { 'list' => 'new' }
)
end
end

context 'when the hash contains $push' do
let(:hash) { { likes: 10, '$push' => { list: 14 } } }

it 'preserves the $push operator' do
expect(prepared).to eq(
'$set' => { 'likes' => 10 },
'$push' => { 'list' => 14 }
)
end
end
end
end
57 changes: 0 additions & 57 deletions spec/mongoid/extensions/hash_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,63 +163,6 @@
end
end

describe "#__consolidate__" do

context "when the hash already contains the key" do

context "when the $set is first" do

let(:hash) do
{ "$set" => { name: "Tool" }, likes: 10, "$inc" => { plays: 1 }}
end

let(:consolidated) do
hash.__consolidate__(Band)
end

it "moves the non hash values under the provided key" do
expect(consolidated).to eq({
"$set" => { 'name' => "Tool", likes: 10 }, "$inc" => { 'plays' => 1 }
})
end
end

context "when the $set is not first" do

let(:hash) do
{ likes: 10, "$inc" => { plays: 1 }, "$set" => { name: "Tool" }}
end

let(:consolidated) do
hash.__consolidate__(Band)
end

it "moves the non hash values under the provided key" do
expect(consolidated).to eq({
"$set" => { likes: 10, 'name' => "Tool" }, "$inc" => { 'plays' => 1 }
})
end
end
end

context "when the hash does not contain the key" do

let(:hash) do
{ likes: 10, "$inc" => { plays: 1 }, name: "Tool"}
end

let(:consolidated) do
hash.__consolidate__(Band)
end

it "moves the non hash values under the provided key" do
expect(consolidated).to eq({
"$set" => { likes: 10, name: "Tool" }, "$inc" => { 'plays' => 1 }
})
end
end
end

describe ".demongoize" do

let(:hash) do
Expand Down

0 comments on commit 5dff6d6

Please sign in to comment.