activerecord 8.1.0.beta1 → 8.1.0.rc1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -4
  3. data/lib/active_record/associations/belongs_to_association.rb +2 -0
  4. data/lib/active_record/attribute_methods/time_zone_conversion.rb +10 -2
  5. data/lib/active_record/attribute_methods.rb +1 -1
  6. data/lib/active_record/autosave_association.rb +2 -2
  7. data/lib/active_record/base.rb +2 -3
  8. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +1 -3
  9. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +31 -29
  10. data/lib/active_record/connection_adapters/abstract/database_statements.rb +32 -13
  11. data/lib/active_record/connection_adapters/abstract/query_cache.rb +17 -8
  12. data/lib/active_record/connection_adapters/abstract/transaction.rb +9 -0
  13. data/lib/active_record/connection_adapters/abstract_adapter.rb +25 -11
  14. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +0 -2
  15. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +1 -1
  16. data/lib/active_record/connection_adapters/mysql2_adapter.rb +0 -2
  17. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
  18. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +8 -1
  19. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +7 -5
  20. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
  21. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +7 -10
  22. data/lib/active_record/connection_adapters/postgresql_adapter.rb +16 -5
  23. data/lib/active_record/connection_handling.rb +2 -1
  24. data/lib/active_record/database_configurations/hash_config.rb +5 -2
  25. data/lib/active_record/encryption/encryptor.rb +12 -0
  26. data/lib/active_record/errors.rb +3 -3
  27. data/lib/active_record/explain.rb +1 -1
  28. data/lib/active_record/explain_registry.rb +51 -1
  29. data/lib/active_record/gem_version.rb +1 -1
  30. data/lib/active_record/log_subscriber.rb +1 -1
  31. data/lib/active_record/migration/compatibility.rb +1 -1
  32. data/lib/active_record/model_schema.rb +26 -3
  33. data/lib/active_record/railties/controller_runtime.rb +11 -6
  34. data/lib/active_record/railties/databases.rake +1 -1
  35. data/lib/active_record/railties/job_runtime.rb +2 -2
  36. data/lib/active_record/relation/batches.rb +3 -3
  37. data/lib/active_record/relation/merger.rb +2 -2
  38. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -9
  39. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +7 -7
  40. data/lib/active_record/relation/predicate_builder.rb +7 -5
  41. data/lib/active_record/relation/query_methods.rb +1 -1
  42. data/lib/active_record/relation/where_clause.rb +2 -0
  43. data/lib/active_record/relation.rb +1 -1
  44. data/lib/active_record/runtime_registry.rb +41 -58
  45. data/lib/active_record/structured_event_subscriber.rb +85 -0
  46. data/lib/active_record/table_metadata.rb +5 -20
  47. data/lib/active_record/tasks/database_tasks.rb +24 -14
  48. data/lib/active_record/test_databases.rb +4 -2
  49. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  50. metadata +9 -9
  51. data/lib/active_record/explain_subscriber.rb +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 001cba0b3b77d6277eef2a61e4b29dc9e80f57d5fcabba9e8c930e093c5aa92b
4
- data.tar.gz: 4bc817edbfa741aeaaea1c77939b0554299ded73ed637e39125169997cd45e72
3
+ metadata.gz: 0f6a8286538d4ed09b1508bed7ded4b5b40629d71cbee9467c8926968413da05
4
+ data.tar.gz: ef5f9492d1efa2285bb03fdfe9d808e86ffadc768f0265257aba7d93bd5c32ad
5
5
  SHA512:
6
- metadata.gz: f82cb78b1415324583ab7a4040c21d5460f0a8d7e0209e2b6b1accb68481199d61334fc9e2db07aa18270ef5b2f584d0fa25492e34ab867e3ee3005f9069aee6
7
- data.tar.gz: 5639613e40ba1b80ce9202d3c25caa407e1298f2a09db29f152f84b2c4ab5bdbf81b42b1d09a5fbbc87ff117c8c081efd49aa93f57fd8a38050054afcd0ee0a2
6
+ metadata.gz: cad145e87e54f6bfd6cc35294e4aa25471f705e08eae4497a10c75af5ff204b8205728bf41e0ff43d1a2713c712d53b56ba35fbdfdb2df8a3204edc8c9c46eef
7
+ data.tar.gz: a8792d05c5c847e1c22daff04e84a662afd6847f07368fea7be61876c434f6e233f1d295c296f0c40adaa24896def1f985edc241c589489e4153554a4b15d5c7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,108 @@
1
+ ## Rails 8.1.0.rc1 (October 15, 2025) ##
2
+
3
+ * Add replicas to test database parallelization setup.
4
+
5
+ Setup and configuration of databases for parallel testing now includes replicas.
6
+
7
+ This fixes an issue when using a replica database, database selector middleware,
8
+ and non-transactional tests, where integration tests running in parallel would select
9
+ the base test database, i.e. `db_test`, instead of the numbered parallel worker database,
10
+ i.e. `db_test_{n}`.
11
+
12
+ *Adam Maas*
13
+
14
+ * Support virtual (not persisted) generated columns on PostgreSQL 18+
15
+
16
+ PostgreSQL 18 introduces virtual (not persisted) generated columns,
17
+ which are now the default unless the `stored: true` option is explicitly specified on PostgreSQL 18+.
18
+
19
+ ```ruby
20
+ create_table :users do |t|
21
+ t.string :name
22
+ t.virtual :lower_name, type: :string, as: "LOWER(name)", stored: false
23
+ t.virtual :name_length, type: :integer, as: "LENGTH(name)"
24
+ end
25
+ ```
26
+
27
+ *Yasuo Honda*
28
+
29
+ * Optimize schema dumping to prevent duplicate file generation.
30
+
31
+ `ActiveRecord::Tasks::DatabaseTasks.dump_all` now tracks which schema files
32
+ have already been dumped and skips dumping the same file multiple times.
33
+ This improves performance when multiple database configurations share the
34
+ same schema dump path.
35
+
36
+ *Mikey Gough*, *Hartley McGuire*
37
+
38
+ * Add structured events for Active Record:
39
+ - `active_record.strict_loading_violation`
40
+ - `active_record.sql`
41
+
42
+ *Gannon McGibbon*
43
+
44
+ * Add support for integer shard keys.
45
+ ```ruby
46
+ # Now accepts symbols as shard keys.
47
+ ActiveRecord::Base.connects_to(shards: {
48
+ 1: { writing: :primary_shard_one, reading: :primary_shard_one },
49
+ 2: { writing: :primary_shard_two, reading: :primary_shard_two},
50
+ })
51
+
52
+ ActiveRecord::Base.connected_to(shard: 1) do
53
+ # ..
54
+ end
55
+ ```
56
+
57
+ *Nony Dutton*
58
+
59
+ * Add `ActiveRecord::Base.only_columns`
60
+
61
+ Similar in use case to `ignored_columns` but listing columns to consider rather than the ones
62
+ to ignore.
63
+
64
+ Can be useful when working with a legacy or shared database schema, or to make safe schema change
65
+ in two deploys rather than three.
66
+
67
+ *Anton Kandratski*
68
+
69
+ * Use `PG::Connection#close_prepared` (protocol level Close) to deallocate
70
+ prepared statements when available.
71
+
72
+ To enable its use, you must have pg >= 1.6.0, libpq >= 17, and a PostgreSQL
73
+ database version >= 17.
74
+
75
+ *Hartley McGuire*, *Andrew Jackson*
76
+
77
+ * Fix query cache for pinned connections in multi threaded transactional tests
78
+
79
+ When a pinned connection is used across separate threads, they now use a separate cache store
80
+ for each thread.
81
+
82
+ This improve accuracy of system tests, and any test using multiple threads.
83
+
84
+ *Heinrich Lee Yu*, *Jean Boussier*
85
+
86
+ * Fix time attribute dirty tracking with timezone conversions.
87
+
88
+ Time-only attributes now maintain a fixed date of 2000-01-01 during timezone conversions,
89
+ preventing them from being incorrectly marked as changed due to date shifts.
90
+
91
+ This fixes an issue where time attributes would be marked as changed when setting the same time value
92
+ due to timezone conversion causing internal date shifts.
93
+
94
+ *Prateek Choudhary*
95
+
96
+ * Skip calling `PG::Connection#cancel` in `cancel_any_running_query`
97
+ when using libpq >= 18 with pg < 1.6.0, due to incompatibility.
98
+ Rollback still runs, but may take longer.
99
+
100
+ *Yasuo Honda*, *Lars Kanis*
101
+
102
+ * Don't add `id_value` attribute alias when attribute/column with that name already exists.
103
+
104
+ *Rob Lewis*
105
+
1
106
  ## Rails 8.1.0.beta1 (September 04, 2025) ##
2
107
 
3
108
  * Remove deprecated `:unsigned_float` and `:unsigned_decimal` column methods for MySQL.
@@ -71,10 +176,6 @@
71
176
 
72
177
  *Kir Shatrov*
73
178
 
74
- * Emit a warning for pg gem < 1.6.0 when using PostgreSQL 18+
75
-
76
- *Yasuo Honda*
77
-
78
179
  * Fix `#merge` with `#or` or `#and` and a mixture of attributes and SQL strings resulting in an incorrect query.
79
180
 
80
181
  ```ruby
@@ -135,7 +135,9 @@ module ActiveRecord
135
135
  target_key_values = record ? Array(primary_key(record.class)).map { |key| record._read_attribute(key) } : []
136
136
 
137
137
  if force || reflection_fk.map { |fk| owner._read_attribute(fk) } != target_key_values
138
+ owner_pk = Array(owner.class.primary_key)
138
139
  reflection_fk.each_with_index do |key, index|
140
+ next if record.nil? && owner_pk.include?(key)
139
141
  owner[key] = target_key_values[index]
140
142
  end
141
143
  end
@@ -21,7 +21,11 @@ module ActiveRecord
21
21
  set_time_zone_without_conversion(super)
22
22
  elsif value.respond_to?(:in_time_zone)
23
23
  begin
24
- super(user_input_in_time_zone(value)) || super
24
+ result = super(user_input_in_time_zone(value)) || super
25
+ if result && type == :time
26
+ result = result.change(year: 2000, month: 1, day: 1)
27
+ end
28
+ result
25
29
  rescue ArgumentError
26
30
  nil
27
31
  end
@@ -41,7 +45,11 @@ module ActiveRecord
41
45
  return if value.nil?
42
46
 
43
47
  if value.acts_like?(:time)
44
- value.in_time_zone
48
+ converted = value.in_time_zone
49
+ if type == :time && converted
50
+ converted = converted.change(year: 2000, month: 1, day: 1)
51
+ end
52
+ converted
45
53
  elsif value.respond_to?(:infinite?) && value.infinite?
46
54
  value
47
55
  else
@@ -113,7 +113,7 @@ module ActiveRecord
113
113
  unless abstract_class?
114
114
  load_schema
115
115
  super(attribute_names)
116
- alias_attribute :id_value, :id if _has_attribute?("id")
116
+ alias_attribute :id_value, :id if _has_attribute?("id") && !_has_attribute?("id_value")
117
117
  end
118
118
 
119
119
  generate_alias_attributes
@@ -374,7 +374,7 @@ module ActiveRecord
374
374
  context = validation_context if custom_validation_context?
375
375
  return true if record.valid?(context)
376
376
 
377
- if record.changed? || record.new_record? || context
377
+ if context || record.changed_for_autosave?
378
378
  associated_errors = record.errors.objects
379
379
  else
380
380
  # If there are existing invalid records in the DB, we should still be able to reference them.
@@ -527,7 +527,7 @@ module ActiveRecord
527
527
  return false unless reflection.inverse_of&.polymorphic?
528
528
 
529
529
  class_name = record._read_attribute(reflection.inverse_of.foreign_type)
530
- reflection.active_record != record.class.polymorphic_class_for(class_name)
530
+ reflection.active_record.polymorphic_name != class_name
531
531
  end
532
532
 
533
533
  # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
@@ -6,7 +6,6 @@ require "active_support/descendants_tracker"
6
6
  require "active_support/time"
7
7
  require "active_support/core_ext/class/subclasses"
8
8
  require "active_record/log_subscriber"
9
- require "active_record/explain_subscriber"
10
9
  require "active_record/relation/delegation"
11
10
  require "active_record/attributes"
12
11
  require "active_record/type_caster"
@@ -256,13 +255,13 @@ module ActiveRecord # :nodoc:
256
255
  # * AssociationTypeMismatch - The object assigned to the association wasn't of the type
257
256
  # specified in the association definition.
258
257
  # * AttributeAssignmentError - An error occurred while doing a mass assignment through the
259
- # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
258
+ # {ActiveRecord::Base#attributes=}[rdoc-ref:ActiveModel::AttributeAssignment#attributes=] method.
260
259
  # You can inspect the +attribute+ property of the exception object to determine which attribute
261
260
  # triggered the error.
262
261
  # * ConnectionNotEstablished - No connection has been established.
263
262
  # Use {ActiveRecord::Base.establish_connection}[rdoc-ref:ConnectionHandling#establish_connection] before querying.
264
263
  # * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
265
- # {ActiveRecord::Base#attributes=}[rdoc-ref:AttributeAssignment#attributes=] method.
264
+ # {ActiveRecord::Base#attributes=}[rdoc-ref:ActiveModel::AttributeAssignment#attributes=] method.
266
265
  # The +errors+ property of this exception contains an array of
267
266
  # AttributeAssignmentError
268
267
  # objects that should be inspected to determine which attributes triggered the errors.
@@ -129,9 +129,7 @@ module ActiveRecord
129
129
  t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
130
130
  elapsed = 0
131
131
  loop do
132
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
133
- @cond.wait(timeout - elapsed)
134
- end
132
+ @cond.wait(timeout - elapsed)
135
133
 
136
134
  return remove if any?
137
135
 
@@ -8,12 +8,7 @@ require "active_record/connection_adapters/abstract/connection_pool/reaper"
8
8
 
9
9
  module ActiveRecord
10
10
  module ConnectionAdapters
11
- module AbstractPool # :nodoc:
12
- end
13
-
14
11
  class NullPool # :nodoc:
15
- include ConnectionAdapters::AbstractPool
16
-
17
12
  class NullConfig
18
13
  def method_missing(...)
19
14
  nil
@@ -36,6 +31,7 @@ module ActiveRecord
36
31
  end
37
32
 
38
33
  def schema_cache; end
34
+ def query_cache; end
39
35
  def connection_descriptor; end
40
36
  def checkin(_); end
41
37
  def remove(_); end
@@ -116,6 +112,7 @@ module ActiveRecord
116
112
  # * +max_age+: number of seconds the pool will allow the connection to
117
113
  # exist before retiring it at next checkin. (default Float::INFINITY).
118
114
  # * +max_connections+: maximum number of connections the pool may manage (default 5).
115
+ # Set to +nil+ or -1 for unlimited connections.
119
116
  # * +min_connections+: minimum number of connections the pool will open and maintain (default 0).
120
117
  # * +pool_jitter+: maximum reduction factor to apply to +max_age+ and
121
118
  # +keepalive+ intervals (default 0.2; range 0.0-1.0).
@@ -183,21 +180,30 @@ module ActiveRecord
183
180
  end
184
181
  end
185
182
 
186
- class LeaseRegistry # :nodoc:
187
- def initialize
188
- @mutex = Mutex.new
189
- @map = WeakThreadKeyMap.new
183
+ if RUBY_ENGINE == "ruby"
184
+ # Thanks to the GVL, the LeaseRegistry doesn't need to be synchronized on MRI
185
+ class LeaseRegistry < WeakThreadKeyMap # :nodoc:
186
+ def [](context)
187
+ super || (self[context] = Lease.new)
188
+ end
190
189
  end
190
+ else
191
+ class LeaseRegistry # :nodoc:
192
+ def initialize
193
+ @mutex = Mutex.new
194
+ @map = WeakThreadKeyMap.new
195
+ end
191
196
 
192
- def [](context)
193
- @mutex.synchronize do
194
- @map[context] ||= Lease.new
197
+ def [](context)
198
+ @mutex.synchronize do
199
+ @map[context] ||= Lease.new
200
+ end
195
201
  end
196
- end
197
202
 
198
- def clear
199
- @mutex.synchronize do
200
- @map.clear
203
+ def clear
204
+ @mutex.synchronize do
205
+ @map.clear
206
+ end
201
207
  end
202
208
  end
203
209
  end
@@ -229,7 +235,6 @@ module ActiveRecord
229
235
 
230
236
  include MonitorMixin
231
237
  prepend QueryCache::ConnectionPoolConfiguration
232
- include ConnectionAdapters::AbstractPool
233
238
 
234
239
  attr_accessor :automatic_reconnect, :checkout_timeout
235
240
  attr_reader :db_config, :max_connections, :min_connections, :max_age, :keepalive, :reaper, :pool_config, :async_executor, :role, :shard
@@ -349,8 +354,9 @@ module ActiveRecord
349
354
  # held in a cache keyed by a thread.
350
355
  def lease_connection
351
356
  lease = connection_lease
352
- lease.sticky = true
353
357
  lease.connection ||= checkout
358
+ lease.sticky = true
359
+ lease.connection
354
360
  end
355
361
 
356
362
  def permanent_lease? # :nodoc:
@@ -368,6 +374,7 @@ module ActiveRecord
368
374
  end
369
375
 
370
376
  @pinned_connection.lock_thread = ActiveSupport::IsolatedExecutionState.context if lock_thread
377
+ @pinned_connection.pinned = true
371
378
  @pinned_connection.verify! # eagerly validate the connection
372
379
  @pinned_connection.begin_transaction joinable: false, _lazy: false
373
380
  end
@@ -390,6 +397,7 @@ module ActiveRecord
390
397
  end
391
398
 
392
399
  if @pinned_connection.nil?
400
+ connection.pinned = false
393
401
  connection.steal!
394
402
  connection.lock_thread = nil
395
403
  checkin(connection)
@@ -653,11 +661,7 @@ module ActiveRecord
653
661
  conn.lock.synchronize do
654
662
  synchronize do
655
663
  connection_lease.clear(conn)
656
-
657
- conn._run_checkin_callbacks do
658
- conn.expire
659
- end
660
-
664
+ conn.expire
661
665
  @available.add conn
662
666
  end
663
667
  end
@@ -784,6 +788,7 @@ module ActiveRecord
784
788
 
785
789
  if need_new_connections
786
790
  while new_conn = try_to_checkout_new_connection { @connections.size < @min_connections }
791
+ new_conn.allow_preconnect = true
787
792
  checkin(new_conn)
788
793
  end
789
794
  end
@@ -929,7 +934,7 @@ module ActiveRecord
929
934
  # Each connection will only be processed once per call to this method,
930
935
  # but (particularly in the async case) there is no protection against
931
936
  # a second call to this method starting to work through the list
932
- # before the first call has completed. (Though regular pool behaviour
937
+ # before the first call has completed. (Though regular pool behavior
933
938
  # will prevent two instances from working on the same specific
934
939
  # connection at the same time.)
935
940
  def sequential_maintenance(candidate_selector, &maintenance_work)
@@ -1220,7 +1225,7 @@ module ActiveRecord
1220
1225
  do_checkout = synchronize do
1221
1226
  return if self.discarded?
1222
1227
 
1223
- if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @max_connections && (!block_given? || yield)
1228
+ if @threads_blocking_new_connections.zero? && (@max_connections.nil? || (@connections.size + @now_connecting) < @max_connections) && (!block_given? || yield)
1224
1229
  if @connections.size > 0 || @original_context != ActiveSupport::IsolatedExecutionState.context
1225
1230
  @activated = true
1226
1231
  end
@@ -1267,10 +1272,7 @@ module ActiveRecord
1267
1272
  end
1268
1273
 
1269
1274
  def checkout_and_verify(c)
1270
- c._run_checkout_callbacks do
1271
- c.clean!
1272
- end
1273
- c
1275
+ c.clean!
1274
1276
  rescue Exception
1275
1277
  remove c
1276
1278
  c.disconnect!
@@ -353,6 +353,22 @@ module ActiveRecord
353
353
  # isolation level.
354
354
  # :args: (requires_new: nil, isolation: nil, &block)
355
355
  def transaction(requires_new: nil, isolation: nil, joinable: true, &block)
356
+ # If we're running inside the single, non-joinable transaction that
357
+ # ActiveRecord::TestFixtures starts around each example (depth == 1),
358
+ # an `isolation:` hint must be validated then ignored so that the
359
+ # adapter isn't asked to change the isolation level mid-transaction.
360
+ if isolation && !requires_new && open_transactions == 1 && !current_transaction.joinable?
361
+ iso = isolation.to_sym
362
+
363
+ unless transaction_isolation_levels.include?(iso)
364
+ raise ActiveRecord::TransactionIsolationError,
365
+ "invalid transaction isolation level: #{iso.inspect}"
366
+ end
367
+
368
+ current_transaction.isolation = iso
369
+ isolation = nil
370
+ end
371
+
356
372
  if !requires_new && current_transaction.joinable?
357
373
  if isolation && current_transaction.isolation != isolation
358
374
  raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
@@ -372,10 +388,10 @@ module ActiveRecord
372
388
  :disable_lazy_transactions!, :enable_lazy_transactions!, :dirty_current_transaction,
373
389
  to: :transaction_manager
374
390
 
375
- def mark_transaction_written_if_write(sql) # :nodoc:
391
+ def mark_transaction_written # :nodoc:
376
392
  transaction = current_transaction
377
393
  if transaction.open?
378
- transaction.written ||= write_query?(sql)
394
+ transaction.written ||= true
379
395
  end
380
396
  end
381
397
 
@@ -420,13 +436,16 @@ module ActiveRecord
420
436
  end
421
437
  end
422
438
 
439
+ TRANSACTION_ISOLATION_LEVELS = {
440
+ read_uncommitted: "READ UNCOMMITTED",
441
+ read_committed: "READ COMMITTED",
442
+ repeatable_read: "REPEATABLE READ",
443
+ serializable: "SERIALIZABLE"
444
+ }.freeze
445
+ private_constant :TRANSACTION_ISOLATION_LEVELS
446
+
423
447
  def transaction_isolation_levels
424
- {
425
- read_uncommitted: "READ UNCOMMITTED",
426
- read_committed: "READ COMMITTED",
427
- repeatable_read: "REPEATABLE READ",
428
- serializable: "SERIALIZABLE"
429
- }
448
+ TRANSACTION_ISOLATION_LEVELS
430
449
  end
431
450
 
432
451
  # Begins the transaction with the isolation level set. Raises an error by
@@ -549,9 +568,7 @@ module ActiveRecord
549
568
  type_casted_binds = type_casted_binds(binds)
550
569
  log(sql, name, binds, type_casted_binds, async: async, allow_retry: allow_retry) do |notification_payload|
551
570
  with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
552
- result = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
553
- perform_query(conn, sql, binds, type_casted_binds, prepare: prepare, notification_payload: notification_payload, batch: batch)
554
- end
571
+ result = perform_query(conn, sql, binds, type_casted_binds, prepare: prepare, notification_payload: notification_payload, batch: batch)
555
572
  handle_warnings(result, sql)
556
573
  result
557
574
  end
@@ -575,8 +592,10 @@ module ActiveRecord
575
592
  end
576
593
 
577
594
  def preprocess_query(sql)
578
- check_if_write_query(sql)
579
- mark_transaction_written_if_write(sql)
595
+ if write_query?(sql)
596
+ ensure_writes_are_allowed(sql)
597
+ mark_transaction_written
598
+ end
580
599
 
581
600
  # We call tranformers after the write checks so we don't add extra parsing work.
582
601
  # This means we assume no transformer whille change a read for a write
@@ -13,8 +13,6 @@ module ActiveRecord
13
13
  dirties_query_cache base, :exec_query, :execute, :create, :insert, :update, :delete, :truncate,
14
14
  :truncate_tables, :rollback_to_savepoint, :rollback_db_transaction, :restart_db_transaction,
15
15
  :exec_insert_all
16
-
17
- base.set_callback :checkin, :after, :unset_query_cache!
18
16
  end
19
17
 
20
18
  def dirties_query_cache(base, *method_names)
@@ -209,15 +207,26 @@ module ActiveRecord
209
207
  end
210
208
  end
211
209
 
212
- attr_accessor :query_cache
213
-
214
210
  def initialize(*)
215
211
  super
216
212
  @query_cache = nil
217
213
  end
218
214
 
215
+ attr_writer :query_cache
216
+
217
+ def query_cache
218
+ if @pinned && @owner != ActiveSupport::IsolatedExecutionState.context
219
+ # With transactional tests, if the connection is pinned, any thread
220
+ # other than the one that pinned the connection need to go through the
221
+ # query cache pool, so each thread get a different cache.
222
+ pool.query_cache
223
+ else
224
+ @query_cache
225
+ end
226
+ end
227
+
219
228
  def query_cache_enabled
220
- @query_cache&.enabled?
229
+ query_cache&.enabled?
221
230
  end
222
231
 
223
232
  # Enable the query cache within the block.
@@ -256,7 +265,7 @@ module ActiveRecord
256
265
 
257
266
  # If arel is locked this is a SELECT ... FOR UPDATE or somesuch.
258
267
  # Such queries should not be cached.
259
- if @query_cache&.enabled? && !(arel.respond_to?(:locked) && arel.locked)
268
+ if query_cache_enabled && !(arel.respond_to?(:locked) && arel.locked)
260
269
  sql, binds, preparable, allow_retry = to_sql_and_binds(arel, binds, preparable, allow_retry)
261
270
 
262
271
  if async
@@ -280,7 +289,7 @@ module ActiveRecord
280
289
 
281
290
  result = nil
282
291
  @lock.synchronize do
283
- result = @query_cache[key]
292
+ result = query_cache[key]
284
293
  end
285
294
 
286
295
  if result
@@ -299,7 +308,7 @@ module ActiveRecord
299
308
  hit = true
300
309
 
301
310
  @lock.synchronize do
302
- result = @query_cache.compute_if_absent(key) do
311
+ result = query_cache.compute_if_absent(key) do
303
312
  hit = false
304
313
  yield
305
314
  end
@@ -124,6 +124,7 @@ module ActiveRecord
124
124
  def after_commit; yield; end
125
125
  def after_rollback; end
126
126
  def user_transaction; ActiveRecord::Transaction::NULL_TRANSACTION; end
127
+ def isolation=(_); end
127
128
  end
128
129
 
129
130
  class Transaction # :nodoc:
@@ -156,6 +157,10 @@ module ActiveRecord
156
157
  @isolation_level
157
158
  end
158
159
 
160
+ def isolation=(isolation) # :nodoc:
161
+ @isolation_level = isolation
162
+ end
163
+
159
164
  def initialize(connection, isolation: nil, joinable: true, run_commit_callbacks: false)
160
165
  super()
161
166
  @connection = connection
@@ -426,6 +431,10 @@ module ActiveRecord
426
431
  @parent_transaction.isolation
427
432
  end
428
433
 
434
+ def isolation=(isolation) # :nodoc:
435
+ @parent_transaction.isolation = isolation
436
+ end
437
+
429
438
  def materialize!
430
439
  connection.create_savepoint(savepoint_name)
431
440
  super
@@ -5,6 +5,7 @@ require "active_record/connection_adapters/abstract/schema_dumper"
5
5
  require "active_record/connection_adapters/abstract/schema_creation"
6
6
  require "active_support/concurrency/null_lock"
7
7
  require "active_support/concurrency/load_interlock_aware_monitor"
8
+ require "active_support/concurrency/thread_monitor"
8
9
  require "arel/collectors/bind"
9
10
  require "arel/collectors/composite"
10
11
  require "arel/collectors/sql_string"
@@ -42,7 +43,8 @@ module ActiveRecord
42
43
 
43
44
  attr_reader :pool
44
45
  attr_reader :visitor, :owner, :logger, :lock
45
- attr_accessor :allow_preconnect
46
+ attr_reader :allow_preconnect # :nodoc:
47
+ attr_accessor :pinned # :nodoc:
46
48
  alias :in_use? :owner
47
49
 
48
50
  def pool=(value)
@@ -51,7 +53,11 @@ module ActiveRecord
51
53
  @pool = value
52
54
  end
53
55
 
54
- set_callback :checkin, :after, :enable_lazy_transactions!
56
+ def allow_preconnect=(value) # :nodoc:
57
+ @lock.synchronize do
58
+ @allow_preconnect = value
59
+ end
60
+ end
55
61
 
56
62
  def self.type_cast_config_to_integer(config)
57
63
  if config.is_a?(Integer)
@@ -153,9 +159,10 @@ module ActiveRecord
153
159
  end
154
160
 
155
161
  @owner = nil
162
+ @pinned = false
156
163
  @pool = ActiveRecord::ConnectionAdapters::NullPool.new
157
164
  @idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
158
- @allow_preconnect = true
165
+ @allow_preconnect = false
159
166
  @visitor = arel_visitor
160
167
  @statements = build_statement_pool
161
168
  self.lock_thread = nil
@@ -188,16 +195,16 @@ module ActiveRecord
188
195
  @lock =
189
196
  case lock_thread
190
197
  when Thread
191
- ActiveSupport::Concurrency::ThreadLoadInterlockAwareMonitor.new
198
+ ActiveSupport::Concurrency::ThreadMonitor.new
192
199
  when Fiber
193
- ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
200
+ ::Monitor.new
194
201
  else
195
202
  ActiveSupport::Concurrency::NullLock
196
203
  end
197
204
  end
198
205
 
199
- def check_if_write_query(sql) # :nodoc:
200
- if preventing_writes? && write_query?(sql)
206
+ def ensure_writes_are_allowed(sql) # :nodoc:
207
+ if preventing_writes?
201
208
  raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
202
209
  end
203
210
  end
@@ -327,8 +334,12 @@ module ActiveRecord
327
334
  "Current thread: #{ActiveSupport::IsolatedExecutionState.context}."
328
335
  end
329
336
 
330
- @idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) if update_idle
331
- @owner = nil
337
+ _run_checkin_callbacks do
338
+ @idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC) if update_idle
339
+ @owner = nil
340
+ enable_lazy_transactions!
341
+ unset_query_cache!
342
+ end
332
343
  else
333
344
  raise ActiveRecordError, "Cannot expire connection, it is not currently leased."
334
345
  end
@@ -825,8 +836,11 @@ module ActiveRecord
825
836
  end
826
837
 
827
838
  def clean! # :nodoc:
828
- @raw_connection_dirty = false
829
- @verified = nil
839
+ _run_checkout_callbacks do
840
+ @raw_connection_dirty = false
841
+ @verified = nil
842
+ end
843
+ self
830
844
  end
831
845
 
832
846
  def verified? # :nodoc:
@@ -209,8 +209,6 @@ module ActiveRecord
209
209
  }
210
210
  end
211
211
 
212
- # HELPER METHODS ===========================================
213
-
214
212
  # Must return the MySQL error number from the exception, if the exception has an
215
213
  # error number.
216
214
  def error_number(exception) # :nodoc:
@@ -48,7 +48,7 @@ module ActiveRecord
48
48
  end
49
49
 
50
50
  # = Active Record MySQL Adapter \Index Definition
51
- class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition
51
+ class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition # :nodoc:
52
52
  attr_accessor :enabled
53
53
 
54
54
  def initialize(*args, **kwargs)