activerecord 8.0.3 → 8.1.0.rc1

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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +520 -514
  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 +2 -0
  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 +2 -0
  16. data/lib/active_record/associations/preloader/branch.rb +1 -0
  17. data/lib/active_record/associations.rb +159 -21
  18. data/lib/active_record/attribute_methods/serialization.rb +16 -3
  19. data/lib/active_record/attribute_methods/time_zone_conversion.rb +10 -2
  20. data/lib/active_record/attributes.rb +3 -0
  21. data/lib/active_record/autosave_association.rb +1 -1
  22. data/lib/active_record/base.rb +0 -2
  23. data/lib/active_record/coders/json.rb +14 -5
  24. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +1 -3
  25. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +16 -3
  26. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +51 -12
  27. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +405 -72
  28. data/lib/active_record/connection_adapters/abstract/database_statements.rb +55 -40
  29. data/lib/active_record/connection_adapters/abstract/query_cache.rb +19 -3
  30. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -24
  31. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +7 -2
  32. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +26 -34
  33. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +2 -1
  34. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +85 -22
  35. data/lib/active_record/connection_adapters/abstract/transaction.rb +25 -3
  36. data/lib/active_record/connection_adapters/abstract_adapter.rb +86 -20
  37. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +43 -13
  38. data/lib/active_record/connection_adapters/column.rb +17 -4
  39. data/lib/active_record/connection_adapters/mysql/database_statements.rb +4 -4
  40. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +2 -0
  41. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +42 -5
  42. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +26 -4
  43. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +27 -22
  44. data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -2
  45. data/lib/active_record/connection_adapters/postgresql/column.rb +4 -0
  46. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +17 -15
  47. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +2 -2
  48. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +1 -1
  49. data/lib/active_record/connection_adapters/postgresql/quoting.rb +21 -10
  50. data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +8 -6
  51. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +8 -21
  52. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +66 -31
  53. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +81 -48
  54. data/lib/active_record/connection_adapters/postgresql_adapter.rb +23 -7
  55. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  56. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +37 -25
  57. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +0 -8
  58. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -13
  59. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +54 -30
  60. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -3
  61. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
  62. data/lib/active_record/connection_adapters.rb +1 -0
  63. data/lib/active_record/connection_handling.rb +2 -1
  64. data/lib/active_record/core.rb +5 -4
  65. data/lib/active_record/counter_cache.rb +33 -8
  66. data/lib/active_record/database_configurations/database_config.rb +5 -1
  67. data/lib/active_record/database_configurations/hash_config.rb +53 -9
  68. data/lib/active_record/database_configurations/url_config.rb +13 -3
  69. data/lib/active_record/database_configurations.rb +7 -3
  70. data/lib/active_record/delegated_type.rb +1 -1
  71. data/lib/active_record/dynamic_matchers.rb +54 -69
  72. data/lib/active_record/encryption/encryptable_record.rb +4 -4
  73. data/lib/active_record/encryption/encrypted_attribute_type.rb +1 -1
  74. data/lib/active_record/encryption/encryptor.rb +12 -0
  75. data/lib/active_record/encryption/scheme.rb +1 -1
  76. data/lib/active_record/enum.rb +24 -8
  77. data/lib/active_record/errors.rb +20 -4
  78. data/lib/active_record/explain.rb +1 -1
  79. data/lib/active_record/explain_registry.rb +51 -2
  80. data/lib/active_record/filter_attribute_handler.rb +73 -0
  81. data/lib/active_record/fixtures.rb +2 -2
  82. data/lib/active_record/gem_version.rb +3 -3
  83. data/lib/active_record/inheritance.rb +1 -1
  84. data/lib/active_record/insert_all.rb +12 -7
  85. data/lib/active_record/locking/optimistic.rb +7 -0
  86. data/lib/active_record/locking/pessimistic.rb +5 -0
  87. data/lib/active_record/log_subscriber.rb +2 -6
  88. data/lib/active_record/middleware/shard_selector.rb +34 -17
  89. data/lib/active_record/migration/command_recorder.rb +14 -1
  90. data/lib/active_record/migration/compatibility.rb +34 -24
  91. data/lib/active_record/migration/default_schema_versions_formatter.rb +30 -0
  92. data/lib/active_record/migration.rb +26 -16
  93. data/lib/active_record/model_schema.rb +36 -10
  94. data/lib/active_record/nested_attributes.rb +2 -0
  95. data/lib/active_record/persistence.rb +34 -3
  96. data/lib/active_record/query_cache.rb +22 -15
  97. data/lib/active_record/query_logs.rb +3 -7
  98. data/lib/active_record/railtie.rb +32 -3
  99. data/lib/active_record/railties/controller_runtime.rb +11 -6
  100. data/lib/active_record/railties/databases.rake +15 -3
  101. data/lib/active_record/railties/job_checkpoints.rb +15 -0
  102. data/lib/active_record/railties/job_runtime.rb +10 -11
  103. data/lib/active_record/reflection.rb +42 -3
  104. data/lib/active_record/relation/batches.rb +25 -11
  105. data/lib/active_record/relation/calculations.rb +20 -9
  106. data/lib/active_record/relation/delegation.rb +0 -1
  107. data/lib/active_record/relation/finder_methods.rb +27 -11
  108. data/lib/active_record/relation/predicate_builder/association_query_value.rb +9 -9
  109. data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +7 -7
  110. data/lib/active_record/relation/predicate_builder.rb +9 -7
  111. data/lib/active_record/relation/query_attribute.rb +3 -1
  112. data/lib/active_record/relation/query_methods.rb +38 -28
  113. data/lib/active_record/relation/where_clause.rb +1 -8
  114. data/lib/active_record/relation.rb +24 -12
  115. data/lib/active_record/result.rb +44 -21
  116. data/lib/active_record/runtime_registry.rb +41 -58
  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/structured_event_subscriber.rb +85 -0
  124. data/lib/active_record/table_metadata.rb +5 -20
  125. data/lib/active_record/tasks/abstract_tasks.rb +76 -0
  126. data/lib/active_record/tasks/database_tasks.rb +25 -34
  127. data/lib/active_record/tasks/mysql_database_tasks.rb +3 -40
  128. data/lib/active_record/tasks/postgresql_database_tasks.rb +5 -39
  129. data/lib/active_record/tasks/sqlite_database_tasks.rb +14 -26
  130. data/lib/active_record/test_databases.rb +14 -4
  131. data/lib/active_record/test_fixtures.rb +27 -2
  132. data/lib/active_record/testing/query_assertions.rb +8 -2
  133. data/lib/active_record/timestamp.rb +4 -2
  134. data/lib/active_record/transaction.rb +2 -5
  135. data/lib/active_record/transactions.rb +32 -10
  136. data/lib/active_record/type/hash_lookup_type_map.rb +2 -1
  137. data/lib/active_record/type/internal/timezone.rb +7 -0
  138. data/lib/active_record/type/json.rb +15 -2
  139. data/lib/active_record/type/serialized.rb +11 -4
  140. data/lib/active_record/type/type_map.rb +1 -1
  141. data/lib/active_record/type_caster/connection.rb +2 -1
  142. data/lib/active_record/validations/associated.rb +1 -1
  143. data/lib/active_record.rb +65 -3
  144. data/lib/arel/alias_predication.rb +2 -0
  145. data/lib/arel/crud.rb +6 -11
  146. data/lib/arel/nodes/count.rb +2 -2
  147. data/lib/arel/nodes/function.rb +4 -10
  148. data/lib/arel/nodes/named_function.rb +2 -2
  149. data/lib/arel/nodes/node.rb +1 -1
  150. data/lib/arel/nodes.rb +0 -2
  151. data/lib/arel/select_manager.rb +7 -2
  152. data/lib/arel/visitors/dot.rb +0 -3
  153. data/lib/arel/visitors/postgresql.rb +55 -0
  154. data/lib/arel/visitors/sqlite.rb +55 -8
  155. data/lib/arel/visitors/to_sql.rb +3 -21
  156. data/lib/arel.rb +3 -1
  157. data/lib/rails/generators/active_record/application_record/USAGE +1 -1
  158. metadata +14 -10
  159. data/lib/active_record/explain_subscriber.rb +0 -34
  160. data/lib/active_record/normalization.rb +0 -163
@@ -61,6 +61,14 @@ module ActiveRecord
61
61
  @previous_read_uncommitted = nil
62
62
  end
63
63
 
64
+ def default_insert_value(column) # :nodoc:
65
+ if column.default_function
66
+ Arel.sql(column.default_function)
67
+ else
68
+ column.default
69
+ end
70
+ end
71
+
64
72
  private
65
73
  def internal_begin_transaction(mode, isolation)
66
74
  if isolation
@@ -76,39 +84,51 @@ module ActiveRecord
76
84
  end
77
85
 
78
86
  def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch: false)
87
+ total_changes_before_query = raw_connection.total_changes
88
+ affected_rows = nil
89
+
79
90
  if batch
80
91
  raw_connection.execute_batch2(sql)
81
- elsif prepare
82
- stmt = @statements[sql] ||= raw_connection.prepare(sql)
83
- stmt.reset!
84
- stmt.bind_params(type_casted_binds)
85
-
86
- result = if stmt.column_count.zero? # No return
87
- stmt.step
88
- ActiveRecord::Result.empty
92
+ else
93
+ stmt = if prepare
94
+ @statements[sql] ||= raw_connection.prepare(sql)
95
+ @statements[sql].reset!
89
96
  else
90
- ActiveRecord::Result.new(stmt.columns, stmt.to_a)
97
+ # Don't cache statements if they are not prepared.
98
+ raw_connection.prepare(sql)
91
99
  end
92
- else
93
- # Don't cache statements if they are not prepared.
94
- stmt = raw_connection.prepare(sql)
95
100
  begin
96
101
  unless binds.nil? || binds.empty?
97
102
  stmt.bind_params(type_casted_binds)
98
103
  end
99
104
  result = if stmt.column_count.zero? # No return
100
105
  stmt.step
101
- ActiveRecord::Result.empty
106
+
107
+ affected_rows = if raw_connection.total_changes > total_changes_before_query
108
+ raw_connection.changes
109
+ else
110
+ 0
111
+ end
112
+
113
+ ActiveRecord::Result.empty(affected_rows: affected_rows)
102
114
  else
103
- ActiveRecord::Result.new(stmt.columns, stmt.to_a)
115
+ rows = stmt.to_a
116
+
117
+ affected_rows = if raw_connection.total_changes > total_changes_before_query
118
+ raw_connection.changes
119
+ else
120
+ 0
121
+ end
122
+
123
+ ActiveRecord::Result.new(stmt.columns, rows, stmt.types.map { |t| type_map.lookup(t) }, affected_rows: affected_rows)
104
124
  end
105
125
  ensure
106
- stmt.close
126
+ stmt.close unless prepare
107
127
  end
108
128
  end
109
- @last_affected_rows = raw_connection.changes
110
129
  verified!
111
130
 
131
+ notification_payload[:affected_rows] = affected_rows
112
132
  notification_payload[:row_count] = result&.length || 0
113
133
  result
114
134
  end
@@ -120,7 +140,7 @@ module ActiveRecord
120
140
  end
121
141
 
122
142
  def affected_rows(result)
123
- @last_affected_rows
143
+ result.affected_rows
124
144
  end
125
145
 
126
146
  def execute_batch(statements, name = nil, **kwargs)
@@ -135,14 +155,6 @@ module ActiveRecord
135
155
  def returning_column_values(result)
136
156
  result.rows.first
137
157
  end
138
-
139
- def default_insert_value(column)
140
- if column.default_function
141
- Arel.sql(column.default_function)
142
- else
143
- column.default
144
- end
145
- end
146
158
  end
147
159
  end
148
160
  end
@@ -80,18 +80,10 @@ module ActiveRecord
80
80
  "x'#{value.hex}'"
81
81
  end
82
82
 
83
- def quoted_true
84
- "1"
85
- end
86
-
87
83
  def unquoted_true
88
84
  1
89
85
  end
90
86
 
91
- def quoted_false
92
- "0"
93
- end
94
-
95
87
  def unquoted_false
96
88
  0
97
89
  end
@@ -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
@@ -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.
@@ -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
@@ -100,7 +100,8 @@ module ActiveRecord
100
100
  db_config = resolve_config_for_connection(database_key)
101
101
 
102
102
  self.connection_class = true
103
- connections << connection_handler.establish_connection(db_config, owner_name: self, role: role, shard: shard.to_sym)
103
+ shard = shard.to_sym unless shard.is_a? Integer
104
+ connections << connection_handler.establish_connection(db_config, owner_name: self, role: role, shard: shard)
104
105
  end
105
106
  end
106
107
 
@@ -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,35 @@ 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
+ max_connections = configuration_hash.fetch(:max_connections) {
75
+ configuration_hash.fetch(:pool, 5)
76
+ }&.to_i
77
+ max_connections if max_connections && max_connections >= 0
74
78
  end
75
79
 
80
+ def min_connections
81
+ (configuration_hash[:min_connections] || 0).to_i
82
+ end
83
+
84
+ alias :pool :max_connections
85
+ deprecate pool: :max_connections, deprecator: ActiveRecord.deprecator
86
+
76
87
  def min_threads
77
88
  (configuration_hash[:min_threads] || 0).to_i
78
89
  end
79
90
 
80
91
  def max_threads
81
- (configuration_hash[:max_threads] || pool).to_i
92
+ (configuration_hash[:max_threads] || (max_connections || 5).clamp(0, 5)).to_i
93
+ end
94
+
95
+ def max_age
96
+ v = configuration_hash[:max_age]&.to_i
97
+ if v && v > 0
98
+ v
99
+ else
100
+ Float::INFINITY
101
+ end
82
102
  end
83
103
 
84
104
  def query_cache
@@ -93,10 +113,8 @@ module ActiveRecord
93
113
  (configuration_hash[:checkout_timeout] || 5).to_f
94
114
  end
95
115
 
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
116
+ def reaping_frequency # :nodoc:
117
+ configuration_hash.fetch(:reaping_frequency, default_reaping_frequency)&.to_f
100
118
  end
101
119
 
102
120
  def idle_timeout
@@ -104,6 +122,11 @@ module ActiveRecord
104
122
  timeout if timeout > 0
105
123
  end
106
124
 
125
+ def keepalive
126
+ keepalive = (configuration_hash[:keepalive] || 600).to_f
127
+ keepalive if keepalive > 0
128
+ end
129
+
107
130
  def adapter
108
131
  configuration_hash[:adapter]&.to_s
109
132
  end
@@ -159,8 +182,8 @@ module ActiveRecord
159
182
  end
160
183
 
161
184
  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
185
+ format = configuration_hash.fetch(:schema_format, ActiveRecord.schema_format).to_sym
186
+ raise "Invalid schema format" unless [:ruby, :sql].include?(format)
164
187
  format
165
188
  end
166
189
 
@@ -181,6 +204,27 @@ module ActiveRecord
181
204
  "structure.sql"
182
205
  end
183
206
  end
207
+
208
+ def default_reaping_frequency
209
+ # Reap every 20 seconds by default, but run more often as necessary to
210
+ # meet other configured timeouts.
211
+ [20, idle_timeout, max_age, keepalive].compact.min
212
+ end
213
+
214
+ def validate_configuration!
215
+ if configuration_hash[:pool] && configuration_hash[:max_connections]
216
+ pool_val = configuration_hash[:pool].to_i
217
+ max_conn_val = configuration_hash[:max_connections].to_i
218
+
219
+ if pool_val != max_conn_val
220
+ raise "Ambiguous configuration: 'pool' (#{pool_val}) and 'max_connections' (#{max_conn_val}) are set to different values. Prefer just 'max_connections'."
221
+ end
222
+ end
223
+
224
+ if configuration_hash[:pool] && configuration_hash[:min_connections]
225
+ raise "Ambiguous configuration: when setting 'min_connections', use 'max_connections' instead of 'pool'."
226
+ end
227
+ end
184
228
  end
185
229
  end
186
230
  end