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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -1
- data/lib/active_record/associations/collection_association.rb +3 -3
- data/lib/active_record/associations/errors.rb +265 -0
- data/lib/active_record/associations.rb +0 -262
- data/lib/active_record/attribute_methods/dirty.rb +1 -1
- data/lib/active_record/attribute_methods/read.rb +3 -3
- data/lib/active_record/attribute_methods/write.rb +3 -3
- data/lib/active_record/attribute_methods.rb +8 -6
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +27 -11
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/transaction.rb +67 -9
- data/lib/active_record/connection_adapters/abstract_adapter.rb +8 -1
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +1 -1
- data/lib/active_record/database_configurations/database_config.rb +4 -0
- data/lib/active_record/errors.rb +20 -8
- data/lib/active_record/gem_version.rb +1 -1
- data/lib/active_record/railtie.rb +2 -3
- data/lib/active_record/railties/databases.rake +1 -1
- data/lib/active_record/relation/calculations.rb +1 -1
- data/lib/active_record/relation/query_methods.rb +21 -9
- data/lib/active_record/signed_id.rb +9 -0
- data/lib/active_record/test_fixtures.rb +10 -3
- data/lib/active_record/timestamp.rb +1 -1
- data/lib/active_record/transaction.rb +56 -55
- data/lib/active_record/transactions.rb +1 -1
- data/lib/active_record.rb +1 -1
- data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
- 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
|
-
|
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
|
-
|
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
|
-
|
342
|
-
|
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
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
@connections
|
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
|
-
|
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
|
125
|
-
def
|
124
|
+
def after_rollback; end
|
125
|
+
def user_transaction; ActiveRecord::Transaction::NULL_TRANSACTION; end
|
126
126
|
end
|
127
127
|
|
128
|
-
class Transaction
|
129
|
-
|
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
|
-
@
|
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
|
data/lib/active_record/errors.rb
CHANGED
@@ -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
|
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
|
147
|
-
#
|
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
|
-
#
|
150
|
-
#
|
151
|
-
#
|
152
|
-
#
|
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"
|
@@ -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
|
-
|
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
|
-
|
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[
|
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
|
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.
|
2071
|
-
|
2072
|
-
|
2073
|
-
|
2074
|
-
|
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
|
-
|
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
|
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]
|
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
|
-
#
|
6
|
+
# Class specifies the interface to interact with the current transaction state.
|
7
7
|
#
|
8
|
-
#
|
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
|
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
|
-
|
48
|
-
|
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
|
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
|
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
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
108
|
+
@internal_transaction&.after_rollback(&block)
|
108
109
|
end
|
109
110
|
|
110
|
-
# Returns true if
|
111
|
+
# Returns true if the transaction exists and isn't finalized yet.
|
111
112
|
def open?
|
112
|
-
|
113
|
+
!closed?
|
113
114
|
end
|
114
115
|
|
115
|
-
# Returns true if
|
116
|
+
# Returns true if the transaction doesn't exist or is finalized.
|
116
117
|
def closed?
|
117
|
-
|
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
|
-
@
|
125
|
+
if @internal_transaction
|
126
|
+
@uuid ||= Digest::UUID.uuid_v4
|
127
|
+
end
|
124
128
|
end
|
125
129
|
|
126
|
-
|
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 ||
|
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:
|