activerecord 7.2.2.1 → 8.1.2
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 +564 -753
- data/README.rdoc +2 -2
- data/lib/active_record/association_relation.rb +2 -1
- data/lib/active_record/associations/alias_tracker.rb +6 -4
- data/lib/active_record/associations/association.rb +35 -11
- data/lib/active_record/associations/belongs_to_association.rb +18 -2
- data/lib/active_record/associations/builder/association.rb +23 -11
- data/lib/active_record/associations/builder/belongs_to.rb +17 -4
- 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 +10 -8
- data/lib/active_record/associations/collection_proxy.rb +22 -4
- data/lib/active_record/associations/deprecation.rb +88 -0
- data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
- data/lib/active_record/associations/errors.rb +3 -0
- data/lib/active_record/associations/has_many_through_association.rb +3 -2
- data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
- data/lib/active_record/associations/join_dependency.rb +4 -2
- data/lib/active_record/associations/preloader/association.rb +2 -2
- 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/singular_association.rb +8 -3
- data/lib/active_record/associations.rb +192 -24
- data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
- data/lib/active_record/attribute_methods/primary_key.rb +4 -8
- data/lib/active_record/attribute_methods/query.rb +34 -0
- data/lib/active_record/attribute_methods/serialization.rb +17 -4
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +12 -14
- data/lib/active_record/attribute_methods.rb +24 -19
- data/lib/active_record/attributes.rb +40 -26
- data/lib/active_record/autosave_association.rb +91 -39
- data/lib/active_record/base.rb +3 -4
- data/lib/active_record/coders/json.rb +14 -5
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +35 -28
- data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -4
- data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -13
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +458 -117
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +136 -74
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +44 -11
- data/lib/active_record/connection_adapters/abstract/quoting.rb +16 -25
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +11 -7
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +37 -36
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +122 -29
- data/lib/active_record/connection_adapters/abstract/transaction.rb +40 -8
- data/lib/active_record/connection_adapters/abstract_adapter.rb +175 -87
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +77 -58
- data/lib/active_record/connection_adapters/column.rb +17 -4
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
- data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -9
- data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
- data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +41 -10
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +73 -46
- data/lib/active_record/connection_adapters/mysql2/database_statements.rb +89 -94
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +10 -11
- data/lib/active_record/connection_adapters/pool_config.rb +7 -7
- data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +76 -45
- data/lib/active_record/connection_adapters/postgresql/oid/array.rb +3 -3
- data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
- 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 +2 -4
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +9 -17
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +28 -45
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +69 -32
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +140 -64
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +83 -105
- data/lib/active_record/connection_adapters/schema_cache.rb +3 -5
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +90 -98
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -8
- data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
- data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +27 -2
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +13 -13
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +112 -42
- data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
- data/lib/active_record/connection_adapters/trilogy/database_statements.rb +38 -67
- data/lib/active_record/connection_adapters/trilogy_adapter.rb +2 -19
- data/lib/active_record/connection_adapters.rb +1 -56
- data/lib/active_record/connection_handling.rb +37 -10
- data/lib/active_record/core.rb +61 -25
- data/lib/active_record/counter_cache.rb +34 -9
- data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
- data/lib/active_record/database_configurations/database_config.rb +9 -1
- data/lib/active_record/database_configurations/hash_config.rb +67 -9
- data/lib/active_record/database_configurations/url_config.rb +13 -3
- data/lib/active_record/database_configurations.rb +7 -3
- data/lib/active_record/delegated_type.rb +19 -19
- data/lib/active_record/dynamic_matchers.rb +54 -69
- data/lib/active_record/encryption/config.rb +3 -1
- data/lib/active_record/encryption/encryptable_record.rb +9 -9
- data/lib/active_record/encryption/encrypted_attribute_type.rb +12 -3
- data/lib/active_record/encryption/encryptor.rb +49 -28
- data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
- data/lib/active_record/encryption/scheme.rb +9 -2
- data/lib/active_record/enum.rb +46 -42
- data/lib/active_record/errors.rb +36 -12
- 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 +73 -0
- data/lib/active_record/fixture_set/table_row.rb +19 -2
- data/lib/active_record/fixtures.rb +2 -4
- data/lib/active_record/future_result.rb +13 -9
- data/lib/active_record/gem_version.rb +3 -3
- data/lib/active_record/inheritance.rb +1 -1
- data/lib/active_record/insert_all.rb +12 -7
- data/lib/active_record/locking/optimistic.rb +8 -1
- data/lib/active_record/locking/pessimistic.rb +5 -0
- data/lib/active_record/log_subscriber.rb +3 -13
- data/lib/active_record/middleware/shard_selector.rb +34 -17
- data/lib/active_record/migration/command_recorder.rb +44 -11
- data/lib/active_record/migration/compatibility.rb +37 -24
- data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
- data/lib/active_record/migration.rb +50 -43
- data/lib/active_record/model_schema.rb +38 -13
- data/lib/active_record/nested_attributes.rb +6 -6
- data/lib/active_record/persistence.rb +162 -133
- data/lib/active_record/query_cache.rb +22 -15
- data/lib/active_record/query_logs.rb +104 -52
- data/lib/active_record/query_logs_formatter.rb +17 -28
- data/lib/active_record/querying.rb +12 -12
- data/lib/active_record/railtie.rb +37 -32
- data/lib/active_record/railties/controller_runtime.rb +11 -6
- data/lib/active_record/railties/databases.rake +26 -37
- 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 +53 -21
- data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
- data/lib/active_record/relation/batches.rb +147 -73
- data/lib/active_record/relation/calculations.rb +80 -63
- data/lib/active_record/relation/delegation.rb +25 -15
- data/lib/active_record/relation/finder_methods.rb +54 -37
- data/lib/active_record/relation/merger.rb +8 -8
- data/lib/active_record/relation/predicate_builder/association_query_value.rb +11 -9
- data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +8 -8
- data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
- data/lib/active_record/relation/predicate_builder.rb +22 -7
- data/lib/active_record/relation/query_attribute.rb +4 -2
- data/lib/active_record/relation/query_methods.rb +156 -95
- data/lib/active_record/relation/spawn_methods.rb +7 -7
- data/lib/active_record/relation/where_clause.rb +10 -11
- data/lib/active_record/relation.rb +122 -80
- data/lib/active_record/result.rb +109 -24
- data/lib/active_record/runtime_registry.rb +42 -58
- data/lib/active_record/sanitization.rb +9 -6
- data/lib/active_record/schema_dumper.rb +47 -22
- data/lib/active_record/schema_migration.rb +2 -1
- data/lib/active_record/scoping/named.rb +5 -2
- data/lib/active_record/scoping.rb +0 -1
- data/lib/active_record/secure_token.rb +3 -3
- data/lib/active_record/signed_id.rb +47 -18
- data/lib/active_record/statement_cache.rb +24 -20
- data/lib/active_record/store.rb +51 -22
- data/lib/active_record/structured_event_subscriber.rb +85 -0
- data/lib/active_record/table_metadata.rb +6 -23
- data/lib/active_record/tasks/abstract_tasks.rb +76 -0
- data/lib/active_record/tasks/database_tasks.rb +85 -85
- data/lib/active_record/tasks/mysql_database_tasks.rb +3 -42
- data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
- data/lib/active_record/tasks/sqlite_database_tasks.rb +16 -28
- data/lib/active_record/test_databases.rb +14 -4
- data/lib/active_record/test_fixtures.rb +39 -2
- data/lib/active_record/testing/query_assertions.rb +8 -2
- data/lib/active_record/timestamp.rb +4 -2
- data/lib/active_record/token_for.rb +1 -1
- data/lib/active_record/transaction.rb +2 -5
- data/lib/active_record/transactions.rb +39 -16
- 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 +15 -2
- data/lib/active_record/type/serialized.rb +11 -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/validations/uniqueness.rb +8 -8
- data/lib/active_record.rb +85 -50
- data/lib/arel/alias_predication.rb +2 -0
- data/lib/arel/collectors/bind.rb +2 -2
- 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/select_manager.rb +13 -4
- data/lib/arel/table.rb +3 -7
- 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 +6 -22
- data/lib/arel.rb +3 -1
- data/lib/rails/generators/active_record/application_record/USAGE +1 -1
- metadata +17 -17
- data/lib/active_record/explain_subscriber.rb +0 -34
- data/lib/active_record/normalization.rb +0 -163
- 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[
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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 :
|
|
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)] }
|
|
@@ -210,7 +232,7 @@ module ActiveRecord
|
|
|
210
232
|
_create_record(attributes, true, &block)
|
|
211
233
|
end
|
|
212
234
|
|
|
213
|
-
# Whether the association
|
|
235
|
+
# Whether the association represents a single record
|
|
214
236
|
# or a collection of records.
|
|
215
237
|
def collection?
|
|
216
238
|
false
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -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
|
|
34
|
-
define_callbacks
|
|
35
|
-
define_validations
|
|
36
|
-
define_change_tracking_methods
|
|
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
|
|
85
|
+
extension.build(model, reflection)
|
|
85
86
|
end
|
|
86
87
|
end
|
|
87
88
|
|
|
@@ -101,7 +102,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
101
102
|
def self.define_readers(mixin, name)
|
|
102
103
|
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
103
104
|
def #{name}
|
|
104
|
-
association(:#{name})
|
|
105
|
+
association = association(:#{name})
|
|
106
|
+
deprecated_associations_api_guard(association, __method__)
|
|
107
|
+
association.reader
|
|
105
108
|
end
|
|
106
109
|
CODE
|
|
107
110
|
end
|
|
@@ -109,7 +112,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
109
112
|
def self.define_writers(mixin, name)
|
|
110
113
|
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
111
114
|
def #{name}=(value)
|
|
112
|
-
association(:#{name})
|
|
115
|
+
association = association(:#{name})
|
|
116
|
+
deprecated_associations_api_guard(association, __method__)
|
|
117
|
+
association.writer(value)
|
|
113
118
|
end
|
|
114
119
|
CODE
|
|
115
120
|
end
|
|
@@ -131,14 +136,21 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
131
136
|
err_message = "A valid destroy_association_async_job is required to use `dependent: :destroy_async` on associations"
|
|
132
137
|
raise ActiveRecord::ConfigurationError, err_message
|
|
133
138
|
end
|
|
134
|
-
unless valid_dependent_options.include?
|
|
139
|
+
unless valid_dependent_options.include?(dependent)
|
|
135
140
|
raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}"
|
|
136
141
|
end
|
|
137
142
|
end
|
|
138
143
|
|
|
139
144
|
def self.add_destroy_callbacks(model, reflection)
|
|
140
|
-
|
|
141
|
-
|
|
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 })
|
|
142
154
|
end
|
|
143
155
|
|
|
144
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
|
|
|
@@ -107,6 +108,14 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
107
108
|
end
|
|
108
109
|
|
|
109
110
|
def self.add_destroy_callbacks(model, reflection)
|
|
111
|
+
if reflection.deprecated?
|
|
112
|
+
# If :dependent is set, destroying the record has some side effect that
|
|
113
|
+
# would no longer happen if the association is removed.
|
|
114
|
+
model.before_destroy do
|
|
115
|
+
report_deprecated_association(reflection, context: ":dependent has a side effect here")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
110
119
|
model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency }
|
|
111
120
|
end
|
|
112
121
|
|
|
@@ -144,11 +153,15 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|
|
144
153
|
def self.define_change_tracking_methods(model, reflection)
|
|
145
154
|
model.generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
146
155
|
def #{reflection.name}_changed?
|
|
147
|
-
association(:#{reflection.name})
|
|
156
|
+
association = association(:#{reflection.name})
|
|
157
|
+
deprecated_associations_api_guard(association, __method__)
|
|
158
|
+
association.target_changed?
|
|
148
159
|
end
|
|
149
160
|
|
|
150
161
|
def #{reflection.name}_previously_changed?
|
|
151
|
-
association(:#{reflection.name})
|
|
162
|
+
association = association(:#{reflection.name})
|
|
163
|
+
deprecated_associations_api_guard(association, __method__)
|
|
164
|
+
association.target_previously_changed?
|
|
152
165
|
end
|
|
153
166
|
CODE
|
|
154
167
|
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
|
|
@@ -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.
|
|
97
|
+
model = scope.model
|
|
98
98
|
|
|
99
99
|
if args_flatten.blank?
|
|
100
100
|
error_message = "Couldn't find #{model.name} without an ID"
|
|
@@ -256,14 +256,16 @@ module ActiveRecord
|
|
|
256
256
|
end
|
|
257
257
|
|
|
258
258
|
def include?(record)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
259
|
+
klass = reflection.klass
|
|
260
|
+
return false unless record.is_a?(klass)
|
|
261
|
+
|
|
262
|
+
if loaded?
|
|
263
|
+
target.include?(record)
|
|
264
|
+
elsif record.new_record?
|
|
265
|
+
include_in_memory?(record)
|
|
265
266
|
else
|
|
266
|
-
|
|
267
|
+
record_id = klass.composite_primary_key? ? klass.primary_key.zip(record.id).to_h : record.id
|
|
268
|
+
scope.exists?(record_id)
|
|
267
269
|
end
|
|
268
270
|
end
|
|
269
271
|
|
|
@@ -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
|
|
@@ -1125,14 +1125,32 @@ module ActiveRecord
|
|
|
1125
1125
|
super
|
|
1126
1126
|
end
|
|
1127
1127
|
|
|
1128
|
+
%w(insert insert_all insert! insert_all! upsert upsert_all).each do |method|
|
|
1129
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
1130
|
+
def #{method}(...)
|
|
1131
|
+
if @association&.target&.any? { |r| r.new_record? }
|
|
1132
|
+
association_name = @association.reflection.name
|
|
1133
|
+
ActiveRecord.deprecator.warn(<<~MSG)
|
|
1134
|
+
Using #{method} on association \#{association_name} with unpersisted records
|
|
1135
|
+
is deprecated and will be removed in Rails 8.2.
|
|
1136
|
+
The unpersisted records will be lost after this operation.
|
|
1137
|
+
Please either persist your records first or store them separately before
|
|
1138
|
+
calling #{method}.
|
|
1139
|
+
MSG
|
|
1140
|
+
scope.#{method}(...)
|
|
1141
|
+
else
|
|
1142
|
+
scope.#{method}(...).tap { reset }
|
|
1143
|
+
end
|
|
1144
|
+
end
|
|
1145
|
+
RUBY
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1128
1148
|
delegate_methods = [
|
|
1129
1149
|
QueryMethods,
|
|
1130
1150
|
SpawnMethods,
|
|
1131
1151
|
].flat_map { |klass|
|
|
1132
1152
|
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
|
-
]
|
|
1153
|
+
} - self.public_instance_methods(false) - [ :select ] + [ :scoping, :values, :load_async ]
|
|
1136
1154
|
|
|
1137
1155
|
delegate(*delegate_methods, to: :scope)
|
|
1138
1156
|
|
|
@@ -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
|
|
@@ -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.
|
|
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.
|
|
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
|