activerecord 7.2.0.beta2 → 7.2.0.rc1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -1
  3. data/lib/active_record/associations/association.rb +6 -0
  4. data/lib/active_record/associations/collection_association.rb +7 -3
  5. data/lib/active_record/associations/errors.rb +265 -0
  6. data/lib/active_record/associations/nested_error.rb +1 -1
  7. data/lib/active_record/associations.rb +2 -264
  8. data/lib/active_record/attribute_methods/dirty.rb +1 -1
  9. data/lib/active_record/attribute_methods/read.rb +3 -3
  10. data/lib/active_record/attribute_methods/write.rb +3 -3
  11. data/lib/active_record/attribute_methods.rb +8 -6
  12. data/lib/active_record/autosave_association.rb +3 -1
  13. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +27 -11
  14. data/lib/active_record/connection_adapters/abstract/database_statements.rb +1 -1
  15. data/lib/active_record/connection_adapters/abstract/query_cache.rb +1 -1
  16. data/lib/active_record/connection_adapters/abstract/transaction.rb +67 -9
  17. data/lib/active_record/connection_adapters/abstract_adapter.rb +8 -1
  18. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +1 -1
  19. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +5 -10
  20. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +28 -10
  21. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +3 -9
  22. data/lib/active_record/database_configurations/database_config.rb +4 -0
  23. data/lib/active_record/errors.rb +32 -11
  24. data/lib/active_record/gem_version.rb +1 -1
  25. data/lib/active_record/railtie.rb +2 -3
  26. data/lib/active_record/railties/databases.rake +1 -1
  27. data/lib/active_record/relation/batches.rb +7 -1
  28. data/lib/active_record/relation/calculations.rb +1 -1
  29. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -3
  30. data/lib/active_record/relation/query_methods.rb +25 -13
  31. data/lib/active_record/signed_id.rb +9 -0
  32. data/lib/active_record/tasks/database_tasks.rb +19 -8
  33. data/lib/active_record/test_fixtures.rb +10 -3
  34. data/lib/active_record/timestamp.rb +1 -1
  35. data/lib/active_record/transaction.rb +56 -55
  36. data/lib/active_record/transactions.rb +1 -1
  37. data/lib/active_record.rb +1 -1
  38. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  39. metadata +13 -12
@@ -173,7 +173,7 @@ module ActiveRecord
173
173
  end # end
174
174
 
175
175
  def #{method_name}=(value) # def includes_values=(value)
176
- assert_mutability! # assert_mutability!
176
+ assert_modifiable! # assert_modifiable!
177
177
  @values[:#{name}] = value # @values[:includes] = value
178
178
  end # end
179
179
  CODE
@@ -796,7 +796,7 @@ module ActiveRecord
796
796
  if !VALID_UNSCOPING_VALUES.include?(scope)
797
797
  raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
798
798
  end
799
- assert_mutability!
799
+ assert_modifiable!
800
800
  @values.delete(scope)
801
801
  when Hash
802
802
  scope.each do |key, target_value|
@@ -1705,8 +1705,8 @@ module ActiveRecord
1705
1705
  )
1706
1706
  end
1707
1707
 
1708
- def assert_mutability!
1709
- raise ImmutableRelation if @loaded || @arel
1708
+ def assert_modifiable!
1709
+ raise UnmodifiableRelation if @loaded || @arel
1710
1710
  end
1711
1711
 
1712
1712
  def build_arel(connection, aliases = nil)
@@ -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.
@@ -192,9 +192,17 @@ module ActiveRecord
192
192
 
193
193
  seed = true
194
194
  end
195
+ end
196
+ end
195
197
 
196
- migrate
197
- dump_schema(db_config) if ActiveRecord.dump_schema_after_migration
198
+ each_current_environment(env) do |environment|
199
+ db_configs_with_versions(environment).sort.each do |version, db_configs|
200
+ db_configs.each do |db_config|
201
+ with_temporary_pool(db_config) do
202
+ migrate(version)
203
+ dump_schema(db_config) if ActiveRecord.dump_schema_after_migration
204
+ end
205
+ end
198
206
  end
199
207
  end
200
208
 
@@ -255,10 +263,10 @@ module ActiveRecord
255
263
  Migration.verbose = verbose_was
256
264
  end
257
265
 
258
- def db_configs_with_versions # :nodoc:
266
+ def db_configs_with_versions(environment = env) # :nodoc:
259
267
  db_configs_with_versions = Hash.new { |h, k| h[k] = [] }
260
268
 
261
- with_temporary_pool_for_each do |pool|
269
+ with_temporary_pool_for_each(env: environment) do |pool|
262
270
  db_config = pool.db_config
263
271
  versions_to_run = pool.migration_context.pending_migration_versions
264
272
  target_version = ActiveRecord::Tasks::DatabaseTasks.target_version
@@ -580,10 +588,7 @@ module ActiveRecord
580
588
  end
581
589
 
582
590
  def each_current_configuration(environment, name = nil)
583
- environments = [environment]
584
- environments << "test" if environment == "development" && !ENV["SKIP_TEST_DATABASE"] && !ENV["DATABASE_URL"]
585
-
586
- environments.each do |env|
591
+ each_current_environment(environment) do |env|
587
592
  configs_for(env_name: env).each do |db_config|
588
593
  next if name && name != db_config.name
589
594
 
@@ -592,6 +597,12 @@ module ActiveRecord
592
597
  end
593
598
  end
594
599
 
600
+ def each_current_environment(environment, &block)
601
+ environments = [environment]
602
+ environments << "test" if environment == "development" && !ENV["SKIP_TEST_DATABASE"] && !ENV["DATABASE_URL"]
603
+ environments.each(&block)
604
+ end
605
+
595
606
  def each_local_configuration
596
607
  configs_for.each do |db_config|
597
608
  next unless db_config.database
@@ -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:
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
@@ -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.beta2
4
+ version: 7.2.0.rc1
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-06-04 00:00:00.000000000 Z
11
+ date: 2024-08-06 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.beta2
19
+ version: 7.2.0.rc1
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.beta2
26
+ version: 7.2.0.rc1
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.beta2
33
+ version: 7.2.0.rc1
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.beta2
40
+ version: 7.2.0.rc1
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.beta2/activerecord/CHANGELOG.md
479
- documentation_uri: https://api.rubyonrails.org/v7.2.0.beta2/
479
+ changelog_uri: https://github.com/rails/rails/blob/v7.2.0.rc1/activerecord/CHANGELOG.md
480
+ documentation_uri: https://api.rubyonrails.org/v7.2.0.rc1/
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.beta2/activerecord
482
+ source_code_uri: https://github.com/rails/rails/tree/v7.2.0.rc1/activerecord
482
483
  rubygems_mfa_required: 'true'
483
484
  post_install_message:
484
485
  rdoc_options:
@@ -493,11 +494,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
493
494
  version: 3.1.0
494
495
  required_rubygems_version: !ruby/object:Gem::Requirement
495
496
  requirements:
496
- - - ">"
497
+ - - ">="
497
498
  - !ruby/object:Gem::Version
498
- version: 1.3.1
499
+ version: '0'
499
500
  requirements: []
500
- rubygems_version: 3.3.27
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).