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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +523 -677
- data/README.rdoc +2 -2
- data/lib/active_record/association_relation.rb +1 -0
- data/lib/active_record/associations/alias_tracker.rb +6 -4
- data/lib/active_record/associations/association.rb +34 -10
- data/lib/active_record/associations/belongs_to_association.rb +18 -2
- data/lib/active_record/associations/builder/association.rb +7 -6
- data/lib/active_record/associations/collection_association.rb +10 -8
- data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
- data/lib/active_record/associations/has_many_through_association.rb +3 -2
- data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
- data/lib/active_record/associations/join_dependency.rb +2 -2
- data/lib/active_record/associations/preloader/association.rb +2 -2
- data/lib/active_record/associations/preloader/batch.rb +7 -1
- data/lib/active_record/associations/singular_association.rb +8 -3
- data/lib/active_record/associations.rb +34 -4
- data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
- data/lib/active_record/attribute_methods/primary_key.rb +4 -8
- data/lib/active_record/attribute_methods/query.rb +34 -0
- data/lib/active_record/attribute_methods/serialization.rb +1 -1
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +2 -12
- data/lib/active_record/attribute_methods.rb +24 -19
- data/lib/active_record/attributes.rb +37 -26
- data/lib/active_record/autosave_association.rb +91 -39
- data/lib/active_record/base.rb +2 -2
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +34 -25
- data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
- data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +55 -47
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +91 -44
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +25 -8
- data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -5
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +12 -3
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +40 -10
- data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
- data/lib/active_record/connection_adapters/abstract_adapter.rb +92 -70
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +35 -46
- data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -9
- data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +50 -45
- data/lib/active_record/connection_adapters/mysql2/database_statements.rb +84 -94
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +9 -9
- data/lib/active_record/connection_adapters/pool_config.rb +7 -7
- data/lib/active_record/connection_adapters/postgresql/column.rb +4 -2
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +72 -43
- data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +2 -4
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -11
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +20 -24
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +3 -2
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +60 -17
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +61 -99
- data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
- data/lib/active_record/connection_adapters/sqlite3/column.rb +8 -2
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +80 -100
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
- data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +27 -2
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +10 -1
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +63 -17
- data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
- data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
- data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -18
- data/lib/active_record/connection_adapters.rb +0 -56
- data/lib/active_record/connection_handling.rb +35 -9
- data/lib/active_record/core.rb +57 -22
- data/lib/active_record/counter_cache.rb +1 -1
- data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
- data/lib/active_record/database_configurations/database_config.rb +4 -0
- data/lib/active_record/database_configurations/hash_config.rb +16 -2
- data/lib/active_record/delegated_type.rb +18 -18
- data/lib/active_record/encryption/config.rb +3 -1
- data/lib/active_record/encryption/encryptable_record.rb +5 -5
- data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
- data/lib/active_record/encryption/encryptor.rb +37 -28
- data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
- data/lib/active_record/encryption/scheme.rb +8 -1
- data/lib/active_record/enum.rb +22 -34
- data/lib/active_record/errors.rb +16 -8
- data/lib/active_record/fixture_set/table_row.rb +19 -2
- data/lib/active_record/fixtures.rb +0 -2
- data/lib/active_record/future_result.rb +15 -9
- data/lib/active_record/gem_version.rb +4 -4
- data/lib/active_record/insert_all.rb +3 -3
- data/lib/active_record/locking/optimistic.rb +1 -1
- data/lib/active_record/log_subscriber.rb +5 -11
- data/lib/active_record/migration/command_recorder.rb +32 -12
- data/lib/active_record/migration/compatibility.rb +5 -2
- data/lib/active_record/migration.rb +40 -43
- data/lib/active_record/model_schema.rb +3 -4
- data/lib/active_record/nested_attributes.rb +4 -6
- data/lib/active_record/persistence.rb +128 -130
- data/lib/active_record/query_logs.rb +106 -50
- data/lib/active_record/query_logs_formatter.rb +17 -28
- data/lib/active_record/querying.rb +12 -12
- data/lib/active_record/railtie.rb +4 -28
- data/lib/active_record/railties/databases.rake +11 -34
- data/lib/active_record/reflection.rb +18 -21
- data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
- data/lib/active_record/relation/batches.rb +132 -72
- data/lib/active_record/relation/calculations.rb +71 -65
- data/lib/active_record/relation/delegation.rb +25 -14
- data/lib/active_record/relation/finder_methods.rb +32 -31
- data/lib/active_record/relation/merger.rb +8 -8
- data/lib/active_record/relation/predicate_builder/array_handler.rb +3 -1
- data/lib/active_record/relation/predicate_builder/association_query_value.rb +2 -0
- data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
- data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
- data/lib/active_record/relation/predicate_builder.rb +13 -0
- data/lib/active_record/relation/query_attribute.rb +1 -1
- data/lib/active_record/relation/query_methods.rb +121 -70
- data/lib/active_record/relation/spawn_methods.rb +7 -7
- data/lib/active_record/relation/where_clause.rb +9 -3
- data/lib/active_record/relation.rb +95 -67
- data/lib/active_record/result.rb +66 -4
- data/lib/active_record/sanitization.rb +7 -6
- data/lib/active_record/schema_dumper.rb +34 -11
- data/lib/active_record/schema_migration.rb +2 -1
- data/lib/active_record/scoping/named.rb +5 -2
- data/lib/active_record/secure_token.rb +3 -3
- data/lib/active_record/signed_id.rb +7 -6
- data/lib/active_record/statement_cache.rb +14 -14
- data/lib/active_record/store.rb +7 -3
- data/lib/active_record/table_metadata.rb +1 -3
- data/lib/active_record/tasks/database_tasks.rb +69 -60
- data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
- data/lib/active_record/tasks/postgresql_database_tasks.rb +9 -1
- data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
- data/lib/active_record/test_databases.rb +1 -1
- data/lib/active_record/test_fixtures.rb +12 -0
- data/lib/active_record/token_for.rb +1 -1
- data/lib/active_record/transactions.rb +8 -7
- data/lib/active_record/type/serialized.rb +5 -0
- data/lib/active_record/validations/uniqueness.rb +8 -8
- data/lib/active_record.rb +22 -49
- data/lib/arel/collectors/bind.rb +2 -2
- data/lib/arel/collectors/sql_string.rb +1 -1
- data/lib/arel/collectors/substitute_binds.rb +2 -2
- data/lib/arel/crud.rb +2 -0
- data/lib/arel/delete_manager.rb +5 -0
- data/lib/arel/nodes/binary.rb +1 -1
- data/lib/arel/nodes/delete_statement.rb +4 -2
- data/lib/arel/nodes/node.rb +1 -1
- data/lib/arel/nodes/sql_literal.rb +1 -1
- data/lib/arel/nodes/update_statement.rb +4 -2
- data/lib/arel/predications.rb +1 -3
- data/lib/arel/select_manager.rb +6 -2
- data/lib/arel/table.rb +3 -7
- data/lib/arel/update_manager.rb +5 -0
- data/lib/arel/visitors/dot.rb +2 -0
- data/lib/arel/visitors/to_sql.rb +3 -1
- metadata +11 -15
- data/lib/active_record/relation/record_fetch_warning.rb +0 -52
data/lib/active_record/core.rb
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
447
|
-
|
|
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 =
|
|
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
|
-
#
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|
84
|
+
if adapter && ActiveRecord.protocol_adapters[adapter]
|
|
85
|
+
adapter = ActiveRecord.protocol_adapters[adapter]
|
|
86
|
+
end
|
|
85
87
|
adapter
|
|
86
88
|
end
|
|
87
89
|
|
|
@@ -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 =
|
|
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
|
-
#
|
|
185
|
-
#
|
|
186
|
-
# Entry.messages
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
# Entry.comments
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
#
|
|
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
|
-
#
|
|
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
|
-
# [
|
|
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
|
-
# [
|
|
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
|
-
# [
|
|
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
|
-
#
|
|
230
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
18
|
-
#
|
|
19
|
-
|
|
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+
|
|
30
|
-
# by default)
|
|
31
|
-
# 4. Encode the result with
|
|
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
|
-
#
|
|
42
|
+
# ==== Options
|
|
34
43
|
#
|
|
35
|
-
# [
|
|
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
|
-
# [
|
|
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
|
-
#
|
|
60
|
+
# ==== Options
|
|
52
61
|
#
|
|
53
|
-
# [
|
|
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
|
-
# [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|