activerecord 7.2.1.1 → 8.0.0.rc1

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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +220 -756
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/associations/association.rb +25 -5
  5. data/lib/active_record/associations/builder/association.rb +7 -6
  6. data/lib/active_record/associations/collection_association.rb +10 -8
  7. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  8. data/lib/active_record/associations/has_many_through_association.rb +10 -3
  9. data/lib/active_record/associations/join_dependency/join_association.rb +3 -2
  10. data/lib/active_record/associations/join_dependency.rb +4 -4
  11. data/lib/active_record/associations/preloader/association.rb +2 -2
  12. data/lib/active_record/associations/singular_association.rb +8 -3
  13. data/lib/active_record/associations.rb +34 -4
  14. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  15. data/lib/active_record/attribute_assignment.rb +9 -1
  16. data/lib/active_record/attribute_methods/primary_key.rb +2 -7
  17. data/lib/active_record/attribute_methods/time_zone_conversion.rb +6 -12
  18. data/lib/active_record/attributes.rb +1 -2
  19. data/lib/active_record/autosave_association.rb +69 -27
  20. data/lib/active_record/callbacks.rb +1 -1
  21. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
  22. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  23. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  24. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +26 -9
  25. data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
  26. data/lib/active_record/connection_adapters/abstract/query_cache.rb +12 -4
  27. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  28. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -5
  29. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +7 -2
  30. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +34 -7
  31. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  32. data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -26
  33. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +28 -42
  34. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  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 +1 -8
  39. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +64 -42
  40. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  41. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +1 -1
  42. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  43. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  44. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +0 -11
  45. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +1 -11
  46. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
  47. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +54 -14
  48. data/lib/active_record/connection_adapters/postgresql_adapter.rb +45 -97
  49. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  50. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
  51. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  52. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
  53. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -1
  54. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -12
  55. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  56. data/lib/active_record/connection_adapters/trilogy_adapter.rb +0 -17
  57. data/lib/active_record/connection_adapters.rb +0 -56
  58. data/lib/active_record/connection_handling.rb +22 -0
  59. data/lib/active_record/core.rb +28 -18
  60. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -1
  61. data/lib/active_record/encryption/config.rb +3 -1
  62. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  63. data/lib/active_record/encryption/encrypted_attribute_type.rb +10 -1
  64. data/lib/active_record/encryption/encryptor.rb +15 -8
  65. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  66. data/lib/active_record/encryption/key_provider.rb +1 -1
  67. data/lib/active_record/encryption/scheme.rb +8 -1
  68. data/lib/active_record/encryption.rb +2 -0
  69. data/lib/active_record/enum.rb +54 -75
  70. data/lib/active_record/errors.rb +13 -5
  71. data/lib/active_record/fixtures.rb +0 -2
  72. data/lib/active_record/future_result.rb +14 -10
  73. data/lib/active_record/gem_version.rb +4 -4
  74. data/lib/active_record/insert_all.rb +1 -1
  75. data/lib/active_record/locking/optimistic.rb +1 -1
  76. data/lib/active_record/log_subscriber.rb +5 -11
  77. data/lib/active_record/marshalling.rb +4 -1
  78. data/lib/active_record/migration/command_recorder.rb +22 -5
  79. data/lib/active_record/migration/compatibility.rb +5 -2
  80. data/lib/active_record/migration.rb +35 -38
  81. data/lib/active_record/model_schema.rb +4 -6
  82. data/lib/active_record/nested_attributes.rb +11 -2
  83. data/lib/active_record/persistence.rb +128 -130
  84. data/lib/active_record/query_cache.rb +0 -4
  85. data/lib/active_record/query_logs.rb +102 -50
  86. data/lib/active_record/query_logs_formatter.rb +17 -28
  87. data/lib/active_record/querying.rb +8 -8
  88. data/lib/active_record/railtie.rb +9 -38
  89. data/lib/active_record/railties/databases.rake +1 -1
  90. data/lib/active_record/reflection.rb +23 -23
  91. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  92. data/lib/active_record/relation/batches.rb +132 -72
  93. data/lib/active_record/relation/calculations.rb +41 -40
  94. data/lib/active_record/relation/delegation.rb +25 -14
  95. data/lib/active_record/relation/finder_methods.rb +18 -18
  96. data/lib/active_record/relation/merger.rb +8 -8
  97. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  98. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  99. data/lib/active_record/relation/predicate_builder.rb +14 -1
  100. data/lib/active_record/relation/query_methods.rb +122 -71
  101. data/lib/active_record/relation/spawn_methods.rb +1 -1
  102. data/lib/active_record/relation.rb +79 -61
  103. data/lib/active_record/result.rb +66 -4
  104. data/lib/active_record/sanitization.rb +7 -6
  105. data/lib/active_record/schema_dumper.rb +5 -0
  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/statement_cache.rb +12 -12
  109. data/lib/active_record/store.rb +7 -3
  110. data/lib/active_record/table_metadata.rb +1 -3
  111. data/lib/active_record/tasks/database_tasks.rb +40 -47
  112. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  113. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  114. data/lib/active_record/test_fixtures.rb +12 -0
  115. data/lib/active_record/testing/query_assertions.rb +2 -2
  116. data/lib/active_record/token_for.rb +1 -1
  117. data/lib/active_record/validations/uniqueness.rb +9 -8
  118. data/lib/active_record.rb +15 -45
  119. data/lib/arel/collectors/bind.rb +1 -1
  120. data/lib/arel/table.rb +3 -7
  121. data/lib/arel/visitors/sqlite.rb +25 -0
  122. metadata +10 -11
  123. data/lib/active_record/relation/record_fetch_warning.rb +0 -52
@@ -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
@@ -234,7 +234,7 @@ module ActiveRecord
234
234
  if operation == "count"
235
235
  unless distinct_value || distinct_select?(column_name || select_for_count)
236
236
  relation.distinct!
237
- relation.select_values = Array(klass.primary_key || table[Arel.star])
237
+ relation.select_values = Array(model.primary_key || table[Arel.star])
238
238
  end
239
239
  # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
240
240
  relation.order_values = [] if group_values.empty?
@@ -275,10 +275,14 @@ module ActiveRecord
275
275
  # # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5
276
276
  # # => [2, 3]
277
277
  #
278
- # Comment.joins(:person).pluck(:id, person: [:id])
279
- # # SELECT comments.id, people.id FROM comments INNER JOIN people on comments.person_id = people.id
278
+ # Comment.joins(:person).pluck(:id, person: :id)
279
+ # # SELECT comments.id, person.id FROM comments INNER JOIN people person ON person.id = comments.person_id
280
280
  # # => [[1, 2], [2, 2]]
281
281
  #
282
+ # Comment.joins(:person).pluck(:id, person: [:id, :name])
283
+ # # SELECT comments.id, person.id, person.name FROM comments INNER JOIN people person ON person.id = comments.person_id
284
+ # # => [[1, 2, 'David'], [2, 2, 'David']]
285
+ #
282
286
  # Person.pluck(Arel.sql('DATEDIFF(updated_at, created_at)'))
283
287
  # # SELECT DATEDIFF(updated_at, created_at) FROM people
284
288
  # # => ['0', '27761', '173']
@@ -306,16 +310,16 @@ module ActiveRecord
306
310
  relation = apply_join_dependency
307
311
  relation.pluck(*column_names)
308
312
  else
309
- klass.disallow_raw_sql!(flattened_args(column_names))
310
- columns = arel_columns(column_names)
313
+ model.disallow_raw_sql!(flattened_args(column_names))
311
314
  relation = spawn
315
+ columns = relation.arel_columns(column_names)
312
316
  relation.select_values = columns
313
317
  result = skip_query_cache_if_necessary do
314
318
  if where_clause.contradiction?
315
319
  ActiveRecord::Result.empty(async: @async)
316
320
  else
317
- klass.with_connection do |c|
318
- c.select_all(relation.arel, "#{klass.name} Pluck", async: @async)
321
+ model.with_connection do |c|
322
+ c.select_all(relation.arel, "#{model.name} Pluck", async: @async)
319
323
  end
320
324
  end
321
325
  end
@@ -391,8 +395,8 @@ module ActiveRecord
391
395
  ActiveRecord::Result.empty
392
396
  else
393
397
  skip_query_cache_if_necessary do
394
- klass.with_connection do |c|
395
- c.select_all(relation, "#{klass.name} Ids", async: @async)
398
+ model.with_connection do |c|
399
+ c.select_all(relation, "#{model.name} Ids", async: @async)
396
400
  end
397
401
  end
398
402
  end
@@ -408,7 +412,7 @@ module ActiveRecord
408
412
 
409
413
  private
410
414
  def all_attributes?(column_names)
411
- (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty?
415
+ (column_names.map(&:to_s) - model.attribute_names - model.attribute_aliases.keys).empty?
412
416
  end
413
417
 
414
418
  def has_include?(column_name)
@@ -447,10 +451,13 @@ module ActiveRecord
447
451
  end
448
452
 
449
453
  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
+ case column_name
455
+ when Arel::Expressions
456
+ column_name
457
+ when :all
458
+ Arel.star
459
+ else
460
+ arel_column(column_name)
454
461
  end
455
462
  end
456
463
 
@@ -482,8 +489,8 @@ module ActiveRecord
482
489
  ActiveRecord::Result.empty
483
490
  else
484
491
  skip_query_cache_if_necessary do
485
- @klass.with_connection do |c|
486
- c.select_all(query_builder, "#{@klass.name} #{operation.capitalize}", async: @async)
492
+ model.with_connection do |c|
493
+ c.select_all(query_builder, "#{model.name} #{operation.capitalize}", async: @async)
487
494
  end
488
495
  end
489
496
  end
@@ -504,13 +511,13 @@ module ActiveRecord
504
511
  group_fields = group_fields.uniq if group_fields.size > 1
505
512
 
506
513
  if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym)
507
- association = klass._reflect_on_association(group_fields.first)
514
+ association = model._reflect_on_association(group_fields.first)
508
515
  associated = association && association.belongs_to? # only count belongs_to associations
509
516
  group_fields = Array(association.foreign_key) if associated
510
517
  end
511
518
  group_fields = arel_columns(group_fields)
512
519
 
513
- @klass.with_connection do |connection|
520
+ model.with_connection do |connection|
514
521
  column_alias_tracker = ColumnAliasTracker.new(connection)
515
522
 
516
523
  group_aliases = group_fields.map { |field|
@@ -522,13 +529,13 @@ module ActiveRecord
522
529
  column = aggregate_column(column_name)
523
530
  column_alias = column_alias_tracker.alias_for("#{operation} #{column_name.to_s.downcase}")
524
531
  select_value = operation_over_aggregate_column(column, operation, distinct)
525
- select_value.as(adapter_class.quote_column_name(column_alias))
532
+ select_value.as(model.adapter_class.quote_column_name(column_alias))
526
533
 
527
534
  select_values = [select_value]
528
535
  select_values += self.select_values unless having_clause.empty?
529
536
 
530
537
  select_values.concat group_columns.map { |aliaz, field|
531
- aliaz = adapter_class.quote_column_name(aliaz)
538
+ aliaz = model.adapter_class.quote_column_name(aliaz)
532
539
  if field.respond_to?(:as)
533
540
  field.as(aliaz)
534
541
  else
@@ -541,7 +548,7 @@ module ActiveRecord
541
548
  relation.select_values = select_values
542
549
 
543
550
  result = skip_query_cache_if_necessary do
544
- connection.select_all(relation.arel, "#{@klass.name} #{operation.capitalize}", async: @async)
551
+ connection.select_all(relation.arel, "#{model.name} #{operation.capitalize}", async: @async)
545
552
  end
546
553
 
547
554
  result.then do |calculated_data|
@@ -583,7 +590,7 @@ module ActiveRecord
583
590
 
584
591
  def type_for(field, &block)
585
592
  field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
586
- @klass.type_for_attribute(field_name, &block)
593
+ model.type_for_attribute(field_name, &block)
587
594
  end
588
595
 
589
596
  def lookup_cast_type_from_join_dependencies(name, join_dependencies = build_join_dependencies)
@@ -596,12 +603,12 @@ module ActiveRecord
596
603
 
597
604
  def type_cast_pluck_values(result, columns)
598
605
  cast_types = if result.columns.size != columns.size
599
- klass.attribute_types
606
+ model.attribute_types
600
607
  else
601
608
  join_dependencies = nil
602
609
  columns.map.with_index do |column, i|
603
610
  column.try(:type_caster) ||
604
- klass.attribute_types.fetch(name = result.columns[i]) do
611
+ model.attribute_types.fetch(name = result.columns[i]) do
605
612
  join_dependencies ||= build_join_dependencies
606
613
  lookup_cast_type_from_join_dependencies(name, join_dependencies) ||
607
614
  result.column_types[i] || Type.default_value
@@ -630,22 +637,12 @@ module ActiveRecord
630
637
  end
631
638
 
632
639
  def select_for_count
633
- if select_values.present?
634
- return select_values.first if select_values.one?
635
-
636
- select_values.map do |field|
637
- column = arel_column(field.to_s) do |attr_name|
638
- Arel.sql(attr_name)
639
- end
640
-
641
- if column.is_a?(Arel::Nodes::SqlLiteral)
642
- column
643
- else
644
- "#{adapter_class.quote_table_name(column.relation.name)}.#{adapter_class.quote_column_name(column.name)}"
645
- end
646
- end.join(", ")
647
- else
640
+ if select_values.empty?
648
641
  :all
642
+ else
643
+ with_connection do |conn|
644
+ arel_columns(select_values).map { |column| conn.visitor.compile(column) }.join(", ")
645
+ end
649
646
  end
650
647
  end
651
648
 
@@ -668,7 +665,11 @@ module ActiveRecord
668
665
  subquery_alias = Arel.sql("subquery_for_count", retryable: true)
669
666
  select_value = operation_over_aggregate_column(column_alias, "count", false)
670
667
 
671
- relation.build_subquery(subquery_alias, select_value)
668
+ if column_name == :all
669
+ relation.unscope(:order).build_subquery(subquery_alias, select_value)
670
+ else
671
+ relation.build_subquery(subquery_alias, select_value)
672
+ end
672
673
  end
673
674
  end
674
675
  end