activerecord 3.1.10 → 4.2.11

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.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (237) hide show
  1. checksums.yaml +6 -6
  2. data/CHANGELOG.md +1837 -338
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +39 -43
  5. data/examples/performance.rb +51 -20
  6. data/examples/simple.rb +4 -4
  7. data/lib/active_record/aggregations.rb +57 -43
  8. data/lib/active_record/association_relation.rb +35 -0
  9. data/lib/active_record/associations/alias_tracker.rb +47 -39
  10. data/lib/active_record/associations/association.rb +71 -85
  11. data/lib/active_record/associations/association_scope.rb +138 -89
  12. data/lib/active_record/associations/belongs_to_association.rb +65 -25
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +9 -3
  14. data/lib/active_record/associations/builder/association.rb +125 -29
  15. data/lib/active_record/associations/builder/belongs_to.rb +91 -60
  16. data/lib/active_record/associations/builder/collection_association.rb +69 -49
  17. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +113 -42
  18. data/lib/active_record/associations/builder/has_many.rb +8 -64
  19. data/lib/active_record/associations/builder/has_one.rb +12 -52
  20. data/lib/active_record/associations/builder/singular_association.rb +22 -29
  21. data/lib/active_record/associations/collection_association.rb +294 -187
  22. data/lib/active_record/associations/collection_proxy.rb +961 -94
  23. data/lib/active_record/associations/foreign_association.rb +11 -0
  24. data/lib/active_record/associations/has_many_association.rb +118 -23
  25. data/lib/active_record/associations/has_many_through_association.rb +115 -45
  26. data/lib/active_record/associations/has_one_association.rb +57 -24
  27. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  28. data/lib/active_record/associations/join_dependency/join_association.rb +76 -102
  29. data/lib/active_record/associations/join_dependency/join_base.rb +7 -9
  30. data/lib/active_record/associations/join_dependency/join_part.rb +30 -37
  31. data/lib/active_record/associations/join_dependency.rb +230 -156
  32. data/lib/active_record/associations/preloader/association.rb +96 -55
  33. data/lib/active_record/associations/preloader/collection_association.rb +3 -3
  34. data/lib/active_record/associations/preloader/has_many_through.rb +7 -3
  35. data/lib/active_record/associations/preloader/has_one.rb +1 -1
  36. data/lib/active_record/associations/preloader/singular_association.rb +3 -3
  37. data/lib/active_record/associations/preloader/through_association.rb +61 -32
  38. data/lib/active_record/associations/preloader.rb +113 -87
  39. data/lib/active_record/associations/singular_association.rb +29 -13
  40. data/lib/active_record/associations/through_association.rb +37 -19
  41. data/lib/active_record/associations.rb +505 -371
  42. data/lib/active_record/attribute.rb +163 -0
  43. data/lib/active_record/attribute_assignment.rb +212 -0
  44. data/lib/active_record/attribute_decorators.rb +66 -0
  45. data/lib/active_record/attribute_methods/before_type_cast.rb +52 -7
  46. data/lib/active_record/attribute_methods/dirty.rb +141 -51
  47. data/lib/active_record/attribute_methods/primary_key.rb +87 -36
  48. data/lib/active_record/attribute_methods/query.rb +5 -4
  49. data/lib/active_record/attribute_methods/read.rb +74 -117
  50. data/lib/active_record/attribute_methods/serialization.rb +70 -0
  51. data/lib/active_record/attribute_methods/time_zone_conversion.rb +49 -47
  52. data/lib/active_record/attribute_methods/write.rb +60 -21
  53. data/lib/active_record/attribute_methods.rb +409 -48
  54. data/lib/active_record/attribute_set/builder.rb +106 -0
  55. data/lib/active_record/attribute_set.rb +81 -0
  56. data/lib/active_record/attributes.rb +147 -0
  57. data/lib/active_record/autosave_association.rb +279 -232
  58. data/lib/active_record/base.rb +84 -1969
  59. data/lib/active_record/callbacks.rb +66 -28
  60. data/lib/active_record/coders/json.rb +13 -0
  61. data/lib/active_record/coders/yaml_column.rb +18 -21
  62. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +422 -243
  63. data/lib/active_record/connection_adapters/abstract/database_limits.rb +9 -0
  64. data/lib/active_record/connection_adapters/abstract/database_statements.rb +170 -194
  65. data/lib/active_record/connection_adapters/abstract/query_cache.rb +32 -19
  66. data/lib/active_record/connection_adapters/abstract/quoting.rb +79 -57
  67. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  68. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +125 -0
  69. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +273 -170
  70. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +50 -0
  71. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +731 -254
  72. data/lib/active_record/connection_adapters/abstract/transaction.rb +215 -0
  73. data/lib/active_record/connection_adapters/abstract_adapter.rb +339 -95
  74. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +946 -0
  75. data/lib/active_record/connection_adapters/column.rb +33 -221
  76. data/lib/active_record/connection_adapters/connection_specification.rb +275 -0
  77. data/lib/active_record/connection_adapters/mysql2_adapter.rb +140 -602
  78. data/lib/active_record/connection_adapters/mysql_adapter.rb +254 -756
  79. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +93 -0
  80. data/lib/active_record/connection_adapters/postgresql/column.rb +20 -0
  81. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +232 -0
  82. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +100 -0
  83. data/lib/active_record/connection_adapters/postgresql/oid/bit.rb +52 -0
  84. data/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +13 -0
  85. data/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +15 -0
  86. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +46 -0
  87. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +11 -0
  88. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +36 -0
  89. data/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +13 -0
  90. data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +19 -0
  91. data/lib/active_record/connection_adapters/postgresql/oid/float.rb +21 -0
  92. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +59 -0
  93. data/lib/active_record/connection_adapters/postgresql/oid/inet.rb +13 -0
  94. data/lib/active_record/connection_adapters/postgresql/oid/infinity.rb +13 -0
  95. data/lib/active_record/connection_adapters/postgresql/oid/integer.rb +11 -0
  96. data/lib/active_record/connection_adapters/postgresql/oid/json.rb +35 -0
  97. data/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +23 -0
  98. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +43 -0
  99. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +43 -0
  100. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +79 -0
  101. data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +19 -0
  102. data/lib/active_record/connection_adapters/postgresql/oid/time.rb +11 -0
  103. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +109 -0
  104. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +21 -0
  105. data/lib/active_record/connection_adapters/postgresql/oid/vector.rb +26 -0
  106. data/lib/active_record/connection_adapters/postgresql/oid/xml.rb +28 -0
  107. data/lib/active_record/connection_adapters/postgresql/oid.rb +36 -0
  108. data/lib/active_record/connection_adapters/postgresql/quoting.rb +108 -0
  109. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  110. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +152 -0
  111. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +596 -0
  112. data/lib/active_record/connection_adapters/postgresql/utils.rb +77 -0
  113. data/lib/active_record/connection_adapters/postgresql_adapter.rb +445 -902
  114. data/lib/active_record/connection_adapters/schema_cache.rb +94 -0
  115. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +578 -25
  116. data/lib/active_record/connection_handling.rb +132 -0
  117. data/lib/active_record/core.rb +579 -0
  118. data/lib/active_record/counter_cache.rb +159 -102
  119. data/lib/active_record/dynamic_matchers.rb +140 -0
  120. data/lib/active_record/enum.rb +197 -0
  121. data/lib/active_record/errors.rb +102 -34
  122. data/lib/active_record/explain.rb +38 -0
  123. data/lib/active_record/explain_registry.rb +30 -0
  124. data/lib/active_record/explain_subscriber.rb +29 -0
  125. data/lib/active_record/fixture_set/file.rb +56 -0
  126. data/lib/active_record/fixtures.rb +318 -260
  127. data/lib/active_record/gem_version.rb +15 -0
  128. data/lib/active_record/inheritance.rb +247 -0
  129. data/lib/active_record/integration.rb +113 -0
  130. data/lib/active_record/legacy_yaml_adapter.rb +30 -0
  131. data/lib/active_record/locale/en.yml +8 -1
  132. data/lib/active_record/locking/optimistic.rb +80 -52
  133. data/lib/active_record/locking/pessimistic.rb +27 -5
  134. data/lib/active_record/log_subscriber.rb +25 -18
  135. data/lib/active_record/migration/command_recorder.rb +130 -38
  136. data/lib/active_record/migration/join_table.rb +15 -0
  137. data/lib/active_record/migration.rb +532 -201
  138. data/lib/active_record/model_schema.rb +342 -0
  139. data/lib/active_record/nested_attributes.rb +229 -139
  140. data/lib/active_record/no_touching.rb +52 -0
  141. data/lib/active_record/null_relation.rb +81 -0
  142. data/lib/active_record/persistence.rb +304 -99
  143. data/lib/active_record/query_cache.rb +25 -43
  144. data/lib/active_record/querying.rb +68 -0
  145. data/lib/active_record/railtie.rb +86 -45
  146. data/lib/active_record/railties/console_sandbox.rb +3 -4
  147. data/lib/active_record/railties/controller_runtime.rb +7 -4
  148. data/lib/active_record/railties/databases.rake +198 -377
  149. data/lib/active_record/railties/jdbcmysql_error.rb +2 -2
  150. data/lib/active_record/readonly_attributes.rb +23 -0
  151. data/lib/active_record/reflection.rb +516 -165
  152. data/lib/active_record/relation/batches.rb +96 -45
  153. data/lib/active_record/relation/calculations.rb +221 -144
  154. data/lib/active_record/relation/delegation.rb +140 -0
  155. data/lib/active_record/relation/finder_methods.rb +362 -243
  156. data/lib/active_record/relation/merger.rb +193 -0
  157. data/lib/active_record/relation/predicate_builder/array_handler.rb +48 -0
  158. data/lib/active_record/relation/predicate_builder/relation_handler.rb +13 -0
  159. data/lib/active_record/relation/predicate_builder.rb +135 -41
  160. data/lib/active_record/relation/query_methods.rb +982 -155
  161. data/lib/active_record/relation/spawn_methods.rb +50 -110
  162. data/lib/active_record/relation.rb +371 -180
  163. data/lib/active_record/result.rb +109 -12
  164. data/lib/active_record/runtime_registry.rb +22 -0
  165. data/lib/active_record/sanitization.rb +191 -0
  166. data/lib/active_record/schema.rb +19 -13
  167. data/lib/active_record/schema_dumper.rb +111 -61
  168. data/lib/active_record/schema_migration.rb +53 -0
  169. data/lib/active_record/scoping/default.rb +135 -0
  170. data/lib/active_record/scoping/named.rb +164 -0
  171. data/lib/active_record/scoping.rb +87 -0
  172. data/lib/active_record/serialization.rb +7 -45
  173. data/lib/active_record/serializers/xml_serializer.rb +14 -65
  174. data/lib/active_record/statement_cache.rb +111 -0
  175. data/lib/active_record/store.rb +205 -0
  176. data/lib/active_record/tasks/database_tasks.rb +299 -0
  177. data/lib/active_record/tasks/mysql_database_tasks.rb +159 -0
  178. data/lib/active_record/tasks/postgresql_database_tasks.rb +101 -0
  179. data/lib/active_record/tasks/sqlite_database_tasks.rb +55 -0
  180. data/lib/active_record/timestamp.rb +35 -14
  181. data/lib/active_record/transactions.rb +141 -74
  182. data/lib/active_record/translation.rb +22 -0
  183. data/lib/active_record/type/big_integer.rb +13 -0
  184. data/lib/active_record/type/binary.rb +50 -0
  185. data/lib/active_record/type/boolean.rb +31 -0
  186. data/lib/active_record/type/date.rb +50 -0
  187. data/lib/active_record/type/date_time.rb +54 -0
  188. data/lib/active_record/type/decimal.rb +64 -0
  189. data/lib/active_record/type/decimal_without_scale.rb +11 -0
  190. data/lib/active_record/type/decorator.rb +14 -0
  191. data/lib/active_record/type/float.rb +19 -0
  192. data/lib/active_record/type/hash_lookup_type_map.rb +23 -0
  193. data/lib/active_record/type/integer.rb +59 -0
  194. data/lib/active_record/type/mutable.rb +16 -0
  195. data/lib/active_record/type/numeric.rb +36 -0
  196. data/lib/active_record/type/serialized.rb +62 -0
  197. data/lib/active_record/type/string.rb +40 -0
  198. data/lib/active_record/type/text.rb +11 -0
  199. data/lib/active_record/type/time.rb +26 -0
  200. data/lib/active_record/type/time_value.rb +38 -0
  201. data/lib/active_record/type/type_map.rb +64 -0
  202. data/lib/active_record/type/unsigned_integer.rb +15 -0
  203. data/lib/active_record/type/value.rb +110 -0
  204. data/lib/active_record/type.rb +23 -0
  205. data/lib/active_record/validations/associated.rb +27 -18
  206. data/lib/active_record/validations/presence.rb +67 -0
  207. data/lib/active_record/validations/uniqueness.rb +125 -66
  208. data/lib/active_record/validations.rb +37 -30
  209. data/lib/active_record/version.rb +5 -7
  210. data/lib/active_record.rb +80 -25
  211. data/lib/rails/generators/active_record/migration/migration_generator.rb +54 -9
  212. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
  213. data/lib/rails/generators/active_record/migration/templates/migration.rb +25 -11
  214. data/lib/rails/generators/active_record/migration.rb +11 -8
  215. data/lib/rails/generators/active_record/model/model_generator.rb +17 -4
  216. data/lib/rails/generators/active_record/model/templates/model.rb +5 -2
  217. data/lib/rails/generators/active_record/model/templates/module.rb +1 -1
  218. data/lib/rails/generators/active_record.rb +3 -11
  219. metadata +132 -53
  220. data/examples/associations.png +0 -0
  221. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -62
  222. data/lib/active_record/associations/join_helper.rb +0 -55
  223. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  224. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -135
  225. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -556
  226. data/lib/active_record/dynamic_finder_match.rb +0 -56
  227. data/lib/active_record/dynamic_scope_match.rb +0 -23
  228. data/lib/active_record/identity_map.rb +0 -163
  229. data/lib/active_record/named_scope.rb +0 -200
  230. data/lib/active_record/observer.rb +0 -121
  231. data/lib/active_record/session_store.rb +0 -358
  232. data/lib/active_record/test_case.rb +0 -69
  233. data/lib/rails/generators/active_record/model/templates/migration.rb +0 -17
  234. data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
  235. data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -4
  236. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -25
  237. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -16
@@ -1,118 +1,175 @@
1
1
  module ActiveRecord
2
2
  # = Active Record Counter Cache
3
3
  module CounterCache
4
- # Resets one or more counter caches to their correct value using an SQL
5
- # count query. This is useful when adding new counter caches, or if the
6
- # counter has been corrupted or modified directly by SQL.
7
- #
8
- # ==== Parameters
9
- #
10
- # * +id+ - The id of the object you wish to reset a counter on.
11
- # * +counters+ - One or more counter names to reset
12
- #
13
- # ==== Examples
14
- #
15
- # # For Post with id #1 records reset the comments_count
16
- # Post.reset_counters(1, :comments)
17
- def reset_counters(id, *counters)
18
- object = find(id)
19
- counters.each do |association|
20
- has_many_association = reflect_on_association(association.to_sym)
21
-
22
- expected_name = if has_many_association.options[:as]
23
- has_many_association.options[:as].to_s.classify
24
- else
25
- self.name
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}"
26
82
  end
27
83
 
28
- child_class = has_many_association.klass
29
- belongs_to = child_class.reflect_on_all_associations(:belongs_to)
30
- reflection = belongs_to.find { |e| e.class_name == expected_name }
31
- counter_name = reflection.counter_cache_column
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
32
106
 
33
- stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
34
- arel_table[counter_name] => object.send(association).count
35
- })
36
- connection.update stmt
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)
37
123
  end
38
- return true
39
124
  end
40
125
 
41
- # A generic "counter updater" implementation, intended primarily to be
42
- # used by increment_counter and decrement_counter, but which may also
43
- # be useful on its own. It simply does a direct SQL update for the record
44
- # with the given ID, altering the given hash of counters by the amount
45
- # given by the corresponding value:
46
- #
47
- # ==== Parameters
48
- #
49
- # * +id+ - The id of the object you wish to update a counter on or an Array of ids.
50
- # * +counters+ - An Array of Hashes containing the names of the fields
51
- # to update as keys and the amount to update the field by as values.
52
- #
53
- # ==== Examples
54
- #
55
- # # For the Post with id of 5, decrement the comment_count by 1, and
56
- # # increment the action_count by 1
57
- # Post.update_counters 5, :comment_count => -1, :action_count => 1
58
- # # Executes the following SQL:
59
- # # UPDATE posts
60
- # # SET comment_count = COALESCE(comment_count, 0) - 1,
61
- # # action_count = COALESCE(action_count, 0) + 1
62
- # # WHERE id = 5
63
- #
64
- # # For the Posts with id of 10 and 15, increment the comment_count by 1
65
- # Post.update_counters [10, 15], :comment_count => 1
66
- # # Executes the following SQL:
67
- # # UPDATE posts
68
- # # SET comment_count = COALESCE(comment_count, 0) + 1,
69
- # # WHERE id IN (10, 15)
70
- def update_counters(id, counters)
71
- updates = counters.map do |counter_name, value|
72
- operator = value < 0 ? '-' : '+'
73
- quoted_column = connection.quote_column_name(counter_name)
74
- "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
126
+ protected
127
+
128
+ def actually_destroyed?
129
+ @_actually_destroyed
75
130
  end
76
131
 
77
- IdentityMap.remove_by_id(symbolized_base_class, id) if IdentityMap.enabled?
132
+ def clear_destroy_state
133
+ @_actually_destroyed = nil
134
+ end
78
135
 
79
- update_all(updates.join(', '), primary_key => id )
80
- end
136
+ private
81
137
 
82
- # Increment a number field by one, usually representing a count.
83
- #
84
- # This is used for caching aggregate values, so that they don't need to be computed every time.
85
- # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is
86
- # shown it would have to run an SQL query to find how many posts and comments there are.
87
- #
88
- # ==== Parameters
89
- #
90
- # * +counter_name+ - The name of the field that should be incremented.
91
- # * +id+ - The id of the object that should be incremented.
92
- #
93
- # ==== Examples
94
- #
95
- # # Increment the post_count column for the record with an id of 5
96
- # DiscussionBoard.increment_counter(:post_count, 5)
97
- def increment_counter(counter_name, id)
98
- update_counters(id, counter_name => 1)
99
- end
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.to_sym) if reflection.belongs_to? && reflection.counter_cache_column
171
+ end
172
+ end
100
173
 
101
- # Decrement a number field by one, usually representing a count.
102
- #
103
- # This works the same as increment_counter but reduces the column value by 1 instead of increasing it.
104
- #
105
- # ==== Parameters
106
- #
107
- # * +counter_name+ - The name of the field that should be decremented.
108
- # * +id+ - The id of the object that should be decremented.
109
- #
110
- # ==== Examples
111
- #
112
- # # Decrement the post_count column for the record with an id of 5
113
- # DiscussionBoard.decrement_counter(:post_count, 5)
114
- def decrement_counter(counter_name, id)
115
- update_counters(id, counter_name => -1)
116
- end
117
174
  end
118
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,197 @@
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.status = 1
22
+ # conversation.status = "archived"
23
+ #
24
+ # conversation.status = nil
25
+ # conversation.status.nil? # => true
26
+ # conversation.status # => nil
27
+ #
28
+ # Scopes based on the allowed values of the enum field will be provided
29
+ # as well. With the above example:
30
+ #
31
+ # Conversation.active
32
+ # Conversation.archived
33
+ #
34
+ # You can set the default value from the database declaration, like:
35
+ #
36
+ # create_table :conversations do |t|
37
+ # t.column :status, :integer, default: 0
38
+ # end
39
+ #
40
+ # Good practice is to let the first declared status be the default.
41
+ #
42
+ # Finally, it's also possible to explicitly map the relation between attribute and
43
+ # database integer with a +Hash+:
44
+ #
45
+ # class Conversation < ActiveRecord::Base
46
+ # enum status: { active: 0, archived: 1 }
47
+ # end
48
+ #
49
+ # Note that when an +Array+ is used, the implicit mapping from the values to database
50
+ # integers is derived from the order the values appear in the array. In the example,
51
+ # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
52
+ # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the
53
+ # database.
54
+ #
55
+ # Therefore, once a value is added to the enum array, its position in the array must
56
+ # be maintained, and new values should only be added to the end of the array. To
57
+ # remove unused values, the explicit +Hash+ syntax should be used.
58
+ #
59
+ # In rare circumstances you might need to access the mapping directly.
60
+ # The mappings are exposed through a class method with the pluralized attribute
61
+ # name:
62
+ #
63
+ # Conversation.statuses # => { "active" => 0, "archived" => 1 }
64
+ #
65
+ # Use that class method when you need to know the ordinal value of an enum:
66
+ #
67
+ # Conversation.where("status <> ?", Conversation.statuses[:archived])
68
+ #
69
+ # Where conditions on an enum attribute must use the ordinal value of an enum.
70
+ module Enum
71
+ def self.extended(base) # :nodoc:
72
+ base.class_attribute(:defined_enums, instance_writer: false)
73
+ base.defined_enums = {}
74
+ end
75
+
76
+ def inherited(base) # :nodoc:
77
+ base.defined_enums = defined_enums.deep_dup
78
+ super
79
+ end
80
+
81
+ def enum(definitions)
82
+ klass = self
83
+ definitions.each do |name, values|
84
+ # statuses = { }
85
+ enum_values = ActiveSupport::HashWithIndifferentAccess.new
86
+ name = name.to_sym
87
+
88
+ # def self.statuses statuses end
89
+ detect_enum_conflict!(name, name.to_s.pluralize, true)
90
+ klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
91
+
92
+ _enum_methods_module.module_eval do
93
+ # def status=(value) self[:status] = statuses[value] end
94
+ klass.send(:detect_enum_conflict!, name, "#{name}=")
95
+ define_method("#{name}=") { |value|
96
+ if enum_values.has_key?(value) || value.blank?
97
+ self[name] = enum_values[value]
98
+ elsif enum_values.has_value?(value)
99
+ # Assigning a value directly is not a end-user feature, hence it's not documented.
100
+ # This is used internally to make building objects from the generated scopes work
101
+ # as expected, i.e. +Conversation.archived.build.archived?+ should be true.
102
+ self[name] = value
103
+ else
104
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
105
+ end
106
+ }
107
+
108
+ # def status() statuses.key self[:status] end
109
+ klass.send(:detect_enum_conflict!, name, name)
110
+ define_method(name) { enum_values.key self[name] }
111
+
112
+ # def status_before_type_cast() statuses.key self[:status] end
113
+ klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast")
114
+ define_method("#{name}_before_type_cast") { enum_values.key self[name] }
115
+
116
+ pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
117
+ pairs.each do |value, i|
118
+ enum_values[value] = i
119
+
120
+ # def active?() status == 0 end
121
+ klass.send(:detect_enum_conflict!, name, "#{value}?")
122
+ define_method("#{value}?") { self[name] == i }
123
+
124
+ # def active!() update! status: :active end
125
+ klass.send(:detect_enum_conflict!, name, "#{value}!")
126
+ define_method("#{value}!") { update! name => value }
127
+
128
+ # scope :active, -> { where status: 0 }
129
+ klass.send(:detect_enum_conflict!, name, value, true)
130
+ klass.scope value, -> { klass.where name => i }
131
+ end
132
+ end
133
+ defined_enums[name.to_s] = enum_values
134
+ end
135
+ end
136
+
137
+ private
138
+ def _enum_methods_module
139
+ @_enum_methods_module ||= begin
140
+ mod = Module.new do
141
+ private
142
+ def save_changed_attribute(attr_name, old)
143
+ if (mapping = self.class.defined_enums[attr_name.to_s])
144
+ value = _read_attribute(attr_name)
145
+ if attribute_changed?(attr_name)
146
+ if mapping[old] == value
147
+ clear_attribute_changes([attr_name])
148
+ end
149
+ else
150
+ if old != value
151
+ set_attribute_was(attr_name, mapping.key(old))
152
+ end
153
+ end
154
+ else
155
+ super
156
+ end
157
+ end
158
+ end
159
+ include mod
160
+ mod
161
+ end
162
+ end
163
+
164
+ ENUM_CONFLICT_MESSAGE = \
165
+ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
166
+ "this will generate a %{type} method \"%{method}\", which is already defined " \
167
+ "by %{source}."
168
+
169
+ def detect_enum_conflict!(enum_name, method_name, klass_method = false)
170
+ if klass_method && dangerous_class_method?(method_name)
171
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
172
+ enum: enum_name,
173
+ klass: self.name,
174
+ type: 'class',
175
+ method: method_name,
176
+ source: 'Active Record'
177
+ }
178
+ elsif !klass_method && dangerous_attribute_method?(method_name)
179
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
180
+ enum: enum_name,
181
+ klass: self.name,
182
+ type: 'instance',
183
+ method: method_name,
184
+ source: 'Active Record'
185
+ }
186
+ elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
187
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
188
+ enum: enum_name,
189
+ klass: self.name,
190
+ type: 'instance',
191
+ method: method_name,
192
+ source: 'another enum'
193
+ }
194
+ end
195
+ end
196
+ end
197
+ end