activerecord 7.2.3 → 8.1.3

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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +612 -1055
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/association_relation.rb +2 -1
  5. data/lib/active_record/associations/association.rb +35 -11
  6. data/lib/active_record/associations/builder/association.rb +23 -11
  7. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  8. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  9. data/lib/active_record/associations/builder/has_one.rb +1 -1
  10. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  11. data/lib/active_record/associations/collection_association.rb +1 -1
  12. data/lib/active_record/associations/collection_proxy.rb +22 -4
  13. data/lib/active_record/associations/deprecation.rb +88 -0
  14. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  15. data/lib/active_record/associations/errors.rb +3 -0
  16. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  17. data/lib/active_record/associations/join_dependency.rb +4 -2
  18. data/lib/active_record/associations/preloader/association.rb +2 -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/singular_association.rb +8 -3
  22. data/lib/active_record/associations.rb +192 -24
  23. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  24. data/lib/active_record/attribute_methods/primary_key.rb +4 -8
  25. data/lib/active_record/attribute_methods/query.rb +34 -0
  26. data/lib/active_record/attribute_methods/serialization.rb +16 -3
  27. data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
  28. data/lib/active_record/attributes.rb +3 -0
  29. data/lib/active_record/autosave_association.rb +69 -27
  30. data/lib/active_record/base.rb +1 -2
  31. data/lib/active_record/coders/json.rb +14 -5
  32. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +35 -28
  33. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -4
  34. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -13
  35. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +412 -88
  36. data/lib/active_record/connection_adapters/abstract/database_statements.rb +137 -75
  37. data/lib/active_record/connection_adapters/abstract/query_cache.rb +27 -5
  38. data/lib/active_record/connection_adapters/abstract/quoting.rb +16 -25
  39. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +11 -7
  40. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +32 -35
  41. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  42. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +122 -32
  43. data/lib/active_record/connection_adapters/abstract/transaction.rb +40 -8
  44. data/lib/active_record/connection_adapters/abstract_adapter.rb +150 -91
  45. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +63 -52
  46. data/lib/active_record/connection_adapters/column.rb +17 -4
  47. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  48. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  49. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  50. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +41 -10
  51. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +73 -46
  52. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +89 -94
  53. data/lib/active_record/connection_adapters/mysql2_adapter.rb +2 -10
  54. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  55. data/lib/active_record/connection_adapters/postgresql/column.rb +8 -2
  56. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +76 -45
  57. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +3 -3
  58. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  59. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  60. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  61. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  62. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +9 -17
  63. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +14 -33
  64. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +71 -32
  65. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +139 -63
  66. data/lib/active_record/connection_adapters/postgresql_adapter.rb +78 -105
  67. data/lib/active_record/connection_adapters/schema_cache.rb +3 -5
  68. data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
  69. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +90 -98
  70. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  71. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  72. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +27 -2
  73. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +13 -14
  74. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +102 -37
  75. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  76. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +38 -67
  77. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -18
  78. data/lib/active_record/connection_adapters.rb +1 -56
  79. data/lib/active_record/connection_handling.rb +25 -2
  80. data/lib/active_record/core.rb +33 -17
  81. data/lib/active_record/counter_cache.rb +33 -8
  82. data/lib/active_record/database_configurations/database_config.rb +9 -1
  83. data/lib/active_record/database_configurations/hash_config.rb +67 -9
  84. data/lib/active_record/database_configurations/url_config.rb +13 -3
  85. data/lib/active_record/database_configurations.rb +7 -3
  86. data/lib/active_record/delegated_type.rb +1 -1
  87. data/lib/active_record/dynamic_matchers.rb +54 -69
  88. data/lib/active_record/encryption/config.rb +3 -1
  89. data/lib/active_record/encryption/encryptable_record.rb +8 -8
  90. data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
  91. data/lib/active_record/encryption/encryptor.rb +28 -8
  92. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  93. data/lib/active_record/encryption/scheme.rb +9 -2
  94. data/lib/active_record/enum.rb +33 -30
  95. data/lib/active_record/errors.rb +33 -9
  96. data/lib/active_record/explain.rb +1 -1
  97. data/lib/active_record/explain_registry.rb +51 -2
  98. data/lib/active_record/filter_attribute_handler.rb +73 -0
  99. data/lib/active_record/fixtures.rb +2 -4
  100. data/lib/active_record/future_result.rb +15 -9
  101. data/lib/active_record/gem_version.rb +2 -2
  102. data/lib/active_record/inheritance.rb +1 -1
  103. data/lib/active_record/insert_all.rb +14 -9
  104. data/lib/active_record/locking/optimistic.rb +8 -1
  105. data/lib/active_record/locking/pessimistic.rb +5 -0
  106. data/lib/active_record/log_subscriber.rb +3 -13
  107. data/lib/active_record/middleware/shard_selector.rb +34 -17
  108. data/lib/active_record/migration/command_recorder.rb +45 -12
  109. data/lib/active_record/migration/compatibility.rb +37 -24
  110. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  111. data/lib/active_record/migration.rb +48 -42
  112. data/lib/active_record/model_schema.rb +38 -13
  113. data/lib/active_record/nested_attributes.rb +6 -6
  114. data/lib/active_record/persistence.rb +162 -133
  115. data/lib/active_record/query_cache.rb +22 -15
  116. data/lib/active_record/query_logs.rb +100 -52
  117. data/lib/active_record/query_logs_formatter.rb +17 -28
  118. data/lib/active_record/querying.rb +8 -8
  119. data/lib/active_record/railtie.rb +35 -30
  120. data/lib/active_record/railties/controller_runtime.rb +11 -6
  121. data/lib/active_record/railties/databases.rake +26 -38
  122. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  123. data/lib/active_record/railties/job_runtime.rb +10 -11
  124. data/lib/active_record/reflection.rb +53 -21
  125. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  126. data/lib/active_record/relation/batches.rb +147 -73
  127. data/lib/active_record/relation/calculations.rb +52 -40
  128. data/lib/active_record/relation/delegation.rb +25 -15
  129. data/lib/active_record/relation/finder_methods.rb +40 -24
  130. data/lib/active_record/relation/merger.rb +8 -8
  131. data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
  132. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -9
  133. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +8 -8
  134. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  135. data/lib/active_record/relation/predicate_builder.rb +22 -7
  136. data/lib/active_record/relation/query_attribute.rb +3 -1
  137. data/lib/active_record/relation/query_methods.rb +140 -86
  138. data/lib/active_record/relation/spawn_methods.rb +7 -7
  139. data/lib/active_record/relation/where_clause.rb +2 -9
  140. data/lib/active_record/relation.rb +107 -75
  141. data/lib/active_record/result.rb +109 -24
  142. data/lib/active_record/runtime_registry.rb +42 -58
  143. data/lib/active_record/sanitization.rb +9 -6
  144. data/lib/active_record/schema_dumper.rb +18 -11
  145. data/lib/active_record/schema_migration.rb +2 -1
  146. data/lib/active_record/scoping/named.rb +5 -2
  147. data/lib/active_record/scoping.rb +0 -1
  148. data/lib/active_record/signed_id.rb +43 -15
  149. data/lib/active_record/statement_cache.rb +24 -20
  150. data/lib/active_record/store.rb +51 -22
  151. data/lib/active_record/structured_event_subscriber.rb +85 -0
  152. data/lib/active_record/table_metadata.rb +6 -23
  153. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  154. data/lib/active_record/tasks/database_tasks.rb +85 -85
  155. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -42
  156. data/lib/active_record/tasks/postgresql_database_tasks.rb +7 -40
  157. data/lib/active_record/tasks/sqlite_database_tasks.rb +16 -28
  158. data/lib/active_record/test_databases.rb +14 -4
  159. data/lib/active_record/test_fixtures.rb +39 -2
  160. data/lib/active_record/testing/query_assertions.rb +8 -2
  161. data/lib/active_record/timestamp.rb +4 -2
  162. data/lib/active_record/token_for.rb +1 -1
  163. data/lib/active_record/transaction.rb +2 -5
  164. data/lib/active_record/transactions.rb +37 -16
  165. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  166. data/lib/active_record/type/internal/timezone.rb +7 -0
  167. data/lib/active_record/type/json.rb +13 -2
  168. data/lib/active_record/type/serialized.rb +16 -4
  169. data/lib/active_record/type/type_map.rb +1 -1
  170. data/lib/active_record/type_caster/connection.rb +2 -1
  171. data/lib/active_record/validations/associated.rb +1 -1
  172. data/lib/active_record/validations/uniqueness.rb +8 -8
  173. data/lib/active_record.rb +84 -49
  174. data/lib/arel/alias_predication.rb +2 -0
  175. data/lib/arel/collectors/bind.rb +2 -2
  176. data/lib/arel/collectors/sql_string.rb +1 -1
  177. data/lib/arel/collectors/substitute_binds.rb +2 -2
  178. data/lib/arel/crud.rb +6 -11
  179. data/lib/arel/nodes/binary.rb +1 -1
  180. data/lib/arel/nodes/count.rb +2 -2
  181. data/lib/arel/nodes/function.rb +4 -10
  182. data/lib/arel/nodes/named_function.rb +2 -2
  183. data/lib/arel/nodes/node.rb +2 -2
  184. data/lib/arel/nodes/sql_literal.rb +1 -1
  185. data/lib/arel/nodes.rb +0 -2
  186. data/lib/arel/predications.rb +1 -3
  187. data/lib/arel/select_manager.rb +7 -2
  188. data/lib/arel/table.rb +3 -7
  189. data/lib/arel/visitors/dot.rb +0 -3
  190. data/lib/arel/visitors/postgresql.rb +55 -0
  191. data/lib/arel/visitors/sqlite.rb +55 -8
  192. data/lib/arel/visitors/to_sql.rb +3 -21
  193. data/lib/arel.rb +3 -1
  194. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  195. metadata +16 -13
  196. data/lib/active_record/explain_subscriber.rb +0 -34
  197. data/lib/active_record/normalization.rb +0 -163
  198. data/lib/active_record/relation/record_fetch_warning.rb +0 -52
@@ -87,6 +87,8 @@ module ActiveRecord
87
87
 
88
88
  connections = []
89
89
 
90
+ @shard_keys = shards.keys
91
+
90
92
  if shards.empty?
91
93
  shards[:default] = database
92
94
  end
@@ -98,7 +100,8 @@ module ActiveRecord
98
100
  db_config = resolve_config_for_connection(database_key)
99
101
 
100
102
  self.connection_class = true
101
- 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)
102
105
  end
103
106
  end
104
107
 
@@ -177,6 +180,18 @@ module ActiveRecord
177
180
  end
178
181
  end
179
182
 
183
+ # Passes the block to +connected_to+ for every +shard+ the
184
+ # model is configured to connect to (if any), and returns the
185
+ # results in an array.
186
+ #
187
+ # Optionally, +role+ and/or +prevent_writes+ can be passed which
188
+ # will be forwarded to each +connected_to+ call.
189
+ def connected_to_all_shards(role: nil, prevent_writes: false, &blk)
190
+ shard_keys.map do |shard|
191
+ connected_to(shard: shard, role: role, prevent_writes: prevent_writes, &blk)
192
+ end
193
+ end
194
+
180
195
  # Use a specified connection.
181
196
  #
182
197
  # This method is useful for ensuring that a specific connection is
@@ -289,7 +304,7 @@ module ActiveRecord
289
304
 
290
305
  # Checkouts a connection from the pool, yield it and then check it back in.
291
306
  # If a connection was already leased via #lease_connection or a parent call to
292
- # #with_connection, that same connection is yieled.
307
+ # #with_connection, that same connection is yielded.
293
308
  # If #lease_connection is called inside the block, the connection won't be checked
294
309
  # back in.
295
310
  # If #connection is called inside the block, the connection won't be checked back in
@@ -361,6 +376,14 @@ module ActiveRecord
361
376
  connection_pool.schema_cache.clear!
362
377
  end
363
378
 
379
+ def shard_keys
380
+ connection_class_for_self.instance_variable_get(:@shard_keys) || []
381
+ end
382
+
383
+ def sharded?
384
+ shard_keys.any?
385
+ end
386
+
364
387
  private
365
388
  def resolve_config_for_connection(config_or_env)
366
389
  raise "Anonymous class is not allowed." unless name
@@ -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
 
@@ -89,6 +88,7 @@ module ActiveRecord
89
88
  class_attribute :belongs_to_required_by_default, instance_accessor: false
90
89
 
91
90
  class_attribute :strict_loading_by_default, instance_accessor: false, default: false
91
+ class_attribute :strict_loading_mode, instance_accessor: false, default: :all
92
92
 
93
93
  class_attribute :has_many_inversing, instance_accessor: false, default: false
94
94
 
@@ -111,7 +111,7 @@ module ActiveRecord
111
111
  # Post.attributes_for_inspect = [:id, :title]
112
112
  # Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!">"
113
113
  #
114
- # 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:
115
115
  #
116
116
  # Post.attributes_for_inspect = :all
117
117
  # Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
@@ -201,6 +201,17 @@ module ActiveRecord
201
201
  false
202
202
  end
203
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
+
204
215
  def self.connected_to_stack # :nodoc:
205
216
  if connected_to_stack = ActiveSupport::IsolatedExecutionState[:active_record_connected_to_stack]
206
217
  connected_to_stack
@@ -346,6 +357,8 @@ module ActiveRecord
346
357
  def filter_attributes=(filter_attributes)
347
358
  @inspection_filter = nil
348
359
  @filter_attributes = filter_attributes
360
+
361
+ FilterAttributeHandler.sensitive_attribute_was_declared(self, filter_attributes)
349
362
  end
350
363
 
351
364
  def inspection_filter # :nodoc:
@@ -381,7 +394,7 @@ module ActiveRecord
381
394
  end
382
395
 
383
396
  def predicate_builder # :nodoc:
384
- @predicate_builder ||= PredicateBuilder.new(table_metadata)
397
+ @predicate_builder ||= PredicateBuilder.new(TableMetadata.new(self, arel_table))
385
398
  end
386
399
 
387
400
  def type_caster # :nodoc:
@@ -426,10 +439,6 @@ module ActiveRecord
426
439
  end
427
440
  end
428
441
 
429
- def table_metadata
430
- TableMetadata.new(self, arel_table)
431
- end
432
-
433
442
  def cached_find_by(keys, values)
434
443
  with_connection do |connection|
435
444
  statement = cached_find_by_statement(connection, keys) { |params|
@@ -443,8 +452,8 @@ module ActiveRecord
443
452
  where(wheres).limit(1)
444
453
  }
445
454
 
446
- begin
447
- statement.execute(values.flatten, connection, allow_retry: true).first
455
+ statement.execute(values.flatten, connection).then do |r|
456
+ r.first
448
457
  rescue TypeError
449
458
  raise ActiveRecord::StatementInvalid
450
459
  end
@@ -540,12 +549,7 @@ module ActiveRecord
540
549
 
541
550
  ##
542
551
  def initialize_dup(other) # :nodoc:
543
- @attributes = @attributes.deep_dup
544
- if self.class.composite_primary_key?
545
- @primary_key.each { |key| @attributes.reset(key) }
546
- else
547
- @attributes.reset(@primary_key)
548
- end
552
+ @attributes = init_attributes(other)
549
553
 
550
554
  _run_initialize_callbacks
551
555
 
@@ -557,6 +561,18 @@ module ActiveRecord
557
561
  super
558
562
  end
559
563
 
564
+ def init_attributes(_) # :nodoc:
565
+ attrs = @attributes.deep_dup
566
+
567
+ if self.class.composite_primary_key?
568
+ @primary_key.each { |key| attrs.reset(key) }
569
+ else
570
+ attrs.reset(@primary_key)
571
+ end
572
+
573
+ attrs
574
+ end
575
+
560
576
  # Populate +coder+ with attributes about this record that should be
561
577
  # serialized. The structure of +coder+ defined in this method is
562
578
  # guaranteed to match the structure of +coder+ passed to the #init_with
@@ -626,7 +642,7 @@ module ActiveRecord
626
642
  def hash
627
643
  id = self.id
628
644
 
629
- if primary_key_values_present?
645
+ if self.class.composite_primary_key? ? primary_key_values_present? : id
630
646
  self.class.hash ^ id.hash
631
647
  else
632
648
  super
@@ -830,7 +846,7 @@ module ActiveRecord
830
846
 
831
847
  @primary_key = klass.primary_key
832
848
  @strict_loading = klass.strict_loading_by_default
833
- @strict_loading_mode = :all
849
+ @strict_loading_mode = klass.strict_loading_mode
834
850
 
835
851
  klass.define_attribute_methods
836
852
  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
+ # # For posts with ids #1 and #2, reset the comments_count
32
+ # Post.reset_counters([1, 2], :comments)
33
+ #
31
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
@@ -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
 
@@ -99,6 +103,10 @@ module ActiveRecord
99
103
  def use_metadata_table?
100
104
  raise NotImplementedError
101
105
  end
106
+
107
+ def seeds?
108
+ raise NotImplementedError
109
+ end
102
110
  end
103
111
  end
104
112
  end
@@ -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
@@ -130,6 +153,14 @@ module ActiveRecord
130
153
  Base.configurations.primary?(name)
131
154
  end
132
155
 
156
+ # Determines whether the db:prepare task should seed the database from db/seeds.rb.
157
+ #
158
+ # If the `seeds` key is present in the config, `seeds?` will return its value. Otherwise, it
159
+ # will return `true` for the primary database and `false` for all other configs.
160
+ def seeds?
161
+ configuration_hash.fetch(:seeds, primary?)
162
+ end
163
+
133
164
  # Determines whether to dump the schema/structure files and the filename that
134
165
  # should be used.
135
166
  #
@@ -138,7 +169,7 @@ module ActiveRecord
138
169
  #
139
170
  # If the config option is set that will be used. Otherwise Rails will generate
140
171
  # the filename from the database config name.
141
- def schema_dump(format = ActiveRecord.schema_format)
172
+ def schema_dump(format = schema_format)
142
173
  if configuration_hash.key?(:schema_dump)
143
174
  if config = configuration_hash[:schema_dump]
144
175
  config
@@ -150,6 +181,12 @@ module ActiveRecord
150
181
  end
151
182
  end
152
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
+
153
190
  def database_tasks? # :nodoc:
154
191
  !replica? && !!configuration_hash.fetch(:database_tasks, true)
155
192
  end
@@ -160,13 +197,34 @@ module ActiveRecord
160
197
 
161
198
  private
162
199
  def schema_file_type(format)
163
- case format
200
+ case format.to_sym
164
201
  when :ruby
165
202
  "schema.rb"
166
203
  when :sql
167
204
  "structure.sql"
168
205
  end
169
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
170
228
  end
171
229
  end
172
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
@@ -229,7 +229,7 @@ module ActiveRecord
229
229
  # @entry.message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
230
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
 
@@ -7,16 +7,18 @@ module ActiveRecord
7
7
  if self == Base
8
8
  super
9
9
  else
10
- match = Method.match(self, name)
11
- match && match.valid? || super
10
+ super || begin
11
+ match = Method.match(name)
12
+ match && match.valid?(self, name)
13
+ end
12
14
  end
13
15
  end
14
16
 
15
17
  def method_missing(name, ...)
16
- match = Method.match(self, name)
18
+ match = Method.match(name)
17
19
 
18
- if match && match.valid?
19
- match.define
20
+ if match && match.valid?(self, name)
21
+ match.define(self, name)
20
22
  send(name, ...)
21
23
  else
22
24
  super
@@ -24,97 +26,80 @@ module ActiveRecord
24
26
  end
25
27
 
26
28
  class Method
27
- @matchers = []
28
-
29
29
  class << self
30
- attr_reader :matchers
31
-
32
- def match(model, name)
33
- klass = matchers.find { |k| k.pattern.match?(name) }
34
- klass.new(model, name) if klass
30
+ def match(name)
31
+ FindBy.match?(name) || FindByBang.match?(name)
35
32
  end
36
33
 
37
- def pattern
38
- @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
34
+ def valid?(model, name)
35
+ attribute_names(model, name.to_s).all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
39
36
  end
40
37
 
41
- def prefix
42
- raise NotImplementedError
38
+ def define(model, name)
39
+ model.class_eval <<-CODE, __FILE__, __LINE__ + 1
40
+ def self.#{name}(#{signature(model, name)})
41
+ #{body(model, name)}
42
+ end
43
+ CODE
43
44
  end
44
45
 
45
- def suffix
46
- ""
47
- end
48
- end
46
+ private
47
+ def make_pattern(prefix, suffix)
48
+ /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
49
+ end
49
50
 
50
- attr_reader :model, :name, :attribute_names
51
+ def attribute_names(model, name)
52
+ attribute_names = name.match(pattern)[1].split("_and_")
53
+ attribute_names.map! { |name| model.attribute_aliases[name] || name }
54
+ end
51
55
 
52
- def initialize(model, method_name)
53
- @model = model
54
- @name = method_name.to_s
55
- @attribute_names = @name.match(self.class.pattern)[1].split("_and_")
56
- @attribute_names.map! { |name| @model.attribute_aliases[name] || name }
57
- end
56
+ def body(model, method_name)
57
+ "#{finder}(#{attributes_hash(model, method_name)})"
58
+ end
58
59
 
59
- def valid?
60
- attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
61
- end
60
+ # The parameters in the signature may have reserved Ruby words, in order
61
+ # to prevent errors, we start each param name with `_`.
62
+ def signature(model, method_name)
63
+ attribute_names(model, method_name.to_s).map { |name| "_#{name}" }.join(", ")
64
+ end
62
65
 
63
- def define
64
- model.class_eval <<-CODE, __FILE__, __LINE__ + 1
65
- def self.#{name}(#{signature})
66
- #{body}
66
+ # Given that the parameters starts with `_`, the finder needs to use the
67
+ # same parameter name.
68
+ def attributes_hash(model, method_name)
69
+ "{" + attribute_names(model, method_name).map { |name| ":#{name} => _#{name}" }.join(",") + "}"
67
70
  end
68
- CODE
69
71
  end
72
+ end
70
73
 
71
- private
72
- def body
73
- "#{finder}(#{attributes_hash})"
74
- end
74
+ class FindBy < Method
75
+ @pattern = make_pattern("find_by", "")
75
76
 
76
- # The parameters in the signature may have reserved Ruby words, in order
77
- # to prevent errors, we start each param name with `_`.
78
- def signature
79
- attribute_names.map { |name| "_#{name}" }.join(", ")
80
- end
77
+ class << self
78
+ attr_reader :pattern
81
79
 
82
- # Given that the parameters starts with `_`, the finder needs to use the
83
- # same parameter name.
84
- def attributes_hash
85
- "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(",") + "}"
80
+ def match?(name)
81
+ pattern.match?(name) && self
86
82
  end
87
83
 
88
84
  def finder
89
- raise NotImplementedError
85
+ "find_by"
90
86
  end
91
- end
92
-
93
- class FindBy < Method
94
- Method.matchers << self
95
-
96
- def self.prefix
97
- "find_by"
98
- end
99
-
100
- def finder
101
- "find_by"
102
87
  end
103
88
  end
104
89
 
105
90
  class FindByBang < Method
106
- Method.matchers << self
91
+ @pattern = make_pattern("find_by", "!")
107
92
 
108
- def self.prefix
109
- "find_by"
110
- end
93
+ class << self
94
+ attr_reader :pattern
111
95
 
112
- def self.suffix
113
- "!"
114
- end
96
+ def match?(name)
97
+ pattern.match?(name) && self
98
+ end
115
99
 
116
- def finder
117
- "find_by!"
100
+ def finder
101
+ "find_by!"
102
+ end
118
103
  end
119
104
  end
120
105
  end