activerecord 7.2.1 → 8.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +188 -786
  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 +3 -2
  9. data/lib/active_record/associations/join_dependency/join_association.rb +3 -2
  10. data/lib/active_record/associations/join_dependency.rb +4 -4
  11. data/lib/active_record/associations/preloader/association.rb +2 -2
  12. data/lib/active_record/associations/singular_association.rb +8 -3
  13. data/lib/active_record/associations.rb +34 -4
  14. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  15. data/lib/active_record/attribute_assignment.rb +9 -1
  16. data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -0
  17. data/lib/active_record/attributes.rb +1 -2
  18. data/lib/active_record/autosave_association.rb +69 -27
  19. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
  20. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  21. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  22. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +0 -1
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
  24. data/lib/active_record/connection_adapters/abstract/query_cache.rb +12 -4
  25. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  26. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  27. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +26 -5
  28. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  29. data/lib/active_record/connection_adapters/abstract_adapter.rb +24 -25
  30. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +20 -38
  31. data/lib/active_record/connection_adapters/mysql/quoting.rb +0 -8
  32. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  33. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +43 -45
  34. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +42 -98
  35. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -8
  36. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +64 -42
  37. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +1 -1
  38. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  39. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +0 -1
  40. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +50 -6
  41. data/lib/active_record/connection_adapters/postgresql_adapter.rb +38 -90
  42. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  43. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
  44. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  45. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
  46. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -1
  47. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -12
  48. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  49. data/lib/active_record/connection_adapters/trilogy_adapter.rb +0 -17
  50. data/lib/active_record/connection_handling.rb +22 -0
  51. data/lib/active_record/core.rb +14 -7
  52. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -1
  53. data/lib/active_record/encryption/config.rb +3 -1
  54. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  55. data/lib/active_record/encryption/encrypted_attribute_type.rb +10 -1
  56. data/lib/active_record/encryption/encryptor.rb +15 -8
  57. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  58. data/lib/active_record/encryption/key_provider.rb +1 -1
  59. data/lib/active_record/encryption/scheme.rb +8 -1
  60. data/lib/active_record/encryption.rb +2 -0
  61. data/lib/active_record/enum.rb +7 -10
  62. data/lib/active_record/errors.rb +13 -5
  63. data/lib/active_record/fixtures.rb +0 -1
  64. data/lib/active_record/future_result.rb +14 -10
  65. data/lib/active_record/gem_version.rb +4 -4
  66. data/lib/active_record/insert_all.rb +1 -1
  67. data/lib/active_record/migration/command_recorder.rb +22 -5
  68. data/lib/active_record/migration/compatibility.rb +5 -2
  69. data/lib/active_record/migration.rb +35 -33
  70. data/lib/active_record/model_schema.rb +2 -3
  71. data/lib/active_record/nested_attributes.rb +11 -2
  72. data/lib/active_record/persistence.rb +128 -130
  73. data/lib/active_record/query_logs.rb +97 -39
  74. data/lib/active_record/query_logs_formatter.rb +17 -28
  75. data/lib/active_record/querying.rb +6 -6
  76. data/lib/active_record/railtie.rb +8 -14
  77. data/lib/active_record/reflection.rb +9 -4
  78. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  79. data/lib/active_record/relation/batches.rb +132 -72
  80. data/lib/active_record/relation/calculations.rb +24 -19
  81. data/lib/active_record/relation/delegation.rb +25 -14
  82. data/lib/active_record/relation/finder_methods.rb +18 -18
  83. data/lib/active_record/relation/merger.rb +8 -8
  84. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  85. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  86. data/lib/active_record/relation/predicate_builder.rb +6 -1
  87. data/lib/active_record/relation/query_methods.rb +58 -37
  88. data/lib/active_record/relation/record_fetch_warning.rb +2 -2
  89. data/lib/active_record/relation/spawn_methods.rb +1 -1
  90. data/lib/active_record/relation.rb +72 -61
  91. data/lib/active_record/result.rb +68 -7
  92. data/lib/active_record/sanitization.rb +7 -6
  93. data/lib/active_record/schema_dumper.rb +5 -0
  94. data/lib/active_record/schema_migration.rb +2 -1
  95. data/lib/active_record/scoping/named.rb +5 -2
  96. data/lib/active_record/statement_cache.rb +12 -12
  97. data/lib/active_record/store.rb +7 -3
  98. data/lib/active_record/tasks/database_tasks.rb +36 -16
  99. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  100. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  101. data/lib/active_record/test_fixtures.rb +12 -0
  102. data/lib/active_record/token_for.rb +1 -1
  103. data/lib/active_record/validations/uniqueness.rb +9 -8
  104. data/lib/active_record.rb +15 -0
  105. data/lib/arel/collectors/bind.rb +1 -1
  106. metadata +14 -14
@@ -11,9 +11,12 @@ require "active_record/connection_adapters/sqlite3/schema_definitions"
11
11
  require "active_record/connection_adapters/sqlite3/schema_dumper"
12
12
  require "active_record/connection_adapters/sqlite3/schema_statements"
13
13
 
14
- gem "sqlite3", ">= 1.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
 
@@ -103,7 +104,7 @@ module ActiveRecord
103
104
  class_attribute :shard_selector, instance_accessor: false, default: nil
104
105
 
105
106
  # Specifies the attributes that will be included in the output of the #inspect method
106
- class_attribute :attributes_for_inspect, instance_accessor: false, default: [:id]
107
+ class_attribute :attributes_for_inspect, instance_accessor: false, default: :all
107
108
 
108
109
  def self.application_record_class? # :nodoc:
109
110
  if ActiveRecord.application_record_class
@@ -349,7 +350,7 @@ module ActiveRecord
349
350
 
350
351
  # Returns a string like 'Post(id:integer, title:string, body:text)'
351
352
  def inspect # :nodoc:
352
- if self == Base
353
+ if self == Base || singleton_class?
353
354
  super
354
355
  elsif abstract_class?
355
356
  "#{super}(abstract)"
@@ -431,8 +432,8 @@ module ActiveRecord
431
432
  where(wheres).limit(1)
432
433
  }
433
434
 
434
- begin
435
- statement.execute(values.flatten, connection, allow_retry: true).first
435
+ statement.execute(values.flatten, connection, allow_retry: true).then do |r|
436
+ r.first
436
437
  rescue TypeError
437
438
  raise ActiveRecord::StatementInvalid
438
439
  end
@@ -731,7 +732,7 @@ module ActiveRecord
731
732
 
732
733
  # Returns the full contents of the record as a nicely formatted string.
733
734
  def full_inspect
734
- inspect_with_attributes(attribute_names)
735
+ inspect_with_attributes(all_attributes_for_inspect)
735
736
  end
736
737
 
737
738
  # Takes a PP and prettily prints this record to it, allowing you to get a nice result from <tt>pp record</tt>
@@ -784,7 +785,7 @@ module ActiveRecord
784
785
 
785
786
  @primary_key = klass.primary_key
786
787
  @strict_loading = klass.strict_loading_by_default
787
- @strict_loading_mode = :all
788
+ @strict_loading_mode = klass.strict_loading_mode
788
789
 
789
790
  klass.define_attribute_methods
790
791
  end
@@ -823,7 +824,13 @@ module ActiveRecord
823
824
  end
824
825
 
825
826
  def attributes_for_inspect
826
- self.class.attributes_for_inspect == :all ? attribute_names : self.class.attributes_for_inspect
827
+ self.class.attributes_for_inspect == :all ? all_attributes_for_inspect : self.class.attributes_for_inspect
828
+ end
829
+
830
+ def all_attributes_for_inspect
831
+ return [] unless @attributes
832
+
833
+ attribute_names
827
834
  end
828
835
  end
829
836
  end
@@ -45,7 +45,7 @@ module ActiveRecord
45
45
  attr_reader :uri
46
46
 
47
47
  def uri_parser
48
- @uri_parser ||= URI::Parser.new
48
+ @uri_parser ||= URI::RFC2396_Parser.new
49
49
  end
50
50
 
51
51
  # Converts the query parameters of the URI into a hash.
@@ -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)
@@ -12,7 +12,7 @@ module ActiveRecord
12
12
  @keys = Array(keys)
13
13
  end
14
14
 
15
- # Returns the first key in the list as the active key to perform encryptions
15
+ # Returns the last key in the list as the active key to perform encryptions
16
16
  #
17
17
  # When +ActiveRecord::Encryption.config.store_key_references+ is true, the key will include
18
18
  # a public tag referencing the key itself. That key will be stored in the public
@@ -11,7 +11,7 @@ module ActiveRecord
11
11
  attr_accessor :previous_schemes
12
12
 
13
13
  def initialize(key_provider: nil, key: nil, deterministic: nil, support_unencrypted_data: nil, downcase: nil, ignore_case: nil,
14
- previous_schemes: nil, **context_properties)
14
+ previous_schemes: nil, compress: true, compressor: nil, **context_properties)
15
15
  # Initializing all attributes to +nil+ as we want to allow a "not set" semantics so that we
16
16
  # can merge schemes without overriding values with defaults. See +#merge+
17
17
 
@@ -24,8 +24,13 @@ module ActiveRecord
24
24
  @previous_schemes_param = previous_schemes
25
25
  @previous_schemes = Array.wrap(previous_schemes)
26
26
  @context_properties = context_properties
27
+ @compress = compress
28
+ @compressor = compressor
27
29
 
28
30
  validate_config!
31
+
32
+ @context_properties[:encryptor] = Encryptor.new(compress: @compress) unless @compress
33
+ @context_properties[:encryptor] = Encryptor.new(compressor: compressor) if compressor
29
34
  end
30
35
 
31
36
  def ignore_case?
@@ -78,6 +83,8 @@ module ActiveRecord
78
83
  def validate_config!
79
84
  raise Errors::Configuration, "ignore_case: can only be used with deterministic encryption" if @ignore_case && !@deterministic
80
85
  raise Errors::Configuration, "key_provider: and key: can't be used simultaneously" if @key_provider_param && @key
86
+ raise Errors::Configuration, "compressor: can't be used with compress: false" if !@compress && @compressor
87
+ raise Errors::Configuration, "compressor: can't be used with encryptor" if @compressor && @context_properties[:encryptor]
81
88
  end
82
89
 
83
90
  def key_provider_from_key
@@ -53,4 +53,6 @@ module ActiveRecord
53
53
  Cipher.eager_load!
54
54
  end
55
55
  end
56
+
57
+ ActiveSupport.run_load_hooks :active_record_encryption, Encryption
56
58
  end
@@ -167,15 +167,6 @@ module ActiveRecord
167
167
  base.class_attribute(:defined_enums, instance_writer: false, default: {})
168
168
  end
169
169
 
170
- def load_schema! # :nodoc:
171
- defined_enums.each_key do |name|
172
- unless columns_hash.key?(resolve_attribute_name(name))
173
- raise "Unknown enum attribute '#{name}' for #{self.name}. Enums must be" \
174
- " backed by a database column."
175
- end
176
- end
177
- end
178
-
179
170
  class EnumType < Type::Value # :nodoc:
180
171
  delegate :type, to: :subtype
181
172
 
@@ -264,7 +255,13 @@ module ActiveRecord
264
255
 
265
256
  attribute(name, **options)
266
257
 
267
- decorate_attributes([name]) do |name, subtype|
258
+ decorate_attributes([name]) do |_name, subtype|
259
+ if subtype == ActiveModel::Type.default_value
260
+ raise "Undeclared attribute type for enum '#{name}' in #{self.name}. Enums must be" \
261
+ " backed by a database column or declared with an explicit type" \
262
+ " via `attribute`."
263
+ end
264
+
268
265
  subtype = subtype.subtype if EnumType === subtype
269
266
  EnumType.new(name, enum_values, subtype, raise_on_invalid_values: !validate)
270
267
  end