activerecord 7.2.0.beta2 → 7.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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).