activerecord 5.2.3

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 (244) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +937 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +217 -0
  5. data/examples/performance.rb +185 -0
  6. data/examples/simple.rb +15 -0
  7. data/lib/active_record.rb +188 -0
  8. data/lib/active_record/aggregations.rb +283 -0
  9. data/lib/active_record/association_relation.rb +40 -0
  10. data/lib/active_record/associations.rb +1860 -0
  11. data/lib/active_record/associations/alias_tracker.rb +81 -0
  12. data/lib/active_record/associations/association.rb +299 -0
  13. data/lib/active_record/associations/association_scope.rb +168 -0
  14. data/lib/active_record/associations/belongs_to_association.rb +130 -0
  15. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +40 -0
  16. data/lib/active_record/associations/builder/association.rb +140 -0
  17. data/lib/active_record/associations/builder/belongs_to.rb +163 -0
  18. data/lib/active_record/associations/builder/collection_association.rb +82 -0
  19. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +135 -0
  20. data/lib/active_record/associations/builder/has_many.rb +17 -0
  21. data/lib/active_record/associations/builder/has_one.rb +30 -0
  22. data/lib/active_record/associations/builder/singular_association.rb +42 -0
  23. data/lib/active_record/associations/collection_association.rb +513 -0
  24. data/lib/active_record/associations/collection_proxy.rb +1131 -0
  25. data/lib/active_record/associations/foreign_association.rb +13 -0
  26. data/lib/active_record/associations/has_many_association.rb +144 -0
  27. data/lib/active_record/associations/has_many_through_association.rb +227 -0
  28. data/lib/active_record/associations/has_one_association.rb +120 -0
  29. data/lib/active_record/associations/has_one_through_association.rb +45 -0
  30. data/lib/active_record/associations/join_dependency.rb +262 -0
  31. data/lib/active_record/associations/join_dependency/join_association.rb +60 -0
  32. data/lib/active_record/associations/join_dependency/join_base.rb +23 -0
  33. data/lib/active_record/associations/join_dependency/join_part.rb +71 -0
  34. data/lib/active_record/associations/preloader.rb +193 -0
  35. data/lib/active_record/associations/preloader/association.rb +131 -0
  36. data/lib/active_record/associations/preloader/through_association.rb +107 -0
  37. data/lib/active_record/associations/singular_association.rb +73 -0
  38. data/lib/active_record/associations/through_association.rb +121 -0
  39. data/lib/active_record/attribute_assignment.rb +88 -0
  40. data/lib/active_record/attribute_decorators.rb +90 -0
  41. data/lib/active_record/attribute_methods.rb +492 -0
  42. data/lib/active_record/attribute_methods/before_type_cast.rb +78 -0
  43. data/lib/active_record/attribute_methods/dirty.rb +150 -0
  44. data/lib/active_record/attribute_methods/primary_key.rb +143 -0
  45. data/lib/active_record/attribute_methods/query.rb +42 -0
  46. data/lib/active_record/attribute_methods/read.rb +85 -0
  47. data/lib/active_record/attribute_methods/serialization.rb +90 -0
  48. data/lib/active_record/attribute_methods/time_zone_conversion.rb +91 -0
  49. data/lib/active_record/attribute_methods/write.rb +68 -0
  50. data/lib/active_record/attributes.rb +266 -0
  51. data/lib/active_record/autosave_association.rb +498 -0
  52. data/lib/active_record/base.rb +329 -0
  53. data/lib/active_record/callbacks.rb +353 -0
  54. data/lib/active_record/coders/json.rb +15 -0
  55. data/lib/active_record/coders/yaml_column.rb +50 -0
  56. data/lib/active_record/collection_cache_key.rb +53 -0
  57. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +1068 -0
  58. data/lib/active_record/connection_adapters/abstract/database_limits.rb +72 -0
  59. data/lib/active_record/connection_adapters/abstract/database_statements.rb +540 -0
  60. data/lib/active_record/connection_adapters/abstract/query_cache.rb +145 -0
  61. data/lib/active_record/connection_adapters/abstract/quoting.rb +200 -0
  62. data/lib/active_record/connection_adapters/abstract/savepoints.rb +23 -0
  63. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +146 -0
  64. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +685 -0
  65. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +95 -0
  66. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +1396 -0
  67. data/lib/active_record/connection_adapters/abstract/transaction.rb +283 -0
  68. data/lib/active_record/connection_adapters/abstract_adapter.rb +628 -0
  69. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +887 -0
  70. data/lib/active_record/connection_adapters/column.rb +91 -0
  71. data/lib/active_record/connection_adapters/connection_specification.rb +287 -0
  72. data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +33 -0
  73. data/lib/active_record/connection_adapters/mysql/column.rb +27 -0
  74. data/lib/active_record/connection_adapters/mysql/database_statements.rb +140 -0
  75. data/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +72 -0
  76. data/lib/active_record/connection_adapters/mysql/quoting.rb +44 -0
  77. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +73 -0
  78. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +87 -0
  79. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +80 -0
  80. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +148 -0
  81. data/lib/active_record/connection_adapters/mysql/type_metadata.rb +35 -0
  82. data/lib/active_record/connection_adapters/mysql2_adapter.rb +129 -0
  83. data/lib/active_record/connection_adapters/postgresql/column.rb +44 -0
  84. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +163 -0
  85. data/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb +44 -0
  86. data/lib/active_record/connection_adapters/postgresql/oid.rb +34 -0
  87. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +92 -0
  88. data/lib/active_record/connection_adapters/postgresql/oid/bit.rb +56 -0
  89. data/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +15 -0
  90. data/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +17 -0
  91. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +50 -0
  92. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +23 -0
  93. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +23 -0
  94. data/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +15 -0
  95. data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +21 -0
  96. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +71 -0
  97. data/lib/active_record/connection_adapters/postgresql/oid/inet.rb +15 -0
  98. data/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +15 -0
  99. data/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb +45 -0
  100. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +41 -0
  101. data/lib/active_record/connection_adapters/postgresql/oid/oid.rb +15 -0
  102. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +65 -0
  103. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +97 -0
  104. data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +18 -0
  105. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +111 -0
  106. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +23 -0
  107. data/lib/active_record/connection_adapters/postgresql/oid/vector.rb +28 -0
  108. data/lib/active_record/connection_adapters/postgresql/oid/xml.rb +30 -0
  109. data/lib/active_record/connection_adapters/postgresql/quoting.rb +168 -0
  110. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +43 -0
  111. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +65 -0
  112. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +206 -0
  113. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +50 -0
  114. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +774 -0
  115. data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +39 -0
  116. data/lib/active_record/connection_adapters/postgresql/utils.rb +81 -0
  117. data/lib/active_record/connection_adapters/postgresql_adapter.rb +863 -0
  118. data/lib/active_record/connection_adapters/schema_cache.rb +118 -0
  119. data/lib/active_record/connection_adapters/sql_type_metadata.rb +34 -0
  120. data/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb +21 -0
  121. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +67 -0
  122. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +17 -0
  123. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +19 -0
  124. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +18 -0
  125. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +106 -0
  126. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +573 -0
  127. data/lib/active_record/connection_adapters/statement_pool.rb +61 -0
  128. data/lib/active_record/connection_handling.rb +145 -0
  129. data/lib/active_record/core.rb +559 -0
  130. data/lib/active_record/counter_cache.rb +218 -0
  131. data/lib/active_record/define_callbacks.rb +22 -0
  132. data/lib/active_record/dynamic_matchers.rb +122 -0
  133. data/lib/active_record/enum.rb +244 -0
  134. data/lib/active_record/errors.rb +380 -0
  135. data/lib/active_record/explain.rb +50 -0
  136. data/lib/active_record/explain_registry.rb +32 -0
  137. data/lib/active_record/explain_subscriber.rb +34 -0
  138. data/lib/active_record/fixture_set/file.rb +82 -0
  139. data/lib/active_record/fixtures.rb +1065 -0
  140. data/lib/active_record/gem_version.rb +17 -0
  141. data/lib/active_record/inheritance.rb +283 -0
  142. data/lib/active_record/integration.rb +155 -0
  143. data/lib/active_record/internal_metadata.rb +45 -0
  144. data/lib/active_record/legacy_yaml_adapter.rb +48 -0
  145. data/lib/active_record/locale/en.yml +48 -0
  146. data/lib/active_record/locking/optimistic.rb +198 -0
  147. data/lib/active_record/locking/pessimistic.rb +89 -0
  148. data/lib/active_record/log_subscriber.rb +137 -0
  149. data/lib/active_record/migration.rb +1378 -0
  150. data/lib/active_record/migration/command_recorder.rb +240 -0
  151. data/lib/active_record/migration/compatibility.rb +217 -0
  152. data/lib/active_record/migration/join_table.rb +17 -0
  153. data/lib/active_record/model_schema.rb +521 -0
  154. data/lib/active_record/nested_attributes.rb +600 -0
  155. data/lib/active_record/no_touching.rb +58 -0
  156. data/lib/active_record/null_relation.rb +68 -0
  157. data/lib/active_record/persistence.rb +763 -0
  158. data/lib/active_record/query_cache.rb +45 -0
  159. data/lib/active_record/querying.rb +70 -0
  160. data/lib/active_record/railtie.rb +226 -0
  161. data/lib/active_record/railties/console_sandbox.rb +7 -0
  162. data/lib/active_record/railties/controller_runtime.rb +56 -0
  163. data/lib/active_record/railties/databases.rake +377 -0
  164. data/lib/active_record/readonly_attributes.rb +24 -0
  165. data/lib/active_record/reflection.rb +1044 -0
  166. data/lib/active_record/relation.rb +629 -0
  167. data/lib/active_record/relation/batches.rb +287 -0
  168. data/lib/active_record/relation/batches/batch_enumerator.rb +69 -0
  169. data/lib/active_record/relation/calculations.rb +417 -0
  170. data/lib/active_record/relation/delegation.rb +147 -0
  171. data/lib/active_record/relation/finder_methods.rb +565 -0
  172. data/lib/active_record/relation/from_clause.rb +26 -0
  173. data/lib/active_record/relation/merger.rb +193 -0
  174. data/lib/active_record/relation/predicate_builder.rb +152 -0
  175. data/lib/active_record/relation/predicate_builder/array_handler.rb +48 -0
  176. data/lib/active_record/relation/predicate_builder/association_query_value.rb +46 -0
  177. data/lib/active_record/relation/predicate_builder/base_handler.rb +19 -0
  178. data/lib/active_record/relation/predicate_builder/basic_object_handler.rb +20 -0
  179. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +56 -0
  180. data/lib/active_record/relation/predicate_builder/range_handler.rb +42 -0
  181. data/lib/active_record/relation/predicate_builder/relation_handler.rb +19 -0
  182. data/lib/active_record/relation/query_attribute.rb +45 -0
  183. data/lib/active_record/relation/query_methods.rb +1231 -0
  184. data/lib/active_record/relation/record_fetch_warning.rb +51 -0
  185. data/lib/active_record/relation/spawn_methods.rb +77 -0
  186. data/lib/active_record/relation/where_clause.rb +186 -0
  187. data/lib/active_record/relation/where_clause_factory.rb +34 -0
  188. data/lib/active_record/result.rb +149 -0
  189. data/lib/active_record/runtime_registry.rb +24 -0
  190. data/lib/active_record/sanitization.rb +222 -0
  191. data/lib/active_record/schema.rb +70 -0
  192. data/lib/active_record/schema_dumper.rb +255 -0
  193. data/lib/active_record/schema_migration.rb +56 -0
  194. data/lib/active_record/scoping.rb +106 -0
  195. data/lib/active_record/scoping/default.rb +152 -0
  196. data/lib/active_record/scoping/named.rb +213 -0
  197. data/lib/active_record/secure_token.rb +40 -0
  198. data/lib/active_record/serialization.rb +22 -0
  199. data/lib/active_record/statement_cache.rb +121 -0
  200. data/lib/active_record/store.rb +211 -0
  201. data/lib/active_record/suppressor.rb +61 -0
  202. data/lib/active_record/table_metadata.rb +82 -0
  203. data/lib/active_record/tasks/database_tasks.rb +337 -0
  204. data/lib/active_record/tasks/mysql_database_tasks.rb +115 -0
  205. data/lib/active_record/tasks/postgresql_database_tasks.rb +143 -0
  206. data/lib/active_record/tasks/sqlite_database_tasks.rb +83 -0
  207. data/lib/active_record/timestamp.rb +153 -0
  208. data/lib/active_record/touch_later.rb +64 -0
  209. data/lib/active_record/transactions.rb +502 -0
  210. data/lib/active_record/translation.rb +24 -0
  211. data/lib/active_record/type.rb +79 -0
  212. data/lib/active_record/type/adapter_specific_registry.rb +136 -0
  213. data/lib/active_record/type/date.rb +9 -0
  214. data/lib/active_record/type/date_time.rb +9 -0
  215. data/lib/active_record/type/decimal_without_scale.rb +15 -0
  216. data/lib/active_record/type/hash_lookup_type_map.rb +25 -0
  217. data/lib/active_record/type/internal/timezone.rb +17 -0
  218. data/lib/active_record/type/json.rb +30 -0
  219. data/lib/active_record/type/serialized.rb +71 -0
  220. data/lib/active_record/type/text.rb +11 -0
  221. data/lib/active_record/type/time.rb +21 -0
  222. data/lib/active_record/type/type_map.rb +62 -0
  223. data/lib/active_record/type/unsigned_integer.rb +17 -0
  224. data/lib/active_record/type_caster.rb +9 -0
  225. data/lib/active_record/type_caster/connection.rb +33 -0
  226. data/lib/active_record/type_caster/map.rb +23 -0
  227. data/lib/active_record/validations.rb +93 -0
  228. data/lib/active_record/validations/absence.rb +25 -0
  229. data/lib/active_record/validations/associated.rb +60 -0
  230. data/lib/active_record/validations/length.rb +26 -0
  231. data/lib/active_record/validations/presence.rb +68 -0
  232. data/lib/active_record/validations/uniqueness.rb +238 -0
  233. data/lib/active_record/version.rb +10 -0
  234. data/lib/rails/generators/active_record.rb +19 -0
  235. data/lib/rails/generators/active_record/application_record/application_record_generator.rb +27 -0
  236. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +5 -0
  237. data/lib/rails/generators/active_record/migration.rb +35 -0
  238. data/lib/rails/generators/active_record/migration/migration_generator.rb +78 -0
  239. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +24 -0
  240. data/lib/rails/generators/active_record/migration/templates/migration.rb.tt +46 -0
  241. data/lib/rails/generators/active_record/model/model_generator.rb +48 -0
  242. data/lib/rails/generators/active_record/model/templates/model.rb.tt +13 -0
  243. data/lib/rails/generators/active_record/model/templates/module.rb.tt +7 -0
  244. metadata +333 -0
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ # = Active Record Counter Cache
5
+ module CounterCache
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ # Resets one or more counter caches to their correct value using an SQL
10
+ # count query. This is useful when adding new counter caches, or if the
11
+ # counter has been corrupted or modified directly by SQL.
12
+ #
13
+ # ==== Parameters
14
+ #
15
+ # * +id+ - The id of the object you wish to reset a counter on.
16
+ # * +counters+ - One or more association counters to reset. Association name or counter name can be given.
17
+ # * <tt>:touch</tt> - Touch timestamp columns when updating.
18
+ # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
19
+ # touch that column or an array of symbols to touch just those ones.
20
+ #
21
+ # ==== Examples
22
+ #
23
+ # # For the Post with id #1, reset the comments_count
24
+ # Post.reset_counters(1, :comments)
25
+ #
26
+ # # Like above, but also touch the +updated_at+ and/or +updated_on+
27
+ # # attributes.
28
+ # Post.reset_counters(1, :comments, touch: true)
29
+ def reset_counters(id, *counters, touch: nil)
30
+ object = find(id)
31
+
32
+ counters.each do |counter_association|
33
+ has_many_association = _reflect_on_association(counter_association)
34
+ unless has_many_association
35
+ has_many = reflect_on_all_associations(:has_many)
36
+ has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym }
37
+ counter_association = has_many_association.plural_name if has_many_association
38
+ end
39
+ raise ArgumentError, "'#{name}' has no association called '#{counter_association}'" unless has_many_association
40
+
41
+ if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
42
+ has_many_association = has_many_association.through_reflection
43
+ end
44
+
45
+ foreign_key = has_many_association.foreign_key.to_s
46
+ child_class = has_many_association.klass
47
+ reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
48
+ counter_name = reflection.counter_cache_column
49
+
50
+ updates = { counter_name => object.send(counter_association).count(:all) }
51
+
52
+ if touch
53
+ names = touch if touch != true
54
+ updates.merge!(touch_attributes_with_time(*names))
55
+ end
56
+
57
+ unscoped.where(primary_key => object.id).update_all(updates)
58
+ end
59
+
60
+ true
61
+ end
62
+
63
+ # A generic "counter updater" implementation, intended primarily to be
64
+ # used by #increment_counter and #decrement_counter, but which may also
65
+ # be useful on its own. It simply does a direct SQL update for the record
66
+ # with the given ID, altering the given hash of counters by the amount
67
+ # given by the corresponding value:
68
+ #
69
+ # ==== Parameters
70
+ #
71
+ # * +id+ - The id of the object you wish to update a counter on or an array of ids.
72
+ # * +counters+ - A Hash containing the names of the fields
73
+ # to update as keys and the amount to update the field by as values.
74
+ # * <tt>:touch</tt> option - Touch timestamp columns when updating.
75
+ # If attribute names are passed, they are updated along with updated_at/on
76
+ # attributes.
77
+ #
78
+ # ==== Examples
79
+ #
80
+ # # For the Post with id of 5, decrement the comment_count by 1, and
81
+ # # increment the action_count by 1
82
+ # Post.update_counters 5, comment_count: -1, action_count: 1
83
+ # # Executes the following SQL:
84
+ # # UPDATE posts
85
+ # # SET comment_count = COALESCE(comment_count, 0) - 1,
86
+ # # action_count = COALESCE(action_count, 0) + 1
87
+ # # WHERE id = 5
88
+ #
89
+ # # For the Posts with id of 10 and 15, increment the comment_count by 1
90
+ # Post.update_counters [10, 15], comment_count: 1
91
+ # # Executes the following SQL:
92
+ # # UPDATE posts
93
+ # # SET comment_count = COALESCE(comment_count, 0) + 1
94
+ # # WHERE id IN (10, 15)
95
+ #
96
+ # # For the Posts with id of 10 and 15, increment the comment_count by 1
97
+ # # and update the updated_at value for each counter.
98
+ # Post.update_counters [10, 15], comment_count: 1, touch: true
99
+ # # Executes the following SQL:
100
+ # # UPDATE posts
101
+ # # SET comment_count = COALESCE(comment_count, 0) + 1,
102
+ # # `updated_at` = '2016-10-13T09:59:23-05:00'
103
+ # # WHERE id IN (10, 15)
104
+ def update_counters(id, counters)
105
+ touch = counters.delete(:touch)
106
+
107
+ updates = counters.map do |counter_name, value|
108
+ operator = value < 0 ? "-" : "+"
109
+ quoted_column = connection.quote_column_name(counter_name)
110
+ "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
111
+ end
112
+
113
+ if touch
114
+ names = touch if touch != true
115
+ touch_updates = touch_attributes_with_time(*names)
116
+ updates << sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty?
117
+ end
118
+
119
+ if id.is_a?(Relation) && self == id.klass
120
+ relation = id
121
+ else
122
+ relation = unscoped.where!(primary_key => id)
123
+ end
124
+
125
+ relation.update_all updates.join(", ")
126
+ end
127
+
128
+ # Increment a numeric field by one, via a direct SQL update.
129
+ #
130
+ # This method is used primarily for maintaining counter_cache columns that are
131
+ # used to store aggregate values. For example, a +DiscussionBoard+ may cache
132
+ # posts_count and comments_count to avoid running an SQL query to calculate the
133
+ # number of posts and comments there are, each time it is displayed.
134
+ #
135
+ # ==== Parameters
136
+ #
137
+ # * +counter_name+ - The name of the field that should be incremented.
138
+ # * +id+ - The id of the object that should be incremented or an array of ids.
139
+ # * <tt>:touch</tt> - Touch timestamp columns when updating.
140
+ # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
141
+ # touch that column or an array of symbols to touch just those ones.
142
+ #
143
+ # ==== Examples
144
+ #
145
+ # # Increment the posts_count column for the record with an id of 5
146
+ # DiscussionBoard.increment_counter(:posts_count, 5)
147
+ #
148
+ # # Increment the posts_count column for the record with an id of 5
149
+ # # and update the updated_at value.
150
+ # DiscussionBoard.increment_counter(:posts_count, 5, touch: true)
151
+ def increment_counter(counter_name, id, touch: nil)
152
+ update_counters(id, counter_name => 1, touch: touch)
153
+ end
154
+
155
+ # Decrement a numeric field by one, via a direct SQL update.
156
+ #
157
+ # This works the same as #increment_counter but reduces the column value by
158
+ # 1 instead of increasing it.
159
+ #
160
+ # ==== Parameters
161
+ #
162
+ # * +counter_name+ - The name of the field that should be decremented.
163
+ # * +id+ - The id of the object that should be decremented or an array of ids.
164
+ # * <tt>:touch</tt> - Touch timestamp columns when updating.
165
+ # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
166
+ # touch that column or an array of symbols to touch just those ones.
167
+ #
168
+ # ==== Examples
169
+ #
170
+ # # Decrement the posts_count column for the record with an id of 5
171
+ # DiscussionBoard.decrement_counter(:posts_count, 5)
172
+ #
173
+ # # Decrement the posts_count column for the record with an id of 5
174
+ # # and update the updated_at value.
175
+ # DiscussionBoard.decrement_counter(:posts_count, 5, touch: true)
176
+ def decrement_counter(counter_name, id, touch: nil)
177
+ update_counters(id, counter_name => -1, touch: touch)
178
+ end
179
+ end
180
+
181
+ private
182
+
183
+ def _create_record(*)
184
+ id = super
185
+
186
+ each_counter_cached_associations do |association|
187
+ if send(association.reflection.name)
188
+ association.increment_counters
189
+ end
190
+ end
191
+
192
+ id
193
+ end
194
+
195
+ def destroy_row
196
+ affected_rows = super
197
+
198
+ if affected_rows > 0
199
+ each_counter_cached_associations do |association|
200
+ foreign_key = association.reflection.foreign_key.to_sym
201
+ unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
202
+ if send(association.reflection.name)
203
+ association.decrement_counters
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ affected_rows
210
+ end
211
+
212
+ def each_counter_cached_associations
213
+ _reflections.each do |name, reflection|
214
+ yield association(name.to_sym) if reflection.belongs_to? && reflection.counter_cache_column
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ # This module exists because ActiveRecord::AttributeMethods::Dirty needs to
5
+ # define callbacks, but continue to have its version of +save+ be the super
6
+ # method of ActiveRecord::Callbacks. This will be removed when the removal
7
+ # of deprecated code removes this need.
8
+ module DefineCallbacks
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods # :nodoc:
12
+ include ActiveModel::Callbacks
13
+ end
14
+
15
+ included do
16
+ include ActiveModel::Validations::Callbacks
17
+
18
+ define_model_callbacks :initialize, :find, :touch, only: :after
19
+ define_model_callbacks :save, :create, :update, :destroy
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module DynamicMatchers #:nodoc:
5
+ private
6
+ def respond_to_missing?(name, _)
7
+ if self == Base
8
+ super
9
+ else
10
+ match = Method.match(self, name)
11
+ match && match.valid? || super
12
+ end
13
+ end
14
+
15
+ def method_missing(name, *arguments, &block)
16
+ match = Method.match(self, name)
17
+
18
+ if match && match.valid?
19
+ match.define
20
+ send(name, *arguments, &block)
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ class Method
27
+ @matchers = []
28
+
29
+ class << self
30
+ attr_reader :matchers
31
+
32
+ def match(model, name)
33
+ klass = matchers.find { |k| k.pattern.match?(name) }
34
+ klass.new(model, name) if klass
35
+ end
36
+
37
+ def pattern
38
+ @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
39
+ end
40
+
41
+ def prefix
42
+ raise NotImplementedError
43
+ end
44
+
45
+ def suffix
46
+ ""
47
+ end
48
+ end
49
+
50
+ attr_reader :model, :name, :attribute_names
51
+
52
+ def initialize(model, name)
53
+ @model = model
54
+ @name = name.to_s
55
+ @attribute_names = @name.match(self.class.pattern)[1].split("_and_")
56
+ @attribute_names.map! { |n| @model.attribute_aliases[n] || n }
57
+ end
58
+
59
+ def valid?
60
+ attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
61
+ end
62
+
63
+ def define
64
+ model.class_eval <<-CODE, __FILE__, __LINE__ + 1
65
+ def self.#{name}(#{signature})
66
+ #{body}
67
+ end
68
+ CODE
69
+ end
70
+
71
+ private
72
+
73
+ def body
74
+ "#{finder}(#{attributes_hash})"
75
+ end
76
+
77
+ # The parameters in the signature may have reserved Ruby words, in order
78
+ # to prevent errors, we start each param name with `_`.
79
+ def signature
80
+ attribute_names.map { |name| "_#{name}" }.join(", ")
81
+ end
82
+
83
+ # Given that the parameters starts with `_`, the finder needs to use the
84
+ # same parameter name.
85
+ def attributes_hash
86
+ "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(",") + "}"
87
+ end
88
+
89
+ def finder
90
+ raise NotImplementedError
91
+ end
92
+ end
93
+
94
+ class FindBy < Method
95
+ Method.matchers << self
96
+
97
+ def self.prefix
98
+ "find_by"
99
+ end
100
+
101
+ def finder
102
+ "find_by"
103
+ end
104
+ end
105
+
106
+ class FindByBang < Method
107
+ Method.matchers << self
108
+
109
+ def self.prefix
110
+ "find_by"
111
+ end
112
+
113
+ def self.suffix
114
+ "!"
115
+ end
116
+
117
+ def finder
118
+ "find_by!"
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/deep_dup"
4
+
5
+ module ActiveRecord
6
+ # Declare an enum attribute where the values map to integers in the database,
7
+ # but can be queried by name. Example:
8
+ #
9
+ # class Conversation < ActiveRecord::Base
10
+ # enum status: [ :active, :archived ]
11
+ # end
12
+ #
13
+ # # conversation.update! status: 0
14
+ # conversation.active!
15
+ # conversation.active? # => true
16
+ # conversation.status # => "active"
17
+ #
18
+ # # conversation.update! status: 1
19
+ # conversation.archived!
20
+ # conversation.archived? # => true
21
+ # conversation.status # => "archived"
22
+ #
23
+ # # conversation.status = 1
24
+ # conversation.status = "archived"
25
+ #
26
+ # conversation.status = nil
27
+ # conversation.status.nil? # => true
28
+ # conversation.status # => nil
29
+ #
30
+ # Scopes based on the allowed values of the enum field will be provided
31
+ # as well. With the above example:
32
+ #
33
+ # Conversation.active
34
+ # Conversation.archived
35
+ #
36
+ # Of course, you can also query them directly if the scopes don't fit your
37
+ # needs:
38
+ #
39
+ # Conversation.where(status: [:active, :archived])
40
+ # Conversation.where.not(status: :active)
41
+ #
42
+ # You can set the default value from the database declaration, like:
43
+ #
44
+ # create_table :conversations do |t|
45
+ # t.column :status, :integer, default: 0
46
+ # end
47
+ #
48
+ # Good practice is to let the first declared status be the default.
49
+ #
50
+ # Finally, it's also possible to explicitly map the relation between attribute and
51
+ # database integer with a hash:
52
+ #
53
+ # class Conversation < ActiveRecord::Base
54
+ # enum status: { active: 0, archived: 1 }
55
+ # end
56
+ #
57
+ # Note that when an array is used, the implicit mapping from the values to database
58
+ # integers is derived from the order the values appear in the array. In the example,
59
+ # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
60
+ # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the
61
+ # database.
62
+ #
63
+ # Therefore, once a value is added to the enum array, its position in the array must
64
+ # be maintained, and new values should only be added to the end of the array. To
65
+ # remove unused values, the explicit hash syntax should be used.
66
+ #
67
+ # In rare circumstances you might need to access the mapping directly.
68
+ # The mappings are exposed through a class method with the pluralized attribute
69
+ # name, which return the mapping in a +HashWithIndifferentAccess+:
70
+ #
71
+ # Conversation.statuses[:active] # => 0
72
+ # Conversation.statuses["archived"] # => 1
73
+ #
74
+ # Use that class method when you need to know the ordinal value of an enum.
75
+ # For example, you can use that when manually building SQL strings:
76
+ #
77
+ # Conversation.where("status <> ?", Conversation.statuses[:archived])
78
+ #
79
+ # You can use the +:_prefix+ or +:_suffix+ options when you need to define
80
+ # multiple enums with same values. If the passed value is +true+, the methods
81
+ # are prefixed/suffixed with the name of the enum. It is also possible to
82
+ # supply a custom value:
83
+ #
84
+ # class Conversation < ActiveRecord::Base
85
+ # enum status: [:active, :archived], _suffix: true
86
+ # enum comments_status: [:active, :inactive], _prefix: :comments
87
+ # end
88
+ #
89
+ # With the above example, the bang and predicate methods along with the
90
+ # associated scopes are now prefixed and/or suffixed accordingly:
91
+ #
92
+ # conversation.active_status!
93
+ # conversation.archived_status? # => false
94
+ #
95
+ # conversation.comments_inactive!
96
+ # conversation.comments_active? # => false
97
+
98
+ module Enum
99
+ def self.extended(base) # :nodoc:
100
+ base.class_attribute(:defined_enums, instance_writer: false, default: {})
101
+ end
102
+
103
+ def inherited(base) # :nodoc:
104
+ base.defined_enums = defined_enums.deep_dup
105
+ super
106
+ end
107
+
108
+ class EnumType < Type::Value # :nodoc:
109
+ delegate :type, to: :subtype
110
+
111
+ def initialize(name, mapping, subtype)
112
+ @name = name
113
+ @mapping = mapping
114
+ @subtype = subtype
115
+ end
116
+
117
+ def cast(value)
118
+ return if value.blank?
119
+
120
+ if mapping.has_key?(value)
121
+ value.to_s
122
+ elsif mapping.has_value?(value)
123
+ mapping.key(value)
124
+ else
125
+ assert_valid_value(value)
126
+ end
127
+ end
128
+
129
+ def deserialize(value)
130
+ return if value.nil?
131
+ mapping.key(subtype.deserialize(value))
132
+ end
133
+
134
+ def serialize(value)
135
+ mapping.fetch(value, value)
136
+ end
137
+
138
+ def assert_valid_value(value)
139
+ unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
140
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
141
+ end
142
+ end
143
+
144
+ # TODO Change this to private once we've dropped Ruby 2.2 support.
145
+ # Workaround for Ruby 2.2 "private attribute?" warning.
146
+ protected
147
+
148
+ attr_reader :name, :mapping, :subtype
149
+ end
150
+
151
+ def enum(definitions)
152
+ klass = self
153
+ enum_prefix = definitions.delete(:_prefix)
154
+ enum_suffix = definitions.delete(:_suffix)
155
+ definitions.each do |name, values|
156
+ # statuses = { }
157
+ enum_values = ActiveSupport::HashWithIndifferentAccess.new
158
+ name = name.to_s
159
+
160
+ # def self.statuses() statuses end
161
+ detect_enum_conflict!(name, name.pluralize, true)
162
+ singleton_class.send(:define_method, name.pluralize) { enum_values }
163
+ defined_enums[name] = enum_values
164
+
165
+ detect_enum_conflict!(name, name)
166
+ detect_enum_conflict!(name, "#{name}=")
167
+
168
+ attr = attribute_alias?(name) ? attribute_alias(name) : name
169
+ decorate_attribute_type(attr, :enum) do |subtype|
170
+ EnumType.new(attr, enum_values, subtype)
171
+ end
172
+
173
+ _enum_methods_module.module_eval do
174
+ pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
175
+ pairs.each do |label, value|
176
+ if enum_prefix == true
177
+ prefix = "#{name}_"
178
+ elsif enum_prefix
179
+ prefix = "#{enum_prefix}_"
180
+ end
181
+ if enum_suffix == true
182
+ suffix = "_#{name}"
183
+ elsif enum_suffix
184
+ suffix = "_#{enum_suffix}"
185
+ end
186
+
187
+ value_method_name = "#{prefix}#{label}#{suffix}"
188
+ enum_values[label] = value
189
+ label = label.to_s
190
+
191
+ # def active?() status == "active" end
192
+ klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
193
+ define_method("#{value_method_name}?") { self[attr] == label }
194
+
195
+ # def active!() update!(status: 0) end
196
+ klass.send(:detect_enum_conflict!, name, "#{value_method_name}!")
197
+ define_method("#{value_method_name}!") { update!(attr => value) }
198
+
199
+ # scope :active, -> { where(status: 0) }
200
+ klass.send(:detect_enum_conflict!, name, value_method_name, true)
201
+ klass.scope value_method_name, -> { where(attr => value) }
202
+ end
203
+ end
204
+ enum_values.freeze
205
+ end
206
+ end
207
+
208
+ private
209
+ def _enum_methods_module
210
+ @_enum_methods_module ||= begin
211
+ mod = Module.new
212
+ include mod
213
+ mod
214
+ end
215
+ end
216
+
217
+ ENUM_CONFLICT_MESSAGE = \
218
+ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
219
+ "this will generate a %{type} method \"%{method}\", which is already defined " \
220
+ "by %{source}."
221
+
222
+ def detect_enum_conflict!(enum_name, method_name, klass_method = false)
223
+ if klass_method && dangerous_class_method?(method_name)
224
+ raise_conflict_error(enum_name, method_name, type: "class")
225
+ elsif klass_method && method_defined_within?(method_name, Relation)
226
+ raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name)
227
+ elsif !klass_method && dangerous_attribute_method?(method_name)
228
+ raise_conflict_error(enum_name, method_name)
229
+ elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
230
+ raise_conflict_error(enum_name, method_name, source: "another enum")
231
+ end
232
+ end
233
+
234
+ def raise_conflict_error(enum_name, method_name, type: "instance", source: "Active Record")
235
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
236
+ enum: enum_name,
237
+ klass: name,
238
+ type: type,
239
+ method: method_name,
240
+ source: source
241
+ }
242
+ end
243
+ end
244
+ end