switchman 3.3.1 → 4.1.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 (48) 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 +5 -3
  6. data/lib/switchman/active_record/associations.rb +91 -51
  7. data/lib/switchman/active_record/attribute_methods.rb +88 -43
  8. data/lib/switchman/active_record/base.rb +113 -40
  9. data/lib/switchman/active_record/calculations.rb +98 -51
  10. data/lib/switchman/active_record/connection_handler.rb +18 -0
  11. data/lib/switchman/active_record/connection_pool.rb +56 -6
  12. data/lib/switchman/active_record/database_configurations.rb +37 -15
  13. data/lib/switchman/active_record/finder_methods.rb +47 -17
  14. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  15. data/lib/switchman/active_record/migration.rb +51 -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 +37 -22
  19. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  20. data/lib/switchman/active_record/query_cache.rb +57 -20
  21. data/lib/switchman/active_record/query_methods.rb +148 -44
  22. data/lib/switchman/active_record/reflection.rb +9 -2
  23. data/lib/switchman/active_record/relation.rb +79 -15
  24. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  25. data/lib/switchman/active_record/statement_cache.rb +2 -2
  26. data/lib/switchman/active_record/table_definition.rb +1 -1
  27. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  28. data/lib/switchman/active_record/test_fixtures.rb +75 -25
  29. data/lib/switchman/active_support/cache.rb +9 -4
  30. data/lib/switchman/arel.rb +34 -18
  31. data/lib/switchman/call_super.rb +2 -8
  32. data/lib/switchman/database_server.rb +72 -34
  33. data/lib/switchman/default_shard.rb +14 -3
  34. data/lib/switchman/engine.rb +38 -22
  35. data/lib/switchman/environment.rb +2 -2
  36. data/lib/switchman/errors.rb +13 -0
  37. data/lib/switchman/guard_rail/relation.rb +1 -2
  38. data/lib/switchman/parallel.rb +6 -6
  39. data/lib/switchman/r_spec_helper.rb +12 -11
  40. data/lib/switchman/shard.rb +185 -71
  41. data/lib/switchman/sharded_instrumenter.rb +3 -3
  42. data/lib/switchman/shared_schema_cache.rb +11 -0
  43. data/lib/switchman/standard_error.rb +4 -0
  44. data/lib/switchman/test_helper.rb +3 -3
  45. data/lib/switchman/version.rb +1 -1
  46. data/lib/switchman.rb +27 -15
  47. data/lib/tasks/switchman.rake +96 -60
  48. metadata +50 -46
@@ -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
@@ -22,20 +22,16 @@ module Switchman
22
22
  @integral_id
23
23
  end
24
24
 
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
- else
34
- db = Shard.current(connection_class_for_self).database_server
35
- db.unguard { super }
36
- end
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 }
37
30
  end
38
- RUBY
31
+ else
32
+ db = Shard.current(connection_class_for_self).database_server
33
+ db.unguard { super }
34
+ end
39
35
  end
40
36
 
41
37
  def reset_column_information
@@ -57,8 +53,17 @@ module Switchman
57
53
  end
58
54
 
59
55
  def clear_query_caches_for_current_thread
60
- ::ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
61
- pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
56
+ pools = if ::Rails.version < "7.1"
57
+ ::ActiveRecord::Base.connection_handler.connection_pool_list
58
+ else
59
+ ::ActiveRecord::Base.connection_handler.connection_pool_list(:all)
60
+ end
61
+ pools.each do |pool|
62
+ if ::Rails.version < "7.2"
63
+ pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
64
+ elsif pool.active_connection?
65
+ pool.lease_connection(switch_shard: false).clear_query_cache
66
+ end
62
67
  end
63
68
  end
64
69
 
@@ -67,7 +72,10 @@ module Switchman
67
72
  end
68
73
 
69
74
  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
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
71
79
 
72
80
  # Ensure we don't randomly surprise change the connection parms associated with a shard/role
73
81
  config_or_env = nil if config_or_env == ::Rails.env.to_sym
@@ -75,16 +83,18 @@ module Switchman
75
83
  config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
76
84
  :primary
77
85
  else
78
- "#{current_shard}/#{current_role}".to_sym
86
+ :"#{current_shard}/#{current_role}"
79
87
  end
80
88
 
81
- super(config_or_env)
89
+ super
82
90
  end
83
91
 
84
92
  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)
93
+ has_own_stack = ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
86
94
 
87
95
  ret = super
96
+ return ret if has_own_stack
97
+
88
98
  DatabaseServer.guard_servers
89
99
  ret
90
100
  end
@@ -96,10 +106,13 @@ module Switchman
96
106
  sharded_role = nil
97
107
  connected_to_stack.reverse_each do |hash|
98
108
  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
109
+ unless shard_role &&
110
+ (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
111
+ next
102
112
  end
113
+
114
+ sharded_role = shard_role
115
+ break
103
116
  end
104
117
  # Allow a shard-specific role to be reverted to regular inheritance
105
118
  return sharded_role if sharded_role && sharded_role != :_switchman_inherit
@@ -119,21 +132,25 @@ module Switchman
119
132
 
120
133
  def current_switchman_shard
121
134
  connected_to_stack.reverse_each do |hash|
122
- return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
135
+ if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
136
+ return hash[:switchman_shard]
137
+ end
123
138
  end
124
139
 
125
140
  Shard.default
126
141
  end
127
-
128
- if ::Rails.version < '7.0'
129
- def connection_class_for_self
130
- connection_classes
131
- end
132
- end
133
142
  end
134
143
 
135
144
  def self.prepended(klass)
136
145
  klass.singleton_class.prepend(ClassMethods)
146
+ klass.singleton_class.prepend(Switchman::ActiveRecord::Relation::InsertUpsertAll) if ::Rails.version < "7.2"
147
+ klass.scope :non_shadow, lambda { |key = primary_key|
148
+ where(key => (QueryMethods::NonTransposingValue.new(0)..
149
+ QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)))
150
+ }
151
+ klass.scope :shadow, lambda { |key = primary_key|
152
+ where(key => QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)..)
153
+ }
137
154
  end
138
155
 
139
156
  def _run_initialize_callbacks
@@ -142,7 +159,17 @@ module Switchman
142
159
  else
143
160
  Shard.current(self.class.connection_class_for_self)
144
161
  end
145
- readonly! if shadow_record? && !Switchman.config[:writable_shadow_records]
162
+
163
+ @loaded_from_shard ||= Shard.current(self.class.connection_class_for_self)
164
+ if shadow_record? && !Switchman.config[:writable_shadow_records]
165
+ @readonly = true
166
+ @readonly_from_shadow ||= true
167
+ end
168
+ super
169
+ end
170
+
171
+ def readonly!
172
+ @readonly_from_shadow = false
146
173
  super
147
174
  end
148
175
 
@@ -153,6 +180,10 @@ module Switchman
153
180
  pkey > Shard::IDS_PER_SHARD
154
181
  end
155
182
 
183
+ def canonical?
184
+ !shadow_record?
185
+ end
186
+
156
187
  def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
157
188
  return if target_shard == shard
158
189
 
@@ -165,31 +196,61 @@ module Switchman
165
196
  end
166
197
  end
167
198
  target_shard.activate do
168
- self.class.upsert(shadow_attrs, unique_by: self.class.primary_key)
199
+ self.class.upsert_all([shadow_attrs], unique_by: self.class.primary_key)
169
200
  end
170
201
  end
171
202
 
203
+ def destroy_shadow_records(target_shards: [Shard.current])
204
+ raise Errors::ShadowRecordError, "Cannot be called on a shadow record." if shadow_record?
205
+
206
+ unless self.class.sharded_column?(self.class.primary_key)
207
+ raise Errors::MethodUnsupportedForUnshardedTableError,
208
+ "Cannot be called on a record belonging to an unsharded table."
209
+ end
210
+
211
+ Array(target_shards).each do |target_shard|
212
+ next if target_shard == shard
213
+
214
+ target_shard.activate { self.class.where("id = ?", global_id).delete_all }
215
+ end
216
+ end
217
+
218
+ # Returns "the shard that this record was actually loaded from" , as
219
+ # opposed to "the shard this record belongs on", which might be
220
+ # different if this is a shadow record.
221
+ def loaded_from_shard
222
+ @loaded_from_shard || shard
223
+ end
224
+
172
225
  def shard
173
- @shard || Shard.current(self.class.connection_class_for_self) || Shard.default
226
+ @shard || fallback_shard
174
227
  end
175
228
 
176
229
  def shard=(new_shard)
177
230
  raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
178
231
 
179
- return if shard == new_shard
232
+ if shard == new_shard
233
+ @loaded_from_shard = new_shard
234
+ return
235
+ end
180
236
 
181
237
  attributes.each do |attr, value|
182
238
  self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
183
239
  end
240
+ @loaded_from_shard = new_shard
184
241
  @shard = new_shard
185
242
  end
186
243
 
187
244
  def save(*, **)
245
+ raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
246
+
188
247
  @shard_set_in_stone = true
189
248
  super
190
249
  end
191
250
 
192
251
  def save!(*, **)
252
+ raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
253
+
193
254
  @shard_set_in_stone = true
194
255
  super
195
256
  end
@@ -207,9 +268,9 @@ module Switchman
207
268
  result
208
269
  end
209
270
 
210
- def transaction(**kwargs, &block)
271
+ def transaction(...)
211
272
  shard.activate(self.class.connection_class_for_self) do
212
- self.class.transaction(**kwargs, &block)
273
+ self.class.transaction(...)
213
274
  end
214
275
  end
215
276
 
@@ -221,7 +282,7 @@ module Switchman
221
282
  end
222
283
 
223
284
  def hash
224
- self.class.sharded_primary_key? ? self.class.hash ^ global_id.hash : super
285
+ self.class.sharded_primary_key? ? [self.class, global_id].hash : super
225
286
  end
226
287
 
227
288
  def to_param
@@ -242,8 +303,10 @@ module Switchman
242
303
 
243
304
  def id_for_database
244
305
  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
306
+ # It's an int, so it's safe to just return it without passing it
307
+ # through anything else. In theory we should do
308
+ # `@attributes[@primary_key].type.serialize(id)`, but that seems to
309
+ # have surprising side-effects
247
310
  id
248
311
  else
249
312
  super
@@ -270,6 +333,16 @@ module Switchman
270
333
  self.class.connection_class_for_self
271
334
  end
272
335
  end
336
+
337
+ private
338
+
339
+ def fallback_shard
340
+ Shard.current(self.class.connection_class_for_self) || Shard.default
341
+ end
342
+
343
+ def creating_shadow_record?
344
+ new_record? && shadow_record?
345
+ end
273
346
  end
274
347
  end
275
348
  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
- opts = { operation: operation, column_name: column_name, distinct: distinct }
162
+ opts = { operation:, column_name:, 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,
151
- group_fields: group_fields)
191
+ opts.merge!(association:,
192
+ associated:,
193
+ group_aliases:,
194
+ group_columns:,
195
+ 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,10 +3,30 @@
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
- connection unless @schemas
26
+ connection_method = (::Rails.version < "7.2") ? :connection : :lease_connection
27
+ send(connection_method) unless @schemas
8
28
  # default shard will not switch databases immediately, so it won't be set yet
9
- @schemas ||= connection.current_schemas
29
+ @schemas ||= send(connection_method).current_schemas
10
30
  @schemas.first
11
31
  end
12
32
 
@@ -24,14 +44,44 @@ module Switchman
24
44
  conn
25
45
  end
26
46
 
47
+ unless ::Rails.version < "7.2"
48
+ def active_connection(switch_shard: true)
49
+ conn = super()
50
+ return nil if conn.nil?
51
+ raise Errors::NonExistentShardError if current_shard.new_record?
52
+
53
+ switch_database(conn) if conn.shard != current_shard && switch_shard
54
+ conn
55
+ end
56
+
57
+ def lease_connection(switch_shard: true)
58
+ conn = super()
59
+ raise Errors::NonExistentShardError if current_shard.new_record?
60
+
61
+ switch_database(conn) if conn.shard != current_shard && switch_shard
62
+ conn
63
+ end
64
+
65
+ def with_connection(switch_shard: true, **kwargs)
66
+ super(**kwargs) do |conn|
67
+ raise Errors::NonExistentShardError if current_shard.new_record?
68
+
69
+ switch_database(conn) if conn.shard != current_shard && switch_shard
70
+ yield conn
71
+ end
72
+ end
73
+ end
74
+
27
75
  def release_connection(with_id = Thread.current)
28
- super(with_id)
76
+ super
29
77
 
30
78
  flush
31
79
  end
32
80
 
33
81
  def switch_database(conn)
34
- @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !current_shard.database_server.config[:shard_name]
82
+ if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
83
+ @schemas = conn.current_schemas
84
+ end
35
85
 
36
86
  conn.shard = current_shard
37
87
  end
@@ -39,11 +89,11 @@ module Switchman
39
89
  private
40
90
 
41
91
  def current_shard
42
- ::Rails.version < '7.0' ? connection_klass.current_switchman_shard : connection_class.current_switchman_shard
92
+ connection_class.current_switchman_shard
43
93
  end
44
94
 
45
95
  def tls_key
46
- "#{object_id}_shard".to_sym
96
+ :"#{object_id}_shard"
47
97
  end
48
98
  end
49
99
  end