activerecord 4.0.0.beta1 → 4.0.0.rc1

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +573 -30
  3. data/README.rdoc +3 -3
  4. data/lib/active_record.rb +8 -2
  5. data/lib/active_record/associations.rb +16 -9
  6. data/lib/active_record/associations/association.rb +8 -6
  7. data/lib/active_record/associations/association_scope.rb +2 -1
  8. data/lib/active_record/associations/belongs_to_association.rb +2 -2
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +1 -1
  10. data/lib/active_record/associations/builder/belongs_to.rb +37 -5
  11. data/lib/active_record/associations/collection_association.rb +38 -14
  12. data/lib/active_record/associations/collection_proxy.rb +18 -15
  13. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +3 -3
  14. data/lib/active_record/associations/has_many_association.rb +4 -3
  15. data/lib/active_record/associations/has_many_through_association.rb +1 -1
  16. data/lib/active_record/associations/has_one_association.rb +1 -1
  17. data/lib/active_record/associations/join_dependency.rb +29 -8
  18. data/lib/active_record/associations/join_dependency/join_association.rb +26 -6
  19. data/lib/active_record/associations/join_dependency/join_base.rb +2 -2
  20. data/lib/active_record/associations/join_dependency/join_part.rb +6 -6
  21. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +1 -1
  22. data/lib/active_record/associations/preloader/has_many_through.rb +6 -2
  23. data/lib/active_record/associations/through_association.rb +1 -1
  24. data/lib/active_record/attribute_assignment.rb +5 -5
  25. data/lib/active_record/attribute_methods.rb +20 -5
  26. data/lib/active_record/attribute_methods/dirty.rb +5 -1
  27. data/lib/active_record/attribute_methods/primary_key.rb +1 -1
  28. data/lib/active_record/attribute_methods/serialization.rb +9 -2
  29. data/lib/active_record/autosave_association.rb +19 -5
  30. data/lib/active_record/base.rb +3 -3
  31. data/lib/active_record/callbacks.rb +1 -1
  32. data/lib/active_record/coders/yaml_column.rb +8 -13
  33. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +3 -9
  34. data/lib/active_record/connection_adapters/abstract/database_limits.rb +1 -1
  35. data/lib/active_record/connection_adapters/abstract/database_statements.rb +2 -1
  36. data/lib/active_record/connection_adapters/abstract/quoting.rb +2 -8
  37. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +60 -61
  38. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +13 -2
  39. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +291 -153
  40. data/lib/active_record/connection_adapters/abstract/transaction.rb +1 -1
  41. data/lib/active_record/connection_adapters/abstract_adapter.rb +92 -1
  42. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +55 -29
  43. data/lib/active_record/connection_adapters/column.rb +4 -4
  44. data/lib/active_record/connection_adapters/connection_specification.rb +2 -2
  45. data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -3
  46. data/lib/active_record/connection_adapters/mysql_adapter.rb +5 -5
  47. data/lib/active_record/connection_adapters/postgresql/cast.rb +22 -2
  48. data/lib/active_record/connection_adapters/postgresql/oid.rb +25 -6
  49. data/lib/active_record/connection_adapters/postgresql/quoting.rb +26 -13
  50. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +50 -9
  51. data/lib/active_record/connection_adapters/postgresql_adapter.rb +53 -24
  52. data/lib/active_record/connection_adapters/schema_cache.rb +35 -7
  53. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +13 -5
  54. data/lib/active_record/connection_handling.rb +7 -7
  55. data/lib/active_record/core.rb +43 -8
  56. data/lib/active_record/counter_cache.rb +2 -1
  57. data/lib/active_record/errors.rb +11 -10
  58. data/lib/active_record/explain.rb +9 -7
  59. data/lib/active_record/explain_registry.rb +30 -0
  60. data/lib/active_record/explain_subscriber.rb +3 -2
  61. data/lib/active_record/fixture_set/file.rb +1 -2
  62. data/lib/active_record/fixtures.rb +13 -7
  63. data/lib/active_record/inheritance.rb +12 -4
  64. data/lib/active_record/integration.rb +3 -3
  65. data/lib/active_record/locking/optimistic.rb +2 -2
  66. data/lib/active_record/log_subscriber.rb +2 -2
  67. data/lib/active_record/migration.rb +69 -21
  68. data/lib/active_record/model_schema.rb +1 -1
  69. data/lib/active_record/nested_attributes.rb +98 -46
  70. data/lib/active_record/persistence.rb +3 -3
  71. data/lib/active_record/querying.rb +1 -1
  72. data/lib/active_record/railtie.rb +18 -4
  73. data/lib/active_record/railties/console_sandbox.rb +3 -2
  74. data/lib/active_record/railties/controller_runtime.rb +2 -1
  75. data/lib/active_record/railties/databases.rake +38 -80
  76. data/lib/active_record/reflection.rb +36 -3
  77. data/lib/active_record/relation.rb +18 -8
  78. data/lib/active_record/relation/calculations.rb +10 -5
  79. data/lib/active_record/relation/delegation.rb +3 -5
  80. data/lib/active_record/relation/finder_methods.rb +27 -14
  81. data/lib/active_record/relation/merger.rb +30 -2
  82. data/lib/active_record/relation/predicate_builder.rb +1 -6
  83. data/lib/active_record/relation/query_methods.rb +113 -16
  84. data/lib/active_record/runtime_registry.rb +17 -0
  85. data/lib/active_record/schema_dumper.rb +5 -1
  86. data/lib/active_record/schema_migration.rb +8 -5
  87. data/lib/active_record/scoping.rb +56 -2
  88. data/lib/active_record/scoping/default.rb +12 -11
  89. data/lib/active_record/scoping/named.rb +7 -3
  90. data/lib/active_record/serialization.rb +1 -1
  91. data/lib/active_record/statement_cache.rb +26 -0
  92. data/lib/active_record/tasks/database_tasks.rb +55 -10
  93. data/lib/active_record/tasks/firebird_database_tasks.rb +56 -0
  94. data/lib/active_record/tasks/mysql_database_tasks.rb +7 -2
  95. data/lib/active_record/tasks/oracle_database_tasks.rb +45 -0
  96. data/lib/active_record/tasks/sqlserver_database_tasks.rb +48 -0
  97. data/lib/active_record/timestamp.rb +6 -0
  98. data/lib/active_record/transactions.rb +7 -3
  99. data/lib/active_record/validations.rb +1 -2
  100. data/lib/active_record/validations/uniqueness.rb +7 -3
  101. data/lib/active_record/version.rb +7 -6
  102. data/lib/rails/generators/active_record/migration/migration_generator.rb +9 -2
  103. data/lib/rails/generators/active_record/{model/templates/migration.rb → migration/templates/create_table_migration.rb} +4 -0
  104. data/lib/rails/generators/active_record/model/model_generator.rb +1 -1
  105. data/lib/rails/generators/active_record/model/templates/model.rb +4 -1
  106. metadata +17 -12
  107. data/examples/associations.png +0 -0
@@ -15,6 +15,9 @@ module ActiveRecord
15
15
  # and if the inheritance column is attr accessible, it initializes an
16
16
  # instance of the given subclass instead of the base class
17
17
  def new(*args, &block)
18
+ if abstract_class? || self == Base
19
+ raise NotImplementedError, "#{self} is an abstract class and can not be instantiated."
20
+ end
18
21
  if (attrs = args.first).is_a?(Hash)
19
22
  if subclass = subclass_from_attrs(attrs)
20
23
  return subclass.new(*args, &block)
@@ -167,11 +170,16 @@ module ActiveRecord
167
170
  # this will ignore the inheritance column and return nil
168
171
  def subclass_from_attrs(attrs)
169
172
  subclass_name = attrs.with_indifferent_access[inheritance_column]
170
- return nil if subclass_name.blank? || subclass_name == self.name
171
- unless subclass = subclasses.detect { |sub| sub.name == subclass_name }
172
- raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
173
+
174
+ if subclass_name.present? && subclass_name != self.name
175
+ subclass = subclass_name.safe_constantize
176
+
177
+ unless descendants.include?(subclass)
178
+ raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
179
+ end
180
+
181
+ subclass
173
182
  end
174
- subclass
175
183
  end
176
184
  end
177
185
 
@@ -21,7 +21,7 @@ module ActiveRecord
21
21
  # <tt>resources :users</tt> route. Normally, +user_path+ will
22
22
  # construct a path with the user object's 'id' in it:
23
23
  #
24
- # user = User.find_by_name('Phusion')
24
+ # user = User.find_by(name: 'Phusion')
25
25
  # user_path(user) # => "/users/1"
26
26
  #
27
27
  # You can override +to_param+ in your model to make +user_path+ construct
@@ -33,7 +33,7 @@ module ActiveRecord
33
33
  # end
34
34
  # end
35
35
  #
36
- # user = User.find_by_name('Phusion')
36
+ # user = User.find_by(name: 'Phusion')
37
37
  # user_path(user) # => "/users/Phusion"
38
38
  def to_param
39
39
  # We can't use alias_method here, because method 'id' optimizes itself on the fly.
@@ -49,7 +49,7 @@ module ActiveRecord
49
49
  case
50
50
  when new_record?
51
51
  "#{self.class.model_name.cache_key}/new"
52
- when timestamp = self[:updated_at]
52
+ when timestamp = max_updated_column_timestamp
53
53
  timestamp = timestamp.utc.to_s(cache_timestamp_format)
54
54
  "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
55
55
  else
@@ -86,7 +86,7 @@ module ActiveRecord
86
86
  )
87
87
  ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names))
88
88
 
89
- affected_rows = connection.update stmt
89
+ affected_rows = self.class.connection.update stmt
90
90
 
91
91
  unless affected_rows == 1
92
92
  raise ActiveRecord::StaleObjectError.new(self, "update")
@@ -117,7 +117,7 @@ module ActiveRecord
117
117
  if locking_enabled?
118
118
  column_name = self.class.locking_column
119
119
  column = self.class.columns_hash[column_name]
120
- substitute = connection.substitute_at(column, relation.bind_values.length)
120
+ substitute = self.class.connection.substitute_at(column, relation.bind_values.length)
121
121
 
122
122
  relation = relation.where(self.class.arel_table[column_name].eq(substitute))
123
123
  relation.bind_values << [column, self[column_name].to_i]
@@ -3,11 +3,11 @@ module ActiveRecord
3
3
  IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
4
4
 
5
5
  def self.runtime=(value)
6
- Thread.current[:active_record_sql_runtime] = value
6
+ ActiveRecord::RuntimeRegistry.sql_runtime = value
7
7
  end
8
8
 
9
9
  def self.runtime
10
- Thread.current[:active_record_sql_runtime] ||= 0
10
+ ActiveRecord::RuntimeRegistry.sql_runtime ||= 0
11
11
  end
12
12
 
13
13
  def self.reset_runtime
@@ -102,7 +102,7 @@ module ActiveRecord
102
102
  # table definition.
103
103
  # * <tt>drop_table(name)</tt>: Drops the table called +name+.
104
104
  # * <tt>change_table(name, options)</tt>: Allows to make column alterations to
105
- # the table called +name+. It makes the table object availabe to a block that
105
+ # the table called +name+. It makes the table object available to a block that
106
106
  # can then add/remove columns, indexes or foreign keys to it.
107
107
  # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+
108
108
  # to +new_name+.
@@ -330,6 +330,24 @@ module ActiveRecord
330
330
  #
331
331
  # For a list of commands that are reversible, please see
332
332
  # <tt>ActiveRecord::Migration::CommandRecorder</tt>.
333
+ #
334
+ # == Transactional Migrations
335
+ #
336
+ # If the database adapter supports DDL transactions, all migrations will
337
+ # automatically be wrapped in a transaction. There are queries that you
338
+ # can't execute inside a transaction though, and for these situations
339
+ # you can turn the automatic transactions off.
340
+ #
341
+ # class ChangeEnum < ActiveRecord::Migration
342
+ # disable_ddl_transaction!
343
+ #
344
+ # def up
345
+ # execute "ALTER TYPE model_size ADD VALUE 'new_value'"
346
+ # end
347
+ # end
348
+ #
349
+ # Remember that you can still open your own transactions, even if you
350
+ # are in a Migration with <tt>self.disable_ddl_transaction!</tt>.
333
351
  class Migration
334
352
  autoload :CommandRecorder, 'active_record/migration/command_recorder'
335
353
 
@@ -351,6 +369,7 @@ module ActiveRecord
351
369
 
352
370
  class << self
353
371
  attr_accessor :delegate # :nodoc:
372
+ attr_accessor :disable_ddl_transaction # :nodoc:
354
373
  end
355
374
 
356
375
  def self.check_pending!
@@ -365,8 +384,16 @@ module ActiveRecord
365
384
  new.migrate direction
366
385
  end
367
386
 
368
- cattr_accessor :verbose
387
+ # Disable DDL transactions for this migration.
388
+ def self.disable_ddl_transaction!
389
+ @disable_ddl_transaction = true
390
+ end
391
+
392
+ def disable_ddl_transaction # :nodoc:
393
+ self.class.disable_ddl_transaction
394
+ end
369
395
 
396
+ cattr_accessor :verbose
370
397
  attr_accessor :name, :version
371
398
 
372
399
  def initialize(name = self.class.name, version = nil)
@@ -375,8 +402,8 @@ module ActiveRecord
375
402
  @connection = nil
376
403
  end
377
404
 
405
+ self.verbose = true
378
406
  # instantiate the delegate object after initialize is defined
379
- self.verbose = true
380
407
  self.delegate = new
381
408
 
382
409
  # Reverses the migration commands for the given block and
@@ -439,7 +466,7 @@ module ActiveRecord
439
466
  @connection.respond_to?(:reverting) && @connection.reverting
440
467
  end
441
468
 
442
- class ReversibleBlockHelper < Struct.new(:reverting)
469
+ class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc:
443
470
  def up
444
471
  yield unless reverting
445
472
  end
@@ -607,8 +634,17 @@ module ActiveRecord
607
634
  source_migrations = ActiveRecord::Migrator.migrations(path)
608
635
 
609
636
  source_migrations.each do |migration|
610
- source = File.read(migration.filename)
611
- source = "# This migration comes from #{scope} (originally #{migration.version})\n#{source}"
637
+ source = File.binread(migration.filename)
638
+ inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n"
639
+ if /\A#.*\b(?:en)?coding:\s*\S+/ =~ source
640
+ # If we have a magic comment in the original migration,
641
+ # insert our comment after the first newline(end of the magic comment line)
642
+ # so the magic keep working.
643
+ # Note that magic comments must be at the first line(except sh-bang).
644
+ source[/\n/] = "\n#{inserted_comment}"
645
+ else
646
+ source = "#{inserted_comment}#{source}"
647
+ end
612
648
 
613
649
  if duplicate = destination_migrations.detect { |m| m.name == migration.name }
614
650
  if options[:on_skip] && duplicate.scope != scope.to_s
@@ -622,7 +658,7 @@ module ActiveRecord
622
658
  old_path, migration.filename = migration.filename, new_path
623
659
  last = migration
624
660
 
625
- File.open(migration.filename, "w") { |f| f.write source }
661
+ File.binwrite(migration.filename, source)
626
662
  copied << migration
627
663
  options[:on_copy].call(scope, migration, old_path) if options[:on_copy]
628
664
  destination_migrations << migration
@@ -663,7 +699,7 @@ module ActiveRecord
663
699
  File.basename(filename)
664
700
  end
665
701
 
666
- delegate :migrate, :announce, :write, :to => :migration
702
+ delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration
667
703
 
668
704
  private
669
705
 
@@ -822,7 +858,7 @@ module ActiveRecord
822
858
  end
823
859
 
824
860
  def current_version
825
- migrated.sort.last || 0
861
+ migrated.max || 0
826
862
  end
827
863
 
828
864
  def current_migration
@@ -831,11 +867,15 @@ module ActiveRecord
831
867
  alias :current :current_migration
832
868
 
833
869
  def run
834
- target = migrations.detect { |m| m.version == @target_version }
835
- raise UnknownMigrationVersionError.new(@target_version) if target.nil?
836
- unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
837
- target.migrate(@direction)
838
- record_version_state_after_migrating(target.version)
870
+ migration = migrations.detect { |m| m.version == @target_version }
871
+ raise UnknownMigrationVersionError.new(@target_version) if migration.nil?
872
+ unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i))
873
+ begin
874
+ execute_migration_in_transaction(migration, @direction)
875
+ rescue => e
876
+ canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : ""
877
+ raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace
878
+ end
839
879
  end
840
880
  end
841
881
 
@@ -856,12 +896,9 @@ module ActiveRecord
856
896
  Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
857
897
 
858
898
  begin
859
- ddl_transaction do
860
- migration.migrate(@direction)
861
- record_version_state_after_migrating(migration.version)
862
- end
899
+ execute_migration_in_transaction(migration, @direction)
863
900
  rescue => e
864
- canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
901
+ canceled_msg = use_transaction?(migration) ? "this and " : ""
865
902
  raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
866
903
  end
867
904
  end
@@ -896,6 +933,13 @@ module ActiveRecord
896
933
  migrated.include?(migration.version.to_i)
897
934
  end
898
935
 
936
+ def execute_migration_in_transaction(migration, direction)
937
+ ddl_transaction(migration) do
938
+ migration.migrate(direction)
939
+ record_version_state_after_migrating(migration.version)
940
+ end
941
+ end
942
+
899
943
  def target
900
944
  migrations.detect { |m| m.version == @target_version }
901
945
  end
@@ -935,12 +979,16 @@ module ActiveRecord
935
979
  end
936
980
 
937
981
  # Wrap the migration in a transaction only if supported by the adapter.
938
- def ddl_transaction
939
- if Base.connection.supports_ddl_transactions?
982
+ def ddl_transaction(migration)
983
+ if use_transaction?(migration)
940
984
  Base.transaction { yield }
941
985
  else
942
986
  yield
943
987
  end
944
988
  end
989
+
990
+ def use_transaction?(migration)
991
+ !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions?
992
+ end
945
993
  end
946
994
  end
@@ -205,7 +205,7 @@ module ActiveRecord
205
205
 
206
206
  # Returns an array of column objects for the table associated with this class.
207
207
  def columns
208
- @columns ||= connection.schema_cache.columns[table_name].map do |col|
208
+ @columns ||= connection.schema_cache.columns(table_name).map do |col|
209
209
  col = col.dup
210
210
  col.primary = (col.name == primary_key)
211
211
  col
@@ -90,8 +90,9 @@ module ActiveRecord
90
90
  # accepts_nested_attributes_for :posts
91
91
  # end
92
92
  #
93
- # You can now set or update attributes on an associated post model through
94
- # the attribute hash.
93
+ # You can now set or update attributes on the associated posts through
94
+ # an attribute hash for a member: include the key +:posts_attributes+
95
+ # with an array of hashes of post attributes as a value.
95
96
  #
96
97
  # For each hash that does _not_ have an <tt>id</tt> key a new record will
97
98
  # be instantiated, unless the hash also contains a <tt>_destroy</tt> key
@@ -114,10 +115,10 @@ module ActiveRecord
114
115
  # hashes if they fail to pass your criteria. For example, the previous
115
116
  # example could be rewritten as:
116
117
  #
117
- # class Member < ActiveRecord::Base
118
- # has_many :posts
119
- # accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
120
- # end
118
+ # class Member < ActiveRecord::Base
119
+ # has_many :posts
120
+ # accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
121
+ # end
121
122
  #
122
123
  # params = { member: {
123
124
  # name: 'joe', posts_attributes: [
@@ -134,19 +135,19 @@ module ActiveRecord
134
135
  #
135
136
  # Alternatively, :reject_if also accepts a symbol for using methods:
136
137
  #
137
- # class Member < ActiveRecord::Base
138
- # has_many :posts
139
- # accepts_nested_attributes_for :posts, reject_if: :new_record?
140
- # end
138
+ # class Member < ActiveRecord::Base
139
+ # has_many :posts
140
+ # accepts_nested_attributes_for :posts, reject_if: :new_record?
141
+ # end
141
142
  #
142
- # class Member < ActiveRecord::Base
143
- # has_many :posts
144
- # accepts_nested_attributes_for :posts, reject_if: :reject_posts
143
+ # class Member < ActiveRecord::Base
144
+ # has_many :posts
145
+ # accepts_nested_attributes_for :posts, reject_if: :reject_posts
145
146
  #
146
- # def reject_posts(attributed)
147
- # attributed['title'].blank?
148
- # end
149
- # end
147
+ # def reject_posts(attributed)
148
+ # attributed['title'].blank?
149
+ # end
150
+ # end
150
151
  #
151
152
  # If the hash contains an <tt>id</tt> key that matches an already
152
153
  # associated record, the matching record will be modified:
@@ -183,6 +184,29 @@ module ActiveRecord
183
184
  # member.save
184
185
  # member.reload.posts.length # => 1
185
186
  #
187
+ # Nested attributes for an associated collection can also be passed in
188
+ # the form of a hash of hashes instead of an array of hashes:
189
+ #
190
+ # Member.create(name: 'joe',
191
+ # posts_attributes: { first: { title: 'Foo' },
192
+ # second: { title: 'Bar' } })
193
+ #
194
+ # has the same effect as
195
+ #
196
+ # Member.create(name: 'joe',
197
+ # posts_attributes: [ { title: 'Foo' },
198
+ # { title: 'Bar' } ])
199
+ #
200
+ # The keys of the hash which is the value for +:posts_attributes+ are
201
+ # ignored in this case.
202
+ # However, it is not allowed to use +'id'+ or +:id+ for one of
203
+ # such keys, otherwise the hash will be wrapped in an array and
204
+ # interpreted as an attribute hash for a single post.
205
+ #
206
+ # Passing attributes for an associated collection in the form of a hash
207
+ # of hashes can be used with hashes generated from HTTP/HTML parameters,
208
+ # where there maybe no natural way to submit an array of hashes.
209
+ #
186
210
  # === Saving
187
211
  #
188
212
  # All changes to models, including the destruction of those marked for
@@ -269,23 +293,36 @@ module ActiveRecord
269
293
  self.nested_attributes_options = nested_attributes_options
270
294
 
271
295
  type = (reflection.collection? ? :collection : :one_to_one)
272
-
273
- # def pirate_attributes=(attributes)
274
- # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options)
275
- # end
276
- generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
277
- if method_defined?(:#{association_name}_attributes=)
278
- remove_method(:#{association_name}_attributes=)
279
- end
280
- def #{association_name}_attributes=(attributes)
281
- assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
282
- end
283
- eoruby
296
+ generate_association_writer(association_name, type)
284
297
  else
285
298
  raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
286
299
  end
287
300
  end
288
301
  end
302
+
303
+ private
304
+
305
+ # Generates a writer method for this association. Serves as a point for
306
+ # accessing the objects in the association. For example, this method
307
+ # could generate the following:
308
+ #
309
+ # def pirate_attributes=(attributes)
310
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
311
+ # end
312
+ #
313
+ # This redirects the attempts to write objects in an association through
314
+ # the helper methods defined below. Makes it seem like the nested
315
+ # associations are just regular associations.
316
+ def generate_association_writer(association_name, type)
317
+ generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
318
+ if method_defined?(:#{association_name}_attributes=)
319
+ remove_method(:#{association_name}_attributes=)
320
+ end
321
+ def #{association_name}_attributes=(attributes)
322
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
323
+ end
324
+ eoruby
325
+ end
289
326
  end
290
327
 
291
328
  # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
@@ -325,7 +362,7 @@ module ActiveRecord
325
362
  assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
326
363
 
327
364
  elsif attributes['id'].present?
328
- raise_nested_attributes_record_not_found(association_name, attributes['id'])
365
+ raise_nested_attributes_record_not_found!(association_name, attributes['id'])
329
366
 
330
367
  elsif !reject_new_record?(association_name, attributes)
331
368
  method = "build_#{association_name}"
@@ -371,20 +408,7 @@ module ActiveRecord
371
408
  raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
372
409
  end
373
410
 
374
- if limit = options[:limit]
375
- limit = case limit
376
- when Symbol
377
- send(limit)
378
- when Proc
379
- limit.call
380
- else
381
- limit
382
- end
383
-
384
- if limit && attributes_collection.size > limit
385
- raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
386
- end
387
- end
411
+ check_record_limit!(options[:limit], attributes_collection)
388
412
 
389
413
  if attributes_collection.is_a? Hash
390
414
  keys = attributes_collection.keys
@@ -428,7 +452,30 @@ module ActiveRecord
428
452
  assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
429
453
  end
430
454
  else
431
- raise_nested_attributes_record_not_found(association_name, attributes['id'])
455
+ raise_nested_attributes_record_not_found!(association_name, attributes['id'])
456
+ end
457
+ end
458
+ end
459
+
460
+ # Takes in a limit and checks if the attributes_collection has too many
461
+ # records. The method will take limits in the form of symbols, procs, and
462
+ # number-like objects (anything that can be compared with an integer).
463
+ #
464
+ # Will raise an TooManyRecords error if the attributes_collection is
465
+ # larger than the limit.
466
+ def check_record_limit!(limit, attributes_collection)
467
+ if limit
468
+ limit = case limit
469
+ when Symbol
470
+ send(limit)
471
+ when Proc
472
+ limit.call
473
+ else
474
+ limit
475
+ end
476
+
477
+ if limit && attributes_collection.size > limit
478
+ raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
432
479
  end
433
480
  end
434
481
  end
@@ -452,6 +499,11 @@ module ActiveRecord
452
499
  has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
453
500
  end
454
501
 
502
+ # Determines if a record with the particular +attributes+ should be
503
+ # rejected by calling the reject_if Symbol or Proc (if defined).
504
+ # The reject_if option is defined by +accepts_nested_attributes_for+.
505
+ #
506
+ # Returns false if there is a +destroy_flag+ on the attributes.
455
507
  def call_reject_if(association_name, attributes)
456
508
  return false if has_destroy_flag?(attributes)
457
509
  case callback = self.nested_attributes_options[association_name][:reject_if]
@@ -462,7 +514,7 @@ module ActiveRecord
462
514
  end
463
515
  end
464
516
 
465
- def raise_nested_attributes_record_not_found(association_name, record_id)
517
+ def raise_nested_attributes_record_not_found!(association_name, record_id)
466
518
  raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
467
519
  end
468
520
  end