activerecord 7.1.4.1 → 7.2.2.1
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 +643 -2274
- data/README.rdoc +15 -15
- data/examples/performance.rb +2 -2
- data/lib/active_record/association_relation.rb +1 -1
- data/lib/active_record/associations/alias_tracker.rb +25 -19
- data/lib/active_record/associations/association.rb +15 -8
- data/lib/active_record/associations/belongs_to_association.rb +14 -7
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
- data/lib/active_record/associations/builder/belongs_to.rb +1 -0
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
- data/lib/active_record/associations/builder/has_many.rb +3 -4
- data/lib/active_record/associations/builder/has_one.rb +3 -4
- data/lib/active_record/associations/collection_association.rb +7 -1
- data/lib/active_record/associations/collection_proxy.rb +14 -1
- data/lib/active_record/associations/errors.rb +265 -0
- data/lib/active_record/associations/has_many_association.rb +1 -1
- data/lib/active_record/associations/has_many_through_association.rb +7 -1
- data/lib/active_record/associations/join_dependency/join_association.rb +30 -27
- data/lib/active_record/associations/join_dependency.rb +4 -4
- data/lib/active_record/associations/nested_error.rb +47 -0
- data/lib/active_record/associations/preloader/association.rb +2 -1
- data/lib/active_record/associations/preloader/branch.rb +7 -1
- data/lib/active_record/associations/preloader/through_association.rb +1 -3
- data/lib/active_record/associations/singular_association.rb +6 -0
- data/lib/active_record/associations/through_association.rb +1 -1
- data/lib/active_record/associations.rb +59 -292
- data/lib/active_record/attribute_assignment.rb +0 -2
- data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
- data/lib/active_record/attribute_methods/primary_key.rb +23 -55
- data/lib/active_record/attribute_methods/read.rb +1 -13
- data/lib/active_record/attribute_methods/serialization.rb +4 -24
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +11 -6
- data/lib/active_record/attribute_methods.rb +54 -63
- data/lib/active_record/attributes.rb +61 -47
- data/lib/active_record/autosave_association.rb +12 -29
- data/lib/active_record/base.rb +2 -3
- data/lib/active_record/callbacks.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +24 -107
- data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +1 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +270 -65
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +34 -17
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +189 -74
- data/lib/active_record/connection_adapters/abstract/quoting.rb +65 -91
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +15 -6
- data/lib/active_record/connection_adapters/abstract/transaction.rb +125 -62
- data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -44
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +40 -10
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +9 -1
- data/lib/active_record/connection_adapters/mysql/quoting.rb +43 -48
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +6 -0
- data/lib/active_record/connection_adapters/mysql2/database_statements.rb +16 -15
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +5 -23
- data/lib/active_record/connection_adapters/pool_config.rb +7 -6
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +27 -4
- data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -0
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +17 -11
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +29 -24
- data/lib/active_record/connection_adapters/schema_cache.rb +123 -128
- data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +10 -6
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +44 -46
- data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +25 -2
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +125 -75
- data/lib/active_record/connection_adapters/trilogy/database_statements.rb +15 -15
- data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -48
- data/lib/active_record/connection_adapters.rb +121 -0
- data/lib/active_record/connection_handling.rb +56 -41
- data/lib/active_record/core.rb +86 -38
- data/lib/active_record/counter_cache.rb +18 -9
- data/lib/active_record/database_configurations/connection_url_resolver.rb +8 -3
- data/lib/active_record/database_configurations/database_config.rb +19 -4
- data/lib/active_record/database_configurations/hash_config.rb +38 -34
- data/lib/active_record/database_configurations/url_config.rb +20 -1
- data/lib/active_record/database_configurations.rb +1 -1
- data/lib/active_record/delegated_type.rb +24 -0
- data/lib/active_record/dynamic_matchers.rb +2 -2
- data/lib/active_record/encryption/encryptable_record.rb +3 -3
- data/lib/active_record/encryption/encrypted_attribute_type.rb +24 -4
- data/lib/active_record/encryption/encryptor.rb +18 -3
- data/lib/active_record/encryption/key_provider.rb +1 -1
- data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
- data/lib/active_record/encryption/message_serializer.rb +4 -0
- data/lib/active_record/encryption/null_encryptor.rb +4 -0
- data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
- data/lib/active_record/encryption.rb +2 -0
- data/lib/active_record/enum.rb +19 -2
- data/lib/active_record/errors.rb +46 -20
- data/lib/active_record/explain.rb +13 -24
- data/lib/active_record/fixtures.rb +37 -31
- data/lib/active_record/future_result.rb +8 -4
- data/lib/active_record/gem_version.rb +2 -2
- data/lib/active_record/inheritance.rb +4 -2
- data/lib/active_record/insert_all.rb +18 -15
- data/lib/active_record/integration.rb +4 -1
- data/lib/active_record/internal_metadata.rb +48 -34
- data/lib/active_record/locking/optimistic.rb +7 -6
- data/lib/active_record/log_subscriber.rb +0 -21
- data/lib/active_record/marshalling.rb +4 -1
- data/lib/active_record/message_pack.rb +1 -1
- data/lib/active_record/migration/command_recorder.rb +2 -3
- data/lib/active_record/migration/compatibility.rb +5 -3
- data/lib/active_record/migration/default_strategy.rb +4 -5
- data/lib/active_record/migration/pending_migration_connection.rb +2 -2
- data/lib/active_record/migration.rb +85 -76
- data/lib/active_record/model_schema.rb +32 -68
- data/lib/active_record/nested_attributes.rb +24 -5
- data/lib/active_record/normalization.rb +3 -7
- data/lib/active_record/persistence.rb +30 -352
- data/lib/active_record/query_cache.rb +19 -8
- data/lib/active_record/query_logs.rb +15 -0
- data/lib/active_record/querying.rb +21 -9
- data/lib/active_record/railtie.rb +42 -57
- data/lib/active_record/railties/controller_runtime.rb +13 -4
- data/lib/active_record/railties/databases.rake +40 -43
- data/lib/active_record/reflection.rb +98 -36
- data/lib/active_record/relation/batches/batch_enumerator.rb +15 -2
- data/lib/active_record/relation/batches.rb +14 -8
- data/lib/active_record/relation/calculations.rb +96 -63
- data/lib/active_record/relation/delegation.rb +8 -11
- data/lib/active_record/relation/finder_methods.rb +16 -2
- data/lib/active_record/relation/merger.rb +4 -6
- data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
- data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -3
- data/lib/active_record/relation/predicate_builder.rb +3 -3
- data/lib/active_record/relation/query_methods.rb +224 -58
- data/lib/active_record/relation/record_fetch_warning.rb +3 -0
- data/lib/active_record/relation/spawn_methods.rb +2 -18
- data/lib/active_record/relation/where_clause.rb +7 -19
- data/lib/active_record/relation.rb +496 -72
- data/lib/active_record/result.rb +31 -44
- data/lib/active_record/runtime_registry.rb +39 -0
- data/lib/active_record/sanitization.rb +24 -19
- data/lib/active_record/schema.rb +8 -6
- data/lib/active_record/schema_dumper.rb +19 -9
- data/lib/active_record/schema_migration.rb +30 -14
- data/lib/active_record/scoping/named.rb +1 -0
- data/lib/active_record/signed_id.rb +20 -1
- data/lib/active_record/statement_cache.rb +7 -7
- data/lib/active_record/table_metadata.rb +1 -10
- data/lib/active_record/tasks/database_tasks.rb +81 -42
- data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
- data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
- data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -1
- data/lib/active_record/test_fixtures.rb +86 -89
- data/lib/active_record/testing/query_assertions.rb +121 -0
- data/lib/active_record/timestamp.rb +2 -2
- data/lib/active_record/token_for.rb +22 -12
- data/lib/active_record/touch_later.rb +1 -1
- data/lib/active_record/transaction.rb +132 -0
- data/lib/active_record/transactions.rb +70 -14
- data/lib/active_record/translation.rb +0 -2
- data/lib/active_record/type/serialized.rb +1 -3
- data/lib/active_record/type_caster/connection.rb +4 -4
- data/lib/active_record/validations/associated.rb +9 -3
- data/lib/active_record/validations/uniqueness.rb +15 -10
- data/lib/active_record/validations.rb +4 -1
- data/lib/active_record.rb +148 -39
- data/lib/arel/alias_predication.rb +1 -1
- data/lib/arel/collectors/bind.rb +2 -0
- data/lib/arel/collectors/composite.rb +7 -0
- data/lib/arel/collectors/sql_string.rb +1 -1
- data/lib/arel/collectors/substitute_binds.rb +1 -1
- data/lib/arel/nodes/binary.rb +0 -6
- data/lib/arel/nodes/bound_sql_literal.rb +9 -5
- data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
- data/lib/arel/nodes/node.rb +4 -3
- data/lib/arel/nodes/sql_literal.rb +7 -0
- data/lib/arel/nodes.rb +2 -2
- data/lib/arel/predications.rb +1 -1
- data/lib/arel/select_manager.rb +1 -1
- data/lib/arel/tree_manager.rb +3 -2
- data/lib/arel/update_manager.rb +2 -1
- data/lib/arel/visitors/dot.rb +1 -0
- data/lib/arel/visitors/mysql.rb +9 -4
- data/lib/arel/visitors/postgresql.rb +1 -12
- data/lib/arel/visitors/sqlite.rb +25 -0
- data/lib/arel/visitors/to_sql.rb +29 -16
- data/lib/arel.rb +7 -3
- data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
- metadata +18 -12
@@ -7,6 +7,7 @@ module ActiveRecord
|
|
7
7
|
|
8
8
|
included do
|
9
9
|
class_attribute :_counter_cache_columns, instance_accessor: false, default: []
|
10
|
+
class_attribute :counter_cached_association_names, instance_writer: false, default: []
|
10
11
|
end
|
11
12
|
|
12
13
|
module ClassMethods
|
@@ -181,14 +182,26 @@ module ActiveRecord
|
|
181
182
|
def counter_cache_column?(name) # :nodoc:
|
182
183
|
_counter_cache_columns.include?(name)
|
183
184
|
end
|
185
|
+
|
186
|
+
def load_schema! # :nodoc:
|
187
|
+
super
|
188
|
+
|
189
|
+
association_names = _reflections.filter_map do |name, reflection|
|
190
|
+
next unless reflection.belongs_to? && reflection.counter_cache_column
|
191
|
+
|
192
|
+
name.to_sym
|
193
|
+
end
|
194
|
+
|
195
|
+
self.counter_cached_association_names |= association_names
|
196
|
+
end
|
184
197
|
end
|
185
198
|
|
186
199
|
private
|
187
200
|
def _create_record(attribute_names = self.attribute_names)
|
188
201
|
id = super
|
189
202
|
|
190
|
-
|
191
|
-
association.increment_counters
|
203
|
+
counter_cached_association_names.each do |association_name|
|
204
|
+
association(association_name).increment_counters
|
192
205
|
end
|
193
206
|
|
194
207
|
id
|
@@ -198,7 +211,9 @@ module ActiveRecord
|
|
198
211
|
affected_rows = super
|
199
212
|
|
200
213
|
if affected_rows > 0
|
201
|
-
|
214
|
+
counter_cached_association_names.each do |association_name|
|
215
|
+
association = association(association_name)
|
216
|
+
|
202
217
|
unless destroyed_by_association && _foreign_keys_equal?(destroyed_by_association.foreign_key, association.reflection.foreign_key)
|
203
218
|
association.decrement_counters
|
204
219
|
end
|
@@ -208,12 +223,6 @@ module ActiveRecord
|
|
208
223
|
affected_rows
|
209
224
|
end
|
210
225
|
|
211
|
-
def each_counter_cached_associations
|
212
|
-
_reflections.each do |name, reflection|
|
213
|
-
yield association(name.to_sym) if reflection.belongs_to? && reflection.counter_cache_column
|
214
|
-
end
|
215
|
-
end
|
216
|
-
|
217
226
|
def _foreign_keys_equal?(fkey1, fkey2)
|
218
227
|
fkey1 == fkey2 || Array(fkey1).map(&:to_sym) == Array(fkey2).map(&:to_sym)
|
219
228
|
end
|
@@ -25,8 +25,7 @@ module ActiveRecord
|
|
25
25
|
def initialize(url)
|
26
26
|
raise "Database URL cannot be empty" if url.blank?
|
27
27
|
@uri = uri_parser.parse(url)
|
28
|
-
@adapter =
|
29
|
-
@adapter = "postgresql" if @adapter == "postgres"
|
28
|
+
@adapter = resolved_adapter
|
30
29
|
|
31
30
|
if @uri.opaque
|
32
31
|
@uri.opaque, @query = @uri.opaque.split("?", 2)
|
@@ -46,7 +45,7 @@ module ActiveRecord
|
|
46
45
|
attr_reader :uri
|
47
46
|
|
48
47
|
def uri_parser
|
49
|
-
@uri_parser ||= URI::
|
48
|
+
@uri_parser ||= URI::RFC2396_Parser.new
|
50
49
|
end
|
51
50
|
|
52
51
|
# Converts the query parameters of the URI into a hash.
|
@@ -80,6 +79,12 @@ module ActiveRecord
|
|
80
79
|
end
|
81
80
|
end
|
82
81
|
|
82
|
+
def resolved_adapter
|
83
|
+
adapter = uri.scheme && @uri.scheme.tr("-", "_")
|
84
|
+
adapter = ActiveRecord.protocol_adapters[adapter] || adapter
|
85
|
+
adapter
|
86
|
+
end
|
87
|
+
|
83
88
|
# Returns name of the database.
|
84
89
|
def database_from_path
|
85
90
|
if @adapter == "sqlite3"
|
@@ -11,14 +11,25 @@ module ActiveRecord
|
|
11
11
|
def initialize(env_name, name)
|
12
12
|
@env_name = env_name
|
13
13
|
@name = name
|
14
|
+
@adapter_class = nil
|
14
15
|
end
|
15
16
|
|
16
|
-
def
|
17
|
-
|
17
|
+
def adapter_class
|
18
|
+
@adapter_class ||= ActiveRecord::ConnectionAdapters.resolve(adapter)
|
18
19
|
end
|
19
20
|
|
20
|
-
def
|
21
|
-
"
|
21
|
+
def inspect # :nodoc:
|
22
|
+
"#<#{self.class.name} env_name=#{@env_name} name=#{@name} adapter_class=#{adapter_class}>"
|
23
|
+
end
|
24
|
+
|
25
|
+
def new_connection
|
26
|
+
adapter_class.new(configuration_hash)
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate!
|
30
|
+
adapter_class if adapter
|
31
|
+
|
32
|
+
true
|
22
33
|
end
|
23
34
|
|
24
35
|
def host
|
@@ -84,6 +95,10 @@ module ActiveRecord
|
|
84
95
|
def schema_cache_path
|
85
96
|
raise NotImplementedError
|
86
97
|
end
|
98
|
+
|
99
|
+
def use_metadata_table?
|
100
|
+
raise NotImplementedError
|
101
|
+
end
|
87
102
|
end
|
88
103
|
end
|
89
104
|
end
|
@@ -1,53 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# :markup: markdown
|
4
|
+
|
3
5
|
module ActiveRecord
|
4
6
|
class DatabaseConfigurations
|
5
|
-
#
|
7
|
+
# # Active Record Database Hash Config
|
6
8
|
#
|
7
|
-
# A
|
8
|
-
#
|
9
|
+
# A `HashConfig` object is created for each database configuration entry that is
|
10
|
+
# created from a hash.
|
9
11
|
#
|
10
12
|
# A hash config:
|
11
13
|
#
|
12
|
-
#
|
14
|
+
# { "development" => { "database" => "db_name" } }
|
13
15
|
#
|
14
16
|
# Becomes:
|
15
17
|
#
|
16
|
-
#
|
17
|
-
#
|
18
|
+
# #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10
|
19
|
+
# @env_name="development", @name="primary", @config={database: "db_name"}>
|
18
20
|
#
|
19
21
|
# See ActiveRecord::DatabaseConfigurations for more info.
|
20
22
|
class HashConfig < DatabaseConfig
|
21
23
|
attr_reader :configuration_hash
|
22
24
|
|
23
|
-
|
24
|
-
#
|
25
|
+
# Initialize a new `HashConfig` object
|
26
|
+
#
|
27
|
+
# #### Parameters
|
25
28
|
#
|
26
|
-
#
|
29
|
+
# * `env_name` - The Rails environment, i.e. "development".
|
30
|
+
# * `name` - The db config name. In a standard two-tier database configuration
|
31
|
+
# this will default to "primary". In a multiple database three-tier database
|
32
|
+
# configuration this corresponds to the name used in the second tier, for
|
33
|
+
# example "primary_readonly".
|
34
|
+
# * `configuration_hash` - The config hash. This is the hash that contains the
|
35
|
+
# database adapter, name, and other important information for database
|
36
|
+
# connections.
|
27
37
|
#
|
28
|
-
# * <tt>:env_name</tt> - The \Rails environment, i.e. "development".
|
29
|
-
# * <tt>:name</tt> - The db config name. In a standard two-tier
|
30
|
-
# database configuration this will default to "primary". In a multiple
|
31
|
-
# database three-tier database configuration this corresponds to the name
|
32
|
-
# used in the second tier, for example "primary_readonly".
|
33
|
-
# * <tt>:config</tt> - The config hash. This is the hash that contains the
|
34
|
-
# database adapter, name, and other important information for database
|
35
|
-
# connections.
|
36
38
|
def initialize(env_name, name, configuration_hash)
|
37
39
|
super(env_name, name)
|
38
40
|
@configuration_hash = configuration_hash.symbolize_keys.freeze
|
39
41
|
end
|
40
42
|
|
41
43
|
# Determines whether a database configuration is for a replica / readonly
|
42
|
-
# connection. If the
|
43
|
-
# return
|
44
|
+
# connection. If the `replica` key is present in the config, `replica?` will
|
45
|
+
# return `true`.
|
44
46
|
def replica?
|
45
47
|
configuration_hash[:replica]
|
46
48
|
end
|
47
49
|
|
48
|
-
# The migrations paths for a database configuration. If the
|
49
|
-
#
|
50
|
-
# will return its value.
|
50
|
+
# The migrations paths for a database configuration. If the `migrations_paths`
|
51
|
+
# key is present in the config, `migrations_paths` will return its value.
|
51
52
|
def migrations_paths
|
52
53
|
configuration_hash[:migrations_paths]
|
53
54
|
end
|
@@ -92,8 +93,8 @@ module ActiveRecord
|
|
92
93
|
(configuration_hash[:checkout_timeout] || 5).to_f
|
93
94
|
end
|
94
95
|
|
95
|
-
#
|
96
|
-
# also be useful if someone wants a very low
|
96
|
+
# `reaping_frequency` is configurable mostly for historical reasons, but it
|
97
|
+
# could also be useful if someone wants a very low `idle_timeout`.
|
97
98
|
def reaping_frequency
|
98
99
|
configuration_hash.fetch(:reaping_frequency, 60)&.to_f
|
99
100
|
end
|
@@ -104,12 +105,11 @@ module ActiveRecord
|
|
104
105
|
end
|
105
106
|
|
106
107
|
def adapter
|
107
|
-
configuration_hash[:adapter]
|
108
|
+
configuration_hash[:adapter]&.to_s
|
108
109
|
end
|
109
110
|
|
110
|
-
# The path to the schema cache dump file for a database.
|
111
|
-
#
|
112
|
-
# default will be derived.
|
111
|
+
# The path to the schema cache dump file for a database. If omitted, the
|
112
|
+
# filename will be read from ENV or a default will be derived.
|
113
113
|
def schema_cache_path
|
114
114
|
configuration_hash[:schema_cache_path]
|
115
115
|
end
|
@@ -130,14 +130,14 @@ module ActiveRecord
|
|
130
130
|
Base.configurations.primary?(name)
|
131
131
|
end
|
132
132
|
|
133
|
-
# Determines whether to dump the schema/structure files and the
|
134
|
-
#
|
133
|
+
# Determines whether to dump the schema/structure files and the filename that
|
134
|
+
# should be used.
|
135
135
|
#
|
136
|
-
# If
|
137
|
-
#
|
136
|
+
# If `configuration_hash[:schema_dump]` is set to `false` or `nil` the schema
|
137
|
+
# will not be dumped.
|
138
138
|
#
|
139
|
-
# If the config option is set that will be used. Otherwise
|
140
|
-
#
|
139
|
+
# If the config option is set that will be used. Otherwise Rails will generate
|
140
|
+
# the filename from the database config name.
|
141
141
|
def schema_dump(format = ActiveRecord.schema_format)
|
142
142
|
if configuration_hash.key?(:schema_dump)
|
143
143
|
if config = configuration_hash[:schema_dump]
|
@@ -154,6 +154,10 @@ module ActiveRecord
|
|
154
154
|
!replica? && !!configuration_hash.fetch(:database_tasks, true)
|
155
155
|
end
|
156
156
|
|
157
|
+
def use_metadata_table? # :nodoc:
|
158
|
+
configuration_hash.fetch(:use_metadata_table, true)
|
159
|
+
end
|
160
|
+
|
157
161
|
private
|
158
162
|
def schema_file_type(format)
|
159
163
|
case format
|
@@ -41,10 +41,29 @@ module ActiveRecord
|
|
41
41
|
super(env_name, name, configuration_hash)
|
42
42
|
|
43
43
|
@url = url
|
44
|
-
@configuration_hash = @configuration_hash.merge(build_url_hash)
|
44
|
+
@configuration_hash = @configuration_hash.merge(build_url_hash)
|
45
|
+
|
46
|
+
if @configuration_hash[:schema_dump] == "false"
|
47
|
+
@configuration_hash[:schema_dump] = false
|
48
|
+
end
|
49
|
+
|
50
|
+
if @configuration_hash[:query_cache] == "false"
|
51
|
+
@configuration_hash[:query_cache] = false
|
52
|
+
end
|
53
|
+
|
54
|
+
to_boolean!(@configuration_hash, :replica)
|
55
|
+
to_boolean!(@configuration_hash, :database_tasks)
|
56
|
+
|
57
|
+
@configuration_hash.freeze
|
45
58
|
end
|
46
59
|
|
47
60
|
private
|
61
|
+
def to_boolean!(configuration_hash, key)
|
62
|
+
if configuration_hash[key].is_a?(String)
|
63
|
+
configuration_hash[key] = configuration_hash[key] != "false"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
48
67
|
# Return a Hash that can be merged into the main config that represents
|
49
68
|
# the passed in url
|
50
69
|
def build_url_hash
|
@@ -113,6 +113,26 @@ module ActiveRecord
|
|
113
113
|
# end
|
114
114
|
# end
|
115
115
|
#
|
116
|
+
# == Querying across records
|
117
|
+
#
|
118
|
+
# A consequence of delegated types is that querying attributes spread across multiple classes becomes slightly more
|
119
|
+
# tricky, but not impossible.
|
120
|
+
#
|
121
|
+
# The simplest method is to join the "superclass" to the "subclass" and apply the query parameters (i.e. <tt>#where</tt>)
|
122
|
+
# in appropriate places:
|
123
|
+
#
|
124
|
+
# Comment.joins(:entry).where(comments: { content: 'Hello!' }, entry: { creator: Current.user } )
|
125
|
+
#
|
126
|
+
# For convenience, add a scope on the concern. Now all classes that implement the concern will automatically include
|
127
|
+
# the method:
|
128
|
+
#
|
129
|
+
# # app/models/concerns/entryable.rb
|
130
|
+
# scope :with_entry, ->(attrs) { joins(:entry).where(entry: attrs) }
|
131
|
+
#
|
132
|
+
# Now the query can be shortened significantly:
|
133
|
+
#
|
134
|
+
# Comment.where(content: 'Hello!').with_entry(creator: Current.user)
|
135
|
+
#
|
116
136
|
# == Adding further delegation
|
117
137
|
#
|
118
138
|
# The delegated type shouldn't just answer the question of what the underlying class is called. In fact, that's
|
@@ -219,6 +239,10 @@ module ActiveRecord
|
|
219
239
|
role_type = options[:foreign_type] || "#{role}_type"
|
220
240
|
role_id = options[:foreign_key] || "#{role}_id"
|
221
241
|
|
242
|
+
define_singleton_method "#{role}_types" do
|
243
|
+
types.map(&:to_s)
|
244
|
+
end
|
245
|
+
|
222
246
|
define_method "#{role}_class" do
|
223
247
|
public_send(role_type).constantize
|
224
248
|
end
|
@@ -12,12 +12,12 @@ module ActiveRecord
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
def method_missing(name,
|
15
|
+
def method_missing(name, ...)
|
16
16
|
match = Method.match(self, name)
|
17
17
|
|
18
18
|
if match && match.valid?
|
19
19
|
match.define
|
20
|
-
send(name,
|
20
|
+
send(name, ...)
|
21
21
|
else
|
22
22
|
super
|
23
23
|
end
|
@@ -84,7 +84,7 @@ module ActiveRecord
|
|
84
84
|
def encrypt_attribute(name, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], **context_properties)
|
85
85
|
encrypted_attributes << name.to_sym
|
86
86
|
|
87
|
-
|
87
|
+
decorate_attributes([name]) do |name, cast_type|
|
88
88
|
scheme = scheme_for key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, \
|
89
89
|
downcase: downcase, ignore_case: ignore_case, previous: previous, **context_properties
|
90
90
|
|
@@ -123,7 +123,7 @@ module ActiveRecord
|
|
123
123
|
end)
|
124
124
|
end
|
125
125
|
|
126
|
-
def load_schema!
|
126
|
+
def load_schema! # :nodoc:
|
127
127
|
super
|
128
128
|
|
129
129
|
add_length_validation_for_encrypted_columns if ActiveRecord::Encryption.config.validate_column_size
|
@@ -221,7 +221,7 @@ module ActiveRecord
|
|
221
221
|
end
|
222
222
|
|
223
223
|
def cant_modify_encrypted_attributes_when_frozen
|
224
|
-
self.class
|
224
|
+
self.class.encrypted_attributes.each do |attribute|
|
225
225
|
errors.add(attribute.to_sym, "can't be modified because it is encrypted") if changed_attributes.include?(attribute)
|
226
226
|
end
|
227
227
|
end
|
@@ -7,13 +7,13 @@ module ActiveRecord
|
|
7
7
|
# This is the central piece that connects the encryption system with +encrypts+ declarations in the
|
8
8
|
# model classes. Whenever you declare an attribute as encrypted, it configures an +EncryptedAttributeType+
|
9
9
|
# for that attribute.
|
10
|
-
class EncryptedAttributeType < ::
|
10
|
+
class EncryptedAttributeType < ::ActiveModel::Type::Value
|
11
11
|
include ActiveModel::Type::Helpers::Mutable
|
12
12
|
|
13
13
|
attr_reader :scheme, :cast_type
|
14
14
|
|
15
15
|
delegate :key_provider, :downcase?, :deterministic?, :previous_schemes, :with_context, :fixed?, to: :scheme
|
16
|
-
delegate :accessor, to: :cast_type
|
16
|
+
delegate :accessor, :type, to: :cast_type
|
17
17
|
|
18
18
|
# === Options
|
19
19
|
#
|
@@ -81,7 +81,7 @@ module ActiveRecord
|
|
81
81
|
@previous_type
|
82
82
|
end
|
83
83
|
|
84
|
-
def
|
84
|
+
def decrypt_as_text(value)
|
85
85
|
with_context do
|
86
86
|
unless value.nil?
|
87
87
|
if @default && @default == value
|
@@ -99,6 +99,10 @@ module ActiveRecord
|
|
99
99
|
end
|
100
100
|
end
|
101
101
|
|
102
|
+
def decrypt(value)
|
103
|
+
text_to_database_type decrypt_as_text(value)
|
104
|
+
end
|
105
|
+
|
102
106
|
def try_to_deserialize_with_previous_encrypted_types(value)
|
103
107
|
previous_types.each.with_index do |type, index|
|
104
108
|
break type.deserialize(value)
|
@@ -129,12 +133,20 @@ module ActiveRecord
|
|
129
133
|
encrypt(casted_value.to_s) unless casted_value.nil?
|
130
134
|
end
|
131
135
|
|
132
|
-
def
|
136
|
+
def encrypt_as_text(value)
|
133
137
|
with_context do
|
138
|
+
if encryptor.binary? && !cast_type.binary?
|
139
|
+
raise Errors::Encoding, "Binary encoded data can only be stored in binary columns"
|
140
|
+
end
|
141
|
+
|
134
142
|
encryptor.encrypt(value, **encryption_options)
|
135
143
|
end
|
136
144
|
end
|
137
145
|
|
146
|
+
def encrypt(value)
|
147
|
+
text_to_database_type encrypt_as_text(value)
|
148
|
+
end
|
149
|
+
|
138
150
|
def encryptor
|
139
151
|
ActiveRecord::Encryption.encryptor
|
140
152
|
end
|
@@ -150,6 +162,14 @@ module ActiveRecord
|
|
150
162
|
def clean_text_scheme
|
151
163
|
@clean_text_scheme ||= ActiveRecord::Encryption::Scheme.new(downcase: downcase?, encryptor: ActiveRecord::Encryption::NullEncryptor.new)
|
152
164
|
end
|
165
|
+
|
166
|
+
def text_to_database_type(value)
|
167
|
+
if value && cast_type.binary?
|
168
|
+
ActiveModel::Type::Binary::Data.new(value)
|
169
|
+
else
|
170
|
+
value
|
171
|
+
end
|
172
|
+
end
|
153
173
|
end
|
154
174
|
end
|
155
175
|
end
|
@@ -12,6 +12,14 @@ module ActiveRecord
|
|
12
12
|
# It interacts with a KeyProvider for getting the keys, and delegate to
|
13
13
|
# ActiveRecord::Encryption::Cipher the actual encryption algorithm.
|
14
14
|
class Encryptor
|
15
|
+
# === Options
|
16
|
+
#
|
17
|
+
# * <tt>:compress</tt> - Boolean indicating whether records should be compressed before encryption.
|
18
|
+
# Defaults to +true+.
|
19
|
+
def initialize(compress: true)
|
20
|
+
@compress = compress
|
21
|
+
end
|
22
|
+
|
15
23
|
# Encrypts +clean_text+ and returns the encrypted result
|
16
24
|
#
|
17
25
|
# Internally, it will:
|
@@ -38,7 +46,7 @@ module ActiveRecord
|
|
38
46
|
serialize_message build_encrypted_message(clear_text, key_provider: key_provider, cipher_options: cipher_options)
|
39
47
|
end
|
40
48
|
|
41
|
-
# Decrypts
|
49
|
+
# Decrypts an +encrypted_text+ and returns the result as clean text
|
42
50
|
#
|
43
51
|
# === Options
|
44
52
|
#
|
@@ -66,6 +74,10 @@ module ActiveRecord
|
|
66
74
|
false
|
67
75
|
end
|
68
76
|
|
77
|
+
def binary?
|
78
|
+
serializer.binary?
|
79
|
+
end
|
80
|
+
|
69
81
|
private
|
70
82
|
DECRYPT_ERRORS = [OpenSSL::Cipher::CipherError, Errors::EncryptedContentIntegrity, Errors::Decryption]
|
71
83
|
ENCODING_ERRORS = [EncodingError, Errors::Encoding]
|
@@ -100,7 +112,6 @@ module ActiveRecord
|
|
100
112
|
end
|
101
113
|
|
102
114
|
def deserialize_message(message)
|
103
|
-
raise Errors::Encoding unless message.is_a?(String)
|
104
115
|
serializer.load message
|
105
116
|
rescue ArgumentError, TypeError, Errors::ForbiddenClass
|
106
117
|
raise Errors::Encoding
|
@@ -112,13 +123,17 @@ module ActiveRecord
|
|
112
123
|
|
113
124
|
# Under certain threshold, ZIP compression is actually worse that not compressing
|
114
125
|
def compress_if_worth_it(string)
|
115
|
-
if string.bytesize > THRESHOLD_TO_JUSTIFY_COMPRESSION
|
126
|
+
if compress? && string.bytesize > THRESHOLD_TO_JUSTIFY_COMPRESSION
|
116
127
|
[compress(string), true]
|
117
128
|
else
|
118
129
|
[string, false]
|
119
130
|
end
|
120
131
|
end
|
121
132
|
|
133
|
+
def compress?
|
134
|
+
@compress
|
135
|
+
end
|
136
|
+
|
122
137
|
def compress(data)
|
123
138
|
Zlib::Deflate.deflate(data).tap do |compressed_data|
|
124
139
|
compressed_data.force_encoding(data.encoding)
|
@@ -12,7 +12,7 @@ module ActiveRecord
|
|
12
12
|
@keys = Array(keys)
|
13
13
|
end
|
14
14
|
|
15
|
-
# Returns the
|
15
|
+
# Returns the last key in the list as the active key to perform encryptions
|
16
16
|
#
|
17
17
|
# When +ActiveRecord::Encryption.config.store_key_references+ is true, the key will include
|
18
18
|
# a public tag referencing the key itself. That key will be stored in the public
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/message_pack"
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module Encryption
|
7
|
+
# A message serializer that serializes +Messages+ with MessagePack.
|
8
|
+
#
|
9
|
+
# The message is converted to a hash with this structure:
|
10
|
+
#
|
11
|
+
# {
|
12
|
+
# p: <payload>,
|
13
|
+
# h: {
|
14
|
+
# header1: value1,
|
15
|
+
# header2: value2,
|
16
|
+
# ...
|
17
|
+
# }
|
18
|
+
# }
|
19
|
+
#
|
20
|
+
# Then it is converted to the MessagePack format.
|
21
|
+
class MessagePackMessageSerializer
|
22
|
+
def dump(message)
|
23
|
+
raise Errors::ForbiddenClass unless message.is_a?(Message)
|
24
|
+
ActiveSupport::MessagePack.dump(message_to_hash(message))
|
25
|
+
end
|
26
|
+
|
27
|
+
def load(serialized_content)
|
28
|
+
data = ActiveSupport::MessagePack.load(serialized_content)
|
29
|
+
hash_to_message(data, 1)
|
30
|
+
rescue RuntimeError
|
31
|
+
raise Errors::Decryption
|
32
|
+
end
|
33
|
+
|
34
|
+
def binary?
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def message_to_hash(message)
|
40
|
+
{
|
41
|
+
"p" => message.payload,
|
42
|
+
"h" => headers_to_hash(message.headers)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def headers_to_hash(headers)
|
47
|
+
headers.transform_values do |value|
|
48
|
+
value.is_a?(Message) ? message_to_hash(value) : value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def hash_to_message(data, level)
|
53
|
+
validate_message_data_format(data, level)
|
54
|
+
Message.new(payload: data["p"], headers: parse_properties(data["h"], level))
|
55
|
+
end
|
56
|
+
|
57
|
+
def validate_message_data_format(data, level)
|
58
|
+
if level > 2
|
59
|
+
raise Errors::Decryption, "More than one level of hash nesting in headers is not supported"
|
60
|
+
end
|
61
|
+
|
62
|
+
unless data.is_a?(Hash) && data.has_key?("p")
|
63
|
+
raise Errors::Decryption, "Invalid data format: hash without payload"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_properties(headers, level)
|
68
|
+
Properties.new.tap do |properties|
|
69
|
+
headers&.each do |key, value|
|
70
|
+
properties[key] = value.is_a?(Hash) ? hash_to_message(value, level + 1) : value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|