activerecord 3.1.12 → 3.2.22.1

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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +804 -338
  3. data/README.rdoc +3 -3
  4. data/examples/performance.rb +20 -1
  5. data/lib/active_record/aggregations.rb +1 -1
  6. data/lib/active_record/associations/alias_tracker.rb +3 -6
  7. data/lib/active_record/associations/association.rb +13 -45
  8. data/lib/active_record/associations/association_scope.rb +3 -15
  9. data/lib/active_record/associations/belongs_to_association.rb +1 -1
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +2 -1
  11. data/lib/active_record/associations/builder/association.rb +6 -4
  12. data/lib/active_record/associations/builder/belongs_to.rb +7 -4
  13. data/lib/active_record/associations/builder/collection_association.rb +2 -2
  14. data/lib/active_record/associations/builder/has_many.rb +4 -4
  15. data/lib/active_record/associations/builder/has_one.rb +5 -6
  16. data/lib/active_record/associations/builder/singular_association.rb +3 -16
  17. data/lib/active_record/associations/collection_association.rb +65 -32
  18. data/lib/active_record/associations/collection_proxy.rb +8 -41
  19. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -0
  20. data/lib/active_record/associations/has_many_association.rb +11 -7
  21. data/lib/active_record/associations/has_many_through_association.rb +19 -9
  22. data/lib/active_record/associations/has_one_association.rb +23 -13
  23. data/lib/active_record/associations/join_dependency/join_association.rb +6 -1
  24. data/lib/active_record/associations/join_dependency.rb +3 -3
  25. data/lib/active_record/associations/preloader/through_association.rb +3 -3
  26. data/lib/active_record/associations/preloader.rb +14 -10
  27. data/lib/active_record/associations/through_association.rb +8 -4
  28. data/lib/active_record/associations.rb +92 -76
  29. data/lib/active_record/attribute_assignment.rb +221 -0
  30. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  31. data/lib/active_record/attribute_methods/dirty.rb +21 -11
  32. data/lib/active_record/attribute_methods/primary_key.rb +62 -25
  33. data/lib/active_record/attribute_methods/read.rb +73 -83
  34. data/lib/active_record/attribute_methods/serialization.rb +120 -0
  35. data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
  36. data/lib/active_record/attribute_methods/write.rb +32 -6
  37. data/lib/active_record/attribute_methods.rb +231 -30
  38. data/lib/active_record/autosave_association.rb +44 -26
  39. data/lib/active_record/base.rb +227 -1708
  40. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +150 -148
  41. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +85 -29
  42. data/lib/active_record/connection_adapters/abstract/database_statements.rb +7 -34
  43. data/lib/active_record/connection_adapters/abstract/query_cache.rb +10 -2
  44. data/lib/active_record/connection_adapters/abstract/quoting.rb +7 -4
  45. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +39 -28
  46. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +48 -19
  47. data/lib/active_record/connection_adapters/abstract_adapter.rb +77 -42
  48. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +676 -0
  49. data/lib/active_record/connection_adapters/column.rb +37 -11
  50. data/lib/active_record/connection_adapters/mysql2_adapter.rb +133 -581
  51. data/lib/active_record/connection_adapters/mysql_adapter.rb +136 -693
  52. data/lib/active_record/connection_adapters/postgresql_adapter.rb +209 -97
  53. data/lib/active_record/connection_adapters/schema_cache.rb +69 -0
  54. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +2 -6
  55. data/lib/active_record/connection_adapters/sqlite_adapter.rb +62 -35
  56. data/lib/active_record/counter_cache.rb +9 -4
  57. data/lib/active_record/dynamic_finder_match.rb +12 -0
  58. data/lib/active_record/dynamic_matchers.rb +84 -0
  59. data/lib/active_record/errors.rb +11 -1
  60. data/lib/active_record/explain.rb +86 -0
  61. data/lib/active_record/explain_subscriber.rb +25 -0
  62. data/lib/active_record/fixtures/file.rb +65 -0
  63. data/lib/active_record/fixtures.rb +57 -86
  64. data/lib/active_record/identity_map.rb +3 -4
  65. data/lib/active_record/inheritance.rb +174 -0
  66. data/lib/active_record/integration.rb +60 -0
  67. data/lib/active_record/locking/optimistic.rb +33 -26
  68. data/lib/active_record/locking/pessimistic.rb +23 -1
  69. data/lib/active_record/log_subscriber.rb +8 -4
  70. data/lib/active_record/migration/command_recorder.rb +8 -8
  71. data/lib/active_record/migration.rb +68 -35
  72. data/lib/active_record/model_schema.rb +368 -0
  73. data/lib/active_record/nested_attributes.rb +60 -24
  74. data/lib/active_record/persistence.rb +57 -11
  75. data/lib/active_record/query_cache.rb +6 -6
  76. data/lib/active_record/querying.rb +58 -0
  77. data/lib/active_record/railtie.rb +37 -29
  78. data/lib/active_record/railties/controller_runtime.rb +3 -1
  79. data/lib/active_record/railties/databases.rake +213 -117
  80. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  81. data/lib/active_record/readonly_attributes.rb +26 -0
  82. data/lib/active_record/reflection.rb +7 -15
  83. data/lib/active_record/relation/batches.rb +7 -4
  84. data/lib/active_record/relation/calculations.rb +55 -16
  85. data/lib/active_record/relation/delegation.rb +49 -0
  86. data/lib/active_record/relation/finder_methods.rb +16 -11
  87. data/lib/active_record/relation/predicate_builder.rb +8 -6
  88. data/lib/active_record/relation/query_methods.rb +75 -9
  89. data/lib/active_record/relation/spawn_methods.rb +48 -7
  90. data/lib/active_record/relation.rb +78 -32
  91. data/lib/active_record/result.rb +10 -4
  92. data/lib/active_record/sanitization.rb +194 -0
  93. data/lib/active_record/schema_dumper.rb +12 -5
  94. data/lib/active_record/scoping/default.rb +142 -0
  95. data/lib/active_record/scoping/named.rb +200 -0
  96. data/lib/active_record/scoping.rb +152 -0
  97. data/lib/active_record/serialization.rb +1 -43
  98. data/lib/active_record/serializers/xml_serializer.rb +4 -45
  99. data/lib/active_record/session_store.rb +18 -16
  100. data/lib/active_record/store.rb +52 -0
  101. data/lib/active_record/test_case.rb +11 -7
  102. data/lib/active_record/timestamp.rb +17 -3
  103. data/lib/active_record/transactions.rb +27 -6
  104. data/lib/active_record/translation.rb +22 -0
  105. data/lib/active_record/validations/associated.rb +5 -4
  106. data/lib/active_record/validations/uniqueness.rb +8 -8
  107. data/lib/active_record/validations.rb +1 -1
  108. data/lib/active_record/version.rb +3 -3
  109. data/lib/active_record.rb +38 -3
  110. data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -1
  111. data/lib/rails/generators/active_record/migration/templates/migration.rb +12 -3
  112. data/lib/rails/generators/active_record/model/model_generator.rb +9 -1
  113. data/lib/rails/generators/active_record/model/templates/migration.rb +3 -5
  114. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  115. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +1 -5
  116. metadata +49 -28
  117. data/lib/active_record/named_scope.rb +0 -200
@@ -1,12 +1,5 @@
1
- require 'active_support/deprecation'
2
-
3
1
  module ActiveRecord
4
2
  module Associations
5
- AssociationCollection = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
6
- 'ActiveRecord::Associations::AssociationCollection',
7
- 'ActiveRecord::Associations::CollectionProxy'
8
- )
9
-
10
3
  # Association proxies in Active Record are middlemen between the object that
11
4
  # holds the association, known as the <tt>@owner</tt>, and the actual associated
12
5
  # object, known as the <tt>@target</tt>. The kind of association any proxy is
@@ -46,7 +39,7 @@ module ActiveRecord
46
39
  instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
47
40
 
48
41
  delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
49
- :lock, :readonly, :having, :to => :scoped
42
+ :lock, :readonly, :having, :pluck, :to => :scoped
50
43
 
51
44
  delegate :target, :load_target, :loaded?, :to => :@association
52
45
 
@@ -84,14 +77,15 @@ module ActiveRecord
84
77
  def method_missing(method, *args, &block)
85
78
  match = DynamicFinderMatch.match(method)
86
79
  if match && match.instantiator?
87
- send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r|
88
- proxy_association.send :set_owner_attributes, r
89
- proxy_association.send :add_to_target, r
90
- yield(r) if block_given?
80
+ send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |record|
81
+ proxy_association.send :set_owner_attributes, record
82
+ proxy_association.send :add_to_target, record
83
+ yield(record) if block_given?
84
+ end.tap do |record|
85
+ proxy_association.send :set_inverse_instance, record
91
86
  end
92
- end
93
87
 
94
- if target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method))
88
+ elsif target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method))
95
89
  if load_target
96
90
  if target.respond_to?(method)
97
91
  target.send(method, *args, &block)
@@ -134,33 +128,6 @@ module ActiveRecord
134
128
  proxy_association.reload
135
129
  self
136
130
  end
137
-
138
- def proxy_owner
139
- ActiveSupport::Deprecation.warn(
140
- "Calling record.#{@association.reflection.name}.proxy_owner is deprecated. Please use " \
141
- "record.association(:#{@association.reflection.name}).owner instead. Or, from an " \
142
- "association extension you can access proxy_association.owner."
143
- )
144
- proxy_association.owner
145
- end
146
-
147
- def proxy_target
148
- ActiveSupport::Deprecation.warn(
149
- "Calling record.#{@association.reflection.name}.proxy_target is deprecated. Please use " \
150
- "record.association(:#{@association.reflection.name}).target instead. Or, from an " \
151
- "association extension you can access proxy_association.target."
152
- )
153
- proxy_association.target
154
- end
155
-
156
- def proxy_reflection
157
- ActiveSupport::Deprecation.warn(
158
- "Calling record.#{@association.reflection.name}.proxy_reflection is deprecated. Please use " \
159
- "record.association(:#{@association.reflection.name}).reflection instead. Or, from an " \
160
- "association extension you can access proxy_association.reflection."
161
- )
162
- proxy_association.reflection
163
- end
164
131
  end
165
132
  end
166
133
  end
@@ -44,6 +44,7 @@ module ActiveRecord
44
44
 
45
45
  def delete_records(records, method)
46
46
  if sql = options[:delete_sql]
47
+ records = load_target if records == :all
47
48
  records.each { |record| owner.connection.delete(interpolate(sql, record)) }
48
49
  else
49
50
  relation = join_table
@@ -9,6 +9,7 @@ module ActiveRecord
9
9
 
10
10
  def insert_record(record, validate = true, raise = false)
11
11
  set_owner_attributes(record)
12
+ set_inverse_instance(record)
12
13
 
13
14
  if raise
14
15
  record.save!(:validate => validate)
@@ -23,7 +24,7 @@ module ActiveRecord
23
24
  #
24
25
  # If the association has a counter cache it gets that value. Otherwise
25
26
  # it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
26
- # there's one. Some configuration options like :group make it impossible
27
+ # there's one. Some configuration options like :group make it impossible
27
28
  # to do an SQL count, in those cases the array count will be used.
28
29
  #
29
30
  # That does not depend on whether the collection has already been loaded
@@ -49,15 +50,15 @@ module ActiveRecord
49
50
  [options[:limit], count].compact.min
50
51
  end
51
52
 
52
- def has_cached_counter?(reflection = reflection)
53
+ def has_cached_counter?(reflection = self.reflection)
53
54
  owner.attribute_present?(cached_counter_attribute_name(reflection))
54
55
  end
55
56
 
56
- def cached_counter_attribute_name(reflection = reflection)
57
+ def cached_counter_attribute_name(reflection = self.reflection)
57
58
  "#{reflection.name}_count"
58
59
  end
59
60
 
60
- def update_counter(difference, reflection = reflection)
61
+ def update_counter(difference, reflection = self.reflection)
61
62
  if has_cached_counter?(reflection)
62
63
  counter = cached_counter_attribute_name(reflection)
63
64
  owner.class.update_counters(owner.id, counter => difference)
@@ -76,7 +77,7 @@ module ActiveRecord
76
77
  # it will be decremented twice.
77
78
  #
78
79
  # Hence this method.
79
- def inverse_updates_counter_cache?(reflection = reflection)
80
+ def inverse_updates_counter_cache?(reflection = self.reflection)
80
81
  counter_name = cached_counter_attribute_name(reflection)
81
82
  reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
82
83
  inverse_reflection.counter_cache_column == counter_name
@@ -89,8 +90,7 @@ module ActiveRecord
89
90
  records.each { |r| r.destroy }
90
91
  update_counter(-records.length) unless inverse_updates_counter_cache?
91
92
  else
92
- keys = records.map { |r| r[reflection.association_primary_key] }
93
- scope = scoped.where(reflection.association_primary_key => keys)
93
+ scope = self.scoped.where(reflection.klass.primary_key => records)
94
94
 
95
95
  if method == :delete_all
96
96
  update_counter(-scope.delete_all)
@@ -99,6 +99,10 @@ module ActiveRecord
99
99
  end
100
100
  end
101
101
  end
102
+
103
+ def foreign_key_present?
104
+ owner.attribute_present?(reflection.association_primary_key)
105
+ end
102
106
  end
103
107
  end
104
108
  end
@@ -8,7 +8,9 @@ module ActiveRecord
8
8
 
9
9
  def initialize(owner, reflection)
10
10
  super
11
- @through_records = {}
11
+
12
+ @through_records = {}
13
+ @through_association = nil
12
14
  end
13
15
 
14
16
  # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
@@ -59,7 +61,7 @@ module ActiveRecord
59
61
  private
60
62
 
61
63
  def through_association
62
- owner.association(through_reflection.name)
64
+ @through_association ||= owner.association(through_reflection.name)
63
65
  end
64
66
 
65
67
  # We temporarily cache through record that has been build, because if we build a
@@ -71,7 +73,9 @@ module ActiveRecord
71
73
  # association
72
74
  def build_through_record(record)
73
75
  @through_records[record.object_id] ||= begin
74
- through_record = through_association.build(construct_join_attributes(record))
76
+ ensure_mutable
77
+
78
+ through_record = through_association.build
75
79
  through_record.send("#{source_reflection.name}=", record)
76
80
  through_record
77
81
  end
@@ -122,8 +126,7 @@ module ActiveRecord
122
126
  def delete_records(records, method)
123
127
  ensure_not_nested
124
128
 
125
- through = through_association
126
- scope = through.scoped.where(construct_join_attributes(*records))
129
+ scope = through_association.scoped.where(construct_join_attributes(*records))
127
130
 
128
131
  case method
129
132
  when :destroy
@@ -134,7 +137,12 @@ module ActiveRecord
134
137
  count = scope.delete_all
135
138
  end
136
139
 
137
- delete_through_records(through, records)
140
+ delete_through_records(records)
141
+
142
+ if source_reflection.options[:counter_cache]
143
+ counter = source_reflection.counter_cache_column
144
+ klass.decrement_counter counter, records.map(&:id)
145
+ end
138
146
 
139
147
  if through_reflection.macro == :has_many && update_through_counter?(method)
140
148
  update_counter(-count, through_reflection)
@@ -149,14 +157,16 @@ module ActiveRecord
149
157
  candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
150
158
  end
151
159
 
152
- def delete_through_records(through, records)
160
+ def delete_through_records(records)
153
161
  records.each do |record|
154
162
  through_records = through_records_for(record)
155
163
 
156
164
  if through_reflection.macro == :has_many
157
- through_records.each { |r| through.target.delete(r) }
165
+ through_records.each { |r| through_association.target.delete(r) }
158
166
  else
159
- through.target = nil if through_records.include?(through.target)
167
+ if through_records.include?(through_association.target)
168
+ through_association.target = nil
169
+ end
160
170
  end
161
171
 
162
172
  @through_records.delete(record.object_id)
@@ -8,19 +8,21 @@ module ActiveRecord
8
8
  raise_on_type_mismatch(record) if record
9
9
  load_target
10
10
 
11
- reflection.klass.transaction do
12
- if target && target != record
13
- remove_target!(options[:dependent]) unless target.destroyed?
14
- end
15
-
16
- if record
17
- set_owner_attributes(record)
18
- set_inverse_instance(record)
19
-
20
- if owner.persisted? && save && !record.save
21
- nullify_owner_attributes(record)
22
- set_owner_attributes(target) if target
23
- raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
11
+ # If target and record are nil, or target is equal to record,
12
+ # we don't need to have transaction.
13
+ if (target || record) && target != record
14
+ transaction_if(save) do
15
+ remove_target!(options[:dependent]) if target && !target.destroyed?
16
+
17
+ if record
18
+ set_owner_attributes(record)
19
+ set_inverse_instance(record)
20
+
21
+ if owner.persisted? && save && !record.save
22
+ nullify_owner_attributes(record)
23
+ set_owner_attributes(target) if target
24
+ raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
25
+ end
24
26
  end
25
27
  end
26
28
  end
@@ -68,6 +70,14 @@ module ActiveRecord
68
70
  def nullify_owner_attributes(record)
69
71
  record[reflection.foreign_key] = nil
70
72
  end
73
+
74
+ def transaction_if(value)
75
+ if value
76
+ reflection.klass.transaction { yield }
77
+ else
78
+ yield
79
+ end
80
+ end
71
81
  end
72
82
  end
73
83
  end
@@ -55,7 +55,12 @@ module ActiveRecord
55
55
 
56
56
  def find_parent_in(other_join_dependency)
57
57
  other_join_dependency.join_parts.detect do |join_part|
58
- parent == join_part
58
+ case parent
59
+ when JoinBase
60
+ parent.active_record == join_part.active_record
61
+ else
62
+ parent == join_part
63
+ end
59
64
  end
60
65
  end
61
66
 
@@ -13,7 +13,7 @@ module ActiveRecord
13
13
  @join_parts = [JoinBase.new(base)]
14
14
  @associations = {}
15
15
  @reflections = []
16
- @alias_tracker = AliasTracker.new(joins)
16
+ @alias_tracker = AliasTracker.new(base.connection, joins)
17
17
  @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
18
18
  build(associations)
19
19
  end
@@ -109,7 +109,7 @@ module ActiveRecord
109
109
  case associations
110
110
  when Symbol, String
111
111
  reflection = parent.reflections[associations.to_s.intern] or
112
- raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
112
+ raise ConfigurationError, "Association named '#{ associations }' was not found on #{parent.active_record.name}; perhaps you misspelled it?"
113
113
  unless join_association = find_join_association(reflection, parent)
114
114
  @reflections << reflection
115
115
  join_association = build_join_association(reflection, parent)
@@ -184,7 +184,7 @@ module ActiveRecord
184
184
 
185
185
  macro = join_part.reflection.macro
186
186
  if macro == :has_one
187
- return if record.association_cache.key?(join_part.reflection.name)
187
+ return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name)
188
188
  association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
189
189
  set_target_and_inverse(join_part, association, record)
190
190
  else
@@ -37,7 +37,8 @@ module ActiveRecord
37
37
  through_records = Array.wrap(owner.send(through_reflection.name))
38
38
 
39
39
  # Dont cache the association - we would only be caching a subset
40
- if reflection.options[:source_type] && through_reflection.collection?
40
+ if (preload_options != through_options) ||
41
+ (reflection.options[:source_type] && through_reflection.collection?)
41
42
  owner.association(through_reflection.name).reset
42
43
  end
43
44
 
@@ -55,8 +56,7 @@ module ActiveRecord
55
56
  through_options[:include] = options[:include] || options[:source]
56
57
  through_options[:conditions] = options[:conditions]
57
58
  end
58
-
59
- through_options[:order] = options[:order]
59
+ through_options[:order] = options[:order] if options.has_key?(:order)
60
60
  end
61
61
 
62
62
  through_options
@@ -30,17 +30,21 @@ module ActiveRecord
30
30
  # option references an association's column), it will fallback to the table
31
31
  # join strategy.
32
32
  class Preloader #:nodoc:
33
- autoload :Association, 'active_record/associations/preloader/association'
34
- autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
35
- autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
36
- autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
33
+ extend ActiveSupport::Autoload
37
34
 
38
- autoload :HasMany, 'active_record/associations/preloader/has_many'
39
- autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
40
- autoload :HasOne, 'active_record/associations/preloader/has_one'
41
- autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
42
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
43
- autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
35
+ eager_autoload do
36
+ autoload :Association, 'active_record/associations/preloader/association'
37
+ autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
38
+ autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
39
+ autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
40
+
41
+ autoload :HasMany, 'active_record/associations/preloader/has_many'
42
+ autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
43
+ autoload :HasOne, 'active_record/associations/preloader/has_one'
44
+ autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
45
+ autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
46
+ autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
47
+ end
44
48
 
45
49
  attr_reader :records, :associations, :options, :model
46
50
 
@@ -37,9 +37,7 @@ module ActiveRecord
37
37
  # situation it is more natural for the user to just create or modify their join records
38
38
  # directly as required.
39
39
  def construct_join_attributes(*records)
40
- if source_reflection.macro != :belongs_to
41
- raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
42
- end
40
+ ensure_mutable
43
41
 
44
42
  join_attributes = {
45
43
  source_reflection.foreign_key =>
@@ -64,7 +62,7 @@ module ActiveRecord
64
62
  # properly support stale-checking for nested associations.
65
63
  def stale_state
66
64
  if through_reflection.macro == :belongs_to
67
- owner[through_reflection.foreign_key].to_s
65
+ owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s
68
66
  end
69
67
  end
70
68
 
@@ -73,6 +71,12 @@ module ActiveRecord
73
71
  !owner[through_reflection.foreign_key].nil?
74
72
  end
75
73
 
74
+ def ensure_mutable
75
+ if source_reflection.macro != :belongs_to
76
+ raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
77
+ end
78
+ end
79
+
76
80
  def ensure_not_nested
77
81
  if reflection.nested?
78
82
  raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection)