activerecord 8.0.3 → 8.1.0.beta1

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +427 -522
  3. data/README.rdoc +1 -1
  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/builder/association.rb +16 -5
  7. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  8. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  9. data/lib/active_record/associations/builder/has_one.rb +1 -1
  10. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  11. data/lib/active_record/associations/collection_proxy.rb +22 -4
  12. data/lib/active_record/associations/deprecation.rb +88 -0
  13. data/lib/active_record/associations/errors.rb +3 -0
  14. data/lib/active_record/associations/join_dependency.rb +2 -0
  15. data/lib/active_record/associations/preloader/branch.rb +1 -0
  16. data/lib/active_record/associations.rb +159 -21
  17. data/lib/active_record/attribute_methods/serialization.rb +16 -3
  18. data/lib/active_record/attribute_methods.rb +1 -1
  19. data/lib/active_record/attributes.rb +3 -0
  20. data/lib/active_record/autosave_association.rb +1 -1
  21. data/lib/active_record/base.rb +2 -3
  22. data/lib/active_record/coders/json.rb +14 -5
  23. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +1 -3
  24. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +15 -0
  25. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -12
  26. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +382 -51
  27. data/lib/active_record/connection_adapters/abstract/database_statements.rb +26 -30
  28. data/lib/active_record/connection_adapters/abstract/query_cache.rb +25 -18
  29. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  30. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  31. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +26 -34
  32. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  33. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +85 -22
  34. data/lib/active_record/connection_adapters/abstract/transaction.rb +16 -3
  35. data/lib/active_record/connection_adapters/abstract_adapter.rb +66 -14
  36. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +43 -11
  37. data/lib/active_record/connection_adapters/column.rb +17 -4
  38. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  39. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  40. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  41. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +26 -4
  42. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +27 -22
  43. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -0
  44. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +18 -23
  45. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  46. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  47. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  48. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -1
  49. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +8 -21
  50. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +65 -30
  51. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +74 -38
  52. data/lib/active_record/connection_adapters/postgresql_adapter.rb +10 -5
  53. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  54. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +37 -25
  55. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  56. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -13
  57. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +54 -30
  58. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
  59. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
  60. data/lib/active_record/connection_adapters.rb +1 -0
  61. data/lib/active_record/core.rb +5 -4
  62. data/lib/active_record/counter_cache.rb +33 -8
  63. data/lib/active_record/database_configurations/database_config.rb +5 -1
  64. data/lib/active_record/database_configurations/hash_config.rb +50 -9
  65. data/lib/active_record/database_configurations/url_config.rb +13 -3
  66. data/lib/active_record/database_configurations.rb +7 -3
  67. data/lib/active_record/delegated_type.rb +1 -1
  68. data/lib/active_record/dynamic_matchers.rb +54 -69
  69. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  70. data/lib/active_record/encryption/encrypted_attribute_type.rb +1 -1
  71. data/lib/active_record/encryption/scheme.rb +1 -1
  72. data/lib/active_record/enum.rb +24 -8
  73. data/lib/active_record/errors.rb +23 -7
  74. data/lib/active_record/explain_registry.rb +0 -1
  75. data/lib/active_record/filter_attribute_handler.rb +73 -0
  76. data/lib/active_record/fixtures.rb +2 -2
  77. data/lib/active_record/gem_version.rb +3 -3
  78. data/lib/active_record/inheritance.rb +1 -1
  79. data/lib/active_record/insert_all.rb +12 -7
  80. data/lib/active_record/locking/optimistic.rb +7 -0
  81. data/lib/active_record/locking/pessimistic.rb +5 -0
  82. data/lib/active_record/log_subscriber.rb +1 -5
  83. data/lib/active_record/middleware/shard_selector.rb +34 -17
  84. data/lib/active_record/migration/command_recorder.rb +14 -1
  85. data/lib/active_record/migration/compatibility.rb +34 -24
  86. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  87. data/lib/active_record/migration.rb +26 -16
  88. data/lib/active_record/model_schema.rb +10 -7
  89. data/lib/active_record/nested_attributes.rb +2 -0
  90. data/lib/active_record/persistence.rb +34 -3
  91. data/lib/active_record/query_cache.rb +22 -15
  92. data/lib/active_record/query_logs.rb +3 -7
  93. data/lib/active_record/railtie.rb +32 -3
  94. data/lib/active_record/railties/databases.rake +16 -4
  95. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  96. data/lib/active_record/railties/job_runtime.rb +10 -11
  97. data/lib/active_record/reflection.rb +42 -3
  98. data/lib/active_record/relation/batches.rb +26 -12
  99. data/lib/active_record/relation/calculations.rb +20 -9
  100. data/lib/active_record/relation/delegation.rb +0 -1
  101. data/lib/active_record/relation/finder_methods.rb +27 -11
  102. data/lib/active_record/relation/merger.rb +2 -2
  103. data/lib/active_record/relation/predicate_builder.rb +2 -2
  104. data/lib/active_record/relation/query_attribute.rb +3 -1
  105. data/lib/active_record/relation/query_methods.rb +39 -29
  106. data/lib/active_record/relation/where_clause.rb +1 -10
  107. data/lib/active_record/relation.rb +25 -13
  108. data/lib/active_record/result.rb +44 -21
  109. data/lib/active_record/sanitization.rb +2 -0
  110. data/lib/active_record/schema_dumper.rb +12 -10
  111. data/lib/active_record/scoping.rb +0 -1
  112. data/lib/active_record/signed_id.rb +43 -15
  113. data/lib/active_record/statement_cache.rb +13 -9
  114. data/lib/active_record/store.rb +44 -19
  115. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  116. data/lib/active_record/tasks/database_tasks.rb +2 -21
  117. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
  118. data/lib/active_record/tasks/postgresql_database_tasks.rb +5 -39
  119. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  120. data/lib/active_record/test_databases.rb +10 -2
  121. data/lib/active_record/test_fixtures.rb +27 -2
  122. data/lib/active_record/testing/query_assertions.rb +8 -2
  123. data/lib/active_record/timestamp.rb +4 -2
  124. data/lib/active_record/transaction.rb +2 -5
  125. data/lib/active_record/transactions.rb +32 -10
  126. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  127. data/lib/active_record/type/internal/timezone.rb +7 -0
  128. data/lib/active_record/type/json.rb +15 -2
  129. data/lib/active_record/type/serialized.rb +11 -4
  130. data/lib/active_record/type/type_map.rb +1 -1
  131. data/lib/active_record/type_caster/connection.rb +2 -1
  132. data/lib/active_record/validations/associated.rb +1 -1
  133. data/lib/active_record.rb +65 -3
  134. data/lib/arel/alias_predication.rb +2 -0
  135. data/lib/arel/crud.rb +6 -11
  136. data/lib/arel/nodes/count.rb +2 -2
  137. data/lib/arel/nodes/function.rb +4 -10
  138. data/lib/arel/nodes/named_function.rb +2 -2
  139. data/lib/arel/nodes/node.rb +1 -1
  140. data/lib/arel/nodes.rb +0 -2
  141. data/lib/arel/select_manager.rb +7 -2
  142. data/lib/arel/visitors/dot.rb +0 -3
  143. data/lib/arel/visitors/postgresql.rb +55 -0
  144. data/lib/arel/visitors/sqlite.rb +55 -8
  145. data/lib/arel/visitors/to_sql.rb +3 -21
  146. data/lib/arel.rb +3 -1
  147. metadata +13 -9
  148. data/lib/active_record/normalization.rb +0 -163
@@ -181,7 +181,7 @@ module ActiveRecord
181
181
  end
182
182
 
183
183
  case exception
184
- when ::Trilogy::ConnectionClosed, ::Trilogy::EOFError
184
+ when ::Trilogy::ConnectionClosed, ::Trilogy::EOFError, ::Trilogy::SSLError
185
185
  return ConnectionFailed.new(message, connection_pool: @pool)
186
186
  when ::Trilogy::Error
187
187
  if exception.is_a?(SystemCallError) || exception.message.include?("TRILOGY_INVALID_SEQUENCE_ID")
@@ -84,6 +84,7 @@ module ActiveRecord
84
84
  autoload_at "active_record/connection_adapters/abstract/schema_definitions" do
85
85
  autoload :IndexDefinition
86
86
  autoload :ColumnDefinition
87
+ autoload :ColumnMethods
87
88
  autoload :ChangeColumnDefinition
88
89
  autoload :ChangeColumnDefaultDefinition
89
90
  autoload :ForeignKeyDefinition
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/enumerable"
4
- require "active_support/core_ext/module/delegation"
5
4
  require "active_support/parameter_filter"
6
5
  require "concurrent/map"
7
6
 
@@ -112,7 +111,7 @@ module ActiveRecord
112
111
  # Post.attributes_for_inspect = [:id, :title]
113
112
  # Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!">"
114
113
  #
115
- # When set to `:all` inspect will list all the record's attributes:
114
+ # When set to +:all+ inspect will list all the record's attributes:
116
115
  #
117
116
  # Post.attributes_for_inspect = :all
118
117
  # Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
@@ -358,6 +357,8 @@ module ActiveRecord
358
357
  def filter_attributes=(filter_attributes)
359
358
  @inspection_filter = nil
360
359
  @filter_attributes = filter_attributes
360
+
361
+ FilterAttributeHandler.sensitive_attribute_was_declared(self, filter_attributes)
361
362
  end
362
363
 
363
364
  def inspection_filter # :nodoc:
@@ -451,7 +452,7 @@ module ActiveRecord
451
452
  where(wheres).limit(1)
452
453
  }
453
454
 
454
- statement.execute(values.flatten, connection, allow_retry: true).then do |r|
455
+ statement.execute(values.flatten, connection).then do |r|
455
456
  r.first
456
457
  rescue TypeError
457
458
  raise ActiveRecord::StatementInvalid
@@ -641,7 +642,7 @@ module ActiveRecord
641
642
  def hash
642
643
  id = self.id
643
644
 
644
- if primary_key_values_present?
645
+ if self.class.composite_primary_key? ? primary_key_values_present? : id
645
646
  self.class.hash ^ id.hash
646
647
  else
647
648
  super
@@ -17,7 +17,7 @@ module ActiveRecord
17
17
  #
18
18
  # ==== Parameters
19
19
  #
20
- # * +id+ - The id of the object you wish to reset a counter on.
20
+ # * +id+ - The id of the object you wish to reset a counter on or an array of ids.
21
21
  # * +counters+ - One or more association counters to reset. Association name or counter name can be given.
22
22
  # * <tt>:touch</tt> - Touch timestamp columns when updating.
23
23
  # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
@@ -28,13 +28,25 @@ module ActiveRecord
28
28
  # # For the Post with id #1, reset the comments_count
29
29
  # Post.reset_counters(1, :comments)
30
30
  #
31
+ # # For posts with ids #1 and #2, reset the comments_count
32
+ # Post.reset_counters([1, 2], :comments)
33
+ #
31
34
  # # Like above, but also touch the updated_at and/or updated_on
32
35
  # # attributes.
33
36
  # Post.reset_counters(1, :comments, touch: true)
34
37
  def reset_counters(id, *counters, touch: nil)
35
- object = find(id)
38
+ ids = if composite_primary_key?
39
+ if id.first.is_a?(Array)
40
+ id
41
+ else
42
+ [id]
43
+ end
44
+ else
45
+ Array(id)
46
+ end
47
+
48
+ updates = Hash.new { |h, k| h[k] = {} }
36
49
 
37
- updates = {}
38
50
  counters.each do |counter_association|
39
51
  has_many_association = _reflect_on_association(counter_association)
40
52
  unless has_many_association
@@ -48,14 +60,22 @@ module ActiveRecord
48
60
  has_many_association = has_many_association.through_reflection
49
61
  end
50
62
 
63
+ counter_association = counter_association.to_sym
51
64
  foreign_key = has_many_association.foreign_key.to_s
52
65
  child_class = has_many_association.klass
53
66
  reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
54
67
  counter_name = reflection.counter_cache_column
55
68
 
56
- count_was = object.send(counter_name)
57
- count = object.send(counter_association).count(:all)
58
- updates[counter_name] = count if count != count_was
69
+ counts =
70
+ unscoped
71
+ .joins(counter_association)
72
+ .where(primary_key => ids)
73
+ .group(primary_key)
74
+ .count(:all)
75
+
76
+ ids.each do |id|
77
+ updates[id].merge!(counter_name => counts[id] || 0)
78
+ end
59
79
  end
60
80
 
61
81
  if touch
@@ -63,10 +83,15 @@ module ActiveRecord
63
83
  names = Array.wrap(names)
64
84
  options = names.extract_options!
65
85
  touch_updates = touch_attributes_with_time(*names, **options)
66
- updates.merge!(touch_updates)
86
+
87
+ updates.each_value do |record_updates|
88
+ record_updates.merge!(touch_updates)
89
+ end
67
90
  end
68
91
 
69
- unscoped.where(primary_key => [object.id]).update_all(updates) if updates.any?
92
+ updates.each do |id, record_updates|
93
+ unscoped.where(primary_key => [id]).update_all(record_updates)
94
+ end
70
95
 
71
96
  true
72
97
  end
@@ -48,7 +48,11 @@ module ActiveRecord
48
48
  raise NotImplementedError
49
49
  end
50
50
 
51
- def pool
51
+ def min_connections
52
+ raise NotImplementedError
53
+ end
54
+
55
+ def max_connections
52
56
  raise NotImplementedError
53
57
  end
54
58
 
@@ -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,32 @@ 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
+ (configuration_hash[:max_connections] || configuration_hash[:pool] || 5).to_i
74
75
  end
75
76
 
77
+ def min_connections
78
+ (configuration_hash[:min_connections] || 0).to_i
79
+ end
80
+
81
+ alias :pool :max_connections
82
+ deprecate pool: :max_connections, deprecator: ActiveRecord.deprecator
83
+
76
84
  def min_threads
77
85
  (configuration_hash[:min_threads] || 0).to_i
78
86
  end
79
87
 
80
88
  def max_threads
81
- (configuration_hash[:max_threads] || pool).to_i
89
+ (configuration_hash[:max_threads] || max_connections).to_i
90
+ end
91
+
92
+ def max_age
93
+ v = configuration_hash[:max_age]&.to_i
94
+ if v && v > 0
95
+ v
96
+ else
97
+ Float::INFINITY
98
+ end
82
99
  end
83
100
 
84
101
  def query_cache
@@ -93,10 +110,8 @@ module ActiveRecord
93
110
  (configuration_hash[:checkout_timeout] || 5).to_f
94
111
  end
95
112
 
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
113
+ def reaping_frequency # :nodoc:
114
+ configuration_hash.fetch(:reaping_frequency, default_reaping_frequency)&.to_f
100
115
  end
101
116
 
102
117
  def idle_timeout
@@ -104,6 +119,11 @@ module ActiveRecord
104
119
  timeout if timeout > 0
105
120
  end
106
121
 
122
+ def keepalive
123
+ keepalive = (configuration_hash[:keepalive] || 600).to_f
124
+ keepalive if keepalive > 0
125
+ end
126
+
107
127
  def adapter
108
128
  configuration_hash[:adapter]&.to_s
109
129
  end
@@ -159,8 +179,8 @@ module ActiveRecord
159
179
  end
160
180
 
161
181
  def schema_format # :nodoc:
162
- format = configuration_hash[:schema_format]&.to_sym || ActiveRecord.schema_format
163
- raise "Invalid schema format" unless [ :ruby, :sql ].include? format
182
+ format = configuration_hash.fetch(:schema_format, ActiveRecord.schema_format).to_sym
183
+ raise "Invalid schema format" unless [:ruby, :sql].include?(format)
164
184
  format
165
185
  end
166
186
 
@@ -181,6 +201,27 @@ module ActiveRecord
181
201
  "structure.sql"
182
202
  end
183
203
  end
204
+
205
+ def default_reaping_frequency
206
+ # Reap every 20 seconds by default, but run more often as necessary to
207
+ # meet other configured timeouts.
208
+ [20, idle_timeout, max_age, keepalive].compact.min
209
+ end
210
+
211
+ def validate_configuration!
212
+ if configuration_hash[:pool] && configuration_hash[:max_connections]
213
+ pool_val = configuration_hash[:pool].to_i
214
+ max_conn_val = configuration_hash[:max_connections].to_i
215
+
216
+ if pool_val != max_conn_val
217
+ raise "Ambiguous configuration: 'pool' (#{pool_val}) and 'max_connections' (#{max_conn_val}) are set to different values. Prefer just 'max_connections'."
218
+ end
219
+ end
220
+
221
+ if configuration_hash[:pool] && configuration_hash[:min_connections]
222
+ raise "Ambiguous configuration: when setting 'min_connections', use 'max_connections' instead of 'pool'."
223
+ end
224
+ end
184
225
  end
185
226
  end
186
227
  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
@@ -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
@@ -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.
@@ -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
@@ -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
@@ -221,7 +221,7 @@ module ActiveRecord
221
221
 
222
222
  private
223
223
  def _enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods: true, validate: false, **options)
224
- assert_valid_enum_definition_values(values)
224
+ values = assert_valid_enum_definition_values(values)
225
225
  assert_valid_enum_options(options)
226
226
 
227
227
  # statuses = { }
@@ -342,6 +342,20 @@ module ActiveRecord
342
342
  if values.keys.any?(&:blank?)
343
343
  raise ArgumentError, "Enum values #{values} must not contain a blank name."
344
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
+
345
359
  when Array
346
360
  if values.empty?
347
361
  raise ArgumentError, "Enum values #{values} must not be empty."
@@ -357,6 +371,8 @@ module ActiveRecord
357
371
  else
358
372
  raise ArgumentError, "Enum values #{values} must be either a non-empty hash or an array."
359
373
  end
374
+
375
+ values
360
376
  end
361
377
 
362
378
  def assert_valid_enum_options(options)
@@ -368,25 +384,25 @@ module ActiveRecord
368
384
 
369
385
  ENUM_CONFLICT_MESSAGE = \
370
386
  "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
371
- "this will generate a %{type} method \"%{method}\", which is already defined " \
387
+ "this will generate %{type} method \"%{method}\", which is already defined " \
372
388
  "by %{source}."
373
389
  private_constant :ENUM_CONFLICT_MESSAGE
374
390
 
375
391
  def detect_enum_conflict!(enum_name, method_name, klass_method = false)
376
392
  if klass_method && dangerous_class_method?(method_name)
377
- raise_conflict_error(enum_name, method_name, type: "class")
393
+ raise_conflict_error(enum_name, method_name, "a class")
378
394
  elsif klass_method && method_defined_within?(method_name, Relation)
379
- 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)
380
396
  elsif klass_method && method_name.to_sym == :id
381
- raise_conflict_error(enum_name, method_name)
397
+ raise_conflict_error(enum_name, method_name, "an instance")
382
398
  elsif !klass_method && dangerous_attribute_method?(method_name)
383
- raise_conflict_error(enum_name, method_name)
399
+ raise_conflict_error(enum_name, method_name, "an instance")
384
400
  elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
385
- raise_conflict_error(enum_name, method_name, source: "another enum")
401
+ raise_conflict_error(enum_name, method_name, "an instance", source: "another enum")
386
402
  end
387
403
  end
388
404
 
389
- 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")
390
406
  raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
391
407
  enum: enum_name,
392
408
  klass: name,