sskirby-activerecord 3.2.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.
Files changed (150) hide show
  1. data/CHANGELOG.md +6749 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +222 -0
  4. data/examples/associations.png +0 -0
  5. data/examples/performance.rb +177 -0
  6. data/examples/simple.rb +14 -0
  7. data/lib/active_record.rb +147 -0
  8. data/lib/active_record/aggregations.rb +255 -0
  9. data/lib/active_record/associations.rb +1604 -0
  10. data/lib/active_record/associations/alias_tracker.rb +79 -0
  11. data/lib/active_record/associations/association.rb +239 -0
  12. data/lib/active_record/associations/association_scope.rb +119 -0
  13. data/lib/active_record/associations/belongs_to_association.rb +79 -0
  14. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +34 -0
  15. data/lib/active_record/associations/builder/association.rb +55 -0
  16. data/lib/active_record/associations/builder/belongs_to.rb +85 -0
  17. data/lib/active_record/associations/builder/collection_association.rb +75 -0
  18. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +57 -0
  19. data/lib/active_record/associations/builder/has_many.rb +71 -0
  20. data/lib/active_record/associations/builder/has_one.rb +62 -0
  21. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  22. data/lib/active_record/associations/collection_association.rb +574 -0
  23. data/lib/active_record/associations/collection_proxy.rb +132 -0
  24. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +62 -0
  25. data/lib/active_record/associations/has_many_association.rb +108 -0
  26. data/lib/active_record/associations/has_many_through_association.rb +180 -0
  27. data/lib/active_record/associations/has_one_association.rb +73 -0
  28. data/lib/active_record/associations/has_one_through_association.rb +36 -0
  29. data/lib/active_record/associations/join_dependency.rb +214 -0
  30. data/lib/active_record/associations/join_dependency/join_association.rb +154 -0
  31. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  32. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  33. data/lib/active_record/associations/join_helper.rb +55 -0
  34. data/lib/active_record/associations/preloader.rb +177 -0
  35. data/lib/active_record/associations/preloader/association.rb +127 -0
  36. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  37. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  38. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  39. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  40. data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
  41. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  42. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  43. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  44. data/lib/active_record/associations/preloader/through_association.rb +67 -0
  45. data/lib/active_record/associations/singular_association.rb +64 -0
  46. data/lib/active_record/associations/through_association.rb +83 -0
  47. data/lib/active_record/attribute_assignment.rb +221 -0
  48. data/lib/active_record/attribute_methods.rb +272 -0
  49. data/lib/active_record/attribute_methods/before_type_cast.rb +31 -0
  50. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  51. data/lib/active_record/attribute_methods/dirty.rb +101 -0
  52. data/lib/active_record/attribute_methods/primary_key.rb +114 -0
  53. data/lib/active_record/attribute_methods/query.rb +39 -0
  54. data/lib/active_record/attribute_methods/read.rb +135 -0
  55. data/lib/active_record/attribute_methods/serialization.rb +93 -0
  56. data/lib/active_record/attribute_methods/time_zone_conversion.rb +62 -0
  57. data/lib/active_record/attribute_methods/write.rb +69 -0
  58. data/lib/active_record/autosave_association.rb +422 -0
  59. data/lib/active_record/base.rb +716 -0
  60. data/lib/active_record/callbacks.rb +275 -0
  61. data/lib/active_record/coders/yaml_column.rb +41 -0
  62. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +452 -0
  63. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +188 -0
  64. data/lib/active_record/connection_adapters/abstract/database_limits.rb +58 -0
  65. data/lib/active_record/connection_adapters/abstract/database_statements.rb +388 -0
  66. data/lib/active_record/connection_adapters/abstract/query_cache.rb +82 -0
  67. data/lib/active_record/connection_adapters/abstract/quoting.rb +115 -0
  68. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +492 -0
  69. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +598 -0
  70. data/lib/active_record/connection_adapters/abstract_adapter.rb +296 -0
  71. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
  72. data/lib/active_record/connection_adapters/column.rb +270 -0
  73. data/lib/active_record/connection_adapters/mysql2_adapter.rb +288 -0
  74. data/lib/active_record/connection_adapters/mysql_adapter.rb +426 -0
  75. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1261 -0
  76. data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
  77. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -0
  78. data/lib/active_record/connection_adapters/sqlite_adapter.rb +577 -0
  79. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  80. data/lib/active_record/counter_cache.rb +119 -0
  81. data/lib/active_record/dynamic_finder_match.rb +56 -0
  82. data/lib/active_record/dynamic_matchers.rb +79 -0
  83. data/lib/active_record/dynamic_scope_match.rb +23 -0
  84. data/lib/active_record/errors.rb +195 -0
  85. data/lib/active_record/explain.rb +85 -0
  86. data/lib/active_record/explain_subscriber.rb +21 -0
  87. data/lib/active_record/fixtures.rb +906 -0
  88. data/lib/active_record/fixtures/file.rb +65 -0
  89. data/lib/active_record/identity_map.rb +156 -0
  90. data/lib/active_record/inheritance.rb +167 -0
  91. data/lib/active_record/integration.rb +49 -0
  92. data/lib/active_record/locale/en.yml +40 -0
  93. data/lib/active_record/locking/optimistic.rb +183 -0
  94. data/lib/active_record/locking/pessimistic.rb +77 -0
  95. data/lib/active_record/log_subscriber.rb +68 -0
  96. data/lib/active_record/migration.rb +765 -0
  97. data/lib/active_record/migration/command_recorder.rb +105 -0
  98. data/lib/active_record/model_schema.rb +366 -0
  99. data/lib/active_record/nested_attributes.rb +469 -0
  100. data/lib/active_record/observer.rb +121 -0
  101. data/lib/active_record/persistence.rb +372 -0
  102. data/lib/active_record/query_cache.rb +74 -0
  103. data/lib/active_record/querying.rb +58 -0
  104. data/lib/active_record/railtie.rb +119 -0
  105. data/lib/active_record/railties/console_sandbox.rb +6 -0
  106. data/lib/active_record/railties/controller_runtime.rb +49 -0
  107. data/lib/active_record/railties/databases.rake +620 -0
  108. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  109. data/lib/active_record/readonly_attributes.rb +26 -0
  110. data/lib/active_record/reflection.rb +534 -0
  111. data/lib/active_record/relation.rb +534 -0
  112. data/lib/active_record/relation/batches.rb +90 -0
  113. data/lib/active_record/relation/calculations.rb +354 -0
  114. data/lib/active_record/relation/delegation.rb +49 -0
  115. data/lib/active_record/relation/finder_methods.rb +398 -0
  116. data/lib/active_record/relation/predicate_builder.rb +58 -0
  117. data/lib/active_record/relation/query_methods.rb +417 -0
  118. data/lib/active_record/relation/spawn_methods.rb +148 -0
  119. data/lib/active_record/result.rb +34 -0
  120. data/lib/active_record/sanitization.rb +194 -0
  121. data/lib/active_record/schema.rb +58 -0
  122. data/lib/active_record/schema_dumper.rb +204 -0
  123. data/lib/active_record/scoping.rb +152 -0
  124. data/lib/active_record/scoping/default.rb +142 -0
  125. data/lib/active_record/scoping/named.rb +202 -0
  126. data/lib/active_record/serialization.rb +18 -0
  127. data/lib/active_record/serializers/xml_serializer.rb +202 -0
  128. data/lib/active_record/session_store.rb +358 -0
  129. data/lib/active_record/store.rb +50 -0
  130. data/lib/active_record/test_case.rb +73 -0
  131. data/lib/active_record/timestamp.rb +113 -0
  132. data/lib/active_record/transactions.rb +360 -0
  133. data/lib/active_record/translation.rb +22 -0
  134. data/lib/active_record/validations.rb +83 -0
  135. data/lib/active_record/validations/associated.rb +43 -0
  136. data/lib/active_record/validations/uniqueness.rb +180 -0
  137. data/lib/active_record/version.rb +10 -0
  138. data/lib/rails/generators/active_record.rb +25 -0
  139. data/lib/rails/generators/active_record/migration.rb +15 -0
  140. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  141. data/lib/rails/generators/active_record/migration/templates/migration.rb +31 -0
  142. data/lib/rails/generators/active_record/model/model_generator.rb +43 -0
  143. data/lib/rails/generators/active_record/model/templates/migration.rb +15 -0
  144. data/lib/rails/generators/active_record/model/templates/model.rb +7 -0
  145. data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
  146. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  147. data/lib/rails/generators/active_record/observer/templates/observer.rb +4 -0
  148. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +25 -0
  149. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +12 -0
  150. metadata +242 -0
@@ -0,0 +1,32 @@
1
+ module ActiveRecord::Associations::Builder
2
+ class SingularAssociation < Association #:nodoc:
3
+ self.valid_options += [:remote, :dependent, :counter_cache, :primary_key, :inverse_of]
4
+
5
+ def constructable?
6
+ true
7
+ end
8
+
9
+ def define_accessors
10
+ super
11
+ define_constructors if constructable?
12
+ end
13
+
14
+ private
15
+
16
+ def define_constructors
17
+ name = self.name
18
+
19
+ mixin.redefine_method("build_#{name}") do |*params, &block|
20
+ association(name).build(*params, &block)
21
+ end
22
+
23
+ mixin.redefine_method("create_#{name}") do |*params, &block|
24
+ association(name).create(*params, &block)
25
+ end
26
+
27
+ mixin.redefine_method("create_#{name}!") do |*params, &block|
28
+ association(name).create!(*params, &block)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,574 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ # = Active Record Association Collection
6
+ #
7
+ # CollectionAssociation is an abstract class that provides common stuff to
8
+ # ease the implementation of association proxies that represent
9
+ # collections. See the class hierarchy in AssociationProxy.
10
+ #
11
+ # You need to be careful with assumptions regarding the target: The proxy
12
+ # does not fetch records from the database until it needs them, but new
13
+ # ones created with +build+ are added to the target. So, the target may be
14
+ # non-empty and still lack children waiting to be read from the database.
15
+ # If you look directly to the database you cannot assume that's the entire
16
+ # collection because new records may have been added to the target, etc.
17
+ #
18
+ # If you need to work on all current children, new and existing records,
19
+ # +load_target+ and the +loaded+ flag are your friends.
20
+ class CollectionAssociation < Association #:nodoc:
21
+ attr_reader :proxy
22
+
23
+ def initialize(owner, reflection)
24
+ super
25
+ @proxy = CollectionProxy.new(self)
26
+ end
27
+
28
+ # Implements the reader method, e.g. foo.items for Foo.has_many :items
29
+ def reader(force_reload = false)
30
+ if force_reload
31
+ klass.uncached { reload }
32
+ elsif stale_target?
33
+ reload
34
+ end
35
+
36
+ proxy
37
+ end
38
+
39
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
40
+ def writer(records)
41
+ replace(records)
42
+ end
43
+
44
+ # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
45
+ def ids_reader
46
+ if loaded? || options[:finder_sql]
47
+ load_target.map do |record|
48
+ record.send(reflection.association_primary_key)
49
+ end
50
+ else
51
+ column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
52
+ relation = scoped
53
+
54
+ including = (relation.eager_load_values + relation.includes_values).uniq
55
+
56
+ if including.any?
57
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(reflection.klass, including, [])
58
+ relation = join_dependency.join_associations.inject(relation) do |r, association|
59
+ association.join_relation(r)
60
+ end
61
+ end
62
+
63
+ relation.pluck(column)
64
+ end
65
+ end
66
+
67
+ # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
68
+ def ids_writer(ids)
69
+ pk_column = reflection.primary_key_column
70
+ ids = Array.wrap(ids).reject { |id| id.blank? }
71
+ ids.map! { |i| pk_column.type_cast(i) }
72
+ replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
73
+ end
74
+
75
+ def reset
76
+ @loaded = false
77
+ @target = []
78
+ end
79
+
80
+ def select(select = nil)
81
+ if block_given?
82
+ load_target.select.each { |e| yield e }
83
+ else
84
+ scoped.select(select)
85
+ end
86
+ end
87
+
88
+ def find(*args)
89
+ if block_given?
90
+ load_target.find(*args) { |*block_args| yield(*block_args) }
91
+ else
92
+ if options[:finder_sql]
93
+ find_by_scan(*args)
94
+ else
95
+ scoped.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 = {}, options = {}, &block)
109
+ if attributes.is_a?(Array)
110
+ attributes.collect { |attr| build(attr, options, &block) }
111
+ else
112
+ add_to_target(build_record(attributes, options)) do |record|
113
+ yield(record) if block_given?
114
+ end
115
+ end
116
+ end
117
+
118
+ def create(attributes = {}, options = {}, &block)
119
+ create_record(attributes, options, &block)
120
+ end
121
+
122
+ def create!(attributes = {}, options = {}, &block)
123
+ create_record(attributes, options, true, &block)
124
+ end
125
+
126
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
127
+ # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
128
+ def concat(*records)
129
+ load_target if owner.new_record?
130
+
131
+ if owner.new_record?
132
+ concat_records(records)
133
+ else
134
+ transaction { concat_records(records) }
135
+ end
136
+ end
137
+
138
+ # Starts a transaction in the association class's database connection.
139
+ #
140
+ # class Author < ActiveRecord::Base
141
+ # has_many :books
142
+ # end
143
+ #
144
+ # Author.first.books.transaction do
145
+ # # same effect as calling Book.transaction
146
+ # end
147
+ def transaction(*args)
148
+ reflection.klass.transaction(*args) do
149
+ yield
150
+ end
151
+ end
152
+
153
+ # Remove all records from this association
154
+ #
155
+ # See delete for more info.
156
+ def delete_all
157
+ delete(load_target).tap do
158
+ reset
159
+ loaded!
160
+ end
161
+ end
162
+
163
+ # Called when the association is declared as :dependent => :delete_all. This is
164
+ # an optimised version which avoids loading the records into memory. Not really
165
+ # for public consumption.
166
+ def delete_all_on_destroy
167
+ scoped.delete_all
168
+ end
169
+
170
+ # Destroy all the records from this association.
171
+ #
172
+ # See destroy for more info.
173
+ def destroy_all
174
+ destroy(load_target).tap do
175
+ reset
176
+ loaded!
177
+ end
178
+ end
179
+
180
+ # Calculate sum using SQL, not Enumerable
181
+ def sum(*args)
182
+ if block_given?
183
+ scoped.sum(*args) { |*block_args| yield(*block_args) }
184
+ else
185
+ scoped.sum(*args)
186
+ end
187
+ end
188
+
189
+ # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the
190
+ # association, it will be used for the query. Otherwise, construct options and pass them with
191
+ # scope to the target class's +count+.
192
+ def count(column_name = nil, count_options = {})
193
+ column_name, count_options = nil, column_name if column_name.is_a?(Hash)
194
+
195
+ if options[:counter_sql] || options[:finder_sql]
196
+ unless count_options.blank?
197
+ raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
198
+ end
199
+
200
+ reflection.klass.count_by_sql(custom_counter_sql)
201
+ else
202
+ if options[:uniq]
203
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
204
+ column_name ||= reflection.klass.primary_key
205
+ count_options.merge!(:distinct => true)
206
+ end
207
+
208
+ value = scoped.count(column_name, count_options)
209
+
210
+ limit = options[:limit]
211
+ offset = options[:offset]
212
+
213
+ if limit || offset
214
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
215
+ else
216
+ value
217
+ end
218
+ end
219
+ end
220
+
221
+ # Removes +records+ from this association calling +before_remove+ and
222
+ # +after_remove+ callbacks.
223
+ #
224
+ # This method is abstract in the sense that +delete_records+ has to be
225
+ # provided by descendants. Note this method does not imply the records
226
+ # are actually removed from the database, that depends precisely on
227
+ # +delete_records+. They are in any case removed from the collection.
228
+ def delete(*records)
229
+ delete_or_destroy(records, options[:dependent])
230
+ end
231
+
232
+ # Destroy +records+ and remove them from this association calling
233
+ # +before_remove+ and +after_remove+ callbacks.
234
+ #
235
+ # Note that this method will _always_ remove records from the database
236
+ # ignoring the +:dependent+ option.
237
+ def destroy(*records)
238
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
239
+ delete_or_destroy(records, :destroy)
240
+ end
241
+
242
+ # Returns the size of the collection by executing a SELECT COUNT(*)
243
+ # query if the collection hasn't been loaded, and calling
244
+ # <tt>collection.size</tt> if it has.
245
+ #
246
+ # If the collection has been already loaded +size+ and +length+ are
247
+ # equivalent. If not and you are going to need the records anyway
248
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
249
+ #
250
+ # This method is abstract in the sense that it relies on
251
+ # +count_records+, which is a method descendants have to provide.
252
+ def size
253
+ if !find_target? || (loaded? && !options[:uniq])
254
+ target.size
255
+ elsif !loaded? && options[:group]
256
+ load_target.size
257
+ elsif !loaded? && !options[:uniq] && target.is_a?(Array)
258
+ unsaved_records = target.select { |r| r.new_record? }
259
+ unsaved_records.size + count_records
260
+ else
261
+ count_records
262
+ end
263
+ end
264
+
265
+ # Returns the size of the collection calling +size+ on the target.
266
+ #
267
+ # If the collection has been already loaded +length+ and +size+ are
268
+ # equivalent. If not and you are going to need the records anyway this
269
+ # method will take one less query. Otherwise +size+ is more efficient.
270
+ def length
271
+ load_target.size
272
+ end
273
+
274
+ # Equivalent to <tt>collection.size.zero?</tt>. If the collection has
275
+ # not been already loaded and you are going to fetch the records anyway
276
+ # it is better to check <tt>collection.length.zero?</tt>.
277
+ def empty?
278
+ size.zero?
279
+ end
280
+
281
+ def any?
282
+ if block_given?
283
+ load_target.any? { |*block_args| yield(*block_args) }
284
+ else
285
+ !empty?
286
+ end
287
+ end
288
+
289
+ # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1.
290
+ def many?
291
+ if block_given?
292
+ load_target.many? { |*block_args| yield(*block_args) }
293
+ else
294
+ size > 1
295
+ end
296
+ end
297
+
298
+ def uniq(collection = load_target)
299
+ seen = {}
300
+ collection.find_all do |record|
301
+ seen[record.id] = true unless seen.key?(record.id)
302
+ end
303
+ end
304
+
305
+ # Replace this collection with +other_array+
306
+ # This will perform a diff and delete/add only records that have changed.
307
+ def replace(other_array)
308
+ other_array.each { |val| raise_on_type_mismatch(val) }
309
+ original_target = load_target.dup
310
+
311
+ if owner.new_record?
312
+ replace_records(other_array, original_target)
313
+ else
314
+ transaction { replace_records(other_array, original_target) }
315
+ end
316
+ end
317
+
318
+ def include?(record)
319
+ if record.is_a?(reflection.klass)
320
+ if record.new_record?
321
+ include_in_memory?(record)
322
+ else
323
+ load_target if options[:finder_sql]
324
+ loaded? ? target.include?(record) : scoped.exists?(record)
325
+ end
326
+ else
327
+ false
328
+ end
329
+ end
330
+
331
+ def load_target
332
+ if find_target?
333
+ @target = merge_target_lists(find_target, target)
334
+ end
335
+
336
+ loaded!
337
+ target
338
+ end
339
+
340
+ def add_to_target(record)
341
+ callback(:before_add, record)
342
+ yield(record) if block_given?
343
+
344
+ if options[:uniq] && index = @target.index(record)
345
+ @target[index] = record
346
+ else
347
+ @target << record
348
+ end
349
+
350
+ callback(:after_add, record)
351
+ set_inverse_instance(record)
352
+
353
+ record
354
+ end
355
+
356
+ private
357
+
358
+ def custom_counter_sql
359
+ if options[:counter_sql]
360
+ interpolate(options[:counter_sql])
361
+ else
362
+ # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
363
+ interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
364
+ count_with = $2.to_s
365
+ count_with = '*' if count_with.blank? || count_with =~ /,/
366
+ "SELECT #{$1}COUNT(#{count_with}) FROM"
367
+ end
368
+ end
369
+ end
370
+
371
+ def custom_finder_sql
372
+ interpolate(options[:finder_sql])
373
+ end
374
+
375
+ def find_target
376
+ records =
377
+ if options[:finder_sql]
378
+ reflection.klass.find_by_sql(custom_finder_sql)
379
+ else
380
+ scoped.all
381
+ end
382
+
383
+ records = options[:uniq] ? uniq(records) : records
384
+ records.each { |record| set_inverse_instance(record) }
385
+ records
386
+ end
387
+
388
+ # We have some records loaded from the database (persisted) and some that are
389
+ # in-memory (memory). The same record may be represented in the persisted array
390
+ # and in the memory array.
391
+ #
392
+ # So the task of this method is to merge them according to the following rules:
393
+ #
394
+ # * The final array must not have duplicates
395
+ # * The order of the persisted array is to be preserved
396
+ # * Any changes made to attributes on objects in the memory array are to be preserved
397
+ # * Otherwise, attributes should have the value found in the database
398
+ def merge_target_lists(persisted, memory)
399
+ return persisted if memory.empty?
400
+ return memory if persisted.empty?
401
+
402
+ persisted.map! do |record|
403
+ # Unfortunately we cannot simply do memory.delete(record) since on 1.8 this returns
404
+ # record rather than memory.at(memory.index(record)). The behavior is fixed in 1.9.
405
+ mem_index = memory.index(record)
406
+
407
+ if mem_index
408
+ mem_record = memory.delete_at(mem_index)
409
+
410
+ (record.attribute_names - mem_record.changes.keys).each do |name|
411
+ mem_record[name] = record[name]
412
+ end
413
+
414
+ mem_record
415
+ else
416
+ record
417
+ end
418
+ end
419
+
420
+ persisted + memory
421
+ end
422
+
423
+ def create_record(attributes, options, raise = false, &block)
424
+ unless owner.persisted?
425
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
426
+ end
427
+
428
+ if attributes.is_a?(Array)
429
+ attributes.collect { |attr| create_record(attr, options, raise, &block) }
430
+ else
431
+ transaction do
432
+ add_to_target(build_record(attributes, options)) do |record|
433
+ yield(record) if block_given?
434
+ insert_record(record, true, raise)
435
+ end
436
+ end
437
+ end
438
+ end
439
+
440
+ # Do the relevant stuff to insert the given record into the association collection.
441
+ def insert_record(record, validate = true, raise = false)
442
+ raise NotImplementedError
443
+ end
444
+
445
+ def create_scope
446
+ scoped.scope_for_create.stringify_keys
447
+ end
448
+
449
+ def delete_or_destroy(records, method)
450
+ records = records.flatten
451
+ records.each { |record| raise_on_type_mismatch(record) }
452
+ existing_records = records.reject { |r| r.new_record? }
453
+
454
+ if existing_records.empty?
455
+ remove_records(existing_records, records, method)
456
+ else
457
+ transaction { remove_records(existing_records, records, method) }
458
+ end
459
+ end
460
+
461
+ def remove_records(existing_records, records, method)
462
+ records.each { |record| callback(:before_remove, record) }
463
+
464
+ delete_records(existing_records, method) if existing_records.any?
465
+ records.each { |record| target.delete(record) }
466
+
467
+ records.each { |record| callback(:after_remove, record) }
468
+ end
469
+
470
+ # Delete the given records from the association, using one of the methods :destroy,
471
+ # :delete_all or :nullify (or nil, in which case a default is used).
472
+ def delete_records(records, method)
473
+ raise NotImplementedError
474
+ end
475
+
476
+ def replace_records(new_target, original_target)
477
+ delete(target - new_target)
478
+
479
+ unless concat(new_target - target)
480
+ @target = original_target
481
+ raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
482
+ "new records could not be saved."
483
+ end
484
+ end
485
+
486
+ def concat_records(records)
487
+ result = true
488
+
489
+ records.flatten.each do |record|
490
+ raise_on_type_mismatch(record)
491
+ add_to_target(record) do |r|
492
+ result &&= insert_record(record) unless owner.new_record?
493
+ end
494
+ end
495
+
496
+ result && records
497
+ end
498
+
499
+ def callback(method, record)
500
+ callbacks_for(method).each do |callback|
501
+ case callback
502
+ when Symbol
503
+ owner.send(callback, record)
504
+ when Proc
505
+ callback.call(owner, record)
506
+ else
507
+ callback.send(method, owner, record)
508
+ end
509
+ end
510
+ end
511
+
512
+ def callbacks_for(callback_name)
513
+ full_callback_name = "#{callback_name}_for_#{reflection.name}"
514
+ owner.class.send(full_callback_name.to_sym) || []
515
+ end
516
+
517
+ # Should we deal with assoc.first or assoc.last by issuing an independent query to
518
+ # the database, or by getting the target, and then taking the first/last item from that?
519
+ #
520
+ # If the args is just a non-empty options hash, go to the database.
521
+ #
522
+ # Otherwise, go to the database only if none of the following are true:
523
+ # * target already loaded
524
+ # * owner is new record
525
+ # * custom :finder_sql exists
526
+ # * target contains new or changed record(s)
527
+ # * the first arg is an integer (which indicates the number of records to be returned)
528
+ def fetch_first_or_last_using_find?(args)
529
+ if args.first.is_a?(Hash)
530
+ true
531
+ else
532
+ !(loaded? ||
533
+ owner.new_record? ||
534
+ options[:finder_sql] ||
535
+ target.any? { |record| record.new_record? || record.changed? } ||
536
+ args.first.kind_of?(Integer))
537
+ end
538
+ end
539
+
540
+ def include_in_memory?(record)
541
+ if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
542
+ owner.send(reflection.through_reflection.name).any? { |source|
543
+ target = source.send(reflection.source_reflection.name)
544
+ target.respond_to?(:include?) ? target.include?(record) : target == record
545
+ } || target.include?(record)
546
+ else
547
+ target.include?(record)
548
+ end
549
+ end
550
+
551
+ # If using a custom finder_sql, #find scans the entire collection.
552
+ def find_by_scan(*args)
553
+ expects_array = args.first.kind_of?(Array)
554
+ ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
555
+
556
+ if ids.size == 1
557
+ id = ids.first
558
+ record = load_target.detect { |r| id == r.id }
559
+ expects_array ? [ record ] : record
560
+ else
561
+ load_target.select { |r| ids.include?(r.id) }
562
+ end
563
+ end
564
+
565
+ # Fetches the first/last using SQL if possible, otherwise from the target array.
566
+ def first_or_last(type, *args)
567
+ args.shift if args.first.is_a?(Hash) && args.first.empty?
568
+
569
+ collection = fetch_first_or_last_using_find?(args) ? scoped : load_target
570
+ collection.send(type, *args)
571
+ end
572
+ end
573
+ end
574
+ end