activerecord 1.0.0 → 3.0.0

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 (178) hide show
  1. data/CHANGELOG +5518 -76
  2. data/README.rdoc +222 -0
  3. data/examples/performance.rb +162 -0
  4. data/examples/simple.rb +14 -0
  5. data/lib/active_record/aggregations.rb +192 -80
  6. data/lib/active_record/association_preload.rb +403 -0
  7. data/lib/active_record/associations/association_collection.rb +545 -53
  8. data/lib/active_record/associations/association_proxy.rb +295 -0
  9. data/lib/active_record/associations/belongs_to_association.rb +91 -0
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +78 -0
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +127 -36
  12. data/lib/active_record/associations/has_many_association.rb +108 -84
  13. data/lib/active_record/associations/has_many_through_association.rb +116 -0
  14. data/lib/active_record/associations/has_one_association.rb +143 -0
  15. data/lib/active_record/associations/has_one_through_association.rb +40 -0
  16. data/lib/active_record/associations/through_association_scope.rb +154 -0
  17. data/lib/active_record/associations.rb +2086 -368
  18. data/lib/active_record/attribute_methods/before_type_cast.rb +33 -0
  19. data/lib/active_record/attribute_methods/dirty.rb +95 -0
  20. data/lib/active_record/attribute_methods/primary_key.rb +50 -0
  21. data/lib/active_record/attribute_methods/query.rb +39 -0
  22. data/lib/active_record/attribute_methods/read.rb +116 -0
  23. data/lib/active_record/attribute_methods/time_zone_conversion.rb +61 -0
  24. data/lib/active_record/attribute_methods/write.rb +37 -0
  25. data/lib/active_record/attribute_methods.rb +60 -0
  26. data/lib/active_record/autosave_association.rb +369 -0
  27. data/lib/active_record/base.rb +1603 -721
  28. data/lib/active_record/callbacks.rb +176 -225
  29. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +365 -0
  30. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
  31. data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
  32. data/lib/active_record/connection_adapters/abstract/database_statements.rb +329 -0
  33. data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
  34. data/lib/active_record/connection_adapters/abstract/quoting.rb +72 -0
  35. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
  36. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +543 -0
  37. data/lib/active_record/connection_adapters/abstract_adapter.rb +165 -279
  38. data/lib/active_record/connection_adapters/mysql_adapter.rb +594 -82
  39. data/lib/active_record/connection_adapters/postgresql_adapter.rb +988 -135
  40. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -0
  41. data/lib/active_record/connection_adapters/sqlite_adapter.rb +365 -71
  42. data/lib/active_record/counter_cache.rb +115 -0
  43. data/lib/active_record/dynamic_finder_match.rb +53 -0
  44. data/lib/active_record/dynamic_scope_match.rb +32 -0
  45. data/lib/active_record/errors.rb +172 -0
  46. data/lib/active_record/fixtures.rb +941 -105
  47. data/lib/active_record/locale/en.yml +40 -0
  48. data/lib/active_record/locking/optimistic.rb +172 -0
  49. data/lib/active_record/locking/pessimistic.rb +55 -0
  50. data/lib/active_record/log_subscriber.rb +48 -0
  51. data/lib/active_record/migration.rb +617 -0
  52. data/lib/active_record/named_scope.rb +138 -0
  53. data/lib/active_record/nested_attributes.rb +417 -0
  54. data/lib/active_record/observer.rb +105 -36
  55. data/lib/active_record/persistence.rb +291 -0
  56. data/lib/active_record/query_cache.rb +36 -0
  57. data/lib/active_record/railtie.rb +91 -0
  58. data/lib/active_record/railties/controller_runtime.rb +38 -0
  59. data/lib/active_record/railties/databases.rake +512 -0
  60. data/lib/active_record/reflection.rb +364 -87
  61. data/lib/active_record/relation/batches.rb +89 -0
  62. data/lib/active_record/relation/calculations.rb +286 -0
  63. data/lib/active_record/relation/finder_methods.rb +355 -0
  64. data/lib/active_record/relation/predicate_builder.rb +41 -0
  65. data/lib/active_record/relation/query_methods.rb +261 -0
  66. data/lib/active_record/relation/spawn_methods.rb +112 -0
  67. data/lib/active_record/relation.rb +393 -0
  68. data/lib/active_record/schema.rb +59 -0
  69. data/lib/active_record/schema_dumper.rb +195 -0
  70. data/lib/active_record/serialization.rb +60 -0
  71. data/lib/active_record/serializers/xml_serializer.rb +244 -0
  72. data/lib/active_record/session_store.rb +340 -0
  73. data/lib/active_record/test_case.rb +67 -0
  74. data/lib/active_record/timestamp.rb +88 -0
  75. data/lib/active_record/transactions.rb +329 -75
  76. data/lib/active_record/validations/associated.rb +48 -0
  77. data/lib/active_record/validations/uniqueness.rb +185 -0
  78. data/lib/active_record/validations.rb +58 -179
  79. data/lib/active_record/version.rb +9 -0
  80. data/lib/active_record.rb +100 -24
  81. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  82. data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
  83. data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
  84. data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
  85. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  86. data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
  87. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  88. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  89. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  90. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
  91. data/lib/rails/generators/active_record.rb +27 -0
  92. metadata +216 -158
  93. data/README +0 -361
  94. data/RUNNING_UNIT_TESTS +0 -36
  95. data/dev-utils/eval_debugger.rb +0 -9
  96. data/examples/associations.rb +0 -87
  97. data/examples/shared_setup.rb +0 -15
  98. data/examples/validation.rb +0 -88
  99. data/install.rb +0 -60
  100. data/lib/active_record/deprecated_associations.rb +0 -70
  101. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  102. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  103. data/lib/active_record/support/clean_logger.rb +0 -10
  104. data/lib/active_record/support/inflector.rb +0 -70
  105. data/lib/active_record/vendor/mysql.rb +0 -1117
  106. data/lib/active_record/vendor/simple.rb +0 -702
  107. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  108. data/lib/active_record/wrappings.rb +0 -59
  109. data/rakefile +0 -122
  110. data/test/abstract_unit.rb +0 -16
  111. data/test/aggregations_test.rb +0 -34
  112. data/test/all.sh +0 -8
  113. data/test/associations_test.rb +0 -477
  114. data/test/base_test.rb +0 -513
  115. data/test/class_inheritable_attributes_test.rb +0 -33
  116. data/test/connections/native_mysql/connection.rb +0 -24
  117. data/test/connections/native_postgresql/connection.rb +0 -24
  118. data/test/connections/native_sqlite/connection.rb +0 -24
  119. data/test/deprecated_associations_test.rb +0 -336
  120. data/test/finder_test.rb +0 -67
  121. data/test/fixtures/accounts/signals37 +0 -3
  122. data/test/fixtures/accounts/unknown +0 -2
  123. data/test/fixtures/auto_id.rb +0 -4
  124. data/test/fixtures/column_name.rb +0 -3
  125. data/test/fixtures/companies/first_client +0 -6
  126. data/test/fixtures/companies/first_firm +0 -4
  127. data/test/fixtures/companies/second_client +0 -6
  128. data/test/fixtures/company.rb +0 -37
  129. data/test/fixtures/company_in_module.rb +0 -33
  130. data/test/fixtures/course.rb +0 -3
  131. data/test/fixtures/courses/java +0 -2
  132. data/test/fixtures/courses/ruby +0 -2
  133. data/test/fixtures/customer.rb +0 -30
  134. data/test/fixtures/customers/david +0 -6
  135. data/test/fixtures/db_definitions/mysql.sql +0 -96
  136. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  137. data/test/fixtures/db_definitions/postgresql.sql +0 -113
  138. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  139. data/test/fixtures/db_definitions/sqlite.sql +0 -85
  140. data/test/fixtures/db_definitions/sqlite2.sql +0 -4
  141. data/test/fixtures/default.rb +0 -2
  142. data/test/fixtures/developer.rb +0 -8
  143. data/test/fixtures/developers/david +0 -2
  144. data/test/fixtures/developers/jamis +0 -2
  145. data/test/fixtures/developers_projects/david_action_controller +0 -2
  146. data/test/fixtures/developers_projects/david_active_record +0 -2
  147. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  148. data/test/fixtures/entrant.rb +0 -3
  149. data/test/fixtures/entrants/first +0 -3
  150. data/test/fixtures/entrants/second +0 -3
  151. data/test/fixtures/entrants/third +0 -3
  152. data/test/fixtures/fixture_database.sqlite +0 -0
  153. data/test/fixtures/fixture_database_2.sqlite +0 -0
  154. data/test/fixtures/movie.rb +0 -5
  155. data/test/fixtures/movies/first +0 -2
  156. data/test/fixtures/movies/second +0 -2
  157. data/test/fixtures/project.rb +0 -3
  158. data/test/fixtures/projects/action_controller +0 -2
  159. data/test/fixtures/projects/active_record +0 -2
  160. data/test/fixtures/reply.rb +0 -21
  161. data/test/fixtures/subscriber.rb +0 -5
  162. data/test/fixtures/subscribers/first +0 -2
  163. data/test/fixtures/subscribers/second +0 -2
  164. data/test/fixtures/topic.rb +0 -20
  165. data/test/fixtures/topics/first +0 -9
  166. data/test/fixtures/topics/second +0 -8
  167. data/test/fixtures_test.rb +0 -20
  168. data/test/inflector_test.rb +0 -104
  169. data/test/inheritance_test.rb +0 -125
  170. data/test/lifecycle_test.rb +0 -110
  171. data/test/modules_test.rb +0 -21
  172. data/test/multiple_db_test.rb +0 -46
  173. data/test/pk_test.rb +0 -57
  174. data/test/reflection_test.rb +0 -78
  175. data/test/thread_safety_test.rb +0 -33
  176. data/test/transactions_test.rb +0 -83
  177. data/test/unconnected_test.rb +0 -24
  178. data/test/validations_test.rb +0 -126
@@ -1,70 +1,562 @@
1
+ require 'set'
2
+ require 'active_support/core_ext/array/wrap'
3
+
1
4
  module ActiveRecord
2
5
  module Associations
3
- class AssociationCollection #:nodoc:
4
- alias_method :proxy_respond_to?, :respond_to?
5
- instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?)/ }
6
-
7
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
8
- @owner = owner
9
- @options = options
10
- @association_name = association_name
11
- @association_class = eval(association_class_name)
12
- @association_class_primary_key_name = association_class_primary_key_name
13
- end
14
-
15
- def method_missing(symbol, *args, &block)
16
- load_collection_to_array
17
- @collection_array.send(symbol, *args, &block)
18
- end
19
-
6
+ # = Active Record Association Collection
7
+ #
8
+ # AssociationCollection is an abstract class that provides common stuff to
9
+ # ease the implementation of association proxies that represent
10
+ # collections. See the class hierarchy in AssociationProxy.
11
+ #
12
+ # You need to be careful with assumptions regarding the target: The proxy
13
+ # does not fetch records from the database until it needs them, but new
14
+ # ones created with +build+ are added to the target. So, the target may be
15
+ # non-empty and still lack children waiting to be read from the database.
16
+ # If you look directly to the database you cannot assume that's the entire
17
+ # collection because new records may have been added to the target, etc.
18
+ #
19
+ # If you need to work on all current children, new and existing records,
20
+ # +load_target+ and the +loaded+ flag are your friends.
21
+ class AssociationCollection < AssociationProxy #:nodoc:
22
+ def initialize(owner, reflection)
23
+ super
24
+ construct_sql
25
+ end
26
+
27
+ delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped
28
+
29
+ def select(select = nil)
30
+ if block_given?
31
+ load_target
32
+ @target.select.each { |e| yield e }
33
+ else
34
+ scoped.select(select)
35
+ end
36
+ end
37
+
38
+ def scoped
39
+ with_scope(construct_scope) { @reflection.klass.scoped }
40
+ end
41
+
42
+ def find(*args)
43
+ options = args.extract_options!
44
+
45
+ # If using a custom finder_sql, scan the entire collection.
46
+ if @reflection.options[:finder_sql]
47
+ expects_array = args.first.kind_of?(Array)
48
+ ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
49
+
50
+ if ids.size == 1
51
+ id = ids.first
52
+ record = load_target.detect { |r| id == r.id }
53
+ expects_array ? [ record ] : record
54
+ else
55
+ load_target.select { |r| ids.include?(r.id) }
56
+ end
57
+ else
58
+ merge_options_from_reflection!(options)
59
+ construct_find_options!(options)
60
+
61
+ find_scope = construct_scope[:find].slice(:conditions, :order)
62
+
63
+ with_scope(:find => find_scope) do
64
+ relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods))
65
+
66
+ case args.first
67
+ when :first, :last
68
+ relation.send(args.first)
69
+ when :all
70
+ records = relation.all
71
+ @reflection.options[:uniq] ? uniq(records) : records
72
+ else
73
+ relation.find(*args)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ # Fetches the first one using SQL if possible.
80
+ def first(*args)
81
+ if fetch_first_or_last_using_find?(args)
82
+ find(:first, *args)
83
+ else
84
+ load_target unless loaded?
85
+ @target.first(*args)
86
+ end
87
+ end
88
+
89
+ # Fetches the last one using SQL if possible.
90
+ def last(*args)
91
+ if fetch_first_or_last_using_find?(args)
92
+ find(:last, *args)
93
+ else
94
+ load_target unless loaded?
95
+ @target.last(*args)
96
+ end
97
+ end
98
+
20
99
  def to_ary
21
- load_collection_to_array
22
- @collection_array.to_ary
100
+ load_target
101
+ if @target.is_a?(Array)
102
+ @target.to_ary
103
+ else
104
+ Array.wrap(@target)
105
+ end
23
106
  end
24
-
25
- def respond_to?(symbol)
26
- proxy_respond_to?(symbol) || [].respond_to?(symbol)
107
+ alias_method :to_a, :to_ary
108
+
109
+ def reset
110
+ reset_target!
111
+ reset_named_scopes_cache!
112
+ @loaded = false
27
113
  end
28
-
29
- def reload
30
- @collection_array = nil
114
+
115
+ def build(attributes = {}, &block)
116
+ if attributes.is_a?(Array)
117
+ attributes.collect { |attr| build(attr, &block) }
118
+ else
119
+ build_record(attributes) do |record|
120
+ block.call(record) if block_given?
121
+ set_belongs_to_association_for(record)
122
+ end
123
+ end
31
124
  end
32
-
33
- def concat(*records)
34
- records.flatten!
35
- records.each {|record| self << record; }
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 <<(*records)
129
+ result = true
130
+ load_target if @owner.new_record?
131
+
132
+ transaction do
133
+ flatten_deeper(records).each do |record|
134
+ raise_on_type_mismatch(record)
135
+ add_record_to_target_with_callbacks(record) do |r|
136
+ result &&= insert_record(record) unless @owner.new_record?
137
+ end
138
+ end
139
+ end
140
+
141
+ result && self
142
+ end
143
+
144
+ alias_method :push, :<<
145
+ alias_method :concat, :<<
146
+
147
+ # Starts a transaction in the association class's database connection.
148
+ #
149
+ # class Author < ActiveRecord::Base
150
+ # has_many :books
151
+ # end
152
+ #
153
+ # Author.first.books.transaction do
154
+ # # same effect as calling Book.transaction
155
+ # end
156
+ def transaction(*args)
157
+ @reflection.klass.transaction(*args) do
158
+ yield
159
+ end
36
160
  end
37
-
161
+
162
+ # Remove all records from this association
163
+ #
164
+ # See delete for more info.
165
+ def delete_all
166
+ load_target
167
+ delete(@target)
168
+ reset_target!
169
+ reset_named_scopes_cache!
170
+ end
171
+
172
+ # Calculate sum using SQL, not Enumerable
173
+ def sum(*args)
174
+ if block_given?
175
+ calculate(:sum, *args) { |*block_args| yield(*block_args) }
176
+ else
177
+ calculate(:sum, *args)
178
+ end
179
+ end
180
+
181
+ # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
182
+ # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
183
+ # descendant's +construct_sql+ method will have set :counter_sql automatically.
184
+ # Otherwise, construct options and pass them with scope to the target class's +count+.
185
+ def count(column_name = nil, options = {})
186
+ column_name, options = nil, column_name if column_name.is_a?(Hash)
187
+
188
+ if @reflection.options[:counter_sql] && !options.blank?
189
+ raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
190
+ elsif @reflection.options[:counter_sql]
191
+ @reflection.klass.count_by_sql(@counter_sql)
192
+ else
193
+
194
+ if @reflection.options[:uniq]
195
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
196
+ column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" unless column_name
197
+ options.merge!(:distinct => true)
198
+ end
199
+
200
+ value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
201
+
202
+ limit = @reflection.options[:limit]
203
+ offset = @reflection.options[:offset]
204
+
205
+ if limit || offset
206
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
207
+ else
208
+ value
209
+ end
210
+ end
211
+ end
212
+
213
+ # Removes +records+ from this association calling +before_remove+ and
214
+ # +after_remove+ callbacks.
215
+ #
216
+ # This method is abstract in the sense that +delete_records+ has to be
217
+ # provided by descendants. Note this method does not imply the records
218
+ # are actually removed from the database, that depends precisely on
219
+ # +delete_records+. They are in any case removed from the collection.
220
+ def delete(*records)
221
+ remove_records(records) do |_records, old_records|
222
+ delete_records(old_records) if old_records.any?
223
+ _records.each { |record| @target.delete(record) }
224
+ end
225
+ end
226
+
227
+ # Destroy +records+ and remove them from this association calling
228
+ # +before_remove+ and +after_remove+ callbacks.
229
+ #
230
+ # Note that this method will _always_ remove records from the database
231
+ # ignoring the +:dependent+ option.
232
+ def destroy(*records)
233
+ records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)}
234
+ remove_records(records) do |_records, old_records|
235
+ old_records.each { |record| record.destroy }
236
+ end
237
+
238
+ load_target
239
+ end
240
+
241
+ # Removes all records from this association. Returns +self+ so method calls may be chained.
242
+ def clear
243
+ return self if length.zero? # forces load_target if it hasn't happened already
244
+
245
+ if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
246
+ destroy_all
247
+ else
248
+ delete_all
249
+ end
250
+
251
+ self
252
+ end
253
+
254
+ # Destroy all the records from this association.
255
+ #
256
+ # See destroy for more info.
38
257
  def destroy_all
39
- load_collection_to_array
40
- @collection_array.each { |object| object.destroy }
41
- @collection_array = []
258
+ load_target
259
+ destroy(@target).tap do
260
+ reset_target!
261
+ reset_named_scopes_cache!
262
+ end
263
+ end
264
+
265
+ def create(attrs = {})
266
+ if attrs.is_a?(Array)
267
+ attrs.collect { |attr| create(attr) }
268
+ else
269
+ create_record(attrs) do |record|
270
+ yield(record) if block_given?
271
+ record.save
272
+ end
273
+ end
42
274
  end
43
-
275
+
276
+ def create!(attrs = {})
277
+ create_record(attrs) do |record|
278
+ yield(record) if block_given?
279
+ record.save!
280
+ end
281
+ end
282
+
283
+ # Returns the size of the collection by executing a SELECT COUNT(*)
284
+ # query if the collection hasn't been loaded, and calling
285
+ # <tt>collection.size</tt> if it has.
286
+ #
287
+ # If the collection has been already loaded +size+ and +length+ are
288
+ # equivalent. If not and you are going to need the records anyway
289
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
290
+ #
291
+ # This method is abstract in the sense that it relies on
292
+ # +count_records+, which is a method descendants have to provide.
44
293
  def size
45
- (@collection_array.nil?) ? count_records : @collection_array.size
294
+ if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
295
+ @target.size
296
+ elsif !loaded? && @reflection.options[:group]
297
+ load_target.size
298
+ elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
299
+ unsaved_records = @target.select { |r| r.new_record? }
300
+ unsaved_records.size + count_records
301
+ else
302
+ count_records
303
+ end
304
+ end
305
+
306
+ # Returns the size of the collection calling +size+ on the target.
307
+ #
308
+ # If the collection has been already loaded +length+ and +size+ are
309
+ # equivalent. If not and you are going to need the records anyway this
310
+ # method will take one less query. Otherwise +size+ is more efficient.
311
+ def length
312
+ load_target.size
46
313
  end
47
-
314
+
315
+ # Equivalent to <tt>collection.size.zero?</tt>. If the collection has
316
+ # not been already loaded and you are going to fetch the records anyway
317
+ # it is better to check <tt>collection.length.zero?</tt>.
48
318
  def empty?
49
- size == 0
319
+ size.zero?
320
+ end
321
+
322
+ def any?
323
+ if block_given?
324
+ method_missing(:any?) { |*block_args| yield(*block_args) }
325
+ else
326
+ !empty?
327
+ end
328
+ end
329
+
330
+ # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1.
331
+ def many?
332
+ if block_given?
333
+ method_missing(:many?) { |*block_args| yield(*block_args) }
334
+ else
335
+ size > 1
336
+ end
50
337
  end
51
-
52
- alias_method :length, :size
53
-
338
+
339
+ def uniq(collection = self)
340
+ seen = Set.new
341
+ collection.inject([]) do |kept, record|
342
+ unless seen.include?(record.id)
343
+ kept << record
344
+ seen << record.id
345
+ end
346
+ kept
347
+ end
348
+ end
349
+
350
+ # Replace this collection with +other_array+
351
+ # This will perform a diff and delete/add only records that have changed.
352
+ def replace(other_array)
353
+ other_array.each { |val| raise_on_type_mismatch(val) }
354
+
355
+ load_target
356
+ other = other_array.size < 100 ? other_array : other_array.to_set
357
+ current = @target.size < 100 ? @target : @target.to_set
358
+
359
+ transaction do
360
+ delete(@target.select { |v| !other.include?(v) })
361
+ concat(other_array.select { |v| !current.include?(v) })
362
+ end
363
+ end
364
+
365
+ def include?(record)
366
+ return false unless record.is_a?(@reflection.klass)
367
+ load_target if @reflection.options[:finder_sql] && !loaded?
368
+ return @target.include?(record) if loaded?
369
+ exists?(record)
370
+ end
371
+
372
+ def proxy_respond_to?(method, include_private = false)
373
+ super || @reflection.klass.respond_to?(method, include_private)
374
+ end
375
+
376
+ protected
377
+ def construct_find_options!(options)
378
+ end
379
+
380
+ def construct_counter_sql
381
+ if @reflection.options[:counter_sql]
382
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
383
+ elsif @reflection.options[:finder_sql]
384
+ # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
385
+ @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
386
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
387
+ else
388
+ @counter_sql = @finder_sql
389
+ end
390
+ end
391
+
392
+ def load_target
393
+ if !@owner.new_record? || foreign_key_present
394
+ begin
395
+ if !loaded?
396
+ if @target.is_a?(Array) && @target.any?
397
+ @target = find_target.map do |f|
398
+ i = @target.index(f)
399
+ if i
400
+ @target.delete_at(i).tap do |t|
401
+ keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names)
402
+ t.attributes = f.attributes.except(*keys)
403
+ end
404
+ else
405
+ f
406
+ end
407
+ end + @target
408
+ else
409
+ @target = find_target
410
+ end
411
+ end
412
+ rescue ActiveRecord::RecordNotFound
413
+ reset
414
+ end
415
+ end
416
+
417
+ loaded if target
418
+ target
419
+ end
420
+
421
+ def method_missing(method, *args)
422
+ match = DynamicFinderMatch.match(method)
423
+ if match && match.creator?
424
+ attributes = match.attribute_names
425
+ return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)])
426
+ end
427
+
428
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
429
+ if block_given?
430
+ super { |*block_args| yield(*block_args) }
431
+ else
432
+ super
433
+ end
434
+ elsif @reflection.klass.scopes[method]
435
+ @_named_scopes_cache ||= {}
436
+ @_named_scopes_cache[method] ||= {}
437
+ @_named_scopes_cache[method][args] ||= with_scope(construct_scope) { @reflection.klass.send(method, *args) }
438
+ else
439
+ with_scope(construct_scope) do
440
+ if block_given?
441
+ @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }
442
+ else
443
+ @reflection.klass.send(method, *args)
444
+ end
445
+ end
446
+ end
447
+ end
448
+
449
+ # overloaded in derived Association classes to provide useful scoping depending on association type.
450
+ def construct_scope
451
+ {}
452
+ end
453
+
454
+ def reset_target!
455
+ @target = Array.new
456
+ end
457
+
458
+ def reset_named_scopes_cache!
459
+ @_named_scopes_cache = {}
460
+ end
461
+
462
+ def find_target
463
+ records =
464
+ if @reflection.options[:finder_sql]
465
+ @reflection.klass.find_by_sql(@finder_sql)
466
+ else
467
+ find(:all)
468
+ end
469
+
470
+ records = @reflection.options[:uniq] ? uniq(records) : records
471
+ records.each do |record|
472
+ set_inverse_instance(record, @owner)
473
+ end
474
+ records
475
+ end
476
+
477
+ def add_record_to_target_with_callbacks(record)
478
+ callback(:before_add, record)
479
+ yield(record) if block_given?
480
+ @target ||= [] unless loaded?
481
+ if index = @target.index(record)
482
+ @target[index] = record
483
+ else
484
+ @target << record
485
+ end
486
+ callback(:after_add, record)
487
+ set_inverse_instance(record, @owner)
488
+ record
489
+ end
490
+
54
491
  private
55
- def load_collection_to_array
56
- return unless @collection_array.nil?
57
- begin
58
- @collection_array = find_all_records
59
- rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
60
- @collection_array = []
61
- end
62
- end
63
-
64
- def duplicated_records_array(records)
65
- records = [records] unless records.is_a?(Array) || records.is_a?(ActiveRecord::Associations::AssociationCollection)
66
- records.dup
492
+ def create_record(attrs)
493
+ attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
494
+ ensure_owner_is_not_new
495
+
496
+ _scope = self.construct_scope[:create]
497
+ csm = @reflection.klass.send(:current_scoped_methods)
498
+ options = (csm.blank? || !_scope.is_a?(Hash)) ? _scope : _scope.merge(csm.where_values_hash)
499
+
500
+ record = @reflection.klass.send(:with_scope, :create => options) do
501
+ @reflection.build_association(attrs)
502
+ end
503
+ if block_given?
504
+ add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
505
+ else
506
+ add_record_to_target_with_callbacks(record)
507
+ end
508
+ end
509
+
510
+ def build_record(attrs)
511
+ attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
512
+ record = @reflection.build_association(attrs)
513
+ if block_given?
514
+ add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
515
+ else
516
+ add_record_to_target_with_callbacks(record)
517
+ end
518
+ end
519
+
520
+ def remove_records(*records)
521
+ records = flatten_deeper(records)
522
+ records.each { |record| raise_on_type_mismatch(record) }
523
+
524
+ transaction do
525
+ records.each { |record| callback(:before_remove, record) }
526
+ old_records = records.reject { |r| r.new_record? }
527
+ yield(records, old_records)
528
+ records.each { |record| callback(:after_remove, record) }
529
+ end
530
+ end
531
+
532
+ def callback(method, record)
533
+ callbacks_for(method).each do |callback|
534
+ case callback
535
+ when Symbol
536
+ @owner.send(callback, record)
537
+ when Proc
538
+ callback.call(@owner, record)
539
+ else
540
+ callback.send(method, @owner, record)
541
+ end
542
+ end
543
+ end
544
+
545
+ def callbacks_for(callback_name)
546
+ full_callback_name = "#{callback_name}_for_#{@reflection.name}"
547
+ @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
548
+ end
549
+
550
+ def ensure_owner_is_not_new
551
+ if @owner.new_record?
552
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
553
+ end
554
+ end
555
+
556
+ def fetch_first_or_last_using_find?(args)
557
+ args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] ||
558
+ @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
67
559
  end
68
560
  end
69
561
  end
70
- end
562
+ end