activerecord 7.2.0.beta2 → 7.2.0.beta3

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 (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: