activerecord 7.0.8.7 → 7.1.0.beta1

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.
Files changed (227) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1339 -1572
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +15 -16
  5. data/lib/active_record/aggregations.rb +16 -13
  6. data/lib/active_record/association_relation.rb +1 -1
  7. data/lib/active_record/associations/association.rb +18 -3
  8. data/lib/active_record/associations/association_scope.rb +16 -9
  9. data/lib/active_record/associations/belongs_to_association.rb +14 -6
  10. data/lib/active_record/associations/builder/association.rb +3 -3
  11. data/lib/active_record/associations/builder/belongs_to.rb +21 -8
  12. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +1 -5
  13. data/lib/active_record/associations/builder/singular_association.rb +4 -0
  14. data/lib/active_record/associations/collection_association.rb +17 -9
  15. data/lib/active_record/associations/collection_proxy.rb +16 -11
  16. data/lib/active_record/associations/foreign_association.rb +10 -3
  17. data/lib/active_record/associations/has_many_association.rb +20 -13
  18. data/lib/active_record/associations/has_many_through_association.rb +10 -6
  19. data/lib/active_record/associations/has_one_association.rb +10 -3
  20. data/lib/active_record/associations/join_dependency.rb +10 -8
  21. data/lib/active_record/associations/preloader/association.rb +27 -6
  22. data/lib/active_record/associations/preloader.rb +12 -9
  23. data/lib/active_record/associations/singular_association.rb +1 -1
  24. data/lib/active_record/associations/through_association.rb +22 -11
  25. data/lib/active_record/associations.rb +193 -97
  26. data/lib/active_record/attribute_assignment.rb +0 -2
  27. data/lib/active_record/attribute_methods/before_type_cast.rb +17 -0
  28. data/lib/active_record/attribute_methods/dirty.rb +40 -26
  29. data/lib/active_record/attribute_methods/primary_key.rb +76 -24
  30. data/lib/active_record/attribute_methods/query.rb +28 -16
  31. data/lib/active_record/attribute_methods/read.rb +18 -5
  32. data/lib/active_record/attribute_methods/serialization.rb +150 -31
  33. data/lib/active_record/attribute_methods/write.rb +3 -3
  34. data/lib/active_record/attribute_methods.rb +105 -21
  35. data/lib/active_record/attributes.rb +3 -3
  36. data/lib/active_record/autosave_association.rb +55 -9
  37. data/lib/active_record/base.rb +7 -2
  38. data/lib/active_record/callbacks.rb +10 -24
  39. data/lib/active_record/coders/column_serializer.rb +61 -0
  40. data/lib/active_record/coders/json.rb +1 -1
  41. data/lib/active_record/coders/yaml_column.rb +70 -42
  42. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +163 -88
  43. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +2 -0
  44. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +3 -1
  45. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +63 -43
  46. data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -0
  47. data/lib/active_record/connection_adapters/abstract/database_statements.rb +109 -32
  48. data/lib/active_record/connection_adapters/abstract/query_cache.rb +60 -22
  49. data/lib/active_record/connection_adapters/abstract/quoting.rb +41 -6
  50. data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
  51. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +18 -4
  52. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +137 -11
  53. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +289 -122
  54. data/lib/active_record/connection_adapters/abstract/transaction.rb +280 -58
  55. data/lib/active_record/connection_adapters/abstract_adapter.rb +502 -91
  56. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +200 -108
  57. data/lib/active_record/connection_adapters/column.rb +9 -0
  58. data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
  59. data/lib/active_record/connection_adapters/mysql/database_statements.rb +22 -143
  60. data/lib/active_record/connection_adapters/mysql/quoting.rb +16 -12
  61. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
  62. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +6 -0
  63. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +1 -1
  64. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +17 -12
  65. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +148 -0
  66. data/lib/active_record/connection_adapters/mysql2_adapter.rb +98 -53
  67. data/lib/active_record/connection_adapters/pool_config.rb +14 -5
  68. data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
  69. data/lib/active_record/connection_adapters/postgresql/column.rb +1 -2
  70. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +76 -29
  71. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +11 -2
  72. data/lib/active_record/connection_adapters/postgresql/quoting.rb +9 -6
  73. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +3 -9
  74. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +76 -6
  75. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +131 -2
  76. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +42 -0
  77. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +351 -54
  78. data/lib/active_record/connection_adapters/postgresql_adapter.rb +336 -168
  79. data/lib/active_record/connection_adapters/schema_cache.rb +287 -59
  80. data/lib/active_record/connection_adapters/sqlite3/column.rb +49 -0
  81. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +42 -36
  82. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +4 -3
  83. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +1 -0
  84. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +26 -7
  85. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +162 -77
  86. data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
  87. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +98 -0
  88. data/lib/active_record/connection_adapters/trilogy_adapter.rb +254 -0
  89. data/lib/active_record/connection_adapters.rb +3 -1
  90. data/lib/active_record/connection_handling.rb +71 -94
  91. data/lib/active_record/core.rb +128 -138
  92. data/lib/active_record/counter_cache.rb +46 -25
  93. data/lib/active_record/database_configurations/database_config.rb +9 -3
  94. data/lib/active_record/database_configurations/hash_config.rb +22 -12
  95. data/lib/active_record/database_configurations/url_config.rb +17 -11
  96. data/lib/active_record/database_configurations.rb +86 -33
  97. data/lib/active_record/delegated_type.rb +8 -3
  98. data/lib/active_record/deprecator.rb +7 -0
  99. data/lib/active_record/destroy_association_async_job.rb +2 -0
  100. data/lib/active_record/encryption/auto_filtered_parameters.rb +66 -0
  101. data/lib/active_record/encryption/cipher/aes256_gcm.rb +4 -1
  102. data/lib/active_record/encryption/config.rb +25 -1
  103. data/lib/active_record/encryption/configurable.rb +12 -19
  104. data/lib/active_record/encryption/context.rb +10 -3
  105. data/lib/active_record/encryption/contexts.rb +5 -1
  106. data/lib/active_record/encryption/derived_secret_key_provider.rb +8 -2
  107. data/lib/active_record/encryption/encryptable_record.rb +36 -18
  108. data/lib/active_record/encryption/encrypted_attribute_type.rb +17 -6
  109. data/lib/active_record/encryption/extended_deterministic_queries.rb +66 -54
  110. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +2 -2
  111. data/lib/active_record/encryption/key_generator.rb +12 -1
  112. data/lib/active_record/encryption/message_serializer.rb +2 -0
  113. data/lib/active_record/encryption/properties.rb +3 -3
  114. data/lib/active_record/encryption/scheme.rb +19 -22
  115. data/lib/active_record/encryption.rb +1 -0
  116. data/lib/active_record/enum.rb +113 -26
  117. data/lib/active_record/errors.rb +89 -15
  118. data/lib/active_record/explain.rb +23 -3
  119. data/lib/active_record/fixture_set/model_metadata.rb +14 -4
  120. data/lib/active_record/fixture_set/render_context.rb +2 -0
  121. data/lib/active_record/fixture_set/table_row.rb +29 -8
  122. data/lib/active_record/fixtures.rb +119 -71
  123. data/lib/active_record/future_result.rb +30 -5
  124. data/lib/active_record/gem_version.rb +4 -4
  125. data/lib/active_record/inheritance.rb +30 -16
  126. data/lib/active_record/insert_all.rb +55 -8
  127. data/lib/active_record/integration.rb +8 -8
  128. data/lib/active_record/internal_metadata.rb +118 -30
  129. data/lib/active_record/locking/pessimistic.rb +5 -2
  130. data/lib/active_record/log_subscriber.rb +29 -12
  131. data/lib/active_record/marshalling.rb +56 -0
  132. data/lib/active_record/message_pack.rb +124 -0
  133. data/lib/active_record/middleware/database_selector/resolver.rb +4 -0
  134. data/lib/active_record/middleware/database_selector.rb +5 -7
  135. data/lib/active_record/middleware/shard_selector.rb +3 -1
  136. data/lib/active_record/migration/command_recorder.rb +100 -4
  137. data/lib/active_record/migration/compatibility.rb +131 -5
  138. data/lib/active_record/migration/default_strategy.rb +23 -0
  139. data/lib/active_record/migration/execution_strategy.rb +19 -0
  140. data/lib/active_record/migration.rb +213 -109
  141. data/lib/active_record/model_schema.rb +47 -27
  142. data/lib/active_record/nested_attributes.rb +28 -3
  143. data/lib/active_record/normalization.rb +158 -0
  144. data/lib/active_record/persistence.rb +183 -33
  145. data/lib/active_record/promise.rb +84 -0
  146. data/lib/active_record/query_cache.rb +3 -21
  147. data/lib/active_record/query_logs.rb +77 -52
  148. data/lib/active_record/query_logs_formatter.rb +41 -0
  149. data/lib/active_record/querying.rb +15 -2
  150. data/lib/active_record/railtie.rb +107 -45
  151. data/lib/active_record/railties/controller_runtime.rb +10 -5
  152. data/lib/active_record/railties/databases.rake +139 -145
  153. data/lib/active_record/railties/job_runtime.rb +23 -0
  154. data/lib/active_record/readonly_attributes.rb +32 -5
  155. data/lib/active_record/reflection.rb +169 -45
  156. data/lib/active_record/relation/batches/batch_enumerator.rb +5 -3
  157. data/lib/active_record/relation/batches.rb +190 -61
  158. data/lib/active_record/relation/calculations.rb +152 -63
  159. data/lib/active_record/relation/delegation.rb +22 -8
  160. data/lib/active_record/relation/finder_methods.rb +85 -15
  161. data/lib/active_record/relation/merger.rb +2 -0
  162. data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -2
  163. data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
  164. data/lib/active_record/relation/predicate_builder.rb +26 -14
  165. data/lib/active_record/relation/query_attribute.rb +2 -1
  166. data/lib/active_record/relation/query_methods.rb +351 -62
  167. data/lib/active_record/relation/spawn_methods.rb +18 -1
  168. data/lib/active_record/relation.rb +76 -35
  169. data/lib/active_record/result.rb +19 -5
  170. data/lib/active_record/runtime_registry.rb +10 -1
  171. data/lib/active_record/sanitization.rb +51 -11
  172. data/lib/active_record/schema.rb +2 -3
  173. data/lib/active_record/schema_dumper.rb +41 -7
  174. data/lib/active_record/schema_migration.rb +68 -33
  175. data/lib/active_record/scoping/default.rb +15 -5
  176. data/lib/active_record/scoping/named.rb +2 -2
  177. data/lib/active_record/scoping.rb +2 -1
  178. data/lib/active_record/secure_password.rb +60 -0
  179. data/lib/active_record/secure_token.rb +21 -3
  180. data/lib/active_record/signed_id.rb +7 -5
  181. data/lib/active_record/store.rb +8 -8
  182. data/lib/active_record/suppressor.rb +3 -1
  183. data/lib/active_record/table_metadata.rb +10 -1
  184. data/lib/active_record/tasks/database_tasks.rb +127 -105
  185. data/lib/active_record/tasks/mysql_database_tasks.rb +15 -6
  186. data/lib/active_record/tasks/postgresql_database_tasks.rb +16 -13
  187. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -7
  188. data/lib/active_record/test_fixtures.rb +113 -96
  189. data/lib/active_record/timestamp.rb +26 -14
  190. data/lib/active_record/token_for.rb +113 -0
  191. data/lib/active_record/touch_later.rb +11 -6
  192. data/lib/active_record/transactions.rb +36 -10
  193. data/lib/active_record/type/adapter_specific_registry.rb +1 -8
  194. data/lib/active_record/type/internal/timezone.rb +7 -2
  195. data/lib/active_record/type/time.rb +4 -0
  196. data/lib/active_record/validations/absence.rb +1 -1
  197. data/lib/active_record/validations/numericality.rb +5 -4
  198. data/lib/active_record/validations/presence.rb +5 -28
  199. data/lib/active_record/validations/uniqueness.rb +47 -2
  200. data/lib/active_record/validations.rb +8 -4
  201. data/lib/active_record/version.rb +1 -1
  202. data/lib/active_record.rb +121 -16
  203. data/lib/arel/errors.rb +10 -0
  204. data/lib/arel/factory_methods.rb +4 -0
  205. data/lib/arel/nodes/binary.rb +6 -1
  206. data/lib/arel/nodes/bound_sql_literal.rb +61 -0
  207. data/lib/arel/nodes/cte.rb +36 -0
  208. data/lib/arel/nodes/fragments.rb +35 -0
  209. data/lib/arel/nodes/homogeneous_in.rb +0 -8
  210. data/lib/arel/nodes/leading_join.rb +8 -0
  211. data/lib/arel/nodes/node.rb +111 -2
  212. data/lib/arel/nodes/sql_literal.rb +6 -0
  213. data/lib/arel/nodes/table_alias.rb +4 -0
  214. data/lib/arel/nodes.rb +4 -0
  215. data/lib/arel/predications.rb +2 -0
  216. data/lib/arel/table.rb +9 -5
  217. data/lib/arel/visitors/mysql.rb +8 -1
  218. data/lib/arel/visitors/to_sql.rb +81 -17
  219. data/lib/arel/visitors/visitor.rb +2 -2
  220. data/lib/arel.rb +16 -2
  221. data/lib/rails/generators/active_record/application_record/USAGE +8 -0
  222. data/lib/rails/generators/active_record/migration.rb +3 -1
  223. data/lib/rails/generators/active_record/model/USAGE +113 -0
  224. data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
  225. metadata +52 -17
  226. data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
  227. data/lib/active_record/null_relation.rb +0 -63
@@ -3,8 +3,10 @@
3
3
  require "active_record/relation/batches/batch_enumerator"
4
4
 
5
5
  module ActiveRecord
6
+ # = Active Record \Batches
6
7
  module Batches
7
8
  ORDER_IGNORE_MESSAGE = "Scoped order is ignored, it's forced to be batch order."
9
+ DEFAULT_ORDER = :asc
8
10
 
9
11
  # Looping through a collection of records from the database
10
12
  # (using the Scoping::Named::ClassMethods.all method, for example)
@@ -37,7 +39,16 @@ module ActiveRecord
37
39
  # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
38
40
  # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
39
41
  # an order is present in the relation.
40
- # * <tt>:order</tt> - Specifies the primary key order (can be +:asc+ or +:desc+). Defaults to +:asc+.
42
+ # * <tt>:order</tt> - Specifies the primary key order (can be +:asc+ or +:desc+ or an array consisting
43
+ # of :asc or :desc). Defaults to +:asc+.
44
+ #
45
+ # class Order < ActiveRecord::Base
46
+ # self.primary_key = [:id_1, :id_2]
47
+ # end
48
+ #
49
+ # Order.find_each(order: [:asc, :desc])
50
+ #
51
+ # In the above code, +id_1+ is sorted in ascending order and +id_2+ in descending order.
41
52
  #
42
53
  # Limits are honored, and if present there is no requirement for the batch
43
54
  # size: it can be less than, equal to, or greater than the limit.
@@ -65,7 +76,7 @@ module ActiveRecord
65
76
  #
66
77
  # NOTE: By its nature, batch processing is subject to race conditions if
67
78
  # other processes are modifying the database.
68
- def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: :asc, &block)
79
+ def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: DEFAULT_ORDER, &block)
69
80
  if block_given?
70
81
  find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, order: order) do |records|
71
82
  records.each(&block)
@@ -73,7 +84,7 @@ module ActiveRecord
73
84
  else
74
85
  enum_for(:find_each, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, order: order) do
75
86
  relation = self
76
- apply_limits(relation, start, finish, order).size
87
+ apply_limits(relation, start, finish, build_batch_orders(order)).size
77
88
  end
78
89
  end
79
90
  end
@@ -102,7 +113,16 @@ module ActiveRecord
102
113
  # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
103
114
  # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
104
115
  # an order is present in the relation.
105
- # * <tt>:order</tt> - Specifies the primary key order (can be +:asc+ or +:desc+). Defaults to +:asc+.
116
+ # * <tt>:order</tt> - Specifies the primary key order (can be +:asc+ or +:desc+ or an array consisting
117
+ # of :asc or :desc). Defaults to +:asc+.
118
+ #
119
+ # class Order < ActiveRecord::Base
120
+ # self.primary_key = [:id_1, :id_2]
121
+ # end
122
+ #
123
+ # Order.find_in_batches(order: [:asc, :desc])
124
+ #
125
+ # In the above code, +id_1+ is sorted in ascending order and +id_2+ in descending order.
106
126
  #
107
127
  # Limits are honored, and if present there is no requirement for the batch
108
128
  # size: it can be less than, equal to, or greater than the limit.
@@ -125,11 +145,11 @@ module ActiveRecord
125
145
  #
126
146
  # NOTE: By its nature, batch processing is subject to race conditions if
127
147
  # other processes are modifying the database.
128
- def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: :asc)
148
+ def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: DEFAULT_ORDER)
129
149
  relation = self
130
150
  unless block_given?
131
151
  return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, order: order) do
132
- total = apply_limits(relation, start, finish, order).size
152
+ total = apply_limits(relation, start, finish, build_batch_orders(order)).size
133
153
  (total - 1).div(batch_size) + 1
134
154
  end
135
155
  end
@@ -167,7 +187,22 @@ module ActiveRecord
167
187
  # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
168
188
  # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
169
189
  # an order is present in the relation.
170
- # * <tt>:order</tt> - Specifies the primary key order (can be +:asc+ or +:desc+). Defaults to +:asc+.
190
+ # * <tt>:order</tt> - Specifies the primary key order (can be +:asc+ or +:desc+ or an array consisting
191
+ # of :asc or :desc). Defaults to +:asc+.
192
+ #
193
+ # class Order < ActiveRecord::Base
194
+ # self.primary_key = [:id_1, :id_2]
195
+ # end
196
+ #
197
+ # Order.in_batches(order: [:asc, :desc])
198
+ #
199
+ # In the above code, +id_1+ is sorted in ascending order and +id_2+ in descending order.
200
+ #
201
+ # * <tt>:use_ranges</tt> - Specifies whether to use range iteration (id >= x AND id <= y).
202
+ # It can make iterating over the whole or almost whole tables several times faster.
203
+ # Only whole table iterations use this style of iteration by default. You can disable this behavior by passing +false+.
204
+ # If you iterate over the table and the only condition is, e.g., <tt>archived_at: nil</tt> (and only a tiny fraction
205
+ # of the records are archived), it makes sense to opt in to this approach.
171
206
  #
172
207
  # Limits are honored, and if present there is no requirement for the batch
173
208
  # size, it can be less than, equal, or greater than the limit.
@@ -201,14 +236,13 @@ module ActiveRecord
201
236
  #
202
237
  # NOTE: By its nature, batch processing is subject to race conditions if
203
238
  # other processes are modifying the database.
204
- def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil, order: :asc)
205
- relation = self
206
- unless block_given?
207
- return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self)
239
+ def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil, order: DEFAULT_ORDER, use_ranges: nil, &block)
240
+ unless Array(order).all? { |ord| [:asc, :desc].include?(ord) }
241
+ raise ArgumentError, ":order must be :asc or :desc or an array consisting of :asc or :desc, got #{order.inspect}"
208
242
  end
209
243
 
210
- unless [:asc, :desc].include?(order)
211
- raise ArgumentError, ":order must be :asc or :desc, got #{order.inspect}"
244
+ unless block
245
+ return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self, order: order, use_ranges: use_ranges)
212
246
  end
213
247
 
214
248
  if arel.orders.present?
@@ -216,71 +250,76 @@ module ActiveRecord
216
250
  end
217
251
 
218
252
  batch_limit = of
253
+
219
254
  if limit_value
220
255
  remaining = limit_value
221
256
  batch_limit = remaining if remaining < batch_limit
222
257
  end
223
258
 
224
- relation = relation.reorder(batch_order(order)).limit(batch_limit)
225
- relation = apply_limits(relation, start, finish, order)
226
- relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching
227
- batch_relation = relation
228
-
229
- loop do
230
- if load
231
- records = batch_relation.records
232
- ids = records.map(&:id)
233
- yielded_relation = where(primary_key => ids)
234
- yielded_relation.load_records(records)
235
- else
236
- ids = batch_relation.pluck(primary_key)
237
- yielded_relation = where(primary_key => ids)
238
- end
239
-
240
- break if ids.empty?
241
-
242
- primary_key_offset = ids.last
243
- raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
244
-
245
- yield yielded_relation
246
-
247
- break if ids.length < batch_limit
248
-
249
- if limit_value
250
- remaining -= ids.length
251
-
252
- if remaining == 0
253
- # Saves a useless iteration when the limit is a multiple of the
254
- # batch size.
255
- break
256
- elsif remaining < batch_limit
257
- relation = relation.limit(remaining)
258
- end
259
- end
260
-
261
- batch_relation = relation.where(
262
- predicate_builder[primary_key, primary_key_offset, order == :desc ? :lt : :gt]
259
+ if self.loaded?
260
+ batch_on_loaded_relation(
261
+ relation: self,
262
+ start: start,
263
+ finish: finish,
264
+ order: order,
265
+ batch_limit: batch_limit,
266
+ &block
267
+ )
268
+ else
269
+ batch_on_unloaded_relation(
270
+ relation: self,
271
+ start: start,
272
+ finish: finish,
273
+ load: load,
274
+ order: order,
275
+ use_ranges: use_ranges,
276
+ remaining: remaining,
277
+ batch_limit: batch_limit,
278
+ &block
263
279
  )
264
280
  end
265
281
  end
266
282
 
267
283
  private
268
- def apply_limits(relation, start, finish, order)
269
- relation = apply_start_limit(relation, start, order) if start
270
- relation = apply_finish_limit(relation, finish, order) if finish
284
+ def apply_limits(relation, start, finish, batch_orders)
285
+ relation = apply_start_limit(relation, start, batch_orders) if start
286
+ relation = apply_finish_limit(relation, finish, batch_orders) if finish
271
287
  relation
272
288
  end
273
289
 
274
- def apply_start_limit(relation, start, order)
275
- relation.where(predicate_builder[primary_key, start, order == :desc ? :lteq : :gteq])
290
+ def apply_start_limit(relation, start, batch_orders)
291
+ operators = batch_orders.map do |_column, order|
292
+ order == :desc ? :lteq : :gteq
293
+ end
294
+ batch_condition(relation, primary_key, start, operators)
295
+ end
296
+
297
+ def apply_finish_limit(relation, finish, batch_orders)
298
+ operators = batch_orders.map do |_column, order|
299
+ order == :desc ? :gteq : :lteq
300
+ end
301
+ batch_condition(relation, primary_key, finish, operators)
276
302
  end
277
303
 
278
- def apply_finish_limit(relation, finish, order)
279
- relation.where(predicate_builder[primary_key, finish, order == :desc ? :gteq : :lteq])
304
+ def batch_condition(relation, columns, values, operators)
305
+ cursor_positions = Array(columns).zip(Array(values), operators)
306
+
307
+ first_clause_column, first_clause_value, operator = cursor_positions.pop
308
+ where_clause = predicate_builder[first_clause_column, first_clause_value, operator]
309
+
310
+ cursor_positions.reverse_each do |column_name, value, operator|
311
+ where_clause = predicate_builder[column_name, value, operator == :lteq ? :lt : :gt].or(
312
+ predicate_builder[column_name, value, :eq].and(where_clause)
313
+ )
314
+ end
315
+
316
+ relation.where(where_clause)
280
317
  end
281
318
 
282
- def batch_order(order)
283
- table[primary_key].public_send(order)
319
+ def build_batch_orders(order)
320
+ get_the_order_of_primary_key(order).map do |column, ord|
321
+ [column, ord || DEFAULT_ORDER]
322
+ end
284
323
  end
285
324
 
286
325
  def act_on_ignored_order(error_on_ignore)
@@ -292,5 +331,95 @@ module ActiveRecord
292
331
  logger.warn(ORDER_IGNORE_MESSAGE)
293
332
  end
294
333
  end
334
+
335
+ def get_the_order_of_primary_key(order)
336
+ Array(primary_key).zip(Array(order))
337
+ end
338
+
339
+ def batch_on_loaded_relation(relation:, start:, finish:, order:, batch_limit:)
340
+ records = relation.to_a
341
+
342
+ if start || finish
343
+ records = records.filter do |record|
344
+ (start.nil? || record.id >= start) && (finish.nil? || record.id <= finish)
345
+ end
346
+ end
347
+
348
+ records = records.sort_by { |record| record.id }
349
+
350
+ if order == :desc
351
+ records.reverse!
352
+ end
353
+
354
+ (0...records.size).step(batch_limit).each do |start|
355
+ subrelation = relation.spawn
356
+ subrelation.load_records(records[start, batch_limit])
357
+
358
+ yield subrelation
359
+ end
360
+
361
+ nil
362
+ end
363
+
364
+ def batch_on_unloaded_relation(relation:, start:, finish:, load:, order:, use_ranges:, remaining:, batch_limit:)
365
+ batch_orders = build_batch_orders(order)
366
+ relation = relation.reorder(batch_orders.to_h).limit(batch_limit)
367
+ relation = apply_limits(relation, start, finish, batch_orders)
368
+ relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching
369
+ batch_relation = relation
370
+ empty_scope = to_sql == klass.unscoped.all.to_sql
371
+
372
+ loop do
373
+ if load
374
+ records = batch_relation.records
375
+ ids = records.map(&:id)
376
+ yielded_relation = where(primary_key => ids)
377
+ yielded_relation.load_records(records)
378
+ elsif (empty_scope && use_ranges != false) || use_ranges
379
+ ids = batch_relation.ids
380
+ finish = ids.last
381
+ if finish
382
+ yielded_relation = apply_finish_limit(batch_relation, finish, batch_orders)
383
+ yielded_relation = yielded_relation.except(:limit, :order)
384
+ yielded_relation.skip_query_cache!(false)
385
+ end
386
+ else
387
+ ids = batch_relation.ids
388
+ yielded_relation = where(primary_key => ids)
389
+ end
390
+
391
+ break if ids.empty?
392
+
393
+ primary_key_offset = ids.last
394
+ raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
395
+
396
+ yield yielded_relation
397
+
398
+ break if ids.length < batch_limit
399
+
400
+ if limit_value
401
+ remaining -= ids.length
402
+
403
+ if remaining == 0
404
+ # Saves a useless iteration when the limit is a multiple of the
405
+ # batch size.
406
+ break
407
+ elsif remaining < batch_limit
408
+ relation = relation.limit(remaining)
409
+ end
410
+ end
411
+
412
+ batch_orders_copy = batch_orders.dup
413
+ _last_column, last_order = batch_orders_copy.pop
414
+ operators = batch_orders_copy.map do |_column, order|
415
+ order == :desc ? :lteq : :gteq
416
+ end
417
+ operators << (last_order == :desc ? :lt : :gt)
418
+
419
+ batch_relation = batch_condition(relation, primary_key, primary_key_offset, operators)
420
+ end
421
+
422
+ nil
423
+ end
295
424
  end
296
425
  end
@@ -3,6 +3,7 @@
3
3
  require "active_support/core_ext/enumerable"
4
4
 
5
5
  module ActiveRecord
6
+ # = Active Record \Calculations
6
7
  module Calculations
7
8
  class ColumnAliasTracker # :nodoc:
8
9
  def initialize(connection)
@@ -71,8 +72,7 @@ module ActiveRecord
71
72
  # of each key would be the #count.
72
73
  #
73
74
  # Article.group(:status, :category).count
74
- # # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
75
- # # ["published", "business"]=>0, ["published", "technology"]=>2}
75
+ # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, ["published", "technology"]=>2}
76
76
  #
77
77
  # If #count is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns:
78
78
  #
@@ -93,6 +93,11 @@ module ActiveRecord
93
93
  end
94
94
  end
95
95
 
96
+ # Same as <tt>#count</tt> but perform the query asynchronously and returns an ActiveRecord::Promise
97
+ def async_count(column_name = nil)
98
+ async.count(column_name)
99
+ end
100
+
96
101
  # Calculates the average value on a given column. Returns +nil+ if there's
97
102
  # no row. See #calculate for examples with options.
98
103
  #
@@ -101,6 +106,11 @@ module ActiveRecord
101
106
  calculate(:average, column_name)
102
107
  end
103
108
 
109
+ # Same as <tt>#average</tt> but perform the query asynchronously and returns an ActiveRecord::Promise
110
+ def async_average(column_name)
111
+ async.average(column_name)
112
+ end
113
+
104
114
  # Calculates the minimum value on a given column. The value is returned
105
115
  # with the same data type of the column, or +nil+ if there's no row. See
106
116
  # #calculate for examples with options.
@@ -110,6 +120,11 @@ module ActiveRecord
110
120
  calculate(:minimum, column_name)
111
121
  end
112
122
 
123
+ # Same as <tt>#minimum</tt> but perform the query asynchronously and returns an ActiveRecord::Promise
124
+ def async_minimum(column_name)
125
+ async.minimum(column_name)
126
+ end
127
+
113
128
  # Calculates the maximum value on a given column. The value is returned
114
129
  # with the same data type of the column, or +nil+ if there's no row. See
115
130
  # #calculate for examples with options.
@@ -119,32 +134,29 @@ module ActiveRecord
119
134
  calculate(:maximum, column_name)
120
135
  end
121
136
 
137
+ # Same as <tt>#maximum</tt> but perform the query asynchronously and returns an ActiveRecord::Promise
138
+ def async_maximum(column_name)
139
+ async.maximum(column_name)
140
+ end
141
+
122
142
  # Calculates the sum of values on a given column. The value is returned
123
143
  # with the same data type of the column, +0+ if there's no row. See
124
144
  # #calculate for examples with options.
125
145
  #
126
146
  # Person.sum(:age) # => 4562
127
- def sum(identity_or_column = nil, &block)
147
+ def sum(initial_value_or_column = 0, &block)
128
148
  if block_given?
129
- values = map(&block)
130
- if identity_or_column.nil? && (values.first.is_a?(Numeric) || values.first(1) == [] || values.first.respond_to?(:coerce))
131
- identity_or_column = 0
132
- end
133
-
134
- if identity_or_column.nil?
135
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
136
- Rails 7.0 has deprecated Enumerable.sum in favor of Ruby's native implementation available since 2.4.
137
- Sum of non-numeric elements requires an initial argument.
138
- MSG
139
- values.inject(:+) || 0
140
- else
141
- values.sum(identity_or_column)
142
- end
149
+ map(&block).sum(initial_value_or_column)
143
150
  else
144
- calculate(:sum, identity_or_column)
151
+ calculate(:sum, initial_value_or_column)
145
152
  end
146
153
  end
147
154
 
155
+ # Same as <tt>#sum</tt> but perform the query asynchronously and returns an ActiveRecord::Promise
156
+ def async_sum(identity_or_column = nil)
157
+ async.sum(identity_or_column)
158
+ end
159
+
148
160
  # This calculates aggregate values in the given column. Methods for #count, #sum, #average,
149
161
  # #minimum, and #maximum have been added as shortcuts.
150
162
  #
@@ -177,10 +189,23 @@ module ActiveRecord
177
189
  # ...
178
190
  # end
179
191
  def calculate(operation, column_name)
192
+ operation = operation.to_s.downcase
193
+
194
+ if @none
195
+ case operation
196
+ when "count", "sum"
197
+ result = group_values.any? ? Hash.new : 0
198
+ return @async ? Promise::Complete.new(result) : result
199
+ when "average", "minimum", "maximum"
200
+ result = group_values.any? ? Hash.new : nil
201
+ return @async ? Promise::Complete.new(result) : result
202
+ end
203
+ end
204
+
180
205
  if has_include?(column_name)
181
206
  relation = apply_join_dependency
182
207
 
183
- if operation.to_s.downcase == "count"
208
+ if operation == "count"
184
209
  unless distinct_value || distinct_select?(column_name || select_for_count)
185
210
  relation.distinct!
186
211
  relation.select_values = [ klass.primary_key || table[Arel.star] ]
@@ -229,31 +254,44 @@ module ActiveRecord
229
254
  # # => ['0', '27761', '173']
230
255
  #
231
256
  # See also #ids.
232
- #
233
257
  def pluck(*column_names)
258
+ return [] if @none
259
+
234
260
  if loaded? && all_attributes?(column_names)
235
- return records.pluck(*column_names)
261
+ result = records.pluck(*column_names)
262
+ if @async
263
+ return Promise::Complete.new(result)
264
+ else
265
+ return result
266
+ end
236
267
  end
237
268
 
238
269
  if has_include?(column_names.first)
239
270
  relation = apply_join_dependency
240
271
  relation.pluck(*column_names)
241
272
  else
242
- klass.disallow_raw_sql!(column_names)
273
+ klass.disallow_raw_sql!(column_names.flatten)
243
274
  columns = arel_columns(column_names)
244
275
  relation = spawn
245
276
  relation.select_values = columns
246
277
  result = skip_query_cache_if_necessary do
247
278
  if where_clause.contradiction?
248
- ActiveRecord::Result.empty
279
+ ActiveRecord::Result.empty(async: @async)
249
280
  else
250
- klass.connection.select_all(relation.arel, "#{klass.name} Pluck")
281
+ klass.connection.select_all(relation.arel, "#{klass.name} Pluck", async: @async)
251
282
  end
252
283
  end
253
- type_cast_pluck_values(result, columns)
284
+ result.then do |result|
285
+ type_cast_pluck_values(result, columns)
286
+ end
254
287
  end
255
288
  end
256
289
 
290
+ # Same as <tt>#pluck</tt> but perform the query asynchronously and returns an ActiveRecord::Promise
291
+ def async_pluck(*column_names)
292
+ async.pluck(*column_names)
293
+ end
294
+
257
295
  # Pick the value(s) from the named column(s) in the current relation.
258
296
  # This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful
259
297
  # when you have a relation that's already narrowed down to a single row.
@@ -270,18 +308,59 @@ module ActiveRecord
270
308
  # # => [ 'David', 'david@loudthinking.com' ]
271
309
  def pick(*column_names)
272
310
  if loaded? && all_attributes?(column_names)
273
- return records.pick(*column_names)
311
+ result = records.pick(*column_names)
312
+ return @async ? Promise::Complete.new(result) : result
274
313
  end
275
314
 
276
- limit(1).pluck(*column_names).first
315
+ limit(1).pluck(*column_names).then(&:first)
277
316
  end
278
317
 
279
- # Pluck all the ID's for the relation using the table's primary key
318
+ # Same as <tt>#pick</tt> but perform the query asynchronously and returns an ActiveRecord::Promise
319
+ def async_pick(*column_names)
320
+ async.pick(*column_names)
321
+ end
322
+
323
+ # Returns the base model's ID's for the relation using the table's primary key
280
324
  #
281
325
  # Person.ids # SELECT people.id FROM people
282
- # Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id
326
+ # Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.id = people.company_id
283
327
  def ids
284
- pluck primary_key
328
+ primary_key_array = Array(primary_key)
329
+
330
+ if loaded?
331
+ result = records.map do |record|
332
+ if primary_key_array.one?
333
+ record._read_attribute(primary_key_array.first)
334
+ else
335
+ primary_key_array.map { |column| record._read_attribute(column) }
336
+ end
337
+ end
338
+ return @async ? Promise::Complete.new(result) : result
339
+ end
340
+
341
+ if has_include?(primary_key)
342
+ relation = apply_join_dependency.group(*primary_key_array)
343
+ return relation.ids
344
+ end
345
+
346
+ columns = arel_columns(primary_key_array)
347
+ relation = spawn
348
+ relation.select_values = columns
349
+
350
+ result = if relation.where_clause.contradiction?
351
+ ActiveRecord::Result.empty
352
+ else
353
+ skip_query_cache_if_necessary do
354
+ klass.connection.select_all(relation, "#{klass.name} Ids", async: @async)
355
+ end
356
+ end
357
+
358
+ result.then { |result| type_cast_pluck_values(result, columns) }
359
+ end
360
+
361
+ # Same as <tt>#ids</tt> but perform the query asynchronously and returns an ActiveRecord::Promise
362
+ def async_ids
363
+ async.ids
285
364
  end
286
365
 
287
366
  private
@@ -341,6 +420,7 @@ module ActiveRecord
341
420
  # Shortcut when limit is zero.
342
421
  return 0 if limit_value == 0
343
422
 
423
+ relation = self
344
424
  query_builder = build_count_subquery(spawn, column_name, distinct)
345
425
  else
346
426
  # PostgreSQL doesn't like ORDER BY when there are no GROUP BY
@@ -355,15 +435,23 @@ module ActiveRecord
355
435
  query_builder = relation.arel
356
436
  end
357
437
 
358
- result = skip_query_cache_if_necessary { @klass.connection.select_all(query_builder, "#{@klass.name} #{operation.capitalize}") }
359
-
360
- if operation != "count"
361
- type = column.try(:type_caster) ||
362
- lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
363
- type = type.subtype if Enum::EnumType === type
438
+ query_result = if relation.where_clause.contradiction?
439
+ ActiveRecord::Result.empty
440
+ else
441
+ skip_query_cache_if_necessary do
442
+ @klass.connection.select_all(query_builder, "#{@klass.name} #{operation.capitalize}", async: @async)
443
+ end
364
444
  end
365
445
 
366
- type_cast_calculated_value(result.cast_values.first, operation, type)
446
+ query_result.then do |result|
447
+ if operation != "count"
448
+ type = column.try(:type_caster) ||
449
+ lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
450
+ type = type.subtype if Enum::EnumType === type
451
+ end
452
+
453
+ type_cast_calculated_value(result.cast_values.first, operation, type)
454
+ end
367
455
  end
368
456
 
369
457
  def execute_grouped_calculation(operation, column_name, distinct) # :nodoc:
@@ -406,39 +494,40 @@ module ActiveRecord
406
494
  relation.group_values = group_fields
407
495
  relation.select_values = select_values
408
496
 
409
- calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "#{@klass.name} #{operation.capitalize}") }
497
+ result = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "#{@klass.name} #{operation.capitalize}", async: @async) }
498
+ result.then do |calculated_data|
499
+ if association
500
+ key_ids = calculated_data.collect { |row| row[group_aliases.first] }
501
+ key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
502
+ key_records = key_records.index_by(&:id)
503
+ end
410
504
 
411
- if association
412
- key_ids = calculated_data.collect { |row| row[group_aliases.first] }
413
- key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
414
- key_records = key_records.index_by(&:id)
415
- end
505
+ key_types = group_columns.each_with_object({}) do |(aliaz, col_name), types|
506
+ types[aliaz] = col_name.try(:type_caster) ||
507
+ type_for(col_name) do
508
+ calculated_data.column_types.fetch(aliaz, Type.default_value)
509
+ end
510
+ end
416
511
 
417
- key_types = group_columns.each_with_object({}) do |(aliaz, col_name), types|
418
- types[aliaz] = col_name.try(:type_caster) ||
419
- type_for(col_name) do
420
- calculated_data.column_types.fetch(aliaz, Type.default_value)
512
+ hash_rows = calculated_data.cast_values(key_types).map! do |row|
513
+ calculated_data.columns.each_with_object({}).with_index do |(col_name, hash), i|
514
+ hash[col_name] = row[i]
421
515
  end
422
- end
423
-
424
- hash_rows = calculated_data.cast_values(key_types).map! do |row|
425
- calculated_data.columns.each_with_object({}).with_index do |(col_name, hash), i|
426
- hash[col_name] = row[i]
427
516
  end
428
- end
429
517
 
430
- if operation != "count"
431
- type = column.try(:type_caster) ||
432
- lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
433
- type = type.subtype if Enum::EnumType === type
434
- end
518
+ if operation != "count"
519
+ type = column.try(:type_caster) ||
520
+ lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
521
+ type = type.subtype if Enum::EnumType === type
522
+ end
435
523
 
436
- hash_rows.each_with_object({}) do |row, result|
437
- key = group_aliases.map { |aliaz| row[aliaz] }
438
- key = key.first if key.size == 1
439
- key = key_records[key] if associated
524
+ hash_rows.each_with_object({}) do |row, result|
525
+ key = group_aliases.map { |aliaz| row[aliaz] }
526
+ key = key.first if key.size == 1
527
+ key = key_records[key] if associated
440
528
 
441
- result[key] = type_cast_calculated_value(row[column_alias], operation, type)
529
+ result[key] = type_cast_calculated_value(row[column_alias], operation, type)
530
+ end
442
531
  end
443
532
  end
444
533