switchman 3.2.1 → 4.0.0
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 +15 -14
- 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 +4 -2
- data/lib/switchman/active_record/associations.rb +89 -49
- data/lib/switchman/active_record/attribute_methods.rb +72 -34
- data/lib/switchman/active_record/base.rb +145 -27
- data/lib/switchman/active_record/calculations.rb +96 -49
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +24 -3
- data/lib/switchman/active_record/database_configurations.rb +37 -15
- data/lib/switchman/active_record/finder_methods.rb +44 -14
- data/lib/switchman/active_record/log_subscriber.rb +11 -5
- data/lib/switchman/active_record/migration.rb +45 -3
- data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
- data/lib/switchman/active_record/persistence.rb +30 -0
- 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 +192 -135
- data/lib/switchman/active_record/relation.rb +28 -13
- data/lib/switchman/active_record/spawn_methods.rb +2 -2
- data/lib/switchman/active_record/statement_cache.rb +2 -2
- 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 +9 -4
- data/lib/switchman/arel.rb +34 -18
- data/lib/switchman/call_super.rb +2 -2
- data/lib/switchman/database_server.rb +69 -31
- data/lib/switchman/default_shard.rb +14 -3
- data/lib/switchman/engine.rb +29 -22
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +13 -0
- data/lib/switchman/guard_rail/relation.rb +1 -1
- data/lib/switchman/parallel.rb +6 -6
- data/lib/switchman/r_spec_helper.rb +12 -11
- data/lib/switchman/shard.rb +180 -68
- data/lib/switchman/sharded_instrumenter.rb +3 -3
- data/lib/switchman/shared_schema_cache.rb +11 -0
- data/lib/switchman/standard_error.rb +4 -0
- data/lib/switchman/test_helper.rb +2 -2
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +27 -15
- data/lib/tasks/switchman.rake +96 -60
- metadata +35 -45
|
@@ -22,13 +22,59 @@ module Switchman
|
|
|
22
22
|
@integral_id
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
def transaction(**)
|
|
26
|
+
if self != ::ActiveRecord::Base && current_scope
|
|
27
|
+
current_scope.activate do
|
|
28
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
29
|
+
db.unguard { super }
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
33
|
+
db.unguard { super }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# NOTE: `returning` values are _not_ transposed back to the current shard
|
|
38
|
+
%w[insert_all upsert_all].each do |method|
|
|
26
39
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
27
|
-
def #{method}(
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
def #{method}(attributes, returning: nil, **)
|
|
41
|
+
scope = self != ::ActiveRecord::Base && current_scope
|
|
42
|
+
if (target_shard = scope&.primary_shard) == (current_shard = Shard.current(connection_class_for_self))
|
|
43
|
+
scope = nil
|
|
44
|
+
end
|
|
45
|
+
if scope
|
|
46
|
+
dupped = false
|
|
47
|
+
attributes.each_with_index do |hash, i|
|
|
48
|
+
if dupped || hash.any? { |k, v| sharded_column?(k) }
|
|
49
|
+
unless dupped
|
|
50
|
+
attributes = attributes.dup
|
|
51
|
+
dupped = true
|
|
52
|
+
end
|
|
53
|
+
attributes[i] = hash.to_h do |k, v|
|
|
54
|
+
if sharded_column?(k)
|
|
55
|
+
[k, Shard.relative_id_for(v, current_shard, target_shard)]
|
|
56
|
+
else
|
|
57
|
+
[k, v]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if scope
|
|
65
|
+
scope.activate do
|
|
30
66
|
db = Shard.current(connection_class_for_self).database_server
|
|
31
|
-
db.unguard { super }
|
|
67
|
+
result = db.unguard { super }
|
|
68
|
+
if result&.columns&.any? { |c| sharded_column?(c) }
|
|
69
|
+
transposed_rows = result.rows.map do |row|
|
|
70
|
+
row.map.with_index do |value, i|
|
|
71
|
+
sharded_column?(result.columns[i]) ? Shard.relative_id_for(value, target_shard, current_shard) : value
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
result = ::ActiveRecord::Result.new(result.columns, transposed_rows, result.column_types)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
result
|
|
32
78
|
end
|
|
33
79
|
else
|
|
34
80
|
db = Shard.current(connection_class_for_self).database_server
|
|
@@ -57,7 +103,12 @@ module Switchman
|
|
|
57
103
|
end
|
|
58
104
|
|
|
59
105
|
def clear_query_caches_for_current_thread
|
|
60
|
-
::
|
|
106
|
+
pools = if ::Rails.version < "7.1"
|
|
107
|
+
::ActiveRecord::Base.connection_handler.connection_pool_list
|
|
108
|
+
else
|
|
109
|
+
::ActiveRecord::Base.connection_handler.connection_pool_list(:all)
|
|
110
|
+
end
|
|
111
|
+
pools.each do |pool|
|
|
61
112
|
pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
|
|
62
113
|
end
|
|
63
114
|
end
|
|
@@ -67,7 +118,10 @@ module Switchman
|
|
|
67
118
|
end
|
|
68
119
|
|
|
69
120
|
def establish_connection(config_or_env = nil)
|
|
70
|
-
|
|
121
|
+
if config_or_env.is_a?(Symbol) && config_or_env != ::Rails.env.to_sym
|
|
122
|
+
raise ArgumentError,
|
|
123
|
+
"establish connection cannot be used on the non-current shard/role"
|
|
124
|
+
end
|
|
71
125
|
|
|
72
126
|
# Ensure we don't randomly surprise change the connection parms associated with a shard/role
|
|
73
127
|
config_or_env = nil if config_or_env == ::Rails.env.to_sym
|
|
@@ -75,16 +129,18 @@ module Switchman
|
|
|
75
129
|
config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
|
|
76
130
|
:primary
|
|
77
131
|
else
|
|
78
|
-
"#{current_shard}/#{current_role}"
|
|
132
|
+
:"#{current_shard}/#{current_role}"
|
|
79
133
|
end
|
|
80
134
|
|
|
81
135
|
super(config_or_env)
|
|
82
136
|
end
|
|
83
137
|
|
|
84
138
|
def connected_to_stack
|
|
85
|
-
|
|
139
|
+
has_own_stack = ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
|
|
86
140
|
|
|
87
141
|
ret = super
|
|
142
|
+
return ret if has_own_stack
|
|
143
|
+
|
|
88
144
|
DatabaseServer.guard_servers
|
|
89
145
|
ret
|
|
90
146
|
end
|
|
@@ -96,10 +152,13 @@ module Switchman
|
|
|
96
152
|
sharded_role = nil
|
|
97
153
|
connected_to_stack.reverse_each do |hash|
|
|
98
154
|
shard_role = hash.dig(:shard_roles, target_shard)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
155
|
+
unless shard_role &&
|
|
156
|
+
(hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
|
|
157
|
+
next
|
|
102
158
|
end
|
|
159
|
+
|
|
160
|
+
sharded_role = shard_role
|
|
161
|
+
break
|
|
103
162
|
end
|
|
104
163
|
# Allow a shard-specific role to be reverted to regular inheritance
|
|
105
164
|
return sharded_role if sharded_role && sharded_role != :_switchman_inherit
|
|
@@ -119,21 +178,24 @@ module Switchman
|
|
|
119
178
|
|
|
120
179
|
def current_switchman_shard
|
|
121
180
|
connected_to_stack.reverse_each do |hash|
|
|
122
|
-
|
|
181
|
+
if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
|
|
182
|
+
return hash[:switchman_shard]
|
|
183
|
+
end
|
|
123
184
|
end
|
|
124
185
|
|
|
125
186
|
Shard.default
|
|
126
187
|
end
|
|
127
|
-
|
|
128
|
-
if ::Rails.version < '7.0'
|
|
129
|
-
def connection_class_for_self
|
|
130
|
-
connection_classes
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
188
|
end
|
|
134
189
|
|
|
135
190
|
def self.prepended(klass)
|
|
136
191
|
klass.singleton_class.prepend(ClassMethods)
|
|
192
|
+
klass.scope :non_shadow, lambda { |key = primary_key|
|
|
193
|
+
where(key => (QueryMethods::NonTransposingValue.new(0)..
|
|
194
|
+
QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)))
|
|
195
|
+
}
|
|
196
|
+
klass.scope :shadow, lambda { |key = primary_key|
|
|
197
|
+
where(key => QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)..)
|
|
198
|
+
}
|
|
137
199
|
end
|
|
138
200
|
|
|
139
201
|
def _run_initialize_callbacks
|
|
@@ -142,7 +204,17 @@ module Switchman
|
|
|
142
204
|
else
|
|
143
205
|
Shard.current(self.class.connection_class_for_self)
|
|
144
206
|
end
|
|
145
|
-
|
|
207
|
+
|
|
208
|
+
@loaded_from_shard ||= Shard.current(self.class.connection_class_for_self)
|
|
209
|
+
if shadow_record? && !Switchman.config[:writable_shadow_records]
|
|
210
|
+
@readonly = true
|
|
211
|
+
@readonly_from_shadow ||= true
|
|
212
|
+
end
|
|
213
|
+
super
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def readonly!
|
|
217
|
+
@readonly_from_shadow = false
|
|
146
218
|
super
|
|
147
219
|
end
|
|
148
220
|
|
|
@@ -153,6 +225,10 @@ module Switchman
|
|
|
153
225
|
pkey > Shard::IDS_PER_SHARD
|
|
154
226
|
end
|
|
155
227
|
|
|
228
|
+
def canonical?
|
|
229
|
+
!shadow_record?
|
|
230
|
+
end
|
|
231
|
+
|
|
156
232
|
def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
|
|
157
233
|
return if target_shard == shard
|
|
158
234
|
|
|
@@ -169,27 +245,57 @@ module Switchman
|
|
|
169
245
|
end
|
|
170
246
|
end
|
|
171
247
|
|
|
248
|
+
def destroy_shadow_records(target_shards: [Shard.current])
|
|
249
|
+
raise Errors::ShadowRecordError, "Cannot be called on a shadow record." if shadow_record?
|
|
250
|
+
|
|
251
|
+
unless self.class.sharded_column?(self.class.primary_key)
|
|
252
|
+
raise Errors::MethodUnsupportedForUnshardedTableError,
|
|
253
|
+
"Cannot be called on a record belonging to an unsharded table."
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
Array(target_shards).each do |target_shard|
|
|
257
|
+
next if target_shard == shard
|
|
258
|
+
|
|
259
|
+
target_shard.activate { self.class.where("id = ?", global_id).delete_all }
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Returns "the shard that this record was actually loaded from" , as
|
|
264
|
+
# opposed to "the shard this record belongs on", which might be
|
|
265
|
+
# different if this is a shadow record.
|
|
266
|
+
def loaded_from_shard
|
|
267
|
+
@loaded_from_shard || shard
|
|
268
|
+
end
|
|
269
|
+
|
|
172
270
|
def shard
|
|
173
|
-
@shard ||
|
|
271
|
+
@shard || fallback_shard
|
|
174
272
|
end
|
|
175
273
|
|
|
176
274
|
def shard=(new_shard)
|
|
177
275
|
raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
|
|
178
276
|
|
|
179
|
-
|
|
277
|
+
if shard == new_shard
|
|
278
|
+
@loaded_from_shard = new_shard
|
|
279
|
+
return
|
|
280
|
+
end
|
|
180
281
|
|
|
181
282
|
attributes.each do |attr, value|
|
|
182
283
|
self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
|
|
183
284
|
end
|
|
285
|
+
@loaded_from_shard = new_shard
|
|
184
286
|
@shard = new_shard
|
|
185
287
|
end
|
|
186
288
|
|
|
187
289
|
def save(*, **)
|
|
290
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
291
|
+
|
|
188
292
|
@shard_set_in_stone = true
|
|
189
293
|
super
|
|
190
294
|
end
|
|
191
295
|
|
|
192
296
|
def save!(*, **)
|
|
297
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
298
|
+
|
|
193
299
|
@shard_set_in_stone = true
|
|
194
300
|
super
|
|
195
301
|
end
|
|
@@ -207,9 +313,9 @@ module Switchman
|
|
|
207
313
|
result
|
|
208
314
|
end
|
|
209
315
|
|
|
210
|
-
def transaction(
|
|
316
|
+
def transaction(...)
|
|
211
317
|
shard.activate(self.class.connection_class_for_self) do
|
|
212
|
-
self.class.transaction(
|
|
318
|
+
self.class.transaction(...)
|
|
213
319
|
end
|
|
214
320
|
end
|
|
215
321
|
|
|
@@ -221,7 +327,7 @@ module Switchman
|
|
|
221
327
|
end
|
|
222
328
|
|
|
223
329
|
def hash
|
|
224
|
-
self.class.sharded_primary_key? ? self.class
|
|
330
|
+
self.class.sharded_primary_key? ? [self.class, global_id].hash : super
|
|
225
331
|
end
|
|
226
332
|
|
|
227
333
|
def to_param
|
|
@@ -242,8 +348,10 @@ module Switchman
|
|
|
242
348
|
|
|
243
349
|
def id_for_database
|
|
244
350
|
if self.class.sharded_primary_key?
|
|
245
|
-
# It's an int, so
|
|
246
|
-
# In theory we should do
|
|
351
|
+
# It's an int, so it's safe to just return it without passing it
|
|
352
|
+
# through anything else. In theory we should do
|
|
353
|
+
# `@attributes[@primary_key].type.serialize(id)`, but that seems to
|
|
354
|
+
# have surprising side-effects
|
|
247
355
|
id
|
|
248
356
|
else
|
|
249
357
|
super
|
|
@@ -270,6 +378,16 @@ module Switchman
|
|
|
270
378
|
self.class.connection_class_for_self
|
|
271
379
|
end
|
|
272
380
|
end
|
|
381
|
+
|
|
382
|
+
private
|
|
383
|
+
|
|
384
|
+
def fallback_shard
|
|
385
|
+
Shard.current(self.class.connection_class_for_self) || Shard.default
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def creating_shadow_record?
|
|
389
|
+
new_record? && shadow_record?
|
|
390
|
+
end
|
|
273
391
|
end
|
|
274
392
|
end
|
|
275
393
|
end
|
|
@@ -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)
|
|
@@ -106,58 +106,104 @@ module Switchman
|
|
|
106
106
|
compact_grouped_calculation_rows(rows, opts)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
if ::Rails.version >= "7.1"
|
|
110
|
+
def ids
|
|
111
|
+
return super unless klass.sharded_primary_key?
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
if loaded?
|
|
114
|
+
result = records.map do |record|
|
|
115
|
+
Shard.relative_id_for(record._read_attribute(primary_key),
|
|
116
|
+
record.shard,
|
|
117
|
+
Shard.current(klass.connection_class_for_self))
|
|
118
|
+
end
|
|
119
|
+
return @async ? Promise::Complete.new(result) : result
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if has_include?(primary_key)
|
|
123
|
+
relation = apply_join_dependency.group(primary_key)
|
|
124
|
+
return relation.ids
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
columns = arel_columns([primary_key])
|
|
128
|
+
base_shard = Shard.current(klass.connection_class_for_self)
|
|
129
|
+
activate do |r|
|
|
130
|
+
relation = r.spawn
|
|
131
|
+
relation.select_values = columns
|
|
132
|
+
|
|
133
|
+
result = if relation.where_clause.contradiction?
|
|
134
|
+
::ActiveRecord::Result.empty
|
|
135
|
+
else
|
|
136
|
+
skip_query_cache_if_necessary do
|
|
137
|
+
klass.connection.select_all(relation, "#{klass.name} Ids", async: @async)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
result.then do |res|
|
|
142
|
+
type_cast_pluck_values(res, columns).map { |id| Shard.relative_id_for(id, Shard.current, base_shard) }
|
|
143
|
+
end
|
|
118
144
|
end
|
|
119
|
-
else
|
|
120
|
-
column = aggregate_column(column_name)
|
|
121
|
-
type ||= column.try(:type_caster) ||
|
|
122
|
-
lookup_cast_type_from_join_dependencies(column_name.to_s) || ::ActiveRecord::Type.default_value
|
|
123
|
-
type_cast_calculated_value(value, operation, type)
|
|
124
145
|
end
|
|
125
146
|
end
|
|
126
147
|
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def type_cast_calculated_value_switchman(value, column_name, operation)
|
|
151
|
+
column = aggregate_column(column_name)
|
|
152
|
+
type ||= column.try(:type_caster) ||
|
|
153
|
+
lookup_cast_type_from_join_dependencies(column_name.to_s) || ::ActiveRecord::Type.default_value
|
|
154
|
+
type_cast_calculated_value(value, operation, type)
|
|
155
|
+
end
|
|
156
|
+
|
|
127
157
|
def column_name_for(field)
|
|
128
|
-
field.respond_to?(:name) ? field.name.to_s : field.to_s.split(
|
|
158
|
+
field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
|
|
129
159
|
end
|
|
130
160
|
|
|
131
161
|
def grouped_calculation_options(operation, column_name, distinct)
|
|
132
162
|
opts = { operation: operation, column_name: column_name, distinct: distinct }
|
|
133
163
|
|
|
134
|
-
|
|
164
|
+
# Rails 7.0.5
|
|
165
|
+
if defined?(::ActiveRecord::Calculations::ColumnAliasTracker)
|
|
166
|
+
column_alias_tracker = ::ActiveRecord::Calculations::ColumnAliasTracker.new(connection)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
opts[:aggregate_alias] = aggregate_alias_for(operation, column_name, column_alias_tracker)
|
|
135
170
|
group_attrs = group_values
|
|
136
171
|
if group_attrs.first.respond_to?(:to_sym)
|
|
137
172
|
association = klass.reflect_on_association(group_attrs.first.to_sym)
|
|
138
|
-
|
|
173
|
+
# only count belongs_to associations
|
|
174
|
+
associated = group_attrs.size == 1 && association && association.macro == :belongs_to
|
|
139
175
|
group_fields = Array(associated ? association.foreign_key : group_attrs)
|
|
140
176
|
else
|
|
141
177
|
group_fields = group_attrs
|
|
142
178
|
end
|
|
143
179
|
|
|
144
|
-
|
|
145
|
-
|
|
180
|
+
group_aliases = group_fields.map do |field|
|
|
181
|
+
field = connection.visitor.compile(field) if ::Arel.arel_node?(field)
|
|
182
|
+
if column_alias_tracker
|
|
183
|
+
column_alias_tracker.alias_for(field.to_s.downcase)
|
|
184
|
+
else
|
|
185
|
+
column_alias_for(field.to_s.downcase)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
146
188
|
group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
|
|
147
189
|
[aliaz, type_for(field), column_name_for(field)]
|
|
148
190
|
end
|
|
149
|
-
opts.merge!(association: association,
|
|
150
|
-
|
|
191
|
+
opts.merge!(association: association,
|
|
192
|
+
associated: associated,
|
|
193
|
+
group_aliases: group_aliases,
|
|
194
|
+
group_columns: group_columns,
|
|
151
195
|
group_fields: group_fields)
|
|
152
196
|
|
|
153
197
|
opts
|
|
154
198
|
end
|
|
155
199
|
|
|
156
|
-
def aggregate_alias_for(operation, column_name)
|
|
157
|
-
if operation ==
|
|
158
|
-
|
|
159
|
-
elsif operation ==
|
|
160
|
-
|
|
200
|
+
def aggregate_alias_for(operation, column_name, column_alias_tracker)
|
|
201
|
+
if operation == "count" && column_name == :all
|
|
202
|
+
"count_all"
|
|
203
|
+
elsif operation == "average"
|
|
204
|
+
"average"
|
|
205
|
+
elsif column_alias_tracker
|
|
206
|
+
column_alias_tracker.alias_for("#{operation} #{column_name}")
|
|
161
207
|
else
|
|
162
208
|
column_alias_for("#{operation} #{column_name}")
|
|
163
209
|
end
|
|
@@ -173,13 +219,14 @@ module Switchman
|
|
|
173
219
|
opts[:distinct]
|
|
174
220
|
).as(opts[:aggregate_alias])
|
|
175
221
|
]
|
|
176
|
-
if opts[:operation] ==
|
|
222
|
+
if opts[:operation] == "average"
|
|
177
223
|
# include count in average so we can recalculate the average
|
|
178
224
|
# across all shards if needed
|
|
179
225
|
select_values << operation_over_aggregate_column(
|
|
180
226
|
aggregate_column(opts[:column_name]),
|
|
181
|
-
|
|
182
|
-
|
|
227
|
+
"count",
|
|
228
|
+
opts[:distinct]
|
|
229
|
+
).as("count")
|
|
183
230
|
end
|
|
184
231
|
|
|
185
232
|
haves = having_clause.send(:predicates)
|
|
@@ -205,22 +252,22 @@ module Switchman
|
|
|
205
252
|
key = key.first if key.size == 1
|
|
206
253
|
value = row[opts[:aggregate_alias]]
|
|
207
254
|
|
|
208
|
-
if opts[:operation] ==
|
|
255
|
+
if opts[:operation] == "average"
|
|
209
256
|
if result.key?(key)
|
|
210
257
|
old_value, old_count = result[key]
|
|
211
|
-
new_count = old_count + row[
|
|
212
|
-
new_value = ((old_value * old_count) + (value * row[
|
|
258
|
+
new_count = old_count + row["count"]
|
|
259
|
+
new_value = ((old_value * old_count) + (value * row["count"])) / new_count
|
|
213
260
|
result[key] = [new_value, new_count]
|
|
214
261
|
else
|
|
215
|
-
result[key] = [value, row[
|
|
262
|
+
result[key] = [value, row["count"]]
|
|
216
263
|
end
|
|
217
264
|
elsif result.key?(key)
|
|
218
265
|
case opts[:operation]
|
|
219
|
-
when
|
|
266
|
+
when "count", "sum"
|
|
220
267
|
result[key] += value
|
|
221
|
-
when
|
|
268
|
+
when "minimum"
|
|
222
269
|
result[key] = value if value < result[key]
|
|
223
|
-
when
|
|
270
|
+
when "maximum"
|
|
224
271
|
result[key] = value if value > result[key]
|
|
225
272
|
end
|
|
226
273
|
else
|
|
@@ -228,7 +275,7 @@ module Switchman
|
|
|
228
275
|
end
|
|
229
276
|
end
|
|
230
277
|
|
|
231
|
-
result.transform_values!(&:first) if opts[:operation] ==
|
|
278
|
+
result.transform_values!(&:first) if opts[:operation] == "average"
|
|
232
279
|
|
|
233
280
|
result
|
|
234
281
|
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
|
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
module Switchman
|
|
4
4
|
module ActiveRecord
|
|
5
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
|
+
|
|
6
25
|
def default_schema
|
|
7
26
|
connection unless @schemas
|
|
8
27
|
# default shard will not switch databases immediately, so it won't be set yet
|
|
@@ -31,7 +50,9 @@ module Switchman
|
|
|
31
50
|
end
|
|
32
51
|
|
|
33
52
|
def switch_database(conn)
|
|
34
|
-
|
|
53
|
+
if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
|
|
54
|
+
@schemas = conn.current_schemas
|
|
55
|
+
end
|
|
35
56
|
|
|
36
57
|
conn.shard = current_shard
|
|
37
58
|
end
|
|
@@ -39,11 +60,11 @@ module Switchman
|
|
|
39
60
|
private
|
|
40
61
|
|
|
41
62
|
def current_shard
|
|
42
|
-
|
|
63
|
+
connection_class.current_switchman_shard
|
|
43
64
|
end
|
|
44
65
|
|
|
45
66
|
def tls_key
|
|
46
|
-
"#{object_id}_shard"
|
|
67
|
+
:"#{object_id}_shard"
|
|
47
68
|
end
|
|
48
69
|
end
|
|
49
70
|
end
|