forked from mongodb/mongoid
/
atomic.rb
292 lines (267 loc) · 8.04 KB
/
atomic.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
# encoding: utf-8
require "mongoid/atomic/modifiers"
require "mongoid/atomic/paths"
module Mongoid
# This module contains the logic for supporting atomic operations against the
# database.
module Atomic
extend ActiveSupport::Concern
UPDATES = [
:atomic_array_pushes,
:atomic_array_pulls,
:atomic_array_add_to_sets,
:atomic_pulls,
:atomic_unsets,
:delayed_atomic_sets,
:delayed_atomic_pulls
]
included do
# When MongoDB finally fully implements the positional operator, we can
# get rid of all indexing related code in Mongoid.
attr_accessor :_index
end
# Add the document as an atomic pull.
#
# @example Add the atomic pull.
# person.add_atomic_pull(address)
#
# @param [ Document ] The embedded document to pull.
#
# @since 2.2.0
def add_atomic_pull(document)
document.flagged_for_destroy = true
(delayed_atomic_pulls[document.metadata.name.to_s] ||= []).push(document)
end
# For array fields these are the pushes that need to happen.
#
# @example Get the array pushes.
# person.atomic_array_pushes
#
# @return [ Hash ] The array pushes.
#
# @since 2.4.0
def atomic_array_pushes
@atomic_array_pushes ||= {}
end
# For array fields these are the pulls that need to happen.
#
# @example Get the array pulls.
# person.atomic_array_pulls
#
# @return [ Hash ] The array pulls.
#
# @since 2.4.0
def atomic_array_pulls
@atomic_array_pulls ||= {}
end
# For array fields these are the unique adds that need to happen.
#
# @example Get the array unique adds.
# person.atomic_array_add_to_sets
#
# @return [ Hash ] The array add_to_sets.
#
# @since 2.4.0
def atomic_array_add_to_sets
@atomic_array_add_to_sets ||= {}
end
# Get all the atomic updates that need to happen for the current
# +Document+. This includes all changes that need to happen in the
# entire hierarchy that exists below where the save call was made.
#
# @note MongoDB does not allow "conflicting modifications" to be
# performed in a single operation. Conflicting modifications are
# detected by the 'haveConflictingMod' function in MongoDB.
# Examination of the code suggests that two modifications (a $set
# and a $pushAll, for example) conflict if:
# (1) the key paths being modified are equal.
# (2) one key path is a prefix of the other.
# So a $set of 'addresses.0.street' will conflict with a $pushAll
# to 'addresses', and we will need to split our update into two
# pieces. We do not, however, attempt to match MongoDB's logic
# exactly. Instead, we assume that two updates conflict if the
# first component of the two key paths matches.
#
# @example Get the updates that need to occur.
# person.atomic_updates(children)
#
# @return [ Hash ] The updates and their modifiers.
#
# @since 2.1.0
def atomic_updates
mods = Modifiers.new
generate_atomic_updates(mods, self)
_children.each do |child|
generate_atomic_updates(mods, child)
end
mods
end
alias :_updates :atomic_updates
# Get the removal modifier for the document. Will be nil on root
# documents, $unset on embeds_one, $set on embeds_many.
#
# @example Get the removal operator.
# name.atomic_delete_modifier
#
# @return [ String ] The pull or unset operation.
def atomic_delete_modifier
atomic_paths.delete_modifier
end
# Get the insertion modifier for the document. Will be nil on root
# documents, $set on embeds_one, $push on embeds_many.
#
# @example Get the insert operation.
# name.atomic_insert_modifier
#
# @return [ String ] The pull or set operator.
def atomic_insert_modifier
atomic_paths.insert_modifier
end
# Return the path to this +Document+ in JSON notation, used for atomic
# updates via $set in MongoDB.
#
# @example Get the path to this document.
# address.atomic_path
#
# @return [ String ] The path to the document in the database.
def atomic_path
atomic_paths.path
end
# Returns the positional operator of this document for modification.
#
# @example Get the positional operator.
# address.atomic_position
#
# @return [ String ] The positional operator with indexes.
def atomic_position
atomic_paths.position
end
# Returns path of the attribute for modification
#
# @example Get path of the attribute
# address.atomic_attribute_name(:city)
#
# @return [ String ] The path to the document attribute in the database
def atomic_attribute_name(name)
embedded? ? "#{atomic_position}.#{name}" : name
end
# Get all the attributes that need to be pulled.
#
# @example Get the pulls.
# person.atomic_pulls
#
# @return [ Array<Hash> ] The $pullAll operations.
#
# @since 2.2.0
def atomic_pulls
pulls = {}
delayed_atomic_pulls.each_pair do |_, docs|
path = nil
ids = docs.map do |doc|
path ||= doc.atomic_path
doc.destroyed = true
doc.flagged_for_destroy = false
doc.id
end
pulls[path] = { "_id" => { "$in" => ids }} and path = nil
end
pulls
end
# Get all the push attributes that need to occur.
#
# @example Get the pushes.
# person.atomic_pushes
#
# @return [ Hash ] The $pushAll operations.
#
# @since 2.1.0
def atomic_pushes
pushable? ? { atomic_path => as_document } : {}
end
# Return the selector for this document to be matched exactly for use
# with MongoDB's $ operator.
#
# @example Get the selector.
# address.atomic_selector
#
# @return [ String ] The exact selector for this document.
def atomic_selector
atomic_paths.selector
end
# Get all the attributes that need to be set.
#
# @example Get the sets.
# person.atomic_sets
#
# @return [ Hash ] The $set operations.
#
# @since 2.1.0
def atomic_sets
updateable? ? setters : settable? ? { atomic_path => as_document } : {}
end
# Get all the attributes that need to be unset.
#
# @example Get the unsets.
# person.atomic_unsets
#
# @return [ Array<Hash> ] The $unset operations.
#
# @since 2.2.0
def atomic_unsets
@atomic_unsets ||= []
end
# Get all the atomic sets that have had their saves delayed.
#
# @example Get the delayed atomic sets.
# person.delayed_atomic_sets
#
# @return [ Hash ] The delayed $sets.
#
# @since 2.3.0
def delayed_atomic_sets
@delayed_atomic_sets ||= {}
end
# Get a hash of atomic pulls that are pending.
#
# @example Get the atomic pulls.
# document.delayed_atomic_pulls
#
# @return [ Hash ] name/document pairs.
#
# @since 2.3.2
def delayed_atomic_pulls
@delayed_atomic_pulls ||= {}
end
private
# Get the atomic paths utility for this document.
#
# @example Get the atomic paths.
# document.atomic_paths
#
# @return [ Object ] The associated path.
#
# @since 2.1.0
def atomic_paths
@atomic_paths ||= metadata ? metadata.path(self) : Atomic::Paths::Root.new(self)
end
# Generates the atomic updates in the correct order.
#
# @example Generate the updates.
# model.generate_atomic_updates(mods, doc)
#
# @param [ Modifiers ] mods The atomic modifications.
# @param [ Document ] doc The document to update for.
#
# @since 2.2.0
def generate_atomic_updates(mods, doc)
mods.unset(doc.atomic_unsets)
mods.pull(doc.atomic_pulls)
mods.set(doc.atomic_sets)
mods.set(doc.delayed_atomic_sets)
mods.push(doc.atomic_pushes)
mods.push(doc.atomic_array_pushes)
mods.add_to_set(doc.atomic_array_add_to_sets)
mods.pull_all(doc.atomic_array_pulls)
end
end
end