switchman 3.0.2 → 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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +16 -15
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
  4. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
  5. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  6. data/lib/switchman/action_controller/caching.rb +2 -2
  7. data/lib/switchman/active_record/abstract_adapter.rb +11 -18
  8. data/lib/switchman/active_record/associations.rb +315 -0
  9. data/lib/switchman/active_record/attribute_methods.rb +191 -79
  10. data/lib/switchman/active_record/base.rb +204 -50
  11. data/lib/switchman/active_record/calculations.rb +92 -49
  12. data/lib/switchman/active_record/connection_handler.rb +18 -0
  13. data/lib/switchman/active_record/connection_pool.rb +47 -34
  14. data/lib/switchman/active_record/database_configurations.rb +32 -6
  15. data/lib/switchman/active_record/finder_methods.rb +22 -16
  16. data/lib/switchman/active_record/log_subscriber.rb +3 -6
  17. data/lib/switchman/active_record/migration.rb +42 -14
  18. data/lib/switchman/active_record/model_schema.rb +1 -1
  19. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  20. data/lib/switchman/active_record/persistence.rb +37 -2
  21. data/lib/switchman/active_record/postgresql_adapter.rb +39 -20
  22. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  23. data/lib/switchman/active_record/query_cache.rb +26 -17
  24. data/lib/switchman/active_record/query_methods.rb +251 -140
  25. data/lib/switchman/active_record/reflection.rb +10 -3
  26. data/lib/switchman/active_record/relation.rb +110 -35
  27. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  28. data/lib/switchman/active_record/statement_cache.rb +13 -9
  29. data/lib/switchman/active_record/table_definition.rb +1 -1
  30. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  31. data/lib/switchman/active_record/test_fixtures.rb +89 -0
  32. data/lib/switchman/active_support/cache.rb +25 -4
  33. data/lib/switchman/arel.rb +20 -7
  34. data/lib/switchman/call_super.rb +2 -2
  35. data/lib/switchman/database_server.rb +123 -83
  36. data/lib/switchman/default_shard.rb +14 -5
  37. data/lib/switchman/engine.rb +85 -131
  38. data/lib/switchman/environment.rb +2 -2
  39. data/lib/switchman/errors.rb +17 -2
  40. data/lib/switchman/guard_rail/relation.rb +7 -10
  41. data/lib/switchman/guard_rail.rb +5 -0
  42. data/lib/switchman/parallel.rb +68 -0
  43. data/lib/switchman/r_spec_helper.rb +17 -28
  44. data/lib/switchman/rails.rb +1 -4
  45. data/{app/models → lib}/switchman/shard.rb +229 -246
  46. data/lib/switchman/sharded_instrumenter.rb +9 -3
  47. data/lib/switchman/shared_schema_cache.rb +11 -0
  48. data/lib/switchman/standard_error.rb +15 -12
  49. data/lib/switchman/test_helper.rb +3 -3
  50. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  51. data/lib/switchman/version.rb +1 -1
  52. data/lib/switchman.rb +44 -12
  53. data/lib/tasks/switchman.rake +101 -54
  54. metadata +34 -176
  55. data/lib/switchman/active_record/association.rb +0 -206
  56. data/lib/switchman/open4.rb +0 -80
@@ -3,6 +3,19 @@
3
3
  module Switchman
4
4
  module ActiveRecord
5
5
  module QueryMethods
6
+ # Use this class to prevent a value from getting transposed across shards
7
+ class NonTransposingValue < SimpleDelegator
8
+ def class
9
+ __getobj__.class
10
+ end
11
+
12
+ def is_a?(other)
13
+ return true if other == NonTransposingValue
14
+
15
+ __getobj__.is_a?(other)
16
+ end
17
+ end
18
+
6
19
  # shard_value is one of:
7
20
  # A shard
8
21
  # An array or relation of shards
@@ -12,8 +25,6 @@ module Switchman
12
25
  # :explicit - explicit set on the relation
13
26
  # :association - a special value that scopes from associations use to use slightly different logic
14
27
  # for foreign key transposition
15
- # :to_a - a special value that Relation#to_a uses when querying multiple shards to
16
- # remove primary keys from conditions that aren't applicable to the current shard
17
28
  def shard_value
18
29
  @values[:shard]
19
30
  end
@@ -23,13 +34,29 @@ module Switchman
23
34
  end
24
35
 
25
36
  def shard_value=(value)
26
- 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
27
46
 
28
47
  @values[:shard] = value
29
48
  end
30
49
 
31
50
  def shard_source_value=(value)
32
- 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
33
60
 
34
61
  @values[:shard_source] = value
35
62
  end
@@ -44,10 +71,7 @@ module Switchman
44
71
  old_primary_shard = primary_shard
45
72
  self.shard_value = value
46
73
  self.shard_source_value = source
47
- if old_primary_shard != primary_shard || source == :to_a
48
- transpose_clauses(old_primary_shard, primary_shard,
49
- remove_nonlocal_primary_keys: source == :to_a)
50
- end
74
+ transpose_predicates(nil, old_primary_shard, primary_shard) if old_primary_shard != primary_shard
51
75
  self
52
76
  end
53
77
 
@@ -65,7 +89,7 @@ module Switchman
65
89
  when ::ActiveRecord::Relation
66
90
  Shard.default
67
91
  when nil
68
- Shard.current(klass.connection_classes)
92
+ Shard.current(klass.connection_class_for_self)
69
93
  else
70
94
  raise ArgumentError, "invalid shard value #{shard_value}"
71
95
  end
@@ -79,7 +103,7 @@ module Switchman
79
103
  when ::ActiveRecord::Base
80
104
  shard_value.respond_to?(:associated_shards) ? shard_value.associated_shards : [shard_value.shard]
81
105
  when nil
82
- [Shard.current(klass.connection_classes)]
106
+ [Shard.current(klass.connection_class_for_self)]
83
107
  else
84
108
  shard_value
85
109
  end
@@ -89,35 +113,41 @@ module Switchman
89
113
  super(other.shard(primary_shard))
90
114
  end
91
115
 
92
- private
116
+ # use a temp variable so that the new where clause is built before self.where_clause is read,
117
+ # since build_where_clause might mutate self.where_clause
118
+ def where!(opts, *rest)
119
+ new_clause = build_where_clause(opts, rest)
120
+ self.where_clause += new_clause
121
+ self
122
+ end
93
123
 
94
- %i[where having].each do |type|
95
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
96
- def transpose_#{type}_clauses(source_shard, target_shard, remove_nonlocal_primary_keys:)
97
- unless (predicates = #{type}_clause.send(:predicates)).empty?
98
- new_predicates = transpose_predicates(predicates, source_shard,
99
- target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
100
- if new_predicates != predicates
101
- self.#{type}_clause = #{type}_clause.dup
102
- if new_predicates != predicates
103
- #{type}_clause.instance_variable_set(:@predicates, new_predicates)
104
- end
105
- end
106
- end
107
- end
108
- RUBY
124
+ protected
125
+
126
+ def arel_columns(columns)
127
+ connection.with_local_table_name { super }
109
128
  end
110
129
 
111
- def transpose_clauses(source_shard, target_shard, remove_nonlocal_primary_keys: false)
112
- transpose_where_clauses(source_shard, target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
113
- transpose_having_clauses(source_shard, target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
130
+ def remove_nonlocal_primary_keys!
131
+ each_transposable_predicate_value do |value, predicate, _relation, _column, type|
132
+ next value unless
133
+ type == :primary &&
134
+ predicate.left.relation.klass == klass &&
135
+ (predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::HomogeneousIn))
136
+
137
+ (value.is_a?(Integer) && value > Shard::IDS_PER_SHARD) ? [] : value
138
+ end
139
+ self
114
140
  end
115
141
 
142
+ private
143
+
116
144
  def infer_shards_from_primary_key(predicates)
117
145
  return unless klass.integral_id?
118
146
 
119
147
  primary_key = predicates.detect do |predicate|
120
- (predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)) &&
148
+ (predicate.is_a?(::Arel::Nodes::Equality) ||
149
+ predicate.is_a?(::Arel::Nodes::In) ||
150
+ predicate.is_a?(::Arel::Nodes::HomogeneousIn)) &&
121
151
  predicate.left.is_a?(::Arel::Attributes::Attribute) &&
122
152
  predicate.left.relation.is_a?(::Arel::Table) && predicate.left.relation.klass == klass &&
123
153
  klass.primary_key == predicate.left.name
@@ -131,7 +161,7 @@ module Switchman
131
161
  id_shards = Set.new
132
162
  right.each do |value|
133
163
  local_id, id_shard = Shard.local_id_for(value)
134
- id_shard ||= Shard.current(klass.connection_classes) if local_id
164
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
135
165
  id_shards << id_shard if id_shard
136
166
  end
137
167
  return if id_shards.empty?
@@ -145,21 +175,24 @@ module Switchman
145
175
  return
146
176
  else
147
177
  id_shards = id_shards.to_a
148
- transpose_clauses(primary_shard, id_shards.first)
178
+ transpose_predicates(nil, primary_shard, id_shards.first)
149
179
  self.shard_value = id_shards
150
180
  return
151
181
  end
152
182
  when ::Arel::Nodes::BindParam
153
183
  local_id, id_shard = Shard.local_id_for(right.value.value_before_type_cast)
154
- id_shard ||= Shard.current(klass.connection_classes) if local_id
184
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
185
+ when ::ActiveModel::Attribute
186
+ local_id, id_shard = Shard.local_id_for(right.value_before_type_cast)
187
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
155
188
  else
156
189
  local_id, id_shard = Shard.local_id_for(right)
157
- id_shard ||= Shard.current(klass.connection_classes) if local_id
190
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
158
191
  end
159
192
 
160
193
  return if !id_shard || id_shard == primary_shard
161
194
 
162
- transpose_clauses(primary_shard, id_shard)
195
+ transpose_predicates(nil, primary_shard, id_shard)
163
196
  self.shard_value = id_shard
164
197
  end
165
198
 
@@ -177,25 +210,25 @@ module Switchman
177
210
  end
178
211
 
179
212
  def sharded_foreign_key?(relation, column)
180
- models_for_table(relation.table_name).any? { |m| m.sharded_column?(column) }
213
+ models_for_table(relation.name).any? { |m| m.sharded_column?(column) }
181
214
  end
182
215
 
183
216
  def sharded_primary_key?(relation, column)
184
217
  column = column.to_s
185
- return column == 'id' if relation.klass == ::ActiveRecord::Base
218
+ return column == "id" if relation.klass == ::ActiveRecord::Base
186
219
 
187
220
  relation.klass.primary_key == column && relation.klass.integral_id?
188
221
  end
189
222
 
190
223
  def source_shard_for_foreign_key(relation, column)
191
224
  reflection = nil
192
- models_for_table(relation.table_name).each do |model|
225
+ models_for_table(relation.name).each do |model|
193
226
  reflection = model.send(:reflection_for_integer_attribute, column)
194
227
  break if reflection
195
228
  end
196
- return Shard.current(klass.connection_classes) if reflection.options[:polymorphic]
229
+ return Shard.current(klass.connection_class_for_self) if reflection.options[:polymorphic]
197
230
 
198
- Shard.current(reflection.klass.connection_classes)
231
+ Shard.current(reflection.klass.connection_class_for_self)
199
232
  end
200
233
 
201
234
  def relation_and_column(attribute)
@@ -209,12 +242,11 @@ module Switchman
209
242
 
210
243
  case opts
211
244
  when String, Array
212
- values = Hash === rest.first ? rest.first.values : rest
245
+ values = (Hash === rest.first) ? rest.first.values : rest
213
246
 
214
- values.grep(ActiveRecord::Relation) do |rel|
215
- # serialize subqueries against the same shard as the outer query is currently
216
- # targeted to run against
217
- rel.shard!(primary_shard) if rel.shard_source_value == :implicit && rel.primary_shard != primary_shard
247
+ if shard_source_value != :explicit && values.grep(ActiveRecord::Relation).first
248
+ raise "Sub-queries are not allowed as simple substitutions; " \
249
+ "please build your relation with more structured methods so that Switchman is able to introspect it."
218
250
  end
219
251
 
220
252
  super
@@ -231,124 +263,203 @@ module Switchman
231
263
  end
232
264
  end
233
265
 
234
- def arel_columns(columns)
235
- connection.with_local_table_name { super }
236
- end
237
-
238
266
  def arel_column(columns)
239
267
  connection.with_local_table_name { super }
240
268
  end
241
269
 
242
- def transpose_predicates(predicates,
243
- source_shard,
244
- target_shard,
245
- remove_nonlocal_primary_keys: false)
246
- predicates.map do |predicate|
247
- transpose_single_predicate(predicate, source_shard, target_shard,
248
- remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
270
+ def table_name_matches?(from)
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
249
280
  end
250
281
  end
251
282
 
252
- def transpose_single_predicate(predicate,
253
- source_shard,
254
- target_shard,
255
- remove_nonlocal_primary_keys: false)
256
- if predicate.is_a?(::Arel::Nodes::Grouping)
257
- return predicate unless predicate.expr.is_a?(::Arel::Nodes::Or)
258
-
259
- # Dang, we have an OR. OK, that means we have other epxressions below this
260
- # level, perhaps many, that may need transposition.
261
- # the left side and right side must each be treated as predicate lists and
262
- # transformed in kind, if neither of them changes we can just return the grouping as is.
263
- # hold on, it's about to get recursive...
264
- or_expr = predicate.expr
265
- left_node = or_expr.left
266
- right_node = or_expr.right
267
- new_left_predicates = transpose_single_predicate(left_node, source_shard,
268
- target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
269
- or_expr.instance_variable_set(:@left, new_left_predicates) if new_left_predicates != left_node
270
- new_right_predicates = transpose_single_predicate(right_node, source_shard,
271
- target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
272
- or_expr.instance_variable_set(:@right, new_right_predicates) if new_right_predicates != right_node
273
- return predicate
274
- end
275
- return predicate unless predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)
276
- return predicate unless predicate.left.is_a?(::Arel::Attributes::Attribute)
277
-
278
- relation, column = relation_and_column(predicate.left)
279
- return predicate unless (type = transposable_attribute_type(relation, column))
280
-
281
- remove = true if type == :primary &&
282
- remove_nonlocal_primary_keys &&
283
- predicate.left.relation.klass == klass &&
284
- (predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::HomogeneousIn))
285
-
286
- current_source_shard =
287
- if source_shard
288
- source_shard
289
- elsif type == :primary
290
- Shard.current(klass.connection_classes)
291
- elsif type == :foreign
292
- source_shard_for_foreign_key(relation, column)
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
293
292
  end
293
+ end
294
+ end
294
295
 
295
- right = if predicate.is_a?(::Arel::Nodes::HomogeneousIn)
296
- predicate.values
297
- else
298
- predicate.right
299
- end
296
+ def each_predicate(predicates = nil, &)
297
+ return predicates.map(&) if predicates
300
298
 
301
- new_right_value =
302
- case right
303
- when Array
304
- right.map { |val| transpose_predicate_value(val, current_source_shard, target_shard, type, remove).presence }.compact
305
- else
306
- transpose_predicate_value(right, current_source_shard, target_shard, type, remove)
299
+ each_predicate_cb(:having_clause, :having_clause=, &)
300
+ each_predicate_cb(:where_clause, :where_clause=, &)
301
+ end
302
+
303
+ def each_predicate_cb(clause_getter, clause_setter, &)
304
+ old_clause = send(clause_getter)
305
+ old_predicates = old_clause.send(:predicates)
306
+ return if old_predicates.empty?
307
+
308
+ new_predicates = old_predicates.map(&)
309
+ return if new_predicates == old_predicates
310
+
311
+ new_clause = old_clause.dup
312
+ new_clause.instance_variable_set(:@predicates, new_predicates)
313
+
314
+ send(clause_setter, new_clause)
315
+ end
316
+
317
+ def each_transposable_predicate(predicates, &block)
318
+ each_predicate(predicates) do |predicate|
319
+ case predicate
320
+ when ::Arel::Nodes::Grouping
321
+ next predicate unless predicate.expr.is_a?(::Arel::Nodes::Or)
322
+
323
+ or_expr = predicate.expr
324
+ old_left = or_expr.left
325
+ old_right = or_expr.right
326
+ new_left, new_right = each_transposable_predicate([old_left, old_right], &block)
327
+
328
+ next predicate if new_left == old_left && new_right == old_right
329
+
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
+
334
+ when ::Arel::Nodes::SelectStatement
335
+ new_cores = predicate.cores.map do |core|
336
+ next core unless core.is_a?(::Arel::Nodes::SelectCore) # just in case something weird is going on
337
+
338
+ new_wheres = each_transposable_predicate(core.wheres, &block)
339
+ new_havings = each_transposable_predicate(core.havings, &block)
340
+
341
+ next core if core.wheres == new_wheres && core.havings == new_havings
342
+
343
+ new_core = core.clone
344
+ new_core.wheres = new_wheres
345
+ new_core.havings = new_havings
346
+ new_core
347
+ end
348
+
349
+ next predicate if predicate.cores == new_cores
350
+
351
+ new_node = predicate.clone
352
+ new_node.instance_variable_set(:@cores, new_cores)
353
+ next new_node
354
+ when ::Arel::Nodes::Not
355
+ old_value = predicate.expr
356
+ new_value = each_transposable_predicate([old_value], &block).first
357
+
358
+ next predicate if old_value == new_value
359
+
360
+ next predicate.class.new(new_value)
361
+ when ::Arel::Nodes::Exists
362
+ old_value = predicate.expressions
363
+ new_value = each_transposable_predicate([old_value], &block).first
364
+
365
+ next predicate if old_value == new_value
366
+
367
+ next predicate.class.new(new_value)
307
368
  end
308
369
 
309
- if new_right_value == right
310
- predicate
311
- elsif predicate.right.is_a?(::Arel::Nodes::Casted)
312
- if new_right_value == right.value
313
- predicate
314
- else
315
- predicate.class.new(predicate.left, right.class.new(new_right_value, right.attribute))
370
+ next predicate unless predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)
371
+ next predicate unless predicate.left.is_a?(::Arel::Attributes::Attribute)
372
+
373
+ relation, column = relation_and_column(predicate.left)
374
+ next predicate unless (type = transposable_attribute_type(relation, column))
375
+
376
+ yield(predicate, relation, column, type)
377
+ end
378
+ end
379
+
380
+ def each_transposable_predicate_value(predicates = nil, &block)
381
+ each_transposable_predicate(predicates) do |predicate, relation, column, type|
382
+ each_transposable_predicate_value_cb(predicate, block) do |value|
383
+ yield(value, predicate, relation, column, type)
316
384
  end
317
- elsif predicate.is_a?(::Arel::Nodes::HomogeneousIn)
385
+ end
386
+ end
387
+
388
+ def each_transposable_predicate_value_cb(node, original_block, &)
389
+ case node
390
+ when Array
391
+ node.filter_map { |val| each_transposable_predicate_value_cb(val, original_block, &).presence }
392
+ when ::ActiveModel::Attribute
393
+ old_value = node.value_before_type_cast
394
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
395
+
396
+ (old_value == new_value) ? node : node.class.new(node.name, new_value, node.type)
397
+ when ::Arel::Nodes::And
398
+ old_value = node.children
399
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
400
+
401
+ (old_value == new_value) ? node : node.class.new(new_value)
402
+ when ::Arel::Nodes::BindParam
403
+ old_value = node.value
404
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
405
+
406
+ (old_value == new_value) ? node : node.class.new(new_value)
407
+ when ::Arel::Nodes::Casted
408
+ old_value = node.value
409
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
410
+
411
+ (old_value == new_value) ? node : node.class.new(new_value, node.attribute)
412
+ when ::Arel::Nodes::HomogeneousIn
413
+ old_value = node.values
414
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
415
+
318
416
  # switch to a regular In, so that Relation::WhereClause#contradiction? knows about it
319
- if new_right_value.empty?
320
- klass = predicate.type == :in ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
321
- klass.new(predicate.attribute, new_right_value)
417
+ if new_value.empty?
418
+ klass = (node.type == :in) ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
419
+ klass.new(node.attribute, new_value)
322
420
  else
323
- predicate.class.new(new_right_value, predicate.attribute, predicate.type)
421
+ (old_value == new_value) ? node : node.class.new(new_value, node.attribute, node.type)
324
422
  end
423
+ when ::Arel::Nodes::Binary
424
+ old_value = node.right
425
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
426
+
427
+ (old_value == new_value) ? node : node.class.new(node.left, new_value)
428
+ when ::Arel::Nodes::SelectStatement
429
+ each_transposable_predicate_value([node], &original_block).first
325
430
  else
326
- predicate.class.new(predicate.left, new_right_value)
431
+ yield(node)
327
432
  end
328
433
  end
329
434
 
330
- def transpose_predicate_value(value, current_shard, target_shard, attribute_type, remove_non_local_ids)
331
- if value.is_a?(::Arel::Nodes::BindParam)
332
- query_att = value.value
333
- current_id = query_att.value_before_type_cast
334
- if current_id.is_a?(::ActiveRecord::StatementCache::Substitute)
335
- current_id.sharded = true # mark for transposition later
336
- current_id.primary = true if attribute_type == :primary
337
- value
338
- else
339
- local_id = Shard.relative_id_for(current_id, current_shard, target_shard) || current_id
340
- local_id = [] if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
341
- if current_id == local_id
342
- # make a new bind param
343
- value
435
+ def transpose_predicates(predicates,
436
+ source_shard,
437
+ target_shard)
438
+ each_transposable_predicate_value(predicates) do |value, _predicate, relation, column, type|
439
+ current_source_shard =
440
+ if source_shard
441
+ source_shard
442
+ elsif type == :primary
443
+ Shard.current(klass.connection_class_for_self)
444
+ elsif type == :foreign
445
+ source_shard_for_foreign_key(relation, column)
344
446
  else
345
- ::Arel::Nodes::BindParam.new(query_att.class.new(query_att.name, local_id, query_att.type))
447
+ primary_shard
346
448
  end
347
- end
449
+
450
+ transpose_predicate_value(value, current_source_shard, target_shard, type)
451
+ end
452
+ end
453
+
454
+ def transpose_predicate_value(value, current_shard, target_shard, attribute_type)
455
+ if value.is_a?(NonTransposingValue)
456
+ value
457
+ elsif value.is_a?(::ActiveRecord::StatementCache::Substitute)
458
+ value.sharded = true # mark for transposition later
459
+ value.primary = true if attribute_type == :primary
460
+ value
348
461
  else
349
- local_id = Shard.relative_id_for(value, current_shard, target_shard) || value
350
- local_id = [] if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
351
- local_id
462
+ Shard.relative_id_for(value, current_shard, target_shard) || value
352
463
  end
353
464
  end
354
465
  end
@@ -5,7 +5,7 @@ module Switchman
5
5
  module Reflection
6
6
  module AbstractReflection
7
7
  def shard(owner)
8
- if polymorphic? || klass.connection_classes == owner.class.connection_classes
8
+ if polymorphic? || klass.connection_class_for_self == owner.class.connection_class_for_self
9
9
  # polymorphic associations assume the same shard as the owning item
10
10
  owner.shard
11
11
  else
@@ -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