switchman 2.0.10 → 3.0.3

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 +10 -2
  3. data/app/models/switchman/shard.rb +234 -270
  4. data/app/models/switchman/unsharded_record.rb +7 -0
  5. data/db/migrate/20130328212039_create_switchman_shards.rb +1 -1
  6. data/db/migrate/20130328224244_create_default_shard.rb +5 -5
  7. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +1 -0
  8. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  9. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +7 -5
  10. data/db/migrate/20190114212900_add_unique_name_indexes.rb +5 -3
  11. data/lib/switchman.rb +3 -3
  12. data/lib/switchman/action_controller/caching.rb +2 -2
  13. data/lib/switchman/active_record/abstract_adapter.rb +1 -0
  14. data/lib/switchman/active_record/association.rb +78 -89
  15. data/lib/switchman/active_record/attribute_methods.rb +106 -52
  16. data/lib/switchman/active_record/base.rb +58 -59
  17. data/lib/switchman/active_record/calculations.rb +73 -66
  18. data/lib/switchman/active_record/connection_pool.rb +14 -41
  19. data/lib/switchman/active_record/database_configurations.rb +34 -0
  20. data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
  21. data/lib/switchman/active_record/finder_methods.rb +11 -16
  22. data/lib/switchman/active_record/log_subscriber.rb +4 -8
  23. data/lib/switchman/active_record/migration.rb +14 -43
  24. data/lib/switchman/active_record/model_schema.rb +1 -1
  25. data/lib/switchman/active_record/persistence.rb +4 -6
  26. data/lib/switchman/active_record/postgresql_adapter.rb +36 -160
  27. data/lib/switchman/active_record/predicate_builder.rb +1 -1
  28. data/lib/switchman/active_record/query_cache.rb +18 -19
  29. data/lib/switchman/active_record/query_methods.rb +183 -182
  30. data/lib/switchman/active_record/reflection.rb +6 -10
  31. data/lib/switchman/active_record/relation.rb +29 -28
  32. data/lib/switchman/active_record/spawn_methods.rb +27 -29
  33. data/lib/switchman/active_record/statement_cache.rb +18 -35
  34. data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
  35. data/lib/switchman/active_support/cache.rb +3 -5
  36. data/lib/switchman/arel.rb +13 -8
  37. data/lib/switchman/database_server.rb +122 -144
  38. data/lib/switchman/default_shard.rb +52 -16
  39. data/lib/switchman/engine.rb +61 -57
  40. data/lib/switchman/environment.rb +4 -8
  41. data/lib/switchman/errors.rb +1 -0
  42. data/lib/switchman/guard_rail.rb +6 -19
  43. data/lib/switchman/guard_rail/relation.rb +5 -7
  44. data/lib/switchman/r_spec_helper.rb +29 -37
  45. data/lib/switchman/rails.rb +14 -12
  46. data/lib/switchman/sharded_instrumenter.rb +1 -1
  47. data/lib/switchman/standard_error.rb +15 -3
  48. data/lib/switchman/test_helper.rb +6 -10
  49. data/lib/switchman/version.rb +1 -1
  50. data/lib/tasks/switchman.rake +54 -69
  51. metadata +86 -45
  52. data/lib/switchman/active_record/batches.rb +0 -11
  53. data/lib/switchman/active_record/connection_handler.rb +0 -172
  54. data/lib/switchman/active_record/where_clause_factory.rb +0 -36
  55. data/lib/switchman/connection_pool_proxy.rb +0 -173
  56. data/lib/switchman/schema_cache.rb +0 -28
@@ -23,4 +23,4 @@ module Switchman
23
23
  end
24
24
  end
25
25
  end
26
- end
26
+ end
@@ -3,31 +3,30 @@
3
3
  module Switchman
4
4
  module ActiveRecord
5
5
  module QueryCache
6
-
7
6
  private
8
7
 
9
8
  def cache_sql(sql, name, binds)
10
9
  # have to include the shard id in the cache key because of switching dbs on the same connection
11
- sql = "#{self.shard.id}::#{sql}"
10
+ sql = "#{shard.id}::#{sql}"
12
11
  @lock.synchronize do
13
12
  result =
14
- if query_cache[sql].key?(binds)
15
- args = {
16
- sql: sql,
17
- binds: binds,
18
- name: name,
19
- connection_id: object_id,
20
- cached: true
21
- }
22
- args[:type_casted_binds] = -> { type_casted_binds(binds) } if ::Rails.version >= '5.1.5'
23
- ::ActiveSupport::Notifications.instrument(
24
- "sql.active_record",
25
- args
26
- )
27
- query_cache[sql][binds]
28
- else
29
- query_cache[sql][binds] = yield
30
- end
13
+ if query_cache[sql].key?(binds)
14
+ args = {
15
+ sql: sql,
16
+ binds: binds,
17
+ name: name,
18
+ connection_id: object_id,
19
+ cached: true,
20
+ type_casted_binds: -> { type_casted_binds(binds) }
21
+ }
22
+ ::ActiveSupport::Notifications.instrument(
23
+ 'sql.active_record',
24
+ args
25
+ )
26
+ query_cache[sql][binds]
27
+ else
28
+ query_cache[sql][binds] = yield
29
+ end
31
30
  result.dup
32
31
  end
33
32
  end
@@ -17,15 +17,20 @@ module Switchman
17
17
  def shard_value
18
18
  @values[:shard]
19
19
  end
20
+
20
21
  def shard_source_value
21
22
  @values[:shard_source]
22
23
  end
24
+
23
25
  def shard_value=(value)
24
26
  raise ::ActiveRecord::ImmutableRelation if @loaded
27
+
25
28
  @values[:shard] = value
26
29
  end
30
+
27
31
  def shard_source_value=(value)
28
32
  raise ::ActiveRecord::ImmutableRelation if @loaded
33
+
29
34
  @values[:shard_source] = value
30
35
  end
31
36
 
@@ -35,11 +40,13 @@ module Switchman
35
40
 
36
41
  def shard!(value, source = :explicit)
37
42
  raise ArgumentError, "shard can't be nil" unless value
38
- old_primary_shard = self.primary_shard
43
+
44
+ old_primary_shard = primary_shard
39
45
  self.shard_value = value
40
46
  self.shard_source_value = source
41
- if (old_primary_shard != self.primary_shard || source == :to_a)
42
- transpose_clauses(old_primary_shard, self.primary_shard, source == :to_a)
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)
43
50
  end
44
51
  self
45
52
  end
@@ -58,7 +65,7 @@ module Switchman
58
65
  when ::ActiveRecord::Relation
59
66
  Shard.default
60
67
  when nil
61
- Shard.current(klass.shard_category)
68
+ Shard.current(klass.connection_classes)
62
69
  else
63
70
  raise ArgumentError, "invalid shard value #{shard_value}"
64
71
  end
@@ -72,114 +79,88 @@ module Switchman
72
79
  when ::ActiveRecord::Base
73
80
  shard_value.respond_to?(:associated_shards) ? shard_value.associated_shards : [shard_value.shard]
74
81
  when nil
75
- [Shard.current(klass.shard_category)]
82
+ [Shard.current(klass.connection_classes)]
76
83
  else
77
84
  shard_value
78
85
  end
79
86
  end
80
87
 
88
+ def or(other)
89
+ super(other.shard(primary_shard))
90
+ end
91
+
81
92
  private
82
93
 
83
- if ::Rails.version >= '5.2'
84
- [:where, :having].each do |type|
85
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
86
- def transpose_#{type}_clauses(source_shard, target_shard, remove_nonlocal_primary_keys)
87
- unless (predicates = #{type}_clause.send(:predicates)).empty?
88
- new_predicates, _binds = transpose_predicates(predicates, source_shard,
89
- target_shard, remove_nonlocal_primary_keys)
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
90
102
  if new_predicates != predicates
91
- self.#{type}_clause = #{type}_clause.dup
92
- if new_predicates != predicates
93
- #{type}_clause.instance_variable_set(:@predicates, new_predicates)
94
- end
103
+ #{type}_clause.instance_variable_set(:@predicates, new_predicates)
95
104
  end
96
105
  end
97
106
  end
98
- RUBY
99
- end
100
- else
101
- [:where, :having].each do |type|
102
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
103
- def transpose_#{type}_clauses(source_shard, target_shard, remove_nonlocal_primary_keys)
104
- unless (predicates = #{type}_clause.send(:predicates)).empty?
105
- new_predicates, new_binds = transpose_predicates(predicates, source_shard,
106
- target_shard, remove_nonlocal_primary_keys,
107
- binds: #{type}_clause.binds,
108
- dup_binds_on_mutation: true)
109
- if new_predicates != predicates || !new_binds.equal?(#{type}_clause.binds)
110
- self.#{type}_clause = #{type}_clause.dup
111
- if new_predicates != predicates
112
- #{type}_clause.instance_variable_set(:@predicates, new_predicates)
113
- end
114
- if !new_binds.equal?(#{type}_clause.binds)
115
- #{type}_clause.instance_variable_set(:@binds, new_binds)
116
- end
117
- end
118
- end
119
- end
120
- RUBY
121
107
  end
108
+ RUBY
122
109
  end
123
110
 
124
- def transpose_clauses(source_shard, target_shard, remove_nonlocal_primary_keys = false)
125
- transpose_where_clauses(source_shard, target_shard, remove_nonlocal_primary_keys)
126
- transpose_having_clauses(source_shard, target_shard, remove_nonlocal_primary_keys)
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)
127
114
  end
128
115
 
129
- def infer_shards_from_primary_key(predicates, binds = nil)
116
+ def infer_shards_from_primary_key(predicates)
130
117
  return unless klass.integral_id?
131
118
 
132
119
  primary_key = predicates.detect do |predicate|
133
- predicate.is_a?(::Arel::Nodes::Binary) && predicate.left.is_a?(::Arel::Attributes::Attribute) &&
134
- predicate.left.relation.is_a?(::Arel::Table) && predicate.left.relation.model == klass &&
120
+ (predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)) &&
121
+ predicate.left.is_a?(::Arel::Attributes::Attribute) &&
122
+ predicate.left.relation.is_a?(::Arel::Table) && predicate.left.relation.klass == klass &&
135
123
  klass.primary_key == predicate.left.name
136
124
  end
137
- if primary_key
138
- case primary_key.right
139
- when Array
140
- id_shards = Set.new
141
- primary_key.right.each do |value|
142
- local_id, id_shard = Shard.local_id_for(value)
143
- id_shard ||= Shard.current(klass.shard_category) if local_id
144
- id_shards << id_shard if id_shard
145
- end
146
- if id_shards.empty?
147
- return
148
- elsif id_shards.length == 1
149
- id_shard = id_shards.first
150
- # prefer to not change the shard
151
- elsif id_shards.include?(primary_shard)
152
- id_shards.delete(primary_shard)
153
- self.shard_value = [primary_shard] + id_shards.to_a
154
- return
155
- else
156
- id_shards = id_shards.to_a
157
- transpose_clauses(primary_shard, id_shards.first)
158
- self.shard_value = id_shards
159
- return
160
- end
161
- when ::Arel::Nodes::BindParam
162
- if ::Rails.version >= "5.2"
163
- local_id, id_shard = Shard.local_id_for(primary_key.right.value.value_before_type_cast)
164
- id_shard ||= Shard.current(klass.shard_category) if local_id
165
- else
166
- # look for a bind param with a matching column name
167
- if binds && bind = binds.detect{|b| b&.name.to_s == klass.primary_key.to_s}
168
- unless bind.value.is_a?(::ActiveRecord::StatementCache::Substitute)
169
- local_id, id_shard = Shard.local_id_for(bind.value)
170
- id_shard ||= Shard.current(klass.shard_category) if local_id
171
- end
172
- end
173
- end
174
- else
175
- local_id, id_shard = Shard.local_id_for(primary_key.right)
176
- id_shard ||= Shard.current(klass.shard_category) if local_id
125
+ return unless primary_key
126
+
127
+ right = primary_key.is_a?(::Arel::Nodes::HomogeneousIn) ? primary_key.values : primary_key.right
128
+
129
+ case right
130
+ when Array
131
+ id_shards = Set.new
132
+ right.each do |value|
133
+ local_id, id_shard = Shard.local_id_for(value)
134
+ id_shard ||= Shard.current(klass.connection_classes) if local_id
135
+ id_shards << id_shard if id_shard
177
136
  end
137
+ return if id_shards.empty?
178
138
 
179
- return if !id_shard || id_shard == primary_shard
180
- transpose_clauses(primary_shard, id_shard)
181
- self.shard_value = id_shard
139
+ if id_shards.length == 1
140
+ id_shard = id_shards.first
141
+ # prefer to not change the shard
142
+ elsif id_shards.include?(primary_shard)
143
+ id_shards.delete(primary_shard)
144
+ self.shard_value = [primary_shard] + id_shards.to_a
145
+ return
146
+ else
147
+ id_shards = id_shards.to_a
148
+ transpose_clauses(primary_shard, id_shards.first)
149
+ self.shard_value = id_shards
150
+ return
151
+ end
152
+ when ::Arel::Nodes::BindParam
153
+ 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
155
+ else
156
+ local_id, id_shard = Shard.local_id_for(right)
157
+ id_shard ||= Shard.current(klass.connection_classes) if local_id
182
158
  end
159
+
160
+ return if !id_shard || id_shard == primary_shard
161
+
162
+ transpose_clauses(primary_shard, id_shard)
163
+ self.shard_value = id_shard
183
164
  end
184
165
 
185
166
  def transposable_attribute_type(relation, column)
@@ -201,8 +182,9 @@ module Switchman
201
182
 
202
183
  def sharded_primary_key?(relation, column)
203
184
  column = column.to_s
204
- return column == 'id' if relation.model == ::ActiveRecord::Base
205
- relation.model.primary_key == column && relation.model.integral_id?
185
+ return column == 'id' if relation.klass == ::ActiveRecord::Base
186
+
187
+ relation.klass.primary_key == column && relation.klass.integral_id?
206
188
  end
207
189
 
208
190
  def source_shard_for_foreign_key(relation, column)
@@ -211,8 +193,9 @@ module Switchman
211
193
  reflection = model.send(:reflection_for_integer_attribute, column)
212
194
  break if reflection
213
195
  end
214
- return Shard.current(klass.shard_category) if reflection.options[:polymorphic]
215
- Shard.current(reflection.klass.shard_category)
196
+ return Shard.current(klass.connection_classes) if reflection.options[:polymorphic]
197
+
198
+ Shard.current(reflection.klass.connection_classes)
216
199
  end
217
200
 
218
201
  def relation_and_column(attribute)
@@ -221,8 +204,31 @@ module Switchman
221
204
  [attribute.relation, column]
222
205
  end
223
206
 
224
- def where_clause_factory
225
- super.tap { |factory| factory.scope = self }
207
+ def build_where_clause(opts, rest = [])
208
+ opts = sanitize_forbidden_attributes(opts)
209
+
210
+ case opts
211
+ when String, Array
212
+ values = Hash === rest.first ? rest.first.values : rest
213
+
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
218
+ end
219
+
220
+ super
221
+ when Hash, ::Arel::Nodes::Node
222
+ where_clause = super
223
+
224
+ predicates = where_clause.send(:predicates)
225
+ infer_shards_from_primary_key(predicates) if shard_source_value == :implicit && shard_value.is_a?(Shard)
226
+ predicates = transpose_predicates(predicates, nil, primary_shard)
227
+ where_clause.instance_variable_set(:@predicates, predicates)
228
+ where_clause
229
+ else
230
+ super
231
+ end
226
232
  end
227
233
 
228
234
  def arel_columns(columns)
@@ -233,101 +239,96 @@ module Switchman
233
239
  connection.with_local_table_name { super }
234
240
  end
235
241
 
236
- # semi-private
237
- public
242
+ def table_name_matches?(from)
243
+ connection.with_global_table_name { super }
244
+ end
245
+
238
246
  def transpose_predicates(predicates,
239
247
  source_shard,
240
248
  target_shard,
241
- remove_nonlocal_primary_keys = false,
242
- binds: nil,
243
- dup_binds_on_mutation: false)
244
- result = predicates.map do |predicate|
245
- next predicate unless predicate.is_a?(::Arel::Nodes::Binary)
246
- next predicate unless predicate.left.is_a?(::Arel::Attributes::Attribute)
247
- relation, column = relation_and_column(predicate.left)
248
- next predicate unless (type = transposable_attribute_type(relation, column))
249
-
250
- remove = true if type == :primary &&
251
- remove_nonlocal_primary_keys &&
252
- predicate.left.relation.model == klass &&
253
- (predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::In))
254
-
255
- current_source_shard =
256
- if source_shard
257
- source_shard
258
- elsif type == :primary
259
- Shard.current(klass.shard_category)
260
- elsif type == :foreign
261
- source_shard_for_foreign_key(relation, column)
262
- end
249
+ remove_nonlocal_primary_keys: false)
250
+ predicates.map do |predicate|
251
+ transpose_single_predicate(predicate, source_shard, target_shard,
252
+ remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
253
+ end
254
+ end
263
255
 
264
- if ::Rails.version >= "5.2"
265
- new_right_value =
266
- case predicate.right
267
- when Array
268
- predicate.right.map {|val| transpose_predicate_value(val, current_source_shard, target_shard, type, remove).presence }.compact
269
- else
270
- transpose_predicate_value(predicate.right, current_source_shard, target_shard, type, remove)
271
- end
272
- else
273
- new_right_value = case predicate.right
274
- when Array
275
- local_ids = []
276
- predicate.right.each do |value|
277
- local_id = Shard.relative_id_for(value, current_source_shard, target_shard)
278
- next unless local_id
279
- unless remove && local_id > Shard::IDS_PER_SHARD
280
- if value.is_a?(::Arel::Nodes::Casted)
281
- if local_id == value.val
282
- local_id = value
283
- elsif local_id != value
284
- local_id = value.class.new(local_id, value.attribute)
285
- end
286
- end
287
- local_ids << local_id
288
- end
289
- end
290
- local_ids
291
- when ::Arel::Nodes::BindParam
292
- # look for a bind param with a matching column name
293
- if binds && bind = binds.detect{|b| b&.name.to_s == predicate.left.name.to_s}
294
- # before we mutate, dup
295
- if dup_binds_on_mutation
296
- binds = binds.map(&:dup)
297
- dup_binds_on_mutation = false
298
- bind = binds.find { |b| b&.name.to_s == predicate.left.name.to_s }
299
- end
300
- if bind.value.is_a?(::ActiveRecord::StatementCache::Substitute)
301
- bind.value.sharded = true # mark for transposition later
302
- bind.value.primary = true if type == :primary
256
+ def transpose_single_predicate(predicate,
257
+ source_shard,
258
+ target_shard,
259
+ remove_nonlocal_primary_keys: false)
260
+ if predicate.is_a?(::Arel::Nodes::Grouping)
261
+ return predicate unless predicate.expr.is_a?(::Arel::Nodes::Or)
262
+
263
+ # Dang, we have an OR. OK, that means we have other epxressions below this
264
+ # level, perhaps many, that may need transposition.
265
+ # the left side and right side must each be treated as predicate lists and
266
+ # transformed in kind, if neither of them changes we can just return the grouping as is.
267
+ # hold on, it's about to get recursive...
268
+ or_expr = predicate.expr
269
+ left_node = or_expr.left
270
+ right_node = or_expr.right
271
+ new_left_predicates = transpose_single_predicate(left_node, source_shard,
272
+ target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
273
+ or_expr.instance_variable_set(:@left, new_left_predicates) if new_left_predicates != left_node
274
+ new_right_predicates = transpose_single_predicate(right_node, source_shard,
275
+ target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
276
+ or_expr.instance_variable_set(:@right, new_right_predicates) if new_right_predicates != right_node
277
+ return predicate
278
+ end
279
+ return predicate unless predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)
280
+ return predicate unless predicate.left.is_a?(::Arel::Attributes::Attribute)
281
+
282
+ relation, column = relation_and_column(predicate.left)
283
+ return predicate unless (type = transposable_attribute_type(relation, column))
284
+
285
+ remove = true if type == :primary &&
286
+ remove_nonlocal_primary_keys &&
287
+ predicate.left.relation.klass == klass &&
288
+ (predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::HomogeneousIn))
289
+
290
+ current_source_shard =
291
+ if source_shard
292
+ source_shard
293
+ elsif type == :primary
294
+ Shard.current(klass.connection_classes)
295
+ elsif type == :foreign
296
+ source_shard_for_foreign_key(relation, column)
297
+ end
298
+
299
+ right = if predicate.is_a?(::Arel::Nodes::HomogeneousIn)
300
+ predicate.values
303
301
  else
304
- local_id = Shard.relative_id_for(bind.value, current_source_shard, target_shard)
305
- local_id = [] if remove && local_id > Shard::IDS_PER_SHARD
306
- bind.instance_variable_set(:@value, local_id)
307
- bind.instance_variable_set(:@value_for_database, nil)
302
+ predicate.right
308
303
  end
309
- end
310
- predicate.right
311
- else
312
- local_id = Shard.relative_id_for(predicate.right, current_source_shard, target_shard) || predicate.right
313
- local_id = [] if remove && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
314
- local_id
315
- end
304
+
305
+ new_right_value =
306
+ case right
307
+ when Array
308
+ right.map { |val| transpose_predicate_value(val, current_source_shard, target_shard, type, remove).presence }.compact
309
+ else
310
+ transpose_predicate_value(right, current_source_shard, target_shard, type, remove)
316
311
  end
317
- if new_right_value == predicate.right
312
+
313
+ if new_right_value == right
314
+ predicate
315
+ elsif predicate.right.is_a?(::Arel::Nodes::Casted)
316
+ if new_right_value == right.value
318
317
  predicate
319
- elsif predicate.right.is_a?(::Arel::Nodes::Casted)
320
- if new_right_value == predicate.right.val
321
- predicate
322
- else
323
- predicate.class.new(predicate.left, predicate.right.class.new(new_right_value, predicate.right.attribute))
324
- end
325
318
  else
326
- predicate.class.new(predicate.left, new_right_value)
319
+ predicate.class.new(predicate.left, right.class.new(new_right_value, right.attribute))
327
320
  end
321
+ elsif predicate.is_a?(::Arel::Nodes::HomogeneousIn)
322
+ # switch to a regular In, so that Relation::WhereClause#contradiction? knows about it
323
+ if new_right_value.empty?
324
+ klass = predicate.type == :in ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
325
+ klass.new(predicate.attribute, new_right_value)
326
+ else
327
+ predicate.class.new(new_right_value, predicate.attribute, predicate.type)
328
+ end
329
+ else
330
+ predicate.class.new(predicate.left, new_right_value)
328
331
  end
329
- result = [result, binds]
330
- result
331
332
  end
332
333
 
333
334
  def transpose_predicate_value(value, current_shard, target_shard, attribute_type, remove_non_local_ids)
@@ -340,12 +341,12 @@ module Switchman
340
341
  value
341
342
  else
342
343
  local_id = Shard.relative_id_for(current_id, current_shard, target_shard) || current_id
343
- return nil if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
344
- if current_id != local_id
344
+ local_id = [] if remove_non_local_ids && local_id.is_a?(Integer) && local_id > Shard::IDS_PER_SHARD
345
+ if current_id == local_id
345
346
  # make a new bind param
346
- ::Arel::Nodes::BindParam.new(query_att.class.new(query_att.name, local_id, query_att.type))
347
- else
348
347
  value
348
+ else
349
+ ::Arel::Nodes::BindParam.new(query_att.class.new(query_att.name, local_id, query_att.type))
349
350
  end
350
351
  end
351
352
  else