activerecord 7.2.3 → 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.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +192 -1261
  3. data/README.rdoc +2 -2
  4. data/lib/active_record/associations/alias_tracker.rb +4 -6
  5. data/lib/active_record/associations/association.rb +25 -5
  6. data/lib/active_record/associations/belongs_to_association.rb +2 -18
  7. data/lib/active_record/associations/builder/association.rb +7 -6
  8. data/lib/active_record/associations/collection_association.rb +4 -4
  9. data/lib/active_record/associations/disable_joins_association_scope.rb +1 -1
  10. data/lib/active_record/associations/has_many_through_association.rb +4 -9
  11. data/lib/active_record/associations/join_dependency/join_association.rb +27 -25
  12. data/lib/active_record/associations/preloader/association.rb +2 -2
  13. data/lib/active_record/associations/singular_association.rb +8 -3
  14. data/lib/active_record/associations.rb +50 -32
  15. data/lib/active_record/asynchronous_queries_tracker.rb +28 -24
  16. data/lib/active_record/attribute_methods/serialization.rb +1 -1
  17. data/lib/active_record/attribute_methods.rb +19 -24
  18. data/lib/active_record/attributes.rb +26 -37
  19. data/lib/active_record/autosave_association.rb +81 -49
  20. data/lib/active_record/base.rb +2 -2
  21. data/lib/active_record/callbacks.rb +1 -1
  22. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +16 -10
  23. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +0 -1
  24. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +0 -1
  25. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +31 -75
  26. data/lib/active_record/connection_adapters/abstract/database_statements.rb +90 -43
  27. data/lib/active_record/connection_adapters/abstract/query_cache.rb +14 -19
  28. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  29. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +2 -6
  30. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +27 -9
  31. data/lib/active_record/connection_adapters/abstract/transaction.rb +15 -5
  32. data/lib/active_record/connection_adapters/abstract_adapter.rb +27 -57
  33. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +28 -58
  34. data/lib/active_record/connection_adapters/mysql/quoting.rb +1 -15
  35. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +2 -8
  36. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +43 -45
  37. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +42 -98
  38. data/lib/active_record/connection_adapters/mysql2_adapter.rb +2 -16
  39. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +64 -42
  40. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +10 -0
  41. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +0 -1
  42. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +12 -14
  43. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
  44. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +51 -9
  45. data/lib/active_record/connection_adapters/postgresql_adapter.rb +44 -101
  46. data/lib/active_record/connection_adapters/schema_cache.rb +1 -3
  47. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +76 -100
  48. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -13
  49. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +0 -6
  50. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +13 -0
  51. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +8 -2
  52. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +60 -22
  53. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +37 -67
  54. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -18
  55. data/lib/active_record/connection_handling.rb +29 -11
  56. data/lib/active_record/core.rb +15 -60
  57. data/lib/active_record/counter_cache.rb +1 -1
  58. data/lib/active_record/database_configurations/connection_url_resolver.rb +1 -3
  59. data/lib/active_record/delegated_type.rb +18 -18
  60. data/lib/active_record/encryption/config.rb +3 -1
  61. data/lib/active_record/encryption/encryptable_record.rb +5 -5
  62. data/lib/active_record/encryption/encrypted_attribute_type.rb +11 -2
  63. data/lib/active_record/encryption/encryptor.rb +35 -29
  64. data/lib/active_record/encryption/extended_deterministic_queries.rb +4 -2
  65. data/lib/active_record/encryption/scheme.rb +8 -1
  66. data/lib/active_record/enum.rb +12 -13
  67. data/lib/active_record/errors.rb +16 -8
  68. data/lib/active_record/fixture_set/table_row.rb +2 -19
  69. data/lib/active_record/fixtures.rb +0 -1
  70. data/lib/active_record/future_result.rb +14 -10
  71. data/lib/active_record/gem_version.rb +4 -4
  72. data/lib/active_record/insert_all.rb +1 -1
  73. data/lib/active_record/marshalling.rb +1 -4
  74. data/lib/active_record/migration/command_recorder.rb +22 -5
  75. data/lib/active_record/migration/compatibility.rb +5 -2
  76. data/lib/active_record/migration.rb +36 -35
  77. data/lib/active_record/model_schema.rb +1 -1
  78. data/lib/active_record/nested_attributes.rb +4 -6
  79. data/lib/active_record/persistence.rb +128 -130
  80. data/lib/active_record/query_cache.rb +5 -4
  81. data/lib/active_record/query_logs.rb +98 -44
  82. data/lib/active_record/query_logs_formatter.rb +17 -28
  83. data/lib/active_record/querying.rb +10 -10
  84. data/lib/active_record/railtie.rb +5 -6
  85. data/lib/active_record/railties/databases.rake +1 -2
  86. data/lib/active_record/reflection.rb +9 -7
  87. data/lib/active_record/relation/batches/batch_enumerator.rb +4 -3
  88. data/lib/active_record/relation/batches.rb +132 -72
  89. data/lib/active_record/relation/calculations.rb +55 -55
  90. data/lib/active_record/relation/delegation.rb +25 -14
  91. data/lib/active_record/relation/finder_methods.rb +31 -32
  92. data/lib/active_record/relation/merger.rb +8 -8
  93. data/lib/active_record/relation/predicate_builder/association_query_value.rb +0 -2
  94. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +1 -1
  95. data/lib/active_record/relation/predicate_builder/relation_handler.rb +4 -3
  96. data/lib/active_record/relation/predicate_builder.rb +5 -0
  97. data/lib/active_record/relation/query_attribute.rb +1 -1
  98. data/lib/active_record/relation/query_methods.rb +90 -91
  99. data/lib/active_record/relation/record_fetch_warning.rb +2 -2
  100. data/lib/active_record/relation/spawn_methods.rb +1 -1
  101. data/lib/active_record/relation/where_clause.rb +2 -8
  102. data/lib/active_record/relation.rb +77 -76
  103. data/lib/active_record/result.rb +68 -7
  104. data/lib/active_record/sanitization.rb +7 -6
  105. data/lib/active_record/schema_dumper.rb +16 -29
  106. data/lib/active_record/schema_migration.rb +2 -1
  107. data/lib/active_record/scoping/named.rb +5 -2
  108. data/lib/active_record/secure_token.rb +3 -3
  109. data/lib/active_record/signed_id.rb +6 -7
  110. data/lib/active_record/statement_cache.rb +12 -12
  111. data/lib/active_record/store.rb +7 -3
  112. data/lib/active_record/tasks/database_tasks.rb +24 -15
  113. data/lib/active_record/tasks/mysql_database_tasks.rb +0 -2
  114. data/lib/active_record/tasks/postgresql_database_tasks.rb +0 -7
  115. data/lib/active_record/tasks/sqlite_database_tasks.rb +2 -2
  116. data/lib/active_record/test_fixtures.rb +12 -0
  117. data/lib/active_record/testing/query_assertions.rb +2 -2
  118. data/lib/active_record/token_for.rb +1 -1
  119. data/lib/active_record/transactions.rb +1 -3
  120. data/lib/active_record/validations/uniqueness.rb +8 -8
  121. data/lib/active_record.rb +16 -1
  122. data/lib/arel/collectors/bind.rb +1 -1
  123. data/lib/arel/crud.rb +0 -2
  124. data/lib/arel/delete_manager.rb +0 -5
  125. data/lib/arel/nodes/delete_statement.rb +2 -4
  126. data/lib/arel/nodes/update_statement.rb +2 -4
  127. data/lib/arel/select_manager.rb +2 -6
  128. data/lib/arel/update_manager.rb +0 -5
  129. data/lib/arel/visitors/dot.rb +0 -2
  130. data/lib/arel/visitors/sqlite.rb +0 -25
  131. data/lib/arel/visitors/to_sql.rb +1 -3
  132. metadata +14 -11
@@ -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
@@ -21,7 +24,7 @@ module ActiveRecord
21
24
  # The SQLite3 adapter works with the sqlite3-ruby drivers
22
25
  # (available as gem from https://rubygems.org/gems/sqlite3).
23
26
  #
24
- # ==== Options
27
+ # Options:
25
28
  #
26
29
  # * <tt>:database</tt> - Path to the database file.
27
30
  class SQLite3Adapter < AbstractAdapter
@@ -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?
@@ -199,12 +207,7 @@ module ActiveRecord
199
207
  !(@raw_connection.nil? || @raw_connection.closed?)
200
208
  end
201
209
 
202
- def active?
203
- if connected?
204
- verified!
205
- true
206
- end
207
- end
210
+ alias_method :active?, :connected?
208
211
 
209
212
  alias :reset! :reconnect!
210
213
 
@@ -283,6 +286,38 @@ module ActiveRecord
283
286
  exec_query "DROP INDEX #{quote_column_name(index_name)}"
284
287
  end
285
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
+
286
321
  # Renames a table.
287
322
  #
288
323
  # Example:
@@ -372,7 +407,7 @@ module ActiveRecord
372
407
  end
373
408
  alias :add_belongs_to :add_reference
374
409
 
375
- FK_REGEX = /.*FOREIGN KEY\s+\("([^"]+)"\)\s+REFERENCES\s+"(\w+)"\s+\("(\w+)"\)/
410
+ FK_REGEX = /.*FOREIGN KEY\s+\("(\w+)"\)\s+REFERENCES\s+"(\w+)"\s+\("(\w+)"\)/
376
411
  DEFERRABLE_REGEX = /DEFERRABLE INITIALLY (\w+)/
377
412
  def foreign_keys(table_name)
378
413
  # SQLite returns 1 row for each column of composite foreign keys.
@@ -433,10 +468,6 @@ module ActiveRecord
433
468
  @config.fetch(:flags, 0).anybits?(::SQLite3::Constants::Open::SHAREDCACHE)
434
469
  end
435
470
 
436
- def use_insert_returning?
437
- @use_insert_returning
438
- end
439
-
440
471
  def get_database_version # :nodoc:
441
472
  SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)", "SCHEMA"))
442
473
  end
@@ -547,8 +578,8 @@ module ActiveRecord
547
578
  yield definition if block_given?
548
579
  end
549
580
 
550
- disable_referential_integrity do
551
- transaction do
581
+ transaction do
582
+ disable_referential_integrity do
552
583
  move_table(table_name, altered_table_name, options.merge(temporary: true))
553
584
  move_table(altered_table_name, table_name, &caller)
554
585
  end
@@ -666,6 +697,8 @@ module ActiveRecord
666
697
  InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
667
698
  elsif exception.message.match?(/called on a closed database/i)
668
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)
669
702
  else
670
703
  super
671
704
  end
@@ -692,6 +725,8 @@ module ActiveRecord
692
725
  end
693
726
 
694
727
  basic_structure.map do |column|
728
+ column = column.to_h
729
+
695
730
  column_name = column["name"]
696
731
 
697
732
  if collation_hash.has_key? column_name
@@ -783,12 +818,15 @@ module ActiveRecord
783
818
  if @config[:timeout] && @config[:retries]
784
819
  raise ArgumentError, "Cannot specify both timeout and retries arguments"
785
820
  elsif @config[:timeout]
786
- @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
787
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
788
828
  retries = self.class.type_cast_config_to_integer(@config[:retries])
789
- raw_connection.busy_handler do |count|
790
- count <= retries
791
- end
829
+ raw_connection.busy_handler { |count| count <= retries }
792
830
  end
793
831
 
794
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
@@ -121,7 +121,7 @@ module ActiveRecord
121
121
  end
122
122
 
123
123
  def active?
124
- connected? && @lock.synchronize { @raw_connection&.ping; verified! } || false
124
+ connected? && @lock.synchronize { @raw_connection&.ping } || false
125
125
  rescue ::Trilogy::Error
126
126
  false
127
127
  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
@@ -170,10 +172,20 @@ module ActiveRecord
170
172
  prevent_writes = true if role == ActiveRecord.reading_role
171
173
 
172
174
  append_to_connected_to_stack(role: role, shard: shard, prevent_writes: prevent_writes, klasses: classes)
173
- begin
174
- yield
175
- ensure
176
- connected_to_stack.pop
175
+ yield
176
+ ensure
177
+ connected_to_stack.pop
178
+ end
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)
177
189
  end
178
190
  end
179
191
 
@@ -361,6 +373,14 @@ module ActiveRecord
361
373
  connection_pool.schema_cache.clear!
362
374
  end
363
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
+
364
384
  private
365
385
  def resolve_config_for_connection(config_or_env)
366
386
  raise "Anonymous class is not allowed." unless name
@@ -375,13 +395,11 @@ module ActiveRecord
375
395
  prevent_writes = true if role == ActiveRecord.reading_role
376
396
 
377
397
  append_to_connected_to_stack(role: role, shard: shard, prevent_writes: prevent_writes, klasses: [self])
378
- begin
379
- return_value = yield
380
- return_value.load if return_value.is_a? ActiveRecord::Relation
381
- return_value
382
- ensure
383
- self.connected_to_stack.pop
384
- end
398
+ return_value = yield
399
+ return_value.load if return_value.is_a? ActiveRecord::Relation
400
+ return_value
401
+ ensure
402
+ self.connected_to_stack.pop
385
403
  end
386
404
 
387
405
  def append_to_connected_to_stack(entry)
@@ -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:
@@ -265,7 +254,7 @@ module ActiveRecord
265
254
  return super if StatementCache.unsupported_value?(id)
266
255
 
267
256
  cached_find_by([primary_key], [id]) ||
268
- raise(RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id.inspect}", name, primary_key, id))
257
+ raise(RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", name, primary_key, id))
269
258
  end
270
259
 
271
260
  def find_by(*args) # :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
@@ -585,7 +574,7 @@ module ActiveRecord
585
574
  #
586
575
  # topic = Topic.new(title: "Budget", author_name: "Jason")
587
576
  # topic.slice(:title, :author_name)
588
- # # => { "title" => "Budget", "author_name" => "Jason" }
577
+ # => { "title" => "Budget", "author_name" => "Jason" }
589
578
  #
590
579
  #--
591
580
  # Implemented by ActiveModel::Access#slice.
@@ -599,7 +588,7 @@ module ActiveRecord
599
588
  #
600
589
  # topic = Topic.new(title: "Budget", author_name: "Jason")
601
590
  # topic.values_at(:title, :author_name)
602
- # # => ["Budget", "Jason"]
591
+ # => ["Budget", "Jason"]
603
592
  #
604
593
  #--
605
594
  # Implemented by ActiveModel::Access#values_at.
@@ -676,14 +665,12 @@ module ActiveRecord
676
665
  # Sets the record to strict_loading mode. This will raise an error
677
666
  # if the record tries to lazily load an association.
678
667
  #
679
- # NOTE: Strict loading is disabled during validation in order to let the record validate its association.
680
- #
681
668
  # user = User.first
682
669
  # user.strict_loading! # => true
683
670
  # user.address.city
684
- # # => ActiveRecord::StrictLoadingViolationError
671
+ # => ActiveRecord::StrictLoadingViolationError
685
672
  # user.comments.to_a
686
- # # => ActiveRecord::StrictLoadingViolationError
673
+ # => ActiveRecord::StrictLoadingViolationError
687
674
  #
688
675
  # ==== Parameters
689
676
  #
@@ -703,7 +690,7 @@ module ActiveRecord
703
690
  # user.address.city # => "Tatooine"
704
691
  # user.comments.to_a # => [#<Comment:0x00...]
705
692
  # user.comments.first.ratings.to_a
706
- # # => ActiveRecord::StrictLoadingViolationError
693
+ # => ActiveRecord::StrictLoadingViolationError
707
694
  def strict_loading!(value = true, mode: :all)
708
695
  unless [:all, :n_plus_one_only].include?(mode)
709
696
  raise ArgumentError, "The :mode option must be one of [:all, :n_plus_one_only] but #{mode.inspect} was provided."
@@ -725,29 +712,11 @@ module ActiveRecord
725
712
  @strict_loading_mode == :all
726
713
  end
727
714
 
728
- # Prevents records from being written to the database:
729
- #
730
- # customer = Customer.new
731
- # customer.readonly!
732
- # customer.save # raises ActiveRecord::ReadOnlyRecord
733
- #
734
- # customer = Customer.first
735
- # customer.readonly!
736
- # customer.update(name: 'New Name') # raises ActiveRecord::ReadOnlyRecord
737
- #
738
- # Read-only records cannot be deleted from the database either:
715
+ # Marks this record as read only.
739
716
  #
740
717
  # customer = Customer.first
741
718
  # customer.readonly!
742
- # customer.destroy # raises ActiveRecord::ReadOnlyRecord
743
- #
744
- # Please, note that the objects themselves are still mutable in memory:
745
- #
746
- # customer = Customer.new
747
- # customer.readonly!
748
- # customer.name = 'New Name' # OK
749
- #
750
- # but you won't be able to persist the changes.
719
+ # customer.save # Raises an ActiveRecord::ReadOnlyRecord
751
720
  def readonly!
752
721
  @readonly = true
753
722
  end
@@ -756,26 +725,12 @@ module ActiveRecord
756
725
  self.class.connection_handler
757
726
  end
758
727
 
759
- # Returns the attributes of the record as a nicely formatted string.
760
- #
761
- # Post.first.inspect
762
- # #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
763
- #
764
- # The attributes can be limited by setting <tt>.attributes_for_inspect</tt>.
765
- #
766
- # Post.attributes_for_inspect = [:id, :title]
767
- # Post.first.inspect
768
- # #=> "#<Post id: 1, title: "Hello, World!">"
728
+ # Returns the attributes specified by <tt>.attributes_for_inspect</tt> as a nicely formatted string.
769
729
  def inspect
770
730
  inspect_with_attributes(attributes_for_inspect)
771
731
  end
772
732
 
773
- # Returns all attributes of the record as a nicely formatted string,
774
- # ignoring <tt>.attributes_for_inspect</tt>.
775
- #
776
- # Post.first.full_inspect
777
- # #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
778
- #
733
+ # Returns the full contents of the record as a nicely formatted string.
779
734
  def full_inspect
780
735
  inspect_with_attributes(all_attributes_for_inspect)
781
736
  end
@@ -830,7 +785,7 @@ module ActiveRecord
830
785
 
831
786
  @primary_key = klass.primary_key
832
787
  @strict_loading = klass.strict_loading_by_default
833
- @strict_loading_mode = :all
788
+ @strict_loading_mode = klass.strict_loading_mode
834
789
 
835
790
  klass.define_attribute_methods
836
791
  end
@@ -28,7 +28,7 @@ module ActiveRecord
28
28
  # # For the Post with id #1, reset the comments_count
29
29
  # Post.reset_counters(1, :comments)
30
30
  #
31
- # # Like above, but also touch the updated_at and/or updated_on
31
+ # # Like above, but also touch the +updated_at+ and/or +updated_on+
32
32
  # # attributes.
33
33
  # Post.reset_counters(1, :comments, touch: true)
34
34
  def reset_counters(id, *counters, touch: nil)
@@ -81,9 +81,7 @@ module ActiveRecord
81
81
 
82
82
  def resolved_adapter
83
83
  adapter = uri.scheme && @uri.scheme.tr("-", "_")
84
- if adapter && ActiveRecord.protocol_adapters[adapter]
85
- adapter = ActiveRecord.protocol_adapters[adapter]
86
- end
84
+ adapter = ActiveRecord.protocol_adapters[adapter] || adapter
87
85
  adapter
88
86
  end
89
87
 
@@ -181,16 +181,16 @@ module ActiveRecord
181
181
  # delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
182
182
  # end
183
183
  #
184
- # @entry.entryable_class # => Message or Comment
185
- # @entry.entryable_name # => "message" or "comment"
186
- # Entry.messages # => Entry.where(entryable_type: "Message")
187
- # @entry.message? # => true when entryable_type == "Message"
188
- # @entry.message # => returns the message record, when entryable_type == "Message", otherwise nil
189
- # @entry.message_id # => returns entryable_id, when entryable_type == "Message", otherwise nil
190
- # Entry.comments # => Entry.where(entryable_type: "Comment")
191
- # @entry.comment? # => true when entryable_type == "Comment"
192
- # @entry.comment # => returns the comment record, when entryable_type == "Comment", otherwise nil
193
- # @entry.comment_id # => returns entryable_id, when entryable_type == "Comment", otherwise nil
184
+ # Entry#entryable_class # => +Message+ or +Comment+
185
+ # Entry#entryable_name # => "message" or "comment"
186
+ # Entry.messages # => Entry.where(entryable_type: "Message")
187
+ # Entry#message? # => true when entryable_type == "Message"
188
+ # Entry#message # => returns the message record, when entryable_type == "Message", otherwise nil
189
+ # Entry#message_id # => returns entryable_id, when entryable_type == "Message", otherwise nil
190
+ # Entry.comments # => Entry.where(entryable_type: "Comment")
191
+ # Entry#comment? # => true when entryable_type == "Comment"
192
+ # Entry#comment # => returns the comment record, when entryable_type == "Comment", otherwise nil
193
+ # Entry#comment_id # => returns entryable_id, when entryable_type == "Comment", otherwise nil
194
194
  #
195
195
  # You can also declare namespaced types:
196
196
  #
@@ -199,25 +199,25 @@ module ActiveRecord
199
199
  # end
200
200
  #
201
201
  # Entry.access_notice_messages
202
- # @entry.access_notice_message
203
- # @entry.access_notice_message?
202
+ # entry.access_notice_message
203
+ # entry.access_notice_message?
204
204
  #
205
- # ==== Options
205
+ # === Options
206
206
  #
207
207
  # The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc.
208
208
  # The following options can be included to specialize the behavior of the delegated type convenience methods.
209
209
  #
210
- # [+:foreign_key+]
210
+ # [:foreign_key]
211
211
  # Specify the foreign key used for the convenience methods. By default this is guessed to be the passed
212
212
  # +role+ with an "_id" suffix. So a class that defines a
213
213
  # <tt>delegated_type :entryable, types: %w[ Message Comment ]</tt> association will use "entryable_id" as
214
214
  # the default <tt>:foreign_key</tt>.
215
- # [+:foreign_type+]
215
+ # [:foreign_type]
216
216
  # Specify the column used to store the associated object's type. By default this is inferred to be the passed
217
217
  # +role+ with a "_type" suffix. A class that defines a
218
218
  # <tt>delegated_type :entryable, types: %w[ Message Comment ]</tt> association will use "entryable_type" as
219
219
  # the default <tt>:foreign_type</tt>.
220
- # [+:primary_key+]
220
+ # [:primary_key]
221
221
  # Specify the method that returns the primary key of associated object used for the convenience methods.
222
222
  # By default this is +id+.
223
223
  #
@@ -226,8 +226,8 @@ module ActiveRecord
226
226
  # delegated_type :entryable, types: %w[ Message Comment ], primary_key: :uuid, foreign_key: :entryable_uuid
227
227
  # end
228
228
  #
229
- # @entry.message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
230
- # @entry.comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil
229
+ # Entry#message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
230
+ # Entry#comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil
231
231
  def delegated_type(role, types:, **options)
232
232
  belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
233
233
  define_delegated_type_methods role, types: types, options: options
@@ -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