switchman 4.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd3fc41cd455d5855db84fd49d132dca58c824dbe50e49ea4ab7c4cd0ee24d00
4
- data.tar.gz: df1df2b43f521a5b5e424165ac167d2de351c4800db6fe50b5d83a78e8e17e16
3
+ metadata.gz: e03940e1a5f3d0e7b933d99b3cbd68e3998b55f14ac0e7eec91ef81a193445e1
4
+ data.tar.gz: 923793689418332ab6ce2ffef2981567bd74b6358916794d958172638b0ec160
5
5
  SHA512:
6
- metadata.gz: 5b721c57764dc47c2da10b027b63fdeed4795c349b2dec36b0d440e98ff0294355a4bbb03d2176f30883de0b842e2f4a47f27ea2fa2f56405594f53d4a278f6c
7
- data.tar.gz: 2729dfb39b1e3d8e3df84bcf1bd321ac39de73e552d8a6c0f15f48f6b9fdb47611a076f8e11fbcbfeb4fd8097c2e6a49a2eab7d76942af92ed769c03b1b9f7d0
6
+ metadata.gz: 6950ac12cf6532f98e3674ded8890e7bb2f9ef50f33657220096de50508715ab047250c163546dc7396c61d15197ee2b517ae342f84cbe007ae22a217ed7751f
7
+ data.tar.gz: c45df067998ec5868bfe2c6a0f52608ffd9de9a19e2ce55ba55fc9986e077833df9964c5783c0f394a9683cb21a610e7f2a781d88e690fbc2bfeef5e8e352826
@@ -5,7 +5,7 @@ module Switchman
5
5
  module AbstractAdapter
6
6
  module ForeignKeyCheck
7
7
  def add_column(table, name, type, limit: nil, **)
8
- Switchman.foreign_key_check(name, type, limit: limit)
8
+ Switchman.foreign_key_check(name, type, limit:)
9
9
  super
10
10
  end
11
11
  end
@@ -174,7 +174,7 @@ module Switchman
174
174
  end
175
175
 
176
176
  # Disabling to keep closer to rails original
177
- # rubocop:disable Naming/AccessorMethodName, Style/GuardClause
177
+ # rubocop:disable Naming/AccessorMethodName
178
178
  # significant changes:
179
179
  # * globalize the key to lookup
180
180
  def set_inverse(record)
@@ -191,7 +191,7 @@ module Switchman
191
191
  association.set_inverse_instance(record)
192
192
  end
193
193
  end
194
- # rubocop:enable Naming/AccessorMethodName, Style/GuardClause
194
+ # rubocop:enable Naming/AccessorMethodName
195
195
 
196
196
  # significant changes:
197
197
  # * partition_by_shard the records_for call
@@ -59,9 +59,9 @@ module Switchman
59
59
  if ::Rails.version < "7.1.4"
60
60
  # https://github.com/rails/rails/commit/a2a12fc2e3f4e6d06f81d4c74c88f8e6b3369ee6#diff-5b59ece6d9396b596f06271cec0ea726e3360911383511c49b1a66f454bfc2b6L30
61
61
  # These arguments were effectively swapped in Rails 7.1.4, so previous versions need them reversed
62
- owner.define_cached_method(as, namespace: namespace, as: name, &block)
62
+ owner.define_cached_method(as, namespace:, as: name, &block)
63
63
  else
64
- owner.define_cached_method(name, namespace: namespace, as: as, &block)
64
+ owner.define_cached_method(name, namespace:, as:, &block)
65
65
  end
66
66
  end
67
67
 
@@ -136,14 +136,14 @@ module Switchman
136
136
  safe_class_name = class_name.unpack1("h*")
137
137
  define_cached_method(owner,
138
138
  "sharded_#{safe_class_name}_#{attr_name}",
139
- as: as,
139
+ as:,
140
140
  namespace: :switchman) do |batch|
141
141
  batch << build_sharded_getter("sharded_#{safe_class_name}_#{attr_name}",
142
142
  "original_#{as}",
143
143
  class_name)
144
144
  end
145
145
  else
146
- define_cached_method(owner, "plain_#{attr_name}", as: as, namespace: :switchman) do |batch|
146
+ define_cached_method(owner, "plain_#{attr_name}", as:, namespace: :switchman) do |batch|
147
147
  batch << <<-RUBY
148
148
  def plain_#{attr_name}
149
149
  _read_attribute("#{attr_name}") { |n| missing_attribute(n, caller) }
@@ -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
@@ -34,56 +34,6 @@ module Switchman
34
34
  end
35
35
  end
36
36
 
37
- # NOTE: `returning` values are _not_ transposed back to the current shard
38
- %w[insert_all upsert_all].each do |method|
39
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
40
- def #{method}(attributes, returning: nil, **)
41
- scope = self != ::ActiveRecord::Base && current_scope
42
- if (target_shard = scope&.primary_shard) == (current_shard = Shard.current(connection_class_for_self))
43
- scope = nil
44
- end
45
- if scope
46
- dupped = false
47
- attributes.each_with_index do |hash, i|
48
- if dupped || hash.any? { |k, v| sharded_column?(k) }
49
- unless dupped
50
- attributes = attributes.dup
51
- dupped = true
52
- end
53
- attributes[i] = hash.to_h do |k, v|
54
- if sharded_column?(k)
55
- [k, Shard.relative_id_for(v, current_shard, target_shard)]
56
- else
57
- [k, v]
58
- end
59
- end
60
- end
61
- end
62
- end
63
-
64
- if scope
65
- scope.activate do
66
- db = Shard.current(connection_class_for_self).database_server
67
- result = db.unguard { super }
68
- if result&.columns&.any? { |c| sharded_column?(c) }
69
- transposed_rows = result.rows.map do |row|
70
- row.map.with_index do |value, i|
71
- sharded_column?(result.columns[i]) ? Shard.relative_id_for(value, target_shard, current_shard) : value
72
- end
73
- end
74
- result = ::ActiveRecord::Result.new(result.columns, transposed_rows, result.column_types)
75
- end
76
-
77
- result
78
- end
79
- else
80
- db = Shard.current(connection_class_for_self).database_server
81
- db.unguard { super }
82
- end
83
- end
84
- RUBY
85
- end
86
-
87
37
  def reset_column_information
88
38
  @sharded_column_values = {}
89
39
  super
@@ -109,7 +59,11 @@ module Switchman
109
59
  ::ActiveRecord::Base.connection_handler.connection_pool_list(:all)
110
60
  end
111
61
  pools.each do |pool|
112
- pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
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
113
67
  end
114
68
  end
115
69
 
@@ -132,7 +86,7 @@ module Switchman
132
86
  :"#{current_shard}/#{current_role}"
133
87
  end
134
88
 
135
- super(config_or_env)
89
+ super
136
90
  end
137
91
 
138
92
  def connected_to_stack
@@ -189,6 +143,7 @@ module Switchman
189
143
 
190
144
  def self.prepended(klass)
191
145
  klass.singleton_class.prepend(ClassMethods)
146
+ klass.singleton_class.prepend(Switchman::ActiveRecord::Relation::InsertUpsertAll) if ::Rails.version < "7.2"
192
147
  klass.scope :non_shadow, lambda { |key = primary_key|
193
148
  where(key => (QueryMethods::NonTransposingValue.new(0)..
194
149
  QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)))
@@ -241,7 +196,7 @@ module Switchman
241
196
  end
242
197
  end
243
198
  target_shard.activate do
244
- self.class.upsert(shadow_attrs, unique_by: self.class.primary_key)
199
+ self.class.upsert_all([shadow_attrs], unique_by: self.class.primary_key)
245
200
  end
246
201
  end
247
202
 
@@ -159,7 +159,7 @@ module Switchman
159
159
  end
160
160
 
161
161
  def grouped_calculation_options(operation, column_name, distinct)
162
- opts = { operation: operation, column_name: column_name, distinct: distinct }
162
+ opts = { operation:, column_name:, distinct: }
163
163
 
164
164
  # Rails 7.0.5
165
165
  if defined?(::ActiveRecord::Calculations::ColumnAliasTracker)
@@ -188,11 +188,11 @@ module Switchman
188
188
  group_columns = group_aliases.zip(group_fields).map do |aliaz, field|
189
189
  [aliaz, type_for(field), column_name_for(field)]
190
190
  end
191
- opts.merge!(association: association,
192
- associated: associated,
193
- group_aliases: group_aliases,
194
- group_columns: group_columns,
195
- group_fields: group_fields)
191
+ opts.merge!(association:,
192
+ associated:,
193
+ group_aliases:,
194
+ group_columns:,
195
+ group_fields:)
196
196
 
197
197
  opts
198
198
  end
@@ -11,7 +11,7 @@ module Switchman
11
11
  self.schema_cache
12
12
  end
13
13
 
14
- # rubocop:disable Naming/AccessorMethodName override method
14
+ # rubocop:disable Naming/AccessorMethodName -- override method
15
15
  def set_schema_cache(cache)
16
16
  schema_cache = get_schema_cache(cache.connection)
17
17
 
@@ -19,13 +19,14 @@ module Switchman
19
19
  schema_cache.instance_variable_set(x, cache.instance_variable_get(x))
20
20
  end
21
21
  end
22
- # rubocop:enable Naming/AccessorMethodName override method
22
+ # rubocop:enable Naming/AccessorMethodName -- override method
23
23
  end
24
24
 
25
25
  def default_schema
26
- connection unless @schemas
26
+ connection_method = (::Rails.version < "7.2") ? :connection : :lease_connection
27
+ send(connection_method) unless @schemas
27
28
  # default shard will not switch databases immediately, so it won't be set yet
28
- @schemas ||= connection.current_schemas
29
+ @schemas ||= send(connection_method).current_schemas
29
30
  @schemas.first
30
31
  end
31
32
 
@@ -43,8 +44,36 @@ module Switchman
43
44
  conn
44
45
  end
45
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
+
46
75
  def release_connection(with_id = Thread.current)
47
- super(with_id)
76
+ super
48
77
 
49
78
  flush
50
79
  end
@@ -4,7 +4,7 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module FinderMethods
6
6
  def find_one(id)
7
- return super(id) unless klass.integral_id?
7
+ return super unless klass.integral_id?
8
8
 
9
9
  if shard_source_value != :implicit
10
10
  current_shard = Shard.current(klass.connection_class_for_self)
@@ -28,14 +28,14 @@ module Switchman
28
28
  if shard
29
29
  shard.activate { super(local_id) }
30
30
  else
31
- super(id)
31
+ super
32
32
  end
33
33
  end
34
34
 
35
35
  def find_some_ordered(ids)
36
36
  current_shard = Shard.current(klass.connection_class_for_self)
37
37
  ids = ids.map { |id| Shard.relative_id_for(id, current_shard, current_shard) }
38
- super(ids)
38
+ super
39
39
  end
40
40
 
41
41
  def find_or_instantiator_by_attributes(match, attributes, *args)
@@ -31,8 +31,10 @@ module Switchman
31
31
  # Store in internalmetadata to allow other tools to be able to lock out migrations
32
32
  if ::Rails.version < "7.1"
33
33
  ::ActiveRecord::InternalMetadata[:migrator_advisory_lock_id] = shard_name_hash
34
- else
34
+ elsif ::Rails.version < "7.2"
35
35
  ::ActiveRecord::InternalMetadata.new(connection)[:migrator_advisory_lock_id] = shard_name_hash
36
+ else
37
+ ::ActiveRecord::InternalMetadata.new(connection.pool)[:migrator_advisory_lock_id] = shard_name_hash
36
38
  end
37
39
  shard_name_hash
38
40
  end
@@ -73,9 +75,13 @@ module Switchman
73
75
  end
74
76
 
75
77
  begin
76
- super(...)
78
+ super
77
79
  ensure
78
- schema_cache_holder.set_schema_cache(previous_schema_cache)
80
+ if ::Rails.version < "7.2"
81
+ schema_cache_holder.set_schema_cache(previous_schema_cache)
82
+ else
83
+ schema_cache_holder.instance_variable_set(:@cache, previous_schema_cache)
84
+ end
79
85
  reset_column_information
80
86
  end
81
87
  end
@@ -24,7 +24,7 @@ module Switchman
24
24
  super
25
25
  end
26
26
 
27
- def create_or_update(**, &block)
27
+ def create_or_update(**, &)
28
28
  writable_shadow_record_warning
29
29
  super
30
30
  end
@@ -73,23 +73,37 @@ module Switchman
73
73
  end
74
74
  end
75
75
 
76
+ module ClassMethods
77
+ def quote_local_table_name(name)
78
+ # postgres quotes tables and columns the same; just pass through
79
+ # (differs from quote_table_name_with_shard below by no logic to
80
+ # explicitly qualify the table)
81
+ quote_column_name(name)
82
+ end
83
+
84
+ def quote_table_name(name, shard: nil)
85
+ # This looks kind of weird at first glance, but older Rails versions do not actually import
86
+ # these methods as class methods.
87
+ shard = self.shard if shard.nil? && ::Rails.version < "7.2" && !@use_local_table_name
88
+
89
+ return quote_local_table_name(name) unless shard
90
+
91
+ name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
92
+ name.instance_variable_set(:@schema, shard.name) unless name.schema
93
+ name.quoted
94
+ end
95
+ end
96
+
76
97
  def quote_local_table_name(name)
77
- # postgres quotes tables and columns the same; just pass through
78
- # (differs from quote_table_name_with_shard below by no logic to
79
- # explicitly qualify the table)
80
- quote_column_name(name)
98
+ self.class.quote_local_table_name(name)
81
99
  end
82
100
 
83
101
  def quote_table_name(name)
84
- return quote_local_table_name(name) if @use_local_table_name
85
-
86
- name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
87
- name.instance_variable_set(:@schema, shard.name) unless name.schema
88
- name.quoted
102
+ self.class.quote_table_name(name, shard: @use_local_table_name ? nil : shard)
89
103
  end
90
104
 
91
- def with_global_table_name(&block)
92
- with_local_table_name(false, &block)
105
+ def with_global_table_name(&)
106
+ with_local_table_name(false, &)
93
107
  end
94
108
 
95
109
  def with_local_table_name(enable = true) # rubocop:disable Style/OptionalBooleanParameter
@@ -129,7 +143,7 @@ module Switchman
129
143
  end
130
144
 
131
145
  def columns(*)
132
- with_local_table_name(false) { super }
146
+ with_global_table_name { super }
133
147
  end
134
148
  end
135
149
  end
@@ -13,9 +13,9 @@ module Switchman
13
13
  result =
14
14
  if query_cache[sql].key?(binds)
15
15
  args = {
16
- sql: sql,
17
- binds: binds,
18
- name: name,
16
+ sql:,
17
+ binds:,
18
+ name:,
19
19
  connection_id: object_id,
20
20
  cached: true,
21
21
  type_casted_binds: -> { type_casted_binds(binds) }
@@ -40,12 +40,20 @@ module Switchman
40
40
  hit = false
41
41
 
42
42
  @lock.synchronize do
43
- if (result = @query_cache.delete(key))
44
- hit = true
45
- @query_cache[key] = result
43
+ if ::Rails.version < "7.2"
44
+ if (result = @query_cache.delete(key))
45
+ hit = true
46
+ @query_cache[key] = result
47
+ else
48
+ result = @query_cache[key] = yield
49
+ @query_cache.shift if @query_cache_max_size && @query_cache.size > @query_cache_max_size
50
+ end
46
51
  else
47
- result = @query_cache[key] = yield
48
- @query_cache.shift if @query_cache_max_size && @query_cache.size > @query_cache_max_size
52
+ hit = true
53
+ result = @query_cache.compute_if_absent(key) do
54
+ hit = false
55
+ yield
56
+ end
49
57
  end
50
58
  end
51
59
 
@@ -34,13 +34,29 @@ module Switchman
34
34
  end
35
35
 
36
36
  def shard_value=(value)
37
- raise ::ActiveRecord::ImmutableRelation if @loaded
37
+ if @loaded
38
+ error_class = if ::Rails.version < "7.2"
39
+ ::ActiveRecord::ImmutableRelation
40
+ else
41
+ ::ActiveRecord::UnmodifiableRelation
42
+ end
43
+
44
+ raise error_class
45
+ end
38
46
 
39
47
  @values[:shard] = value
40
48
  end
41
49
 
42
50
  def shard_source_value=(value)
43
- raise ::ActiveRecord::ImmutableRelation if @loaded
51
+ if @loaded
52
+ error_class = if ::Rails.version < "7.2"
53
+ ::ActiveRecord::ImmutableRelation
54
+ else
55
+ ::ActiveRecord::UnmodifiableRelation
56
+ end
57
+
58
+ raise error_class
59
+ end
44
60
 
45
61
  @values[:shard_source] = value
46
62
  end
@@ -107,6 +123,10 @@ module Switchman
107
123
 
108
124
  protected
109
125
 
126
+ def arel_columns(columns)
127
+ connection.with_local_table_name { super }
128
+ end
129
+
110
130
  def remove_nonlocal_primary_keys!
111
131
  each_transposable_predicate_value do |value, predicate, _relation, _column, type|
112
132
  next value unless
@@ -243,31 +263,49 @@ module Switchman
243
263
  end
244
264
  end
245
265
 
246
- def arel_columns(columns)
247
- connection.with_local_table_name { super }
248
- end
249
-
250
266
  def arel_column(columns)
251
267
  connection.with_local_table_name { super }
252
268
  end
253
269
 
254
270
  def table_name_matches?(from)
255
- connection.with_global_table_name { super }
271
+ if ::Rails.version < "7.2"
272
+ connection.with_global_table_name { super }
273
+ else
274
+ connection.with_global_table_name do
275
+ table_name = Regexp.escape(table.name)
276
+ # INST: adapter_class -> connection
277
+ quoted_table_name = Regexp.escape(connection.quote_table_name(table.name))
278
+ /(?:\A|(?<!FROM)\s)(?:\b#{table_name}\b|#{quoted_table_name})(?!\.)/i.match?(from.to_s)
279
+ end
280
+ end
281
+ end
282
+
283
+ unless ::Rails.version < "7.2"
284
+ def order_column(field)
285
+ arel_column(field) do |attr_name|
286
+ if attr_name == "count" && !group_values.empty?
287
+ table[attr_name]
288
+ else
289
+ # INST: adapter_class -> connection
290
+ ::Arel.sql(connection.quote_table_name(attr_name), retryable: true)
291
+ end
292
+ end
293
+ end
256
294
  end
257
295
 
258
- def each_predicate(predicates = nil, &block)
259
- return predicates.map(&block) if predicates
296
+ def each_predicate(predicates = nil, &)
297
+ return predicates.map(&) if predicates
260
298
 
261
- each_predicate_cb(:having_clause, :having_clause=, &block)
262
- each_predicate_cb(:where_clause, :where_clause=, &block)
299
+ each_predicate_cb(:having_clause, :having_clause=, &)
300
+ each_predicate_cb(:where_clause, :where_clause=, &)
263
301
  end
264
302
 
265
- def each_predicate_cb(clause_getter, clause_setter, &block)
303
+ def each_predicate_cb(clause_getter, clause_setter, &)
266
304
  old_clause = send(clause_getter)
267
305
  old_predicates = old_clause.send(:predicates)
268
306
  return if old_predicates.empty?
269
307
 
270
- new_predicates = old_predicates.map(&block)
308
+ new_predicates = old_predicates.map(&)
271
309
  return if new_predicates == old_predicates
272
310
 
273
311
  new_clause = old_clause.dup
@@ -289,7 +327,10 @@ module Switchman
289
327
 
290
328
  next predicate if new_left == old_left && new_right == old_right
291
329
 
292
- next predicate.class.new predicate.expr.class.new(new_left, new_right)
330
+ next predicate.class.new predicate.expr.class.new(new_left, new_right) if ::Rails.version < "7.2"
331
+
332
+ next predicate.class.new predicate.expr.class.new([new_left, new_right])
333
+
293
334
  when ::Arel::Nodes::SelectStatement
294
335
  new_cores = predicate.cores.map do |core|
295
336
  next core unless core.is_a?(::Arel::Nodes::SelectCore) # just in case something weird is going on
@@ -344,33 +385,33 @@ module Switchman
344
385
  end
345
386
  end
346
387
 
347
- def each_transposable_predicate_value_cb(node, original_block, &block)
388
+ def each_transposable_predicate_value_cb(node, original_block, &)
348
389
  case node
349
390
  when Array
350
- node.filter_map { |val| each_transposable_predicate_value_cb(val, original_block, &block).presence }
391
+ node.filter_map { |val| each_transposable_predicate_value_cb(val, original_block, &).presence }
351
392
  when ::ActiveModel::Attribute
352
393
  old_value = node.value_before_type_cast
353
- new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
394
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
354
395
 
355
396
  (old_value == new_value) ? node : node.class.new(node.name, new_value, node.type)
356
397
  when ::Arel::Nodes::And
357
398
  old_value = node.children
358
- new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
399
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
359
400
 
360
401
  (old_value == new_value) ? node : node.class.new(new_value)
361
402
  when ::Arel::Nodes::BindParam
362
403
  old_value = node.value
363
- new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
404
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
364
405
 
365
406
  (old_value == new_value) ? node : node.class.new(new_value)
366
407
  when ::Arel::Nodes::Casted
367
408
  old_value = node.value
368
- new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
409
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
369
410
 
370
411
  (old_value == new_value) ? node : node.class.new(new_value, node.attribute)
371
412
  when ::Arel::Nodes::HomogeneousIn
372
413
  old_value = node.values
373
- new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
414
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
374
415
 
375
416
  # switch to a regular In, so that Relation::WhereClause#contradiction? knows about it
376
417
  if new_value.empty?
@@ -381,7 +422,7 @@ module Switchman
381
422
  end
382
423
  when ::Arel::Nodes::Binary
383
424
  old_value = node.right
384
- new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
425
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
385
426
 
386
427
  (old_value == new_value) ? node : node.class.new(node.left, new_value)
387
428
  when ::Arel::Nodes::SelectStatement
@@ -28,11 +28,18 @@ module Switchman
28
28
  # this technically belongs on AssociationReflection, but we put it on
29
29
  # ThroughReflection as well, instead of delegating to its internal
30
30
  # HasManyAssociation, losing its proper `klass`
31
- def association_scope_cache(klass, owner, &block)
31
+ def association_scope_cache(klass, owner, &)
32
32
  key = self
33
33
  key = [key, owner._read_attribute(@foreign_type)] if polymorphic?
34
34
  key = [key, shard(owner).id].flatten
35
- klass.cached_find_by_statement(key, &block)
35
+
36
+ if ::Rails.version < "7.2"
37
+ klass.cached_find_by_statement(key, &)
38
+ else
39
+ klass.with_connection do |connection|
40
+ klass.cached_find_by_statement(connection, key, &)
41
+ end
42
+ end
36
43
  end
37
44
  end
38
45
 
@@ -28,15 +28,15 @@ module Switchman
28
28
  relation
29
29
  end
30
30
 
31
- def new(*, &block)
31
+ def new(*, &)
32
32
  primary_shard.activate(klass.connection_class_for_self) { super }
33
33
  end
34
34
 
35
- def create(*, &block)
35
+ def create(*, &)
36
36
  primary_shard.activate(klass.connection_class_for_self) { super }
37
37
  end
38
38
 
39
- def create!(*, &block)
39
+ def create!(*, &)
40
40
  primary_shard.activate(klass.connection_class_for_self) { super }
41
41
  end
42
42
 
@@ -73,6 +73,59 @@ module Switchman
73
73
  RUBY
74
74
  end
75
75
 
76
+ # https://github.com/rails/rails/commit/ed2c15b52450ff927a05629f031376f25b670335
77
+ # once the minimum version is Rails 7.2, we can drop this separate module
78
+ module InsertUpsertAll
79
+ %w[insert_all upsert_all].each do |method|
80
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
81
+ def #{method}(attributes, returning: nil, **)
82
+ scope = self != ::ActiveRecord::Base && current_scope
83
+ if (target_shard = scope&.primary_shard) == (current_shard = Shard.current(connection_class_for_self))
84
+ scope = nil
85
+ end
86
+ if scope
87
+ dupped = false
88
+ attributes.each_with_index do |hash, i|
89
+ if dupped || hash.any? { |k, v| sharded_column?(k) }
90
+ unless dupped
91
+ attributes = attributes.dup
92
+ dupped = true
93
+ end
94
+ attributes[i] = hash.to_h do |k, v|
95
+ if sharded_column?(k)
96
+ [k, Shard.relative_id_for(v, current_shard, target_shard)]
97
+ else
98
+ [k, v]
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ if scope
106
+ scope.activate do
107
+ db = Shard.current(connection_class_for_self).database_server
108
+ result = db.unguard { super }
109
+ if result&.columns&.any? { |c| sharded_column?(c) }
110
+ transposed_rows = result.rows.map do |row|
111
+ row.map.with_index do |value, i|
112
+ sharded_column?(result.columns[i]) ? Shard.relative_id_for(value, target_shard, current_shard) : value
113
+ end
114
+ end
115
+ result = ::ActiveRecord::Result.new(result.columns, transposed_rows, result.column_types)
116
+ end
117
+
118
+ result
119
+ end
120
+ else
121
+ db = Shard.current(connection_class_for_self).database_server
122
+ db.unguard { super }
123
+ end
124
+ end
125
+ RUBY
126
+ end
127
+ end
128
+
76
129
  def find_ids_in_ranges(options = {})
77
130
  is_integer = columns_hash[primary_key.to_s].type == :integer
78
131
  loose_mode = options[:loose] && is_integer
@@ -62,16 +62,12 @@ module Switchman
62
62
  if primary_shard != final_primary_shard && rhs.primary_shard != final_primary_shard
63
63
  shard!(final_primary_shard)
64
64
  rhs = rhs.shard(final_primary_shard)
65
- super(rhs)
66
65
  elsif primary_shard != final_primary_shard
67
66
  shard!(final_primary_shard)
68
- super(rhs)
69
67
  elsif rhs.primary_shard != final_primary_shard
70
68
  rhs = rhs.shard(final_primary_shard)
71
- super(rhs)
72
- else
73
- super
74
69
  end
70
+ super
75
71
 
76
72
  self.shard_value = final_shard_value
77
73
  self.shard_source_value = final_shard_source_value
@@ -4,7 +4,7 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module TableDefinition
6
6
  def column(name, type, limit: nil, **)
7
- Switchman.foreign_key_check(name, type, limit: limit)
7
+ Switchman.foreign_key_check(name, type, limit:)
8
8
  super
9
9
  end
10
10
  end
@@ -4,49 +4,89 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module TestFixtures
6
6
  FORBIDDEN_DB_ENVS = %i[development production].freeze
7
- def setup_fixtures(config = ::ActiveRecord::Base)
8
- super
9
-
10
- return unless run_in_transaction?
11
-
12
- # Replace the one that activerecord natively uses with a switchman-optimized one
13
- ::ActiveSupport::Notifications.unsubscribe(@connection_subscriber)
14
- # Code adapted from the code in rails proper
15
- @connection_subscriber =
16
- ::ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
17
- spec_name = if ::Rails.version < "7.1"
18
- payload[:spec_name] if payload.key?(:spec_name)
19
- elsif payload.key?(:connection_name)
20
- payload[:connection_name]
21
- end
22
- shard = payload[:shard] if payload.key?(:shard)
23
7
 
24
- if spec_name && !FORBIDDEN_DB_ENVS.include?(shard)
25
- begin
26
- connection = ::ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
27
- connection.connect! if ::Rails.version >= "7.1" # eagerly validate the connection
28
- rescue ::ActiveRecord::ConnectionNotEstablished, ::ActiveRecord::NoDatabaseError
29
- connection = nil
30
- end
8
+ if ::Rails.version < "7.2"
9
+ def setup_fixtures(config = ::ActiveRecord::Base)
10
+ super
11
+ return unless run_in_transaction?
31
12
 
32
- if connection
33
- setup_shared_connection_pool
34
- unless @fixture_connections.include?(connection)
35
- connection.begin_transaction joinable: false, _lazy: false
36
- connection.pool.lock_thread = true if lock_threads
37
- @fixture_connections << connection
13
+ # Replace the one that activerecord natively uses with a switchman-optimized one
14
+ ::ActiveSupport::Notifications.unsubscribe(@connection_subscriber)
15
+ # Code adapted from the code in rails proper
16
+ @connection_subscriber =
17
+ ::ActiveSupport::Notifications.subscribe("!connection.active_record") do |_, _, _, _, payload|
18
+ spec_name = if ::Rails.version < "7.1"
19
+ payload[:spec_name] if payload.key?(:spec_name)
20
+ elsif payload.key?(:connection_name)
21
+ payload[:connection_name]
22
+ end
23
+ shard = payload[:shard] if payload.key?(:shard)
24
+
25
+ if spec_name && !FORBIDDEN_DB_ENVS.include?(shard)
26
+ begin
27
+ connection = ::ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
28
+ connection.connect! if ::Rails.version >= "7.1" # eagerly validate the connection
29
+ rescue ::ActiveRecord::ConnectionNotEstablished
30
+ connection = nil
31
+ end
32
+
33
+ if connection
34
+ setup_shared_connection_pool
35
+ unless @fixture_connections.include?(connection)
36
+ connection.begin_transaction joinable: false, _lazy: false
37
+ connection.pool.lock_thread = true if lock_threads
38
+ @fixture_connections << connection
39
+ end
38
40
  end
39
41
  end
40
42
  end
43
+ end
44
+
45
+ def enlist_fixture_connections
46
+ setup_shared_connection_pool
47
+
48
+ ::ActiveRecord::Base.connection_handler.connection_pool_list(:primary).reject do |cp|
49
+ FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym)
50
+ end.map(&:connection)
51
+ end
52
+ else
53
+ def setup_transactional_fixtures
54
+ setup_shared_connection_pool
55
+
56
+ # Begin transactions for connections already established
57
+ # INST: :writing -> :primary
58
+ @fixture_connection_pools = ::ActiveRecord::Base.connection_handler.connection_pool_list(:primary)
59
+ # INST: filter by FORBIDDEN_DB_ENVS
60
+ @fixture_connection_pools = @fixture_connection_pools.reject do |cp|
61
+ FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym)
41
62
  end
42
- end
43
63
 
44
- def enlist_fixture_connections
45
- setup_shared_connection_pool
64
+ @fixture_connection_pools.each do |pool|
65
+ pool.pin_connection!(lock_threads)
66
+ pool.lease_connection
67
+ end
68
+
69
+ # When connections are established in the future, begin a transaction too
70
+ @connection_subscriber = ::ActiveSupport::Notifications
71
+ .subscribe("!connection.active_record") do |_, _, _, _, payload|
72
+ connection_name = payload[:connection_name] if payload.key?(:connection_name)
73
+ shard = payload[:shard] if payload.key?(:shard)
74
+
75
+ # INST: filter by FORBIDDEN_DB_ENVS
76
+ if connection_name && !FORBIDDEN_DB_ENVS.include?(shard)
77
+ pool = ::ActiveRecord::Base.connection_handler.retrieve_connection_pool(connection_name, shard: shard)
78
+ if pool
79
+ setup_shared_connection_pool
46
80
 
47
- ::ActiveRecord::Base.connection_handler.connection_pool_list(:primary).reject do |cp|
48
- FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym)
49
- end.map(&:connection)
81
+ unless @fixture_connection_pools.include?(pool)
82
+ pool.pin_connection!(lock_threads)
83
+ pool.lease_connection
84
+ @fixture_connection_pools << pool
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
50
90
  end
51
91
  end
52
92
  end
@@ -121,7 +121,7 @@ module Switchman
121
121
  Shard.sharded_models.each do |klass|
122
122
  self.class.all_roles.each do |role|
123
123
  klass.connection_handler.remove_connection_pool(klass.connection_specification_name,
124
- role: role,
124
+ role:,
125
125
  shard: id.to_sym)
126
126
  end
127
127
  end
@@ -218,7 +218,7 @@ module Switchman
218
218
  if config_create_statement
219
219
  create_commands = Array(config_create_statement).dup
220
220
  create_statement = lambda {
221
- create_commands.map { |statement| format(statement, name: name, password: password) }
221
+ create_commands.map { |statement| format(statement, name:, password:) }
222
222
  }
223
223
  end
224
224
 
@@ -236,8 +236,8 @@ module Switchman
236
236
  self.class.creating_new_shard = true
237
237
  DatabaseServer.send(:reference_role, :deploy)
238
238
  ::ActiveRecord::Base.connected_to(shard: self.id.to_sym, role: :deploy) do
239
- shard = Shard.create!(id: id,
240
- name: name,
239
+ shard = Shard.create!(id:,
240
+ name:,
241
241
  database_server_id: self.id)
242
242
  if create_statement
243
243
  if ::ActiveRecord::Base.connection.select_value(
@@ -53,6 +53,14 @@ module Switchman
53
53
  ::ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(ActiveRecord::ConnectionPool)
54
54
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::QueryCache)
55
55
  ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter)
56
+ # https://github.com/rails/rails/commit/0016280f4fde55d96738887093dc333aae0d107b
57
+ if ::Rails.version < "7.2"
58
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter::ClassMethods)
59
+ else
60
+ ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.singleton_class.prepend(
61
+ ActiveRecord::PostgreSQLAdapter::ClassMethods
62
+ )
63
+ end
56
64
 
57
65
  ::ActiveRecord::DatabaseConfigurations.prepend(ActiveRecord::DatabaseConfigurations)
58
66
  ::ActiveRecord::DatabaseConfigurations::DatabaseConfig.prepend(
@@ -79,6 +87,7 @@ module Switchman
79
87
  ::ActiveRecord::Relation.include(ActiveRecord::QueryMethods)
80
88
  ::ActiveRecord::Relation.prepend(GuardRail::Relation)
81
89
  ::ActiveRecord::Relation.prepend(ActiveRecord::Relation)
90
+ ::ActiveRecord::Relation.prepend(ActiveRecord::Relation::InsertUpsertAll) if ::Rails.version >= "7.2"
82
91
  ::ActiveRecord::Relation.include(ActiveRecord::SpawnMethods)
83
92
  ::ActiveRecord::Relation.include(CallSuper)
84
93
 
@@ -5,8 +5,10 @@ module Switchman
5
5
  # ten trillion possible ids per shard. yup.
6
6
  IDS_PER_SHARD = 10_000_000_000_000
7
7
 
8
+ # rubocop:disable Style/SymbolProc -- transforming to a lambda produces "no receiver given"
8
9
  # only allow one default
9
10
  validates_uniqueness_of :default, if: ->(s) { s.default? }
11
+ # rubocop:enable Style/SymbolProc
10
12
 
11
13
  after_save :clear_cache
12
14
  after_destroy :clear_cache
@@ -51,7 +53,7 @@ module Switchman
51
53
  return [default] unless default.is_a?(Switchman::Shard)
52
54
  return all if !Switchman.region || DatabaseServer.none?(&:region)
53
55
 
54
- in_region(Switchman.region, include_regionless: include_regionless)
56
+ in_region(Switchman.region, include_regionless:)
55
57
  end)
56
58
 
57
59
  class << self
@@ -141,7 +143,7 @@ module Switchman
141
143
 
142
144
  unless cached_shards.key?(id)
143
145
  cached_shards[id] = Shard.default.activate do
144
- find_cached(["shard", id]) { find_by(id: id) }
146
+ find_cached(["shard", id]) { find_by(id:) }
145
147
  end
146
148
  end
147
149
  cached_shards[id]
@@ -233,7 +235,7 @@ module Switchman
233
235
  Switchman.config[:on_fork_proc]&.call
234
236
  with_each_shard(subscope,
235
237
  classes,
236
- exception: exception,
238
+ exception:,
237
239
  output: output || :decorated) do
238
240
  last_description = Shard.current.description
239
241
  Parallel::ResultWrapper.new(yield)
@@ -459,7 +461,7 @@ module Switchman
459
461
  connects_to_hash.each do |(db_name, role_hash)|
460
462
  role_hash.each_key do |role|
461
463
  role_hash.delete(role) if klass.connection_handler.retrieve_connection_pool(
462
- klass.connection_specification_name, role: role, shard: db_name
464
+ klass.connection_specification_name, role:, shard: db_name
463
465
  )
464
466
  end
465
467
  end
@@ -590,9 +592,9 @@ module Switchman
590
592
  id
591
593
  end
592
594
 
593
- def activate(*classes, &block)
595
+ def activate(*classes, &)
594
596
  shards = hashify_classes(classes)
595
- Shard.activate(shards, &block)
597
+ Shard.activate(shards, &)
596
598
  end
597
599
 
598
600
  # for use from console ONLY
@@ -19,7 +19,7 @@ module Switchman
19
19
  env: @shard_host.pool.connection_class&.current_role
20
20
  }
21
21
  end
22
- super(name, payload)
22
+ super
23
23
  end
24
24
  end
25
25
  end
@@ -63,7 +63,7 @@ module Switchman
63
63
 
64
64
  def find_existing_test_shard(server, name)
65
65
  if server == Shard.default.database_server
66
- server.shards.where(name: name).first
66
+ server.shards.where(name:).first
67
67
  else
68
68
  shard = Shard.where("database_server_id IS NOT NULL AND name=?", name).first
69
69
  # if somehow databases got created in a different order, change the shard to match
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = "4.0.1"
4
+ VERSION = "4.1.0"
5
5
  end
@@ -94,7 +94,7 @@ module Switchman
94
94
  else
95
95
  nil
96
96
  end
97
- Shard.with_each_shard(scope, classes, output: output, **options) do
97
+ Shard.with_each_shard(scope, classes, output:, **options) do
98
98
  shard = Shard.current
99
99
 
100
100
  if log_format == "json"
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  - James Williams
9
9
  - Jacob Fugal
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2025-02-18 00:00:00.000000000 Z
13
+ date: 2025-03-21 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: '7.0'
22
22
  - - "<"
23
23
  - !ruby/object:Gem::Version
24
- version: '7.2'
24
+ version: '7.3'
25
25
  type: :runtime
26
26
  prerelease: false
27
27
  version_requirements: !ruby/object:Gem::Requirement
@@ -31,7 +31,7 @@ dependencies:
31
31
  version: '7.0'
32
32
  - - "<"
33
33
  - !ruby/object:Gem::Version
34
- version: '7.2'
34
+ version: '7.3'
35
35
  - !ruby/object:Gem::Dependency
36
36
  name: guardrail
37
37
  requirement: !ruby/object:Gem::Requirement
@@ -69,7 +69,7 @@ dependencies:
69
69
  version: '7.0'
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
- version: '7.2'
72
+ version: '7.3'
73
73
  type: :runtime
74
74
  prerelease: false
75
75
  version_requirements: !ruby/object:Gem::Requirement
@@ -79,7 +79,7 @@ dependencies:
79
79
  version: '7.0'
80
80
  - - "<"
81
81
  - !ruby/object:Gem::Version
82
- version: '7.2'
82
+ version: '7.3'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: debug
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -198,14 +198,28 @@ dependencies:
198
198
  requirements:
199
199
  - - "~>"
200
200
  - !ruby/object:Gem::Version
201
- version: '2.2'
201
+ version: '3.0'
202
202
  type: :development
203
203
  prerelease: false
204
204
  version_requirements: !ruby/object:Gem::Requirement
205
205
  requirements:
206
206
  - - "~>"
207
207
  - !ruby/object:Gem::Version
208
- version: '2.2'
208
+ version: '3.0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: rubocop-rspec_rails
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: '2.29'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: '2.29'
209
223
  - !ruby/object:Gem::Dependency
210
224
  name: simplecov
211
225
  requirement: !ruby/object:Gem::Requirement
@@ -290,7 +304,7 @@ licenses:
290
304
  metadata:
291
305
  rubygems_mfa_required: 'true'
292
306
  source_code_uri: https://github.com/instructure/switchman
293
- post_install_message:
307
+ post_install_message:
294
308
  rdoc_options: []
295
309
  require_paths:
296
310
  - lib
@@ -298,15 +312,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
298
312
  requirements:
299
313
  - - ">="
300
314
  - !ruby/object:Gem::Version
301
- version: '3.0'
315
+ version: '3.1'
302
316
  required_rubygems_version: !ruby/object:Gem::Requirement
303
317
  requirements:
304
318
  - - ">="
305
319
  - !ruby/object:Gem::Version
306
320
  version: '0'
307
321
  requirements: []
308
- rubygems_version: 3.2.33
309
- signing_key:
322
+ rubygems_version: 3.3.27
323
+ signing_key:
310
324
  specification_version: 4
311
325
  summary: Rails sharding magic
312
326
  test_files: []