square-activerecord 3.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. data/CHANGELOG +6140 -0
  2. data/README.rdoc +222 -0
  3. data/examples/associations.png +0 -0
  4. data/examples/performance.rb +179 -0
  5. data/examples/simple.rb +14 -0
  6. data/lib/active_record.rb +124 -0
  7. data/lib/active_record/aggregations.rb +277 -0
  8. data/lib/active_record/association_preload.rb +430 -0
  9. data/lib/active_record/associations.rb +2307 -0
  10. data/lib/active_record/associations/association_collection.rb +572 -0
  11. data/lib/active_record/associations/association_proxy.rb +299 -0
  12. data/lib/active_record/associations/belongs_to_association.rb +91 -0
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +82 -0
  14. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +143 -0
  15. data/lib/active_record/associations/has_many_association.rb +128 -0
  16. data/lib/active_record/associations/has_many_through_association.rb +115 -0
  17. data/lib/active_record/associations/has_one_association.rb +143 -0
  18. data/lib/active_record/associations/has_one_through_association.rb +40 -0
  19. data/lib/active_record/associations/through_association_scope.rb +154 -0
  20. data/lib/active_record/attribute_methods.rb +60 -0
  21. data/lib/active_record/attribute_methods/before_type_cast.rb +30 -0
  22. data/lib/active_record/attribute_methods/dirty.rb +95 -0
  23. data/lib/active_record/attribute_methods/primary_key.rb +56 -0
  24. data/lib/active_record/attribute_methods/query.rb +39 -0
  25. data/lib/active_record/attribute_methods/read.rb +145 -0
  26. data/lib/active_record/attribute_methods/time_zone_conversion.rb +64 -0
  27. data/lib/active_record/attribute_methods/write.rb +43 -0
  28. data/lib/active_record/autosave_association.rb +369 -0
  29. data/lib/active_record/base.rb +1904 -0
  30. data/lib/active_record/callbacks.rb +284 -0
  31. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +364 -0
  32. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
  33. data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
  34. data/lib/active_record/connection_adapters/abstract/database_statements.rb +333 -0
  35. data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
  36. data/lib/active_record/connection_adapters/abstract/quoting.rb +73 -0
  37. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
  38. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +539 -0
  39. data/lib/active_record/connection_adapters/abstract_adapter.rb +217 -0
  40. data/lib/active_record/connection_adapters/mysql_adapter.rb +657 -0
  41. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1031 -0
  42. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -0
  43. data/lib/active_record/connection_adapters/sqlite_adapter.rb +401 -0
  44. data/lib/active_record/counter_cache.rb +115 -0
  45. data/lib/active_record/dynamic_finder_match.rb +56 -0
  46. data/lib/active_record/dynamic_scope_match.rb +23 -0
  47. data/lib/active_record/errors.rb +172 -0
  48. data/lib/active_record/fixtures.rb +1006 -0
  49. data/lib/active_record/locale/en.yml +40 -0
  50. data/lib/active_record/locking/optimistic.rb +172 -0
  51. data/lib/active_record/locking/pessimistic.rb +55 -0
  52. data/lib/active_record/log_subscriber.rb +48 -0
  53. data/lib/active_record/migration.rb +617 -0
  54. data/lib/active_record/named_scope.rb +138 -0
  55. data/lib/active_record/nested_attributes.rb +419 -0
  56. data/lib/active_record/observer.rb +125 -0
  57. data/lib/active_record/persistence.rb +290 -0
  58. data/lib/active_record/query_cache.rb +36 -0
  59. data/lib/active_record/railtie.rb +91 -0
  60. data/lib/active_record/railties/controller_runtime.rb +38 -0
  61. data/lib/active_record/railties/databases.rake +512 -0
  62. data/lib/active_record/reflection.rb +411 -0
  63. data/lib/active_record/relation.rb +394 -0
  64. data/lib/active_record/relation/batches.rb +89 -0
  65. data/lib/active_record/relation/calculations.rb +295 -0
  66. data/lib/active_record/relation/finder_methods.rb +363 -0
  67. data/lib/active_record/relation/predicate_builder.rb +48 -0
  68. data/lib/active_record/relation/query_methods.rb +303 -0
  69. data/lib/active_record/relation/spawn_methods.rb +132 -0
  70. data/lib/active_record/schema.rb +59 -0
  71. data/lib/active_record/schema_dumper.rb +195 -0
  72. data/lib/active_record/serialization.rb +60 -0
  73. data/lib/active_record/serializers/xml_serializer.rb +244 -0
  74. data/lib/active_record/session_store.rb +340 -0
  75. data/lib/active_record/test_case.rb +67 -0
  76. data/lib/active_record/timestamp.rb +88 -0
  77. data/lib/active_record/transactions.rb +359 -0
  78. data/lib/active_record/validations.rb +84 -0
  79. data/lib/active_record/validations/associated.rb +48 -0
  80. data/lib/active_record/validations/uniqueness.rb +190 -0
  81. data/lib/active_record/version.rb +10 -0
  82. data/lib/rails/generators/active_record.rb +19 -0
  83. data/lib/rails/generators/active_record/migration.rb +15 -0
  84. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  85. data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
  86. data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
  87. data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
  88. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  89. data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
  90. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  91. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  92. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  93. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
  94. metadata +223 -0
@@ -0,0 +1,145 @@
1
+ module ActiveRecord
2
+ module AttributeMethods
3
+ module Read
4
+ extend ActiveSupport::Concern
5
+
6
+ ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
7
+
8
+ included do
9
+ attribute_method_suffix ""
10
+
11
+ cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
12
+ self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
13
+
14
+ # Undefine id so it can be used as an attribute name
15
+ undef_method(:id) if method_defined?(:id)
16
+ end
17
+
18
+ module ClassMethods
19
+ # +cache_attributes+ allows you to declare which converted attribute values should
20
+ # be cached. Usually caching only pays off for attributes with expensive conversion
21
+ # methods, like time related columns (e.g. +created_at+, +updated_at+).
22
+ def cache_attributes(*attribute_names)
23
+ attribute_names.each {|attr| cached_attributes << attr.to_s}
24
+ end
25
+
26
+ # Returns the attributes which are cached. By default time related columns
27
+ # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
28
+ def cached_attributes
29
+ @cached_attributes ||=
30
+ columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map{|col| col.name}.to_set
31
+ end
32
+
33
+ # Returns +true+ if the provided attribute is being cached.
34
+ def cache_attribute?(attr_name)
35
+ cached_attributes.include?(attr_name)
36
+ end
37
+
38
+ protected
39
+ def define_method_attribute(attr_name)
40
+ if self.serialized_attributes[attr_name]
41
+ define_read_method_for_serialized_attribute(attr_name)
42
+ else
43
+ define_read_method(attr_name, attr_name, columns_hash[attr_name])
44
+ end
45
+
46
+ if attr_name == primary_key && attr_name != "id"
47
+ define_read_method('id', attr_name, columns_hash[attr_name])
48
+ end
49
+ end
50
+
51
+ private
52
+ # Define read method for serialized attribute.
53
+ def define_read_method_for_serialized_attribute(attr_name)
54
+ generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__)
55
+ end
56
+
57
+ # Define an attribute reader method. Cope with nil column.
58
+ # method_name is the same as attr_name except when a non-standard primary key is used,
59
+ # we still define #id as an accessor for the key
60
+ def define_read_method(method_name, attr_name, column)
61
+ cast_code = column.type_cast_code('v') if column
62
+ access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
63
+
64
+ unless attr_name.to_s == self.primary_key.to_s
65
+ access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
66
+ end
67
+
68
+ if cache_attribute?(attr_name)
69
+ access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
70
+ end
71
+
72
+ # Where possible, generate the method by evalling a string, as this will result in
73
+ # faster accesses because it avoids the block eval and then string eval incurred
74
+ # by the second branch.
75
+ #
76
+ # The second, slower, branch is necessary to support instances where the database
77
+ # returns columns with extra stuff in (like 'my_column(omg)').
78
+ if method_name =~ /^[a-zA-Z_]\w*[!?=]?$/
79
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__
80
+ def _#{method_name}
81
+ #{access_code}
82
+ end
83
+
84
+ alias #{method_name} _#{method_name}
85
+ STR
86
+ else
87
+ generated_attribute_methods.module_eval do
88
+ define_method("_#{method_name}") { eval(access_code) }
89
+ alias_method(method_name, "_#{method_name}")
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
96
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
97
+ def read_attribute(attr_name)
98
+ if respond_to? "_#{attr_name}"
99
+ send "_#{attr_name}" if @attributes.has_key?(attr_name.to_s)
100
+ else
101
+ _read_attribute attr_name
102
+ end
103
+ end
104
+
105
+ def _read_attribute(attr_name)
106
+ attr_name = attr_name.to_s
107
+ attr_name = self.class.primary_key if attr_name == 'id'
108
+ value = @attributes[attr_name]
109
+ unless value.nil?
110
+ if column = column_for_attribute(attr_name)
111
+ if unserializable_attribute?(attr_name, column)
112
+ unserialize_attribute(attr_name)
113
+ else
114
+ column.type_cast(value)
115
+ end
116
+ else
117
+ value
118
+ end
119
+ end
120
+ end
121
+
122
+ # Returns true if the attribute is of a text column and marked for serialization.
123
+ def unserializable_attribute?(attr_name, column)
124
+ column.text? && self.class.serialized_attributes[attr_name]
125
+ end
126
+
127
+ # Returns the unserialized object of the attribute.
128
+ def unserialize_attribute(attr_name)
129
+ unserialized_object = object_from_yaml(@attributes[attr_name])
130
+
131
+ if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
132
+ @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
133
+ else
134
+ raise SerializationTypeMismatch,
135
+ "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
136
+ end
137
+ end
138
+
139
+ private
140
+ def attribute(attribute_name)
141
+ read_attribute(attribute_name)
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,64 @@
1
+ module ActiveRecord
2
+ module AttributeMethods
3
+ module TimeZoneConversion
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ cattr_accessor :time_zone_aware_attributes, :instance_writer => false
8
+ self.time_zone_aware_attributes = false
9
+
10
+ class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
11
+ self.skip_time_zone_conversion_for_attributes = []
12
+ end
13
+
14
+ module ClassMethods
15
+ protected
16
+ # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
17
+ # This enhanced read method automatically converts the UTC time stored in the database to the time
18
+ # zone stored in Time.zone.
19
+ def define_method_attribute(attr_name)
20
+ if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
21
+ method_body, line = <<-EOV, __LINE__ + 1
22
+ def _#{attr_name}(reload = false)
23
+ cached = @attributes_cache['#{attr_name}']
24
+ return cached if cached && !reload
25
+ time = _read_attribute('#{attr_name}')
26
+ @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
27
+ end
28
+ alias #{attr_name} _#{attr_name}
29
+ EOV
30
+ generated_attribute_methods.module_eval(method_body, __FILE__, line)
31
+ else
32
+ super
33
+ end
34
+ end
35
+
36
+ # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
37
+ # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
38
+ def define_method_attribute=(attr_name)
39
+ if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
40
+ method_body, line = <<-EOV, __LINE__ + 1
41
+ def #{attr_name}=(original_time)
42
+ time = original_time
43
+ unless time.acts_like?(:time)
44
+ time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
45
+ end
46
+ time = time.in_time_zone rescue nil if time
47
+ write_attribute(:#{attr_name}, original_time)
48
+ @attributes_cache["#{attr_name}"] = time
49
+ end
50
+ EOV
51
+ generated_attribute_methods.module_eval(method_body, __FILE__, line)
52
+ else
53
+ super
54
+ end
55
+ end
56
+
57
+ private
58
+ def create_time_zone_conversion_attribute?(name, column)
59
+ time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,43 @@
1
+ module ActiveRecord
2
+ module AttributeMethods
3
+ module Write
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attribute_method_suffix "="
8
+ end
9
+
10
+ module ClassMethods
11
+ protected
12
+ def define_method_attribute=(attr_name)
13
+ if attr_name =~ /^[a-zA-Z_]\w*[!?=]?$/
14
+ generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
15
+ else
16
+ generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value|
17
+ write_attribute(attr_name, new_value)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings
24
+ # for fixnum and float columns are turned into +nil+.
25
+ def write_attribute(attr_name, value)
26
+ attr_name = attr_name.to_s
27
+ attr_name = self.class.primary_key if attr_name == 'id'
28
+ @attributes_cache.delete(attr_name)
29
+ if (column = column_for_attribute(attr_name)) && column.number?
30
+ @attributes[attr_name] = convert_number_column_value(value)
31
+ else
32
+ @attributes[attr_name] = value
33
+ end
34
+ end
35
+
36
+ private
37
+ # Handle *= for method_missing.
38
+ def attribute=(attribute_name, value)
39
+ write_attribute(attribute_name, value)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,369 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module ActiveRecord
4
+ # = Active Record Autosave Association
5
+ #
6
+ # +AutosaveAssociation+ is a module that takes care of automatically saving
7
+ # associacted records when their parent is saved. In addition to saving, it
8
+ # also destroys any associated records that were marked for destruction.
9
+ # (See +mark_for_destruction+ and <tt>marked_for_destruction?</tt>).
10
+ #
11
+ # Saving of the parent, its associations, and the destruction of marked
12
+ # associations, all happen inside a transaction. This should never leave the
13
+ # database in an inconsistent state.
14
+ #
15
+ # If validations for any of the associations fail, their error messages will
16
+ # be applied to the parent.
17
+ #
18
+ # Note that it also means that associations marked for destruction won't
19
+ # be destroyed directly. They will however still be marked for destruction.
20
+ #
21
+ # Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>.
22
+ # When the <tt>:autosave</tt> option is not present new associations are saved.
23
+ #
24
+ # === One-to-one Example
25
+ #
26
+ # class Post
27
+ # has_one :author, :autosave => true
28
+ # end
29
+ #
30
+ # Saving changes to the parent and its associated model can now be performed
31
+ # automatically _and_ atomically:
32
+ #
33
+ # post = Post.find(1)
34
+ # post.title # => "The current global position of migrating ducks"
35
+ # post.author.name # => "alloy"
36
+ #
37
+ # post.title = "On the migration of ducks"
38
+ # post.author.name = "Eloy Duran"
39
+ #
40
+ # post.save
41
+ # post.reload
42
+ # post.title # => "On the migration of ducks"
43
+ # post.author.name # => "Eloy Duran"
44
+ #
45
+ # Destroying an associated model, as part of the parent's save action, is as
46
+ # simple as marking it for destruction:
47
+ #
48
+ # post.author.mark_for_destruction
49
+ # post.author.marked_for_destruction? # => true
50
+ #
51
+ # Note that the model is _not_ yet removed from the database:
52
+ #
53
+ # id = post.author.id
54
+ # Author.find_by_id(id).nil? # => false
55
+ #
56
+ # post.save
57
+ # post.reload.author # => nil
58
+ #
59
+ # Now it _is_ removed from the database:
60
+ #
61
+ # Author.find_by_id(id).nil? # => true
62
+ #
63
+ # === One-to-many Example
64
+ #
65
+ # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
66
+ #
67
+ # class Post
68
+ # has_many :comments # :autosave option is no declared
69
+ # end
70
+ #
71
+ # post = Post.new(:title => 'ruby rocks')
72
+ # post.comments.build(:body => 'hello world')
73
+ # post.save # => saves both post and comment
74
+ #
75
+ # post = Post.create(:title => 'ruby rocks')
76
+ # post.comments.build(:body => 'hello world')
77
+ # post.save # => saves both post and comment
78
+ #
79
+ # post = Post.create(:title => 'ruby rocks')
80
+ # post.comments.create(:body => 'hello world')
81
+ # post.save # => saves both post and comment
82
+ #
83
+ # When <tt>:autosave</tt> is true all children is saved, no matter whether they are new records:
84
+ #
85
+ # class Post
86
+ # has_many :comments, :autosave => true
87
+ # end
88
+ #
89
+ # post = Post.create(:title => 'ruby rocks')
90
+ # post.comments.create(:body => 'hello world')
91
+ # post.comments[0].body = 'hi everyone'
92
+ # post.save # => saves both post and comment, with 'hi everyone' as body
93
+ #
94
+ # Destroying one of the associated models as part of the parent's save action
95
+ # is as simple as marking it for destruction:
96
+ #
97
+ # post.comments.last.mark_for_destruction
98
+ # post.comments.last.marked_for_destruction? # => true
99
+ # post.comments.length # => 2
100
+ #
101
+ # Note that the model is _not_ yet removed from the database:
102
+ #
103
+ # id = post.comments.last.id
104
+ # Comment.find_by_id(id).nil? # => false
105
+ #
106
+ # post.save
107
+ # post.reload.comments.length # => 1
108
+ #
109
+ # Now it _is_ removed from the database:
110
+ #
111
+ # Comment.find_by_id(id).nil? # => true
112
+ #
113
+ # === Validation
114
+ #
115
+ # Children records are validated unless <tt>:validate</tt> is +false+.
116
+ module AutosaveAssociation
117
+ extend ActiveSupport::Concern
118
+
119
+ ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }
120
+
121
+ included do
122
+ ASSOCIATION_TYPES.each do |type|
123
+ send("valid_keys_for_#{type}_association") << :autosave
124
+ end
125
+ end
126
+
127
+ module ClassMethods
128
+ private
129
+
130
+ # def belongs_to(name, options = {})
131
+ # super
132
+ # add_autosave_association_callbacks(reflect_on_association(name))
133
+ # end
134
+ ASSOCIATION_TYPES.each do |type|
135
+ module_eval <<-CODE, __FILE__, __LINE__ + 1
136
+ def #{type}(name, options = {})
137
+ super
138
+ add_autosave_association_callbacks(reflect_on_association(name))
139
+ end
140
+ CODE
141
+ end
142
+
143
+ # Adds validation and save callbacks for the association as specified by
144
+ # the +reflection+.
145
+ #
146
+ # For performance reasons, we don't check whether to validate at runtime.
147
+ # However the validation and callback methods are lazy and those methods
148
+ # get created when they are invoked for the very first time. However,
149
+ # this can change, for instance, when using nested attributes, which is
150
+ # called _after_ the association has been defined. Since we don't want
151
+ # the callbacks to get defined multiple times, there are guards that
152
+ # check if the save or validation methods have already been defined
153
+ # before actually defining them.
154
+ def add_autosave_association_callbacks(reflection)
155
+ save_method = :"autosave_associated_records_for_#{reflection.name}"
156
+ validation_method = :"validate_associated_records_for_#{reflection.name}"
157
+ collection = reflection.collection?
158
+
159
+ unless method_defined?(save_method)
160
+ if collection
161
+ before_save :before_save_collection_association
162
+
163
+ define_method(save_method) { save_collection_association(reflection) }
164
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
165
+ after_create save_method
166
+ after_update save_method
167
+ else
168
+ if reflection.macro == :has_one
169
+ define_method(save_method) { save_has_one_association(reflection) }
170
+ after_save save_method
171
+ else
172
+ define_method(save_method) { save_belongs_to_association(reflection) }
173
+ before_save save_method
174
+ end
175
+ end
176
+ end
177
+
178
+ if reflection.validate? && !method_defined?(validation_method)
179
+ method = (collection ? :validate_collection_association : :validate_single_association)
180
+ define_method(validation_method) { send(method, reflection) }
181
+ validate validation_method
182
+ end
183
+ end
184
+ end
185
+
186
+ # Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
187
+ def reload(options = nil)
188
+ @marked_for_destruction = false
189
+ super
190
+ end
191
+
192
+ # Marks this record to be destroyed as part of the parents save transaction.
193
+ # This does _not_ actually destroy the record instantly, rather child record will be destroyed
194
+ # when <tt>parent.save</tt> is called.
195
+ #
196
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
197
+ def mark_for_destruction
198
+ @marked_for_destruction = true
199
+ end
200
+
201
+ # Returns whether or not this record will be destroyed as part of the parents save transaction.
202
+ #
203
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
204
+ def marked_for_destruction?
205
+ @marked_for_destruction
206
+ end
207
+
208
+ # Returns whether or not this record has been changed in any way (including whether
209
+ # any of its nested autosave associations are likewise changed)
210
+ def changed_for_autosave?
211
+ new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
212
+ end
213
+
214
+ private
215
+
216
+ # Returns the record for an association collection that should be validated
217
+ # or saved. If +autosave+ is +false+ only new records will be returned,
218
+ # unless the parent is/was a new record itself.
219
+ def associated_records_to_validate_or_save(association, new_record, autosave)
220
+ if new_record
221
+ association
222
+ elsif autosave
223
+ association.target.find_all { |record| record.changed_for_autosave? }
224
+ else
225
+ association.target.find_all { |record| record.new_record? }
226
+ end
227
+ end
228
+
229
+ # go through nested autosave associations that are loaded in memory (without loading
230
+ # any new ones), and return true if is changed for autosave
231
+ def nested_records_changed_for_autosave?
232
+ self.class.reflect_on_all_autosave_associations.any? do |reflection|
233
+ association = association_instance_get(reflection.name)
234
+ association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
235
+ end
236
+ end
237
+
238
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
239
+ # turned on for the association.
240
+ def validate_single_association(reflection)
241
+ if (association = association_instance_get(reflection.name)) && !association.target.nil?
242
+ association_valid?(reflection, association)
243
+ end
244
+ end
245
+
246
+ # Validate the associated records if <tt>:validate</tt> or
247
+ # <tt>:autosave</tt> is turned on for the association specified by
248
+ # +reflection+.
249
+ def validate_collection_association(reflection)
250
+ if association = association_instance_get(reflection.name)
251
+ if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
252
+ records.each { |record| association_valid?(reflection, record) }
253
+ end
254
+ end
255
+ end
256
+
257
+ # Returns whether or not the association is valid and applies any errors to
258
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
259
+ # enabled records if they're marked_for_destruction? or destroyed.
260
+ def association_valid?(reflection, association)
261
+ return true if association.destroyed? || association.marked_for_destruction?
262
+
263
+ unless valid = association.valid?
264
+ if reflection.options[:autosave]
265
+ association.errors.each do |attribute, message|
266
+ attribute = "#{reflection.name}.#{attribute}"
267
+ errors[attribute] << message
268
+ errors[attribute].uniq!
269
+ end
270
+ else
271
+ errors.add(reflection.name)
272
+ end
273
+ end
274
+ valid
275
+ end
276
+
277
+ # Is used as a before_save callback to check while saving a collection
278
+ # association whether or not the parent was a new record before saving.
279
+ def before_save_collection_association
280
+ @new_record_before_save = new_record?
281
+ true
282
+ end
283
+
284
+ # Saves any new associated records, or all loaded autosave associations if
285
+ # <tt>:autosave</tt> is enabled on the association.
286
+ #
287
+ # In addition, it destroys all children that were marked for destruction
288
+ # with mark_for_destruction.
289
+ #
290
+ # This all happens inside a transaction, _if_ the Transactions module is included into
291
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
292
+ def save_collection_association(reflection)
293
+ if association = association_instance_get(reflection.name)
294
+ autosave = reflection.options[:autosave]
295
+
296
+ if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
297
+ records.each do |record|
298
+ next if record.destroyed?
299
+
300
+ if autosave && record.marked_for_destruction?
301
+ association.destroy(record)
302
+ elsif autosave != false && (@new_record_before_save || record.new_record?)
303
+ if autosave
304
+ saved = association.send(:insert_record, record, false, false)
305
+ else
306
+ association.send(:insert_record, record)
307
+ end
308
+ elsif autosave
309
+ saved = record.save(:validate => false)
310
+ end
311
+
312
+ raise ActiveRecord::Rollback if saved == false
313
+ end
314
+ end
315
+
316
+ # reconstruct the SQL queries now that we know the owner's id
317
+ association.send(:construct_sql) if association.respond_to?(:construct_sql)
318
+ end
319
+ end
320
+
321
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
322
+ # on the association.
323
+ #
324
+ # In addition, it will destroy the association if it was marked for
325
+ # destruction with mark_for_destruction.
326
+ #
327
+ # This all happens inside a transaction, _if_ the Transactions module is included into
328
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
329
+ def save_has_one_association(reflection)
330
+ if (association = association_instance_get(reflection.name)) && !association.target.nil? && !association.destroyed?
331
+ autosave = reflection.options[:autosave]
332
+
333
+ if autosave && association.marked_for_destruction?
334
+ association.destroy
335
+ else
336
+ key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
337
+ if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
338
+ association[reflection.primary_key_name] = key
339
+ saved = association.save(:validate => !autosave)
340
+ raise ActiveRecord::Rollback if !saved && autosave
341
+ saved
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
348
+ #
349
+ # In addition, it will destroy the association if it was marked for destruction.
350
+ def save_belongs_to_association(reflection)
351
+ if (association = association_instance_get(reflection.name)) && !association.destroyed?
352
+ autosave = reflection.options[:autosave]
353
+
354
+ if autosave && association.marked_for_destruction?
355
+ association.destroy
356
+ elsif autosave != false
357
+ saved = association.save(:validate => !autosave) if association.new_record? || autosave
358
+
359
+ if association.updated?
360
+ association_id = association.send(reflection.options[:primary_key] || :id)
361
+ self[reflection.primary_key_name] = association_id
362
+ end
363
+
364
+ saved if autosave
365
+ end
366
+ end
367
+ end
368
+ end
369
+ end