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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +192 -1261
- data/README.rdoc +2 -2
- data/lib/active_record/associations/alias_tracker.rb +4 -6
- data/lib/active_record/associations/association.rb +25 -5
- data/lib/active_record/associations/belongs_to_association.rb +2 -18
- data/lib/active_record/associations/builder/association.rb +7 -6
- data/lib/active_record/associations/collection_association.rb +4 -4
- data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
- data/lib/active_record/associations/has_many_through_association.rb +4 -9
- data/lib/active_record/associations/join_dependency/join_association.rb +27 -25
- data/lib/active_record/associations/preloader/association.rb +2 -2
- data/lib/active_record/associations/singular_association.rb +8 -3
- data/lib/active_record/associations.rb +50 -32
- data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
- data/lib/active_record/attribute_methods/serialization.rb +1 -1
- data/lib/active_record/attribute_methods.rb +19 -24
- data/lib/active_record/attributes.rb +26 -37
- data/lib/active_record/autosave_association.rb +81 -49
- data/lib/active_record/base.rb +2 -2
- data/lib/active_record/callbacks.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
- data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
- data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +31 -75
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +14 -19
- data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +2 -6
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +27 -9
- data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
- data/lib/active_record/connection_adapters/abstract_adapter.rb +27 -57
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +28 -58
- data/lib/active_record/connection_adapters/mysql/quoting.rb +1 -15
- data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +43 -45
- data/lib/active_record/connection_adapters/mysql2/database_statements.rb +42 -98
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +2 -16
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +64 -42
- data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +0 -1
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +12 -14
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +51 -9
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +44 -101
- data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -13
- data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
- data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -2
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +60 -22
- data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
- data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -18
- data/lib/active_record/connection_handling.rb +29 -11
- data/lib/active_record/core.rb +15 -60
- data/lib/active_record/counter_cache.rb +1 -1
- data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -3
- data/lib/active_record/delegated_type.rb +18 -18
- data/lib/active_record/encryption/config.rb +3 -1
- data/lib/active_record/encryption/encryptable_record.rb +5 -5
- data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
- data/lib/active_record/encryption/encryptor.rb +35 -29
- data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
- data/lib/active_record/encryption/scheme.rb +8 -1
- data/lib/active_record/enum.rb +12 -13
- data/lib/active_record/errors.rb +16 -8
- data/lib/active_record/fixture_set/table_row.rb +2 -19
- data/lib/active_record/fixtures.rb +0 -1
- data/lib/active_record/future_result.rb +14 -10
- data/lib/active_record/gem_version.rb +4 -4
- data/lib/active_record/insert_all.rb +1 -1
- data/lib/active_record/marshalling.rb +1 -4
- data/lib/active_record/migration/command_recorder.rb +22 -5
- data/lib/active_record/migration/compatibility.rb +5 -2
- data/lib/active_record/migration.rb +36 -35
- data/lib/active_record/model_schema.rb +1 -1
- data/lib/active_record/nested_attributes.rb +4 -6
- data/lib/active_record/persistence.rb +128 -130
- data/lib/active_record/query_cache.rb +5 -4
- data/lib/active_record/query_logs.rb +98 -44
- data/lib/active_record/query_logs_formatter.rb +17 -28
- data/lib/active_record/querying.rb +10 -10
- data/lib/active_record/railtie.rb +5 -6
- data/lib/active_record/railties/databases.rake +1 -2
- data/lib/active_record/reflection.rb +9 -7
- data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
- data/lib/active_record/relation/batches.rb +132 -72
- data/lib/active_record/relation/calculations.rb +55 -55
- data/lib/active_record/relation/delegation.rb +25 -14
- data/lib/active_record/relation/finder_methods.rb +31 -32
- data/lib/active_record/relation/merger.rb +8 -8
- data/lib/active_record/relation/predicate_builder/association_query_value.rb +0 -2
- data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
- data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
- data/lib/active_record/relation/predicate_builder.rb +5 -0
- data/lib/active_record/relation/query_attribute.rb +1 -1
- data/lib/active_record/relation/query_methods.rb +90 -91
- data/lib/active_record/relation/record_fetch_warning.rb +2 -2
- data/lib/active_record/relation/spawn_methods.rb +1 -1
- data/lib/active_record/relation/where_clause.rb +2 -8
- data/lib/active_record/relation.rb +77 -76
- data/lib/active_record/result.rb +68 -7
- data/lib/active_record/sanitization.rb +7 -6
- data/lib/active_record/schema_dumper.rb +16 -29
- data/lib/active_record/schema_migration.rb +2 -1
- data/lib/active_record/scoping/named.rb +5 -2
- data/lib/active_record/secure_token.rb +3 -3
- data/lib/active_record/signed_id.rb +6 -7
- data/lib/active_record/statement_cache.rb +12 -12
- data/lib/active_record/store.rb +7 -3
- data/lib/active_record/tasks/database_tasks.rb +24 -15
- data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
- data/lib/active_record/tasks/postgresql_database_tasks.rb +0 -7
- data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
- data/lib/active_record/test_fixtures.rb +12 -0
- data/lib/active_record/testing/query_assertions.rb +2 -2
- data/lib/active_record/token_for.rb +1 -1
- data/lib/active_record/transactions.rb +1 -3
- data/lib/active_record/validations/uniqueness.rb +8 -8
- data/lib/active_record.rb +16 -1
- data/lib/arel/collectors/bind.rb +1 -1
- data/lib/arel/crud.rb +0 -2
- data/lib/arel/delete_manager.rb +0 -5
- data/lib/arel/nodes/delete_statement.rb +2 -4
- data/lib/arel/nodes/update_statement.rb +2 -4
- data/lib/arel/select_manager.rb +2 -6
- data/lib/arel/update_manager.rb +0 -5
- data/lib/arel/visitors/dot.rb +0 -2
- data/lib/arel/visitors/sqlite.rb +0 -25
- data/lib/arel/visitors/to_sql.rb +1 -3
- 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,
|
|
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
|
|
39
|
-
# * <tt>:finish</tt> - Specifies the
|
|
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>:
|
|
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
|
|
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
|
-
|
|
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
|
|
113
|
-
# * <tt>:finish</tt> - Specifies the
|
|
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>:
|
|
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
|
|
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
|
-
|
|
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
|
|
187
|
-
# * <tt>:finish</tt> - Specifies the
|
|
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>:
|
|
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
|
|
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
|
-
|
|
241
|
-
|
|
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
|
|
285
|
-
|
|
286
|
-
|
|
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,
|
|
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,
|
|
345
|
+
batch_condition(relation, cursor, finish, operators)
|
|
302
346
|
end
|
|
303
347
|
|
|
304
|
-
def batch_condition(relation,
|
|
305
|
-
cursor_positions =
|
|
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
|
-
|
|
321
|
-
[column,
|
|
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
|
|
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
|
-
|
|
385
|
+
values = record_cursor_values(record, cursor)
|
|
345
386
|
|
|
346
|
-
|
|
347
|
-
(
|
|
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.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
371
|
-
|
|
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 ==
|
|
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
|
-
|
|
382
|
-
yielded_relation = where(
|
|
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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
394
|
-
yielded_relation = where(
|
|
450
|
+
values = batch_relation.pluck(*cursor)
|
|
451
|
+
yielded_relation = where(cursor => values)
|
|
395
452
|
end
|
|
396
453
|
|
|
397
|
-
break if
|
|
454
|
+
break if values.empty?
|
|
398
455
|
|
|
399
|
-
|
|
400
|
-
|
|
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
|
|
463
|
+
break if values.length < batch_limit
|
|
405
464
|
|
|
406
465
|
if limit_value
|
|
407
|
-
remaining -=
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
86
|
-
#
|
|
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
|
|
92
|
-
#
|
|
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,
|
|
163
|
-
#
|
|
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
|
|
169
|
-
#
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
c.select_all(relation.arel, "#{
|
|
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
|
-
|
|
394
|
-
c.select_all(relation, "#{
|
|
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) -
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
490
|
-
c.select_all(query_builder, "#{
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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, "#{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
643
|
-
|
|
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 = [
|
|
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)
|