activerecord 8.0.0 → 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 +703 -248
- 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 +1 -1
- data/lib/active_record/associations/belongs_to_association.rb +18 -2
- data/lib/active_record/associations/builder/association.rb +16 -5
- 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 +3 -3
- data/lib/active_record/associations/collection_proxy.rb +22 -4
- data/lib/active_record/associations/deprecation.rb +88 -0
- data/lib/active_record/associations/errors.rb +3 -0
- 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/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 +17 -4
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +10 -2
- data/lib/active_record/attribute_methods.rb +23 -18
- 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/coders/json.rb +14 -5
- 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 +51 -12
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +458 -108
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +55 -40
- 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 +89 -23
- data/lib/active_record/connection_adapters/abstract/transaction.rb +25 -3
- data/lib/active_record/connection_adapters/abstract_adapter.rb +154 -64
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +57 -20
- 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 -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 +33 -4
- 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 +4 -0
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +26 -17
- data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
- 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/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 +67 -31
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +82 -49
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +38 -10
- data/lib/active_record/connection_adapters/schema_cache.rb +2 -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 +14 -2
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +5 -12
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +65 -36
- data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
- data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
- data/lib/active_record/connection_adapters/trilogy_adapter.rb +2 -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 +34 -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 +59 -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/encryptable_record.rb +5 -5
- data/lib/active_record/encryption/encrypted_attribute_type.rb +2 -2
- 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 +73 -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 +3 -3
- data/lib/active_record/gem_version.rb +2 -2
- data/lib/active_record/inheritance.rb +1 -1
- data/lib/active_record/insert_all.rb +12 -7
- 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 +2 -6
- data/lib/active_record/middleware/shard_selector.rb +34 -17
- data/lib/active_record/migration/command_recorder.rb +19 -3
- 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 +36 -10
- data/lib/active_record/nested_attributes.rb +2 -0
- data/lib/active_record/persistence.rb +34 -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 +4 -4
- 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 +35 -0
- data/lib/active_record/relation/batches.rb +25 -11
- data/lib/active_record/relation/calculations.rb +54 -38
- data/lib/active_record/relation/delegation.rb +0 -1
- data/lib/active_record/relation/finder_methods.rb +42 -25
- 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 +9 -7
- data/lib/active_record/relation/query_attribute.rb +4 -2
- data/lib/active_record/relation/query_methods.rb +43 -32
- 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 +43 -19
- 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_dumper.rb +42 -22
- 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 +15 -11
- data/lib/active_record/store.rb +44 -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 +76 -0
- data/lib/active_record/tasks/database_tasks.rb +44 -45
- data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
- 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 +14 -4
- data/lib/active_record/test_fixtures.rb +27 -2
- 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 +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.rb +71 -6
- 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/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 +6 -22
- 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,7 @@ module ActiveRecord
|
|
|
232
232
|
_create_record(attributes, true, &block)
|
|
233
233
|
end
|
|
234
234
|
|
|
235
|
-
# Whether the association
|
|
235
|
+
# Whether the association represents a single record
|
|
236
236
|
# or a collection of records.
|
|
237
237
|
def collection?
|
|
238
238
|
false
|
|
@@ -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)
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -259,10 +259,10 @@ module ActiveRecord
|
|
|
259
259
|
klass = reflection.klass
|
|
260
260
|
return false unless record.is_a?(klass)
|
|
261
261
|
|
|
262
|
-
if
|
|
263
|
-
include_in_memory?(record)
|
|
264
|
-
elsif loaded?
|
|
262
|
+
if loaded?
|
|
265
263
|
target.include?(record)
|
|
264
|
+
elsif record.new_record?
|
|
265
|
+
include_in_memory?(record)
|
|
266
266
|
else
|
|
267
267
|
record_id = klass.composite_primary_key? ? klass.primary_key.zip(record.id).to_h : record.id
|
|
268
268
|
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
|
|
@@ -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
|
|
@@ -38,41 +38,39 @@ module ActiveRecord
|
|
|
38
38
|
chain << [reflection, table]
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
klass = reflection.klass
|
|
41
|
+
# The chain starts with the target table, but we want to end with it here (makes
|
|
42
|
+
# more sense in this context), so we reverse
|
|
43
|
+
chain.reverse_each do |reflection, table|
|
|
44
|
+
klass = reflection.klass
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
scope = reflection.join_scope(table, foreign_table, foreign_klass)
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
unless scope.references_values.empty?
|
|
49
|
+
associations = scope.eager_load_values | scope.includes_values
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
end
|
|
51
|
+
unless associations.empty?
|
|
52
|
+
scope.joins! scope.construct_join_dependency(associations, Arel::Nodes::OuterJoin)
|
|
55
53
|
end
|
|
54
|
+
end
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
arel = scope.arel(alias_tracker.aliases)
|
|
57
|
+
nodes = arel.constraints.first
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
end
|
|
59
|
+
if nodes.is_a?(Arel::Nodes::And)
|
|
60
|
+
others = nodes.children.extract! do |node|
|
|
61
|
+
!Arel.fetch_attribute(node) { |attr| attr.relation.name == table.name }
|
|
64
62
|
end
|
|
63
|
+
end
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
joins << join_type.new(table, Arel::Nodes::On.new(nodes))
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# The current table in this iteration becomes the foreign table in the next
|
|
74
|
-
foreign_table, foreign_klass = table, klass
|
|
67
|
+
if others && !others.empty?
|
|
68
|
+
joins.concat arel.join_sources
|
|
69
|
+
append_constraints(joins.last, others)
|
|
75
70
|
end
|
|
71
|
+
|
|
72
|
+
# The current table in this iteration becomes the foreign table in the next
|
|
73
|
+
foreign_table, foreign_klass = table, klass
|
|
76
74
|
end
|
|
77
75
|
|
|
78
76
|
joins
|
|
@@ -91,10 +89,10 @@ module ActiveRecord
|
|
|
91
89
|
end
|
|
92
90
|
|
|
93
91
|
private
|
|
94
|
-
def append_constraints(
|
|
92
|
+
def append_constraints(join, constraints)
|
|
95
93
|
if join.is_a?(Arel::Nodes::StringJoin)
|
|
96
94
|
join_string = Arel::Nodes::And.new(constraints.unshift join.left)
|
|
97
|
-
join.left =
|
|
95
|
+
join.left = join_string
|
|
98
96
|
else
|
|
99
97
|
right = join.right
|
|
100
98
|
right.expr = Arel::Nodes::And.new(constraints.unshift right.expr)
|
|
@@ -103,7 +103,7 @@ module ActiveRecord
|
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def instantiate(result_set, strict_loading_value, &block)
|
|
106
|
-
primary_key = aliases.column_alias(join_root,
|
|
106
|
+
primary_key = Array(join_root.primary_key).map { |column| aliases.column_alias(join_root, column) }
|
|
107
107
|
|
|
108
108
|
seen = Hash.new { |i, parent|
|
|
109
109
|
i[parent] = Hash.new { |j, child_class|
|
|
@@ -141,7 +141,7 @@ module ActiveRecord
|
|
|
141
141
|
|
|
142
142
|
message_bus.instrument("instantiation.active_record", payload) do
|
|
143
143
|
result_set.each { |row_hash|
|
|
144
|
-
parent_key = primary_key ? row_hash
|
|
144
|
+
parent_key = primary_key.empty? ? row_hash : row_hash.values_at(*primary_key)
|
|
145
145
|
parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, column_types, &block)
|
|
146
146
|
construct(parent, join_root, row_hash, seen, model_cache, strict_loading_value)
|
|
147
147
|
}
|
|
@@ -235,6 +235,8 @@ module ActiveRecord
|
|
|
235
235
|
raise EagerLoadPolymorphicError.new(reflection)
|
|
236
236
|
end
|
|
237
237
|
|
|
238
|
+
Deprecation.guard(reflection) { "referenced in query to join its table" }
|
|
239
|
+
|
|
238
240
|
JoinAssociation.new(reflection, build(right, reflection.klass))
|
|
239
241
|
end
|
|
240
242
|
end
|
|
@@ -38,7 +38,13 @@ module ActiveRecord
|
|
|
38
38
|
attr_reader :loaders
|
|
39
39
|
|
|
40
40
|
def group_and_load_similar(loaders)
|
|
41
|
-
loaders.grep_v(ThroughAssociation)
|
|
41
|
+
non_through = loaders.grep_v(ThroughAssociation)
|
|
42
|
+
|
|
43
|
+
grouped = non_through.group_by do |loader|
|
|
44
|
+
[loader.loader_query, loader.klass]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
grouped.each do |(query, _klass), similar_loaders|
|
|
42
48
|
query.load_records_in_batch(similar_loaders)
|
|
43
49
|
end
|
|
44
50
|
end
|
|
@@ -118,6 +118,7 @@ module ActiveRecord
|
|
|
118
118
|
def loaders
|
|
119
119
|
@loaders ||=
|
|
120
120
|
grouped_records.flat_map do |reflection, reflection_records|
|
|
121
|
+
Deprecation.guard(reflection) { "referenced in query to preload records" }
|
|
121
122
|
preloaders_for_reflection(reflection, reflection_records)
|
|
122
123
|
end
|
|
123
124
|
end
|