switchman 3.3.6 → 4.2.5

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 (47) 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 +10 -6
  6. data/lib/switchman/active_record/associations.rb +72 -49
  7. data/lib/switchman/active_record/attribute_methods.rb +89 -44
  8. data/lib/switchman/active_record/base.rb +109 -40
  9. data/lib/switchman/active_record/calculations.rb +90 -54
  10. data/lib/switchman/active_record/connection_handler.rb +18 -0
  11. data/lib/switchman/active_record/connection_pool.rb +41 -23
  12. data/lib/switchman/active_record/database_configurations.rb +23 -13
  13. data/lib/switchman/active_record/finder_methods.rb +20 -14
  14. data/lib/switchman/active_record/log_subscriber.rb +3 -6
  15. data/lib/switchman/active_record/migration.rb +35 -12
  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 +26 -17
  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 +87 -17
  24. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  25. data/lib/switchman/active_record/statement_cache.rb +4 -4
  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 +71 -25
  29. data/lib/switchman/active_support/cache.rb +9 -4
  30. data/lib/switchman/arel.rb +16 -25
  31. data/lib/switchman/call_super.rb +2 -8
  32. data/lib/switchman/database_server.rb +67 -48
  33. data/lib/switchman/default_shard.rb +14 -3
  34. data/lib/switchman/engine.rb +35 -23
  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 +168 -68
  41. data/lib/switchman/sharded_instrumenter.rb +9 -3
  42. data/lib/switchman/standard_error.rb +4 -0
  43. data/lib/switchman/test_helper.rb +3 -3
  44. data/lib/switchman/version.rb +1 -1
  45. data/lib/switchman.rb +27 -15
  46. data/lib/tasks/switchman.rake +96 -60
  47. metadata +28 -173
@@ -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,13 @@ 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 = ::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
62
63
  end
63
64
  end
64
65
 
@@ -67,7 +68,10 @@ module Switchman
67
68
  end
68
69
 
69
70
  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
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
71
75
 
72
76
  # Ensure we don't randomly surprise change the connection parms associated with a shard/role
73
77
  config_or_env = nil if config_or_env == ::Rails.env.to_sym
@@ -75,16 +79,18 @@ module Switchman
75
79
  config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
76
80
  :primary
77
81
  else
78
- "#{current_shard}/#{current_role}".to_sym
82
+ :"#{current_shard}/#{current_role}"
79
83
  end
80
84
 
81
- super(config_or_env)
85
+ super
82
86
  end
83
87
 
84
88
  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)
89
+ has_own_stack = ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
86
90
 
87
91
  ret = super
92
+ return ret if has_own_stack
93
+
88
94
  DatabaseServer.guard_servers
89
95
  ret
90
96
  end
@@ -96,10 +102,13 @@ module Switchman
96
102
  sharded_role = nil
97
103
  connected_to_stack.reverse_each do |hash|
98
104
  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
105
+ unless shard_role &&
106
+ (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
107
+ next
102
108
  end
109
+
110
+ sharded_role = shard_role
111
+ break
103
112
  end
104
113
  # Allow a shard-specific role to be reverted to regular inheritance
105
114
  return sharded_role if sharded_role && sharded_role != :_switchman_inherit
@@ -119,21 +128,25 @@ module Switchman
119
128
 
120
129
  def current_switchman_shard
121
130
  connected_to_stack.reverse_each do |hash|
122
- return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
131
+ if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
132
+ return hash[:switchman_shard]
133
+ end
123
134
  end
124
135
 
125
136
  Shard.default
126
137
  end
127
-
128
- if ::Rails.version < '7.0'
129
- def connection_class_for_self
130
- connection_classes
131
- end
132
- end
133
138
  end
134
139
 
135
140
  def self.prepended(klass)
136
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
+ }
137
150
  end
138
151
 
139
152
  def _run_initialize_callbacks
@@ -142,7 +155,17 @@ module Switchman
142
155
  else
143
156
  Shard.current(self.class.connection_class_for_self)
144
157
  end
145
- readonly! if shadow_record? && !Switchman.config[:writable_shadow_records]
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
164
+ super
165
+ end
166
+
167
+ def readonly!
168
+ @readonly_from_shadow = false
146
169
  super
147
170
  end
148
171
 
@@ -153,6 +176,10 @@ module Switchman
153
176
  pkey > Shard::IDS_PER_SHARD
154
177
  end
155
178
 
179
+ def canonical?
180
+ !shadow_record?
181
+ end
182
+
156
183
  def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
157
184
  return if target_shard == shard
158
185
 
@@ -165,31 +192,61 @@ module Switchman
165
192
  end
166
193
  end
167
194
  target_shard.activate do
168
- self.class.upsert(shadow_attrs, unique_by: self.class.primary_key)
195
+ self.class.upsert_all([shadow_attrs], unique_by: self.class.primary_key)
169
196
  end
170
197
  end
171
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
+
172
221
  def shard
173
- @shard || Shard.current(self.class.connection_class_for_self) || Shard.default
222
+ @shard || fallback_shard
174
223
  end
175
224
 
176
225
  def shard=(new_shard)
177
226
  raise ::ActiveRecord::ReadOnlyRecord if !new_record? || @shard_set_in_stone
178
227
 
179
- return if shard == new_shard
228
+ if shard == new_shard
229
+ @loaded_from_shard = new_shard
230
+ return
231
+ end
180
232
 
181
233
  attributes.each do |attr, value|
182
234
  self[attr] = Shard.relative_id_for(value, shard, new_shard) if self.class.sharded_column?(attr)
183
235
  end
236
+ @loaded_from_shard = new_shard
184
237
  @shard = new_shard
185
238
  end
186
239
 
187
240
  def save(*, **)
241
+ raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
242
+
188
243
  @shard_set_in_stone = true
189
244
  super
190
245
  end
191
246
 
192
247
  def save!(*, **)
248
+ raise Errors::ManuallyCreatedShadowRecordError if creating_shadow_record?
249
+
193
250
  @shard_set_in_stone = true
194
251
  super
195
252
  end
@@ -207,9 +264,9 @@ module Switchman
207
264
  result
208
265
  end
209
266
 
210
- def transaction(**kwargs, &block)
267
+ def transaction(...)
211
268
  shard.activate(self.class.connection_class_for_self) do
212
- self.class.transaction(**kwargs, &block)
269
+ self.class.transaction(...)
213
270
  end
214
271
  end
215
272
 
@@ -221,7 +278,7 @@ module Switchman
221
278
  end
222
279
 
223
280
  def hash
224
- self.class.sharded_primary_key? ? self.class.hash ^ global_id.hash : super
281
+ self.class.sharded_primary_key? ? [self.class, global_id].hash : super
225
282
  end
226
283
 
227
284
  def to_param
@@ -242,8 +299,10 @@ module Switchman
242
299
 
243
300
  def id_for_database
244
301
  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
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
247
306
  id
248
307
  else
249
308
  super
@@ -270,6 +329,16 @@ module Switchman
270
329
  self.class.connection_class_for_self
271
330
  end
272
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
273
342
  end
274
343
  end
275
344
  end
@@ -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,60 +106,95 @@ module Switchman
106
106
  compact_grouped_calculation_rows(rows, opts)
107
107
  end
108
108
 
109
- private
109
+ def ids
110
+ return super unless klass.sharded_primary_key?
110
111
 
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)
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) }
118
142
  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
143
  end
125
144
  end
126
145
 
146
+ private
147
+
148
+ def type_cast_calculated_value_switchman(value, column_name, operation)
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)
153
+ end
154
+
127
155
  def column_name_for(field)
128
- 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
129
157
  end
130
158
 
131
159
  def grouped_calculation_options(operation, column_name, distinct)
132
- 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)
133
163
 
134
- opts[:aggregate_alias] = aggregate_alias_for(operation, column_name)
164
+ opts[:aggregate_alias] = aggregate_alias_for(operation, column_name, column_alias_tracker)
135
165
  group_attrs = group_values
136
166
  if group_attrs.first.respond_to?(:to_sym)
137
167
  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
168
+ # only count belongs_to associations
169
+ associated = group_attrs.size == 1 && association && association.macro == :belongs_to
139
170
  group_fields = Array(associated ? association.foreign_key : group_attrs)
140
171
  else
141
172
  group_fields = group_attrs
142
173
  end
143
174
 
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 }
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
146
179
  group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
147
180
  [aliaz, type_for(field), column_name_for(field)]
148
181
  end
149
- opts.merge!(association: association, associated: associated,
150
- group_aliases: group_aliases, group_columns: group_columns,
151
- group_fields: group_fields)
182
+ opts.merge!(association:,
183
+ associated:,
184
+ group_aliases:,
185
+ group_columns:,
186
+ group_fields:)
152
187
 
153
188
  opts
154
189
  end
155
190
 
156
- def aggregate_alias_for(operation, column_name)
157
- if operation == 'count' && column_name == :all
158
- 'count_all'
159
- elsif operation == 'average'
160
- '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"
161
196
  else
162
- column_alias_for("#{operation} #{column_name}")
197
+ column_alias_tracker.alias_for("#{operation} #{column_name}")
163
198
  end
164
199
  end
165
200
 
@@ -173,13 +208,14 @@ module Switchman
173
208
  opts[:distinct]
174
209
  ).as(opts[:aggregate_alias])
175
210
  ]
176
- if opts[:operation] == 'average'
211
+ if opts[:operation] == "average"
177
212
  # include count in average so we can recalculate the average
178
213
  # across all shards if needed
179
214
  select_values << operation_over_aggregate_column(
180
215
  aggregate_column(opts[:column_name]),
181
- 'count', opts[:distinct]
182
- ).as('count')
216
+ "count",
217
+ opts[:distinct]
218
+ ).as("count")
183
219
  end
184
220
 
185
221
  haves = having_clause.send(:predicates)
@@ -205,22 +241,22 @@ module Switchman
205
241
  key = key.first if key.size == 1
206
242
  value = row[opts[:aggregate_alias]]
207
243
 
208
- if opts[:operation] == 'average'
244
+ if opts[:operation] == "average"
209
245
  if result.key?(key)
210
246
  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
247
+ new_count = old_count + row["count"]
248
+ new_value = ((old_value * old_count) + (value * row["count"])) / new_count
213
249
  result[key] = [new_value, new_count]
214
250
  else
215
- result[key] = [value, row['count']]
251
+ result[key] = [value, row["count"]]
216
252
  end
217
253
  elsif result.key?(key)
218
254
  case opts[:operation]
219
- when 'count', 'sum'
255
+ when "count", "sum"
220
256
  result[key] += value
221
- when 'minimum'
257
+ when "minimum"
222
258
  result[key] = value if value < result[key]
223
- when 'maximum'
259
+ when "maximum"
224
260
  result[key] = value if value > result[key]
225
261
  end
226
262
  else
@@ -228,7 +264,7 @@ module Switchman
228
264
  end
229
265
  end
230
266
 
231
- result.transform_values!(&:first) if opts[:operation] == 'average'
267
+ result.transform_values!(&:first) if opts[:operation] == "average"
232
268
 
233
269
  result
234
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
@@ -3,27 +3,11 @@
3
3
  module Switchman
4
4
  module ActiveRecord
5
5
  module ConnectionPool
6
- def get_schema_cache(connection)
7
- self.schema_cache ||= SharedSchemaCache.get_schema_cache(connection)
8
- self.schema_cache.connection = connection
9
-
10
- self.schema_cache
11
- end
12
-
13
- # rubocop:disable Naming/AccessorMethodName override method
14
- def set_schema_cache(cache)
15
- schema_cache = get_schema_cache(cache.connection)
16
-
17
- cache.instance_variables.each do |x|
18
- schema_cache.instance_variable_set(x, cache.instance_variable_get(x))
19
- end
20
- end
21
- # rubocop:enable Naming/AccessorMethodName override method
22
-
23
6
  def default_schema
24
- connection unless @schemas
7
+ connection_method = (::Rails.version < "7.2") ? :connection : :lease_connection
8
+ send(connection_method) unless @schemas
25
9
  # default shard will not switch databases immediately, so it won't be set yet
26
- @schemas ||= connection.current_schemas
10
+ @schemas ||= send(connection_method).current_schemas
27
11
  @schemas.first
28
12
  end
29
13
 
@@ -41,14 +25,44 @@ module Switchman
41
25
  conn
42
26
  end
43
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
+
44
56
  def release_connection(with_id = Thread.current)
45
- super(with_id)
57
+ super
46
58
 
47
59
  flush
48
60
  end
49
61
 
50
62
  def switch_database(conn)
51
- @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
52
66
 
53
67
  conn.shard = current_shard
54
68
  end
@@ -56,11 +70,15 @@ module Switchman
56
70
  private
57
71
 
58
72
  def current_shard
59
- ::Rails.version < '7.0' ? connection_klass.current_switchman_shard : connection_class.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
60
78
  end
61
79
 
62
80
  def tls_key
63
- "#{object_id}_shard".to_sym
81
+ :"#{object_id}_shard"
64
82
  end
65
83
  end
66
84
  end