activerecord 7.1.4.1 → 7.2.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 (189) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +643 -2274
  3. data/README.rdoc +15 -15
  4. data/examples/performance.rb +2 -2
  5. data/lib/active_record/association_relation.rb +1 -1
  6. data/lib/active_record/associations/alias_tracker.rb +25 -19
  7. data/lib/active_record/associations/association.rb +15 -8
  8. data/lib/active_record/associations/belongs_to_association.rb +14 -7
  9. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +3 -2
  10. data/lib/active_record/associations/builder/belongs_to.rb +1 -0
  11. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +2 -2
  12. data/lib/active_record/associations/builder/has_many.rb +3 -4
  13. data/lib/active_record/associations/builder/has_one.rb +3 -4
  14. data/lib/active_record/associations/collection_association.rb +7 -1
  15. data/lib/active_record/associations/collection_proxy.rb +14 -1
  16. data/lib/active_record/associations/errors.rb +265 -0
  17. data/lib/active_record/associations/has_many_association.rb +1 -1
  18. data/lib/active_record/associations/has_many_through_association.rb +7 -1
  19. data/lib/active_record/associations/join_dependency/join_association.rb +30 -27
  20. data/lib/active_record/associations/join_dependency.rb +4 -4
  21. data/lib/active_record/associations/nested_error.rb +47 -0
  22. data/lib/active_record/associations/preloader/association.rb +2 -1
  23. data/lib/active_record/associations/preloader/branch.rb +7 -1
  24. data/lib/active_record/associations/preloader/through_association.rb +1 -3
  25. data/lib/active_record/associations/singular_association.rb +6 -0
  26. data/lib/active_record/associations/through_association.rb +1 -1
  27. data/lib/active_record/associations.rb +59 -292
  28. data/lib/active_record/attribute_assignment.rb +0 -2
  29. data/lib/active_record/attribute_methods/composite_primary_key.rb +84 -0
  30. data/lib/active_record/attribute_methods/primary_key.rb +23 -55
  31. data/lib/active_record/attribute_methods/read.rb +1 -13
  32. data/lib/active_record/attribute_methods/serialization.rb +4 -24
  33. data/lib/active_record/attribute_methods/time_zone_conversion.rb +11 -6
  34. data/lib/active_record/attribute_methods.rb +54 -63
  35. data/lib/active_record/attributes.rb +61 -47
  36. data/lib/active_record/autosave_association.rb +12 -29
  37. data/lib/active_record/base.rb +2 -3
  38. data/lib/active_record/callbacks.rb +1 -1
  39. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +24 -107
  40. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +1 -0
  41. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +270 -65
  42. data/lib/active_record/connection_adapters/abstract/database_statements.rb +34 -17
  43. data/lib/active_record/connection_adapters/abstract/query_cache.rb +189 -74
  44. data/lib/active_record/connection_adapters/abstract/quoting.rb +65 -91
  45. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  46. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +15 -6
  47. data/lib/active_record/connection_adapters/abstract/transaction.rb +125 -62
  48. data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -44
  49. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +40 -10
  50. data/lib/active_record/connection_adapters/mysql/database_statements.rb +9 -1
  51. data/lib/active_record/connection_adapters/mysql/quoting.rb +43 -48
  52. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +6 -0
  53. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +16 -15
  54. data/lib/active_record/connection_adapters/mysql2_adapter.rb +5 -23
  55. data/lib/active_record/connection_adapters/pool_config.rb +7 -6
  56. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +27 -4
  57. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +1 -1
  58. data/lib/active_record/connection_adapters/postgresql/oid/interval.rb +1 -1
  59. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +14 -4
  60. data/lib/active_record/connection_adapters/postgresql/quoting.rb +58 -58
  61. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -0
  62. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +17 -11
  63. data/lib/active_record/connection_adapters/postgresql_adapter.rb +29 -24
  64. data/lib/active_record/connection_adapters/schema_cache.rb +123 -128
  65. data/lib/active_record/connection_adapters/sqlite3/column.rb +14 -1
  66. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +10 -6
  67. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +44 -46
  68. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  69. data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +13 -0
  70. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +16 -0
  71. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +25 -2
  72. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +125 -75
  73. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +15 -15
  74. data/lib/active_record/connection_adapters/trilogy_adapter.rb +19 -48
  75. data/lib/active_record/connection_adapters.rb +121 -0
  76. data/lib/active_record/connection_handling.rb +56 -41
  77. data/lib/active_record/core.rb +86 -38
  78. data/lib/active_record/counter_cache.rb +18 -9
  79. data/lib/active_record/database_configurations/connection_url_resolver.rb +8 -3
  80. data/lib/active_record/database_configurations/database_config.rb +19 -4
  81. data/lib/active_record/database_configurations/hash_config.rb +38 -34
  82. data/lib/active_record/database_configurations/url_config.rb +20 -1
  83. data/lib/active_record/database_configurations.rb +1 -1
  84. data/lib/active_record/delegated_type.rb +24 -0
  85. data/lib/active_record/dynamic_matchers.rb +2 -2
  86. data/lib/active_record/encryption/encryptable_record.rb +3 -3
  87. data/lib/active_record/encryption/encrypted_attribute_type.rb +24 -4
  88. data/lib/active_record/encryption/encryptor.rb +18 -3
  89. data/lib/active_record/encryption/key_provider.rb +1 -1
  90. data/lib/active_record/encryption/message_pack_message_serializer.rb +76 -0
  91. data/lib/active_record/encryption/message_serializer.rb +4 -0
  92. data/lib/active_record/encryption/null_encryptor.rb +4 -0
  93. data/lib/active_record/encryption/read_only_null_encryptor.rb +4 -0
  94. data/lib/active_record/encryption.rb +2 -0
  95. data/lib/active_record/enum.rb +19 -2
  96. data/lib/active_record/errors.rb +46 -20
  97. data/lib/active_record/explain.rb +13 -24
  98. data/lib/active_record/fixtures.rb +37 -31
  99. data/lib/active_record/future_result.rb +8 -4
  100. data/lib/active_record/gem_version.rb +2 -2
  101. data/lib/active_record/inheritance.rb +4 -2
  102. data/lib/active_record/insert_all.rb +18 -15
  103. data/lib/active_record/integration.rb +4 -1
  104. data/lib/active_record/internal_metadata.rb +48 -34
  105. data/lib/active_record/locking/optimistic.rb +7 -6
  106. data/lib/active_record/log_subscriber.rb +0 -21
  107. data/lib/active_record/marshalling.rb +4 -1
  108. data/lib/active_record/message_pack.rb +1 -1
  109. data/lib/active_record/migration/command_recorder.rb +2 -3
  110. data/lib/active_record/migration/compatibility.rb +5 -3
  111. data/lib/active_record/migration/default_strategy.rb +4 -5
  112. data/lib/active_record/migration/pending_migration_connection.rb +2 -2
  113. data/lib/active_record/migration.rb +85 -76
  114. data/lib/active_record/model_schema.rb +32 -68
  115. data/lib/active_record/nested_attributes.rb +24 -5
  116. data/lib/active_record/normalization.rb +3 -7
  117. data/lib/active_record/persistence.rb +30 -352
  118. data/lib/active_record/query_cache.rb +19 -8
  119. data/lib/active_record/query_logs.rb +15 -0
  120. data/lib/active_record/querying.rb +21 -9
  121. data/lib/active_record/railtie.rb +42 -57
  122. data/lib/active_record/railties/controller_runtime.rb +13 -4
  123. data/lib/active_record/railties/databases.rake +40 -43
  124. data/lib/active_record/reflection.rb +98 -36
  125. data/lib/active_record/relation/batches/batch_enumerator.rb +15 -2
  126. data/lib/active_record/relation/batches.rb +14 -8
  127. data/lib/active_record/relation/calculations.rb +96 -63
  128. data/lib/active_record/relation/delegation.rb +8 -11
  129. data/lib/active_record/relation/finder_methods.rb +16 -2
  130. data/lib/active_record/relation/merger.rb +4 -6
  131. data/lib/active_record/relation/predicate_builder/array_handler.rb +2 -2
  132. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -3
  133. data/lib/active_record/relation/predicate_builder.rb +3 -3
  134. data/lib/active_record/relation/query_methods.rb +224 -58
  135. data/lib/active_record/relation/record_fetch_warning.rb +3 -0
  136. data/lib/active_record/relation/spawn_methods.rb +2 -18
  137. data/lib/active_record/relation/where_clause.rb +7 -19
  138. data/lib/active_record/relation.rb +496 -72
  139. data/lib/active_record/result.rb +31 -44
  140. data/lib/active_record/runtime_registry.rb +39 -0
  141. data/lib/active_record/sanitization.rb +24 -19
  142. data/lib/active_record/schema.rb +8 -6
  143. data/lib/active_record/schema_dumper.rb +19 -9
  144. data/lib/active_record/schema_migration.rb +30 -14
  145. data/lib/active_record/scoping/named.rb +1 -0
  146. data/lib/active_record/signed_id.rb +20 -1
  147. data/lib/active_record/statement_cache.rb +7 -7
  148. data/lib/active_record/table_metadata.rb +1 -10
  149. data/lib/active_record/tasks/database_tasks.rb +81 -42
  150. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  151. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  152. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -1
  153. data/lib/active_record/test_fixtures.rb +86 -89
  154. data/lib/active_record/testing/query_assertions.rb +121 -0
  155. data/lib/active_record/timestamp.rb +2 -2
  156. data/lib/active_record/token_for.rb +22 -12
  157. data/lib/active_record/touch_later.rb +1 -1
  158. data/lib/active_record/transaction.rb +132 -0
  159. data/lib/active_record/transactions.rb +70 -14
  160. data/lib/active_record/translation.rb +0 -2
  161. data/lib/active_record/type/serialized.rb +1 -3
  162. data/lib/active_record/type_caster/connection.rb +4 -4
  163. data/lib/active_record/validations/associated.rb +9 -3
  164. data/lib/active_record/validations/uniqueness.rb +15 -10
  165. data/lib/active_record/validations.rb +4 -1
  166. data/lib/active_record.rb +148 -39
  167. data/lib/arel/alias_predication.rb +1 -1
  168. data/lib/arel/collectors/bind.rb +2 -0
  169. data/lib/arel/collectors/composite.rb +7 -0
  170. data/lib/arel/collectors/sql_string.rb +1 -1
  171. data/lib/arel/collectors/substitute_binds.rb +1 -1
  172. data/lib/arel/nodes/binary.rb +0 -6
  173. data/lib/arel/nodes/bound_sql_literal.rb +9 -5
  174. data/lib/arel/nodes/{and.rb → nary.rb} +5 -2
  175. data/lib/arel/nodes/node.rb +4 -3
  176. data/lib/arel/nodes/sql_literal.rb +7 -0
  177. data/lib/arel/nodes.rb +2 -2
  178. data/lib/arel/predications.rb +1 -1
  179. data/lib/arel/select_manager.rb +1 -1
  180. data/lib/arel/tree_manager.rb +3 -2
  181. data/lib/arel/update_manager.rb +2 -1
  182. data/lib/arel/visitors/dot.rb +1 -0
  183. data/lib/arel/visitors/mysql.rb +9 -4
  184. data/lib/arel/visitors/postgresql.rb +1 -12
  185. data/lib/arel/visitors/sqlite.rb +25 -0
  186. data/lib/arel/visitors/to_sql.rb +29 -16
  187. data/lib/arel.rb +7 -3
  188. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt +4 -1
  189. metadata +18 -12
@@ -7,6 +7,7 @@ module ActiveRecord
7
7
 
8
8
  included do
9
9
  class_attribute :_counter_cache_columns, instance_accessor: false, default: []
10
+ class_attribute :counter_cached_association_names, instance_writer: false, default: []
10
11
  end
11
12
 
12
13
  module ClassMethods
@@ -181,14 +182,26 @@ module ActiveRecord
181
182
  def counter_cache_column?(name) # :nodoc:
182
183
  _counter_cache_columns.include?(name)
183
184
  end
185
+
186
+ def load_schema! # :nodoc:
187
+ super
188
+
189
+ association_names = _reflections.filter_map do |name, reflection|
190
+ next unless reflection.belongs_to? && reflection.counter_cache_column
191
+
192
+ name.to_sym
193
+ end
194
+
195
+ self.counter_cached_association_names |= association_names
196
+ end
184
197
  end
185
198
 
186
199
  private
187
200
  def _create_record(attribute_names = self.attribute_names)
188
201
  id = super
189
202
 
190
- each_counter_cached_associations do |association|
191
- association.increment_counters
203
+ counter_cached_association_names.each do |association_name|
204
+ association(association_name).increment_counters
192
205
  end
193
206
 
194
207
  id
@@ -198,7 +211,9 @@ module ActiveRecord
198
211
  affected_rows = super
199
212
 
200
213
  if affected_rows > 0
201
- each_counter_cached_associations do |association|
214
+ counter_cached_association_names.each do |association_name|
215
+ association = association(association_name)
216
+
202
217
  unless destroyed_by_association && _foreign_keys_equal?(destroyed_by_association.foreign_key, association.reflection.foreign_key)
203
218
  association.decrement_counters
204
219
  end
@@ -208,12 +223,6 @@ module ActiveRecord
208
223
  affected_rows
209
224
  end
210
225
 
211
- def each_counter_cached_associations
212
- _reflections.each do |name, reflection|
213
- yield association(name.to_sym) if reflection.belongs_to? && reflection.counter_cache_column
214
- end
215
- end
216
-
217
226
  def _foreign_keys_equal?(fkey1, fkey2)
218
227
  fkey1 == fkey2 || Array(fkey1).map(&:to_sym) == Array(fkey2).map(&:to_sym)
219
228
  end
@@ -25,8 +25,7 @@ module ActiveRecord
25
25
  def initialize(url)
26
26
  raise "Database URL cannot be empty" if url.blank?
27
27
  @uri = uri_parser.parse(url)
28
- @adapter = @uri.scheme && @uri.scheme.tr("-", "_")
29
- @adapter = "postgresql" if @adapter == "postgres"
28
+ @adapter = resolved_adapter
30
29
 
31
30
  if @uri.opaque
32
31
  @uri.opaque, @query = @uri.opaque.split("?", 2)
@@ -46,7 +45,7 @@ module ActiveRecord
46
45
  attr_reader :uri
47
46
 
48
47
  def uri_parser
49
- @uri_parser ||= URI::Parser.new
48
+ @uri_parser ||= URI::RFC2396_Parser.new
50
49
  end
51
50
 
52
51
  # Converts the query parameters of the URI into a hash.
@@ -80,6 +79,12 @@ module ActiveRecord
80
79
  end
81
80
  end
82
81
 
82
+ def resolved_adapter
83
+ adapter = uri.scheme && @uri.scheme.tr("-", "_")
84
+ adapter = ActiveRecord.protocol_adapters[adapter] || adapter
85
+ adapter
86
+ end
87
+
83
88
  # Returns name of the database.
84
89
  def database_from_path
85
90
  if @adapter == "sqlite3"
@@ -11,14 +11,25 @@ module ActiveRecord
11
11
  def initialize(env_name, name)
12
12
  @env_name = env_name
13
13
  @name = name
14
+ @adapter_class = nil
14
15
  end
15
16
 
16
- def adapter_method
17
- "#{adapter}_connection"
17
+ def adapter_class
18
+ @adapter_class ||= ActiveRecord::ConnectionAdapters.resolve(adapter)
18
19
  end
19
20
 
20
- def adapter_class_method
21
- "#{adapter}_adapter_class"
21
+ def inspect # :nodoc:
22
+ "#<#{self.class.name} env_name=#{@env_name} name=#{@name} adapter_class=#{adapter_class}>"
23
+ end
24
+
25
+ def new_connection
26
+ adapter_class.new(configuration_hash)
27
+ end
28
+
29
+ def validate!
30
+ adapter_class if adapter
31
+
32
+ true
22
33
  end
23
34
 
24
35
  def host
@@ -84,6 +95,10 @@ module ActiveRecord
84
95
  def schema_cache_path
85
96
  raise NotImplementedError
86
97
  end
98
+
99
+ def use_metadata_table?
100
+ raise NotImplementedError
101
+ end
87
102
  end
88
103
  end
89
104
  end
@@ -1,53 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActiveRecord
4
6
  class DatabaseConfigurations
5
- # = Active Record Database Hash Config
7
+ # # Active Record Database Hash Config
6
8
  #
7
- # A +HashConfig+ object is created for each database configuration entry that
8
- # is created from a hash.
9
+ # A `HashConfig` object is created for each database configuration entry that is
10
+ # created from a hash.
9
11
  #
10
12
  # A hash config:
11
13
  #
12
- # { "development" => { "database" => "db_name" } }
14
+ # { "development" => { "database" => "db_name" } }
13
15
  #
14
16
  # Becomes:
15
17
  #
16
- # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10
17
- # @env_name="development", @name="primary", @config={database: "db_name"}>
18
+ # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10
19
+ # @env_name="development", @name="primary", @config={database: "db_name"}>
18
20
  #
19
21
  # See ActiveRecord::DatabaseConfigurations for more info.
20
22
  class HashConfig < DatabaseConfig
21
23
  attr_reader :configuration_hash
22
24
 
23
-
24
- # Initialize a new +HashConfig+ object
25
+ # Initialize a new `HashConfig` object
26
+ #
27
+ # #### Parameters
25
28
  #
26
- # ==== Options
29
+ # * `env_name` - The Rails environment, i.e. "development".
30
+ # * `name` - The db config name. In a standard two-tier database configuration
31
+ # this will default to "primary". In a multiple database three-tier database
32
+ # configuration this corresponds to the name used in the second tier, for
33
+ # example "primary_readonly".
34
+ # * `configuration_hash` - The config hash. This is the hash that contains the
35
+ # database adapter, name, and other important information for database
36
+ # connections.
27
37
  #
28
- # * <tt>:env_name</tt> - The \Rails environment, i.e. "development".
29
- # * <tt>:name</tt> - The db config name. In a standard two-tier
30
- # database configuration this will default to "primary". In a multiple
31
- # database three-tier database configuration this corresponds to the name
32
- # used in the second tier, for example "primary_readonly".
33
- # * <tt>:config</tt> - The config hash. This is the hash that contains the
34
- # database adapter, name, and other important information for database
35
- # connections.
36
38
  def initialize(env_name, name, configuration_hash)
37
39
  super(env_name, name)
38
40
  @configuration_hash = configuration_hash.symbolize_keys.freeze
39
41
  end
40
42
 
41
43
  # Determines whether a database configuration is for a replica / readonly
42
- # connection. If the +replica+ key is present in the config, +replica?+ will
43
- # return +true+.
44
+ # connection. If the `replica` key is present in the config, `replica?` will
45
+ # return `true`.
44
46
  def replica?
45
47
  configuration_hash[:replica]
46
48
  end
47
49
 
48
- # The migrations paths for a database configuration. If the
49
- # +migrations_paths+ key is present in the config, +migrations_paths+
50
- # will return its value.
50
+ # The migrations paths for a database configuration. If the `migrations_paths`
51
+ # key is present in the config, `migrations_paths` will return its value.
51
52
  def migrations_paths
52
53
  configuration_hash[:migrations_paths]
53
54
  end
@@ -92,8 +93,8 @@ module ActiveRecord
92
93
  (configuration_hash[:checkout_timeout] || 5).to_f
93
94
  end
94
95
 
95
- # +reaping_frequency+ is configurable mostly for historical reasons, but it could
96
- # also be useful if someone wants a very low +idle_timeout+.
96
+ # `reaping_frequency` is configurable mostly for historical reasons, but it
97
+ # could also be useful if someone wants a very low `idle_timeout`.
97
98
  def reaping_frequency
98
99
  configuration_hash.fetch(:reaping_frequency, 60)&.to_f
99
100
  end
@@ -104,12 +105,11 @@ module ActiveRecord
104
105
  end
105
106
 
106
107
  def adapter
107
- configuration_hash[:adapter]
108
+ configuration_hash[:adapter]&.to_s
108
109
  end
109
110
 
110
- # The path to the schema cache dump file for a database.
111
- # If omitted, the filename will be read from ENV or a
112
- # default will be derived.
111
+ # The path to the schema cache dump file for a database. If omitted, the
112
+ # filename will be read from ENV or a default will be derived.
113
113
  def schema_cache_path
114
114
  configuration_hash[:schema_cache_path]
115
115
  end
@@ -130,14 +130,14 @@ module ActiveRecord
130
130
  Base.configurations.primary?(name)
131
131
  end
132
132
 
133
- # Determines whether to dump the schema/structure files and the
134
- # filename that should be used.
133
+ # Determines whether to dump the schema/structure files and the filename that
134
+ # should be used.
135
135
  #
136
- # If +configuration_hash[:schema_dump]+ is set to +false+ or +nil+
137
- # the schema will not be dumped.
136
+ # If `configuration_hash[:schema_dump]` is set to `false` or `nil` the schema
137
+ # will not be dumped.
138
138
  #
139
- # If the config option is set that will be used. Otherwise \Rails
140
- # will generate the filename from the database config name.
139
+ # If the config option is set that will be used. Otherwise Rails will generate
140
+ # the filename from the database config name.
141
141
  def schema_dump(format = ActiveRecord.schema_format)
142
142
  if configuration_hash.key?(:schema_dump)
143
143
  if config = configuration_hash[:schema_dump]
@@ -154,6 +154,10 @@ module ActiveRecord
154
154
  !replica? && !!configuration_hash.fetch(:database_tasks, true)
155
155
  end
156
156
 
157
+ def use_metadata_table? # :nodoc:
158
+ configuration_hash.fetch(:use_metadata_table, true)
159
+ end
160
+
157
161
  private
158
162
  def schema_file_type(format)
159
163
  case format
@@ -41,10 +41,29 @@ module ActiveRecord
41
41
  super(env_name, name, configuration_hash)
42
42
 
43
43
  @url = url
44
- @configuration_hash = @configuration_hash.merge(build_url_hash).freeze
44
+ @configuration_hash = @configuration_hash.merge(build_url_hash)
45
+
46
+ if @configuration_hash[:schema_dump] == "false"
47
+ @configuration_hash[:schema_dump] = false
48
+ end
49
+
50
+ if @configuration_hash[:query_cache] == "false"
51
+ @configuration_hash[:query_cache] = false
52
+ end
53
+
54
+ to_boolean!(@configuration_hash, :replica)
55
+ to_boolean!(@configuration_hash, :database_tasks)
56
+
57
+ @configuration_hash.freeze
45
58
  end
46
59
 
47
60
  private
61
+ def to_boolean!(configuration_hash, key)
62
+ if configuration_hash[key].is_a?(String)
63
+ configuration_hash[key] = configuration_hash[key] != "false"
64
+ end
65
+ end
66
+
48
67
  # Return a Hash that can be merged into the main config that represents
49
68
  # the passed in url
50
69
  def build_url_hash
@@ -113,7 +113,7 @@ module ActiveRecord
113
113
 
114
114
  if name
115
115
  configs.find do |db_config|
116
- db_config.name == name
116
+ db_config.name == name.to_s
117
117
  end
118
118
  else
119
119
  configs
@@ -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
@@ -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
@@ -84,7 +84,7 @@ module ActiveRecord
84
84
  def encrypt_attribute(name, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], **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
89
  downcase: downcase, ignore_case: ignore_case, previous: previous, **context_properties
90
90
 
@@ -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(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,14 @@ 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
153
173
  end
154
174
  end
155
175
  end
@@ -12,6 +12,14 @@ module ActiveRecord
12
12
  # It interacts with a KeyProvider for getting the keys, and delegate to
13
13
  # ActiveRecord::Encryption::Cipher the actual encryption algorithm.
14
14
  class Encryptor
15
+ # === Options
16
+ #
17
+ # * <tt>:compress</tt> - Boolean indicating whether records should be compressed before encryption.
18
+ # Defaults to +true+.
19
+ def initialize(compress: true)
20
+ @compress = compress
21
+ end
22
+
15
23
  # Encrypts +clean_text+ and returns the encrypted result
16
24
  #
17
25
  # Internally, it will:
@@ -38,7 +46,7 @@ module ActiveRecord
38
46
  serialize_message build_encrypted_message(clear_text, key_provider: key_provider, cipher_options: cipher_options)
39
47
  end
40
48
 
41
- # Decrypts a +clean_text+ and returns the result as clean text
49
+ # Decrypts an +encrypted_text+ and returns the result as clean text
42
50
  #
43
51
  # === Options
44
52
  #
@@ -66,6 +74,10 @@ module ActiveRecord
66
74
  false
67
75
  end
68
76
 
77
+ def binary?
78
+ serializer.binary?
79
+ end
80
+
69
81
  private
70
82
  DECRYPT_ERRORS = [OpenSSL::Cipher::CipherError, Errors::EncryptedContentIntegrity, Errors::Decryption]
71
83
  ENCODING_ERRORS = [EncodingError, Errors::Encoding]
@@ -100,7 +112,6 @@ module ActiveRecord
100
112
  end
101
113
 
102
114
  def deserialize_message(message)
103
- raise Errors::Encoding unless message.is_a?(String)
104
115
  serializer.load message
105
116
  rescue ArgumentError, TypeError, Errors::ForbiddenClass
106
117
  raise Errors::Encoding
@@ -112,13 +123,17 @@ module ActiveRecord
112
123
 
113
124
  # Under certain threshold, ZIP compression is actually worse that not compressing
114
125
  def compress_if_worth_it(string)
115
- if string.bytesize > THRESHOLD_TO_JUSTIFY_COMPRESSION
126
+ if compress? && string.bytesize > THRESHOLD_TO_JUSTIFY_COMPRESSION
116
127
  [compress(string), true]
117
128
  else
118
129
  [string, false]
119
130
  end
120
131
  end
121
132
 
133
+ def compress?
134
+ @compress
135
+ end
136
+
122
137
  def compress(data)
123
138
  Zlib::Deflate.deflate(data).tap do |compressed_data|
124
139
  compressed_data.force_encoding(data.encoding)
@@ -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
@@ -53,4 +53,6 @@ module ActiveRecord
53
53
  Cipher.eager_load!
54
54
  end
55
55
  end
56
+
57
+ ActiveSupport.run_load_hooks :active_record_encryption, Encryption
56
58
  end