switchman 3.4.1 → 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 +71 -48
  7. data/lib/switchman/active_record/attribute_methods.rb +84 -37
  8. data/lib/switchman/active_record/base.rb +72 -41
  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 +19 -19
  16. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  17. data/lib/switchman/active_record/persistence.rb +22 -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 +86 -16
  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 -24
  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 +4 -1
  37. data/lib/switchman/guard_rail/relation.rb +1 -2
  38. data/lib/switchman/parallel.rb +5 -5
  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 -187
@@ -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
@@ -144,7 +157,15 @@ module Switchman
144
157
  end
145
158
 
146
159
  @loaded_from_shard ||= Shard.current(self.class.connection_class_for_self)
147
- readonly! if shadow_record? && !Switchman.config[:writable_shadow_records]
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
148
169
  super
149
170
  end
150
171
 
@@ -155,6 +176,10 @@ module Switchman
155
176
  pkey > Shard::IDS_PER_SHARD
156
177
  end
157
178
 
179
+ def canonical?
180
+ !shadow_record?
181
+ end
182
+
158
183
  def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
159
184
  return if target_shard == shard
160
185
 
@@ -167,18 +192,22 @@ module Switchman
167
192
  end
168
193
  end
169
194
  target_shard.activate do
170
- self.class.upsert(shadow_attrs, unique_by: self.class.primary_key)
195
+ self.class.upsert_all([shadow_attrs], unique_by: self.class.primary_key)
171
196
  end
172
197
  end
173
198
 
174
199
  def destroy_shadow_records(target_shards: [Shard.current])
175
- raise Errors::ShadowRecordError, 'Cannot be called on a shadow record.' if shadow_record?
176
- raise Errors::MethodUnsupportedForUnshardedTableError, 'Cannot be called on a record belonging to an unsharded table.' unless self.class.sharded_column?(self.class.primary_key)
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
177
206
 
178
207
  Array(target_shards).each do |target_shard|
179
208
  next if target_shard == shard
180
209
 
181
- target_shard.activate { self.class.where('id = ?', global_id).delete_all }
210
+ target_shard.activate { self.class.where("id = ?", global_id).delete_all }
182
211
  end
183
212
  end
184
213
 
@@ -186,7 +215,7 @@ module Switchman
186
215
  # opposed to "the shard this record belongs on", which might be
187
216
  # different if this is a shadow record.
188
217
  def loaded_from_shard
189
- @loaded_from_shard || fallback_shard
218
+ @loaded_from_shard || shard
190
219
  end
191
220
 
192
221
  def shard
@@ -235,9 +264,9 @@ module Switchman
235
264
  result
236
265
  end
237
266
 
238
- def transaction(**kwargs, &block)
267
+ def transaction(...)
239
268
  shard.activate(self.class.connection_class_for_self) do
240
- self.class.transaction(**kwargs, &block)
269
+ self.class.transaction(...)
241
270
  end
242
271
  end
243
272
 
@@ -270,8 +299,10 @@ module Switchman
270
299
 
271
300
  def id_for_database
272
301
  if self.class.sharded_primary_key?
273
- # It's an int, so so it's safe to just return it without passing it through anything else
274
- # 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
275
306
  id
276
307
  else
277
308
  super
@@ -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
@@ -7,12 +7,12 @@ module Switchman
7
7
  # since all should point to the same data, even if multiple are writable
8
8
  # (Picks 'primary' since it is guaranteed to exist and switchman handles activating
9
9
  # deploy through other means)
10
- def configs_for(include_replicas: false, name: nil, **)
10
+ def configs_for(include_hidden: false, name: nil, **)
11
11
  res = super
12
- if name && !include_replicas
13
- return nil unless name.end_with?('primary')
14
- elsif !include_replicas
15
- return res.select { |config| config.name.end_with?('primary') }
12
+ if name && !include_hidden
13
+ return nil unless name.end_with?("primary")
14
+ elsif !include_hidden
15
+ return res.select { |config| config.name.end_with?("primary") }
16
16
  end
17
17
  res
18
18
  end
@@ -27,21 +27,31 @@ module Switchman
27
27
  return configs if configs.is_a?(Array)
28
28
 
29
29
  db_configs = configs.flat_map do |env_name, config|
30
- # It would be nice to do the auto-fallback that we want here, but we haven't
31
- # actually done that for years (or maybe ever) and it will be a big lift to get working
32
- roles = config.keys.select { |k| config[k].is_a?(Hash) || (config[k].is_a?(Array) && config[k].all? { |ck| ck.is_a?(Hash) }) }
33
- base_config = config.except(*roles)
30
+ if config.is_a?(Hash)
31
+ # It would be nice to do the auto-fallback that we want here, but we haven't
32
+ # actually done that for years (or maybe ever) and it will be a big lift to get working
33
+ roles = config.keys.select do |k|
34
+ config[k].is_a?(Hash) || (config[k].is_a?(Array) && config[k].all?(Hash))
35
+ end
36
+ base_config = config.except(*roles)
37
+ else
38
+ base_config = config
39
+ roles = []
40
+ end
34
41
 
35
42
  name = "#{env_name}/primary"
36
- name = 'primary' if env_name == default_env
43
+ name = "primary" if env_name == default_env
37
44
  base_db = build_db_config_from_raw_config(env_name, name, base_config)
38
45
  [base_db] + roles.map do |role|
39
- build_db_config_from_raw_config(env_name, "#{env_name}/#{role}",
40
- base_config.merge(config[role].is_a?(Array) ? config[role].first : config[role]))
46
+ build_db_config_from_raw_config(
47
+ env_name,
48
+ "#{env_name}/#{role}",
49
+ base_config.merge(config[role].is_a?(Array) ? config[role].first : config[role])
50
+ )
41
51
  end
42
52
  end
43
53
 
44
- db_configs << environment_url_config(default_env, 'primary', {}) unless db_configs.find(&:for_current_env?)
54
+ db_configs << environment_url_config(default_env, "primary", {}) unless db_configs.find(&:for_current_env?)
45
55
 
46
56
  merge_db_environment_variables(default_env, db_configs.compact)
47
57
  end