switchman 3.0.24 → 4.2.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/Rakefile +16 -15
- data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
- data/lib/switchman/active_record/abstract_adapter.rb +11 -7
- data/lib/switchman/active_record/associations.rb +157 -50
- data/lib/switchman/active_record/attribute_methods.rb +192 -101
- data/lib/switchman/active_record/base.rb +136 -33
- data/lib/switchman/active_record/calculations.rb +91 -48
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +41 -6
- data/lib/switchman/active_record/database_configurations.rb +23 -13
- data/lib/switchman/active_record/finder_methods.rb +22 -16
- data/lib/switchman/active_record/log_subscriber.rb +3 -6
- data/lib/switchman/active_record/migration.rb +42 -17
- data/lib/switchman/active_record/model_schema.rb +1 -1
- data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
- data/lib/switchman/active_record/persistence.rb +32 -2
- data/lib/switchman/active_record/postgresql_adapter.rb +37 -22
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +26 -17
- data/lib/switchman/active_record/query_methods.rb +249 -142
- data/lib/switchman/active_record/reflection.rb +10 -3
- data/lib/switchman/active_record/relation.rb +103 -32
- data/lib/switchman/active_record/spawn_methods.rb +3 -7
- data/lib/switchman/active_record/statement_cache.rb +13 -9
- data/lib/switchman/active_record/table_definition.rb +1 -1
- data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
- data/lib/switchman/active_record/test_fixtures.rb +71 -25
- data/lib/switchman/active_support/cache.rb +9 -4
- data/lib/switchman/arel.rb +16 -25
- data/lib/switchman/call_super.rb +2 -2
- data/lib/switchman/database_server.rb +68 -34
- data/lib/switchman/default_shard.rb +14 -3
- data/lib/switchman/engine.rb +36 -19
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +13 -0
- data/lib/switchman/guard_rail/relation.rb +3 -3
- data/lib/switchman/parallel.rb +6 -6
- data/lib/switchman/r_spec_helper.rb +12 -11
- data/lib/switchman/shard.rb +182 -72
- data/lib/switchman/sharded_instrumenter.rb +9 -3
- data/lib/switchman/shared_schema_cache.rb +11 -0
- data/lib/switchman/standard_error.rb +4 -0
- data/lib/switchman/test_helper.rb +3 -3
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +27 -15
- data/lib/tasks/switchman.rake +96 -60
- metadata +18 -168
|
@@ -6,9 +6,9 @@ module Switchman
|
|
|
6
6
|
module ClassMethods
|
|
7
7
|
delegate :shard, to: :all
|
|
8
8
|
|
|
9
|
-
def find_ids_in_ranges(opts = {}, &
|
|
9
|
+
def find_ids_in_ranges(opts = {}, &)
|
|
10
10
|
opts.reverse_merge!(loose: true)
|
|
11
|
-
all.find_ids_in_ranges(opts, &
|
|
11
|
+
all.find_ids_in_ranges(opts, &)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def sharded_model
|
|
@@ -25,11 +25,11 @@ module Switchman
|
|
|
25
25
|
def transaction(**)
|
|
26
26
|
if self != ::ActiveRecord::Base && current_scope
|
|
27
27
|
current_scope.activate do
|
|
28
|
-
db = Shard.current(
|
|
28
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
29
29
|
db.unguard { super }
|
|
30
30
|
end
|
|
31
31
|
else
|
|
32
|
-
db = Shard.current(
|
|
32
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
33
33
|
db.unguard { super }
|
|
34
34
|
end
|
|
35
35
|
end
|
|
@@ -53,8 +53,13 @@ module Switchman
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def clear_query_caches_for_current_thread
|
|
56
|
-
::ActiveRecord::Base.connection_handler.connection_pool_list
|
|
57
|
-
|
|
56
|
+
pools = ::ActiveRecord::Base.connection_handler.connection_pool_list(:all)
|
|
57
|
+
pools.each do |pool|
|
|
58
|
+
if ::Rails.version < "7.2"
|
|
59
|
+
pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
|
|
60
|
+
elsif pool.active_connection?
|
|
61
|
+
pool.lease_connection(switch_shard: false).clear_query_cache
|
|
62
|
+
end
|
|
58
63
|
end
|
|
59
64
|
end
|
|
60
65
|
|
|
@@ -63,7 +68,10 @@ module Switchman
|
|
|
63
68
|
end
|
|
64
69
|
|
|
65
70
|
def establish_connection(config_or_env = nil)
|
|
66
|
-
|
|
71
|
+
if config_or_env.is_a?(Symbol) && config_or_env != ::Rails.env.to_sym
|
|
72
|
+
raise ArgumentError,
|
|
73
|
+
"establish connection cannot be used on the non-current shard/role"
|
|
74
|
+
end
|
|
67
75
|
|
|
68
76
|
# Ensure we don't randomly surprise change the connection parms associated with a shard/role
|
|
69
77
|
config_or_env = nil if config_or_env == ::Rails.env.to_sym
|
|
@@ -71,16 +79,18 @@ module Switchman
|
|
|
71
79
|
config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
|
|
72
80
|
:primary
|
|
73
81
|
else
|
|
74
|
-
"#{current_shard}/#{current_role}"
|
|
82
|
+
:"#{current_shard}/#{current_role}"
|
|
75
83
|
end
|
|
76
84
|
|
|
77
|
-
super
|
|
85
|
+
super
|
|
78
86
|
end
|
|
79
87
|
|
|
80
88
|
def connected_to_stack
|
|
81
|
-
|
|
89
|
+
has_own_stack = ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
|
|
82
90
|
|
|
83
91
|
ret = super
|
|
92
|
+
return ret if has_own_stack
|
|
93
|
+
|
|
84
94
|
DatabaseServer.guard_servers
|
|
85
95
|
ret
|
|
86
96
|
end
|
|
@@ -92,10 +102,13 @@ module Switchman
|
|
|
92
102
|
sharded_role = nil
|
|
93
103
|
connected_to_stack.reverse_each do |hash|
|
|
94
104
|
shard_role = hash.dig(:shard_roles, target_shard)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
unless shard_role &&
|
|
106
|
+
(hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
|
|
107
|
+
next
|
|
98
108
|
end
|
|
109
|
+
|
|
110
|
+
sharded_role = shard_role
|
|
111
|
+
break
|
|
99
112
|
end
|
|
100
113
|
# Allow a shard-specific role to be reverted to regular inheritance
|
|
101
114
|
return sharded_role if sharded_role && sharded_role != :_switchman_inherit
|
|
@@ -107,7 +120,7 @@ module Switchman
|
|
|
107
120
|
# i.e. other sharded models don't inherit the current shard of Base
|
|
108
121
|
def current_shard
|
|
109
122
|
connected_to_stack.reverse_each do |hash|
|
|
110
|
-
return hash[:shard] if hash[:shard] && hash[:klasses].include?(
|
|
123
|
+
return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_class_for_self)
|
|
111
124
|
end
|
|
112
125
|
|
|
113
126
|
default_shard
|
|
@@ -115,7 +128,9 @@ module Switchman
|
|
|
115
128
|
|
|
116
129
|
def current_switchman_shard
|
|
117
130
|
connected_to_stack.reverse_each do |hash|
|
|
118
|
-
|
|
131
|
+
if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
|
|
132
|
+
return hash[:switchman_shard]
|
|
133
|
+
end
|
|
119
134
|
end
|
|
120
135
|
|
|
121
136
|
Shard.default
|
|
@@ -124,44 +139,120 @@ module Switchman
|
|
|
124
139
|
|
|
125
140
|
def self.prepended(klass)
|
|
126
141
|
klass.singleton_class.prepend(ClassMethods)
|
|
142
|
+
klass.singleton_class.prepend(Switchman::ActiveRecord::Relation::InsertUpsertAll) if ::Rails.version < "7.2"
|
|
143
|
+
klass.scope :non_shadow, lambda { |key = primary_key|
|
|
144
|
+
where(key => (QueryMethods::NonTransposingValue.new(0)...
|
|
145
|
+
QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)))
|
|
146
|
+
}
|
|
147
|
+
klass.scope :shadow, lambda { |key = primary_key|
|
|
148
|
+
where(key => QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)..)
|
|
149
|
+
}
|
|
127
150
|
end
|
|
128
151
|
|
|
129
152
|
def _run_initialize_callbacks
|
|
130
153
|
@shard ||= if self.class.sharded_primary_key?
|
|
131
|
-
Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.
|
|
154
|
+
Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_class_for_self))
|
|
132
155
|
else
|
|
133
|
-
Shard.current(self.class.
|
|
156
|
+
Shard.current(self.class.connection_class_for_self)
|
|
134
157
|
end
|
|
158
|
+
|
|
159
|
+
@loaded_from_shard ||= Shard.current(self.class.connection_class_for_self)
|
|
160
|
+
if shadow_record? && !Switchman.config[:writable_shadow_records]
|
|
161
|
+
@readonly = true
|
|
162
|
+
@readonly_from_shadow ||= true
|
|
163
|
+
end
|
|
135
164
|
super
|
|
136
165
|
end
|
|
137
166
|
|
|
167
|
+
def readonly!
|
|
168
|
+
@readonly_from_shadow = false
|
|
169
|
+
super
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def shadow_record?
|
|
173
|
+
pkey = self[self.class.primary_key]
|
|
174
|
+
return false unless self.class.sharded_column?(self.class.primary_key) && pkey
|
|
175
|
+
|
|
176
|
+
pkey > Shard::IDS_PER_SHARD
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def canonical?
|
|
180
|
+
!shadow_record?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
|
|
184
|
+
return if target_shard == shard
|
|
185
|
+
|
|
186
|
+
shadow_attrs = {}
|
|
187
|
+
new_attrs.each do |attr, value|
|
|
188
|
+
shadow_attrs[attr] = if self.class.sharded_column?(attr)
|
|
189
|
+
Shard.relative_id_for(value, shard, target_shard)
|
|
190
|
+
else
|
|
191
|
+
value
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
target_shard.activate do
|
|
195
|
+
self.class.upsert_all([shadow_attrs], unique_by: self.class.primary_key)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def destroy_shadow_records(target_shards: [Shard.current])
|
|
200
|
+
raise Errors::ShadowRecordError, "Cannot be called on a shadow record." if shadow_record?
|
|
201
|
+
|
|
202
|
+
unless self.class.sharded_column?(self.class.primary_key)
|
|
203
|
+
raise Errors::MethodUnsupportedForUnshardedTableError,
|
|
204
|
+
"Cannot be called on a record belonging to an unsharded table."
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
Array(target_shards).each do |target_shard|
|
|
208
|
+
next if target_shard == shard
|
|
209
|
+
|
|
210
|
+
target_shard.activate { self.class.where("id = ?", global_id).delete_all }
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Returns "the shard that this record was actually loaded from" , as
|
|
215
|
+
# opposed to "the shard this record belongs on", which might be
|
|
216
|
+
# different if this is a shadow record.
|
|
217
|
+
def loaded_from_shard
|
|
218
|
+
@loaded_from_shard || shard
|
|
219
|
+
end
|
|
220
|
+
|
|
138
221
|
def shard
|
|
139
|
-
@shard ||
|
|
222
|
+
@shard || fallback_shard
|
|
140
223
|
end
|
|
141
224
|
|
|
142
225
|
def shard=(new_shard)
|
|
143
226
|
raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
|
|
144
227
|
|
|
145
|
-
|
|
228
|
+
if shard == new_shard
|
|
229
|
+
@loaded_from_shard = new_shard
|
|
230
|
+
return
|
|
231
|
+
end
|
|
146
232
|
|
|
147
233
|
attributes.each do |attr, value|
|
|
148
234
|
self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
|
|
149
235
|
end
|
|
236
|
+
@loaded_from_shard = new_shard
|
|
150
237
|
@shard = new_shard
|
|
151
238
|
end
|
|
152
239
|
|
|
153
240
|
def save(*, **)
|
|
241
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
242
|
+
|
|
154
243
|
@shard_set_in_stone = true
|
|
155
244
|
super
|
|
156
245
|
end
|
|
157
246
|
|
|
158
247
|
def save!(*, **)
|
|
248
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
249
|
+
|
|
159
250
|
@shard_set_in_stone = true
|
|
160
251
|
super
|
|
161
252
|
end
|
|
162
253
|
|
|
163
254
|
def destroy
|
|
164
|
-
shard.activate(self.class.
|
|
255
|
+
shard.activate(self.class.connection_class_for_self) { super }
|
|
165
256
|
end
|
|
166
257
|
|
|
167
258
|
def clone
|
|
@@ -173,21 +264,21 @@ module Switchman
|
|
|
173
264
|
result
|
|
174
265
|
end
|
|
175
266
|
|
|
176
|
-
def transaction(
|
|
177
|
-
shard.activate(self.class.
|
|
178
|
-
self.class.transaction(
|
|
267
|
+
def transaction(...)
|
|
268
|
+
shard.activate(self.class.connection_class_for_self) do
|
|
269
|
+
self.class.transaction(...)
|
|
179
270
|
end
|
|
180
271
|
end
|
|
181
272
|
|
|
182
273
|
def with_transaction_returning_status
|
|
183
|
-
shard.activate(self.class.
|
|
184
|
-
db = Shard.current(self.class.
|
|
274
|
+
shard.activate(self.class.connection_class_for_self) do
|
|
275
|
+
db = Shard.current(self.class.connection_class_for_self).database_server
|
|
185
276
|
db.unguard { super }
|
|
186
277
|
end
|
|
187
278
|
end
|
|
188
279
|
|
|
189
280
|
def hash
|
|
190
|
-
self.class.sharded_primary_key? ? self.class
|
|
281
|
+
self.class.sharded_primary_key? ? [self.class, global_id].hash : super
|
|
191
282
|
end
|
|
192
283
|
|
|
193
284
|
def to_param
|
|
@@ -208,8 +299,10 @@ module Switchman
|
|
|
208
299
|
|
|
209
300
|
def id_for_database
|
|
210
301
|
if self.class.sharded_primary_key?
|
|
211
|
-
# It's an int, so
|
|
212
|
-
# In theory we should do
|
|
302
|
+
# It's an int, so it's safe to just return it without passing it
|
|
303
|
+
# through anything else. In theory we should do
|
|
304
|
+
# `@attributes[@primary_key].type.serialize(id)`, but that seems to
|
|
305
|
+
# have surprising side-effects
|
|
213
306
|
id
|
|
214
307
|
else
|
|
215
308
|
super
|
|
@@ -218,24 +311,34 @@ module Switchman
|
|
|
218
311
|
|
|
219
312
|
protected
|
|
220
313
|
|
|
221
|
-
# see also AttributeMethods#
|
|
222
|
-
def
|
|
314
|
+
# see also AttributeMethods#connection_class_for_self_code_for_reflection
|
|
315
|
+
def connection_class_for_self_for_reflection(reflection)
|
|
223
316
|
if reflection
|
|
224
317
|
if reflection.options[:polymorphic]
|
|
225
318
|
begin
|
|
226
|
-
read_attribute(reflection.foreign_type)&.constantize&.
|
|
319
|
+
read_attribute(reflection.foreign_type)&.constantize&.connection_class_for_self || ::ActiveRecord::Base
|
|
227
320
|
rescue NameError
|
|
228
321
|
# in case someone is abusing foreign_type to not point to an actual class
|
|
229
322
|
::ActiveRecord::Base
|
|
230
323
|
end
|
|
231
324
|
else
|
|
232
325
|
# otherwise we can just return a symbol for the statically known type of the association
|
|
233
|
-
reflection.klass.
|
|
326
|
+
reflection.klass.connection_class_for_self
|
|
234
327
|
end
|
|
235
328
|
else
|
|
236
|
-
self.class.
|
|
329
|
+
self.class.connection_class_for_self
|
|
237
330
|
end
|
|
238
331
|
end
|
|
332
|
+
|
|
333
|
+
private
|
|
334
|
+
|
|
335
|
+
def fallback_shard
|
|
336
|
+
Shard.current(self.class.connection_class_for_self) || Shard.default
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def creating_shadow_record?
|
|
340
|
+
new_record? && shadow_record?
|
|
341
|
+
end
|
|
239
342
|
end
|
|
240
343
|
end
|
|
241
344
|
end
|
|
@@ -4,7 +4,7 @@ module Switchman
|
|
|
4
4
|
module ActiveRecord
|
|
5
5
|
module Calculations
|
|
6
6
|
def pluck(*column_names)
|
|
7
|
-
target_shard = Shard.current(klass.
|
|
7
|
+
target_shard = Shard.current(klass.connection_class_for_self)
|
|
8
8
|
shard_count = 0
|
|
9
9
|
result = activate do |relation, shard|
|
|
10
10
|
shard_count += 1
|
|
@@ -28,19 +28,19 @@ module Switchman
|
|
|
28
28
|
|
|
29
29
|
def execute_simple_calculation(operation, column_name, distinct)
|
|
30
30
|
operation = operation.to_s.downcase
|
|
31
|
-
if operation ==
|
|
31
|
+
if operation == "average"
|
|
32
32
|
result = calculate_simple_average(column_name, distinct)
|
|
33
33
|
else
|
|
34
|
-
result = activate do |relation|
|
|
34
|
+
result = activate(count: operation == "count") do |relation|
|
|
35
35
|
relation.call_super(:execute_simple_calculation, Calculations, operation, column_name, distinct)
|
|
36
36
|
end
|
|
37
37
|
if result.is_a?(Array)
|
|
38
38
|
case operation
|
|
39
|
-
when
|
|
39
|
+
when "count", "sum"
|
|
40
40
|
result = result.sum
|
|
41
|
-
when
|
|
41
|
+
when "minimum"
|
|
42
42
|
result = result.min
|
|
43
|
-
when
|
|
43
|
+
when "maximum"
|
|
44
44
|
result = result.max
|
|
45
45
|
end
|
|
46
46
|
end
|
|
@@ -52,20 +52,20 @@ module Switchman
|
|
|
52
52
|
# See activerecord#execute_simple_calculation
|
|
53
53
|
relation = except(:order)
|
|
54
54
|
column = aggregate_column(column_name)
|
|
55
|
-
relation.select_values = [operation_over_aggregate_column(column,
|
|
56
|
-
operation_over_aggregate_column(column,
|
|
55
|
+
relation.select_values = [operation_over_aggregate_column(column, "average", distinct).as("average"),
|
|
56
|
+
operation_over_aggregate_column(column, "count", distinct).as("count")]
|
|
57
57
|
|
|
58
58
|
initial_results = relation.activate { |rel| klass.connection.select_all(rel) }
|
|
59
59
|
if initial_results.is_a?(Array)
|
|
60
60
|
initial_results.each do |r|
|
|
61
|
-
r[
|
|
62
|
-
r[
|
|
61
|
+
r["average"] = type_cast_calculated_value_switchman(r["average"], column_name, "average")
|
|
62
|
+
r["count"] = type_cast_calculated_value_switchman(r["count"], column_name, "count")
|
|
63
63
|
end
|
|
64
|
-
result = initial_results.
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
result = initial_results.sum { |r| r["average"] * r["count"] } / initial_results.sum do |r|
|
|
65
|
+
r["count"]
|
|
66
|
+
end
|
|
67
67
|
else
|
|
68
|
-
result = type_cast_calculated_value_switchman(initial_results.first[
|
|
68
|
+
result = type_cast_calculated_value_switchman(initial_results.first["average"], column_name, "average")
|
|
69
69
|
end
|
|
70
70
|
result
|
|
71
71
|
end
|
|
@@ -90,12 +90,12 @@ module Switchman
|
|
|
90
90
|
row[opts[:aggregate_alias]] = type_cast_calculated_value_switchman(
|
|
91
91
|
row[opts[:aggregate_alias]], column_name, opts[:operation]
|
|
92
92
|
)
|
|
93
|
-
row[
|
|
93
|
+
row["count"] = row["count"].to_i if opts[:operation] == "average"
|
|
94
94
|
|
|
95
95
|
opts[:group_columns].each do |aliaz, _type, group_column_name|
|
|
96
96
|
if opts[:associated] && (aliaz == opts[:group_aliases].first)
|
|
97
97
|
row[aliaz] = key_records[Shard.relative_id_for(row[aliaz], shard, target_shard)]
|
|
98
|
-
elsif group_column_name &&
|
|
98
|
+
elsif group_column_name && klass.sharded_column?(group_column_name)
|
|
99
99
|
row[aliaz] = Shard.relative_id_for(row[aliaz], shard, target_shard)
|
|
100
100
|
end
|
|
101
101
|
end
|
|
@@ -106,53 +106,95 @@ module Switchman
|
|
|
106
106
|
compact_grouped_calculation_rows(rows, opts)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
def ids
|
|
110
|
+
return super unless klass.sharded_primary_key?
|
|
111
|
+
|
|
112
|
+
if loaded?
|
|
113
|
+
result = records.map do |record|
|
|
114
|
+
Shard.relative_id_for(record._read_attribute(primary_key),
|
|
115
|
+
record.shard,
|
|
116
|
+
Shard.current(klass.connection_class_for_self))
|
|
117
|
+
end
|
|
118
|
+
return @async ? Promise::Complete.new(result) : result
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if has_include?(primary_key)
|
|
122
|
+
relation = apply_join_dependency.group(primary_key)
|
|
123
|
+
return relation.ids
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
columns = arel_columns([primary_key])
|
|
127
|
+
base_shard = Shard.current(klass.connection_class_for_self)
|
|
128
|
+
activate do |r|
|
|
129
|
+
relation = r.spawn
|
|
130
|
+
relation.select_values = columns
|
|
131
|
+
|
|
132
|
+
result = if relation.where_clause.contradiction?
|
|
133
|
+
::ActiveRecord::Result.empty
|
|
134
|
+
else
|
|
135
|
+
skip_query_cache_if_necessary do
|
|
136
|
+
klass.connection.select_all(relation, "#{klass.name} Ids", async: @async)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
result.then do |res|
|
|
141
|
+
type_cast_pluck_values(res, columns).map { |id| Shard.relative_id_for(id, Shard.current, base_shard) }
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
109
146
|
private
|
|
110
147
|
|
|
111
148
|
def type_cast_calculated_value_switchman(value, column_name, operation)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
type.deserialize(val)
|
|
117
|
-
end
|
|
149
|
+
column = aggregate_column(column_name)
|
|
150
|
+
type ||= column.try(:type_caster) ||
|
|
151
|
+
lookup_cast_type_from_join_dependencies(column_name.to_s) || ::ActiveRecord::Type.default_value
|
|
152
|
+
type_cast_calculated_value(value, operation, type)
|
|
118
153
|
end
|
|
119
154
|
|
|
120
155
|
def column_name_for(field)
|
|
121
|
-
field.respond_to?(:name) ? field.name.to_s : field.to_s.split(
|
|
156
|
+
field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
|
|
122
157
|
end
|
|
123
158
|
|
|
124
159
|
def grouped_calculation_options(operation, column_name, distinct)
|
|
125
|
-
opts = { operation
|
|
160
|
+
opts = { operation:, column_name:, distinct: }
|
|
161
|
+
|
|
162
|
+
column_alias_tracker = ::ActiveRecord::Calculations::ColumnAliasTracker.new(connection)
|
|
126
163
|
|
|
127
|
-
opts[:aggregate_alias] = aggregate_alias_for(operation, column_name)
|
|
164
|
+
opts[:aggregate_alias] = aggregate_alias_for(operation, column_name, column_alias_tracker)
|
|
128
165
|
group_attrs = group_values
|
|
129
166
|
if group_attrs.first.respond_to?(:to_sym)
|
|
130
167
|
association = klass.reflect_on_association(group_attrs.first.to_sym)
|
|
131
|
-
|
|
168
|
+
# only count belongs_to associations
|
|
169
|
+
associated = group_attrs.size == 1 && association && association.macro == :belongs_to
|
|
132
170
|
group_fields = Array(associated ? association.foreign_key : group_attrs)
|
|
133
171
|
else
|
|
134
172
|
group_fields = group_attrs
|
|
135
173
|
end
|
|
136
174
|
|
|
137
|
-
|
|
138
|
-
|
|
175
|
+
group_aliases = group_fields.map do |field|
|
|
176
|
+
field = connection.visitor.compile(field) if ::Arel.arel_node?(field)
|
|
177
|
+
column_alias_tracker.alias_for(field.to_s.downcase)
|
|
178
|
+
end
|
|
139
179
|
group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
|
|
140
180
|
[aliaz, type_for(field), column_name_for(field)]
|
|
141
181
|
end
|
|
142
|
-
opts.merge!(association
|
|
143
|
-
|
|
144
|
-
|
|
182
|
+
opts.merge!(association:,
|
|
183
|
+
associated:,
|
|
184
|
+
group_aliases:,
|
|
185
|
+
group_columns:,
|
|
186
|
+
group_fields:)
|
|
145
187
|
|
|
146
188
|
opts
|
|
147
189
|
end
|
|
148
190
|
|
|
149
|
-
def aggregate_alias_for(operation, column_name)
|
|
150
|
-
if operation ==
|
|
151
|
-
|
|
152
|
-
elsif operation ==
|
|
153
|
-
|
|
191
|
+
def aggregate_alias_for(operation, column_name, column_alias_tracker)
|
|
192
|
+
if operation == "count" && column_name == :all
|
|
193
|
+
"count_all"
|
|
194
|
+
elsif operation == "average"
|
|
195
|
+
"average"
|
|
154
196
|
else
|
|
155
|
-
|
|
197
|
+
column_alias_tracker.alias_for("#{operation} #{column_name}")
|
|
156
198
|
end
|
|
157
199
|
end
|
|
158
200
|
|
|
@@ -166,13 +208,14 @@ module Switchman
|
|
|
166
208
|
opts[:distinct]
|
|
167
209
|
).as(opts[:aggregate_alias])
|
|
168
210
|
]
|
|
169
|
-
if opts[:operation] ==
|
|
211
|
+
if opts[:operation] == "average"
|
|
170
212
|
# include count in average so we can recalculate the average
|
|
171
213
|
# across all shards if needed
|
|
172
214
|
select_values << operation_over_aggregate_column(
|
|
173
215
|
aggregate_column(opts[:column_name]),
|
|
174
|
-
|
|
175
|
-
|
|
216
|
+
"count",
|
|
217
|
+
opts[:distinct]
|
|
218
|
+
).as("count")
|
|
176
219
|
end
|
|
177
220
|
|
|
178
221
|
haves = having_clause.send(:predicates)
|
|
@@ -198,22 +241,22 @@ module Switchman
|
|
|
198
241
|
key = key.first if key.size == 1
|
|
199
242
|
value = row[opts[:aggregate_alias]]
|
|
200
243
|
|
|
201
|
-
if opts[:operation] ==
|
|
244
|
+
if opts[:operation] == "average"
|
|
202
245
|
if result.key?(key)
|
|
203
246
|
old_value, old_count = result[key]
|
|
204
|
-
new_count = old_count + row[
|
|
205
|
-
new_value = ((old_value * old_count) + (value * row[
|
|
247
|
+
new_count = old_count + row["count"]
|
|
248
|
+
new_value = ((old_value * old_count) + (value * row["count"])) / new_count
|
|
206
249
|
result[key] = [new_value, new_count]
|
|
207
250
|
else
|
|
208
|
-
result[key] = [value, row[
|
|
251
|
+
result[key] = [value, row["count"]]
|
|
209
252
|
end
|
|
210
253
|
elsif result.key?(key)
|
|
211
254
|
case opts[:operation]
|
|
212
|
-
when
|
|
255
|
+
when "count", "sum"
|
|
213
256
|
result[key] += value
|
|
214
|
-
when
|
|
257
|
+
when "minimum"
|
|
215
258
|
result[key] = value if value < result[key]
|
|
216
|
-
when
|
|
259
|
+
when "maximum"
|
|
217
260
|
result[key] = value if value > result[key]
|
|
218
261
|
end
|
|
219
262
|
else
|
|
@@ -221,7 +264,7 @@ module Switchman
|
|
|
221
264
|
end
|
|
222
265
|
end
|
|
223
266
|
|
|
224
|
-
result.transform_values!(&:first) if opts[:operation] ==
|
|
267
|
+
result.transform_values!(&:first) if opts[:operation] == "average"
|
|
225
268
|
|
|
226
269
|
result
|
|
227
270
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Switchman
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
module ConnectionHandler
|
|
6
|
+
def resolve_pool_config(config, connection_name, role, shard)
|
|
7
|
+
ret = super
|
|
8
|
+
# Make *all* pool configs use the same schema reflection
|
|
9
|
+
ret.schema_reflection = ConnectionHandler.global_schema_reflection
|
|
10
|
+
ret
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.global_schema_reflection
|
|
14
|
+
@global_schema_reflection ||= ::ActiveRecord::ConnectionAdapters::SchemaReflection.new(nil)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -4,9 +4,10 @@ module Switchman
|
|
|
4
4
|
module ActiveRecord
|
|
5
5
|
module ConnectionPool
|
|
6
6
|
def default_schema
|
|
7
|
-
connection
|
|
7
|
+
connection_method = (::Rails.version < "7.2") ? :connection : :lease_connection
|
|
8
|
+
send(connection_method) unless @schemas
|
|
8
9
|
# default shard will not switch databases immediately, so it won't be set yet
|
|
9
|
-
@schemas ||=
|
|
10
|
+
@schemas ||= send(connection_method).current_schemas
|
|
10
11
|
@schemas.first
|
|
11
12
|
end
|
|
12
13
|
|
|
@@ -24,14 +25,44 @@ module Switchman
|
|
|
24
25
|
conn
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
unless ::Rails.version < "7.2"
|
|
29
|
+
def active_connection(switch_shard: true)
|
|
30
|
+
conn = super()
|
|
31
|
+
return nil if conn.nil?
|
|
32
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
33
|
+
|
|
34
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
35
|
+
conn
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def lease_connection(switch_shard: true)
|
|
39
|
+
conn = super()
|
|
40
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
41
|
+
|
|
42
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
43
|
+
conn
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def with_connection(switch_shard: true, **kwargs)
|
|
47
|
+
super(**kwargs) do |conn|
|
|
48
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
49
|
+
|
|
50
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
51
|
+
yield conn
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
27
56
|
def release_connection(with_id = Thread.current)
|
|
28
|
-
super
|
|
57
|
+
super
|
|
29
58
|
|
|
30
59
|
flush
|
|
31
60
|
end
|
|
32
61
|
|
|
33
62
|
def switch_database(conn)
|
|
34
|
-
|
|
63
|
+
if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
|
|
64
|
+
@schemas = conn.current_schemas
|
|
65
|
+
end
|
|
35
66
|
|
|
36
67
|
conn.shard = current_shard
|
|
37
68
|
end
|
|
@@ -39,11 +70,15 @@ module Switchman
|
|
|
39
70
|
private
|
|
40
71
|
|
|
41
72
|
def current_shard
|
|
42
|
-
|
|
73
|
+
if ::Rails.version < "8.0"
|
|
74
|
+
connection_class.current_switchman_shard
|
|
75
|
+
else
|
|
76
|
+
connection_descriptor.name.constantize.current_switchman_shard
|
|
77
|
+
end
|
|
43
78
|
end
|
|
44
79
|
|
|
45
80
|
def tls_key
|
|
46
|
-
"#{object_id}_shard"
|
|
81
|
+
:"#{object_id}_shard"
|
|
47
82
|
end
|
|
48
83
|
end
|
|
49
84
|
end
|