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.
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 +6 -15
  8. data/lib/switchman/active_record/associations.rb +331 -0
  9. data/lib/switchman/active_record/attribute_methods.rb +182 -77
  10. data/lib/switchman/active_record/base.rb +249 -46
  11. data/lib/switchman/active_record/calculations.rb +98 -44
  12. data/lib/switchman/active_record/connection_handler.rb +18 -0
  13. data/lib/switchman/active_record/connection_pool.rb +27 -28
  14. data/lib/switchman/active_record/database_configurations.rb +44 -6
  15. data/lib/switchman/active_record/finder_methods.rb +46 -16
  16. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  17. data/lib/switchman/active_record/migration.rb +52 -5
  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 +12 -11
  22. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  23. data/lib/switchman/active_record/query_cache.rb +49 -20
  24. data/lib/switchman/active_record/query_methods.rb +202 -136
  25. data/lib/switchman/active_record/reflection.rb +1 -1
  26. data/lib/switchman/active_record/relation.rb +40 -28
  27. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  28. data/lib/switchman/active_record/statement_cache.rb +11 -7
  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 +53 -0
  32. data/lib/switchman/active_support/cache.rb +25 -4
  33. data/lib/switchman/arel.rb +45 -7
  34. data/lib/switchman/call_super.rb +2 -2
  35. data/lib/switchman/database_server.rb +123 -79
  36. data/lib/switchman/default_shard.rb +14 -5
  37. data/lib/switchman/engine.rb +79 -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 +226 -241
  46. data/lib/switchman/sharded_instrumenter.rb +3 -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 +2 -2
  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 +50 -58
  55. data/lib/switchman/active_record/association.rb +0 -206
  56. 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(connection_classes).database_server
31
- if ::GuardRail.environment == db.guard_rail_environment
32
- super
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
- else
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
- ::ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
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?(connection_classes)
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.included(klass)
190
+ def self.prepended(klass)
83
191
  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
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 || Shard.current(self.class.connection_classes) || Shard.default
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
- return if shard == new_shard
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
- (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
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
- (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
300
+ super
116
301
  end
117
302
 
118
303
  def destroy
119
- shard.activate(self.class.connection_classes) { super }
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(**kwargs, &block)
132
- shard.activate(self.class.connection_classes) do
133
- self.class.transaction(**kwargs, &block)
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.hash ^ global_id.hash : super
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 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)
344
+ def update_columns(*)
345
+ db = shard.database_server
346
+ db.unguard { super }
157
347
  end
158
348
 
159
- def update_columns(*)
160
- db = Shard.current(self.class.connection_classes).database_server
161
- if ::GuardRail.environment == db.guard_rail_environment
162
- super
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
- db.unguard { super }
357
+ super
165
358
  end
166
359
  end
167
360
 
168
361
  protected
169
362
 
170
- # see also AttributeMethods#connection_classes_code_for_reflection
171
- def connection_classes_for_reflection(reflection)
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&.connection_classes
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.connection_classes
375
+ reflection.klass.connection_class_for_self
183
376
  end
184
377
  else
185
- connection_classes
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.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,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 == 'average'
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 '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,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.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)
@@ -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
- 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
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('.').last
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
- opts[:aggregate_alias] = aggregate_alias_for(operation, column_name)
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
- associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
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
- # 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 }
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, associated: associated,
143
- group_aliases: group_aliases, group_columns: group_columns,
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 == 'count' && column_name == :all
151
- 'count_all'
152
- elsif operation == 'average'
153
- 'average'
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] == 'average'
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
- 'count', opts[:distinct]
175
- ).as('count')
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] == 'average'
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['count']
205
- new_value = ((old_value * old_count) + (value * row['count'])) / new_count
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['count']]
262
+ result[key] = [value, row["count"]]
209
263
  end
210
264
  elsif result.key?(key)
211
265
  case opts[:operation]
212
- when 'count', 'sum'
266
+ when "count", "sum"
213
267
  result[key] += value
214
- when 'minimum'
268
+ when "minimum"
215
269
  result[key] = value if value < result[key]
216
- when 'maximum'
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] == 'average'
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