activerecord 8.0.0 → 8.1.4

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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1381 -241
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/association_relation.rb +1 -1
  5. data/lib/active_record/associations/alias_tracker.rb +6 -4
  6. data/lib/active_record/associations/association.rb +35 -17
  7. data/lib/active_record/associations/association_scope.rb +1 -1
  8. data/lib/active_record/associations/belongs_to_association.rb +20 -4
  9. data/lib/active_record/associations/builder/association.rb +16 -5
  10. data/lib/active_record/associations/builder/belongs_to.rb +36 -8
  11. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  12. data/lib/active_record/associations/builder/has_one.rb +1 -1
  13. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  14. data/lib/active_record/associations/collection_association.rb +9 -5
  15. data/lib/active_record/associations/collection_proxy.rb +26 -4
  16. data/lib/active_record/associations/deprecation.rb +88 -0
  17. data/lib/active_record/associations/errors.rb +3 -0
  18. data/lib/active_record/associations/foreign_association.rb +3 -1
  19. data/lib/active_record/associations/has_one_association.rb +1 -0
  20. data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
  21. data/lib/active_record/associations/join_dependency.rb +5 -4
  22. data/lib/active_record/associations/preloader/association.rb +1 -1
  23. data/lib/active_record/associations/preloader/batch.rb +7 -1
  24. data/lib/active_record/associations/preloader/branch.rb +1 -0
  25. data/lib/active_record/associations.rb +159 -21
  26. data/lib/active_record/attribute_methods/primary_key.rb +2 -1
  27. data/lib/active_record/attribute_methods/query.rb +34 -0
  28. data/lib/active_record/attribute_methods/serialization.rb +19 -6
  29. data/lib/active_record/attribute_methods/time_zone_conversion.rb +10 -2
  30. data/lib/active_record/attribute_methods.rb +24 -17
  31. data/lib/active_record/attributes.rb +40 -26
  32. data/lib/active_record/autosave_association.rb +22 -12
  33. data/lib/active_record/base.rb +3 -4
  34. data/lib/active_record/callbacks.rb +1 -2
  35. data/lib/active_record/coders/column_serializer.rb +1 -1
  36. data/lib/active_record/coders/json.rb +16 -6
  37. data/lib/active_record/coders/yaml_column.rb +2 -2
  38. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +19 -18
  39. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -3
  40. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +68 -12
  41. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +474 -112
  42. data/lib/active_record/connection_adapters/abstract/database_statements.rb +68 -41
  43. data/lib/active_record/connection_adapters/abstract/query_cache.rb +36 -9
  44. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  45. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  46. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +31 -35
  47. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  48. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +94 -28
  49. data/lib/active_record/connection_adapters/abstract/transaction.rb +25 -3
  50. data/lib/active_record/connection_adapters/abstract_adapter.rb +166 -64
  51. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +133 -40
  52. data/lib/active_record/connection_adapters/column.rb +14 -4
  53. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  54. data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -1
  55. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  56. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  57. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +40 -25
  58. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +66 -15
  59. data/lib/active_record/connection_adapters/mysql2_adapter.rb +9 -3
  60. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  61. data/lib/active_record/connection_adapters/postgresql/column.rb +8 -2
  62. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +29 -33
  63. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  64. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +46 -3
  65. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  66. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  67. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +1 -1
  68. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +8 -6
  69. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +22 -33
  70. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +70 -32
  71. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +99 -54
  72. data/lib/active_record/connection_adapters/postgresql_adapter.rb +40 -12
  73. data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
  74. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +39 -23
  75. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -8
  76. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +15 -3
  77. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +7 -14
  78. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +68 -37
  79. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  80. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +8 -3
  81. data/lib/active_record/connection_adapters/trilogy_adapter.rb +16 -2
  82. data/lib/active_record/connection_adapters.rb +1 -0
  83. data/lib/active_record/connection_handling.rb +15 -10
  84. data/lib/active_record/core.rb +44 -12
  85. data/lib/active_record/counter_cache.rb +36 -9
  86. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
  87. data/lib/active_record/database_configurations/database_config.rb +5 -1
  88. data/lib/active_record/database_configurations/hash_config.rb +66 -9
  89. data/lib/active_record/database_configurations/url_config.rb +15 -3
  90. data/lib/active_record/database_configurations.rb +7 -3
  91. data/lib/active_record/delegated_type.rb +19 -19
  92. data/lib/active_record/disable_joins_association_relation.rb +5 -1
  93. data/lib/active_record/dynamic_matchers.rb +54 -69
  94. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  95. data/lib/active_record/encryption/encrypted_attribute_type.rb +2 -2
  96. data/lib/active_record/encryption/encrypted_fixtures.rb +12 -7
  97. data/lib/active_record/encryption/encryptor.rb +39 -25
  98. data/lib/active_record/encryption/scheme.rb +1 -1
  99. data/lib/active_record/enum.rb +37 -20
  100. data/lib/active_record/errors.rb +23 -7
  101. data/lib/active_record/explain.rb +1 -1
  102. data/lib/active_record/explain_registry.rb +51 -2
  103. data/lib/active_record/filter_attribute_handler.rb +76 -0
  104. data/lib/active_record/fixture_set/table_row.rb +19 -2
  105. data/lib/active_record/fixtures.rb +2 -2
  106. data/lib/active_record/future_result.rb +5 -3
  107. data/lib/active_record/gem_version.rb +2 -2
  108. data/lib/active_record/inheritance.rb +9 -4
  109. data/lib/active_record/insert_all.rb +14 -9
  110. data/lib/active_record/locking/optimistic.rb +7 -0
  111. data/lib/active_record/locking/pessimistic.rb +5 -0
  112. data/lib/active_record/log_subscriber.rb +30 -22
  113. data/lib/active_record/message_pack.rb +13 -0
  114. data/lib/active_record/middleware/shard_selector.rb +34 -17
  115. data/lib/active_record/migration/command_recorder.rb +32 -6
  116. data/lib/active_record/migration/compatibility.rb +34 -24
  117. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  118. data/lib/active_record/migration.rb +31 -21
  119. data/lib/active_record/model_schema.rb +38 -12
  120. data/lib/active_record/nested_attributes.rb +10 -6
  121. data/lib/active_record/persistence.rb +35 -3
  122. data/lib/active_record/query_cache.rb +22 -15
  123. data/lib/active_record/query_logs.rb +7 -7
  124. data/lib/active_record/querying.rb +17 -5
  125. data/lib/active_record/railtie.rb +35 -6
  126. data/lib/active_record/railties/controller_runtime.rb +11 -6
  127. data/lib/active_record/railties/databases.rake +24 -20
  128. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  129. data/lib/active_record/railties/job_runtime.rb +10 -11
  130. data/lib/active_record/reflection.rb +36 -1
  131. data/lib/active_record/relation/batches.rb +25 -11
  132. data/lib/active_record/relation/calculations.rb +62 -41
  133. data/lib/active_record/relation/delegation.rb +0 -1
  134. data/lib/active_record/relation/finder_methods.rb +76 -45
  135. data/lib/active_record/relation/merger.rb +4 -2
  136. data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
  137. data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -9
  138. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +7 -7
  139. data/lib/active_record/relation/predicate_builder.rb +10 -8
  140. data/lib/active_record/relation/query_attribute.rb +4 -2
  141. data/lib/active_record/relation/query_methods.rb +106 -45
  142. data/lib/active_record/relation/spawn_methods.rb +6 -6
  143. data/lib/active_record/relation/where_clause.rb +10 -11
  144. data/lib/active_record/relation.rb +63 -31
  145. data/lib/active_record/result.rb +44 -21
  146. data/lib/active_record/runtime_registry.rb +42 -58
  147. data/lib/active_record/sanitization.rb +2 -0
  148. data/lib/active_record/schema.rb +1 -1
  149. data/lib/active_record/schema_dumper.rb +41 -21
  150. data/lib/active_record/scoping.rb +0 -1
  151. data/lib/active_record/secure_token.rb +3 -3
  152. data/lib/active_record/signed_id.rb +48 -19
  153. data/lib/active_record/statement_cache.rb +16 -12
  154. data/lib/active_record/store.rb +46 -19
  155. data/lib/active_record/structured_event_subscriber.rb +85 -0
  156. data/lib/active_record/table_metadata.rb +5 -20
  157. data/lib/active_record/tasks/abstract_tasks.rb +83 -0
  158. data/lib/active_record/tasks/database_tasks.rb +44 -45
  159. data/lib/active_record/tasks/mysql_database_tasks.rb +4 -56
  160. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  161. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  162. data/lib/active_record/test_databases.rb +18 -4
  163. data/lib/active_record/test_fixtures.rb +40 -6
  164. data/lib/active_record/testing/query_assertions.rb +8 -2
  165. data/lib/active_record/timestamp.rb +4 -2
  166. data/lib/active_record/transaction.rb +2 -5
  167. data/lib/active_record/transactions.rb +58 -32
  168. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  169. data/lib/active_record/type/internal/timezone.rb +7 -0
  170. data/lib/active_record/type/json.rb +13 -2
  171. data/lib/active_record/type/serialized.rb +16 -4
  172. data/lib/active_record/type/type_map.rb +1 -1
  173. data/lib/active_record/type_caster/connection.rb +2 -1
  174. data/lib/active_record/validations/associated.rb +1 -1
  175. data/lib/active_record.rb +72 -7
  176. data/lib/arel/alias_predication.rb +2 -0
  177. data/lib/arel/collectors/bind.rb +1 -1
  178. data/lib/arel/collectors/sql_string.rb +1 -1
  179. data/lib/arel/collectors/substitute_binds.rb +2 -2
  180. data/lib/arel/crud.rb +8 -11
  181. data/lib/arel/delete_manager.rb +5 -0
  182. data/lib/arel/nodes/binary.rb +1 -1
  183. data/lib/arel/nodes/count.rb +2 -2
  184. data/lib/arel/nodes/delete_statement.rb +4 -2
  185. data/lib/arel/nodes/function.rb +4 -10
  186. data/lib/arel/nodes/named_function.rb +2 -2
  187. data/lib/arel/nodes/node.rb +2 -2
  188. data/lib/arel/nodes/sql_literal.rb +1 -1
  189. data/lib/arel/nodes/update_statement.rb +4 -2
  190. data/lib/arel/nodes.rb +0 -2
  191. data/lib/arel/predications.rb +1 -3
  192. data/lib/arel/select_manager.rb +13 -4
  193. data/lib/arel/update_manager.rb +5 -0
  194. data/lib/arel/visitors/dot.rb +2 -3
  195. data/lib/arel/visitors/postgresql.rb +55 -0
  196. data/lib/arel/visitors/sqlite.rb +55 -8
  197. data/lib/arel/visitors/to_sql.rb +7 -23
  198. data/lib/arel.rb +3 -1
  199. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  200. metadata +16 -15
  201. data/lib/active_record/explain_subscriber.rb +0 -34
  202. data/lib/active_record/normalization.rb +0 -163
@@ -60,37 +60,37 @@ module ActiveRecord
60
60
  # Person.distinct.count(:age)
61
61
  # # => counts the number of different age values
62
62
  #
63
- # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group],
63
+ # If +count+ is used with {Relation#group}[rdoc-ref:QueryMethods#group],
64
64
  # it returns a Hash whose keys represent the aggregated column,
65
65
  # and the values are the respective amounts:
66
66
  #
67
67
  # Person.group(:city).count
68
68
  # # => { 'Rome' => 5, 'Paris' => 3 }
69
69
  #
70
- # If #count is used with {Relation#group}[rdoc-ref:QueryMethods#group] for multiple columns, it returns a Hash whose
70
+ # If +count+ is used with {Relation#group}[rdoc-ref:QueryMethods#group] for multiple columns, it returns a Hash whose
71
71
  # keys are an array containing the individual values of each column and the value
72
- # of each key would be the #count.
72
+ # of each key would be the count.
73
73
  #
74
74
  # Article.group(:status, :category).count
75
75
  # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, ["published", "technology"]=>2}
76
76
  #
77
- # If #count is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns:
77
+ # If +count+ is used with {Relation#select}[rdoc-ref:QueryMethods#select], it will count the selected columns:
78
78
  #
79
79
  # Person.select(:age).count
80
80
  # # => counts the number of different age values
81
81
  #
82
- # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ
82
+ # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid +count+ expressions. The specifics differ
83
83
  # between databases. In invalid cases, an error from the database is thrown.
84
84
  #
85
- # When given a block, 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.
85
+ # When given a block, calls the block with each record in the relation and
86
+ # returns the number of records for which the block returns a truthy value.
88
87
  #
89
88
  # Person.count { |person| person.age > 21 }
90
89
  # # => counts the number of people older that 21
91
90
  #
92
- # Note: If there are a lot of records in the relation, loading all records
93
- # could result in performance issues.
91
+ # If the relation hasn't been loaded yet, calling +count+ with a block will
92
+ # load all records in the relation. If there are a lot of records in the
93
+ # relation, loading all records could result in performance issues.
94
94
  def count(column_name = nil)
95
95
  if block_given?
96
96
  unless column_name.nil?
@@ -159,16 +159,15 @@ module ActiveRecord
159
159
  #
160
160
  # Person.sum(:age) # => 4562
161
161
  #
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:
162
+ # When given a block, calls the block with each record in the relation and
163
+ # returns the sum of +initial_value_or_column+ plus the block return values:
166
164
  #
167
165
  # Person.sum { |person| person.age } # => 4562
168
166
  # Person.sum(1000) { |person| person.age } # => 5562
169
167
  #
170
- # Note: If there are a lot of records in the relation, loading all records
171
- # could result in performance issues.
168
+ # If the relation hasn't been loaded yet, calling +sum+ with a block will
169
+ # load all records in the relation. If there are a lot of records in the
170
+ # relation, loading all records could result in performance issues.
172
171
  def sum(initial_value_or_column = 0, &block)
173
172
  if block_given?
174
173
  map(&block).sum(initial_value_or_column)
@@ -287,6 +286,11 @@ module ActiveRecord
287
286
  # # SELECT DATEDIFF(updated_at, created_at) FROM people
288
287
  # # => ['0', '27761', '173']
289
288
  #
289
+ # Be aware that #pluck ignores any previous select clauses
290
+ #
291
+ # Person.select(:name).pluck(:id)
292
+ # # SELECT people.id FROM people
293
+ #
290
294
  # See also #ids.
291
295
  def pluck(*column_names)
292
296
  if @none
@@ -315,7 +319,7 @@ module ActiveRecord
315
319
  columns = relation.arel_columns(column_names)
316
320
  relation.select_values = columns
317
321
  result = skip_query_cache_if_necessary do
318
- if where_clause.contradiction?
322
+ if where_clause.contradiction? && !possible_aggregation?(column_names)
319
323
  ActiveRecord::Result.empty(async: @async)
320
324
  else
321
325
  model.with_connection do |c|
@@ -392,7 +396,7 @@ module ActiveRecord
392
396
  relation.select_values = columns
393
397
 
394
398
  result = if relation.where_clause.contradiction?
395
- ActiveRecord::Result.empty
399
+ ActiveRecord::Result.empty(async: @async)
396
400
  else
397
401
  skip_query_cache_if_necessary do
398
402
  model.with_connection do |c|
@@ -410,6 +414,18 @@ module ActiveRecord
410
414
  async.ids
411
415
  end
412
416
 
417
+ protected
418
+ def aggregate_column(column_name)
419
+ case column_name
420
+ when Arel::Expressions
421
+ column_name
422
+ when :all
423
+ Arel.star
424
+ else
425
+ arel_column(column_name.to_s)
426
+ end
427
+ end
428
+
413
429
  private
414
430
  def all_attributes?(column_names)
415
431
  (column_names.map(&:to_s) - model.attribute_names - model.attribute_aliases.keys).empty?
@@ -450,14 +466,13 @@ module ActiveRecord
450
466
  column_name.is_a?(::String) && /\bDISTINCT[\s(]/i.match?(column_name)
451
467
  end
452
468
 
453
- def aggregate_column(column_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)
469
+ def possible_aggregation?(column_names)
470
+ column_names.all? do |column_name|
471
+ if column_name.is_a?(String)
472
+ column_name.include?("(")
473
+ else
474
+ Arel.arel_node?(column_name)
475
+ end
461
476
  end
462
477
  end
463
478
 
@@ -476,9 +491,9 @@ module ActiveRecord
476
491
  # PostgreSQL doesn't like ORDER BY when there are no GROUP BY
477
492
  relation = unscope(:order).distinct!(false)
478
493
 
479
- column = aggregate_column(column_name)
494
+ column = relation.aggregate_column(column_name)
480
495
  select_value = operation_over_aggregate_column(column, operation, distinct)
481
- select_value.distinct = true if operation == "sum" && distinct
496
+ select_value.distinct = true if distinct && (operation == "sum" || operation == "average")
482
497
 
483
498
  relation.select_values = [select_value]
484
499
 
@@ -486,7 +501,11 @@ module ActiveRecord
486
501
  end
487
502
 
488
503
  query_result = if relation.where_clause.contradiction?
489
- ActiveRecord::Result.empty
504
+ if @async
505
+ FutureResult.wrap(ActiveRecord::Result.empty)
506
+ else
507
+ ActiveRecord::Result.empty
508
+ end
490
509
  else
491
510
  skip_query_cache_if_necessary do
492
511
  model.with_connection do |c|
@@ -508,14 +527,15 @@ module ActiveRecord
508
527
 
509
528
  def execute_grouped_calculation(operation, column_name, distinct) # :nodoc:
510
529
  group_fields = group_values
511
- group_fields = group_fields.uniq if group_fields.size > 1
512
530
 
513
531
  if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym)
514
532
  association = model._reflect_on_association(group_fields.first)
515
533
  associated = association && association.belongs_to? # only count belongs_to associations
516
534
  group_fields = Array(association.foreign_key) if associated
517
535
  end
518
- group_fields = arel_columns(group_fields)
536
+
537
+ relation = except(:group).distinct!(false)
538
+ group_fields = relation.arel_columns(group_fields)
519
539
 
520
540
  model.with_connection do |connection|
521
541
  column_alias_tracker = ColumnAliasTracker.new(connection)
@@ -526,10 +546,11 @@ module ActiveRecord
526
546
  }
527
547
  group_columns = group_aliases.zip(group_fields)
528
548
 
529
- column = aggregate_column(column_name)
549
+ column = relation.aggregate_column(column_name)
530
550
  column_alias = column_alias_tracker.alias_for("#{operation} #{column_name.to_s.downcase}")
531
551
  select_value = operation_over_aggregate_column(column, operation, distinct)
532
- select_value.as(model.adapter_class.quote_column_name(column_alias))
552
+ select_value.distinct = true if distinct && (operation == "sum" || operation == "average")
553
+ select_value = select_value.as(model.adapter_class.quote_column_name(column_alias))
533
554
 
534
555
  select_values = [select_value]
535
556
  select_values += self.select_values unless having_clause.empty?
@@ -543,7 +564,6 @@ module ActiveRecord
543
564
  end
544
565
  }
545
566
 
546
- relation = except(:group).distinct!(false)
547
567
  relation.group_values = group_fields
548
568
  relation.select_values = select_values
549
569
 
@@ -553,7 +573,11 @@ module ActiveRecord
553
573
 
554
574
  result.then do |calculated_data|
555
575
  if association
556
- key_ids = calculated_data.collect { |row| row[group_aliases.first] }
576
+ key_ids = if association.klass.base_class.composite_primary_key?
577
+ calculated_data.collect { |row| group_aliases.map { |group_alias| row[group_alias] } }
578
+ else
579
+ calculated_data.collect { |row| row[group_aliases.first] }
580
+ end
557
581
  key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
558
582
  key_records = key_records.index_by(&:id)
559
583
  end
@@ -657,19 +681,16 @@ module ActiveRecord
657
681
  if column_name == :all
658
682
  column_alias = Arel.star
659
683
  relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct
684
+ relation.unscope!(:order)
660
685
  else
661
686
  column_alias = Arel.sql("count_column")
662
- relation.select_values = [ aggregate_column(column_name).as(column_alias) ]
687
+ relation.select_values = [ relation.aggregate_column(column_name).as(column_alias) ]
663
688
  end
664
689
 
665
690
  subquery_alias = Arel.sql("subquery_for_count", retryable: true)
666
691
  select_value = operation_over_aggregate_column(column_alias, "count", false)
667
692
 
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
693
+ relation.build_subquery(subquery_alias, select_value)
673
694
  end
674
695
  end
675
696
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/module/delegation"
4
3
 
5
4
  module ActiveRecord
6
5
  module Delegation # :nodoc:
@@ -24,22 +24,22 @@ module ActiveRecord
24
24
  # TravelRoute.primary_key = [:origin, :destination]
25
25
  #
26
26
  # TravelRoute.find(["Ottawa", "London"])
27
- # => #<TravelRoute origin: "Ottawa", destination: "London">
27
+ # # => #<TravelRoute origin: "Ottawa", destination: "London">
28
28
  #
29
29
  # TravelRoute.find([["Paris", "Montreal"]])
30
- # => [#<TravelRoute origin: "Paris", destination: "Montreal">]
30
+ # # => [#<TravelRoute origin: "Paris", destination: "Montreal">]
31
31
  #
32
32
  # TravelRoute.find(["New York", "Las Vegas"], ["New York", "Portland"])
33
- # => [
34
- # #<TravelRoute origin: "New York", destination: "Las Vegas">,
35
- # #<TravelRoute origin: "New York", destination: "Portland">
36
- # ]
33
+ # # => [
34
+ # # #<TravelRoute origin: "New York", destination: "Las Vegas">,
35
+ # # #<TravelRoute origin: "New York", destination: "Portland">
36
+ # # ]
37
37
  #
38
38
  # TravelRoute.find([["Berlin", "London"], ["Barcelona", "Lisbon"]])
39
- # => [
40
- # #<TravelRoute origin: "Berlin", destination: "London">,
41
- # #<TravelRoute origin: "Barcelona", destination: "Lisbon">
42
- # ]
39
+ # # => [
40
+ # # #<TravelRoute origin: "Berlin", destination: "London">,
41
+ # # #<TravelRoute origin: "Barcelona", destination: "Lisbon">
42
+ # # ]
43
43
  #
44
44
  # NOTE: The returned records are in the same order as the ids you provide.
45
45
  # If you want the results to be sorted by database, you can use ActiveRecord::QueryMethods#where
@@ -141,14 +141,14 @@ module ActiveRecord
141
141
  #
142
142
  # Product.where(["price = %?", price]).sole
143
143
  def sole
144
- found, undesired = first(2)
144
+ found, undesired = take(2)
145
145
 
146
146
  if found.nil?
147
147
  raise_record_not_found_exception!
148
148
  elsif undesired.nil?
149
149
  found
150
150
  else
151
- raise ActiveRecord::SoleRecordExceeded.new(model)
151
+ raise ActiveRecord::SoleRecordExceeded.new(self)
152
152
  end
153
153
  end
154
154
 
@@ -424,12 +424,13 @@ module ActiveRecord
424
424
  error << " with#{conditions}" if conditions
425
425
  raise RecordNotFound.new(error, name, key)
426
426
  elsif Array.wrap(ids).size == 1
427
- error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}"
427
+ id = Array.wrap(ids)[0]
428
+ error = "Couldn't find #{name} with '#{key}'=#{id.inspect}#{conditions}"
428
429
  raise RecordNotFound.new(error, name, key, ids)
429
430
  else
430
431
  error = +"Couldn't find all #{name.pluralize} with '#{key}': "
431
- error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})."
432
- error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.join(', ')}." if not_found_ids
432
+ error << "(#{ids.map(&:inspect).join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})."
433
+ error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.map(&:inspect).join(', ')}." if not_found_ids
433
434
  raise RecordNotFound.new(error, name, key, ids)
434
435
  end
435
436
  end
@@ -441,7 +442,7 @@ module ActiveRecord
441
442
  if distinct_value && offset_value
442
443
  relation = except(:order).limit!(1)
443
444
  else
444
- relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
445
+ relation = except(:select, :distinct, :order)._select!(Arel.sql(ONE_AS_ONE, retryable: true)).limit!(1)
445
446
  end
446
447
 
447
448
  case conditions
@@ -461,15 +462,15 @@ module ActiveRecord
461
462
  relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
462
463
 
463
464
  if eager_loading && has_limit_or_offset? && !(
464
- using_limitable_reflections?(join_dependency.reflections) &&
465
- using_limitable_reflections?(
466
- construct_join_dependency(
467
- select_association_list(joins_values).concat(
468
- select_association_list(left_outer_joins_values)
469
- ), nil
470
- ).reflections
471
- )
465
+ using_limitable_reflections?(join_dependency.reflections) &&
466
+ using_limitable_reflections?(
467
+ construct_join_dependency(
468
+ select_association_list(joins_values).concat(
469
+ select_association_list(left_outer_joins_values)
470
+ ), nil
471
+ ).reflections
472
472
  )
473
+ )
473
474
  relation = skip_query_cache_if_necessary do
474
475
  model.with_connection do |c|
475
476
  c.distinct_relation_for_primary_key(relation)
@@ -491,15 +492,16 @@ module ActiveRecord
491
492
  def find_with_ids(*ids)
492
493
  raise UnknownPrimaryKey.new(model) if primary_key.nil?
493
494
 
495
+ first_item = ids.first
496
+ return [] if first_item.is_a?(Array) && first_item.empty?
497
+
494
498
  expects_array = if model.composite_primary_key?
495
- ids.first.first.is_a?(Array)
499
+ first_item.first.is_a?(Array)
496
500
  else
497
- ids.first.is_a?(Array)
501
+ first_item.is_a?(Array)
498
502
  end
499
503
 
500
- return [] if expects_array && ids.first.empty?
501
-
502
- ids = ids.first if expects_array
504
+ ids = first_item if expects_array
503
505
 
504
506
  ids = ids.compact.uniq
505
507
 
@@ -541,10 +543,6 @@ module ActiveRecord
541
543
  def find_some(ids)
542
544
  return find_some_ordered(ids) unless order_values.present?
543
545
 
544
- relation = where(primary_key => ids)
545
- relation = relation.select(table[primary_key]) unless select_values.empty?
546
- result = relation.to_a
547
-
548
546
  expected_size =
549
547
  if limit_value && ids.size > limit_value
550
548
  limit_value
@@ -553,10 +551,18 @@ module ActiveRecord
553
551
  end
554
552
 
555
553
  # 11 ids with limit 3, offset 9 should give 2 results.
556
- if offset_value && (ids.size - offset_value < expected_size)
557
- expected_size = ids.size - offset_value
554
+ if offset_value
555
+ if ids.size <= offset_value
556
+ return []
557
+ elsif ids.size - offset_value < expected_size
558
+ expected_size = ids.size - offset_value
559
+ end
558
560
  end
559
561
 
562
+ relation = where(primary_key => ids)
563
+ relation = relation.select(table[primary_key]) unless select_values.empty?
564
+ result = relation.to_a
565
+
560
566
  if result.size == expected_size
561
567
  result
562
568
  else
@@ -566,6 +572,7 @@ module ActiveRecord
566
572
 
567
573
  def find_some_ordered(ids)
568
574
  ids = ids.slice(offset_value || 0, limit_value || ids.size) || []
575
+ return [] if ids.empty?
569
576
 
570
577
  relation = except(:limit, :offset)
571
578
  relation = relation.where(primary_key => ids)
@@ -573,12 +580,20 @@ module ActiveRecord
573
580
  result = relation.records
574
581
 
575
582
  if result.size == ids.size
576
- result.in_order_of(:id, ids.map { |id| model.type_for_attribute(primary_key).cast(id) })
583
+ result.in_order_of(:id, ids.map { |id| cast_primary_key(id) })
577
584
  else
578
585
  raise_record_not_found_exception!(ids, result.size, ids.size)
579
586
  end
580
587
  end
581
588
 
589
+ def cast_primary_key(id)
590
+ if model.composite_primary_key?
591
+ primary_key.zip(id).map! { |attr, value| model.type_for_attribute(attr).cast(value) }
592
+ else
593
+ model.type_for_attribute(primary_key).cast(id)
594
+ end
595
+ end
596
+
582
597
  def find_take
583
598
  if loaded?
584
599
  records.first
@@ -638,24 +653,40 @@ module ActiveRecord
638
653
  end
639
654
 
640
655
  def ordered_relation
641
- if order_values.empty? && (model.implicit_order_column || !model.query_constraints_list.nil? || primary_key)
642
- order(_order_columns.map { |column| table[column].asc })
656
+ if order_values.empty?
657
+ if !_order_columns.empty?
658
+ return order(_order_columns.map { |column| table[column].asc })
659
+ end
660
+
661
+ if ActiveRecord.raise_on_missing_required_finder_order_columns
662
+ raise MissingRequiredOrderError, <<~MSG.squish
663
+ Relation has no order values, and #{model} has no order columns to use as a default.
664
+ Set at least one of `implicit_order_column`, `query_constraints` or `primary_key` on
665
+ the model when no `order `is specified on the relation.
666
+ MSG
667
+ else
668
+ ActiveRecord.deprecator.warn(<<~MSG)
669
+ Calling order dependent finder methods (e.g. `#first`, `#second`) without `order` values on the relation,
670
+ and on a model (#{model}) that does not have any order columns (`implicit_order_column`, `query_constraints`,
671
+ or `primary_key`) to fall back on is deprecated and will raise `ActiveRecord::MissingRequiredOrderError`
672
+ in Rails 8.2.
673
+ MSG
674
+
675
+ self
676
+ end
643
677
  else
644
678
  self
645
679
  end
646
680
  end
647
681
 
648
682
  def _order_columns
649
- oc = []
683
+ columns = Array(model.implicit_order_column)
650
684
 
651
- oc << model.implicit_order_column if model.implicit_order_column
652
- oc << model.query_constraints_list if model.query_constraints_list
685
+ return columns.compact if columns.length.positive? && columns.last.nil?
653
686
 
654
- if model.primary_key && model.query_constraints_list.nil?
655
- oc << model.primary_key
656
- end
687
+ columns += Array(model.query_constraints_list || model.primary_key)
657
688
 
658
- oc.flatten.uniq.compact
689
+ columns.uniq.compact
659
690
  end
660
691
  end
661
692
  end
@@ -161,8 +161,10 @@ module ActiveRecord
161
161
  relation.order!(*other.order_values)
162
162
  end
163
163
 
164
- extensions = other.extensions - relation.extensions
165
- relation.extending!(*extensions) if extensions.any?
164
+ unless other.extensions.empty?
165
+ extensions = other.extensions - relation.extensions
166
+ relation.extending!(*extensions) if extensions.any?
167
+ end
166
168
  end
167
169
 
168
170
  def merge_single_values
@@ -31,7 +31,9 @@ module ActiveRecord
31
31
  values_predicate
32
32
  else
33
33
  array_predicates = ranges.map! { |range| predicate_builder.build(attribute, range) }
34
- array_predicates.inject(values_predicate, &:or)
34
+ values_predicate.or(
35
+ Arel::Nodes::Grouping.new Arel::Nodes::Or.new(array_predicates)
36
+ )
35
37
  end
36
38
  end
37
39
 
@@ -3,24 +3,24 @@
3
3
  module ActiveRecord
4
4
  class PredicateBuilder
5
5
  class AssociationQueryValue # :nodoc:
6
- def initialize(associated_table, value)
7
- @associated_table = associated_table
6
+ def initialize(reflection, value)
7
+ @reflection = reflection
8
8
  @value = value
9
9
  end
10
10
 
11
11
  def queries
12
- if associated_table.join_foreign_key.is_a?(Array)
12
+ if reflection.join_foreign_key.is_a?(Array)
13
13
  id_list = ids
14
14
  id_list = id_list.pluck(primary_key) if id_list.is_a?(Relation)
15
15
 
16
- id_list.map { |ids_set| associated_table.join_foreign_key.zip(ids_set).to_h }
16
+ id_list.map { |ids_set| reflection.join_foreign_key.zip(ids_set).to_h }
17
17
  else
18
- [ associated_table.join_foreign_key => ids ]
18
+ [ reflection.join_foreign_key => ids ]
19
19
  end
20
20
  end
21
21
 
22
22
  private
23
- attr_reader :associated_table, :value
23
+ attr_reader :reflection, :value
24
24
 
25
25
  def ids
26
26
  case value
@@ -37,15 +37,15 @@ module ActiveRecord
37
37
  end
38
38
 
39
39
  def primary_key
40
- associated_table.join_primary_key
40
+ reflection.join_primary_key
41
41
  end
42
42
 
43
43
  def primary_type
44
- associated_table.join_primary_type
44
+ reflection.join_primary_type
45
45
  end
46
46
 
47
47
  def polymorphic_name
48
- associated_table.polymorphic_name_association
48
+ reflection.polymorphic_name
49
49
  end
50
50
 
51
51
  def select_clause?
@@ -59,6 +59,8 @@ module ActiveRecord
59
59
  def convert_to_id(value)
60
60
  if primary_key.is_a?(Array)
61
61
  primary_key.map do |attribute|
62
+ next nil if value.nil?
63
+
62
64
  if attribute == "id"
63
65
  value.id_value
64
66
  else
@@ -3,24 +3,24 @@
3
3
  module ActiveRecord
4
4
  class PredicateBuilder
5
5
  class PolymorphicArrayValue # :nodoc:
6
- def initialize(associated_table, values)
7
- @associated_table = associated_table
6
+ def initialize(reflection, values)
7
+ @reflection = reflection
8
8
  @values = values
9
9
  end
10
10
 
11
11
  def queries
12
- return [ associated_table.join_foreign_key => values ] if values.empty?
12
+ return [ reflection.join_foreign_key => values ] if values.empty?
13
13
 
14
14
  type_to_ids_mapping.map do |type, ids|
15
15
  query = {}
16
- query[associated_table.join_foreign_type] = type if type
17
- query[associated_table.join_foreign_key] = ids
16
+ query[reflection.join_foreign_type] = type if type
17
+ query[reflection.join_foreign_key] = ids
18
18
  query
19
19
  end
20
20
  end
21
21
 
22
22
  private
23
- attr_reader :associated_table, :values
23
+ attr_reader :reflection, :values
24
24
 
25
25
  def type_to_ids_mapping
26
26
  default_hash = Hash.new { |hsh, key| hsh[key] = [] }
@@ -30,7 +30,7 @@ module ActiveRecord
30
30
  end
31
31
 
32
32
  def primary_key(value)
33
- associated_table.join_primary_key(klass(value))
33
+ reflection.join_primary_key(klass(value))
34
34
  end
35
35
 
36
36
  def klass(value)
@@ -37,7 +37,7 @@ module ActiveRecord
37
37
 
38
38
  # Define how a class is converted to Arel nodes when passed to +where+.
39
39
  # The handler can be any object that responds to +call+, and will be used
40
- # for any value that +===+ the class given. For example:
40
+ # for any value that <tt>===</tt> the class given. For example:
41
41
  #
42
42
  # MyCustomDateRange = Struct.new(:start, :end)
43
43
  # handler = proc do |column, range|
@@ -82,7 +82,7 @@ module ActiveRecord
82
82
  attr_writer :table
83
83
 
84
84
  def expand_from_hash(attributes, &block)
85
- return ["1=0"] if attributes.empty?
85
+ return [Arel.sql("1=0", retryable: true)] if attributes.empty?
86
86
 
87
87
  attributes.flat_map do |key, value|
88
88
  if key.is_a?(Array) && key.size == 1
@@ -93,30 +93,32 @@ module ActiveRecord
93
93
  if key.is_a?(Array)
94
94
  queries = Array(value).map do |ids_set|
95
95
  raise ArgumentError, "Expected corresponding value for #{key} to be an Array" unless ids_set.is_a?(Array)
96
- expand_from_hash(key.zip(ids_set).to_h)
96
+ expand_from_hash(convert_dot_notation_to_hash(key.zip(ids_set).to_h))
97
97
  end
98
98
  grouping_queries(queries)
99
99
  elsif value.is_a?(Hash) && !table.has_column?(key)
100
100
  table.associated_table(key, &block)
101
101
  .predicate_builder.expand_from_hash(value.stringify_keys)
102
- elsif table.associated_with?(key)
102
+ elsif (associated_reflection = table.associated_with(key))
103
103
  # Find the foreign key when using queries such as:
104
104
  # Post.where(author: author)
105
105
  #
106
106
  # For polymorphic relationships, find the foreign key and type:
107
107
  # PriceEstimate.where(estimate_of: treasure)
108
- associated_table = table.associated_table(key)
109
- if associated_table.polymorphic_association?
108
+
109
+ if associated_reflection.polymorphic?
110
110
  value = [value] unless value.is_a?(Array)
111
111
  klass = PolymorphicArrayValue
112
- elsif associated_table.through_association?
112
+ elsif associated_reflection.through_reflection?
113
+ associated_table = table.associated_table(key)
114
+
113
115
  next associated_table.predicate_builder.expand_from_hash(
114
116
  associated_table.primary_key => value
115
117
  )
116
118
  end
117
119
 
118
120
  klass ||= AssociationQueryValue
119
- queries = klass.new(associated_table, value).queries.map! do |query|
121
+ queries = klass.new(associated_reflection, value).queries.map! do |query|
120
122
  # If the query produced is identical to attributes don't go any deeper.
121
123
  # Prevents stack level too deep errors when association and foreign_key are identical.
122
124
  query == attributes ? self[key, value] : expand_from_hash(query)