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