switchman 3.3.6 → 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 +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 +10 -6
- data/lib/switchman/active_record/associations.rb +72 -49
- data/lib/switchman/active_record/attribute_methods.rb +89 -44
- data/lib/switchman/active_record/base.rb +109 -40
- data/lib/switchman/active_record/calculations.rb +90 -54
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +41 -23
- data/lib/switchman/active_record/database_configurations.rb +23 -13
- data/lib/switchman/active_record/finder_methods.rb +20 -14
- data/lib/switchman/active_record/log_subscriber.rb +3 -6
- data/lib/switchman/active_record/migration.rb +35 -12
- 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 +37 -22
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +26 -17
- data/lib/switchman/active_record/query_methods.rb +148 -44
- data/lib/switchman/active_record/reflection.rb +9 -2
- data/lib/switchman/active_record/relation.rb +87 -17
- data/lib/switchman/active_record/spawn_methods.rb +3 -7
- data/lib/switchman/active_record/statement_cache.rb +4 -4
- data/lib/switchman/active_record/table_definition.rb +1 -1
- data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
- data/lib/switchman/active_record/test_fixtures.rb +71 -25
- data/lib/switchman/active_support/cache.rb +9 -4
- data/lib/switchman/arel.rb +16 -25
- data/lib/switchman/call_super.rb +2 -8
- data/lib/switchman/database_server.rb +67 -48
- data/lib/switchman/default_shard.rb +14 -3
- data/lib/switchman/engine.rb +35 -23
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +13 -0
- data/lib/switchman/guard_rail/relation.rb +1 -2
- data/lib/switchman/parallel.rb +6 -6
- data/lib/switchman/r_spec_helper.rb +12 -11
- data/lib/switchman/shard.rb +168 -68
- data/lib/switchman/sharded_instrumenter.rb +9 -3
- data/lib/switchman/standard_error.rb +4 -0
- data/lib/switchman/test_helper.rb +3 -3
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +27 -15
- data/lib/tasks/switchman.rake +96 -60
- metadata +28 -173
|
@@ -6,9 +6,9 @@ module Switchman
|
|
|
6
6
|
module ClassMethods
|
|
7
7
|
delegate :shard, to: :all
|
|
8
8
|
|
|
9
|
-
def find_ids_in_ranges(opts = {}, &
|
|
9
|
+
def find_ids_in_ranges(opts = {}, &)
|
|
10
10
|
opts.reverse_merge!(loose: true)
|
|
11
|
-
all.find_ids_in_ranges(opts, &
|
|
11
|
+
all.find_ids_in_ranges(opts, &)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def sharded_model
|
|
@@ -22,20 +22,16 @@ module Switchman
|
|
|
22
22
|
@integral_id
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
db = Shard.current(connection_class_for_self).database_server
|
|
31
|
-
db.unguard { super }
|
|
32
|
-
end
|
|
33
|
-
else
|
|
34
|
-
db = Shard.current(connection_class_for_self).database_server
|
|
35
|
-
db.unguard { super }
|
|
36
|
-
end
|
|
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 }
|
|
37
30
|
end
|
|
38
|
-
|
|
31
|
+
else
|
|
32
|
+
db = Shard.current(connection_class_for_self).database_server
|
|
33
|
+
db.unguard { super }
|
|
34
|
+
end
|
|
39
35
|
end
|
|
40
36
|
|
|
41
37
|
def reset_column_information
|
|
@@ -57,8 +53,13 @@ module Switchman
|
|
|
57
53
|
end
|
|
58
54
|
|
|
59
55
|
def clear_query_caches_for_current_thread
|
|
60
|
-
::ActiveRecord::Base.connection_handler.connection_pool_list
|
|
61
|
-
|
|
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
|
|
62
63
|
end
|
|
63
64
|
end
|
|
64
65
|
|
|
@@ -67,7 +68,10 @@ module Switchman
|
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def establish_connection(config_or_env = nil)
|
|
70
|
-
|
|
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
|
|
71
75
|
|
|
72
76
|
# Ensure we don't randomly surprise change the connection parms associated with a shard/role
|
|
73
77
|
config_or_env = nil if config_or_env == ::Rails.env.to_sym
|
|
@@ -75,16 +79,18 @@ module Switchman
|
|
|
75
79
|
config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
|
|
76
80
|
:primary
|
|
77
81
|
else
|
|
78
|
-
"#{current_shard}/#{current_role}"
|
|
82
|
+
:"#{current_shard}/#{current_role}"
|
|
79
83
|
end
|
|
80
84
|
|
|
81
|
-
super
|
|
85
|
+
super
|
|
82
86
|
end
|
|
83
87
|
|
|
84
88
|
def connected_to_stack
|
|
85
|
-
|
|
89
|
+
has_own_stack = ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
|
|
86
90
|
|
|
87
91
|
ret = super
|
|
92
|
+
return ret if has_own_stack
|
|
93
|
+
|
|
88
94
|
DatabaseServer.guard_servers
|
|
89
95
|
ret
|
|
90
96
|
end
|
|
@@ -96,10 +102,13 @@ module Switchman
|
|
|
96
102
|
sharded_role = nil
|
|
97
103
|
connected_to_stack.reverse_each do |hash|
|
|
98
104
|
shard_role = hash.dig(:shard_roles, target_shard)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
unless shard_role &&
|
|
106
|
+
(hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
|
|
107
|
+
next
|
|
102
108
|
end
|
|
109
|
+
|
|
110
|
+
sharded_role = shard_role
|
|
111
|
+
break
|
|
103
112
|
end
|
|
104
113
|
# Allow a shard-specific role to be reverted to regular inheritance
|
|
105
114
|
return sharded_role if sharded_role && sharded_role != :_switchman_inherit
|
|
@@ -119,21 +128,25 @@ module Switchman
|
|
|
119
128
|
|
|
120
129
|
def current_switchman_shard
|
|
121
130
|
connected_to_stack.reverse_each do |hash|
|
|
122
|
-
|
|
131
|
+
if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
|
|
132
|
+
return hash[:switchman_shard]
|
|
133
|
+
end
|
|
123
134
|
end
|
|
124
135
|
|
|
125
136
|
Shard.default
|
|
126
137
|
end
|
|
127
|
-
|
|
128
|
-
if ::Rails.version < '7.0'
|
|
129
|
-
def connection_class_for_self
|
|
130
|
-
connection_classes
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
138
|
end
|
|
134
139
|
|
|
135
140
|
def self.prepended(klass)
|
|
136
141
|
klass.singleton_class.prepend(ClassMethods)
|
|
142
|
+
klass.singleton_class.prepend(Switchman::ActiveRecord::Relation::InsertUpsertAll) if ::Rails.version < "7.2"
|
|
143
|
+
klass.scope :non_shadow, lambda { |key = primary_key|
|
|
144
|
+
where(key => (QueryMethods::NonTransposingValue.new(0)...
|
|
145
|
+
QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)))
|
|
146
|
+
}
|
|
147
|
+
klass.scope :shadow, lambda { |key = primary_key|
|
|
148
|
+
where(key => QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)..)
|
|
149
|
+
}
|
|
137
150
|
end
|
|
138
151
|
|
|
139
152
|
def _run_initialize_callbacks
|
|
@@ -142,7 +155,17 @@ module Switchman
|
|
|
142
155
|
else
|
|
143
156
|
Shard.current(self.class.connection_class_for_self)
|
|
144
157
|
end
|
|
145
|
-
|
|
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
|
|
146
169
|
super
|
|
147
170
|
end
|
|
148
171
|
|
|
@@ -153,6 +176,10 @@ module Switchman
|
|
|
153
176
|
pkey > Shard::IDS_PER_SHARD
|
|
154
177
|
end
|
|
155
178
|
|
|
179
|
+
def canonical?
|
|
180
|
+
!shadow_record?
|
|
181
|
+
end
|
|
182
|
+
|
|
156
183
|
def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
|
|
157
184
|
return if target_shard == shard
|
|
158
185
|
|
|
@@ -165,31 +192,61 @@ module Switchman
|
|
|
165
192
|
end
|
|
166
193
|
end
|
|
167
194
|
target_shard.activate do
|
|
168
|
-
self.class.
|
|
195
|
+
self.class.upsert_all([shadow_attrs], unique_by: self.class.primary_key)
|
|
169
196
|
end
|
|
170
197
|
end
|
|
171
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
|
+
|
|
172
221
|
def shard
|
|
173
|
-
@shard ||
|
|
222
|
+
@shard || fallback_shard
|
|
174
223
|
end
|
|
175
224
|
|
|
176
225
|
def shard=(new_shard)
|
|
177
226
|
raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
|
|
178
227
|
|
|
179
|
-
|
|
228
|
+
if shard == new_shard
|
|
229
|
+
@loaded_from_shard = new_shard
|
|
230
|
+
return
|
|
231
|
+
end
|
|
180
232
|
|
|
181
233
|
attributes.each do |attr, value|
|
|
182
234
|
self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
|
|
183
235
|
end
|
|
236
|
+
@loaded_from_shard = new_shard
|
|
184
237
|
@shard = new_shard
|
|
185
238
|
end
|
|
186
239
|
|
|
187
240
|
def save(*, **)
|
|
241
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
242
|
+
|
|
188
243
|
@shard_set_in_stone = true
|
|
189
244
|
super
|
|
190
245
|
end
|
|
191
246
|
|
|
192
247
|
def save!(*, **)
|
|
248
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
249
|
+
|
|
193
250
|
@shard_set_in_stone = true
|
|
194
251
|
super
|
|
195
252
|
end
|
|
@@ -207,9 +264,9 @@ module Switchman
|
|
|
207
264
|
result
|
|
208
265
|
end
|
|
209
266
|
|
|
210
|
-
def transaction(
|
|
267
|
+
def transaction(...)
|
|
211
268
|
shard.activate(self.class.connection_class_for_self) do
|
|
212
|
-
self.class.transaction(
|
|
269
|
+
self.class.transaction(...)
|
|
213
270
|
end
|
|
214
271
|
end
|
|
215
272
|
|
|
@@ -221,7 +278,7 @@ module Switchman
|
|
|
221
278
|
end
|
|
222
279
|
|
|
223
280
|
def hash
|
|
224
|
-
self.class.sharded_primary_key? ? self.class
|
|
281
|
+
self.class.sharded_primary_key? ? [self.class, global_id].hash : super
|
|
225
282
|
end
|
|
226
283
|
|
|
227
284
|
def to_param
|
|
@@ -242,8 +299,10 @@ module Switchman
|
|
|
242
299
|
|
|
243
300
|
def id_for_database
|
|
244
301
|
if self.class.sharded_primary_key?
|
|
245
|
-
# It's an int, so
|
|
246
|
-
# In theory we should do
|
|
302
|
+
# It's an int, so it's safe to just return it without passing it
|
|
303
|
+
# through anything else. In theory we should do
|
|
304
|
+
# `@attributes[@primary_key].type.serialize(id)`, but that seems to
|
|
305
|
+
# have surprising side-effects
|
|
247
306
|
id
|
|
248
307
|
else
|
|
249
308
|
super
|
|
@@ -270,6 +329,16 @@ module Switchman
|
|
|
270
329
|
self.class.connection_class_for_self
|
|
271
330
|
end
|
|
272
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
|
|
273
342
|
end
|
|
274
343
|
end
|
|
275
344
|
end
|
|
@@ -28,19 +28,19 @@ module Switchman
|
|
|
28
28
|
|
|
29
29
|
def execute_simple_calculation(operation, column_name, distinct)
|
|
30
30
|
operation = operation.to_s.downcase
|
|
31
|
-
if operation ==
|
|
31
|
+
if operation == "average"
|
|
32
32
|
result = calculate_simple_average(column_name, distinct)
|
|
33
33
|
else
|
|
34
|
-
result = activate do |relation|
|
|
34
|
+
result = activate(count: operation == "count") do |relation|
|
|
35
35
|
relation.call_super(:execute_simple_calculation, Calculations, operation, column_name, distinct)
|
|
36
36
|
end
|
|
37
37
|
if result.is_a?(Array)
|
|
38
38
|
case operation
|
|
39
|
-
when
|
|
39
|
+
when "count", "sum"
|
|
40
40
|
result = result.sum
|
|
41
|
-
when
|
|
41
|
+
when "minimum"
|
|
42
42
|
result = result.min
|
|
43
|
-
when
|
|
43
|
+
when "maximum"
|
|
44
44
|
result = result.max
|
|
45
45
|
end
|
|
46
46
|
end
|
|
@@ -52,20 +52,20 @@ module Switchman
|
|
|
52
52
|
# See activerecord#execute_simple_calculation
|
|
53
53
|
relation = except(:order)
|
|
54
54
|
column = aggregate_column(column_name)
|
|
55
|
-
relation.select_values = [operation_over_aggregate_column(column,
|
|
56
|
-
operation_over_aggregate_column(column,
|
|
55
|
+
relation.select_values = [operation_over_aggregate_column(column, "average", distinct).as("average"),
|
|
56
|
+
operation_over_aggregate_column(column, "count", distinct).as("count")]
|
|
57
57
|
|
|
58
58
|
initial_results = relation.activate { |rel| klass.connection.select_all(rel) }
|
|
59
59
|
if initial_results.is_a?(Array)
|
|
60
60
|
initial_results.each do |r|
|
|
61
|
-
r[
|
|
62
|
-
r[
|
|
61
|
+
r["average"] = type_cast_calculated_value_switchman(r["average"], column_name, "average")
|
|
62
|
+
r["count"] = type_cast_calculated_value_switchman(r["count"], column_name, "count")
|
|
63
63
|
end
|
|
64
|
-
result = initial_results.
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
result = initial_results.sum { |r| r["average"] * r["count"] } / initial_results.sum do |r|
|
|
65
|
+
r["count"]
|
|
66
|
+
end
|
|
67
67
|
else
|
|
68
|
-
result = type_cast_calculated_value_switchman(initial_results.first[
|
|
68
|
+
result = type_cast_calculated_value_switchman(initial_results.first["average"], column_name, "average")
|
|
69
69
|
end
|
|
70
70
|
result
|
|
71
71
|
end
|
|
@@ -90,12 +90,12 @@ module Switchman
|
|
|
90
90
|
row[opts[:aggregate_alias]] = type_cast_calculated_value_switchman(
|
|
91
91
|
row[opts[:aggregate_alias]], column_name, opts[:operation]
|
|
92
92
|
)
|
|
93
|
-
row[
|
|
93
|
+
row["count"] = row["count"].to_i if opts[:operation] == "average"
|
|
94
94
|
|
|
95
95
|
opts[:group_columns].each do |aliaz, _type, group_column_name|
|
|
96
96
|
if opts[:associated] && (aliaz == opts[:group_aliases].first)
|
|
97
97
|
row[aliaz] = key_records[Shard.relative_id_for(row[aliaz], shard, target_shard)]
|
|
98
|
-
elsif group_column_name &&
|
|
98
|
+
elsif group_column_name && klass.sharded_column?(group_column_name)
|
|
99
99
|
row[aliaz] = Shard.relative_id_for(row[aliaz], shard, target_shard)
|
|
100
100
|
end
|
|
101
101
|
end
|
|
@@ -106,60 +106,95 @@ module Switchman
|
|
|
106
106
|
compact_grouped_calculation_rows(rows, opts)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
def ids
|
|
110
|
+
return super unless klass.sharded_primary_key?
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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) }
|
|
118
142
|
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
143
|
end
|
|
125
144
|
end
|
|
126
145
|
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def type_cast_calculated_value_switchman(value, column_name, operation)
|
|
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)
|
|
153
|
+
end
|
|
154
|
+
|
|
127
155
|
def column_name_for(field)
|
|
128
|
-
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
|
|
129
157
|
end
|
|
130
158
|
|
|
131
159
|
def grouped_calculation_options(operation, column_name, distinct)
|
|
132
|
-
opts = { operation
|
|
160
|
+
opts = { operation:, column_name:, distinct: }
|
|
161
|
+
|
|
162
|
+
column_alias_tracker = ::ActiveRecord::Calculations::ColumnAliasTracker.new(connection)
|
|
133
163
|
|
|
134
|
-
opts[:aggregate_alias] = aggregate_alias_for(operation, column_name)
|
|
164
|
+
opts[:aggregate_alias] = aggregate_alias_for(operation, column_name, column_alias_tracker)
|
|
135
165
|
group_attrs = group_values
|
|
136
166
|
if group_attrs.first.respond_to?(:to_sym)
|
|
137
167
|
association = klass.reflect_on_association(group_attrs.first.to_sym)
|
|
138
|
-
|
|
168
|
+
# only count belongs_to associations
|
|
169
|
+
associated = group_attrs.size == 1 && association && association.macro == :belongs_to
|
|
139
170
|
group_fields = Array(associated ? association.foreign_key : group_attrs)
|
|
140
171
|
else
|
|
141
172
|
group_fields = group_attrs
|
|
142
173
|
end
|
|
143
174
|
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
146
179
|
group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
|
|
147
180
|
[aliaz, type_for(field), column_name_for(field)]
|
|
148
181
|
end
|
|
149
|
-
opts.merge!(association
|
|
150
|
-
|
|
151
|
-
|
|
182
|
+
opts.merge!(association:,
|
|
183
|
+
associated:,
|
|
184
|
+
group_aliases:,
|
|
185
|
+
group_columns:,
|
|
186
|
+
group_fields:)
|
|
152
187
|
|
|
153
188
|
opts
|
|
154
189
|
end
|
|
155
190
|
|
|
156
|
-
def aggregate_alias_for(operation, column_name)
|
|
157
|
-
if operation ==
|
|
158
|
-
|
|
159
|
-
elsif operation ==
|
|
160
|
-
|
|
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"
|
|
161
196
|
else
|
|
162
|
-
|
|
197
|
+
column_alias_tracker.alias_for("#{operation} #{column_name}")
|
|
163
198
|
end
|
|
164
199
|
end
|
|
165
200
|
|
|
@@ -173,13 +208,14 @@ module Switchman
|
|
|
173
208
|
opts[:distinct]
|
|
174
209
|
).as(opts[:aggregate_alias])
|
|
175
210
|
]
|
|
176
|
-
if opts[:operation] ==
|
|
211
|
+
if opts[:operation] == "average"
|
|
177
212
|
# include count in average so we can recalculate the average
|
|
178
213
|
# across all shards if needed
|
|
179
214
|
select_values << operation_over_aggregate_column(
|
|
180
215
|
aggregate_column(opts[:column_name]),
|
|
181
|
-
|
|
182
|
-
|
|
216
|
+
"count",
|
|
217
|
+
opts[:distinct]
|
|
218
|
+
).as("count")
|
|
183
219
|
end
|
|
184
220
|
|
|
185
221
|
haves = having_clause.send(:predicates)
|
|
@@ -205,22 +241,22 @@ module Switchman
|
|
|
205
241
|
key = key.first if key.size == 1
|
|
206
242
|
value = row[opts[:aggregate_alias]]
|
|
207
243
|
|
|
208
|
-
if opts[:operation] ==
|
|
244
|
+
if opts[:operation] == "average"
|
|
209
245
|
if result.key?(key)
|
|
210
246
|
old_value, old_count = result[key]
|
|
211
|
-
new_count = old_count + row[
|
|
212
|
-
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
|
|
213
249
|
result[key] = [new_value, new_count]
|
|
214
250
|
else
|
|
215
|
-
result[key] = [value, row[
|
|
251
|
+
result[key] = [value, row["count"]]
|
|
216
252
|
end
|
|
217
253
|
elsif result.key?(key)
|
|
218
254
|
case opts[:operation]
|
|
219
|
-
when
|
|
255
|
+
when "count", "sum"
|
|
220
256
|
result[key] += value
|
|
221
|
-
when
|
|
257
|
+
when "minimum"
|
|
222
258
|
result[key] = value if value < result[key]
|
|
223
|
-
when
|
|
259
|
+
when "maximum"
|
|
224
260
|
result[key] = value if value > result[key]
|
|
225
261
|
end
|
|
226
262
|
else
|
|
@@ -228,7 +264,7 @@ module Switchman
|
|
|
228
264
|
end
|
|
229
265
|
end
|
|
230
266
|
|
|
231
|
-
result.transform_values!(&:first) if opts[:operation] ==
|
|
267
|
+
result.transform_values!(&:first) if opts[:operation] == "average"
|
|
232
268
|
|
|
233
269
|
result
|
|
234
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
|
|
@@ -3,27 +3,11 @@
|
|
|
3
3
|
module Switchman
|
|
4
4
|
module ActiveRecord
|
|
5
5
|
module ConnectionPool
|
|
6
|
-
def get_schema_cache(connection)
|
|
7
|
-
self.schema_cache ||= SharedSchemaCache.get_schema_cache(connection)
|
|
8
|
-
self.schema_cache.connection = connection
|
|
9
|
-
|
|
10
|
-
self.schema_cache
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# rubocop:disable Naming/AccessorMethodName override method
|
|
14
|
-
def set_schema_cache(cache)
|
|
15
|
-
schema_cache = get_schema_cache(cache.connection)
|
|
16
|
-
|
|
17
|
-
cache.instance_variables.each do |x|
|
|
18
|
-
schema_cache.instance_variable_set(x, cache.instance_variable_get(x))
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
# rubocop:enable Naming/AccessorMethodName override method
|
|
22
|
-
|
|
23
6
|
def default_schema
|
|
24
|
-
connection
|
|
7
|
+
connection_method = (::Rails.version < "7.2") ? :connection : :lease_connection
|
|
8
|
+
send(connection_method) unless @schemas
|
|
25
9
|
# default shard will not switch databases immediately, so it won't be set yet
|
|
26
|
-
@schemas ||=
|
|
10
|
+
@schemas ||= send(connection_method).current_schemas
|
|
27
11
|
@schemas.first
|
|
28
12
|
end
|
|
29
13
|
|
|
@@ -41,14 +25,44 @@ module Switchman
|
|
|
41
25
|
conn
|
|
42
26
|
end
|
|
43
27
|
|
|
28
|
+
unless ::Rails.version < "7.2"
|
|
29
|
+
def active_connection(switch_shard: true)
|
|
30
|
+
conn = super()
|
|
31
|
+
return nil if conn.nil?
|
|
32
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
33
|
+
|
|
34
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
35
|
+
conn
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def lease_connection(switch_shard: true)
|
|
39
|
+
conn = super()
|
|
40
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
41
|
+
|
|
42
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
43
|
+
conn
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def with_connection(switch_shard: true, **kwargs)
|
|
47
|
+
super(**kwargs) do |conn|
|
|
48
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
49
|
+
|
|
50
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
51
|
+
yield conn
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
44
56
|
def release_connection(with_id = Thread.current)
|
|
45
|
-
super
|
|
57
|
+
super
|
|
46
58
|
|
|
47
59
|
flush
|
|
48
60
|
end
|
|
49
61
|
|
|
50
62
|
def switch_database(conn)
|
|
51
|
-
|
|
63
|
+
if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
|
|
64
|
+
@schemas = conn.current_schemas
|
|
65
|
+
end
|
|
52
66
|
|
|
53
67
|
conn.shard = current_shard
|
|
54
68
|
end
|
|
@@ -56,11 +70,15 @@ module Switchman
|
|
|
56
70
|
private
|
|
57
71
|
|
|
58
72
|
def current_shard
|
|
59
|
-
::Rails.version <
|
|
73
|
+
if ::Rails.version < "8.0"
|
|
74
|
+
connection_class.current_switchman_shard
|
|
75
|
+
else
|
|
76
|
+
connection_descriptor.name.constantize.current_switchman_shard
|
|
77
|
+
end
|
|
60
78
|
end
|
|
61
79
|
|
|
62
80
|
def tls_key
|
|
63
|
-
"#{object_id}_shard"
|
|
81
|
+
:"#{object_id}_shard"
|
|
64
82
|
end
|
|
65
83
|
end
|
|
66
84
|
end
|