activerecord 7.2.2.1 → 8.0.5

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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +523 -677
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/association_relation.rb +1 -0
  5. data/lib/active_record/associations/alias_tracker.rb +6 -4
  6. data/lib/active_record/associations/association.rb +34 -10
  7. data/lib/active_record/associations/belongs_to_association.rb +18 -2
  8. data/lib/active_record/associations/builder/association.rb +7 -6
  9. data/lib/active_record/associations/collection_association.rb +10 -8
  10. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  11. data/lib/active_record/associations/has_many_through_association.rb +3 -2
  12. data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
  13. data/lib/active_record/associations/join_dependency.rb +2 -2
  14. data/lib/active_record/associations/preloader/association.rb +2 -2
  15. data/lib/active_record/associations/preloader/batch.rb +7 -1
  16. data/lib/active_record/associations/singular_association.rb +8 -3
  17. data/lib/active_record/associations.rb +34 -4
  18. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  19. data/lib/active_record/attribute_methods/primary_key.rb +4 -8
  20. data/lib/active_record/attribute_methods/query.rb +34 -0
  21. data/lib/active_record/attribute_methods/serialization.rb +1 -1
  22. data/lib/active_record/attribute_methods/time_zone_conversion.rb +2 -12
  23. data/lib/active_record/attribute_methods.rb +24 -19
  24. data/lib/active_record/attributes.rb +37 -26
  25. data/lib/active_record/autosave_association.rb +91 -39
  26. data/lib/active_record/base.rb +2 -2
  27. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +34 -25
  28. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  29. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  30. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +55 -47
  31. data/lib/active_record/connection_adapters/abstract/database_statements.rb +91 -44
  32. data/lib/active_record/connection_adapters/abstract/query_cache.rb +25 -8
  33. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  34. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -5
  35. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +12 -3
  36. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +40 -10
  37. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  38. data/lib/active_record/connection_adapters/abstract_adapter.rb +92 -70
  39. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +35 -46
  40. data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -9
  41. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  42. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +50 -45
  43. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +84 -94
  44. data/lib/active_record/connection_adapters/mysql2_adapter.rb +9 -9
  45. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  46. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -2
  47. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +72 -43
  48. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
  49. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  50. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
  51. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -11
  52. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -24
  53. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +3 -2
  54. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +60 -17
  55. data/lib/active_record/connection_adapters/postgresql_adapter.rb +61 -99
  56. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  57. data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
  58. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +80 -100
  59. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -0
  60. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  61. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +27 -2
  62. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +10 -1
  63. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +63 -17
  64. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  65. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  66. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -18
  67. data/lib/active_record/connection_adapters.rb +0 -56
  68. data/lib/active_record/connection_handling.rb +35 -9
  69. data/lib/active_record/core.rb +57 -22
  70. data/lib/active_record/counter_cache.rb +1 -1
  71. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
  72. data/lib/active_record/database_configurations/database_config.rb +4 -0
  73. data/lib/active_record/database_configurations/hash_config.rb +16 -2
  74. data/lib/active_record/delegated_type.rb +18 -18
  75. data/lib/active_record/encryption/config.rb +3 -1
  76. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  77. data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
  78. data/lib/active_record/encryption/encryptor.rb +37 -28
  79. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  80. data/lib/active_record/encryption/scheme.rb +8 -1
  81. data/lib/active_record/enum.rb +22 -34
  82. data/lib/active_record/errors.rb +16 -8
  83. data/lib/active_record/fixture_set/table_row.rb +19 -2
  84. data/lib/active_record/fixtures.rb +0 -2
  85. data/lib/active_record/future_result.rb +15 -9
  86. data/lib/active_record/gem_version.rb +4 -4
  87. data/lib/active_record/insert_all.rb +3 -3
  88. data/lib/active_record/locking/optimistic.rb +1 -1
  89. data/lib/active_record/log_subscriber.rb +5 -11
  90. data/lib/active_record/migration/command_recorder.rb +32 -12
  91. data/lib/active_record/migration/compatibility.rb +5 -2
  92. data/lib/active_record/migration.rb +40 -43
  93. data/lib/active_record/model_schema.rb +3 -4
  94. data/lib/active_record/nested_attributes.rb +4 -6
  95. data/lib/active_record/persistence.rb +128 -130
  96. data/lib/active_record/query_logs.rb +106 -50
  97. data/lib/active_record/query_logs_formatter.rb +17 -28
  98. data/lib/active_record/querying.rb +12 -12
  99. data/lib/active_record/railtie.rb +4 -28
  100. data/lib/active_record/railties/databases.rake +11 -34
  101. data/lib/active_record/reflection.rb +18 -21
  102. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  103. data/lib/active_record/relation/batches.rb +132 -72
  104. data/lib/active_record/relation/calculations.rb +71 -65
  105. data/lib/active_record/relation/delegation.rb +25 -14
  106. data/lib/active_record/relation/finder_methods.rb +32 -31
  107. data/lib/active_record/relation/merger.rb +8 -8
  108. data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
  109. data/lib/active_record/relation/predicate_builder/association_query_value.rb +2 -0
  110. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  111. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  112. data/lib/active_record/relation/predicate_builder.rb +13 -0
  113. data/lib/active_record/relation/query_attribute.rb +1 -1
  114. data/lib/active_record/relation/query_methods.rb +121 -70
  115. data/lib/active_record/relation/spawn_methods.rb +7 -7
  116. data/lib/active_record/relation/where_clause.rb +9 -3
  117. data/lib/active_record/relation.rb +95 -67
  118. data/lib/active_record/result.rb +66 -4
  119. data/lib/active_record/sanitization.rb +7 -6
  120. data/lib/active_record/schema_dumper.rb +34 -11
  121. data/lib/active_record/schema_migration.rb +2 -1
  122. data/lib/active_record/scoping/named.rb +5 -2
  123. data/lib/active_record/secure_token.rb +3 -3
  124. data/lib/active_record/signed_id.rb +7 -6
  125. data/lib/active_record/statement_cache.rb +14 -14
  126. data/lib/active_record/store.rb +7 -3
  127. data/lib/active_record/table_metadata.rb +1 -3
  128. data/lib/active_record/tasks/database_tasks.rb +69 -60
  129. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  130. data/lib/active_record/tasks/postgresql_database_tasks.rb +9 -1
  131. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  132. data/lib/active_record/test_databases.rb +1 -1
  133. data/lib/active_record/test_fixtures.rb +12 -0
  134. data/lib/active_record/token_for.rb +1 -1
  135. data/lib/active_record/transactions.rb +8 -7
  136. data/lib/active_record/type/serialized.rb +5 -0
  137. data/lib/active_record/validations/uniqueness.rb +8 -8
  138. data/lib/active_record.rb +22 -49
  139. data/lib/arel/collectors/bind.rb +2 -2
  140. data/lib/arel/collectors/sql_string.rb +1 -1
  141. data/lib/arel/collectors/substitute_binds.rb +2 -2
  142. data/lib/arel/crud.rb +2 -0
  143. data/lib/arel/delete_manager.rb +5 -0
  144. data/lib/arel/nodes/binary.rb +1 -1
  145. data/lib/arel/nodes/delete_statement.rb +4 -2
  146. data/lib/arel/nodes/node.rb +1 -1
  147. data/lib/arel/nodes/sql_literal.rb +1 -1
  148. data/lib/arel/nodes/update_statement.rb +4 -2
  149. data/lib/arel/predications.rb +1 -3
  150. data/lib/arel/select_manager.rb +6 -2
  151. data/lib/arel/table.rb +3 -7
  152. data/lib/arel/update_manager.rb +5 -0
  153. data/lib/arel/visitors/dot.rb +2 -0
  154. data/lib/arel/visitors/to_sql.rb +3 -1
  155. metadata +11 -15
  156. data/lib/active_record/relation/record_fetch_warning.rb +0 -52
@@ -89,6 +89,7 @@ module ActiveRecord
89
89
  class_attribute :belongs_to_required_by_default, instance_accessor: false
90
90
 
91
91
  class_attribute :strict_loading_by_default, instance_accessor: false, default: false
92
+ class_attribute :strict_loading_mode, instance_accessor: false, default: :all
92
93
 
93
94
  class_attribute :has_many_inversing, instance_accessor: false, default: false
94
95
 
@@ -201,6 +202,17 @@ module ActiveRecord
201
202
  false
202
203
  end
203
204
 
205
+ # Intended to behave like `.current_preventing_writes` given the class name as input.
206
+ # See PoolConfig and ConnectionHandler::ConnectionDescriptor.
207
+ def self.preventing_writes?(class_name) # :nodoc:
208
+ connected_to_stack.reverse_each do |hash|
209
+ return hash[:prevent_writes] if !hash[:prevent_writes].nil? && hash[:klasses].include?(Base)
210
+ return hash[:prevent_writes] if !hash[:prevent_writes].nil? && hash[:klasses].any? { |klass| klass.name == class_name }
211
+ end
212
+
213
+ false
214
+ end
215
+
204
216
  def self.connected_to_stack # :nodoc:
205
217
  if connected_to_stack = ActiveSupport::IsolatedExecutionState[:active_record_connected_to_stack]
206
218
  connected_to_stack
@@ -265,7 +277,7 @@ module ActiveRecord
265
277
  return super if StatementCache.unsupported_value?(id)
266
278
 
267
279
  cached_find_by([primary_key], [id]) ||
268
- raise(RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", name, primary_key, id))
280
+ raise(RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id.inspect}", name, primary_key, id))
269
281
  end
270
282
 
271
283
  def find_by(*args) # :nodoc:
@@ -381,7 +393,7 @@ module ActiveRecord
381
393
  end
382
394
 
383
395
  def predicate_builder # :nodoc:
384
- @predicate_builder ||= PredicateBuilder.new(table_metadata)
396
+ @predicate_builder ||= PredicateBuilder.new(TableMetadata.new(self, arel_table))
385
397
  end
386
398
 
387
399
  def type_caster # :nodoc:
@@ -426,10 +438,6 @@ module ActiveRecord
426
438
  end
427
439
  end
428
440
 
429
- def table_metadata
430
- TableMetadata.new(self, arel_table)
431
- end
432
-
433
441
  def cached_find_by(keys, values)
434
442
  with_connection do |connection|
435
443
  statement = cached_find_by_statement(connection, keys) { |params|
@@ -443,8 +451,8 @@ module ActiveRecord
443
451
  where(wheres).limit(1)
444
452
  }
445
453
 
446
- begin
447
- statement.execute(values.flatten, connection, allow_retry: true).first
454
+ statement.execute(values.flatten, connection, allow_retry: true).then do |r|
455
+ r.first
448
456
  rescue TypeError
449
457
  raise ActiveRecord::StatementInvalid
450
458
  end
@@ -540,12 +548,7 @@ module ActiveRecord
540
548
 
541
549
  ##
542
550
  def initialize_dup(other) # :nodoc:
543
- @attributes = @attributes.deep_dup
544
- if self.class.composite_primary_key?
545
- @primary_key.each { |key| @attributes.reset(key) }
546
- else
547
- @attributes.reset(@primary_key)
548
- end
551
+ @attributes = init_attributes(other)
549
552
 
550
553
  _run_initialize_callbacks
551
554
 
@@ -557,6 +560,18 @@ module ActiveRecord
557
560
  super
558
561
  end
559
562
 
563
+ def init_attributes(_) # :nodoc:
564
+ attrs = @attributes.deep_dup
565
+
566
+ if self.class.composite_primary_key?
567
+ @primary_key.each { |key| attrs.reset(key) }
568
+ else
569
+ attrs.reset(@primary_key)
570
+ end
571
+
572
+ attrs
573
+ end
574
+
560
575
  # Populate +coder+ with attributes about this record that should be
561
576
  # serialized. The structure of +coder+ defined in this method is
562
577
  # guaranteed to match the structure of +coder+ passed to the #init_with
@@ -585,7 +600,7 @@ module ActiveRecord
585
600
  #
586
601
  # topic = Topic.new(title: "Budget", author_name: "Jason")
587
602
  # topic.slice(:title, :author_name)
588
- # => { "title" => "Budget", "author_name" => "Jason" }
603
+ # # => { "title" => "Budget", "author_name" => "Jason" }
589
604
  #
590
605
  #--
591
606
  # Implemented by ActiveModel::Access#slice.
@@ -599,7 +614,7 @@ module ActiveRecord
599
614
  #
600
615
  # topic = Topic.new(title: "Budget", author_name: "Jason")
601
616
  # topic.values_at(:title, :author_name)
602
- # => ["Budget", "Jason"]
617
+ # # => ["Budget", "Jason"]
603
618
  #
604
619
  #--
605
620
  # Implemented by ActiveModel::Access#values_at.
@@ -676,12 +691,14 @@ module ActiveRecord
676
691
  # Sets the record to strict_loading mode. This will raise an error
677
692
  # if the record tries to lazily load an association.
678
693
  #
694
+ # NOTE: Strict loading is disabled during validation in order to let the record validate its association.
695
+ #
679
696
  # user = User.first
680
697
  # user.strict_loading! # => true
681
698
  # user.address.city
682
- # => ActiveRecord::StrictLoadingViolationError
699
+ # # => ActiveRecord::StrictLoadingViolationError
683
700
  # user.comments.to_a
684
- # => ActiveRecord::StrictLoadingViolationError
701
+ # # => ActiveRecord::StrictLoadingViolationError
685
702
  #
686
703
  # ==== Parameters
687
704
  #
@@ -701,7 +718,7 @@ module ActiveRecord
701
718
  # user.address.city # => "Tatooine"
702
719
  # user.comments.to_a # => [#<Comment:0x00...]
703
720
  # user.comments.first.ratings.to_a
704
- # => ActiveRecord::StrictLoadingViolationError
721
+ # # => ActiveRecord::StrictLoadingViolationError
705
722
  def strict_loading!(value = true, mode: :all)
706
723
  unless [:all, :n_plus_one_only].include?(mode)
707
724
  raise ArgumentError, "The :mode option must be one of [:all, :n_plus_one_only] but #{mode.inspect} was provided."
@@ -723,11 +740,29 @@ module ActiveRecord
723
740
  @strict_loading_mode == :all
724
741
  end
725
742
 
726
- # Marks this record as read only.
743
+ # Prevents records from being written to the database:
744
+ #
745
+ # customer = Customer.new
746
+ # customer.readonly!
747
+ # customer.save # raises ActiveRecord::ReadOnlyRecord
727
748
  #
728
749
  # customer = Customer.first
729
750
  # customer.readonly!
730
- # customer.save # Raises an ActiveRecord::ReadOnlyRecord
751
+ # customer.update(name: 'New Name') # raises ActiveRecord::ReadOnlyRecord
752
+ #
753
+ # Read-only records cannot be deleted from the database either:
754
+ #
755
+ # customer = Customer.first
756
+ # customer.readonly!
757
+ # customer.destroy # raises ActiveRecord::ReadOnlyRecord
758
+ #
759
+ # Please, note that the objects themselves are still mutable in memory:
760
+ #
761
+ # customer = Customer.new
762
+ # customer.readonly!
763
+ # customer.name = 'New Name' # OK
764
+ #
765
+ # but you won't be able to persist the changes.
731
766
  def readonly!
732
767
  @readonly = true
733
768
  end
@@ -810,7 +845,7 @@ module ActiveRecord
810
845
 
811
846
  @primary_key = klass.primary_key
812
847
  @strict_loading = klass.strict_loading_by_default
813
- @strict_loading_mode = :all
848
+ @strict_loading_mode = klass.strict_loading_mode
814
849
 
815
850
  klass.define_attribute_methods
816
851
  end
@@ -28,7 +28,7 @@ module ActiveRecord
28
28
  # # For the Post with id #1, reset the comments_count
29
29
  # Post.reset_counters(1, :comments)
30
30
  #
31
- # # Like above, but also touch the +updated_at+ and/or +updated_on+
31
+ # # Like above, but also touch the updated_at and/or updated_on
32
32
  # # attributes.
33
33
  # Post.reset_counters(1, :comments, touch: true)
34
34
  def reset_counters(id, *counters, touch: nil)
@@ -81,7 +81,9 @@ module ActiveRecord
81
81
 
82
82
  def resolved_adapter
83
83
  adapter = uri.scheme && @uri.scheme.tr("-", "_")
84
- adapter = ActiveRecord.protocol_adapters[adapter] || adapter
84
+ if adapter && ActiveRecord.protocol_adapters[adapter]
85
+ adapter = ActiveRecord.protocol_adapters[adapter]
86
+ end
85
87
  adapter
86
88
  end
87
89
 
@@ -99,6 +99,10 @@ module ActiveRecord
99
99
  def use_metadata_table?
100
100
  raise NotImplementedError
101
101
  end
102
+
103
+ def seeds?
104
+ raise NotImplementedError
105
+ end
102
106
  end
103
107
  end
104
108
  end
@@ -130,6 +130,14 @@ module ActiveRecord
130
130
  Base.configurations.primary?(name)
131
131
  end
132
132
 
133
+ # Determines whether the db:prepare task should seed the database from db/seeds.rb.
134
+ #
135
+ # If the `seeds` key is present in the config, `seeds?` will return its value. Otherwise, it
136
+ # will return `true` for the primary database and `false` for all other configs.
137
+ def seeds?
138
+ configuration_hash.fetch(:seeds, primary?)
139
+ end
140
+
133
141
  # Determines whether to dump the schema/structure files and the filename that
134
142
  # should be used.
135
143
  #
@@ -138,7 +146,7 @@ module ActiveRecord
138
146
  #
139
147
  # If the config option is set that will be used. Otherwise Rails will generate
140
148
  # the filename from the database config name.
141
- def schema_dump(format = ActiveRecord.schema_format)
149
+ def schema_dump(format = schema_format)
142
150
  if configuration_hash.key?(:schema_dump)
143
151
  if config = configuration_hash[:schema_dump]
144
152
  config
@@ -150,6 +158,12 @@ module ActiveRecord
150
158
  end
151
159
  end
152
160
 
161
+ def schema_format # :nodoc:
162
+ format = configuration_hash[:schema_format]&.to_sym || ActiveRecord.schema_format
163
+ raise "Invalid schema format" unless [ :ruby, :sql ].include? format
164
+ format
165
+ end
166
+
153
167
  def database_tasks? # :nodoc:
154
168
  !replica? && !!configuration_hash.fetch(:database_tasks, true)
155
169
  end
@@ -160,7 +174,7 @@ module ActiveRecord
160
174
 
161
175
  private
162
176
  def schema_file_type(format)
163
- case format
177
+ case format.to_sym
164
178
  when :ruby
165
179
  "schema.rb"
166
180
  when :sql
@@ -181,16 +181,16 @@ module ActiveRecord
181
181
  # delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
182
182
  # end
183
183
  #
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
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
194
194
  #
195
195
  # You can also declare namespaced types:
196
196
  #
@@ -199,25 +199,25 @@ module ActiveRecord
199
199
  # end
200
200
  #
201
201
  # Entry.access_notice_messages
202
- # entry.access_notice_message
203
- # entry.access_notice_message?
202
+ # @entry.access_notice_message
203
+ # @entry.access_notice_message?
204
204
  #
205
- # === Options
205
+ # ==== Options
206
206
  #
207
207
  # The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc.
208
208
  # The following options can be included to specialize the behavior of the delegated type convenience methods.
209
209
  #
210
- # [:foreign_key]
210
+ # [+:foreign_key+]
211
211
  # Specify the foreign key used for the convenience methods. By default this is guessed to be the passed
212
212
  # +role+ with an "_id" suffix. So a class that defines a
213
213
  # <tt>delegated_type :entryable, types: %w[ Message Comment ]</tt> association will use "entryable_id" as
214
214
  # the default <tt>:foreign_key</tt>.
215
- # [:foreign_type]
215
+ # [+:foreign_type+]
216
216
  # Specify the column used to store the associated object's type. By default this is inferred to be the passed
217
217
  # +role+ with a "_type" suffix. A class that defines a
218
218
  # <tt>delegated_type :entryable, types: %w[ Message Comment ]</tt> association will use "entryable_type" as
219
219
  # the default <tt>:foreign_type</tt>.
220
- # [:primary_key]
220
+ # [+:primary_key+]
221
221
  # Specify the method that returns the primary key of associated object used for the convenience methods.
222
222
  # By default this is +id+.
223
223
  #
@@ -226,8 +226,8 @@ module ActiveRecord
226
226
  # delegated_type :entryable, types: %w[ Message Comment ], primary_key: :uuid, foreign_key: :entryable_uuid
227
227
  # end
228
228
  #
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
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
231
231
  def delegated_type(role, types:, **options)
232
232
  belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
233
233
  define_delegated_type_methods role, types: types, options: options
@@ -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
@@ -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+.
@@ -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
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
@@ -15,7 +15,7 @@ module ActiveRecord
15
15
  delegate :key_provider, :downcase?, :deterministic?, :previous_schemes, :with_context, :fixed?, to: :scheme
16
16
  delegate :accessor, :type, to: :cast_type
17
17
 
18
- # === Options
18
+ # ==== Options
19
19
  #
20
20
  # * <tt>:scheme</tt> - A +Scheme+ with the encryption properties for this attribute.
21
21
  # * <tt>:cast_type</tt> - A type that will be used to serialize (before encrypting) and deserialize
@@ -100,7 +100,7 @@ module ActiveRecord
100
100
  end
101
101
 
102
102
  def decrypt(value)
103
- text_to_database_type decrypt_as_text(value)
103
+ text_to_database_type decrypt_as_text(database_type_to_text(value))
104
104
  end
105
105
 
106
106
  def try_to_deserialize_with_previous_encrypted_types(value)
@@ -170,6 +170,15 @@ module ActiveRecord
170
170
  value
171
171
  end
172
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
173
182
  end
174
183
  end
175
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,33 +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
- # === Options
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+.
16
22
  #
17
- # * <tt>:compress</tt> - Boolean indicating whether records should be compressed before encryption.
18
- # Defaults to +true+.
19
- def initialize(compress: true)
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)
20
28
  @compress = compress
29
+ @compressor = compressor || ActiveRecord::Encryption.config.compressor
21
30
  end
22
31
 
23
- # Encrypts +clean_text+ and returns the encrypted result
32
+ # Encrypts +clean_text+ and returns the encrypted result.
24
33
  #
25
34
  # Internally, it will:
26
35
  #
27
- # 1. Create a new ActiveRecord::Encryption::Message
28
- # 2. Compress and encrypt +clean_text+ as the message payload
29
- # 3. Serialize it with +ActiveRecord::Encryption.message_serializer+ (+ActiveRecord::Encryption::SafeMarshal+
30
- # by default)
31
- # 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.
32
41
  #
33
- # === Options
42
+ # ==== Options
34
43
  #
35
- # [:key_provider]
44
+ # [+:key_provider+]
36
45
  # Key provider to use for the encryption operation. It will default to
37
46
  # +ActiveRecord::Encryption.key_provider+ when not provided.
38
47
  #
39
- # [:cipher_options]
48
+ # [+:cipher_options+]
40
49
  # Cipher-specific options that will be passed to the Cipher configured in
41
- # +ActiveRecord::Encryption.cipher+
50
+ # +ActiveRecord::Encryption.cipher+.
42
51
  def encrypt(clear_text, key_provider: default_key_provider, cipher_options: {})
43
52
  clear_text = force_encoding_if_needed(clear_text) if cipher_options[:deterministic]
44
53
 
@@ -46,17 +55,17 @@ module ActiveRecord
46
55
  serialize_message build_encrypted_message(clear_text, key_provider: key_provider, cipher_options: cipher_options)
47
56
  end
48
57
 
49
- # Decrypts an +encrypted_text+ and returns the result as clean text
58
+ # Decrypts an +encrypted_text+ and returns the result as clean text.
50
59
  #
51
- # === Options
60
+ # ==== Options
52
61
  #
53
- # [:key_provider]
62
+ # [+:key_provider+]
54
63
  # Key provider to use for the encryption operation. It will default to
55
- # +ActiveRecord::Encryption.key_provider+ when not provided
64
+ # +ActiveRecord::Encryption.key_provider+ when not provided.
56
65
  #
57
- # [:cipher_options]
66
+ # [+:cipher_options+]
58
67
  # Cipher-specific options that will be passed to the Cipher configured in
59
- # +ActiveRecord::Encryption.cipher+
68
+ # +ActiveRecord::Encryption.cipher+.
60
69
  def decrypt(encrypted_text, key_provider: default_key_provider, cipher_options: {})
61
70
  message = deserialize_message(encrypted_text)
62
71
  keys = key_provider.decryption_keys(message)
@@ -66,7 +75,7 @@ module ActiveRecord
66
75
  raise Errors::Decryption
67
76
  end
68
77
 
69
- # Returns whether the text is encrypted or not
78
+ # Returns whether the text is encrypted or not.
70
79
  def encrypted?(text)
71
80
  deserialize_message(text)
72
81
  true
@@ -78,6 +87,10 @@ module ActiveRecord
78
87
  serializer.binary?
79
88
  end
80
89
 
90
+ def compress? # :nodoc:
91
+ @compress
92
+ end
93
+
81
94
  private
82
95
  DECRYPT_ERRORS = [OpenSSL::Cipher::CipherError, Errors::EncryptedContentIntegrity, Errors::Decryption]
83
96
  ENCODING_ERRORS = [EncodingError, Errors::Encoding]
@@ -130,12 +143,8 @@ module ActiveRecord
130
143
  end
131
144
  end
132
145
 
133
- def compress?
134
- @compress
135
- end
136
-
137
146
  def compress(data)
138
- Zlib::Deflate.deflate(data).tap do |compressed_data|
147
+ @compressor.deflate(data).tap do |compressed_data|
139
148
  compressed_data.force_encoding(data.encoding)
140
149
  end
141
150
  end
@@ -149,7 +158,7 @@ module ActiveRecord
149
158
  end
150
159
 
151
160
  def uncompress(data)
152
- Zlib::Inflate.inflate(data).tap do |uncompressed_data|
161
+ @compressor.inflate(data).tap do |uncompressed_data|
153
162
  uncompressed_data.force_encoding(data.encoding)
154
163
  end
155
164
  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)
@@ -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