activerecord 7.2.2 → 8.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +173 -920
- 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 +4 -9
- 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 +50 -32
- data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
- data/lib/active_record/autosave_association.rb +69 -27
- data/lib/active_record/callbacks.rb +1 -1
- 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 -27
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +8 -2
- 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 +27 -6
- 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 +23 -45
- 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/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 -8
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +41 -93
- 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 +7 -32
- 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/scheme.rb +8 -1
- 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/marshalling.rb +1 -4
- 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 +1 -1
- data/lib/active_record/nested_attributes.rb +4 -6
- data/lib/active_record/persistence.rb +128 -130
- data/lib/active_record/query_cache.rb +5 -4
- data/lib/active_record/query_logs.rb +98 -40
- data/lib/active_record/query_logs_formatter.rb +17 -28
- data/lib/active_record/querying.rb +6 -6
- data/lib/active_record/railtie.rb +3 -4
- data/lib/active_record/reflection.rb +9 -7
- 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 +25 -20
- 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 +5 -0
- data/lib/active_record/relation/query_methods.rb +81 -75
- 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 +24 -15
- 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/testing/query_assertions.rb +2 -2
- data/lib/active_record/token_for.rb +1 -1
- data/lib/active_record/validations/uniqueness.rb +8 -8
- data/lib/active_record.rb +15 -0
- data/lib/arel/collectors/bind.rb +1 -1
- data/lib/arel/visitors/sqlite.rb +0 -25
- metadata +10 -10
@@ -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
|
|
@@ -102,19 +103,7 @@ module ActiveRecord
|
|
102
103
|
|
103
104
|
class_attribute :shard_selector, instance_accessor: false, default: nil
|
104
105
|
|
105
|
-
|
106
|
-
# :singleton-method:
|
107
|
-
#
|
108
|
-
# Specifies the attributes that will be included in the output of the
|
109
|
-
# #inspect method:
|
110
|
-
#
|
111
|
-
# Post.attributes_for_inspect = [:id, :title]
|
112
|
-
# Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!">"
|
113
|
-
#
|
114
|
-
# When set to `:all` inspect will list all the record's attributes:
|
115
|
-
#
|
116
|
-
# Post.attributes_for_inspect = :all
|
117
|
-
# Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
|
106
|
+
# Specifies the attributes that will be included in the output of the #inspect method
|
118
107
|
class_attribute :attributes_for_inspect, instance_accessor: false, default: :all
|
119
108
|
|
120
109
|
def self.application_record_class? # :nodoc:
|
@@ -443,8 +432,8 @@ module ActiveRecord
|
|
443
432
|
where(wheres).limit(1)
|
444
433
|
}
|
445
434
|
|
446
|
-
|
447
|
-
|
435
|
+
statement.execute(values.flatten, connection, allow_retry: true).then do |r|
|
436
|
+
r.first
|
448
437
|
rescue TypeError
|
449
438
|
raise ActiveRecord::StatementInvalid
|
450
439
|
end
|
@@ -736,26 +725,12 @@ module ActiveRecord
|
|
736
725
|
self.class.connection_handler
|
737
726
|
end
|
738
727
|
|
739
|
-
# Returns the attributes
|
740
|
-
#
|
741
|
-
# Post.first.inspect
|
742
|
-
# #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
|
743
|
-
#
|
744
|
-
# The attributes can be limited by setting <tt>.attributes_for_inspect</tt>.
|
745
|
-
#
|
746
|
-
# Post.attributes_for_inspect = [:id, :title]
|
747
|
-
# Post.first.inspect
|
748
|
-
# #=> "#<Post id: 1, title: "Hello, World!">"
|
728
|
+
# Returns the attributes specified by <tt>.attributes_for_inspect</tt> as a nicely formatted string.
|
749
729
|
def inspect
|
750
730
|
inspect_with_attributes(attributes_for_inspect)
|
751
731
|
end
|
752
732
|
|
753
|
-
# Returns
|
754
|
-
# ignoring <tt>.attributes_for_inspect</tt>.
|
755
|
-
#
|
756
|
-
# Post.first.full_inspect
|
757
|
-
# #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
|
758
|
-
#
|
733
|
+
# Returns the full contents of the record as a nicely formatted string.
|
759
734
|
def full_inspect
|
760
735
|
inspect_with_attributes(all_attributes_for_inspect)
|
761
736
|
end
|
@@ -810,7 +785,7 @@ module ActiveRecord
|
|
810
785
|
|
811
786
|
@primary_key = klass.primary_key
|
812
787
|
@strict_loading = klass.strict_loading_by_default
|
813
|
-
@strict_loading_mode =
|
788
|
+
@strict_loading_mode = klass.strict_loading_mode
|
814
789
|
|
815
790
|
klass.define_attribute_methods
|
816
791
|
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)
|
@@ -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/errors.rb
CHANGED
@@ -84,6 +84,19 @@ module ActiveRecord
|
|
84
84
|
class ConnectionTimeoutError < ConnectionNotEstablished
|
85
85
|
end
|
86
86
|
|
87
|
+
# Raised when a database connection pool is requested but
|
88
|
+
# has not been defined.
|
89
|
+
class ConnectionNotDefined < ConnectionNotEstablished
|
90
|
+
def initialize(message = nil, connection_name: nil, role: nil, shard: nil)
|
91
|
+
super(message)
|
92
|
+
@connection_name = connection_name
|
93
|
+
@role = role
|
94
|
+
@shard = shard
|
95
|
+
end
|
96
|
+
|
97
|
+
attr_reader :connection_name, :role, :shard
|
98
|
+
end
|
99
|
+
|
87
100
|
# Raised when connection to the database could not been established because it was not
|
88
101
|
# able to connect to the host or when the authorization failed.
|
89
102
|
class DatabaseConnectionError < ConnectionNotEstablished
|
@@ -484,11 +497,6 @@ module ActiveRecord
|
|
484
497
|
# relation.limit!(5) # => ActiveRecord::UnmodifiableRelation
|
485
498
|
class UnmodifiableRelation < ActiveRecordError
|
486
499
|
end
|
487
|
-
deprecate_constant(
|
488
|
-
:ImmutableRelation,
|
489
|
-
"ActiveRecord::UnmodifiableRelation",
|
490
|
-
deprecator: ActiveRecord.deprecator
|
491
|
-
)
|
492
500
|
|
493
501
|
# TransactionIsolationError will be raised under the following conditions:
|
494
502
|
#
|