switchman 3.0.24 → 4.2.4

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +16 -15
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  4. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  5. data/lib/switchman/active_record/abstract_adapter.rb +11 -7
  6. data/lib/switchman/active_record/associations.rb +157 -50
  7. data/lib/switchman/active_record/attribute_methods.rb +192 -101
  8. data/lib/switchman/active_record/base.rb +136 -33
  9. data/lib/switchman/active_record/calculations.rb +91 -48
  10. data/lib/switchman/active_record/connection_handler.rb +18 -0
  11. data/lib/switchman/active_record/connection_pool.rb +41 -6
  12. data/lib/switchman/active_record/database_configurations.rb +23 -13
  13. data/lib/switchman/active_record/finder_methods.rb +22 -16
  14. data/lib/switchman/active_record/log_subscriber.rb +3 -6
  15. data/lib/switchman/active_record/migration.rb +42 -17
  16. data/lib/switchman/active_record/model_schema.rb +1 -1
  17. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  18. data/lib/switchman/active_record/persistence.rb +32 -2
  19. data/lib/switchman/active_record/postgresql_adapter.rb +37 -22
  20. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  21. data/lib/switchman/active_record/query_cache.rb +26 -17
  22. data/lib/switchman/active_record/query_methods.rb +249 -142
  23. data/lib/switchman/active_record/reflection.rb +10 -3
  24. data/lib/switchman/active_record/relation.rb +103 -32
  25. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  26. data/lib/switchman/active_record/statement_cache.rb +13 -9
  27. data/lib/switchman/active_record/table_definition.rb +1 -1
  28. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  29. data/lib/switchman/active_record/test_fixtures.rb +71 -25
  30. data/lib/switchman/active_support/cache.rb +9 -4
  31. data/lib/switchman/arel.rb +16 -25
  32. data/lib/switchman/call_super.rb +2 -2
  33. data/lib/switchman/database_server.rb +68 -34
  34. data/lib/switchman/default_shard.rb +14 -3
  35. data/lib/switchman/engine.rb +36 -19
  36. data/lib/switchman/environment.rb +2 -2
  37. data/lib/switchman/errors.rb +13 -0
  38. data/lib/switchman/guard_rail/relation.rb +3 -3
  39. data/lib/switchman/parallel.rb +6 -6
  40. data/lib/switchman/r_spec_helper.rb +12 -11
  41. data/lib/switchman/shard.rb +182 -72
  42. data/lib/switchman/sharded_instrumenter.rb +9 -3
  43. data/lib/switchman/shared_schema_cache.rb +11 -0
  44. data/lib/switchman/standard_error.rb +4 -0
  45. data/lib/switchman/test_helper.rb +3 -3
  46. data/lib/switchman/version.rb +1 -1
  47. data/lib/switchman.rb +27 -15
  48. data/lib/tasks/switchman.rake +96 -60
  49. metadata +18 -168
@@ -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 = {}, &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
@@ -25,11 +25,11 @@ module Switchman
25
25
  def transaction(**)
26
26
  if self != ::ActiveRecord::Base && current_scope
27
27
  current_scope.activate do
28
- db = Shard.current(connection_classes).database_server
28
+ db = Shard.current(connection_class_for_self).database_server
29
29
  db.unguard { super }
30
30
  end
31
31
  else
32
- db = Shard.current(connection_classes).database_server
32
+ db = Shard.current(connection_class_for_self).database_server
33
33
  db.unguard { super }
34
34
  end
35
35
  end
@@ -53,8 +53,13 @@ module Switchman
53
53
  end
54
54
 
55
55
  def clear_query_caches_for_current_thread
56
- ::ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
57
- 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
58
63
  end
59
64
  end
60
65
 
@@ -63,7 +68,10 @@ module Switchman
63
68
  end
64
69
 
65
70
  def establish_connection(config_or_env = nil)
66
- raise ArgumentError, 'establish connection cannot be used on the non-current shard/role' if config_or_env.is_a?(Symbol) && config_or_env != ::Rails.env.to_sym
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
67
75
 
68
76
  # Ensure we don't randomly surprise change the connection parms associated with a shard/role
69
77
  config_or_env = nil if config_or_env == ::Rails.env.to_sym
@@ -71,16 +79,18 @@ module Switchman
71
79
  config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
72
80
  :primary
73
81
  else
74
- "#{current_shard}/#{current_role}".to_sym
82
+ :"#{current_shard}/#{current_role}"
75
83
  end
76
84
 
77
- super(config_or_env)
85
+ super
78
86
  end
79
87
 
80
88
  def connected_to_stack
81
- return super if Thread.current.thread_variable?(:ar_connected_to_stack)
89
+ has_own_stack = ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
82
90
 
83
91
  ret = super
92
+ return ret if has_own_stack
93
+
84
94
  DatabaseServer.guard_servers
85
95
  ret
86
96
  end
@@ -92,10 +102,13 @@ module Switchman
92
102
  sharded_role = nil
93
103
  connected_to_stack.reverse_each do |hash|
94
104
  shard_role = hash.dig(:shard_roles, target_shard)
95
- if shard_role && (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_classes))
96
- sharded_role = shard_role
97
- break
105
+ unless shard_role &&
106
+ (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
107
+ next
98
108
  end
109
+
110
+ sharded_role = shard_role
111
+ break
99
112
  end
100
113
  # Allow a shard-specific role to be reverted to regular inheritance
101
114
  return sharded_role if sharded_role && sharded_role != :_switchman_inherit
@@ -107,7 +120,7 @@ module Switchman
107
120
  # i.e. other sharded models don't inherit the current shard of Base
108
121
  def current_shard
109
122
  connected_to_stack.reverse_each do |hash|
110
- 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)
111
124
  end
112
125
 
113
126
  default_shard
@@ -115,7 +128,9 @@ module Switchman
115
128
 
116
129
  def current_switchman_shard
117
130
  connected_to_stack.reverse_each do |hash|
118
- return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_classes)
131
+ if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
132
+ return hash[:switchman_shard]
133
+ end
119
134
  end
120
135
 
121
136
  Shard.default
@@ -124,44 +139,120 @@ module Switchman
124
139
 
125
140
  def self.prepended(klass)
126
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
+ }
127
150
  end
128
151
 
129
152
  def _run_initialize_callbacks
130
153
  @shard ||= if self.class.sharded_primary_key?
131
- Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_classes))
154
+ Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_class_for_self))
132
155
  else
133
- Shard.current(self.class.connection_classes)
156
+ Shard.current(self.class.connection_class_for_self)
134
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
135
164
  super
136
165
  end
137
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)
196
+ end
197
+ end
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
+
138
221
  def shard
139
- @shard || Shard.current(self.class.connection_classes) || Shard.default
222
+ @shard || fallback_shard
140
223
  end
141
224
 
142
225
  def shard=(new_shard)
143
226
  raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
144
227
 
145
- return if shard == new_shard
228
+ if shard == new_shard
229
+ @loaded_from_shard = new_shard
230
+ return
231
+ end
146
232
 
147
233
  attributes.each do |attr, value|
148
234
  self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
149
235
  end
236
+ @loaded_from_shard = new_shard
150
237
  @shard = new_shard
151
238
  end
152
239
 
153
240
  def save(*, **)
241
+ raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
242
+
154
243
  @shard_set_in_stone = true
155
244
  super
156
245
  end
157
246
 
158
247
  def save!(*, **)
248
+ raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
249
+
159
250
  @shard_set_in_stone = true
160
251
  super
161
252
  end
162
253
 
163
254
  def destroy
164
- shard.activate(self.class.connection_classes) { super }
255
+ shard.activate(self.class.connection_class_for_self) { super }
165
256
  end
166
257
 
167
258
  def clone
@@ -173,21 +264,21 @@ module Switchman
173
264
  result
174
265
  end
175
266
 
176
- def transaction(**kwargs, &block)
177
- shard.activate(self.class.connection_classes) do
178
- self.class.transaction(**kwargs, &block)
267
+ def transaction(...)
268
+ shard.activate(self.class.connection_class_for_self) do
269
+ self.class.transaction(...)
179
270
  end
180
271
  end
181
272
 
182
273
  def with_transaction_returning_status
183
- shard.activate(self.class.connection_classes) do
184
- db = Shard.current(self.class.connection_classes).database_server
274
+ shard.activate(self.class.connection_class_for_self) do
275
+ db = Shard.current(self.class.connection_class_for_self).database_server
185
276
  db.unguard { super }
186
277
  end
187
278
  end
188
279
 
189
280
  def hash
190
- self.class.sharded_primary_key? ? self.class.hash ^ global_id.hash : super
281
+ self.class.sharded_primary_key? ? [self.class, global_id].hash : super
191
282
  end
192
283
 
193
284
  def to_param
@@ -208,8 +299,10 @@ module Switchman
208
299
 
209
300
  def id_for_database
210
301
  if self.class.sharded_primary_key?
211
- # It's an int, so so it's safe to just return it without passing it through anything else
212
- # In theory we should do `@attributes[@primary_key].type.serialize(id)`, but that seems to have surprising side-effects
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
213
306
  id
214
307
  else
215
308
  super
@@ -218,24 +311,34 @@ module Switchman
218
311
 
219
312
  protected
220
313
 
221
- # see also AttributeMethods#connection_classes_code_for_reflection
222
- 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)
223
316
  if reflection
224
317
  if reflection.options[:polymorphic]
225
318
  begin
226
- read_attribute(reflection.foreign_type)&.constantize&.connection_classes || ::ActiveRecord::Base
319
+ read_attribute(reflection.foreign_type)&.constantize&.connection_class_for_self || ::ActiveRecord::Base
227
320
  rescue NameError
228
321
  # in case someone is abusing foreign_type to not point to an actual class
229
322
  ::ActiveRecord::Base
230
323
  end
231
324
  else
232
325
  # otherwise we can just return a symbol for the statically known type of the association
233
- reflection.klass.connection_classes
326
+ reflection.klass.connection_class_for_self
234
327
  end
235
328
  else
236
- self.class.connection_classes
329
+ self.class.connection_class_for_self
237
330
  end
238
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
239
342
  end
240
343
  end
241
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
@@ -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['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
@@ -4,9 +4,10 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module ConnectionPool
6
6
  def default_schema
7
- connection unless @schemas
7
+ connection_method = (::Rails.version < "7.2") ? :connection : :lease_connection
8
+ send(connection_method) unless @schemas
8
9
  # default shard will not switch databases immediately, so it won't be set yet
9
- @schemas ||= connection.current_schemas
10
+ @schemas ||= send(connection_method).current_schemas
10
11
  @schemas.first
11
12
  end
12
13
 
@@ -24,14 +25,44 @@ module Switchman
24
25
  conn
25
26
  end
26
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
+
27
56
  def release_connection(with_id = Thread.current)
28
- super(with_id)
57
+ super
29
58
 
30
59
  flush
31
60
  end
32
61
 
33
62
  def switch_database(conn)
34
- @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !current_shard.database_server.config[:shard_name]
63
+ if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
64
+ @schemas = conn.current_schemas
65
+ end
35
66
 
36
67
  conn.shard = current_shard
37
68
  end
@@ -39,11 +70,15 @@ module Switchman
39
70
  private
40
71
 
41
72
  def current_shard
42
- connection_klass.current_switchman_shard
73
+ if ::Rails.version < "8.0"
74
+ connection_class.current_switchman_shard
75
+ else
76
+ connection_descriptor.name.constantize.current_switchman_shard
77
+ end
43
78
  end
44
79
 
45
80
  def tls_key
46
- "#{object_id}_shard".to_sym
81
+ :"#{object_id}_shard"
47
82
  end
48
83
  end
49
84
  end