activerecord 3.2.22.5 → 4.0.0.beta1

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 (162) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1024 -543
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +20 -29
  5. data/examples/performance.rb +1 -1
  6. data/lib/active_record.rb +55 -44
  7. data/lib/active_record/aggregations.rb +40 -34
  8. data/lib/active_record/associations.rb +204 -276
  9. data/lib/active_record/associations/alias_tracker.rb +1 -1
  10. data/lib/active_record/associations/association.rb +30 -35
  11. data/lib/active_record/associations/association_scope.rb +40 -40
  12. data/lib/active_record/associations/belongs_to_association.rb +15 -2
  13. data/lib/active_record/associations/builder/association.rb +81 -28
  14. data/lib/active_record/associations/builder/belongs_to.rb +35 -57
  15. data/lib/active_record/associations/builder/collection_association.rb +54 -40
  16. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +23 -41
  17. data/lib/active_record/associations/builder/has_many.rb +8 -64
  18. data/lib/active_record/associations/builder/has_one.rb +13 -50
  19. data/lib/active_record/associations/builder/singular_association.rb +13 -13
  20. data/lib/active_record/associations/collection_association.rb +92 -88
  21. data/lib/active_record/associations/collection_proxy.rb +913 -63
  22. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +12 -10
  23. data/lib/active_record/associations/has_many_association.rb +35 -9
  24. data/lib/active_record/associations/has_many_through_association.rb +24 -14
  25. data/lib/active_record/associations/has_one_association.rb +33 -13
  26. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  27. data/lib/active_record/associations/join_dependency.rb +2 -2
  28. data/lib/active_record/associations/join_dependency/join_association.rb +17 -22
  29. data/lib/active_record/associations/join_dependency/join_part.rb +1 -1
  30. data/lib/active_record/associations/join_helper.rb +1 -11
  31. data/lib/active_record/associations/preloader.rb +14 -17
  32. data/lib/active_record/associations/preloader/association.rb +29 -33
  33. data/lib/active_record/associations/preloader/collection_association.rb +1 -1
  34. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +1 -1
  35. data/lib/active_record/associations/preloader/has_many_through.rb +1 -1
  36. data/lib/active_record/associations/preloader/has_one.rb +1 -1
  37. data/lib/active_record/associations/preloader/through_association.rb +13 -17
  38. data/lib/active_record/associations/singular_association.rb +11 -11
  39. data/lib/active_record/associations/through_association.rb +2 -2
  40. data/lib/active_record/attribute_assignment.rb +133 -153
  41. data/lib/active_record/attribute_methods.rb +196 -93
  42. data/lib/active_record/attribute_methods/before_type_cast.rb +44 -5
  43. data/lib/active_record/attribute_methods/dirty.rb +31 -28
  44. data/lib/active_record/attribute_methods/primary_key.rb +38 -30
  45. data/lib/active_record/attribute_methods/query.rb +5 -4
  46. data/lib/active_record/attribute_methods/read.rb +62 -91
  47. data/lib/active_record/attribute_methods/serialization.rb +97 -66
  48. data/lib/active_record/attribute_methods/time_zone_conversion.rb +39 -45
  49. data/lib/active_record/attribute_methods/write.rb +32 -39
  50. data/lib/active_record/autosave_association.rb +56 -70
  51. data/lib/active_record/base.rb +53 -450
  52. data/lib/active_record/callbacks.rb +53 -18
  53. data/lib/active_record/coders/yaml_column.rb +11 -9
  54. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +353 -197
  55. data/lib/active_record/connection_adapters/abstract/database_limits.rb +9 -0
  56. data/lib/active_record/connection_adapters/abstract/database_statements.rb +130 -131
  57. data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -19
  58. data/lib/active_record/connection_adapters/abstract/quoting.rb +23 -3
  59. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +101 -91
  60. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +59 -0
  61. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +225 -96
  62. data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
  63. data/lib/active_record/connection_adapters/abstract_adapter.rb +99 -46
  64. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +114 -36
  65. data/lib/active_record/connection_adapters/column.rb +46 -24
  66. data/lib/active_record/connection_adapters/connection_specification.rb +96 -0
  67. data/lib/active_record/connection_adapters/mysql2_adapter.rb +16 -32
  68. data/lib/active_record/connection_adapters/mysql_adapter.rb +181 -64
  69. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +97 -0
  70. data/lib/active_record/connection_adapters/postgresql/cast.rb +132 -0
  71. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +242 -0
  72. data/lib/active_record/connection_adapters/postgresql/oid.rb +347 -0
  73. data/lib/active_record/connection_adapters/postgresql/quoting.rb +158 -0
  74. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  75. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +448 -0
  76. data/lib/active_record/connection_adapters/postgresql_adapter.rb +454 -885
  77. data/lib/active_record/connection_adapters/schema_cache.rb +48 -16
  78. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +574 -13
  79. data/lib/active_record/connection_handling.rb +98 -0
  80. data/lib/active_record/core.rb +428 -0
  81. data/lib/active_record/counter_cache.rb +106 -108
  82. data/lib/active_record/dynamic_matchers.rb +110 -63
  83. data/lib/active_record/errors.rb +25 -8
  84. data/lib/active_record/explain.rb +8 -58
  85. data/lib/active_record/explain_subscriber.rb +6 -3
  86. data/lib/active_record/fixture_set/file.rb +56 -0
  87. data/lib/active_record/fixtures.rb +146 -148
  88. data/lib/active_record/inheritance.rb +77 -59
  89. data/lib/active_record/integration.rb +5 -5
  90. data/lib/active_record/locale/en.yml +8 -1
  91. data/lib/active_record/locking/optimistic.rb +38 -42
  92. data/lib/active_record/locking/pessimistic.rb +4 -4
  93. data/lib/active_record/log_subscriber.rb +19 -9
  94. data/lib/active_record/migration.rb +318 -153
  95. data/lib/active_record/migration/command_recorder.rb +90 -31
  96. data/lib/active_record/migration/join_table.rb +15 -0
  97. data/lib/active_record/model_schema.rb +69 -92
  98. data/lib/active_record/nested_attributes.rb +113 -148
  99. data/lib/active_record/null_relation.rb +65 -0
  100. data/lib/active_record/persistence.rb +188 -97
  101. data/lib/active_record/query_cache.rb +18 -36
  102. data/lib/active_record/querying.rb +19 -15
  103. data/lib/active_record/railtie.rb +91 -36
  104. data/lib/active_record/railties/console_sandbox.rb +0 -2
  105. data/lib/active_record/railties/controller_runtime.rb +2 -2
  106. data/lib/active_record/railties/databases.rake +90 -309
  107. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  108. data/lib/active_record/readonly_attributes.rb +7 -3
  109. data/lib/active_record/reflection.rb +72 -56
  110. data/lib/active_record/relation.rb +241 -157
  111. data/lib/active_record/relation/batches.rb +25 -22
  112. data/lib/active_record/relation/calculations.rb +143 -121
  113. data/lib/active_record/relation/delegation.rb +96 -18
  114. data/lib/active_record/relation/finder_methods.rb +117 -183
  115. data/lib/active_record/relation/merger.rb +133 -0
  116. data/lib/active_record/relation/predicate_builder.rb +90 -42
  117. data/lib/active_record/relation/query_methods.rb +666 -136
  118. data/lib/active_record/relation/spawn_methods.rb +43 -150
  119. data/lib/active_record/result.rb +33 -6
  120. data/lib/active_record/sanitization.rb +24 -50
  121. data/lib/active_record/schema.rb +19 -12
  122. data/lib/active_record/schema_dumper.rb +31 -39
  123. data/lib/active_record/schema_migration.rb +36 -0
  124. data/lib/active_record/scoping.rb +0 -124
  125. data/lib/active_record/scoping/default.rb +48 -45
  126. data/lib/active_record/scoping/named.rb +74 -103
  127. data/lib/active_record/serialization.rb +6 -2
  128. data/lib/active_record/serializers/xml_serializer.rb +9 -15
  129. data/lib/active_record/store.rb +119 -15
  130. data/lib/active_record/tasks/database_tasks.rb +158 -0
  131. data/lib/active_record/tasks/mysql_database_tasks.rb +138 -0
  132. data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
  133. data/lib/active_record/tasks/sqlite_database_tasks.rb +51 -0
  134. data/lib/active_record/test_case.rb +61 -38
  135. data/lib/active_record/timestamp.rb +8 -9
  136. data/lib/active_record/transactions.rb +65 -51
  137. data/lib/active_record/validations.rb +17 -15
  138. data/lib/active_record/validations/associated.rb +20 -14
  139. data/lib/active_record/validations/presence.rb +65 -0
  140. data/lib/active_record/validations/uniqueness.rb +93 -52
  141. data/lib/active_record/version.rb +4 -4
  142. data/lib/rails/generators/active_record.rb +3 -5
  143. data/lib/rails/generators/active_record/migration/migration_generator.rb +37 -7
  144. data/lib/rails/generators/active_record/migration/templates/migration.rb +20 -15
  145. data/lib/rails/generators/active_record/model/model_generator.rb +4 -3
  146. data/lib/rails/generators/active_record/model/templates/model.rb +1 -6
  147. data/lib/rails/generators/active_record/model/templates/module.rb +1 -1
  148. metadata +53 -46
  149. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +0 -32
  150. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -191
  151. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -583
  152. data/lib/active_record/dynamic_finder_match.rb +0 -68
  153. data/lib/active_record/dynamic_scope_match.rb +0 -23
  154. data/lib/active_record/fixtures/file.rb +0 -65
  155. data/lib/active_record/identity_map.rb +0 -162
  156. data/lib/active_record/observer.rb +0 -121
  157. data/lib/active_record/session_store.rb +0 -360
  158. data/lib/rails/generators/active_record/migration.rb +0 -15
  159. data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
  160. data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -4
  161. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -25
  162. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -12
@@ -1,47 +1,36 @@
1
1
  require 'active_support/core_ext/array'
2
2
  require 'active_support/core_ext/hash/except'
3
3
  require 'active_support/core_ext/kernel/singleton_class'
4
- require 'active_support/core_ext/object/blank'
5
- require 'active_support/core_ext/class/attribute'
6
4
 
7
5
  module ActiveRecord
8
- # = Active Record Named \Scopes
6
+ # = Active Record \Named \Scopes
9
7
  module Scoping
10
8
  module Named
11
9
  extend ActiveSupport::Concern
12
10
 
13
11
  module ClassMethods
14
- # Returns an anonymous \scope.
12
+ # Returns an <tt>ActiveRecord::Relation</tt> scope object.
15
13
  #
16
- # posts = Post.scoped
14
+ # posts = Post.all
17
15
  # posts.size # Fires "select count(*) from posts" and returns the count
18
16
  # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
19
17
  #
20
- # fruits = Fruit.scoped
21
- # fruits = fruits.where(:color => 'red') if options[:red_only]
18
+ # fruits = Fruit.all
19
+ # fruits = fruits.where(color: 'red') if options[:red_only]
22
20
  # fruits = fruits.limit(10) if limited?
23
21
  #
24
- # Anonymous \scopes tend to be useful when procedurally generating complex
25
- # queries, where passing intermediate values (\scopes) around as first-class
26
- # objects is convenient.
27
- #
28
- # You can define a \scope that applies to all finders using
29
- # ActiveRecord::Base.default_scope.
30
- def scoped(options = nil)
31
- if options
32
- scoped.apply_finder_options(options)
22
+ # You can define a scope that applies to all finders using
23
+ # <tt>ActiveRecord::Base.default_scope</tt>.
24
+ def all
25
+ if current_scope
26
+ current_scope.clone
33
27
  else
34
- if current_scope
35
- current_scope.clone
36
- else
37
- scope = relation
38
- scope.default_scoped = true
39
- scope
40
- end
28
+ scope = relation
29
+ scope.default_scoped = true
30
+ scope
41
31
  end
42
32
  end
43
33
 
44
- ##
45
34
  # Collects attributes from scopes that should be applied when creating
46
35
  # an AR instance for the particular class this is called on.
47
36
  def scope_attributes # :nodoc:
@@ -54,86 +43,70 @@ module ActiveRecord
54
43
  end
55
44
  end
56
45
 
57
- ##
58
46
  # Are there default attributes associated with this scope?
59
47
  def scope_attributes? # :nodoc:
60
48
  current_scope || default_scopes.any?
61
49
  end
62
50
 
63
- # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query,
64
- # such as <tt>where(:color => :red).select('shirts.*').includes(:washing_instructions)</tt>.
51
+ # Adds a class method for retrieving and querying objects. A \scope
52
+ # represents a narrowing of a database query, such as
53
+ # <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>.
65
54
  #
66
55
  # class Shirt < ActiveRecord::Base
67
- # scope :red, where(:color => 'red')
68
- # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true)
56
+ # scope :red, -> { where(color: 'red') }
57
+ # scope :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) }
69
58
  # end
70
59
  #
71
- # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
72
- # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>.
60
+ # The above calls to +scope+ define class methods <tt>Shirt.red</tt> and
61
+ # <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>, in effect,
62
+ # represents the query <tt>Shirt.where(color: 'red')</tt>.
73
63
  #
74
- # Note that this is simply 'syntactic sugar' for defining an actual class method:
64
+ # You should always pass a callable object to the scopes defined
65
+ # with +scope+. This ensures that the scope is re-evaluated each
66
+ # time it is called.
67
+ #
68
+ # Note that this is simply 'syntactic sugar' for defining an actual
69
+ # class method:
75
70
  #
76
71
  # class Shirt < ActiveRecord::Base
77
72
  # def self.red
78
- # where(:color => 'red')
73
+ # where(color: 'red')
79
74
  # end
80
75
  # end
81
76
  #
82
- # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it
83
- # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance,
84
- # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>.
85
- # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable;
86
- # <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt>
87
- # all behave as if Shirt.red really was an Array.
88
- #
89
- # These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce
90
- # all shirts that are both red and dry clean only.
91
- # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
92
- # returns the number of garments for which these criteria obtain. Similarly with
93
- # <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
94
- #
95
- # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which
96
- # the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If,
77
+ # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by
78
+ # <tt>Shirt.red</tt> is not an Array; it resembles the association object
79
+ # constructed by a +has_many+ declaration. For instance, you can invoke
80
+ # <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>,
81
+ # <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the
82
+ # association objects, named \scopes act like an Array, implementing
83
+ # Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>,
84
+ # and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if
85
+ # <tt>Shirt.red</tt> really was an Array.
86
+ #
87
+ # These named \scopes are composable. For instance,
88
+ # <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are
89
+ # both red and dry clean only. Nested finds and calculations also work
90
+ # with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
91
+ # returns the number of garments for which these criteria obtain.
92
+ # Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
93
+ #
94
+ # All scopes are available as class methods on the ActiveRecord::Base
95
+ # descendant upon which the \scopes were defined. But they are also
96
+ # available to +has_many+ associations. If,
97
97
  #
98
98
  # class Person < ActiveRecord::Base
99
99
  # has_many :shirts
100
100
  # end
101
101
  #
102
- # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
103
- # only shirts.
104
- #
105
- # Named \scopes can also be procedural:
106
- #
107
- # class Shirt < ActiveRecord::Base
108
- # scope :colored, lambda { |color| where(:color => color) }
109
- # end
110
- #
111
- # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
112
- #
113
- # On Ruby 1.9 you can use the 'stabby lambda' syntax:
114
- #
115
- # scope :colored, ->(color) { where(:color => color) }
116
- #
117
- # Note that scopes defined with \scope will be evaluated when they are defined, rather than
118
- # when they are used. For example, the following would be incorrect:
119
- #
120
- # class Post < ActiveRecord::Base
121
- # scope :recent, where('published_at >= ?', Time.current - 1.week)
122
- # end
123
- #
124
- # The example above would be 'frozen' to the <tt>Time.current</tt> value when the <tt>Post</tt>
125
- # class was defined, and so the resultant SQL query would always be the same. The correct
126
- # way to do this would be via a lambda, which will re-evaluate the scope each time
127
- # it is called:
128
- #
129
- # class Post < ActiveRecord::Base
130
- # scope :recent, lambda { where('published_at >= ?', Time.current - 1.week) }
131
- # end
102
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of
103
+ # Elton's red, dry clean only shirts.
132
104
  #
133
- # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
105
+ # \Named scopes can also have extensions, just as with +has_many+
106
+ # declarations:
134
107
  #
135
108
  # class Shirt < ActiveRecord::Base
136
- # scope :red, where(:color => 'red') do
109
+ # scope :red, -> { where(color: 'red') } do
137
110
  # def dom_id
138
111
  # 'red_shirts'
139
112
  # end
@@ -143,18 +116,18 @@ module ActiveRecord
143
116
  # Scopes can also be used while creating/building a record.
144
117
  #
145
118
  # class Article < ActiveRecord::Base
146
- # scope :published, where(:published => true)
119
+ # scope :published, -> { where(published: true) }
147
120
  # end
148
121
  #
149
122
  # Article.published.new.published # => true
150
123
  # Article.published.create.published # => true
151
124
  #
152
- # Class methods on your model are automatically available
125
+ # \Class methods on your model are automatically available
153
126
  # on scopes. Assuming the following setup:
154
127
  #
155
128
  # class Article < ActiveRecord::Base
156
- # scope :published, where(:published => true)
157
- # scope :featured, where(:featured => true)
129
+ # scope :published, -> { where(published: true) }
130
+ # scope :featured, -> { where(featured: true) }
158
131
  #
159
132
  # def self.latest_article
160
133
  # order('published_at desc').first
@@ -169,29 +142,27 @@ module ActiveRecord
169
142
  #
170
143
  # Article.published.featured.latest_article
171
144
  # Article.featured.titles
172
- def scope(name, scope_options = {})
173
- name = name.to_sym
174
- valid_scope_name?(name)
175
- extension = Module.new(&Proc.new) if block_given?
145
+ def scope(name, body, &block)
146
+ extension = Module.new(&block) if block
176
147
 
177
- scope_proc = lambda do |*args|
178
- options = scope_options.respond_to?(:call) ? unscoped { scope_options.call(*args) } : scope_options
179
- options = scoped.apply_finder_options(options) if options.is_a?(Hash)
180
-
181
- relation = scoped.merge(options)
182
-
183
- extension ? relation.extending(extension) : relation
148
+ # Check body.is_a?(Relation) to prevent the relation actually being
149
+ # loaded by respond_to?
150
+ if body.is_a?(Relation) || !body.respond_to?(:call)
151
+ ActiveSupport::Deprecation.warn(
152
+ "Using #scope without passing a callable object is deprecated. For " \
153
+ "example `scope :red, where(color: 'red')` should be changed to " \
154
+ "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \
155
+ "in the former usage and it makes the implementation more complicated " \
156
+ "and buggy. (If you prefer, you can just define a class method named " \
157
+ "`self.red`.)"
158
+ )
184
159
  end
185
160
 
186
- singleton_class.send(:redefine_method, name, &scope_proc)
187
- end
188
-
189
- protected
161
+ singleton_class.send(:define_method, name) do |*args|
162
+ options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body
163
+ relation = all.merge(options)
190
164
 
191
- def valid_scope_name?(name)
192
- if logger && respond_to?(name, true)
193
- logger.warn "Creating scope :#{name}. " \
194
- "Overwriting existing method #{self.name}.#{name}."
165
+ extension ? relation.extending(extension) : relation
195
166
  end
196
167
  end
197
168
  end
@@ -4,11 +4,15 @@ module ActiveRecord #:nodoc:
4
4
  extend ActiveSupport::Concern
5
5
  include ActiveModel::Serializers::JSON
6
6
 
7
+ included do
8
+ self.include_root_in_json = true
9
+ end
10
+
7
11
  def serializable_hash(options = nil)
8
12
  options = options.try(:clone) || {}
9
13
 
10
- options[:except] = Array.wrap(options[:except]).map { |n| n.to_s }
11
- options[:except] |= Array.wrap(self.class.inheritance_column)
14
+ options[:except] = Array(options[:except]).map { |n| n.to_s }
15
+ options[:except] |= Array(self.class.inheritance_column)
12
16
 
13
17
  super(options)
14
18
  end
@@ -1,4 +1,3 @@
1
- require 'active_support/core_ext/array/wrap'
2
1
  require 'active_support/core_ext/hash/conversions'
3
2
 
4
3
  module ActiveRecord #:nodoc:
@@ -19,8 +18,8 @@ module ActiveRecord #:nodoc:
19
18
  # <id type="integer">1</id>
20
19
  # <approved type="boolean">false</approved>
21
20
  # <replies-count type="integer">0</replies-count>
22
- # <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
23
- # <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
21
+ # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time>
22
+ # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on>
24
23
  # <content>Have a nice day</content>
25
24
  # <author-email-address>david@loudthinking.com</author-email-address>
26
25
  # <parent-id></parent-id>
@@ -37,7 +36,7 @@ module ActiveRecord #:nodoc:
37
36
  #
38
37
  # For instance:
39
38
  #
40
- # topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
39
+ # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ])
41
40
  #
42
41
  # <topic>
43
42
  # <title>The First Topic</title>
@@ -51,7 +50,7 @@ module ActiveRecord #:nodoc:
51
50
  #
52
51
  # To include first level associations use <tt>:include</tt>:
53
52
  #
54
- # firm.to_xml :include => [ :account, :clients ]
53
+ # firm.to_xml include: [ :account, :clients ]
55
54
  #
56
55
  # <?xml version="1.0" encoding="UTF-8"?>
57
56
  # <firm>
@@ -82,7 +81,7 @@ module ActiveRecord #:nodoc:
82
81
  # associated with models.
83
82
  #
84
83
  # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
85
- # firm.to_xml :procs => [ proc ]
84
+ # firm.to_xml procs: [ proc ]
86
85
  #
87
86
  # <firm>
88
87
  # # ... normal attributes as shown above ...
@@ -91,7 +90,7 @@ module ActiveRecord #:nodoc:
91
90
  #
92
91
  # To include deeper levels of associations pass a hash like this:
93
92
  #
94
- # firm.to_xml :include => {:account => {}, :clients => {:include => :address}}
93
+ # firm.to_xml include: {account: {}, clients: {include: :address}}
95
94
  # <?xml version="1.0" encoding="UTF-8"?>
96
95
  # <firm>
97
96
  # <id type="integer">1</id>
@@ -121,7 +120,7 @@ module ActiveRecord #:nodoc:
121
120
  #
122
121
  # To include any methods on the model being called use <tt>:methods</tt>:
123
122
  #
124
- # firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
123
+ # firm.to_xml methods: [ :calculated_earnings, :real_earnings ]
125
124
  #
126
125
  # <firm>
127
126
  # # ... normal attributes as shown above ...
@@ -133,7 +132,7 @@ module ActiveRecord #:nodoc:
133
132
  # modified version of the options hash that was given to +to_xml+:
134
133
  #
135
134
  # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
136
- # firm.to_xml :procs => [ proc ]
135
+ # firm.to_xml procs: [ proc ]
137
136
  #
138
137
  # <firm>
139
138
  # # ... normal attributes as shown above ...
@@ -165,7 +164,7 @@ module ActiveRecord #:nodoc:
165
164
  # def to_xml(options = {})
166
165
  # require 'builder'
167
166
  # options[:indent] ||= 2
168
- # xml = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
167
+ # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
169
168
  # xml.instruct! unless options[:skip_instruct]
170
169
  # xml.level_one do
171
170
  # xml.tag!(:second_level, 'content')
@@ -178,11 +177,6 @@ module ActiveRecord #:nodoc:
178
177
  end
179
178
 
180
179
  class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
181
- def initialize(*args)
182
- super
183
- options[:except] = Array.wrap(options[:except]) | Array.wrap(@serializable.class.inheritance_column)
184
- end
185
-
186
180
  class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
187
181
  def compute_type
188
182
  klass = @serializable.class
@@ -1,6 +1,8 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+
1
3
  module ActiveRecord
2
4
  # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column.
3
- # It's like a simple key/value store backed into your record when you don't care about being able to
5
+ # It's like a simple key/value store baked into your record when you don't care about being able to
4
6
  # query that store outside the context of a single record.
5
7
  #
6
8
  # You can then declare accessors to this store that are then accessible just like any other attribute
@@ -10,43 +12,145 @@ module ActiveRecord
10
12
  # Make sure that you declare the database column used for the serialized store as a text, so there's
11
13
  # plenty of room.
12
14
  #
15
+ # You can set custom coder to encode/decode your serialized attributes to/from different formats.
16
+ # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
17
+ #
13
18
  # Examples:
14
19
  #
15
20
  # class User < ActiveRecord::Base
16
- # store :settings, accessors: [ :color, :homepage ]
21
+ # store :settings, accessors: [ :color, :homepage ], coder: JSON
17
22
  # end
18
- #
23
+ #
19
24
  # u = User.new(color: 'black', homepage: '37signals.com')
20
25
  # u.color # Accessor stored attribute
21
26
  # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
22
27
  #
28
+ # # There is no difference between strings and symbols for accessing custom attributes
29
+ # u.settings[:country] # => 'Denmark'
30
+ # u.settings['country'] # => 'Denmark'
31
+ #
23
32
  # # Add additional accessors to an existing store through store_accessor
24
33
  # class SuperUser < User
25
34
  # store_accessor :settings, :privileges, :servants
26
35
  # end
36
+ #
37
+ # The stored attribute names can be retrieved using +stored_attributes+.
38
+ #
39
+ # User.stored_attributes[:settings] # [:color, :homepage]
40
+ #
41
+ # == Overwriting default accessors
42
+ #
43
+ # All stored values are automatically available through accessors on the Active Record
44
+ # object, but sometimes you want to specialize this behavior. This can be done by overwriting
45
+ # the default accessors (using the same name as the attribute) and calling <tt>super</tt>
46
+ # to actually change things.
47
+ #
48
+ # class Song < ActiveRecord::Base
49
+ # # Uses a stored integer to hold the volume adjustment of the song
50
+ # store :settings, accessors: [:volume_adjustment]
51
+ #
52
+ # def volume_adjustment=(decibels)
53
+ # super(decibels.to_i)
54
+ # end
55
+ #
56
+ # def volume_adjustment
57
+ # super.to_i
58
+ # end
59
+ # end
27
60
  module Store
28
61
  extend ActiveSupport::Concern
29
-
62
+
63
+ included do
64
+ class_attribute :stored_attributes, instance_accessor: false
65
+ self.stored_attributes = {}
66
+ end
67
+
30
68
  module ClassMethods
31
69
  def store(store_attribute, options = {})
32
- serialize store_attribute, Hash
70
+ serialize store_attribute, IndifferentCoder.new(options[:coder])
33
71
  store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
34
72
  end
35
73
 
36
74
  def store_accessor(store_attribute, *keys)
37
- Array(keys).flatten.each do |key|
38
- define_method("#{key}=") do |value|
39
- send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash)
40
- send("#{store_attribute}_will_change!")
41
- send(store_attribute)[key] = value
75
+ keys = keys.flatten
76
+
77
+ _store_accessors_module.module_eval do
78
+ keys.each do |key|
79
+ define_method("#{key}=") do |value|
80
+ write_store_attribute(store_attribute, key, value)
81
+ end
82
+
83
+ define_method(key) do
84
+ read_store_attribute(store_attribute, key)
85
+ end
42
86
  end
43
-
44
- define_method(key) do
45
- send("#{store_attribute}=", {}) unless send(store_attribute).is_a?(Hash)
46
- send(store_attribute)[key]
87
+ end
88
+
89
+ self.stored_attributes[store_attribute] ||= []
90
+ self.stored_attributes[store_attribute] |= keys
91
+ end
92
+
93
+ def _store_accessors_module
94
+ @_store_accessors_module ||= begin
95
+ mod = Module.new
96
+ include mod
97
+ mod
98
+ end
99
+ end
100
+ end
101
+
102
+ protected
103
+ def read_store_attribute(store_attribute, key)
104
+ attribute = initialize_store_attribute(store_attribute)
105
+ attribute[key]
106
+ end
107
+
108
+ def write_store_attribute(store_attribute, key, value)
109
+ attribute = initialize_store_attribute(store_attribute)
110
+ if value != attribute[key]
111
+ send :"#{store_attribute}_will_change!"
112
+ attribute[key] = value
113
+ end
114
+ end
115
+
116
+ private
117
+ def initialize_store_attribute(store_attribute)
118
+ attribute = send(store_attribute)
119
+ unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
120
+ attribute = IndifferentCoder.as_indifferent_hash(attribute)
121
+ send :"#{store_attribute}=", attribute
122
+ end
123
+ attribute
124
+ end
125
+
126
+ class IndifferentCoder # :nodoc:
127
+ def initialize(coder_or_class_name)
128
+ @coder =
129
+ if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump)
130
+ coder_or_class_name
131
+ else
132
+ ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object)
47
133
  end
134
+ end
135
+
136
+ def dump(obj)
137
+ @coder.dump self.class.as_indifferent_hash(obj)
138
+ end
139
+
140
+ def load(yaml)
141
+ self.class.as_indifferent_hash @coder.load(yaml)
142
+ end
143
+
144
+ def self.as_indifferent_hash(obj)
145
+ case obj
146
+ when ActiveSupport::HashWithIndifferentAccess
147
+ obj
148
+ when Hash
149
+ obj.with_indifferent_access
150
+ else
151
+ ActiveSupport::HashWithIndifferentAccess.new
48
152
  end
49
153
  end
50
154
  end
51
155
  end
52
- end
156
+ end