activerecord 7.2.0.beta1 → 7.2.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -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 +29 -13
  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 -0
  13. data/lib/active_record/connection_adapters/abstract/transaction.rb +69 -9
  14. data/lib/active_record/connection_adapters/abstract_adapter.rb +8 -0
  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/enum.rb +1 -1
  18. data/lib/active_record/errors.rb +20 -8
  19. data/lib/active_record/gem_version.rb +1 -1
  20. data/lib/active_record/railtie.rb +3 -4
  21. data/lib/active_record/railties/databases.rake +1 -1
  22. data/lib/active_record/relation/calculations.rb +1 -1
  23. data/lib/active_record/relation/query_methods.rb +22 -10
  24. data/lib/active_record/signed_id.rb +9 -0
  25. data/lib/active_record/tasks/database_tasks.rb +2 -2
  26. data/lib/active_record/test_fixtures.rb +10 -3
  27. data/lib/active_record/timestamp.rb +1 -1
  28. data/lib/active_record/transaction.rb +106 -42
  29. data/lib/active_record/transactions.rb +29 -2
  30. data/lib/active_record.rb +5 -5
  31. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  32. metadata +11 -10
@@ -1,68 +1,132 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/digest"
4
+
3
5
  module ActiveRecord
6
+ # Class specifies the interface to interact with the current transaction state.
7
+ #
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.
30
+ #
31
+ # == Callbacks
32
+ #
33
+ # After updating the database state, you may sometimes need to perform some extra work, or reflect these
34
+ # changes in a remote system like clearing or updating a cache:
35
+ #
36
+ # def publish_article(article)
37
+ # article.update!(published: true)
38
+ # NotificationService.article_published(article)
39
+ # end
40
+ #
41
+ # The above code works but has one important flaw, which is that it no longer works properly if called inside
42
+ # a transaction, as it will interact with the remote system before the changes are persisted:
43
+ #
44
+ # Article.transaction do
45
+ # article = create_article(article)
46
+ # publish_article(article)
47
+ # end
48
+ #
49
+ # The callbacks offered by ActiveRecord::Transaction allow to rewriting this method in a way that is compatible
50
+ # with transactions:
51
+ #
52
+ # def publish_article(article)
53
+ # article.update!(published: true)
54
+ # Article.current_transaction.after_commit do
55
+ # NotificationService.article_published(article)
56
+ # end
57
+ # end
58
+ #
59
+ # In the above example, if +publish_article+ is called inside a transaction, the callback will be invoked
60
+ # after the transaction is successfully committed, and if called outside a transaction, the callback will be invoked
61
+ # immediately.
62
+ #
63
+ # == Caveats
64
+ #
65
+ # When using after_commit callbacks, it is important to note that if the callback raises an error, the transaction
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.
4
68
  class Transaction
5
- class Callback # :nodoc:
6
- def initialize(event, callback)
7
- @event = event
8
- @callback = callback
9
- end
10
-
11
- def before_commit
12
- @callback.call if @event == :before_commit
13
- end
14
-
15
- def after_commit
16
- @callback.call if @event == :after_commit
17
- end
18
-
19
- def after_rollback
20
- @callback.call if @event == :after_rollback
21
- end
22
- end
23
-
24
- def initialize # :nodoc:
25
- @callbacks = nil
69
+ def initialize(internal_transaction) # :nodoc:
70
+ @internal_transaction = internal_transaction
71
+ @uuid = nil
26
72
  end
27
73
 
28
- # Registers a block to be called before the current transaction is fully committed.
74
+ # Registers a block to be called after the transaction is fully committed.
29
75
  #
30
- # 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.
31
79
  #
32
- # 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
33
81
  # the parent when the current transaction commits, or dropped when the current transaction
34
82
  # is rolled back. This operation is repeated until the outermost transaction is reached.
35
- def before_commit(&block)
36
- (@callbacks ||= []) << Callback.new(:before_commit, block)
37
- end
38
-
39
- # Registers a block to be called after the current transaction is fully committed.
40
83
  #
41
- # If there is no currently open transactions, the block is called immediately.
42
- #
43
- # If the current transaction has a parent transaction, the callback is transferred to
44
- # the parent when the current transaction commits, or dropped when the current transaction
45
- # is rolled back. This operation is repeated until the outermost transaction is reached.
84
+ # If the callback raises an error, the transaction remains committed.
46
85
  def after_commit(&block)
47
- (@callbacks ||= []) << Callback.new(:after_commit, block)
86
+ if @internal_transaction.nil?
87
+ yield
88
+ else
89
+ @internal_transaction.after_commit(&block)
90
+ end
48
91
  end
49
92
 
50
- # 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.
51
94
  #
52
- # 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.
53
98
  #
54
- # If the current transaction is successfully committed but has a parent
99
+ # If the transaction is successfully committed but has a parent
55
100
  # transaction, the callback is automatically added to the parent transaction.
56
101
  #
57
102
  # If the entire chain of nested transactions are all successfully committed,
58
103
  # the block is never called.
104
+ #
105
+ # If the transaction is already finalized, attempting to register a callback
106
+ # will raise ActiveRecord::ActiveRecordError.
59
107
  def after_rollback(&block)
60
- (@callbacks ||= []) << Callback.new(:after_rollback, block)
108
+ @internal_transaction&.after_rollback(&block)
109
+ end
110
+
111
+ # Returns true if the transaction exists and isn't finalized yet.
112
+ def open?
113
+ !closed?
114
+ end
115
+
116
+ # Returns true if the transaction doesn't exist or is finalized.
117
+ def closed?
118
+ @internal_transaction.nil? || @internal_transaction.state.finalized?
61
119
  end
62
120
 
63
- protected
64
- def append_callbacks(callbacks)
65
- (@callbacks ||= []).concat(callbacks)
121
+ alias_method :blank?, :closed?
122
+
123
+ # Returns a UUID for this transaction or +nil+ if no transaction is open.
124
+ def uuid
125
+ if @internal_transaction
126
+ @uuid ||= Digest::UUID.uuid_v4
66
127
  end
128
+ end
129
+
130
+ NULL_TRANSACTION = new(nil).freeze
67
131
  end
68
132
  end
@@ -188,6 +188,27 @@ module ActiveRecord
188
188
  # #after_commit is a good spot to put in a hook to clearing a cache since clearing it from
189
189
  # within a transaction could trigger the cache to be regenerated before the database is updated.
190
190
  #
191
+ # ==== NOTE: Callbacks are deduplicated per callback by filter.
192
+ #
193
+ # Trying to define multiple callbacks with the same filter will result in a single callback being run.
194
+ #
195
+ # For example:
196
+ #
197
+ # after_commit :do_something
198
+ # after_commit :do_something # only the last one will be called
199
+ #
200
+ # This applies to all variations of <tt>after_*_commit</tt> callbacks as well.
201
+ #
202
+ # after_commit :do_something
203
+ # after_create_commit :do_something
204
+ # after_save_commit :do_something
205
+ #
206
+ # It is recommended to use the +on:+ option to specify when the callback should be run.
207
+ #
208
+ # after_commit :do_something, on: [:create, :update]
209
+ #
210
+ # This is equivalent to using +after_create_commit+ and +after_update_commit+, but will not be deduplicated.
211
+ #
191
212
  # === Caveats
192
213
  #
193
214
  # If you're on MySQL, then do not use Data Definition Language (DDL) operations in nested
@@ -214,9 +235,15 @@ module ActiveRecord
214
235
  end
215
236
  end
216
237
 
217
- # Returns the current transaction. See ActiveRecord::Transactions API docs.
238
+ # Returns a representation of the current transaction state,
239
+ # which can be a top level transaction, a savepoint, or the absence of a transaction.
240
+ #
241
+ # An object is always returned, whether or not a transaction is currently active.
242
+ # To check if a transaction was opened, use <tt>current_transaction.open?</tt>.
243
+ #
244
+ # See the ActiveRecord::Transaction documentation for detailed behavior.
218
245
  def current_transaction
219
- connection_pool.active_connection&.current_transaction || ConnectionAdapters::TransactionManager::NULL_TRANSACTION
246
+ connection_pool.active_connection&.current_transaction&.user_transaction || Transaction::NULL_TRANSACTION
220
247
  end
221
248
 
222
249
  def before_commit(*args, &block) # :nodoc:
data/lib/active_record.rb CHANGED
@@ -290,7 +290,7 @@ module ActiveRecord
290
290
  # with the global thread pool async query executor.
291
291
  def self.global_executor_concurrency=(global_executor_concurrency)
292
292
  if self.async_query_executor.nil? || self.async_query_executor == :multi_thread_pool
293
- raise ArgumentError, "`global_executor_concurrency` cannot be set when using the executor is nil or set to multi_thead_pool. For multiple thread pools, please set the concurrency in your database configuration."
293
+ raise ArgumentError, "`global_executor_concurrency` cannot be set when the executor is nil or set to `:multi_thread_pool`. For multiple thread pools, please set the concurrency in your database configuration."
294
294
  end
295
295
 
296
296
  @global_executor_concurrency = global_executor_concurrency
@@ -347,14 +347,14 @@ module ActiveRecord
347
347
  def self.commit_transaction_on_non_local_return
348
348
  ActiveRecord.deprecator.warn <<-WARNING.squish
349
349
  `Rails.application.config.active_record.commit_transaction_on_non_local_return`
350
- is deprecated and will be removed in Rails 7.3.
350
+ is deprecated and will be removed in Rails 8.0.
351
351
  WARNING
352
352
  end
353
353
 
354
354
  def self.commit_transaction_on_non_local_return=(value)
355
355
  ActiveRecord.deprecator.warn <<-WARNING.squish
356
356
  `Rails.application.config.active_record.commit_transaction_on_non_local_return`
357
- is deprecated and will be removed in Rails 7.3.
357
+ is deprecated and will be removed in Rails 8.0.
358
358
  WARNING
359
359
  end
360
360
 
@@ -447,14 +447,14 @@ module ActiveRecord
447
447
  def self.allow_deprecated_singular_associations_name
448
448
  ActiveRecord.deprecator.warn <<-WARNING.squish
449
449
  `Rails.application.config.active_record.allow_deprecated_singular_associations_name`
450
- is deprecated and will be removed in Rails 7.3.
450
+ is deprecated and will be removed in Rails 8.0.
451
451
  WARNING
452
452
  end
453
453
 
454
454
  def self.allow_deprecated_singular_associations_name=(value)
455
455
  ActiveRecord.deprecator.warn <<-WARNING.squish
456
456
  `Rails.application.config.active_record.allow_deprecated_singular_associations_name`
457
- is deprecated and will be removed in Rails 7.3.
457
+ is deprecated and will be removed in Rails 8.0.
458
458
  WARNING
459
459
  end
460
460
 
@@ -12,7 +12,10 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
12
12
  t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
13
13
  <% end -%>
14
14
  <% end -%>
15
- <% if options[:timestamps] %>
15
+ <% unless attributes.empty? -%>
16
+
17
+ <% end -%>
18
+ <% if options[:timestamps] -%>
16
19
  t.timestamps
17
20
  <% end -%>
18
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.0.beta1
4
+ version: 7.2.0.beta3
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-29 00:00:00.000000000 Z
11
+ date: 2024-07-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 7.2.0.beta1
19
+ version: 7.2.0.beta3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 7.2.0.beta1
26
+ version: 7.2.0.beta3
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activemodel
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - '='
32
32
  - !ruby/object:Gem::Version
33
- version: 7.2.0.beta1
33
+ version: 7.2.0.beta3
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - '='
39
39
  - !ruby/object:Gem::Version
40
- version: 7.2.0.beta1
40
+ version: 7.2.0.beta3
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: timeout
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -85,6 +85,7 @@ files:
85
85
  - lib/active_record/associations/collection_association.rb
86
86
  - lib/active_record/associations/collection_proxy.rb
87
87
  - lib/active_record/associations/disable_joins_association_scope.rb
88
+ - lib/active_record/associations/errors.rb
88
89
  - lib/active_record/associations/foreign_association.rb
89
90
  - lib/active_record/associations/has_many_association.rb
90
91
  - lib/active_record/associations/has_many_through_association.rb
@@ -475,10 +476,10 @@ licenses:
475
476
  - MIT
476
477
  metadata:
477
478
  bug_tracker_uri: https://github.com/rails/rails/issues
478
- changelog_uri: https://github.com/rails/rails/blob/v7.2.0.beta1/activerecord/CHANGELOG.md
479
- documentation_uri: https://api.rubyonrails.org/v7.2.0.beta1/
479
+ changelog_uri: https://github.com/rails/rails/blob/v7.2.0.beta3/activerecord/CHANGELOG.md
480
+ documentation_uri: https://api.rubyonrails.org/v7.2.0.beta3/
480
481
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
481
- source_code_uri: https://github.com/rails/rails/tree/v7.2.0.beta1/activerecord
482
+ source_code_uri: https://github.com/rails/rails/tree/v7.2.0.beta3/activerecord
482
483
  rubygems_mfa_required: 'true'
483
484
  post_install_message:
484
485
  rdoc_options:
@@ -497,7 +498,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
497
498
  - !ruby/object:Gem::Version
498
499
  version: '0'
499
500
  requirements: []
500
- rubygems_version: 3.5.10
501
+ rubygems_version: 3.5.11
501
502
  signing_key:
502
503
  specification_version: 4
503
504
  summary: Object-relational mapper framework (part of Rails).