activerecord 8.0.5.1 → 8.1.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 (157) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +421 -657
  3. data/README.rdoc +1 -1
  4. data/lib/active_record/association_relation.rb +1 -1
  5. data/lib/active_record/associations/association.rb +1 -1
  6. data/lib/active_record/associations/belongs_to_association.rb +0 -2
  7. data/lib/active_record/associations/builder/association.rb +16 -5
  8. data/lib/active_record/associations/builder/belongs_to.rb +17 -4
  9. data/lib/active_record/associations/builder/collection_association.rb +7 -3
  10. data/lib/active_record/associations/builder/has_one.rb +1 -1
  11. data/lib/active_record/associations/builder/singular_association.rb +33 -5
  12. data/lib/active_record/associations/collection_proxy.rb +22 -4
  13. data/lib/active_record/associations/deprecation.rb +88 -0
  14. data/lib/active_record/associations/errors.rb +3 -0
  15. data/lib/active_record/associations/join_dependency.rb +4 -2
  16. data/lib/active_record/associations/preloader/batch.rb +1 -7
  17. data/lib/active_record/associations/preloader/branch.rb +1 -0
  18. data/lib/active_record/associations.rb +159 -21
  19. data/lib/active_record/attribute_methods/serialization.rb +16 -3
  20. data/lib/active_record/attribute_methods.rb +1 -1
  21. data/lib/active_record/attributes.rb +3 -0
  22. data/lib/active_record/autosave_association.rb +2 -2
  23. data/lib/active_record/base.rb +2 -3
  24. data/lib/active_record/coders/json.rb +14 -5
  25. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +1 -3
  26. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +15 -0
  27. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -12
  28. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +383 -52
  29. data/lib/active_record/connection_adapters/abstract/database_statements.rb +27 -31
  30. data/lib/active_record/connection_adapters/abstract/query_cache.rb +25 -18
  31. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  32. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  33. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +26 -34
  34. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  35. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +88 -25
  36. data/lib/active_record/connection_adapters/abstract/transaction.rb +16 -3
  37. data/lib/active_record/connection_adapters/abstract_adapter.rb +96 -49
  38. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +43 -11
  39. data/lib/active_record/connection_adapters/column.rb +17 -4
  40. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  41. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  42. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  43. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +26 -4
  44. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +27 -22
  45. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -0
  46. data/lib/active_record/connection_adapters/postgresql/column.rb +2 -4
  47. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +18 -23
  48. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  49. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  50. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  51. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +1 -1
  52. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +8 -21
  53. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +65 -30
  54. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +74 -38
  55. data/lib/active_record/connection_adapters/postgresql_adapter.rb +10 -12
  56. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  57. data/lib/active_record/connection_adapters/sqlite3/column.rb +2 -8
  58. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +37 -25
  59. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  60. data/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +3 -15
  61. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -13
  62. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +57 -33
  63. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
  64. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
  65. data/lib/active_record/connection_adapters.rb +1 -0
  66. data/lib/active_record/connection_handling.rb +8 -12
  67. data/lib/active_record/core.rb +5 -4
  68. data/lib/active_record/counter_cache.rb +33 -8
  69. data/lib/active_record/database_configurations/database_config.rb +5 -1
  70. data/lib/active_record/database_configurations/hash_config.rb +50 -9
  71. data/lib/active_record/database_configurations/url_config.rb +13 -3
  72. data/lib/active_record/database_configurations.rb +7 -3
  73. data/lib/active_record/delegated_type.rb +1 -1
  74. data/lib/active_record/dynamic_matchers.rb +54 -69
  75. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  76. data/lib/active_record/encryption/encrypted_attribute_type.rb +1 -1
  77. data/lib/active_record/encryption/scheme.rb +1 -1
  78. data/lib/active_record/enum.rb +24 -8
  79. data/lib/active_record/errors.rb +23 -7
  80. data/lib/active_record/explain_registry.rb +0 -1
  81. data/lib/active_record/filter_attribute_handler.rb +73 -0
  82. data/lib/active_record/fixtures.rb +2 -2
  83. data/lib/active_record/future_result.rb +0 -2
  84. data/lib/active_record/gem_version.rb +3 -3
  85. data/lib/active_record/inheritance.rb +1 -1
  86. data/lib/active_record/insert_all.rb +14 -9
  87. data/lib/active_record/locking/optimistic.rb +7 -0
  88. data/lib/active_record/locking/pessimistic.rb +5 -0
  89. data/lib/active_record/log_subscriber.rb +1 -5
  90. data/lib/active_record/middleware/shard_selector.rb +34 -17
  91. data/lib/active_record/migration/command_recorder.rb +15 -2
  92. data/lib/active_record/migration/compatibility.rb +34 -24
  93. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  94. data/lib/active_record/migration.rb +26 -16
  95. data/lib/active_record/model_schema.rb +10 -7
  96. data/lib/active_record/nested_attributes.rb +2 -0
  97. data/lib/active_record/persistence.rb +34 -3
  98. data/lib/active_record/query_cache.rb +22 -15
  99. data/lib/active_record/query_logs.rb +3 -7
  100. data/lib/active_record/railtie.rb +32 -3
  101. data/lib/active_record/railties/databases.rake +16 -4
  102. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  103. data/lib/active_record/railties/job_runtime.rb +10 -11
  104. data/lib/active_record/reflection.rb +42 -3
  105. data/lib/active_record/relation/batches.rb +26 -12
  106. data/lib/active_record/relation/calculations.rb +20 -9
  107. data/lib/active_record/relation/delegation.rb +0 -1
  108. data/lib/active_record/relation/finder_methods.rb +28 -12
  109. data/lib/active_record/relation/merger.rb +2 -2
  110. data/lib/active_record/relation/predicate_builder/array_handler.rb +1 -3
  111. data/lib/active_record/relation/predicate_builder.rb +2 -2
  112. data/lib/active_record/relation/query_attribute.rb +3 -1
  113. data/lib/active_record/relation/query_methods.rb +40 -31
  114. data/lib/active_record/relation/where_clause.rb +2 -11
  115. data/lib/active_record/relation.rb +26 -14
  116. data/lib/active_record/result.rb +44 -21
  117. data/lib/active_record/sanitization.rb +2 -0
  118. data/lib/active_record/schema_dumper.rb +12 -10
  119. data/lib/active_record/scoping.rb +0 -1
  120. data/lib/active_record/signed_id.rb +43 -15
  121. data/lib/active_record/statement_cache.rb +13 -9
  122. data/lib/active_record/store.rb +44 -19
  123. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  124. data/lib/active_record/tasks/database_tasks.rb +2 -21
  125. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
  126. data/lib/active_record/tasks/postgresql_database_tasks.rb +5 -39
  127. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  128. data/lib/active_record/test_databases.rb +10 -2
  129. data/lib/active_record/test_fixtures.rb +27 -2
  130. data/lib/active_record/testing/query_assertions.rb +8 -2
  131. data/lib/active_record/timestamp.rb +4 -2
  132. data/lib/active_record/transaction.rb +2 -5
  133. data/lib/active_record/transactions.rb +32 -10
  134. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  135. data/lib/active_record/type/internal/timezone.rb +7 -0
  136. data/lib/active_record/type/json.rb +15 -2
  137. data/lib/active_record/type/serialized.rb +11 -9
  138. data/lib/active_record/type/type_map.rb +1 -1
  139. data/lib/active_record/type_caster/connection.rb +2 -1
  140. data/lib/active_record/validations/associated.rb +1 -1
  141. data/lib/active_record.rb +65 -3
  142. data/lib/arel/alias_predication.rb +2 -0
  143. data/lib/arel/crud.rb +6 -11
  144. data/lib/arel/nodes/count.rb +2 -2
  145. data/lib/arel/nodes/function.rb +4 -10
  146. data/lib/arel/nodes/named_function.rb +2 -2
  147. data/lib/arel/nodes/node.rb +1 -1
  148. data/lib/arel/nodes.rb +0 -2
  149. data/lib/arel/predications.rb +3 -1
  150. data/lib/arel/select_manager.rb +7 -2
  151. data/lib/arel/visitors/dot.rb +0 -3
  152. data/lib/arel/visitors/postgresql.rb +55 -0
  153. data/lib/arel/visitors/sqlite.rb +55 -8
  154. data/lib/arel/visitors/to_sql.rb +3 -21
  155. data/lib/arel.rb +3 -1
  156. metadata +14 -10
  157. data/lib/active_record/normalization.rb +0 -163
@@ -6,7 +6,7 @@ module ActiveRecord
6
6
  class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
7
7
  private
8
8
  def virtual_tables(stream)
9
- virtual_tables = @connection.virtual_tables.reject { |name, _| ignored?(name) }
9
+ virtual_tables = @connection.virtual_tables
10
10
  if virtual_tables.any?
11
11
  stream.puts
12
12
  stream.puts " # Virtual tables defined in this database."
@@ -19,23 +19,11 @@ module ActiveRecord
19
19
  end
20
20
 
21
21
  def default_primary_key?(column)
22
- schema_type(column) == :integer && primary_key_has_autoincrement?
22
+ schema_type(column) == :integer
23
23
  end
24
24
 
25
25
  def explicit_primary_key_default?(column)
26
- column.bigint? || (column.type == :integer && !primary_key_has_autoincrement?)
27
- end
28
-
29
- def primary_key_has_autoincrement?
30
- return false unless table_name
31
-
32
- table_sql = @connection.query_value(<<~SQL, "SCHEMA")
33
- SELECT sql FROM sqlite_master WHERE name = #{@connection.quote(table_name)} AND type = 'table'
34
- UNION ALL
35
- SELECT sql FROM sqlite_temp_master WHERE name = #{@connection.quote(table_name)} AND type = 'table'
36
- SQL
37
-
38
- table_sql.to_s.match?(/\bAUTOINCREMENT\b/i)
26
+ column.bigint?
39
27
  end
40
28
 
41
29
  def prepare_column_options(column)
@@ -63,23 +63,13 @@ module ActiveRecord
63
63
  end
64
64
 
65
65
  def remove_foreign_key(from_table, to_table = nil, **options)
66
- return if options.delete(:if_exists) == true && !foreign_key_exists?(from_table, to_table)
66
+ return if options.delete(:if_exists) && !foreign_key_exists?(from_table, to_table, **options.slice(:column))
67
67
 
68
68
  to_table ||= options[:to_table]
69
69
  options = options.except(:name, :to_table, :validate)
70
- foreign_keys = foreign_keys(from_table)
71
-
72
- fkey = foreign_keys.detect do |fk|
73
- table = to_table || begin
74
- table = options[:column].to_s.delete_suffix("_id")
75
- Base.pluralize_table_names ? table.pluralize : table
76
- end
77
- table = strip_table_name_prefix_and_suffix(table)
78
- options = options.slice(*fk.options.keys)
79
- fk_to_table = strip_table_name_prefix_and_suffix(fk.to_table)
80
- fk_to_table == table && options.all? { |k, v| fk.options[k].to_s == v.to_s }
81
- end || raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table || options}")
70
+ fkey = foreign_key_for!(from_table, to_table: to_table, **options)
82
71
 
72
+ foreign_keys = foreign_keys(from_table)
83
73
  foreign_keys.delete(fkey)
84
74
  alter_table(from_table, foreign_keys)
85
75
  end
@@ -157,6 +147,7 @@ module ActiveRecord
157
147
 
158
148
  Column.new(
159
149
  field["name"],
150
+ lookup_cast_type(field["type"]),
160
151
  default_value,
161
152
  type_metadata,
162
153
  field["notnull"].to_i == 0,
@@ -19,14 +19,30 @@ SQLite3::ForkSafety.suppress_warnings!
19
19
 
20
20
  module ActiveRecord
21
21
  module ConnectionAdapters # :nodoc:
22
- # = Active Record SQLite3 Adapter
22
+ # = Active Record \SQLite3 Adapter
23
23
  #
24
- # The SQLite3 adapter works with the sqlite3-ruby drivers
25
- # (available as gem from https://rubygems.org/gems/sqlite3).
24
+ # The \SQLite3 adapter works with the sqlite3[https://sparklemotion.github.io/sqlite3-ruby/]
25
+ # driver.
26
26
  #
27
27
  # ==== Options
28
28
  #
29
- # * <tt>:database</tt> - Path to the database file.
29
+ # * +:database+ (String): Filesystem path to the database file.
30
+ # * +:statement_limit+ (Integer): Maximum number of prepared statements to cache per database connection. (default: 1000)
31
+ # * +:timeout+ (Integer): Timeout in milliseconds to use when waiting for a lock. (default: no wait)
32
+ # * +:strict+ (Boolean): Enable or disable strict mode. When enabled, this will
33
+ # {disallow double-quoted string literals in SQL
34
+ # statements}[https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted].
35
+ # (default: see strict_strings_by_default)
36
+ # * +:extensions+ (Array): (<b>requires sqlite3 v2.4.0</b>) Each entry specifies a sqlite extension
37
+ # to load for this database. The entry may be a filesystem path, or the name of a class that
38
+ # responds to +.to_path+ to provide the filesystem path for the extension. See {sqlite3-ruby
39
+ # documentation}[https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html#class-SQLite3::Database-label-SQLite+Extensions]
40
+ # for more information.
41
+ #
42
+ # There may be other options available specific to the SQLite3 driver. Please read the
43
+ # documentation for
44
+ # {SQLite3::Database.new}[https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html#method-c-new]
45
+ #
30
46
  class SQLite3Adapter < AbstractAdapter
31
47
  ADAPTER_NAME = "SQLite"
32
48
 
@@ -46,10 +62,14 @@ module ActiveRecord
46
62
 
47
63
  args << "-#{options[:mode]}" if options[:mode]
48
64
  args << "-header" if options[:header]
49
- args << File.expand_path(config.database, Rails.respond_to?(:root) ? Rails.root : nil)
65
+ args << File.expand_path(config.database, defined?(Rails.root) ? Rails.root : nil)
50
66
 
51
67
  find_cmd_and_exec(ActiveRecord.database_cli[:sqlite], *args)
52
68
  end
69
+
70
+ def native_database_types # :nodoc:
71
+ NATIVE_DATABASE_TYPES
72
+ end
53
73
  end
54
74
 
55
75
  include SQLite3::Quoting
@@ -58,12 +78,19 @@ module ActiveRecord
58
78
 
59
79
  ##
60
80
  # :singleton-method:
61
- # Configure the SQLite3Adapter to be used in a strict strings mode.
62
- # This will disable double-quoted string literals, because otherwise typos can silently go unnoticed.
63
- # For example, it is possible to create an index for a non existing column.
81
+ #
82
+ # Configure the SQLite3Adapter to be used in a "strict strings" mode. When enabled, this will
83
+ # {disallow double-quoted string literals in SQL
84
+ # statements}[https://www.sqlite.org/quirks.html#double_quoted_string_literals_are_accepted],
85
+ # which may prevent some typographical errors like creating an index for a non-existent
86
+ # column. The default is +false+.
87
+ #
64
88
  # If you wish to enable this mode you can add the following line to your application.rb file:
65
89
  #
66
90
  # config.active_record.sqlite3_adapter_strict_strings_by_default = true
91
+ #
92
+ # This can also be configured on individual databases by setting the +strict:+ option.
93
+ #
67
94
  class_attribute :strict_strings_by_default, default: false
68
95
 
69
96
  NATIVE_DATABASE_TYPES = {
@@ -122,13 +149,18 @@ module ActiveRecord
122
149
  end
123
150
  end
124
151
 
125
- @last_affected_rows = nil
126
152
  @previous_read_uncommitted = nil
127
153
  @config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict)
154
+
155
+ extensions = @config.fetch(:extensions, []).map do |extension|
156
+ extension.safe_constantize || extension
157
+ end
158
+
128
159
  @connection_parameters = @config.merge(
129
160
  database: @config[:database].to_s,
130
161
  results_as_hash: true,
131
162
  default_transaction_mode: :immediate,
163
+ extensions: extensions
132
164
  )
133
165
  end
134
166
 
@@ -153,7 +185,7 @@ module ActiveRecord
153
185
  end
154
186
 
155
187
  def supports_expression_index?
156
- database_version >= "3.9.0"
188
+ true
157
189
  end
158
190
 
159
191
  def requires_reloading?
@@ -181,7 +213,7 @@ module ActiveRecord
181
213
  end
182
214
 
183
215
  def supports_common_table_expressions?
184
- database_version >= "3.8.3"
216
+ true
185
217
  end
186
218
 
187
219
  def supports_insert_returning?
@@ -229,10 +261,6 @@ module ActiveRecord
229
261
  true
230
262
  end
231
263
 
232
- def native_database_types # :nodoc:
233
- NATIVE_DATABASE_TYPES
234
- end
235
-
236
264
  # Returns the current database encoding format as a string, e.g. 'UTF-8'
237
265
  def encoding
238
266
  any_raw_connection.encoding.to_s
@@ -291,7 +319,7 @@ module ActiveRecord
291
319
  exec_query "DROP INDEX #{quote_column_name(index_name)}"
292
320
  end
293
321
 
294
- VIRTUAL_TABLE_REGEX = /USING\s+(\w+)\s*\((.*)\)/i
322
+ VIRTUAL_TABLE_REGEX = /USING\s+(\w+)\s*\((.+)\)/i
295
323
 
296
324
  # Returns a list of defined virtual tables
297
325
  def virtual_tables
@@ -478,8 +506,8 @@ module ActiveRecord
478
506
  end
479
507
 
480
508
  def check_version # :nodoc:
481
- if database_version < "3.8.0"
482
- raise "Your version of SQLite (#{database_version}) is too old. Active Record supports SQLite >= 3.8."
509
+ if database_version < "3.23.0"
510
+ raise "Your version of SQLite (#{database_version}) is too old. Active Record supports SQLite >= 3.23.0."
483
511
  end
484
512
  end
485
513
 
@@ -535,6 +563,8 @@ module ActiveRecord
535
563
  # Binary columns
536
564
  when /x'(.*)'/
537
565
  [ $1 ].pack("H*")
566
+ when "TRUE", "FALSE"
567
+ default
538
568
  else
539
569
  # Anything else is blank or some function
540
570
  # and we can't know the value of that, so return nil.
@@ -583,8 +613,8 @@ module ActiveRecord
583
613
  yield definition if block_given?
584
614
  end
585
615
 
586
- disable_referential_integrity do
587
- transaction do
616
+ transaction do
617
+ disable_referential_integrity do
588
618
  move_table(table_name, altered_table_name, options.merge(temporary: true))
589
619
  move_table(altered_table_name, table_name, &caller)
590
620
  end
@@ -625,8 +655,8 @@ module ActiveRecord
625
655
  column_options[:stored] = column.virtual_stored?
626
656
  column_options[:type] = column.type
627
657
  elsif column.has_default?
628
- type = lookup_cast_type_from_column(column)
629
- default = type.deserialize(column.default)
658
+ # TODO: Remove fetch_cast_type and the need for connection after we release 8.1.
659
+ default = column.fetch_cast_type(self).deserialize(column.default)
630
660
  default = -> { column.default_function } if default.nil?
631
661
 
632
662
  unless column.auto_increment?
@@ -700,6 +730,8 @@ module ActiveRecord
700
730
  NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
701
731
  elsif exception.message.match?(/FOREIGN KEY constraint failed/i)
702
732
  InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
733
+ elsif exception.message.match?(/CHECK constraint failed: .*/i)
734
+ CheckViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
703
735
  elsif exception.message.match?(/called on a closed database/i)
704
736
  ConnectionNotEstablished.new(exception, connection_pool: @pool)
705
737
  elsif exception.is_a?(::SQLite3::BusyException)
@@ -789,9 +821,9 @@ module ActiveRecord
789
821
 
790
822
  def table_info(table_name)
791
823
  if supports_virtual_columns?
792
- internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
824
+ internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA", allow_retry: true)
793
825
  else
794
- internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
826
+ internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA", allow_retry: true)
795
827
  end
796
828
  end
797
829
 
@@ -818,18 +850,10 @@ module ActiveRecord
818
850
  end
819
851
 
820
852
  def configure_connection
821
- if @config[:timeout] && @config[:retries]
822
- raise ArgumentError, "Cannot specify both timeout and retries arguments"
823
- elsif @config[:timeout]
853
+ if @config[:timeout]
824
854
  timeout = self.class.type_cast_config_to_integer(@config[:timeout])
825
855
  raise TypeError, "timeout must be integer, not #{timeout}" unless timeout.is_a?(Integer)
826
856
  @raw_connection.busy_handler_timeout = timeout
827
- elsif @config[:retries]
828
- ActiveRecord.deprecator.warn(<<~MSG)
829
- The retries option is deprecated and will be removed in Rails 8.1. Use timeout instead.
830
- MSG
831
- retries = self.class.type_cast_config_to_integer(@config[:retries])
832
- raw_connection.busy_handler { |count| count <= retries }
833
857
  end
834
858
 
835
859
  super
@@ -29,7 +29,8 @@ module ActiveRecord
29
29
  raw_connection.next_result
30
30
  end
31
31
  verified!
32
- handle_warnings(sql)
32
+
33
+ notification_payload[:affected_rows] = result.affected_rows
33
34
  notification_payload[:row_count] = result.count
34
35
  result
35
36
  ensure
@@ -40,9 +41,9 @@ module ActiveRecord
40
41
 
41
42
  def cast_result(result)
42
43
  if result.fields.empty?
43
- ActiveRecord::Result.empty
44
+ ActiveRecord::Result.empty(affected_rows: result.affected_rows)
44
45
  else
45
- ActiveRecord::Result.new(result.fields, result.rows)
46
+ ActiveRecord::Result.new(result.fields, result.rows, affected_rows: result.affected_rows)
46
47
  end
47
48
  end
48
49
 
@@ -181,7 +181,7 @@ module ActiveRecord
181
181
  end
182
182
 
183
183
  case exception
184
- when ::Trilogy::ConnectionClosed, ::Trilogy::EOFError
184
+ when ::Trilogy::ConnectionClosed, ::Trilogy::EOFError, ::Trilogy::SSLError
185
185
  return ConnectionFailed.new(message, connection_pool: @pool)
186
186
  when ::Trilogy::Error
187
187
  if exception.is_a?(SystemCallError) || exception.message.include?("TRILOGY_INVALID_SEQUENCE_ID")
@@ -84,6 +84,7 @@ module ActiveRecord
84
84
  autoload_at "active_record/connection_adapters/abstract/schema_definitions" do
85
85
  autoload :IndexDefinition
86
86
  autoload :ColumnDefinition
87
+ autoload :ColumnMethods
87
88
  autoload :ChangeColumnDefinition
88
89
  autoload :ChangeColumnDefaultDefinition
89
90
  autoload :ForeignKeyDefinition
@@ -172,11 +172,9 @@ module ActiveRecord
172
172
  prevent_writes = true if role == ActiveRecord.reading_role
173
173
 
174
174
  append_to_connected_to_stack(role: role, shard: shard, prevent_writes: prevent_writes, klasses: classes)
175
- begin
176
- yield
177
- ensure
178
- connected_to_stack.pop
179
- end
175
+ yield
176
+ ensure
177
+ connected_to_stack.pop
180
178
  end
181
179
 
182
180
  # Passes the block to +connected_to+ for every +shard+ the
@@ -397,13 +395,11 @@ module ActiveRecord
397
395
  prevent_writes = true if role == ActiveRecord.reading_role
398
396
 
399
397
  append_to_connected_to_stack(role: role, shard: shard, prevent_writes: prevent_writes, klasses: [self])
400
- begin
401
- return_value = yield
402
- return_value.load if return_value.is_a? ActiveRecord::Relation
403
- return_value
404
- ensure
405
- self.connected_to_stack.pop
406
- 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
407
403
  end
408
404
 
409
405
  def append_to_connected_to_stack(entry)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/enumerable"
4
- require "active_support/core_ext/module/delegation"
5
4
  require "active_support/parameter_filter"
6
5
  require "concurrent/map"
7
6
 
@@ -112,7 +111,7 @@ module ActiveRecord
112
111
  # Post.attributes_for_inspect = [:id, :title]
113
112
  # Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!">"
114
113
  #
115
- # When set to `:all` inspect will list all the record's attributes:
114
+ # When set to +:all+ inspect will list all the record's attributes:
116
115
  #
117
116
  # Post.attributes_for_inspect = :all
118
117
  # Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
@@ -358,6 +357,8 @@ module ActiveRecord
358
357
  def filter_attributes=(filter_attributes)
359
358
  @inspection_filter = nil
360
359
  @filter_attributes = filter_attributes
360
+
361
+ FilterAttributeHandler.sensitive_attribute_was_declared(self, filter_attributes)
361
362
  end
362
363
 
363
364
  def inspection_filter # :nodoc:
@@ -451,7 +452,7 @@ module ActiveRecord
451
452
  where(wheres).limit(1)
452
453
  }
453
454
 
454
- statement.execute(values.flatten, connection, allow_retry: true).then do |r|
455
+ statement.execute(values.flatten, connection).then do |r|
455
456
  r.first
456
457
  rescue TypeError
457
458
  raise ActiveRecord::StatementInvalid
@@ -641,7 +642,7 @@ module ActiveRecord
641
642
  def hash
642
643
  id = self.id
643
644
 
644
- if primary_key_values_present?
645
+ if self.class.composite_primary_key? ? primary_key_values_present? : id
645
646
  self.class.hash ^ id.hash
646
647
  else
647
648
  super
@@ -17,7 +17,7 @@ module ActiveRecord
17
17
  #
18
18
  # ==== Parameters
19
19
  #
20
- # * +id+ - The id of the object you wish to reset a counter on.
20
+ # * +id+ - The id of the object you wish to reset a counter on or an array of ids.
21
21
  # * +counters+ - One or more association counters to reset. Association name or counter name can be given.
22
22
  # * <tt>:touch</tt> - Touch timestamp columns when updating.
23
23
  # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
@@ -28,13 +28,25 @@ module ActiveRecord
28
28
  # # For the Post with id #1, reset the comments_count
29
29
  # Post.reset_counters(1, :comments)
30
30
  #
31
+ # # For posts with ids #1 and #2, reset the comments_count
32
+ # Post.reset_counters([1, 2], :comments)
33
+ #
31
34
  # # Like above, but also touch the updated_at and/or updated_on
32
35
  # # attributes.
33
36
  # Post.reset_counters(1, :comments, touch: true)
34
37
  def reset_counters(id, *counters, touch: nil)
35
- object = find(id)
38
+ ids = if composite_primary_key?
39
+ if id.first.is_a?(Array)
40
+ id
41
+ else
42
+ [id]
43
+ end
44
+ else
45
+ Array(id)
46
+ end
47
+
48
+ updates = Hash.new { |h, k| h[k] = {} }
36
49
 
37
- updates = {}
38
50
  counters.each do |counter_association|
39
51
  has_many_association = _reflect_on_association(counter_association)
40
52
  unless has_many_association
@@ -48,14 +60,22 @@ module ActiveRecord
48
60
  has_many_association = has_many_association.through_reflection
49
61
  end
50
62
 
63
+ counter_association = counter_association.to_sym
51
64
  foreign_key = has_many_association.foreign_key.to_s
52
65
  child_class = has_many_association.klass
53
66
  reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
54
67
  counter_name = reflection.counter_cache_column
55
68
 
56
- count_was = object.send(counter_name)
57
- count = object.send(counter_association).count(:all)
58
- updates[counter_name] = count if count != count_was
69
+ counts =
70
+ unscoped
71
+ .joins(counter_association)
72
+ .where(primary_key => ids)
73
+ .group(primary_key)
74
+ .count(:all)
75
+
76
+ ids.each do |id|
77
+ updates[id].merge!(counter_name => counts[id] || 0)
78
+ end
59
79
  end
60
80
 
61
81
  if touch
@@ -63,10 +83,15 @@ module ActiveRecord
63
83
  names = Array.wrap(names)
64
84
  options = names.extract_options!
65
85
  touch_updates = touch_attributes_with_time(*names, **options)
66
- updates.merge!(touch_updates)
86
+
87
+ updates.each_value do |record_updates|
88
+ record_updates.merge!(touch_updates)
89
+ end
67
90
  end
68
91
 
69
- unscoped.where(primary_key => [object.id]).update_all(updates) if updates.any?
92
+ updates.each do |id, record_updates|
93
+ unscoped.where(primary_key => [id]).update_all(record_updates)
94
+ end
70
95
 
71
96
  true
72
97
  end
@@ -48,7 +48,11 @@ module ActiveRecord
48
48
  raise NotImplementedError
49
49
  end
50
50
 
51
- def pool
51
+ def min_connections
52
+ raise NotImplementedError
53
+ end
54
+
55
+ def max_connections
52
56
  raise NotImplementedError
53
57
  end
54
58
 
@@ -38,6 +38,7 @@ module ActiveRecord
38
38
  def initialize(env_name, name, configuration_hash)
39
39
  super(env_name, name)
40
40
  @configuration_hash = configuration_hash.symbolize_keys.freeze
41
+ validate_configuration!
41
42
  end
42
43
 
43
44
  # Determines whether a database configuration is for a replica / readonly
@@ -69,16 +70,32 @@ module ActiveRecord
69
70
  @configuration_hash = configuration_hash.merge(database: database).freeze
70
71
  end
71
72
 
72
- def pool
73
- (configuration_hash[:pool] || 5).to_i
73
+ def max_connections
74
+ (configuration_hash[:max_connections] || configuration_hash[:pool] || 5).to_i
74
75
  end
75
76
 
77
+ def min_connections
78
+ (configuration_hash[:min_connections] || 0).to_i
79
+ end
80
+
81
+ alias :pool :max_connections
82
+ deprecate pool: :max_connections, deprecator: ActiveRecord.deprecator
83
+
76
84
  def min_threads
77
85
  (configuration_hash[:min_threads] || 0).to_i
78
86
  end
79
87
 
80
88
  def max_threads
81
- (configuration_hash[:max_threads] || pool).to_i
89
+ (configuration_hash[:max_threads] || max_connections).to_i
90
+ end
91
+
92
+ def max_age
93
+ v = configuration_hash[:max_age]&.to_i
94
+ if v && v > 0
95
+ v
96
+ else
97
+ Float::INFINITY
98
+ end
82
99
  end
83
100
 
84
101
  def query_cache
@@ -93,10 +110,8 @@ module ActiveRecord
93
110
  (configuration_hash[:checkout_timeout] || 5).to_f
94
111
  end
95
112
 
96
- # `reaping_frequency` is configurable mostly for historical reasons, but it
97
- # could also be useful if someone wants a very low `idle_timeout`.
98
- def reaping_frequency
99
- configuration_hash.fetch(:reaping_frequency, 60)&.to_f
113
+ def reaping_frequency # :nodoc:
114
+ configuration_hash.fetch(:reaping_frequency, default_reaping_frequency)&.to_f
100
115
  end
101
116
 
102
117
  def idle_timeout
@@ -104,6 +119,11 @@ module ActiveRecord
104
119
  timeout if timeout > 0
105
120
  end
106
121
 
122
+ def keepalive
123
+ keepalive = (configuration_hash[:keepalive] || 600).to_f
124
+ keepalive if keepalive > 0
125
+ end
126
+
107
127
  def adapter
108
128
  configuration_hash[:adapter]&.to_s
109
129
  end
@@ -159,8 +179,8 @@ module ActiveRecord
159
179
  end
160
180
 
161
181
  def schema_format # :nodoc:
162
- format = configuration_hash[:schema_format]&.to_sym || ActiveRecord.schema_format
163
- raise "Invalid schema format" unless [ :ruby, :sql ].include? format
182
+ format = configuration_hash.fetch(:schema_format, ActiveRecord.schema_format).to_sym
183
+ raise "Invalid schema format" unless [:ruby, :sql].include?(format)
164
184
  format
165
185
  end
166
186
 
@@ -181,6 +201,27 @@ module ActiveRecord
181
201
  "structure.sql"
182
202
  end
183
203
  end
204
+
205
+ def default_reaping_frequency
206
+ # Reap every 20 seconds by default, but run more often as necessary to
207
+ # meet other configured timeouts.
208
+ [20, idle_timeout, max_age, keepalive].compact.min
209
+ end
210
+
211
+ def validate_configuration!
212
+ if configuration_hash[:pool] && configuration_hash[:max_connections]
213
+ pool_val = configuration_hash[:pool].to_i
214
+ max_conn_val = configuration_hash[:max_connections].to_i
215
+
216
+ if pool_val != max_conn_val
217
+ raise "Ambiguous configuration: 'pool' (#{pool_val}) and 'max_connections' (#{max_conn_val}) are set to different values. Prefer just 'max_connections'."
218
+ end
219
+ end
220
+
221
+ if configuration_hash[:pool] && configuration_hash[:min_connections]
222
+ raise "Ambiguous configuration: when setting 'min_connections', use 'max_connections' instead of 'pool'."
223
+ end
224
+ end
184
225
  end
185
226
  end
186
227
  end
@@ -47,9 +47,8 @@ module ActiveRecord
47
47
  @configuration_hash[:schema_dump] = false
48
48
  end
49
49
 
50
- if @configuration_hash[:query_cache] == "false"
51
- @configuration_hash[:query_cache] = false
52
- end
50
+ query_cache = parse_query_cache
51
+ @configuration_hash[:query_cache] = query_cache unless query_cache.nil?
53
52
 
54
53
  to_boolean!(@configuration_hash, :replica)
55
54
  to_boolean!(@configuration_hash, :database_tasks)
@@ -58,6 +57,17 @@ module ActiveRecord
58
57
  end
59
58
 
60
59
  private
60
+ def parse_query_cache
61
+ case value = @configuration_hash[:query_cache]
62
+ when /\A\d+\z/
63
+ value.to_i
64
+ when "false"
65
+ false
66
+ else
67
+ value
68
+ end
69
+ end
70
+
61
71
  def to_boolean!(configuration_hash, key)
62
72
  if configuration_hash[key].is_a?(String)
63
73
  configuration_hash[key] = configuration_hash[key] != "false"
@@ -36,9 +36,11 @@ module ActiveRecord
36
36
  # to respond to `sharded?`. To implement this define the following in an
37
37
  # initializer:
38
38
  #
39
- # ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
40
- # next unless config.key?(:vitess)
41
- # VitessConfig.new(env_name, name, config)
39
+ # ActiveSupport.on_load(:active_record_database_configurations) do
40
+ # ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
41
+ # next unless config.key?(:vitess)
42
+ # VitessConfig.new(env_name, name, config)
43
+ # end
42
44
  # end
43
45
  #
44
46
  # Note: applications must handle the condition in which custom config should be
@@ -306,4 +308,6 @@ module ActiveRecord
306
308
  url
307
309
  end
308
310
  end
311
+
312
+ ActiveSupport.run_load_hooks(:active_record_database_configurations, DatabaseConfigurations)
309
313
  end
@@ -229,7 +229,7 @@ module ActiveRecord
229
229
  # @entry.message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
230
230
  # @entry.comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil
231
231
  def delegated_type(role, types:, **options)
232
- belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
232
+ belongs_to role, options.delete(:scope), **options, polymorphic: true
233
233
  define_delegated_type_methods role, types: types, options: options
234
234
  end
235
235