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.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +555 -388
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/association_relation.rb +1 -1
  5. data/lib/active_record/associations/association.rb +1 -1
  6. data/lib/active_record/associations/belongs_to_association.rb +11 -1
  7. data/lib/active_record/associations/builder/association.rb +16 -5
  8. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  9. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  10. data/lib/active_record/associations/builder/has_one.rb +1 -1
  11. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  12. data/lib/active_record/associations/collection_association.rb +3 -3
  13. data/lib/active_record/associations/collection_proxy.rb +22 -4
  14. data/lib/active_record/associations/deprecation.rb +88 -0
  15. data/lib/active_record/associations/errors.rb +3 -0
  16. data/lib/active_record/associations/join_dependency.rb +2 -0
  17. data/lib/active_record/associations/preloader/branch.rb +1 -0
  18. data/lib/active_record/associations.rb +159 -21
  19. data/lib/active_record/attribute_methods/query.rb +34 -0
  20. data/lib/active_record/attribute_methods/serialization.rb +17 -4
  21. data/lib/active_record/attribute_methods/time_zone_conversion.rb +10 -2
  22. data/lib/active_record/attribute_methods.rb +1 -1
  23. data/lib/active_record/attributes.rb +38 -24
  24. data/lib/active_record/autosave_association.rb +2 -2
  25. data/lib/active_record/base.rb +2 -4
  26. data/lib/active_record/coders/json.rb +14 -5
  27. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +2 -4
  28. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -3
  29. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -12
  30. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +413 -76
  31. data/lib/active_record/connection_adapters/abstract/database_statements.rb +55 -40
  32. data/lib/active_record/connection_adapters/abstract/query_cache.rb +36 -9
  33. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  34. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  35. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +26 -34
  36. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  37. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +89 -23
  38. data/lib/active_record/connection_adapters/abstract/transaction.rb +25 -3
  39. data/lib/active_record/connection_adapters/abstract_adapter.rb +89 -21
  40. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +43 -13
  41. data/lib/active_record/connection_adapters/column.rb +17 -4
  42. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  43. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  44. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  45. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +26 -4
  46. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +27 -22
  47. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -2
  48. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
  49. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +26 -17
  50. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  51. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  52. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  53. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +8 -6
  54. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +8 -21
  55. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +67 -31
  56. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +81 -48
  57. data/lib/active_record/connection_adapters/postgresql_adapter.rb +25 -9
  58. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  59. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +39 -27
  60. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  61. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -13
  62. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +58 -34
  63. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
  64. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
  65. data/lib/active_record/connection_adapters.rb +1 -0
  66. data/lib/active_record/connection_handling.rb +15 -10
  67. data/lib/active_record/core.rb +13 -10
  68. data/lib/active_record/counter_cache.rb +33 -8
  69. data/lib/active_record/database_configurations/database_config.rb +5 -1
  70. data/lib/active_record/database_configurations/hash_config.rb +59 -9
  71. data/lib/active_record/database_configurations/url_config.rb +13 -3
  72. data/lib/active_record/database_configurations.rb +7 -3
  73. data/lib/active_record/delegated_type.rb +2 -2
  74. data/lib/active_record/dynamic_matchers.rb +54 -69
  75. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  76. data/lib/active_record/encryption/encrypted_attribute_type.rb +2 -2
  77. data/lib/active_record/encryption/encryptor.rb +39 -25
  78. data/lib/active_record/encryption/scheme.rb +1 -1
  79. data/lib/active_record/enum.rb +37 -20
  80. data/lib/active_record/errors.rb +23 -7
  81. data/lib/active_record/explain.rb +1 -1
  82. data/lib/active_record/explain_registry.rb +51 -2
  83. data/lib/active_record/filter_attribute_handler.rb +73 -0
  84. data/lib/active_record/fixture_set/table_row.rb +19 -2
  85. data/lib/active_record/fixtures.rb +2 -2
  86. data/lib/active_record/gem_version.rb +2 -2
  87. data/lib/active_record/inheritance.rb +1 -1
  88. data/lib/active_record/insert_all.rb +12 -7
  89. data/lib/active_record/locking/optimistic.rb +7 -0
  90. data/lib/active_record/locking/pessimistic.rb +5 -0
  91. data/lib/active_record/log_subscriber.rb +2 -6
  92. data/lib/active_record/middleware/shard_selector.rb +34 -17
  93. data/lib/active_record/migration/command_recorder.rb +14 -1
  94. data/lib/active_record/migration/compatibility.rb +34 -24
  95. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  96. data/lib/active_record/migration.rb +31 -21
  97. data/lib/active_record/model_schema.rb +36 -10
  98. data/lib/active_record/nested_attributes.rb +2 -0
  99. data/lib/active_record/persistence.rb +34 -3
  100. data/lib/active_record/query_cache.rb +22 -15
  101. data/lib/active_record/query_logs.rb +7 -7
  102. data/lib/active_record/querying.rb +4 -4
  103. data/lib/active_record/railtie.rb +34 -5
  104. data/lib/active_record/railties/controller_runtime.rb +11 -6
  105. data/lib/active_record/railties/databases.rake +23 -19
  106. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  107. data/lib/active_record/railties/job_runtime.rb +10 -11
  108. data/lib/active_record/reflection.rb +35 -0
  109. data/lib/active_record/relation/batches.rb +25 -11
  110. data/lib/active_record/relation/calculations.rb +35 -25
  111. data/lib/active_record/relation/delegation.rb +0 -1
  112. data/lib/active_record/relation/finder_methods.rb +41 -24
  113. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -9
  114. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +7 -7
  115. data/lib/active_record/relation/predicate_builder.rb +9 -7
  116. data/lib/active_record/relation/query_attribute.rb +3 -1
  117. data/lib/active_record/relation/query_methods.rb +44 -33
  118. data/lib/active_record/relation/spawn_methods.rb +6 -6
  119. data/lib/active_record/relation/where_clause.rb +9 -10
  120. data/lib/active_record/relation.rb +38 -16
  121. data/lib/active_record/result.rb +44 -21
  122. data/lib/active_record/runtime_registry.rb +41 -58
  123. data/lib/active_record/sanitization.rb +2 -0
  124. data/lib/active_record/schema_dumper.rb +12 -10
  125. data/lib/active_record/scoping.rb +0 -1
  126. data/lib/active_record/secure_token.rb +3 -3
  127. data/lib/active_record/signed_id.rb +46 -18
  128. data/lib/active_record/statement_cache.rb +13 -9
  129. data/lib/active_record/store.rb +44 -19
  130. data/lib/active_record/structured_event_subscriber.rb +85 -0
  131. data/lib/active_record/table_metadata.rb +5 -20
  132. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  133. data/lib/active_record/tasks/database_tasks.rb +44 -45
  134. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
  135. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  136. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  137. data/lib/active_record/test_databases.rb +14 -4
  138. data/lib/active_record/test_fixtures.rb +27 -2
  139. data/lib/active_record/testing/query_assertions.rb +8 -2
  140. data/lib/active_record/timestamp.rb +4 -2
  141. data/lib/active_record/transaction.rb +2 -5
  142. data/lib/active_record/transactions.rb +34 -10
  143. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  144. data/lib/active_record/type/internal/timezone.rb +7 -0
  145. data/lib/active_record/type/json.rb +15 -2
  146. data/lib/active_record/type/serialized.rb +11 -4
  147. data/lib/active_record/type/type_map.rb +1 -1
  148. data/lib/active_record/type_caster/connection.rb +2 -1
  149. data/lib/active_record/validations/associated.rb +1 -1
  150. data/lib/active_record.rb +68 -5
  151. data/lib/arel/alias_predication.rb +2 -0
  152. data/lib/arel/crud.rb +8 -11
  153. data/lib/arel/delete_manager.rb +5 -0
  154. data/lib/arel/nodes/count.rb +2 -2
  155. data/lib/arel/nodes/delete_statement.rb +4 -2
  156. data/lib/arel/nodes/function.rb +4 -10
  157. data/lib/arel/nodes/named_function.rb +2 -2
  158. data/lib/arel/nodes/node.rb +1 -1
  159. data/lib/arel/nodes/update_statement.rb +4 -2
  160. data/lib/arel/nodes.rb +0 -2
  161. data/lib/arel/select_manager.rb +13 -4
  162. data/lib/arel/update_manager.rb +5 -0
  163. data/lib/arel/visitors/dot.rb +2 -3
  164. data/lib/arel/visitors/postgresql.rb +55 -0
  165. data/lib/arel/visitors/sqlite.rb +55 -8
  166. data/lib/arel/visitors/to_sql.rb +5 -21
  167. data/lib/arel.rb +3 -1
  168. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  169. metadata +16 -12
  170. data/lib/active_record/explain_subscriber.rb +0 -34
  171. 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 pool
73
- (configuration_hash[:pool] || 5).to_i
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] || pool).to_i
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
- # `reaping_frequency` is configurable mostly for historical reasons, but it
97
- # could also be useful if someone wants a very low `idle_timeout`.
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 = ActiveRecord.schema_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
- if @configuration_hash[:query_cache] == "false"
51
- @configuration_hash[:query_cache] = false
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
- # ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
40
- # next unless config.key?(:vitess)
41
- # VitessConfig.new(env_name, name, config)
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
- # === Options
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.merge(polymorphic: true)
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
- match = Method.match(self, name)
11
- match && match.valid? || super
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(self, name)
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
- attr_reader :matchers
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 pattern
38
- @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
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 prefix
42
- raise NotImplementedError
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
- def suffix
46
- ""
47
- end
48
- end
46
+ private
47
+ def make_pattern(prefix, suffix)
48
+ /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
49
+ end
49
50
 
50
- attr_reader :model, :name, :attribute_names
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
- def initialize(model, method_name)
53
- @model = model
54
- @name = method_name.to_s
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
- def valid?
60
- attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
61
- end
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
- def define
64
- model.class_eval <<-CODE, __FILE__, __LINE__ + 1
65
- def self.#{name}(#{signature})
66
- #{body}
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
- private
72
- def body
73
- "#{finder}(#{attributes_hash})"
74
- end
74
+ class FindBy < Method
75
+ @pattern = make_pattern("find_by", "")
75
76
 
76
- # The parameters in the signature may have reserved Ruby words, in order
77
- # to prevent errors, we start each param name with `_`.
78
- def signature
79
- attribute_names.map { |name| "_#{name}" }.join(", ")
80
- end
77
+ class << self
78
+ attr_reader :pattern
81
79
 
82
- # Given that the parameters starts with `_`, the finder needs to use the
83
- # same parameter name.
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
- raise NotImplementedError
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
- Method.matchers << self
91
+ @pattern = make_pattern("find_by", "!")
107
92
 
108
- def self.prefix
109
- "find_by"
110
- end
93
+ class << self
94
+ attr_reader :pattern
111
95
 
112
- def self.suffix
113
- "!"
114
- end
96
+ def match?(name)
97
+ pattern.match?(name) && self
98
+ end
115
99
 
116
- def finder
117
- "find_by!"
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
- # === Options
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> - If `config.active_record.encryption.support_unencrypted_data` is +true+,
34
- # you can set this to +false+ to opt out of unencrypted data support for this attribute. This is useful for
35
- # scenarios where you encrypt one column, and want to disable support for unencrypted data without having to tweak
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
- # === Options
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
- ActiveRecord::Encryption.config.support_unencrypted_data && scheme.support_unencrypted_data? && !previous_type?
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
- # === Options
17
+ # ==== Options
18
18
  #
19
- # * <tt>:compress</tt> - Boolean indicating whether records should be compressed before encryption.
20
- # Defaults to +true+.
21
- # * <tt>:compressor</tt> - The compressor to use.
22
- # 1. If compressor is provided, it will be used.
23
- # 2. If not, it will use ActiveRecord::Encryption.config.compressor which default value is +Zlib+.
24
- # If you want to use a custom compressor, it must respond to +deflate+ and +inflate+.
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+ (+ActiveRecord::Encryption::SafeMarshal+
37
- # by default)
38
- # 4. Encode the result with Base 64
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
- # === Options
42
+ # ==== Options
41
43
  #
42
- # [:key_provider]
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
- # [:cipher_options]
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
- # === Options
60
+ # ==== Options
59
61
  #
60
- # [:key_provider]
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
- # [:cipher_options]
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
@@ -59,7 +59,7 @@ module ActiveRecord
59
59
  end
60
60
 
61
61
  def merge(other_scheme)
62
- self.class.new(**to_h.merge(other_scheme.to_h))
62
+ self.class.new(**to_h, **other_scheme.to_h)
63
63
  end
64
64
 
65
65
  def to_h
@@ -119,7 +119,18 @@ module ActiveRecord
119
119
  # enum :status, [ :active, :archived ], instance_methods: false
120
120
  # end
121
121
  #
122
- # If you want the enum value to be validated before saving, use the option +:validate+:
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
- # It is also possible to pass additional validation options:
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 a %{type} method \"%{method}\", which is already defined " \
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, type: "class")
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, type: "class", source: Relation.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: "instance", source: "Active Record")
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,