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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +617 -0
- data/lib/active_record/associations/association.rb +34 -16
- data/lib/active_record/associations/association_scope.rb +1 -1
- data/lib/active_record/associations/belongs_to_association.rb +2 -2
- data/lib/active_record/associations/builder/belongs_to.rb +19 -4
- data/lib/active_record/associations/collection_association.rb +6 -2
- data/lib/active_record/associations/collection_proxy.rb +4 -0
- data/lib/active_record/associations/foreign_association.rb +3 -1
- data/lib/active_record/associations/has_one_association.rb +1 -0
- data/lib/active_record/associations/join_dependency.rb +1 -2
- data/lib/active_record/associations/preloader/association.rb +1 -1
- data/lib/active_record/attribute_methods/serialization.rb +2 -2
- data/lib/active_record/attribute_methods.rb +6 -4
- data/lib/active_record/callbacks.rb +1 -2
- data/lib/active_record/coders/column_serializer.rb +1 -1
- data/lib/active_record/coders/json.rb +7 -6
- data/lib/active_record/coders/yaml_column.rb +2 -2
- data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +17 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +32 -20
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +14 -2
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +2 -2
- data/lib/active_record/connection_adapters/abstract_adapter.rb +12 -0
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +79 -23
- data/lib/active_record/connection_adapters/column.rb +2 -5
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +7 -21
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +3 -16
- data/lib/active_record/connection_adapters/postgresql/oid/range.rb +46 -3
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +17 -5
- data/lib/active_record/connection_adapters/schema_cache.rb +2 -2
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +2 -2
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +4 -2
- data/lib/active_record/connection_adapters/trilogy/database_statements.rb +4 -0
- data/lib/active_record/connection_adapters/trilogy_adapter.rb +14 -0
- data/lib/active_record/counter_cache.rb +8 -6
- data/lib/active_record/database_configurations/hash_config.rb +9 -2
- data/lib/active_record/database_configurations/url_config.rb +2 -0
- data/lib/active_record/disable_joins_association_relation.rb +5 -1
- data/lib/active_record/encryption/encrypted_fixtures.rb +12 -7
- data/lib/active_record/filter_attribute_handler.rb +6 -3
- data/lib/active_record/gem_version.rb +2 -2
- data/lib/active_record/inheritance.rb +8 -3
- data/lib/active_record/log_subscriber.rb +28 -16
- data/lib/active_record/message_pack.rb +13 -0
- data/lib/active_record/migration/command_recorder.rb +12 -2
- data/lib/active_record/model_schema.rb +2 -2
- data/lib/active_record/nested_attributes.rb +8 -6
- data/lib/active_record/persistence.rb +1 -0
- data/lib/active_record/querying.rb +13 -1
- data/lib/active_record/reflection.rb +1 -1
- data/lib/active_record/relation/calculations.rb +8 -3
- data/lib/active_record/relation/finder_methods.rb +34 -20
- data/lib/active_record/relation/merger.rb +4 -2
- data/lib/active_record/relation/predicate_builder.rb +1 -1
- data/lib/active_record/relation/query_methods.rb +63 -13
- data/lib/active_record/relation.rb +20 -12
- data/lib/active_record/schema.rb +1 -1
- data/lib/active_record/schema_dumper.rb +1 -1
- data/lib/active_record/signed_id.rb +1 -1
- data/lib/active_record/statement_cache.rb +1 -1
- data/lib/active_record/store.rb +4 -2
- data/lib/active_record/tasks/abstract_tasks.rb +8 -1
- data/lib/active_record/tasks/mysql_database_tasks.rb +1 -16
- data/lib/active_record/test_databases.rb +4 -0
- data/lib/active_record/test_fixtures.rb +13 -4
- data/lib/active_record/transactions.rb +19 -16
- data/lib/active_record/type_caster/connection.rb +1 -1
- data/lib/active_record.rb +1 -1
- data/lib/arel/visitors/to_sql.rb +1 -1
- 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.
|
|
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?(
|
|
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?(
|
|
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 =
|
|
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
|
-
|
|
144
|
-
record.attribute_changed?(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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) &&
|
|
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 =
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
|
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
|
#
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
require "active_support/json"
|
|
4
4
|
|
|
5
5
|
module ActiveRecord
|
|
6
|
-
module Coders
|
|
6
|
+
module Coders
|
|
7
7
|
class JSON # :nodoc:
|
|
8
|
-
|
|
8
|
+
DEFAULT_ENCODE_OPTIONS = { escape: false }.freeze
|
|
9
9
|
|
|
10
|
-
def initialize(
|
|
11
|
-
|
|
12
|
-
@
|
|
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, @
|
|
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
|
|
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
|
-
@
|
|
21
|
+
@monitor = Monitor.new
|
|
22
22
|
@server_version = nil
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def server_version(connection) # :nodoc:
|
|
26
|
-
@server_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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
#
|
|
1198
|
-
#
|
|
1199
|
-
#
|
|
1200
|
-
|
|
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
|
-
|
|
572
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
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
|