switchman 3.0.2 → 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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +16 -15
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
  4. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
  5. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  6. data/lib/switchman/action_controller/caching.rb +2 -2
  7. data/lib/switchman/active_record/abstract_adapter.rb +11 -18
  8. data/lib/switchman/active_record/associations.rb +315 -0
  9. data/lib/switchman/active_record/attribute_methods.rb +191 -79
  10. data/lib/switchman/active_record/base.rb +204 -50
  11. data/lib/switchman/active_record/calculations.rb +92 -49
  12. data/lib/switchman/active_record/connection_handler.rb +18 -0
  13. data/lib/switchman/active_record/connection_pool.rb +47 -34
  14. data/lib/switchman/active_record/database_configurations.rb +32 -6
  15. data/lib/switchman/active_record/finder_methods.rb +22 -16
  16. data/lib/switchman/active_record/log_subscriber.rb +3 -6
  17. data/lib/switchman/active_record/migration.rb +42 -14
  18. data/lib/switchman/active_record/model_schema.rb +1 -1
  19. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  20. data/lib/switchman/active_record/persistence.rb +37 -2
  21. data/lib/switchman/active_record/postgresql_adapter.rb +39 -20
  22. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  23. data/lib/switchman/active_record/query_cache.rb +26 -17
  24. data/lib/switchman/active_record/query_methods.rb +251 -140
  25. data/lib/switchman/active_record/reflection.rb +10 -3
  26. data/lib/switchman/active_record/relation.rb +110 -35
  27. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  28. data/lib/switchman/active_record/statement_cache.rb +13 -9
  29. data/lib/switchman/active_record/table_definition.rb +1 -1
  30. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  31. data/lib/switchman/active_record/test_fixtures.rb +89 -0
  32. data/lib/switchman/active_support/cache.rb +25 -4
  33. data/lib/switchman/arel.rb +20 -7
  34. data/lib/switchman/call_super.rb +2 -2
  35. data/lib/switchman/database_server.rb +123 -83
  36. data/lib/switchman/default_shard.rb +14 -5
  37. data/lib/switchman/engine.rb +85 -131
  38. data/lib/switchman/environment.rb +2 -2
  39. data/lib/switchman/errors.rb +17 -2
  40. data/lib/switchman/guard_rail/relation.rb +7 -10
  41. data/lib/switchman/guard_rail.rb +5 -0
  42. data/lib/switchman/parallel.rb +68 -0
  43. data/lib/switchman/r_spec_helper.rb +17 -28
  44. data/lib/switchman/rails.rb +1 -4
  45. data/{app/models → lib}/switchman/shard.rb +229 -246
  46. data/lib/switchman/sharded_instrumenter.rb +9 -3
  47. data/lib/switchman/shared_schema_cache.rb +11 -0
  48. data/lib/switchman/standard_error.rb +15 -12
  49. data/lib/switchman/test_helper.rb +3 -3
  50. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  51. data/lib/switchman/version.rb +1 -1
  52. data/lib/switchman.rb +44 -12
  53. data/lib/tasks/switchman.rake +101 -54
  54. metadata +34 -176
  55. data/lib/switchman/active_record/association.rb +0 -206
  56. 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 = {}, &block)
9
+ def find_ids_in_ranges(opts = {}, &)
10
10
  opts.reverse_merge!(loose: true)
11
- all.find_ids_in_ranges(opts, &block)
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(connection_classes).database_server
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.each do |pool|
67
- pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
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?(connection_classes)
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.included(klass)
140
+ def self.prepended(klass)
83
141
  klass.singleton_class.prepend(ClassMethods)
84
- klass.set_callback(:initialize, :before) do
85
- @shard ||= if self.class.sharded_primary_key?
86
- Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_classes))
87
- else
88
- Shard.current(self.class.connection_classes)
89
- end
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 || Shard.current(self.class.connection_classes) || Shard.default
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
- return if shard == new_shard
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
- (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
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
- (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
251
+ super
116
252
  end
117
253
 
118
254
  def destroy
119
- shard.activate(self.class.connection_classes) { super }
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(**kwargs, &block)
132
- shard.activate(self.class.connection_classes) do
133
- self.class.transaction(**kwargs, &block)
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.hash ^ global_id.hash : super
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 quoted_id
153
- return super unless self.class.sharded_primary_key?
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 update_columns(*)
160
- db = Shard.current(self.class.connection_classes).database_server
161
- if ::GuardRail.environment == db.guard_rail_environment
162
- super
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
- db.unguard { super }
308
+ super
165
309
  end
166
310
  end
167
311
 
168
312
  protected
169
313
 
170
- # see also AttributeMethods#connection_classes_code_for_reflection
171
- def connection_classes_for_reflection(reflection)
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&.connection_classes
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.connection_classes
326
+ reflection.klass.connection_class_for_self
183
327
  end
184
328
  else
185
- connection_classes
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.connection_classes)
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 == 'average'
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 'count', 'sum'
39
+ when "count", "sum"
40
40
  result = result.sum
41
- when 'minimum'
41
+ when "minimum"
42
42
  result = result.min
43
- when 'maximum'
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, 'average', distinct).as('average'),
56
- operation_over_aggregate_column(column, 'count', distinct).as('count')]
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['average'] = type_cast_calculated_value_switchman(r['average'], column_name, 'average')
62
- r['count'] = type_cast_calculated_value_switchman(r['count'], column_name, 'count')
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.map { |r| r['average'] * r['count'] }.sum / initial_results.map do |r|
65
- r['count']
66
- end.sum
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['average'], column_name, 'average')
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.map { |r| [Shard.relative_id_for(r, shard, target_shard), r] }.to_h
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['count'] = row['count'].to_i if opts[:operation] == 'average'
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 && @klass.sharded_column?(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
- type_cast_calculated_value(value, operation) do |val|
113
- column = aggregate_column(column_name)
114
- type ||= column.try(:type_caster) ||
115
- lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
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('.').last
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: operation, column_name: column_name, distinct: distinct }
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
- associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
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
- # to_s is because Rails 5 returns a string but Rails 6 returns a symbol.
138
- group_aliases = group_fields.map { |field| column_alias_for(field.downcase.to_s).to_s }
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: association, associated: associated,
143
- group_aliases: group_aliases, group_columns: group_columns,
144
- group_fields: group_fields)
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 == 'count' && column_name == :all
151
- 'count_all'
152
- elsif operation == 'average'
153
- 'average'
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
- column_alias_for("#{operation} #{column_name}")
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] == 'average'
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
- 'count', opts[:distinct]
175
- ).as('count')
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] == 'average'
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['count']
205
- new_value = ((old_value * old_count) + (value * row['count'])) / new_count
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['count']]
251
+ result[key] = [value, row["count"]]
209
252
  end
210
253
  elsif result.key?(key)
211
254
  case opts[:operation]
212
- when 'count', 'sum'
255
+ when "count", "sum"
213
256
  result[key] += value
214
- when 'minimum'
257
+ when "minimum"
215
258
  result[key] = value if value < result[key]
216
- when 'maximum'
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] == 'average'
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