activerecord 3.2.22.5 → 5.2.8

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 (275) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +657 -621
  3. data/MIT-LICENSE +2 -2
  4. data/README.rdoc +41 -46
  5. data/examples/performance.rb +55 -42
  6. data/examples/simple.rb +6 -5
  7. data/lib/active_record/aggregations.rb +264 -236
  8. data/lib/active_record/association_relation.rb +40 -0
  9. data/lib/active_record/associations/alias_tracker.rb +47 -42
  10. data/lib/active_record/associations/association.rb +127 -75
  11. data/lib/active_record/associations/association_scope.rb +126 -92
  12. data/lib/active_record/associations/belongs_to_association.rb +78 -27
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +9 -4
  14. data/lib/active_record/associations/builder/association.rb +117 -32
  15. data/lib/active_record/associations/builder/belongs_to.rb +135 -60
  16. data/lib/active_record/associations/builder/collection_association.rb +61 -54
  17. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +120 -42
  18. data/lib/active_record/associations/builder/has_many.rb +10 -64
  19. data/lib/active_record/associations/builder/has_one.rb +19 -51
  20. data/lib/active_record/associations/builder/singular_association.rb +28 -18
  21. data/lib/active_record/associations/collection_association.rb +226 -293
  22. data/lib/active_record/associations/collection_proxy.rb +1067 -69
  23. data/lib/active_record/associations/foreign_association.rb +13 -0
  24. data/lib/active_record/associations/has_many_association.rb +83 -47
  25. data/lib/active_record/associations/has_many_through_association.rb +98 -65
  26. data/lib/active_record/associations/has_one_association.rb +57 -20
  27. data/lib/active_record/associations/has_one_through_association.rb +18 -9
  28. data/lib/active_record/associations/join_dependency/join_association.rb +48 -126
  29. data/lib/active_record/associations/join_dependency/join_base.rb +11 -12
  30. data/lib/active_record/associations/join_dependency/join_part.rb +35 -42
  31. data/lib/active_record/associations/join_dependency.rb +212 -164
  32. data/lib/active_record/associations/preloader/association.rb +95 -89
  33. data/lib/active_record/associations/preloader/through_association.rb +84 -44
  34. data/lib/active_record/associations/preloader.rb +123 -111
  35. data/lib/active_record/associations/singular_association.rb +33 -24
  36. data/lib/active_record/associations/through_association.rb +60 -26
  37. data/lib/active_record/associations.rb +1759 -1506
  38. data/lib/active_record/attribute_assignment.rb +60 -193
  39. data/lib/active_record/attribute_decorators.rb +90 -0
  40. data/lib/active_record/attribute_methods/before_type_cast.rb +55 -8
  41. data/lib/active_record/attribute_methods/dirty.rb +113 -74
  42. data/lib/active_record/attribute_methods/primary_key.rb +106 -77
  43. data/lib/active_record/attribute_methods/query.rb +8 -5
  44. data/lib/active_record/attribute_methods/read.rb +63 -114
  45. data/lib/active_record/attribute_methods/serialization.rb +60 -90
  46. data/lib/active_record/attribute_methods/time_zone_conversion.rb +69 -43
  47. data/lib/active_record/attribute_methods/write.rb +43 -45
  48. data/lib/active_record/attribute_methods.rb +366 -149
  49. data/lib/active_record/attributes.rb +266 -0
  50. data/lib/active_record/autosave_association.rb +312 -225
  51. data/lib/active_record/base.rb +114 -505
  52. data/lib/active_record/callbacks.rb +145 -67
  53. data/lib/active_record/coders/json.rb +15 -0
  54. data/lib/active_record/coders/yaml_column.rb +32 -23
  55. data/lib/active_record/collection_cache_key.rb +53 -0
  56. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +883 -284
  57. data/lib/active_record/connection_adapters/abstract/database_limits.rb +16 -2
  58. data/lib/active_record/connection_adapters/abstract/database_statements.rb +350 -200
  59. data/lib/active_record/connection_adapters/abstract/query_cache.rb +82 -27
  60. data/lib/active_record/connection_adapters/abstract/quoting.rb +150 -65
  61. data/lib/active_record/connection_adapters/abstract/savepoints.rb +23 -0
  62. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +146 -0
  63. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +477 -284
  64. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +95 -0
  65. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +1100 -310
  66. data/lib/active_record/connection_adapters/abstract/transaction.rb +283 -0
  67. data/lib/active_record/connection_adapters/abstract_adapter.rb +450 -118
  68. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +657 -446
  69. data/lib/active_record/connection_adapters/column.rb +50 -255
  70. data/lib/active_record/connection_adapters/connection_specification.rb +287 -0
  71. data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +33 -0
  72. data/lib/active_record/connection_adapters/mysql/column.rb +27 -0
  73. data/lib/active_record/connection_adapters/mysql/database_statements.rb +140 -0
  74. data/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +72 -0
  75. data/lib/active_record/connection_adapters/mysql/quoting.rb +44 -0
  76. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +73 -0
  77. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +87 -0
  78. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +80 -0
  79. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +148 -0
  80. data/lib/active_record/connection_adapters/mysql/type_metadata.rb +35 -0
  81. data/lib/active_record/connection_adapters/mysql2_adapter.rb +59 -210
  82. data/lib/active_record/connection_adapters/postgresql/column.rb +44 -0
  83. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +163 -0
  84. data/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb +44 -0
  85. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +92 -0
  86. data/lib/active_record/connection_adapters/postgresql/oid/bit.rb +56 -0
  87. data/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +15 -0
  88. data/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +17 -0
  89. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +50 -0
  90. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +23 -0
  91. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +23 -0
  92. data/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +15 -0
  93. data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +21 -0
  94. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +71 -0
  95. data/lib/active_record/connection_adapters/postgresql/oid/inet.rb +15 -0
  96. data/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +15 -0
  97. data/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb +45 -0
  98. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +41 -0
  99. data/lib/active_record/connection_adapters/postgresql/oid/oid.rb +15 -0
  100. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +65 -0
  101. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +97 -0
  102. data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +18 -0
  103. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +111 -0
  104. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +23 -0
  105. data/lib/active_record/connection_adapters/postgresql/oid/vector.rb +28 -0
  106. data/lib/active_record/connection_adapters/postgresql/oid/xml.rb +30 -0
  107. data/lib/active_record/connection_adapters/postgresql/oid.rb +34 -0
  108. data/lib/active_record/connection_adapters/postgresql/quoting.rb +168 -0
  109. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +43 -0
  110. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +65 -0
  111. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +206 -0
  112. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +50 -0
  113. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +774 -0
  114. data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +39 -0
  115. data/lib/active_record/connection_adapters/postgresql/utils.rb +81 -0
  116. data/lib/active_record/connection_adapters/postgresql_adapter.rb +620 -1080
  117. data/lib/active_record/connection_adapters/schema_cache.rb +85 -36
  118. data/lib/active_record/connection_adapters/sql_type_metadata.rb +34 -0
  119. data/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb +21 -0
  120. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +67 -0
  121. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +17 -0
  122. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +19 -0
  123. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +18 -0
  124. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +106 -0
  125. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +545 -27
  126. data/lib/active_record/connection_adapters/statement_pool.rb +34 -13
  127. data/lib/active_record/connection_handling.rb +145 -0
  128. data/lib/active_record/core.rb +559 -0
  129. data/lib/active_record/counter_cache.rb +200 -105
  130. data/lib/active_record/define_callbacks.rb +22 -0
  131. data/lib/active_record/dynamic_matchers.rb +107 -69
  132. data/lib/active_record/enum.rb +244 -0
  133. data/lib/active_record/errors.rb +245 -60
  134. data/lib/active_record/explain.rb +35 -71
  135. data/lib/active_record/explain_registry.rb +32 -0
  136. data/lib/active_record/explain_subscriber.rb +18 -9
  137. data/lib/active_record/fixture_set/file.rb +82 -0
  138. data/lib/active_record/fixtures.rb +418 -275
  139. data/lib/active_record/gem_version.rb +17 -0
  140. data/lib/active_record/inheritance.rb +209 -100
  141. data/lib/active_record/integration.rb +116 -21
  142. data/lib/active_record/internal_metadata.rb +45 -0
  143. data/lib/active_record/legacy_yaml_adapter.rb +48 -0
  144. data/lib/active_record/locale/en.yml +9 -1
  145. data/lib/active_record/locking/optimistic.rb +107 -94
  146. data/lib/active_record/locking/pessimistic.rb +20 -8
  147. data/lib/active_record/log_subscriber.rb +99 -34
  148. data/lib/active_record/migration/command_recorder.rb +199 -64
  149. data/lib/active_record/migration/compatibility.rb +217 -0
  150. data/lib/active_record/migration/join_table.rb +17 -0
  151. data/lib/active_record/migration.rb +893 -296
  152. data/lib/active_record/model_schema.rb +328 -175
  153. data/lib/active_record/nested_attributes.rb +338 -242
  154. data/lib/active_record/no_touching.rb +58 -0
  155. data/lib/active_record/null_relation.rb +68 -0
  156. data/lib/active_record/persistence.rb +557 -170
  157. data/lib/active_record/query_cache.rb +14 -43
  158. data/lib/active_record/querying.rb +36 -24
  159. data/lib/active_record/railtie.rb +147 -52
  160. data/lib/active_record/railties/console_sandbox.rb +5 -4
  161. data/lib/active_record/railties/controller_runtime.rb +13 -6
  162. data/lib/active_record/railties/databases.rake +206 -488
  163. data/lib/active_record/readonly_attributes.rb +4 -6
  164. data/lib/active_record/reflection.rb +734 -228
  165. data/lib/active_record/relation/batches/batch_enumerator.rb +69 -0
  166. data/lib/active_record/relation/batches.rb +249 -52
  167. data/lib/active_record/relation/calculations.rb +330 -284
  168. data/lib/active_record/relation/delegation.rb +135 -37
  169. data/lib/active_record/relation/finder_methods.rb +450 -287
  170. data/lib/active_record/relation/from_clause.rb +26 -0
  171. data/lib/active_record/relation/merger.rb +193 -0
  172. data/lib/active_record/relation/predicate_builder/array_handler.rb +48 -0
  173. data/lib/active_record/relation/predicate_builder/association_query_value.rb +46 -0
  174. data/lib/active_record/relation/predicate_builder/base_handler.rb +19 -0
  175. data/lib/active_record/relation/predicate_builder/basic_object_handler.rb +20 -0
  176. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +56 -0
  177. data/lib/active_record/relation/predicate_builder/range_handler.rb +42 -0
  178. data/lib/active_record/relation/predicate_builder/relation_handler.rb +19 -0
  179. data/lib/active_record/relation/predicate_builder.rb +132 -43
  180. data/lib/active_record/relation/query_attribute.rb +45 -0
  181. data/lib/active_record/relation/query_methods.rb +1037 -221
  182. data/lib/active_record/relation/record_fetch_warning.rb +51 -0
  183. data/lib/active_record/relation/spawn_methods.rb +48 -151
  184. data/lib/active_record/relation/where_clause.rb +186 -0
  185. data/lib/active_record/relation/where_clause_factory.rb +34 -0
  186. data/lib/active_record/relation.rb +451 -359
  187. data/lib/active_record/result.rb +129 -20
  188. data/lib/active_record/runtime_registry.rb +24 -0
  189. data/lib/active_record/sanitization.rb +164 -136
  190. data/lib/active_record/schema.rb +31 -19
  191. data/lib/active_record/schema_dumper.rb +154 -107
  192. data/lib/active_record/schema_migration.rb +56 -0
  193. data/lib/active_record/scoping/default.rb +108 -98
  194. data/lib/active_record/scoping/named.rb +125 -112
  195. data/lib/active_record/scoping.rb +77 -123
  196. data/lib/active_record/secure_token.rb +40 -0
  197. data/lib/active_record/serialization.rb +10 -6
  198. data/lib/active_record/statement_cache.rb +121 -0
  199. data/lib/active_record/store.rb +175 -16
  200. data/lib/active_record/suppressor.rb +61 -0
  201. data/lib/active_record/table_metadata.rb +82 -0
  202. data/lib/active_record/tasks/database_tasks.rb +337 -0
  203. data/lib/active_record/tasks/mysql_database_tasks.rb +115 -0
  204. data/lib/active_record/tasks/postgresql_database_tasks.rb +143 -0
  205. data/lib/active_record/tasks/sqlite_database_tasks.rb +83 -0
  206. data/lib/active_record/timestamp.rb +80 -41
  207. data/lib/active_record/touch_later.rb +64 -0
  208. data/lib/active_record/transactions.rb +240 -119
  209. data/lib/active_record/translation.rb +2 -0
  210. data/lib/active_record/type/adapter_specific_registry.rb +136 -0
  211. data/lib/active_record/type/date.rb +9 -0
  212. data/lib/active_record/type/date_time.rb +9 -0
  213. data/lib/active_record/type/decimal_without_scale.rb +15 -0
  214. data/lib/active_record/type/hash_lookup_type_map.rb +25 -0
  215. data/lib/active_record/type/internal/timezone.rb +17 -0
  216. data/lib/active_record/type/json.rb +30 -0
  217. data/lib/active_record/type/serialized.rb +71 -0
  218. data/lib/active_record/type/text.rb +11 -0
  219. data/lib/active_record/type/time.rb +21 -0
  220. data/lib/active_record/type/type_map.rb +62 -0
  221. data/lib/active_record/type/unsigned_integer.rb +17 -0
  222. data/lib/active_record/type.rb +79 -0
  223. data/lib/active_record/type_caster/connection.rb +33 -0
  224. data/lib/active_record/type_caster/map.rb +23 -0
  225. data/lib/active_record/type_caster.rb +9 -0
  226. data/lib/active_record/validations/absence.rb +25 -0
  227. data/lib/active_record/validations/associated.rb +35 -18
  228. data/lib/active_record/validations/length.rb +26 -0
  229. data/lib/active_record/validations/presence.rb +68 -0
  230. data/lib/active_record/validations/uniqueness.rb +133 -75
  231. data/lib/active_record/validations.rb +53 -43
  232. data/lib/active_record/version.rb +7 -7
  233. data/lib/active_record.rb +89 -57
  234. data/lib/rails/generators/active_record/application_record/application_record_generator.rb +27 -0
  235. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +5 -0
  236. data/lib/rails/generators/active_record/migration/migration_generator.rb +61 -8
  237. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +24 -0
  238. data/lib/rails/generators/active_record/migration/templates/migration.rb.tt +46 -0
  239. data/lib/rails/generators/active_record/migration.rb +28 -8
  240. data/lib/rails/generators/active_record/model/model_generator.rb +23 -22
  241. data/lib/rails/generators/active_record/model/templates/model.rb.tt +13 -0
  242. data/lib/rails/generators/active_record/model/templates/{module.rb → module.rb.tt} +1 -1
  243. data/lib/rails/generators/active_record.rb +10 -16
  244. metadata +141 -62
  245. data/examples/associations.png +0 -0
  246. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -63
  247. data/lib/active_record/associations/join_helper.rb +0 -55
  248. data/lib/active_record/associations/preloader/belongs_to.rb +0 -17
  249. data/lib/active_record/associations/preloader/collection_association.rb +0 -24
  250. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  251. data/lib/active_record/associations/preloader/has_many.rb +0 -17
  252. data/lib/active_record/associations/preloader/has_many_through.rb +0 -15
  253. data/lib/active_record/associations/preloader/has_one.rb +0 -23
  254. data/lib/active_record/associations/preloader/has_one_through.rb +0 -9
  255. data/lib/active_record/associations/preloader/singular_association.rb +0 -21
  256. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +0 -32
  257. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -191
  258. data/lib/active_record/connection_adapters/mysql_adapter.rb +0 -441
  259. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -583
  260. data/lib/active_record/dynamic_finder_match.rb +0 -68
  261. data/lib/active_record/dynamic_scope_match.rb +0 -23
  262. data/lib/active_record/fixtures/file.rb +0 -65
  263. data/lib/active_record/identity_map.rb +0 -162
  264. data/lib/active_record/observer.rb +0 -121
  265. data/lib/active_record/railties/jdbcmysql_error.rb +0 -16
  266. data/lib/active_record/serializers/xml_serializer.rb +0 -203
  267. data/lib/active_record/session_store.rb +0 -360
  268. data/lib/active_record/test_case.rb +0 -73
  269. data/lib/rails/generators/active_record/migration/templates/migration.rb +0 -34
  270. data/lib/rails/generators/active_record/model/templates/migration.rb +0 -15
  271. data/lib/rails/generators/active_record/model/templates/model.rb +0 -12
  272. data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
  273. data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -4
  274. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -25
  275. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -12
@@ -1,123 +1,218 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  # = Active Record Counter Cache
3
5
  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
- if has_many_association.options[:as]
23
- has_many_association.options[:as].to_s.classify
24
- else
25
- self.name
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}"
26
111
  end
27
112
 
28
- if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
29
- has_many_association = has_many_association.through_reflection
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?
30
117
  end
31
118
 
32
- foreign_key = has_many_association.foreign_key.to_s
33
- child_class = has_many_association.klass
34
- belongs_to = child_class.reflect_on_all_associations(:belongs_to)
35
- reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
36
- counter_name = reflection.counter_cache_column
119
+ if id.is_a?(Relation) && self == id.klass
120
+ relation = id
121
+ else
122
+ relation = unscoped.where!(primary_key => id)
123
+ end
37
124
 
38
- stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
39
- arel_table[counter_name] => object.send(association).count
40
- })
41
- connection.update stmt
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)
42
178
  end
43
- return true
44
179
  end
45
180
 
46
- # A generic "counter updater" implementation, intended primarily to be
47
- # used by increment_counter and decrement_counter, but which may also
48
- # be useful on its own. It simply does a direct SQL update for the record
49
- # with the given ID, altering the given hash of counters by the amount
50
- # given by the corresponding value:
51
- #
52
- # ==== Parameters
53
- #
54
- # * +id+ - The id of the object you wish to update a counter on or an Array of ids.
55
- # * +counters+ - An Array of Hashes containing the names of the fields
56
- # to update as keys and the amount to update the field by as values.
57
- #
58
- # ==== Examples
59
- #
60
- # # For the Post with id of 5, decrement the comment_count by 1, and
61
- # # increment the action_count by 1
62
- # Post.update_counters 5, :comment_count => -1, :action_count => 1
63
- # # Executes the following SQL:
64
- # # UPDATE posts
65
- # # SET comment_count = COALESCE(comment_count, 0) - 1,
66
- # # action_count = COALESCE(action_count, 0) + 1
67
- # # WHERE id = 5
68
- #
69
- # # For the Posts with id of 10 and 15, increment the comment_count by 1
70
- # Post.update_counters [10, 15], :comment_count => 1
71
- # # Executes the following SQL:
72
- # # UPDATE posts
73
- # # SET comment_count = COALESCE(comment_count, 0) + 1
74
- # # WHERE id IN (10, 15)
75
- def update_counters(id, counters)
76
- updates = counters.map do |counter_name, value|
77
- operator = value < 0 ? '-' : '+'
78
- quoted_column = connection.quote_column_name(counter_name)
79
- "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
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
80
193
  end
81
194
 
82
- IdentityMap.remove_by_id(symbolized_base_class, id) if IdentityMap.enabled?
195
+ def destroy_row
196
+ affected_rows = super
83
197
 
84
- update_all(updates.join(', '), primary_key => id )
85
- end
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
86
208
 
87
- # Increment a number field by one, usually representing a count.
88
- #
89
- # This is used for caching aggregate values, so that they don't need to be computed every time.
90
- # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is
91
- # shown it would have to run an SQL query to find how many posts and comments there are.
92
- #
93
- # ==== Parameters
94
- #
95
- # * +counter_name+ - The name of the field that should be incremented.
96
- # * +id+ - The id of the object that should be incremented.
97
- #
98
- # ==== Examples
99
- #
100
- # # Increment the post_count column for the record with an id of 5
101
- # DiscussionBoard.increment_counter(:post_count, 5)
102
- def increment_counter(counter_name, id)
103
- update_counters(id, counter_name => 1)
104
- end
209
+ affected_rows
210
+ end
105
211
 
106
- # Decrement a number field by one, usually representing a count.
107
- #
108
- # This works the same as increment_counter but reduces the column value by 1 instead of increasing it.
109
- #
110
- # ==== Parameters
111
- #
112
- # * +counter_name+ - The name of the field that should be decremented.
113
- # * +id+ - The id of the object that should be decremented.
114
- #
115
- # ==== Examples
116
- #
117
- # # Decrement the post_count column for the record with an id of 5
118
- # DiscussionBoard.decrement_counter(:post_count, 5)
119
- def decrement_counter(counter_name, id)
120
- update_counters(id, counter_name => -1)
121
- end
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
122
217
  end
123
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
@@ -1,84 +1,122 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
- module DynamicMatchers
3
- def respond_to?(method_id, include_private = false)
4
- if match = DynamicFinderMatch.match(method_id)
5
- return true if all_attributes_exists?(match.attribute_names)
6
- elsif match = DynamicScopeMatch.match(method_id)
7
- return true if all_attributes_exists?(match.attribute_names)
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
8
13
  end
9
14
 
10
- super
11
- end
15
+ def method_missing(name, *arguments, &block)
16
+ match = Method.match(self, name)
12
17
 
13
- private
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
14
40
 
15
- # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
16
- # <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders
17
- # section at the top of this file for more detailed information.
18
- #
19
- # It's even possible to use all the additional parameters to +find+. For example, the
20
- # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
21
- #
22
- # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
23
- # is first invoked, so that future attempts to use it do not run through method_missing.
24
- def method_missing(method_id, *arguments, &block)
25
- if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id))
26
- attribute_names = match.attribute_names
27
- super unless all_attributes_exists?(attribute_names)
28
- if !(match.is_a?(DynamicFinderMatch) && match.instantiator? && arguments.first.is_a?(Hash)) && arguments.size < attribute_names.size
29
- method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'"
30
- backtrace = [method_trace] + caller
31
- raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace
41
+ def prefix
42
+ raise NotImplementedError
43
+ end
44
+
45
+ def suffix
46
+ ""
47
+ end
32
48
  end
33
- if match.respond_to?(:scope?) && match.scope?
34
- self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
35
- def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
36
- attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
37
- #
38
- scoped(:conditions => attributes) # scoped(:conditions => attributes)
39
- end # end
40
- METHOD
41
- send(method_id, *arguments)
42
- elsif match.finder?
43
- options = if arguments.length > attribute_names.size
44
- arguments.extract_options!
45
- else
46
- {}
47
- end
48
-
49
- relation = options.any? ? scoped(options) : scoped
50
- relation.send :find_by_attributes, match, attribute_names, *arguments, &block
51
- elsif match.instantiator?
52
- scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
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 }
53
57
  end
54
- else
55
- super
56
- end
57
- end
58
-
59
- # Similar in purpose to +expand_hash_conditions_for_aggregates+.
60
- def expand_attribute_names_for_aggregates(attribute_names)
61
- attribute_names.map { |attribute_name|
62
- unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
63
- aggregate_mapping(aggregation).map do |field_attr, _|
64
- field_attr.to_sym
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})"
65
75
  end
66
- else
67
- attribute_name.to_sym
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"
68
103
  end
69
- }.flatten
70
- end
104
+ end
71
105
 
72
- def all_attributes_exists?(attribute_names)
73
- (expand_attribute_names_for_aggregates(attribute_names) -
74
- column_methods_hash.keys).empty?
75
- end
106
+ class FindByBang < Method
107
+ Method.matchers << self
76
108
 
77
- def aggregate_mapping(reflection)
78
- mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
79
- mapping.first.is_a?(Array) ? mapping : [mapping]
80
- end
109
+ def self.prefix
110
+ "find_by"
111
+ end
81
112
 
113
+ def self.suffix
114
+ "!"
115
+ end
82
116
 
117
+ def finder
118
+ "find_by!"
119
+ end
120
+ end
83
121
  end
84
122
  end