activerecord 7.2.0.beta1 → 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 (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).