activerecord 7.1.5.1 → 8.0.2

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 (206) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +369 -2484
  3. data/README.rdoc +15 -15
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +2 -1
  6. data/lib/active_record/associations/alias_tracker.rb +31 -23
  7. data/lib/active_record/associations/association.rb +43 -12
  8. data/lib/active_record/associations/belongs_to_association.rb +21 -8
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/association.rb +7 -6
  11. data/lib/active_record/associations/builder/belongs_to.rb +1 -0
  12. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  13. data/lib/active_record/associations/builder/has_many.rb +3 -4
  14. data/lib/active_record/associations/builder/has_one.rb +3 -4
  15. data/lib/active_record/associations/collection_association.rb +17 -9
  16. data/lib/active_record/associations/collection_proxy.rb +14 -1
  17. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  18. data/lib/active_record/associations/errors.rb +265 -0
  19. data/lib/active_record/associations/has_many_association.rb +1 -1
  20. data/lib/active_record/associations/has_many_through_association.rb +10 -3
  21. data/lib/active_record/associations/join_dependency/join_association.rb +1 -1
  22. data/lib/active_record/associations/nested_error.rb +47 -0
  23. data/lib/active_record/associations/preloader/association.rb +4 -3
  24. data/lib/active_record/associations/preloader/branch.rb +7 -1
  25. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  26. data/lib/active_record/associations/singular_association.rb +14 -3
  27. data/lib/active_record/associations/through_association.rb +1 -1
  28. data/lib/active_record/associations.rb +92 -295
  29. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  30. data/lib/active_record/attribute_assignment.rb +0 -2
  31. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  32. data/lib/active_record/attribute_methods/primary_key.rb +25 -61
  33. data/lib/active_record/attribute_methods/read.rb +1 -13
  34. data/lib/active_record/attribute_methods/serialization.rb +4 -24
  35. data/lib/active_record/attribute_methods/time_zone_conversion.rb +9 -18
  36. data/lib/active_record/attribute_methods.rb +71 -75
  37. data/lib/active_record/attributes.rb +63 -49
  38. data/lib/active_record/autosave_association.rb +92 -57
  39. data/lib/active_record/base.rb +2 -3
  40. data/lib/active_record/callbacks.rb +1 -1
  41. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +48 -122
  42. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  43. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +1 -1
  44. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +286 -77
  45. data/lib/active_record/connection_adapters/abstract/database_statements.rb +119 -55
  46. data/lib/active_record/connection_adapters/abstract/query_cache.rb +197 -76
  47. data/lib/active_record/connection_adapters/abstract/quoting.rb +66 -92
  48. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -5
  49. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +12 -3
  50. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +48 -12
  51. data/lib/active_record/connection_adapters/abstract/transaction.rb +140 -67
  52. data/lib/active_record/connection_adapters/abstract_adapter.rb +85 -90
  53. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +71 -52
  54. data/lib/active_record/connection_adapters/mysql/database_statements.rb +9 -1
  55. data/lib/active_record/connection_adapters/mysql/quoting.rb +50 -57
  56. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  57. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +56 -45
  58. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +92 -101
  59. data/lib/active_record/connection_adapters/mysql2_adapter.rb +13 -31
  60. data/lib/active_record/connection_adapters/pool_config.rb +14 -13
  61. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +86 -41
  62. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  63. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  64. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  65. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  66. data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
  67. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  68. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -11
  69. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +36 -20
  70. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +3 -2
  71. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +75 -28
  72. data/lib/active_record/connection_adapters/postgresql_adapter.rb +73 -113
  73. data/lib/active_record/connection_adapters/schema_cache.rb +124 -131
  74. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  75. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +81 -97
  76. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +57 -46
  77. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +16 -0
  78. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  79. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +29 -0
  80. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +35 -3
  81. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +183 -87
  82. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  83. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +39 -69
  84. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -65
  85. data/lib/active_record/connection_adapters.rb +65 -0
  86. data/lib/active_record/connection_handling.rb +74 -37
  87. data/lib/active_record/core.rb +132 -51
  88. data/lib/active_record/counter_cache.rb +19 -10
  89. data/lib/active_record/database_configurations/connection_url_resolver.rb +9 -2
  90. data/lib/active_record/database_configurations/database_config.rb +23 -4
  91. data/lib/active_record/database_configurations/hash_config.rb +46 -34
  92. data/lib/active_record/database_configurations/url_config.rb +20 -1
  93. data/lib/active_record/database_configurations.rb +1 -1
  94. data/lib/active_record/delegated_type.rb +41 -17
  95. data/lib/active_record/dynamic_matchers.rb +2 -2
  96. data/lib/active_record/encryption/config.rb +3 -1
  97. data/lib/active_record/encryption/encryptable_record.rb +7 -7
  98. data/lib/active_record/encryption/encrypted_attribute_type.rb +33 -4
  99. data/lib/active_record/encryption/encryptor.rb +28 -6
  100. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  101. data/lib/active_record/encryption/key_provider.rb +1 -1
  102. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  103. data/lib/active_record/encryption/message_serializer.rb +4 -0
  104. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  105. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  106. data/lib/active_record/encryption/scheme.rb +8 -1
  107. data/lib/active_record/enum.rb +20 -16
  108. data/lib/active_record/errors.rb +54 -20
  109. data/lib/active_record/explain.rb +13 -24
  110. data/lib/active_record/fixtures.rb +37 -33
  111. data/lib/active_record/future_result.rb +21 -13
  112. data/lib/active_record/gem_version.rb +4 -4
  113. data/lib/active_record/inheritance.rb +4 -2
  114. data/lib/active_record/insert_all.rb +19 -16
  115. data/lib/active_record/integration.rb +4 -1
  116. data/lib/active_record/internal_metadata.rb +48 -34
  117. data/lib/active_record/locking/optimistic.rb +8 -7
  118. data/lib/active_record/log_subscriber.rb +5 -32
  119. data/lib/active_record/message_pack.rb +1 -1
  120. data/lib/active_record/migration/command_recorder.rb +33 -14
  121. data/lib/active_record/migration/compatibility.rb +8 -3
  122. data/lib/active_record/migration/default_strategy.rb +4 -5
  123. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  124. data/lib/active_record/migration.rb +104 -98
  125. data/lib/active_record/model_schema.rb +32 -70
  126. data/lib/active_record/nested_attributes.rb +15 -9
  127. data/lib/active_record/normalization.rb +3 -7
  128. data/lib/active_record/persistence.rb +127 -451
  129. data/lib/active_record/query_cache.rb +19 -8
  130. data/lib/active_record/query_logs.rb +104 -37
  131. data/lib/active_record/query_logs_formatter.rb +17 -28
  132. data/lib/active_record/querying.rb +24 -12
  133. data/lib/active_record/railtie.rb +26 -68
  134. data/lib/active_record/railties/controller_runtime.rb +13 -4
  135. data/lib/active_record/railties/databases.rake +43 -61
  136. data/lib/active_record/reflection.rb +112 -53
  137. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  138. data/lib/active_record/relation/batches.rb +138 -72
  139. data/lib/active_record/relation/calculations.rb +122 -82
  140. data/lib/active_record/relation/delegation.rb +30 -22
  141. data/lib/active_record/relation/finder_methods.rb +32 -18
  142. data/lib/active_record/relation/merger.rb +12 -14
  143. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  144. data/lib/active_record/relation/predicate_builder/association_query_value.rb +10 -2
  145. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  146. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  147. data/lib/active_record/relation/predicate_builder.rb +16 -3
  148. data/lib/active_record/relation/query_attribute.rb +1 -1
  149. data/lib/active_record/relation/query_methods.rb +317 -101
  150. data/lib/active_record/relation/spawn_methods.rb +3 -19
  151. data/lib/active_record/relation/where_clause.rb +7 -19
  152. data/lib/active_record/relation.rb +561 -119
  153. data/lib/active_record/result.rb +95 -46
  154. data/lib/active_record/runtime_registry.rb +39 -0
  155. data/lib/active_record/sanitization.rb +31 -25
  156. data/lib/active_record/schema.rb +8 -6
  157. data/lib/active_record/schema_dumper.rb +53 -20
  158. data/lib/active_record/schema_migration.rb +31 -14
  159. data/lib/active_record/scoping/named.rb +6 -2
  160. data/lib/active_record/signed_id.rb +24 -4
  161. data/lib/active_record/statement_cache.rb +19 -19
  162. data/lib/active_record/store.rb +7 -3
  163. data/lib/active_record/table_metadata.rb +2 -13
  164. data/lib/active_record/tasks/database_tasks.rb +87 -58
  165. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -3
  166. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  167. data/lib/active_record/tasks/sqlite_database_tasks.rb +4 -3
  168. data/lib/active_record/test_fixtures.rb +98 -89
  169. data/lib/active_record/testing/query_assertions.rb +121 -0
  170. data/lib/active_record/timestamp.rb +2 -2
  171. data/lib/active_record/token_for.rb +22 -12
  172. data/lib/active_record/touch_later.rb +1 -1
  173. data/lib/active_record/transaction.rb +132 -0
  174. data/lib/active_record/transactions.rb +72 -17
  175. data/lib/active_record/translation.rb +0 -2
  176. data/lib/active_record/type/serialized.rb +1 -3
  177. data/lib/active_record/type_caster/connection.rb +4 -4
  178. data/lib/active_record/validations/associated.rb +9 -3
  179. data/lib/active_record/validations/uniqueness.rb +23 -18
  180. data/lib/active_record/validations.rb +4 -1
  181. data/lib/active_record.rb +138 -57
  182. data/lib/arel/alias_predication.rb +1 -1
  183. data/lib/arel/collectors/bind.rb +4 -2
  184. data/lib/arel/collectors/composite.rb +7 -0
  185. data/lib/arel/collectors/sql_string.rb +2 -2
  186. data/lib/arel/collectors/substitute_binds.rb +3 -3
  187. data/lib/arel/nodes/binary.rb +1 -7
  188. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  189. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  190. data/lib/arel/nodes/node.rb +5 -4
  191. data/lib/arel/nodes/sql_literal.rb +8 -1
  192. data/lib/arel/nodes.rb +2 -2
  193. data/lib/arel/predications.rb +1 -1
  194. data/lib/arel/select_manager.rb +1 -1
  195. data/lib/arel/table.rb +3 -7
  196. data/lib/arel/tree_manager.rb +3 -2
  197. data/lib/arel/update_manager.rb +2 -1
  198. data/lib/arel/visitors/dot.rb +1 -0
  199. data/lib/arel/visitors/mysql.rb +9 -4
  200. data/lib/arel/visitors/postgresql.rb +1 -12
  201. data/lib/arel/visitors/sqlite.rb +25 -0
  202. data/lib/arel/visitors/to_sql.rb +29 -16
  203. data/lib/arel.rb +7 -3
  204. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  205. metadata +18 -16
  206. data/lib/active_record/relation/record_fetch_warning.rb +0 -49
@@ -113,6 +113,26 @@ module ActiveRecord
113
113
  # end
114
114
  # end
115
115
  #
116
+ # == Querying across records
117
+ #
118
+ # A consequence of delegated types is that querying attributes spread across multiple classes becomes slightly more
119
+ # tricky, but not impossible.
120
+ #
121
+ # The simplest method is to join the "superclass" to the "subclass" and apply the query parameters (i.e. <tt>#where</tt>)
122
+ # in appropriate places:
123
+ #
124
+ # Comment.joins(:entry).where(comments: { content: 'Hello!' }, entry: { creator: Current.user } )
125
+ #
126
+ # For convenience, add a scope on the concern. Now all classes that implement the concern will automatically include
127
+ # the method:
128
+ #
129
+ # # app/models/concerns/entryable.rb
130
+ # scope :with_entry, ->(attrs) { joins(:entry).where(entry: attrs) }
131
+ #
132
+ # Now the query can be shortened significantly:
133
+ #
134
+ # Comment.where(content: 'Hello!').with_entry(creator: Current.user)
135
+ #
116
136
  # == Adding further delegation
117
137
  #
118
138
  # The delegated type shouldn't just answer the question of what the underlying class is called. In fact, that's
@@ -161,16 +181,16 @@ module ActiveRecord
161
181
  # delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
162
182
  # end
163
183
  #
164
- # Entry#entryable_class # => +Message+ or +Comment+
165
- # Entry#entryable_name # => "message" or "comment"
166
- # Entry.messages # => Entry.where(entryable_type: "Message")
167
- # Entry#message? # => true when entryable_type == "Message"
168
- # Entry#message # => returns the message record, when entryable_type == "Message", otherwise nil
169
- # Entry#message_id # => returns entryable_id, when entryable_type == "Message", otherwise nil
170
- # Entry.comments # => Entry.where(entryable_type: "Comment")
171
- # Entry#comment? # => true when entryable_type == "Comment"
172
- # Entry#comment # => returns the comment record, when entryable_type == "Comment", otherwise nil
173
- # Entry#comment_id # => returns entryable_id, when entryable_type == "Comment", otherwise nil
184
+ # @entry.entryable_class # => Message or Comment
185
+ # @entry.entryable_name # => "message" or "comment"
186
+ # Entry.messages # => Entry.where(entryable_type: "Message")
187
+ # @entry.message? # => true when entryable_type == "Message"
188
+ # @entry.message # => returns the message record, when entryable_type == "Message", otherwise nil
189
+ # @entry.message_id # => returns entryable_id, when entryable_type == "Message", otherwise nil
190
+ # Entry.comments # => Entry.where(entryable_type: "Comment")
191
+ # @entry.comment? # => true when entryable_type == "Comment"
192
+ # @entry.comment # => returns the comment record, when entryable_type == "Comment", otherwise nil
193
+ # @entry.comment_id # => returns entryable_id, when entryable_type == "Comment", otherwise nil
174
194
  #
175
195
  # You can also declare namespaced types:
176
196
  #
@@ -179,25 +199,25 @@ module ActiveRecord
179
199
  # end
180
200
  #
181
201
  # Entry.access_notice_messages
182
- # entry.access_notice_message
183
- # entry.access_notice_message?
202
+ # @entry.access_notice_message
203
+ # @entry.access_notice_message?
184
204
  #
185
205
  # === Options
186
206
  #
187
207
  # The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc.
188
208
  # The following options can be included to specialize the behavior of the delegated type convenience methods.
189
209
  #
190
- # [:foreign_key]
210
+ # [+:foreign_key+]
191
211
  # Specify the foreign key used for the convenience methods. By default this is guessed to be the passed
192
212
  # +role+ with an "_id" suffix. So a class that defines a
193
213
  # <tt>delegated_type :entryable, types: %w[ Message Comment ]</tt> association will use "entryable_id" as
194
214
  # the default <tt>:foreign_key</tt>.
195
- # [:foreign_type]
215
+ # [+:foreign_type+]
196
216
  # Specify the column used to store the associated object's type. By default this is inferred to be the passed
197
217
  # +role+ with a "_type" suffix. A class that defines a
198
218
  # <tt>delegated_type :entryable, types: %w[ Message Comment ]</tt> association will use "entryable_type" as
199
219
  # the default <tt>:foreign_type</tt>.
200
- # [:primary_key]
220
+ # [+:primary_key+]
201
221
  # Specify the method that returns the primary key of associated object used for the convenience methods.
202
222
  # By default this is +id+.
203
223
  #
@@ -206,8 +226,8 @@ module ActiveRecord
206
226
  # delegated_type :entryable, types: %w[ Message Comment ], primary_key: :uuid, foreign_key: :entryable_uuid
207
227
  # end
208
228
  #
209
- # Entry#message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
210
- # Entry#comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil
229
+ # @entry.message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
230
+ # @entry.comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil
211
231
  def delegated_type(role, types:, **options)
212
232
  belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
213
233
  define_delegated_type_methods role, types: types, options: options
@@ -219,6 +239,10 @@ module ActiveRecord
219
239
  role_type = options[:foreign_type] || "#{role}_type"
220
240
  role_id = options[:foreign_key] || "#{role}_id"
221
241
 
242
+ define_singleton_method "#{role}_types" do
243
+ types.map(&:to_s)
244
+ end
245
+
222
246
  define_method "#{role}_class" do
223
247
  public_send(role_type).constantize
224
248
  end
@@ -12,12 +12,12 @@ module ActiveRecord
12
12
  end
13
13
  end
14
14
 
15
- def method_missing(name, *arguments, &block)
15
+ def method_missing(name, ...)
16
16
  match = Method.match(self, name)
17
17
 
18
18
  if match && match.valid?
19
19
  match.define
20
- send(name, *arguments, &block)
20
+ send(name, ...)
21
21
  else
22
22
  super
23
23
  end
@@ -8,7 +8,8 @@ module ActiveRecord
8
8
  class Config
9
9
  attr_accessor :primary_key, :deterministic_key, :store_key_references, :key_derivation_salt, :hash_digest_class,
10
10
  :support_unencrypted_data, :encrypt_fixtures, :validate_column_size, :add_to_filter_parameters,
11
- :excluded_from_filter_parameters, :extend_queries, :previous_schemes, :forced_encoding_for_deterministic_encryption
11
+ :excluded_from_filter_parameters, :extend_queries, :previous_schemes, :forced_encoding_for_deterministic_encryption,
12
+ :compressor
12
13
 
13
14
  def initialize
14
15
  set_defaults
@@ -55,6 +56,7 @@ module ActiveRecord
55
56
  self.previous_schemes = []
56
57
  self.forced_encoding_for_deterministic_encryption = Encoding::UTF_8
57
58
  self.hash_digest_class = OpenSSL::Digest::SHA1
59
+ self.compressor = Zlib
58
60
 
59
61
  # TODO: Setting to false for now as the implementation is a bit experimental
60
62
  self.extend_queries = false
@@ -46,11 +46,11 @@ module ActiveRecord
46
46
  # * <tt>:previous</tt> - List of previous encryption schemes. When provided, they will be used in order when trying to read
47
47
  # the attribute. Each entry of the list can contain the properties supported by #encrypts. Also, when deterministic
48
48
  # encryption is used, they will be used to generate additional ciphertexts to check in the queries.
49
- def encrypts(*names, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], **context_properties)
49
+ def encrypts(*names, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], compress: true, compressor: nil, **context_properties)
50
50
  self.encrypted_attributes ||= Set.new # not using :default because the instance would be shared across classes
51
51
 
52
52
  names.each do |name|
53
- encrypt_attribute name, key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, downcase: downcase, ignore_case: ignore_case, previous: previous, **context_properties
53
+ encrypt_attribute name, key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, downcase: downcase, ignore_case: ignore_case, previous: previous, compress: compress, compressor: compressor, **context_properties
54
54
  end
55
55
  end
56
56
 
@@ -81,12 +81,12 @@ module ActiveRecord
81
81
  end
82
82
  end
83
83
 
84
- def encrypt_attribute(name, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], **context_properties)
84
+ def encrypt_attribute(name, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], compress: true, compressor: nil, **context_properties)
85
85
  encrypted_attributes << name.to_sym
86
86
 
87
- attribute name do |cast_type|
87
+ decorate_attributes([name]) do |name, cast_type|
88
88
  scheme = scheme_for key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, \
89
- downcase: downcase, ignore_case: ignore_case, previous: previous, **context_properties
89
+ downcase: downcase, ignore_case: ignore_case, previous: previous, compress: compress, compressor: compressor, **context_properties
90
90
 
91
91
  ActiveRecord::Encryption::EncryptedAttributeType.new(scheme: scheme, cast_type: cast_type, default: columns_hash[name.to_s]&.default)
92
92
  end
@@ -123,7 +123,7 @@ module ActiveRecord
123
123
  end)
124
124
  end
125
125
 
126
- def load_schema!
126
+ def load_schema! # :nodoc:
127
127
  super
128
128
 
129
129
  add_length_validation_for_encrypted_columns if ActiveRecord::Encryption.config.validate_column_size
@@ -221,7 +221,7 @@ module ActiveRecord
221
221
  end
222
222
 
223
223
  def cant_modify_encrypted_attributes_when_frozen
224
- self.class&.encrypted_attributes.each do |attribute|
224
+ self.class.encrypted_attributes.each do |attribute|
225
225
  errors.add(attribute.to_sym, "can't be modified because it is encrypted") if changed_attributes.include?(attribute)
226
226
  end
227
227
  end
@@ -7,13 +7,13 @@ module ActiveRecord
7
7
  # This is the central piece that connects the encryption system with +encrypts+ declarations in the
8
8
  # model classes. Whenever you declare an attribute as encrypted, it configures an +EncryptedAttributeType+
9
9
  # for that attribute.
10
- class EncryptedAttributeType < ::ActiveRecord::Type::Text
10
+ class EncryptedAttributeType < ::ActiveModel::Type::Value
11
11
  include ActiveModel::Type::Helpers::Mutable
12
12
 
13
13
  attr_reader :scheme, :cast_type
14
14
 
15
15
  delegate :key_provider, :downcase?, :deterministic?, :previous_schemes, :with_context, :fixed?, to: :scheme
16
- delegate :accessor, to: :cast_type
16
+ delegate :accessor, :type, to: :cast_type
17
17
 
18
18
  # === Options
19
19
  #
@@ -81,7 +81,7 @@ module ActiveRecord
81
81
  @previous_type
82
82
  end
83
83
 
84
- def decrypt(value)
84
+ def decrypt_as_text(value)
85
85
  with_context do
86
86
  unless value.nil?
87
87
  if @default && @default == value
@@ -99,6 +99,10 @@ module ActiveRecord
99
99
  end
100
100
  end
101
101
 
102
+ def decrypt(value)
103
+ text_to_database_type decrypt_as_text(database_type_to_text(value))
104
+ end
105
+
102
106
  def try_to_deserialize_with_previous_encrypted_types(value)
103
107
  previous_types.each.with_index do |type, index|
104
108
  break type.deserialize(value)
@@ -129,12 +133,20 @@ module ActiveRecord
129
133
  encrypt(casted_value.to_s) unless casted_value.nil?
130
134
  end
131
135
 
132
- def encrypt(value)
136
+ def encrypt_as_text(value)
133
137
  with_context do
138
+ if encryptor.binary? && !cast_type.binary?
139
+ raise Errors::Encoding, "Binary encoded data can only be stored in binary columns"
140
+ end
141
+
134
142
  encryptor.encrypt(value, **encryption_options)
135
143
  end
136
144
  end
137
145
 
146
+ def encrypt(value)
147
+ text_to_database_type encrypt_as_text(value)
148
+ end
149
+
138
150
  def encryptor
139
151
  ActiveRecord::Encryption.encryptor
140
152
  end
@@ -150,6 +162,23 @@ module ActiveRecord
150
162
  def clean_text_scheme
151
163
  @clean_text_scheme ||= ActiveRecord::Encryption::Scheme.new(downcase: downcase?, encryptor: ActiveRecord::Encryption::NullEncryptor.new)
152
164
  end
165
+
166
+ def text_to_database_type(value)
167
+ if value && cast_type.binary?
168
+ ActiveModel::Type::Binary::Data.new(value)
169
+ else
170
+ value
171
+ end
172
+ end
173
+
174
+ def database_type_to_text(value)
175
+ if value && cast_type.binary?
176
+ binary_cast_type = cast_type.serialized? ? cast_type.subtype : cast_type
177
+ binary_cast_type.deserialize(value)
178
+ else
179
+ value
180
+ end
181
+ end
153
182
  end
154
183
  end
155
184
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "openssl"
4
- require "zlib"
5
4
  require "active_support/core_ext/numeric"
6
5
 
7
6
  module ActiveRecord
@@ -12,6 +11,22 @@ module ActiveRecord
12
11
  # It interacts with a KeyProvider for getting the keys, and delegate to
13
12
  # ActiveRecord::Encryption::Cipher the actual encryption algorithm.
14
13
  class Encryptor
14
+ # The compressor to use for compressing the payload
15
+ attr_reader :compressor
16
+
17
+ # === Options
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+.
25
+ def initialize(compress: true, compressor: nil)
26
+ @compress = compress
27
+ @compressor = compressor || ActiveRecord::Encryption.config.compressor
28
+ end
29
+
15
30
  # Encrypts +clean_text+ and returns the encrypted result
16
31
  #
17
32
  # Internally, it will:
@@ -38,7 +53,7 @@ module ActiveRecord
38
53
  serialize_message build_encrypted_message(clear_text, key_provider: key_provider, cipher_options: cipher_options)
39
54
  end
40
55
 
41
- # Decrypts a +clean_text+ and returns the result as clean text
56
+ # Decrypts an +encrypted_text+ and returns the result as clean text
42
57
  #
43
58
  # === Options
44
59
  #
@@ -66,6 +81,14 @@ module ActiveRecord
66
81
  false
67
82
  end
68
83
 
84
+ def binary?
85
+ serializer.binary?
86
+ end
87
+
88
+ def compress? # :nodoc:
89
+ @compress
90
+ end
91
+
69
92
  private
70
93
  DECRYPT_ERRORS = [OpenSSL::Cipher::CipherError, Errors::EncryptedContentIntegrity, Errors::Decryption]
71
94
  ENCODING_ERRORS = [EncodingError, Errors::Encoding]
@@ -100,7 +123,6 @@ module ActiveRecord
100
123
  end
101
124
 
102
125
  def deserialize_message(message)
103
- raise Errors::Encoding unless message.is_a?(String)
104
126
  serializer.load message
105
127
  rescue ArgumentError, TypeError, Errors::ForbiddenClass
106
128
  raise Errors::Encoding
@@ -112,7 +134,7 @@ module ActiveRecord
112
134
 
113
135
  # Under certain threshold, ZIP compression is actually worse that not compressing
114
136
  def compress_if_worth_it(string)
115
- if string.bytesize > THRESHOLD_TO_JUSTIFY_COMPRESSION
137
+ if compress? && string.bytesize > THRESHOLD_TO_JUSTIFY_COMPRESSION
116
138
  [compress(string), true]
117
139
  else
118
140
  [string, false]
@@ -120,7 +142,7 @@ module ActiveRecord
120
142
  end
121
143
 
122
144
  def compress(data)
123
- Zlib::Deflate.deflate(data).tap do |compressed_data|
145
+ @compressor.deflate(data).tap do |compressed_data|
124
146
  compressed_data.force_encoding(data.encoding)
125
147
  end
126
148
  end
@@ -134,7 +156,7 @@ module ActiveRecord
134
156
  end
135
157
 
136
158
  def uncompress(data)
137
- Zlib::Inflate.inflate(data).tap do |uncompressed_data|
159
+ @compressor.inflate(data).tap do |uncompressed_data|
138
160
  uncompressed_data.force_encoding(data.encoding)
139
161
  end
140
162
  end
@@ -41,6 +41,8 @@ module ActiveRecord
41
41
  module EncryptedQuery # :nodoc:
42
42
  class << self
43
43
  def process_arguments(owner, args, check_for_additional_values)
44
+ owner = owner.model if owner.is_a?(Relation)
45
+
44
46
  return args if owner.deterministic_encrypted_attributes&.empty?
45
47
 
46
48
  if args.is_a?(Array) && (options = args.first).is_a?(Hash)
@@ -102,12 +104,12 @@ module ActiveRecord
102
104
  end
103
105
 
104
106
  def scope_for_create
105
- return super unless klass.deterministic_encrypted_attributes&.any?
107
+ return super unless model.deterministic_encrypted_attributes&.any?
106
108
 
107
109
  scope_attributes = super
108
110
  wheres = where_values_hash
109
111
 
110
- klass.deterministic_encrypted_attributes.each do |attribute_name|
112
+ model.deterministic_encrypted_attributes.each do |attribute_name|
111
113
  attribute_name = attribute_name.to_s
112
114
  values = wheres[attribute_name]
113
115
  if values.is_a?(Array) && values[1..].all?(AdditionalValue)
@@ -12,7 +12,7 @@ module ActiveRecord
12
12
  @keys = Array(keys)
13
13
  end
14
14
 
15
- # Returns the first key in the list as the active key to perform encryptions
15
+ # Returns the last key in the list as the active key to perform encryptions
16
16
  #
17
17
  # When +ActiveRecord::Encryption.config.store_key_references+ is true, the key will include
18
18
  # a public tag referencing the key itself. That key will be stored in the public
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/message_pack"
4
+
5
+ module ActiveRecord
6
+ module Encryption
7
+ # A message serializer that serializes +Messages+ with MessagePack.
8
+ #
9
+ # The message is converted to a hash with this structure:
10
+ #
11
+ # {
12
+ # p: <payload>,
13
+ # h: {
14
+ # header1: value1,
15
+ # header2: value2,
16
+ # ...
17
+ # }
18
+ # }
19
+ #
20
+ # Then it is converted to the MessagePack format.
21
+ class MessagePackMessageSerializer
22
+ def dump(message)
23
+ raise Errors::ForbiddenClass unless message.is_a?(Message)
24
+ ActiveSupport::MessagePack.dump(message_to_hash(message))
25
+ end
26
+
27
+ def load(serialized_content)
28
+ data = ActiveSupport::MessagePack.load(serialized_content)
29
+ hash_to_message(data, 1)
30
+ rescue RuntimeError
31
+ raise Errors::Decryption
32
+ end
33
+
34
+ def binary?
35
+ true
36
+ end
37
+
38
+ private
39
+ def message_to_hash(message)
40
+ {
41
+ "p" => message.payload,
42
+ "h" => headers_to_hash(message.headers)
43
+ }
44
+ end
45
+
46
+ def headers_to_hash(headers)
47
+ headers.transform_values do |value|
48
+ value.is_a?(Message) ? message_to_hash(value) : value
49
+ end
50
+ end
51
+
52
+ def hash_to_message(data, level)
53
+ validate_message_data_format(data, level)
54
+ Message.new(payload: data["p"], headers: parse_properties(data["h"], level))
55
+ end
56
+
57
+ def validate_message_data_format(data, level)
58
+ if level > 2
59
+ raise Errors::Decryption, "More than one level of hash nesting in headers is not supported"
60
+ end
61
+
62
+ unless data.is_a?(Hash) && data.has_key?("p")
63
+ raise Errors::Decryption, "Invalid data format: hash without payload"
64
+ end
65
+ end
66
+
67
+ def parse_properties(headers, level)
68
+ Properties.new.tap do |properties|
69
+ headers&.each do |key, value|
70
+ properties[key] = value.is_a?(Hash) ? hash_to_message(value, level + 1) : value
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -33,6 +33,10 @@ module ActiveRecord
33
33
  JSON.dump message_to_json(message)
34
34
  end
35
35
 
36
+ def binary?
37
+ false
38
+ end
39
+
36
40
  private
37
41
  def parse_message(data, level)
38
42
  validate_message_data_format(data, level)
@@ -16,6 +16,10 @@ module ActiveRecord
16
16
  def encrypted?(text)
17
17
  false
18
18
  end
19
+
20
+ def binary?
21
+ false
22
+ end
19
23
  end
20
24
  end
21
25
  end
@@ -19,6 +19,10 @@ module ActiveRecord
19
19
  def encrypted?(text)
20
20
  false
21
21
  end
22
+
23
+ def binary?
24
+ false
25
+ end
22
26
  end
23
27
  end
24
28
  end
@@ -11,7 +11,7 @@ module ActiveRecord
11
11
  attr_accessor :previous_schemes
12
12
 
13
13
  def initialize(key_provider: nil, key: nil, deterministic: nil, support_unencrypted_data: nil, downcase: nil, ignore_case: nil,
14
- previous_schemes: nil, **context_properties)
14
+ previous_schemes: nil, compress: true, compressor: nil, **context_properties)
15
15
  # Initializing all attributes to +nil+ as we want to allow a "not set" semantics so that we
16
16
  # can merge schemes without overriding values with defaults. See +#merge+
17
17
 
@@ -24,8 +24,13 @@ module ActiveRecord
24
24
  @previous_schemes_param = previous_schemes
25
25
  @previous_schemes = Array.wrap(previous_schemes)
26
26
  @context_properties = context_properties
27
+ @compress = compress
28
+ @compressor = compressor
27
29
 
28
30
  validate_config!
31
+
32
+ @context_properties[:encryptor] = Encryptor.new(compress: @compress) unless @compress
33
+ @context_properties[:encryptor] = Encryptor.new(compressor: compressor) if compressor
29
34
  end
30
35
 
31
36
  def ignore_case?
@@ -78,6 +83,8 @@ module ActiveRecord
78
83
  def validate_config!
79
84
  raise Errors::Configuration, "ignore_case: can only be used with deterministic encryption" if @ignore_case && !@deterministic
80
85
  raise Errors::Configuration, "key_provider: and key: can't be used simultaneously" if @key_provider_param && @key
86
+ raise Errors::Configuration, "compressor: can't be used with compress: false" if !@compress && @compressor
87
+ raise Errors::Configuration, "compressor: can't be used with encryptor" if @compressor && @context_properties[:encryptor]
81
88
  end
82
89
 
83
90
  def key_provider_from_key
@@ -213,26 +213,16 @@ module ActiveRecord
213
213
  attr_reader :name, :mapping
214
214
  end
215
215
 
216
- def enum(name = nil, values = nil, **options)
217
- if name
218
- values, options = options, {} unless values
219
- return _enum(name, values, **options)
220
- end
221
-
222
- definitions = options.slice!(:_prefix, :_suffix, :_scopes, :_default, :_instance_methods)
223
- options.transform_keys! { |key| :"#{key[1..-1]}" }
224
-
225
- definitions.each { |name, values| _enum(name, values, **options) }
216
+ def enum(name, values = nil, **options)
217
+ values, options = options, {} unless values
218
+ _enum(name, values, **options)
226
219
  end
227
220
 
228
221
  private
229
- def inherited(base)
230
- base.defined_enums = defined_enums.deep_dup
231
- super
232
- end
233
-
234
222
  def _enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods: true, validate: false, **options)
235
223
  assert_valid_enum_definition_values(values)
224
+ assert_valid_enum_options(options)
225
+
236
226
  # statuses = { }
237
227
  enum_values = ActiveSupport::HashWithIndifferentAccess.new
238
228
  name = name.to_s
@@ -245,7 +235,9 @@ module ActiveRecord
245
235
  detect_enum_conflict!(name, name)
246
236
  detect_enum_conflict!(name, "#{name}=")
247
237
 
248
- attribute(name, **options) do |subtype|
238
+ attribute(name, **options)
239
+
240
+ decorate_attributes([name]) do |_name, subtype|
249
241
  if subtype == ActiveModel::Type.default_value
250
242
  raise "Undeclared attribute type for enum '#{name}' in #{self.name}. Enums must be" \
251
243
  " backed by a database column or declared with an explicit type" \
@@ -294,6 +286,11 @@ module ActiveRecord
294
286
  enum_values.freeze
295
287
  end
296
288
 
289
+ def inherited(base)
290
+ base.defined_enums = defined_enums.deep_dup
291
+ super
292
+ end
293
+
297
294
  class EnumMethods < Module # :nodoc:
298
295
  def initialize(klass)
299
296
  @klass = klass
@@ -361,6 +358,13 @@ module ActiveRecord
361
358
  end
362
359
  end
363
360
 
361
+ def assert_valid_enum_options(options)
362
+ invalid_keys = options.keys & %i[_prefix _suffix _scopes _default _instance_methods]
363
+ unless invalid_keys.empty?
364
+ raise ArgumentError, "invalid option(s): #{invalid_keys.map(&:inspect).join(", ")}. Valid options are: :prefix, :suffix, :scopes, :default, :instance_methods, and :validate."
365
+ end
366
+ end
367
+
364
368
  ENUM_CONFLICT_MESSAGE = \
365
369
  "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
366
370
  "this will generate a %{type} method \"%{method}\", which is already defined " \