activerecord 7.2.2 → 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 +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
|
#
|