activerecord 6.1.4 → 7.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1049 -977
- data/MIT-LICENSE +1 -1
- data/README.rdoc +1 -1
- data/lib/active_record/aggregations.rb +1 -1
- data/lib/active_record/association_relation.rb +0 -10
- data/lib/active_record/associations/association.rb +33 -17
- data/lib/active_record/associations/association_scope.rb +1 -3
- data/lib/active_record/associations/belongs_to_association.rb +15 -4
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +10 -2
- data/lib/active_record/associations/builder/association.rb +8 -2
- data/lib/active_record/associations/builder/belongs_to.rb +19 -6
- data/lib/active_record/associations/builder/collection_association.rb +10 -3
- data/lib/active_record/associations/builder/has_many.rb +3 -2
- data/lib/active_record/associations/builder/has_one.rb +2 -1
- data/lib/active_record/associations/builder/singular_association.rb +2 -2
- data/lib/active_record/associations/collection_association.rb +34 -27
- data/lib/active_record/associations/collection_proxy.rb +8 -3
- data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
- data/lib/active_record/associations/has_many_association.rb +1 -1
- data/lib/active_record/associations/has_many_through_association.rb +2 -1
- data/lib/active_record/associations/has_one_association.rb +10 -7
- data/lib/active_record/associations/has_one_through_association.rb +1 -1
- data/lib/active_record/associations/preloader/association.rb +187 -55
- data/lib/active_record/associations/preloader/batch.rb +48 -0
- data/lib/active_record/associations/preloader/branch.rb +147 -0
- data/lib/active_record/associations/preloader/through_association.rb +49 -13
- data/lib/active_record/associations/preloader.rb +39 -113
- data/lib/active_record/associations/singular_association.rb +8 -2
- data/lib/active_record/associations/through_association.rb +3 -3
- data/lib/active_record/associations.rb +90 -82
- data/lib/active_record/asynchronous_queries_tracker.rb +60 -0
- data/lib/active_record/attribute_assignment.rb +1 -1
- data/lib/active_record/attribute_methods/before_type_cast.rb +7 -2
- data/lib/active_record/attribute_methods/dirty.rb +49 -16
- data/lib/active_record/attribute_methods/primary_key.rb +2 -2
- data/lib/active_record/attribute_methods/query.rb +2 -2
- data/lib/active_record/attribute_methods/read.rb +7 -5
- data/lib/active_record/attribute_methods/serialization.rb +66 -12
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -3
- data/lib/active_record/attribute_methods/write.rb +7 -10
- data/lib/active_record/attribute_methods.rb +13 -14
- data/lib/active_record/attributes.rb +24 -35
- data/lib/active_record/autosave_association.rb +6 -21
- data/lib/active_record/base.rb +19 -1
- data/lib/active_record/callbacks.rb +2 -2
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +292 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +209 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +76 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +47 -561
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +0 -17
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +46 -22
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -12
- data/lib/active_record/connection_adapters/abstract/quoting.rb +42 -72
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -17
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +34 -9
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +69 -18
- data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -22
- data/lib/active_record/connection_adapters/abstract_adapter.rb +149 -74
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +97 -81
- data/lib/active_record/connection_adapters/column.rb +4 -0
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +35 -23
- data/lib/active_record/connection_adapters/mysql/quoting.rb +35 -21
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +4 -1
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -6
- data/lib/active_record/connection_adapters/pool_config.rb +7 -7
- data/lib/active_record/connection_adapters/postgresql/column.rb +17 -1
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +19 -12
- data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
- data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
- data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
- data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
- data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +28 -0
- data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
- data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +50 -50
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +32 -0
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +21 -1
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +22 -1
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +25 -0
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +27 -16
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +205 -105
- data/lib/active_record/connection_adapters/schema_cache.rb +29 -4
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +25 -19
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +15 -16
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -2
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -30
- data/lib/active_record/connection_adapters.rb +6 -5
- data/lib/active_record/connection_handling.rb +47 -53
- data/lib/active_record/core.rb +122 -132
- data/lib/active_record/database_configurations/connection_url_resolver.rb +2 -1
- data/lib/active_record/database_configurations/database_config.rb +12 -9
- data/lib/active_record/database_configurations/hash_config.rb +63 -5
- data/lib/active_record/database_configurations/url_config.rb +2 -2
- data/lib/active_record/database_configurations.rb +16 -32
- data/lib/active_record/delegated_type.rb +52 -11
- data/lib/active_record/destroy_association_async_job.rb +1 -1
- data/lib/active_record/disable_joins_association_relation.rb +39 -0
- data/lib/active_record/dynamic_matchers.rb +1 -1
- data/lib/active_record/encryption/cipher/aes256_gcm.rb +98 -0
- data/lib/active_record/encryption/cipher.rb +53 -0
- data/lib/active_record/encryption/config.rb +44 -0
- data/lib/active_record/encryption/configurable.rb +61 -0
- data/lib/active_record/encryption/context.rb +35 -0
- data/lib/active_record/encryption/contexts.rb +72 -0
- data/lib/active_record/encryption/derived_secret_key_provider.rb +12 -0
- data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
- data/lib/active_record/encryption/encryptable_record.rb +208 -0
- data/lib/active_record/encryption/encrypted_attribute_type.rb +140 -0
- data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
- data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
- data/lib/active_record/encryption/encryptor.rb +155 -0
- data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
- data/lib/active_record/encryption/errors.rb +15 -0
- data/lib/active_record/encryption/extended_deterministic_queries.rb +160 -0
- data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +28 -0
- data/lib/active_record/encryption/key.rb +28 -0
- data/lib/active_record/encryption/key_generator.rb +42 -0
- data/lib/active_record/encryption/key_provider.rb +46 -0
- data/lib/active_record/encryption/message.rb +33 -0
- data/lib/active_record/encryption/message_serializer.rb +90 -0
- data/lib/active_record/encryption/null_encryptor.rb +21 -0
- data/lib/active_record/encryption/properties.rb +76 -0
- data/lib/active_record/encryption/read_only_null_encryptor.rb +24 -0
- data/lib/active_record/encryption/scheme.rb +99 -0
- data/lib/active_record/encryption.rb +55 -0
- data/lib/active_record/enum.rb +49 -42
- data/lib/active_record/errors.rb +67 -4
- data/lib/active_record/explain_registry.rb +11 -6
- data/lib/active_record/fixture_set/file.rb +15 -1
- data/lib/active_record/fixture_set/table_row.rb +41 -6
- data/lib/active_record/fixture_set/table_rows.rb +4 -4
- data/lib/active_record/fixtures.rb +17 -20
- data/lib/active_record/future_result.rb +139 -0
- data/lib/active_record/gem_version.rb +4 -4
- data/lib/active_record/inheritance.rb +55 -17
- data/lib/active_record/insert_all.rb +80 -14
- data/lib/active_record/integration.rb +4 -3
- data/lib/active_record/internal_metadata.rb +3 -5
- data/lib/active_record/legacy_yaml_adapter.rb +2 -39
- data/lib/active_record/locking/optimistic.rb +10 -9
- data/lib/active_record/locking/pessimistic.rb +9 -3
- data/lib/active_record/log_subscriber.rb +14 -3
- data/lib/active_record/middleware/database_selector/resolver.rb +6 -10
- data/lib/active_record/middleware/database_selector.rb +8 -3
- data/lib/active_record/middleware/shard_selector.rb +60 -0
- data/lib/active_record/migration/command_recorder.rb +4 -4
- data/lib/active_record/migration/compatibility.rb +83 -1
- data/lib/active_record/migration/join_table.rb +1 -1
- data/lib/active_record/migration.rb +109 -79
- data/lib/active_record/model_schema.rb +45 -58
- data/lib/active_record/nested_attributes.rb +13 -12
- data/lib/active_record/no_touching.rb +3 -3
- data/lib/active_record/null_relation.rb +2 -6
- data/lib/active_record/persistence.rb +219 -52
- data/lib/active_record/query_cache.rb +2 -2
- data/lib/active_record/query_logs.rb +138 -0
- data/lib/active_record/querying.rb +15 -5
- data/lib/active_record/railtie.rb +127 -17
- data/lib/active_record/railties/controller_runtime.rb +1 -1
- data/lib/active_record/railties/databases.rake +66 -129
- data/lib/active_record/readonly_attributes.rb +11 -0
- data/lib/active_record/reflection.rb +67 -50
- data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
- data/lib/active_record/relation/batches.rb +3 -3
- data/lib/active_record/relation/calculations.rb +40 -36
- data/lib/active_record/relation/delegation.rb +6 -6
- data/lib/active_record/relation/finder_methods.rb +31 -35
- data/lib/active_record/relation/merger.rb +20 -13
- data/lib/active_record/relation/predicate_builder.rb +1 -6
- data/lib/active_record/relation/query_attribute.rb +5 -11
- data/lib/active_record/relation/query_methods.rb +235 -61
- data/lib/active_record/relation/record_fetch_warning.rb +7 -9
- data/lib/active_record/relation/spawn_methods.rb +2 -2
- data/lib/active_record/relation/where_clause.rb +10 -19
- data/lib/active_record/relation.rb +171 -84
- data/lib/active_record/result.rb +17 -7
- data/lib/active_record/runtime_registry.rb +9 -13
- data/lib/active_record/sanitization.rb +11 -7
- data/lib/active_record/schema_dumper.rb +10 -3
- data/lib/active_record/schema_migration.rb +0 -4
- data/lib/active_record/scoping/default.rb +61 -12
- data/lib/active_record/scoping/named.rb +3 -11
- data/lib/active_record/scoping.rb +64 -34
- data/lib/active_record/serialization.rb +1 -1
- data/lib/active_record/signed_id.rb +1 -1
- data/lib/active_record/suppressor.rb +11 -15
- data/lib/active_record/tasks/database_tasks.rb +116 -58
- data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
- data/lib/active_record/tasks/postgresql_database_tasks.rb +19 -12
- data/lib/active_record/test_databases.rb +1 -1
- data/lib/active_record/test_fixtures.rb +4 -4
- data/lib/active_record/timestamp.rb +3 -4
- data/lib/active_record/transactions.rb +9 -14
- data/lib/active_record/translation.rb +2 -2
- data/lib/active_record/type/adapter_specific_registry.rb +32 -7
- data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
- data/lib/active_record/type/internal/timezone.rb +2 -2
- data/lib/active_record/type/serialized.rb +1 -1
- data/lib/active_record/type/type_map.rb +17 -20
- data/lib/active_record/type.rb +1 -2
- data/lib/active_record/validations/associated.rb +1 -1
- data/lib/active_record/validations/uniqueness.rb +1 -1
- data/lib/active_record.rb +204 -28
- data/lib/arel/attributes/attribute.rb +0 -8
- data/lib/arel/crud.rb +28 -22
- data/lib/arel/delete_manager.rb +18 -4
- data/lib/arel/filter_predications.rb +9 -0
- data/lib/arel/insert_manager.rb +2 -3
- data/lib/arel/nodes/casted.rb +1 -1
- data/lib/arel/nodes/delete_statement.rb +12 -13
- data/lib/arel/nodes/filter.rb +10 -0
- data/lib/arel/nodes/function.rb +1 -0
- data/lib/arel/nodes/insert_statement.rb +2 -2
- data/lib/arel/nodes/select_core.rb +2 -2
- data/lib/arel/nodes/select_statement.rb +2 -2
- data/lib/arel/nodes/update_statement.rb +8 -3
- data/lib/arel/nodes.rb +1 -0
- data/lib/arel/predications.rb +11 -3
- data/lib/arel/select_manager.rb +10 -4
- data/lib/arel/table.rb +0 -1
- data/lib/arel/tree_manager.rb +0 -12
- data/lib/arel/update_manager.rb +18 -4
- data/lib/arel/visitors/dot.rb +80 -90
- data/lib/arel/visitors/mysql.rb +8 -2
- data/lib/arel/visitors/postgresql.rb +0 -10
- data/lib/arel/visitors/to_sql.rb +58 -2
- data/lib/arel.rb +2 -1
- data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
- data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
- data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
- data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
- metadata +56 -13
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "uri"
|
3
4
|
require "active_record/database_configurations/database_config"
|
4
5
|
require "active_record/database_configurations/hash_config"
|
5
6
|
require "active_record/database_configurations/url_config"
|
@@ -20,7 +21,7 @@ module ActiveRecord
|
|
20
21
|
end
|
21
22
|
|
22
23
|
# Collects the configs for the environment and optionally the specification
|
23
|
-
# name passed in. To include replica configurations pass <tt>
|
24
|
+
# name passed in. To include replica configurations pass <tt>include_hidden: true</tt>.
|
24
25
|
#
|
25
26
|
# If a name is provided a single DatabaseConfig object will be
|
26
27
|
# returned, otherwise an array of DatabaseConfig objects will be
|
@@ -33,22 +34,26 @@ module ActiveRecord
|
|
33
34
|
# * <tt>name:</tt> The db config name (i.e. primary, animals, etc.). Defaults
|
34
35
|
# to +nil+. If no +env_name+ is specified the config for the default env and the
|
35
36
|
# passed +name+ will be returned.
|
36
|
-
# * <tt>include_replicas:</tt> Determines whether to include replicas in
|
37
|
+
# * <tt>include_replicas:</tt> Deprecated. Determines whether to include replicas in
|
37
38
|
# the returned list. Most of the time we're only iterating over the write
|
38
39
|
# connection (i.e. migrations don't need to run for the write and read connection).
|
39
40
|
# Defaults to +false+.
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
# * <tt>include_hidden:</tte Determines whether to include replicas and configurations
|
42
|
+
# hidden by +database_tasks: false+ in the returned list. Most of the time we're only
|
43
|
+
# iterating over the primary connections (i.e. migrations don't need to run for the
|
44
|
+
# write and read connection). Defaults to +false+.
|
45
|
+
def configs_for(env_name: nil, name: nil, include_replicas: false, include_hidden: false)
|
46
|
+
if include_replicas
|
47
|
+
include_hidden = include_replicas
|
48
|
+
ActiveSupport::Deprecation.warn("The kwarg `include_replicas` is deprecated in favor of `include_hidden`. When `include_hidden` is passed, configurations with `replica: true` or `database_tasks: false` will be returned. `include_replicas` will be removed in Rails 7.1.")
|
44
49
|
end
|
45
50
|
|
46
51
|
env_name ||= default_env if name
|
47
52
|
configs = env_with_configs(env_name)
|
48
53
|
|
49
|
-
unless
|
54
|
+
unless include_hidden
|
50
55
|
configs = configs.select do |db_config|
|
51
|
-
|
56
|
+
db_config.database_tasks?
|
52
57
|
end
|
53
58
|
end
|
54
59
|
|
@@ -61,19 +66,6 @@ module ActiveRecord
|
|
61
66
|
end
|
62
67
|
end
|
63
68
|
|
64
|
-
# Returns the config hash that corresponds with the environment
|
65
|
-
#
|
66
|
-
# If the application has multiple databases +default_hash+ will
|
67
|
-
# return the first config hash for the environment.
|
68
|
-
#
|
69
|
-
# { database: "my_db", adapter: "mysql2" }
|
70
|
-
def default_hash(env = default_env)
|
71
|
-
default = find_db_config(env)
|
72
|
-
default.configuration_hash if default
|
73
|
-
end
|
74
|
-
alias :[] :default_hash
|
75
|
-
deprecate "[]": "Use configs_for", default_hash: "Use configs_for"
|
76
|
-
|
77
69
|
# Returns a single DatabaseConfig object based on the requested environment.
|
78
70
|
#
|
79
71
|
# If the application has multiple databases +find_db_config+ will return
|
@@ -100,14 +92,6 @@ module ActiveRecord
|
|
100
92
|
first_config && name == first_config.name
|
101
93
|
end
|
102
94
|
|
103
|
-
# Returns the DatabaseConfigurations object as a Hash.
|
104
|
-
def to_h
|
105
|
-
configurations.inject({}) do |memo, db_config|
|
106
|
-
memo.merge(db_config.env_name => db_config.configuration_hash.stringify_keys)
|
107
|
-
end
|
108
|
-
end
|
109
|
-
deprecate to_h: "You can use `ActiveRecord::Base.configurations.configs_for(env_name: 'env', name: 'primary').configuration_hash` to get the configuration hashes."
|
110
|
-
|
111
95
|
# Checks if the application's configurations are empty.
|
112
96
|
#
|
113
97
|
# Aliased to blank?
|
@@ -166,7 +150,7 @@ module ActiveRecord
|
|
166
150
|
return configs if configs.is_a?(Array)
|
167
151
|
|
168
152
|
db_configs = configs.flat_map do |env_name, config|
|
169
|
-
if config.is_a?(Hash) && config.all?
|
153
|
+
if config.is_a?(Hash) && config.values.all?(Hash)
|
170
154
|
walk_configs(env_name.to_s, config)
|
171
155
|
else
|
172
156
|
build_db_config_from_raw_config(env_name.to_s, "primary", config)
|
@@ -193,7 +177,7 @@ module ActiveRecord
|
|
193
177
|
raise AdapterNotSpecified, <<~MSG
|
194
178
|
The `#{name}` database is not configured for the `#{default_env}` environment.
|
195
179
|
|
196
|
-
Available
|
180
|
+
Available database configurations are:
|
197
181
|
|
198
182
|
#{build_configuration_sentence}
|
199
183
|
MSG
|
@@ -201,7 +185,7 @@ module ActiveRecord
|
|
201
185
|
end
|
202
186
|
|
203
187
|
def build_configuration_sentence
|
204
|
-
configs = configs_for(
|
188
|
+
configs = configs_for(include_hidden: true)
|
205
189
|
|
206
190
|
configs.group_by(&:env_name).map do |env, config|
|
207
191
|
names = config.map(&:name)
|
@@ -51,10 +51,9 @@ module ActiveRecord
|
|
51
51
|
# end
|
52
52
|
# end
|
53
53
|
#
|
54
|
-
# # Schema: messages[ id, subject ]
|
54
|
+
# # Schema: messages[ id, subject, body ]
|
55
55
|
# class Message < ApplicationRecord
|
56
56
|
# include Entryable
|
57
|
-
# has_rich_text :content
|
58
57
|
# end
|
59
58
|
#
|
60
59
|
# # Schema: comments[ id, content ]
|
@@ -66,7 +65,7 @@ module ActiveRecord
|
|
66
65
|
# resides in the +Entry+ "superclass". But the +Entry+ absolutely can stand alone in terms of querying capacity
|
67
66
|
# in particular. You can now easily do things like:
|
68
67
|
#
|
69
|
-
# Account.entries.order(created_at: :desc).limit(50)
|
68
|
+
# Account.find(1).entries.order(created_at: :desc).limit(50)
|
70
69
|
#
|
71
70
|
# Which is exactly what you want when displaying both comments and messages together. The entry itself can
|
72
71
|
# be rendered as its delegated type easily, like so:
|
@@ -76,7 +75,9 @@ module ActiveRecord
|
|
76
75
|
#
|
77
76
|
# # entries/entryables/_message.html.erb
|
78
77
|
# <div class="message">
|
79
|
-
#
|
78
|
+
# <div class="subject"><%= entry.message.subject %></div>
|
79
|
+
# <p><%= entry.message.body %></p>
|
80
|
+
# <i>Posted on <%= entry.created_at %> by <%= entry.creator.name %></i>
|
80
81
|
# </div>
|
81
82
|
#
|
82
83
|
# # entries/entryables/_comment.html.erb
|
@@ -136,6 +137,21 @@ module ActiveRecord
|
|
136
137
|
# end
|
137
138
|
#
|
138
139
|
# Now you can list a bunch of entries, call +Entry#title+, and polymorphism will provide you with the answer.
|
140
|
+
#
|
141
|
+
# == Nested Attributes
|
142
|
+
#
|
143
|
+
# Enabling nested attributes on a delegated_type association allows you to
|
144
|
+
# create the entry and message in one go:
|
145
|
+
#
|
146
|
+
# class Entry < ApplicationRecord
|
147
|
+
# delegated_type :entryable, types: %w[ Message Comment ]
|
148
|
+
# accepts_nested_attributes_for :entryable
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# params = { entry: { entryable_type: 'Message', entryable_attributes: { subject: 'Smiling' } } }
|
152
|
+
# entry = Entry.create(params[:entry])
|
153
|
+
# entry.entryable.id # => 2
|
154
|
+
# entry.entryable.subject # => 'Smiling'
|
139
155
|
module DelegatedType
|
140
156
|
# Defines this as a class that'll delegate its type for the passed +role+ to the class references in +types+.
|
141
157
|
# That'll create a polymorphic +belongs_to+ relationship to that +role+, and it'll add all the delegated
|
@@ -156,8 +172,6 @@ module ActiveRecord
|
|
156
172
|
# Entry#comment # => returns the comment record, when entryable_type == "Comment", otherwise nil
|
157
173
|
# Entry#comment_id # => returns entryable_id, when entryable_type == "Comment", otherwise nil
|
158
174
|
#
|
159
|
-
# The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc.
|
160
|
-
#
|
161
175
|
# You can also declare namespaced types:
|
162
176
|
#
|
163
177
|
# class Entry < ApplicationRecord
|
@@ -167,15 +181,38 @@ module ActiveRecord
|
|
167
181
|
# Entry.access_notice_messages
|
168
182
|
# entry.access_notice_message
|
169
183
|
# entry.access_notice_message?
|
184
|
+
#
|
185
|
+
# === Options
|
186
|
+
#
|
187
|
+
# The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc.
|
188
|
+
# The following options can be included to specialize the behavior of the delegated type convenience methods.
|
189
|
+
#
|
190
|
+
# [:foreign_key]
|
191
|
+
# Specify the foreign key used for the convenience methods. By default this is guessed to be the passed
|
192
|
+
# +role+ with an "_id" suffix. So a class that defines a
|
193
|
+
# <tt>delegated_type :entryable, types: %w[ Message Comment ]</tt> association will use "entryable_id" as
|
194
|
+
# the default <tt>:foreign_key</tt>.
|
195
|
+
# [:primary_key]
|
196
|
+
# Specify the method that returns the primary key of associated object used for the convenience methods.
|
197
|
+
# By default this is +id+.
|
198
|
+
#
|
199
|
+
# Option examples:
|
200
|
+
# class Entry < ApplicationRecord
|
201
|
+
# delegated_type :entryable, types: %w[ Message Comment ], primary_key: :uuid, foreign_key: :entryable_uuid
|
202
|
+
# end
|
203
|
+
#
|
204
|
+
# Entry#message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
|
205
|
+
# Entry#comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil
|
170
206
|
def delegated_type(role, types:, **options)
|
171
207
|
belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
|
172
|
-
define_delegated_type_methods role, types: types
|
208
|
+
define_delegated_type_methods role, types: types, options: options
|
173
209
|
end
|
174
210
|
|
175
211
|
private
|
176
|
-
def define_delegated_type_methods(role, types:)
|
212
|
+
def define_delegated_type_methods(role, types:, options:)
|
213
|
+
primary_key = options[:primary_key] || "id"
|
177
214
|
role_type = "#{role}_type"
|
178
|
-
role_id = "#{role}_id"
|
215
|
+
role_id = options[:foreign_key] || "#{role}_id"
|
179
216
|
|
180
217
|
define_method "#{role}_class" do
|
181
218
|
public_send("#{role}_type").constantize
|
@@ -185,8 +222,12 @@ module ActiveRecord
|
|
185
222
|
public_send("#{role}_class").model_name.singular.inquiry
|
186
223
|
end
|
187
224
|
|
225
|
+
define_method "build_#{role}" do |*params|
|
226
|
+
public_send("#{role}=", public_send("#{role}_class").new(*params))
|
227
|
+
end
|
228
|
+
|
188
229
|
types.each do |type|
|
189
|
-
scope_name = type.tableize.
|
230
|
+
scope_name = type.tableize.tr("/", "_")
|
190
231
|
singular = scope_name.singularize
|
191
232
|
query = "#{singular}?"
|
192
233
|
|
@@ -200,7 +241,7 @@ module ActiveRecord
|
|
200
241
|
public_send(role) if public_send(query)
|
201
242
|
end
|
202
243
|
|
203
|
-
define_method "#{singular}
|
244
|
+
define_method "#{singular}_#{primary_key}" do
|
204
245
|
public_send(role_id) if public_send(query)
|
205
246
|
end
|
206
247
|
end
|
@@ -6,7 +6,7 @@ module ActiveRecord
|
|
6
6
|
|
7
7
|
# Job to destroy the records associated with a destroyed record in background.
|
8
8
|
class DestroyAssociationAsyncJob < ActiveJob::Base
|
9
|
-
queue_as { ActiveRecord
|
9
|
+
queue_as { ActiveRecord.queues[:destroy] }
|
10
10
|
|
11
11
|
discard_on ActiveJob::DeserializationError
|
12
12
|
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class DisableJoinsAssociationRelation < Relation # :nodoc:
|
5
|
+
attr_reader :ids, :key
|
6
|
+
|
7
|
+
def initialize(klass, key, ids)
|
8
|
+
@ids = ids.uniq
|
9
|
+
@key = key
|
10
|
+
super(klass)
|
11
|
+
end
|
12
|
+
|
13
|
+
def limit(value)
|
14
|
+
records.take(value)
|
15
|
+
end
|
16
|
+
|
17
|
+
def first(limit = nil)
|
18
|
+
if limit
|
19
|
+
records.limit(limit).first
|
20
|
+
else
|
21
|
+
records.first
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def load
|
26
|
+
super
|
27
|
+
records = @records
|
28
|
+
|
29
|
+
records_by_id = records.group_by do |record|
|
30
|
+
record[key]
|
31
|
+
end
|
32
|
+
|
33
|
+
records = ids.flat_map { |id| records_by_id[id.to_i] }
|
34
|
+
records.compact!
|
35
|
+
|
36
|
+
@records = records
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "base64"
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module Encryption
|
8
|
+
class Cipher
|
9
|
+
# A 256-GCM cipher.
|
10
|
+
#
|
11
|
+
# By default it will use random initialization vectors. For deterministic encryption, it will use a SHA-256 hash of
|
12
|
+
# the text to encrypt and the secret.
|
13
|
+
#
|
14
|
+
# See +Encryptor+
|
15
|
+
class Aes256Gcm
|
16
|
+
CIPHER_TYPE = "aes-256-gcm"
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def key_length
|
20
|
+
OpenSSL::Cipher.new(CIPHER_TYPE).key_len
|
21
|
+
end
|
22
|
+
|
23
|
+
def iv_length
|
24
|
+
OpenSSL::Cipher.new(CIPHER_TYPE).iv_len
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# When iv not provided, it will generate a random iv on each encryption operation (default and
|
29
|
+
# recommended operation)
|
30
|
+
def initialize(secret, deterministic: false)
|
31
|
+
@secret = secret
|
32
|
+
@deterministic = deterministic
|
33
|
+
end
|
34
|
+
|
35
|
+
def encrypt(clear_text)
|
36
|
+
# This code is extracted from +ActiveSupport::MessageEncryptor+. Not using it directly because we want to control
|
37
|
+
# the message format and only serialize things once at the +ActiveRecord::Encryption::Message+ level. Also, this
|
38
|
+
# cipher is prepared to deal with deterministic/non deterministic encryption modes.
|
39
|
+
|
40
|
+
cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
|
41
|
+
cipher.encrypt
|
42
|
+
cipher.key = @secret
|
43
|
+
|
44
|
+
iv = generate_iv(cipher, clear_text)
|
45
|
+
cipher.iv = iv
|
46
|
+
|
47
|
+
encrypted_data = clear_text.empty? ? clear_text.dup : cipher.update(clear_text)
|
48
|
+
encrypted_data << cipher.final
|
49
|
+
|
50
|
+
ActiveRecord::Encryption::Message.new(payload: encrypted_data).tap do |message|
|
51
|
+
message.headers.iv = iv
|
52
|
+
message.headers.auth_tag = cipher.auth_tag
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def decrypt(encrypted_message)
|
57
|
+
encrypted_data = encrypted_message.payload
|
58
|
+
iv = encrypted_message.headers.iv
|
59
|
+
auth_tag = encrypted_message.headers.auth_tag
|
60
|
+
|
61
|
+
# Currently the OpenSSL bindings do not raise an error if auth_tag is
|
62
|
+
# truncated, which would allow an attacker to easily forge it. See
|
63
|
+
# https://github.com/ruby/openssl/issues/63
|
64
|
+
raise ActiveRecord::Encryption::Errors::EncryptedContentIntegrity if auth_tag.nil? || auth_tag.bytes.length != 16
|
65
|
+
|
66
|
+
cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
|
67
|
+
|
68
|
+
cipher.decrypt
|
69
|
+
cipher.key = @secret
|
70
|
+
cipher.iv = iv
|
71
|
+
|
72
|
+
cipher.auth_tag = auth_tag
|
73
|
+
cipher.auth_data = ""
|
74
|
+
|
75
|
+
decrypted_data = encrypted_data.empty? ? encrypted_data : cipher.update(encrypted_data)
|
76
|
+
decrypted_data << cipher.final
|
77
|
+
|
78
|
+
decrypted_data
|
79
|
+
rescue OpenSSL::Cipher::CipherError, TypeError, ArgumentError
|
80
|
+
raise ActiveRecord::Encryption::Errors::Decryption
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
def generate_iv(cipher, clear_text)
|
85
|
+
if @deterministic
|
86
|
+
generate_deterministic_iv(clear_text)
|
87
|
+
else
|
88
|
+
cipher.random_iv
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def generate_deterministic_iv(clear_text)
|
93
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @secret, clear_text)[0, ActiveRecord::Encryption.cipher.iv_length]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# The algorithm used for encrypting and decrypting +Message+ objects.
|
6
|
+
#
|
7
|
+
# It uses AES-256-GCM. It will generate a random IV for non deterministic encryption (default)
|
8
|
+
# or derive an initialization vector from the encrypted content for deterministic encryption.
|
9
|
+
#
|
10
|
+
# See +Cipher::Aes256Gcm+.
|
11
|
+
class Cipher
|
12
|
+
DEFAULT_ENCODING = Encoding::UTF_8
|
13
|
+
|
14
|
+
# Encrypts the provided text and return an encrypted +Message+.
|
15
|
+
def encrypt(clean_text, key:, deterministic: false)
|
16
|
+
cipher_for(key, deterministic: deterministic).encrypt(clean_text).tap do |message|
|
17
|
+
message.headers.encoding = clean_text.encoding.name unless clean_text.encoding == DEFAULT_ENCODING
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Decrypt the provided +Message+.
|
22
|
+
#
|
23
|
+
# When +key+ is an Array, it will try all the keys raising a
|
24
|
+
# +ActiveRecord::Encryption::Errors::Decryption+ if none works.
|
25
|
+
def decrypt(encrypted_message, key:)
|
26
|
+
try_to_decrypt_with_each(encrypted_message, keys: Array(key)).tap do |decrypted_text|
|
27
|
+
decrypted_text.force_encoding(encrypted_message.headers.encoding || DEFAULT_ENCODING)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def key_length
|
32
|
+
Aes256Gcm.key_length
|
33
|
+
end
|
34
|
+
|
35
|
+
def iv_length
|
36
|
+
Aes256Gcm.iv_length
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def try_to_decrypt_with_each(encrypted_text, keys:)
|
41
|
+
keys.each.with_index do |key, index|
|
42
|
+
return cipher_for(key).decrypt(encrypted_text)
|
43
|
+
rescue ActiveRecord::Encryption::Errors::Decryption
|
44
|
+
raise if index == keys.length - 1
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def cipher_for(secret, deterministic: false)
|
49
|
+
Aes256Gcm.new(secret, deterministic: deterministic)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# Container of configuration options
|
6
|
+
class Config
|
7
|
+
attr_accessor :primary_key, :deterministic_key, :store_key_references, :key_derivation_salt,
|
8
|
+
:support_unencrypted_data, :encrypt_fixtures, :validate_column_size, :add_to_filter_parameters,
|
9
|
+
:excluded_from_filter_parameters, :extend_queries, :previous_schemes, :forced_encoding_for_deterministic_encryption
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
set_defaults
|
13
|
+
end
|
14
|
+
|
15
|
+
# Configure previous encryption schemes.
|
16
|
+
#
|
17
|
+
# config.active_record.encryption.previous = [ { key_provider: MyOldKeyProvider.new } ]
|
18
|
+
def previous=(previous_schemes_properties)
|
19
|
+
previous_schemes_properties.each do |properties|
|
20
|
+
add_previous_scheme(**properties)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def set_defaults
|
26
|
+
self.store_key_references = false
|
27
|
+
self.support_unencrypted_data = false
|
28
|
+
self.encrypt_fixtures = false
|
29
|
+
self.validate_column_size = true
|
30
|
+
self.add_to_filter_parameters = true
|
31
|
+
self.excluded_from_filter_parameters = []
|
32
|
+
self.previous_schemes = []
|
33
|
+
self.forced_encoding_for_deterministic_encryption = Encoding::UTF_8
|
34
|
+
|
35
|
+
# TODO: Setting to false for now as the implementation is a bit experimental
|
36
|
+
self.extend_queries = false
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_previous_scheme(**properties)
|
40
|
+
previous_schemes << ActiveRecord::Encryption::Scheme.new(**properties)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# Configuration API for +ActiveRecord::Encryption+
|
6
|
+
module Configurable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
mattr_reader :config, default: Config.new
|
11
|
+
mattr_accessor :encrypted_attribute_declaration_listeners
|
12
|
+
end
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
# Expose getters for context properties
|
16
|
+
Context::PROPERTIES.each do |name|
|
17
|
+
delegate name, to: :context
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure(primary_key:, deterministic_key:, key_derivation_salt:, **properties) # :nodoc:
|
21
|
+
config.primary_key = primary_key
|
22
|
+
config.deterministic_key = deterministic_key
|
23
|
+
config.key_derivation_salt = key_derivation_salt
|
24
|
+
|
25
|
+
context.key_provider = ActiveRecord::Encryption::DerivedSecretKeyProvider.new(primary_key)
|
26
|
+
|
27
|
+
properties.each do |name, value|
|
28
|
+
[:context, :config].each do |configurable_object_name|
|
29
|
+
configurable_object = ActiveRecord::Encryption.send(configurable_object_name)
|
30
|
+
configurable_object.send "#{name}=", value if configurable_object.respond_to?("#{name}=")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Register callback to be invoked when an encrypted attribute is declared.
|
36
|
+
#
|
37
|
+
# === Example:
|
38
|
+
#
|
39
|
+
# ActiveRecord::Encryption.on_encrypted_attribute_declared do |klass, attribute_name|
|
40
|
+
# ...
|
41
|
+
# end
|
42
|
+
def on_encrypted_attribute_declared(&block)
|
43
|
+
self.encrypted_attribute_declaration_listeners ||= Concurrent::Array.new
|
44
|
+
self.encrypted_attribute_declaration_listeners << block
|
45
|
+
end
|
46
|
+
|
47
|
+
def encrypted_attribute_was_declared(klass, name) # :nodoc:
|
48
|
+
self.encrypted_attribute_declaration_listeners&.each do |block|
|
49
|
+
block.call(klass, name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def install_auto_filtered_parameters(application) # :nodoc:
|
54
|
+
ActiveRecord::Encryption.on_encrypted_attribute_declared do |klass, encrypted_attribute_name|
|
55
|
+
application.config.filter_parameters << encrypted_attribute_name unless ActiveRecord::Encryption.config.excluded_from_filter_parameters.include?(name)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# An encryption context configures the different entities used to perform encryption:
|
6
|
+
#
|
7
|
+
# * A key provider
|
8
|
+
# * A key generator
|
9
|
+
# * An encryptor, the facade to encrypt data
|
10
|
+
# * A cipher, the encryption algorithm
|
11
|
+
# * A message serializer
|
12
|
+
class Context
|
13
|
+
PROPERTIES = %i[ key_provider key_generator cipher message_serializer encryptor frozen_encryption ]
|
14
|
+
|
15
|
+
PROPERTIES.each do |name|
|
16
|
+
attr_accessor name
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
set_defaults
|
21
|
+
end
|
22
|
+
|
23
|
+
alias frozen_encryption? frozen_encryption
|
24
|
+
|
25
|
+
private
|
26
|
+
def set_defaults
|
27
|
+
self.frozen_encryption = false
|
28
|
+
self.key_generator = ActiveRecord::Encryption::KeyGenerator.new
|
29
|
+
self.cipher = ActiveRecord::Encryption::Cipher.new
|
30
|
+
self.encryptor = ActiveRecord::Encryption::Encryptor.new
|
31
|
+
self.message_serializer = ActiveRecord::Encryption::MessageSerializer.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# +ActiveRecord::Encryption+ uses encryption contexts to configure the different entities used to
|
6
|
+
# encrypt/decrypt at a given moment in time.
|
7
|
+
#
|
8
|
+
# By default, the library uses a default encryption context. This is the +Context+ that gets configured
|
9
|
+
# initially via +config.active_record.encryption+ options. Library users can define nested encryption contexts
|
10
|
+
# when running blocks of code.
|
11
|
+
#
|
12
|
+
# See +Context+.
|
13
|
+
module Contexts
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
mattr_reader :default_context, default: Context.new
|
18
|
+
thread_mattr_accessor :custom_contexts
|
19
|
+
end
|
20
|
+
|
21
|
+
class_methods do
|
22
|
+
# Configures a custom encryption context to use when running the provided block of code.
|
23
|
+
#
|
24
|
+
# It supports overriding all the properties defined in +Context+.
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
#
|
28
|
+
# ActiveRecord::Encryption.with_encryption_context(encryptor: ActiveRecord::Encryption::NullEncryptor.new) do
|
29
|
+
# ...
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# Encryption contexts can be nested.
|
33
|
+
def with_encryption_context(properties)
|
34
|
+
self.custom_contexts ||= []
|
35
|
+
self.custom_contexts << default_context.dup
|
36
|
+
properties.each do |key, value|
|
37
|
+
self.current_custom_context.send("#{key}=", value)
|
38
|
+
end
|
39
|
+
|
40
|
+
yield
|
41
|
+
ensure
|
42
|
+
self.custom_contexts.pop
|
43
|
+
end
|
44
|
+
|
45
|
+
# Runs the provided block in an encryption context where encryption is disabled:
|
46
|
+
#
|
47
|
+
# * Reading encrypted content will return its ciphertexts.
|
48
|
+
# * Writing encrypted content will write its clear text.
|
49
|
+
def without_encryption(&block)
|
50
|
+
with_encryption_context encryptor: ActiveRecord::Encryption::NullEncryptor.new, &block
|
51
|
+
end
|
52
|
+
|
53
|
+
# Runs the provided block in an encryption context where:
|
54
|
+
#
|
55
|
+
# * Reading encrypted content will return its ciphertext.
|
56
|
+
# * Writing encrypted content will fail.
|
57
|
+
def protecting_encrypted_data(&block)
|
58
|
+
with_encryption_context encryptor: ActiveRecord::Encryption::EncryptingOnlyEncryptor.new, frozen_encryption: true, &block
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the current context. By default it will return the current context.
|
62
|
+
def context
|
63
|
+
self.current_custom_context || self.default_context
|
64
|
+
end
|
65
|
+
|
66
|
+
def current_custom_context
|
67
|
+
self.custom_contexts&.last
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# A +KeyProvider+ that derives keys from passwords.
|
6
|
+
class DerivedSecretKeyProvider < KeyProvider
|
7
|
+
def initialize(passwords)
|
8
|
+
super(Array(passwords).collect { |password| Key.derive_from(password) })
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|