activerecord 8.0.0 → 8.1.2

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 (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +703 -248
  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 +1 -1
  7. data/lib/active_record/associations/belongs_to_association.rb +18 -2
  8. data/lib/active_record/associations/builder/association.rb +16 -5
  9. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  10. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  11. data/lib/active_record/associations/builder/has_one.rb +1 -1
  12. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  13. data/lib/active_record/associations/collection_association.rb +3 -3
  14. data/lib/active_record/associations/collection_proxy.rb +22 -4
  15. data/lib/active_record/associations/deprecation.rb +88 -0
  16. data/lib/active_record/associations/errors.rb +3 -0
  17. data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
  18. data/lib/active_record/associations/join_dependency.rb +4 -2
  19. data/lib/active_record/associations/preloader/batch.rb +7 -1
  20. data/lib/active_record/associations/preloader/branch.rb +1 -0
  21. data/lib/active_record/associations.rb +159 -21
  22. data/lib/active_record/attribute_methods/primary_key.rb +2 -1
  23. data/lib/active_record/attribute_methods/query.rb +34 -0
  24. data/lib/active_record/attribute_methods/serialization.rb +17 -4
  25. data/lib/active_record/attribute_methods/time_zone_conversion.rb +10 -2
  26. data/lib/active_record/attribute_methods.rb +23 -18
  27. data/lib/active_record/attributes.rb +40 -26
  28. data/lib/active_record/autosave_association.rb +22 -12
  29. data/lib/active_record/base.rb +3 -4
  30. data/lib/active_record/coders/json.rb +14 -5
  31. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +19 -18
  32. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -3
  33. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -12
  34. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +458 -108
  35. data/lib/active_record/connection_adapters/abstract/database_statements.rb +55 -40
  36. data/lib/active_record/connection_adapters/abstract/query_cache.rb +36 -9
  37. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  38. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  39. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +31 -35
  40. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  41. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +89 -23
  42. data/lib/active_record/connection_adapters/abstract/transaction.rb +25 -3
  43. data/lib/active_record/connection_adapters/abstract_adapter.rb +154 -64
  44. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +57 -20
  45. data/lib/active_record/connection_adapters/column.rb +17 -4
  46. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  47. data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -1
  48. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  49. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  50. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +33 -4
  51. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +66 -15
  52. data/lib/active_record/connection_adapters/mysql2_adapter.rb +9 -3
  53. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  54. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
  55. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +26 -17
  56. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  57. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  58. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  59. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +8 -6
  60. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +22 -33
  61. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +67 -31
  62. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +82 -49
  63. data/lib/active_record/connection_adapters/postgresql_adapter.rb +38 -10
  64. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  65. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +39 -23
  66. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -8
  67. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +14 -2
  68. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +5 -12
  69. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +65 -36
  70. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  71. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
  72. data/lib/active_record/connection_adapters/trilogy_adapter.rb +2 -2
  73. data/lib/active_record/connection_adapters.rb +1 -0
  74. data/lib/active_record/connection_handling.rb +15 -10
  75. data/lib/active_record/core.rb +44 -12
  76. data/lib/active_record/counter_cache.rb +34 -9
  77. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
  78. data/lib/active_record/database_configurations/database_config.rb +5 -1
  79. data/lib/active_record/database_configurations/hash_config.rb +59 -9
  80. data/lib/active_record/database_configurations/url_config.rb +13 -3
  81. data/lib/active_record/database_configurations.rb +7 -3
  82. data/lib/active_record/delegated_type.rb +19 -19
  83. data/lib/active_record/dynamic_matchers.rb +54 -69
  84. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  85. data/lib/active_record/encryption/encrypted_attribute_type.rb +2 -2
  86. data/lib/active_record/encryption/encryptor.rb +39 -25
  87. data/lib/active_record/encryption/scheme.rb +1 -1
  88. data/lib/active_record/enum.rb +37 -20
  89. data/lib/active_record/errors.rb +23 -7
  90. data/lib/active_record/explain.rb +1 -1
  91. data/lib/active_record/explain_registry.rb +51 -2
  92. data/lib/active_record/filter_attribute_handler.rb +73 -0
  93. data/lib/active_record/fixture_set/table_row.rb +19 -2
  94. data/lib/active_record/fixtures.rb +2 -2
  95. data/lib/active_record/future_result.rb +3 -3
  96. data/lib/active_record/gem_version.rb +2 -2
  97. data/lib/active_record/inheritance.rb +1 -1
  98. data/lib/active_record/insert_all.rb +12 -7
  99. data/lib/active_record/locking/optimistic.rb +7 -0
  100. data/lib/active_record/locking/pessimistic.rb +5 -0
  101. data/lib/active_record/log_subscriber.rb +2 -6
  102. data/lib/active_record/middleware/shard_selector.rb +34 -17
  103. data/lib/active_record/migration/command_recorder.rb +19 -3
  104. data/lib/active_record/migration/compatibility.rb +34 -24
  105. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  106. data/lib/active_record/migration.rb +31 -21
  107. data/lib/active_record/model_schema.rb +36 -10
  108. data/lib/active_record/nested_attributes.rb +2 -0
  109. data/lib/active_record/persistence.rb +34 -3
  110. data/lib/active_record/query_cache.rb +22 -15
  111. data/lib/active_record/query_logs.rb +7 -7
  112. data/lib/active_record/querying.rb +4 -4
  113. data/lib/active_record/railtie.rb +35 -6
  114. data/lib/active_record/railties/controller_runtime.rb +11 -6
  115. data/lib/active_record/railties/databases.rake +24 -20
  116. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  117. data/lib/active_record/railties/job_runtime.rb +10 -11
  118. data/lib/active_record/reflection.rb +35 -0
  119. data/lib/active_record/relation/batches.rb +25 -11
  120. data/lib/active_record/relation/calculations.rb +54 -38
  121. data/lib/active_record/relation/delegation.rb +0 -1
  122. data/lib/active_record/relation/finder_methods.rb +42 -25
  123. data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -9
  124. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +7 -7
  125. data/lib/active_record/relation/predicate_builder.rb +9 -7
  126. data/lib/active_record/relation/query_attribute.rb +4 -2
  127. data/lib/active_record/relation/query_methods.rb +43 -32
  128. data/lib/active_record/relation/spawn_methods.rb +6 -6
  129. data/lib/active_record/relation/where_clause.rb +10 -11
  130. data/lib/active_record/relation.rb +43 -19
  131. data/lib/active_record/result.rb +44 -21
  132. data/lib/active_record/runtime_registry.rb +42 -58
  133. data/lib/active_record/sanitization.rb +2 -0
  134. data/lib/active_record/schema_dumper.rb +42 -22
  135. data/lib/active_record/scoping.rb +0 -1
  136. data/lib/active_record/secure_token.rb +3 -3
  137. data/lib/active_record/signed_id.rb +47 -18
  138. data/lib/active_record/statement_cache.rb +15 -11
  139. data/lib/active_record/store.rb +44 -19
  140. data/lib/active_record/structured_event_subscriber.rb +85 -0
  141. data/lib/active_record/table_metadata.rb +5 -20
  142. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  143. data/lib/active_record/tasks/database_tasks.rb +44 -45
  144. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
  145. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  146. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  147. data/lib/active_record/test_databases.rb +14 -4
  148. data/lib/active_record/test_fixtures.rb +27 -2
  149. data/lib/active_record/testing/query_assertions.rb +8 -2
  150. data/lib/active_record/timestamp.rb +4 -2
  151. data/lib/active_record/transaction.rb +2 -5
  152. data/lib/active_record/transactions.rb +39 -16
  153. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  154. data/lib/active_record/type/internal/timezone.rb +7 -0
  155. data/lib/active_record/type/json.rb +15 -2
  156. data/lib/active_record/type/serialized.rb +11 -4
  157. data/lib/active_record/type/type_map.rb +1 -1
  158. data/lib/active_record/type_caster/connection.rb +2 -1
  159. data/lib/active_record/validations/associated.rb +1 -1
  160. data/lib/active_record.rb +71 -6
  161. data/lib/arel/alias_predication.rb +2 -0
  162. data/lib/arel/collectors/bind.rb +1 -1
  163. data/lib/arel/collectors/sql_string.rb +1 -1
  164. data/lib/arel/collectors/substitute_binds.rb +2 -2
  165. data/lib/arel/crud.rb +8 -11
  166. data/lib/arel/delete_manager.rb +5 -0
  167. data/lib/arel/nodes/binary.rb +1 -1
  168. data/lib/arel/nodes/count.rb +2 -2
  169. data/lib/arel/nodes/delete_statement.rb +4 -2
  170. data/lib/arel/nodes/function.rb +4 -10
  171. data/lib/arel/nodes/named_function.rb +2 -2
  172. data/lib/arel/nodes/node.rb +2 -2
  173. data/lib/arel/nodes/sql_literal.rb +1 -1
  174. data/lib/arel/nodes/update_statement.rb +4 -2
  175. data/lib/arel/nodes.rb +0 -2
  176. data/lib/arel/select_manager.rb +13 -4
  177. data/lib/arel/update_manager.rb +5 -0
  178. data/lib/arel/visitors/dot.rb +2 -3
  179. data/lib/arel/visitors/postgresql.rb +55 -0
  180. data/lib/arel/visitors/sqlite.rb +55 -8
  181. data/lib/arel/visitors/to_sql.rb +6 -22
  182. data/lib/arel.rb +3 -1
  183. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  184. metadata +16 -15
  185. data/lib/active_record/explain_subscriber.rb +0 -34
  186. data/lib/active_record/normalization.rb +0 -163
@@ -100,7 +100,8 @@ module ActiveRecord
100
100
  db_config = resolve_config_for_connection(database_key)
101
101
 
102
102
  self.connection_class = true
103
- connections << connection_handler.establish_connection(db_config, owner_name: self, role: role, shard: shard.to_sym)
103
+ shard = shard.to_sym unless shard.is_a? Integer
104
+ connections << connection_handler.establish_connection(db_config, owner_name: self, role: role, shard: shard)
104
105
  end
105
106
  end
106
107
 
@@ -172,9 +173,11 @@ module ActiveRecord
172
173
  prevent_writes = true if role == ActiveRecord.reading_role
173
174
 
174
175
  append_to_connected_to_stack(role: role, shard: shard, prevent_writes: prevent_writes, klasses: classes)
175
- yield
176
- ensure
177
- connected_to_stack.pop
176
+ begin
177
+ yield
178
+ ensure
179
+ connected_to_stack.pop
180
+ end
178
181
  end
179
182
 
180
183
  # Passes the block to +connected_to+ for every +shard+ the
@@ -301,7 +304,7 @@ module ActiveRecord
301
304
 
302
305
  # Checkouts a connection from the pool, yield it and then check it back in.
303
306
  # If a connection was already leased via #lease_connection or a parent call to
304
- # #with_connection, that same connection is yieled.
307
+ # #with_connection, that same connection is yielded.
305
308
  # If #lease_connection is called inside the block, the connection won't be checked
306
309
  # back in.
307
310
  # If #connection is called inside the block, the connection won't be checked back in
@@ -395,11 +398,13 @@ module ActiveRecord
395
398
  prevent_writes = true if role == ActiveRecord.reading_role
396
399
 
397
400
  append_to_connected_to_stack(role: role, shard: shard, prevent_writes: prevent_writes, klasses: [self])
398
- return_value = yield
399
- return_value.load if return_value.is_a? ActiveRecord::Relation
400
- return_value
401
- ensure
402
- self.connected_to_stack.pop
401
+ begin
402
+ return_value = yield
403
+ return_value.load if return_value.is_a? ActiveRecord::Relation
404
+ return_value
405
+ ensure
406
+ self.connected_to_stack.pop
407
+ end
403
408
  end
404
409
 
405
410
  def append_to_connected_to_stack(entry)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/enumerable"
4
- require "active_support/core_ext/module/delegation"
5
4
  require "active_support/parameter_filter"
6
5
  require "concurrent/map"
7
6
 
@@ -112,7 +111,7 @@ module ActiveRecord
112
111
  # Post.attributes_for_inspect = [:id, :title]
113
112
  # Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!">"
114
113
  #
115
- # When set to `:all` inspect will list all the record's attributes:
114
+ # When set to +:all+ inspect will list all the record's attributes:
116
115
  #
117
116
  # Post.attributes_for_inspect = :all
118
117
  # Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
@@ -202,6 +201,17 @@ module ActiveRecord
202
201
  false
203
202
  end
204
203
 
204
+ # Intended to behave like `.current_preventing_writes` given the class name as input.
205
+ # See PoolConfig and ConnectionHandler::ConnectionDescriptor.
206
+ def self.preventing_writes?(class_name) # :nodoc:
207
+ connected_to_stack.reverse_each do |hash|
208
+ return hash[:prevent_writes] if !hash[:prevent_writes].nil? && hash[:klasses].include?(Base)
209
+ return hash[:prevent_writes] if !hash[:prevent_writes].nil? && hash[:klasses].any? { |klass| klass.name == class_name }
210
+ end
211
+
212
+ false
213
+ end
214
+
205
215
  def self.connected_to_stack # :nodoc:
206
216
  if connected_to_stack = ActiveSupport::IsolatedExecutionState[:active_record_connected_to_stack]
207
217
  connected_to_stack
@@ -266,7 +276,7 @@ module ActiveRecord
266
276
  return super if StatementCache.unsupported_value?(id)
267
277
 
268
278
  cached_find_by([primary_key], [id]) ||
269
- raise(RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", name, primary_key, id))
279
+ raise(RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id.inspect}", name, primary_key, id))
270
280
  end
271
281
 
272
282
  def find_by(*args) # :nodoc:
@@ -347,6 +357,8 @@ module ActiveRecord
347
357
  def filter_attributes=(filter_attributes)
348
358
  @inspection_filter = nil
349
359
  @filter_attributes = filter_attributes
360
+
361
+ FilterAttributeHandler.sensitive_attribute_was_declared(self, filter_attributes)
350
362
  end
351
363
 
352
364
  def inspection_filter # :nodoc:
@@ -440,7 +452,7 @@ module ActiveRecord
440
452
  where(wheres).limit(1)
441
453
  }
442
454
 
443
- statement.execute(values.flatten, connection, allow_retry: true).then do |r|
455
+ statement.execute(values.flatten, connection).then do |r|
444
456
  r.first
445
457
  rescue TypeError
446
458
  raise ActiveRecord::StatementInvalid
@@ -589,7 +601,7 @@ module ActiveRecord
589
601
  #
590
602
  # topic = Topic.new(title: "Budget", author_name: "Jason")
591
603
  # topic.slice(:title, :author_name)
592
- # => { "title" => "Budget", "author_name" => "Jason" }
604
+ # # => { "title" => "Budget", "author_name" => "Jason" }
593
605
  #
594
606
  #--
595
607
  # Implemented by ActiveModel::Access#slice.
@@ -603,7 +615,7 @@ module ActiveRecord
603
615
  #
604
616
  # topic = Topic.new(title: "Budget", author_name: "Jason")
605
617
  # topic.values_at(:title, :author_name)
606
- # => ["Budget", "Jason"]
618
+ # # => ["Budget", "Jason"]
607
619
  #
608
620
  #--
609
621
  # Implemented by ActiveModel::Access#values_at.
@@ -630,7 +642,7 @@ module ActiveRecord
630
642
  def hash
631
643
  id = self.id
632
644
 
633
- if primary_key_values_present?
645
+ if self.class.composite_primary_key? ? primary_key_values_present? : id
634
646
  self.class.hash ^ id.hash
635
647
  else
636
648
  super
@@ -680,12 +692,14 @@ module ActiveRecord
680
692
  # Sets the record to strict_loading mode. This will raise an error
681
693
  # if the record tries to lazily load an association.
682
694
  #
695
+ # NOTE: Strict loading is disabled during validation in order to let the record validate its association.
696
+ #
683
697
  # user = User.first
684
698
  # user.strict_loading! # => true
685
699
  # user.address.city
686
- # => ActiveRecord::StrictLoadingViolationError
700
+ # # => ActiveRecord::StrictLoadingViolationError
687
701
  # user.comments.to_a
688
- # => ActiveRecord::StrictLoadingViolationError
702
+ # # => ActiveRecord::StrictLoadingViolationError
689
703
  #
690
704
  # ==== Parameters
691
705
  #
@@ -705,7 +719,7 @@ module ActiveRecord
705
719
  # user.address.city # => "Tatooine"
706
720
  # user.comments.to_a # => [#<Comment:0x00...]
707
721
  # user.comments.first.ratings.to_a
708
- # => ActiveRecord::StrictLoadingViolationError
722
+ # # => ActiveRecord::StrictLoadingViolationError
709
723
  def strict_loading!(value = true, mode: :all)
710
724
  unless [:all, :n_plus_one_only].include?(mode)
711
725
  raise ArgumentError, "The :mode option must be one of [:all, :n_plus_one_only] but #{mode.inspect} was provided."
@@ -727,11 +741,29 @@ module ActiveRecord
727
741
  @strict_loading_mode == :all
728
742
  end
729
743
 
730
- # Marks this record as read only.
744
+ # Prevents records from being written to the database:
745
+ #
746
+ # customer = Customer.new
747
+ # customer.readonly!
748
+ # customer.save # raises ActiveRecord::ReadOnlyRecord
749
+ #
750
+ # customer = Customer.first
751
+ # customer.readonly!
752
+ # customer.update(name: 'New Name') # raises ActiveRecord::ReadOnlyRecord
753
+ #
754
+ # Read-only records cannot be deleted from the database either:
731
755
  #
732
756
  # customer = Customer.first
733
757
  # customer.readonly!
734
- # customer.save # Raises an ActiveRecord::ReadOnlyRecord
758
+ # customer.destroy # raises ActiveRecord::ReadOnlyRecord
759
+ #
760
+ # Please, note that the objects themselves are still mutable in memory:
761
+ #
762
+ # customer = Customer.new
763
+ # customer.readonly!
764
+ # customer.name = 'New Name' # OK
765
+ #
766
+ # but you won't be able to persist the changes.
735
767
  def readonly!
736
768
  @readonly = true
737
769
  end
@@ -17,7 +17,7 @@ module ActiveRecord
17
17
  #
18
18
  # ==== Parameters
19
19
  #
20
- # * +id+ - The id of the object you wish to reset a counter on.
20
+ # * +id+ - The id of the object you wish to reset a counter on or an array of ids.
21
21
  # * +counters+ - One or more association counters to reset. Association name or counter name can be given.
22
22
  # * <tt>:touch</tt> - Touch timestamp columns when updating.
23
23
  # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
@@ -28,13 +28,25 @@ module ActiveRecord
28
28
  # # For the Post with id #1, reset the comments_count
29
29
  # Post.reset_counters(1, :comments)
30
30
  #
31
- # # Like above, but also touch the +updated_at+ and/or +updated_on+
31
+ # # For posts with ids #1 and #2, reset the comments_count
32
+ # Post.reset_counters([1, 2], :comments)
33
+ #
34
+ # # Like above, but also touch the updated_at and/or updated_on
32
35
  # # attributes.
33
36
  # Post.reset_counters(1, :comments, touch: true)
34
37
  def reset_counters(id, *counters, touch: nil)
35
- object = find(id)
38
+ ids = if composite_primary_key?
39
+ if id.first.is_a?(Array)
40
+ id
41
+ else
42
+ [id]
43
+ end
44
+ else
45
+ Array(id)
46
+ end
47
+
48
+ updates = Hash.new { |h, k| h[k] = {} }
36
49
 
37
- updates = {}
38
50
  counters.each do |counter_association|
39
51
  has_many_association = _reflect_on_association(counter_association)
40
52
  unless has_many_association
@@ -48,14 +60,22 @@ module ActiveRecord
48
60
  has_many_association = has_many_association.through_reflection
49
61
  end
50
62
 
63
+ counter_association = counter_association.to_sym
51
64
  foreign_key = has_many_association.foreign_key.to_s
52
65
  child_class = has_many_association.klass
53
66
  reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
54
67
  counter_name = reflection.counter_cache_column
55
68
 
56
- count_was = object.send(counter_name)
57
- count = object.send(counter_association).count(:all)
58
- updates[counter_name] = count if count != count_was
69
+ counts =
70
+ unscoped
71
+ .joins(counter_association)
72
+ .where(primary_key => ids)
73
+ .group(primary_key)
74
+ .count(:all)
75
+
76
+ ids.each do |id|
77
+ updates[id].merge!(counter_name => counts[id] || 0)
78
+ end
59
79
  end
60
80
 
61
81
  if touch
@@ -63,10 +83,15 @@ module ActiveRecord
63
83
  names = Array.wrap(names)
64
84
  options = names.extract_options!
65
85
  touch_updates = touch_attributes_with_time(*names, **options)
66
- updates.merge!(touch_updates)
86
+
87
+ updates.each_value do |record_updates|
88
+ record_updates.merge!(touch_updates)
89
+ end
67
90
  end
68
91
 
69
- unscoped.where(primary_key => [object.id]).update_all(updates) if updates.any?
92
+ updates.each do |id, record_updates|
93
+ unscoped.where(primary_key => [id]).update_all(record_updates)
94
+ end
70
95
 
71
96
  true
72
97
  end
@@ -81,7 +81,9 @@ module ActiveRecord
81
81
 
82
82
  def resolved_adapter
83
83
  adapter = uri.scheme && @uri.scheme.tr("-", "_")
84
- adapter = ActiveRecord.protocol_adapters[adapter] || adapter
84
+ if adapter && ActiveRecord.protocol_adapters[adapter]
85
+ adapter = ActiveRecord.protocol_adapters[adapter]
86
+ end
85
87
  adapter
86
88
  end
87
89
 
@@ -48,7 +48,11 @@ module ActiveRecord
48
48
  raise NotImplementedError
49
49
  end
50
50
 
51
- def pool
51
+ def min_connections
52
+ raise NotImplementedError
53
+ end
54
+
55
+ def max_connections
52
56
  raise NotImplementedError
53
57
  end
54
58
 
@@ -38,6 +38,7 @@ module ActiveRecord
38
38
  def initialize(env_name, name, configuration_hash)
39
39
  super(env_name, name)
40
40
  @configuration_hash = configuration_hash.symbolize_keys.freeze
41
+ validate_configuration!
41
42
  end
42
43
 
43
44
  # Determines whether a database configuration is for a replica / readonly
@@ -69,16 +70,35 @@ module ActiveRecord
69
70
  @configuration_hash = configuration_hash.merge(database: database).freeze
70
71
  end
71
72
 
72
- def pool
73
- (configuration_hash[:pool] || 5).to_i
73
+ def max_connections
74
+ max_connections = configuration_hash.fetch(:max_connections) {
75
+ configuration_hash.fetch(:pool, 5)
76
+ }&.to_i
77
+ max_connections if max_connections && max_connections >= 0
74
78
  end
75
79
 
80
+ def min_connections
81
+ (configuration_hash[:min_connections] || 0).to_i
82
+ end
83
+
84
+ alias :pool :max_connections
85
+ deprecate pool: :max_connections, deprecator: ActiveRecord.deprecator
86
+
76
87
  def min_threads
77
88
  (configuration_hash[:min_threads] || 0).to_i
78
89
  end
79
90
 
80
91
  def max_threads
81
- (configuration_hash[:max_threads] || pool).to_i
92
+ (configuration_hash[:max_threads] || (max_connections || 5).clamp(0, 5)).to_i
93
+ end
94
+
95
+ def max_age
96
+ v = configuration_hash[:max_age]&.to_i
97
+ if v && v > 0
98
+ v
99
+ else
100
+ Float::INFINITY
101
+ end
82
102
  end
83
103
 
84
104
  def query_cache
@@ -93,10 +113,8 @@ module ActiveRecord
93
113
  (configuration_hash[:checkout_timeout] || 5).to_f
94
114
  end
95
115
 
96
- # `reaping_frequency` is configurable mostly for historical reasons, but it
97
- # could also be useful if someone wants a very low `idle_timeout`.
98
- def reaping_frequency
99
- configuration_hash.fetch(:reaping_frequency, 60)&.to_f
116
+ def reaping_frequency # :nodoc:
117
+ configuration_hash.fetch(:reaping_frequency, default_reaping_frequency)&.to_f
100
118
  end
101
119
 
102
120
  def idle_timeout
@@ -104,6 +122,11 @@ module ActiveRecord
104
122
  timeout if timeout > 0
105
123
  end
106
124
 
125
+ def keepalive
126
+ keepalive = (configuration_hash[:keepalive] || 600).to_f
127
+ keepalive if keepalive > 0
128
+ end
129
+
107
130
  def adapter
108
131
  configuration_hash[:adapter]&.to_s
109
132
  end
@@ -146,7 +169,7 @@ module ActiveRecord
146
169
  #
147
170
  # If the config option is set that will be used. Otherwise Rails will generate
148
171
  # the filename from the database config name.
149
- def schema_dump(format = ActiveRecord.schema_format)
172
+ def schema_dump(format = schema_format)
150
173
  if configuration_hash.key?(:schema_dump)
151
174
  if config = configuration_hash[:schema_dump]
152
175
  config
@@ -158,6 +181,12 @@ module ActiveRecord
158
181
  end
159
182
  end
160
183
 
184
+ def schema_format # :nodoc:
185
+ format = configuration_hash.fetch(:schema_format, ActiveRecord.schema_format).to_sym
186
+ raise "Invalid schema format" unless [:ruby, :sql].include?(format)
187
+ format
188
+ end
189
+
161
190
  def database_tasks? # :nodoc:
162
191
  !replica? && !!configuration_hash.fetch(:database_tasks, true)
163
192
  end
@@ -168,13 +197,34 @@ module ActiveRecord
168
197
 
169
198
  private
170
199
  def schema_file_type(format)
171
- case format
200
+ case format.to_sym
172
201
  when :ruby
173
202
  "schema.rb"
174
203
  when :sql
175
204
  "structure.sql"
176
205
  end
177
206
  end
207
+
208
+ def default_reaping_frequency
209
+ # Reap every 20 seconds by default, but run more often as necessary to
210
+ # meet other configured timeouts.
211
+ [20, idle_timeout, max_age, keepalive].compact.min
212
+ end
213
+
214
+ def validate_configuration!
215
+ if configuration_hash[:pool] && configuration_hash[:max_connections]
216
+ pool_val = configuration_hash[:pool].to_i
217
+ max_conn_val = configuration_hash[:max_connections].to_i
218
+
219
+ if pool_val != max_conn_val
220
+ raise "Ambiguous configuration: 'pool' (#{pool_val}) and 'max_connections' (#{max_conn_val}) are set to different values. Prefer just 'max_connections'."
221
+ end
222
+ end
223
+
224
+ if configuration_hash[:pool] && configuration_hash[:min_connections]
225
+ raise "Ambiguous configuration: when setting 'min_connections', use 'max_connections' instead of 'pool'."
226
+ end
227
+ end
178
228
  end
179
229
  end
180
230
  end
@@ -47,9 +47,8 @@ module ActiveRecord
47
47
  @configuration_hash[:schema_dump] = false
48
48
  end
49
49
 
50
- if @configuration_hash[:query_cache] == "false"
51
- @configuration_hash[:query_cache] = false
52
- end
50
+ query_cache = parse_query_cache
51
+ @configuration_hash[:query_cache] = query_cache unless query_cache.nil?
53
52
 
54
53
  to_boolean!(@configuration_hash, :replica)
55
54
  to_boolean!(@configuration_hash, :database_tasks)
@@ -58,6 +57,17 @@ module ActiveRecord
58
57
  end
59
58
 
60
59
  private
60
+ def parse_query_cache
61
+ case value = @configuration_hash[:query_cache]
62
+ when /\A\d+\z/
63
+ value.to_i
64
+ when "false"
65
+ false
66
+ else
67
+ value
68
+ end
69
+ end
70
+
61
71
  def to_boolean!(configuration_hash, key)
62
72
  if configuration_hash[key].is_a?(String)
63
73
  configuration_hash[key] = configuration_hash[key] != "false"
@@ -36,9 +36,11 @@ module ActiveRecord
36
36
  # to respond to `sharded?`. To implement this define the following in an
37
37
  # initializer:
38
38
  #
39
- # ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
40
- # next unless config.key?(:vitess)
41
- # VitessConfig.new(env_name, name, config)
39
+ # ActiveSupport.on_load(:active_record_database_configurations) do
40
+ # ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
41
+ # next unless config.key?(:vitess)
42
+ # VitessConfig.new(env_name, name, config)
43
+ # end
42
44
  # end
43
45
  #
44
46
  # Note: applications must handle the condition in which custom config should be
@@ -306,4 +308,6 @@ module ActiveRecord
306
308
  url
307
309
  end
308
310
  end
311
+
312
+ ActiveSupport.run_load_hooks(:active_record_database_configurations, DatabaseConfigurations)
309
313
  end
@@ -181,16 +181,16 @@ module ActiveRecord
181
181
  # delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
182
182
  # end
183
183
  #
184
- # Entry#entryable_class # => +Message+ or +Comment+
185
- # Entry#entryable_name # => "message" or "comment"
186
- # Entry.messages # => Entry.where(entryable_type: "Message")
187
- # Entry#message? # => true when entryable_type == "Message"
188
- # Entry#message # => returns the message record, when entryable_type == "Message", otherwise nil
189
- # Entry#message_id # => returns entryable_id, when entryable_type == "Message", otherwise nil
190
- # Entry.comments # => Entry.where(entryable_type: "Comment")
191
- # Entry#comment? # => true when entryable_type == "Comment"
192
- # Entry#comment # => returns the comment record, when entryable_type == "Comment", otherwise nil
193
- # Entry#comment_id # => returns entryable_id, when entryable_type == "Comment", otherwise nil
184
+ # @entry.entryable_class # => Message or Comment
185
+ # @entry.entryable_name # => "message" or "comment"
186
+ # Entry.messages # => Entry.where(entryable_type: "Message")
187
+ # @entry.message? # => true when entryable_type == "Message"
188
+ # @entry.message # => returns the message record, when entryable_type == "Message", otherwise nil
189
+ # @entry.message_id # => returns entryable_id, when entryable_type == "Message", otherwise nil
190
+ # Entry.comments # => Entry.where(entryable_type: "Comment")
191
+ # @entry.comment? # => true when entryable_type == "Comment"
192
+ # @entry.comment # => returns the comment record, when entryable_type == "Comment", otherwise nil
193
+ # @entry.comment_id # => returns entryable_id, when entryable_type == "Comment", otherwise nil
194
194
  #
195
195
  # You can also declare namespaced types:
196
196
  #
@@ -199,25 +199,25 @@ module ActiveRecord
199
199
  # end
200
200
  #
201
201
  # Entry.access_notice_messages
202
- # entry.access_notice_message
203
- # entry.access_notice_message?
202
+ # @entry.access_notice_message
203
+ # @entry.access_notice_message?
204
204
  #
205
- # === Options
205
+ # ==== Options
206
206
  #
207
207
  # The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc.
208
208
  # The following options can be included to specialize the behavior of the delegated type convenience methods.
209
209
  #
210
- # [:foreign_key]
210
+ # [+:foreign_key+]
211
211
  # Specify the foreign key used for the convenience methods. By default this is guessed to be the passed
212
212
  # +role+ with an "_id" suffix. So a class that defines a
213
213
  # <tt>delegated_type :entryable, types: %w[ Message Comment ]</tt> association will use "entryable_id" as
214
214
  # the default <tt>:foreign_key</tt>.
215
- # [:foreign_type]
215
+ # [+:foreign_type+]
216
216
  # Specify the column used to store the associated object's type. By default this is inferred to be the passed
217
217
  # +role+ with a "_type" suffix. A class that defines a
218
218
  # <tt>delegated_type :entryable, types: %w[ Message Comment ]</tt> association will use "entryable_type" as
219
219
  # the default <tt>:foreign_type</tt>.
220
- # [:primary_key]
220
+ # [+:primary_key+]
221
221
  # Specify the method that returns the primary key of associated object used for the convenience methods.
222
222
  # By default this is +id+.
223
223
  #
@@ -226,10 +226,10 @@ module ActiveRecord
226
226
  # delegated_type :entryable, types: %w[ Message Comment ], primary_key: :uuid, foreign_key: :entryable_uuid
227
227
  # end
228
228
  #
229
- # Entry#message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
230
- # Entry#comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil
229
+ # @entry.message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
230
+ # @entry.comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil
231
231
  def delegated_type(role, types:, **options)
232
- belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
232
+ belongs_to role, options.delete(:scope), **options, polymorphic: true
233
233
  define_delegated_type_methods role, types: types, options: options
234
234
  end
235
235