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