activerecord 8.1.3.1 → 8.1.4

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +617 -0
  3. data/lib/active_record/associations/association.rb +34 -16
  4. data/lib/active_record/associations/association_scope.rb +1 -1
  5. data/lib/active_record/associations/belongs_to_association.rb +2 -2
  6. data/lib/active_record/associations/builder/belongs_to.rb +19 -4
  7. data/lib/active_record/associations/collection_association.rb +6 -2
  8. data/lib/active_record/associations/collection_proxy.rb +4 -0
  9. data/lib/active_record/associations/foreign_association.rb +3 -1
  10. data/lib/active_record/associations/has_one_association.rb +1 -0
  11. data/lib/active_record/associations/join_dependency.rb +1 -2
  12. data/lib/active_record/associations/preloader/association.rb +1 -1
  13. data/lib/active_record/attribute_methods/serialization.rb +2 -2
  14. data/lib/active_record/attribute_methods.rb +6 -4
  15. data/lib/active_record/callbacks.rb +1 -2
  16. data/lib/active_record/coders/column_serializer.rb +1 -1
  17. data/lib/active_record/coders/json.rb +7 -6
  18. data/lib/active_record/coders/yaml_column.rb +2 -2
  19. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +17 -0
  20. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +32 -20
  21. data/lib/active_record/connection_adapters/abstract/database_statements.rb +14 -2
  22. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +2 -2
  23. data/lib/active_record/connection_adapters/abstract_adapter.rb +12 -0
  24. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +79 -23
  25. data/lib/active_record/connection_adapters/column.rb +2 -5
  26. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +7 -21
  27. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +3 -16
  28. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +46 -3
  29. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +1 -1
  30. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
  31. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +17 -5
  32. data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
  33. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +2 -2
  34. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +4 -2
  35. data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -0
  36. data/lib/active_record/connection_adapters/trilogy_adapter.rb +14 -0
  37. data/lib/active_record/counter_cache.rb +8 -6
  38. data/lib/active_record/database_configurations/hash_config.rb +9 -2
  39. data/lib/active_record/database_configurations/url_config.rb +2 -0
  40. data/lib/active_record/disable_joins_association_relation.rb +5 -1
  41. data/lib/active_record/encryption/encrypted_fixtures.rb +12 -7
  42. data/lib/active_record/filter_attribute_handler.rb +6 -3
  43. data/lib/active_record/gem_version.rb +2 -2
  44. data/lib/active_record/inheritance.rb +8 -3
  45. data/lib/active_record/log_subscriber.rb +28 -16
  46. data/lib/active_record/message_pack.rb +13 -0
  47. data/lib/active_record/migration/command_recorder.rb +12 -2
  48. data/lib/active_record/model_schema.rb +2 -2
  49. data/lib/active_record/nested_attributes.rb +8 -6
  50. data/lib/active_record/persistence.rb +1 -0
  51. data/lib/active_record/querying.rb +13 -1
  52. data/lib/active_record/reflection.rb +1 -1
  53. data/lib/active_record/relation/calculations.rb +8 -3
  54. data/lib/active_record/relation/finder_methods.rb +34 -20
  55. data/lib/active_record/relation/merger.rb +4 -2
  56. data/lib/active_record/relation/predicate_builder.rb +1 -1
  57. data/lib/active_record/relation/query_methods.rb +63 -13
  58. data/lib/active_record/relation.rb +20 -12
  59. data/lib/active_record/schema.rb +1 -1
  60. data/lib/active_record/schema_dumper.rb +1 -1
  61. data/lib/active_record/signed_id.rb +1 -1
  62. data/lib/active_record/statement_cache.rb +1 -1
  63. data/lib/active_record/store.rb +4 -2
  64. data/lib/active_record/tasks/abstract_tasks.rb +8 -1
  65. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -16
  66. data/lib/active_record/test_databases.rb +4 -0
  67. data/lib/active_record/test_fixtures.rb +13 -4
  68. data/lib/active_record/transactions.rb +19 -16
  69. data/lib/active_record/type_caster/connection.rb +1 -1
  70. data/lib/active_record.rb +1 -1
  71. data/lib/arel/visitors/to_sql.rb +1 -1
  72. metadata +9 -9
@@ -149,7 +149,7 @@ module ActiveRecord
149
149
  scope.includes_values |= item.includes_values
150
150
  end
151
151
 
152
- scope.unscope!(*item.unscope_values)
152
+ scope.unscope!(*item.table_name_qualified_unscope_values)
153
153
  scope.where_clause += item.where_clause
154
154
  scope.order_values = item.order_values | scope.order_values
155
155
  end
@@ -80,11 +80,11 @@ module ActiveRecord
80
80
  end
81
81
 
82
82
  def target_changed?
83
- owner.attribute_changed?(reflection.foreign_key) || (!foreign_key_present? && target&.new_record?)
83
+ Array(reflection.foreign_key).any? { |fk| owner.attribute_changed?(fk) } || (!foreign_key_present? && target&.new_record?)
84
84
  end
85
85
 
86
86
  def target_previously_changed?
87
- owner.attribute_previously_changed?(reflection.foreign_key)
87
+ Array(reflection.foreign_key).any? { |fk| owner.attribute_previously_changed?(fk) }
88
88
  end
89
89
 
90
90
  def saved_change_to_target?
@@ -43,7 +43,16 @@ module ActiveRecord::Associations::Builder # :nodoc:
43
43
  end
44
44
 
45
45
  def self.touch_record(o, changes, foreign_key, name, touch) # :nodoc:
46
- old_foreign_id = changes[foreign_key] && changes[foreign_key].first
46
+ old_foreign_id =
47
+ if foreign_key.is_a?(Array)
48
+ if foreign_key.any? { |fk| changes[fk] }
49
+ foreign_key.map do |fk|
50
+ changes[fk] ? changes[fk].first : o.read_attribute(fk)
51
+ end
52
+ end
53
+ elsif changes[foreign_key]
54
+ changes[foreign_key].first
55
+ end
47
56
 
48
57
  if old_foreign_id
49
58
  association = o.association(name)
@@ -56,7 +65,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
56
65
  klass = association.klass
57
66
  end
58
67
  primary_key = reflection.association_primary_key(klass)
59
- old_record = klass.find_by(primary_key => old_foreign_id)
68
+ old_record = klass.find_by(primary_key => [old_foreign_id])
60
69
 
61
70
  if old_record
62
71
  if touch != true
@@ -140,8 +149,14 @@ module ActiveRecord::Associations::Builder # :nodoc:
140
149
  foreign_key = reflection.foreign_key
141
150
  foreign_type = reflection.foreign_type
142
151
 
143
- record.read_attribute(foreign_key).nil? ||
144
- record.attribute_changed?(foreign_key) ||
152
+ fk_missing_or_changed = if foreign_key.is_a?(Array)
153
+ foreign_key.any? { |fk| record.read_attribute(fk).nil? || record.attribute_changed?(fk) }
154
+ else
155
+ record.read_attribute(foreign_key).nil? ||
156
+ record.attribute_changed?(foreign_key)
157
+ end
158
+
159
+ fk_missing_or_changed ||
145
160
  (reflection.polymorphic? && (record.read_attribute(foreign_type).nil? || record.attribute_changed?(foreign_type)))
146
161
  }
147
162
 
@@ -61,15 +61,19 @@ module ActiveRecord
61
61
  # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
62
62
  def ids_writer(ids)
63
63
  primary_key = reflection.association_primary_key
64
- pk_type = klass.type_for_attribute(primary_key)
65
64
  ids = Array(ids).compact_blank
66
- ids.map! { |id| pk_type.cast(id) }
67
65
 
68
66
  records = if klass.composite_primary_key?
67
+ pk_types = primary_key.map { |column| klass.type_for_attribute(column) }
68
+ ids.map! { |id| pk_types.zip(id).map! { |type, value| type.cast(value) } }
69
+
69
70
  klass.where(primary_key => ids).index_by do |record|
70
71
  primary_key.map { |primary_key| record._read_attribute(primary_key) }
71
72
  end
72
73
  else
74
+ pk_type = klass.type_for_attribute(primary_key)
75
+ ids.map! { |id| pk_type.cast(id) }
76
+
73
77
  klass.where(primary_key => ids).index_by do |record|
74
78
  record._read_attribute(primary_key)
75
79
  end
@@ -726,6 +726,10 @@ module ActiveRecord
726
726
  end
727
727
 
728
728
  def pluck(*column_names)
729
+ if proxy_association.violates_strict_loading?
730
+ Base.strict_loading_violation!(owner: proxy_association.owner.class, reflection: proxy_association.reflection)
731
+ end
732
+
729
733
  null_scope? ? scope.pluck(*column_names) : super
730
734
  end
731
735
 
@@ -4,7 +4,9 @@ module ActiveRecord::Associations
4
4
  module ForeignAssociation # :nodoc:
5
5
  def foreign_key_present?
6
6
  if reflection.klass.primary_key
7
- owner.attribute_present?(reflection.active_record_primary_key)
7
+ Array(reflection.active_record_primary_key).all? do |key|
8
+ owner.attribute_present?(key)
9
+ end
8
10
  else
9
11
  false
10
12
  end
@@ -120,6 +120,7 @@ module ActiveRecord
120
120
  Array(reflection.foreign_key).each do |foreign_key_column|
121
121
  record[foreign_key_column] = nil unless foreign_key_column.in?(Array(record.class.primary_key))
122
122
  end
123
+ record[reflection.type] = nil if reflection.type.present?
123
124
  end
124
125
 
125
126
  def transaction_if(value, &block)
@@ -168,8 +168,7 @@ module ActiveRecord
168
168
  def aliases
169
169
  @aliases ||= Aliases.new join_root.each_with_index.map { |join_part, i|
170
170
  column_names = if join_part == join_root && !join_root_alias
171
- primary_key = join_root.primary_key
172
- primary_key ? [primary_key] : []
171
+ Array(join_root.primary_key)
173
172
  else
174
173
  join_part.column_names
175
174
  end
@@ -169,7 +169,7 @@ module ActiveRecord
169
169
  def owners_by_key
170
170
  @owners_by_key ||= owners.each_with_object({}) do |owner, result|
171
171
  key = derive_key(owner, owner_key_name)
172
- (result[key] ||= []) << owner if key
172
+ (result[key] ||= []) << owner if key.is_a?(Array) ? key.all? : key
173
173
  end
174
174
  end
175
175
 
@@ -205,7 +205,7 @@ module ActiveRecord
205
205
  attribute(attr_name, **options)
206
206
 
207
207
  decorate_attributes([attr_name]) do |attr_name, cast_type|
208
- if type_incompatible_with_serialize?(cast_type, coder, type)
208
+ if type_incompatible_with_serialize?(cast_type, column_serializer, type)
209
209
  raise ColumnNotSerializableError.new(attr_name, cast_type)
210
210
  end
211
211
 
@@ -236,7 +236,7 @@ module ActiveRecord
236
236
  end
237
237
 
238
238
  def type_incompatible_with_serialize?(cast_type, coder, type)
239
- cast_type.is_a?(ActiveRecord::Type::Json) && coder == ::JSON ||
239
+ cast_type.is_a?(ActiveRecord::Type::Json) && Coders::JSON === coder ||
240
240
  cast_type.respond_to?(:type_cast_array, true) && type == ::Array
241
241
  end
242
242
  end
@@ -480,10 +480,12 @@ module ActiveRecord
480
480
  self.class.define_attribute_methods
481
481
 
482
482
  # So in all cases we must behave as if the method was just defined.
483
- method = begin
484
- self.class.public_instance_method(name)
485
- rescue NameError
486
- nil
483
+ method = if self.class.public_method_defined?(name)
484
+ begin
485
+ self.class.public_instance_method(name)
486
+ rescue NameError
487
+ nil
488
+ end
487
489
  end
488
490
 
489
491
  # The method might be explicitly defined in the model, but call a generated
@@ -173,8 +173,7 @@ module ActiveRecord
173
173
  #
174
174
  # If a <tt>before_*</tt> callback throws +:abort+, all the later callbacks and
175
175
  # the associated action are cancelled.
176
- # \Callbacks are generally run in the order they are defined, with the exception of callbacks defined as
177
- # methods on the model, which are called last.
176
+ # \Callbacks are run in the order they are defined.
178
177
  #
179
178
  # == Ordering callbacks
180
179
  #
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecord
4
- module Coders # :nodoc:
4
+ module Coders
5
5
  class ColumnSerializer # :nodoc:
6
6
  attr_reader :object_class
7
7
  attr_reader :coder
@@ -3,13 +3,14 @@
3
3
  require "active_support/json"
4
4
 
5
5
  module ActiveRecord
6
- module Coders # :nodoc:
6
+ module Coders
7
7
  class JSON # :nodoc:
8
- DEFAULT_OPTIONS = { escape: false }.freeze
8
+ DEFAULT_ENCODE_OPTIONS = { escape: false }.freeze
9
9
 
10
- def initialize(options = nil)
11
- @options = options ? DEFAULT_OPTIONS.merge(options) : DEFAULT_OPTIONS
12
- @encoder = ActiveSupport::JSON::Encoding.json_encoder.new(options)
10
+ def initialize(encode_options: nil, decode_options: nil)
11
+ encode_options = encode_options ? DEFAULT_ENCODE_OPTIONS.merge(encode_options) : DEFAULT_ENCODE_OPTIONS
12
+ @decode_options = decode_options
13
+ @encoder = ActiveSupport::JSON::Encoding.json_encoder.new(encode_options)
13
14
  end
14
15
 
15
16
  def dump(obj)
@@ -17,7 +18,7 @@ module ActiveRecord
17
18
  end
18
19
 
19
20
  def load(json)
20
- ActiveSupport::JSON.decode(json, @options) unless json.blank?
21
+ ActiveSupport::JSON.decode(json, @decode_options) unless json.blank?
21
22
  end
22
23
  end
23
24
  end
@@ -3,9 +3,9 @@
3
3
  require "yaml"
4
4
 
5
5
  module ActiveRecord
6
- module Coders # :nodoc:
6
+ module Coders
7
7
  class YAMLColumn < ColumnSerializer # :nodoc:
8
- class SafeCoder
8
+ class SafeCoder # :nodoc:
9
9
  def initialize(permitted_classes: [], unsafe_load: nil)
10
10
  @permitted_classes = permitted_classes
11
11
  @unsafe_load = unsafe_load
@@ -53,6 +53,23 @@ module ActiveRecord
53
53
  end
54
54
  end
55
55
 
56
+ def discard_pool(pool) # :nodoc:
57
+ @mutex.synchronize do
58
+ @pools.each do |frequency, refs|
59
+ refs.reject! do |ref|
60
+ ref.__getobj__ == pool
61
+ rescue WeakRef::RefError
62
+ true
63
+ end
64
+
65
+ if refs.empty?
66
+ @pools.delete(frequency)
67
+ @threads.delete(frequency)&.tap { |t| t.kill; t.join }
68
+ end
69
+ end
70
+ end
71
+ end
72
+
56
73
  def pools(refs = nil) # :nodoc:
57
74
  refs ||= @mutex.synchronize { @pools.values.flatten(1) }
58
75
 
@@ -18,12 +18,12 @@ module ActiveRecord
18
18
 
19
19
  def initialize
20
20
  super()
21
- @mutex = Mutex.new
21
+ @monitor = Monitor.new
22
22
  @server_version = nil
23
23
  end
24
24
 
25
25
  def server_version(connection) # :nodoc:
26
- @server_version || @mutex.synchronize { @server_version ||= connection.get_database_version }
26
+ @server_version || @monitor.synchronize { @server_version ||= connection.get_database_version }
27
27
  end
28
28
 
29
29
  def schema_reflection
@@ -129,14 +129,14 @@ module ActiveRecord
129
129
  class ConnectionPool
130
130
  # Prior to 3.3.5, WeakKeyMap had a use after free bug
131
131
  # https://bugs.ruby-lang.org/issues/20688
132
- if ObjectSpace.const_defined?(:WeakKeyMap) && Gem::Version.new(RUBY_VERSION) >= "3.3.5"
132
+ if ObjectSpace.const_defined?(:WeakKeyMap) && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.3.5")
133
133
  WeakThreadKeyMap = ObjectSpace::WeakKeyMap
134
134
  else
135
135
  class WeakThreadKeyMap # :nodoc:
136
136
  # FIXME: On 3.3 we could use ObjectSpace::WeakKeyMap
137
137
  # but it currently causes GC crashes: https://github.com/byroot/rails/pull/3
138
138
  def initialize
139
- @map = {}
139
+ @map = Concurrent::Map.new
140
140
  end
141
141
 
142
142
  def clear
@@ -148,7 +148,9 @@ module ActiveRecord
148
148
  end
149
149
 
150
150
  def []=(key, value)
151
- @map.select! { |c, _| c&.alive? }
151
+ @map.each_pair do |thread, _|
152
+ @map.delete(thread) unless thread&.alive?
153
+ end
152
154
  @map[key] = value
153
155
  end
154
156
  end
@@ -456,13 +458,13 @@ module ActiveRecord
456
458
  begin
457
459
  yield lease.connection
458
460
  ensure
459
- lease.sticky = sticky_was if prevent_permanent_checkout && !sticky_was
461
+ lease.sticky = sticky_was if prevent_permanent_checkout
460
462
  end
461
463
  else
462
464
  begin
463
465
  yield lease.connection = checkout
464
466
  ensure
465
- lease.sticky = sticky_was if prevent_permanent_checkout && !sticky_was
467
+ lease.sticky = sticky_was if prevent_permanent_checkout
466
468
  release_connection(lease) unless lease.sticky
467
469
  end
468
470
  end
@@ -522,9 +524,15 @@ module ActiveRecord
522
524
  @connections.each do |conn|
523
525
  if conn.in_use?
524
526
  conn.steal!
525
- checkin conn
527
+
528
+ ActiveSupport.error_reporter.handle(source: "active_record.connection_pool") do
529
+ checkin conn
530
+ end
531
+ end
532
+
533
+ ActiveSupport.error_reporter.handle(source: "active_record.connection_pool") do
534
+ conn.disconnect!
526
535
  end
527
- conn.disconnect!
528
536
  end
529
537
  @connections = @pinned_connection ? [@pinned_connection] : []
530
538
  @leases.clear
@@ -556,6 +564,7 @@ module ActiveRecord
556
564
  @reaper_lock.synchronize do
557
565
  synchronize do
558
566
  return if self.discarded?
567
+ Reaper.discard_pool(self)
559
568
  @connections.each do |conn|
560
569
  conn.discard!
561
570
  end
@@ -808,13 +817,15 @@ module ActiveRecord
808
817
  # having to wait for a connection to be established when first using it
809
818
  # after checkout.
810
819
  def preconnect
811
- sequential_maintenance -> c { (!c.connected? || !c.verified?) && c.allow_preconnect } do |conn|
812
- conn.connect!
813
- rescue
814
- # Wholesale rescue: there's nothing we can do but move on. The
815
- # connection will go back to the pool, and the next consumer will
816
- # presumably try to connect again -- which will either work, or
817
- # fail and they'll be able to report the exception.
820
+ reaper_lock do
821
+ sequential_maintenance -> c { (!c.connected? || !c.verified?) && c.allow_preconnect } do |conn|
822
+ conn.connect!
823
+ rescue
824
+ # Wholesale rescue: there's nothing we can do but move on. The
825
+ # connection will go back to the pool, and the next consumer will
826
+ # presumably try to connect again -- which will either work, or
827
+ # fail and they'll be able to report the exception.
828
+ end
818
829
  end
819
830
  end
820
831
 
@@ -1194,10 +1205,11 @@ module ActiveRecord
1194
1205
  synchronize do
1195
1206
  return unless @maintaining > @available.num_waiting
1196
1207
 
1197
- # We are guaranteed the "maintaining" thread will return its promised
1198
- # connection within one maintenance-unit of time. Thus we can safely
1199
- # do a blocking wait with (functionally) no timeout.
1200
- @available.poll(100)
1208
+ # Maintenance work (e.g. a keepalive ping) can stall for reasons outside
1209
+ # our control, such as a slow or unresponsive database server, so this
1210
+ # wait must stay bounded by the caller's own checkout_timeout rather than
1211
+ # assume maintenance always finishes quickly.
1212
+ @available.poll(checkout_timeout)
1201
1213
  end
1202
1214
  end
1203
1215
 
@@ -568,8 +568,20 @@ module ActiveRecord
568
568
  type_casted_binds = type_casted_binds(binds)
569
569
  log(sql, name, binds, type_casted_binds, async: async, allow_retry: allow_retry) do |notification_payload|
570
570
  with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
571
- result = perform_query(conn, sql, binds, type_casted_binds, prepare: prepare, notification_payload: notification_payload, batch: batch)
572
- handle_warnings(result, sql)
571
+ begin
572
+ result = perform_query(conn, sql, binds, type_casted_binds, prepare: prepare, notification_payload: notification_payload, batch: batch)
573
+ query_completed = true
574
+ ensure
575
+ begin
576
+ handle_warnings(result, sql)
577
+ rescue
578
+ raise if query_completed
579
+
580
+ # The query failed, so we need to swallow this exception
581
+ # from handle_warnings to avoid masking the original.
582
+ end
583
+ end
584
+
573
585
  result
574
586
  end
575
587
  end
@@ -1017,7 +1017,7 @@ module ActiveRecord
1017
1017
  # this is a naive implementation; some DBs may support this more efficiently (PostgreSQL, for instance)
1018
1018
  old_index_def = indexes(table_name).detect { |i| i.name == old_name }
1019
1019
  return unless old_index_def
1020
- add_index(table_name, old_index_def.columns, name: new_name, unique: old_index_def.unique)
1020
+ add_index(table_name, old_index_def.columns, name: new_name, unique: old_index_def.unique, where: old_index_def.where)
1021
1021
  remove_index(table_name, name: old_name)
1022
1022
  end
1023
1023
 
@@ -1357,7 +1357,7 @@ module ActiveRecord
1357
1357
  def remove_check_constraint(table_name, expression = nil, if_exists: false, **options)
1358
1358
  return unless supports_check_constraints?
1359
1359
 
1360
- return if if_exists && !check_constraint_exists?(table_name, **options)
1360
+ return if if_exists && !check_constraint_exists?(table_name, expression: expression, **options)
1361
1361
 
1362
1362
  chk_name_to_delete = check_constraint_for!(table_name, expression: expression, **options).name
1363
1363
 
@@ -649,6 +649,11 @@ module ActiveRecord
649
649
  def drop_virtual_table(*) # :nodoc:
650
650
  end
651
651
 
652
+ # Lock used to read an existing record after a duplicate INSERT in a transaction.
653
+ def create_or_find_by_lock # :nodoc:
654
+ true
655
+ end
656
+
652
657
  def advisory_locks_enabled? # :nodoc:
653
658
  supports_advisory_locks? && @advisory_locks_enabled
654
659
  end
@@ -1113,6 +1118,13 @@ module ActiveRecord
1113
1118
  end
1114
1119
 
1115
1120
  raise translated_exception
1121
+ rescue Exception
1122
+ # A non-StandardError (a Timeout, or a fiber scheduler's cancel) abandoned
1123
+ # the query partway through, so we mark the connection unverified, just as
1124
+ # a failed query would, forcing a reconnect before it's used again.
1125
+ @last_activity = nil
1126
+ @verified = false
1127
+ raise
1116
1128
  ensure
1117
1129
  dirty_current_transaction if materialize_transactions
1118
1130
  end
@@ -28,6 +28,21 @@ module ActiveRecord
28
28
  # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false
29
29
  class_attribute :emulate_booleans, default: true
30
30
 
31
+ class_attribute :cli_arg_map, instance_accessor: false, default: {
32
+ host: "--host",
33
+ port: "--port",
34
+ socket: "--socket",
35
+ username: "--user",
36
+ password: "--password",
37
+ encoding: "--default-character-set",
38
+ sslca: "--ssl-ca",
39
+ sslcert: "--ssl-cert",
40
+ sslcapath: "--ssl-capath",
41
+ sslcipher: "--ssl-cipher",
42
+ sslkey: "--ssl-key",
43
+ ssl_mode: "--ssl-mode"
44
+ }.freeze
45
+
31
46
  NATIVE_DATABASE_TYPES = {
32
47
  primary_key: "bigint auto_increment PRIMARY KEY",
33
48
  string: { name: "varchar", limit: 255 },
@@ -54,22 +69,14 @@ module ActiveRecord
54
69
  end
55
70
 
56
71
  class << self
72
+ def cli_args(config) # :nodoc:
73
+ cli_arg_map.filter_map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }
74
+ end
75
+
57
76
  def dbconsole(config, options = {})
58
77
  mysql_config = config.configuration_hash
59
78
 
60
- args = {
61
- host: "--host",
62
- port: "--port",
63
- socket: "--socket",
64
- username: "--user",
65
- encoding: "--default-character-set",
66
- sslca: "--ssl-ca",
67
- sslcert: "--ssl-cert",
68
- sslcapath: "--ssl-capath",
69
- sslcipher: "--ssl-cipher",
70
- sslkey: "--ssl-key",
71
- ssl_mode: "--ssl-mode"
72
- }.filter_map { |opt, arg| "#{arg}=#{mysql_config[opt]}" if mysql_config[opt] }
79
+ args = cli_args(mysql_config.except(:password))
73
80
 
74
81
  if mysql_config[:password] && options[:include_password]
75
82
  args << "--password=#{mysql_config[:password]}"
@@ -192,6 +199,16 @@ module ActiveRecord
192
199
  end
193
200
  end
194
201
 
202
+ def create_or_find_by_lock # :nodoc:
203
+ # A shared lock provides a current read under REPEATABLE READ without
204
+ # upgrading the shared record lock acquired by a duplicate INSERT.
205
+ if mariadb? || database_version < "8.0.1"
206
+ "LOCK IN SHARE MODE"
207
+ else
208
+ "FOR SHARE"
209
+ end
210
+ end
211
+
195
212
  def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
196
213
  query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
197
214
  end
@@ -239,12 +256,24 @@ module ActiveRecord
239
256
  def begin_isolated_db_transaction(isolation) # :nodoc:
240
257
  # From MySQL manual: The [SET TRANSACTION] statement applies only to the next single transaction performed within the session.
241
258
  # So we don't need to implement #reset_isolation_level
242
- execute_batch(
243
- ["SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}", "BEGIN"],
244
- "TRANSACTION",
245
- allow_retry: true,
246
- materialize_transactions: false,
247
- )
259
+ if multi_statements_enabled?
260
+ execute_batch(
261
+ ["SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}", "BEGIN"],
262
+ "TRANSACTION",
263
+ allow_retry: true,
264
+ materialize_transactions: false,
265
+ )
266
+ else
267
+ with_raw_connection(allow_retry: true, materialize_transactions: false) do
268
+ internal_execute(
269
+ "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}",
270
+ "TRANSACTION",
271
+ allow_retry: false,
272
+ materialize_transactions: false,
273
+ )
274
+ internal_execute("BEGIN", "TRANSACTION", allow_retry: false, materialize_transactions: false)
275
+ end
276
+ end
248
277
  end
249
278
 
250
279
  def commit_db_transaction # :nodoc:
@@ -469,7 +498,7 @@ module ActiveRecord
469
498
  raise NotImplementedError unless supports_disabling_indexes?
470
499
 
471
500
  query = <<~SQL
472
- ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} #{mariadb? ? "NOT IGNORED" : "VISIBLE"}
501
+ ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{quote_column_name(index_name)} #{mariadb? ? "NOT IGNORED" : "VISIBLE"}
473
502
  SQL
474
503
  execute(query)
475
504
  end
@@ -478,7 +507,7 @@ module ActiveRecord
478
507
  raise NotImplementedError unless supports_disabling_indexes?
479
508
 
480
509
  query = <<~SQL
481
- ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} #{mariadb? ? "IGNORED" : "INVISIBLE"}
510
+ ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{quote_column_name(index_name)} #{mariadb? ? "IGNORED" : "INVISIBLE"}
482
511
  SQL
483
512
  execute(query)
484
513
  end
@@ -816,6 +845,7 @@ module ActiveRecord
816
845
  # See https://dev.mysql.com/doc/mysql-errors/en/server-error-reference.html
817
846
  ER_DB_CREATE_EXISTS = 1007
818
847
  ER_FILSORT_ABORT = 1028
848
+ ER_NO_DB_ERROR = 1046
819
849
  ER_DUP_ENTRY = 1062
820
850
  ER_SERVER_SHUTDOWN = 1053
821
851
  ER_NOT_NULL_VIOLATION = 1048
@@ -848,7 +878,7 @@ module ActiveRecord
848
878
  else
849
879
  super
850
880
  end
851
- when ER_CONNECTION_KILLED, ER_SERVER_SHUTDOWN, CR_SERVER_GONE_ERROR, CR_SERVER_LOST, ER_CLIENT_INTERACTION_TIMEOUT
881
+ when ER_CONNECTION_KILLED, ER_SERVER_SHUTDOWN, CR_SERVER_GONE_ERROR, CR_SERVER_LOST, ER_CLIENT_INTERACTION_TIMEOUT, ER_NO_DB_ERROR
852
882
  ConnectionFailed.new(message, sql: sql, binds: binds, connection_pool: @pool)
853
883
  when ER_DB_CREATE_EXISTS
854
884
  DatabaseAlreadyExists.new(message, sql: sql, binds: binds, connection_pool: @pool)
@@ -990,7 +1020,33 @@ module ActiveRecord
990
1020
  end
991
1021
 
992
1022
  def column_definitions(table_name) # :nodoc:
993
- internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA", allow_retry: true)
1023
+ fields = internal_exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA", allow_retry: true)
1024
+
1025
+ update_fields_for_mariadb(table_name, fields) if mariadb?
1026
+
1027
+ fields
1028
+ end
1029
+
1030
+ def update_fields_for_mariadb(table_name, fields)
1031
+ has_function_default_candidate = fields.any? do |field|
1032
+ default = field["Default"]
1033
+ default&.match?(/[a-zA-Z_]\w*\(/) && !/\ACURRENT_TIMESTAMP/i.match?(default)
1034
+ end
1035
+
1036
+ if has_function_default_candidate
1037
+ table_info = create_table_info(table_name)
1038
+ fields.each do |field|
1039
+ default = field["Default"]
1040
+ next unless default&.match?(/[a-zA-Z_]\w*\(/)
1041
+ next if /\ACURRENT_TIMESTAMP/i.match?(default)
1042
+
1043
+ field_name = field["Field"]
1044
+ match = table_info&.match(/`#{field_name}` .+ DEFAULT ('|\d+|[A-z]+)/)
1045
+ if match && match[1].match?(/\A[A-z]/)
1046
+ field["Extra"] = "DEFAULT_GENERATED"
1047
+ end
1048
+ end
1049
+ end
994
1050
  end
995
1051
 
996
1052
  def create_table_info(table_name) # :nodoc:
@@ -7,7 +7,7 @@ module ActiveRecord
7
7
  class Column
8
8
  include Deduplicable
9
9
 
10
- attr_reader :name, :default, :sql_type_metadata, :null, :default_function, :collation, :comment
10
+ attr_reader :name, :default, :sql_type_metadata, :null, :default_function, :collation, :comment, :cast_type
11
11
 
12
12
  delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
13
13
 
@@ -22,7 +22,7 @@ module ActiveRecord
22
22
  @cast_type = cast_type
23
23
  @sql_type_metadata = sql_type_metadata
24
24
  @null = null
25
- @default = default.nil? || cast_type.mutable? ? default : cast_type.deserialize(default)
25
+ @default = default
26
26
  @default_function = default_function
27
27
  @collation = collation
28
28
  @comment = comment
@@ -110,9 +110,6 @@ module ActiveRecord
110
110
  false
111
111
  end
112
112
 
113
- protected
114
- attr_reader :cast_type
115
-
116
113
  private
117
114
  def deduplicated
118
115
  @name = -name