activerecord 3.0.20 → 3.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

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