activerecord 7.2.0.beta2 → 7.2.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -1
  3. data/lib/active_record/associations/collection_association.rb +3 -3
  4. data/lib/active_record/associations/errors.rb +265 -0
  5. data/lib/active_record/associations.rb +0 -262
  6. data/lib/active_record/attribute_methods/dirty.rb +1 -1
  7. data/lib/active_record/attribute_methods/read.rb +3 -3
  8. data/lib/active_record/attribute_methods/write.rb +3 -3
  9. data/lib/active_record/attribute_methods.rb +8 -6
  10. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +27 -11
  11. data/lib/active_record/connection_adapters/abstract/database_statements.rb +1 -1
  12. data/lib/active_record/connection_adapters/abstract/query_cache.rb +1 -1
  13. data/lib/active_record/connection_adapters/abstract/transaction.rb +67 -9
  14. data/lib/active_record/connection_adapters/abstract_adapter.rb +8 -1
  15. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +1 -1
  16. data/lib/active_record/database_configurations/database_config.rb +4 -0
  17. data/lib/active_record/errors.rb +20 -8
  18. data/lib/active_record/gem_version.rb +1 -1
  19. data/lib/active_record/railtie.rb +2 -3
  20. data/lib/active_record/railties/databases.rake +1 -1
  21. data/lib/active_record/relation/calculations.rb +1 -1
  22. data/lib/active_record/relation/query_methods.rb +21 -9
  23. data/lib/active_record/signed_id.rb +9 -0
  24. data/lib/active_record/test_fixtures.rb +10 -3
  25. data/lib/active_record/timestamp.rb +1 -1
  26. data/lib/active_record/transaction.rb +56 -55
  27. data/lib/active_record/transactions.rb +1 -1
  28. data/lib/active_record.rb +1 -1
  29. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  30. metadata +13 -12
@@ -253,6 +253,7 @@ module ActiveRecord
253
253
 
254
254
  @available = ConnectionLeasingQueue.new self
255
255
  @pinned_connection = nil
256
+ @pinned_connections_depth = 0
256
257
 
257
258
  @async_executor = build_async_executor
258
259
 
@@ -262,6 +263,13 @@ module ActiveRecord
262
263
  @reaper.run
263
264
  end
264
265
 
266
+ def inspect # :nodoc:
267
+ name_field = " name=#{db_config.name.inspect}" unless db_config.name == "primary"
268
+ shard_field = " shard=#{@shard.inspect}" unless @shard == :default
269
+
270
+ "#<#{self.class.name} env_name=#{db_config.env_name.inspect}#{name_field} role=#{role.inspect}#{shard_field}>"
271
+ end
272
+
265
273
  def schema_cache
266
274
  @schema_cache ||= BoundSchemaReflection.new(schema_reflection, self)
267
275
  end
@@ -311,9 +319,9 @@ module ActiveRecord
311
319
  end
312
320
 
313
321
  def pin_connection!(lock_thread) # :nodoc:
314
- raise "There is already a pinned connection" if @pinned_connection
322
+ @pinned_connection ||= (connection_lease&.connection || checkout)
323
+ @pinned_connections_depth += 1
315
324
 
316
- @pinned_connection = (connection_lease&.connection || checkout)
317
325
  # Any leased connection must be in @connections otherwise
318
326
  # some methods like #connected? won't behave correctly
319
327
  unless @connections.include?(@pinned_connection)
@@ -330,7 +338,10 @@ module ActiveRecord
330
338
 
331
339
  clean = true
332
340
  @pinned_connection.lock.synchronize do
333
- connection, @pinned_connection = @pinned_connection, nil
341
+ @pinned_connections_depth -= 1
342
+ connection = @pinned_connection
343
+ @pinned_connection = nil if @pinned_connections_depth.zero?
344
+
334
345
  if connection.transaction_open?
335
346
  connection.rollback_transaction
336
347
  else
@@ -338,8 +349,11 @@ module ActiveRecord
338
349
  clean = false
339
350
  connection.reset!
340
351
  end
341
- connection.lock_thread = nil
342
- checkin(connection)
352
+
353
+ if @pinned_connection.nil?
354
+ connection.lock_thread = nil
355
+ checkin(connection)
356
+ end
343
357
  end
344
358
 
345
359
  clean
@@ -527,12 +541,14 @@ module ActiveRecord
527
541
  # - ActiveRecord::ConnectionTimeoutError no connection can be obtained from the pool.
528
542
  def checkout(checkout_timeout = @checkout_timeout)
529
543
  if @pinned_connection
530
- synchronize do
531
- @pinned_connection.verify!
532
- # Any leased connection must be in @connections otherwise
533
- # some methods like #connected? won't behave correctly
534
- unless @connections.include?(@pinned_connection)
535
- @connections << @pinned_connection
544
+ @pinned_connection.lock.synchronize do
545
+ synchronize do
546
+ @pinned_connection.verify!
547
+ # Any leased connection must be in @connections otherwise
548
+ # some methods like #connected? won't behave correctly
549
+ unless @connections.include?(@pinned_connection)
550
+ @connections << @pinned_connection
551
+ end
536
552
  end
537
553
  end
538
554
  @pinned_connection
@@ -356,7 +356,7 @@ module ActiveRecord
356
356
  if isolation
357
357
  raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
358
358
  end
359
- yield current_transaction
359
+ yield current_transaction.user_transaction
360
360
  else
361
361
  transaction_manager.within_new_transaction(isolation: isolation, joinable: joinable, &block)
362
362
  end
@@ -277,7 +277,7 @@ module ActiveRecord
277
277
  type_casted_binds: -> { type_casted_binds(binds) },
278
278
  name: name,
279
279
  connection: self,
280
- transaction: current_transaction.presence,
280
+ transaction: current_transaction.user_transaction.presence,
281
281
  cached: true
282
282
  }
283
283
  end
@@ -91,7 +91,9 @@ module ActiveRecord
91
91
  raise InstrumentationAlreadyStartedError.new("Called start on an already started transaction") if @started
92
92
  @started = true
93
93
 
94
- @payload = @base_payload.dup
94
+ ActiveSupport::Notifications.instrument("start_transaction.active_record", @base_payload)
95
+
96
+ @payload = @base_payload.dup # We dup because the payload for a given event is mutated later to add the outcome.
95
97
  @handle = ActiveSupport::Notifications.instrumenter.build_handle("transaction.active_record", @payload)
96
98
  @handle.start
97
99
  end
@@ -106,10 +108,8 @@ module ActiveRecord
106
108
  end
107
109
 
108
110
  class NullTransaction # :nodoc:
109
- def initialize; end
110
111
  def state; end
111
112
  def closed?; true; end
112
- alias_method :blank?, :closed?
113
113
  def open?; false; end
114
114
  def joinable?; false; end
115
115
  def add_record(record, _ = true); end
@@ -121,12 +121,31 @@ module ActiveRecord
121
121
  def materialized?; false; end
122
122
  def before_commit; yield; end
123
123
  def after_commit; yield; end
124
- def after_rollback; end # noop
125
- def uuid; Digest::UUID.nil_uuid; end
124
+ def after_rollback; end
125
+ def user_transaction; ActiveRecord::Transaction::NULL_TRANSACTION; end
126
126
  end
127
127
 
128
- class Transaction < ActiveRecord::Transaction # :nodoc:
129
- attr_reader :connection, :state, :savepoint_name, :isolation_level
128
+ class Transaction # :nodoc:
129
+ class Callback # :nodoc:
130
+ def initialize(event, callback)
131
+ @event = event
132
+ @callback = callback
133
+ end
134
+
135
+ def before_commit
136
+ @callback.call if @event == :before_commit
137
+ end
138
+
139
+ def after_commit
140
+ @callback.call if @event == :after_commit
141
+ end
142
+
143
+ def after_rollback
144
+ @callback.call if @event == :after_rollback
145
+ end
146
+ end
147
+
148
+ attr_reader :connection, :state, :savepoint_name, :isolation_level, :user_transaction
130
149
  attr_accessor :written
131
150
 
132
151
  delegate :invalidate!, :invalidated?, to: :@state
@@ -135,6 +154,7 @@ module ActiveRecord
135
154
  super()
136
155
  @connection = connection
137
156
  @state = TransactionState.new
157
+ @callbacks = nil
138
158
  @records = nil
139
159
  @isolation_level = isolation
140
160
  @materialized = false
@@ -142,7 +162,8 @@ module ActiveRecord
142
162
  @run_commit_callbacks = run_commit_callbacks
143
163
  @lazy_enrollment_records = nil
144
164
  @dirty = false
145
- @instrumenter = TransactionInstrumenter.new(connection: connection, transaction: self)
165
+ @user_transaction = joinable ? ActiveRecord::Transaction.new(self) : ActiveRecord::Transaction::NULL_TRANSACTION
166
+ @instrumenter = TransactionInstrumenter.new(connection: connection, transaction: @user_transaction)
146
167
  end
147
168
 
148
169
  def dirty!
@@ -153,6 +174,14 @@ module ActiveRecord
153
174
  @dirty
154
175
  end
155
176
 
177
+ def open?
178
+ true
179
+ end
180
+
181
+ def closed?
182
+ false
183
+ end
184
+
156
185
  def add_record(record, ensure_finalize = true)
157
186
  @records ||= []
158
187
  if ensure_finalize
@@ -163,6 +192,30 @@ module ActiveRecord
163
192
  end
164
193
  end
165
194
 
195
+ def before_commit(&block)
196
+ if @state.finalized?
197
+ raise ActiveRecordError, "Cannot register callbacks on a finalized transaction"
198
+ end
199
+
200
+ (@callbacks ||= []) << Callback.new(:before_commit, block)
201
+ end
202
+
203
+ def after_commit(&block)
204
+ if @state.finalized?
205
+ raise ActiveRecordError, "Cannot register callbacks on a finalized transaction"
206
+ end
207
+
208
+ (@callbacks ||= []) << Callback.new(:after_commit, block)
209
+ end
210
+
211
+ def after_rollback(&block)
212
+ if @state.finalized?
213
+ raise ActiveRecordError, "Cannot register callbacks on a finalized transaction"
214
+ end
215
+
216
+ (@callbacks ||= []) << Callback.new(:after_rollback, block)
217
+ end
218
+
166
219
  def records
167
220
  if @lazy_enrollment_records
168
221
  @records.concat @lazy_enrollment_records.values
@@ -274,6 +327,11 @@ module ActiveRecord
274
327
  def full_rollback?; true; end
275
328
  def joinable?; @joinable; end
276
329
 
330
+ protected
331
+ def append_callbacks(callbacks) # :nodoc:
332
+ (@callbacks ||= []).concat(callbacks)
333
+ end
334
+
277
335
  private
278
336
  def unique_records
279
337
  records.uniq(&:__id__)
@@ -555,7 +613,7 @@ module ActiveRecord
555
613
  @connection.lock.synchronize do
556
614
  transaction = begin_transaction(isolation: isolation, joinable: joinable)
557
615
  begin
558
- yield transaction
616
+ yield transaction.user_transaction
559
617
  rescue Exception => error
560
618
  rollback_transaction
561
619
  after_failure_actions(transaction, error)
@@ -172,6 +172,13 @@ module ActiveRecord
172
172
  @verified = false
173
173
  end
174
174
 
175
+ def inspect # :nodoc:
176
+ name_field = " name=#{pool.db_config.name.inspect}" unless pool.db_config.name == "primary"
177
+ shard_field = " shard=#{shard.inspect}" unless shard == :default
178
+
179
+ "#<#{self.class.name}:#{'%#016x' % (object_id << 1)} env_name=#{pool.db_config.env_name.inspect}#{name_field} role=#{role.inspect}#{shard_field}>"
180
+ end
181
+
175
182
  def lock_thread=(lock_thread) # :nodoc:
176
183
  @lock =
177
184
  case lock_thread
@@ -1118,7 +1125,7 @@ module ActiveRecord
1118
1125
  statement_name: statement_name,
1119
1126
  async: async,
1120
1127
  connection: self,
1121
- transaction: current_transaction.presence,
1128
+ transaction: current_transaction.user_transaction.presence,
1122
1129
  row_count: 0,
1123
1130
  &block
1124
1131
  )
@@ -644,7 +644,7 @@ module ActiveRecord
644
644
  # MySQL 8.0.19 replaces `VALUES(<expression>)` clauses with row and column alias names, see https://dev.mysql.com/worklog/task/?id=6312 .
645
645
  # then MySQL 8.0.20 deprecates the `VALUES(<expression>)` see https://dev.mysql.com/worklog/task/?id=13325 .
646
646
  if supports_insert_raw_alias_syntax?
647
- values_alias = quote_table_name("#{insert.model.table_name}_values")
647
+ values_alias = quote_table_name("#{insert.model.table_name.parameterize}_values")
648
648
  sql = +"INSERT #{insert.into} #{insert.values_list} AS #{values_alias}"
649
649
 
650
650
  if insert.skip_duplicates?
@@ -18,6 +18,10 @@ module ActiveRecord
18
18
  @adapter_class ||= ActiveRecord::ConnectionAdapters.resolve(adapter)
19
19
  end
20
20
 
21
+ def inspect # :nodoc:
22
+ "#<#{self.class.name} env_name=#{@env_name} name=#{@name} adapter_class=#{adapter_class}>"
23
+ end
24
+
21
25
  def new_connection
22
26
  adapter_class.new(configuration_hash)
23
27
  end
@@ -130,9 +130,17 @@ module ActiveRecord
130
130
 
131
131
  # Raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and
132
132
  # {ActiveRecord::Base.update_attribute!}[rdoc-ref:Persistence#update_attribute!]
133
- # methods when a record is failed to validate or cannot be saved due to any of the
133
+ # methods when a record failed to validate or cannot be saved due to any of the
134
134
  # <tt>before_*</tt> callbacks throwing +:abort+. See
135
- # ActiveRecord::Callbacks for further details
135
+ # ActiveRecord::Callbacks for further details.
136
+ #
137
+ # class Product < ActiveRecord::Base
138
+ # before_save do
139
+ # throw :abort if price < 0
140
+ # end
141
+ # end
142
+ #
143
+ # Product.create! # => raises an ActiveRecord::RecordNotSaved
136
144
  class RecordNotSaved < ActiveRecordError
137
145
  attr_reader :record
138
146
 
@@ -143,15 +151,17 @@ module ActiveRecord
143
151
  end
144
152
 
145
153
  # Raised by {ActiveRecord::Base#destroy!}[rdoc-ref:Persistence#destroy!]
146
- # when a call to {#destroy}[rdoc-ref:Persistence#destroy]
147
- # would return false.
154
+ # when a record cannot be destroyed due to any of the
155
+ # <tt>before_destroy</tt> callbacks throwing +:abort+. See
156
+ # ActiveRecord::Callbacks for further details.
148
157
  #
149
- # begin
150
- # complex_operation_that_internally_calls_destroy!
151
- # rescue ActiveRecord::RecordNotDestroyed => invalid
152
- # puts invalid.record.errors
158
+ # class User < ActiveRecord::Base
159
+ # before_destroy do
160
+ # throw :abort if still_active?
161
+ # end
153
162
  # end
154
163
  #
164
+ # User.first.destroy! # => raises an ActiveRecord::RecordNotDestroyed
155
165
  class RecordNotDestroyed < ActiveRecordError
156
166
  attr_reader :record
157
167
 
@@ -583,3 +593,5 @@ module ActiveRecord
583
593
  class DatabaseVersionError < ActiveRecordError
584
594
  end
585
595
  end
596
+
597
+ require "active_record/associations/errors"
@@ -10,7 +10,7 @@ module ActiveRecord
10
10
  MAJOR = 7
11
11
  MINOR = 2
12
12
  TINY = 0
13
- PRE = "beta2"
13
+ PRE = "beta3"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -241,8 +241,7 @@ To keep using the current cache store, you can turn off cache versioning entirel
241
241
  end
242
242
 
243
243
  ActiveSupport.on_load(:active_record) do
244
- # Configs used in other initializers
245
- configs = configs.except(
244
+ configs_used_in_other_initializers = configs.except(
246
245
  :migration_error,
247
246
  :database_selector,
248
247
  :database_resolver,
@@ -259,7 +258,7 @@ To keep using the current cache store, you can turn off cache versioning entirel
259
258
  :postgresql_adapter_decode_dates,
260
259
  )
261
260
 
262
- configs.each do |k, v|
261
+ configs_used_in_other_initializers.each do |k, v|
263
262
  next if k == :encryption
264
263
  setter = "#{k}="
265
264
  # Some existing initializers might rely on Active Record configuration
@@ -89,7 +89,7 @@ db_namespace = namespace :db do
89
89
  task migrate: :load_config do
90
90
  db_configs = ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env)
91
91
 
92
- if db_configs.size == 1
92
+ if db_configs.size == 1 && db_configs.first.primary?
93
93
  ActiveRecord::Tasks::DatabaseTasks.migrate
94
94
  else
95
95
  mapped_versions = ActiveRecord::Tasks::DatabaseTasks.db_configs_with_versions
@@ -604,7 +604,7 @@ module ActiveRecord
604
604
  klass.attribute_types.fetch(name = result.columns[i]) do
605
605
  join_dependencies ||= build_join_dependencies
606
606
  lookup_cast_type_from_join_dependencies(name, join_dependencies) ||
607
- result.column_types[name] || Type.default_value
607
+ result.column_types[i] || Type.default_value
608
608
  end
609
609
  end
610
610
  end
@@ -2063,17 +2063,29 @@ module ActiveRecord
2063
2063
  order_args.flat_map do |arg|
2064
2064
  case arg
2065
2065
  when String, Symbol
2066
- arg
2066
+ extract_table_name_from(arg)
2067
2067
  when Hash
2068
- arg.keys.select { |e| e.is_a?(String) || e.is_a?(Symbol) }
2068
+ arg
2069
+ .map do |key, value|
2070
+ case value
2071
+ when Hash
2072
+ key.to_s
2073
+ else
2074
+ extract_table_name_from(key) if key.is_a?(String) || key.is_a?(Symbol)
2075
+ end
2076
+ end
2077
+ when Arel::Attribute
2078
+ arg.relation.name
2079
+ when Arel::Nodes::Ordering
2080
+ if arg.expr.is_a?(Arel::Attribute)
2081
+ arg.expr.relation.name
2082
+ end
2069
2083
  end
2070
- end.filter_map do |arg|
2071
- arg =~ /^\W?(\w+)\W?\./ && $1
2072
- end +
2073
- order_args
2074
- .select { |e| e.is_a?(Hash) }
2075
- .flat_map { |e| e.map { |k, v| k if v.is_a?(Hash) } }
2076
- .compact
2084
+ end.compact
2085
+ end
2086
+
2087
+ def extract_table_name_from(string)
2088
+ string.match(/^\W?(\w+)\W?\./) && $1
2077
2089
  end
2078
2090
 
2079
2091
  def order_column(field)
@@ -106,7 +106,16 @@ module ActiveRecord
106
106
 
107
107
 
108
108
  # Returns a signed id that's generated using a preconfigured +ActiveSupport::MessageVerifier+ instance.
109
+ #
109
110
  # This signed id is tamper proof, so it's safe to send in an email or otherwise share with the outside world.
111
+ # However, as with any message signed with a +ActiveSupport::MessageVerifier+,
112
+ # {the signed id is not encrypted}[link:classes/ActiveSupport/MessageVerifier.html#class-ActiveSupport::MessageVerifier-label-Signing+is+not+encryption].
113
+ # It's just encoded and protected against tampering.
114
+ #
115
+ # This means that the ID can be decoded by anyone; however, if tampered with (so to point to a different ID),
116
+ # the cryptographic signature will no longer match, and the signed id will be considered invalid and return nil
117
+ # when passed to +find_signed+ (or raise with +find_signed!+).
118
+ #
110
119
  # It can furthermore be set to expire (the default is not to expire), and scoped down with a specific purpose.
111
120
  # If the expiration date has been exceeded before +find_signed+ is called, the id won't find the designated
112
121
  # record. If a purpose is set, this too must match.
@@ -96,6 +96,14 @@ module ActiveRecord
96
96
  end
97
97
  end
98
98
 
99
+ # Generic fixture accessor for fixture names that may conflict with other methods.
100
+ #
101
+ # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
102
+ # assert_equal "Ruby on Rails", fixture(:web_sites, :rubyonrails).name
103
+ def fixture(fixture_set_name, *fixture_names)
104
+ active_record_fixture(fixture_set_name, *fixture_names)
105
+ end
106
+
99
107
  private
100
108
  def run_in_transaction?
101
109
  use_transactional_tests &&
@@ -255,7 +263,7 @@ module ActiveRecord
255
263
 
256
264
  def method_missing(method, ...)
257
265
  if fixture_sets.key?(method.name)
258
- _active_record_fixture(method, ...)
266
+ active_record_fixture(method, ...)
259
267
  else
260
268
  super
261
269
  end
@@ -269,14 +277,13 @@ module ActiveRecord
269
277
  end
270
278
  end
271
279
 
272
- def _active_record_fixture(fixture_set_name, *fixture_names)
280
+ def active_record_fixture(fixture_set_name, *fixture_names)
273
281
  if fs_name = fixture_sets[fixture_set_name.name]
274
282
  access_fixture(fs_name, *fixture_names)
275
283
  else
276
284
  raise StandardError, "No fixture set named '#{fixture_set_name.inspect}'"
277
285
  end
278
286
  end
279
- alias_method :fixture, :_active_record_fixture
280
287
 
281
288
  def access_fixture(fs_name, *fixture_names)
282
289
  force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
@@ -162,7 +162,7 @@ module ActiveRecord
162
162
 
163
163
  def max_updated_column_timestamp
164
164
  timestamp_attributes_for_update_in_model
165
- .filter_map { |attr| self[attr]&.to_time }
165
+ .filter_map { |attr| (v = self[attr]) && (v.is_a?(::Time) ? v : v.to_time) }
166
166
  .max
167
167
  end
168
168
 
@@ -3,9 +3,30 @@
3
3
  require "active_support/core_ext/digest"
4
4
 
5
5
  module ActiveRecord
6
- # This abstract class specifies the interface to interact with the current transaction state.
6
+ # Class specifies the interface to interact with the current transaction state.
7
7
  #
8
- # Any other methods not specified here are considered to be private interfaces.
8
+ # It can either map to an actual transaction/savepoint, or represent the
9
+ # absence of a transaction.
10
+ #
11
+ # == State
12
+ #
13
+ # We say that a transaction is _finalized_ when it wraps a real transaction
14
+ # that has been either committed or rolled back.
15
+ #
16
+ # A transaction is _open_ if it wraps a real transaction that is not finalized.
17
+ #
18
+ # On the other hand, a transaction is _closed_ when it is not open. That is,
19
+ # when it represents absence of transaction, or it wraps a real but finalized
20
+ # one.
21
+ #
22
+ # You can check whether a transaction is open or closed with the +open?+ and
23
+ # +closed?+ predicates:
24
+ #
25
+ # if Article.current_transaction.open?
26
+ # # We are inside a real and not finalized transaction.
27
+ # end
28
+ #
29
+ # Closed transactions are `blank?` too.
9
30
  #
10
31
  # == Callbacks
11
32
  #
@@ -42,90 +63,70 @@ module ActiveRecord
42
63
  # == Caveats
43
64
  #
44
65
  # When using after_commit callbacks, it is important to note that if the callback raises an error, the transaction
45
- # won't be rolled back. Relying solely on these to synchronize state between multiple systems may lead to consistency issues.
66
+ # won't be rolled back as it was already committed. Relying solely on these to synchronize state between multiple
67
+ # systems may lead to consistency issues.
46
68
  class Transaction
47
- class Callback # :nodoc:
48
- def initialize(event, callback)
49
- @event = event
50
- @callback = callback
51
- end
52
-
53
- def before_commit
54
- @callback.call if @event == :before_commit
55
- end
56
-
57
- def after_commit
58
- @callback.call if @event == :after_commit
59
- end
60
-
61
- def after_rollback
62
- @callback.call if @event == :after_rollback
63
- end
64
- end
65
-
66
- def initialize # :nodoc:
67
- @callbacks = nil
69
+ def initialize(internal_transaction) # :nodoc:
70
+ @internal_transaction = internal_transaction
68
71
  @uuid = nil
69
72
  end
70
73
 
71
- # Registers a block to be called before the current transaction is fully committed.
72
- #
73
- # If there is no currently open transactions, the block is called immediately.
74
- #
75
- # If the current transaction has a parent transaction, the callback is transferred to
76
- # the parent when the current transaction commits, or dropped when the current transaction
77
- # is rolled back. This operation is repeated until the outermost transaction is reached.
78
- #
79
- # If the callback raises an error, the transaction is rolled back.
80
- def before_commit(&block)
81
- (@callbacks ||= []) << Callback.new(:before_commit, block)
82
- end
83
-
84
- # Registers a block to be called after the current transaction is fully committed.
74
+ # Registers a block to be called after the transaction is fully committed.
85
75
  #
86
- # If there is no currently open transactions, the block is called immediately.
76
+ # If there is no currently open transactions, the block is called
77
+ # immediately, unless the transaction is finalized, in which case attempting
78
+ # to register the callback raises ActiveRecord::ActiveRecordError.
87
79
  #
88
- # If the current transaction has a parent transaction, the callback is transferred to
80
+ # If the transaction has a parent transaction, the callback is transferred to
89
81
  # the parent when the current transaction commits, or dropped when the current transaction
90
82
  # is rolled back. This operation is repeated until the outermost transaction is reached.
91
83
  #
92
84
  # If the callback raises an error, the transaction remains committed.
93
85
  def after_commit(&block)
94
- (@callbacks ||= []) << Callback.new(:after_commit, block)
86
+ if @internal_transaction.nil?
87
+ yield
88
+ else
89
+ @internal_transaction.after_commit(&block)
90
+ end
95
91
  end
96
92
 
97
- # Registers a block to be called after the current transaction is rolled back.
93
+ # Registers a block to be called after the transaction is rolled back.
98
94
  #
99
- # If there is no currently open transactions, the block is never called.
95
+ # If there is no currently open transactions, the block is not called. But
96
+ # if the transaction is finalized, attempting to register the callback
97
+ # raises ActiveRecord::ActiveRecordError.
100
98
  #
101
- # If the current transaction is successfully committed but has a parent
99
+ # If the transaction is successfully committed but has a parent
102
100
  # transaction, the callback is automatically added to the parent transaction.
103
101
  #
104
102
  # If the entire chain of nested transactions are all successfully committed,
105
103
  # the block is never called.
104
+ #
105
+ # If the transaction is already finalized, attempting to register a callback
106
+ # will raise ActiveRecord::ActiveRecordError.
106
107
  def after_rollback(&block)
107
- (@callbacks ||= []) << Callback.new(:after_rollback, block)
108
+ @internal_transaction&.after_rollback(&block)
108
109
  end
109
110
 
110
- # Returns true if a transaction was started.
111
+ # Returns true if the transaction exists and isn't finalized yet.
111
112
  def open?
112
- true
113
+ !closed?
113
114
  end
114
115
 
115
- # Returns true if no transaction is currently active.
116
+ # Returns true if the transaction doesn't exist or is finalized.
116
117
  def closed?
117
- false
118
+ @internal_transaction.nil? || @internal_transaction.state.finalized?
118
119
  end
120
+
119
121
  alias_method :blank?, :closed?
120
122
 
121
- # Returns a UUID for this transaction.
123
+ # Returns a UUID for this transaction or +nil+ if no transaction is open.
122
124
  def uuid
123
- @uuid ||= Digest::UUID.uuid_v4
125
+ if @internal_transaction
126
+ @uuid ||= Digest::UUID.uuid_v4
127
+ end
124
128
  end
125
129
 
126
- protected
127
- def append_callbacks(callbacks) # :nodoc:
128
- (@callbacks ||= []).concat(callbacks)
129
- end
130
+ NULL_TRANSACTION = new(nil).freeze
130
131
  end
131
132
  end
@@ -243,7 +243,7 @@ module ActiveRecord
243
243
  #
244
244
  # See the ActiveRecord::Transaction documentation for detailed behavior.
245
245
  def current_transaction
246
- connection_pool.active_connection&.current_transaction || ConnectionAdapters::TransactionManager::NULL_TRANSACTION
246
+ connection_pool.active_connection&.current_transaction&.user_transaction || Transaction::NULL_TRANSACTION
247
247
  end
248
248
 
249
249
  def before_commit(*args, &block) # :nodoc: