sskirby-activerecord 3.2.1

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 (150) hide show
  1. data/CHANGELOG.md +6749 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +222 -0
  4. data/examples/associations.png +0 -0
  5. data/examples/performance.rb +177 -0
  6. data/examples/simple.rb +14 -0
  7. data/lib/active_record.rb +147 -0
  8. data/lib/active_record/aggregations.rb +255 -0
  9. data/lib/active_record/associations.rb +1604 -0
  10. data/lib/active_record/associations/alias_tracker.rb +79 -0
  11. data/lib/active_record/associations/association.rb +239 -0
  12. data/lib/active_record/associations/association_scope.rb +119 -0
  13. data/lib/active_record/associations/belongs_to_association.rb +79 -0
  14. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +34 -0
  15. data/lib/active_record/associations/builder/association.rb +55 -0
  16. data/lib/active_record/associations/builder/belongs_to.rb +85 -0
  17. data/lib/active_record/associations/builder/collection_association.rb +75 -0
  18. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +57 -0
  19. data/lib/active_record/associations/builder/has_many.rb +71 -0
  20. data/lib/active_record/associations/builder/has_one.rb +62 -0
  21. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  22. data/lib/active_record/associations/collection_association.rb +574 -0
  23. data/lib/active_record/associations/collection_proxy.rb +132 -0
  24. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +62 -0
  25. data/lib/active_record/associations/has_many_association.rb +108 -0
  26. data/lib/active_record/associations/has_many_through_association.rb +180 -0
  27. data/lib/active_record/associations/has_one_association.rb +73 -0
  28. data/lib/active_record/associations/has_one_through_association.rb +36 -0
  29. data/lib/active_record/associations/join_dependency.rb +214 -0
  30. data/lib/active_record/associations/join_dependency/join_association.rb +154 -0
  31. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  32. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  33. data/lib/active_record/associations/join_helper.rb +55 -0
  34. data/lib/active_record/associations/preloader.rb +177 -0
  35. data/lib/active_record/associations/preloader/association.rb +127 -0
  36. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  37. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  38. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  39. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  40. data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
  41. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  42. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  43. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  44. data/lib/active_record/associations/preloader/through_association.rb +67 -0
  45. data/lib/active_record/associations/singular_association.rb +64 -0
  46. data/lib/active_record/associations/through_association.rb +83 -0
  47. data/lib/active_record/attribute_assignment.rb +221 -0
  48. data/lib/active_record/attribute_methods.rb +272 -0
  49. data/lib/active_record/attribute_methods/before_type_cast.rb +31 -0
  50. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  51. data/lib/active_record/attribute_methods/dirty.rb +101 -0
  52. data/lib/active_record/attribute_methods/primary_key.rb +114 -0
  53. data/lib/active_record/attribute_methods/query.rb +39 -0
  54. data/lib/active_record/attribute_methods/read.rb +135 -0
  55. data/lib/active_record/attribute_methods/serialization.rb +93 -0
  56. data/lib/active_record/attribute_methods/time_zone_conversion.rb +62 -0
  57. data/lib/active_record/attribute_methods/write.rb +69 -0
  58. data/lib/active_record/autosave_association.rb +422 -0
  59. data/lib/active_record/base.rb +716 -0
  60. data/lib/active_record/callbacks.rb +275 -0
  61. data/lib/active_record/coders/yaml_column.rb +41 -0
  62. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +452 -0
  63. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +188 -0
  64. data/lib/active_record/connection_adapters/abstract/database_limits.rb +58 -0
  65. data/lib/active_record/connection_adapters/abstract/database_statements.rb +388 -0
  66. data/lib/active_record/connection_adapters/abstract/query_cache.rb +82 -0
  67. data/lib/active_record/connection_adapters/abstract/quoting.rb +115 -0
  68. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +492 -0
  69. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +598 -0
  70. data/lib/active_record/connection_adapters/abstract_adapter.rb +296 -0
  71. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
  72. data/lib/active_record/connection_adapters/column.rb +270 -0
  73. data/lib/active_record/connection_adapters/mysql2_adapter.rb +288 -0
  74. data/lib/active_record/connection_adapters/mysql_adapter.rb +426 -0
  75. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1261 -0
  76. data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
  77. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -0
  78. data/lib/active_record/connection_adapters/sqlite_adapter.rb +577 -0
  79. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  80. data/lib/active_record/counter_cache.rb +119 -0
  81. data/lib/active_record/dynamic_finder_match.rb +56 -0
  82. data/lib/active_record/dynamic_matchers.rb +79 -0
  83. data/lib/active_record/dynamic_scope_match.rb +23 -0
  84. data/lib/active_record/errors.rb +195 -0
  85. data/lib/active_record/explain.rb +85 -0
  86. data/lib/active_record/explain_subscriber.rb +21 -0
  87. data/lib/active_record/fixtures.rb +906 -0
  88. data/lib/active_record/fixtures/file.rb +65 -0
  89. data/lib/active_record/identity_map.rb +156 -0
  90. data/lib/active_record/inheritance.rb +167 -0
  91. data/lib/active_record/integration.rb +49 -0
  92. data/lib/active_record/locale/en.yml +40 -0
  93. data/lib/active_record/locking/optimistic.rb +183 -0
  94. data/lib/active_record/locking/pessimistic.rb +77 -0
  95. data/lib/active_record/log_subscriber.rb +68 -0
  96. data/lib/active_record/migration.rb +765 -0
  97. data/lib/active_record/migration/command_recorder.rb +105 -0
  98. data/lib/active_record/model_schema.rb +366 -0
  99. data/lib/active_record/nested_attributes.rb +469 -0
  100. data/lib/active_record/observer.rb +121 -0
  101. data/lib/active_record/persistence.rb +372 -0
  102. data/lib/active_record/query_cache.rb +74 -0
  103. data/lib/active_record/querying.rb +58 -0
  104. data/lib/active_record/railtie.rb +119 -0
  105. data/lib/active_record/railties/console_sandbox.rb +6 -0
  106. data/lib/active_record/railties/controller_runtime.rb +49 -0
  107. data/lib/active_record/railties/databases.rake +620 -0
  108. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  109. data/lib/active_record/readonly_attributes.rb +26 -0
  110. data/lib/active_record/reflection.rb +534 -0
  111. data/lib/active_record/relation.rb +534 -0
  112. data/lib/active_record/relation/batches.rb +90 -0
  113. data/lib/active_record/relation/calculations.rb +354 -0
  114. data/lib/active_record/relation/delegation.rb +49 -0
  115. data/lib/active_record/relation/finder_methods.rb +398 -0
  116. data/lib/active_record/relation/predicate_builder.rb +58 -0
  117. data/lib/active_record/relation/query_methods.rb +417 -0
  118. data/lib/active_record/relation/spawn_methods.rb +148 -0
  119. data/lib/active_record/result.rb +34 -0
  120. data/lib/active_record/sanitization.rb +194 -0
  121. data/lib/active_record/schema.rb +58 -0
  122. data/lib/active_record/schema_dumper.rb +204 -0
  123. data/lib/active_record/scoping.rb +152 -0
  124. data/lib/active_record/scoping/default.rb +142 -0
  125. data/lib/active_record/scoping/named.rb +202 -0
  126. data/lib/active_record/serialization.rb +18 -0
  127. data/lib/active_record/serializers/xml_serializer.rb +202 -0
  128. data/lib/active_record/session_store.rb +358 -0
  129. data/lib/active_record/store.rb +50 -0
  130. data/lib/active_record/test_case.rb +73 -0
  131. data/lib/active_record/timestamp.rb +113 -0
  132. data/lib/active_record/transactions.rb +360 -0
  133. data/lib/active_record/translation.rb +22 -0
  134. data/lib/active_record/validations.rb +83 -0
  135. data/lib/active_record/validations/associated.rb +43 -0
  136. data/lib/active_record/validations/uniqueness.rb +180 -0
  137. data/lib/active_record/version.rb +10 -0
  138. data/lib/rails/generators/active_record.rb +25 -0
  139. data/lib/rails/generators/active_record/migration.rb +15 -0
  140. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  141. data/lib/rails/generators/active_record/migration/templates/migration.rb +31 -0
  142. data/lib/rails/generators/active_record/model/model_generator.rb +43 -0
  143. data/lib/rails/generators/active_record/model/templates/migration.rb +15 -0
  144. data/lib/rails/generators/active_record/model/templates/model.rb +7 -0
  145. data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
  146. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  147. data/lib/rails/generators/active_record/observer/templates/observer.rb +4 -0
  148. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +25 -0
  149. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +12 -0
  150. metadata +242 -0
@@ -0,0 +1,39 @@
1
+ require 'active_support/core_ext/object/blank'
2
+
3
+ module ActiveRecord
4
+ module AttributeMethods
5
+ module Query
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attribute_method_suffix "?"
10
+ end
11
+
12
+ def query_attribute(attr_name)
13
+ unless value = read_attribute(attr_name)
14
+ false
15
+ else
16
+ column = self.class.columns_hash[attr_name]
17
+ if column.nil?
18
+ if Numeric === value || value !~ /[^0-9]/
19
+ !value.to_i.zero?
20
+ else
21
+ return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
22
+ !value.blank?
23
+ end
24
+ elsif column.number?
25
+ !value.zero?
26
+ else
27
+ !value.blank?
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+ # Handle *? for method_missing.
34
+ def attribute?(attribute_name)
35
+ query_attribute(attribute_name)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,135 @@
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
+ cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
10
+ self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
11
+ end
12
+
13
+ module ClassMethods
14
+ # +cache_attributes+ allows you to declare which converted attribute values should
15
+ # be cached. Usually caching only pays off for attributes with expensive conversion
16
+ # methods, like time related columns (e.g. +created_at+, +updated_at+).
17
+ def cache_attributes(*attribute_names)
18
+ cached_attributes.merge attribute_names.map { |attr| attr.to_s }
19
+ end
20
+
21
+ # Returns the attributes which are cached. By default time related columns
22
+ # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
23
+ def cached_attributes
24
+ @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set
25
+ end
26
+
27
+ # Returns +true+ if the provided attribute is being cached.
28
+ def cache_attribute?(attr_name)
29
+ cached_attributes.include?(attr_name)
30
+ end
31
+
32
+ def undefine_attribute_methods
33
+ generated_external_attribute_methods.module_eval do
34
+ instance_methods.each { |m| undef_method(m) }
35
+ end
36
+
37
+ super
38
+ end
39
+
40
+ def type_cast_attribute(attr_name, attributes, cache = {}) #:nodoc:
41
+ return unless attr_name
42
+ attr_name = attr_name.to_s
43
+
44
+ if generated_external_attribute_methods.method_defined?(attr_name)
45
+ if attributes.has_key?(attr_name) || attr_name == 'id'
46
+ generated_external_attribute_methods.send(attr_name, attributes[attr_name], attributes, cache, attr_name)
47
+ end
48
+ elsif !attribute_methods_generated?
49
+ # If we haven't generated the caster methods yet, do that and
50
+ # then try again
51
+ define_attribute_methods
52
+ type_cast_attribute(attr_name, attributes, cache)
53
+ else
54
+ # If we get here, the attribute has no associated DB column, so
55
+ # just return it verbatim.
56
+ attributes[attr_name]
57
+ end
58
+ end
59
+
60
+ protected
61
+ # We want to generate the methods via module_eval rather than define_method,
62
+ # because define_method is slower on dispatch and uses more memory (because it
63
+ # creates a closure).
64
+ #
65
+ # But sometimes the database might return columns with characters that are not
66
+ # allowed in normal method names (like 'my_column(omg)'. So to work around this
67
+ # we first define with the __temp__ identifier, and then use alias method to
68
+ # rename it to what we want.
69
+ def define_method_attribute(attr_name)
70
+ cast_code = attribute_cast_code(attr_name)
71
+
72
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
73
+ def __temp__
74
+ #{internal_attribute_access_code(attr_name, cast_code)}
75
+ end
76
+ alias_method '#{attr_name}', :__temp__
77
+ undef_method :__temp__
78
+ STR
79
+
80
+ generated_external_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
81
+ def __temp__(v, attributes, attributes_cache, attr_name)
82
+ #{external_attribute_access_code(attr_name, cast_code)}
83
+ end
84
+ alias_method '#{attr_name}', :__temp__
85
+ undef_method :__temp__
86
+ STR
87
+ end
88
+
89
+ private
90
+ def cacheable_column?(column)
91
+ attribute_types_cached_by_default.include?(column.type)
92
+ end
93
+
94
+ def internal_attribute_access_code(attr_name, cast_code)
95
+ access_code = "(v=@attributes[attr_name]) && #{cast_code}"
96
+
97
+ unless attr_name == primary_key
98
+ access_code.insert(0, "missing_attribute(attr_name, caller) unless @attributes.has_key?(attr_name); ")
99
+ end
100
+
101
+ if cache_attribute?(attr_name)
102
+ access_code = "@attributes_cache[attr_name] ||= (#{access_code})"
103
+ end
104
+
105
+ "attr_name = '#{attr_name}'; #{access_code}"
106
+ end
107
+
108
+ def external_attribute_access_code(attr_name, cast_code)
109
+ access_code = "v && #{cast_code}"
110
+
111
+ if cache_attribute?(attr_name)
112
+ access_code = "attributes_cache[attr_name] ||= (#{access_code})"
113
+ end
114
+
115
+ access_code
116
+ end
117
+
118
+ def attribute_cast_code(attr_name)
119
+ columns_hash[attr_name].type_cast_code('v')
120
+ end
121
+ end
122
+
123
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
124
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
125
+ def read_attribute(attr_name)
126
+ self.class.type_cast_attribute(attr_name, @attributes, @attributes_cache)
127
+ end
128
+
129
+ private
130
+ def attribute(attribute_name)
131
+ read_attribute(attribute_name)
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,93 @@
1
+ module ActiveRecord
2
+ module AttributeMethods
3
+ module Serialization
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ # Returns a hash of all the attributes that have been specified for serialization as
8
+ # keys and their class restriction as values.
9
+ class_attribute :serialized_attributes
10
+ self.serialized_attributes = {}
11
+ end
12
+
13
+ class Attribute < Struct.new(:coder, :value, :state)
14
+ def unserialized_value
15
+ state == :serialized ? unserialize : value
16
+ end
17
+
18
+ def serialized_value
19
+ state == :unserialized ? serialize : value
20
+ end
21
+
22
+ def unserialize
23
+ self.state = :unserialized
24
+ self.value = coder.load(value)
25
+ end
26
+
27
+ def serialize
28
+ self.state = :serialized
29
+ self.value = coder.dump(value)
30
+ end
31
+ end
32
+
33
+ module ClassMethods
34
+ # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
35
+ # then specify the name of that attribute using this method and it will be handled automatically.
36
+ # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that
37
+ # class on retrieval or SerializationTypeMismatch will be raised.
38
+ #
39
+ # ==== Parameters
40
+ #
41
+ # * +attr_name+ - The field name that should be serialized.
42
+ # * +class_name+ - Optional, class name that the object type should be equal to.
43
+ #
44
+ # ==== Example
45
+ # # Serialize a preferences attribute
46
+ # class User < ActiveRecord::Base
47
+ # serialize :preferences
48
+ # end
49
+ def serialize(attr_name, class_name = Object)
50
+ coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
51
+ class_name
52
+ else
53
+ Coders::YAMLColumn.new(class_name)
54
+ end
55
+
56
+ # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
57
+ # has its own hash of own serialized attributes
58
+ self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
59
+ end
60
+
61
+ def initialize_attributes(attributes) #:nodoc:
62
+ super
63
+
64
+ serialized_attributes.each do |key, coder|
65
+ if attributes.key?(key)
66
+ attributes[key] = Attribute.new(coder, attributes[key], :serialized)
67
+ end
68
+ end
69
+
70
+ attributes
71
+ end
72
+
73
+ private
74
+
75
+ def attribute_cast_code(attr_name)
76
+ if serialized_attributes.include?(attr_name)
77
+ "v.unserialized_value"
78
+ else
79
+ super
80
+ end
81
+ end
82
+ end
83
+
84
+ def type_cast_attribute_for_write(column, value)
85
+ if column && coder = self.class.serialized_attributes[column.name]
86
+ Attribute.new(coder, value, :unserialized)
87
+ else
88
+ super
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,62 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+ require 'active_support/core_ext/object/inclusion'
3
+
4
+ module ActiveRecord
5
+ module AttributeMethods
6
+ module TimeZoneConversion
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ cattr_accessor :time_zone_aware_attributes, :instance_writer => false
11
+ self.time_zone_aware_attributes = false
12
+
13
+ class_attribute :skip_time_zone_conversion_for_attributes, :instance_writer => false
14
+ self.skip_time_zone_conversion_for_attributes = []
15
+ end
16
+
17
+ module ClassMethods
18
+ protected
19
+ # The enhanced read method automatically converts the UTC time stored in the database to the time
20
+ # zone stored in Time.zone.
21
+ def attribute_cast_code(attr_name)
22
+ column = columns_hash[attr_name]
23
+
24
+ if create_time_zone_conversion_attribute?(attr_name, column)
25
+ typecast = "v = #{super}"
26
+ time_zone_conversion = "v.acts_like?(:time) ? v.in_time_zone : v"
27
+
28
+ "((#{typecast}) && (#{time_zone_conversion}))"
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
35
+ # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
36
+ def define_method_attribute=(attr_name)
37
+ if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
38
+ method_body, line = <<-EOV, __LINE__ + 1
39
+ def #{attr_name}=(original_time)
40
+ time = original_time
41
+ unless time.acts_like?(:time)
42
+ time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
43
+ end
44
+ time = time.in_time_zone rescue nil if time
45
+ write_attribute(:#{attr_name}, original_time)
46
+ @attributes_cache["#{attr_name}"] = time
47
+ end
48
+ EOV
49
+ generated_attribute_methods.module_eval(method_body, __FILE__, line)
50
+ else
51
+ super
52
+ end
53
+ end
54
+
55
+ private
56
+ def create_time_zone_conversion_attribute?(name, column)
57
+ time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && column.type.in?([:datetime, :timestamp])
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,69 @@
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 =~ ActiveModel::AttributeMethods::NAME_COMPILABLE_REGEXP
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' && self.class.primary_key
28
+ @attributes_cache.delete(attr_name)
29
+ column = column_for_attribute(attr_name)
30
+
31
+ unless column || @attributes.has_key?(attr_name)
32
+ ActiveSupport::Deprecation.warn(
33
+ "You're trying to create an attribute `#{attr_name}'. Writing arbitrary " \
34
+ "attributes on a model is deprecated. Please just use `attr_writer` etc."
35
+ )
36
+ end
37
+
38
+ @attributes[attr_name] = type_cast_attribute_for_write(column, value)
39
+ end
40
+ alias_method :raw_write_attribute, :write_attribute
41
+
42
+ private
43
+ # Handle *= for method_missing.
44
+ def attribute=(attribute_name, value)
45
+ write_attribute(attribute_name, value)
46
+ end
47
+
48
+ def type_cast_attribute_for_write(column, value)
49
+ if column && column.number?
50
+ convert_number_column_value(value)
51
+ else
52
+ value
53
+ end
54
+ end
55
+
56
+ def convert_number_column_value(value)
57
+ if value == false
58
+ 0
59
+ elsif value == true
60
+ 1
61
+ elsif value.is_a?(String) && value.blank?
62
+ nil
63
+ else
64
+ value
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,422 @@
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
+ # associated 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
+ # == Validation
25
+ #
26
+ # Children records are validated unless <tt>:validate</tt> is +false+.
27
+ #
28
+ # == Callbacks
29
+ #
30
+ # Association with autosave option defines several callbacks on your
31
+ # model (before_save, after_create, after_update). Please note that
32
+ # callbacks are executed in the order they were defined in
33
+ # model. You should avoid modyfing the association content, before
34
+ # autosave callbacks are executed. Placing your callbacks after
35
+ # associations is usually a good practice.
36
+ #
37
+ # == Examples
38
+ #
39
+ # === One-to-one Example
40
+ #
41
+ # class Post
42
+ # has_one :author, :autosave => true
43
+ # end
44
+ #
45
+ # Saving changes to the parent and its associated model can now be performed
46
+ # automatically _and_ atomically:
47
+ #
48
+ # post = Post.find(1)
49
+ # post.title # => "The current global position of migrating ducks"
50
+ # post.author.name # => "alloy"
51
+ #
52
+ # post.title = "On the migration of ducks"
53
+ # post.author.name = "Eloy Duran"
54
+ #
55
+ # post.save
56
+ # post.reload
57
+ # post.title # => "On the migration of ducks"
58
+ # post.author.name # => "Eloy Duran"
59
+ #
60
+ # Destroying an associated model, as part of the parent's save action, is as
61
+ # simple as marking it for destruction:
62
+ #
63
+ # post.author.mark_for_destruction
64
+ # post.author.marked_for_destruction? # => true
65
+ #
66
+ # Note that the model is _not_ yet removed from the database:
67
+ #
68
+ # id = post.author.id
69
+ # Author.find_by_id(id).nil? # => false
70
+ #
71
+ # post.save
72
+ # post.reload.author # => nil
73
+ #
74
+ # Now it _is_ removed from the database:
75
+ #
76
+ # Author.find_by_id(id).nil? # => true
77
+ #
78
+ # === One-to-many Example
79
+ #
80
+ # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
81
+ #
82
+ # class Post
83
+ # has_many :comments # :autosave option is no declared
84
+ # end
85
+ #
86
+ # post = Post.new(:title => 'ruby rocks')
87
+ # post.comments.build(:body => 'hello world')
88
+ # post.save # => saves both post and comment
89
+ #
90
+ # post = Post.create(:title => 'ruby rocks')
91
+ # post.comments.build(:body => 'hello world')
92
+ # post.save # => saves both post and comment
93
+ #
94
+ # post = Post.create(:title => 'ruby rocks')
95
+ # post.comments.create(:body => 'hello world')
96
+ # post.save # => saves both post and comment
97
+ #
98
+ # When <tt>:autosave</tt> is true all children is saved, no matter whether they are new records:
99
+ #
100
+ # class Post
101
+ # has_many :comments, :autosave => true
102
+ # end
103
+ #
104
+ # post = Post.create(:title => 'ruby rocks')
105
+ # post.comments.create(:body => 'hello world')
106
+ # post.comments[0].body = 'hi everyone'
107
+ # post.save # => saves both post and comment, with 'hi everyone' as body
108
+ #
109
+ # Destroying one of the associated models as part of the parent's save action
110
+ # is as simple as marking it for destruction:
111
+ #
112
+ # post.comments.last.mark_for_destruction
113
+ # post.comments.last.marked_for_destruction? # => true
114
+ # post.comments.length # => 2
115
+ #
116
+ # Note that the model is _not_ yet removed from the database:
117
+ #
118
+ # id = post.comments.last.id
119
+ # Comment.find_by_id(id).nil? # => false
120
+ #
121
+ # post.save
122
+ # post.reload.comments.length # => 1
123
+ #
124
+ # Now it _is_ removed from the database:
125
+ #
126
+ # Comment.find_by_id(id).nil? # => true
127
+
128
+ module AutosaveAssociation
129
+ extend ActiveSupport::Concern
130
+
131
+ ASSOCIATION_TYPES = %w{ HasOne HasMany BelongsTo HasAndBelongsToMany }
132
+
133
+ module AssociationBuilderExtension #:nodoc:
134
+ def self.included(base)
135
+ base.valid_options << :autosave
136
+ end
137
+
138
+ def build
139
+ reflection = super
140
+ model.send(:add_autosave_association_callbacks, reflection)
141
+ reflection
142
+ end
143
+ end
144
+
145
+ included do
146
+ ASSOCIATION_TYPES.each do |type|
147
+ Associations::Builder.const_get(type).send(:include, AssociationBuilderExtension)
148
+ end
149
+ end
150
+
151
+ module ClassMethods
152
+ private
153
+
154
+ def define_non_cyclic_method(name, reflection, &block)
155
+ define_method(name) do |*args|
156
+ result = true; @_already_called ||= {}
157
+ # Loop prevention for validation of associations
158
+ unless @_already_called[[name, reflection.name]]
159
+ begin
160
+ @_already_called[[name, reflection.name]]=true
161
+ result = instance_eval(&block)
162
+ ensure
163
+ @_already_called[[name, reflection.name]]=false
164
+ end
165
+ end
166
+
167
+ result
168
+ end
169
+ end
170
+
171
+ # Adds validation and save callbacks for the association as specified by
172
+ # the +reflection+.
173
+ #
174
+ # For performance reasons, we don't check whether to validate at runtime.
175
+ # However the validation and callback methods are lazy and those methods
176
+ # get created when they are invoked for the very first time. However,
177
+ # this can change, for instance, when using nested attributes, which is
178
+ # called _after_ the association has been defined. Since we don't want
179
+ # the callbacks to get defined multiple times, there are guards that
180
+ # check if the save or validation methods have already been defined
181
+ # before actually defining them.
182
+ def add_autosave_association_callbacks(reflection)
183
+ save_method = :"autosave_associated_records_for_#{reflection.name}"
184
+ validation_method = :"validate_associated_records_for_#{reflection.name}"
185
+ collection = reflection.collection?
186
+
187
+ unless method_defined?(save_method)
188
+ if collection
189
+ before_save :before_save_collection_association
190
+
191
+ define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) }
192
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
193
+ after_create save_method
194
+ after_update save_method
195
+ else
196
+ if reflection.macro == :has_one
197
+ define_method(save_method) { save_has_one_association(reflection) }
198
+ # Configures two callbacks instead of a single after_save so that
199
+ # the model may rely on their execution order relative to its
200
+ # own callbacks.
201
+ #
202
+ # For example, given that after_creates run before after_saves, if
203
+ # we configured instead an after_save there would be no way to fire
204
+ # a custom after_create callback after the child association gets
205
+ # created.
206
+ after_create save_method
207
+ after_update save_method
208
+ else
209
+ define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
210
+ before_save save_method
211
+ end
212
+ end
213
+ end
214
+
215
+ if reflection.validate? && !method_defined?(validation_method)
216
+ method = (collection ? :validate_collection_association : :validate_single_association)
217
+ define_non_cyclic_method(validation_method, reflection) { send(method, reflection) }
218
+ validate validation_method
219
+ end
220
+ end
221
+ end
222
+
223
+ # Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
224
+ def reload(options = nil)
225
+ @marked_for_destruction = false
226
+ super
227
+ end
228
+
229
+ # Marks this record to be destroyed as part of the parents save transaction.
230
+ # This does _not_ actually destroy the record instantly, rather child record will be destroyed
231
+ # when <tt>parent.save</tt> is called.
232
+ #
233
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
234
+ def mark_for_destruction
235
+ @marked_for_destruction = true
236
+ end
237
+
238
+ # Returns whether or not this record will be destroyed as part of the parents save transaction.
239
+ #
240
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
241
+ def marked_for_destruction?
242
+ @marked_for_destruction
243
+ end
244
+
245
+ # Returns whether or not this record has been changed in any way (including whether
246
+ # any of its nested autosave associations are likewise changed)
247
+ def changed_for_autosave?
248
+ new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
249
+ end
250
+
251
+ private
252
+
253
+ # Returns the record for an association collection that should be validated
254
+ # or saved. If +autosave+ is +false+ only new records will be returned,
255
+ # unless the parent is/was a new record itself.
256
+ def associated_records_to_validate_or_save(association, new_record, autosave)
257
+ if new_record
258
+ association && association.target
259
+ elsif autosave
260
+ association.target.find_all { |record| record.changed_for_autosave? }
261
+ else
262
+ association.target.find_all { |record| record.new_record? }
263
+ end
264
+ end
265
+
266
+ # go through nested autosave associations that are loaded in memory (without loading
267
+ # any new ones), and return true if is changed for autosave
268
+ def nested_records_changed_for_autosave?
269
+ self.class.reflect_on_all_autosave_associations.any? do |reflection|
270
+ association = association_instance_get(reflection.name)
271
+ association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
272
+ end
273
+ end
274
+
275
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
276
+ # turned on for the association.
277
+ def validate_single_association(reflection)
278
+ association = association_instance_get(reflection.name)
279
+ record = association && association.reader
280
+ association_valid?(reflection, record) if record
281
+ end
282
+
283
+ # Validate the associated records if <tt>:validate</tt> or
284
+ # <tt>:autosave</tt> is turned on for the association specified by
285
+ # +reflection+.
286
+ def validate_collection_association(reflection)
287
+ if association = association_instance_get(reflection.name)
288
+ if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
289
+ records.each { |record| association_valid?(reflection, record) }
290
+ end
291
+ end
292
+ end
293
+
294
+ # Returns whether or not the association is valid and applies any errors to
295
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
296
+ # enabled records if they're marked_for_destruction? or destroyed.
297
+ def association_valid?(reflection, record)
298
+ return true if record.destroyed? || record.marked_for_destruction?
299
+
300
+ unless valid = record.valid?
301
+ if reflection.options[:autosave]
302
+ record.errors.each do |attribute, message|
303
+ attribute = "#{reflection.name}.#{attribute}"
304
+ errors[attribute] << message
305
+ errors[attribute].uniq!
306
+ end
307
+ else
308
+ errors.add(reflection.name)
309
+ end
310
+ end
311
+ valid
312
+ end
313
+
314
+ # Is used as a before_save callback to check while saving a collection
315
+ # association whether or not the parent was a new record before saving.
316
+ def before_save_collection_association
317
+ @new_record_before_save = new_record?
318
+ true
319
+ end
320
+
321
+ # Saves any new associated records, or all loaded autosave associations if
322
+ # <tt>:autosave</tt> is enabled on the association.
323
+ #
324
+ # In addition, it destroys all children that were marked for destruction
325
+ # 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_collection_association(reflection)
330
+ if association = association_instance_get(reflection.name)
331
+ autosave = reflection.options[:autosave]
332
+
333
+ if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
334
+ begin
335
+ records.each do |record|
336
+ next if record.destroyed?
337
+
338
+ saved = true
339
+
340
+ if autosave && record.marked_for_destruction?
341
+ association.proxy.destroy(record)
342
+ elsif autosave != false && (@new_record_before_save || record.new_record?)
343
+ if autosave
344
+ saved = association.insert_record(record, false)
345
+ else
346
+ association.insert_record(record) unless reflection.nested?
347
+ end
348
+ elsif autosave
349
+ saved = record.save(:validate => false)
350
+ end
351
+
352
+ raise ActiveRecord::Rollback unless saved
353
+ end
354
+ rescue
355
+ records.each {|x| IdentityMap.remove(x) } if IdentityMap.enabled?
356
+ raise
357
+ end
358
+
359
+ end
360
+
361
+ # reconstruct the scope now that we know the owner's id
362
+ association.send(:reset_scope) if association.respond_to?(:reset_scope)
363
+ end
364
+ end
365
+
366
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
367
+ # on the association.
368
+ #
369
+ # In addition, it will destroy the association if it was marked for
370
+ # destruction with mark_for_destruction.
371
+ #
372
+ # This all happens inside a transaction, _if_ the Transactions module is included into
373
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
374
+ def save_has_one_association(reflection)
375
+ association = association_instance_get(reflection.name)
376
+ record = association && association.load_target
377
+ if record && !record.destroyed?
378
+ autosave = reflection.options[:autosave]
379
+
380
+ if autosave && record.marked_for_destruction?
381
+ record.destroy
382
+ else
383
+ key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
384
+ if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave)
385
+ unless reflection.through_reflection
386
+ record[reflection.foreign_key] = key
387
+ end
388
+
389
+ saved = record.save(:validate => !autosave)
390
+ raise ActiveRecord::Rollback if !saved && autosave
391
+ saved
392
+ end
393
+ end
394
+ end
395
+ end
396
+
397
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
398
+ #
399
+ # In addition, it will destroy the association if it was marked for destruction.
400
+ def save_belongs_to_association(reflection)
401
+ association = association_instance_get(reflection.name)
402
+ record = association && association.load_target
403
+ if record && !record.destroyed?
404
+ autosave = reflection.options[:autosave]
405
+
406
+ if autosave && record.marked_for_destruction?
407
+ record.destroy
408
+ elsif autosave != false
409
+ saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
410
+
411
+ if association.updated?
412
+ association_id = record.send(reflection.options[:primary_key] || :id)
413
+ self[reflection.foreign_key] = association_id
414
+ association.loaded!
415
+ end
416
+
417
+ saved if autosave
418
+ end
419
+ end
420
+ end
421
+ end
422
+ end