activerecord 8.0.0.1 → 8.0.2

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +208 -0
  3. data/lib/active_record/associations/alias_tracker.rb +6 -4
  4. data/lib/active_record/associations/belongs_to_association.rb +7 -1
  5. data/lib/active_record/associations/join_dependency/join_association.rb +25 -27
  6. data/lib/active_record/attribute_methods/primary_key.rb +2 -1
  7. data/lib/active_record/attribute_methods.rb +22 -17
  8. data/lib/active_record/attributes.rb +2 -2
  9. data/lib/active_record/autosave_association.rb +21 -11
  10. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +17 -14
  11. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +47 -34
  12. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +5 -1
  13. data/lib/active_record/connection_adapters/abstract_adapter.rb +39 -22
  14. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +14 -7
  15. data/lib/active_record/connection_adapters/mysql/quoting.rb +7 -1
  16. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +7 -0
  17. data/lib/active_record/connection_adapters/mysql2/database_statements.rb +57 -11
  18. data/lib/active_record/connection_adapters/mysql2_adapter.rb +8 -1
  19. data/lib/active_record/connection_adapters/pool_config.rb +7 -7
  20. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +14 -12
  21. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
  22. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +1 -1
  23. data/lib/active_record/connection_adapters/postgresql_adapter.rb +7 -2
  24. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +5 -1
  25. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +13 -0
  26. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +2 -0
  27. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +7 -2
  28. data/lib/active_record/connection_adapters/statement_pool.rb +4 -2
  29. data/lib/active_record/connection_adapters/trilogy_adapter.rb +1 -1
  30. data/lib/active_record/core.rb +31 -2
  31. data/lib/active_record/counter_cache.rb +1 -1
  32. data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -1
  33. data/lib/active_record/delegated_type.rb +17 -17
  34. data/lib/active_record/future_result.rb +3 -3
  35. data/lib/active_record/gem_version.rb +2 -2
  36. data/lib/active_record/migration/command_recorder.rb +5 -2
  37. data/lib/active_record/railties/databases.rake +1 -1
  38. data/lib/active_record/relation/calculations.rb +23 -17
  39. data/lib/active_record/relation/predicate_builder/association_query_value.rb +2 -0
  40. data/lib/active_record/relation/query_attribute.rb +1 -1
  41. data/lib/active_record/relation/query_methods.rb +1 -1
  42. data/lib/active_record/relation.rb +1 -1
  43. data/lib/active_record/schema_dumper.rb +29 -11
  44. data/lib/active_record/signed_id.rb +4 -3
  45. data/lib/active_record/statement_cache.rb +2 -2
  46. data/lib/active_record/transactions.rb +5 -6
  47. data/lib/active_record.rb +4 -2
  48. data/lib/arel/collectors/bind.rb +1 -1
  49. data/lib/arel/collectors/sql_string.rb +1 -1
  50. data/lib/arel/collectors/substitute_binds.rb +2 -2
  51. data/lib/arel/nodes/binary.rb +1 -1
  52. data/lib/arel/nodes/node.rb +1 -1
  53. data/lib/arel/nodes/sql_literal.rb +1 -1
  54. data/lib/arel/visitors/to_sql.rb +1 -1
  55. metadata +10 -13
@@ -36,7 +36,7 @@ module ActiveRecord
36
36
  end
37
37
 
38
38
  def schema_cache; end
39
- def connection_class; end
39
+ def connection_descriptor; end
40
40
  def checkin(_); end
41
41
  def remove(_); end
42
42
  def async_executor; end
@@ -117,24 +117,30 @@ module ActiveRecord
117
117
  # * private methods that require being called in a +synchronize+ blocks
118
118
  # are now explicitly documented
119
119
  class ConnectionPool
120
- class WeakThreadKeyMap # :nodoc:
121
- # FIXME: On 3.3 we could use ObjectSpace::WeakKeyMap
122
- # but it currently cause GC crashes: https://github.com/byroot/rails/pull/3
123
- def initialize
124
- @map = {}
125
- end
126
-
127
- def clear
128
- @map.clear
129
- end
120
+ # Prior to 3.3.5, WeakKeyMap had a use after free bug
121
+ # https://bugs.ruby-lang.org/issues/20688
122
+ if ObjectSpace.const_defined?(:WeakKeyMap) && RUBY_VERSION >= "3.3.5"
123
+ WeakThreadKeyMap = ObjectSpace::WeakKeyMap
124
+ else
125
+ class WeakThreadKeyMap # :nodoc:
126
+ # FIXME: On 3.3 we could use ObjectSpace::WeakKeyMap
127
+ # but it currently cause GC crashes: https://github.com/byroot/rails/pull/3
128
+ def initialize
129
+ @map = {}
130
+ end
131
+
132
+ def clear
133
+ @map.clear
134
+ end
130
135
 
131
- def [](key)
132
- @map[key]
133
- end
136
+ def [](key)
137
+ @map[key]
138
+ end
134
139
 
135
- def []=(key, value)
136
- @map.select! { |c, _| c.alive? }
137
- @map[key] = value
140
+ def []=(key, value)
141
+ @map.select! { |c, _| c&.alive? }
142
+ @map[key] = value
143
+ end
138
144
  end
139
145
  end
140
146
 
@@ -358,8 +364,8 @@ module ActiveRecord
358
364
  clean
359
365
  end
360
366
 
361
- def connection_class # :nodoc:
362
- pool_config.connection_class
367
+ def connection_descriptor # :nodoc:
368
+ pool_config.connection_descriptor
363
369
  end
364
370
 
365
371
  # Returns true if there is an open connection being used for the current thread.
@@ -539,20 +545,25 @@ module ActiveRecord
539
545
  # Raises:
540
546
  # - ActiveRecord::ConnectionTimeoutError no connection can be obtained from the pool.
541
547
  def checkout(checkout_timeout = @checkout_timeout)
542
- if @pinned_connection
543
- @pinned_connection.lock.synchronize do
544
- synchronize do
548
+ return checkout_and_verify(acquire_connection(checkout_timeout)) unless @pinned_connection
549
+
550
+ @pinned_connection.lock.synchronize do
551
+ synchronize do
552
+ # The pinned connection may have been cleaned up before we synchronized, so check if it is still present
553
+ if @pinned_connection
545
554
  @pinned_connection.verify!
555
+
546
556
  # Any leased connection must be in @connections otherwise
547
557
  # some methods like #connected? won't behave correctly
548
558
  unless @connections.include?(@pinned_connection)
549
559
  @connections << @pinned_connection
550
560
  end
561
+
562
+ @pinned_connection
563
+ else
564
+ checkout_and_verify(acquire_connection(checkout_timeout))
551
565
  end
552
566
  end
553
- @pinned_connection
554
- else
555
- checkout_and_verify(acquire_connection(checkout_timeout))
556
567
  end
557
568
  end
558
569
 
@@ -687,6 +698,14 @@ module ActiveRecord
687
698
  Thread.pass
688
699
  end
689
700
 
701
+ def new_connection # :nodoc:
702
+ connection = db_config.new_connection
703
+ connection.pool = self
704
+ connection
705
+ rescue ConnectionNotEstablished => ex
706
+ raise ex.set_pool(self)
707
+ end
708
+
690
709
  private
691
710
  def connection_lease
692
711
  @leases[ActiveSupport::IsolatedExecutionState.context]
@@ -866,18 +885,12 @@ module ActiveRecord
866
885
  #--
867
886
  # if owner_thread param is omitted, this must be called in synchronize block
868
887
  def remove_connection_from_thread_cache(conn, owner_thread = conn.owner)
869
- @leases[owner_thread].clear(conn)
888
+ if owner_thread
889
+ @leases[owner_thread].clear(conn)
890
+ end
870
891
  end
871
892
  alias_method :release, :remove_connection_from_thread_cache
872
893
 
873
- def new_connection
874
- connection = db_config.new_connection
875
- connection.pool = self
876
- connection
877
- rescue ConnectionNotEstablished => ex
878
- raise ex.set_pool(self)
879
- end
880
-
881
894
  # If the pool is not at a <tt>@size</tt> limit, establish new connection. Connecting
882
895
  # to the DB is done outside main synchronized section.
883
896
  #--
@@ -159,6 +159,8 @@ module ActiveRecord
159
159
  end
160
160
 
161
161
  def defined_for?(to_table: nil, validate: nil, **options)
162
+ options = options.slice(*self.options.keys)
163
+
162
164
  (to_table.nil? || to_table.to_s == self.to_table) &&
163
165
  (validate.nil? || validate == self.options.fetch(:validate, validate)) &&
164
166
  options.all? { |k, v| Array(self.options[k]).map(&:to_s) == Array(v).map(&:to_s) }
@@ -185,6 +187,8 @@ module ActiveRecord
185
187
  end
186
188
 
187
189
  def defined_for?(name:, expression: nil, validate: nil, **options)
190
+ options = options.slice(*self.options.keys)
191
+
188
192
  self.name == name.to_s &&
189
193
  (validate.nil? || validate == self.options.fetch(:validate, validate)) &&
190
194
  options.all? { |k, v| self.options[k].to_s == v.to_s }
@@ -430,7 +434,7 @@ module ActiveRecord
430
434
  #
431
435
  # == Examples
432
436
  #
433
- # # Assuming +td+ is an instance of TableDefinition
437
+ # # Assuming `td` is an instance of TableDefinition
434
438
  # td.column(:granted, :boolean, index: true)
435
439
  #
436
440
  # == Short-hand examples
@@ -150,7 +150,6 @@ module ActiveRecord
150
150
  end
151
151
 
152
152
  @owner = nil
153
- @instrumenter = ActiveSupport::Notifications.instrumenter
154
153
  @pool = ActiveRecord::ConnectionAdapters::NullPool.new
155
154
  @idle_since = Process.clock_gettime(Process::CLOCK_MONOTONIC)
156
155
  @visitor = arel_visitor
@@ -168,6 +167,7 @@ module ActiveRecord
168
167
  @default_timezone = self.class.validate_default_timezone(@config[:default_timezone])
169
168
 
170
169
  @raw_connection_dirty = false
170
+ @last_activity = nil
171
171
  @verified = false
172
172
  end
173
173
 
@@ -190,19 +190,6 @@ module ActiveRecord
190
190
  end
191
191
  end
192
192
 
193
- EXCEPTION_NEVER = { Exception => :never }.freeze # :nodoc:
194
- EXCEPTION_IMMEDIATE = { Exception => :immediate }.freeze # :nodoc:
195
- private_constant :EXCEPTION_NEVER, :EXCEPTION_IMMEDIATE
196
- def with_instrumenter(instrumenter, &block) # :nodoc:
197
- Thread.handle_interrupt(EXCEPTION_NEVER) do
198
- previous_instrumenter = @instrumenter
199
- @instrumenter = instrumenter
200
- Thread.handle_interrupt(EXCEPTION_IMMEDIATE, &block)
201
- ensure
202
- @instrumenter = previous_instrumenter
203
- end
204
- end
205
-
206
193
  def check_if_write_query(sql) # :nodoc:
207
194
  if preventing_writes? && write_query?(sql)
208
195
  raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
@@ -217,6 +204,10 @@ module ActiveRecord
217
204
  (@config[:connection_retries] || 1).to_i
218
205
  end
219
206
 
207
+ def verify_timeout
208
+ (@config[:verify_timeout] || 2).to_i
209
+ end
210
+
220
211
  def retry_deadline
221
212
  if @config[:retry_deadline]
222
213
  @config[:retry_deadline].to_f
@@ -235,9 +226,9 @@ module ActiveRecord
235
226
  # the value of +current_preventing_writes+.
236
227
  def preventing_writes?
237
228
  return true if replica?
238
- return false if connection_class.nil?
229
+ return false if connection_descriptor.nil?
239
230
 
240
- connection_class.current_preventing_writes
231
+ connection_descriptor.current_preventing_writes
241
232
  end
242
233
 
243
234
  def prepared_statements?
@@ -288,8 +279,8 @@ module ActiveRecord
288
279
  @owner = ActiveSupport::IsolatedExecutionState.context
289
280
  end
290
281
 
291
- def connection_class # :nodoc:
292
- @pool.connection_class
282
+ def connection_descriptor # :nodoc:
283
+ @pool.connection_descriptor
293
284
  end
294
285
 
295
286
  # The role (e.g. +:writing+) for the current connection. In a
@@ -343,6 +334,13 @@ module ActiveRecord
343
334
  Process.clock_gettime(Process::CLOCK_MONOTONIC) - @idle_since
344
335
  end
345
336
 
337
+ # Seconds since this connection last communicated with the server
338
+ def seconds_since_last_activity # :nodoc:
339
+ if @raw_connection && @last_activity
340
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - @last_activity
341
+ end
342
+ end
343
+
346
344
  def unprepared_statement
347
345
  cache = prepared_statements_disabled_cache.add?(object_id) if @prepared_statements
348
346
  yield
@@ -670,11 +668,12 @@ module ActiveRecord
670
668
 
671
669
  enable_lazy_transactions!
672
670
  @raw_connection_dirty = false
671
+ @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
673
672
  @verified = true
674
673
 
675
674
  reset_transaction(restore: restore_transactions) do
676
675
  clear_cache!(new_connection: true)
677
- configure_connection
676
+ attempt_configure_connection
678
677
  end
679
678
  rescue => original_exception
680
679
  translated_exception = translate_exception_class(original_exception, nil, nil)
@@ -689,6 +688,7 @@ module ActiveRecord
689
688
  end
690
689
  end
691
690
 
691
+ @last_activity = nil
692
692
  @verified = false
693
693
 
694
694
  raise translated_exception
@@ -726,7 +726,7 @@ module ActiveRecord
726
726
  def reset!
727
727
  clear_cache!(new_connection: true)
728
728
  reset_transaction
729
- configure_connection
729
+ attempt_configure_connection
730
730
  end
731
731
 
732
732
  # Removes the connection from the pool and disconnect it.
@@ -762,7 +762,8 @@ module ActiveRecord
762
762
  if @unconfigured_connection
763
763
  @raw_connection = @unconfigured_connection
764
764
  @unconfigured_connection = nil
765
- configure_connection
765
+ attempt_configure_connection
766
+ @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
766
767
  @verified = true
767
768
  return
768
769
  end
@@ -992,6 +993,9 @@ module ActiveRecord
992
993
  if @verified
993
994
  # Cool, we're confident the connection's ready to use. (Note this might have
994
995
  # become true during the above #materialize_transactions.)
996
+ elsif (last_activity = seconds_since_last_activity) && last_activity < verify_timeout
997
+ # We haven't actually verified the connection since we acquired it, but it
998
+ # has been used very recently. We're going to assume it's still okay.
995
999
  elsif reconnectable
996
1000
  if allow_retry
997
1001
  # Not sure about the connection yet, but if anything goes wrong we can
@@ -1033,6 +1037,7 @@ module ActiveRecord
1033
1037
  # Barring a known-retryable error inside the query (regardless of
1034
1038
  # whether we were in a _position_ to retry it), we should infer that
1035
1039
  # there's likely a real problem with the connection.
1040
+ @last_activity = nil
1036
1041
  @verified = false
1037
1042
  end
1038
1043
 
@@ -1047,6 +1052,7 @@ module ActiveRecord
1047
1052
  # `with_raw_connection` block only when the block is guaranteed to
1048
1053
  # exercise the raw connection.
1049
1054
  def verified!
1055
+ @last_activity = Process.clock_gettime(Process::CLOCK_MONOTONIC)
1050
1056
  @verified = true
1051
1057
  end
1052
1058
 
@@ -1126,7 +1132,7 @@ module ActiveRecord
1126
1132
  end
1127
1133
 
1128
1134
  def log(sql, name = "SQL", binds = [], type_casted_binds = [], async: false, &block) # :doc:
1129
- @instrumenter.instrument(
1135
+ instrumenter.instrument(
1130
1136
  "sql.active_record",
1131
1137
  sql: sql,
1132
1138
  name: name,
@@ -1142,6 +1148,10 @@ module ActiveRecord
1142
1148
  raise ex.set_query(sql, binds)
1143
1149
  end
1144
1150
 
1151
+ def instrumenter # :nodoc:
1152
+ ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] ||= ActiveSupport::Notifications.instrumenter
1153
+ end
1154
+
1145
1155
  def translate_exception(exception, message:, sql:, binds:)
1146
1156
  # override in derived class
1147
1157
  case exception
@@ -1203,6 +1213,13 @@ module ActiveRecord
1203
1213
  check_version
1204
1214
  end
1205
1215
 
1216
+ def attempt_configure_connection
1217
+ configure_connection
1218
+ rescue
1219
+ disconnect!
1220
+ raise
1221
+ end
1222
+
1206
1223
  def default_prepared_statements
1207
1224
  true
1208
1225
  end
@@ -174,6 +174,10 @@ module ActiveRecord
174
174
  mariadb? && database_version >= "10.5.0"
175
175
  end
176
176
 
177
+ def return_value_after_insert?(column) # :nodoc:
178
+ supports_insert_returning? ? column.auto_populated? : column.auto_increment?
179
+ end
180
+
177
181
  def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
178
182
  query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
179
183
  end
@@ -403,7 +407,11 @@ module ActiveRecord
403
407
  type ||= column.sql_type
404
408
 
405
409
  unless options.key?(:default)
406
- options[:default] = column.default
410
+ options[:default] = if column.default_function
411
+ -> { column.default_function }
412
+ else
413
+ column.default
414
+ end
407
415
  end
408
416
 
409
417
  unless options.key?(:null)
@@ -628,24 +636,26 @@ module ActiveRecord
628
636
  end
629
637
 
630
638
  def build_insert_sql(insert) # :nodoc:
639
+ # Can use any column as it will be assigned to itself.
631
640
  no_op_column = quote_column_name(insert.keys.first) if insert.keys.first
632
641
 
633
642
  # MySQL 8.0.19 replaces `VALUES(<expression>)` clauses with row and column alias names, see https://dev.mysql.com/worklog/task/?id=6312 .
634
643
  # then MySQL 8.0.20 deprecates the `VALUES(<expression>)` see https://dev.mysql.com/worklog/task/?id=13325 .
635
644
  if supports_insert_raw_alias_syntax?
645
+ quoted_table_name = insert.model.quoted_table_name
636
646
  values_alias = quote_table_name("#{insert.model.table_name.parameterize}_values")
637
647
  sql = +"INSERT #{insert.into} #{insert.values_list} AS #{values_alias}"
638
648
 
639
649
  if insert.skip_duplicates?
640
650
  if no_op_column
641
- sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{values_alias}.#{no_op_column}"
651
+ sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{quoted_table_name}.#{no_op_column}"
642
652
  end
643
653
  elsif insert.update_duplicates?
644
654
  if insert.raw_update_sql?
645
655
  sql = +"INSERT #{insert.into} #{insert.values_list} ON DUPLICATE KEY UPDATE #{insert.raw_update_sql}"
646
656
  else
647
657
  sql << " ON DUPLICATE KEY UPDATE "
648
- sql << insert.touch_model_timestamps_unless { |column| "#{insert.model.quoted_table_name}.#{column}<=>#{values_alias}.#{column}" }
658
+ sql << insert.touch_model_timestamps_unless { |column| "#{quoted_table_name}.#{column}<=>#{values_alias}.#{column}" }
649
659
  sql << insert.updatable_columns.map { |column| "#{column}=#{values_alias}.#{column}" }.join(",")
650
660
  end
651
661
  end
@@ -746,9 +756,7 @@ module ActiveRecord
746
756
 
747
757
  private
748
758
  def strip_whitespace_characters(expression)
749
- expression = expression.gsub(/\\n|\\\\/, "")
750
- expression = expression.gsub(/\s{2,}/, " ")
751
- expression
759
+ expression.gsub('\\\n', "").gsub("x0A", "").squish
752
760
  end
753
761
 
754
762
  def extended_type_map_key
@@ -762,7 +770,6 @@ module ActiveRecord
762
770
  def handle_warnings(sql)
763
771
  return if ActiveRecord.db_warnings_action.nil? || @raw_connection.warning_count == 0
764
772
 
765
- @affected_rows_before_warnings = @raw_connection.affected_rows
766
773
  warning_count = @raw_connection.warning_count
767
774
  result = @raw_connection.query("SHOW WARNINGS")
768
775
  result = [
@@ -102,7 +102,13 @@ module ActiveRecord
102
102
  else
103
103
  value.getlocal
104
104
  end
105
- when Date, Time
105
+ when Time
106
+ if default_timezone == :utc
107
+ value.utc? ? value : value.getutc
108
+ else
109
+ value.utc? ? value.getlocal : value
110
+ end
111
+ when Date
106
112
  value
107
113
  else
108
114
  super
@@ -85,6 +85,13 @@ module ActiveRecord
85
85
  super
86
86
  end
87
87
 
88
+ def remove_foreign_key(from_table, to_table = nil, **options)
89
+ # RESTRICT is by default in MySQL.
90
+ options.delete(:on_update) if options[:on_update] == :restrict
91
+ options.delete(:on_delete) if options[:on_delete] == :restrict
92
+ super
93
+ end
94
+
88
95
  def internal_string_options_for_primary_key
89
96
  super.tap do |options|
90
97
  if !row_format_dynamic_by_default? && CHARSETS_OF_4BYTES_MAXLEN.include?(charset)
@@ -48,26 +48,55 @@ module ActiveRecord
48
48
  # made since we established the connection
49
49
  raw_connection.query_options[:database_timezone] = default_timezone
50
50
 
51
- result = if prepare
51
+ result = nil
52
+ if binds.nil? || binds.empty?
53
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
54
+ result = raw_connection.query(sql)
55
+ # Ref: https://github.com/brianmario/mysql2/pull/1383
56
+ # As of mysql2 0.5.6 `#affected_rows` might raise Mysql2::Error if a prepared statement
57
+ # from that same connection was GCed while `#query` released the GVL.
58
+ # By avoiding to call `#affected_rows` when we have a result, we reduce the likeliness
59
+ # of hitting the bug.
60
+ @affected_rows_before_warnings = result&.size || raw_connection.affected_rows
61
+ end
62
+ elsif prepare
52
63
  stmt = @statements[sql] ||= raw_connection.prepare(sql)
53
-
54
64
  begin
55
65
  ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
56
- stmt.execute(*type_casted_binds)
66
+ result = stmt.execute(*type_casted_binds)
67
+ @affected_rows_before_warnings = stmt.affected_rows
57
68
  end
58
69
  rescue ::Mysql2::Error
59
70
  @statements.delete(sql)
60
- stmt.close
61
71
  raise
62
72
  end
63
- verified!
64
73
  else
65
- raw_connection.query(sql)
74
+ stmt = raw_connection.prepare(sql)
75
+
76
+ begin
77
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
78
+ result = stmt.execute(*type_casted_binds)
79
+ @affected_rows_before_warnings = stmt.affected_rows
80
+ end
81
+
82
+ # Ref: https://github.com/brianmario/mysql2/pull/1383
83
+ # by eagerly closing uncached prepared statements, we also reduce the chances of
84
+ # that bug happening. It can still happen if `#execute` is used as we have no callback
85
+ # to eagerly close the statement.
86
+ if result
87
+ result.instance_variable_set(:@_ar_stmt_to_close, stmt)
88
+ else
89
+ stmt.close
90
+ end
91
+ rescue ::Mysql2::Error
92
+ stmt.close
93
+ raise
94
+ end
66
95
  end
67
96
 
97
+ notification_payload[:affected_rows] = @affected_rows_before_warnings
68
98
  notification_payload[:row_count] = result&.size || 0
69
99
 
70
- @affected_rows_before_warnings = raw_connection.affected_rows
71
100
  raw_connection.abandon_results!
72
101
 
73
102
  verified!
@@ -79,17 +108,34 @@ module ActiveRecord
79
108
  end
80
109
  end
81
110
 
82
- def cast_result(result)
83
- if result.nil? || result.fields.empty?
111
+ def cast_result(raw_result)
112
+ return ActiveRecord::Result.empty if raw_result.nil?
113
+
114
+ fields = raw_result.fields
115
+
116
+ result = if fields.empty?
84
117
  ActiveRecord::Result.empty
85
118
  else
86
- ActiveRecord::Result.new(result.fields, result.to_a)
119
+ ActiveRecord::Result.new(fields, raw_result.to_a)
87
120
  end
121
+
122
+ free_raw_result(raw_result)
123
+
124
+ result
88
125
  end
89
126
 
90
- def affected_rows(result)
127
+ def affected_rows(raw_result)
128
+ free_raw_result(raw_result) if raw_result
129
+
91
130
  @affected_rows_before_warnings
92
131
  end
132
+
133
+ def free_raw_result(raw_result)
134
+ raw_result.free
135
+ if stmt = raw_result.instance_variable_get(:@_ar_stmt_to_close)
136
+ stmt.close
137
+ end
138
+ end
93
139
  end
94
140
  end
95
141
  end
@@ -106,7 +106,14 @@ module ActiveRecord
106
106
  end
107
107
 
108
108
  def active?
109
- connected? && @lock.synchronize { @raw_connection&.ping } || false
109
+ if connected?
110
+ @lock.synchronize do
111
+ if @raw_connection&.ping
112
+ verified!
113
+ true
114
+ end
115
+ end
116
+ end || false
110
117
  end
111
118
 
112
119
  alias :reset! :reconnect!
@@ -5,9 +5,8 @@ module ActiveRecord
5
5
  class PoolConfig # :nodoc:
6
6
  include MonitorMixin
7
7
 
8
- attr_reader :db_config, :role, :shard
8
+ attr_reader :db_config, :role, :shard, :connection_descriptor
9
9
  attr_writer :schema_reflection, :server_version
10
- attr_accessor :connection_class
11
10
 
12
11
  def schema_reflection
13
12
  @schema_reflection ||= SchemaReflection.new(db_config.lazy_schema_cache_path)
@@ -29,7 +28,7 @@ module ActiveRecord
29
28
  def initialize(connection_class, db_config, role, shard)
30
29
  super()
31
30
  @server_version = nil
32
- @connection_class = connection_class
31
+ self.connection_descriptor = connection_class
33
32
  @db_config = db_config
34
33
  @role = role
35
34
  @shard = shard
@@ -41,11 +40,12 @@ module ActiveRecord
41
40
  @server_version || synchronize { @server_version ||= connection.get_database_version }
42
41
  end
43
42
 
44
- def connection_name
45
- if connection_class.primary_class?
46
- "ActiveRecord::Base"
43
+ def connection_descriptor=(connection_descriptor)
44
+ case connection_descriptor
45
+ when ConnectionHandler::ConnectionDescriptor
46
+ @connection_descriptor = connection_descriptor
47
47
  else
48
- connection_class.name
48
+ @connection_descriptor = ConnectionHandler::ConnectionDescriptor.new(connection_descriptor.name, connection_descriptor.primary_class?)
49
49
  end
50
50
  end
51
51
 
@@ -233,6 +233,8 @@ module ActiveRecord
233
233
  end
234
234
 
235
235
  def defined_for?(name: nil, column: nil, **options)
236
+ options = options.slice(*self.options.keys)
237
+
236
238
  (name.nil? || self.name == name.to_s) &&
237
239
  (column.nil? || Array(self.column) == Array(column).map(&:to_s)) &&
238
240
  options.all? { |k, v| self.options[k].to_s == v.to_s }
@@ -306,8 +308,8 @@ module ActiveRecord
306
308
  # t.exclusion_constraint("price WITH =, availability_range WITH &&", using: :gist, name: "price_check")
307
309
  #
308
310
  # See {connection.add_exclusion_constraint}[rdoc-ref:SchemaStatements#add_exclusion_constraint]
309
- def exclusion_constraint(*args)
310
- @base.add_exclusion_constraint(name, *args)
311
+ def exclusion_constraint(...)
312
+ @base.add_exclusion_constraint(name, ...)
311
313
  end
312
314
 
313
315
  # Removes the given exclusion constraint from the table.
@@ -315,8 +317,8 @@ module ActiveRecord
315
317
  # t.remove_exclusion_constraint(name: "price_check")
316
318
  #
317
319
  # See {connection.remove_exclusion_constraint}[rdoc-ref:SchemaStatements#remove_exclusion_constraint]
318
- def remove_exclusion_constraint(*args)
319
- @base.remove_exclusion_constraint(name, *args)
320
+ def remove_exclusion_constraint(...)
321
+ @base.remove_exclusion_constraint(name, ...)
320
322
  end
321
323
 
322
324
  # Adds a unique constraint.
@@ -324,8 +326,8 @@ module ActiveRecord
324
326
  # t.unique_constraint(:position, name: 'unique_position', deferrable: :deferred, nulls_not_distinct: true)
325
327
  #
326
328
  # See {connection.add_unique_constraint}[rdoc-ref:SchemaStatements#add_unique_constraint]
327
- def unique_constraint(*args)
328
- @base.add_unique_constraint(name, *args)
329
+ def unique_constraint(...)
330
+ @base.add_unique_constraint(name, ...)
329
331
  end
330
332
 
331
333
  # Removes the given unique constraint from the table.
@@ -333,8 +335,8 @@ module ActiveRecord
333
335
  # t.remove_unique_constraint(name: "unique_position")
334
336
  #
335
337
  # See {connection.remove_unique_constraint}[rdoc-ref:SchemaStatements#remove_unique_constraint]
336
- def remove_unique_constraint(*args)
337
- @base.remove_unique_constraint(name, *args)
338
+ def remove_unique_constraint(...)
339
+ @base.remove_unique_constraint(name, ...)
338
340
  end
339
341
 
340
342
  # Validates the given constraint on the table.
@@ -343,8 +345,8 @@ module ActiveRecord
343
345
  # t.validate_constraint "price_check"
344
346
  #
345
347
  # See {connection.validate_constraint}[rdoc-ref:SchemaStatements#validate_constraint]
346
- def validate_constraint(*args)
347
- @base.validate_constraint(name, *args)
348
+ def validate_constraint(...)
349
+ @base.validate_constraint(name, ...)
348
350
  end
349
351
 
350
352
  # Validates the given check constraint on the table
@@ -353,8 +355,8 @@ module ActiveRecord
353
355
  # t.validate_check_constraint name: "price_check"
354
356
  #
355
357
  # See {connection.validate_check_constraint}[rdoc-ref:SchemaStatements#validate_check_constraint]
356
- def validate_check_constraint(*args)
357
- @base.validate_check_constraint(name, *args)
358
+ def validate_check_constraint(...)
359
+ @base.validate_check_constraint(name, ...)
358
360
  end
359
361
  end
360
362