activerecord 1.10.1 → 1.11.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 (84) hide show
  1. data/CHANGELOG +187 -19
  2. data/RUNNING_UNIT_TESTS +11 -0
  3. data/lib/active_record.rb +3 -1
  4. data/lib/active_record/acts/list.rb +25 -14
  5. data/lib/active_record/acts/nested_set.rb +4 -4
  6. data/lib/active_record/acts/tree.rb +18 -1
  7. data/lib/active_record/associations.rb +90 -17
  8. data/lib/active_record/associations/association_collection.rb +44 -5
  9. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +17 -4
  10. data/lib/active_record/associations/has_many_association.rb +13 -3
  11. data/lib/active_record/associations/has_one_association.rb +19 -0
  12. data/lib/active_record/base.rb +292 -268
  13. data/lib/active_record/callbacks.rb +14 -14
  14. data/lib/active_record/connection_adapters/abstract_adapter.rb +137 -75
  15. data/lib/active_record/connection_adapters/db2_adapter.rb +10 -8
  16. data/lib/active_record/connection_adapters/mysql_adapter.rb +91 -64
  17. data/lib/active_record/connection_adapters/oci_adapter.rb +6 -6
  18. data/lib/active_record/connection_adapters/postgresql_adapter.rb +113 -60
  19. data/lib/active_record/connection_adapters/sqlite_adapter.rb +15 -12
  20. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +159 -132
  21. data/lib/active_record/fixtures.rb +59 -12
  22. data/lib/active_record/locking.rb +10 -9
  23. data/lib/active_record/migration.rb +112 -5
  24. data/lib/active_record/query_cache.rb +64 -0
  25. data/lib/active_record/timestamp.rb +10 -8
  26. data/lib/active_record/validations.rb +121 -26
  27. data/rakefile +16 -10
  28. data/test/aaa_create_tables_test.rb +26 -48
  29. data/test/abstract_unit.rb +3 -0
  30. data/test/aggregations_test.rb +19 -19
  31. data/test/association_callbacks_test.rb +110 -0
  32. data/test/associations_go_eager_test.rb +48 -14
  33. data/test/associations_test.rb +344 -142
  34. data/test/base_test.rb +150 -31
  35. data/test/binary_test.rb +7 -0
  36. data/test/callbacks_test.rb +24 -5
  37. data/test/column_alias_test.rb +2 -2
  38. data/test/connections/native_sqlserver_odbc/connection.rb +26 -0
  39. data/test/deprecated_associations_test.rb +27 -28
  40. data/test/deprecated_finder_test.rb +8 -9
  41. data/test/finder_test.rb +52 -17
  42. data/test/fixtures/author.rb +39 -0
  43. data/test/fixtures/categories.yml +7 -0
  44. data/test/fixtures/categories_posts.yml +8 -0
  45. data/test/fixtures/category.rb +2 -0
  46. data/test/fixtures/comment.rb +3 -1
  47. data/test/fixtures/comments.yml +43 -1
  48. data/test/fixtures/companies.yml +14 -0
  49. data/test/fixtures/company.rb +1 -1
  50. data/test/fixtures/computers.yml +2 -1
  51. data/test/fixtures/db_definitions/db2.sql +7 -2
  52. data/test/fixtures/db_definitions/mysql.drop.sql +2 -0
  53. data/test/fixtures/db_definitions/mysql.sql +11 -6
  54. data/test/fixtures/db_definitions/oci.sql +7 -2
  55. data/test/fixtures/db_definitions/postgresql.drop.sql +3 -1
  56. data/test/fixtures/db_definitions/postgresql.sql +8 -5
  57. data/test/fixtures/db_definitions/sqlite.drop.sql +2 -0
  58. data/test/fixtures/db_definitions/sqlite.sql +9 -4
  59. data/test/fixtures/db_definitions/sqlserver.drop.sql +2 -0
  60. data/test/fixtures/db_definitions/sqlserver.sql +12 -7
  61. data/test/fixtures/developer.rb +8 -1
  62. data/test/fixtures/migrations/3_innocent_jointable.rb +12 -0
  63. data/test/fixtures/post.rb +8 -2
  64. data/test/fixtures/posts.yml +21 -0
  65. data/test/fixtures/project.rb +14 -1
  66. data/test/fixtures/subscriber.rb +3 -0
  67. data/test/fixtures_test.rb +14 -0
  68. data/test/inheritance_test.rb +30 -22
  69. data/test/lifecycle_test.rb +3 -4
  70. data/test/locking_test.rb +2 -4
  71. data/test/migration_test.rb +186 -0
  72. data/test/mixin_nested_set_test.rb +19 -19
  73. data/test/mixin_test.rb +88 -88
  74. data/test/modules_test.rb +5 -10
  75. data/test/multiple_db_test.rb +2 -0
  76. data/test/pk_test.rb +8 -12
  77. data/test/reflection_test.rb +8 -4
  78. data/test/schema_test_postgresql.rb +63 -0
  79. data/test/thread_safety_test.rb +4 -1
  80. data/test/transactions_test.rb +9 -2
  81. data/test/unconnected_test.rb +1 -0
  82. data/test/validations_test.rb +151 -8
  83. metadata +11 -5
  84. data/test/migration_mysql.rb +0 -104
@@ -181,17 +181,17 @@ module ActiveRecord
181
181
 
182
182
  # Returns a set of itself and all of it's nested children
183
183
  def full_set
184
- self.class.find_all( "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" )
184
+ self.class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} and #{self[right_col_name]})" )
185
185
  end
186
186
 
187
187
  # Returns a set of all of it's children and nested children
188
188
  def all_children
189
- self.class.find_all( "#{scope_condition} AND (#{left_col_name} > #{self[left_col_name]}) and (#{right_col_name} < #{self[right_col_name]})" )
189
+ self.class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} > #{self[left_col_name]}) and (#{right_col_name} < #{self[right_col_name]})" )
190
190
  end
191
191
 
192
192
  # Returns a set of only this entries immediate children
193
193
  def direct_children
194
- self.class.find_all( "#{scope_condition} and #{parent_column} = #{self.id}")
194
+ self.class.find(:all, :conditions => "#{scope_condition} and #{parent_column} = #{self.id}")
195
195
  end
196
196
 
197
197
  # Prunes a branch off of the tree, shifting all of the elements on the right
@@ -209,4 +209,4 @@ module ActiveRecord
209
209
  end
210
210
  end
211
211
  end
212
- end
212
+ end
@@ -35,9 +35,26 @@ module ActiveRecord
35
35
  def acts_as_tree(options = {})
36
36
  configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil }
37
37
  configuration.update(options) if options.is_a?(Hash)
38
-
38
+
39
39
  belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
40
40
  has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => true
41
+
42
+ module_eval <<-END
43
+ def self.roots
44
+ self.find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => "#{configuration[:order]}")
45
+ end
46
+ def self.root
47
+ self.find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => "#{configuration[:order]}")
48
+ end
49
+ END
50
+
51
+ define_method(:siblings) do
52
+ if parent
53
+ self.class.find(:all, :conditions => [ "#{configuration[:foreign_key]} = ?", parent.id ], :order => configuration[:order])
54
+ else
55
+ self.class.roots
56
+ end
57
+ end
41
58
  end
42
59
  end
43
60
  end
@@ -17,7 +17,7 @@ module ActiveRecord
17
17
  def clear_association_cache #:nodoc:
18
18
  self.class.reflect_on_all_associations.to_a.each do |assoc|
19
19
  instance_variable_set "@#{assoc.name}", nil
20
- end
20
+ end unless self.new_record?
21
21
  end
22
22
 
23
23
  # Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
@@ -43,7 +43,7 @@ module ActiveRecord
43
43
  #
44
44
  # == Example
45
45
  #
46
- # link:../../examples/associations.png
46
+ # link:files/examples/associations.png
47
47
  #
48
48
  # == Is it belongs_to or has_one?
49
49
  #
@@ -96,6 +96,30 @@ module ActiveRecord
96
96
  # * You can add an object to a collection without automatically saving it by using the #collection.build method (documented below).
97
97
  # * All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved.
98
98
  #
99
+ # === Association callbacks
100
+ #
101
+ # Similiar to the normal callbacks that hook into the lifecycle of an Active Record object, you can also define callbacks that get
102
+ # trigged when you add an object to or removing an object from a association collection. Example:
103
+ #
104
+ # class Project
105
+ # has_and_belongs_to_many :developers, :after_add => :evaluate_velocity
106
+ #
107
+ # def evaluate_velocity(developer)
108
+ # ...
109
+ # end
110
+ # end
111
+ #
112
+ # It's possible to stack callbacks by passing them as an array. Example:
113
+ #
114
+ # class Project
115
+ # has_and_belongs_to_many :developers, :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
116
+ # end
117
+ #
118
+ # Possible callbacks are: before_add, after_add, before_remove and after_remove.
119
+ #
120
+ # Should any of the before_add callbacks throw an exception, the object does not get added to the collection. Same with
121
+ # the before_remove callbacks, if an exception is thrown the object doesn't get removed.
122
+ #
99
123
  # == Caching
100
124
  #
101
125
  # All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
@@ -150,7 +174,8 @@ module ActiveRecord
150
174
  # in both conditions and orders. So :order => "posts.id DESC" will work while :order => "id DESC" will not. This may require that
151
175
  # you alter the :order and :conditions on the association definitions themselves.
152
176
  #
153
- # It's currently not possible to use eager loading on multiple associations from the same table.
177
+ # It's currently not possible to use eager loading on multiple associations from the same table. Eager loading will also not pull
178
+ # additional attributes on join tables, so "rich associations" with has_and_belongs_to_many is not a good fit for eager loading.
154
179
  #
155
180
  # == Modules
156
181
  #
@@ -199,6 +224,8 @@ module ActiveRecord
199
224
  # * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
200
225
  # * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by setting their foreign keys to NULL.
201
226
  # This will also destroy the objects if they're declared as belongs_to and dependent on this model.
227
+ # * <tt>collection=objects</tt> - replaces the collections content by deleting and adding objects as appropriate.
228
+ # * <tt>collection_singular_ids=ids</tt> - replace the collection by the objects identified by the primary keys in +ids+
202
229
  # * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
203
230
  # * <tt>collection.empty?</tt> - returns true if there are no associated objects.
204
231
  # * <tt>collection.size</tt> - returns the number of associated objects.
@@ -214,12 +241,14 @@ module ActiveRecord
214
241
  # * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => "firm_id = #{id}"</tt>)
215
242
  # * <tt>Firm#clients<<</tt>
216
243
  # * <tt>Firm#clients.delete</tt>
244
+ # * <tt>Firm#clients=</tt>
245
+ # * <tt>Firm#client_ids=</tt>
217
246
  # * <tt>Firm#clients.clear</tt>
218
247
  # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
219
248
  # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
220
249
  # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
221
250
  # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
222
- # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("client_id" => id); c.save; c</tt>)
251
+ # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
223
252
  # The declaration can also include an options hash to specialize the behavior of the association.
224
253
  #
225
254
  # Options are:
@@ -254,7 +283,8 @@ module ActiveRecord
254
283
  # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
255
284
  # 'ORDER BY p.first_name'
256
285
  def has_many(association_id, options = {})
257
- validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql, :counter_sql ], options.keys)
286
+ validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql, :counter_sql,
287
+ :before_add, :after_add, :before_remove, :after_remove ], options.keys)
258
288
  association_name, association_class_name, association_class_primary_key_name =
259
289
  associate_identification(association_id, options[:class_name], options[:foreign_key])
260
290
 
@@ -271,7 +301,8 @@ module ActiveRecord
271
301
  end
272
302
 
273
303
  add_multiple_associated_save_callbacks(association_name)
274
-
304
+ add_association_callbacks(association_name, options)
305
+
275
306
  collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasManyAssociation)
276
307
 
277
308
  # deprecated api
@@ -299,7 +330,7 @@ module ActiveRecord
299
330
  # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
300
331
  #
301
332
  # Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
302
- # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
333
+ # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find(:first, :conditions => "account_id = #{id}")</tt>)
303
334
  # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
304
335
  # * <tt>Account#beneficiary.nil?</tt>
305
336
  # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
@@ -371,8 +402,8 @@ module ActiveRecord
371
402
  # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
372
403
  # * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
373
404
  # * <tt>Post#author.nil?</tt>
374
- # * <tt>Post#build_author</tt> (similar to <tt>Author.new("post_id" => id)</tt>)
375
- # * <tt>Post#create_author</tt> (similar to <tt>b = Author.new("post_id" => id); b.save; b</tt>)
405
+ # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
406
+ # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
376
407
  # The declaration can also include an options hash to specialize the behavior of the association.
377
408
  #
378
409
  # Options are:
@@ -447,7 +478,6 @@ module ActiveRecord
447
478
  # that you want available on retrieval. Note that any fields in the join table will override matching field names
448
479
  # in the two joined tables. As a consequence, having an "id" field in the join table usually has the undesirable
449
480
  # result of clobbering the "id" fields in either of the other two tables.
450
- #
451
481
  #
452
482
  # Adds the following methods for retrieval and query.
453
483
  # +collection+ is replaced with the symbol passed as the first argument, so
@@ -462,6 +492,8 @@ module ActiveRecord
462
492
  # (collection.concat_with_attributes is an alias to this method).
463
493
  # * <tt>collection.delete(object, ...)</tt> - removes one or more objects from the collection by removing their associations from the join table.
464
494
  # This does not destroy the objects.
495
+ # * <tt>collection=objects</tt> - replaces the collections content by deleting and adding objects as appropriate.
496
+ # * <tt>collection_singular_ids=ids</tt> - replace the collection by the objects identified by the primary keys in +ids+
465
497
  # * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
466
498
  # * <tt>collection.empty?</tt> - returns true if there are no associated objects.
467
499
  # * <tt>collection.size</tt> - returns the number of associated objects.
@@ -473,6 +505,8 @@ module ActiveRecord
473
505
  # * <tt>Developer#projects<<</tt>
474
506
  # * <tt>Developer#projects.push_with_attributes</tt>
475
507
  # * <tt>Developer#projects.delete</tt>
508
+ # * <tt>Developer#projects=</tt>
509
+ # * <tt>Developer#project_ids=</tt>
476
510
  # * <tt>Developer#projects.clear</tt>
477
511
  # * <tt>Developer#projects.empty?</tt>
478
512
  # * <tt>Developer#projects.size</tt>
@@ -506,9 +540,12 @@ module ActiveRecord
506
540
  # has_and_belongs_to_many :projects
507
541
  # has_and_belongs_to_many :nations, :class_name => "Country"
508
542
  # has_and_belongs_to_many :categories, :join_table => "prods_cats"
543
+ # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
544
+ # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
509
545
  def has_and_belongs_to_many(association_id, options = {})
510
546
  validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions,
511
- :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq ], options.keys)
547
+ :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq, :before_add, :after_add,
548
+ :before_remove, :after_remove ], options.keys)
512
549
  association_name, association_class_name, association_class_primary_key_name =
513
550
  associate_identification(association_id, options[:class_name], options[:foreign_key])
514
551
 
@@ -522,6 +559,7 @@ module ActiveRecord
522
559
 
523
560
  before_destroy_sql = "DELETE FROM #{options[:join_table]} WHERE #{association_class_primary_key_name} = \\\#{self.quoted_id}"
524
561
  module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
562
+ add_association_callbacks(association_name, options)
525
563
 
526
564
  # deprecated api
527
565
  deprecated_collection_count_method(association_name)
@@ -631,6 +669,10 @@ module ActiveRecord
631
669
  association.replace(new_value)
632
670
  association
633
671
  end
672
+
673
+ define_method("#{Inflector.singularize(association_name)}_ids=") do |new_value|
674
+ send("#{association_name}=", association_class_name.constantize.find(new_value))
675
+ end
634
676
  end
635
677
 
636
678
  def require_association_class(class_name)
@@ -656,7 +698,7 @@ module ActiveRecord
656
698
  end
657
699
 
658
700
  module_eval do
659
- after_save <<-end_eval
701
+ after_callback = <<-end_eval
660
702
  association = instance_variable_get("@#{association_name}")
661
703
  if association.respond_to?(:loaded?)
662
704
  if @new_record_before_save
@@ -668,13 +710,18 @@ module ActiveRecord
668
710
  association.send(:construct_sql) # reconstruct the SQL queries now that we know the owner's id
669
711
  end
670
712
  end_eval
713
+
714
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
715
+ after_create(after_callback)
716
+ after_update(after_callback)
671
717
  end
672
718
  end
673
719
 
674
720
  def association_constructor_method(constructor, association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
675
721
  define_method("#{constructor}_#{association_name}") do |*params|
676
- attributees = params.first unless params.empty?
677
- association = instance_variable_get("@#{association_name}")
722
+ attributees = params.first unless params.empty?
723
+ replace_existing = params[1].nil? ? true : params[1]
724
+ association = instance_variable_get("@#{association_name}")
678
725
 
679
726
  if association.nil?
680
727
  association = association_proxy_class.new(self,
@@ -683,7 +730,11 @@ module ActiveRecord
683
730
  instance_variable_set("@#{association_name}", association)
684
731
  end
685
732
 
686
- association.send(constructor, attributees)
733
+ if association_proxy_class == HasOneAssociation
734
+ association.send(constructor, attributees, replace_existing)
735
+ else
736
+ association.send(constructor, attributees)
737
+ end
687
738
  end
688
739
  end
689
740
 
@@ -715,7 +766,7 @@ module ActiveRecord
715
766
  next unless row[primary_key_table[reflection.table_name]]
716
767
 
717
768
  record.send(
718
- "#{reflection.name}=",
769
+ "set_#{reflection.name}_target",
719
770
  reflection.klass.send(:instantiate, extract_record(schema_abbreviations, reflection.table_name, row))
720
771
  )
721
772
  end
@@ -770,11 +821,21 @@ module ActiveRecord
770
821
  sql << reflections.collect { |reflection| association_join(reflection) }.to_s
771
822
  sql << "#{options[:joins]} " if options[:joins]
772
823
  add_conditions!(sql, options[:conditions])
824
+ add_sti_conditions!(sql, reflections)
773
825
  sql << "ORDER BY #{options[:order]} " if options[:order]
774
826
 
775
827
  return sanitize_sql(sql)
776
828
  end
777
829
 
830
+ def add_sti_conditions!(sql, reflections)
831
+ sti_sql = ""
832
+ reflections.each do |reflection|
833
+ sti_sql << " AND #{reflection.klass.send(:type_condition)}" unless reflection.klass.descends_from_active_record?
834
+ end
835
+ sti_sql.sub!(/AND/, "WHERE") unless sql =~ /where/i
836
+ sql << sti_sql
837
+ end
838
+
778
839
  def column_aliases(schema_abbreviations)
779
840
  schema_abbreviations.collect { |cn, tc| "#{tc.join(".")} AS #{cn}" }.join(", ")
780
841
  end
@@ -786,7 +847,7 @@ module ActiveRecord
786
847
  "#{reflection.options[:join_table]}.#{reflection.options[:foreign_key] || table_name.classify.foreign_key} = " +
787
848
  "#{table_name}.#{primary_key} " +
788
849
  " LEFT OUTER JOIN #{reflection.klass.table_name} ON " +
789
- "#{reflection.options[:join_table]}.#{reflection.options[:associated_foreign_key] || reflection.klass.table_name.classify.foreign_key} = " +
850
+ "#{reflection.options[:join_table]}.#{reflection.options[:association_foreign_key] || reflection.klass.table_name.classify.foreign_key} = " +
790
851
  "#{reflection.klass.table_name}.#{reflection.klass.primary_key} "
791
852
  when :has_many, :has_one
792
853
  " LEFT OUTER JOIN #{reflection.klass.table_name} ON " +
@@ -801,6 +862,18 @@ module ActiveRecord
801
862
  end
802
863
  end
803
864
 
865
+ def add_association_callbacks(association_name, options)
866
+ callbacks = %w(before_add after_add before_remove after_remove)
867
+ callbacks.each do |callback_name|
868
+ full_callback_name = "#{callback_name.to_s}_for_#{association_name.to_s}"
869
+ defined_callbacks = options[callback_name.to_sym]
870
+ if options.has_key?(callback_name.to_sym)
871
+ callback_array = defined_callbacks.kind_of?(Array) ? defined_callbacks : [defined_callbacks]
872
+ class_inheritable_reader full_callback_name.to_sym
873
+ write_inheritable_array(full_callback_name.to_sym, callback_array)
874
+ end
875
+ end
876
+ end
804
877
 
805
878
  def extract_record(schema_abbreviations, table_name, row)
806
879
  record = {}
@@ -1,3 +1,5 @@
1
+ require 'set'
2
+
1
3
  module ActiveRecord
2
4
  module Associations
3
5
  class AssociationCollection < AssociationProxy #:nodoc:
@@ -19,11 +21,13 @@ module ActiveRecord
19
21
  @owner.transaction do
20
22
  flatten_deeper(records).each do |record|
21
23
  raise_on_type_mismatch(record)
24
+ callback(:before_add, record)
22
25
  result &&= insert_record(record) unless @owner.new_record?
23
26
  @target << record
27
+ callback(:after_add, record)
24
28
  end
25
29
  end
26
-
30
+
27
31
  result and self
28
32
  end
29
33
 
@@ -38,8 +42,12 @@ module ActiveRecord
38
42
  return if records.empty?
39
43
 
40
44
  @owner.transaction do
45
+ records.each { |record| callback(:before_remove, record) }
41
46
  delete_records(records)
42
- records.each { |record| @target.delete(record) }
47
+ records.each do |record|
48
+ @target.delete(record)
49
+ callback(:after_remove, record)
50
+ end
43
51
  end
44
52
  end
45
53
 
@@ -83,11 +91,19 @@ module ActiveRecord
83
91
  collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
84
92
  end
85
93
 
94
+ # Replace this collection with +other_array+
95
+ # This will perform a diff and delete/add only records that have changed.
86
96
  def replace(other_array)
87
- other_array.each{ |val| raise_on_type_mismatch(val) }
97
+ other_array.each { |val| raise_on_type_mismatch(val) }
88
98
 
89
- @target = other_array
90
- @loaded = true
99
+ load_target
100
+ other = other_array.size < 100 ? other_array : other_array.to_set
101
+ current = @target.size < 100 ? @target : @target.to_set
102
+
103
+ @owner.transaction do
104
+ delete(@target.select { |v| !other.include?(v) })
105
+ concat(other_array.select { |v| !current.include?(v) })
106
+ end
91
107
  end
92
108
 
93
109
  private
@@ -103,6 +119,29 @@ module ActiveRecord
103
119
  def flatten_deeper(array)
104
120
  array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
105
121
  end
122
+
123
+ def callback(method, record)
124
+ callbacks_for(method).each do |callback|
125
+ case callback
126
+ when Symbol
127
+ @owner.send(callback, record)
128
+ when Proc, Method
129
+ callback.call(@owner, record)
130
+ else
131
+ if callback.respond_to?(method)
132
+ callback.send(method, @owner, record)
133
+ else
134
+ raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ def callbacks_for(callback_name)
141
+ full_callback_name = "#{callback_name.to_s}_for_#{@association_name.to_s}"
142
+ @owner.class.read_inheritable_attribute(full_callback_name.to_sym) or []
143
+ end
144
+
106
145
  end
107
146
  end
108
147
  end
@@ -66,7 +66,7 @@ module ActiveRecord
66
66
  # Otherwise, construct a query.
67
67
  else
68
68
  ids_list = ids.map { |id| @owner.send(:quote, id) }.join(',')
69
- records = find_target(@finder_sql.sub(/(ORDER BY|$)/, "AND j.#{@association_foreign_key} IN (#{ids_list}) \\1"))
69
+ records = find_target(@finder_sql.sub(/(ORDER BY|$)/, " AND j.#{@association_foreign_key} IN (#{ids_list}) \\1"))
70
70
  if records.size == ids.size
71
71
  if ids.size == 1 and !expects_array
72
72
  records.first
@@ -82,8 +82,10 @@ module ActiveRecord
82
82
  def push_with_attributes(record, join_attributes = {})
83
83
  raise_on_type_mismatch(record)
84
84
  join_attributes.each { |key, value| record[key.to_s] = value }
85
+ callback(:before_add, record)
85
86
  insert_record(record) unless @owner.new_record?
86
87
  @target << record
88
+ callback(:after_add, record)
87
89
  self
88
90
  end
89
91
 
@@ -104,7 +106,9 @@ module ActiveRecord
104
106
  end
105
107
 
106
108
  def insert_record(record)
107
- return false unless record.save
109
+ if record.new_record?
110
+ return false unless record.save
111
+ end
108
112
 
109
113
  if @options[:insert_sql]
110
114
  @owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
@@ -136,7 +140,7 @@ module ActiveRecord
136
140
 
137
141
  def delete_records(records)
138
142
  if sql = @options[:delete_sql]
139
- records.each { |record| @owner.connection.execute(sql) }
143
+ records.each { |record| @owner.connection.execute(interpolate_sql(sql, record)) }
140
144
  else
141
145
  ids = quoted_record_ids(records)
142
146
  sql = "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_foreign_key} IN (#{ids})"
@@ -145,7 +149,7 @@ module ActiveRecord
145
149
  end
146
150
 
147
151
  def construct_sql
148
- interpolate_sql_options!(@options, :finder_sql, :delete_sql)
152
+ interpolate_sql_options!(@options, :finder_sql)
149
153
 
150
154
  if @options[:finder_sql]
151
155
  @finder_sql = @options[:finder_sql]
@@ -156,6 +160,15 @@ module ActiveRecord
156
160
  "j.#{@association_class_primary_key_name} = #{@owner.quoted_id} "
157
161
 
158
162
  @finder_sql << " AND #{interpolate_sql(@options[:conditions])}" if @options[:conditions]
163
+
164
+ unless @association_class.descends_from_active_record?
165
+ type_condition = @association_class.send(:subclasses).inject("t.#{@association_class.inheritance_column} = '#{@association_class.name.demodulize}' ") do |condition, subclass|
166
+ condition << "OR t.#{@association_class.inheritance_column} = '#{subclass.name.demodulize}' "
167
+ end
168
+
169
+ @finder_sql << " AND (#{type_condition})"
170
+ end
171
+
159
172
  @finder_sql << " ORDER BY #{@order}" if @order
160
173
  end
161
174
  end