activerecord 4.2.0

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 (221) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1372 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +218 -0
  5. data/examples/performance.rb +184 -0
  6. data/examples/simple.rb +14 -0
  7. data/lib/active_record.rb +173 -0
  8. data/lib/active_record/aggregations.rb +266 -0
  9. data/lib/active_record/association_relation.rb +22 -0
  10. data/lib/active_record/associations.rb +1724 -0
  11. data/lib/active_record/associations/alias_tracker.rb +87 -0
  12. data/lib/active_record/associations/association.rb +253 -0
  13. data/lib/active_record/associations/association_scope.rb +194 -0
  14. data/lib/active_record/associations/belongs_to_association.rb +111 -0
  15. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +40 -0
  16. data/lib/active_record/associations/builder/association.rb +149 -0
  17. data/lib/active_record/associations/builder/belongs_to.rb +116 -0
  18. data/lib/active_record/associations/builder/collection_association.rb +91 -0
  19. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +124 -0
  20. data/lib/active_record/associations/builder/has_many.rb +15 -0
  21. data/lib/active_record/associations/builder/has_one.rb +23 -0
  22. data/lib/active_record/associations/builder/singular_association.rb +38 -0
  23. data/lib/active_record/associations/collection_association.rb +634 -0
  24. data/lib/active_record/associations/collection_proxy.rb +1027 -0
  25. data/lib/active_record/associations/has_many_association.rb +184 -0
  26. data/lib/active_record/associations/has_many_through_association.rb +238 -0
  27. data/lib/active_record/associations/has_one_association.rb +105 -0
  28. data/lib/active_record/associations/has_one_through_association.rb +36 -0
  29. data/lib/active_record/associations/join_dependency.rb +282 -0
  30. data/lib/active_record/associations/join_dependency/join_association.rb +122 -0
  31. data/lib/active_record/associations/join_dependency/join_base.rb +22 -0
  32. data/lib/active_record/associations/join_dependency/join_part.rb +71 -0
  33. data/lib/active_record/associations/preloader.rb +203 -0
  34. data/lib/active_record/associations/preloader/association.rb +162 -0
  35. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  36. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  37. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  38. data/lib/active_record/associations/preloader/has_many_through.rb +19 -0
  39. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  40. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  41. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  42. data/lib/active_record/associations/preloader/through_association.rb +96 -0
  43. data/lib/active_record/associations/singular_association.rb +86 -0
  44. data/lib/active_record/associations/through_association.rb +96 -0
  45. data/lib/active_record/attribute.rb +149 -0
  46. data/lib/active_record/attribute_assignment.rb +212 -0
  47. data/lib/active_record/attribute_decorators.rb +66 -0
  48. data/lib/active_record/attribute_methods.rb +439 -0
  49. data/lib/active_record/attribute_methods/before_type_cast.rb +71 -0
  50. data/lib/active_record/attribute_methods/dirty.rb +181 -0
  51. data/lib/active_record/attribute_methods/primary_key.rb +128 -0
  52. data/lib/active_record/attribute_methods/query.rb +40 -0
  53. data/lib/active_record/attribute_methods/read.rb +103 -0
  54. data/lib/active_record/attribute_methods/serialization.rb +70 -0
  55. data/lib/active_record/attribute_methods/time_zone_conversion.rb +65 -0
  56. data/lib/active_record/attribute_methods/write.rb +83 -0
  57. data/lib/active_record/attribute_set.rb +77 -0
  58. data/lib/active_record/attribute_set/builder.rb +86 -0
  59. data/lib/active_record/attributes.rb +139 -0
  60. data/lib/active_record/autosave_association.rb +439 -0
  61. data/lib/active_record/base.rb +317 -0
  62. data/lib/active_record/callbacks.rb +313 -0
  63. data/lib/active_record/coders/json.rb +13 -0
  64. data/lib/active_record/coders/yaml_column.rb +38 -0
  65. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +659 -0
  66. data/lib/active_record/connection_adapters/abstract/database_limits.rb +67 -0
  67. data/lib/active_record/connection_adapters/abstract/database_statements.rb +373 -0
  68. data/lib/active_record/connection_adapters/abstract/query_cache.rb +95 -0
  69. data/lib/active_record/connection_adapters/abstract/quoting.rb +133 -0
  70. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  71. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +125 -0
  72. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +574 -0
  73. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +50 -0
  74. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +991 -0
  75. data/lib/active_record/connection_adapters/abstract/transaction.rb +219 -0
  76. data/lib/active_record/connection_adapters/abstract_adapter.rb +487 -0
  77. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +883 -0
  78. data/lib/active_record/connection_adapters/column.rb +82 -0
  79. data/lib/active_record/connection_adapters/connection_specification.rb +275 -0
  80. data/lib/active_record/connection_adapters/mysql2_adapter.rb +282 -0
  81. data/lib/active_record/connection_adapters/mysql_adapter.rb +491 -0
  82. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +93 -0
  83. data/lib/active_record/connection_adapters/postgresql/column.rb +20 -0
  84. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +232 -0
  85. data/lib/active_record/connection_adapters/postgresql/oid.rb +36 -0
  86. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +99 -0
  87. data/lib/active_record/connection_adapters/postgresql/oid/bit.rb +52 -0
  88. data/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +13 -0
  89. data/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +14 -0
  90. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +46 -0
  91. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +11 -0
  92. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +27 -0
  93. data/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +13 -0
  94. data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +17 -0
  95. data/lib/active_record/connection_adapters/postgresql/oid/float.rb +21 -0
  96. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +59 -0
  97. data/lib/active_record/connection_adapters/postgresql/oid/inet.rb +13 -0
  98. data/lib/active_record/connection_adapters/postgresql/oid/infinity.rb +13 -0
  99. data/lib/active_record/connection_adapters/postgresql/oid/integer.rb +11 -0
  100. data/lib/active_record/connection_adapters/postgresql/oid/json.rb +35 -0
  101. data/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +23 -0
  102. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +43 -0
  103. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +43 -0
  104. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +79 -0
  105. data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +15 -0
  106. data/lib/active_record/connection_adapters/postgresql/oid/time.rb +11 -0
  107. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +97 -0
  108. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +21 -0
  109. data/lib/active_record/connection_adapters/postgresql/oid/vector.rb +26 -0
  110. data/lib/active_record/connection_adapters/postgresql/oid/xml.rb +28 -0
  111. data/lib/active_record/connection_adapters/postgresql/quoting.rb +108 -0
  112. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  113. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +152 -0
  114. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +588 -0
  115. data/lib/active_record/connection_adapters/postgresql/utils.rb +77 -0
  116. data/lib/active_record/connection_adapters/postgresql_adapter.rb +754 -0
  117. data/lib/active_record/connection_adapters/schema_cache.rb +94 -0
  118. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +628 -0
  119. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  120. data/lib/active_record/connection_handling.rb +132 -0
  121. data/lib/active_record/core.rb +566 -0
  122. data/lib/active_record/counter_cache.rb +175 -0
  123. data/lib/active_record/dynamic_matchers.rb +140 -0
  124. data/lib/active_record/enum.rb +198 -0
  125. data/lib/active_record/errors.rb +252 -0
  126. data/lib/active_record/explain.rb +38 -0
  127. data/lib/active_record/explain_registry.rb +30 -0
  128. data/lib/active_record/explain_subscriber.rb +29 -0
  129. data/lib/active_record/fixture_set/file.rb +56 -0
  130. data/lib/active_record/fixtures.rb +1007 -0
  131. data/lib/active_record/gem_version.rb +15 -0
  132. data/lib/active_record/inheritance.rb +247 -0
  133. data/lib/active_record/integration.rb +113 -0
  134. data/lib/active_record/locale/en.yml +47 -0
  135. data/lib/active_record/locking/optimistic.rb +204 -0
  136. data/lib/active_record/locking/pessimistic.rb +77 -0
  137. data/lib/active_record/log_subscriber.rb +75 -0
  138. data/lib/active_record/migration.rb +1051 -0
  139. data/lib/active_record/migration/command_recorder.rb +197 -0
  140. data/lib/active_record/migration/join_table.rb +15 -0
  141. data/lib/active_record/model_schema.rb +340 -0
  142. data/lib/active_record/nested_attributes.rb +548 -0
  143. data/lib/active_record/no_touching.rb +52 -0
  144. data/lib/active_record/null_relation.rb +81 -0
  145. data/lib/active_record/persistence.rb +532 -0
  146. data/lib/active_record/query_cache.rb +56 -0
  147. data/lib/active_record/querying.rb +68 -0
  148. data/lib/active_record/railtie.rb +162 -0
  149. data/lib/active_record/railties/console_sandbox.rb +5 -0
  150. data/lib/active_record/railties/controller_runtime.rb +50 -0
  151. data/lib/active_record/railties/databases.rake +391 -0
  152. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  153. data/lib/active_record/readonly_attributes.rb +23 -0
  154. data/lib/active_record/reflection.rb +881 -0
  155. data/lib/active_record/relation.rb +681 -0
  156. data/lib/active_record/relation/batches.rb +138 -0
  157. data/lib/active_record/relation/calculations.rb +403 -0
  158. data/lib/active_record/relation/delegation.rb +140 -0
  159. data/lib/active_record/relation/finder_methods.rb +528 -0
  160. data/lib/active_record/relation/merger.rb +170 -0
  161. data/lib/active_record/relation/predicate_builder.rb +126 -0
  162. data/lib/active_record/relation/predicate_builder/array_handler.rb +47 -0
  163. data/lib/active_record/relation/predicate_builder/relation_handler.rb +13 -0
  164. data/lib/active_record/relation/query_methods.rb +1176 -0
  165. data/lib/active_record/relation/spawn_methods.rb +75 -0
  166. data/lib/active_record/result.rb +131 -0
  167. data/lib/active_record/runtime_registry.rb +22 -0
  168. data/lib/active_record/sanitization.rb +191 -0
  169. data/lib/active_record/schema.rb +64 -0
  170. data/lib/active_record/schema_dumper.rb +251 -0
  171. data/lib/active_record/schema_migration.rb +56 -0
  172. data/lib/active_record/scoping.rb +87 -0
  173. data/lib/active_record/scoping/default.rb +134 -0
  174. data/lib/active_record/scoping/named.rb +164 -0
  175. data/lib/active_record/serialization.rb +22 -0
  176. data/lib/active_record/serializers/xml_serializer.rb +193 -0
  177. data/lib/active_record/statement_cache.rb +111 -0
  178. data/lib/active_record/store.rb +205 -0
  179. data/lib/active_record/tasks/database_tasks.rb +296 -0
  180. data/lib/active_record/tasks/mysql_database_tasks.rb +145 -0
  181. data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
  182. data/lib/active_record/tasks/sqlite_database_tasks.rb +55 -0
  183. data/lib/active_record/timestamp.rb +121 -0
  184. data/lib/active_record/transactions.rb +417 -0
  185. data/lib/active_record/translation.rb +22 -0
  186. data/lib/active_record/type.rb +23 -0
  187. data/lib/active_record/type/big_integer.rb +13 -0
  188. data/lib/active_record/type/binary.rb +50 -0
  189. data/lib/active_record/type/boolean.rb +30 -0
  190. data/lib/active_record/type/date.rb +46 -0
  191. data/lib/active_record/type/date_time.rb +43 -0
  192. data/lib/active_record/type/decimal.rb +40 -0
  193. data/lib/active_record/type/decimal_without_scale.rb +11 -0
  194. data/lib/active_record/type/decorator.rb +14 -0
  195. data/lib/active_record/type/float.rb +19 -0
  196. data/lib/active_record/type/hash_lookup_type_map.rb +17 -0
  197. data/lib/active_record/type/integer.rb +55 -0
  198. data/lib/active_record/type/mutable.rb +16 -0
  199. data/lib/active_record/type/numeric.rb +36 -0
  200. data/lib/active_record/type/serialized.rb +56 -0
  201. data/lib/active_record/type/string.rb +36 -0
  202. data/lib/active_record/type/text.rb +11 -0
  203. data/lib/active_record/type/time.rb +26 -0
  204. data/lib/active_record/type/time_value.rb +38 -0
  205. data/lib/active_record/type/type_map.rb +64 -0
  206. data/lib/active_record/type/unsigned_integer.rb +15 -0
  207. data/lib/active_record/type/value.rb +101 -0
  208. data/lib/active_record/validations.rb +90 -0
  209. data/lib/active_record/validations/associated.rb +51 -0
  210. data/lib/active_record/validations/presence.rb +67 -0
  211. data/lib/active_record/validations/uniqueness.rb +229 -0
  212. data/lib/active_record/version.rb +8 -0
  213. data/lib/rails/generators/active_record.rb +17 -0
  214. data/lib/rails/generators/active_record/migration.rb +18 -0
  215. data/lib/rails/generators/active_record/migration/migration_generator.rb +70 -0
  216. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +22 -0
  217. data/lib/rails/generators/active_record/migration/templates/migration.rb +45 -0
  218. data/lib/rails/generators/active_record/model/model_generator.rb +52 -0
  219. data/lib/rails/generators/active_record/model/templates/model.rb +10 -0
  220. data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
  221. metadata +309 -0
@@ -0,0 +1,175 @@
1
+ module ActiveRecord
2
+ # = Active Record Counter Cache
3
+ module CounterCache
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ # Resets one or more counter caches to their correct value using an SQL
8
+ # count query. This is useful when adding new counter caches, or if the
9
+ # counter has been corrupted or modified directly by SQL.
10
+ #
11
+ # ==== Parameters
12
+ #
13
+ # * +id+ - The id of the object you wish to reset a counter on.
14
+ # * +counters+ - One or more association counters to reset. Association name or counter name can be given.
15
+ #
16
+ # ==== Examples
17
+ #
18
+ # # For Post with id #1 records reset the comments_count
19
+ # Post.reset_counters(1, :comments)
20
+ def reset_counters(id, *counters)
21
+ object = find(id)
22
+ counters.each do |counter_association|
23
+ has_many_association = _reflect_on_association(counter_association)
24
+ unless has_many_association
25
+ has_many = reflect_on_all_associations(:has_many)
26
+ has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym }
27
+ counter_association = has_many_association.plural_name if has_many_association
28
+ end
29
+ raise ArgumentError, "'#{self.name}' has no association called '#{counter_association}'" unless has_many_association
30
+
31
+ if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
32
+ has_many_association = has_many_association.through_reflection
33
+ end
34
+
35
+ foreign_key = has_many_association.foreign_key.to_s
36
+ child_class = has_many_association.klass
37
+ reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
38
+ counter_name = reflection.counter_cache_column
39
+
40
+ stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
41
+ arel_table[counter_name] => object.send(counter_association).count(:all)
42
+ }, primary_key)
43
+ connection.update stmt
44
+ end
45
+ return true
46
+ end
47
+
48
+ # A generic "counter updater" implementation, intended primarily to be
49
+ # used by increment_counter and decrement_counter, but which may also
50
+ # be useful on its own. It simply does a direct SQL update for the record
51
+ # with the given ID, altering the given hash of counters by the amount
52
+ # given by the corresponding value:
53
+ #
54
+ # ==== Parameters
55
+ #
56
+ # * +id+ - The id of the object you wish to update a counter on or an Array of ids.
57
+ # * +counters+ - A Hash containing the names of the fields
58
+ # to update as keys and the amount to update the field by as values.
59
+ #
60
+ # ==== Examples
61
+ #
62
+ # # For the Post with id of 5, decrement the comment_count by 1, and
63
+ # # increment the action_count by 1
64
+ # Post.update_counters 5, comment_count: -1, action_count: 1
65
+ # # Executes the following SQL:
66
+ # # UPDATE posts
67
+ # # SET comment_count = COALESCE(comment_count, 0) - 1,
68
+ # # action_count = COALESCE(action_count, 0) + 1
69
+ # # WHERE id = 5
70
+ #
71
+ # # For the Posts with id of 10 and 15, increment the comment_count by 1
72
+ # Post.update_counters [10, 15], comment_count: 1
73
+ # # Executes the following SQL:
74
+ # # UPDATE posts
75
+ # # SET comment_count = COALESCE(comment_count, 0) + 1
76
+ # # WHERE id IN (10, 15)
77
+ def update_counters(id, counters)
78
+ updates = counters.map do |counter_name, value|
79
+ operator = value < 0 ? '-' : '+'
80
+ quoted_column = connection.quote_column_name(counter_name)
81
+ "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
82
+ end
83
+
84
+ unscoped.where(primary_key => id).update_all updates.join(', ')
85
+ end
86
+
87
+ # Increment a numeric field by one, via a direct SQL update.
88
+ #
89
+ # This method is used primarily for maintaining counter_cache columns that are
90
+ # used to store aggregate values. For example, a DiscussionBoard may cache
91
+ # posts_count and comments_count to avoid running an SQL query to calculate the
92
+ # number of posts and comments there are, each time it is displayed.
93
+ #
94
+ # ==== Parameters
95
+ #
96
+ # * +counter_name+ - The name of the field that should be incremented.
97
+ # * +id+ - The id of the object that should be incremented or an Array of ids.
98
+ #
99
+ # ==== Examples
100
+ #
101
+ # # Increment the post_count column for the record with an id of 5
102
+ # DiscussionBoard.increment_counter(:post_count, 5)
103
+ def increment_counter(counter_name, id)
104
+ update_counters(id, counter_name => 1)
105
+ end
106
+
107
+ # Decrement a numeric field by one, via a direct SQL update.
108
+ #
109
+ # This works the same as increment_counter but reduces the column value by
110
+ # 1 instead of increasing it.
111
+ #
112
+ # ==== Parameters
113
+ #
114
+ # * +counter_name+ - The name of the field that should be decremented.
115
+ # * +id+ - The id of the object that should be decremented or an Array of ids.
116
+ #
117
+ # ==== Examples
118
+ #
119
+ # # Decrement the post_count column for the record with an id of 5
120
+ # DiscussionBoard.decrement_counter(:post_count, 5)
121
+ def decrement_counter(counter_name, id)
122
+ update_counters(id, counter_name => -1)
123
+ end
124
+ end
125
+
126
+ protected
127
+
128
+ def actually_destroyed?
129
+ @_actually_destroyed
130
+ end
131
+
132
+ def clear_destroy_state
133
+ @_actually_destroyed = nil
134
+ end
135
+
136
+ private
137
+
138
+ def _create_record(*)
139
+ id = super
140
+
141
+ each_counter_cached_associations do |association|
142
+ if send(association.reflection.name)
143
+ association.increment_counters
144
+ @_after_create_counter_called = true
145
+ end
146
+ end
147
+
148
+ id
149
+ end
150
+
151
+ def destroy_row
152
+ affected_rows = super
153
+
154
+ if affected_rows > 0
155
+ each_counter_cached_associations do |association|
156
+ foreign_key = association.reflection.foreign_key.to_sym
157
+ unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
158
+ if send(association.reflection.name)
159
+ association.decrement_counters
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ affected_rows
166
+ end
167
+
168
+ def each_counter_cached_associations
169
+ _reflections.each do |name, reflection|
170
+ yield association(name) if reflection.belongs_to? && reflection.counter_cache_column
171
+ end
172
+ end
173
+
174
+ end
175
+ end
@@ -0,0 +1,140 @@
1
+ module ActiveRecord
2
+ module DynamicMatchers #:nodoc:
3
+ # This code in this file seems to have a lot of indirection, but the indirection
4
+ # is there to provide extension points for the activerecord-deprecated_finders
5
+ # gem. When we stop supporting activerecord-deprecated_finders (from Rails 5),
6
+ # then we can remove the indirection.
7
+
8
+ def respond_to?(name, include_private = false)
9
+ if self == Base
10
+ super
11
+ else
12
+ match = Method.match(self, name)
13
+ match && match.valid? || super
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def method_missing(name, *arguments, &block)
20
+ match = Method.match(self, name)
21
+
22
+ if match && match.valid?
23
+ match.define
24
+ send(name, *arguments, &block)
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ class Method
31
+ @matchers = []
32
+
33
+ class << self
34
+ attr_reader :matchers
35
+
36
+ def match(model, name)
37
+ klass = matchers.find { |k| name =~ k.pattern }
38
+ klass.new(model, name) if klass
39
+ end
40
+
41
+ def pattern
42
+ @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
43
+ end
44
+
45
+ def prefix
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def suffix
50
+ ''
51
+ end
52
+ end
53
+
54
+ attr_reader :model, :name, :attribute_names
55
+
56
+ def initialize(model, name)
57
+ @model = model
58
+ @name = name.to_s
59
+ @attribute_names = @name.match(self.class.pattern)[1].split('_and_')
60
+ @attribute_names.map! { |n| @model.attribute_aliases[n] || n }
61
+ end
62
+
63
+ def valid?
64
+ attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
65
+ end
66
+
67
+ def define
68
+ model.class_eval <<-CODE, __FILE__, __LINE__ + 1
69
+ def self.#{name}(#{signature})
70
+ #{body}
71
+ end
72
+ CODE
73
+ end
74
+
75
+ def body
76
+ raise NotImplementedError
77
+ end
78
+ end
79
+
80
+ module Finder
81
+ # Extended in activerecord-deprecated_finders
82
+ def body
83
+ result
84
+ end
85
+
86
+ # Extended in activerecord-deprecated_finders
87
+ def result
88
+ "#{finder}(#{attributes_hash})"
89
+ end
90
+
91
+ # The parameters in the signature may have reserved Ruby words, in order
92
+ # to prevent errors, we start each param name with `_`.
93
+ #
94
+ # Extended in activerecord-deprecated_finders
95
+ def signature
96
+ attribute_names.map { |name| "_#{name}" }.join(', ')
97
+ end
98
+
99
+ # Given that the parameters starts with `_`, the finder needs to use the
100
+ # same parameter name.
101
+ def attributes_hash
102
+ "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(',') + "}"
103
+ end
104
+
105
+ def finder
106
+ raise NotImplementedError
107
+ end
108
+ end
109
+
110
+ class FindBy < Method
111
+ Method.matchers << self
112
+ include Finder
113
+
114
+ def self.prefix
115
+ "find_by"
116
+ end
117
+
118
+ def finder
119
+ "find_by"
120
+ end
121
+ end
122
+
123
+ class FindByBang < Method
124
+ Method.matchers << self
125
+ include Finder
126
+
127
+ def self.prefix
128
+ "find_by"
129
+ end
130
+
131
+ def self.suffix
132
+ "!"
133
+ end
134
+
135
+ def finder
136
+ "find_by!"
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,198 @@
1
+ require 'active_support/core_ext/object/deep_dup'
2
+
3
+ module ActiveRecord
4
+ # Declare an enum attribute where the values map to integers in the database,
5
+ # but can be queried by name. Example:
6
+ #
7
+ # class Conversation < ActiveRecord::Base
8
+ # enum status: [ :active, :archived ]
9
+ # end
10
+ #
11
+ # # conversation.update! status: 0
12
+ # conversation.active!
13
+ # conversation.active? # => true
14
+ # conversation.status # => "active"
15
+ #
16
+ # # conversation.update! status: 1
17
+ # conversation.archived!
18
+ # conversation.archived? # => true
19
+ # conversation.status # => "archived"
20
+ #
21
+ # # conversation.update! status: 1
22
+ # conversation.status = "archived"
23
+ #
24
+ # # conversation.update! status: nil
25
+ # conversation.status = nil
26
+ # conversation.status.nil? # => true
27
+ # conversation.status # => nil
28
+ #
29
+ # Scopes based on the allowed values of the enum field will be provided
30
+ # as well. With the above example:
31
+ #
32
+ # Conversation.active
33
+ # Conversation.archived
34
+ #
35
+ # You can set the default value from the database declaration, like:
36
+ #
37
+ # create_table :conversations do |t|
38
+ # t.column :status, :integer, default: 0
39
+ # end
40
+ #
41
+ # Good practice is to let the first declared status be the default.
42
+ #
43
+ # Finally, it's also possible to explicitly map the relation between attribute and
44
+ # database integer with a +Hash+:
45
+ #
46
+ # class Conversation < ActiveRecord::Base
47
+ # enum status: { active: 0, archived: 1 }
48
+ # end
49
+ #
50
+ # Note that when an +Array+ is used, the implicit mapping from the values to database
51
+ # integers is derived from the order the values appear in the array. In the example,
52
+ # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
53
+ # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the
54
+ # database.
55
+ #
56
+ # Therefore, once a value is added to the enum array, its position in the array must
57
+ # be maintained, and new values should only be added to the end of the array. To
58
+ # remove unused values, the explicit +Hash+ syntax should be used.
59
+ #
60
+ # In rare circumstances you might need to access the mapping directly.
61
+ # The mappings are exposed through a class method with the pluralized attribute
62
+ # name:
63
+ #
64
+ # Conversation.statuses # => { "active" => 0, "archived" => 1 }
65
+ #
66
+ # Use that class method when you need to know the ordinal value of an enum:
67
+ #
68
+ # Conversation.where("status <> ?", Conversation.statuses[:archived])
69
+ #
70
+ # Where conditions on an enum attribute must use the ordinal value of an enum.
71
+ module Enum
72
+ def self.extended(base) # :nodoc:
73
+ base.class_attribute(:defined_enums)
74
+ base.defined_enums = {}
75
+ end
76
+
77
+ def inherited(base) # :nodoc:
78
+ base.defined_enums = defined_enums.deep_dup
79
+ super
80
+ end
81
+
82
+ def enum(definitions)
83
+ klass = self
84
+ definitions.each do |name, values|
85
+ # statuses = { }
86
+ enum_values = ActiveSupport::HashWithIndifferentAccess.new
87
+ name = name.to_sym
88
+
89
+ # def self.statuses statuses end
90
+ detect_enum_conflict!(name, name.to_s.pluralize, true)
91
+ klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
92
+
93
+ _enum_methods_module.module_eval do
94
+ # def status=(value) self[:status] = statuses[value] end
95
+ klass.send(:detect_enum_conflict!, name, "#{name}=")
96
+ define_method("#{name}=") { |value|
97
+ if enum_values.has_key?(value) || value.blank?
98
+ self[name] = enum_values[value]
99
+ elsif enum_values.has_value?(value)
100
+ # Assigning a value directly is not a end-user feature, hence it's not documented.
101
+ # This is used internally to make building objects from the generated scopes work
102
+ # as expected, i.e. +Conversation.archived.build.archived?+ should be true.
103
+ self[name] = value
104
+ else
105
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
106
+ end
107
+ }
108
+
109
+ # def status() statuses.key self[:status] end
110
+ klass.send(:detect_enum_conflict!, name, name)
111
+ define_method(name) { enum_values.key self[name] }
112
+
113
+ # def status_before_type_cast() statuses.key self[:status] end
114
+ klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast")
115
+ define_method("#{name}_before_type_cast") { enum_values.key self[name] }
116
+
117
+ pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
118
+ pairs.each do |value, i|
119
+ enum_values[value] = i
120
+
121
+ # def active?() status == 0 end
122
+ klass.send(:detect_enum_conflict!, name, "#{value}?")
123
+ define_method("#{value}?") { self[name] == i }
124
+
125
+ # def active!() update! status: :active end
126
+ klass.send(:detect_enum_conflict!, name, "#{value}!")
127
+ define_method("#{value}!") { update! name => value }
128
+
129
+ # scope :active, -> { where status: 0 }
130
+ klass.send(:detect_enum_conflict!, name, value, true)
131
+ klass.scope value, -> { klass.where name => i }
132
+ end
133
+ end
134
+ defined_enums[name.to_s] = enum_values
135
+ end
136
+ end
137
+
138
+ private
139
+ def _enum_methods_module
140
+ @_enum_methods_module ||= begin
141
+ mod = Module.new do
142
+ private
143
+ def save_changed_attribute(attr_name, old)
144
+ if (mapping = self.class.defined_enums[attr_name.to_s])
145
+ value = _read_attribute(attr_name)
146
+ if attribute_changed?(attr_name)
147
+ if mapping[old] == value
148
+ clear_attribute_changes([attr_name])
149
+ end
150
+ else
151
+ if old != value
152
+ set_attribute_was(attr_name, mapping.key(old))
153
+ end
154
+ end
155
+ else
156
+ super
157
+ end
158
+ end
159
+ end
160
+ include mod
161
+ mod
162
+ end
163
+ end
164
+
165
+ ENUM_CONFLICT_MESSAGE = \
166
+ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
167
+ "this will generate a %{type} method \"%{method}\", which is already defined " \
168
+ "by %{source}."
169
+
170
+ def detect_enum_conflict!(enum_name, method_name, klass_method = false)
171
+ if klass_method && dangerous_class_method?(method_name)
172
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
173
+ enum: enum_name,
174
+ klass: self.name,
175
+ type: 'class',
176
+ method: method_name,
177
+ source: 'Active Record'
178
+ }
179
+ elsif !klass_method && dangerous_attribute_method?(method_name)
180
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
181
+ enum: enum_name,
182
+ klass: self.name,
183
+ type: 'instance',
184
+ method: method_name,
185
+ source: 'Active Record'
186
+ }
187
+ elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
188
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
189
+ enum: enum_name,
190
+ klass: self.name,
191
+ type: 'instance',
192
+ method: method_name,
193
+ source: 'another enum'
194
+ }
195
+ end
196
+ end
197
+ end
198
+ end