activerecord 8.1.3.1 → 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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +617 -0
  3. data/lib/active_record/associations/association.rb +34 -16
  4. data/lib/active_record/associations/association_scope.rb +1 -1
  5. data/lib/active_record/associations/belongs_to_association.rb +2 -2
  6. data/lib/active_record/associations/builder/belongs_to.rb +19 -4
  7. data/lib/active_record/associations/collection_association.rb +6 -2
  8. data/lib/active_record/associations/collection_proxy.rb +4 -0
  9. data/lib/active_record/associations/foreign_association.rb +3 -1
  10. data/lib/active_record/associations/has_one_association.rb +1 -0
  11. data/lib/active_record/associations/join_dependency.rb +1 -2
  12. data/lib/active_record/associations/preloader/association.rb +1 -1
  13. data/lib/active_record/attribute_methods/serialization.rb +2 -2
  14. data/lib/active_record/attribute_methods.rb +6 -4
  15. data/lib/active_record/callbacks.rb +1 -2
  16. data/lib/active_record/coders/column_serializer.rb +1 -1
  17. data/lib/active_record/coders/json.rb +7 -6
  18. data/lib/active_record/coders/yaml_column.rb +2 -2
  19. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +17 -0
  20. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +32 -20
  21. data/lib/active_record/connection_adapters/abstract/database_statements.rb +14 -2
  22. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +2 -2
  23. data/lib/active_record/connection_adapters/abstract_adapter.rb +12 -0
  24. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +79 -23
  25. data/lib/active_record/connection_adapters/column.rb +2 -5
  26. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +7 -21
  27. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +3 -16
  28. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +46 -3
  29. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +1 -1
  30. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
  31. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +17 -5
  32. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  33. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +2 -2
  34. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +4 -2
  35. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -0
  36. data/lib/active_record/connection_adapters/trilogy_adapter.rb +14 -0
  37. data/lib/active_record/counter_cache.rb +8 -6
  38. data/lib/active_record/database_configurations/hash_config.rb +9 -2
  39. data/lib/active_record/database_configurations/url_config.rb +2 -0
  40. data/lib/active_record/disable_joins_association_relation.rb +5 -1
  41. data/lib/active_record/encryption/encrypted_fixtures.rb +12 -7
  42. data/lib/active_record/filter_attribute_handler.rb +6 -3
  43. data/lib/active_record/gem_version.rb +2 -2
  44. data/lib/active_record/inheritance.rb +8 -3
  45. data/lib/active_record/log_subscriber.rb +28 -16
  46. data/lib/active_record/message_pack.rb +13 -0
  47. data/lib/active_record/migration/command_recorder.rb +12 -2
  48. data/lib/active_record/model_schema.rb +2 -2
  49. data/lib/active_record/nested_attributes.rb +8 -6
  50. data/lib/active_record/persistence.rb +1 -0
  51. data/lib/active_record/querying.rb +13 -1
  52. data/lib/active_record/reflection.rb +1 -1
  53. data/lib/active_record/relation/calculations.rb +8 -3
  54. data/lib/active_record/relation/finder_methods.rb +34 -20
  55. data/lib/active_record/relation/merger.rb +4 -2
  56. data/lib/active_record/relation/predicate_builder.rb +1 -1
  57. data/lib/active_record/relation/query_methods.rb +63 -13
  58. data/lib/active_record/relation.rb +20 -12
  59. data/lib/active_record/schema.rb +1 -1
  60. data/lib/active_record/schema_dumper.rb +1 -1
  61. data/lib/active_record/signed_id.rb +1 -1
  62. data/lib/active_record/statement_cache.rb +1 -1
  63. data/lib/active_record/store.rb +4 -2
  64. data/lib/active_record/tasks/abstract_tasks.rb +8 -1
  65. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -16
  66. data/lib/active_record/test_databases.rb +4 -0
  67. data/lib/active_record/test_fixtures.rb +13 -4
  68. data/lib/active_record/transactions.rb +19 -16
  69. data/lib/active_record/type_caster/connection.rb +1 -1
  70. data/lib/active_record.rb +1 -1
  71. data/lib/arel/visitors/to_sql.rb +1 -1
  72. metadata +9 -9
@@ -103,7 +103,7 @@ module ActiveRecord
103
103
  #
104
104
  # class Member < ActiveRecord::Base
105
105
  # has_many :posts
106
- # accepts_nested_attributes_for :posts
106
+ # accepts_nested_attributes_for :posts, allow_destroy: true
107
107
  # end
108
108
  #
109
109
  # You can now set or update attributes on the associated posts through
@@ -112,7 +112,8 @@ module ActiveRecord
112
112
  #
113
113
  # For each hash that does _not_ have an <tt>id</tt> key a new record will
114
114
  # be instantiated, unless the hash also contains a <tt>_destroy</tt> key
115
- # that evaluates to +true+.
115
+ # that evaluates to +true+ and the <tt>:allow_destroy</tt> option is
116
+ # enabled.
116
117
  #
117
118
  # params = { member: {
118
119
  # name: 'joe', posts_attributes: [
@@ -313,8 +314,9 @@ module ActiveRecord
313
314
  # that checks whether a record should be built for a certain attribute
314
315
  # hash. The hash is passed to the supplied Proc or the method
315
316
  # and it should return either +true+ or +false+. When no +:reject_if+
316
- # is specified, a record will be built for all attribute hashes that
317
- # do not have a <tt>_destroy</tt> value that evaluates to true.
317
+ # is specified, a record will be built for all attribute hashes, unless
318
+ # the <tt>:allow_destroy</tt> option is enabled and the hash has a
319
+ # <tt>_destroy</tt> value that evaluates to true.
318
320
  # Passing <tt>:all_blank</tt> instead of a Proc will create a proc
319
321
  # that will reject a record where all the attributes are blank excluding
320
322
  # any value for +_destroy+.
@@ -496,8 +498,6 @@ module ActiveRecord
496
498
  raise ArgumentError, "Hash or Array expected for `#{association_name}` attributes, got #{attributes_collection.class.name}"
497
499
  end
498
500
 
499
- check_record_limit!(options[:limit], attributes_collection)
500
-
501
501
  if attributes_collection.is_a? Hash
502
502
  keys = attributes_collection.keys
503
503
  attributes_collection = if keys.include?("id") || keys.include?(:id)
@@ -507,6 +507,8 @@ module ActiveRecord
507
507
  end
508
508
  end
509
509
 
510
+ check_record_limit!(options[:limit], attributes_collection)
511
+
510
512
  association = association(association_name)
511
513
 
512
514
  existing_records = if association.loaded?
@@ -494,6 +494,7 @@ module ActiveRecord
494
494
  becoming.instance_variable_set(:@new_record, new_record?)
495
495
  becoming.instance_variable_set(:@previously_new_record, previously_new_record?)
496
496
  becoming.instance_variable_set(:@destroyed, destroyed?)
497
+ becoming.instance_variable_set(:@marked_for_destruction, marked_for_destruction?)
497
498
  becoming.errors.copy!(errors)
498
499
  end
499
500
 
@@ -17,12 +17,24 @@ module ActiveRecord
17
17
  :and, :or, :annotate, :optimizer_hints, :extending,
18
18
  :having, :create_with, :distinct, :references, :none, :unscope, :merge, :except, :only,
19
19
  :count, :average, :minimum, :maximum, :sum, :calculate,
20
- :pluck, :pick, :ids, :async_ids, :strict_loading, :excluding, :without, :with, :with_recursive,
20
+ :pluck, :pick, :ids, :async_ids, :strict_loading, :excluding, :without, :with_recursive,
21
21
  :async_count, :async_average, :async_minimum, :async_maximum, :async_sum, :async_pluck, :async_pick,
22
22
  :insert, :insert_all, :insert!, :insert_all!, :upsert, :upsert_all
23
23
  ].freeze # :nodoc:
24
24
  delegate(*QUERYING_METHODS, to: :all)
25
25
 
26
+ # Add a Common Table Expression (CTE) that you can then reference within another SELECT statement.
27
+ #
28
+ # See ActiveRecord::QueryMethods#with for more information.
29
+ #
30
+ # When given a block, and the Object#with core extension is loaded, this
31
+ # delegates to it instead, temporarily setting the given attributes on the
32
+ # class for the duration of the block and restoring them afterwards.
33
+ def with(...)
34
+ return super if block_given? && defined?(super)
35
+ all.with(...)
36
+ end
37
+
26
38
  # Executes a custom SQL query against your database and returns all the results. The results will
27
39
  # be returned as an array, with the requested columns encapsulated as attributes of the model you call
28
40
  # this method from. For example, if you call <tt>Product.find_by_sql</tt>, then the results will be returned in
@@ -250,7 +250,7 @@ module ActiveRecord
250
250
  counter_cache[:column] || -"#{active_record.name.demodulize.underscore.pluralize}_count"
251
251
  end
252
252
  else
253
- -((counter_cache && -counter_cache[:column]) || "#{name}_count")
253
+ -((counter_cache && counter_cache[:column]) || "#{name}_count")
254
254
  end
255
255
  end
256
256
  end
@@ -396,7 +396,7 @@ module ActiveRecord
396
396
  relation.select_values = columns
397
397
 
398
398
  result = if relation.where_clause.contradiction?
399
- ActiveRecord::Result.empty
399
+ ActiveRecord::Result.empty(async: @async)
400
400
  else
401
401
  skip_query_cache_if_necessary do
402
402
  model.with_connection do |c|
@@ -493,7 +493,7 @@ module ActiveRecord
493
493
 
494
494
  column = relation.aggregate_column(column_name)
495
495
  select_value = operation_over_aggregate_column(column, operation, distinct)
496
- select_value.distinct = true if operation == "sum" && distinct
496
+ select_value.distinct = true if distinct && (operation == "sum" || operation == "average")
497
497
 
498
498
  relation.select_values = [select_value]
499
499
 
@@ -549,6 +549,7 @@ module ActiveRecord
549
549
  column = relation.aggregate_column(column_name)
550
550
  column_alias = column_alias_tracker.alias_for("#{operation} #{column_name.to_s.downcase}")
551
551
  select_value = operation_over_aggregate_column(column, operation, distinct)
552
+ select_value.distinct = true if distinct && (operation == "sum" || operation == "average")
552
553
  select_value = select_value.as(model.adapter_class.quote_column_name(column_alias))
553
554
 
554
555
  select_values = [select_value]
@@ -572,7 +573,11 @@ module ActiveRecord
572
573
 
573
574
  result.then do |calculated_data|
574
575
  if association
575
- 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
576
581
  key_records = association.klass.base_class.where(association.klass.base_class.primary_key => key_ids)
577
582
  key_records = key_records.index_by(&:id)
578
583
  end
@@ -462,15 +462,15 @@ module ActiveRecord
462
462
  relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
463
463
 
464
464
  if eager_loading && has_limit_or_offset? && !(
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
- )
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
473
472
  )
473
+ )
474
474
  relation = skip_query_cache_if_necessary do
475
475
  model.with_connection do |c|
476
476
  c.distinct_relation_for_primary_key(relation)
@@ -492,15 +492,16 @@ module ActiveRecord
492
492
  def find_with_ids(*ids)
493
493
  raise UnknownPrimaryKey.new(model) if primary_key.nil?
494
494
 
495
+ first_item = ids.first
496
+ return [] if first_item.is_a?(Array) && first_item.empty?
497
+
495
498
  expects_array = if model.composite_primary_key?
496
- ids.first.first.is_a?(Array)
499
+ first_item.first.is_a?(Array)
497
500
  else
498
- ids.first.is_a?(Array)
501
+ first_item.is_a?(Array)
499
502
  end
500
503
 
501
- return [] if expects_array && ids.first.empty?
502
-
503
- ids = ids.first if expects_array
504
+ ids = first_item if expects_array
504
505
 
505
506
  ids = ids.compact.uniq
506
507
 
@@ -542,10 +543,6 @@ module ActiveRecord
542
543
  def find_some(ids)
543
544
  return find_some_ordered(ids) unless order_values.present?
544
545
 
545
- relation = where(primary_key => ids)
546
- relation = relation.select(table[primary_key]) unless select_values.empty?
547
- result = relation.to_a
548
-
549
546
  expected_size =
550
547
  if limit_value && ids.size > limit_value
551
548
  limit_value
@@ -554,10 +551,18 @@ module ActiveRecord
554
551
  end
555
552
 
556
553
  # 11 ids with limit 3, offset 9 should give 2 results.
557
- if offset_value && (ids.size - offset_value < expected_size)
558
- 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
559
560
  end
560
561
 
562
+ relation = where(primary_key => ids)
563
+ relation = relation.select(table[primary_key]) unless select_values.empty?
564
+ result = relation.to_a
565
+
561
566
  if result.size == expected_size
562
567
  result
563
568
  else
@@ -567,6 +572,7 @@ module ActiveRecord
567
572
 
568
573
  def find_some_ordered(ids)
569
574
  ids = ids.slice(offset_value || 0, limit_value || ids.size) || []
575
+ return [] if ids.empty?
570
576
 
571
577
  relation = except(:limit, :offset)
572
578
  relation = relation.where(primary_key => ids)
@@ -574,12 +580,20 @@ module ActiveRecord
574
580
  result = relation.records
575
581
 
576
582
  if result.size == ids.size
577
- 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) })
578
584
  else
579
585
  raise_record_not_found_exception!(ids, result.size, ids.size)
580
586
  end
581
587
  end
582
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
+
583
597
  def find_take
584
598
  if loaded?
585
599
  records.first
@@ -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
@@ -93,7 +93,7 @@ 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)
@@ -721,8 +721,16 @@ module ActiveRecord
721
721
  references = column_references([column])
722
722
  self.references_values |= references unless references.empty?
723
723
 
724
- values = values.map { |value| model.type_caster.type_cast_for_database(column, value) }
725
- arel_column = column.is_a?(Arel::Nodes::SqlLiteral) ? column : order_column(column.to_s)
724
+ if column.is_a?(Arel::Nodes::SqlLiteral)
725
+ arel_column = column
726
+ else
727
+ arel_column = order_column(column.to_s)
728
+
729
+ unless arel_column.is_a?(Arel::Nodes::SqlLiteral)
730
+ values = cast_values_for_in_order_of(values, arel_column.type_caster)
731
+ end
732
+ return spawn.none! if values.empty?
733
+ end
726
734
 
727
735
  scope = spawn.order!(build_case_for_value_position(arel_column, values, filter: filter))
728
736
 
@@ -837,6 +845,26 @@ module ActiveRecord
837
845
  self
838
846
  end
839
847
 
848
+ def table_name_qualified_unscope_values
849
+ self.unscope_values.map do |scope|
850
+ case scope
851
+ when Hash
852
+ scope.transform_values do |target_value|
853
+ case target_value
854
+ when Array
855
+ target_value.map { |value| qualify_attribute_with_table_name(value) }
856
+ when Symbol, String
857
+ qualify_attribute_with_table_name(target_value)
858
+ else
859
+ target_value
860
+ end
861
+ end
862
+ else
863
+ scope
864
+ end
865
+ end
866
+ end
867
+
840
868
  # Performs JOINs on +args+. The given symbol(s) should match the name of
841
869
  # the association(s).
842
870
  #
@@ -1048,13 +1076,13 @@ module ActiveRecord
1048
1076
  # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
1049
1077
  #
1050
1078
  # Post.where(trashed: true).where(trashed: false)
1051
- # # WHERE `trashed` = 1 AND `trashed` = 0
1079
+ # # WHERE `trashed` = true AND `trashed` = false
1052
1080
  #
1053
1081
  # Post.where(trashed: true).rewhere(trashed: false)
1054
- # # WHERE `trashed` = 0
1082
+ # # WHERE `trashed` = false
1055
1083
  #
1056
1084
  # Post.where(active: true).where(trashed: true).rewhere(trashed: false)
1057
- # # WHERE `active` = 1 AND `trashed` = 0
1085
+ # # WHERE `active` = true AND `trashed` = false
1058
1086
  #
1059
1087
  # This is short-hand for <tt>unscope(where: conditions.keys).where(conditions)</tt>.
1060
1088
  # Note that unlike reorder, we're only unscoping the named conditions -- not the entire where statement.
@@ -1076,16 +1104,16 @@ module ActiveRecord
1076
1104
  # end
1077
1105
  #
1078
1106
  # User.where(accepted: true)
1079
- # # WHERE `accepted` = 1
1107
+ # # WHERE `accepted` = true
1080
1108
  #
1081
1109
  # User.where(accepted: true).invert_where
1082
- # # WHERE `accepted` != 1
1110
+ # # WHERE `accepted` != true
1083
1111
  #
1084
1112
  # User.active
1085
- # # WHERE `accepted` = 1 AND `locked` = 0
1113
+ # # WHERE `accepted` = true AND `locked` = false
1086
1114
  #
1087
1115
  # User.active.invert_where
1088
- # # WHERE NOT (`accepted` = 1 AND `locked` = 0)
1116
+ # # WHERE NOT (`accepted` = true AND `locked` = false)
1089
1117
  #
1090
1118
  # Be careful because this inverts all conditions before +invert_where+ call.
1091
1119
  #
@@ -1096,7 +1124,7 @@ module ActiveRecord
1096
1124
  #
1097
1125
  # # It also inverts `where(role: 'admin')` unexpectedly.
1098
1126
  # User.where(role: 'admin').inactive
1099
- # # WHERE NOT (`role` = 'admin' AND `accepted` = 1 AND `locked` = 0)
1127
+ # # WHERE NOT (`role` = 'admin' AND `accepted` = true AND `locked` = false)
1100
1128
  #
1101
1129
  def invert_where
1102
1130
  spawn.invert_where!
@@ -1586,8 +1614,8 @@ module ActiveRecord
1586
1614
  alias :without :excluding
1587
1615
 
1588
1616
  def excluding!(records) # :nodoc:
1589
- predicates = [ predicate_builder[primary_key, records].invert ]
1590
- self.where_clause += Relation::WhereClause.new(predicates)
1617
+ ids = records.map { |record| record.is_a?(model) ? record.id : record }
1618
+ self.where_clause += build_where_clause(primary_key => ids).invert
1591
1619
  self
1592
1620
  end
1593
1621
 
@@ -1898,7 +1926,7 @@ module ActiveRecord
1898
1926
  def build_select(arel)
1899
1927
  if select_values.any?
1900
1928
  arel.project(*arel_columns(select_values))
1901
- elsif model.ignored_columns.any? || model.enumerate_columns_in_select_statements
1929
+ elsif model.ignored_columns.any? || model.enumerate_columns_in_select_statements || model.only_columns.any?
1902
1930
  arel.project(*model.column_names.map { |field| table[field] })
1903
1931
  else
1904
1932
  arel.project(table[Arel.star])
@@ -2205,6 +2233,10 @@ module ActiveRecord
2205
2233
  end
2206
2234
  end
2207
2235
 
2236
+ def qualify_attribute_with_table_name(attr)
2237
+ attr.to_s.include?(".") ? attr : predicate_builder.resolve_arel_attribute(table_name, attr)
2238
+ end
2239
+
2208
2240
  # Checks to make sure that the arguments are not blank. Note that if some
2209
2241
  # blank-like object were initially passed into the query method, then this
2210
2242
  # method will not raise an error.
@@ -2242,6 +2274,24 @@ module ActiveRecord
2242
2274
  end
2243
2275
  end
2244
2276
 
2277
+ def cast_values_for_in_order_of(values, type_caster)
2278
+ bad_value = Symbol # use some object that can not be a valid value
2279
+
2280
+ values = values.map do |value|
2281
+ serialized = type_caster.serialize(value) if type_caster.serializable?(value)
2282
+
2283
+ if value.nil?
2284
+ nil
2285
+ elsif !serialized.nil?
2286
+ serialized
2287
+ else
2288
+ bad_value
2289
+ end
2290
+ end
2291
+
2292
+ values.without(bad_value)
2293
+ end
2294
+
2245
2295
  def arel_column_aliases_from_hash(fields)
2246
2296
  fields.flat_map do |key, columns_aliases|
2247
2297
  table_name = key.is_a?(Symbol) ? key.name : key
@@ -280,7 +280,7 @@ module ActiveRecord
280
280
  record
281
281
  rescue ActiveRecord::RecordNotUnique
282
282
  if connection.transaction_open?
283
- where(attributes).lock.find_by!(attributes)
283
+ rewhere(attributes).lock(connection.create_or_find_by_lock).take!
284
284
  else
285
285
  find_by!(attributes)
286
286
  end
@@ -300,7 +300,7 @@ module ActiveRecord
300
300
  record
301
301
  rescue ActiveRecord::RecordNotUnique
302
302
  if connection.transaction_open?
303
- where(attributes).lock.find_by!(attributes)
303
+ rewhere(attributes).lock(connection.create_or_find_by_lock).take!
304
304
  else
305
305
  find_by!(attributes)
306
306
  end
@@ -381,10 +381,11 @@ module ActiveRecord
381
381
 
382
382
  # Returns true if there are no records.
383
383
  #
384
- # When a pattern argument is given, this method checks whether elements in
385
- # the Enumerable match the pattern via the case-equality operator (<tt>===</tt>).
384
+ # When an argument is given, returns true if no records match the argument
385
+ # via the case-equality operator (<tt>===</tt>).
386
386
  #
387
- # posts.none?(Comment) # => true or false
387
+ # posts.none?(Post) # => true if posts is empty
388
+ # posts.none?(Comment) # => true
388
389
  def none?(*args)
389
390
  return true if @none
390
391
 
@@ -394,10 +395,12 @@ module ActiveRecord
394
395
 
395
396
  # Returns true if there are any records.
396
397
  #
397
- # When a pattern argument is given, this method checks whether elements in
398
- # the Enumerable match the pattern via the case-equality operator (<tt>===</tt>).
399
398
  #
400
- # posts.any?(Post) # => true or false
399
+ # When an argument is given, returns true if at least one records matches
400
+ # the argument via the case-equality operator (<tt>===</tt>).
401
+ #
402
+ # posts.any?(Post) # => true if at least one record
403
+ # posts.any?(Comment) # => false
401
404
  def any?(*args)
402
405
  return false if @none
403
406
 
@@ -407,10 +410,11 @@ module ActiveRecord
407
410
 
408
411
  # Returns true if there is exactly one record.
409
412
  #
410
- # When a pattern argument is given, this method checks whether elements in
411
- # the Enumerable match the pattern via the case-equality operator (<tt>===</tt>).
413
+ # When an argument is given, returns true if exactly one records matches the
414
+ # argument via the case-equality operator (<tt>===</tt>).
412
415
  #
413
- # posts.one?(Post) # => true or false
416
+ # posts.one?(Post) # => true if exactly one record
417
+ # posts.any?(Comment) # => false
414
418
  def one?(*args)
415
419
  return false if @none
416
420
 
@@ -485,7 +489,7 @@ module ActiveRecord
485
489
  if loaded?
486
490
  size = records.size
487
491
  if size > 0
488
- timestamp = records.map { |record| record.read_attribute(timestamp_column) }.max
492
+ timestamp = records.filter_map { |record| record.read_attribute(timestamp_column) }.max
489
493
  end
490
494
  else
491
495
  collection = eager_loading? ? apply_join_dependency : self
@@ -1077,6 +1081,10 @@ module ActiveRecord
1077
1081
  def delete(id_or_array)
1078
1082
  return 0 if id_or_array.nil? || (id_or_array.is_a?(Array) && id_or_array.empty?)
1079
1083
 
1084
+ if model.composite_primary_key? && !id_or_array.first.is_a?(Array)
1085
+ id_or_array = [id_or_array]
1086
+ end
1087
+
1080
1088
  where(model.primary_key => id_or_array).delete_all
1081
1089
  end
1082
1090
 
@@ -15,7 +15,7 @@ module ActiveRecord
15
15
  # t.string :name, null: false
16
16
  # end
17
17
  #
18
- # add_index :authors, :name, :unique
18
+ # add_index :authors, :name, unique: true
19
19
  #
20
20
  # create_table :posts do |t|
21
21
  # t.integer :author_id, null: false
@@ -192,7 +192,7 @@ module ActiveRecord
192
192
  tbl.puts ", force: :cascade do |t|"
193
193
 
194
194
  # then dump all non-primary key columns
195
- columns.sort_by(&:name).each do |column|
195
+ columns.each do |column|
196
196
  raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
197
197
  next if column.name == pk
198
198
 
@@ -70,7 +70,7 @@ module ActiveRecord
70
70
 
71
71
  options = { on_rotation: on_rotation }.compact
72
72
  if id = signed_id_verifier.verified(signed_id, purpose: combine_signed_id_purposes(purpose), **options)
73
- find_by primary_key => id
73
+ find_by(primary_key => [id])
74
74
  end
75
75
  end
76
76
 
@@ -156,7 +156,7 @@ module ActiveRecord
156
156
  @model.find_by_sql(sql, bind_values, preparable: true, allow_retry: @query_builder.retryable, &block)
157
157
  end
158
158
  rescue ::RangeError
159
- async ? Promise.wrap([]) : []
159
+ async ? Promise::Complete.new([]) : []
160
160
  end
161
161
 
162
162
  def self.unsupported_value?(value)
@@ -154,7 +154,8 @@ module ActiveRecord
154
154
  return unless attribute_changed?(store_attribute)
155
155
  prev_store, new_store = changes[store_attribute]
156
156
  accessor = store_accessor_for(store_attribute)
157
- [accessor.get(prev_store, key), accessor.get(new_store, key)]
157
+ prev_value, new_value = accessor.get(prev_store, key), accessor.get(new_store, key)
158
+ [prev_value, new_value] unless prev_value == new_value
158
159
  end
159
160
 
160
161
  define_method("#{accessor_key}_was") do
@@ -175,7 +176,8 @@ module ActiveRecord
175
176
  return unless saved_change_to_attribute?(store_attribute)
176
177
  prev_store, new_store = saved_changes[store_attribute]
177
178
  accessor = store_accessor_for(store_attribute)
178
- [accessor.get(prev_store, key), accessor.get(new_store, key)]
179
+ prev_value, new_value = accessor.get(prev_store, key), accessor.get(new_store, key)
180
+ [prev_value, new_value] unless prev_value == new_value
179
181
  end
180
182
 
181
183
  define_method("#{accessor_key}_before_last_save") do
@@ -58,11 +58,18 @@ module ActiveRecord
58
58
 
59
59
  def run_cmd_error(cmd, args)
60
60
  msg = +"failed to execute:\n"
61
- msg << "#{cmd} #{args.join(' ')}\n\n"
61
+ msg << "#{cmd} #{filter_sensitive_args(args).join(' ')}\n\n"
62
62
  msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
63
63
  msg
64
64
  end
65
65
 
66
+ # The failed command is echoed back, so anything secret it was given has
67
+ # to be kept out of the message.
68
+ def filter_sensitive_args(args)
69
+ filtered = "--password=#{ActiveSupport::ParameterFilter::FILTERED}"
70
+ args.map { |arg| arg.to_s.sub(/\A--password=.+/m, filtered) }
71
+ end
72
+
66
73
  def with_temporary_pool(db_config, migration_class, clobber: false)
67
74
  original_db_config = migration_class.connection_db_config
68
75
  pool = migration_class.connection_handler.establish_connection(db_config, clobber: clobber)
@@ -61,22 +61,7 @@ module ActiveRecord
61
61
  end
62
62
 
63
63
  def prepare_command_options
64
- args = {
65
- host: "--host",
66
- port: "--port",
67
- socket: "--socket",
68
- username: "--user",
69
- password: "--password",
70
- encoding: "--default-character-set",
71
- sslca: "--ssl-ca",
72
- sslcert: "--ssl-cert",
73
- sslcapath: "--ssl-capath",
74
- sslcipher: "--ssl-cipher",
75
- sslkey: "--ssl-key",
76
- ssl_mode: "--ssl-mode"
77
- }.filter_map { |opt, arg| "#{arg}=#{configuration_hash[opt]}" if configuration_hash[opt] }
78
-
79
- args
64
+ db_config.adapter_class.cli_args(configuration_hash)
80
65
  end
81
66
  end
82
67
  end
@@ -16,6 +16,10 @@ module ActiveRecord
16
16
  end
17
17
  end
18
18
 
19
+ ActiveSupport::Testing::Parallelization.run_cleanup_hook do
20
+ ActiveRecord::Base.connection_handler.each_connection_pool.each(&:discard!)
21
+ end
22
+
19
23
  def self.create_and_load_schema(i, env_name:)
20
24
  old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
21
25
 
@@ -263,19 +263,28 @@ module ActiveRecord
263
263
 
264
264
  def teardown_shared_connection_pool
265
265
  handler = ActiveRecord::Base.connection_handler
266
+ removed_pool_configs = []
267
+ pool_managers = handler.send(:connection_name_to_pool_manager)
266
268
 
267
269
  @saved_pool_configs.each_pair do |name, shards|
268
- pool_manager = handler.send(:connection_name_to_pool_manager)[name]
270
+ pool_manager = pool_managers[name]
269
271
  shards.each_pair do |shard_name, roles|
270
272
  roles.each_pair do |role, pool_config|
271
- next unless pool_manager.get_pool_config(role, shard_name)
272
-
273
- pool_manager.set_pool_config(role, shard_name, pool_config)
273
+ if pool_manager.get_pool_config(role, shard_name)
274
+ pool_manager.set_pool_config(role, shard_name, pool_config)
275
+ else
276
+ removed_pool_configs << pool_config
277
+ end
274
278
  end
275
279
  end
276
280
  end
277
281
 
278
282
  @saved_pool_configs.clear
283
+
284
+ remaining_pool_configs = pool_managers.values.flat_map(&:pool_configs)
285
+ removed_pool_configs.compact.uniq.each do |pool_config|
286
+ pool_config.disconnect! unless remaining_pool_configs.include?(pool_config)
287
+ end
279
288
  end
280
289
 
281
290
  def load_fixtures(config)