ocean-dynamo 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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