switchman 3.0.14 → 3.5.20

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 (54) 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/action_controller/caching.rb +2 -2
  6. data/lib/switchman/active_record/abstract_adapter.rb +6 -6
  7. data/lib/switchman/active_record/associations.rb +365 -0
  8. data/lib/switchman/active_record/attribute_methods.rb +188 -99
  9. data/lib/switchman/active_record/base.rb +185 -40
  10. data/lib/switchman/active_record/calculations.rb +64 -40
  11. data/lib/switchman/active_record/connection_handler.rb +18 -0
  12. data/lib/switchman/active_record/connection_pool.rb +24 -5
  13. data/lib/switchman/active_record/database_configurations.rb +37 -13
  14. data/lib/switchman/active_record/finder_methods.rb +46 -16
  15. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  16. data/lib/switchman/active_record/migration.rb +52 -8
  17. data/lib/switchman/active_record/model_schema.rb +1 -1
  18. data/lib/switchman/active_record/persistence.rb +31 -3
  19. data/lib/switchman/active_record/postgresql_adapter.rb +11 -10
  20. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  21. data/lib/switchman/active_record/query_cache.rb +49 -20
  22. data/lib/switchman/active_record/query_methods.rb +187 -136
  23. data/lib/switchman/active_record/reflection.rb +1 -1
  24. data/lib/switchman/active_record/relation.rb +33 -26
  25. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  26. data/lib/switchman/active_record/statement_cache.rb +11 -7
  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 +26 -16
  30. data/lib/switchman/active_support/cache.rb +20 -1
  31. data/lib/switchman/arel.rb +34 -18
  32. data/lib/switchman/call_super.rb +8 -2
  33. data/lib/switchman/database_server.rb +91 -45
  34. data/lib/switchman/default_shard.rb +14 -5
  35. data/lib/switchman/engine.rb +79 -126
  36. data/lib/switchman/environment.rb +2 -2
  37. data/lib/switchman/errors.rb +17 -2
  38. data/lib/switchman/guard_rail/relation.rb +8 -10
  39. data/lib/switchman/guard_rail.rb +5 -0
  40. data/lib/switchman/parallel.rb +68 -0
  41. data/lib/switchman/r_spec_helper.rb +14 -11
  42. data/lib/switchman/rails.rb +2 -5
  43. data/{app/models → lib}/switchman/shard.rb +186 -189
  44. data/lib/switchman/sharded_instrumenter.rb +5 -1
  45. data/lib/switchman/shared_schema_cache.rb +11 -0
  46. data/lib/switchman/standard_error.rb +6 -5
  47. data/lib/switchman/test_helper.rb +2 -2
  48. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  49. data/lib/switchman/version.rb +1 -1
  50. data/lib/switchman.rb +44 -12
  51. data/lib/tasks/switchman.rake +74 -53
  52. metadata +42 -53
  53. data/lib/switchman/active_record/association.rb +0 -206
  54. 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
 
@@ -24,24 +22,20 @@ module Switchman
24
22
  @integral_id
25
23
  end
26
24
 
27
- def transaction(**)
28
- if self != ::ActiveRecord::Base && current_scope
29
- current_scope.activate do
30
- db = Shard.current(connection_classes).database_server
31
- if ::GuardRail.environment == db.guard_rail_environment
32
- super
25
+ %w[transaction insert_all upsert_all].each do |method|
26
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
27
+ def #{method}(*, **)
28
+ if self != ::ActiveRecord::Base && current_scope
29
+ current_scope.activate do
30
+ db = Shard.current(connection_class_for_self).database_server
31
+ db.unguard { super }
32
+ end
33
33
  else
34
+ db = Shard.current(connection_class_for_self).database_server
34
35
  db.unguard { super }
35
36
  end
36
37
  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
38
+ RUBY
45
39
  end
46
40
 
47
41
  def reset_column_information
@@ -63,16 +57,78 @@ module Switchman
63
57
  end
64
58
 
65
59
  def clear_query_caches_for_current_thread
66
- ::ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
60
+ pools = if ::Rails.version < "7.1"
61
+ ::ActiveRecord::Base.connection_handler.connection_pool_list
62
+ else
63
+ ::ActiveRecord::Base.connection_handler.connection_pool_list(:all)
64
+ end
65
+ pools.each do |pool|
67
66
  pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
68
67
  end
69
68
  end
70
69
 
70
+ def role_overriden?(shard_id)
71
+ current_role(target_shard: shard_id) != current_role(without_overrides: true)
72
+ end
73
+
74
+ def establish_connection(config_or_env = nil)
75
+ if config_or_env.is_a?(Symbol) && config_or_env != ::Rails.env.to_sym
76
+ raise ArgumentError,
77
+ "establish connection cannot be used on the non-current shard/role"
78
+ end
79
+
80
+ # Ensure we don't randomly surprise change the connection parms associated with a shard/role
81
+ config_or_env = nil if config_or_env == ::Rails.env.to_sym
82
+
83
+ config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
84
+ :primary
85
+ else
86
+ "#{current_shard}/#{current_role}".to_sym
87
+ end
88
+
89
+ super(config_or_env)
90
+ end
91
+
92
+ def connected_to_stack
93
+ has_own_stack = if ::Rails.version < "7.0"
94
+ Thread.current.thread_variable?(:ar_connected_to_stack)
95
+ else
96
+ ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
97
+ end
98
+
99
+ ret = super
100
+ return ret if has_own_stack
101
+
102
+ DatabaseServer.guard_servers
103
+ ret
104
+ end
105
+
106
+ # significant change: Allow per-shard roles
107
+ def current_role(without_overrides: false, target_shard: current_shard)
108
+ return super() if without_overrides
109
+
110
+ sharded_role = nil
111
+ connected_to_stack.reverse_each do |hash|
112
+ shard_role = hash.dig(:shard_roles, target_shard)
113
+ unless shard_role &&
114
+ (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
115
+ next
116
+ end
117
+
118
+ sharded_role = shard_role
119
+ break
120
+ end
121
+ # Allow a shard-specific role to be reverted to regular inheritance
122
+ return sharded_role if sharded_role && sharded_role != :_switchman_inherit
123
+
124
+ super()
125
+ end
126
+
71
127
  # significant change: _don't_ check if klasses.include?(Base)
72
128
  # i.e. other sharded models don't inherit the current shard of Base
73
129
  def current_shard
74
130
  connected_to_stack.reverse_each do |hash|
75
- return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_classes)
131
+ return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_class_for_self)
76
132
  end
77
133
 
78
134
  default_shard
@@ -80,53 +136,131 @@ module Switchman
80
136
 
81
137
  def current_switchman_shard
82
138
  connected_to_stack.reverse_each do |hash|
83
- return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_classes)
139
+ if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
140
+ return hash[:switchman_shard]
141
+ end
84
142
  end
85
143
 
86
144
  Shard.default
87
145
  end
146
+
147
+ if ::Rails.version < "7.0"
148
+ def connection_class_for_self
149
+ connection_classes
150
+ end
151
+ end
88
152
  end
89
153
 
90
154
  def self.prepended(klass)
91
155
  klass.singleton_class.prepend(ClassMethods)
156
+ klass.scope :non_shadow, ->(key = primary_key) { where("#{key}<=? AND #{key}>?", Shard::IDS_PER_SHARD, 0) }
157
+ klass.scope :shadow, ->(key = primary_key) { where("#{key}>?", Shard::IDS_PER_SHARD) }
92
158
  end
93
159
 
94
160
  def _run_initialize_callbacks
95
161
  @shard ||= if self.class.sharded_primary_key?
96
- Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_classes))
162
+ Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_class_for_self))
97
163
  else
98
- Shard.current(self.class.connection_classes)
164
+ Shard.current(self.class.connection_class_for_self)
99
165
  end
166
+
167
+ @loaded_from_shard ||= Shard.current(self.class.connection_class_for_self)
168
+ if shadow_record? && !Switchman.config[:writable_shadow_records]
169
+ @readonly = true
170
+ @readonly_from_shadow ||= true
171
+ end
100
172
  super
101
173
  end
102
174
 
175
+ def readonly!
176
+ @readonly_from_shadow = false
177
+ super
178
+ end
179
+
180
+ def shadow_record?
181
+ pkey = self[self.class.primary_key]
182
+ return false unless self.class.sharded_column?(self.class.primary_key) && pkey
183
+
184
+ pkey > Shard::IDS_PER_SHARD
185
+ end
186
+
187
+ def canonical?
188
+ !shadow_record?
189
+ end
190
+
191
+ def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
192
+ return if target_shard == shard
193
+
194
+ shadow_attrs = {}
195
+ new_attrs.each do |attr, value|
196
+ shadow_attrs[attr] = if self.class.sharded_column?(attr)
197
+ Shard.relative_id_for(value, shard, target_shard)
198
+ else
199
+ value
200
+ end
201
+ end
202
+ target_shard.activate do
203
+ self.class.upsert(shadow_attrs, unique_by: self.class.primary_key)
204
+ end
205
+ end
206
+
207
+ def destroy_shadow_records(target_shards: [Shard.current])
208
+ raise Errors::ShadowRecordError, "Cannot be called on a shadow record." if shadow_record?
209
+
210
+ unless self.class.sharded_column?(self.class.primary_key)
211
+ raise Errors::MethodUnsupportedForUnshardedTableError,
212
+ "Cannot be called on a record belonging to an unsharded table."
213
+ end
214
+
215
+ Array(target_shards).each do |target_shard|
216
+ next if target_shard == shard
217
+
218
+ target_shard.activate { self.class.where("id = ?", global_id).delete_all }
219
+ end
220
+ end
221
+
222
+ # Returns "the shard that this record was actually loaded from" , as
223
+ # opposed to "the shard this record belongs on", which might be
224
+ # different if this is a shadow record.
225
+ def loaded_from_shard
226
+ @loaded_from_shard || shard
227
+ end
228
+
103
229
  def shard
104
- @shard || Shard.current(self.class.connection_classes) || Shard.default
230
+ @shard || fallback_shard
105
231
  end
106
232
 
107
233
  def shard=(new_shard)
108
234
  raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
109
235
 
110
- return if shard == new_shard
236
+ if shard == new_shard
237
+ @loaded_from_shard = new_shard
238
+ return
239
+ end
111
240
 
112
241
  attributes.each do |attr, value|
113
242
  self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
114
243
  end
244
+ @loaded_from_shard = new_shard
115
245
  @shard = new_shard
116
246
  end
117
247
 
118
248
  def save(*, **)
249
+ raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
250
+
119
251
  @shard_set_in_stone = true
120
252
  super
121
253
  end
122
254
 
123
255
  def save!(*, **)
256
+ raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
257
+
124
258
  @shard_set_in_stone = true
125
259
  super
126
260
  end
127
261
 
128
262
  def destroy
129
- shard.activate(self.class.connection_classes) { super }
263
+ shard.activate(self.class.connection_class_for_self) { super }
130
264
  end
131
265
 
132
266
  def clone
@@ -138,20 +272,21 @@ module Switchman
138
272
  result
139
273
  end
140
274
 
141
- def transaction(**kwargs, &block)
142
- shard.activate(self.class.connection_classes) do
143
- self.class.transaction(**kwargs, &block)
275
+ def transaction(...)
276
+ shard.activate(self.class.connection_class_for_self) do
277
+ self.class.transaction(...)
144
278
  end
145
279
  end
146
280
 
147
281
  def with_transaction_returning_status
148
- shard.activate(self.class.connection_classes) do
149
- super
282
+ shard.activate(self.class.connection_class_for_self) do
283
+ db = Shard.current(self.class.connection_class_for_self).database_server
284
+ db.unguard { super }
150
285
  end
151
286
  end
152
287
 
153
288
  def hash
154
- self.class.sharded_primary_key? ? self.class.hash ^ global_id.hash : super
289
+ self.class.sharded_primary_key? ? [self.class, global_id].hash : super
155
290
  end
156
291
 
157
292
  def to_param
@@ -167,15 +302,15 @@ module Switchman
167
302
 
168
303
  def update_columns(*)
169
304
  db = shard.database_server
170
- return db.unguard { super } if ::GuardRail.environment != db.guard_rail_environment
171
-
172
- super
305
+ db.unguard { super }
173
306
  end
174
307
 
175
308
  def id_for_database
176
309
  if self.class.sharded_primary_key?
177
- # It's an int, so so it's safe to just return it without passing it through anything else
178
- # In theory we should do `@attributes[@primary_key].type.serialize(id)`, but that seems to have surprising side-effects
310
+ # It's an int, so it's safe to just return it without passing it
311
+ # through anything else. In theory we should do
312
+ # `@attributes[@primary_key].type.serialize(id)`, but that seems to
313
+ # have surprising side-effects
179
314
  id
180
315
  else
181
316
  super
@@ -184,24 +319,34 @@ module Switchman
184
319
 
185
320
  protected
186
321
 
187
- # see also AttributeMethods#connection_classes_code_for_reflection
188
- def connection_classes_for_reflection(reflection)
322
+ # see also AttributeMethods#connection_class_for_self_code_for_reflection
323
+ def connection_class_for_self_for_reflection(reflection)
189
324
  if reflection
190
325
  if reflection.options[:polymorphic]
191
326
  begin
192
- read_attribute(reflection.foreign_type)&.constantize&.connection_classes || ::ActiveRecord::Base
327
+ read_attribute(reflection.foreign_type)&.constantize&.connection_class_for_self || ::ActiveRecord::Base
193
328
  rescue NameError
194
329
  # in case someone is abusing foreign_type to not point to an actual class
195
330
  ::ActiveRecord::Base
196
331
  end
197
332
  else
198
333
  # otherwise we can just return a symbol for the statically known type of the association
199
- reflection.klass.connection_classes
334
+ reflection.klass.connection_class_for_self
200
335
  end
201
336
  else
202
- self.class.connection_classes
337
+ self.class.connection_class_for_self
203
338
  end
204
339
  end
340
+
341
+ private
342
+
343
+ def fallback_shard
344
+ Shard.current(self.class.connection_class_for_self) || Shard.default
345
+ end
346
+
347
+ def creating_shadow_record?
348
+ new_record? && shadow_record?
349
+ end
205
350
  end
206
351
  end
207
352
  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
@@ -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)
@@ -109,48 +109,71 @@ module Switchman
109
109
  private
110
110
 
111
111
  def type_cast_calculated_value_switchman(value, column_name, operation)
112
- type_cast_calculated_value(value, operation) do |val|
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)
118
+ end
119
+ else
113
120
  column = aggregate_column(column_name)
114
121
  type ||= column.try(:type_caster) ||
115
- lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
116
- type.deserialize(val)
122
+ lookup_cast_type_from_join_dependencies(column_name.to_s) || ::ActiveRecord::Type.default_value
123
+ type_cast_calculated_value(value, operation, type)
117
124
  end
118
125
  end
119
126
 
120
127
  def column_name_for(field)
121
- field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
128
+ field.respond_to?(:name) ? field.name.to_s : field.to_s.split(".").last
122
129
  end
123
130
 
124
131
  def grouped_calculation_options(operation, column_name, distinct)
125
132
  opts = { operation: operation, column_name: column_name, distinct: distinct }
126
133
 
127
- opts[:aggregate_alias] = aggregate_alias_for(operation, column_name)
134
+ # Rails 7.0.5
135
+ if defined?(::ActiveRecord::Calculations::ColumnAliasTracker)
136
+ column_alias_tracker = ::ActiveRecord::Calculations::ColumnAliasTracker.new(connection)
137
+ end
138
+
139
+ opts[:aggregate_alias] = aggregate_alias_for(operation, column_name, column_alias_tracker)
128
140
  group_attrs = group_values
129
141
  if group_attrs.first.respond_to?(:to_sym)
130
142
  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
143
+ # only count belongs_to associations
144
+ associated = group_attrs.size == 1 && association && association.macro == :belongs_to
132
145
  group_fields = Array(associated ? association.foreign_key : group_attrs)
133
146
  else
134
147
  group_fields = group_attrs
135
148
  end
136
149
 
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 }
150
+ group_aliases = group_fields.map do |field|
151
+ field = connection.visitor.compile(field) if ::Arel.arel_node?(field)
152
+ if column_alias_tracker
153
+ column_alias_tracker.alias_for(field.to_s.downcase)
154
+ else
155
+ column_alias_for(field.to_s.downcase)
156
+ end
157
+ end
139
158
  group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
140
159
  [aliaz, type_for(field), column_name_for(field)]
141
160
  end
142
- opts.merge!(association: association, associated: associated,
143
- group_aliases: group_aliases, group_columns: group_columns,
161
+ opts.merge!(association: association,
162
+ associated: associated,
163
+ group_aliases: group_aliases,
164
+ group_columns: group_columns,
144
165
  group_fields: group_fields)
145
166
 
146
167
  opts
147
168
  end
148
169
 
149
- def aggregate_alias_for(operation, column_name)
150
- if operation == 'count' && column_name == :all
151
- 'count_all'
152
- elsif operation == 'average'
153
- 'average'
170
+ def aggregate_alias_for(operation, column_name, column_alias_tracker)
171
+ if operation == "count" && column_name == :all
172
+ "count_all"
173
+ elsif operation == "average"
174
+ "average"
175
+ elsif column_alias_tracker
176
+ column_alias_tracker.alias_for("#{operation} #{column_name}")
154
177
  else
155
178
  column_alias_for("#{operation} #{column_name}")
156
179
  end
@@ -166,13 +189,14 @@ module Switchman
166
189
  opts[:distinct]
167
190
  ).as(opts[:aggregate_alias])
168
191
  ]
169
- if opts[:operation] == 'average'
192
+ if opts[:operation] == "average"
170
193
  # include count in average so we can recalculate the average
171
194
  # across all shards if needed
172
195
  select_values << operation_over_aggregate_column(
173
196
  aggregate_column(opts[:column_name]),
174
- 'count', opts[:distinct]
175
- ).as('count')
197
+ "count",
198
+ opts[:distinct]
199
+ ).as("count")
176
200
  end
177
201
 
178
202
  haves = having_clause.send(:predicates)
@@ -198,22 +222,22 @@ module Switchman
198
222
  key = key.first if key.size == 1
199
223
  value = row[opts[:aggregate_alias]]
200
224
 
201
- if opts[:operation] == 'average'
225
+ if opts[:operation] == "average"
202
226
  if result.key?(key)
203
227
  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
228
+ new_count = old_count + row["count"]
229
+ new_value = ((old_value * old_count) + (value * row["count"])) / new_count
206
230
  result[key] = [new_value, new_count]
207
231
  else
208
- result[key] = [value, row['count']]
232
+ result[key] = [value, row["count"]]
209
233
  end
210
234
  elsif result.key?(key)
211
235
  case opts[:operation]
212
- when 'count', 'sum'
236
+ when "count", "sum"
213
237
  result[key] += value
214
- when 'minimum'
238
+ when "minimum"
215
239
  result[key] = value if value < result[key]
216
- when 'maximum'
240
+ when "maximum"
217
241
  result[key] = value if value > result[key]
218
242
  end
219
243
  else
@@ -221,7 +245,7 @@ module Switchman
221
245
  end
222
246
  end
223
247
 
224
- result.transform_values!(&:first) if opts[:operation] == 'average'
248
+ result.transform_values!(&:first) if opts[:operation] == "average"
225
249
 
226
250
  result
227
251
  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
@@ -1,10 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'switchman/errors'
4
-
5
3
  module Switchman
6
4
  module ActiveRecord
7
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
+
8
25
  def default_schema
9
26
  connection unless @schemas
10
27
  # default shard will not switch databases immediately, so it won't be set yet
@@ -20,7 +37,7 @@ module Switchman
20
37
 
21
38
  def connection(switch_shard: true)
22
39
  conn = super()
23
- raise NonExistentShardError if current_shard.new_record?
40
+ raise Errors::NonExistentShardError if current_shard.new_record?
24
41
 
25
42
  switch_database(conn) if conn.shard != current_shard && switch_shard
26
43
  conn
@@ -33,7 +50,9 @@ module Switchman
33
50
  end
34
51
 
35
52
  def switch_database(conn)
36
- @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
37
56
 
38
57
  conn.shard = current_shard
39
58
  end
@@ -41,7 +60,7 @@ module Switchman
41
60
  private
42
61
 
43
62
  def current_shard
44
- connection_klass.current_switchman_shard
63
+ (::Rails.version < "7.0") ? connection_klass.current_switchman_shard : connection_class.current_switchman_shard
45
64
  end
46
65
 
47
66
  def tls_key