activerecord 7.2.3 → 8.0.4

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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +391 -958
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/association_relation.rb +1 -0
  5. data/lib/active_record/associations/association.rb +34 -10
  6. data/lib/active_record/associations/builder/association.rb +7 -6
  7. data/lib/active_record/associations/collection_association.rb +1 -1
  8. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  9. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  10. data/lib/active_record/associations/preloader/association.rb +2 -2
  11. data/lib/active_record/associations/singular_association.rb +8 -3
  12. data/lib/active_record/associations.rb +34 -4
  13. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  14. data/lib/active_record/attribute_methods/primary_key.rb +4 -8
  15. data/lib/active_record/attribute_methods/query.rb +34 -0
  16. data/lib/active_record/attribute_methods/time_zone_conversion.rb +2 -12
  17. data/lib/active_record/autosave_association.rb +69 -27
  18. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +34 -25
  19. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  20. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  21. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +6 -15
  22. data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
  23. data/lib/active_record/connection_adapters/abstract/query_cache.rb +8 -2
  24. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  25. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -5
  26. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +7 -2
  27. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +34 -7
  28. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  29. data/lib/active_record/connection_adapters/abstract_adapter.rb +31 -43
  30. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +21 -40
  31. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  32. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  33. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +50 -45
  34. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +84 -94
  35. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -8
  36. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  37. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +72 -43
  38. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  39. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  40. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  41. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -11
  42. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +6 -12
  43. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +2 -1
  44. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +59 -16
  45. data/lib/active_record/connection_adapters/postgresql_adapter.rb +46 -96
  46. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  47. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +80 -100
  48. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  49. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
  50. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +9 -1
  51. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -12
  52. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  53. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  54. data/lib/active_record/connection_adapters/trilogy_adapter.rb +0 -17
  55. data/lib/active_record/connection_adapters.rb +0 -56
  56. data/lib/active_record/connection_handling.rb +23 -1
  57. data/lib/active_record/core.rb +29 -14
  58. data/lib/active_record/database_configurations/database_config.rb +4 -0
  59. data/lib/active_record/database_configurations/hash_config.rb +16 -2
  60. data/lib/active_record/encryption/config.rb +3 -1
  61. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  62. data/lib/active_record/encryption/encrypted_attribute_type.rb +10 -1
  63. data/lib/active_record/encryption/encryptor.rb +16 -8
  64. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  65. data/lib/active_record/encryption/scheme.rb +8 -1
  66. data/lib/active_record/enum.rb +9 -22
  67. data/lib/active_record/errors.rb +13 -5
  68. data/lib/active_record/fixtures.rb +0 -2
  69. data/lib/active_record/future_result.rb +13 -9
  70. data/lib/active_record/gem_version.rb +3 -3
  71. data/lib/active_record/insert_all.rb +1 -1
  72. data/lib/active_record/locking/optimistic.rb +1 -1
  73. data/lib/active_record/log_subscriber.rb +5 -11
  74. data/lib/active_record/migration/command_recorder.rb +31 -11
  75. data/lib/active_record/migration/compatibility.rb +5 -2
  76. data/lib/active_record/migration.rb +38 -42
  77. data/lib/active_record/model_schema.rb +3 -4
  78. data/lib/active_record/nested_attributes.rb +4 -6
  79. data/lib/active_record/persistence.rb +128 -130
  80. data/lib/active_record/query_logs.rb +102 -50
  81. data/lib/active_record/query_logs_formatter.rb +17 -28
  82. data/lib/active_record/querying.rb +8 -8
  83. data/lib/active_record/railtie.rb +2 -26
  84. data/lib/active_record/railties/databases.rake +11 -35
  85. data/lib/active_record/reflection.rb +18 -21
  86. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  87. data/lib/active_record/relation/batches.rb +132 -72
  88. data/lib/active_record/relation/calculations.rb +40 -39
  89. data/lib/active_record/relation/delegation.rb +25 -14
  90. data/lib/active_record/relation/finder_methods.rb +18 -18
  91. data/lib/active_record/relation/merger.rb +8 -8
  92. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  93. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  94. data/lib/active_record/relation/predicate_builder.rb +13 -0
  95. data/lib/active_record/relation/query_methods.rb +105 -61
  96. data/lib/active_record/relation/spawn_methods.rb +7 -7
  97. data/lib/active_record/relation.rb +79 -61
  98. data/lib/active_record/result.rb +66 -4
  99. data/lib/active_record/sanitization.rb +7 -6
  100. data/lib/active_record/schema_dumper.rb +5 -0
  101. data/lib/active_record/schema_migration.rb +2 -1
  102. data/lib/active_record/scoping/named.rb +5 -2
  103. data/lib/active_record/statement_cache.rb +14 -14
  104. data/lib/active_record/store.rb +7 -3
  105. data/lib/active_record/table_metadata.rb +1 -3
  106. data/lib/active_record/tasks/database_tasks.rb +69 -60
  107. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  108. data/lib/active_record/tasks/postgresql_database_tasks.rb +2 -1
  109. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  110. data/lib/active_record/test_databases.rb +1 -1
  111. data/lib/active_record/test_fixtures.rb +12 -0
  112. data/lib/active_record/token_for.rb +1 -1
  113. data/lib/active_record/transactions.rb +5 -6
  114. data/lib/active_record/validations/uniqueness.rb +8 -8
  115. data/lib/active_record.rb +21 -48
  116. data/lib/arel/collectors/bind.rb +2 -2
  117. data/lib/arel/collectors/sql_string.rb +1 -1
  118. data/lib/arel/collectors/substitute_binds.rb +2 -2
  119. data/lib/arel/nodes/binary.rb +1 -1
  120. data/lib/arel/nodes/node.rb +1 -1
  121. data/lib/arel/nodes/sql_literal.rb +1 -1
  122. data/lib/arel/table.rb +3 -7
  123. metadata +9 -10
  124. data/lib/active_record/relation/record_fetch_warning.rb +0 -52
data/README.rdoc CHANGED
@@ -139,7 +139,7 @@ A short rundown of some of the major features:
139
139
 
140
140
  * Database agnostic schema management with Migrations.
141
141
 
142
- class AddSystemSettings < ActiveRecord::Migration[7.2]
142
+ class AddSystemSettings < ActiveRecord::Migration[8.0]
143
143
  def up
144
144
  create_table :system_settings do |t|
145
145
  t.string :name
@@ -43,6 +43,7 @@ module ActiveRecord
43
43
  def exec_queries
44
44
  super do |record|
45
45
  @association.set_inverse_instance_from_queries(record)
46
+ @association.set_strict_loading(record)
46
47
  yield record if block_given?
47
48
  end
48
49
  end
@@ -34,7 +34,7 @@ module ActiveRecord
34
34
  # the <tt>reflection</tt> object represents a <tt>:has_many</tt> macro.
35
35
  class Association # :nodoc:
36
36
  attr_accessor :owner
37
- attr_reader :target, :reflection, :disable_joins
37
+ attr_reader :reflection, :disable_joins
38
38
 
39
39
  delegate :options, to: :reflection
40
40
 
@@ -50,6 +50,13 @@ module ActiveRecord
50
50
  @skip_strict_loading = nil
51
51
  end
52
52
 
53
+ def target
54
+ if @target.is_a?(Promise)
55
+ @target = @target.value
56
+ end
57
+ @target
58
+ end
59
+
53
60
  # Resets the \loaded flag to +false+ and sets the \target to +nil+.
54
61
  def reset
55
62
  @loaded = false
@@ -113,6 +120,14 @@ module ActiveRecord
113
120
  @association_scope = nil
114
121
  end
115
122
 
123
+ def set_strict_loading(record)
124
+ if owner.strict_loading_n_plus_one_only? && reflection.macro == :has_many
125
+ record.strict_loading!
126
+ else
127
+ record.strict_loading!(false, mode: owner.strict_loading_mode)
128
+ end
129
+ end
130
+
116
131
  # Set the inverse association, if possible
117
132
  def set_inverse_instance(record)
118
133
  if inverse = inverse_association_for(record)
@@ -172,7 +187,7 @@ module ActiveRecord
172
187
  # ActiveRecord::RecordNotFound is rescued within the method, and it is
173
188
  # not reraised. The proxy is \reset and +nil+ is the return value.
174
189
  def load_target
175
- @target = find_target if (@stale_state && stale_target?) || find_target?
190
+ @target = find_target(async: false) if (@stale_state && stale_target?) || find_target?
176
191
 
177
192
  loaded! unless loaded?
178
193
  target
@@ -180,6 +195,13 @@ module ActiveRecord
180
195
  reset
181
196
  end
182
197
 
198
+ def async_load_target # :nodoc:
199
+ @target = find_target(async: true) if (@stale_state && stale_target?) || find_target?
200
+
201
+ loaded! unless loaded?
202
+ nil
203
+ end
204
+
183
205
  # We can't dump @reflection and @through_reflection since it contains the scope proc
184
206
  def marshal_dump
185
207
  ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] }
@@ -223,13 +245,19 @@ module ActiveRecord
223
245
  klass
224
246
  end
225
247
 
226
- def find_target
248
+ def find_target(async: false)
227
249
  if violates_strict_loading?
228
250
  Base.strict_loading_violation!(owner: owner.class, reflection: reflection)
229
251
  end
230
252
 
231
253
  scope = self.scope
232
- return scope.to_a if skip_statement_cache?(scope)
254
+ if skip_statement_cache?(scope)
255
+ if async
256
+ return scope.load_async.then(&:to_a)
257
+ else
258
+ return scope.to_a
259
+ end
260
+ end
233
261
 
234
262
  sc = reflection.association_scope_cache(klass, owner) do |params|
235
263
  as = AssociationScope.create { params.bind }
@@ -238,13 +266,9 @@ module ActiveRecord
238
266
 
239
267
  binds = AssociationScope.get_bind_values(owner, reflection.chain)
240
268
  klass.with_connection do |c|
241
- sc.execute(binds, c) do |record|
269
+ sc.execute(binds, c, async: async) do |record|
242
270
  set_inverse_instance(record)
243
- if owner.strict_loading_n_plus_one_only? && reflection.macro == :has_many
244
- record.strict_loading!
245
- else
246
- record.strict_loading!(false, mode: owner.strict_loading_mode)
247
- end
271
+ set_strict_loading(record)
248
272
  end
249
273
  end
250
274
  end
@@ -30,10 +30,10 @@ module ActiveRecord::Associations::Builder # :nodoc:
30
30
  end
31
31
 
32
32
  reflection = create_reflection(model, name, scope, options, &block)
33
- define_accessors model, reflection
34
- define_callbacks model, reflection
35
- define_validations model, reflection
36
- define_change_tracking_methods model, reflection
33
+ define_accessors(model, reflection)
34
+ define_callbacks(model, reflection)
35
+ define_validations(model, reflection)
36
+ define_change_tracking_methods(model, reflection)
37
37
  reflection
38
38
  end
39
39
 
@@ -71,6 +71,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
71
71
  end
72
72
 
73
73
  def self.define_extensions(model, name)
74
+ # noop
74
75
  end
75
76
 
76
77
  def self.define_callbacks(model, reflection)
@@ -81,7 +82,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
81
82
  end
82
83
 
83
84
  Association.extensions.each do |extension|
84
- extension.build model, reflection
85
+ extension.build(model, reflection)
85
86
  end
86
87
  end
87
88
 
@@ -131,7 +132,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
131
132
  err_message = "A valid destroy_association_async_job is required to use `dependent: :destroy_async` on associations"
132
133
  raise ActiveRecord::ConfigurationError, err_message
133
134
  end
134
- unless valid_dependent_options.include? dependent
135
+ unless valid_dependent_options.include?(dependent)
135
136
  raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}"
136
137
  end
137
138
  end
@@ -94,7 +94,7 @@ module ActiveRecord
94
94
  def find(*args)
95
95
  if options[:inverse_of] && loaded?
96
96
  args_flatten = args.flatten
97
- model = scope.klass
97
+ model = scope.model
98
98
 
99
99
  if args_flatten.blank?
100
100
  error_message = "Couldn't find #{model.name} without an ID"
@@ -47,7 +47,7 @@ module ActiveRecord
47
47
  end
48
48
 
49
49
  if scope.order_values.empty? && ordered
50
- split_scope = DisableJoinsAssociationRelation.create(scope.klass, key, join_ids)
50
+ split_scope = DisableJoinsAssociationRelation.create(scope.model, key, join_ids)
51
51
  split_scope.where_clause += scope.where_clause
52
52
  split_scope
53
53
  else
@@ -146,7 +146,7 @@ module ActiveRecord
146
146
 
147
147
  case method
148
148
  when :destroy
149
- if scope.klass.primary_key
149
+ if scope.model.primary_key
150
150
  count = scope.destroy_all.count(&:destroyed?)
151
151
  else
152
152
  scope.each(&:_run_destroy_callbacks)
@@ -222,7 +222,8 @@ module ActiveRecord
222
222
  end
223
223
  end
224
224
 
225
- def find_target
225
+ def find_target(async: false)
226
+ raise NotImplementedError, "No async loading for HasManyThroughAssociation yet" if async
226
227
  return [] unless target_reflection_has_associated_record?
227
228
  return scope.to_a if disable_joins
228
229
  super
@@ -17,12 +17,12 @@ module ActiveRecord
17
17
  def eql?(other)
18
18
  association_key_name == other.association_key_name &&
19
19
  scope.table_name == other.scope.table_name &&
20
- scope.connection_specification_name == other.scope.connection_specification_name &&
20
+ scope.model.connection_specification_name == other.scope.model.connection_specification_name &&
21
21
  scope.values_for_queries == other.scope.values_for_queries
22
22
  end
23
23
 
24
24
  def hash
25
- [association_key_name, scope.table_name, scope.connection_specification_name, scope.values_for_queries].hash
25
+ [association_key_name, scope.model.table_name, scope.model.connection_specification_name, scope.values_for_queries].hash
26
26
  end
27
27
 
28
28
  def records_for(loaders)
@@ -18,6 +18,7 @@ module ActiveRecord
18
18
  def reset
19
19
  super
20
20
  @target = nil
21
+ @future_target = nil
21
22
  end
22
23
 
23
24
  # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
@@ -43,11 +44,15 @@ module ActiveRecord
43
44
  super.except!(*Array(klass.primary_key))
44
45
  end
45
46
 
46
- def find_target
47
+ def find_target(async: false)
47
48
  if disable_joins
48
- scope.first
49
+ if async
50
+ scope.load_async.then(&:first)
51
+ else
52
+ scope.first
53
+ end
49
54
  else
50
- super.first
55
+ super.then(&:first)
51
56
  end
52
57
  end
53
58
 
@@ -379,21 +379,43 @@ module ActiveRecord
379
379
  # after_add: :congratulate_client,
380
380
  # after_remove: :log_after_remove
381
381
  #
382
- # def congratulate_client(record)
382
+ # def congratulate_client(client)
383
383
  # # ...
384
384
  # end
385
385
  #
386
- # def log_after_remove(record)
386
+ # def log_after_remove(client)
387
387
  # # ...
388
388
  # end
389
389
  # end
390
390
  #
391
+ # Callbacks can be defined in three ways:
392
+ #
393
+ # 1. A symbol that references a method defined on the class with the
394
+ # associated collection. For example, <tt>after_add: :congratulate_client</tt>
395
+ # invokes <tt>Firm#congratulate_client(client)</tt>.
396
+ # 2. A callable with a signature that accepts both the record with the
397
+ # associated collection and the record being added or removed. For
398
+ # example, <tt>after_add: ->(firm, client) { ... }</tt>.
399
+ # 3. An object that responds to the callback name. For example, passing
400
+ # <tt>after_add: CallbackObject.new</tt> invokes <tt>CallbackObject#after_add(firm,
401
+ # client)</tt>.
402
+ #
391
403
  # It's possible to stack callbacks by passing them as an array. Example:
392
404
  #
405
+ # class CallbackObject
406
+ # def after_add(firm, client)
407
+ # firm.log << "after_adding #{client.id}"
408
+ # end
409
+ # end
410
+ #
393
411
  # class Firm < ActiveRecord::Base
394
412
  # has_many :clients,
395
413
  # dependent: :destroy,
396
- # after_add: [:congratulate_client, -> (firm, record) { firm.log << "after_adding#{record.id}" }],
414
+ # after_add: [
415
+ # :congratulate_client,
416
+ # -> (firm, client) { firm.log << "after_adding #{client.id}" },
417
+ # CallbackObject.new
418
+ # ],
397
419
  # after_remove: :log_after_remove
398
420
  # end
399
421
  #
@@ -1255,6 +1277,14 @@ module ActiveRecord
1255
1277
  # persisted new records placed at the end.
1256
1278
  # When set to +:nested_attributes_order+, the index is based on the record order received by
1257
1279
  # nested attributes setter, when accepts_nested_attributes_for is used.
1280
+ # [:before_add]
1281
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>before an object is added</b> to the association collection.
1282
+ # [:after_add]
1283
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>after an object is added</b> to the association collection.
1284
+ # [:before_remove]
1285
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>before an object is removed</b> from the association collection.
1286
+ # [:after_remove]
1287
+ # Defines an {association callback}[rdoc-ref:Associations::ClassMethods@Association+callbacks] that gets triggered <b>after an object is removed</b> from the association collection.
1258
1288
  #
1259
1289
  # Option examples:
1260
1290
  # has_many :comments, -> { order("posted_on") }
@@ -1678,7 +1708,7 @@ module ActiveRecord
1678
1708
  # The join table should not have a primary key or a model associated with it. You must manually generate the
1679
1709
  # join table with a migration such as this:
1680
1710
  #
1681
- # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[7.2]
1711
+ # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[8.0]
1682
1712
  # def change
1683
1713
  # create_join_table :developers, :projects
1684
1714
  # end
@@ -1,29 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/atomic/atomic_boolean"
4
+ require "concurrent/atomic/read_write_lock"
5
+
3
6
  module ActiveRecord
4
7
  class AsynchronousQueriesTracker # :nodoc:
5
- module NullSession # :nodoc:
6
- class << self
7
- def active?
8
- true
9
- end
10
-
11
- def finalize
12
- end
13
- end
14
- end
15
-
16
8
  class Session # :nodoc:
17
9
  def initialize
18
- @active = true
10
+ @active = Concurrent::AtomicBoolean.new(true)
11
+ @lock = Concurrent::ReadWriteLock.new
19
12
  end
20
13
 
21
14
  def active?
22
- @active
15
+ @active.true?
23
16
  end
24
17
 
25
- def finalize
26
- @active = false
18
+ def synchronize(&block)
19
+ @lock.with_read_lock(&block)
20
+ end
21
+
22
+ def finalize(wait = false)
23
+ @active.make_false
24
+ if wait
25
+ # Wait until all thread with a read lock are done
26
+ @lock.with_write_lock { }
27
+ end
27
28
  end
28
29
  end
29
30
 
@@ -33,7 +34,7 @@ module ActiveRecord
33
34
  end
34
35
 
35
36
  def run
36
- ActiveRecord::Base.asynchronous_queries_tracker.start_session
37
+ ActiveRecord::Base.asynchronous_queries_tracker.tap(&:start_session)
37
38
  end
38
39
 
39
40
  def complete(asynchronous_queries_tracker)
@@ -41,20 +42,23 @@ module ActiveRecord
41
42
  end
42
43
  end
43
44
 
44
- attr_reader :current_session
45
-
46
45
  def initialize
47
- @current_session = NullSession
46
+ @stack = []
47
+ end
48
+
49
+ def current_session
50
+ @stack.last or raise ActiveRecordError, "Can't perform asynchronous queries without a query session"
48
51
  end
49
52
 
50
53
  def start_session
51
- @current_session = Session.new
52
- self
54
+ session = Session.new
55
+ @stack << session
53
56
  end
54
57
 
55
- def finalize_session
56
- @current_session.finalize
57
- @current_session = NullSession
58
+ def finalize_session(wait = false)
59
+ session = @stack.pop
60
+ session&.finalize(wait)
61
+ self
58
62
  end
59
63
  end
60
64
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module ActiveRecord
6
4
  module AttributeMethods
7
5
  # = Active Record Attribute Methods Primary Key
@@ -89,10 +87,9 @@ module ActiveRecord
89
87
  @composite_primary_key
90
88
  end
91
89
 
92
- # Returns a quoted version of the primary key name, used to construct
93
- # SQL statements.
90
+ # Returns a quoted version of the primary key name.
94
91
  def quoted_primary_key
95
- @quoted_primary_key ||= adapter_class.quote_column_name(primary_key)
92
+ adapter_class.quote_column_name(primary_key)
96
93
  end
97
94
 
98
95
  def reset_primary_key # :nodoc:
@@ -132,13 +129,13 @@ module ActiveRecord
132
129
  # Project.primary_key # => "foo_id"
133
130
  def primary_key=(value)
134
131
  @primary_key = if value.is_a?(Array)
135
- @composite_primary_key = true
136
132
  include CompositePrimaryKey
137
133
  @primary_key = value.map { |v| -v.to_s }.freeze
138
134
  elsif value
139
135
  -value.to_s
140
136
  end
141
- @quoted_primary_key = nil
137
+
138
+ @composite_primary_key = value.is_a?(Array)
142
139
  @attributes_builder = nil
143
140
  end
144
141
 
@@ -148,7 +145,6 @@ module ActiveRecord
148
145
  base.class_eval do
149
146
  @primary_key = PRIMARY_KEY_NOT_SET
150
147
  @composite_primary_key = false
151
- @quoted_primary_key = nil
152
148
  @attributes_builder = nil
153
149
  end
154
150
  end
@@ -3,6 +3,38 @@
3
3
  module ActiveRecord
4
4
  module AttributeMethods
5
5
  # = Active Record Attribute Methods \Query
6
+ #
7
+ # Adds query methods for attributes that return either +true+ or +false+
8
+ # depending on the attribute type and value.
9
+ #
10
+ # For Boolean attributes this will return +true+ if the value is present
11
+ # and return +false+ otherwise:
12
+ #
13
+ # class Product < ActiveRecord::Base
14
+ # end
15
+ #
16
+ # product = Product.new(archived: false)
17
+ # product.archived? # => false
18
+ # product.archived = true
19
+ # product.archived? # => true
20
+ #
21
+ # For Numeric attributes this will return +true+ if the value is a non-zero
22
+ # number and return +false+ otherwise:
23
+ #
24
+ # product.inventory_count = 0
25
+ # product.inventory_count? # => false
26
+ # product.inventory_count = 1
27
+ # product.inventory_count? # => true
28
+ #
29
+ # For other attributes it will return +true+ if the value is present
30
+ # and return +false+ otherwise:
31
+ #
32
+ # product.name = nil
33
+ # product.name? # => false
34
+ # product.name = " "
35
+ # product.name? # => false
36
+ # product.name = "Orange"
37
+ # product.name? # => true
6
38
  module Query
7
39
  extend ActiveSupport::Concern
8
40
 
@@ -10,6 +42,8 @@ module ActiveRecord
10
42
  attribute_method_suffix "?", parameters: false
11
43
  end
12
44
 
45
+ # Returns +true+ or +false+ for the attribute identified by +attr_name+,
46
+ # depending on the attribute type and value.
13
47
  def query_attribute(attr_name)
14
48
  value = self.public_send(attr_name)
15
49
 
@@ -28,7 +28,7 @@ module ActiveRecord
28
28
  elsif value.respond_to?(:infinite?) && value.infinite?
29
29
  value
30
30
  else
31
- map_avoiding_infinite_recursion(super) { |v| cast(v) }
31
+ map(super) { |v| cast(v) }
32
32
  end
33
33
  end
34
34
 
@@ -45,23 +45,13 @@ module ActiveRecord
45
45
  elsif value.respond_to?(:infinite?) && value.infinite?
46
46
  value
47
47
  else
48
- map_avoiding_infinite_recursion(value) { |v| convert_time_to_time_zone(v) }
48
+ map(value) { |v| convert_time_to_time_zone(v) }
49
49
  end
50
50
  end
51
51
 
52
52
  def set_time_zone_without_conversion(value)
53
53
  ::Time.zone.local_to_utc(value).try(:in_time_zone) if value
54
54
  end
55
-
56
- def map_avoiding_infinite_recursion(value)
57
- map(value) do |v|
58
- if value.equal?(v)
59
- nil
60
- else
61
- yield(v)
62
- end
63
- end
64
- end
65
55
  end
66
56
 
67
57
  extend ActiveSupport::Concern
@@ -221,8 +221,10 @@ module ActiveRecord
221
221
  if reflection.validate? && !method_defined?(validation_method)
222
222
  if reflection.collection?
223
223
  method = :validate_collection_association
224
+ elsif reflection.has_one?
225
+ method = :validate_has_one_association
224
226
  else
225
- method = :validate_single_association
227
+ method = :validate_belongs_to_association
226
228
  end
227
229
 
228
230
  define_non_cyclic_method(validation_method) { send(method, reflection) }
@@ -274,6 +276,16 @@ module ActiveRecord
274
276
  new_record? || has_changes_to_save? || marked_for_destruction? || nested_records_changed_for_autosave?
275
277
  end
276
278
 
279
+ def validating_belongs_to_for?(association)
280
+ @validating_belongs_to_for ||= {}
281
+ @validating_belongs_to_for[association]
282
+ end
283
+
284
+ def autosaving_belongs_to_for?(association)
285
+ @autosaving_belongs_to_for ||= {}
286
+ @autosaving_belongs_to_for[association]
287
+ end
288
+
277
289
  private
278
290
  def init_internals
279
291
  super
@@ -313,11 +325,33 @@ module ActiveRecord
313
325
  end
314
326
 
315
327
  # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
316
- # turned on for the association.
317
- def validate_single_association(reflection)
328
+ # turned on for the has_one association.
329
+ def validate_has_one_association(reflection)
330
+ association = association_instance_get(reflection.name)
331
+ record = association && association.reader
332
+ return unless record && (record.changed_for_autosave? || custom_validation_context?)
333
+
334
+ inverse_association = reflection.inverse_of && record.association(reflection.inverse_of.name)
335
+ return if inverse_association && (record.validating_belongs_to_for?(inverse_association) ||
336
+ record.autosaving_belongs_to_for?(inverse_association))
337
+
338
+ association_valid?(association, record)
339
+ end
340
+
341
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
342
+ # turned on for the belongs_to association.
343
+ def validate_belongs_to_association(reflection)
318
344
  association = association_instance_get(reflection.name)
319
345
  record = association && association.reader
320
- association_valid?(association, record) if record && (record.changed_for_autosave? || custom_validation_context?)
346
+ return unless record && (record.changed_for_autosave? || custom_validation_context?)
347
+
348
+ begin
349
+ @validating_belongs_to_for ||= {}
350
+ @validating_belongs_to_for[association] = true
351
+ association_valid?(association, record)
352
+ ensure
353
+ @validating_belongs_to_for[association] = false
354
+ end
321
355
  end
322
356
 
323
357
  # Validate the associated records if <tt>:validate</tt> or
@@ -441,33 +475,34 @@ module ActiveRecord
441
475
  return unless association && association.loaded?
442
476
 
443
477
  record = association.load_target
478
+ return unless record && !record.destroyed?
444
479
 
445
- if record && !record.destroyed?
446
- autosave = reflection.options[:autosave]
447
-
448
- if autosave && record.marked_for_destruction?
449
- record.destroy
450
- elsif autosave != false
451
- primary_key = Array(compute_primary_key(reflection, self)).map(&:to_s)
452
- primary_key_value = primary_key.map { |key| _read_attribute(key) }
480
+ autosave = reflection.options[:autosave]
453
481
 
454
- if (autosave && record.changed_for_autosave?) || _record_changed?(reflection, record, primary_key_value)
455
- unless reflection.through_reflection
456
- foreign_key = Array(reflection.foreign_key)
457
- primary_key_foreign_key_pairs = primary_key.zip(foreign_key)
482
+ if autosave && record.marked_for_destruction?
483
+ record.destroy
484
+ elsif autosave != false
485
+ primary_key = Array(compute_primary_key(reflection, self)).map(&:to_s)
486
+ primary_key_value = primary_key.map { |key| _read_attribute(key) }
487
+ return unless (autosave && record.changed_for_autosave?) || _record_changed?(reflection, record, primary_key_value)
458
488
 
459
- primary_key_foreign_key_pairs.each do |primary_key, foreign_key|
460
- association_id = _read_attribute(primary_key)
461
- record[foreign_key] = association_id unless record[foreign_key] == association_id
462
- end
463
- association.set_inverse_instance(record)
464
- end
489
+ unless reflection.through_reflection
490
+ foreign_key = Array(reflection.foreign_key)
491
+ primary_key_foreign_key_pairs = primary_key.zip(foreign_key)
465
492
 
466
- saved = record.save(validate: !autosave)
467
- raise ActiveRecord::Rollback if !saved && autosave
468
- saved
493
+ primary_key_foreign_key_pairs.each do |primary_key, foreign_key|
494
+ association_id = _read_attribute(primary_key)
495
+ record[foreign_key] = association_id unless record[foreign_key] == association_id
469
496
  end
497
+ association.set_inverse_instance(record)
470
498
  end
499
+
500
+ inverse_association = reflection.inverse_of && record.association(reflection.inverse_of.name)
501
+ return if inverse_association && record.autosaving_belongs_to_for?(inverse_association)
502
+
503
+ saved = record.save(validate: !autosave)
504
+ raise ActiveRecord::Rollback if !saved && autosave
505
+ saved
471
506
  end
472
507
  end
473
508
 
@@ -492,7 +527,6 @@ module ActiveRecord
492
527
  return false unless reflection.inverse_of&.polymorphic?
493
528
 
494
529
  class_name = record._read_attribute(reflection.inverse_of.foreign_type)
495
-
496
530
  reflection.active_record.polymorphic_name != class_name
497
531
  end
498
532
 
@@ -512,7 +546,15 @@ module ActiveRecord
512
546
  foreign_key.each { |key| self[key] = nil }
513
547
  record.destroy
514
548
  elsif autosave != false
515
- saved = record.save(validate: !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
549
+ saved = if record.new_record? || (autosave && record.changed_for_autosave?)
550
+ begin
551
+ @autosaving_belongs_to_for ||= {}
552
+ @autosaving_belongs_to_for[association] = true
553
+ record.save(validate: !autosave)
554
+ ensure
555
+ @autosaving_belongs_to_for[association] = false
556
+ end
557
+ end
516
558
 
517
559
  if association.updated?
518
560
  primary_key = Array(compute_primary_key(reflection, record)).map(&:to_s)