switchman 3.4.2 → 3.6.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) 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 +4 -2
  6. data/lib/switchman/active_record/associations.rb +89 -16
  7. data/lib/switchman/active_record/attribute_methods.rb +67 -22
  8. data/lib/switchman/active_record/base.rb +112 -22
  9. data/lib/switchman/active_record/calculations.rb +93 -37
  10. data/lib/switchman/active_record/connection_handler.rb +18 -0
  11. data/lib/switchman/active_record/connection_pool.rb +18 -14
  12. data/lib/switchman/active_record/database_configurations.rb +37 -15
  13. data/lib/switchman/active_record/finder_methods.rb +44 -14
  14. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  15. data/lib/switchman/active_record/migration.rb +28 -9
  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 +11 -10
  19. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  20. data/lib/switchman/active_record/query_cache.rb +49 -20
  21. data/lib/switchman/active_record/query_methods.rb +93 -30
  22. data/lib/switchman/active_record/relation.rb +22 -11
  23. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  24. data/lib/switchman/active_record/statement_cache.rb +2 -2
  25. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  26. data/lib/switchman/active_record/test_fixtures.rb +26 -16
  27. data/lib/switchman/active_support/cache.rb +9 -4
  28. data/lib/switchman/arel.rb +34 -18
  29. data/lib/switchman/call_super.rb +2 -8
  30. data/lib/switchman/database_server.rb +68 -21
  31. data/lib/switchman/default_shard.rb +14 -3
  32. data/lib/switchman/engine.rb +39 -19
  33. data/lib/switchman/environment.rb +2 -2
  34. data/lib/switchman/errors.rb +4 -1
  35. data/lib/switchman/guard_rail/relation.rb +1 -2
  36. data/lib/switchman/parallel.rb +5 -5
  37. data/lib/switchman/r_spec_helper.rb +11 -11
  38. data/lib/switchman/shard.rb +166 -64
  39. data/lib/switchman/sharded_instrumenter.rb +7 -3
  40. data/lib/switchman/standard_error.rb +4 -0
  41. data/lib/switchman/test_helper.rb +2 -2
  42. data/lib/switchman/version.rb +1 -1
  43. data/lib/switchman.rb +27 -15
  44. data/lib/tasks/switchman.rake +117 -51
  45. metadata +19 -44
@@ -42,26 +42,56 @@ module Switchman
42
42
  primary_shard.activate { super }
43
43
  end
44
44
 
45
- def exists?(conditions = :none)
46
- conditions = conditions.id if ::ActiveRecord::Base === conditions
47
- return false unless conditions
45
+ if ::Rails.version < "7.1"
46
+ def exists?(conditions = :none)
47
+ conditions = conditions.id if ::ActiveRecord::Base === conditions
48
+ return false unless conditions
48
49
 
49
- relation = apply_join_dependency(eager_loading: false)
50
- return false if ::ActiveRecord::NullRelation === relation
50
+ relation = apply_join_dependency(eager_loading: false)
51
+ return false if ::ActiveRecord::NullRelation === relation
51
52
 
52
- relation = relation.except(:select, :order).select('1 AS one').limit(1)
53
+ relation = relation.except(:select, :order).select("1 AS one").limit(1)
53
54
 
54
- case conditions
55
- when Array, Hash
56
- relation = relation.where(conditions)
57
- else
58
- relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
55
+ case conditions
56
+ when Array, Hash
57
+ relation = relation.where(conditions)
58
+ else
59
+ relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
60
+ end
61
+
62
+ relation.activate do |shard_rel|
63
+ return true if connection.select_value(shard_rel.arel, "#{name} Exists")
64
+ end
65
+ false
59
66
  end
67
+ else
68
+ def exists?(conditions = :none)
69
+ return false if @none
70
+
71
+ if Base === conditions
72
+ raise ArgumentError, <<-TEXT.squish
73
+ You are passing an instance of ActiveRecord::Base to `exists?`.
74
+ Please pass the id of the object by calling `.id`.
75
+ TEXT
76
+ end
60
77
 
61
- relation.activate do |shard_rel|
62
- return true if connection.select_value(shard_rel.arel, "#{name} Exists")
78
+ return false if !conditions || limit_value == 0 # rubocop:disable Style/NumericPredicate
79
+
80
+ if eager_loading?
81
+ relation = apply_join_dependency(eager_loading: false)
82
+ return relation.exists?(conditions)
83
+ end
84
+
85
+ relation = construct_relation_for_exists(conditions)
86
+ return false if relation.where_clause.contradiction?
87
+
88
+ relation.activate do |shard_rel|
89
+ return true if skip_query_cache_if_necessary do
90
+ connection.select_rows(shard_rel.arel, "#{name} Exists?").size == 1
91
+ end
92
+ end
93
+ false
63
94
  end
64
- false
65
95
  end
66
96
  end
67
97
  end
@@ -5,8 +5,10 @@ module Switchman
5
5
  module LogSubscriber
6
6
  # sadly, have to completely replace this
7
7
  def sql(event)
8
- self.class.runtime += event.duration
9
- return unless logger.debug?
8
+ if ::Rails.version < "7.1"
9
+ self.class.runtime += event.duration
10
+ return unless logger.debug?
11
+ end
10
12
 
11
13
  payload = event.payload
12
14
 
@@ -14,20 +16,24 @@ module Switchman
14
16
 
15
17
  name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
16
18
  name = "CACHE #{name}" if payload[:cached]
17
- sql = payload[:sql].squeeze(' ')
19
+ sql = payload[:sql].squeeze(" ")
18
20
  binds = nil
19
21
  shard = payload[:shard]
20
22
  shard = " [#{shard[:database_server_id]}:#{shard[:id]} #{shard[:env]}]" if shard
21
23
 
22
24
  unless (payload[:binds] || []).empty?
23
25
  casted_params = type_casted_binds(payload[:type_casted_binds])
24
- binds = ' ' + payload[:binds].zip(casted_params).map do |attr, value|
26
+ binds = " " + payload[:binds].zip(casted_params).map do |attr, value|
25
27
  render_bind(attr, value)
26
28
  end.inspect
27
29
  end
28
30
 
29
31
  name = colorize_payload_name(name, payload[:name])
30
- sql = color(sql, sql_color(sql), true)
32
+ sql = if ::Rails.version < "7.1"
33
+ color(sql, sql_color(sql), true)
34
+ else
35
+ color(sql, sql_color(sql), bold: true)
36
+ end
31
37
 
32
38
  debug " #{name} #{sql}#{binds}#{shard}"
33
39
  end
@@ -14,7 +14,9 @@ module Switchman
14
14
 
15
15
  def connection
16
16
  conn = super
17
- ::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base.current_switchman_shard
17
+ if conn.shard != ::ActiveRecord::Base.current_switchman_shard
18
+ ::ActiveRecord::Base.connection_pool.switch_database(conn)
19
+ end
18
20
  conn
19
21
  end
20
22
  end
@@ -27,7 +29,11 @@ module Switchman
27
29
  db_name_hash = Zlib.crc32(Shard.current.name)
28
30
  shard_name_hash = ::ActiveRecord::Migrator::MIGRATOR_SALT * db_name_hash
29
31
  # Store in internalmetadata to allow other tools to be able to lock out migrations
30
- ::ActiveRecord::InternalMetadata[:migrator_advisory_lock_id] = shard_name_hash
32
+ if ::Rails.version < "7.1"
33
+ ::ActiveRecord::InternalMetadata[:migrator_advisory_lock_id] = shard_name_hash
34
+ else
35
+ ::ActiveRecord::InternalMetadata.new(connection)[:migrator_advisory_lock_id] = shard_name_hash
36
+ end
31
37
  shard_name_hash
32
38
  end
33
39
 
@@ -46,17 +52,30 @@ module Switchman
46
52
  module MigrationContext
47
53
  def migrate(...)
48
54
  connection = ::ActiveRecord::Base.connection
49
- connection_pool = ::ActiveRecord::Base.connection_pool
50
- previous_schema_cache = connection_pool.get_schema_cache(connection)
51
- temporary_schema_cache = ::ActiveRecord::ConnectionAdapters::SchemaCache.new(connection)
55
+ schema_cache_holder = ::ActiveRecord::Base.connection_pool
56
+ schema_cache_holder = schema_cache_holder.schema_reflection if ::Rails.version >= "7.1"
57
+ previous_schema_cache = if ::Rails.version < "7.1"
58
+ schema_cache_holder.get_schema_cache(connection)
59
+ else
60
+ schema_cache_holder.instance_variable_get(:@cache)
61
+ end
52
62
 
53
- reset_column_information
54
- connection_pool.set_schema_cache(temporary_schema_cache)
63
+ if ::Rails.version < "7.1"
64
+ temporary_schema_cache = ::ActiveRecord::ConnectionAdapters::SchemaCache.new(connection)
65
+
66
+ reset_column_information
67
+ schema_cache_holder.set_schema_cache(temporary_schema_cache)
68
+ else
69
+ schema_cache_holder.instance_variable_get(:@cache)
70
+
71
+ reset_column_information
72
+ schema_cache_holder.clear!
73
+ end
55
74
 
56
75
  begin
57
76
  super(...)
58
77
  ensure
59
- connection_pool.set_schema_cache(previous_schema_cache)
78
+ schema_cache_holder.set_schema_cache(previous_schema_cache)
60
79
  reset_column_information
61
80
  end
62
81
  end
@@ -65,7 +84,7 @@ module Switchman
65
84
  return @migrations if instance_variable_defined?(:@migrations)
66
85
 
67
86
  migrations_cache = Thread.current[:migrations_cache] ||= {}
68
- key = Digest::MD5.hexdigest(migration_files.sort.join(','))
87
+ key = Digest::MD5.hexdigest(migration_files.sort.join(","))
69
88
  @migrations = migrations_cache[key] ||= super
70
89
  end
71
90
 
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module PendingMigrationConnection
6
+ module ClassMethods
7
+ def current_role
8
+ ::ActiveRecord::Base.current_role
9
+ end
10
+
11
+ def current_switchman_shard
12
+ ::ActiveRecord::Base.current_switchman_shard
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -5,10 +5,12 @@ module Switchman
5
5
  module Persistence
6
6
  # touch reads the id attribute directly, so it's not relative to the current shard
7
7
  def touch(*, **)
8
+ writable_shadow_record_warning
8
9
  shard.activate(self.class.connection_class_for_self) { super }
9
10
  end
10
11
 
11
12
  def update_columns(*)
13
+ writable_shadow_record_warning
12
14
  shard.activate(self.class.connection_class_for_self) { super }
13
15
  end
14
16
 
@@ -17,13 +19,33 @@ module Switchman
17
19
  db.unguard { super }
18
20
  end
19
21
 
22
+ def destroy
23
+ writable_shadow_record_warning
24
+ super
25
+ end
26
+
27
+ def create_or_update(**, &block)
28
+ writable_shadow_record_warning
29
+ super
30
+ end
31
+
20
32
  def reload(*)
21
33
  res = super
22
34
  # When a shadow record is reloaded the real record is returned. So
23
35
  # we need to ensure the loaded_from_shard is set correctly after a reload.
24
36
  @loaded_from_shard = @shard
37
+ if @readonly_from_shadow
38
+ @readonly_from_shadow = false
39
+ @readonly = false
40
+ end
25
41
  res
26
42
  end
43
+
44
+ def writable_shadow_record_warning
45
+ return unless shadow_record? && Switchman.config[:writable_shadow_records]
46
+
47
+ Switchman::Deprecation.warn("writing to shadow records is not supported")
48
+ end
27
49
  end
28
50
  end
29
51
  end
@@ -5,9 +5,9 @@ module Switchman
5
5
  module PostgreSQLAdapter
6
6
  # copy/paste; use quote_local_table_name
7
7
  def create_database(name, options = {})
8
- options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
8
+ options = { encoding: "utf8" }.merge!(options.symbolize_keys)
9
9
 
10
- option_string = options.sum('') do |key, value|
10
+ option_string = options.sum("") do |key, value|
11
11
  case key
12
12
  when :owner
13
13
  " OWNER = \"#{value}\""
@@ -24,7 +24,7 @@ module Switchman
24
24
  when :connection_limit
25
25
  " CONNECTION LIMIT = #{value}"
26
26
  else
27
- ''
27
+ ""
28
28
  end
29
29
  end
30
30
 
@@ -37,7 +37,7 @@ module Switchman
37
37
  end
38
38
 
39
39
  def current_schemas
40
- select_values('SELECT * FROM unnest(current_schemas(false))')
40
+ select_values("SELECT * FROM unnest(current_schemas(false))")
41
41
  end
42
42
 
43
43
  def extract_schema_qualified_name(string)
@@ -49,13 +49,13 @@ module Switchman
49
49
  # significant change: use the shard name if no explicit schema
50
50
  def quoted_scope(name = nil, type: nil)
51
51
  schema, name = extract_schema_qualified_name(name)
52
- type = \
52
+ type =
53
53
  case type # rubocop:disable Style/HashLikeCase
54
- when 'BASE TABLE'
54
+ when "BASE TABLE"
55
55
  "'r','p'"
56
- when 'VIEW'
56
+ when "VIEW"
57
57
  "'v','m'"
58
- when 'FOREIGN TABLE'
58
+ when "FOREIGN TABLE"
59
59
  "'f'"
60
60
  end
61
61
  scope = {}
@@ -67,7 +67,8 @@ module Switchman
67
67
 
68
68
  def foreign_keys(table_name)
69
69
  super.each do |fk|
70
- to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
70
+ to_table_qualified_name =
71
+ ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
71
72
  fk.to_table = to_table_qualified_name.identifier if to_table_qualified_name.schema == shard.name
72
73
  end
73
74
  end
@@ -101,7 +102,7 @@ module Switchman
101
102
 
102
103
  def add_index_options(_table_name, _column_name, **)
103
104
  index, algorithm, if_not_exists = super
104
- algorithm = nil if DatabaseServer.creating_new_shard && algorithm == 'CONCURRENTLY'
105
+ algorithm = nil if DatabaseServer.creating_new_shard && algorithm == "CONCURRENTLY"
105
106
  [index, algorithm, if_not_exists]
106
107
  end
107
108
 
@@ -11,11 +11,11 @@ module Switchman
11
11
  end
12
12
  end
13
13
 
14
- module AssociationQueryValue
14
+ module PolymorphicArrayValue
15
15
  def convert_to_id(value)
16
16
  case value
17
17
  when ::ActiveRecord::Base
18
- value.id
18
+ value.send(primary_key(value))
19
19
  else
20
20
  super
21
21
  end
@@ -5,28 +5,57 @@ module Switchman
5
5
  module QueryCache
6
6
  private
7
7
 
8
- def cache_sql(sql, name, binds)
9
- # have to include the shard id in the cache key because of switching dbs on the same connection
10
- sql = "#{shard.id}::#{sql}"
11
- @lock.synchronize do
12
- result =
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]
8
+ if ::Rails.version < "7.1"
9
+ def cache_sql(sql, name, binds)
10
+ # have to include the shard id in the cache key because of switching dbs on the same connection
11
+ sql = "#{shard.id}::#{sql}"
12
+ @lock.synchronize do
13
+ 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
+ type_casted_binds: -> { type_casted_binds(binds) }
22
+ }
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
31
+ result.dup
32
+ end
33
+ end
34
+ else
35
+ def cache_sql(sql, name, binds)
36
+ # have to include the shard id in the cache key because of switching dbs on the same connection
37
+ sql = "#{shard.id}::#{sql}"
38
+ key = binds.empty? ? sql : [sql, binds]
39
+ result = nil
40
+ hit = false
41
+
42
+ @lock.synchronize do
43
+ if (result = @query_cache.delete(key))
44
+ hit = true
45
+ @query_cache[key] = result
27
46
  else
28
- query_cache[sql][binds] = yield
47
+ result = @query_cache[key] = yield
48
+ @query_cache.shift if @query_cache_max_size && @query_cache.size > @query_cache_max_size
29
49
  end
50
+ end
51
+
52
+ if hit
53
+ ::ActiveSupport::Notifications.instrument(
54
+ "sql.active_record",
55
+ cache_notification_info(sql, name, binds)
56
+ )
57
+ end
58
+
30
59
  result.dup
31
60
  end
32
61
  end
@@ -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
@@ -84,6 +97,14 @@ module Switchman
84
97
  super(other.shard(primary_shard))
85
98
  end
86
99
 
100
+ # use a temp variable so that the new where clause is built before self.where_clause is read,
101
+ # since build_where_clause might mutate self.where_clause
102
+ def where!(opts, *rest)
103
+ new_clause = build_where_clause(opts, rest)
104
+ self.where_clause += new_clause
105
+ self
106
+ end
107
+
87
108
  protected
88
109
 
89
110
  def remove_nonlocal_primary_keys!
@@ -93,7 +114,7 @@ module Switchman
93
114
  predicate.left.relation.klass == klass &&
94
115
  (predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::HomogeneousIn))
95
116
 
96
- value.is_a?(Integer) && value > Shard::IDS_PER_SHARD ? [] : value
117
+ (value.is_a?(Integer) && value > Shard::IDS_PER_SHARD) ? [] : value
97
118
  end
98
119
  self
99
120
  end
@@ -104,7 +125,9 @@ module Switchman
104
125
  return unless klass.integral_id?
105
126
 
106
127
  primary_key = predicates.detect do |predicate|
107
- (predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)) &&
128
+ (predicate.is_a?(::Arel::Nodes::Equality) ||
129
+ predicate.is_a?(::Arel::Nodes::In) ||
130
+ predicate.is_a?(::Arel::Nodes::HomogeneousIn)) &&
108
131
  predicate.left.is_a?(::Arel::Attributes::Attribute) &&
109
132
  predicate.left.relation.is_a?(::Arel::Table) && predicate.left.relation.klass == klass &&
110
133
  klass.primary_key == predicate.left.name
@@ -167,19 +190,19 @@ module Switchman
167
190
  end
168
191
 
169
192
  def sharded_foreign_key?(relation, column)
170
- models_for_table(relation.table_name).any? { |m| m.sharded_column?(column) }
193
+ models_for_table(relation.name).any? { |m| m.sharded_column?(column) }
171
194
  end
172
195
 
173
196
  def sharded_primary_key?(relation, column)
174
197
  column = column.to_s
175
- return column == 'id' if relation.klass == ::ActiveRecord::Base
198
+ return column == "id" if relation.klass == ::ActiveRecord::Base
176
199
 
177
200
  relation.klass.primary_key == column && relation.klass.integral_id?
178
201
  end
179
202
 
180
203
  def source_shard_for_foreign_key(relation, column)
181
204
  reflection = nil
182
- models_for_table(relation.table_name).each do |model|
205
+ models_for_table(relation.name).each do |model|
183
206
  reflection = model.send(:reflection_for_integer_attribute, column)
184
207
  break if reflection
185
208
  end
@@ -199,12 +222,11 @@ module Switchman
199
222
 
200
223
  case opts
201
224
  when String, Array
202
- values = Hash === rest.first ? rest.first.values : rest
225
+ values = (Hash === rest.first) ? rest.first.values : rest
203
226
 
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
227
+ if shard_source_value != :explicit && values.grep(ActiveRecord::Relation).first
228
+ raise "Sub-queries are not allowed as simple substitutions; " \
229
+ "please build your relation with more structured methods so that Switchman is able to introspect it."
208
230
  end
209
231
 
210
232
  super
@@ -254,9 +276,10 @@ module Switchman
254
276
  send(clause_setter, new_clause)
255
277
  end
256
278
 
257
- def each_transposable_predicate(predicates = nil, &block)
279
+ def each_transposable_predicate(predicates, &block)
258
280
  each_predicate(predicates) do |predicate|
259
- if predicate.is_a?(::Arel::Nodes::Grouping)
281
+ case predicate
282
+ when ::Arel::Nodes::Grouping
260
283
  next predicate unless predicate.expr.is_a?(::Arel::Nodes::Or)
261
284
 
262
285
  or_expr = predicate.expr
@@ -267,6 +290,40 @@ module Switchman
267
290
  next predicate if new_left == old_left && new_right == old_right
268
291
 
269
292
  next predicate.class.new predicate.expr.class.new(new_left, new_right)
293
+ when ::Arel::Nodes::SelectStatement
294
+ new_cores = predicate.cores.map do |core|
295
+ next core unless core.is_a?(::Arel::Nodes::SelectCore) # just in case something weird is going on
296
+
297
+ new_wheres = each_transposable_predicate(core.wheres, &block)
298
+ new_havings = each_transposable_predicate(core.havings, &block)
299
+
300
+ next core if core.wheres == new_wheres && core.havings == new_havings
301
+
302
+ new_core = core.clone
303
+ new_core.wheres = new_wheres
304
+ new_core.havings = new_havings
305
+ new_core
306
+ end
307
+
308
+ next predicate if predicate.cores == new_cores
309
+
310
+ new_node = predicate.clone
311
+ new_node.instance_variable_set(:@cores, new_cores)
312
+ next new_node
313
+ when ::Arel::Nodes::Not
314
+ old_value = predicate.expr
315
+ new_value = each_transposable_predicate([old_value], &block).first
316
+
317
+ next predicate if old_value == new_value
318
+
319
+ next predicate.class.new(new_value)
320
+ when ::Arel::Nodes::Exists
321
+ old_value = predicate.expressions
322
+ new_value = each_transposable_predicate([old_value], &block).first
323
+
324
+ next predicate if old_value == new_value
325
+
326
+ next predicate.class.new(new_value)
270
327
  end
271
328
 
272
329
  next predicate unless predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)
@@ -279,54 +336,56 @@ module Switchman
279
336
  end
280
337
  end
281
338
 
282
- def each_transposable_predicate_value(predicates = nil)
339
+ def each_transposable_predicate_value(predicates = nil, &block)
283
340
  each_transposable_predicate(predicates) do |predicate, relation, column, type|
284
- each_transposable_predicate_value_cb(predicate) do |value|
341
+ each_transposable_predicate_value_cb(predicate, block) do |value|
285
342
  yield(value, predicate, relation, column, type)
286
343
  end
287
344
  end
288
345
  end
289
346
 
290
- def each_transposable_predicate_value_cb(node, &block)
347
+ def each_transposable_predicate_value_cb(node, original_block, &block)
291
348
  case node
292
349
  when Array
293
- node.map { |val| each_transposable_predicate_value_cb(val, &block).presence }.compact
350
+ node.filter_map { |val| each_transposable_predicate_value_cb(val, original_block, &block).presence }
294
351
  when ::ActiveModel::Attribute
295
352
  old_value = node.value_before_type_cast
296
- new_value = each_transposable_predicate_value_cb(old_value, &block)
353
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
297
354
 
298
- old_value == new_value ? node : node.class.new(node.name, new_value, node.type)
355
+ (old_value == new_value) ? node : node.class.new(node.name, new_value, node.type)
299
356
  when ::Arel::Nodes::And
300
357
  old_value = node.children
301
- new_value = each_transposable_predicate_value_cb(old_value, &block)
358
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
302
359
 
303
- old_value == new_value ? node : node.class.new(new_value)
360
+ (old_value == new_value) ? node : node.class.new(new_value)
304
361
  when ::Arel::Nodes::BindParam
305
362
  old_value = node.value
306
- new_value = each_transposable_predicate_value_cb(old_value, &block)
363
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
307
364
 
308
- old_value == new_value ? node : node.class.new(new_value)
365
+ (old_value == new_value) ? node : node.class.new(new_value)
309
366
  when ::Arel::Nodes::Casted
310
367
  old_value = node.value
311
- new_value = each_transposable_predicate_value_cb(old_value, &block)
368
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
312
369
 
313
- old_value == new_value ? node : node.class.new(new_value, node.attribute)
370
+ (old_value == new_value) ? node : node.class.new(new_value, node.attribute)
314
371
  when ::Arel::Nodes::HomogeneousIn
315
372
  old_value = node.values
316
- new_value = each_transposable_predicate_value_cb(old_value, &block)
373
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
317
374
 
318
375
  # switch to a regular In, so that Relation::WhereClause#contradiction? knows about it
319
376
  if new_value.empty?
320
- klass = node.type == :in ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
377
+ klass = (node.type == :in) ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
321
378
  klass.new(node.attribute, new_value)
322
379
  else
323
- old_value == new_value ? node : node.class.new(new_value, node.attribute, node.type)
380
+ (old_value == new_value) ? node : node.class.new(new_value, node.attribute, node.type)
324
381
  end
325
382
  when ::Arel::Nodes::Binary
326
383
  old_value = node.right
327
- new_value = each_transposable_predicate_value_cb(old_value, &block)
384
+ new_value = each_transposable_predicate_value_cb(old_value, original_block, &block)
328
385
 
329
- old_value == new_value ? node : node.class.new(node.left, new_value)
386
+ (old_value == new_value) ? node : node.class.new(node.left, new_value)
387
+ when ::Arel::Nodes::SelectStatement
388
+ each_transposable_predicate_value([node], &original_block).first
330
389
  else
331
390
  yield(node)
332
391
  end
@@ -343,6 +402,8 @@ module Switchman
343
402
  Shard.current(klass.connection_class_for_self)
344
403
  elsif type == :foreign
345
404
  source_shard_for_foreign_key(relation, column)
405
+ else
406
+ primary_shard
346
407
  end
347
408
 
348
409
  transpose_predicate_value(value, current_source_shard, target_shard, type)
@@ -350,7 +411,9 @@ module Switchman
350
411
  end
351
412
 
352
413
  def transpose_predicate_value(value, current_shard, target_shard, attribute_type)
353
- if value.is_a?(::ActiveRecord::StatementCache::Substitute)
414
+ if value.is_a?(NonTransposingValue)
415
+ value
416
+ elsif value.is_a?(::ActiveRecord::StatementCache::Substitute)
354
417
  value.sharded = true # mark for transposition later
355
418
  value.primary = true if attribute_type == :primary
356
419
  value