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
@@ -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
@@ -21,13 +34,29 @@ module Switchman
21
34
  end
22
35
 
23
36
  def shard_value=(value)
24
- 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
25
46
 
26
47
  @values[:shard] = value
27
48
  end
28
49
 
29
50
  def shard_source_value=(value)
30
- 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
31
60
 
32
61
  @values[:shard_source] = value
33
62
  end
@@ -84,8 +113,20 @@ module Switchman
84
113
  super(other.shard(primary_shard))
85
114
  end
86
115
 
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
123
+
87
124
  protected
88
125
 
126
+ def arel_columns(columns)
127
+ connection.with_local_table_name { super }
128
+ end
129
+
89
130
  def remove_nonlocal_primary_keys!
90
131
  each_transposable_predicate_value do |value, predicate, _relation, _column, type|
91
132
  next value unless
@@ -93,7 +134,7 @@ module Switchman
93
134
  predicate.left.relation.klass == klass &&
94
135
  (predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::HomogeneousIn))
95
136
 
96
- value.is_a?(Integer) && value > Shard::IDS_PER_SHARD ? [] : value
137
+ (value.is_a?(Integer) && value > Shard::IDS_PER_SHARD) ? [] : value
97
138
  end
98
139
  self
99
140
  end
@@ -104,7 +145,9 @@ module Switchman
104
145
  return unless klass.integral_id?
105
146
 
106
147
  primary_key = predicates.detect do |predicate|
107
- (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)) &&
108
151
  predicate.left.is_a?(::Arel::Attributes::Attribute) &&
109
152
  predicate.left.relation.is_a?(::Arel::Table) && predicate.left.relation.klass == klass &&
110
153
  klass.primary_key == predicate.left.name
@@ -167,19 +210,19 @@ module Switchman
167
210
  end
168
211
 
169
212
  def sharded_foreign_key?(relation, column)
170
- models_for_table(relation.table_name).any? { |m| m.sharded_column?(column) }
213
+ models_for_table(relation.name).any? { |m| m.sharded_column?(column) }
171
214
  end
172
215
 
173
216
  def sharded_primary_key?(relation, column)
174
217
  column = column.to_s
175
- return column == 'id' if relation.klass == ::ActiveRecord::Base
218
+ return column == "id" if relation.klass == ::ActiveRecord::Base
176
219
 
177
220
  relation.klass.primary_key == column && relation.klass.integral_id?
178
221
  end
179
222
 
180
223
  def source_shard_for_foreign_key(relation, column)
181
224
  reflection = nil
182
- models_for_table(relation.table_name).each do |model|
225
+ models_for_table(relation.name).each do |model|
183
226
  reflection = model.send(:reflection_for_integer_attribute, column)
184
227
  break if reflection
185
228
  end
@@ -199,12 +242,11 @@ module Switchman
199
242
 
200
243
  case opts
201
244
  when String, Array
202
- values = Hash === rest.first ? rest.first.values : rest
245
+ values = (Hash === rest.first) ? rest.first.values : rest
203
246
 
204
- values.grep(ActiveRecord::Relation) do |rel|
205
- # serialize subqueries against the same shard as the outer query is currently
206
- # targeted to run against
207
- 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."
208
250
  end
209
251
 
210
252
  super
@@ -221,31 +263,49 @@ module Switchman
221
263
  end
222
264
  end
223
265
 
224
- def arel_columns(columns)
225
- connection.with_local_table_name { super }
226
- end
227
-
228
266
  def arel_column(columns)
229
267
  connection.with_local_table_name { super }
230
268
  end
231
269
 
232
270
  def table_name_matches?(from)
233
- 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
234
294
  end
235
295
 
236
- def each_predicate(predicates = nil, &block)
237
- return predicates.map(&block) if predicates
296
+ def each_predicate(predicates = nil, &)
297
+ return predicates.map(&) if predicates
238
298
 
239
- each_predicate_cb(:having_clause, :having_clause=, &block)
240
- each_predicate_cb(:where_clause, :where_clause=, &block)
299
+ each_predicate_cb(:having_clause, :having_clause=, &)
300
+ each_predicate_cb(:where_clause, :where_clause=, &)
241
301
  end
242
302
 
243
- def each_predicate_cb(clause_getter, clause_setter, &block)
303
+ def each_predicate_cb(clause_getter, clause_setter, &)
244
304
  old_clause = send(clause_getter)
245
305
  old_predicates = old_clause.send(:predicates)
246
306
  return if old_predicates.empty?
247
307
 
248
- new_predicates = old_predicates.map(&block)
308
+ new_predicates = old_predicates.map(&)
249
309
  return if new_predicates == old_predicates
250
310
 
251
311
  new_clause = old_clause.dup
@@ -254,9 +314,10 @@ module Switchman
254
314
  send(clause_setter, new_clause)
255
315
  end
256
316
 
257
- def each_transposable_predicate(predicates = nil, &block)
317
+ def each_transposable_predicate(predicates, &block)
258
318
  each_predicate(predicates) do |predicate|
259
- if predicate.is_a?(::Arel::Nodes::Grouping)
319
+ case predicate
320
+ when ::Arel::Nodes::Grouping
260
321
  next predicate unless predicate.expr.is_a?(::Arel::Nodes::Or)
261
322
 
262
323
  or_expr = predicate.expr
@@ -266,7 +327,44 @@ module Switchman
266
327
 
267
328
  next predicate if new_left == old_left && new_right == old_right
268
329
 
269
- 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
+
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)
270
368
  end
271
369
 
272
370
  next predicate unless predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)
@@ -279,54 +377,56 @@ module Switchman
279
377
  end
280
378
  end
281
379
 
282
- def each_transposable_predicate_value(predicates = nil)
380
+ def each_transposable_predicate_value(predicates = nil, &block)
283
381
  each_transposable_predicate(predicates) do |predicate, relation, column, type|
284
- each_transposable_predicate_value_cb(predicate) do |value|
382
+ each_transposable_predicate_value_cb(predicate, block) do |value|
285
383
  yield(value, predicate, relation, column, type)
286
384
  end
287
385
  end
288
386
  end
289
387
 
290
- def each_transposable_predicate_value_cb(node, &block)
388
+ def each_transposable_predicate_value_cb(node, original_block, &)
291
389
  case node
292
390
  when Array
293
- node.map { |val| each_transposable_predicate_value_cb(val, &block).presence }.compact
391
+ node.filter_map { |val| each_transposable_predicate_value_cb(val, original_block, &).presence }
294
392
  when ::ActiveModel::Attribute
295
393
  old_value = node.value_before_type_cast
296
- new_value = each_transposable_predicate_value_cb(old_value, &block)
394
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
297
395
 
298
- old_value == new_value ? node : node.class.new(node.name, new_value, node.type)
396
+ (old_value == new_value) ? node : node.class.new(node.name, new_value, node.type)
299
397
  when ::Arel::Nodes::And
300
398
  old_value = node.children
301
- new_value = each_transposable_predicate_value_cb(old_value, &block)
399
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
302
400
 
303
- old_value == new_value ? node : node.class.new(new_value)
401
+ (old_value == new_value) ? node : node.class.new(new_value)
304
402
  when ::Arel::Nodes::BindParam
305
403
  old_value = node.value
306
- new_value = each_transposable_predicate_value_cb(old_value, &block)
404
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
307
405
 
308
- old_value == new_value ? node : node.class.new(new_value)
406
+ (old_value == new_value) ? node : node.class.new(new_value)
309
407
  when ::Arel::Nodes::Casted
310
408
  old_value = node.value
311
- new_value = each_transposable_predicate_value_cb(old_value, &block)
409
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
312
410
 
313
- old_value == new_value ? node : node.class.new(new_value, node.attribute)
411
+ (old_value == new_value) ? node : node.class.new(new_value, node.attribute)
314
412
  when ::Arel::Nodes::HomogeneousIn
315
413
  old_value = node.values
316
- new_value = each_transposable_predicate_value_cb(old_value, &block)
414
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
317
415
 
318
416
  # switch to a regular In, so that Relation::WhereClause#contradiction? knows about it
319
417
  if new_value.empty?
320
- klass = node.type == :in ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
418
+ klass = (node.type == :in) ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
321
419
  klass.new(node.attribute, new_value)
322
420
  else
323
- old_value == new_value ? node : node.class.new(new_value, node.attribute, node.type)
421
+ (old_value == new_value) ? node : node.class.new(new_value, node.attribute, node.type)
324
422
  end
325
423
  when ::Arel::Nodes::Binary
326
424
  old_value = node.right
327
- new_value = each_transposable_predicate_value_cb(old_value, &block)
425
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &)
328
426
 
329
- old_value == new_value ? node : node.class.new(node.left, new_value)
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
330
430
  else
331
431
  yield(node)
332
432
  end
@@ -343,6 +443,8 @@ module Switchman
343
443
  Shard.current(klass.connection_class_for_self)
344
444
  elsif type == :foreign
345
445
  source_shard_for_foreign_key(relation, column)
446
+ else
447
+ primary_shard
346
448
  end
347
449
 
348
450
  transpose_predicate_value(value, current_source_shard, target_shard, type)
@@ -350,7 +452,9 @@ module Switchman
350
452
  end
351
453
 
352
454
  def transpose_predicate_value(value, current_shard, target_shard, attribute_type)
353
- if value.is_a?(::ActiveRecord::StatementCache::Substitute)
455
+ if value.is_a?(NonTransposingValue)
456
+ value
457
+ elsif value.is_a?(::ActiveRecord::StatementCache::Substitute)
354
458
  value.sharded = true # mark for transposition later
355
459
  value.primary = true if attribute_type == :primary
356
460
  value
@@ -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
 
@@ -44,12 +44,18 @@ module Switchman
44
44
  primary_shard.activate(klass.connection_class_for_self) { super }
45
45
  end
46
46
 
47
- def explain
48
- activate { |relation| relation.call_super(:explain, Relation) }
47
+ if ::Rails.version > "7.1.2"
48
+ def transaction(...)
49
+ primary_shard.activate(klass.connection_class_for_self) { super }
50
+ end
51
+ end
52
+
53
+ def explain(*args)
54
+ activate { |relation| relation.call_super(:explain, Relation, *args) }
49
55
  end
50
56
 
51
57
  def load(&block)
52
- if !loaded? || (::Rails.version >= '7.0' && scheduled?)
58
+ if !loaded? || scheduled?
53
59
  @records = activate { |relation| relation.send(:exec_queries, &block) }
54
60
  @loaded = true
55
61
  end
@@ -58,27 +64,83 @@ module Switchman
58
64
  end
59
65
 
60
66
  %I[update_all delete_all].each do |method|
61
- arg_params = RUBY_VERSION <= '2.8' ? '*args' : '*args, **kwargs'
62
67
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
63
- def #{method}(#{arg_params})
64
- result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation, #{arg_params}) }
68
+ def #{method}(*args, **kwargs)
69
+ result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation, *args, **kwargs) }
65
70
  result = result.sum if result.is_a?(Array)
66
71
  result
67
72
  end
68
73
  RUBY
69
74
  end
70
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
+
71
129
  def find_ids_in_ranges(options = {})
72
130
  is_integer = columns_hash[primary_key.to_s].type == :integer
73
131
  loose_mode = options[:loose] && is_integer
74
132
  # loose_mode: if we don't care about getting exactly batch_size ids in between
75
133
  # don't get the max - just get the min and add batch_size so we get that many _at most_
76
- values = loose_mode ? 'MIN(id)' : 'MIN(id), MAX(id)'
134
+ values = loose_mode ? "MIN(id)" : "MIN(id), MAX(id)"
77
135
 
78
136
  batch_size = options[:batch_size].try(:to_i) || 1000
79
- quoted_primary_key = "#{klass.connection.quote_local_table_name(table_name)}.#{klass.connection.quote_column_name(primary_key)}"
80
- as_id = ' AS id' unless primary_key == 'id'
81
- subquery_scope = except(:select).select("#{quoted_primary_key}#{as_id}").reorder(primary_key.to_sym).limit(loose_mode ? 1 : batch_size)
137
+ quoted_primary_key =
138
+ "#{klass.connection.quote_local_table_name(table_name)}.#{klass.connection.quote_column_name(primary_key)}"
139
+ as_id = " AS id" unless primary_key == "id"
140
+ subquery_scope = except(:select)
141
+ .select("#{quoted_primary_key}#{as_id}")
142
+ .reorder(primary_key.to_sym)
143
+ .limit(loose_mode ? 1 : batch_size)
82
144
  subquery_scope = subquery_scope.where("#{quoted_primary_key} <= ?", options[:end_at]) if options[:end_at]
83
145
 
84
146
  first_subquery_scope = if options[:start_at]
@@ -101,7 +163,7 @@ module Switchman
101
163
  end
102
164
  end
103
165
 
104
- def activate(unordered: false, &block)
166
+ def activate(count: false, unordered: false, &block)
105
167
  shards = all_shards
106
168
  if Array === shards && shards.length == 1
107
169
  if !loaded? && shard_value != shards.first
@@ -121,11 +183,19 @@ module Switchman
121
183
  relation = shard(Shard.current(klass.connection_class_for_self))
122
184
  relation.remove_nonlocal_primary_keys!
123
185
  # do a minimal query if possible
124
- relation = relation.limit(limit_value - result_count) if limit_value && !result_count.zero? && order_values.empty?
186
+ if limit_value && !result_count.zero? && order_values.empty?
187
+ relation = relation.limit(limit_value - result_count)
188
+ end
125
189
 
126
190
  shard_results = relation.activate(&block)
127
191
 
128
- if shard_results.present? && !unordered
192
+ if shard_results.present? && count
193
+ unless shard_results.is_a?(Integer)
194
+ raise "expected integer result for count, got #{shard_results.class.name}"
195
+ end
196
+
197
+ result_count += shard_results
198
+ elsif shard_results.present? && !unordered
129
199
  can_order ||= can_order_cross_shard_results? unless order_values.empty?
130
200
  raise OrderOnMultiShardQuery if !can_order && !order_values.empty? && result_count.positive?
131
201
 
@@ -17,7 +17,7 @@ module Switchman
17
17
  final_shard_source_value = %i[explicit association].detect do |source_value|
18
18
  shard_source_value == source_value || rhs.shard_source_value == source_value
19
19
  end
20
- raise 'unknown shard_source_value' unless final_shard_source_value
20
+ raise "unknown shard_source_value" unless final_shard_source_value
21
21
 
22
22
  # have to merge shard_value
23
23
  lhs_shard_value = all_shards
@@ -36,7 +36,7 @@ module Switchman
36
36
  final_shard_source_value = %i[explicit association implicit].detect do |source_value|
37
37
  shard_source_value == source_value || rhs.shard_source_value == source_value
38
38
  end
39
- raise 'unknown shard_source_value' unless final_shard_source_value
39
+ raise "unknown shard_source_value" unless final_shard_source_value
40
40
  end
41
41
 
42
42
  [final_shard_value, final_primary_shard, final_shard_source_value]
@@ -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,8 +4,8 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module StatementCache
6
6
  module ClassMethods
7
- def create(connection, &block)
8
- relation = block.call ::ActiveRecord::StatementCache::Params.new
7
+ def create(connection)
8
+ relation = yield ::ActiveRecord::StatementCache::Params.new
9
9
 
10
10
  _query_builder, binds = connection.cacheable_query(self, relation.arel)
11
11
  bind_map = ::ActiveRecord::StatementCache::BindMap.new(binds)
@@ -29,14 +29,14 @@ module Switchman
29
29
  params, connection = args
30
30
  klass = @klass
31
31
  target_shard = nil
32
- if (primary_index = bind_map.primary_value_index)
32
+ if (primary_index = @bind_map.primary_value_index)
33
33
  primary_value = params[primary_index]
34
34
  target_shard = Shard.local_id_for(primary_value)[1]
35
35
  end
36
36
  current_shard = Shard.current(klass.connection_class_for_self)
37
37
  target_shard ||= current_shard
38
38
 
39
- bind_values = bind_map.bind(params, current_shard, target_shard)
39
+ bind_values = @bind_map.bind(params, current_shard, target_shard)
40
40
 
41
41
  target_shard.activate(klass.connection_class_for_self) do
42
42
  sql = qualified_query_builder(target_shard, klass).sql_for(bind_values, connection)
@@ -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
@@ -7,9 +7,14 @@ module Switchman
7
7
  def drop(*)
8
8
  super
9
9
  # no really, it's gone
10
- Switchman.cache.delete('default_shard')
10
+ Switchman.cache.delete("default_shard")
11
11
  Shard.default(reload: true)
12
12
  end
13
+
14
+ def raise_for_multi_db(*)
15
+ # ignore; Switchman doesn't use namespaced tasks for multiple shards; it uses
16
+ # environment variables to filter which shards you want to target
17
+ end
13
18
  end
14
19
  end
15
20
  end