switchman 3.0.1 → 4.2.5
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 +2 -2
- data/db/migrate/20180828192111_add_timestamps_to_shards.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 +11 -18
- data/lib/switchman/active_record/associations.rb +315 -0
- data/lib/switchman/active_record/attribute_methods.rb +191 -79
- data/lib/switchman/active_record/base.rb +204 -50
- data/lib/switchman/active_record/calculations.rb +93 -50
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +47 -34
- data/lib/switchman/active_record/database_configurations.rb +32 -6
- 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 -14
- 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 +37 -2
- data/lib/switchman/active_record/postgresql_adapter.rb +39 -20
- 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 +252 -135
- data/lib/switchman/active_record/reflection.rb +10 -3
- data/lib/switchman/active_record/relation.rb +154 -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 +89 -0
- data/lib/switchman/active_support/cache.rb +25 -4
- data/lib/switchman/arel.rb +20 -7
- data/lib/switchman/call_super.rb +2 -2
- data/lib/switchman/database_server.rb +123 -83
- data/lib/switchman/default_shard.rb +14 -5
- data/lib/switchman/engine.rb +85 -131
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +17 -2
- data/lib/switchman/guard_rail/relation.rb +7 -10
- data/lib/switchman/guard_rail.rb +5 -0
- data/lib/switchman/parallel.rb +68 -0
- data/lib/switchman/r_spec_helper.rb +17 -28
- data/lib/switchman/rails.rb +1 -4
- data/{app/models → lib}/switchman/shard.rb +229 -246
- data/lib/switchman/sharded_instrumenter.rb +9 -3
- data/lib/switchman/shared_schema_cache.rb +11 -0
- data/lib/switchman/standard_error.rb +15 -12
- data/lib/switchman/test_helper.rb +3 -3
- data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +46 -12
- data/lib/tasks/switchman.rake +101 -54
- metadata +34 -176
- data/lib/switchman/active_record/association.rb +0 -206
- data/lib/switchman/open4.rb +0 -80
|
@@ -6,16 +6,14 @@ 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
|
|
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
|
|
|
@@ -27,20 +25,12 @@ module Switchman
|
|
|
27
25
|
def transaction(**)
|
|
28
26
|
if self != ::ActiveRecord::Base && current_scope
|
|
29
27
|
current_scope.activate do
|
|
30
|
-
db = Shard.current(
|
|
31
|
-
if ::GuardRail.environment == db.guard_rail_environment
|
|
32
|
-
super
|
|
33
|
-
else
|
|
34
|
-
db.unguard { super }
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
else
|
|
38
|
-
db = Shard.current(connection_classes).database_server
|
|
39
|
-
if ::GuardRail.environment == db.guard_rail_environment
|
|
40
|
-
super
|
|
41
|
-
else
|
|
28
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
42
29
|
db.unguard { super }
|
|
43
30
|
end
|
|
31
|
+
else
|
|
32
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
33
|
+
db.unguard { super }
|
|
44
34
|
end
|
|
45
35
|
end
|
|
46
36
|
|
|
@@ -63,60 +53,206 @@ module Switchman
|
|
|
63
53
|
end
|
|
64
54
|
|
|
65
55
|
def clear_query_caches_for_current_thread
|
|
66
|
-
::ActiveRecord::Base.connection_handler.connection_pool_list
|
|
67
|
-
|
|
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
|
|
68
63
|
end
|
|
69
64
|
end
|
|
70
65
|
|
|
66
|
+
def role_overriden?(shard_id)
|
|
67
|
+
current_role(target_shard: shard_id) != current_role(without_overrides: true)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def establish_connection(config_or_env = nil)
|
|
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
|
|
75
|
+
|
|
76
|
+
# Ensure we don't randomly surprise change the connection parms associated with a shard/role
|
|
77
|
+
config_or_env = nil if config_or_env == ::Rails.env.to_sym
|
|
78
|
+
|
|
79
|
+
config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
|
|
80
|
+
:primary
|
|
81
|
+
else
|
|
82
|
+
:"#{current_shard}/#{current_role}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
super
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def connected_to_stack
|
|
89
|
+
has_own_stack = ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
|
|
90
|
+
|
|
91
|
+
ret = super
|
|
92
|
+
return ret if has_own_stack
|
|
93
|
+
|
|
94
|
+
DatabaseServer.guard_servers
|
|
95
|
+
ret
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# significant change: Allow per-shard roles
|
|
99
|
+
def current_role(without_overrides: false, target_shard: current_shard)
|
|
100
|
+
return super() if without_overrides
|
|
101
|
+
|
|
102
|
+
sharded_role = nil
|
|
103
|
+
connected_to_stack.reverse_each do |hash|
|
|
104
|
+
shard_role = hash.dig(:shard_roles, target_shard)
|
|
105
|
+
unless shard_role &&
|
|
106
|
+
(hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
|
|
107
|
+
next
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
sharded_role = shard_role
|
|
111
|
+
break
|
|
112
|
+
end
|
|
113
|
+
# Allow a shard-specific role to be reverted to regular inheritance
|
|
114
|
+
return sharded_role if sharded_role && sharded_role != :_switchman_inherit
|
|
115
|
+
|
|
116
|
+
super()
|
|
117
|
+
end
|
|
118
|
+
|
|
71
119
|
# significant change: _don't_ check if klasses.include?(Base)
|
|
72
120
|
# i.e. other sharded models don't inherit the current shard of Base
|
|
73
121
|
def current_shard
|
|
74
122
|
connected_to_stack.reverse_each do |hash|
|
|
75
|
-
return hash[:shard] if hash[:shard] && hash[:klasses].include?(
|
|
123
|
+
return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_class_for_self)
|
|
76
124
|
end
|
|
77
125
|
|
|
78
126
|
default_shard
|
|
79
127
|
end
|
|
128
|
+
|
|
129
|
+
def current_switchman_shard
|
|
130
|
+
connected_to_stack.reverse_each do |hash|
|
|
131
|
+
if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
|
|
132
|
+
return hash[:switchman_shard]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
Shard.default
|
|
137
|
+
end
|
|
80
138
|
end
|
|
81
139
|
|
|
82
|
-
def self.
|
|
140
|
+
def self.prepended(klass)
|
|
83
141
|
klass.singleton_class.prepend(ClassMethods)
|
|
84
|
-
klass.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def _run_initialize_callbacks
|
|
153
|
+
@shard ||= if self.class.sharded_primary_key?
|
|
154
|
+
Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_class_for_self))
|
|
155
|
+
else
|
|
156
|
+
Shard.current(self.class.connection_class_for_self)
|
|
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
|
|
164
|
+
super
|
|
165
|
+
end
|
|
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)
|
|
90
196
|
end
|
|
91
197
|
end
|
|
92
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
|
+
|
|
93
221
|
def shard
|
|
94
|
-
@shard ||
|
|
222
|
+
@shard || fallback_shard
|
|
95
223
|
end
|
|
96
224
|
|
|
97
225
|
def shard=(new_shard)
|
|
98
226
|
raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
|
|
99
227
|
|
|
100
|
-
|
|
228
|
+
if shard == new_shard
|
|
229
|
+
@loaded_from_shard = new_shard
|
|
230
|
+
return
|
|
231
|
+
end
|
|
101
232
|
|
|
102
233
|
attributes.each do |attr, value|
|
|
103
234
|
self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
|
|
104
235
|
end
|
|
236
|
+
@loaded_from_shard = new_shard
|
|
105
237
|
@shard = new_shard
|
|
106
238
|
end
|
|
107
239
|
|
|
108
240
|
def save(*, **)
|
|
241
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
242
|
+
|
|
109
243
|
@shard_set_in_stone = true
|
|
110
|
-
|
|
244
|
+
super
|
|
111
245
|
end
|
|
112
246
|
|
|
113
247
|
def save!(*, **)
|
|
248
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
249
|
+
|
|
114
250
|
@shard_set_in_stone = true
|
|
115
|
-
|
|
251
|
+
super
|
|
116
252
|
end
|
|
117
253
|
|
|
118
254
|
def destroy
|
|
119
|
-
shard.activate(self.class.
|
|
255
|
+
shard.activate(self.class.connection_class_for_self) { super }
|
|
120
256
|
end
|
|
121
257
|
|
|
122
258
|
def clone
|
|
@@ -128,14 +264,21 @@ module Switchman
|
|
|
128
264
|
result
|
|
129
265
|
end
|
|
130
266
|
|
|
131
|
-
def transaction(
|
|
132
|
-
shard.activate(self.class.
|
|
133
|
-
self.class.transaction(
|
|
267
|
+
def transaction(...)
|
|
268
|
+
shard.activate(self.class.connection_class_for_self) do
|
|
269
|
+
self.class.transaction(...)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def with_transaction_returning_status
|
|
274
|
+
shard.activate(self.class.connection_class_for_self) do
|
|
275
|
+
db = Shard.current(self.class.connection_class_for_self).database_server
|
|
276
|
+
db.unguard { super }
|
|
134
277
|
end
|
|
135
278
|
end
|
|
136
279
|
|
|
137
280
|
def hash
|
|
138
|
-
self.class.sharded_primary_key? ? self.class
|
|
281
|
+
self.class.sharded_primary_key? ? [self.class, global_id].hash : super
|
|
139
282
|
end
|
|
140
283
|
|
|
141
284
|
def to_param
|
|
@@ -149,42 +292,53 @@ module Switchman
|
|
|
149
292
|
copy
|
|
150
293
|
end
|
|
151
294
|
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
# do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
|
|
156
|
-
self.class.connection.quote(id)
|
|
295
|
+
def update_columns(*)
|
|
296
|
+
db = shard.database_server
|
|
297
|
+
db.unguard { super }
|
|
157
298
|
end
|
|
158
299
|
|
|
159
|
-
def
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
300
|
+
def id_for_database
|
|
301
|
+
if self.class.sharded_primary_key?
|
|
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
|
|
306
|
+
id
|
|
163
307
|
else
|
|
164
|
-
|
|
308
|
+
super
|
|
165
309
|
end
|
|
166
310
|
end
|
|
167
311
|
|
|
168
312
|
protected
|
|
169
313
|
|
|
170
|
-
# see also AttributeMethods#
|
|
171
|
-
def
|
|
314
|
+
# see also AttributeMethods#connection_class_for_self_code_for_reflection
|
|
315
|
+
def connection_class_for_self_for_reflection(reflection)
|
|
172
316
|
if reflection
|
|
173
317
|
if reflection.options[:polymorphic]
|
|
174
318
|
begin
|
|
175
|
-
read_attribute(reflection.foreign_type)&.constantize&.
|
|
319
|
+
read_attribute(reflection.foreign_type)&.constantize&.connection_class_for_self || ::ActiveRecord::Base
|
|
176
320
|
rescue NameError
|
|
177
321
|
# in case someone is abusing foreign_type to not point to an actual class
|
|
178
322
|
::ActiveRecord::Base
|
|
179
323
|
end
|
|
180
324
|
else
|
|
181
325
|
# otherwise we can just return a symbol for the statically known type of the association
|
|
182
|
-
reflection.klass.
|
|
326
|
+
reflection.klass.connection_class_for_self
|
|
183
327
|
end
|
|
184
328
|
else
|
|
185
|
-
|
|
329
|
+
self.class.connection_class_for_self
|
|
186
330
|
end
|
|
187
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
|
|
188
342
|
end
|
|
189
343
|
end
|
|
190
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
|
|
@@ -50,22 +50,22 @@ module Switchman
|
|
|
50
50
|
|
|
51
51
|
def calculate_simple_average(column_name, distinct)
|
|
52
52
|
# See activerecord#execute_simple_calculation
|
|
53
|
-
relation =
|
|
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
|
|
@@ -83,19 +83,19 @@ module Switchman
|
|
|
83
83
|
if opts[:association]
|
|
84
84
|
key_ids = calculated_data.collect { |row| row[opts[:group_aliases].first] }
|
|
85
85
|
key_records = opts[:association].klass.base_class.where(id: key_ids)
|
|
86
|
-
key_records = key_records.
|
|
86
|
+
key_records = key_records.to_h { |r| [Shard.relative_id_for(r, shard, target_shard), r] }
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
calculated_data.map do |row|
|
|
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
|