activerecord 7.1.5.2 → 8.1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +567 -2443
  3. data/README.rdoc +16 -16
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +3 -2
  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 +31 -8
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/association.rb +23 -11
  11. data/lib/active_record/associations/builder/belongs_to.rb +18 -4
  12. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  13. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  14. data/lib/active_record/associations/builder/has_many.rb +3 -4
  15. data/lib/active_record/associations/builder/has_one.rb +3 -4
  16. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  17. data/lib/active_record/associations/collection_association.rb +17 -9
  18. data/lib/active_record/associations/collection_proxy.rb +36 -5
  19. data/lib/active_record/associations/deprecation.rb +88 -0
  20. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  21. data/lib/active_record/associations/errors.rb +268 -0
  22. data/lib/active_record/associations/has_many_association.rb +1 -1
  23. data/lib/active_record/associations/has_many_through_association.rb +10 -3
  24. data/lib/active_record/associations/join_dependency/join_association.rb +1 -1
  25. data/lib/active_record/associations/join_dependency.rb +4 -2
  26. data/lib/active_record/associations/nested_error.rb +47 -0
  27. data/lib/active_record/associations/preloader/association.rb +4 -3
  28. data/lib/active_record/associations/preloader/batch.rb +7 -1
  29. data/lib/active_record/associations/preloader/branch.rb +8 -1
  30. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  31. data/lib/active_record/associations/singular_association.rb +14 -3
  32. data/lib/active_record/associations/through_association.rb +1 -1
  33. data/lib/active_record/associations.rb +250 -315
  34. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  35. data/lib/active_record/attribute_assignment.rb +0 -2
  36. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  37. data/lib/active_record/attribute_methods/primary_key.rb +25 -61
  38. data/lib/active_record/attribute_methods/query.rb +34 -0
  39. data/lib/active_record/attribute_methods/read.rb +1 -13
  40. data/lib/active_record/attribute_methods/serialization.rb +20 -27
  41. data/lib/active_record/attribute_methods/time_zone_conversion.rb +19 -20
  42. data/lib/active_record/attribute_methods.rb +71 -75
  43. data/lib/active_record/attributes.rb +96 -68
  44. data/lib/active_record/autosave_association.rb +93 -58
  45. data/lib/active_record/base.rb +5 -7
  46. data/lib/active_record/callbacks.rb +1 -1
  47. data/lib/active_record/coders/json.rb +14 -5
  48. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +50 -126
  49. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -4
  50. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +52 -13
  51. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +681 -135
  52. data/lib/active_record/connection_adapters/abstract/database_statements.rb +165 -86
  53. data/lib/active_record/connection_adapters/abstract/query_cache.rb +226 -78
  54. data/lib/active_record/connection_adapters/abstract/quoting.rb +79 -114
  55. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +11 -7
  56. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +37 -36
  57. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  58. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +136 -34
  59. data/lib/active_record/connection_adapters/abstract/transaction.rb +163 -68
  60. data/lib/active_record/connection_adapters/abstract_adapter.rb +197 -129
  61. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +112 -63
  62. data/lib/active_record/connection_adapters/column.rb +17 -4
  63. data/lib/active_record/connection_adapters/mysql/database_statements.rb +11 -3
  64. data/lib/active_record/connection_adapters/mysql/quoting.rb +50 -57
  65. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  66. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +41 -10
  67. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +79 -46
  68. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +97 -101
  69. data/lib/active_record/connection_adapters/mysql2_adapter.rb +14 -33
  70. data/lib/active_record/connection_adapters/pool_config.rb +14 -13
  71. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
  72. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +98 -44
  73. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +3 -3
  74. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  75. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  76. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  77. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  78. data/lib/active_record/connection_adapters/postgresql/quoting.rb +79 -68
  79. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  80. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +9 -17
  81. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +44 -41
  82. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +69 -32
  83. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +155 -75
  84. data/lib/active_record/connection_adapters/postgresql_adapter.rb +104 -121
  85. data/lib/active_record/connection_adapters/schema_cache.rb +126 -133
  86. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  87. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +91 -95
  88. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +57 -54
  89. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +16 -0
  90. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  91. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +43 -2
  92. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +38 -15
  93. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +231 -111
  94. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  95. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +40 -69
  96. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -65
  97. data/lib/active_record/connection_adapters.rb +66 -0
  98. data/lib/active_record/connection_handling.rb +88 -46
  99. data/lib/active_record/core.rb +142 -58
  100. data/lib/active_record/counter_cache.rb +52 -18
  101. data/lib/active_record/database_configurations/connection_url_resolver.rb +9 -2
  102. data/lib/active_record/database_configurations/database_config.rb +28 -5
  103. data/lib/active_record/database_configurations/hash_config.rb +103 -41
  104. data/lib/active_record/database_configurations/url_config.rb +30 -1
  105. data/lib/active_record/database_configurations.rb +8 -4
  106. data/lib/active_record/delegated_type.rb +43 -19
  107. data/lib/active_record/dynamic_matchers.rb +56 -71
  108. data/lib/active_record/encryption/config.rb +3 -1
  109. data/lib/active_record/encryption/encryptable_record.rb +12 -12
  110. data/lib/active_record/encryption/encrypted_attribute_type.rb +35 -6
  111. data/lib/active_record/encryption/encryptor.rb +58 -22
  112. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  113. data/lib/active_record/encryption/key_provider.rb +1 -1
  114. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  115. data/lib/active_record/encryption/message_serializer.rb +4 -0
  116. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  117. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  118. data/lib/active_record/encryption/scheme.rb +9 -2
  119. data/lib/active_record/enum.rb +57 -36
  120. data/lib/active_record/errors.rb +76 -26
  121. data/lib/active_record/explain.rb +14 -25
  122. data/lib/active_record/explain_registry.rb +51 -2
  123. data/lib/active_record/filter_attribute_handler.rb +73 -0
  124. data/lib/active_record/fixture_set/table_row.rb +19 -2
  125. data/lib/active_record/fixtures.rb +39 -35
  126. data/lib/active_record/future_result.rb +21 -13
  127. data/lib/active_record/gem_version.rb +3 -3
  128. data/lib/active_record/inheritance.rb +5 -3
  129. data/lib/active_record/insert_all.rb +28 -20
  130. data/lib/active_record/integration.rb +4 -1
  131. data/lib/active_record/internal_metadata.rb +48 -34
  132. data/lib/active_record/locking/optimistic.rb +15 -7
  133. data/lib/active_record/locking/pessimistic.rb +5 -0
  134. data/lib/active_record/log_subscriber.rb +3 -34
  135. data/lib/active_record/message_pack.rb +1 -1
  136. data/lib/active_record/middleware/shard_selector.rb +34 -17
  137. data/lib/active_record/migration/command_recorder.rb +46 -14
  138. data/lib/active_record/migration/compatibility.rb +40 -25
  139. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  140. data/lib/active_record/migration/default_strategy.rb +4 -5
  141. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  142. data/lib/active_record/migration.rb +119 -103
  143. data/lib/active_record/model_schema.rb +66 -78
  144. data/lib/active_record/nested_attributes.rb +17 -9
  145. data/lib/active_record/persistence.rb +161 -454
  146. data/lib/active_record/query_cache.rb +33 -15
  147. data/lib/active_record/query_logs.rb +106 -39
  148. data/lib/active_record/query_logs_formatter.rb +17 -28
  149. data/lib/active_record/querying.rb +28 -16
  150. data/lib/active_record/railtie.rb +61 -74
  151. data/lib/active_record/railties/controller_runtime.rb +19 -5
  152. data/lib/active_record/railties/databases.rake +64 -78
  153. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  154. data/lib/active_record/railties/job_runtime.rb +10 -11
  155. data/lib/active_record/reflection.rb +147 -53
  156. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  157. data/lib/active_record/relation/batches.rb +153 -73
  158. data/lib/active_record/relation/calculations.rb +146 -96
  159. data/lib/active_record/relation/delegation.rb +30 -23
  160. data/lib/active_record/relation/finder_methods.rb +64 -34
  161. data/lib/active_record/relation/merger.rb +12 -14
  162. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  163. data/lib/active_record/relation/predicate_builder/association_query_value.rb +19 -11
  164. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +8 -8
  165. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  166. data/lib/active_record/relation/predicate_builder.rb +25 -10
  167. data/lib/active_record/relation/query_attribute.rb +4 -2
  168. data/lib/active_record/relation/query_methods.rb +351 -124
  169. data/lib/active_record/relation/spawn_methods.rb +9 -25
  170. data/lib/active_record/relation/where_clause.rb +17 -30
  171. data/lib/active_record/relation.rb +590 -124
  172. data/lib/active_record/result.rb +131 -59
  173. data/lib/active_record/runtime_registry.rb +44 -21
  174. data/lib/active_record/sanitization.rb +33 -25
  175. data/lib/active_record/schema.rb +8 -6
  176. data/lib/active_record/schema_dumper.rb +66 -31
  177. data/lib/active_record/schema_migration.rb +31 -14
  178. data/lib/active_record/scoping/named.rb +6 -2
  179. data/lib/active_record/scoping.rb +0 -1
  180. data/lib/active_record/secure_token.rb +3 -3
  181. data/lib/active_record/signed_id.rb +68 -20
  182. data/lib/active_record/statement_cache.rb +29 -25
  183. data/lib/active_record/store.rb +51 -22
  184. data/lib/active_record/structured_event_subscriber.rb +85 -0
  185. data/lib/active_record/table_metadata.rb +7 -33
  186. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  187. data/lib/active_record/tasks/database_tasks.rb +118 -90
  188. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -42
  189. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -40
  190. data/lib/active_record/tasks/sqlite_database_tasks.rb +17 -28
  191. data/lib/active_record/test_databases.rb +14 -4
  192. data/lib/active_record/test_fixtures.rb +123 -89
  193. data/lib/active_record/testing/query_assertions.rb +127 -0
  194. data/lib/active_record/timestamp.rb +6 -4
  195. data/lib/active_record/token_for.rb +22 -12
  196. data/lib/active_record/touch_later.rb +1 -1
  197. data/lib/active_record/transaction.rb +129 -0
  198. data/lib/active_record/transactions.rb +97 -18
  199. data/lib/active_record/translation.rb +0 -2
  200. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  201. data/lib/active_record/type/internal/timezone.rb +7 -0
  202. data/lib/active_record/type/json.rb +15 -2
  203. data/lib/active_record/type/serialized.rb +12 -7
  204. data/lib/active_record/type/type_map.rb +1 -1
  205. data/lib/active_record/type_caster/connection.rb +5 -4
  206. data/lib/active_record/validations/associated.rb +10 -4
  207. data/lib/active_record/validations/uniqueness.rb +23 -18
  208. data/lib/active_record/validations.rb +4 -1
  209. data/lib/active_record.rb +203 -59
  210. data/lib/arel/alias_predication.rb +3 -1
  211. data/lib/arel/collectors/bind.rb +4 -2
  212. data/lib/arel/collectors/composite.rb +7 -0
  213. data/lib/arel/collectors/sql_string.rb +2 -2
  214. data/lib/arel/collectors/substitute_binds.rb +3 -3
  215. data/lib/arel/crud.rb +8 -11
  216. data/lib/arel/delete_manager.rb +5 -0
  217. data/lib/arel/nodes/binary.rb +1 -7
  218. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  219. data/lib/arel/nodes/count.rb +2 -2
  220. data/lib/arel/nodes/delete_statement.rb +4 -2
  221. data/lib/arel/nodes/function.rb +4 -10
  222. data/lib/arel/nodes/named_function.rb +2 -2
  223. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  224. data/lib/arel/nodes/node.rb +6 -5
  225. data/lib/arel/nodes/sql_literal.rb +8 -1
  226. data/lib/arel/nodes/update_statement.rb +4 -2
  227. data/lib/arel/nodes.rb +2 -4
  228. data/lib/arel/predications.rb +1 -1
  229. data/lib/arel/select_manager.rb +14 -5
  230. data/lib/arel/table.rb +3 -7
  231. data/lib/arel/tree_manager.rb +3 -2
  232. data/lib/arel/update_manager.rb +7 -1
  233. data/lib/arel/visitors/dot.rb +3 -3
  234. data/lib/arel/visitors/mysql.rb +9 -4
  235. data/lib/arel/visitors/postgresql.rb +56 -12
  236. data/lib/arel/visitors/sqlite.rb +80 -8
  237. data/lib/arel/visitors/to_sql.rb +34 -37
  238. data/lib/arel.rb +10 -4
  239. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  240. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  241. metadata +23 -14
  242. data/lib/active_record/explain_subscriber.rb +0 -34
  243. data/lib/active_record/normalization.rb +0 -167
  244. data/lib/active_record/relation/record_fetch_warning.rb +0 -49
@@ -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.
@@ -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,15 +7,15 @@ 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
- # === 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
@@ -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,25 +11,43 @@ 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
15
- # Encrypts +clean_text+ and returns the encrypted result
14
+ # The compressor to use for compressing the payload.
15
+ attr_reader :compressor
16
+
17
+ # ==== Options
18
+ #
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+.
27
+ def initialize(compress: true, compressor: nil)
28
+ @compress = compress
29
+ @compressor = compressor || ActiveRecord::Encryption.config.compressor
30
+ end
31
+
32
+ # Encrypts +clean_text+ and returns the encrypted result.
16
33
  #
17
34
  # Internally, it will:
18
35
  #
19
- # 1. Create a new ActiveRecord::Encryption::Message
20
- # 2. Compress and encrypt +clean_text+ as the message payload
21
- # 3. Serialize it with +ActiveRecord::Encryption.message_serializer+ (+ActiveRecord::Encryption::SafeMarshal+
22
- # by default)
23
- # 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.
24
41
  #
25
- # === Options
42
+ # ==== Options
26
43
  #
27
- # [:key_provider]
44
+ # [+:key_provider+]
28
45
  # Key provider to use for the encryption operation. It will default to
29
46
  # +ActiveRecord::Encryption.key_provider+ when not provided.
30
47
  #
31
- # [:cipher_options]
48
+ # [+:cipher_options+]
32
49
  # Cipher-specific options that will be passed to the Cipher configured in
33
- # +ActiveRecord::Encryption.cipher+
50
+ # +ActiveRecord::Encryption.cipher+.
34
51
  def encrypt(clear_text, key_provider: default_key_provider, cipher_options: {})
35
52
  clear_text = force_encoding_if_needed(clear_text) if cipher_options[:deterministic]
36
53
 
@@ -38,17 +55,17 @@ module ActiveRecord
38
55
  serialize_message build_encrypted_message(clear_text, key_provider: key_provider, cipher_options: cipher_options)
39
56
  end
40
57
 
41
- # Decrypts a +clean_text+ and returns the result as clean text
58
+ # Decrypts an +encrypted_text+ and returns the result as clean text.
42
59
  #
43
- # === Options
60
+ # ==== Options
44
61
  #
45
- # [:key_provider]
62
+ # [+:key_provider+]
46
63
  # Key provider to use for the encryption operation. It will default to
47
- # +ActiveRecord::Encryption.key_provider+ when not provided
64
+ # +ActiveRecord::Encryption.key_provider+ when not provided.
48
65
  #
49
- # [:cipher_options]
66
+ # [+:cipher_options+]
50
67
  # Cipher-specific options that will be passed to the Cipher configured in
51
- # +ActiveRecord::Encryption.cipher+
68
+ # +ActiveRecord::Encryption.cipher+.
52
69
  def decrypt(encrypted_text, key_provider: default_key_provider, cipher_options: {})
53
70
  message = deserialize_message(encrypted_text)
54
71
  keys = key_provider.decryption_keys(message)
@@ -58,7 +75,7 @@ module ActiveRecord
58
75
  raise Errors::Decryption
59
76
  end
60
77
 
61
- # Returns whether the text is encrypted or not
78
+ # Returns whether the text is encrypted or not.
62
79
  def encrypted?(text)
63
80
  deserialize_message(text)
64
81
  true
@@ -66,9 +83,29 @@ module ActiveRecord
66
83
  false
67
84
  end
68
85
 
86
+ def binary?
87
+ serializer.binary?
88
+ end
89
+
90
+ def compress? # :nodoc:
91
+ @compress
92
+ end
93
+
69
94
  private
70
95
  DECRYPT_ERRORS = [OpenSSL::Cipher::CipherError, Errors::EncryptedContentIntegrity, Errors::Decryption]
71
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.
72
109
  THRESHOLD_TO_JUSTIFY_COMPRESSION = 140.bytes
73
110
 
74
111
  def default_key_provider
@@ -100,7 +137,6 @@ module ActiveRecord
100
137
  end
101
138
 
102
139
  def deserialize_message(message)
103
- raise Errors::Encoding unless message.is_a?(String)
104
140
  serializer.load message
105
141
  rescue ArgumentError, TypeError, Errors::ForbiddenClass
106
142
  raise Errors::Encoding
@@ -112,7 +148,7 @@ module ActiveRecord
112
148
 
113
149
  # Under certain threshold, ZIP compression is actually worse that not compressing
114
150
  def compress_if_worth_it(string)
115
- if string.bytesize > THRESHOLD_TO_JUSTIFY_COMPRESSION
151
+ if compress? && string.bytesize > THRESHOLD_TO_JUSTIFY_COMPRESSION
116
152
  [compress(string), true]
117
153
  else
118
154
  [string, false]
@@ -120,7 +156,7 @@ module ActiveRecord
120
156
  end
121
157
 
122
158
  def compress(data)
123
- Zlib::Deflate.deflate(data).tap do |compressed_data|
159
+ @compressor.deflate(data).tap do |compressed_data|
124
160
  compressed_data.force_encoding(data.encoding)
125
161
  end
126
162
  end
@@ -134,7 +170,7 @@ module ActiveRecord
134
170
  end
135
171
 
136
172
  def uncompress(data)
137
- Zlib::Inflate.inflate(data).tap do |uncompressed_data|
173
+ @compressor.inflate(data).tap do |uncompressed_data|
138
174
  uncompressed_data.force_encoding(data.encoding)
139
175
  end
140
176
  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?
@@ -54,7 +59,7 @@ module ActiveRecord
54
59
  end
55
60
 
56
61
  def merge(other_scheme)
57
- self.class.new(**to_h.merge(other_scheme.to_h))
62
+ self.class.new(**to_h, **other_scheme.to_h)
58
63
  end
59
64
 
60
65
  def to_h
@@ -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
@@ -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: {})
@@ -213,26 +214,16 @@ module ActiveRecord
213
214
  attr_reader :name, :mapping
214
215
  end
215
216
 
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) }
217
+ def enum(name, values = nil, **options)
218
+ values, options = options, {} unless values
219
+ _enum(name, values, **options)
226
220
  end
227
221
 
228
222
  private
229
- def inherited(base)
230
- base.defined_enums = defined_enums.deep_dup
231
- super
232
- end
233
-
234
223
  def _enum(name, values, prefix: nil, suffix: nil, scopes: true, instance_methods: true, validate: false, **options)
235
- assert_valid_enum_definition_values(values)
224
+ values = assert_valid_enum_definition_values(values)
225
+ assert_valid_enum_options(options)
226
+
236
227
  # statuses = { }
237
228
  enum_values = ActiveSupport::HashWithIndifferentAccess.new
238
229
  name = name.to_s
@@ -245,7 +236,9 @@ module ActiveRecord
245
236
  detect_enum_conflict!(name, name)
246
237
  detect_enum_conflict!(name, "#{name}=")
247
238
 
248
- attribute(name, **options) do |subtype|
239
+ attribute(name, **options)
240
+
241
+ decorate_attributes([name]) do |_name, subtype|
249
242
  if subtype == ActiveModel::Type.default_value
250
243
  raise "Undeclared attribute type for enum '#{name}' in #{self.name}. Enums must be" \
251
244
  " backed by a database column or declared with an explicit type" \
@@ -294,6 +287,11 @@ module ActiveRecord
294
287
  enum_values.freeze
295
288
  end
296
289
 
290
+ def inherited(base)
291
+ base.defined_enums = defined_enums.deep_dup
292
+ super
293
+ end
294
+
297
295
  class EnumMethods < Module # :nodoc:
298
296
  def initialize(klass)
299
297
  @klass = klass
@@ -344,6 +342,20 @@ module ActiveRecord
344
342
  if values.keys.any?(&:blank?)
345
343
  raise ArgumentError, "Enum values #{values} must not contain a blank name."
346
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, Float, true, false, nil
353
+ # noop
354
+ else
355
+ raise ArgumentError, "Enum values #{values} must be only booleans, integers, floats, symbols or strings, got: #{value.class}"
356
+ end
357
+ end
358
+
347
359
  when Array
348
360
  if values.empty?
349
361
  raise ArgumentError, "Enum values #{values} must not be empty."
@@ -359,29 +371,38 @@ module ActiveRecord
359
371
  else
360
372
  raise ArgumentError, "Enum values #{values} must be either a non-empty hash or an array."
361
373
  end
374
+
375
+ values
376
+ end
377
+
378
+ def assert_valid_enum_options(options)
379
+ invalid_keys = options.keys & %i[_prefix _suffix _scopes _default _instance_methods]
380
+ unless invalid_keys.empty?
381
+ raise ArgumentError, "invalid option(s): #{invalid_keys.map(&:inspect).join(", ")}. Valid options are: :prefix, :suffix, :scopes, :default, :instance_methods, and :validate."
382
+ end
362
383
  end
363
384
 
364
385
  ENUM_CONFLICT_MESSAGE = \
365
386
  "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
366
- "this will generate a %{type} method \"%{method}\", which is already defined " \
387
+ "this will generate %{type} method \"%{method}\", which is already defined " \
367
388
  "by %{source}."
368
389
  private_constant :ENUM_CONFLICT_MESSAGE
369
390
 
370
391
  def detect_enum_conflict!(enum_name, method_name, klass_method = false)
371
392
  if klass_method && dangerous_class_method?(method_name)
372
- raise_conflict_error(enum_name, method_name, type: "class")
393
+ raise_conflict_error(enum_name, method_name, "a class")
373
394
  elsif klass_method && method_defined_within?(method_name, Relation)
374
- 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)
375
396
  elsif klass_method && method_name.to_sym == :id
376
- raise_conflict_error(enum_name, method_name)
397
+ raise_conflict_error(enum_name, method_name, "an instance")
377
398
  elsif !klass_method && dangerous_attribute_method?(method_name)
378
- raise_conflict_error(enum_name, method_name)
399
+ raise_conflict_error(enum_name, method_name, "an instance")
379
400
  elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
380
- raise_conflict_error(enum_name, method_name, source: "another enum")
401
+ raise_conflict_error(enum_name, method_name, "an instance", source: "another enum")
381
402
  end
382
403
  end
383
404
 
384
- 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")
385
406
  raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
386
407
  enum: enum_name,
387
408
  klass: name,