activerecord 8.0.1 → 8.0.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -1
  3. data/lib/active_record/associations/alias_tracker.rb +6 -4
  4. data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
  5. data/lib/active_record/attributes.rb +2 -2
  6. data/lib/active_record/autosave_association.rb +21 -11
  7. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +17 -14
  8. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +14 -9
  9. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  10. data/lib/active_record/connection_adapters/abstract_adapter.rb +39 -22
  11. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +4 -0
  12. data/lib/active_record/connection_adapters/mysql2_adapter.rb +8 -1
  13. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  14. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +12 -12
  15. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
  16. data/lib/active_record/connection_adapters/postgresql_adapter.rb +7 -2
  17. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -0
  18. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +6 -1
  19. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
  20. data/lib/active_record/core.rb +31 -2
  21. data/lib/active_record/counter_cache.rb +1 -1
  22. data/lib/active_record/delegated_type.rb +17 -17
  23. data/lib/active_record/future_result.rb +3 -3
  24. data/lib/active_record/gem_version.rb +1 -1
  25. data/lib/active_record/migration/command_recorder.rb +5 -2
  26. data/lib/active_record/railties/databases.rake +1 -1
  27. data/lib/active_record/relation/calculations.rb +23 -17
  28. data/lib/active_record/relation.rb +1 -1
  29. data/lib/active_record/signed_id.rb +4 -3
  30. data/lib/active_record/statement_cache.rb +2 -2
  31. data/lib/active_record/transactions.rb +5 -6
  32. data/lib/arel/collectors/bind.rb +1 -1
  33. data/lib/arel/collectors/sql_string.rb +1 -1
  34. data/lib/arel/collectors/substitute_binds.rb +2 -2
  35. data/lib/arel/nodes/binary.rb +1 -1
  36. data/lib/arel/nodes/node.rb +1 -1
  37. data/lib/arel/nodes/sql_literal.rb +1 -1
  38. data/lib/arel/visitors/to_sql.rb +1 -1
  39. metadata +10 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02db25501500ec8083c47291264f535dc9841c45d56a94e9aff93e826addf5f9
4
- data.tar.gz: 504ac046514f4f733317b633f311e47ec0ba358d8d94fb92c9341988a49bd203
3
+ metadata.gz: 82c8ca907ebe4ec74622fb27ce477cdd5a4536e05bb25334c3551560324507cd
4
+ data.tar.gz: 88787eb24c8acb59dc3517d85e919a50d85bf3c0bff149f59f76d2bf6b8495d8
5
5
  SHA512:
6
- metadata.gz: e13e206ccfcf6c7735ba154e0de054d211a49532206dadd5e0a678afd3c84f14a05da270afec364785f0dac69c117985164a61ca2021a82c0b8b724ea52b6ea0
7
- data.tar.gz: 38d0436cb8ed82caf1ecad812d36be93c65a48919695a249e2182fa65504f585744362a35aeb93e9e6f83e285e9b34fed398c75e5ff9c97d9a974a952740ab20
6
+ metadata.gz: ea3714004780235f5bb1c78e8b667d229e642942b3ea12163b923d72fa66668e1bd9fc848c87347d96a83c85d3934e499411118883fd2a9c38779a87f031d83c
7
+ data.tar.gz: 7bacd8bf3eb5712fecdcbba881a2a92f9f4f6fe609c644802b12e423a3f95e7b5953669715ad4215b7170a80f37c70686d2bf090872ef585fb0f4ed8001ff03e
data/CHANGELOG.md CHANGED
@@ -1,6 +1,143 @@
1
+ ## Rails 8.0.2 (March 12, 2025) ##
2
+
3
+ * No changes.
4
+
5
+
6
+ ## Rails 8.0.2 (March 12, 2025) ##
7
+
8
+ * Fix inverting `rename_enum_value` when `:from`/`:to` are provided.
9
+
10
+ *fatkodima*
11
+
12
+ * Prevent persisting invalid record.
13
+
14
+ *Edouard Chin*
15
+
16
+ * Fix inverting `drop_table` without options.
17
+
18
+ *fatkodima*
19
+
20
+ * Fix count with group by qualified name on loaded relation.
21
+
22
+ *Ryuta Kamizono*
23
+
24
+ * Fix `sum` with qualified name on loaded relation.
25
+
26
+ *Chris Gunther*
27
+
28
+ * The SQLite3 adapter quotes non-finite Numeric values like "Infinity" and "NaN".
29
+
30
+ *Mike Dalessio*
31
+
32
+ * Handle libpq returning a database version of 0 on no/bad connection in `PostgreSQLAdapter`.
33
+
34
+ Before, this version would be cached and an error would be raised during connection configuration when
35
+ comparing it with the minimum required version for the adapter. This meant that the connection could
36
+ never be successfully configured on subsequent reconnection attempts.
37
+
38
+ Now, this is treated as a connection failure consistent with libpq, raising a `ActiveRecord::ConnectionFailed`
39
+ and ensuring the version isn't cached, which allows the version to be retrieved on the next connection attempt.
40
+
41
+ *Joshua Young*, *Rian McGuire*
42
+
43
+ * Fix error handling during connection configuration.
44
+
45
+ Active Record wasn't properly handling errors during the connection configuration phase.
46
+ This could lead to a partially configured connection being used, resulting in various exceptions,
47
+ the most common being with the PostgreSQLAdapter raising `undefined method `key?' for nil`
48
+ or `TypeError: wrong argument type nil (expected PG::TypeMap)`.
49
+
50
+ *Jean Boussier*
51
+
52
+ * Fix a case where a non-retryable query could be marked retryable.
53
+
54
+ *Hartley McGuire*
55
+
56
+ * Handle circular references when autosaving associations.
57
+
58
+ *zzak*
59
+
60
+ * PoolConfig no longer keeps a reference to the connection class.
61
+
62
+ Keeping a reference to the class caused subtle issues when combined with reloading in
63
+ development. Fixes #54343.
64
+
65
+ *Mike Dalessio*
66
+
67
+ * Fix SQL notifications sometimes not sent when using async queries.
68
+
69
+ ```ruby
70
+ Post.async_count
71
+ ActiveSupport::Notifications.subscribed(->(*) { "Will never reach here" }) do
72
+ Post.count
73
+ end
74
+ ```
75
+
76
+ In rare circumstances and under the right race condition, Active Support notifications
77
+ would no longer be dispatched after using an asynchronous query.
78
+ This is now fixed.
79
+
80
+ *Edouard Chin*
81
+
82
+ * Fix support for PostgreSQL enum types with commas in their name.
83
+
84
+ *Arthur Hess*
85
+
86
+ * Fix inserts on MySQL with no RETURNING support for a table with multiple auto populated columns.
87
+
88
+ *Nikita Vasilevsky*
89
+
90
+ * Fix joining on a scoped association with string joins and bind parameters.
91
+
92
+ ```ruby
93
+ class Instructor < ActiveRecord::Base
94
+ has_many :instructor_roles, -> { active }
95
+ end
96
+
97
+ class InstructorRole < ActiveRecord::Base
98
+ scope :active, -> {
99
+ joins("JOIN students ON instructor_roles.student_id = students.id")
100
+ .where(students { status: 1 })
101
+ }
102
+ end
103
+
104
+ Instructor.joins(:instructor_roles).first
105
+ ```
106
+
107
+ The above example would result in `ActiveRecord::StatementInvalid` because the
108
+ `active` scope bind parameters would be lost.
109
+
110
+ *Jean Boussier*
111
+
112
+ * Fix a potential race condition with system tests and transactional fixtures.
113
+
114
+ *Sjoerd Lagarde*
115
+
116
+ * Fix autosave associations to no longer validated unmodified associated records.
117
+
118
+ Active Record was incorrectly performing validation on associated record that
119
+ weren't created nor modified as part of the transaction:
120
+
121
+ ```ruby
122
+ Post.create!(author: User.find(1)) # Fail if user is invalid
123
+ ```
124
+
125
+ *Jean Boussier*
126
+
127
+ * Remember when a database connection has recently been verified (for
128
+ two seconds, by default), to avoid repeated reverifications during a
129
+ single request.
130
+
131
+ This should recreate a similar rate of verification as in Rails 7.1,
132
+ where connections are leased for the duration of a request, and thus
133
+ only verified once.
134
+
135
+ *Matthew Draper*
136
+
137
+
1
138
  ## Rails 8.0.1 (December 13, 2024) ##
2
139
 
3
- * Fix removing foreign keys with :restrict action for MySQ
140
+ * Fix removing foreign keys with :restrict action for MySQL.
4
141
 
5
142
  *fatkodima*
6
143
 
@@ -26,16 +26,18 @@ module ActiveRecord
26
26
  end
27
27
 
28
28
  def self.initial_count_for(connection, name, table_joins)
29
- quoted_name = nil
29
+ quoted_name_escaped = nil
30
+ name_escaped = nil
30
31
 
31
32
  counts = table_joins.map do |join|
32
33
  if join.is_a?(Arel::Nodes::StringJoin)
33
- # quoted_name should be case ignored as some database adapters (Oracle) return quoted name in uppercase
34
- quoted_name ||= connection.quote_table_name(name)
34
+ # quoted_name_escaped should be case ignored as some database adapters (Oracle) return quoted name in uppercase
35
+ quoted_name_escaped ||= Regexp.escape(connection.quote_table_name(name))
36
+ name_escaped ||= Regexp.escape(name)
35
37
 
36
38
  # Table names + table aliases
37
39
  join.left.scan(
38
- /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i
40
+ /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name_escaped}|#{name_escaped})\sON/i
39
41
  ).size
40
42
  elsif join.is_a?(Arel::Nodes::Join)
41
43
  join.left.name == name ? 1 : 0
@@ -38,41 +38,39 @@ module ActiveRecord
38
38
  chain << [reflection, table]
39
39
  end
40
40
 
41
- base_klass.with_connection do |connection|
42
- # The chain starts with the target table, but we want to end with it here (makes
43
- # more sense in this context), so we reverse
44
- chain.reverse_each do |reflection, table|
45
- klass = reflection.klass
41
+ # The chain starts with the target table, but we want to end with it here (makes
42
+ # more sense in this context), so we reverse
43
+ chain.reverse_each do |reflection, table|
44
+ klass = reflection.klass
46
45
 
47
- scope = reflection.join_scope(table, foreign_table, foreign_klass)
46
+ scope = reflection.join_scope(table, foreign_table, foreign_klass)
48
47
 
49
- unless scope.references_values.empty?
50
- associations = scope.eager_load_values | scope.includes_values
48
+ unless scope.references_values.empty?
49
+ associations = scope.eager_load_values | scope.includes_values
51
50
 
52
- unless associations.empty?
53
- scope.joins! scope.construct_join_dependency(associations, Arel::Nodes::OuterJoin)
54
- end
51
+ unless associations.empty?
52
+ scope.joins! scope.construct_join_dependency(associations, Arel::Nodes::OuterJoin)
55
53
  end
54
+ end
56
55
 
57
- arel = scope.arel(alias_tracker.aliases)
58
- nodes = arel.constraints.first
56
+ arel = scope.arel(alias_tracker.aliases)
57
+ nodes = arel.constraints.first
59
58
 
60
- if nodes.is_a?(Arel::Nodes::And)
61
- others = nodes.children.extract! do |node|
62
- !Arel.fetch_attribute(node) { |attr| attr.relation.name == table.name }
63
- end
59
+ if nodes.is_a?(Arel::Nodes::And)
60
+ others = nodes.children.extract! do |node|
61
+ !Arel.fetch_attribute(node) { |attr| attr.relation.name == table.name }
64
62
  end
63
+ end
65
64
 
66
- joins << join_type.new(table, Arel::Nodes::On.new(nodes))
65
+ joins << join_type.new(table, Arel::Nodes::On.new(nodes))
67
66
 
68
- if others && !others.empty?
69
- joins.concat arel.join_sources
70
- append_constraints(connection, joins.last, others)
71
- end
72
-
73
- # The current table in this iteration becomes the foreign table in the next
74
- foreign_table, foreign_klass = table, klass
67
+ if others && !others.empty?
68
+ joins.concat arel.join_sources
69
+ append_constraints(joins.last, others)
75
70
  end
71
+
72
+ # The current table in this iteration becomes the foreign table in the next
73
+ foreign_table, foreign_klass = table, klass
76
74
  end
77
75
 
78
76
  joins
@@ -91,10 +89,10 @@ module ActiveRecord
91
89
  end
92
90
 
93
91
  private
94
- def append_constraints(connection, join, constraints)
92
+ def append_constraints(join, constraints)
95
93
  if join.is_a?(Arel::Nodes::StringJoin)
96
94
  join_string = Arel::Nodes::And.new(constraints.unshift join.left)
97
- join.left = Arel.sql(connection.visitor.compile(join_string))
95
+ join.left = join_string
98
96
  else
99
97
  right = join.right
100
98
  right.expr = Arel::Nodes::And.new(constraints.unshift right.expr)
@@ -178,8 +178,8 @@ module ActiveRecord
178
178
  # @currency_converter = currency_converter
179
179
  # end
180
180
  #
181
- # # value will be the result of +deserialize+ or
182
- # # +cast+. Assumed to be an instance of +Money+ in
181
+ # # value will be the result of #deserialize or
182
+ # # #cast. Assumed to be an instance of Money in
183
183
  # # this case.
184
184
  # def serialize(value)
185
185
  # value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
@@ -372,19 +372,29 @@ module ActiveRecord
372
372
  return true if record.destroyed? || (association.options[:autosave] && record.marked_for_destruction?)
373
373
 
374
374
  context = validation_context if custom_validation_context?
375
+ return true if record.valid?(context)
375
376
 
376
- unless valid = record.valid?(context)
377
- if association.options[:autosave]
378
- record.errors.each { |error|
379
- self.errors.objects.append(
380
- Associations::NestedError.new(association, error)
381
- )
382
- }
383
- else
384
- errors.add(association.reflection.name)
385
- end
377
+ if record.changed? || record.new_record? || context
378
+ associated_errors = record.errors.objects
379
+ else
380
+ # If there are existing invalid records in the DB, we should still be able to reference them.
381
+ # Unless a record (no matter where in the association chain) is invalid and is being changed.
382
+ associated_errors = record.errors.objects.select { |error| error.is_a?(Associations::NestedError) }
386
383
  end
387
- valid
384
+
385
+ if association.options[:autosave]
386
+ return if equal?(record)
387
+
388
+ associated_errors.each { |error|
389
+ errors.objects.append(
390
+ Associations::NestedError.new(association, error)
391
+ )
392
+ }
393
+ elsif associated_errors.any?
394
+ errors.add(association.reflection.name)
395
+ end
396
+
397
+ errors.any?
388
398
  end
389
399
 
390
400
  # Is used as an around_save callback to check while saving a collection
@@ -54,19 +54,22 @@ module ActiveRecord
54
54
  # about the model. The model needs to pass a connection specification name to the handler,
55
55
  # in order to look up the correct connection pool.
56
56
  class ConnectionHandler
57
- class StringConnectionName # :nodoc:
58
- attr_reader :name
59
-
60
- def initialize(name)
57
+ class ConnectionDescriptor # :nodoc:
58
+ def initialize(name, primary = false)
61
59
  @name = name
60
+ @primary = primary
61
+ end
62
+
63
+ def name
64
+ primary_class? ? "ActiveRecord::Base" : @name
62
65
  end
63
66
 
64
67
  def primary_class?
65
- false
68
+ @primary
66
69
  end
67
70
 
68
71
  def current_preventing_writes
69
- false
72
+ ActiveRecord::Base.preventing_writes?(@name)
70
73
  end
71
74
  end
72
75
 
@@ -115,7 +118,7 @@ module ActiveRecord
115
118
  pool_config = resolve_pool_config(config, owner_name, role, shard)
116
119
  db_config = pool_config.db_config
117
120
 
118
- pool_manager = set_pool_manager(pool_config.connection_name)
121
+ pool_manager = set_pool_manager(pool_config.connection_descriptor)
119
122
 
120
123
  # If there is an existing pool with the same values as the pool_config
121
124
  # don't remove the connection. Connections should only be removed if we are
@@ -127,8 +130,8 @@ module ActiveRecord
127
130
  # Update the pool_config's connection class if it differs. This is used
128
131
  # for ensuring that ActiveRecord::Base and the primary_abstract_class use
129
132
  # the same pool. Without this granular swapping will not work correctly.
130
- if owner_name.primary_class? && (existing_pool_config.connection_class != owner_name)
131
- existing_pool_config.connection_class = owner_name
133
+ if owner_name.primary_class? && (existing_pool_config.connection_descriptor != owner_name)
134
+ existing_pool_config.connection_descriptor = owner_name
132
135
  end
133
136
 
134
137
  existing_pool_config.pool
@@ -137,7 +140,7 @@ module ActiveRecord
137
140
  pool_manager.set_pool_config(role, shard, pool_config)
138
141
 
139
142
  payload = {
140
- connection_name: pool_config.connection_name,
143
+ connection_name: pool_config.connection_descriptor.name,
141
144
  role: role,
142
145
  shard: shard,
143
146
  config: db_config.configuration_hash
@@ -242,8 +245,8 @@ module ActiveRecord
242
245
  end
243
246
 
244
247
  # Get the existing pool manager or initialize and assign a new one.
245
- def set_pool_manager(connection_name)
246
- connection_name_to_pool_manager[connection_name] ||= PoolManager.new
248
+ def set_pool_manager(connection_descriptor)
249
+ connection_name_to_pool_manager[connection_descriptor.name] ||= PoolManager.new
247
250
  end
248
251
 
249
252
  def pool_managers
@@ -278,9 +281,9 @@ module ActiveRecord
278
281
 
279
282
  def determine_owner_name(owner_name, config)
280
283
  if owner_name.is_a?(String) || owner_name.is_a?(Symbol)
281
- StringConnectionName.new(owner_name.to_s)
284
+ ConnectionDescriptor.new(owner_name.to_s)
282
285
  elsif config.is_a?(Symbol)
283
- StringConnectionName.new(config.to_s)
286
+ ConnectionDescriptor.new(config.to_s)
284
287
  else
285
288
  owner_name
286
289
  end
@@ -36,7 +36,7 @@ module ActiveRecord
36
36
  end
37
37
 
38
38
  def schema_cache; end
39
- def connection_class; end
39
+ def connection_descriptor; end
40
40
  def checkin(_); end
41
41
  def remove(_); end
42
42
  def async_executor; end
@@ -364,8 +364,8 @@ module ActiveRecord
364
364
  clean
365
365
  end
366
366
 
367
- def connection_class # :nodoc:
368
- pool_config.connection_class
367
+ def connection_descriptor # :nodoc:
368
+ pool_config.connection_descriptor
369
369
  end
370
370
 
371
371
  # Returns true if there is an open connection being used for the current thread.
@@ -545,20 +545,25 @@ module ActiveRecord
545
545
  # Raises:
546
546
  # - ActiveRecord::ConnectionTimeoutError no connection can be obtained from the pool.
547
547
  def checkout(checkout_timeout = @checkout_timeout)
548
- if @pinned_connection
549
- @pinned_connection.lock.synchronize do
550
- synchronize do
548
+ return checkout_and_verify(acquire_connection(checkout_timeout)) unless @pinned_connection
549
+
550
+ @pinned_connection.lock.synchronize do
551
+ synchronize do
552
+ # The pinned connection may have been cleaned up before we synchronized, so check if it is still present
553
+ if @pinned_connection
551
554
  @pinned_connection.verify!
555
+
552
556
  # Any leased connection must be in @connections otherwise
553
557
  # some methods like #connected? won't behave correctly
554
558
  unless @connections.include?(@pinned_connection)
555
559
  @connections << @pinned_connection
556
560
  end
561
+
562
+ @pinned_connection
563
+ else
564
+ checkout_and_verify(acquire_connection(checkout_timeout))
557
565
  end
558
566
  end
559
- @pinned_connection
560
- else
561
- checkout_and_verify(acquire_connection(checkout_timeout))
562
567
  end
563
568
  end
564
569
 
@@ -434,7 +434,7 @@ module ActiveRecord
434
434
  #
435
435
  # == Examples
436
436
  #
437
- # # Assuming +td+ is an instance of TableDefinition
437
+ # # Assuming `td` is an instance of TableDefinition
438
438
  # td.column(:granted, :boolean, index: true)
439
439
  #
440
440
  # == Short-hand examples
@@ -150,7 +150,6 @@ module ActiveRecord
150
150
  end
151
151
 
152
152
  @owner = nil
153
- @instrumenter = ActiveSupport::Notifications.instrumenter
154
153
  @pool = ActiveRecord::ConnectionAdapters::NullPool.new
155
154
  @idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
156
155
  @visitor = arel_visitor
@@ -168,6 +167,7 @@ module ActiveRecord
168
167
  @default_timezone = self.class.validate_default_timezone(@config[:default_timezone])
169
168
 
170
169
  @raw_connection_dirty = false
170
+ @last_activity = nil
171
171
  @verified = false
172
172
  end
173
173
 
@@ -190,19 +190,6 @@ module ActiveRecord
190
190
  end
191
191
  end
192
192
 
193
- EXCEPTION_NEVER = { Exception => :never }.freeze # :nodoc:
194
- EXCEPTION_IMMEDIATE = { Exception => :immediate }.freeze # :nodoc:
195
- private_constant :EXCEPTION_NEVER, :EXCEPTION_IMMEDIATE
196
- def with_instrumenter(instrumenter, &block) # :nodoc:
197
- Thread.handle_interrupt(EXCEPTION_NEVER) do
198
- previous_instrumenter = @instrumenter
199
- @instrumenter = instrumenter
200
- Thread.handle_interrupt(EXCEPTION_IMMEDIATE, &block)
201
- ensure
202
- @instrumenter = previous_instrumenter
203
- end
204
- end
205
-
206
193
  def check_if_write_query(sql) # :nodoc:
207
194
  if preventing_writes? && write_query?(sql)
208
195
  raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
@@ -217,6 +204,10 @@ module ActiveRecord
217
204
  (@config[:connection_retries] || 1).to_i
218
205
  end
219
206
 
207
+ def verify_timeout
208
+ (@config[:verify_timeout] || 2).to_i
209
+ end
210
+
220
211
  def retry_deadline
221
212
  if @config[:retry_deadline]
222
213
  @config[:retry_deadline].to_f
@@ -235,9 +226,9 @@ module ActiveRecord
235
226
  # the value of +current_preventing_writes+.
236
227
  def preventing_writes?
237
228
  return true if replica?
238
- return false if connection_class.nil?
229
+ return false if connection_descriptor.nil?
239
230
 
240
- connection_class.current_preventing_writes
231
+ connection_descriptor.current_preventing_writes
241
232
  end
242
233
 
243
234
  def prepared_statements?
@@ -288,8 +279,8 @@ module ActiveRecord
288
279
  @owner = ActiveSupport::IsolatedExecutionState.context
289
280
  end
290
281
 
291
- def connection_class # :nodoc:
292
- @pool.connection_class
282
+ def connection_descriptor # :nodoc:
283
+ @pool.connection_descriptor
293
284
  end
294
285
 
295
286
  # The role (e.g. +:writing+) for the current connection. In a
@@ -343,6 +334,13 @@ module ActiveRecord
343
334
  Process.clock_gettime(Process::CLOCK_MONOTONIC) - @idle_since
344
335
  end
345
336
 
337
+ # Seconds since this connection last communicated with the server
338
+ def seconds_since_last_activity # :nodoc:
339
+ if @raw_connection && @last_activity
340
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - @last_activity
341
+ end
342
+ end
343
+
346
344
  def unprepared_statement
347
345
  cache = prepared_statements_disabled_cache.add?(object_id) if @prepared_statements
348
346
  yield
@@ -670,11 +668,12 @@ module ActiveRecord
670
668
 
671
669
  enable_lazy_transactions!
672
670
  @raw_connection_dirty = false
671
+ @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
673
672
  @verified = true
674
673
 
675
674
  reset_transaction(restore: restore_transactions) do
676
675
  clear_cache!(new_connection: true)
677
- configure_connection
676
+ attempt_configure_connection
678
677
  end
679
678
  rescue => original_exception
680
679
  translated_exception = translate_exception_class(original_exception, nil, nil)
@@ -689,6 +688,7 @@ module ActiveRecord
689
688
  end
690
689
  end
691
690
 
691
+ @last_activity = nil
692
692
  @verified = false
693
693
 
694
694
  raise translated_exception
@@ -726,7 +726,7 @@ module ActiveRecord
726
726
  def reset!
727
727
  clear_cache!(new_connection: true)
728
728
  reset_transaction
729
- configure_connection
729
+ attempt_configure_connection
730
730
  end
731
731
 
732
732
  # Removes the connection from the pool and disconnect it.
@@ -762,7 +762,8 @@ module ActiveRecord
762
762
  if @unconfigured_connection
763
763
  @raw_connection = @unconfigured_connection
764
764
  @unconfigured_connection = nil
765
- configure_connection
765
+ attempt_configure_connection
766
+ @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
766
767
  @verified = true
767
768
  return
768
769
  end
@@ -992,6 +993,9 @@ module ActiveRecord
992
993
  if @verified
993
994
  # Cool, we're confident the connection's ready to use. (Note this might have
994
995
  # become true during the above #materialize_transactions.)
996
+ elsif (last_activity = seconds_since_last_activity) && last_activity < verify_timeout
997
+ # We haven't actually verified the connection since we acquired it, but it
998
+ # has been used very recently. We're going to assume it's still okay.
995
999
  elsif reconnectable
996
1000
  if allow_retry
997
1001
  # Not sure about the connection yet, but if anything goes wrong we can
@@ -1033,6 +1037,7 @@ module ActiveRecord
1033
1037
  # Barring a known-retryable error inside the query (regardless of
1034
1038
  # whether we were in a _position_ to retry it), we should infer that
1035
1039
  # there's likely a real problem with the connection.
1040
+ @last_activity = nil
1036
1041
  @verified = false
1037
1042
  end
1038
1043
 
@@ -1047,6 +1052,7 @@ module ActiveRecord
1047
1052
  # `with_raw_connection` block only when the block is guaranteed to
1048
1053
  # exercise the raw connection.
1049
1054
  def verified!
1055
+ @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1050
1056
  @verified = true
1051
1057
  end
1052
1058
 
@@ -1126,7 +1132,7 @@ module ActiveRecord
1126
1132
  end
1127
1133
 
1128
1134
  def log(sql, name = "SQL", binds = [], type_casted_binds = [], async: false, &block) # :doc:
1129
- @instrumenter.instrument(
1135
+ instrumenter.instrument(
1130
1136
  "sql.active_record",
1131
1137
  sql: sql,
1132
1138
  name: name,
@@ -1142,6 +1148,10 @@ module ActiveRecord
1142
1148
  raise ex.set_query(sql, binds)
1143
1149
  end
1144
1150
 
1151
+ def instrumenter # :nodoc:
1152
+ ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] ||= ActiveSupport::Notifications.instrumenter
1153
+ end
1154
+
1145
1155
  def translate_exception(exception, message:, sql:, binds:)
1146
1156
  # override in derived class
1147
1157
  case exception
@@ -1203,6 +1213,13 @@ module ActiveRecord
1203
1213
  check_version
1204
1214
  end
1205
1215
 
1216
+ def attempt_configure_connection
1217
+ configure_connection
1218
+ rescue
1219
+ disconnect!
1220
+ raise
1221
+ end
1222
+
1206
1223
  def default_prepared_statements
1207
1224
  true
1208
1225
  end
@@ -174,6 +174,10 @@ module ActiveRecord
174
174
  mariadb? && database_version >= "10.5.0"
175
175
  end
176
176
 
177
+ def return_value_after_insert?(column) # :nodoc:
178
+ supports_insert_returning? ? column.auto_populated? : column.auto_increment?
179
+ end
180
+
177
181
  def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
178
182
  query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
179
183
  end
@@ -106,7 +106,14 @@ module ActiveRecord
106
106
  end
107
107
 
108
108
  def active?
109
- connected? && @lock.synchronize { @raw_connection&.ping } || false
109
+ if connected?
110
+ @lock.synchronize do
111
+ if @raw_connection&.ping
112
+ verified!
113
+ true
114
+ end
115
+ end
116
+ end || false
110
117
  end
111
118
 
112
119
  alias :reset! :reconnect!