switchman 3.3.1 → 4.1.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 +5 -3
- data/lib/switchman/active_record/associations.rb +91 -51
- data/lib/switchman/active_record/attribute_methods.rb +88 -43
- data/lib/switchman/active_record/base.rb +113 -40
- data/lib/switchman/active_record/calculations.rb +98 -51
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +56 -6
- data/lib/switchman/active_record/database_configurations.rb +37 -15
- data/lib/switchman/active_record/finder_methods.rb +47 -17
- data/lib/switchman/active_record/log_subscriber.rb +11 -5
- data/lib/switchman/active_record/migration.rb +51 -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 +37 -22
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +57 -20
- 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 +79 -15
- data/lib/switchman/active_record/spawn_methods.rb +3 -7
- data/lib/switchman/active_record/statement_cache.rb +2 -2
- 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 +75 -25
- data/lib/switchman/active_support/cache.rb +9 -4
- data/lib/switchman/arel.rb +34 -18
- data/lib/switchman/call_super.rb +2 -8
- data/lib/switchman/database_server.rb +72 -34
- data/lib/switchman/default_shard.rb +14 -3
- data/lib/switchman/engine.rb +38 -22
- 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 +185 -71
- 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 +3 -3
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +27 -15
- data/lib/tasks/switchman.rake +96 -60
- metadata +50 -46
|
@@ -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,17 @@ module Switchman
|
|
|
57
53
|
end
|
|
58
54
|
|
|
59
55
|
def clear_query_caches_for_current_thread
|
|
60
|
-
::
|
|
61
|
-
|
|
56
|
+
pools = if ::Rails.version < "7.1"
|
|
57
|
+
::ActiveRecord::Base.connection_handler.connection_pool_list
|
|
58
|
+
else
|
|
59
|
+
::ActiveRecord::Base.connection_handler.connection_pool_list(:all)
|
|
60
|
+
end
|
|
61
|
+
pools.each do |pool|
|
|
62
|
+
if ::Rails.version < "7.2"
|
|
63
|
+
pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
|
|
64
|
+
elsif pool.active_connection?
|
|
65
|
+
pool.lease_connection(switch_shard: false).clear_query_cache
|
|
66
|
+
end
|
|
62
67
|
end
|
|
63
68
|
end
|
|
64
69
|
|
|
@@ -67,7 +72,10 @@ module Switchman
|
|
|
67
72
|
end
|
|
68
73
|
|
|
69
74
|
def establish_connection(config_or_env = nil)
|
|
70
|
-
|
|
75
|
+
if config_or_env.is_a?(Symbol) && config_or_env != ::Rails.env.to_sym
|
|
76
|
+
raise ArgumentError,
|
|
77
|
+
"establish connection cannot be used on the non-current shard/role"
|
|
78
|
+
end
|
|
71
79
|
|
|
72
80
|
# Ensure we don't randomly surprise change the connection parms associated with a shard/role
|
|
73
81
|
config_or_env = nil if config_or_env == ::Rails.env.to_sym
|
|
@@ -75,16 +83,18 @@ module Switchman
|
|
|
75
83
|
config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
|
|
76
84
|
:primary
|
|
77
85
|
else
|
|
78
|
-
"#{current_shard}/#{current_role}"
|
|
86
|
+
:"#{current_shard}/#{current_role}"
|
|
79
87
|
end
|
|
80
88
|
|
|
81
|
-
super
|
|
89
|
+
super
|
|
82
90
|
end
|
|
83
91
|
|
|
84
92
|
def connected_to_stack
|
|
85
|
-
|
|
93
|
+
has_own_stack = ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
|
|
86
94
|
|
|
87
95
|
ret = super
|
|
96
|
+
return ret if has_own_stack
|
|
97
|
+
|
|
88
98
|
DatabaseServer.guard_servers
|
|
89
99
|
ret
|
|
90
100
|
end
|
|
@@ -96,10 +106,13 @@ module Switchman
|
|
|
96
106
|
sharded_role = nil
|
|
97
107
|
connected_to_stack.reverse_each do |hash|
|
|
98
108
|
shard_role = hash.dig(:shard_roles, target_shard)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
unless shard_role &&
|
|
110
|
+
(hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
|
|
111
|
+
next
|
|
102
112
|
end
|
|
113
|
+
|
|
114
|
+
sharded_role = shard_role
|
|
115
|
+
break
|
|
103
116
|
end
|
|
104
117
|
# Allow a shard-specific role to be reverted to regular inheritance
|
|
105
118
|
return sharded_role if sharded_role && sharded_role != :_switchman_inherit
|
|
@@ -119,21 +132,25 @@ module Switchman
|
|
|
119
132
|
|
|
120
133
|
def current_switchman_shard
|
|
121
134
|
connected_to_stack.reverse_each do |hash|
|
|
122
|
-
|
|
135
|
+
if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
|
|
136
|
+
return hash[:switchman_shard]
|
|
137
|
+
end
|
|
123
138
|
end
|
|
124
139
|
|
|
125
140
|
Shard.default
|
|
126
141
|
end
|
|
127
|
-
|
|
128
|
-
if ::Rails.version < '7.0'
|
|
129
|
-
def connection_class_for_self
|
|
130
|
-
connection_classes
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
142
|
end
|
|
134
143
|
|
|
135
144
|
def self.prepended(klass)
|
|
136
145
|
klass.singleton_class.prepend(ClassMethods)
|
|
146
|
+
klass.singleton_class.prepend(Switchman::ActiveRecord::Relation::InsertUpsertAll) if ::Rails.version < "7.2"
|
|
147
|
+
klass.scope :non_shadow, lambda { |key = primary_key|
|
|
148
|
+
where(key => (QueryMethods::NonTransposingValue.new(0)..
|
|
149
|
+
QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)))
|
|
150
|
+
}
|
|
151
|
+
klass.scope :shadow, lambda { |key = primary_key|
|
|
152
|
+
where(key => QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)..)
|
|
153
|
+
}
|
|
137
154
|
end
|
|
138
155
|
|
|
139
156
|
def _run_initialize_callbacks
|
|
@@ -142,7 +159,17 @@ module Switchman
|
|
|
142
159
|
else
|
|
143
160
|
Shard.current(self.class.connection_class_for_self)
|
|
144
161
|
end
|
|
145
|
-
|
|
162
|
+
|
|
163
|
+
@loaded_from_shard ||= Shard.current(self.class.connection_class_for_self)
|
|
164
|
+
if shadow_record? && !Switchman.config[:writable_shadow_records]
|
|
165
|
+
@readonly = true
|
|
166
|
+
@readonly_from_shadow ||= true
|
|
167
|
+
end
|
|
168
|
+
super
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def readonly!
|
|
172
|
+
@readonly_from_shadow = false
|
|
146
173
|
super
|
|
147
174
|
end
|
|
148
175
|
|
|
@@ -153,6 +180,10 @@ module Switchman
|
|
|
153
180
|
pkey > Shard::IDS_PER_SHARD
|
|
154
181
|
end
|
|
155
182
|
|
|
183
|
+
def canonical?
|
|
184
|
+
!shadow_record?
|
|
185
|
+
end
|
|
186
|
+
|
|
156
187
|
def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
|
|
157
188
|
return if target_shard == shard
|
|
158
189
|
|
|
@@ -165,31 +196,61 @@ module Switchman
|
|
|
165
196
|
end
|
|
166
197
|
end
|
|
167
198
|
target_shard.activate do
|
|
168
|
-
self.class.
|
|
199
|
+
self.class.upsert_all([shadow_attrs], unique_by: self.class.primary_key)
|
|
169
200
|
end
|
|
170
201
|
end
|
|
171
202
|
|
|
203
|
+
def destroy_shadow_records(target_shards: [Shard.current])
|
|
204
|
+
raise Errors::ShadowRecordError, "Cannot be called on a shadow record." if shadow_record?
|
|
205
|
+
|
|
206
|
+
unless self.class.sharded_column?(self.class.primary_key)
|
|
207
|
+
raise Errors::MethodUnsupportedForUnshardedTableError,
|
|
208
|
+
"Cannot be called on a record belonging to an unsharded table."
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
Array(target_shards).each do |target_shard|
|
|
212
|
+
next if target_shard == shard
|
|
213
|
+
|
|
214
|
+
target_shard.activate { self.class.where("id = ?", global_id).delete_all }
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Returns "the shard that this record was actually loaded from" , as
|
|
219
|
+
# opposed to "the shard this record belongs on", which might be
|
|
220
|
+
# different if this is a shadow record.
|
|
221
|
+
def loaded_from_shard
|
|
222
|
+
@loaded_from_shard || shard
|
|
223
|
+
end
|
|
224
|
+
|
|
172
225
|
def shard
|
|
173
|
-
@shard ||
|
|
226
|
+
@shard || fallback_shard
|
|
174
227
|
end
|
|
175
228
|
|
|
176
229
|
def shard=(new_shard)
|
|
177
230
|
raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
|
|
178
231
|
|
|
179
|
-
|
|
232
|
+
if shard == new_shard
|
|
233
|
+
@loaded_from_shard = new_shard
|
|
234
|
+
return
|
|
235
|
+
end
|
|
180
236
|
|
|
181
237
|
attributes.each do |attr, value|
|
|
182
238
|
self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
|
|
183
239
|
end
|
|
240
|
+
@loaded_from_shard = new_shard
|
|
184
241
|
@shard = new_shard
|
|
185
242
|
end
|
|
186
243
|
|
|
187
244
|
def save(*, **)
|
|
245
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
246
|
+
|
|
188
247
|
@shard_set_in_stone = true
|
|
189
248
|
super
|
|
190
249
|
end
|
|
191
250
|
|
|
192
251
|
def save!(*, **)
|
|
252
|
+
raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
|
|
253
|
+
|
|
193
254
|
@shard_set_in_stone = true
|
|
194
255
|
super
|
|
195
256
|
end
|
|
@@ -207,9 +268,9 @@ module Switchman
|
|
|
207
268
|
result
|
|
208
269
|
end
|
|
209
270
|
|
|
210
|
-
def transaction(
|
|
271
|
+
def transaction(...)
|
|
211
272
|
shard.activate(self.class.connection_class_for_self) do
|
|
212
|
-
self.class.transaction(
|
|
273
|
+
self.class.transaction(...)
|
|
213
274
|
end
|
|
214
275
|
end
|
|
215
276
|
|
|
@@ -221,7 +282,7 @@ module Switchman
|
|
|
221
282
|
end
|
|
222
283
|
|
|
223
284
|
def hash
|
|
224
|
-
self.class.sharded_primary_key? ? self.class
|
|
285
|
+
self.class.sharded_primary_key? ? [self.class, global_id].hash : super
|
|
225
286
|
end
|
|
226
287
|
|
|
227
288
|
def to_param
|
|
@@ -242,8 +303,10 @@ module Switchman
|
|
|
242
303
|
|
|
243
304
|
def id_for_database
|
|
244
305
|
if self.class.sharded_primary_key?
|
|
245
|
-
# It's an int, so
|
|
246
|
-
# In theory we should do
|
|
306
|
+
# It's an int, so it's safe to just return it without passing it
|
|
307
|
+
# through anything else. In theory we should do
|
|
308
|
+
# `@attributes[@primary_key].type.serialize(id)`, but that seems to
|
|
309
|
+
# have surprising side-effects
|
|
247
310
|
id
|
|
248
311
|
else
|
|
249
312
|
super
|
|
@@ -270,6 +333,16 @@ module Switchman
|
|
|
270
333
|
self.class.connection_class_for_self
|
|
271
334
|
end
|
|
272
335
|
end
|
|
336
|
+
|
|
337
|
+
private
|
|
338
|
+
|
|
339
|
+
def fallback_shard
|
|
340
|
+
Shard.current(self.class.connection_class_for_self) || Shard.default
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def creating_shadow_record?
|
|
344
|
+
new_record? && shadow_record?
|
|
345
|
+
end
|
|
273
346
|
end
|
|
274
347
|
end
|
|
275
348
|
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
|
-
opts = { operation
|
|
162
|
+
opts = { operation:, column_name:, 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
|
|
150
|
-
|
|
151
|
-
|
|
191
|
+
opts.merge!(association:,
|
|
192
|
+
associated:,
|
|
193
|
+
group_aliases:,
|
|
194
|
+
group_columns:,
|
|
195
|
+
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,10 +3,30 @@
|
|
|
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
|
-
connection
|
|
26
|
+
connection_method = (::Rails.version < "7.2") ? :connection : :lease_connection
|
|
27
|
+
send(connection_method) unless @schemas
|
|
8
28
|
# default shard will not switch databases immediately, so it won't be set yet
|
|
9
|
-
@schemas ||=
|
|
29
|
+
@schemas ||= send(connection_method).current_schemas
|
|
10
30
|
@schemas.first
|
|
11
31
|
end
|
|
12
32
|
|
|
@@ -24,14 +44,44 @@ module Switchman
|
|
|
24
44
|
conn
|
|
25
45
|
end
|
|
26
46
|
|
|
47
|
+
unless ::Rails.version < "7.2"
|
|
48
|
+
def active_connection(switch_shard: true)
|
|
49
|
+
conn = super()
|
|
50
|
+
return nil if conn.nil?
|
|
51
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
52
|
+
|
|
53
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
54
|
+
conn
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def lease_connection(switch_shard: true)
|
|
58
|
+
conn = super()
|
|
59
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
60
|
+
|
|
61
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
62
|
+
conn
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def with_connection(switch_shard: true, **kwargs)
|
|
66
|
+
super(**kwargs) do |conn|
|
|
67
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
68
|
+
|
|
69
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
70
|
+
yield conn
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
27
75
|
def release_connection(with_id = Thread.current)
|
|
28
|
-
super
|
|
76
|
+
super
|
|
29
77
|
|
|
30
78
|
flush
|
|
31
79
|
end
|
|
32
80
|
|
|
33
81
|
def switch_database(conn)
|
|
34
|
-
|
|
82
|
+
if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
|
|
83
|
+
@schemas = conn.current_schemas
|
|
84
|
+
end
|
|
35
85
|
|
|
36
86
|
conn.shard = current_shard
|
|
37
87
|
end
|
|
@@ -39,11 +89,11 @@ module Switchman
|
|
|
39
89
|
private
|
|
40
90
|
|
|
41
91
|
def current_shard
|
|
42
|
-
|
|
92
|
+
connection_class.current_switchman_shard
|
|
43
93
|
end
|
|
44
94
|
|
|
45
95
|
def tls_key
|
|
46
|
-
"#{object_id}_shard"
|
|
96
|
+
:"#{object_id}_shard"
|
|
47
97
|
end
|
|
48
98
|
end
|
|
49
99
|
end
|