activerecord 8.0.0 → 8.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1381 -241
- data/README.rdoc +2 -2
- data/lib/active_record/association_relation.rb +1 -1
- data/lib/active_record/associations/alias_tracker.rb +6 -4
- data/lib/active_record/associations/association.rb +35 -17
- data/lib/active_record/associations/association_scope.rb +1 -1
- data/lib/active_record/associations/belongs_to_association.rb +20 -4
- data/lib/active_record/associations/builder/association.rb +16 -5
- data/lib/active_record/associations/builder/belongs_to.rb +36 -8
- data/lib/active_record/associations/builder/collection_association.rb +7 -3
- data/lib/active_record/associations/builder/has_one.rb +1 -1
- data/lib/active_record/associations/builder/singular_association.rb +33 -5
- data/lib/active_record/associations/collection_association.rb +9 -5
- data/lib/active_record/associations/collection_proxy.rb +26 -4
- data/lib/active_record/associations/deprecation.rb +88 -0
- data/lib/active_record/associations/errors.rb +3 -0
- data/lib/active_record/associations/foreign_association.rb +3 -1
- data/lib/active_record/associations/has_one_association.rb +1 -0
- data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
- data/lib/active_record/associations/join_dependency.rb +5 -4
- data/lib/active_record/associations/preloader/association.rb +1 -1
- data/lib/active_record/associations/preloader/batch.rb +7 -1
- data/lib/active_record/associations/preloader/branch.rb +1 -0
- data/lib/active_record/associations.rb +159 -21
- data/lib/active_record/attribute_methods/primary_key.rb +2 -1
- data/lib/active_record/attribute_methods/query.rb +34 -0
- data/lib/active_record/attribute_methods/serialization.rb +19 -6
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +10 -2
- data/lib/active_record/attribute_methods.rb +24 -17
- data/lib/active_record/attributes.rb +40 -26
- data/lib/active_record/autosave_association.rb +22 -12
- data/lib/active_record/base.rb +3 -4
- data/lib/active_record/callbacks.rb +1 -2
- data/lib/active_record/coders/column_serializer.rb +1 -1
- data/lib/active_record/coders/json.rb +16 -6
- data/lib/active_record/coders/yaml_column.rb +2 -2
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +19 -18
- data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -3
- data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +68 -12
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +474 -112
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +68 -41
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +36 -9
- data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +31 -35
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +94 -28
- data/lib/active_record/connection_adapters/abstract/transaction.rb +25 -3
- data/lib/active_record/connection_adapters/abstract_adapter.rb +166 -64
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +133 -40
- data/lib/active_record/connection_adapters/column.rb +14 -4
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
- data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -1
- data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
- data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +40 -25
- data/lib/active_record/connection_adapters/mysql2/database_statements.rb +66 -15
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +9 -3
- data/lib/active_record/connection_adapters/pool_config.rb +7 -7
- data/lib/active_record/connection_adapters/postgresql/column.rb +8 -2
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +29 -33
- data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
- data/lib/active_record/connection_adapters/postgresql/oid/range.rb +46 -3
- data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +8 -6
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +22 -33
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +70 -32
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +99 -54
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +40 -12
- data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +39 -23
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -8
- data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +15 -3
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +7 -14
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +68 -37
- data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
- data/lib/active_record/connection_adapters/trilogy/database_statements.rb +8 -3
- data/lib/active_record/connection_adapters/trilogy_adapter.rb +16 -2
- data/lib/active_record/connection_adapters.rb +1 -0
- data/lib/active_record/connection_handling.rb +15 -10
- data/lib/active_record/core.rb +44 -12
- data/lib/active_record/counter_cache.rb +36 -9
- data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
- data/lib/active_record/database_configurations/database_config.rb +5 -1
- data/lib/active_record/database_configurations/hash_config.rb +66 -9
- data/lib/active_record/database_configurations/url_config.rb +15 -3
- data/lib/active_record/database_configurations.rb +7 -3
- data/lib/active_record/delegated_type.rb +19 -19
- data/lib/active_record/disable_joins_association_relation.rb +5 -1
- data/lib/active_record/dynamic_matchers.rb +54 -69
- data/lib/active_record/encryption/encryptable_record.rb +5 -5
- data/lib/active_record/encryption/encrypted_attribute_type.rb +2 -2
- data/lib/active_record/encryption/encrypted_fixtures.rb +12 -7
- data/lib/active_record/encryption/encryptor.rb +39 -25
- data/lib/active_record/encryption/scheme.rb +1 -1
- data/lib/active_record/enum.rb +37 -20
- data/lib/active_record/errors.rb +23 -7
- data/lib/active_record/explain.rb +1 -1
- data/lib/active_record/explain_registry.rb +51 -2
- data/lib/active_record/filter_attribute_handler.rb +76 -0
- data/lib/active_record/fixture_set/table_row.rb +19 -2
- data/lib/active_record/fixtures.rb +2 -2
- data/lib/active_record/future_result.rb +5 -3
- data/lib/active_record/gem_version.rb +2 -2
- data/lib/active_record/inheritance.rb +9 -4
- data/lib/active_record/insert_all.rb +14 -9
- data/lib/active_record/locking/optimistic.rb +7 -0
- data/lib/active_record/locking/pessimistic.rb +5 -0
- data/lib/active_record/log_subscriber.rb +30 -22
- data/lib/active_record/message_pack.rb +13 -0
- data/lib/active_record/middleware/shard_selector.rb +34 -17
- data/lib/active_record/migration/command_recorder.rb +32 -6
- data/lib/active_record/migration/compatibility.rb +34 -24
- data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
- data/lib/active_record/migration.rb +31 -21
- data/lib/active_record/model_schema.rb +38 -12
- data/lib/active_record/nested_attributes.rb +10 -6
- data/lib/active_record/persistence.rb +35 -3
- data/lib/active_record/query_cache.rb +22 -15
- data/lib/active_record/query_logs.rb +7 -7
- data/lib/active_record/querying.rb +17 -5
- data/lib/active_record/railtie.rb +35 -6
- data/lib/active_record/railties/controller_runtime.rb +11 -6
- data/lib/active_record/railties/databases.rake +24 -20
- data/lib/active_record/railties/job_checkpoints.rb +15 -0
- data/lib/active_record/railties/job_runtime.rb +10 -11
- data/lib/active_record/reflection.rb +36 -1
- data/lib/active_record/relation/batches.rb +25 -11
- data/lib/active_record/relation/calculations.rb +62 -41
- data/lib/active_record/relation/delegation.rb +0 -1
- data/lib/active_record/relation/finder_methods.rb +76 -45
- data/lib/active_record/relation/merger.rb +4 -2
- data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
- data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -9
- data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +7 -7
- data/lib/active_record/relation/predicate_builder.rb +10 -8
- data/lib/active_record/relation/query_attribute.rb +4 -2
- data/lib/active_record/relation/query_methods.rb +106 -45
- data/lib/active_record/relation/spawn_methods.rb +6 -6
- data/lib/active_record/relation/where_clause.rb +10 -11
- data/lib/active_record/relation.rb +63 -31
- data/lib/active_record/result.rb +44 -21
- data/lib/active_record/runtime_registry.rb +42 -58
- data/lib/active_record/sanitization.rb +2 -0
- data/lib/active_record/schema.rb +1 -1
- data/lib/active_record/schema_dumper.rb +41 -21
- data/lib/active_record/scoping.rb +0 -1
- data/lib/active_record/secure_token.rb +3 -3
- data/lib/active_record/signed_id.rb +48 -19
- data/lib/active_record/statement_cache.rb +16 -12
- data/lib/active_record/store.rb +46 -19
- data/lib/active_record/structured_event_subscriber.rb +85 -0
- data/lib/active_record/table_metadata.rb +5 -20
- data/lib/active_record/tasks/abstract_tasks.rb +83 -0
- data/lib/active_record/tasks/database_tasks.rb +44 -45
- data/lib/active_record/tasks/mysql_database_tasks.rb +4 -56
- data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
- data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
- data/lib/active_record/test_databases.rb +18 -4
- data/lib/active_record/test_fixtures.rb +40 -6
- data/lib/active_record/testing/query_assertions.rb +8 -2
- data/lib/active_record/timestamp.rb +4 -2
- data/lib/active_record/transaction.rb +2 -5
- data/lib/active_record/transactions.rb +58 -32
- data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
- data/lib/active_record/type/internal/timezone.rb +7 -0
- data/lib/active_record/type/json.rb +13 -2
- data/lib/active_record/type/serialized.rb +16 -4
- data/lib/active_record/type/type_map.rb +1 -1
- data/lib/active_record/type_caster/connection.rb +2 -1
- data/lib/active_record/validations/associated.rb +1 -1
- data/lib/active_record.rb +72 -7
- data/lib/arel/alias_predication.rb +2 -0
- data/lib/arel/collectors/bind.rb +1 -1
- data/lib/arel/collectors/sql_string.rb +1 -1
- data/lib/arel/collectors/substitute_binds.rb +2 -2
- data/lib/arel/crud.rb +8 -11
- data/lib/arel/delete_manager.rb +5 -0
- data/lib/arel/nodes/binary.rb +1 -1
- data/lib/arel/nodes/count.rb +2 -2
- data/lib/arel/nodes/delete_statement.rb +4 -2
- data/lib/arel/nodes/function.rb +4 -10
- data/lib/arel/nodes/named_function.rb +2 -2
- data/lib/arel/nodes/node.rb +2 -2
- data/lib/arel/nodes/sql_literal.rb +1 -1
- data/lib/arel/nodes/update_statement.rb +4 -2
- data/lib/arel/nodes.rb +0 -2
- data/lib/arel/predications.rb +1 -3
- data/lib/arel/select_manager.rb +13 -4
- data/lib/arel/update_manager.rb +5 -0
- data/lib/arel/visitors/dot.rb +2 -3
- data/lib/arel/visitors/postgresql.rb +55 -0
- data/lib/arel/visitors/sqlite.rb +55 -8
- data/lib/arel/visitors/to_sql.rb +7 -23
- data/lib/arel.rb +3 -1
- data/lib/rails/generators/active_record/application_record/USAGE +1 -1
- metadata +16 -15
- data/lib/active_record/explain_subscriber.rb +0 -34
- data/lib/active_record/normalization.rb +0 -163
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[8.
|
|
142
|
+
class AddSystemSettings < ActiveRecord::Migration[8.1]
|
|
143
143
|
def up
|
|
144
144
|
create_table :system_settings do |t|
|
|
145
145
|
t.string :name
|
|
@@ -214,6 +214,6 @@ Bug reports for the Ruby on \Rails project can be filed here:
|
|
|
214
214
|
|
|
215
215
|
* https://github.com/rails/rails/issues
|
|
216
216
|
|
|
217
|
-
Feature requests should be discussed on the
|
|
217
|
+
Feature requests should be discussed on the rubyonrails-core forum here:
|
|
218
218
|
|
|
219
219
|
* https://discuss.rubyonrails.org/c/rubyonrails-core
|
|
@@ -17,7 +17,7 @@ module ActiveRecord
|
|
|
17
17
|
|
|
18
18
|
%w(insert insert_all insert! insert_all! upsert upsert_all).each do |method|
|
|
19
19
|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
20
|
-
def #{method}(
|
|
20
|
+
def #{method}(...)
|
|
21
21
|
if @association.reflection.through_reflection?
|
|
22
22
|
raise ArgumentError, "Bulk insert or upsert is currently not supported for has_many through association"
|
|
23
23
|
end
|
|
@@ -26,16 +26,18 @@ module ActiveRecord
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def self.initial_count_for(connection, name, table_joins)
|
|
29
|
-
|
|
29
|
+
quoted_name_escaped = nil
|
|
30
|
+
name_escaped = nil
|
|
30
31
|
|
|
31
32
|
counts = table_joins.map do |join|
|
|
32
33
|
if join.is_a?(Arel::Nodes::StringJoin)
|
|
33
|
-
#
|
|
34
|
-
|
|
34
|
+
# quoted_name_escaped should be case ignored as some database adapters (Oracle) return quoted name in uppercase
|
|
35
|
+
quoted_name_escaped ||= Regexp.escape(connection.quote_table_name(name))
|
|
36
|
+
name_escaped ||= Regexp.escape(name)
|
|
35
37
|
|
|
36
38
|
# Table names + table aliases
|
|
37
39
|
join.left.scan(
|
|
38
|
-
/JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{
|
|
40
|
+
/JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name_escaped}|#{name_escaped})\sON/i
|
|
39
41
|
).size
|
|
40
42
|
elsif join.is_a?(Arel::Nodes::Join)
|
|
41
43
|
join.left.name == name ? 1 : 0
|
|
@@ -232,7 +232,19 @@ module ActiveRecord
|
|
|
232
232
|
_create_record(attributes, true, &block)
|
|
233
233
|
end
|
|
234
234
|
|
|
235
|
-
|
|
235
|
+
def violates_strict_loading?
|
|
236
|
+
return unless find_target?
|
|
237
|
+
|
|
238
|
+
return if @skip_strict_loading
|
|
239
|
+
|
|
240
|
+
return unless owner.validation_context.nil?
|
|
241
|
+
|
|
242
|
+
return reflection.strict_loading? if reflection.options.key?(:strict_loading)
|
|
243
|
+
|
|
244
|
+
owner.strict_loading? && !owner.strict_loading_n_plus_one_only?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Whether the association represents a single record
|
|
236
248
|
# or a collection of records.
|
|
237
249
|
def collection?
|
|
238
250
|
false
|
|
@@ -281,16 +293,6 @@ module ActiveRecord
|
|
|
281
293
|
@skip_strict_loading = skip_strict_loading_was
|
|
282
294
|
end
|
|
283
295
|
|
|
284
|
-
def violates_strict_loading?
|
|
285
|
-
return if @skip_strict_loading
|
|
286
|
-
|
|
287
|
-
return unless owner.validation_context.nil?
|
|
288
|
-
|
|
289
|
-
return reflection.strict_loading? if reflection.options.key?(:strict_loading)
|
|
290
|
-
|
|
291
|
-
owner.strict_loading? && !owner.strict_loading_n_plus_one_only?
|
|
292
|
-
end
|
|
293
|
-
|
|
294
296
|
# The scope for this association.
|
|
295
297
|
#
|
|
296
298
|
# Note that the association_scope is merged into the target_scope only when the
|
|
@@ -409,12 +411,28 @@ module ActiveRecord
|
|
|
409
411
|
end
|
|
410
412
|
|
|
411
413
|
def matches_foreign_key?(record)
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
414
|
+
(foreign_key_for?(record) && record_foreign_key_matches_owner?(record)) ||
|
|
415
|
+
(foreign_key_for?(owner) && owner_foreign_key_matches_record?(record))
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def record_foreign_key_matches_owner?(record)
|
|
419
|
+
foreign_key_values(record) == active_record_primary_key_values(owner)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def owner_foreign_key_matches_record?(record)
|
|
423
|
+
foreign_key_values(owner) == association_primary_key_values(record)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def foreign_key_values(record)
|
|
427
|
+
Array(reflection.foreign_key).map { |key| record.read_attribute(key) }
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def active_record_primary_key_values(record)
|
|
431
|
+
Array(reflection.active_record_primary_key).map { |key| record.read_attribute(key) }
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def association_primary_key_values(record)
|
|
435
|
+
Array(reflection.association_primary_key(record.class)).map { |key| record.read_attribute(key) }
|
|
418
436
|
end
|
|
419
437
|
end
|
|
420
438
|
end
|
|
@@ -149,7 +149,7 @@ module ActiveRecord
|
|
|
149
149
|
scope.includes_values |= item.includes_values
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
-
scope.unscope!(*item.
|
|
152
|
+
scope.unscope!(*item.table_name_qualified_unscope_values)
|
|
153
153
|
scope.where_clause += item.where_clause
|
|
154
154
|
scope.order_values = item.order_values | scope.order_values
|
|
155
155
|
end
|
|
@@ -19,10 +19,16 @@ module ActiveRecord
|
|
|
19
19
|
id = owner.public_send(reflection.foreign_key)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
association_class = if reflection.polymorphic?
|
|
23
|
+
owner.public_send(reflection.foreign_type)
|
|
24
|
+
else
|
|
25
|
+
reflection.klass
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
enqueue_destroy_association(
|
|
23
29
|
owner_model_name: owner.class.to_s,
|
|
24
30
|
owner_id: owner.id,
|
|
25
|
-
association_class:
|
|
31
|
+
association_class: association_class.to_s,
|
|
26
32
|
association_ids: [id],
|
|
27
33
|
association_primary_key_column: primary_key_column,
|
|
28
34
|
ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
|
|
@@ -74,11 +80,11 @@ module ActiveRecord
|
|
|
74
80
|
end
|
|
75
81
|
|
|
76
82
|
def target_changed?
|
|
77
|
-
owner.attribute_changed?(
|
|
83
|
+
Array(reflection.foreign_key).any? { |fk| owner.attribute_changed?(fk) } || (!foreign_key_present? && target&.new_record?)
|
|
78
84
|
end
|
|
79
85
|
|
|
80
86
|
def target_previously_changed?
|
|
81
|
-
owner.attribute_previously_changed?(
|
|
87
|
+
Array(reflection.foreign_key).any? { |fk| owner.attribute_previously_changed?(fk) }
|
|
82
88
|
end
|
|
83
89
|
|
|
84
90
|
def saved_change_to_target?
|
|
@@ -129,7 +135,9 @@ module ActiveRecord
|
|
|
129
135
|
target_key_values = record ? Array(primary_key(record.class)).map { |key| record._read_attribute(key) } : []
|
|
130
136
|
|
|
131
137
|
if force || reflection_fk.map { |fk| owner._read_attribute(fk) } != target_key_values
|
|
138
|
+
owner_pk = Array(owner.class.primary_key)
|
|
132
139
|
reflection_fk.each_with_index do |key, index|
|
|
140
|
+
next if record.nil? && owner_pk.include?(key)
|
|
133
141
|
owner[key] = target_key_values[index]
|
|
134
142
|
end
|
|
135
143
|
end
|
|
@@ -156,7 +164,15 @@ module ActiveRecord
|
|
|
156
164
|
end
|
|
157
165
|
|
|
158
166
|
def stale_state
|
|
159
|
-
|
|
167
|
+
foreign_key = reflection.foreign_key
|
|
168
|
+
if foreign_key.is_a?(Array)
|
|
169
|
+
attributes = foreign_key.map do |fk|
|
|
170
|
+
owner._read_attribute(fk) { |n| owner.send(:missing_attribute, n, caller) }
|
|
171
|
+
end
|
|
172
|
+
attributes if attributes.any?
|
|
173
|
+
else
|
|
174
|
+
owner._read_attribute(foreign_key) { |n| owner.send(:missing_attribute, n, caller) }
|
|
175
|
+
end
|
|
160
176
|
end
|
|
161
177
|
end
|
|
162
178
|
end
|
|
@@ -19,7 +19,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
19
19
|
self.extensions = []
|
|
20
20
|
|
|
21
21
|
VALID_OPTIONS = [
|
|
22
|
-
:
|
|
22
|
+
:anonymous_class, :primary_key, :foreign_key, :dependent, :validate, :inverse_of, :strict_loading, :query_constraints, :deprecated
|
|
23
23
|
].freeze # :nodoc:
|
|
24
24
|
|
|
25
25
|
def self.build(model, name, scope, options, &block)
|
|
@@ -102,7 +102,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
102
102
|
def self.define_readers(mixin, name)
|
|
103
103
|
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
104
104
|
def #{name}
|
|
105
|
-
association(:#{name})
|
|
105
|
+
association = association(:#{name})
|
|
106
|
+
deprecated_associations_api_guard(association, __method__)
|
|
107
|
+
association.reader
|
|
106
108
|
end
|
|
107
109
|
CODE
|
|
108
110
|
end
|
|
@@ -110,7 +112,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
110
112
|
def self.define_writers(mixin, name)
|
|
111
113
|
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
112
114
|
def #{name}=(value)
|
|
113
|
-
association(:#{name})
|
|
115
|
+
association = association(:#{name})
|
|
116
|
+
deprecated_associations_api_guard(association, __method__)
|
|
117
|
+
association.writer(value)
|
|
114
118
|
end
|
|
115
119
|
CODE
|
|
116
120
|
end
|
|
@@ -138,8 +142,15 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
138
142
|
end
|
|
139
143
|
|
|
140
144
|
def self.add_destroy_callbacks(model, reflection)
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
if reflection.deprecated?
|
|
146
|
+
# If :dependent is set, destroying the record has a side effect that
|
|
147
|
+
# would no longer happen if the association is removed.
|
|
148
|
+
model.before_destroy do
|
|
149
|
+
report_deprecated_association(reflection, context: ":dependent has a side effect here")
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
model.before_destroy(->(o) { o.association(reflection.name).handle_dependency })
|
|
143
154
|
end
|
|
144
155
|
|
|
145
156
|
def self.add_after_commit_jobs_callback(model, dependent)
|
|
@@ -8,8 +8,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
8
8
|
|
|
9
9
|
def self.valid_options(options)
|
|
10
10
|
valid = super + [:polymorphic, :counter_cache, :optional, :default]
|
|
11
|
-
valid
|
|
12
|
-
valid
|
|
11
|
+
valid << :class_name unless options[:polymorphic]
|
|
12
|
+
valid << :foreign_type if options[:polymorphic]
|
|
13
|
+
valid << :ensuring_owner_was if options[:dependent] == :destroy_async
|
|
13
14
|
valid
|
|
14
15
|
end
|
|
15
16
|
|
|
@@ -42,7 +43,16 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
def self.touch_record(o, changes, foreign_key, name, touch) # :nodoc:
|
|
45
|
-
old_foreign_id =
|
|
46
|
+
old_foreign_id =
|
|
47
|
+
if foreign_key.is_a?(Array)
|
|
48
|
+
if foreign_key.any? { |fk| changes[fk] }
|
|
49
|
+
foreign_key.map do |fk|
|
|
50
|
+
changes[fk] ? changes[fk].first : o.read_attribute(fk)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
elsif changes[foreign_key]
|
|
54
|
+
changes[foreign_key].first
|
|
55
|
+
end
|
|
46
56
|
|
|
47
57
|
if old_foreign_id
|
|
48
58
|
association = o.association(name)
|
|
@@ -55,7 +65,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
55
65
|
klass = association.klass
|
|
56
66
|
end
|
|
57
67
|
primary_key = reflection.association_primary_key(klass)
|
|
58
|
-
old_record = klass.find_by(primary_key => old_foreign_id)
|
|
68
|
+
old_record = klass.find_by(primary_key => [old_foreign_id])
|
|
59
69
|
|
|
60
70
|
if old_record
|
|
61
71
|
if touch != true
|
|
@@ -107,6 +117,14 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
107
117
|
end
|
|
108
118
|
|
|
109
119
|
def self.add_destroy_callbacks(model, reflection)
|
|
120
|
+
if reflection.deprecated?
|
|
121
|
+
# If :dependent is set, destroying the record has some side effect that
|
|
122
|
+
# would no longer happen if the association is removed.
|
|
123
|
+
model.before_destroy do
|
|
124
|
+
report_deprecated_association(reflection, context: ":dependent has a side effect here")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
110
128
|
model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency }
|
|
111
129
|
end
|
|
112
130
|
|
|
@@ -131,8 +149,14 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
131
149
|
foreign_key = reflection.foreign_key
|
|
132
150
|
foreign_type = reflection.foreign_type
|
|
133
151
|
|
|
134
|
-
|
|
135
|
-
record.attribute_changed?(
|
|
152
|
+
fk_missing_or_changed = if foreign_key.is_a?(Array)
|
|
153
|
+
foreign_key.any? { |fk| record.read_attribute(fk).nil? || record.attribute_changed?(fk) }
|
|
154
|
+
else
|
|
155
|
+
record.read_attribute(foreign_key).nil? ||
|
|
156
|
+
record.attribute_changed?(foreign_key)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
fk_missing_or_changed ||
|
|
136
160
|
(reflection.polymorphic? && (record.read_attribute(foreign_type).nil? || record.attribute_changed?(foreign_type)))
|
|
137
161
|
}
|
|
138
162
|
|
|
@@ -144,11 +168,15 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
144
168
|
def self.define_change_tracking_methods(model, reflection)
|
|
145
169
|
model.generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
146
170
|
def #{reflection.name}_changed?
|
|
147
|
-
association(:#{reflection.name})
|
|
171
|
+
association = association(:#{reflection.name})
|
|
172
|
+
deprecated_associations_api_guard(association, __method__)
|
|
173
|
+
association.target_changed?
|
|
148
174
|
end
|
|
149
175
|
|
|
150
176
|
def #{reflection.name}_previously_changed?
|
|
151
|
-
association(:#{reflection.name})
|
|
177
|
+
association = association(:#{reflection.name})
|
|
178
|
+
deprecated_associations_api_guard(association, __method__)
|
|
179
|
+
association.target_previously_changed?
|
|
152
180
|
end
|
|
153
181
|
CODE
|
|
154
182
|
end
|
|
@@ -7,7 +7,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
7
7
|
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
|
|
8
8
|
|
|
9
9
|
def self.valid_options(options)
|
|
10
|
-
super + [:before_add, :after_add, :before_remove, :after_remove, :extend]
|
|
10
|
+
super + [:class_name, :before_add, :after_add, :before_remove, :after_remove, :extend]
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def self.define_callbacks(model, reflection)
|
|
@@ -60,7 +60,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
60
60
|
|
|
61
61
|
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
62
62
|
def #{name.to_s.singularize}_ids
|
|
63
|
-
association(:#{name})
|
|
63
|
+
association = association(:#{name})
|
|
64
|
+
deprecated_associations_api_guard(association, __method__)
|
|
65
|
+
association.ids_reader
|
|
64
66
|
end
|
|
65
67
|
CODE
|
|
66
68
|
end
|
|
@@ -70,7 +72,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
70
72
|
|
|
71
73
|
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
72
74
|
def #{name.to_s.singularize}_ids=(ids)
|
|
73
|
-
association(:#{name})
|
|
75
|
+
association = association(:#{name})
|
|
76
|
+
deprecated_associations_api_guard(association, __method__)
|
|
77
|
+
association.ids_writer(ids)
|
|
74
78
|
end
|
|
75
79
|
CODE
|
|
76
80
|
end
|
|
@@ -7,7 +7,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def self.valid_options(options)
|
|
10
|
-
valid = super + [:as, :through]
|
|
10
|
+
valid = super + [:class_name, :as, :through]
|
|
11
11
|
valid += [:foreign_type] if options[:as]
|
|
12
12
|
valid += [:ensuring_owner_was] if options[:dependent] == :destroy_async
|
|
13
13
|
valid += [:source, :source_type, :disable_joins] if options[:through]
|
|
@@ -17,11 +17,15 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
17
17
|
|
|
18
18
|
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
19
19
|
def reload_#{name}
|
|
20
|
-
association(:#{name})
|
|
20
|
+
association = association(:#{name})
|
|
21
|
+
deprecated_associations_api_guard(association, __method__)
|
|
22
|
+
association.force_reload_reader
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def reset_#{name}
|
|
24
|
-
association(:#{name})
|
|
26
|
+
association = association(:#{name})
|
|
27
|
+
deprecated_associations_api_guard(association, __method__)
|
|
28
|
+
association.reset
|
|
25
29
|
end
|
|
26
30
|
CODE
|
|
27
31
|
end
|
|
@@ -30,19 +34,43 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
30
34
|
def self.define_constructors(mixin, name)
|
|
31
35
|
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
32
36
|
def build_#{name}(*args, &block)
|
|
33
|
-
association(:#{name})
|
|
37
|
+
association = association(:#{name})
|
|
38
|
+
deprecated_associations_api_guard(association, __method__)
|
|
39
|
+
association.build(*args, &block)
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
def create_#{name}(*args, &block)
|
|
37
|
-
association(:#{name})
|
|
43
|
+
association = association(:#{name})
|
|
44
|
+
deprecated_associations_api_guard(association, __method__)
|
|
45
|
+
association.create(*args, &block)
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
def create_#{name}!(*args, &block)
|
|
41
|
-
association(:#{name})
|
|
49
|
+
association = association(:#{name})
|
|
50
|
+
deprecated_associations_api_guard(association, __method__)
|
|
51
|
+
association.create!(*args, &block)
|
|
42
52
|
end
|
|
43
53
|
CODE
|
|
44
54
|
end
|
|
45
55
|
|
|
56
|
+
def self.define_callbacks(model, reflection)
|
|
57
|
+
super
|
|
58
|
+
|
|
59
|
+
# If the record is saved or destroyed and `:touch` is set, the parent
|
|
60
|
+
# record gets a timestamp updated. We want to know about it, because
|
|
61
|
+
# deleting the association would change that side-effect and perhaps there
|
|
62
|
+
# is code relying on it.
|
|
63
|
+
if reflection.deprecated? && reflection.options[:touch]
|
|
64
|
+
model.before_save do
|
|
65
|
+
report_deprecated_association(reflection, context: ":touch has a side effect here")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
model.before_destroy do
|
|
69
|
+
report_deprecated_association(reflection, context: ":touch has a side effect here")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
46
74
|
private_class_method :valid_options, :define_accessors, :define_constructors
|
|
47
75
|
end
|
|
48
76
|
end
|
|
@@ -61,15 +61,19 @@ module ActiveRecord
|
|
|
61
61
|
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
|
|
62
62
|
def ids_writer(ids)
|
|
63
63
|
primary_key = reflection.association_primary_key
|
|
64
|
-
pk_type = klass.type_for_attribute(primary_key)
|
|
65
64
|
ids = Array(ids).compact_blank
|
|
66
|
-
ids.map! { |id| pk_type.cast(id) }
|
|
67
65
|
|
|
68
66
|
records = if klass.composite_primary_key?
|
|
67
|
+
pk_types = primary_key.map { |column| klass.type_for_attribute(column) }
|
|
68
|
+
ids.map! { |id| pk_types.zip(id).map! { |type, value| type.cast(value) } }
|
|
69
|
+
|
|
69
70
|
klass.where(primary_key => ids).index_by do |record|
|
|
70
71
|
primary_key.map { |primary_key| record._read_attribute(primary_key) }
|
|
71
72
|
end
|
|
72
73
|
else
|
|
74
|
+
pk_type = klass.type_for_attribute(primary_key)
|
|
75
|
+
ids.map! { |id| pk_type.cast(id) }
|
|
76
|
+
|
|
73
77
|
klass.where(primary_key => ids).index_by do |record|
|
|
74
78
|
record._read_attribute(primary_key)
|
|
75
79
|
end
|
|
@@ -259,10 +263,10 @@ module ActiveRecord
|
|
|
259
263
|
klass = reflection.klass
|
|
260
264
|
return false unless record.is_a?(klass)
|
|
261
265
|
|
|
262
|
-
if
|
|
263
|
-
include_in_memory?(record)
|
|
264
|
-
elsif loaded?
|
|
266
|
+
if loaded?
|
|
265
267
|
target.include?(record)
|
|
268
|
+
elsif record.new_record?
|
|
269
|
+
include_in_memory?(record)
|
|
266
270
|
else
|
|
267
271
|
record_id = klass.composite_primary_key? ? klass.primary_key.zip(record.id).to_h : record.id
|
|
268
272
|
scope.exists?(record_id)
|
|
@@ -110,7 +110,7 @@ module ActiveRecord
|
|
|
110
110
|
# # ]
|
|
111
111
|
|
|
112
112
|
# Finds an object in the collection responding to the +id+. Uses the same
|
|
113
|
-
# rules as ActiveRecord::FinderMethods.find.
|
|
113
|
+
# rules as ActiveRecord::FinderMethods.find. Raises ActiveRecord::RecordNotFound
|
|
114
114
|
# error if the object cannot be found.
|
|
115
115
|
#
|
|
116
116
|
# class Person < ActiveRecord::Base
|
|
@@ -726,6 +726,10 @@ module ActiveRecord
|
|
|
726
726
|
end
|
|
727
727
|
|
|
728
728
|
def pluck(*column_names)
|
|
729
|
+
if proxy_association.violates_strict_loading?
|
|
730
|
+
Base.strict_loading_violation!(owner: proxy_association.owner.class, reflection: proxy_association.reflection)
|
|
731
|
+
end
|
|
732
|
+
|
|
729
733
|
null_scope? ? scope.pluck(*column_names) : super
|
|
730
734
|
end
|
|
731
735
|
|
|
@@ -1125,14 +1129,32 @@ module ActiveRecord
|
|
|
1125
1129
|
super
|
|
1126
1130
|
end
|
|
1127
1131
|
|
|
1132
|
+
%w(insert insert_all insert! insert_all! upsert upsert_all).each do |method|
|
|
1133
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
1134
|
+
def #{method}(...)
|
|
1135
|
+
if @association&.target&.any? { |r| r.new_record? }
|
|
1136
|
+
association_name = @association.reflection.name
|
|
1137
|
+
ActiveRecord.deprecator.warn(<<~MSG)
|
|
1138
|
+
Using #{method} on association \#{association_name} with unpersisted records
|
|
1139
|
+
is deprecated and will be removed in Rails 8.2.
|
|
1140
|
+
The unpersisted records will be lost after this operation.
|
|
1141
|
+
Please either persist your records first or store them separately before
|
|
1142
|
+
calling #{method}.
|
|
1143
|
+
MSG
|
|
1144
|
+
scope.#{method}(...)
|
|
1145
|
+
else
|
|
1146
|
+
scope.#{method}(...).tap { reset }
|
|
1147
|
+
end
|
|
1148
|
+
end
|
|
1149
|
+
RUBY
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1128
1152
|
delegate_methods = [
|
|
1129
1153
|
QueryMethods,
|
|
1130
1154
|
SpawnMethods,
|
|
1131
1155
|
].flat_map { |klass|
|
|
1132
1156
|
klass.public_instance_methods(false)
|
|
1133
|
-
} - self.public_instance_methods(false) - [:select] + [
|
|
1134
|
-
:scoping, :values, :insert, :insert_all, :insert!, :insert_all!, :upsert, :upsert_all, :load_async
|
|
1135
|
-
]
|
|
1157
|
+
} - self.public_instance_methods(false) - [ :select ] + [ :scoping, :values, :load_async ]
|
|
1136
1158
|
|
|
1137
1159
|
delegate(*delegate_methods, to: :scope)
|
|
1138
1160
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
require "active_support/core_ext/array/conversions"
|
|
5
|
+
|
|
6
|
+
module ActiveRecord::Associations::Deprecation # :nodoc:
|
|
7
|
+
EVENT = "deprecated_association.active_record"
|
|
8
|
+
private_constant :EVENT
|
|
9
|
+
|
|
10
|
+
MODES = [:warn, :raise, :notify].freeze
|
|
11
|
+
private_constant :MODES
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
attr_reader :mode, :backtrace
|
|
15
|
+
|
|
16
|
+
def mode=(value) # private setter
|
|
17
|
+
unless MODES.include?(value)
|
|
18
|
+
raise ArgumentError, "invalid deprecated associations mode #{value.inspect} (valid modes are #{MODES.map(&:inspect).to_sentence})"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@mode = value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def backtrace=(value)
|
|
25
|
+
@backtrace = !!value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def guard(reflection)
|
|
29
|
+
report(reflection, context: yield) if reflection.deprecated?
|
|
30
|
+
|
|
31
|
+
if reflection.through_reflection?
|
|
32
|
+
reflection.deprecated_nested_reflections.each do |deprecated_nested_reflection|
|
|
33
|
+
context = "referenced as nested association of the through #{reflection.active_record}##{reflection.name}"
|
|
34
|
+
report(deprecated_nested_reflection, context: context)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def report(reflection, context:)
|
|
40
|
+
reflection = user_facing_reflection(reflection)
|
|
41
|
+
|
|
42
|
+
message = +"The association #{reflection.active_record}##{reflection.name} is deprecated, #{context}"
|
|
43
|
+
message << " (#{backtrace_cleaner.first_clean_frame})"
|
|
44
|
+
|
|
45
|
+
case @mode
|
|
46
|
+
when :warn
|
|
47
|
+
message = [message, *clean_frames].join("\n\t") if @backtrace
|
|
48
|
+
ActiveRecord::Base.logger&.warn(message)
|
|
49
|
+
when :raise
|
|
50
|
+
error = ActiveRecord::DeprecatedAssociationError.new(message)
|
|
51
|
+
if set_backtrace_supports_array_of_locations?
|
|
52
|
+
error.set_backtrace(clean_locations)
|
|
53
|
+
else
|
|
54
|
+
error.set_backtrace(clean_frames)
|
|
55
|
+
end
|
|
56
|
+
raise error
|
|
57
|
+
else
|
|
58
|
+
payload = { reflection: reflection, message: message, location: backtrace_cleaner.first_clean_location }
|
|
59
|
+
payload[:backtrace] = clean_locations if @backtrace
|
|
60
|
+
ActiveSupport::Notifications.instrument(EVENT, payload)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
def backtrace_cleaner
|
|
66
|
+
ActiveRecord::LogSubscriber.backtrace_cleaner
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def clean_frames
|
|
70
|
+
backtrace_cleaner.clean(caller)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def clean_locations
|
|
74
|
+
backtrace_cleaner.clean_locations(caller_locations)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def set_backtrace_supports_array_of_locations?
|
|
78
|
+
@backtrace_supports_array_of_locations ||= Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def user_facing_reflection(reflection)
|
|
82
|
+
reflection.active_record.reflect_on_association(reflection.name)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
self.mode = :warn
|
|
87
|
+
self.backtrace = false
|
|
88
|
+
end
|
|
@@ -4,7 +4,9 @@ module ActiveRecord::Associations
|
|
|
4
4
|
module ForeignAssociation # :nodoc:
|
|
5
5
|
def foreign_key_present?
|
|
6
6
|
if reflection.klass.primary_key
|
|
7
|
-
|
|
7
|
+
Array(reflection.active_record_primary_key).all? do |key|
|
|
8
|
+
owner.attribute_present?(key)
|
|
9
|
+
end
|
|
8
10
|
else
|
|
9
11
|
false
|
|
10
12
|
end
|
|
@@ -120,6 +120,7 @@ module ActiveRecord
|
|
|
120
120
|
Array(reflection.foreign_key).each do |foreign_key_column|
|
|
121
121
|
record[foreign_key_column] = nil unless foreign_key_column.in?(Array(record.class.primary_key))
|
|
122
122
|
end
|
|
123
|
+
record[reflection.type] = nil if reflection.type.present?
|
|
123
124
|
end
|
|
124
125
|
|
|
125
126
|
def transaction_if(value, &block)
|