activerecord 7.2.1 → 8.0.0.beta1
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 +188 -786
- data/README.rdoc +1 -1
- data/lib/active_record/associations/association.rb +25 -5
- 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 +3 -2
- data/lib/active_record/associations/join_dependency.rb +4 -4
- data/lib/active_record/associations/preloader/association.rb +2 -2
- 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_assignment.rb +9 -1
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -0
- data/lib/active_record/attributes.rb +1 -2
- data/lib/active_record/autosave_association.rb +69 -27
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
- 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 +0 -1
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +12 -4
- data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +26 -5
- data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
- data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -25
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +20 -38
- data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
- data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +43 -45
- data/lib/active_record/connection_adapters/mysql2/database_statements.rb +42 -98
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -8
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +64 -42
- data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +0 -1
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +50 -6
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +38 -90
- data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
- data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
- data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -1
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -12
- data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
- data/lib/active_record/connection_adapters/trilogy_adapter.rb +0 -17
- data/lib/active_record/connection_handling.rb +22 -0
- data/lib/active_record/core.rb +14 -7
- data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -1
- data/lib/active_record/encryption/config.rb +3 -1
- data/lib/active_record/encryption/encryptable_record.rb +4 -4
- data/lib/active_record/encryption/encrypted_attribute_type.rb +10 -1
- data/lib/active_record/encryption/encryptor.rb +15 -8
- data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
- data/lib/active_record/encryption/key_provider.rb +1 -1
- data/lib/active_record/encryption/scheme.rb +8 -1
- data/lib/active_record/encryption.rb +2 -0
- data/lib/active_record/enum.rb +7 -10
- data/lib/active_record/errors.rb +13 -5
- data/lib/active_record/fixtures.rb +0 -1
- data/lib/active_record/future_result.rb +14 -10
- data/lib/active_record/gem_version.rb +4 -4
- data/lib/active_record/insert_all.rb +1 -1
- data/lib/active_record/migration/command_recorder.rb +22 -5
- data/lib/active_record/migration/compatibility.rb +5 -2
- data/lib/active_record/migration.rb +35 -33
- data/lib/active_record/model_schema.rb +2 -3
- data/lib/active_record/nested_attributes.rb +11 -2
- data/lib/active_record/persistence.rb +128 -130
- data/lib/active_record/query_logs.rb +97 -39
- data/lib/active_record/query_logs_formatter.rb +17 -28
- data/lib/active_record/querying.rb +6 -6
- data/lib/active_record/railtie.rb +8 -14
- data/lib/active_record/reflection.rb +9 -4
- 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 +24 -19
- data/lib/active_record/relation/delegation.rb +25 -14
- data/lib/active_record/relation/finder_methods.rb +18 -18
- data/lib/active_record/relation/merger.rb +8 -8
- 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 +6 -1
- data/lib/active_record/relation/query_methods.rb +58 -37
- data/lib/active_record/relation/record_fetch_warning.rb +2 -2
- data/lib/active_record/relation/spawn_methods.rb +1 -1
- data/lib/active_record/relation.rb +72 -61
- data/lib/active_record/result.rb +68 -7
- data/lib/active_record/sanitization.rb +7 -6
- data/lib/active_record/schema_dumper.rb +5 -0
- data/lib/active_record/schema_migration.rb +2 -1
- data/lib/active_record/scoping/named.rb +5 -2
- data/lib/active_record/statement_cache.rb +12 -12
- data/lib/active_record/store.rb +7 -3
- data/lib/active_record/tasks/database_tasks.rb +36 -16
- data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
- data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
- data/lib/active_record/test_fixtures.rb +12 -0
- data/lib/active_record/token_for.rb +1 -1
- data/lib/active_record/validations/uniqueness.rb +9 -8
- data/lib/active_record.rb +15 -0
- data/lib/arel/collectors/bind.rb +1 -1
- metadata +14 -14
@@ -11,9 +11,12 @@ require "active_record/connection_adapters/sqlite3/schema_definitions"
|
|
11
11
|
require "active_record/connection_adapters/sqlite3/schema_dumper"
|
12
12
|
require "active_record/connection_adapters/sqlite3/schema_statements"
|
13
13
|
|
14
|
-
gem "sqlite3", ">= 1
|
14
|
+
gem "sqlite3", ">= 2.1"
|
15
15
|
require "sqlite3"
|
16
16
|
|
17
|
+
# Suppress the warning that SQLite3 issues when open writable connections are carried across fork()
|
18
|
+
SQLite3::ForkSafety.suppress_warnings!
|
19
|
+
|
17
20
|
module ActiveRecord
|
18
21
|
module ConnectionAdapters # :nodoc:
|
19
22
|
# = Active Record SQLite3 Adapter
|
@@ -45,7 +48,7 @@ module ActiveRecord
|
|
45
48
|
args << "-header" if options[:header]
|
46
49
|
args << File.expand_path(config.database, Rails.respond_to?(:root) ? Rails.root : nil)
|
47
50
|
|
48
|
-
find_cmd_and_exec(
|
51
|
+
find_cmd_and_exec(ActiveRecord.database_cli[:sqlite], *args)
|
49
52
|
end
|
50
53
|
end
|
51
54
|
|
@@ -119,9 +122,14 @@ module ActiveRecord
|
|
119
122
|
end
|
120
123
|
end
|
121
124
|
|
125
|
+
@last_affected_rows = nil
|
126
|
+
@previous_read_uncommitted = nil
|
122
127
|
@config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict)
|
123
|
-
@connection_parameters = @config.merge(
|
124
|
-
|
128
|
+
@connection_parameters = @config.merge(
|
129
|
+
database: @config[:database].to_s,
|
130
|
+
results_as_hash: true,
|
131
|
+
default_transaction_mode: :immediate,
|
132
|
+
)
|
125
133
|
end
|
126
134
|
|
127
135
|
def database_exists?
|
@@ -278,6 +286,38 @@ module ActiveRecord
|
|
278
286
|
exec_query "DROP INDEX #{quote_column_name(index_name)}"
|
279
287
|
end
|
280
288
|
|
289
|
+
VIRTUAL_TABLE_REGEX = /USING\s+(\w+)\s*\((.+)\)/i
|
290
|
+
|
291
|
+
# Returns a list of defined virtual tables
|
292
|
+
def virtual_tables
|
293
|
+
query = <<~SQL
|
294
|
+
SELECT name, sql FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL %';
|
295
|
+
SQL
|
296
|
+
|
297
|
+
exec_query(query, "SCHEMA").cast_values.each_with_object({}) do |row, memo|
|
298
|
+
table_name, sql = row[0], row[1]
|
299
|
+
_, module_name, arguments = sql.match(VIRTUAL_TABLE_REGEX).to_a
|
300
|
+
memo[table_name] = [module_name, arguments]
|
301
|
+
end.to_a
|
302
|
+
end
|
303
|
+
|
304
|
+
# Creates a virtual table
|
305
|
+
#
|
306
|
+
# Example:
|
307
|
+
# create_virtual_table :emails, :fts5, ['sender', 'title',' body']
|
308
|
+
def create_virtual_table(table_name, module_name, values)
|
309
|
+
exec_query "CREATE VIRTUAL TABLE IF NOT EXISTS #{table_name} USING #{module_name} (#{values.join(", ")})"
|
310
|
+
end
|
311
|
+
|
312
|
+
# Drops a virtual table
|
313
|
+
#
|
314
|
+
# Although this command ignores +module_name+ and +values+,
|
315
|
+
# it can be helpful to provide these in a migration's +change+ method so it can be reverted.
|
316
|
+
# In that case, +module_name+, +values+ and +options+ will be used by #create_virtual_table.
|
317
|
+
def drop_virtual_table(table_name, module_name, values, **options)
|
318
|
+
drop_table(table_name)
|
319
|
+
end
|
320
|
+
|
281
321
|
# Renames a table.
|
282
322
|
#
|
283
323
|
# Example:
|
@@ -428,10 +468,6 @@ module ActiveRecord
|
|
428
468
|
@config.fetch(:flags, 0).anybits?(::SQLite3::Constants::Open::SHAREDCACHE)
|
429
469
|
end
|
430
470
|
|
431
|
-
def use_insert_returning?
|
432
|
-
@use_insert_returning
|
433
|
-
end
|
434
|
-
|
435
471
|
def get_database_version # :nodoc:
|
436
472
|
SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)", "SCHEMA"))
|
437
473
|
end
|
@@ -661,6 +697,8 @@ module ActiveRecord
|
|
661
697
|
InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
662
698
|
elsif exception.message.match?(/called on a closed database/i)
|
663
699
|
ConnectionNotEstablished.new(exception, connection_pool: @pool)
|
700
|
+
elsif exception.is_a?(::SQLite3::BusyException)
|
701
|
+
StatementTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
664
702
|
else
|
665
703
|
super
|
666
704
|
end
|
@@ -687,6 +725,8 @@ module ActiveRecord
|
|
687
725
|
end
|
688
726
|
|
689
727
|
basic_structure.map do |column|
|
728
|
+
column = column.to_h
|
729
|
+
|
690
730
|
column_name = column["name"]
|
691
731
|
|
692
732
|
if collation_hash.has_key? column_name
|
@@ -778,12 +818,15 @@ module ActiveRecord
|
|
778
818
|
if @config[:timeout] && @config[:retries]
|
779
819
|
raise ArgumentError, "Cannot specify both timeout and retries arguments"
|
780
820
|
elsif @config[:timeout]
|
781
|
-
|
821
|
+
timeout = self.class.type_cast_config_to_integer(@config[:timeout])
|
822
|
+
raise TypeError, "timeout must be integer, not #{timeout}" unless timeout.is_a?(Integer)
|
823
|
+
@raw_connection.busy_handler_timeout = timeout
|
782
824
|
elsif @config[:retries]
|
825
|
+
ActiveRecord.deprecator.warn(<<~MSG)
|
826
|
+
The retries option is deprecated and will be removed in Rails 8.1. Use timeout instead.
|
827
|
+
MSG
|
783
828
|
retries = self.class.type_cast_config_to_integer(@config[:retries])
|
784
|
-
raw_connection.busy_handler
|
785
|
-
count <= retries
|
786
|
-
end
|
829
|
+
raw_connection.busy_handler { |count| count <= retries }
|
787
830
|
end
|
788
831
|
|
789
832
|
super
|
@@ -4,93 +4,63 @@ module ActiveRecord
|
|
4
4
|
module ConnectionAdapters
|
5
5
|
module Trilogy
|
6
6
|
module DatabaseStatements
|
7
|
-
def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false) # :nodoc:
|
8
|
-
sql = transform_query(sql)
|
9
|
-
check_if_write_query(sql)
|
10
|
-
mark_transaction_written_if_write(sql)
|
11
|
-
|
12
|
-
result = raw_execute(sql, name, async: async, allow_retry: allow_retry)
|
13
|
-
ActiveRecord::Result.new(result.fields, result.to_a)
|
14
|
-
end
|
15
|
-
|
16
7
|
def exec_insert(sql, name, binds, pk = nil, sequence_name = nil, returning: nil) # :nodoc:
|
17
|
-
sql = transform_query(sql)
|
18
|
-
check_if_write_query(sql)
|
19
|
-
mark_transaction_written_if_write(sql)
|
20
|
-
|
21
8
|
sql, _binds = sql_for_insert(sql, pk, binds, returning)
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
def exec_delete(sql, name = nil, binds = []) # :nodoc:
|
26
|
-
sql = transform_query(sql)
|
27
|
-
check_if_write_query(sql)
|
28
|
-
mark_transaction_written_if_write(sql)
|
29
|
-
|
30
|
-
result = raw_execute(to_sql(sql, binds), name)
|
31
|
-
result.affected_rows
|
9
|
+
internal_execute(sql, name)
|
32
10
|
end
|
33
11
|
|
34
|
-
alias :exec_update :exec_delete # :nodoc:
|
35
|
-
|
36
12
|
private
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
result = conn.query(sql)
|
42
|
-
while conn.more_results_exist?
|
43
|
-
conn.next_result
|
44
|
-
end
|
45
|
-
verified!
|
46
|
-
handle_warnings(sql)
|
47
|
-
notification_payload[:row_count] = result.count
|
48
|
-
result
|
49
|
-
end
|
13
|
+
def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
|
14
|
+
reset_multi_statement = if batch && !@config[:multi_statement]
|
15
|
+
raw_connection.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_ON)
|
16
|
+
true
|
50
17
|
end
|
51
|
-
end
|
52
18
|
|
53
|
-
|
54
|
-
|
55
|
-
|
19
|
+
# Make sure we carry over any changes to ActiveRecord.default_timezone that have been
|
20
|
+
# made since we established the connection
|
21
|
+
if default_timezone == :local
|
22
|
+
raw_connection.query_flags |= ::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
|
56
23
|
else
|
57
|
-
|
24
|
+
raw_connection.query_flags &= ~::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
|
58
25
|
end
|
59
|
-
end
|
60
26
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
27
|
+
result = raw_connection.query(sql)
|
28
|
+
while raw_connection.more_results_exist?
|
29
|
+
raw_connection.next_result
|
30
|
+
end
|
31
|
+
verified!
|
32
|
+
handle_warnings(sql)
|
33
|
+
notification_payload[:row_count] = result.count
|
34
|
+
result
|
35
|
+
ensure
|
36
|
+
if reset_multi_statement && active?
|
37
|
+
raw_connection.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF)
|
67
38
|
end
|
68
39
|
end
|
69
40
|
|
70
|
-
def
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
end
|
41
|
+
def cast_result(result)
|
42
|
+
if result.fields.empty?
|
43
|
+
ActiveRecord::Result.empty
|
44
|
+
else
|
45
|
+
ActiveRecord::Result.new(result.fields, result.rows)
|
76
46
|
end
|
77
47
|
end
|
78
48
|
|
79
|
-
def
|
80
|
-
|
49
|
+
def affected_rows(result)
|
50
|
+
result.affected_rows
|
81
51
|
end
|
82
52
|
|
83
|
-
def
|
84
|
-
if
|
85
|
-
|
53
|
+
def last_inserted_id(result)
|
54
|
+
if supports_insert_returning?
|
55
|
+
super
|
56
|
+
else
|
57
|
+
result.last_insert_id
|
86
58
|
end
|
59
|
+
end
|
87
60
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
yield
|
92
|
-
ensure
|
93
|
-
conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF) if active?
|
61
|
+
def execute_batch(statements, name = nil, **kwargs)
|
62
|
+
combine_multi_statements(statements).each do |statement|
|
63
|
+
raw_execute(statement, name, batch: true, **kwargs)
|
94
64
|
end
|
95
65
|
end
|
96
66
|
end
|
@@ -149,23 +149,6 @@ module ActiveRecord
|
|
149
149
|
TYPE_MAP.lookup(type).is_a?(Type::String) || TYPE_MAP.lookup(type).is_a?(Type::Text)
|
150
150
|
end
|
151
151
|
|
152
|
-
def each_hash(result)
|
153
|
-
return to_enum(:each_hash, result) unless block_given?
|
154
|
-
|
155
|
-
keys = result.fields.map(&:to_sym)
|
156
|
-
result.rows.each do |row|
|
157
|
-
hash = {}
|
158
|
-
idx = 0
|
159
|
-
row.each do |value|
|
160
|
-
hash[keys[idx]] = value
|
161
|
-
idx += 1
|
162
|
-
end
|
163
|
-
yield hash
|
164
|
-
end
|
165
|
-
|
166
|
-
nil
|
167
|
-
end
|
168
|
-
|
169
152
|
def error_number(exception)
|
170
153
|
exception.error_code if exception.respond_to?(:error_code)
|
171
154
|
end
|
@@ -87,6 +87,8 @@ module ActiveRecord
|
|
87
87
|
|
88
88
|
connections = []
|
89
89
|
|
90
|
+
@shard_keys = shards.keys
|
91
|
+
|
90
92
|
if shards.empty?
|
91
93
|
shards[:default] = database
|
92
94
|
end
|
@@ -175,6 +177,18 @@ module ActiveRecord
|
|
175
177
|
connected_to_stack.pop
|
176
178
|
end
|
177
179
|
|
180
|
+
# Passes the block to +connected_to+ for every +shard+ the
|
181
|
+
# model is configured to connect to (if any), and returns the
|
182
|
+
# results in an array.
|
183
|
+
#
|
184
|
+
# Optionally, +role+ and/or +prevent_writes+ can be passed which
|
185
|
+
# will be forwarded to each +connected_to+ call.
|
186
|
+
def connected_to_all_shards(role: nil, prevent_writes: false, &blk)
|
187
|
+
shard_keys.map do |shard|
|
188
|
+
connected_to(shard: shard, role: role, prevent_writes: prevent_writes, &blk)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
178
192
|
# Use a specified connection.
|
179
193
|
#
|
180
194
|
# This method is useful for ensuring that a specific connection is
|
@@ -359,6 +373,14 @@ module ActiveRecord
|
|
359
373
|
connection_pool.schema_cache.clear!
|
360
374
|
end
|
361
375
|
|
376
|
+
def shard_keys
|
377
|
+
connection_class_for_self.instance_variable_get(:@shard_keys) || []
|
378
|
+
end
|
379
|
+
|
380
|
+
def sharded?
|
381
|
+
shard_keys.any?
|
382
|
+
end
|
383
|
+
|
362
384
|
private
|
363
385
|
def resolve_config_for_connection(config_or_env)
|
364
386
|
raise "Anonymous class is not allowed." unless name
|
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
|
|
@@ -103,7 +104,7 @@ module ActiveRecord
|
|
103
104
|
class_attribute :shard_selector, instance_accessor: false, default: nil
|
104
105
|
|
105
106
|
# Specifies the attributes that will be included in the output of the #inspect method
|
106
|
-
class_attribute :attributes_for_inspect, instance_accessor: false, default:
|
107
|
+
class_attribute :attributes_for_inspect, instance_accessor: false, default: :all
|
107
108
|
|
108
109
|
def self.application_record_class? # :nodoc:
|
109
110
|
if ActiveRecord.application_record_class
|
@@ -349,7 +350,7 @@ module ActiveRecord
|
|
349
350
|
|
350
351
|
# Returns a string like 'Post(id:integer, title:string, body:text)'
|
351
352
|
def inspect # :nodoc:
|
352
|
-
if self == Base
|
353
|
+
if self == Base || singleton_class?
|
353
354
|
super
|
354
355
|
elsif abstract_class?
|
355
356
|
"#{super}(abstract)"
|
@@ -431,8 +432,8 @@ module ActiveRecord
|
|
431
432
|
where(wheres).limit(1)
|
432
433
|
}
|
433
434
|
|
434
|
-
|
435
|
-
|
435
|
+
statement.execute(values.flatten, connection, allow_retry: true).then do |r|
|
436
|
+
r.first
|
436
437
|
rescue TypeError
|
437
438
|
raise ActiveRecord::StatementInvalid
|
438
439
|
end
|
@@ -731,7 +732,7 @@ module ActiveRecord
|
|
731
732
|
|
732
733
|
# Returns the full contents of the record as a nicely formatted string.
|
733
734
|
def full_inspect
|
734
|
-
inspect_with_attributes(
|
735
|
+
inspect_with_attributes(all_attributes_for_inspect)
|
735
736
|
end
|
736
737
|
|
737
738
|
# Takes a PP and prettily prints this record to it, allowing you to get a nice result from <tt>pp record</tt>
|
@@ -784,7 +785,7 @@ module ActiveRecord
|
|
784
785
|
|
785
786
|
@primary_key = klass.primary_key
|
786
787
|
@strict_loading = klass.strict_loading_by_default
|
787
|
-
@strict_loading_mode =
|
788
|
+
@strict_loading_mode = klass.strict_loading_mode
|
788
789
|
|
789
790
|
klass.define_attribute_methods
|
790
791
|
end
|
@@ -823,7 +824,13 @@ module ActiveRecord
|
|
823
824
|
end
|
824
825
|
|
825
826
|
def attributes_for_inspect
|
826
|
-
self.class.attributes_for_inspect == :all ?
|
827
|
+
self.class.attributes_for_inspect == :all ? all_attributes_for_inspect : self.class.attributes_for_inspect
|
828
|
+
end
|
829
|
+
|
830
|
+
def all_attributes_for_inspect
|
831
|
+
return [] unless @attributes
|
832
|
+
|
833
|
+
attribute_names
|
827
834
|
end
|
828
835
|
end
|
829
836
|
end
|
@@ -8,7 +8,8 @@ module ActiveRecord
|
|
8
8
|
class Config
|
9
9
|
attr_accessor :primary_key, :deterministic_key, :store_key_references, :key_derivation_salt, :hash_digest_class,
|
10
10
|
:support_unencrypted_data, :encrypt_fixtures, :validate_column_size, :add_to_filter_parameters,
|
11
|
-
:excluded_from_filter_parameters, :extend_queries, :previous_schemes, :forced_encoding_for_deterministic_encryption
|
11
|
+
:excluded_from_filter_parameters, :extend_queries, :previous_schemes, :forced_encoding_for_deterministic_encryption,
|
12
|
+
:compressor
|
12
13
|
|
13
14
|
def initialize
|
14
15
|
set_defaults
|
@@ -55,6 +56,7 @@ module ActiveRecord
|
|
55
56
|
self.previous_schemes = []
|
56
57
|
self.forced_encoding_for_deterministic_encryption = Encoding::UTF_8
|
57
58
|
self.hash_digest_class = OpenSSL::Digest::SHA1
|
59
|
+
self.compressor = Zlib
|
58
60
|
|
59
61
|
# TODO: Setting to false for now as the implementation is a bit experimental
|
60
62
|
self.extend_queries = false
|
@@ -46,11 +46,11 @@ module ActiveRecord
|
|
46
46
|
# * <tt>:previous</tt> - List of previous encryption schemes. When provided, they will be used in order when trying to read
|
47
47
|
# the attribute. Each entry of the list can contain the properties supported by #encrypts. Also, when deterministic
|
48
48
|
# encryption is used, they will be used to generate additional ciphertexts to check in the queries.
|
49
|
-
def encrypts(*names, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], **context_properties)
|
49
|
+
def encrypts(*names, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], compress: true, compressor: nil, **context_properties)
|
50
50
|
self.encrypted_attributes ||= Set.new # not using :default because the instance would be shared across classes
|
51
51
|
|
52
52
|
names.each do |name|
|
53
|
-
encrypt_attribute name, key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, downcase: downcase, ignore_case: ignore_case, previous: previous, **context_properties
|
53
|
+
encrypt_attribute name, key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, downcase: downcase, ignore_case: ignore_case, previous: previous, compress: compress, compressor: compressor, **context_properties
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
@@ -81,12 +81,12 @@ module ActiveRecord
|
|
81
81
|
end
|
82
82
|
end
|
83
83
|
|
84
|
-
def encrypt_attribute(name, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], **context_properties)
|
84
|
+
def encrypt_attribute(name, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], compress: true, compressor: nil, **context_properties)
|
85
85
|
encrypted_attributes << name.to_sym
|
86
86
|
|
87
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
|
@@ -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,12 +11,20 @@ module ActiveRecord
|
|
12
11
|
# It interacts with a KeyProvider for getting the keys, and delegate to
|
13
12
|
# ActiveRecord::Encryption::Cipher the actual encryption algorithm.
|
14
13
|
class Encryptor
|
14
|
+
# The compressor to use for compressing the payload
|
15
|
+
attr_reader :compressor
|
16
|
+
|
15
17
|
# === Options
|
16
18
|
#
|
17
19
|
# * <tt>:compress</tt> - Boolean indicating whether records should be compressed before encryption.
|
18
20
|
# Defaults to +true+.
|
19
|
-
|
21
|
+
# * <tt>:compressor</tt> - The compressor to use.
|
22
|
+
# 1. If compressor is provided, it will be used.
|
23
|
+
# 2. If not, it will use ActiveRecord::Encryption.config.compressor which default value is +Zlib+.
|
24
|
+
# If you want to use a custom compressor, it must respond to +deflate+ and +inflate+.
|
25
|
+
def initialize(compress: true, compressor: nil)
|
20
26
|
@compress = compress
|
27
|
+
@compressor = compressor || ActiveRecord::Encryption.config.compressor
|
21
28
|
end
|
22
29
|
|
23
30
|
# Encrypts +clean_text+ and returns the encrypted result
|
@@ -78,6 +85,10 @@ module ActiveRecord
|
|
78
85
|
serializer.binary?
|
79
86
|
end
|
80
87
|
|
88
|
+
def compress? # :nodoc:
|
89
|
+
@compress
|
90
|
+
end
|
91
|
+
|
81
92
|
private
|
82
93
|
DECRYPT_ERRORS = [OpenSSL::Cipher::CipherError, Errors::EncryptedContentIntegrity, Errors::Decryption]
|
83
94
|
ENCODING_ERRORS = [EncodingError, Errors::Encoding]
|
@@ -130,12 +141,8 @@ module ActiveRecord
|
|
130
141
|
end
|
131
142
|
end
|
132
143
|
|
133
|
-
def compress?
|
134
|
-
@compress
|
135
|
-
end
|
136
|
-
|
137
144
|
def compress(data)
|
138
|
-
|
145
|
+
@compressor.deflate(data).tap do |compressed_data|
|
139
146
|
compressed_data.force_encoding(data.encoding)
|
140
147
|
end
|
141
148
|
end
|
@@ -149,7 +156,7 @@ module ActiveRecord
|
|
149
156
|
end
|
150
157
|
|
151
158
|
def uncompress(data)
|
152
|
-
|
159
|
+
@compressor.inflate(data).tap do |uncompressed_data|
|
153
160
|
uncompressed_data.force_encoding(data.encoding)
|
154
161
|
end
|
155
162
|
end
|
@@ -41,6 +41,8 @@ module ActiveRecord
|
|
41
41
|
module EncryptedQuery # :nodoc:
|
42
42
|
class << self
|
43
43
|
def process_arguments(owner, args, check_for_additional_values)
|
44
|
+
owner = owner.model if owner.is_a?(Relation)
|
45
|
+
|
44
46
|
return args if owner.deterministic_encrypted_attributes&.empty?
|
45
47
|
|
46
48
|
if args.is_a?(Array) && (options = args.first).is_a?(Hash)
|
@@ -102,12 +104,12 @@ module ActiveRecord
|
|
102
104
|
end
|
103
105
|
|
104
106
|
def scope_for_create
|
105
|
-
return super unless
|
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)
|
@@ -12,7 +12,7 @@ module ActiveRecord
|
|
12
12
|
@keys = Array(keys)
|
13
13
|
end
|
14
14
|
|
15
|
-
# Returns the
|
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
|
@@ -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
|
data/lib/active_record/enum.rb
CHANGED
@@ -167,15 +167,6 @@ module ActiveRecord
|
|
167
167
|
base.class_attribute(:defined_enums, instance_writer: false, default: {})
|
168
168
|
end
|
169
169
|
|
170
|
-
def load_schema! # :nodoc:
|
171
|
-
defined_enums.each_key do |name|
|
172
|
-
unless columns_hash.key?(resolve_attribute_name(name))
|
173
|
-
raise "Unknown enum attribute '#{name}' for #{self.name}. Enums must be" \
|
174
|
-
" backed by a database column."
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
170
|
class EnumType < Type::Value # :nodoc:
|
180
171
|
delegate :type, to: :subtype
|
181
172
|
|
@@ -264,7 +255,13 @@ module ActiveRecord
|
|
264
255
|
|
265
256
|
attribute(name, **options)
|
266
257
|
|
267
|
-
decorate_attributes([name]) do |
|
258
|
+
decorate_attributes([name]) do |_name, subtype|
|
259
|
+
if subtype == ActiveModel::Type.default_value
|
260
|
+
raise "Undeclared attribute type for enum '#{name}' in #{self.name}. Enums must be" \
|
261
|
+
" backed by a database column or declared with an explicit type" \
|
262
|
+
" via `attribute`."
|
263
|
+
end
|
264
|
+
|
268
265
|
subtype = subtype.subtype if EnumType === subtype
|
269
266
|
EnumType.new(name, enum_values, subtype, raise_on_invalid_values: !validate)
|
270
267
|
end
|