switchman 3.3.1 → 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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +15 -14
  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 +4 -2
  6. data/lib/switchman/active_record/associations.rb +89 -49
  7. data/lib/switchman/active_record/attribute_methods.rb +72 -34
  8. data/lib/switchman/active_record/base.rb +145 -27
  9. data/lib/switchman/active_record/calculations.rb +96 -49
  10. data/lib/switchman/active_record/connection_handler.rb +18 -0
  11. data/lib/switchman/active_record/connection_pool.rb +24 -3
  12. data/lib/switchman/active_record/database_configurations.rb +37 -15
  13. data/lib/switchman/active_record/finder_methods.rb +44 -14
  14. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  15. data/lib/switchman/active_record/migration.rb +45 -3
  16. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  17. data/lib/switchman/active_record/persistence.rb +30 -0
  18. data/lib/switchman/active_record/postgresql_adapter.rb +11 -10
  19. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  20. data/lib/switchman/active_record/query_cache.rb +49 -20
  21. data/lib/switchman/active_record/query_methods.rb +93 -30
  22. data/lib/switchman/active_record/relation.rb +23 -12
  23. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  24. data/lib/switchman/active_record/statement_cache.rb +2 -2
  25. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  26. data/lib/switchman/active_record/test_fixtures.rb +26 -16
  27. data/lib/switchman/active_support/cache.rb +9 -4
  28. data/lib/switchman/arel.rb +34 -18
  29. data/lib/switchman/call_super.rb +2 -8
  30. data/lib/switchman/database_server.rb +69 -31
  31. data/lib/switchman/default_shard.rb +14 -3
  32. data/lib/switchman/engine.rb +29 -22
  33. data/lib/switchman/environment.rb +2 -2
  34. data/lib/switchman/errors.rb +13 -0
  35. data/lib/switchman/guard_rail/relation.rb +1 -2
  36. data/lib/switchman/parallel.rb +6 -6
  37. data/lib/switchman/r_spec_helper.rb +12 -11
  38. data/lib/switchman/shard.rb +180 -68
  39. data/lib/switchman/sharded_instrumenter.rb +3 -3
  40. data/lib/switchman/shared_schema_cache.rb +11 -0
  41. data/lib/switchman/standard_error.rb +4 -0
  42. data/lib/switchman/test_helper.rb +2 -2
  43. data/lib/switchman/version.rb +1 -1
  44. data/lib/switchman.rb +27 -15
  45. data/lib/tasks/switchman.rake +96 -60
  46. metadata +38 -48
@@ -22,13 +22,59 @@ module Switchman
22
22
  @integral_id
23
23
  end
24
24
 
25
- %w[transaction insert_all upsert_all].each do |method|
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 }
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|
26
39
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
27
- def #{method}(*, **)
28
- if self != ::ActiveRecord::Base && current_scope
29
- current_scope.activate do
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
30
66
  db = Shard.current(connection_class_for_self).database_server
31
- db.unguard { super }
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
32
78
  end
33
79
  else
34
80
  db = Shard.current(connection_class_for_self).database_server
@@ -57,7 +103,12 @@ module Switchman
57
103
  end
58
104
 
59
105
  def clear_query_caches_for_current_thread
60
- ::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|
61
112
  pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
62
113
  end
63
114
  end
@@ -67,7 +118,10 @@ module Switchman
67
118
  end
68
119
 
69
120
  def establish_connection(config_or_env = nil)
70
- 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
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
71
125
 
72
126
  # Ensure we don't randomly surprise change the connection parms associated with a shard/role
73
127
  config_or_env = nil if config_or_env == ::Rails.env.to_sym
@@ -75,16 +129,18 @@ module Switchman
75
129
  config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
76
130
  :primary
77
131
  else
78
- "#{current_shard}/#{current_role}".to_sym
132
+ :"#{current_shard}/#{current_role}"
79
133
  end
80
134
 
81
135
  super(config_or_env)
82
136
  end
83
137
 
84
138
  def connected_to_stack
85
- return super if ::Rails.version < '7.0' ? Thread.current.thread_variable?(:ar_connected_to_stack) : ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
139
+ has_own_stack = ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
86
140
 
87
141
  ret = super
142
+ return ret if has_own_stack
143
+
88
144
  DatabaseServer.guard_servers
89
145
  ret
90
146
  end
@@ -96,10 +152,13 @@ module Switchman
96
152
  sharded_role = nil
97
153
  connected_to_stack.reverse_each do |hash|
98
154
  shard_role = hash.dig(:shard_roles, target_shard)
99
- if shard_role && (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
100
- sharded_role = shard_role
101
- break
155
+ unless shard_role &&
156
+ (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
157
+ next
102
158
  end
159
+
160
+ sharded_role = shard_role
161
+ break
103
162
  end
104
163
  # Allow a shard-specific role to be reverted to regular inheritance
105
164
  return sharded_role if sharded_role && sharded_role != :_switchman_inherit
@@ -119,21 +178,24 @@ module Switchman
119
178
 
120
179
  def current_switchman_shard
121
180
  connected_to_stack.reverse_each do |hash|
122
- return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
181
+ if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
182
+ return hash[:switchman_shard]
183
+ end
123
184
  end
124
185
 
125
186
  Shard.default
126
187
  end
127
-
128
- if ::Rails.version < '7.0'
129
- def connection_class_for_self
130
- connection_classes
131
- end
132
- end
133
188
  end
134
189
 
135
190
  def self.prepended(klass)
136
191
  klass.singleton_class.prepend(ClassMethods)
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
+ }
137
199
  end
138
200
 
139
201
  def _run_initialize_callbacks
@@ -142,7 +204,17 @@ module Switchman
142
204
  else
143
205
  Shard.current(self.class.connection_class_for_self)
144
206
  end
145
- readonly! if shadow_record? && !Switchman.config[:writable_shadow_records]
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
146
218
  super
147
219
  end
148
220
 
@@ -153,6 +225,10 @@ module Switchman
153
225
  pkey > Shard::IDS_PER_SHARD
154
226
  end
155
227
 
228
+ def canonical?
229
+ !shadow_record?
230
+ end
231
+
156
232
  def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
157
233
  return if target_shard == shard
158
234
 
@@ -169,27 +245,57 @@ module Switchman
169
245
  end
170
246
  end
171
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 }
260
+ end
261
+ end
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
+
172
270
  def shard
173
- @shard || Shard.current(self.class.connection_class_for_self) || Shard.default
271
+ @shard || fallback_shard
174
272
  end
175
273
 
176
274
  def shard=(new_shard)
177
275
  raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
178
276
 
179
- return if shard == new_shard
277
+ if shard == new_shard
278
+ @loaded_from_shard = new_shard
279
+ return
280
+ end
180
281
 
181
282
  attributes.each do |attr, value|
182
283
  self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
183
284
  end
285
+ @loaded_from_shard = new_shard
184
286
  @shard = new_shard
185
287
  end
186
288
 
187
289
  def save(*, **)
290
+ raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
291
+
188
292
  @shard_set_in_stone = true
189
293
  super
190
294
  end
191
295
 
192
296
  def save!(*, **)
297
+ raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
298
+
193
299
  @shard_set_in_stone = true
194
300
  super
195
301
  end
@@ -207,9 +313,9 @@ module Switchman
207
313
  result
208
314
  end
209
315
 
210
- def transaction(**kwargs, &block)
316
+ def transaction(...)
211
317
  shard.activate(self.class.connection_class_for_self) do
212
- self.class.transaction(**kwargs, &block)
318
+ self.class.transaction(...)
213
319
  end
214
320
  end
215
321
 
@@ -221,7 +327,7 @@ module Switchman
221
327
  end
222
328
 
223
329
  def hash
224
- self.class.sharded_primary_key? ? self.class.hash ^ global_id.hash : super
330
+ self.class.sharded_primary_key? ? [self.class, global_id].hash : super
225
331
  end
226
332
 
227
333
  def to_param
@@ -242,8 +348,10 @@ module Switchman
242
348
 
243
349
  def id_for_database
244
350
  if self.class.sharded_primary_key?
245
- # It's an int, so so it's safe to just return it without passing it through anything else
246
- # In theory we should do `@attributes[@primary_key].type.serialize(id)`, but that seems to have surprising side-effects
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
247
355
  id
248
356
  else
249
357
  super
@@ -270,6 +378,16 @@ module Switchman
270
378
  self.class.connection_class_for_self
271
379
  end
272
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
273
391
  end
274
392
  end
275
393
  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 == '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
@@ -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['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,58 +106,104 @@ module Switchman
106
106
  compact_grouped_calculation_rows(rows, opts)
107
107
  end
108
108
 
109
- private
109
+ if ::Rails.version >= "7.1"
110
+ def ids
111
+ return super unless klass.sharded_primary_key?
110
112
 
111
- def type_cast_calculated_value_switchman(value, column_name, operation)
112
- if ::Rails.version < '7.0'
113
- type_cast_calculated_value(value, operation) do |val|
114
- column = aggregate_column(column_name)
115
- type ||= column.try(:type_caster) ||
116
- lookup_cast_type_from_join_dependencies(column_name.to_s) || ::ActiveRecord::Type.default_value
117
- type.deserialize(val)
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('.').last
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
162
  opts = { operation: operation, column_name: column_name, distinct: distinct }
133
163
 
134
- 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)
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
- 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
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
- # to_s is because Rails 5 returns a string but Rails 6 returns a symbol.
145
- 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
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: association, associated: associated,
150
- 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,
151
195
  group_fields: group_fields)
152
196
 
153
197
  opts
154
198
  end
155
199
 
156
- def aggregate_alias_for(operation, column_name)
157
- if operation == 'count' && column_name == :all
158
- 'count_all'
159
- elsif operation == 'average'
160
- '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}")
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] == 'average'
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
- 'count', opts[:distinct]
182
- ).as('count')
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] == 'average'
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['count']
212
- 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
213
260
  result[key] = [new_value, new_count]
214
261
  else
215
- result[key] = [value, row['count']]
262
+ result[key] = [value, row["count"]]
216
263
  end
217
264
  elsif result.key?(key)
218
265
  case opts[:operation]
219
- when 'count', 'sum'
266
+ when "count", "sum"
220
267
  result[key] += value
221
- when 'minimum'
268
+ when "minimum"
222
269
  result[key] = value if value < result[key]
223
- when 'maximum'
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] == 'average'
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,6 +3,25 @@
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
26
  connection unless @schemas
8
27
  # default shard will not switch databases immediately, so it won't be set yet
@@ -31,7 +50,9 @@ module Switchman
31
50
  end
32
51
 
33
52
  def switch_database(conn)
34
- @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !current_shard.database_server.config[:shard_name]
53
+ if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
54
+ @schemas = conn.current_schemas
55
+ end
35
56
 
36
57
  conn.shard = current_shard
37
58
  end
@@ -39,11 +60,11 @@ module Switchman
39
60
  private
40
61
 
41
62
  def current_shard
42
- ::Rails.version < '7.0' ? connection_klass.current_switchman_shard : connection_class.current_switchman_shard
63
+ connection_class.current_switchman_shard
43
64
  end
44
65
 
45
66
  def tls_key
46
- "#{object_id}_shard".to_sym
67
+ :"#{object_id}_shard"
47
68
  end
48
69
  end
49
70
  end