ocean-dynamo 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f06f0f651adf9199dd190cedd203390528548a1f
4
- data.tar.gz: 1082719f92e830ef6e60658b77048719cfaed6b1
3
+ metadata.gz: 050bb409c80700917429d0826f6e7f2f57665ff8
4
+ data.tar.gz: 04dfbcbcf7a927b5616ae070f56ec7d536550689
5
5
  SHA512:
6
- metadata.gz: 67d5231c8cef81f6b566c4e9a1f630c32dc510289a5fe3d27a05cb2a83a4bf5f31db1df13b16a6369e7482351dbbf87470d4bb8fa859710eae6290d050701ec5
7
- data.tar.gz: b529ee10710b85465e3b82668268ed5c78226b83a2a3619f54c213492ebf4d8aa334b0344fefb960548db84e536dd92e8f614da51592a7849405e0f758402ccc
6
+ metadata.gz: 0158fc7b07d5f594a88efd0499fe0466233a6de48fbc120e760968003c26b4a8c337e81110defb98cf66dd8d9093510056e2b7313696d7ec9035d19c598a50e5
7
+ data.tar.gz: 1705bacb9d7df7102790ce612b5fc6753b30965094e5ede5300b241251e577e11c01a09f1d509bfd570463feaafb311c50d82b015a6db981be80fe10075b7408
@@ -170,8 +170,8 @@ controllers. OceanDynamo implements much of the infrastructure of ActiveRecord;
170
170
  for instance, +read_attribute+, +write_attribute+, and much of the control logic and
171
171
  internal organisation.
172
172
 
173
- * +has_many+ can now be nested in multiple layers through the use of +:composite_key+
174
- in +belongs_to+. See the documentation for +belongs_to+.
173
+ * <tt>belongs_to :thingy now defines <tt>.build_thingy</tt> and <tt>.create_thingy</tt>.
174
+ * Work begun on collection proxies, etc.
175
175
 
176
176
  === Future milestones
177
177
 
@@ -14,6 +14,9 @@ require "ocean-dynamo/attributes"
14
14
  require "ocean-dynamo/persistence"
15
15
  require "ocean-dynamo/queries"
16
16
  require "ocean-dynamo/associations/associations"
17
+ require "ocean-dynamo/associations/association"
18
+ require "ocean-dynamo/associations/collection_association"
19
+ require "ocean-dynamo/associations/relation"
17
20
  require "ocean-dynamo/associations/belongs_to"
18
21
  require "ocean-dynamo/associations/has_many"
19
22
 
@@ -0,0 +1,244 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module ActiveRecord #:nodoc:
4
+ module Associations #:nodoc:
5
+ # = Active Record Associations
6
+ #
7
+ # This is the root class of all associations ('+ Foo' signifies an included module Foo):
8
+ #
9
+ # Association
10
+ # SingularAssociation
11
+ # HasOneAssociation
12
+ # HasOneThroughAssociation + ThroughAssociation
13
+ # BelongsToAssociation
14
+ # BelongsToPolymorphicAssociation
15
+ # CollectionAssociation
16
+ # HasAndBelongsToManyAssociation
17
+ # HasManyAssociation
18
+ # HasManyThroughAssociation + ThroughAssociation
19
+ class Association #:nodoc:
20
+ attr_reader :owner, :target, :reflection
21
+
22
+ delegate :options, :to => :reflection
23
+
24
+ def initialize(owner, reflection)
25
+ reflection.check_validity!
26
+
27
+ @owner, @reflection = owner, reflection
28
+
29
+ reset
30
+ reset_scope
31
+ end
32
+
33
+ # Returns the name of the table of the associated class:
34
+ #
35
+ # post.comments.aliased_table_name # => "comments"
36
+ #
37
+ def aliased_table_name
38
+ klass.table_name
39
+ end
40
+
41
+ # Resets the \loaded flag to +false+ and sets the \target to +nil+.
42
+ def reset
43
+ @loaded = false
44
+ @target = nil
45
+ @stale_state = nil
46
+ end
47
+
48
+ # Reloads the \target and returns +self+ on success.
49
+ def reload
50
+ reset
51
+ reset_scope
52
+ load_target
53
+ self unless target.nil?
54
+ end
55
+
56
+ # Has the \target been already \loaded?
57
+ def loaded?
58
+ @loaded
59
+ end
60
+
61
+ # Asserts the \target has been loaded setting the \loaded flag to +true+.
62
+ def loaded!
63
+ @loaded = true
64
+ @stale_state = stale_state
65
+ end
66
+
67
+ # The target is stale if the target no longer points to the record(s) that the
68
+ # relevant foreign_key(s) refers to. If stale, the association accessor method
69
+ # on the owner will reload the target. It's up to subclasses to implement the
70
+ # stale_state method if relevant.
71
+ #
72
+ # Note that if the target has not been loaded, it is not considered stale.
73
+ def stale_target?
74
+ loaded? && @stale_state != stale_state
75
+ end
76
+
77
+ # Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
78
+ def target=(target)
79
+ @target = target
80
+ loaded!
81
+ end
82
+
83
+ def scope
84
+ target_scope.merge(association_scope)
85
+ end
86
+
87
+ def scoped
88
+ ActiveSupport::Deprecation.warn "#scoped is deprecated. use #scope instead."
89
+ scope
90
+ end
91
+
92
+ # The scope for this association.
93
+ #
94
+ # Note that the association_scope is merged into the target_scope only when the
95
+ # scope method is called. This is because at that point the call may be surrounded
96
+ # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
97
+ # actually gets built.
98
+ def association_scope
99
+ if klass
100
+ @association_scope ||= AssociationScope.new(self).scope
101
+ end
102
+ end
103
+
104
+ def reset_scope
105
+ @association_scope = nil
106
+ end
107
+
108
+ # Set the inverse association, if possible
109
+ def set_inverse_instance(record)
110
+ if record && invertible_for?(record)
111
+ inverse = record.association(inverse_reflection_for(record).name)
112
+ inverse.target = owner
113
+ end
114
+ end
115
+
116
+ # Returns the class of the target. belongs_to polymorphic overrides this to look at the
117
+ # polymorphic_type field on the owner.
118
+ def klass
119
+ reflection.klass
120
+ end
121
+
122
+ # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
123
+ # through association's scope)
124
+ def target_scope
125
+ klass.all
126
+ end
127
+
128
+ # Loads the \target if needed and returns it.
129
+ #
130
+ # This method is abstract in the sense that it relies on +find_target+,
131
+ # which is expected to be provided by descendants.
132
+ #
133
+ # If the \target is already \loaded it is just returned. Thus, you can call
134
+ # +load_target+ unconditionally to get the \target.
135
+ #
136
+ # ActiveRecord::RecordNotFound is rescued within the method, and it is
137
+ # not reraised. The proxy is \reset and +nil+ is the return value.
138
+ def load_target
139
+ @target = find_target if (@stale_state && stale_target?) || find_target?
140
+
141
+ loaded! unless loaded?
142
+ target
143
+ rescue ActiveRecord::RecordNotFound
144
+ reset
145
+ end
146
+
147
+ def interpolate(sql, record = nil)
148
+ if sql.respond_to?(:to_proc)
149
+ owner.instance_exec(record, &sql)
150
+ else
151
+ sql
152
+ end
153
+ end
154
+
155
+ # We can't dump @reflection since it contains the scope proc
156
+ def marshal_dump
157
+ ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] }
158
+ [@reflection.name, ivars]
159
+ end
160
+
161
+ def marshal_load(data)
162
+ reflection_name, ivars = data
163
+ ivars.each { |name, val| instance_variable_set(name, val) }
164
+ @reflection = @owner.class.reflect_on_association(reflection_name)
165
+ end
166
+
167
+ private
168
+
169
+ def find_target?
170
+ !loaded? && (!owner.new_record? || foreign_key_present?) && klass
171
+ end
172
+
173
+ def creation_attributes
174
+ attributes = {}
175
+
176
+ if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through]
177
+ attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
178
+
179
+ if reflection.options[:as]
180
+ attributes[reflection.type] = owner.class.base_class.name
181
+ end
182
+ end
183
+
184
+ attributes
185
+ end
186
+
187
+ # Sets the owner attributes on the given record
188
+ def set_owner_attributes(record)
189
+ creation_attributes.each { |key, value| record[key] = value }
190
+ end
191
+
192
+ # Should be true if there is a foreign key present on the owner which
193
+ # references the target. This is used to determine whether we can load
194
+ # the target if the owner is currently a new record (and therefore
195
+ # without a key).
196
+ #
197
+ # Currently implemented by belongs_to (vanilla and polymorphic) and
198
+ # has_one/has_many :through associations which go through a belongs_to
199
+ def foreign_key_present?
200
+ false
201
+ end
202
+
203
+ # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
204
+ # the kind of the class of the associated objects. Meant to be used as
205
+ # a sanity check when you are about to assign an associated record.
206
+ def raise_on_type_mismatch!(record)
207
+ unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize)
208
+ message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
209
+ raise ActiveRecord::AssociationTypeMismatch, message
210
+ end
211
+ end
212
+
213
+ # Can be redefined by subclasses, notably polymorphic belongs_to
214
+ # The record parameter is necessary to support polymorphic inverses as we must check for
215
+ # the association in the specific class of the record.
216
+ def inverse_reflection_for(record)
217
+ reflection.inverse_of
218
+ end
219
+
220
+ # Returns true if inverse association on the given record needs to be set.
221
+ # This method is redefined by subclasses.
222
+ def invertible_for?(record)
223
+ inverse_reflection_for(record)
224
+ end
225
+
226
+ # This should be implemented to return the values of the relevant key(s) on the owner,
227
+ # so that when stale_state is different from the value stored on the last find_target,
228
+ # the target is stale.
229
+ #
230
+ # This is only relevant to certain associations, which is why it returns nil by default.
231
+ def stale_state
232
+ end
233
+
234
+ def build_record(attributes)
235
+ reflection.build_association(attributes) do |record|
236
+ skip_assign = [reflection.foreign_key, reflection.type].compact
237
+ attributes = create_scope.except(*(record.changed - skip_assign))
238
+ record.assign_attributes(attributes)
239
+ set_inverse_instance(record)
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,580 @@
1
+ module ActiveRecord #:nodoc:
2
+ module Associations #:nodoc:
3
+ # = Active Record Association Collection
4
+ #
5
+ # CollectionAssociation is an abstract class that provides common stuff to
6
+ # ease the implementation of association proxies that represent
7
+ # collections. See the class hierarchy in AssociationProxy.
8
+ #
9
+ # CollectionAssociation:
10
+ # HasAndBelongsToManyAssociation => has_and_belongs_to_many
11
+ # HasManyAssociation => has_many
12
+ # HasManyThroughAssociation + ThroughAssociation => has_many :through
13
+ #
14
+ # CollectionAssociation class provides common methods to the collections
15
+ # defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with
16
+ # +:through association+ option.
17
+ #
18
+ # You need to be careful with assumptions regarding the target: The proxy
19
+ # does not fetch records from the database until it needs them, but new
20
+ # ones created with +build+ are added to the target. So, the target may be
21
+ # non-empty and still lack children waiting to be read from the database.
22
+ # If you look directly to the database you cannot assume that's the entire
23
+ # collection because new records may have been added to the target, etc.
24
+ #
25
+ # If you need to work on all current children, new and existing records,
26
+ # +load_target+ and the +loaded+ flag are your friends.
27
+ class CollectionAssociation < Association #:nodoc:
28
+
29
+ # Implements the reader method, e.g. foo.items for Foo.has_many :items
30
+ def reader(force_reload = false)
31
+ if force_reload
32
+ klass.uncached { reload }
33
+ elsif stale_target?
34
+ reload
35
+ end
36
+
37
+ @proxy ||= CollectionProxy.create(klass, self)
38
+ end
39
+
40
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
41
+ def writer(records)
42
+ replace(records)
43
+ end
44
+
45
+ # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
46
+ def ids_reader
47
+ if loaded?
48
+ load_target.map do |record|
49
+ record.send(reflection.association_primary_key)
50
+ end
51
+ else
52
+ column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
53
+ scope.pluck(column)
54
+ end
55
+ end
56
+
57
+ # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
58
+ def ids_writer(ids)
59
+ pk_column = reflection.primary_key_column
60
+ ids = Array(ids).reject { |id| id.blank? }
61
+ ids.map! { |i| pk_column.type_cast(i) }
62
+ replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
63
+ end
64
+
65
+ def reset
66
+ super
67
+ @target = []
68
+ end
69
+
70
+ def select(select = nil)
71
+ if block_given?
72
+ load_target.select.each { |e| yield e }
73
+ else
74
+ scope.select(select)
75
+ end
76
+ end
77
+
78
+ def find(*args)
79
+ if block_given?
80
+ load_target.find(*args) { |*block_args| yield(*block_args) }
81
+ else
82
+ if options[:inverse_of] && loaded?
83
+ args = args.flatten
84
+ raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args.blank?
85
+
86
+ result = find_by_scan(*args)
87
+
88
+ result_size = Array(result).size
89
+ if !result || result_size != args.size
90
+ scope.raise_record_not_found_exception!(args, result_size, args.size)
91
+ else
92
+ result
93
+ end
94
+ else
95
+ scope.find(*args)
96
+ end
97
+ end
98
+ end
99
+
100
+ def first(*args)
101
+ first_or_last(:first, *args)
102
+ end
103
+
104
+ def last(*args)
105
+ first_or_last(:last, *args)
106
+ end
107
+
108
+ def build(attributes = {}, &block)
109
+ if attributes.is_a?(Array)
110
+ attributes.collect { |attr| build(attr, &block) }
111
+ else
112
+ add_to_target(build_record(attributes)) do |record|
113
+ yield(record) if block_given?
114
+ end
115
+ end
116
+ end
117
+
118
+ def create(attributes = {}, &block)
119
+ create_record(attributes, &block)
120
+ end
121
+
122
+ def create!(attributes = {}, &block)
123
+ create_record(attributes, true, &block)
124
+ end
125
+
126
+ # Add +records+ to this association. Returns +self+ so method calls may
127
+ # be chained. Since << flattens its argument list and inserts each record,
128
+ # +push+ and +concat+ behave identically.
129
+ def concat(*records)
130
+ load_target if owner.new_record?
131
+
132
+ if owner.new_record?
133
+ concat_records(records)
134
+ else
135
+ transaction { concat_records(records) }
136
+ end
137
+ end
138
+
139
+ # Starts a transaction in the association class's database connection.
140
+ #
141
+ # class Author < ActiveRecord::Base
142
+ # has_many :books
143
+ # end
144
+ #
145
+ # Author.first.books.transaction do
146
+ # # same effect as calling Book.transaction
147
+ # end
148
+ def transaction(*args)
149
+ reflection.klass.transaction(*args) do
150
+ yield
151
+ end
152
+ end
153
+
154
+ # Removes all records from the association without calling callbacks
155
+ # on the associated records. It honors the `:dependent` option. However
156
+ # if the `:dependent` value is `:destroy` then in that case the default
157
+ # deletion strategy for the association is applied.
158
+ #
159
+ # You can force a particular deletion strategy by passing a parameter.
160
+ #
161
+ # Example:
162
+ #
163
+ # @author.books.delete_all(:nullify)
164
+ # @author.books.delete_all(:delete_all)
165
+ #
166
+ # See delete for more info.
167
+ def delete_all(dependent = nil)
168
+ if dependent.present? && ![:nullify, :delete_all].include?(dependent)
169
+ raise ArgumentError, "Valid values are :nullify or :delete_all"
170
+ end
171
+
172
+ dependent = if dependent.present?
173
+ dependent
174
+ elsif options[:dependent] == :destroy
175
+ # since delete_all should not invoke callbacks so use the default deletion strategy
176
+ # for :destroy
177
+ reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) ? :delete_all : :nullify
178
+ else
179
+ options[:dependent]
180
+ end
181
+
182
+ delete(:all, dependent: dependent).tap do
183
+ reset
184
+ loaded!
185
+ end
186
+ end
187
+
188
+ # Destroy all the records from this association.
189
+ #
190
+ # See destroy for more info.
191
+ def destroy_all
192
+ destroy(load_target).tap do
193
+ reset
194
+ loaded!
195
+ end
196
+ end
197
+
198
+ # Count all records using SQL. Construct options and pass them with
199
+ # scope to the target class's +count+.
200
+ def count(column_name = nil, count_options = {})
201
+ column_name, count_options = nil, column_name if column_name.is_a?(Hash)
202
+
203
+ relation = scope
204
+ if association_scope.distinct_value
205
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
206
+ column_name ||= reflection.klass.primary_key
207
+ relation = relation.distinct
208
+ end
209
+
210
+ value = relation.count(column_name)
211
+
212
+ limit = options[:limit]
213
+ offset = options[:offset]
214
+
215
+ if limit || offset
216
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
217
+ else
218
+ value
219
+ end
220
+ end
221
+
222
+ # Removes +records+ from this association calling +before_remove+ and
223
+ # +after_remove+ callbacks.
224
+ #
225
+ # This method is abstract in the sense that +delete_records+ has to be
226
+ # provided by descendants. Note this method does not imply the records
227
+ # are actually removed from the database, that depends precisely on
228
+ # +delete_records+. They are in any case removed from the collection.
229
+ def delete(*records)
230
+ _options = records.extract_options!
231
+ dependent = _options[:dependent] || options[:dependent]
232
+
233
+ if records.first == :all
234
+ if loaded? || dependent == :destroy
235
+ delete_or_destroy(load_target, dependent)
236
+ else
237
+ delete_records(:all, dependent)
238
+ end
239
+ else
240
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
241
+ delete_or_destroy(records, dependent)
242
+ end
243
+ end
244
+
245
+ # Deletes the +records+ and removes them from this association calling
246
+ # +before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
247
+ #
248
+ # Note that this method removes records from the database ignoring the
249
+ # +:dependent+ option.
250
+ def destroy(*records)
251
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
252
+ delete_or_destroy(records, :destroy)
253
+ end
254
+
255
+ # Returns the size of the collection by executing a SELECT COUNT(*)
256
+ # query if the collection hasn't been loaded, and calling
257
+ # <tt>collection.size</tt> if it has.
258
+ #
259
+ # If the collection has been already loaded +size+ and +length+ are
260
+ # equivalent. If not and you are going to need the records anyway
261
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
262
+ #
263
+ # This method is abstract in the sense that it relies on
264
+ # +count_records+, which is a method descendants have to provide.
265
+ def size
266
+ if !find_target? || loaded?
267
+ if association_scope.distinct_value
268
+ target.uniq.size
269
+ else
270
+ target.size
271
+ end
272
+ elsif !loaded? && !association_scope.group_values.empty?
273
+ load_target.size
274
+ elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array)
275
+ unsaved_records = target.select { |r| r.new_record? }
276
+ unsaved_records.size + count_records
277
+ else
278
+ count_records
279
+ end
280
+ end
281
+
282
+ # Returns the size of the collection calling +size+ on the target.
283
+ #
284
+ # If the collection has been already loaded +length+ and +size+ are
285
+ # equivalent. If not and you are going to need the records anyway this
286
+ # method will take one less query. Otherwise +size+ is more efficient.
287
+ def length
288
+ load_target.size
289
+ end
290
+
291
+ # Returns true if the collection is empty.
292
+ #
293
+ # If the collection has been loaded
294
+ # it is equivalent to <tt>collection.size.zero?</tt>. If the
295
+ # collection has not been loaded, it is equivalent to
296
+ # <tt>collection.exists?</tt>. If the collection has not already been
297
+ # loaded and you are going to fetch the records anyway it is better to
298
+ # check <tt>collection.length.zero?</tt>.
299
+ def empty?
300
+ if loaded?
301
+ size.zero?
302
+ else
303
+ @target.blank? && !scope.exists?
304
+ end
305
+ end
306
+
307
+ # Returns true if the collections is not empty.
308
+ # Equivalent to +!collection.empty?+.
309
+ def any?
310
+ if block_given?
311
+ load_target.any? { |*block_args| yield(*block_args) }
312
+ else
313
+ !empty?
314
+ end
315
+ end
316
+
317
+ # Returns true if the collection has more than 1 record.
318
+ # Equivalent to +collection.size > 1+.
319
+ def many?
320
+ if block_given?
321
+ load_target.many? { |*block_args| yield(*block_args) }
322
+ else
323
+ size > 1
324
+ end
325
+ end
326
+
327
+ def distinct
328
+ seen = {}
329
+ load_target.find_all do |record|
330
+ seen[record.id] = true unless seen.key?(record.id)
331
+ end
332
+ end
333
+ alias uniq distinct
334
+
335
+ # Replace this collection with +other_array+. This will perform a diff
336
+ # and delete/add only records that have changed.
337
+ def replace(other_array)
338
+ other_array.each { |val| raise_on_type_mismatch!(val) }
339
+ original_target = load_target.dup
340
+
341
+ if owner.new_record?
342
+ replace_records(other_array, original_target)
343
+ else
344
+ transaction { replace_records(other_array, original_target) }
345
+ end
346
+ end
347
+
348
+ def include?(record)
349
+ if record.is_a?(reflection.klass)
350
+ if record.new_record?
351
+ include_in_memory?(record)
352
+ else
353
+ loaded? ? target.include?(record) : scope.exists?(record)
354
+ end
355
+ else
356
+ false
357
+ end
358
+ end
359
+
360
+ def load_target
361
+ if find_target?
362
+ @target = merge_target_lists(find_target, target)
363
+ end
364
+
365
+ loaded!
366
+ target
367
+ end
368
+
369
+ def add_to_target(record, skip_callbacks = false)
370
+ callback(:before_add, record) unless skip_callbacks
371
+ yield(record) if block_given?
372
+
373
+ if association_scope.distinct_value && index = @target.index(record)
374
+ @target[index] = record
375
+ else
376
+ @target << record
377
+ end
378
+
379
+ callback(:after_add, record) unless skip_callbacks
380
+ set_inverse_instance(record)
381
+
382
+ record
383
+ end
384
+
385
+ def scope(opts = {})
386
+ scope = super()
387
+ scope.none! if opts.fetch(:nullify, true) && null_scope?
388
+ scope
389
+ end
390
+
391
+ def null_scope?
392
+ owner.new_record? && !foreign_key_present?
393
+ end
394
+
395
+ private
396
+
397
+ def find_target
398
+ records = scope.to_a
399
+ records.each { |record| set_inverse_instance(record) }
400
+ records
401
+ end
402
+
403
+ # We have some records loaded from the database (persisted) and some that are
404
+ # in-memory (memory). The same record may be represented in the persisted array
405
+ # and in the memory array.
406
+ #
407
+ # So the task of this method is to merge them according to the following rules:
408
+ #
409
+ # * The final array must not have duplicates
410
+ # * The order of the persisted array is to be preserved
411
+ # * Any changes made to attributes on objects in the memory array are to be preserved
412
+ # * Otherwise, attributes should have the value found in the database
413
+ def merge_target_lists(persisted, memory)
414
+ return persisted if memory.empty?
415
+ return memory if persisted.empty?
416
+
417
+ persisted.map! do |record|
418
+ if mem_record = memory.delete(record)
419
+
420
+ ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name|
421
+ mem_record[name] = record[name]
422
+ end
423
+
424
+ mem_record
425
+ else
426
+ record
427
+ end
428
+ end
429
+
430
+ persisted + memory
431
+ end
432
+
433
+ def create_record(attributes, raise = false, &block)
434
+ unless owner.persisted?
435
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
436
+ end
437
+
438
+ if attributes.is_a?(Array)
439
+ attributes.collect { |attr| create_record(attr, raise, &block) }
440
+ else
441
+ transaction do
442
+ add_to_target(build_record(attributes)) do |record|
443
+ yield(record) if block_given?
444
+ insert_record(record, true, raise)
445
+ end
446
+ end
447
+ end
448
+ end
449
+
450
+ # Do the relevant stuff to insert the given record into the association collection.
451
+ def insert_record(record, validate = true, raise = false)
452
+ raise NotImplementedError
453
+ end
454
+
455
+ def create_scope
456
+ scope.scope_for_create.stringify_keys
457
+ end
458
+
459
+ def delete_or_destroy(records, method)
460
+ records = records.flatten
461
+ records.each { |record| raise_on_type_mismatch!(record) }
462
+ existing_records = records.reject { |r| r.new_record? }
463
+
464
+ if existing_records.empty?
465
+ remove_records(existing_records, records, method)
466
+ else
467
+ transaction { remove_records(existing_records, records, method) }
468
+ end
469
+ end
470
+
471
+ def remove_records(existing_records, records, method)
472
+ records.each { |record| callback(:before_remove, record) }
473
+
474
+ delete_records(existing_records, method) if existing_records.any?
475
+ records.each { |record| target.delete(record) }
476
+
477
+ records.each { |record| callback(:after_remove, record) }
478
+ end
479
+
480
+ # Delete the given records from the association, using one of the methods :destroy,
481
+ # :delete_all or :nullify (or nil, in which case a default is used).
482
+ def delete_records(records, method)
483
+ raise NotImplementedError
484
+ end
485
+
486
+ def replace_records(new_target, original_target)
487
+ delete(target - new_target)
488
+
489
+ unless concat(new_target - target)
490
+ @target = original_target
491
+ raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
492
+ "new records could not be saved."
493
+ end
494
+
495
+ target
496
+ end
497
+
498
+ def concat_records(records)
499
+ result = true
500
+
501
+ records.flatten.each do |record|
502
+ raise_on_type_mismatch!(record)
503
+ add_to_target(record) do |rec|
504
+ result &&= insert_record(rec) unless owner.new_record?
505
+ end
506
+ end
507
+
508
+ result && records
509
+ end
510
+
511
+ def callback(method, record)
512
+ callbacks_for(method).each do |callback|
513
+ callback.call(method, owner, record)
514
+ end
515
+ end
516
+
517
+ def callbacks_for(callback_name)
518
+ full_callback_name = "#{callback_name}_for_#{reflection.name}"
519
+ owner.class.send(full_callback_name)
520
+ end
521
+
522
+ # Should we deal with assoc.first or assoc.last by issuing an independent query to
523
+ # the database, or by getting the target, and then taking the first/last item from that?
524
+ #
525
+ # If the args is just a non-empty options hash, go to the database.
526
+ #
527
+ # Otherwise, go to the database only if none of the following are true:
528
+ # * target already loaded
529
+ # * owner is new record
530
+ # * target contains new or changed record(s)
531
+ # * the first arg is an integer (which indicates the number of records to be returned)
532
+ def fetch_first_or_last_using_find?(args)
533
+ if args.first.is_a?(Hash)
534
+ true
535
+ else
536
+ !(loaded? ||
537
+ owner.new_record? ||
538
+ target.any? { |record| record.new_record? || record.changed? } ||
539
+ args.first.kind_of?(Integer))
540
+ end
541
+ end
542
+
543
+ def include_in_memory?(record)
544
+ if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
545
+ owner.send(reflection.through_reflection.name).any? { |source|
546
+ target = source.send(reflection.source_reflection.name)
547
+ target.respond_to?(:include?) ? target.include?(record) : target == record
548
+ } || target.include?(record)
549
+ else
550
+ target.include?(record)
551
+ end
552
+ end
553
+
554
+ # If the :inverse_of option has been
555
+ # specified, then #find scans the entire collection.
556
+ def find_by_scan(*args)
557
+ expects_array = args.first.kind_of?(Array)
558
+ ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq
559
+
560
+ if ids.size == 1
561
+ id = ids.first
562
+ record = load_target.detect { |r| id == r.id }
563
+ expects_array ? [ record ] : record
564
+ else
565
+ load_target.select { |r| ids.include?(r.id) }
566
+ end
567
+ end
568
+
569
+ # Fetches the first/last using SQL if possible, otherwise from the target array.
570
+ def first_or_last(type, *args)
571
+ args.shift if args.first.is_a?(Hash) && args.first.empty?
572
+
573
+ collection = fetch_first_or_last_using_find?(args) ? scope : load_target
574
+ collection.send(type, *args).tap do |record|
575
+ set_inverse_instance record if record.is_a? ActiveRecord::Base
576
+ end
577
+ end
578
+ end
579
+ end
580
+ end