switchman 3.0.5 → 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 +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 +6 -15
- data/lib/switchman/active_record/associations.rb +331 -0
- data/lib/switchman/active_record/attribute_methods.rb +182 -77
- data/lib/switchman/active_record/base.rb +249 -46
- data/lib/switchman/active_record/calculations.rb +98 -44
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +27 -28
- data/lib/switchman/active_record/database_configurations.rb +44 -6
- 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 -5
- 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 +12 -11
- 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 +202 -136
- data/lib/switchman/active_record/reflection.rb +1 -1
- data/lib/switchman/active_record/relation.rb +40 -28
- 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 +53 -0
- data/lib/switchman/active_support/cache.rb +25 -4
- data/lib/switchman/arel.rb +45 -7
- data/lib/switchman/call_super.rb +2 -2
- data/lib/switchman/database_server.rb +123 -79
- data/lib/switchman/default_shard.rb +14 -5
- data/lib/switchman/engine.rb +79 -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 +226 -241
- data/lib/switchman/sharded_instrumenter.rb +3 -3
- data/lib/switchman/shared_schema_cache.rb +11 -0
- data/lib/switchman/standard_error.rb +15 -12
- 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 +101 -54
- metadata +50 -58
- 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
|
|
|
@@ -27,21 +25,63 @@ 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
|
-
|
|
32
|
-
|
|
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|
|
|
39
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
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
|
|
66
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
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
|
|
78
|
+
end
|
|
33
79
|
else
|
|
80
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
34
81
|
db.unguard { super }
|
|
35
82
|
end
|
|
36
83
|
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
|
|
84
|
+
RUBY
|
|
45
85
|
end
|
|
46
86
|
|
|
47
87
|
def reset_column_information
|
|
@@ -63,60 +103,205 @@ module Switchman
|
|
|
63
103
|
end
|
|
64
104
|
|
|
65
105
|
def clear_query_caches_for_current_thread
|
|
66
|
-
::
|
|
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|
|
|
67
112
|
pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
|
|
68
113
|
end
|
|
69
114
|
end
|
|
70
115
|
|
|
116
|
+
def role_overriden?(shard_id)
|
|
117
|
+
current_role(target_shard: shard_id) != current_role(without_overrides: true)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def establish_connection(config_or_env = nil)
|
|
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
|
|
125
|
+
|
|
126
|
+
# Ensure we don't randomly surprise change the connection parms associated with a shard/role
|
|
127
|
+
config_or_env = nil if config_or_env == ::Rails.env.to_sym
|
|
128
|
+
|
|
129
|
+
config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
|
|
130
|
+
:primary
|
|
131
|
+
else
|
|
132
|
+
:"#{current_shard}/#{current_role}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
super(config_or_env)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def connected_to_stack
|
|
139
|
+
has_own_stack = ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
|
|
140
|
+
|
|
141
|
+
ret = super
|
|
142
|
+
return ret if has_own_stack
|
|
143
|
+
|
|
144
|
+
DatabaseServer.guard_servers
|
|
145
|
+
ret
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# significant change: Allow per-shard roles
|
|
149
|
+
def current_role(without_overrides: false, target_shard: current_shard)
|
|
150
|
+
return super() if without_overrides
|
|
151
|
+
|
|
152
|
+
sharded_role = nil
|
|
153
|
+
connected_to_stack.reverse_each do |hash|
|
|
154
|
+
shard_role = hash.dig(:shard_roles, target_shard)
|
|
155
|
+
unless shard_role &&
|
|
156
|
+
(hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
|
|
157
|
+
next
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
sharded_role = shard_role
|
|
161
|
+
break
|
|
162
|
+
end
|
|
163
|
+
# Allow a shard-specific role to be reverted to regular inheritance
|
|
164
|
+
return sharded_role if sharded_role && sharded_role != :_switchman_inherit
|
|
165
|
+
|
|
166
|
+
super()
|
|
167
|
+
end
|
|
168
|
+
|
|
71
169
|
# significant change: _don't_ check if klasses.include?(Base)
|
|
72
170
|
# i.e. other sharded models don't inherit the current shard of Base
|
|
73
171
|
def current_shard
|
|
74
172
|
connected_to_stack.reverse_each do |hash|
|
|
75
|
-
return hash[:shard] if hash[:shard] && hash[:klasses].include?(
|
|
173
|
+
return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_class_for_self)
|
|
76
174
|
end
|
|
77
175
|
|
|
78
176
|
default_shard
|
|
79
177
|
end
|
|
178
|
+
|
|
179
|
+
def current_switchman_shard
|
|
180
|
+
connected_to_stack.reverse_each do |hash|
|
|
181
|
+
if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
|
|
182
|
+
return hash[:switchman_shard]
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
Shard.default
|
|
187
|
+
end
|
|
80
188
|
end
|
|
81
189
|
|
|
82
|
-
def self.
|
|
190
|
+
def self.prepended(klass)
|
|
83
191
|
klass.singleton_class.prepend(ClassMethods)
|
|
84
|
-
klass.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def _run_initialize_callbacks
|
|
202
|
+
@shard ||= if self.class.sharded_primary_key?
|
|
203
|
+
Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_class_for_self))
|
|
204
|
+
else
|
|
205
|
+
Shard.current(self.class.connection_class_for_self)
|
|
206
|
+
end
|
|
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
|
|
218
|
+
super
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def shadow_record?
|
|
222
|
+
pkey = self[self.class.primary_key]
|
|
223
|
+
return false unless self.class.sharded_column?(self.class.primary_key) && pkey
|
|
224
|
+
|
|
225
|
+
pkey > Shard::IDS_PER_SHARD
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def canonical?
|
|
229
|
+
!shadow_record?
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
|
|
233
|
+
return if target_shard == shard
|
|
234
|
+
|
|
235
|
+
shadow_attrs = {}
|
|
236
|
+
new_attrs.each do |attr, value|
|
|
237
|
+
shadow_attrs[attr] = if self.class.sharded_column?(attr)
|
|
238
|
+
Shard.relative_id_for(value, shard, target_shard)
|
|
239
|
+
else
|
|
240
|
+
value
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
target_shard.activate do
|
|
244
|
+
self.class.upsert(shadow_attrs, unique_by: self.class.primary_key)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
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 }
|
|
90
260
|
end
|
|
91
261
|
end
|
|
92
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
|
+
|
|
93
270
|
def shard
|
|
94
|
-
@shard ||
|
|
271
|
+
@shard || fallback_shard
|
|
95
272
|
end
|
|
96
273
|
|
|
97
274
|
def shard=(new_shard)
|
|
98
275
|
raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
|
|
99
276
|
|
|
100
|
-
|
|
277
|
+
if shard == new_shard
|
|
278
|
+
@loaded_from_shard = new_shard
|
|
279
|
+
return
|
|
280
|
+
end
|
|
101
281
|
|
|
102
282
|
attributes.each do |attr, value|
|
|
103
283
|
self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
|
|
104
284
|
end
|
|
285
|
+
@loaded_from_shard = new_shard
|
|
105
286
|
@shard = new_shard
|
|
106
287
|
end
|
|
107
288
|
|
|
108
289
|
def save(*, **)
|
|
290
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
291
|
+
|
|
109
292
|
@shard_set_in_stone = true
|
|
110
|
-
|
|
293
|
+
super
|
|
111
294
|
end
|
|
112
295
|
|
|
113
296
|
def save!(*, **)
|
|
297
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
298
|
+
|
|
114
299
|
@shard_set_in_stone = true
|
|
115
|
-
|
|
300
|
+
super
|
|
116
301
|
end
|
|
117
302
|
|
|
118
303
|
def destroy
|
|
119
|
-
shard.activate(self.class.
|
|
304
|
+
shard.activate(self.class.connection_class_for_self) { super }
|
|
120
305
|
end
|
|
121
306
|
|
|
122
307
|
def clone
|
|
@@ -128,14 +313,21 @@ module Switchman
|
|
|
128
313
|
result
|
|
129
314
|
end
|
|
130
315
|
|
|
131
|
-
def transaction(
|
|
132
|
-
shard.activate(self.class.
|
|
133
|
-
self.class.transaction(
|
|
316
|
+
def transaction(...)
|
|
317
|
+
shard.activate(self.class.connection_class_for_self) do
|
|
318
|
+
self.class.transaction(...)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def with_transaction_returning_status
|
|
323
|
+
shard.activate(self.class.connection_class_for_self) do
|
|
324
|
+
db = Shard.current(self.class.connection_class_for_self).database_server
|
|
325
|
+
db.unguard { super }
|
|
134
326
|
end
|
|
135
327
|
end
|
|
136
328
|
|
|
137
329
|
def hash
|
|
138
|
-
self.class.sharded_primary_key? ? self.class
|
|
330
|
+
self.class.sharded_primary_key? ? [self.class, global_id].hash : super
|
|
139
331
|
end
|
|
140
332
|
|
|
141
333
|
def to_param
|
|
@@ -149,42 +341,53 @@ module Switchman
|
|
|
149
341
|
copy
|
|
150
342
|
end
|
|
151
343
|
|
|
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)
|
|
344
|
+
def update_columns(*)
|
|
345
|
+
db = shard.database_server
|
|
346
|
+
db.unguard { super }
|
|
157
347
|
end
|
|
158
348
|
|
|
159
|
-
def
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
349
|
+
def id_for_database
|
|
350
|
+
if self.class.sharded_primary_key?
|
|
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
|
|
355
|
+
id
|
|
163
356
|
else
|
|
164
|
-
|
|
357
|
+
super
|
|
165
358
|
end
|
|
166
359
|
end
|
|
167
360
|
|
|
168
361
|
protected
|
|
169
362
|
|
|
170
|
-
# see also AttributeMethods#
|
|
171
|
-
def
|
|
363
|
+
# see also AttributeMethods#connection_class_for_self_code_for_reflection
|
|
364
|
+
def connection_class_for_self_for_reflection(reflection)
|
|
172
365
|
if reflection
|
|
173
366
|
if reflection.options[:polymorphic]
|
|
174
367
|
begin
|
|
175
|
-
read_attribute(reflection.foreign_type)&.constantize&.
|
|
368
|
+
read_attribute(reflection.foreign_type)&.constantize&.connection_class_for_self || ::ActiveRecord::Base
|
|
176
369
|
rescue NameError
|
|
177
370
|
# in case someone is abusing foreign_type to not point to an actual class
|
|
178
371
|
::ActiveRecord::Base
|
|
179
372
|
end
|
|
180
373
|
else
|
|
181
374
|
# otherwise we can just return a symbol for the statically known type of the association
|
|
182
|
-
reflection.klass.
|
|
375
|
+
reflection.klass.connection_class_for_self
|
|
183
376
|
end
|
|
184
377
|
else
|
|
185
|
-
|
|
378
|
+
self.class.connection_class_for_self
|
|
186
379
|
end
|
|
187
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
|
|
188
391
|
end
|
|
189
392
|
end
|
|
190
393
|
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
|
|
@@ -83,14 +83,14 @@ 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)
|
|
@@ -106,51 +106,104 @@ module Switchman
|
|
|
106
106
|
compact_grouped_calculation_rows(rows, opts)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
if ::Rails.version >= "7.1"
|
|
110
|
+
def ids
|
|
111
|
+
return super unless klass.sharded_primary_key?
|
|
112
|
+
|
|
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
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
109
148
|
private
|
|
110
149
|
|
|
111
150
|
def type_cast_calculated_value_switchman(value, column_name, operation)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
type.deserialize(val)
|
|
117
|
-
end
|
|
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)
|
|
118
155
|
end
|
|
119
156
|
|
|
120
157
|
def column_name_for(field)
|
|
121
|
-
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
|
|
122
159
|
end
|
|
123
160
|
|
|
124
161
|
def grouped_calculation_options(operation, column_name, distinct)
|
|
125
162
|
opts = { operation: operation, column_name: column_name, distinct: distinct }
|
|
126
163
|
|
|
127
|
-
|
|
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)
|
|
128
170
|
group_attrs = group_values
|
|
129
171
|
if group_attrs.first.respond_to?(:to_sym)
|
|
130
172
|
association = klass.reflect_on_association(group_attrs.first.to_sym)
|
|
131
|
-
|
|
173
|
+
# only count belongs_to associations
|
|
174
|
+
associated = group_attrs.size == 1 && association && association.macro == :belongs_to
|
|
132
175
|
group_fields = Array(associated ? association.foreign_key : group_attrs)
|
|
133
176
|
else
|
|
134
177
|
group_fields = group_attrs
|
|
135
178
|
end
|
|
136
179
|
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
139
188
|
group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
|
|
140
189
|
[aliaz, type_for(field), column_name_for(field)]
|
|
141
190
|
end
|
|
142
|
-
opts.merge!(association: association,
|
|
143
|
-
|
|
191
|
+
opts.merge!(association: association,
|
|
192
|
+
associated: associated,
|
|
193
|
+
group_aliases: group_aliases,
|
|
194
|
+
group_columns: group_columns,
|
|
144
195
|
group_fields: group_fields)
|
|
145
196
|
|
|
146
197
|
opts
|
|
147
198
|
end
|
|
148
199
|
|
|
149
|
-
def aggregate_alias_for(operation, column_name)
|
|
150
|
-
if operation ==
|
|
151
|
-
|
|
152
|
-
elsif operation ==
|
|
153
|
-
|
|
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}")
|
|
154
207
|
else
|
|
155
208
|
column_alias_for("#{operation} #{column_name}")
|
|
156
209
|
end
|
|
@@ -166,13 +219,14 @@ module Switchman
|
|
|
166
219
|
opts[:distinct]
|
|
167
220
|
).as(opts[:aggregate_alias])
|
|
168
221
|
]
|
|
169
|
-
if opts[:operation] ==
|
|
222
|
+
if opts[:operation] == "average"
|
|
170
223
|
# include count in average so we can recalculate the average
|
|
171
224
|
# across all shards if needed
|
|
172
225
|
select_values << operation_over_aggregate_column(
|
|
173
226
|
aggregate_column(opts[:column_name]),
|
|
174
|
-
|
|
175
|
-
|
|
227
|
+
"count",
|
|
228
|
+
opts[:distinct]
|
|
229
|
+
).as("count")
|
|
176
230
|
end
|
|
177
231
|
|
|
178
232
|
haves = having_clause.send(:predicates)
|
|
@@ -198,22 +252,22 @@ module Switchman
|
|
|
198
252
|
key = key.first if key.size == 1
|
|
199
253
|
value = row[opts[:aggregate_alias]]
|
|
200
254
|
|
|
201
|
-
if opts[:operation] ==
|
|
255
|
+
if opts[:operation] == "average"
|
|
202
256
|
if result.key?(key)
|
|
203
257
|
old_value, old_count = result[key]
|
|
204
|
-
new_count = old_count + row[
|
|
205
|
-
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
|
|
206
260
|
result[key] = [new_value, new_count]
|
|
207
261
|
else
|
|
208
|
-
result[key] = [value, row[
|
|
262
|
+
result[key] = [value, row["count"]]
|
|
209
263
|
end
|
|
210
264
|
elsif result.key?(key)
|
|
211
265
|
case opts[:operation]
|
|
212
|
-
when
|
|
266
|
+
when "count", "sum"
|
|
213
267
|
result[key] += value
|
|
214
|
-
when
|
|
268
|
+
when "minimum"
|
|
215
269
|
result[key] = value if value < result[key]
|
|
216
|
-
when
|
|
270
|
+
when "maximum"
|
|
217
271
|
result[key] = value if value > result[key]
|
|
218
272
|
end
|
|
219
273
|
else
|
|
@@ -221,7 +275,7 @@ module Switchman
|
|
|
221
275
|
end
|
|
222
276
|
end
|
|
223
277
|
|
|
224
|
-
result.transform_values!(&:first) if opts[:operation] ==
|
|
278
|
+
result.transform_values!(&:first) if opts[:operation] == "average"
|
|
225
279
|
|
|
226
280
|
result
|
|
227
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
|