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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +173 -920
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/associations/association.rb +25 -5
  5. data/lib/active_record/associations/builder/association.rb +7 -6
  6. data/lib/active_record/associations/collection_association.rb +10 -8
  7. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  8. data/lib/active_record/associations/has_many_through_association.rb +4 -9
  9. data/lib/active_record/associations/preloader/association.rb +2 -2
  10. data/lib/active_record/associations/singular_association.rb +8 -3
  11. data/lib/active_record/associations.rb +50 -32
  12. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  13. data/lib/active_record/autosave_association.rb +69 -27
  14. data/lib/active_record/callbacks.rb +1 -1
  15. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
  16. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  17. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  18. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +0 -27
  19. data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
  20. data/lib/active_record/connection_adapters/abstract/query_cache.rb +8 -2
  21. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  22. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  23. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +27 -6
  24. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  25. data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -25
  26. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +23 -45
  27. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  28. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  29. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +43 -45
  30. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +42 -98
  31. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -8
  32. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +64 -42
  33. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  34. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +0 -1
  35. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +50 -8
  36. data/lib/active_record/connection_adapters/postgresql_adapter.rb +41 -93
  37. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  38. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
  39. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  40. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
  41. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -1
  42. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -12
  43. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  44. data/lib/active_record/connection_adapters/trilogy_adapter.rb +0 -17
  45. data/lib/active_record/connection_handling.rb +22 -0
  46. data/lib/active_record/core.rb +7 -32
  47. data/lib/active_record/encryption/config.rb +3 -1
  48. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  49. data/lib/active_record/encryption/encrypted_attribute_type.rb +10 -1
  50. data/lib/active_record/encryption/encryptor.rb +15 -8
  51. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  52. data/lib/active_record/encryption/scheme.rb +8 -1
  53. data/lib/active_record/errors.rb +13 -5
  54. data/lib/active_record/fixtures.rb +0 -1
  55. data/lib/active_record/future_result.rb +14 -10
  56. data/lib/active_record/gem_version.rb +4 -4
  57. data/lib/active_record/insert_all.rb +1 -1
  58. data/lib/active_record/marshalling.rb +1 -4
  59. data/lib/active_record/migration/command_recorder.rb +22 -5
  60. data/lib/active_record/migration/compatibility.rb +5 -2
  61. data/lib/active_record/migration.rb +35 -33
  62. data/lib/active_record/model_schema.rb +1 -1
  63. data/lib/active_record/nested_attributes.rb +4 -6
  64. data/lib/active_record/persistence.rb +128 -130
  65. data/lib/active_record/query_cache.rb +5 -4
  66. data/lib/active_record/query_logs.rb +98 -40
  67. data/lib/active_record/query_logs_formatter.rb +17 -28
  68. data/lib/active_record/querying.rb +6 -6
  69. data/lib/active_record/railtie.rb +3 -4
  70. data/lib/active_record/reflection.rb +9 -7
  71. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  72. data/lib/active_record/relation/batches.rb +132 -72
  73. data/lib/active_record/relation/calculations.rb +25 -20
  74. data/lib/active_record/relation/delegation.rb +25 -14
  75. data/lib/active_record/relation/finder_methods.rb +18 -18
  76. data/lib/active_record/relation/merger.rb +8 -8
  77. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  78. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  79. data/lib/active_record/relation/predicate_builder.rb +5 -0
  80. data/lib/active_record/relation/query_methods.rb +81 -75
  81. data/lib/active_record/relation/record_fetch_warning.rb +2 -2
  82. data/lib/active_record/relation/spawn_methods.rb +1 -1
  83. data/lib/active_record/relation.rb +72 -61
  84. data/lib/active_record/result.rb +68 -7
  85. data/lib/active_record/sanitization.rb +7 -6
  86. data/lib/active_record/schema_dumper.rb +5 -0
  87. data/lib/active_record/schema_migration.rb +2 -1
  88. data/lib/active_record/scoping/named.rb +5 -2
  89. data/lib/active_record/statement_cache.rb +12 -12
  90. data/lib/active_record/store.rb +7 -3
  91. data/lib/active_record/tasks/database_tasks.rb +24 -15
  92. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  93. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  94. data/lib/active_record/test_fixtures.rb +12 -0
  95. data/lib/active_record/testing/query_assertions.rb +2 -2
  96. data/lib/active_record/token_for.rb +1 -1
  97. data/lib/active_record/validations/uniqueness.rb +8 -8
  98. data/lib/active_record.rb +15 -0
  99. data/lib/arel/collectors/bind.rb +1 -1
  100. data/lib/arel/visitors/sqlite.rb +0 -25
  101. 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.4"
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("sqlite3", *args)
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(database: @config[:database].to_s, results_as_hash: true)
124
- @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
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
- @raw_connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout]))
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 do |count|
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
- raw_execute(sql, name)
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 raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
38
- log(sql, name, async: async) do |notification_payload|
39
- with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
40
- sync_timezone_changes(conn)
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
- def last_inserted_id(result)
54
- if supports_insert_returning?
55
- super
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
- result.last_insert_id
24
+ raw_connection.query_flags &= ~::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
58
25
  end
59
- end
60
26
 
61
- def sync_timezone_changes(conn)
62
- # Sync any changes since connection last established.
63
- if default_timezone == :local
64
- conn.query_flags |= ::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
65
- else
66
- conn.query_flags &= ~::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
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 execute_batch(statements, name = nil)
71
- statements = statements.map { |sql| transform_query(sql) }
72
- combine_multi_statements(statements).each do |statement|
73
- with_raw_connection do |conn|
74
- raw_execute(statement, name)
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 multi_statements_enabled?
80
- !!@config[:multi_statement]
49
+ def affected_rows(result)
50
+ result.affected_rows
81
51
  end
82
52
 
83
- def with_multi_statements
84
- if multi_statements_enabled?
85
- return yield
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
- with_raw_connection do |conn|
89
- conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_ON)
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
@@ -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
- begin
447
- statement.execute(values.flatten, connection, allow_retry: true).first
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 of the record as a nicely formatted string.
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 all attributes of the record as a nicely formatted string,
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 = :all
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
- def initialize(compress: true)
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
- Zlib::Deflate.deflate(data).tap do |compressed_data|
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
- Zlib::Inflate.inflate(data).tap do |uncompressed_data|
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 klass.deterministic_encrypted_attributes&.any?
107
+ return super unless model.deterministic_encrypted_attributes&.any?
106
108
 
107
109
  scope_attributes = super
108
110
  wheres = where_values_hash
109
111
 
110
- klass.deterministic_encrypted_attributes.each do |attribute_name|
112
+ model.deterministic_encrypted_attributes.each do |attribute_name|
111
113
  attribute_name = attribute_name.to_s
112
114
  values = wheres[attribute_name]
113
115
  if values.is_a?(Array) && values[1..].all?(AdditionalValue)
@@ -11,7 +11,7 @@ module ActiveRecord
11
11
  attr_accessor :previous_schemes
12
12
 
13
13
  def initialize(key_provider: nil, key: nil, deterministic: nil, support_unencrypted_data: nil, downcase: nil, ignore_case: nil,
14
- previous_schemes: nil, **context_properties)
14
+ previous_schemes: nil, compress: true, compressor: nil, **context_properties)
15
15
  # Initializing all attributes to +nil+ as we want to allow a "not set" semantics so that we
16
16
  # can merge schemes without overriding values with defaults. See +#merge+
17
17
 
@@ -24,8 +24,13 @@ module ActiveRecord
24
24
  @previous_schemes_param = previous_schemes
25
25
  @previous_schemes = Array.wrap(previous_schemes)
26
26
  @context_properties = context_properties
27
+ @compress = compress
28
+ @compressor = compressor
27
29
 
28
30
  validate_config!
31
+
32
+ @context_properties[:encryptor] = Encryptor.new(compress: @compress) unless @compress
33
+ @context_properties[:encryptor] = Encryptor.new(compressor: compressor) if compressor
29
34
  end
30
35
 
31
36
  def ignore_case?
@@ -78,6 +83,8 @@ module ActiveRecord
78
83
  def validate_config!
79
84
  raise Errors::Configuration, "ignore_case: can only be used with deterministic encryption" if @ignore_case && !@deterministic
80
85
  raise Errors::Configuration, "key_provider: and key: can't be used simultaneously" if @key_provider_param && @key
86
+ raise Errors::Configuration, "compressor: can't be used with compress: false" if !@compress && @compressor
87
+ raise Errors::Configuration, "compressor: can't be used with encryptor" if @compressor && @context_properties[:encryptor]
81
88
  end
82
89
 
83
90
  def key_provider_from_key
@@ -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
  #
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "erb"
4
4
  require "yaml"
5
- require "zlib"
6
5
  require "set"
7
6
  require "active_support/dependencies"
8
7
  require "active_support/core_ext/digest/uuid"