switchman 3.4.2 → 3.6.7

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 (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