activerecord 7.2.3 → 8.0.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.

Potentially problematic release.


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

Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +192 -1261
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/associations/alias_tracker.rb +4 -6
  5. data/lib/active_record/associations/association.rb +25 -5
  6. data/lib/active_record/associations/belongs_to_association.rb +2 -18
  7. data/lib/active_record/associations/builder/association.rb +7 -6
  8. data/lib/active_record/associations/collection_association.rb +4 -4
  9. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  10. data/lib/active_record/associations/has_many_through_association.rb +4 -9
  11. data/lib/active_record/associations/join_dependency/join_association.rb +27 -25
  12. data/lib/active_record/associations/preloader/association.rb +2 -2
  13. data/lib/active_record/associations/singular_association.rb +8 -3
  14. data/lib/active_record/associations.rb +50 -32
  15. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  16. data/lib/active_record/attribute_methods/serialization.rb +1 -1
  17. data/lib/active_record/attribute_methods.rb +19 -24
  18. data/lib/active_record/attributes.rb +26 -37
  19. data/lib/active_record/autosave_association.rb +81 -49
  20. data/lib/active_record/base.rb +2 -2
  21. data/lib/active_record/callbacks.rb +1 -1
  22. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
  23. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  24. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  25. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +31 -75
  26. data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
  27. data/lib/active_record/connection_adapters/abstract/query_cache.rb +14 -19
  28. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  29. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +2 -6
  30. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +27 -9
  31. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  32. data/lib/active_record/connection_adapters/abstract_adapter.rb +27 -57
  33. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +28 -58
  34. data/lib/active_record/connection_adapters/mysql/quoting.rb +1 -15
  35. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  36. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +43 -45
  37. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +42 -98
  38. data/lib/active_record/connection_adapters/mysql2_adapter.rb +2 -16
  39. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +64 -42
  40. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  41. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +0 -1
  42. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +12 -14
  43. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
  44. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +51 -9
  45. data/lib/active_record/connection_adapters/postgresql_adapter.rb +44 -101
  46. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  47. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
  48. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -13
  49. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  50. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
  51. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -2
  52. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +60 -22
  53. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  54. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -18
  55. data/lib/active_record/connection_handling.rb +29 -11
  56. data/lib/active_record/core.rb +15 -60
  57. data/lib/active_record/counter_cache.rb +1 -1
  58. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -3
  59. data/lib/active_record/delegated_type.rb +18 -18
  60. data/lib/active_record/encryption/config.rb +3 -1
  61. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  62. data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
  63. data/lib/active_record/encryption/encryptor.rb +35 -29
  64. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  65. data/lib/active_record/encryption/scheme.rb +8 -1
  66. data/lib/active_record/enum.rb +12 -13
  67. data/lib/active_record/errors.rb +16 -8
  68. data/lib/active_record/fixture_set/table_row.rb +2 -19
  69. data/lib/active_record/fixtures.rb +0 -1
  70. data/lib/active_record/future_result.rb +14 -10
  71. data/lib/active_record/gem_version.rb +4 -4
  72. data/lib/active_record/insert_all.rb +1 -1
  73. data/lib/active_record/marshalling.rb +1 -4
  74. data/lib/active_record/migration/command_recorder.rb +22 -5
  75. data/lib/active_record/migration/compatibility.rb +5 -2
  76. data/lib/active_record/migration.rb +36 -35
  77. data/lib/active_record/model_schema.rb +1 -1
  78. data/lib/active_record/nested_attributes.rb +4 -6
  79. data/lib/active_record/persistence.rb +128 -130
  80. data/lib/active_record/query_cache.rb +5 -4
  81. data/lib/active_record/query_logs.rb +98 -44
  82. data/lib/active_record/query_logs_formatter.rb +17 -28
  83. data/lib/active_record/querying.rb +10 -10
  84. data/lib/active_record/railtie.rb +5 -6
  85. data/lib/active_record/railties/databases.rake +1 -2
  86. data/lib/active_record/reflection.rb +9 -7
  87. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  88. data/lib/active_record/relation/batches.rb +132 -72
  89. data/lib/active_record/relation/calculations.rb +55 -55
  90. data/lib/active_record/relation/delegation.rb +25 -14
  91. data/lib/active_record/relation/finder_methods.rb +31 -32
  92. data/lib/active_record/relation/merger.rb +8 -8
  93. data/lib/active_record/relation/predicate_builder/association_query_value.rb +0 -2
  94. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  95. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  96. data/lib/active_record/relation/predicate_builder.rb +5 -0
  97. data/lib/active_record/relation/query_attribute.rb +1 -1
  98. data/lib/active_record/relation/query_methods.rb +90 -91
  99. data/lib/active_record/relation/record_fetch_warning.rb +2 -2
  100. data/lib/active_record/relation/spawn_methods.rb +1 -1
  101. data/lib/active_record/relation/where_clause.rb +2 -8
  102. data/lib/active_record/relation.rb +77 -76
  103. data/lib/active_record/result.rb +68 -7
  104. data/lib/active_record/sanitization.rb +7 -6
  105. data/lib/active_record/schema_dumper.rb +16 -29
  106. data/lib/active_record/schema_migration.rb +2 -1
  107. data/lib/active_record/scoping/named.rb +5 -2
  108. data/lib/active_record/secure_token.rb +3 -3
  109. data/lib/active_record/signed_id.rb +6 -7
  110. data/lib/active_record/statement_cache.rb +12 -12
  111. data/lib/active_record/store.rb +7 -3
  112. data/lib/active_record/tasks/database_tasks.rb +24 -15
  113. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  114. data/lib/active_record/tasks/postgresql_database_tasks.rb +0 -7
  115. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  116. data/lib/active_record/test_fixtures.rb +12 -0
  117. data/lib/active_record/testing/query_assertions.rb +2 -2
  118. data/lib/active_record/token_for.rb +1 -1
  119. data/lib/active_record/transactions.rb +1 -3
  120. data/lib/active_record/validations/uniqueness.rb +8 -8
  121. data/lib/active_record.rb +16 -1
  122. data/lib/arel/collectors/bind.rb +1 -1
  123. data/lib/arel/crud.rb +0 -2
  124. data/lib/arel/delete_manager.rb +0 -5
  125. data/lib/arel/nodes/delete_statement.rb +2 -4
  126. data/lib/arel/nodes/update_statement.rb +2 -4
  127. data/lib/arel/select_manager.rb +2 -6
  128. data/lib/arel/update_manager.rb +0 -5
  129. data/lib/arel/visitors/dot.rb +0 -2
  130. data/lib/arel/visitors/sqlite.rb +0 -25
  131. data/lib/arel/visitors/to_sql.rb +1 -3
  132. metadata +14 -11
@@ -5,7 +5,7 @@ require "active_record/relation/batches/batch_enumerator"
5
5
  module ActiveRecord
6
6
  # = Active Record \Batches
7
7
  module Batches
8
- ORDER_IGNORE_MESSAGE = "Scoped order is ignored, it's forced to be batch order."
8
+ ORDER_IGNORE_MESSAGE = "Scoped order is ignored, use :cursor with :order to configure custom order."
9
9
  DEFAULT_ORDER = :asc
10
10
 
11
11
  # Looping through a collection of records from the database
@@ -35,11 +35,13 @@ module ActiveRecord
35
35
  #
36
36
  # ==== Options
37
37
  # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000.
38
- # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
39
- # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
38
+ # * <tt>:start</tt> - Specifies the cursor column value to start from, inclusive of the value.
39
+ # * <tt>:finish</tt> - Specifies the cursor column value to end at, inclusive of the value.
40
40
  # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
41
41
  # an order is present in the relation.
42
- # * <tt>:order</tt> - Specifies the primary key order (can be +:asc+ or +:desc+ or an array consisting
42
+ # * <tt>:cursor</tt> - Specifies the column to use for batching (can be a column name or an array
43
+ # of column names). Defaults to primary key.
44
+ # * <tt>:order</tt> - Specifies the cursor column order (can be +:asc+ or +:desc+ or an array consisting
43
45
  # of :asc or :desc). Defaults to +:asc+.
44
46
  #
45
47
  # class Order < ActiveRecord::Base
@@ -71,20 +73,25 @@ module ActiveRecord
71
73
  #
72
74
  # NOTE: Order can be ascending (:asc) or descending (:desc). It is automatically set to
73
75
  # ascending on the primary key ("id ASC").
74
- # This also means that this method only works when the primary key is
76
+ # This also means that this method only works when the cursor column is
75
77
  # orderable (e.g. an integer or string).
76
78
  #
79
+ # NOTE: When using custom columns for batching, they should include at least one unique column
80
+ # (e.g. primary key) as a tiebreaker. Also, to reduce the likelihood of race conditions,
81
+ # all columns should be static (unchangeable after it was set).
82
+ #
77
83
  # NOTE: By its nature, batch processing is subject to race conditions if
78
84
  # other processes are modifying the database.
79
- def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: DEFAULT_ORDER, &block)
85
+ def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, cursor: primary_key, order: DEFAULT_ORDER, &block)
80
86
  if block_given?
81
- find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, order: order) do |records|
87
+ find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, cursor: cursor, order: order) do |records|
82
88
  records.each(&block)
83
89
  end
84
90
  else
85
- enum_for(:find_each, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, order: order) do
91
+ enum_for(:find_each, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, cursor: cursor, order: order) do
86
92
  relation = self
87
- apply_limits(relation, start, finish, build_batch_orders(order)).size
93
+ cursor = Array(cursor)
94
+ apply_limits(relation, cursor, start, finish, build_batch_orders(cursor, order)).size
88
95
  end
89
96
  end
90
97
  end
@@ -109,11 +116,13 @@ module ActiveRecord
109
116
  #
110
117
  # ==== Options
111
118
  # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000.
112
- # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
113
- # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
119
+ # * <tt>:start</tt> - Specifies the cursor column value to start from, inclusive of the value.
120
+ # * <tt>:finish</tt> - Specifies the cursor column value to end at, inclusive of the value.
114
121
  # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
115
122
  # an order is present in the relation.
116
- # * <tt>:order</tt> - Specifies the primary key order (can be +:asc+ or +:desc+ or an array consisting
123
+ # * <tt>:cursor</tt> - Specifies the column to use for batching (can be a column name or an array
124
+ # of column names). Defaults to primary key.
125
+ # * <tt>:order</tt> - Specifies the cursor column order (can be +:asc+ or +:desc+ or an array consisting
117
126
  # of :asc or :desc). Defaults to +:asc+.
118
127
  #
119
128
  # class Order < ActiveRecord::Base
@@ -140,21 +149,26 @@ module ActiveRecord
140
149
  #
141
150
  # NOTE: Order can be ascending (:asc) or descending (:desc). It is automatically set to
142
151
  # ascending on the primary key ("id ASC").
143
- # This also means that this method only works when the primary key is
152
+ # This also means that this method only works when the cursor column is
144
153
  # orderable (e.g. an integer or string).
145
154
  #
155
+ # NOTE: When using custom columns for batching, they should include at least one unique column
156
+ # (e.g. primary key) as a tiebreaker. Also, to reduce the likelihood of race conditions,
157
+ # all columns should be static (unchangeable after it was set).
158
+ #
146
159
  # NOTE: By its nature, batch processing is subject to race conditions if
147
160
  # other processes are modifying the database.
148
- def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: DEFAULT_ORDER)
161
+ def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, cursor: primary_key, order: DEFAULT_ORDER)
149
162
  relation = self
150
163
  unless block_given?
151
- return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, order: order) do
152
- total = apply_limits(relation, start, finish, build_batch_orders(order)).size
164
+ return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore, cursor: cursor, order: order) do
165
+ cursor = Array(cursor)
166
+ total = apply_limits(relation, cursor, start, finish, build_batch_orders(cursor, order)).size
153
167
  (total - 1).div(batch_size) + 1
154
168
  end
155
169
  end
156
170
 
157
- in_batches(of: batch_size, start: start, finish: finish, load: true, error_on_ignore: error_on_ignore, order: order) do |batch|
171
+ in_batches(of: batch_size, start: start, finish: finish, load: true, error_on_ignore: error_on_ignore, cursor: cursor, order: order) do |batch|
158
172
  yield batch.to_a
159
173
  end
160
174
  end
@@ -183,11 +197,13 @@ module ActiveRecord
183
197
  # ==== Options
184
198
  # * <tt>:of</tt> - Specifies the size of the batch. Defaults to 1000.
185
199
  # * <tt>:load</tt> - Specifies if the relation should be loaded. Defaults to false.
186
- # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
187
- # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
200
+ # * <tt>:start</tt> - Specifies the cursor column value to start from, inclusive of the value.
201
+ # * <tt>:finish</tt> - Specifies the cursor column value to end at, inclusive of the value.
188
202
  # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
189
203
  # an order is present in the relation.
190
- # * <tt>:order</tt> - Specifies the primary key order (can be +:asc+ or +:desc+ or an array consisting
204
+ # * <tt>:cursor</tt> - Specifies the column to use for batching (can be a column name or an array
205
+ # of column names). Defaults to primary key.
206
+ # * <tt>:order</tt> - Specifies the cursor column order (can be +:asc+ or +:desc+ or an array consisting
191
207
  # of :asc or :desc). Defaults to +:asc+.
192
208
  #
193
209
  # class Order < ActiveRecord::Base
@@ -231,22 +247,25 @@ module ActiveRecord
231
247
  #
232
248
  # NOTE: Order can be ascending (:asc) or descending (:desc). It is automatically set to
233
249
  # ascending on the primary key ("id ASC").
234
- # This also means that this method only works when the primary key is
250
+ # This also means that this method only works when the cursor column is
235
251
  # orderable (e.g. an integer or string).
236
252
  #
253
+ # NOTE: When using custom columns for batching, they should include at least one unique column
254
+ # (e.g. primary key) as a tiebreaker. Also, to reduce the likelihood of race conditions,
255
+ # all columns should be static (unchangeable after it was set).
256
+ #
237
257
  # NOTE: By its nature, batch processing is subject to race conditions if
238
258
  # other processes are modifying the database.
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}"
242
- end
259
+ def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil, cursor: primary_key, order: DEFAULT_ORDER, use_ranges: nil, &block)
260
+ cursor = Array(cursor).map(&:to_s)
261
+ ensure_valid_options_for_batching!(cursor, start, finish, order)
243
262
 
244
263
  if arel.orders.present?
245
264
  act_on_ignored_order(error_on_ignore)
246
265
  end
247
266
 
248
267
  unless block
249
- return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self, order: order, use_ranges: use_ranges)
268
+ return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self, cursor: cursor, order: order, use_ranges: use_ranges)
250
269
  end
251
270
 
252
271
  batch_limit = of
@@ -261,6 +280,7 @@ module ActiveRecord
261
280
  relation: self,
262
281
  start: start,
263
282
  finish: finish,
283
+ cursor: cursor,
264
284
  order: order,
265
285
  batch_limit: batch_limit,
266
286
  &block
@@ -271,6 +291,7 @@ module ActiveRecord
271
291
  start: start,
272
292
  finish: finish,
273
293
  load: load,
294
+ cursor: cursor,
274
295
  order: order,
275
296
  use_ranges: use_ranges,
276
297
  remaining: remaining,
@@ -281,28 +302,51 @@ module ActiveRecord
281
302
  end
282
303
 
283
304
  private
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
305
+ def ensure_valid_options_for_batching!(cursor, start, finish, order)
306
+ if start && Array(start).size != cursor.size
307
+ raise ArgumentError, ":start must contain one value per cursor column"
308
+ end
309
+
310
+ if finish && Array(finish).size != cursor.size
311
+ raise ArgumentError, ":finish must contain one value per cursor column"
312
+ end
313
+
314
+ if (Array(primary_key) - cursor).any?
315
+ indexes = model.schema_cache.indexes(table_name)
316
+ unique_index = indexes.find { |index| index.unique && index.where.nil? && (Array(index.columns) - cursor).empty? }
317
+
318
+ unless unique_index
319
+ raise ArgumentError, ":cursor must include a primary key or other unique column(s)"
320
+ end
321
+ end
322
+
323
+ if (Array(order) - [:asc, :desc]).any?
324
+ raise ArgumentError, ":order must be :asc or :desc or an array consisting of :asc or :desc, got #{order.inspect}"
325
+ end
326
+ end
327
+
328
+ def apply_limits(relation, cursor, start, finish, batch_orders)
329
+ relation = apply_start_limit(relation, cursor, start, batch_orders) if start
330
+ relation = apply_finish_limit(relation, cursor, finish, batch_orders) if finish
287
331
  relation
288
332
  end
289
333
 
290
- def apply_start_limit(relation, start, batch_orders)
334
+ def apply_start_limit(relation, cursor, start, batch_orders)
291
335
  operators = batch_orders.map do |_column, order|
292
336
  order == :desc ? :lteq : :gteq
293
337
  end
294
- batch_condition(relation, primary_key, start, operators)
338
+ batch_condition(relation, cursor, start, operators)
295
339
  end
296
340
 
297
- def apply_finish_limit(relation, finish, batch_orders)
341
+ def apply_finish_limit(relation, cursor, finish, batch_orders)
298
342
  operators = batch_orders.map do |_column, order|
299
343
  order == :desc ? :gteq : :lteq
300
344
  end
301
- batch_condition(relation, primary_key, finish, operators)
345
+ batch_condition(relation, cursor, finish, operators)
302
346
  end
303
347
 
304
- def batch_condition(relation, columns, values, operators)
305
- cursor_positions = Array(columns).zip(Array(values), operators)
348
+ def batch_condition(relation, cursor, values, operators)
349
+ cursor_positions = cursor.zip(Array(values), operators)
306
350
 
307
351
  first_clause_column, first_clause_value, operator = cursor_positions.pop
308
352
  where_clause = predicate_builder[first_clause_column, first_clause_value, operator]
@@ -316,9 +360,9 @@ module ActiveRecord
316
360
  relation.where(where_clause)
317
361
  end
318
362
 
319
- def build_batch_orders(order)
320
- get_the_order_of_primary_key(order).map do |column, ord|
321
- [column, ord || DEFAULT_ORDER]
363
+ def build_batch_orders(cursor, order)
364
+ cursor.zip(Array(order)).map do |column, order_|
365
+ [column, order_ || DEFAULT_ORDER]
322
366
  end
323
367
  end
324
368
 
@@ -327,34 +371,28 @@ module ActiveRecord
327
371
 
328
372
  if raise_error
329
373
  raise ArgumentError.new(ORDER_IGNORE_MESSAGE)
330
- elsif logger
331
- logger.warn(ORDER_IGNORE_MESSAGE)
374
+ elsif model.logger
375
+ model.logger.warn(ORDER_IGNORE_MESSAGE)
332
376
  end
333
377
  end
334
378
 
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:)
379
+ def batch_on_loaded_relation(relation:, start:, finish:, cursor:, order:, batch_limit:)
340
380
  records = relation.to_a
381
+ order = build_batch_orders(cursor, order).map(&:second)
341
382
 
342
383
  if start || finish
343
384
  records = records.filter do |record|
344
- id = record.id
385
+ values = record_cursor_values(record, cursor)
345
386
 
346
- if order == :asc
347
- (start.nil? || id >= start) && (finish.nil? || id <= finish)
348
- else
349
- (start.nil? || id <= start) && (finish.nil? || id >= finish)
350
- end
387
+ (start.nil? || compare_values_for_order(values, Array(start), order) >= 0) &&
388
+ (finish.nil? || compare_values_for_order(values, Array(finish), order) <= 0)
351
389
  end
352
390
  end
353
391
 
354
- records.sort_by!(&:id)
355
-
356
- if order == :desc
357
- records.reverse!
392
+ records.sort! do |record1, record2|
393
+ values1 = record_cursor_values(record1, cursor)
394
+ values2 = record_cursor_values(record2, cursor)
395
+ compare_values_for_order(values1, values2, order)
358
396
  end
359
397
 
360
398
  records.each_slice(batch_limit) do |subrecords|
@@ -367,44 +405,65 @@ module ActiveRecord
367
405
  nil
368
406
  end
369
407
 
370
- def batch_on_unloaded_relation(relation:, start:, finish:, load:, order:, use_ranges:, remaining:, batch_limit:)
371
- batch_orders = build_batch_orders(order)
408
+ def record_cursor_values(record, cursor)
409
+ record.attributes.slice(*cursor).values
410
+ end
411
+
412
+ # This is a custom implementation of `<=>` operator,
413
+ # which also takes into account how the collection will be ordered.
414
+ def compare_values_for_order(values1, values2, order)
415
+ values1.each_with_index do |element1, index|
416
+ element2 = values2[index]
417
+ direction = order[index]
418
+ comparison = element1 <=> element2
419
+ comparison = -comparison if direction == :desc
420
+ return comparison if comparison != 0
421
+ end
422
+
423
+ 0
424
+ end
425
+
426
+ def batch_on_unloaded_relation(relation:, start:, finish:, load:, cursor:, order:, use_ranges:, remaining:, batch_limit:)
427
+ batch_orders = build_batch_orders(cursor, order)
372
428
  relation = relation.reorder(batch_orders.to_h).limit(batch_limit)
373
- relation = apply_limits(relation, start, finish, batch_orders)
429
+ relation = apply_limits(relation, cursor, start, finish, batch_orders)
374
430
  relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching
375
431
  batch_relation = relation
376
- empty_scope = to_sql == klass.unscoped.all.to_sql
432
+ empty_scope = to_sql == model.unscoped.all.to_sql
377
433
 
378
434
  loop do
379
435
  if load
380
436
  records = batch_relation.records
381
- ids = records.map(&:id)
382
- yielded_relation = where(primary_key => ids)
437
+ values = records.pluck(*cursor)
438
+ yielded_relation = where(cursor => values)
383
439
  yielded_relation.load_records(records)
384
440
  elsif (empty_scope && use_ranges != false) || use_ranges
385
- ids = batch_relation.ids
386
- finish = ids.last
441
+ values = batch_relation.pluck(*cursor)
442
+
443
+ finish = values.last
387
444
  if finish
388
- yielded_relation = apply_finish_limit(batch_relation, finish, batch_orders)
445
+ yielded_relation = apply_finish_limit(batch_relation, cursor, finish, batch_orders)
389
446
  yielded_relation = yielded_relation.except(:limit, :order)
390
447
  yielded_relation.skip_query_cache!(false)
391
448
  end
392
449
  else
393
- ids = batch_relation.ids
394
- yielded_relation = where(primary_key => ids)
450
+ values = batch_relation.pluck(*cursor)
451
+ yielded_relation = where(cursor => values)
395
452
  end
396
453
 
397
- break if ids.empty?
454
+ break if values.empty?
398
455
 
399
- primary_key_offset = ids.last
400
- raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
456
+ if values.flatten.any?(nil)
457
+ raise ArgumentError, "Not all of the batch cursor columns were included in the custom select clause "\
458
+ "or some columns contain nil."
459
+ end
401
460
 
402
461
  yield yielded_relation
403
462
 
404
- break if ids.length < batch_limit
463
+ break if values.length < batch_limit
405
464
 
406
465
  if limit_value
407
- remaining -= ids.length
466
+ remaining -= values.length
408
467
 
409
468
  if remaining == 0
410
469
  # Saves a useless iteration when the limit is a multiple of the
@@ -422,7 +481,8 @@ module ActiveRecord
422
481
  end
423
482
  operators << (last_order == :desc ? :lt : :gt)
424
483
 
425
- batch_relation = batch_condition(relation, primary_key, primary_key_offset, operators)
484
+ cursor_value = values.last
485
+ batch_relation = batch_condition(relation, cursor, cursor_value, operators)
426
486
  end
427
487
 
428
488
  nil
@@ -60,37 +60,37 @@ module ActiveRecord
60
60
  # Person.distinct.count(:age)
61
61
  # # => counts the number of different age values
62
62
  #
63
- # If +count+ is used with {Relation#group}[rdoc-ref:QueryMethods#group],
63
+ # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group],
64
64
  # it returns a Hash whose keys represent the aggregated column,
65
65
  # and the values are the respective amounts:
66
66
  #
67
67
  # Person.group(:city).count
68
68
  # # => { 'Rome' => 5, 'Paris' => 3 }
69
69
  #
70
- # If +count+ is used with {Relation#group}[rdoc-ref:QueryMethods#group] for multiple columns, it returns a Hash whose
70
+ # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group] for multiple columns, it returns a Hash whose
71
71
  # keys are an array containing the individual values of each column and the value
72
- # of each key would be the count.
72
+ # of each key would be the #count.
73
73
  #
74
74
  # Article.group(:status, :category).count
75
75
  # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, ["published", "technology"]=>2}
76
76
  #
77
- # If +count+ is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns:
77
+ # If #count is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns:
78
78
  #
79
79
  # Person.select(:age).count
80
80
  # # => counts the number of different age values
81
81
  #
82
- # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid +count+ expressions. The specifics differ
82
+ # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ
83
83
  # between databases. In invalid cases, an error from the database is thrown.
84
84
  #
85
- # When given a block, calls the block with each record in the relation and
86
- # returns the number of records for which the block returns a truthy value.
85
+ # When given a block, loads all records in the relation, if the relation
86
+ # hasn't been loaded yet. Calls the block with each record in the relation.
87
+ # Returns the number of records for which the block returns a truthy value.
87
88
  #
88
89
  # Person.count { |person| person.age > 21 }
89
90
  # # => counts the number of people older that 21
90
91
  #
91
- # If the relation hasn't been loaded yet, calling +count+ with a block will
92
- # load all records in the relation. If there are a lot of records in the
93
- # relation, loading all records could result in performance issues.
92
+ # Note: If there are a lot of records in the relation, loading all records
93
+ # could result in performance issues.
94
94
  def count(column_name = nil)
95
95
  if block_given?
96
96
  unless column_name.nil?
@@ -159,15 +159,16 @@ module ActiveRecord
159
159
  #
160
160
  # Person.sum(:age) # => 4562
161
161
  #
162
- # When given a block, calls the block with each record in the relation and
163
- # returns the sum of +initial_value_or_column+ plus the block return values:
162
+ # When given a block, loads all records in the relation, if the relation
163
+ # hasn't been loaded yet. Calls the block with each record in the relation.
164
+ # Returns the sum of +initial_value_or_column+ and the block return
165
+ # values:
164
166
  #
165
167
  # Person.sum { |person| person.age } # => 4562
166
168
  # Person.sum(1000) { |person| person.age } # => 5562
167
169
  #
168
- # If the relation hasn't been loaded yet, calling +sum+ with a block will
169
- # load all records in the relation. If there are a lot of records in the
170
- # relation, loading all records could result in performance issues.
170
+ # Note: If there are a lot of records in the relation, loading all records
171
+ # could result in performance issues.
171
172
  def sum(initial_value_or_column = 0, &block)
172
173
  if block_given?
173
174
  map(&block).sum(initial_value_or_column)
@@ -233,7 +234,7 @@ module ActiveRecord
233
234
  if operation == "count"
234
235
  unless distinct_value || distinct_select?(column_name || select_for_count)
235
236
  relation.distinct!
236
- relation.select_values = Array(klass.primary_key || table[Arel.star])
237
+ relation.select_values = Array(model.primary_key || table[Arel.star])
237
238
  end
238
239
  # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
239
240
  relation.order_values = [] if group_values.empty?
@@ -305,16 +306,16 @@ module ActiveRecord
305
306
  relation = apply_join_dependency
306
307
  relation.pluck(*column_names)
307
308
  else
308
- klass.disallow_raw_sql!(flattened_args(column_names))
309
+ model.disallow_raw_sql!(flattened_args(column_names))
310
+ columns = arel_columns(column_names)
309
311
  relation = spawn
310
- columns = relation.arel_columns(column_names)
311
312
  relation.select_values = columns
312
313
  result = skip_query_cache_if_necessary do
313
314
  if where_clause.contradiction?
314
315
  ActiveRecord::Result.empty(async: @async)
315
316
  else
316
- klass.with_connection do |c|
317
- c.select_all(relation.arel, "#{klass.name} Pluck", async: @async)
317
+ model.with_connection do |c|
318
+ c.select_all(relation.arel, "#{model.name} Pluck", async: @async)
318
319
  end
319
320
  end
320
321
  end
@@ -390,8 +391,8 @@ module ActiveRecord
390
391
  ActiveRecord::Result.empty
391
392
  else
392
393
  skip_query_cache_if_necessary do
393
- klass.with_connection do |c|
394
- c.select_all(relation, "#{klass.name} Ids", async: @async)
394
+ model.with_connection do |c|
395
+ c.select_all(relation, "#{model.name} Ids", async: @async)
395
396
  end
396
397
  end
397
398
  end
@@ -405,18 +406,9 @@ module ActiveRecord
405
406
  async.ids
406
407
  end
407
408
 
408
- protected
409
- def aggregate_column(column_name)
410
- return column_name if Arel::Expressions === column_name
411
-
412
- arel_column(column_name.to_s) do |name|
413
- column_name == :all ? Arel.sql("*", retryable: true) : Arel.sql(name)
414
- end
415
- end
416
-
417
409
  private
418
410
  def all_attributes?(column_names)
419
- (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty?
411
+ (column_names.map(&:to_s) - model.attribute_names - model.attribute_aliases.keys).empty?
420
412
  end
421
413
 
422
414
  def has_include?(column_name)
@@ -454,6 +446,14 @@ module ActiveRecord
454
446
  column_name.is_a?(::String) && /\bDISTINCT[\s(]/i.match?(column_name)
455
447
  end
456
448
 
449
+ def aggregate_column(column_name)
450
+ return column_name if Arel::Expressions === column_name
451
+
452
+ arel_column(column_name.to_s) do |name|
453
+ column_name == :all ? Arel.sql("*", retryable: true) : Arel.sql(name)
454
+ end
455
+ end
456
+
457
457
  def operation_over_aggregate_column(column, operation, distinct)
458
458
  operation == "count" ? column.count(distinct) : column.public_send(operation)
459
459
  end
@@ -469,7 +469,7 @@ module ActiveRecord
469
469
  # PostgreSQL doesn't like ORDER BY when there are no GROUP BY
470
470
  relation = unscope(:order).distinct!(false)
471
471
 
472
- column = relation.aggregate_column(column_name)
472
+ column = aggregate_column(column_name)
473
473
  select_value = operation_over_aggregate_column(column, operation, distinct)
474
474
  select_value.distinct = true if operation == "sum" && distinct
475
475
 
@@ -479,15 +479,11 @@ module ActiveRecord
479
479
  end
480
480
 
481
481
  query_result = if relation.where_clause.contradiction?
482
- if @async
483
- FutureResult.wrap(ActiveRecord::Result.empty)
484
- else
485
- ActiveRecord::Result.empty
486
- end
482
+ ActiveRecord::Result.empty
487
483
  else
488
484
  skip_query_cache_if_necessary do
489
- @klass.with_connection do |c|
490
- c.select_all(query_builder, "#{@klass.name} #{operation.capitalize}", async: @async)
485
+ model.with_connection do |c|
486
+ c.select_all(query_builder, "#{model.name} #{operation.capitalize}", async: @async)
491
487
  end
492
488
  end
493
489
  end
@@ -508,15 +504,13 @@ module ActiveRecord
508
504
  group_fields = group_fields.uniq if group_fields.size > 1
509
505
 
510
506
  if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym)
511
- association = klass._reflect_on_association(group_fields.first)
507
+ association = model._reflect_on_association(group_fields.first)
512
508
  associated = association && association.belongs_to? # only count belongs_to associations
513
509
  group_fields = Array(association.foreign_key) if associated
514
510
  end
511
+ group_fields = arel_columns(group_fields)
515
512
 
516
- relation = except(:group).distinct!(false)
517
- group_fields = relation.arel_columns(group_fields)
518
-
519
- @klass.with_connection do |connection|
513
+ model.with_connection do |connection|
520
514
  column_alias_tracker = ColumnAliasTracker.new(connection)
521
515
 
522
516
  group_aliases = group_fields.map { |field|
@@ -525,16 +519,16 @@ module ActiveRecord
525
519
  }
526
520
  group_columns = group_aliases.zip(group_fields)
527
521
 
528
- column = relation.aggregate_column(column_name)
522
+ column = aggregate_column(column_name)
529
523
  column_alias = column_alias_tracker.alias_for("#{operation} #{column_name.to_s.downcase}")
530
524
  select_value = operation_over_aggregate_column(column, operation, distinct)
531
- select_value.as(adapter_class.quote_column_name(column_alias))
525
+ select_value.as(model.adapter_class.quote_column_name(column_alias))
532
526
 
533
527
  select_values = [select_value]
534
528
  select_values += self.select_values unless having_clause.empty?
535
529
 
536
530
  select_values.concat group_columns.map { |aliaz, field|
537
- aliaz = adapter_class.quote_column_name(aliaz)
531
+ aliaz = model.adapter_class.quote_column_name(aliaz)
538
532
  if field.respond_to?(:as)
539
533
  field.as(aliaz)
540
534
  else
@@ -542,11 +536,12 @@ module ActiveRecord
542
536
  end
543
537
  }
544
538
 
539
+ relation = except(:group).distinct!(false)
545
540
  relation.group_values = group_fields
546
541
  relation.select_values = select_values
547
542
 
548
543
  result = skip_query_cache_if_necessary do
549
- connection.select_all(relation.arel, "#{@klass.name} #{operation.capitalize}", async: @async)
544
+ connection.select_all(relation.arel, "#{model.name} #{operation.capitalize}", async: @async)
550
545
  end
551
546
 
552
547
  result.then do |calculated_data|
@@ -588,7 +583,7 @@ module ActiveRecord
588
583
 
589
584
  def type_for(field, &block)
590
585
  field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
591
- @klass.type_for_attribute(field_name, &block)
586
+ model.type_for_attribute(field_name, &block)
592
587
  end
593
588
 
594
589
  def lookup_cast_type_from_join_dependencies(name, join_dependencies = build_join_dependencies)
@@ -601,12 +596,12 @@ module ActiveRecord
601
596
 
602
597
  def type_cast_pluck_values(result, columns)
603
598
  cast_types = if result.columns.size != columns.size
604
- klass.attribute_types
599
+ model.attribute_types
605
600
  else
606
601
  join_dependencies = nil
607
602
  columns.map.with_index do |column, i|
608
603
  column.try(:type_caster) ||
609
- klass.attribute_types.fetch(name = result.columns[i]) do
604
+ model.attribute_types.fetch(name = result.columns[i]) do
610
605
  join_dependencies ||= build_join_dependencies
611
606
  lookup_cast_type_from_join_dependencies(name, join_dependencies) ||
612
607
  result.column_types[i] || Type.default_value
@@ -638,9 +633,14 @@ module ActiveRecord
638
633
  if select_values.present?
639
634
  return select_values.first if select_values.one?
640
635
 
636
+ adapter_class = model.adapter_class
641
637
  select_values.map do |field|
642
- column = arel_column(field.to_s) do |attr_name|
643
- Arel.sql(attr_name)
638
+ column = if Arel.arel_node?(field)
639
+ field
640
+ else
641
+ arel_column(field.to_s) do |attr_name|
642
+ Arel.sql(attr_name)
643
+ end
644
644
  end
645
645
 
646
646
  if column.is_a?(Arel::Nodes::SqlLiteral)
@@ -667,7 +667,7 @@ module ActiveRecord
667
667
  relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct
668
668
  else
669
669
  column_alias = Arel.sql("count_column")
670
- relation.select_values = [ relation.aggregate_column(column_name).as(column_alias) ]
670
+ relation.select_values = [ aggregate_column(column_name).as(column_alias) ]
671
671
  end
672
672
 
673
673
  subquery_alias = Arel.sql("subquery_for_count", retryable: true)