activerecord 7.2.0 → 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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +189 -745
  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 +3 -2
  9. data/lib/active_record/associations/join_dependency/join_association.rb +3 -2
  10. data/lib/active_record/associations/join_dependency.rb +5 -5
  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/time_zone_conversion.rb +4 -0
  17. data/lib/active_record/attributes.rb +6 -5
  18. data/lib/active_record/autosave_association.rb +69 -27
  19. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
  20. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  21. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  22. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +23 -44
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
  24. data/lib/active_record/connection_adapters/abstract/query_cache.rb +53 -18
  25. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  26. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  27. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +26 -5
  28. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  29. data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -25
  30. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +20 -38
  31. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  32. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  33. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +44 -46
  34. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +42 -98
  35. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -8
  36. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +64 -42
  37. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +1 -1
  38. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  39. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +0 -1
  40. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +50 -6
  41. data/lib/active_record/connection_adapters/postgresql_adapter.rb +38 -90
  42. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  43. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
  44. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  45. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
  46. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -1
  47. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -12
  48. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  49. data/lib/active_record/connection_adapters/trilogy_adapter.rb +0 -17
  50. data/lib/active_record/connection_handling.rb +22 -0
  51. data/lib/active_record/core.rb +16 -9
  52. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -1
  53. data/lib/active_record/encryption/config.rb +3 -1
  54. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  55. data/lib/active_record/encryption/encrypted_attribute_type.rb +12 -3
  56. data/lib/active_record/encryption/encryptor.rb +16 -9
  57. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  58. data/lib/active_record/encryption/key_provider.rb +1 -1
  59. data/lib/active_record/encryption/scheme.rb +8 -1
  60. data/lib/active_record/encryption.rb +2 -0
  61. data/lib/active_record/enum.rb +8 -0
  62. data/lib/active_record/errors.rb +13 -5
  63. data/lib/active_record/fixtures.rb +0 -1
  64. data/lib/active_record/future_result.rb +14 -10
  65. data/lib/active_record/gem_version.rb +3 -3
  66. data/lib/active_record/insert_all.rb +1 -1
  67. data/lib/active_record/migration/command_recorder.rb +22 -5
  68. data/lib/active_record/migration/compatibility.rb +5 -2
  69. data/lib/active_record/migration.rb +35 -33
  70. data/lib/active_record/model_schema.rb +6 -3
  71. data/lib/active_record/nested_attributes.rb +11 -2
  72. data/lib/active_record/persistence.rb +128 -130
  73. data/lib/active_record/query_logs.rb +97 -39
  74. data/lib/active_record/query_logs_formatter.rb +17 -28
  75. data/lib/active_record/querying.rb +6 -6
  76. data/lib/active_record/railtie.rb +8 -14
  77. data/lib/active_record/reflection.rb +19 -10
  78. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  79. data/lib/active_record/relation/batches.rb +135 -75
  80. data/lib/active_record/relation/calculations.rb +24 -19
  81. data/lib/active_record/relation/delegation.rb +25 -14
  82. data/lib/active_record/relation/finder_methods.rb +18 -18
  83. data/lib/active_record/relation/merger.rb +8 -8
  84. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  85. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  86. data/lib/active_record/relation/predicate_builder.rb +6 -1
  87. data/lib/active_record/relation/query_methods.rb +58 -37
  88. data/lib/active_record/relation/record_fetch_warning.rb +2 -2
  89. data/lib/active_record/relation/spawn_methods.rb +1 -1
  90. data/lib/active_record/relation.rb +72 -61
  91. data/lib/active_record/result.rb +68 -7
  92. data/lib/active_record/sanitization.rb +7 -6
  93. data/lib/active_record/schema_dumper.rb +5 -0
  94. data/lib/active_record/schema_migration.rb +2 -1
  95. data/lib/active_record/scoping/named.rb +6 -2
  96. data/lib/active_record/statement_cache.rb +12 -12
  97. data/lib/active_record/store.rb +7 -3
  98. data/lib/active_record/tasks/database_tasks.rb +36 -16
  99. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  100. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  101. data/lib/active_record/test_fixtures.rb +12 -0
  102. data/lib/active_record/token_for.rb +1 -1
  103. data/lib/active_record/validations/uniqueness.rb +9 -8
  104. data/lib/active_record.rb +15 -0
  105. data/lib/arel/collectors/bind.rb +1 -1
  106. metadata +14 -14
@@ -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,24 +247,27 @@ 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
243
-
244
- unless block
245
- return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self, order: order, use_ranges: use_ranges)
246
- 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)
247
262
 
248
263
  if arel.orders.present?
249
264
  act_on_ignored_order(error_on_ignore)
250
265
  end
251
266
 
267
+ unless block
268
+ return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self, cursor: cursor, order: order, use_ranges: use_ranges)
269
+ end
270
+
252
271
  batch_limit = of
253
272
 
254
273
  if limit_value
@@ -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?
@@ -306,7 +306,7 @@ module ActiveRecord
306
306
  relation = apply_join_dependency
307
307
  relation.pluck(*column_names)
308
308
  else
309
- klass.disallow_raw_sql!(flattened_args(column_names))
309
+ model.disallow_raw_sql!(flattened_args(column_names))
310
310
  columns = arel_columns(column_names)
311
311
  relation = spawn
312
312
  relation.select_values = columns
@@ -314,8 +314,8 @@ module ActiveRecord
314
314
  if where_clause.contradiction?
315
315
  ActiveRecord::Result.empty(async: @async)
316
316
  else
317
- klass.with_connection do |c|
318
- 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)
319
319
  end
320
320
  end
321
321
  end
@@ -391,8 +391,8 @@ module ActiveRecord
391
391
  ActiveRecord::Result.empty
392
392
  else
393
393
  skip_query_cache_if_necessary do
394
- klass.with_connection do |c|
395
- 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)
396
396
  end
397
397
  end
398
398
  end
@@ -408,7 +408,7 @@ module ActiveRecord
408
408
 
409
409
  private
410
410
  def all_attributes?(column_names)
411
- (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?
412
412
  end
413
413
 
414
414
  def has_include?(column_name)
@@ -482,8 +482,8 @@ module ActiveRecord
482
482
  ActiveRecord::Result.empty
483
483
  else
484
484
  skip_query_cache_if_necessary do
485
- @klass.with_connection do |c|
486
- 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)
487
487
  end
488
488
  end
489
489
  end
@@ -504,13 +504,13 @@ module ActiveRecord
504
504
  group_fields = group_fields.uniq if group_fields.size > 1
505
505
 
506
506
  if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym)
507
- association = klass._reflect_on_association(group_fields.first)
507
+ association = model._reflect_on_association(group_fields.first)
508
508
  associated = association && association.belongs_to? # only count belongs_to associations
509
509
  group_fields = Array(association.foreign_key) if associated
510
510
  end
511
511
  group_fields = arel_columns(group_fields)
512
512
 
513
- @klass.with_connection do |connection|
513
+ model.with_connection do |connection|
514
514
  column_alias_tracker = ColumnAliasTracker.new(connection)
515
515
 
516
516
  group_aliases = group_fields.map { |field|
@@ -522,13 +522,13 @@ module ActiveRecord
522
522
  column = aggregate_column(column_name)
523
523
  column_alias = column_alias_tracker.alias_for("#{operation} #{column_name.to_s.downcase}")
524
524
  select_value = operation_over_aggregate_column(column, operation, distinct)
525
- select_value.as(adapter_class.quote_column_name(column_alias))
525
+ select_value.as(model.adapter_class.quote_column_name(column_alias))
526
526
 
527
527
  select_values = [select_value]
528
528
  select_values += self.select_values unless having_clause.empty?
529
529
 
530
530
  select_values.concat group_columns.map { |aliaz, field|
531
- aliaz = adapter_class.quote_column_name(aliaz)
531
+ aliaz = model.adapter_class.quote_column_name(aliaz)
532
532
  if field.respond_to?(:as)
533
533
  field.as(aliaz)
534
534
  else
@@ -541,7 +541,7 @@ module ActiveRecord
541
541
  relation.select_values = select_values
542
542
 
543
543
  result = skip_query_cache_if_necessary do
544
- connection.select_all(relation.arel, "#{@klass.name} #{operation.capitalize}", async: @async)
544
+ connection.select_all(relation.arel, "#{model.name} #{operation.capitalize}", async: @async)
545
545
  end
546
546
 
547
547
  result.then do |calculated_data|
@@ -583,7 +583,7 @@ module ActiveRecord
583
583
 
584
584
  def type_for(field, &block)
585
585
  field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
586
- @klass.type_for_attribute(field_name, &block)
586
+ model.type_for_attribute(field_name, &block)
587
587
  end
588
588
 
589
589
  def lookup_cast_type_from_join_dependencies(name, join_dependencies = build_join_dependencies)
@@ -596,12 +596,12 @@ module ActiveRecord
596
596
 
597
597
  def type_cast_pluck_values(result, columns)
598
598
  cast_types = if result.columns.size != columns.size
599
- klass.attribute_types
599
+ model.attribute_types
600
600
  else
601
601
  join_dependencies = nil
602
602
  columns.map.with_index do |column, i|
603
603
  column.try(:type_caster) ||
604
- klass.attribute_types.fetch(name = result.columns[i]) do
604
+ model.attribute_types.fetch(name = result.columns[i]) do
605
605
  join_dependencies ||= build_join_dependencies
606
606
  lookup_cast_type_from_join_dependencies(name, join_dependencies) ||
607
607
  result.column_types[i] || Type.default_value
@@ -633,9 +633,14 @@ module ActiveRecord
633
633
  if select_values.present?
634
634
  return select_values.first if select_values.one?
635
635
 
636
+ adapter_class = model.adapter_class
636
637
  select_values.map do |field|
637
- column = arel_column(field.to_s) do |attr_name|
638
- 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
639
644
  end
640
645
 
641
646
  if column.is_a?(Arel::Nodes::SqlLiteral)
@@ -22,6 +22,9 @@ module ActiveRecord
22
22
  end
23
23
 
24
24
  module DelegateCache # :nodoc:
25
+ @delegate_base_methods = true
26
+ singleton_class.attr_accessor :delegate_base_methods
27
+
25
28
  def relation_delegate_class(klass)
26
29
  @relation_delegate_cache[klass]
27
30
  end
@@ -75,12 +78,12 @@ module ActiveRecord
75
78
  if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method) && !::ActiveSupport::Delegation::RESERVED_METHOD_NAMES.include?(method.to_s)
76
79
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
77
80
  def #{method}(...)
78
- scoping { klass.#{method}(...) }
81
+ scoping { model.#{method}(...) }
79
82
  end
80
83
  RUBY
81
84
  else
82
85
  define_method(method) do |*args, **kwargs, &block|
83
- scoping { klass.public_send(method, *args, **kwargs, &block) }
86
+ scoping { model.public_send(method, *args, **kwargs, &block) }
84
87
  end
85
88
  end
86
89
  end
@@ -92,15 +95,15 @@ module ActiveRecord
92
95
 
93
96
  # This module creates compiled delegation methods dynamically at runtime, which makes
94
97
  # subsequent calls to that method faster by avoiding method_missing. The delegations
95
- # may vary depending on the klass of a relation, so we create a subclass of Relation
96
- # for each different klass, and the delegations are compiled into that subclass only.
98
+ # may vary depending on the model of a relation, so we create a subclass of Relation
99
+ # for each different model, and the delegations are compiled into that subclass only.
97
100
 
98
101
  delegate :to_xml, :encode_with, :length, :each, :join, :intersect?,
99
102
  :[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of,
100
103
  :to_sentence, :to_fs, :to_formatted_s, :as_json,
101
104
  :shuffle, :split, :slice, :index, :rindex, to: :records
102
105
 
103
- delegate :primary_key, :lease_connection, :connection, :with_connection, :transaction, to: :klass
106
+ delegate :primary_key, :with_connection, :connection, :table_name, :transaction, :sanitize_sql_like, :unscoped, :name, to: :model
104
107
 
105
108
  module ClassSpecificRelation # :nodoc:
106
109
  extend ActiveSupport::Concern
@@ -113,11 +116,19 @@ module ActiveRecord
113
116
 
114
117
  private
115
118
  def method_missing(method, ...)
116
- if @klass.respond_to?(method)
117
- unless Delegation.uncacheable_methods.include?(method)
118
- @klass.generate_relation_method(method)
119
+ if model.respond_to?(method)
120
+ if !DelegateCache.delegate_base_methods && Base.respond_to?(method)
121
+ # A common mistake in Active Record's own code is to call `ActiveRecord::Base`
122
+ # class methods on Association. It works because it's automatically delegated, but
123
+ # can introduce subtle bugs because it sets the global scope.
124
+ # We can't deprecate this behavior because gems might depend on it, however we
125
+ # can ban it from Active Record's own test suite to avoid regressions.
126
+ raise NotImplementedError, "Active Record code shouldn't rely on association delegation into ActiveRecord::Base methods"
127
+ elsif !Delegation.uncacheable_methods.include?(method)
128
+ model.generate_relation_method(method)
119
129
  end
120
- scoping { @klass.public_send(method, ...) }
130
+
131
+ scoping { model.public_send(method, ...) }
121
132
  else
122
133
  super
123
134
  end
@@ -125,19 +136,19 @@ module ActiveRecord
125
136
  end
126
137
 
127
138
  module ClassMethods # :nodoc:
128
- def create(klass, *args, **kwargs)
129
- relation_class_for(klass).new(klass, *args, **kwargs)
139
+ def create(model, ...)
140
+ relation_class_for(model).new(model, ...)
130
141
  end
131
142
 
132
143
  private
133
- def relation_class_for(klass)
134
- klass.relation_delegate_class(self)
144
+ def relation_class_for(model)
145
+ model.relation_delegate_class(self)
135
146
  end
136
147
  end
137
148
 
138
149
  private
139
150
  def respond_to_missing?(method, _)
140
- super || @klass.respond_to?(method)
151
+ super || model.respond_to?(method)
141
152
  end
142
153
  end
143
154
  end