activerecord 8.0.2 → 8.1.0
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 +555 -388
- data/README.rdoc +2 -2
- data/lib/active_record/association_relation.rb +1 -1
- data/lib/active_record/associations/association.rb +1 -1
- data/lib/active_record/associations/belongs_to_association.rb +11 -1
- 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.rb +2 -0
- data/lib/active_record/associations/preloader/branch.rb +1 -0
- data/lib/active_record/associations.rb +159 -21
- 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 +1 -1
- data/lib/active_record/attributes.rb +38 -24
- data/lib/active_record/autosave_association.rb +2 -2
- data/lib/active_record/base.rb +2 -4
- data/lib/active_record/coders/json.rb +14 -5
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +2 -4
- 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 +413 -76
- 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 +26 -34
- 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 +89 -21
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +43 -13
- 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/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 +26 -4
- data/lib/active_record/connection_adapters/mysql2/database_statements.rb +27 -22
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -2
- 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 +8 -21
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +67 -31
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +81 -48
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +25 -9
- data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +39 -27
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -13
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +58 -34
- data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
- data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
- data/lib/active_record/connection_adapters.rb +1 -0
- data/lib/active_record/connection_handling.rb +15 -10
- data/lib/active_record/core.rb +13 -10
- data/lib/active_record/counter_cache.rb +33 -8
- 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 +2 -2
- 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/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 +14 -1
- 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 +34 -5
- data/lib/active_record/railties/controller_runtime.rb +11 -6
- data/lib/active_record/railties/databases.rake +23 -19
- 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 +35 -25
- data/lib/active_record/relation/delegation.rb +0 -1
- data/lib/active_record/relation/finder_methods.rb +41 -24
- data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -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 +3 -1
- data/lib/active_record/relation/query_methods.rb +44 -33
- data/lib/active_record/relation/spawn_methods.rb +6 -6
- data/lib/active_record/relation/where_clause.rb +9 -10
- data/lib/active_record/relation.rb +38 -16
- data/lib/active_record/result.rb +44 -21
- data/lib/active_record/runtime_registry.rb +41 -58
- data/lib/active_record/sanitization.rb +2 -0
- data/lib/active_record/schema_dumper.rb +12 -10
- data/lib/active_record/scoping.rb +0 -1
- data/lib/active_record/secure_token.rb +3 -3
- data/lib/active_record/signed_id.rb +46 -18
- data/lib/active_record/statement_cache.rb +13 -9
- 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 +34 -10
- 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 +68 -5
- data/lib/arel/alias_predication.rb +2 -0
- data/lib/arel/crud.rb +8 -11
- data/lib/arel/delete_manager.rb +5 -0
- 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 +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 +5 -21
- data/lib/arel.rb +3 -1
- data/lib/rails/generators/active_record/application_record/USAGE +1 -1
- metadata +16 -12
- data/lib/active_record/explain_subscriber.rb +0 -34
- data/lib/active_record/normalization.rb +0 -163
|
@@ -38,6 +38,7 @@ module ActiveRecord
|
|
|
38
38
|
def initialize(env_name, name, configuration_hash)
|
|
39
39
|
super(env_name, name)
|
|
40
40
|
@configuration_hash = configuration_hash.symbolize_keys.freeze
|
|
41
|
+
validate_configuration!
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
# Determines whether a database configuration is for a replica / readonly
|
|
@@ -69,16 +70,35 @@ module ActiveRecord
|
|
|
69
70
|
@configuration_hash = configuration_hash.merge(database: database).freeze
|
|
70
71
|
end
|
|
71
72
|
|
|
72
|
-
def
|
|
73
|
-
(
|
|
73
|
+
def max_connections
|
|
74
|
+
max_connections = configuration_hash.fetch(:max_connections) {
|
|
75
|
+
configuration_hash.fetch(:pool, 5)
|
|
76
|
+
}&.to_i
|
|
77
|
+
max_connections if max_connections && max_connections >= 0
|
|
74
78
|
end
|
|
75
79
|
|
|
80
|
+
def min_connections
|
|
81
|
+
(configuration_hash[:min_connections] || 0).to_i
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
alias :pool :max_connections
|
|
85
|
+
deprecate pool: :max_connections, deprecator: ActiveRecord.deprecator
|
|
86
|
+
|
|
76
87
|
def min_threads
|
|
77
88
|
(configuration_hash[:min_threads] || 0).to_i
|
|
78
89
|
end
|
|
79
90
|
|
|
80
91
|
def max_threads
|
|
81
|
-
(configuration_hash[:max_threads] ||
|
|
92
|
+
(configuration_hash[:max_threads] || (max_connections || 5).clamp(0, 5)).to_i
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def max_age
|
|
96
|
+
v = configuration_hash[:max_age]&.to_i
|
|
97
|
+
if v && v > 0
|
|
98
|
+
v
|
|
99
|
+
else
|
|
100
|
+
Float::INFINITY
|
|
101
|
+
end
|
|
82
102
|
end
|
|
83
103
|
|
|
84
104
|
def query_cache
|
|
@@ -93,10 +113,8 @@ module ActiveRecord
|
|
|
93
113
|
(configuration_hash[:checkout_timeout] || 5).to_f
|
|
94
114
|
end
|
|
95
115
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def reaping_frequency
|
|
99
|
-
configuration_hash.fetch(:reaping_frequency, 60)&.to_f
|
|
116
|
+
def reaping_frequency # :nodoc:
|
|
117
|
+
configuration_hash.fetch(:reaping_frequency, default_reaping_frequency)&.to_f
|
|
100
118
|
end
|
|
101
119
|
|
|
102
120
|
def idle_timeout
|
|
@@ -104,6 +122,11 @@ module ActiveRecord
|
|
|
104
122
|
timeout if timeout > 0
|
|
105
123
|
end
|
|
106
124
|
|
|
125
|
+
def keepalive
|
|
126
|
+
keepalive = (configuration_hash[:keepalive] || 600).to_f
|
|
127
|
+
keepalive if keepalive > 0
|
|
128
|
+
end
|
|
129
|
+
|
|
107
130
|
def adapter
|
|
108
131
|
configuration_hash[:adapter]&.to_s
|
|
109
132
|
end
|
|
@@ -146,7 +169,7 @@ module ActiveRecord
|
|
|
146
169
|
#
|
|
147
170
|
# If the config option is set that will be used. Otherwise Rails will generate
|
|
148
171
|
# the filename from the database config name.
|
|
149
|
-
def schema_dump(format =
|
|
172
|
+
def schema_dump(format = schema_format)
|
|
150
173
|
if configuration_hash.key?(:schema_dump)
|
|
151
174
|
if config = configuration_hash[:schema_dump]
|
|
152
175
|
config
|
|
@@ -158,6 +181,12 @@ module ActiveRecord
|
|
|
158
181
|
end
|
|
159
182
|
end
|
|
160
183
|
|
|
184
|
+
def schema_format # :nodoc:
|
|
185
|
+
format = configuration_hash.fetch(:schema_format, ActiveRecord.schema_format).to_sym
|
|
186
|
+
raise "Invalid schema format" unless [:ruby, :sql].include?(format)
|
|
187
|
+
format
|
|
188
|
+
end
|
|
189
|
+
|
|
161
190
|
def database_tasks? # :nodoc:
|
|
162
191
|
!replica? && !!configuration_hash.fetch(:database_tasks, true)
|
|
163
192
|
end
|
|
@@ -168,13 +197,34 @@ module ActiveRecord
|
|
|
168
197
|
|
|
169
198
|
private
|
|
170
199
|
def schema_file_type(format)
|
|
171
|
-
case format
|
|
200
|
+
case format.to_sym
|
|
172
201
|
when :ruby
|
|
173
202
|
"schema.rb"
|
|
174
203
|
when :sql
|
|
175
204
|
"structure.sql"
|
|
176
205
|
end
|
|
177
206
|
end
|
|
207
|
+
|
|
208
|
+
def default_reaping_frequency
|
|
209
|
+
# Reap every 20 seconds by default, but run more often as necessary to
|
|
210
|
+
# meet other configured timeouts.
|
|
211
|
+
[20, idle_timeout, max_age, keepalive].compact.min
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def validate_configuration!
|
|
215
|
+
if configuration_hash[:pool] && configuration_hash[:max_connections]
|
|
216
|
+
pool_val = configuration_hash[:pool].to_i
|
|
217
|
+
max_conn_val = configuration_hash[:max_connections].to_i
|
|
218
|
+
|
|
219
|
+
if pool_val != max_conn_val
|
|
220
|
+
raise "Ambiguous configuration: 'pool' (#{pool_val}) and 'max_connections' (#{max_conn_val}) are set to different values. Prefer just 'max_connections'."
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if configuration_hash[:pool] && configuration_hash[:min_connections]
|
|
225
|
+
raise "Ambiguous configuration: when setting 'min_connections', use 'max_connections' instead of 'pool'."
|
|
226
|
+
end
|
|
227
|
+
end
|
|
178
228
|
end
|
|
179
229
|
end
|
|
180
230
|
end
|
|
@@ -47,9 +47,8 @@ module ActiveRecord
|
|
|
47
47
|
@configuration_hash[:schema_dump] = false
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
end
|
|
50
|
+
query_cache = parse_query_cache
|
|
51
|
+
@configuration_hash[:query_cache] = query_cache unless query_cache.nil?
|
|
53
52
|
|
|
54
53
|
to_boolean!(@configuration_hash, :replica)
|
|
55
54
|
to_boolean!(@configuration_hash, :database_tasks)
|
|
@@ -58,6 +57,17 @@ module ActiveRecord
|
|
|
58
57
|
end
|
|
59
58
|
|
|
60
59
|
private
|
|
60
|
+
def parse_query_cache
|
|
61
|
+
case value = @configuration_hash[:query_cache]
|
|
62
|
+
when /\A\d+\z/
|
|
63
|
+
value.to_i
|
|
64
|
+
when "false"
|
|
65
|
+
false
|
|
66
|
+
else
|
|
67
|
+
value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
61
71
|
def to_boolean!(configuration_hash, key)
|
|
62
72
|
if configuration_hash[key].is_a?(String)
|
|
63
73
|
configuration_hash[key] = configuration_hash[key] != "false"
|
|
@@ -36,9 +36,11 @@ module ActiveRecord
|
|
|
36
36
|
# to respond to `sharded?`. To implement this define the following in an
|
|
37
37
|
# initializer:
|
|
38
38
|
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
39
|
+
# ActiveSupport.on_load(:active_record_database_configurations) do
|
|
40
|
+
# ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
|
|
41
|
+
# next unless config.key?(:vitess)
|
|
42
|
+
# VitessConfig.new(env_name, name, config)
|
|
43
|
+
# end
|
|
42
44
|
# end
|
|
43
45
|
#
|
|
44
46
|
# Note: applications must handle the condition in which custom config should be
|
|
@@ -306,4 +308,6 @@ module ActiveRecord
|
|
|
306
308
|
url
|
|
307
309
|
end
|
|
308
310
|
end
|
|
311
|
+
|
|
312
|
+
ActiveSupport.run_load_hooks(:active_record_database_configurations, DatabaseConfigurations)
|
|
309
313
|
end
|
|
@@ -202,7 +202,7 @@ module ActiveRecord
|
|
|
202
202
|
# @entry.access_notice_message
|
|
203
203
|
# @entry.access_notice_message?
|
|
204
204
|
#
|
|
205
|
-
#
|
|
205
|
+
# ==== Options
|
|
206
206
|
#
|
|
207
207
|
# The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc.
|
|
208
208
|
# The following options can be included to specialize the behavior of the delegated type convenience methods.
|
|
@@ -229,7 +229,7 @@ module ActiveRecord
|
|
|
229
229
|
# @entry.message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
|
|
230
230
|
# @entry.comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil
|
|
231
231
|
def delegated_type(role, types:, **options)
|
|
232
|
-
belongs_to role, options.delete(:scope), **options
|
|
232
|
+
belongs_to role, options.delete(:scope), **options, polymorphic: true
|
|
233
233
|
define_delegated_type_methods role, types: types, options: options
|
|
234
234
|
end
|
|
235
235
|
|
|
@@ -7,16 +7,18 @@ module ActiveRecord
|
|
|
7
7
|
if self == Base
|
|
8
8
|
super
|
|
9
9
|
else
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
super || begin
|
|
11
|
+
match = Method.match(name)
|
|
12
|
+
match && match.valid?(self, name)
|
|
13
|
+
end
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def method_missing(name, ...)
|
|
16
|
-
match = Method.match(
|
|
18
|
+
match = Method.match(name)
|
|
17
19
|
|
|
18
|
-
if match && match.valid?
|
|
19
|
-
match.define
|
|
20
|
+
if match && match.valid?(self, name)
|
|
21
|
+
match.define(self, name)
|
|
20
22
|
send(name, ...)
|
|
21
23
|
else
|
|
22
24
|
super
|
|
@@ -24,97 +26,80 @@ module ActiveRecord
|
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
class Method
|
|
27
|
-
@matchers = []
|
|
28
|
-
|
|
29
29
|
class << self
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def match(model, name)
|
|
33
|
-
klass = matchers.find { |k| k.pattern.match?(name) }
|
|
34
|
-
klass.new(model, name) if klass
|
|
30
|
+
def match(name)
|
|
31
|
+
FindBy.match?(name) || FindByBang.match?(name)
|
|
35
32
|
end
|
|
36
33
|
|
|
37
|
-
def
|
|
38
|
-
|
|
34
|
+
def valid?(model, name)
|
|
35
|
+
attribute_names(model, name.to_s).all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
|
|
39
36
|
end
|
|
40
37
|
|
|
41
|
-
def
|
|
42
|
-
|
|
38
|
+
def define(model, name)
|
|
39
|
+
model.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
|
40
|
+
def self.#{name}(#{signature(model, name)})
|
|
41
|
+
#{body(model, name)}
|
|
42
|
+
end
|
|
43
|
+
CODE
|
|
43
44
|
end
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
private
|
|
47
|
+
def make_pattern(prefix, suffix)
|
|
48
|
+
/\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
|
|
49
|
+
end
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
def attribute_names(model, name)
|
|
52
|
+
attribute_names = name.match(pattern)[1].split("_and_")
|
|
53
|
+
attribute_names.map! { |name| model.attribute_aliases[name] || name }
|
|
54
|
+
end
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@attribute_names = @name.match(self.class.pattern)[1].split("_and_")
|
|
56
|
-
@attribute_names.map! { |name| @model.attribute_aliases[name] || name }
|
|
57
|
-
end
|
|
56
|
+
def body(model, method_name)
|
|
57
|
+
"#{finder}(#{attributes_hash(model, method_name)})"
|
|
58
|
+
end
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
# The parameters in the signature may have reserved Ruby words, in order
|
|
61
|
+
# to prevent errors, we start each param name with `_`.
|
|
62
|
+
def signature(model, method_name)
|
|
63
|
+
attribute_names(model, method_name.to_s).map { |name| "_#{name}" }.join(", ")
|
|
64
|
+
end
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def
|
|
66
|
-
#{
|
|
66
|
+
# Given that the parameters starts with `_`, the finder needs to use the
|
|
67
|
+
# same parameter name.
|
|
68
|
+
def attributes_hash(model, method_name)
|
|
69
|
+
"{" + attribute_names(model, method_name).map { |name| ":#{name} => _#{name}" }.join(",") + "}"
|
|
67
70
|
end
|
|
68
|
-
CODE
|
|
69
71
|
end
|
|
72
|
+
end
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"#{finder}(#{attributes_hash})"
|
|
74
|
-
end
|
|
74
|
+
class FindBy < Method
|
|
75
|
+
@pattern = make_pattern("find_by", "")
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def signature
|
|
79
|
-
attribute_names.map { |name| "_#{name}" }.join(", ")
|
|
80
|
-
end
|
|
77
|
+
class << self
|
|
78
|
+
attr_reader :pattern
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def attributes_hash
|
|
85
|
-
"{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(",") + "}"
|
|
80
|
+
def match?(name)
|
|
81
|
+
pattern.match?(name) && self
|
|
86
82
|
end
|
|
87
83
|
|
|
88
84
|
def finder
|
|
89
|
-
|
|
85
|
+
"find_by"
|
|
90
86
|
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
class FindBy < Method
|
|
94
|
-
Method.matchers << self
|
|
95
|
-
|
|
96
|
-
def self.prefix
|
|
97
|
-
"find_by"
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def finder
|
|
101
|
-
"find_by"
|
|
102
87
|
end
|
|
103
88
|
end
|
|
104
89
|
|
|
105
90
|
class FindByBang < Method
|
|
106
|
-
|
|
91
|
+
@pattern = make_pattern("find_by", "!")
|
|
107
92
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
end
|
|
93
|
+
class << self
|
|
94
|
+
attr_reader :pattern
|
|
111
95
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
96
|
+
def match?(name)
|
|
97
|
+
pattern.match?(name) && self
|
|
98
|
+
end
|
|
115
99
|
|
|
116
|
-
|
|
117
|
-
|
|
100
|
+
def finder
|
|
101
|
+
"find_by!"
|
|
102
|
+
end
|
|
118
103
|
end
|
|
119
104
|
end
|
|
120
105
|
end
|
|
@@ -16,7 +16,7 @@ module ActiveRecord
|
|
|
16
16
|
class_methods do
|
|
17
17
|
# Encrypts the +name+ attribute.
|
|
18
18
|
#
|
|
19
|
-
#
|
|
19
|
+
# ==== Options
|
|
20
20
|
#
|
|
21
21
|
# * <tt>:key_provider</tt> - A key provider to provide encryption and decryption keys. Defaults to
|
|
22
22
|
# +ActiveRecord::Encryption.key_provider+.
|
|
@@ -30,10 +30,10 @@ module ActiveRecord
|
|
|
30
30
|
# will use the oldest encryption scheme to encrypt new data by default. You can change this by setting
|
|
31
31
|
# <tt>deterministic: { fixed: false }</tt>. That will make it use the newest encryption scheme for encrypting new
|
|
32
32
|
# data.
|
|
33
|
-
# * <tt>:support_unencrypted_data</tt> -
|
|
34
|
-
#
|
|
35
|
-
# scenarios where you encrypt one column, and want to disable support for unencrypted data
|
|
36
|
-
# the global setting.
|
|
33
|
+
# * <tt>:support_unencrypted_data</tt> - When true, unencrypted data can be read normally. When false, it will raise errors.
|
|
34
|
+
# Falls back to +config.active_record.encryption.support_unencrypted_data+ if no value is provided.
|
|
35
|
+
# This is useful for scenarios where you encrypt one column, and want to disable support for unencrypted data
|
|
36
|
+
# without having to tweak the global setting.
|
|
37
37
|
# * <tt>:downcase</tt> - When true, it converts the encrypted content to downcase automatically. This allows to
|
|
38
38
|
# effectively ignore case when querying data. Notice that the case is lost. Use +:ignore_case+ if you are interested
|
|
39
39
|
# in preserving it.
|
|
@@ -15,7 +15,7 @@ module ActiveRecord
|
|
|
15
15
|
delegate :key_provider, :downcase?, :deterministic?, :previous_schemes, :with_context, :fixed?, to: :scheme
|
|
16
16
|
delegate :accessor, :type, to: :cast_type
|
|
17
17
|
|
|
18
|
-
#
|
|
18
|
+
# ==== Options
|
|
19
19
|
#
|
|
20
20
|
# * <tt>:scheme</tt> - A +Scheme+ with the encryption properties for this attribute.
|
|
21
21
|
# * <tt>:cast_type</tt> - A type that will be used to serialize (before encrypting) and deserialize
|
|
@@ -59,7 +59,7 @@ module ActiveRecord
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def support_unencrypted_data?
|
|
62
|
-
|
|
62
|
+
scheme.support_unencrypted_data? && !previous_type?
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
private
|
|
@@ -11,41 +11,43 @@ module ActiveRecord
|
|
|
11
11
|
# It interacts with a KeyProvider for getting the keys, and delegate to
|
|
12
12
|
# ActiveRecord::Encryption::Cipher the actual encryption algorithm.
|
|
13
13
|
class Encryptor
|
|
14
|
-
# The compressor to use for compressing the payload
|
|
14
|
+
# The compressor to use for compressing the payload.
|
|
15
15
|
attr_reader :compressor
|
|
16
16
|
|
|
17
|
-
#
|
|
17
|
+
# ==== Options
|
|
18
18
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
19
|
+
# [+:compress+]
|
|
20
|
+
# Boolean indicating whether records should be compressed before
|
|
21
|
+
# encryption. Defaults to +true+.
|
|
22
|
+
#
|
|
23
|
+
# [+:compressor+]
|
|
24
|
+
# The compressor to use. It must respond to +deflate+ and +inflate+.
|
|
25
|
+
# If not provided, will default to +ActiveRecord::Encryption.config.compressor+,
|
|
26
|
+
# which itself defaults to +Zlib+.
|
|
25
27
|
def initialize(compress: true, compressor: nil)
|
|
26
28
|
@compress = compress
|
|
27
29
|
@compressor = compressor || ActiveRecord::Encryption.config.compressor
|
|
28
30
|
end
|
|
29
31
|
|
|
30
|
-
# Encrypts +clean_text+ and returns the encrypted result
|
|
32
|
+
# Encrypts +clean_text+ and returns the encrypted result.
|
|
31
33
|
#
|
|
32
34
|
# Internally, it will:
|
|
33
35
|
#
|
|
34
|
-
# 1. Create a new ActiveRecord::Encryption::Message
|
|
35
|
-
# 2. Compress and encrypt +clean_text+ as the message payload
|
|
36
|
-
# 3. Serialize it with +ActiveRecord::Encryption.message_serializer+
|
|
37
|
-
# by default)
|
|
38
|
-
# 4. Encode the result with
|
|
36
|
+
# 1. Create a new ActiveRecord::Encryption::Message.
|
|
37
|
+
# 2. Compress and encrypt +clean_text+ as the message payload.
|
|
38
|
+
# 3. Serialize it with +ActiveRecord::Encryption.message_serializer+
|
|
39
|
+
# (+ActiveRecord::Encryption::SafeMarshal+ by default).
|
|
40
|
+
# 4. Encode the result with Base64.
|
|
39
41
|
#
|
|
40
|
-
#
|
|
42
|
+
# ==== Options
|
|
41
43
|
#
|
|
42
|
-
# [
|
|
44
|
+
# [+:key_provider+]
|
|
43
45
|
# Key provider to use for the encryption operation. It will default to
|
|
44
46
|
# +ActiveRecord::Encryption.key_provider+ when not provided.
|
|
45
47
|
#
|
|
46
|
-
# [
|
|
48
|
+
# [+:cipher_options+]
|
|
47
49
|
# Cipher-specific options that will be passed to the Cipher configured in
|
|
48
|
-
# +ActiveRecord::Encryption.cipher
|
|
50
|
+
# +ActiveRecord::Encryption.cipher+.
|
|
49
51
|
def encrypt(clear_text, key_provider: default_key_provider, cipher_options: {})
|
|
50
52
|
clear_text = force_encoding_if_needed(clear_text) if cipher_options[:deterministic]
|
|
51
53
|
|
|
@@ -53,17 +55,17 @@ module ActiveRecord
|
|
|
53
55
|
serialize_message build_encrypted_message(clear_text, key_provider: key_provider, cipher_options: cipher_options)
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
# Decrypts an +encrypted_text+ and returns the result as clean text
|
|
58
|
+
# Decrypts an +encrypted_text+ and returns the result as clean text.
|
|
57
59
|
#
|
|
58
|
-
#
|
|
60
|
+
# ==== Options
|
|
59
61
|
#
|
|
60
|
-
# [
|
|
62
|
+
# [+:key_provider+]
|
|
61
63
|
# Key provider to use for the encryption operation. It will default to
|
|
62
|
-
# +ActiveRecord::Encryption.key_provider+ when not provided
|
|
64
|
+
# +ActiveRecord::Encryption.key_provider+ when not provided.
|
|
63
65
|
#
|
|
64
|
-
# [
|
|
66
|
+
# [+:cipher_options+]
|
|
65
67
|
# Cipher-specific options that will be passed to the Cipher configured in
|
|
66
|
-
# +ActiveRecord::Encryption.cipher
|
|
68
|
+
# +ActiveRecord::Encryption.cipher+.
|
|
67
69
|
def decrypt(encrypted_text, key_provider: default_key_provider, cipher_options: {})
|
|
68
70
|
message = deserialize_message(encrypted_text)
|
|
69
71
|
keys = key_provider.decryption_keys(message)
|
|
@@ -73,7 +75,7 @@ module ActiveRecord
|
|
|
73
75
|
raise Errors::Decryption
|
|
74
76
|
end
|
|
75
77
|
|
|
76
|
-
# Returns whether the text is encrypted or not
|
|
78
|
+
# Returns whether the text is encrypted or not.
|
|
77
79
|
def encrypted?(text)
|
|
78
80
|
deserialize_message(text)
|
|
79
81
|
true
|
|
@@ -92,6 +94,18 @@ module ActiveRecord
|
|
|
92
94
|
private
|
|
93
95
|
DECRYPT_ERRORS = [OpenSSL::Cipher::CipherError, Errors::EncryptedContentIntegrity, Errors::Decryption]
|
|
94
96
|
ENCODING_ERRORS = [EncodingError, Errors::Encoding]
|
|
97
|
+
|
|
98
|
+
# This threshold cannot be changed.
|
|
99
|
+
#
|
|
100
|
+
# Users can search for attributes encrypted with `deterministic: true`.
|
|
101
|
+
# That is possible because we are able to generate the message for the
|
|
102
|
+
# given clear text deterministically, and with that perform a regular
|
|
103
|
+
# string lookup in SQL.
|
|
104
|
+
#
|
|
105
|
+
# Problem is, messages may have a "c" header that is present or not
|
|
106
|
+
# depending on whether compression was applied on encryption. If this
|
|
107
|
+
# threshold was modified, the message generated for lookup could vary
|
|
108
|
+
# for the same clear text, and searches on exisiting data could fail.
|
|
95
109
|
THRESHOLD_TO_JUSTIFY_COMPRESSION = 140.bytes
|
|
96
110
|
|
|
97
111
|
def default_key_provider
|
data/lib/active_record/enum.rb
CHANGED
|
@@ -119,7 +119,18 @@ module ActiveRecord
|
|
|
119
119
|
# enum :status, [ :active, :archived ], instance_methods: false
|
|
120
120
|
# end
|
|
121
121
|
#
|
|
122
|
-
#
|
|
122
|
+
# By default, an +ArgumentError+ will be raised when assigning an invalid value:
|
|
123
|
+
#
|
|
124
|
+
# class Conversation < ActiveRecord::Base
|
|
125
|
+
# enum :status, [ :active, :archived ]
|
|
126
|
+
# end
|
|
127
|
+
#
|
|
128
|
+
# conversation = Conversation.new
|
|
129
|
+
#
|
|
130
|
+
# conversation.status = :unknown # 'unknown' is not a valid status (ArgumentError)
|
|
131
|
+
#
|
|
132
|
+
# If, instead, you want the enum value to be validated before saving, use the
|
|
133
|
+
# +:validate+ option:
|
|
123
134
|
#
|
|
124
135
|
# class Conversation < ActiveRecord::Base
|
|
125
136
|
# enum :status, [ :active, :archived ], validate: true
|
|
@@ -136,7 +147,7 @@ module ActiveRecord
|
|
|
136
147
|
# conversation.status = :active
|
|
137
148
|
# conversation.valid? # => true
|
|
138
149
|
#
|
|
139
|
-
#
|
|
150
|
+
# You may also pass additional validation options:
|
|
140
151
|
#
|
|
141
152
|
# class Conversation < ActiveRecord::Base
|
|
142
153
|
# enum :status, [ :active, :archived ], validate: { allow_nil: true }
|
|
@@ -152,16 +163,6 @@ module ActiveRecord
|
|
|
152
163
|
#
|
|
153
164
|
# conversation.status = :active
|
|
154
165
|
# conversation.valid? # => true
|
|
155
|
-
#
|
|
156
|
-
# Otherwise +ArgumentError+ will raise:
|
|
157
|
-
#
|
|
158
|
-
# class Conversation < ActiveRecord::Base
|
|
159
|
-
# enum :status, [ :active, :archived ]
|
|
160
|
-
# end
|
|
161
|
-
#
|
|
162
|
-
# conversation = Conversation.new
|
|
163
|
-
#
|
|
164
|
-
# conversation.status = :unknown # 'unknown' is not a valid status (ArgumentError)
|
|
165
166
|
module Enum
|
|
166
167
|
def self.extended(base) # :nodoc:
|
|
167
168
|
base.class_attribute(:defined_enums, instance_writer: false, default: {})
|
|
@@ -220,7 +221,7 @@ module ActiveRecord
|
|
|
220
221
|
|
|
221
222
|
private
|
|
222
223
|
def _enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods: true, validate: false, **options)
|
|
223
|
-
assert_valid_enum_definition_values(values)
|
|
224
|
+
values = assert_valid_enum_definition_values(values)
|
|
224
225
|
assert_valid_enum_options(options)
|
|
225
226
|
|
|
226
227
|
# statuses = { }
|
|
@@ -341,6 +342,20 @@ module ActiveRecord
|
|
|
341
342
|
if values.keys.any?(&:blank?)
|
|
342
343
|
raise ArgumentError, "Enum values #{values} must not contain a blank name."
|
|
343
344
|
end
|
|
345
|
+
|
|
346
|
+
values = values.transform_values do |value|
|
|
347
|
+
value.is_a?(Symbol) ? value.name : value
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
values.each_value do |value|
|
|
351
|
+
case value
|
|
352
|
+
when String, Integer, true, false, nil
|
|
353
|
+
# noop
|
|
354
|
+
else
|
|
355
|
+
raise ArgumentError, "Enum values #{values} must be only booleans, integers, symbols or strings, got: #{value.class}"
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
344
359
|
when Array
|
|
345
360
|
if values.empty?
|
|
346
361
|
raise ArgumentError, "Enum values #{values} must not be empty."
|
|
@@ -356,6 +371,8 @@ module ActiveRecord
|
|
|
356
371
|
else
|
|
357
372
|
raise ArgumentError, "Enum values #{values} must be either a non-empty hash or an array."
|
|
358
373
|
end
|
|
374
|
+
|
|
375
|
+
values
|
|
359
376
|
end
|
|
360
377
|
|
|
361
378
|
def assert_valid_enum_options(options)
|
|
@@ -367,25 +384,25 @@ module ActiveRecord
|
|
|
367
384
|
|
|
368
385
|
ENUM_CONFLICT_MESSAGE = \
|
|
369
386
|
"You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
|
|
370
|
-
"this will generate
|
|
387
|
+
"this will generate %{type} method \"%{method}\", which is already defined " \
|
|
371
388
|
"by %{source}."
|
|
372
389
|
private_constant :ENUM_CONFLICT_MESSAGE
|
|
373
390
|
|
|
374
391
|
def detect_enum_conflict!(enum_name, method_name, klass_method = false)
|
|
375
392
|
if klass_method && dangerous_class_method?(method_name)
|
|
376
|
-
raise_conflict_error(enum_name, method_name,
|
|
393
|
+
raise_conflict_error(enum_name, method_name, "a class")
|
|
377
394
|
elsif klass_method && method_defined_within?(method_name, Relation)
|
|
378
|
-
raise_conflict_error(enum_name, method_name,
|
|
395
|
+
raise_conflict_error(enum_name, method_name, "a class", source: Relation.name)
|
|
379
396
|
elsif klass_method && method_name.to_sym == :id
|
|
380
|
-
raise_conflict_error(enum_name, method_name)
|
|
397
|
+
raise_conflict_error(enum_name, method_name, "an instance")
|
|
381
398
|
elsif !klass_method && dangerous_attribute_method?(method_name)
|
|
382
|
-
raise_conflict_error(enum_name, method_name)
|
|
399
|
+
raise_conflict_error(enum_name, method_name, "an instance")
|
|
383
400
|
elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
|
|
384
|
-
raise_conflict_error(enum_name, method_name, source: "another enum")
|
|
401
|
+
raise_conflict_error(enum_name, method_name, "an instance", source: "another enum")
|
|
385
402
|
end
|
|
386
403
|
end
|
|
387
404
|
|
|
388
|
-
def raise_conflict_error(enum_name, method_name, type
|
|
405
|
+
def raise_conflict_error(enum_name, method_name, type, source: "Active Record")
|
|
389
406
|
raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
|
|
390
407
|
enum: enum_name,
|
|
391
408
|
klass: name,
|