switchman 3.0.2 → 3.1.0

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 +1 -1
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  4. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
  5. data/lib/switchman/action_controller/caching.rb +2 -2
  6. data/lib/switchman/active_record/abstract_adapter.rb +2 -13
  7. data/lib/switchman/active_record/associations.rb +223 -0
  8. data/lib/switchman/active_record/attribute_methods.rb +144 -63
  9. data/lib/switchman/active_record/base.rb +100 -43
  10. data/lib/switchman/active_record/calculations.rb +12 -5
  11. data/lib/switchman/active_record/connection_pool.rb +9 -31
  12. data/lib/switchman/active_record/database_configurations.rb +18 -2
  13. data/lib/switchman/active_record/finder_methods.rb +2 -2
  14. data/lib/switchman/active_record/migration.rb +7 -4
  15. data/lib/switchman/active_record/model_schema.rb +1 -1
  16. data/lib/switchman/active_record/persistence.rb +7 -2
  17. data/lib/switchman/active_record/postgresql_adapter.rb +6 -2
  18. data/lib/switchman/active_record/predicate_builder.rb +1 -1
  19. data/lib/switchman/active_record/query_methods.rb +27 -14
  20. data/lib/switchman/active_record/reflection.rb +1 -1
  21. data/lib/switchman/active_record/relation.rb +25 -24
  22. data/lib/switchman/active_record/statement_cache.rb +2 -2
  23. data/lib/switchman/active_record/table_definition.rb +1 -1
  24. data/lib/switchman/active_record/test_fixtures.rb +43 -0
  25. data/lib/switchman/active_support/cache.rb +16 -0
  26. data/lib/switchman/arel.rb +28 -6
  27. data/lib/switchman/database_server.rb +71 -65
  28. data/lib/switchman/default_shard.rb +0 -2
  29. data/lib/switchman/engine.rb +67 -125
  30. data/lib/switchman/errors.rb +4 -2
  31. data/lib/switchman/guard_rail/relation.rb +6 -9
  32. data/lib/switchman/guard_rail.rb +5 -0
  33. data/lib/switchman/parallel.rb +68 -0
  34. data/lib/switchman/r_spec_helper.rb +5 -17
  35. data/lib/switchman/rails.rb +1 -4
  36. data/{app/models → lib}/switchman/shard.rb +61 -188
  37. data/lib/switchman/sharded_instrumenter.rb +1 -1
  38. data/lib/switchman/standard_error.rb +11 -12
  39. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  40. data/lib/switchman/version.rb +1 -1
  41. data/lib/switchman.rb +22 -2
  42. data/lib/tasks/switchman.rake +24 -13
  43. metadata +24 -22
  44. data/lib/switchman/active_record/association.rb +0 -206
  45. data/lib/switchman/open4.rb +0 -80
@@ -14,8 +14,6 @@ module Switchman
14
14
  def sharded_model
15
15
  self.abstract_class = true
16
16
 
17
- return if self == UnshardedRecord
18
-
19
17
  Shard.send(:add_sharded_model, self)
20
18
  end
21
19
 
@@ -27,20 +25,12 @@ module Switchman
27
25
  def transaction(**)
28
26
  if self != ::ActiveRecord::Base && current_scope
29
27
  current_scope.activate do
30
- db = Shard.current(connection_classes).database_server
31
- if ::GuardRail.environment == db.guard_rail_environment
32
- super
33
- else
34
- db.unguard { super }
35
- end
36
- end
37
- else
38
- db = Shard.current(connection_classes).database_server
39
- if ::GuardRail.environment == db.guard_rail_environment
40
- super
41
- else
28
+ db = Shard.current(connection_class_for_self).database_server
42
29
  db.unguard { super }
43
30
  end
31
+ else
32
+ db = Shard.current(connection_class_for_self).database_server
33
+ db.unguard { super }
44
34
  end
45
35
  end
46
36
 
@@ -68,30 +58,91 @@ module Switchman
68
58
  end
69
59
  end
70
60
 
61
+ def role_overriden?(shard_id)
62
+ current_role(target_shard: shard_id) != current_role(without_overrides: true)
63
+ end
64
+
65
+ def establish_connection(config_or_env = nil)
66
+ raise ArgumentError, 'establish connection cannot be used on the non-current shard/role' if config_or_env.is_a?(Symbol) && config_or_env != ::Rails.env.to_sym
67
+
68
+ # Ensure we don't randomly surprise change the connection parms associated with a shard/role
69
+ config_or_env = nil if config_or_env == ::Rails.env.to_sym
70
+
71
+ config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
72
+ :primary
73
+ else
74
+ "#{current_shard}/#{current_role}".to_sym
75
+ end
76
+
77
+ super(config_or_env)
78
+ end
79
+
80
+ def connected_to_stack
81
+ return super if ::Rails.version < '7.0' ? Thread.current.thread_variable?(:ar_connected_to_stack) : ::ActiveSupport::IsolatedExecutionState.key?(:active_record_connected_to_stack)
82
+
83
+ ret = super
84
+ DatabaseServer.guard_servers
85
+ ret
86
+ end
87
+
88
+ # significant change: Allow per-shard roles
89
+ def current_role(without_overrides: false, target_shard: current_shard)
90
+ return super() if without_overrides
91
+
92
+ sharded_role = nil
93
+ connected_to_stack.reverse_each do |hash|
94
+ shard_role = hash.dig(:shard_roles, target_shard)
95
+ if shard_role && (hash[:klasses].include?(::ActiveRecord::Base) || hash[:klasses].include?(connection_class_for_self))
96
+ sharded_role = shard_role
97
+ break
98
+ end
99
+ end
100
+ # Allow a shard-specific role to be reverted to regular inheritance
101
+ return sharded_role if sharded_role && sharded_role != :_switchman_inherit
102
+
103
+ super()
104
+ end
105
+
71
106
  # significant change: _don't_ check if klasses.include?(Base)
72
107
  # i.e. other sharded models don't inherit the current shard of Base
73
108
  def current_shard
74
109
  connected_to_stack.reverse_each do |hash|
75
- return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_classes)
110
+ return hash[:shard] if hash[:shard] && hash[:klasses].include?(connection_class_for_self)
76
111
  end
77
112
 
78
113
  default_shard
79
114
  end
115
+
116
+ def current_switchman_shard
117
+ connected_to_stack.reverse_each do |hash|
118
+ return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_class_for_self)
119
+ end
120
+
121
+ Shard.default
122
+ end
123
+
124
+ if ::Rails.version < '7.0'
125
+ def connection_class_for_self
126
+ connection_classes
127
+ end
128
+ end
80
129
  end
81
130
 
82
- def self.included(klass)
131
+ def self.prepended(klass)
83
132
  klass.singleton_class.prepend(ClassMethods)
84
- klass.set_callback(:initialize, :before) do
85
- @shard ||= if self.class.sharded_primary_key?
86
- Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_classes))
87
- else
88
- Shard.current(self.class.connection_classes)
89
- end
90
- end
133
+ end
134
+
135
+ def _run_initialize_callbacks
136
+ @shard ||= if self.class.sharded_primary_key?
137
+ Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_class_for_self))
138
+ else
139
+ Shard.current(self.class.connection_class_for_self)
140
+ end
141
+ super
91
142
  end
92
143
 
93
144
  def shard
94
- @shard || Shard.current(self.class.connection_classes) || Shard.default
145
+ @shard || Shard.current(self.class.connection_class_for_self) || Shard.default
95
146
  end
96
147
 
97
148
  def shard=(new_shard)
@@ -107,16 +158,16 @@ module Switchman
107
158
 
108
159
  def save(*, **)
109
160
  @shard_set_in_stone = true
110
- (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
161
+ super
111
162
  end
112
163
 
113
164
  def save!(*, **)
114
165
  @shard_set_in_stone = true
115
- (self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
166
+ super
116
167
  end
117
168
 
118
169
  def destroy
119
- shard.activate(self.class.connection_classes) { super }
170
+ shard.activate(self.class.connection_class_for_self) { super }
120
171
  end
121
172
 
122
173
  def clone
@@ -129,11 +180,18 @@ module Switchman
129
180
  end
130
181
 
131
182
  def transaction(**kwargs, &block)
132
- shard.activate(self.class.connection_classes) do
183
+ shard.activate(self.class.connection_class_for_self) do
133
184
  self.class.transaction(**kwargs, &block)
134
185
  end
135
186
  end
136
187
 
188
+ def with_transaction_returning_status
189
+ shard.activate(self.class.connection_class_for_self) do
190
+ db = Shard.current(self.class.connection_class_for_self).database_server
191
+ db.unguard { super }
192
+ end
193
+ end
194
+
137
195
  def hash
138
196
  self.class.sharded_primary_key? ? self.class.hash ^ global_id.hash : super
139
197
  end
@@ -149,40 +207,39 @@ module Switchman
149
207
  copy
150
208
  end
151
209
 
152
- def quoted_id
153
- return super unless self.class.sharded_primary_key?
154
-
155
- # do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
156
- self.class.connection.quote(id)
210
+ def update_columns(*)
211
+ db = shard.database_server
212
+ db.unguard { super }
157
213
  end
158
214
 
159
- def update_columns(*)
160
- db = Shard.current(self.class.connection_classes).database_server
161
- if ::GuardRail.environment == db.guard_rail_environment
162
- super
215
+ def id_for_database
216
+ if self.class.sharded_primary_key?
217
+ # It's an int, so so it's safe to just return it without passing it through anything else
218
+ # In theory we should do `@attributes[@primary_key].type.serialize(id)`, but that seems to have surprising side-effects
219
+ id
163
220
  else
164
- db.unguard { super }
221
+ super
165
222
  end
166
223
  end
167
224
 
168
225
  protected
169
226
 
170
- # see also AttributeMethods#connection_classes_code_for_reflection
171
- def connection_classes_for_reflection(reflection)
227
+ # see also AttributeMethods#connection_class_for_self_code_for_reflection
228
+ def connection_class_for_self_for_reflection(reflection)
172
229
  if reflection
173
230
  if reflection.options[:polymorphic]
174
231
  begin
175
- read_attribute(reflection.foreign_type)&.constantize&.connection_classes
232
+ read_attribute(reflection.foreign_type)&.constantize&.connection_class_for_self || ::ActiveRecord::Base
176
233
  rescue NameError
177
234
  # in case someone is abusing foreign_type to not point to an actual class
178
235
  ::ActiveRecord::Base
179
236
  end
180
237
  else
181
238
  # otherwise we can just return a symbol for the statically known type of the association
182
- reflection.klass.connection_classes
239
+ reflection.klass.connection_class_for_self
183
240
  end
184
241
  else
185
- connection_classes
242
+ self.class.connection_class_for_self
186
243
  end
187
244
  end
188
245
  end
@@ -4,7 +4,7 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module Calculations
6
6
  def pluck(*column_names)
7
- target_shard = Shard.current(klass.connection_classes)
7
+ target_shard = Shard.current(klass.connection_class_for_self)
8
8
  shard_count = 0
9
9
  result = activate do |relation, shard|
10
10
  shard_count += 1
@@ -83,7 +83,7 @@ module Switchman
83
83
  if opts[:association]
84
84
  key_ids = calculated_data.collect { |row| row[opts[:group_aliases].first] }
85
85
  key_records = opts[:association].klass.base_class.where(id: key_ids)
86
- key_records = key_records.map { |r| [Shard.relative_id_for(r, shard, target_shard), r] }.to_h
86
+ key_records = key_records.to_h { |r| [Shard.relative_id_for(r, shard, target_shard), r] }
87
87
  end
88
88
 
89
89
  calculated_data.map do |row|
@@ -109,11 +109,18 @@ module Switchman
109
109
  private
110
110
 
111
111
  def type_cast_calculated_value_switchman(value, column_name, operation)
112
- type_cast_calculated_value(value, operation) do |val|
112
+ if ::Rails.version < '7.0'
113
+ type_cast_calculated_value(value, operation) do |val|
114
+ column = aggregate_column(column_name)
115
+ type ||= column.try(:type_caster) ||
116
+ lookup_cast_type_from_join_dependencies(column_name.to_s) || ::ActiveRecord::Type.default_value
117
+ type.deserialize(val)
118
+ end
119
+ else
113
120
  column = aggregate_column(column_name)
114
121
  type ||= column.try(:type_caster) ||
115
- lookup_cast_type_from_join_dependencies(column_name.to_s) || Type.default_value
116
- type.deserialize(val)
122
+ lookup_cast_type_from_join_dependencies(column_name.to_s) || ::ActiveRecord::Type.default_value
123
+ type_cast_calculated_value(value, operation, type)
117
124
  end
118
125
  end
119
126
 
@@ -1,22 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'switchman/errors'
4
-
5
3
  module Switchman
6
4
  module ActiveRecord
7
5
  module ConnectionPool
8
- def shard
9
- shard_stack.last || Shard.default
10
- end
11
-
12
- def shard_stack
13
- unless (shard_stack = Thread.current.thread_variable_get(tls_key))
14
- shard_stack = Concurrent::Array.new
15
- Thread.current.thread_variable_set(tls_key, shard_stack)
16
- end
17
- shard_stack
18
- end
19
-
20
6
  def default_schema
21
7
  connection unless @schemas
22
8
  # default shard will not switch databases immediately, so it won't be set yet
@@ -26,15 +12,15 @@ module Switchman
26
12
 
27
13
  def checkout_new_connection
28
14
  conn = super
29
- conn.shard = shard
15
+ conn.shard = current_shard
30
16
  conn
31
17
  end
32
18
 
33
19
  def connection(switch_shard: true)
34
20
  conn = super()
35
- raise NonExistentShardError if shard.new_record?
21
+ raise Errors::NonExistentShardError if current_shard.new_record?
36
22
 
37
- switch_database(conn) if conn.shard != shard && switch_shard
23
+ switch_database(conn) if conn.shard != current_shard && switch_shard
38
24
  conn
39
25
  end
40
26
 
@@ -44,26 +30,18 @@ module Switchman
44
30
  flush
45
31
  end
46
32
 
47
- def remove_shard!(shard)
48
- synchronize do
49
- # The shard might be currently active, so we need to update our own shard
50
- self.shard = Shard.default if self.shard == shard
51
- # Update out any connections that may be using this shard
52
- @connections.each do |conn|
53
- # This will also update the connection's shard to the default shard
54
- switch_database(conn) if conn.shard == shard
55
- end
56
- end
57
- end
58
-
59
33
  def switch_database(conn)
60
- @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !shard.database_server.config[:shard_name]
34
+ @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !current_shard.database_server.config[:shard_name]
61
35
 
62
- conn.shard = shard
36
+ conn.shard = current_shard
63
37
  end
64
38
 
65
39
  private
66
40
 
41
+ def current_shard
42
+ ::Rails.version < '7.0' ? connection_klass.current_switchman_shard : connection_class.current_switchman_shard
43
+ end
44
+
67
45
  def tls_key
68
46
  "#{object_id}_shard".to_sym
69
47
  end
@@ -3,6 +3,20 @@
3
3
  module Switchman
4
4
  module ActiveRecord
5
5
  module DatabaseConfigurations
6
+ # key difference: For each env name, ensure only one writable config is returned
7
+ # since all should point to the same data, even if multiple are writable
8
+ # (Picks 'primary' since it is guaranteed to exist and switchman handles activating
9
+ # deploy through other means)
10
+ def configs_for(include_replicas: false, name: nil, **)
11
+ res = super
12
+ if name && !include_replicas
13
+ return nil unless name.end_with?('primary')
14
+ elsif !include_replicas
15
+ return res.select { |config| config.name.end_with?('primary') }
16
+ end
17
+ res
18
+ end
19
+
6
20
  private
7
21
 
8
22
  # key difference: assumes a hybrid two-tier structure; each third tier
@@ -13,7 +27,9 @@ module Switchman
13
27
  return configs if configs.is_a?(Array)
14
28
 
15
29
  db_configs = configs.flat_map do |env_name, config|
16
- roles = config.keys.select { |k| config[k].is_a?(Hash) }
30
+ # It would be nice to do the auto-fallback that we want here, but we haven't
31
+ # actually done that for years (or maybe ever) and it will be a big lift to get working
32
+ roles = config.keys.select { |k| config[k].is_a?(Hash) || (config[k].is_a?(Array) && config[k].all? { |ck| ck.is_a?(Hash) }) }
17
33
  base_config = config.except(*roles)
18
34
 
19
35
  name = "#{env_name}/primary"
@@ -21,7 +37,7 @@ module Switchman
21
37
  base_db = build_db_config_from_raw_config(env_name, name, base_config)
22
38
  [base_db] + roles.map do |role|
23
39
  build_db_config_from_raw_config(env_name, "#{env_name}/#{role}",
24
- base_config.merge(config[role]).merge(replica: true))
40
+ base_config.merge(config[role].is_a?(Array) ? config[role].first : config[role]))
25
41
  end
26
42
  end
27
43
 
@@ -7,7 +7,7 @@ module Switchman
7
7
  return super(id) unless klass.integral_id?
8
8
 
9
9
  if shard_source_value != :implicit
10
- current_shard = Shard.current(klass.connection_classes)
10
+ current_shard = Shard.current(klass.connection_class_for_self)
11
11
  result = activate do |relation, shard|
12
12
  current_id = Shard.relative_id_for(id, current_shard, shard)
13
13
  # current_id will be nil for non-integral id
@@ -33,7 +33,7 @@ module Switchman
33
33
  end
34
34
 
35
35
  def find_some_ordered(ids)
36
- current_shard = Shard.current(klass.connection_classes)
36
+ current_shard = Shard.current(klass.connection_class_for_self)
37
37
  ids = ids.map { |id| Shard.relative_id_for(id, current_shard, current_shard) }
38
38
  super(ids)
39
39
  end
@@ -14,16 +14,19 @@ 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.connection_pool.shard
17
+ ::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base.current_switchman_shard
18
18
  conn
19
19
  end
20
20
  end
21
21
 
22
22
  module Migrator
23
- # significant change: hash shard name, not database name
23
+ # significant change: just return MIGRATOR_SALT directly
24
+ # especially if you're going through pgbouncer, the database
25
+ # name you're accessing may not be consistent. it is NOT allowed
26
+ # to run migrations against multiple shards in the same database
27
+ # concurrently
24
28
  def generate_migrator_advisory_lock_id
25
- shard_name_hash = Zlib.crc32(Shard.current.name)
26
- ::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
29
+ ::ActiveRecord::Migrator::MIGRATOR_SALT
27
30
  end
28
31
 
29
32
  # significant change: strip out prefer_secondary from config
@@ -6,7 +6,7 @@ module Switchman
6
6
  module ClassMethods
7
7
  def quoted_table_name
8
8
  @quoted_table_name ||= {}
9
- @quoted_table_name[Shard.current(connection_classes).id] ||= connection.quote_table_name(table_name)
9
+ @quoted_table_name[Shard.current(connection_class_for_self).id] ||= connection.quote_table_name(table_name)
10
10
  end
11
11
  end
12
12
  end
@@ -5,11 +5,16 @@ 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
- shard.activate(self.class.connection_classes) { super }
8
+ shard.activate(self.class.connection_class_for_self) { super }
9
9
  end
10
10
 
11
11
  def update_columns(*)
12
- shard.activate(self.class.connection_classes) { super }
12
+ shard.activate(self.class.connection_class_for_self) { super }
13
+ end
14
+
15
+ def delete
16
+ db = shard.database_server
17
+ db.unguard { super }
13
18
  end
14
19
  end
15
20
  end
@@ -7,7 +7,7 @@ module Switchman
7
7
  def create_database(name, options = {})
8
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}\""
@@ -32,7 +32,7 @@ module Switchman
32
32
  end
33
33
 
34
34
  # copy/paste; use quote_local_table_name
35
- def drop_database(name) #:nodoc:
35
+ def drop_database(name) # :nodoc:
36
36
  execute "DROP DATABASE IF EXISTS #{quote_local_table_name(name)}"
37
37
  end
38
38
 
@@ -87,6 +87,10 @@ module Switchman
87
87
  name.quoted
88
88
  end
89
89
 
90
+ def with_global_table_name(&block)
91
+ with_local_table_name(false, &block)
92
+ end
93
+
90
94
  def with_local_table_name(enable = true) # rubocop:disable Style/OptionalBooleanParameter
91
95
  old_value = @use_local_table_name
92
96
  @use_local_table_name = enable
@@ -15,7 +15,7 @@ module Switchman
15
15
  def convert_to_id(value)
16
16
  case value
17
17
  when ::ActiveRecord::Base
18
- value.send(primary_key)
18
+ value.id
19
19
  else
20
20
  super
21
21
  end
@@ -65,7 +65,7 @@ module Switchman
65
65
  when ::ActiveRecord::Relation
66
66
  Shard.default
67
67
  when nil
68
- Shard.current(klass.connection_classes)
68
+ Shard.current(klass.connection_class_for_self)
69
69
  else
70
70
  raise ArgumentError, "invalid shard value #{shard_value}"
71
71
  end
@@ -79,7 +79,7 @@ module Switchman
79
79
  when ::ActiveRecord::Base
80
80
  shard_value.respond_to?(:associated_shards) ? shard_value.associated_shards : [shard_value.shard]
81
81
  when nil
82
- [Shard.current(klass.connection_classes)]
82
+ [Shard.current(klass.connection_class_for_self)]
83
83
  else
84
84
  shard_value
85
85
  end
@@ -131,7 +131,7 @@ module Switchman
131
131
  id_shards = Set.new
132
132
  right.each do |value|
133
133
  local_id, id_shard = Shard.local_id_for(value)
134
- id_shard ||= Shard.current(klass.connection_classes) if local_id
134
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
135
135
  id_shards << id_shard if id_shard
136
136
  end
137
137
  return if id_shards.empty?
@@ -151,10 +151,13 @@ module Switchman
151
151
  end
152
152
  when ::Arel::Nodes::BindParam
153
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
154
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
155
+ when ::ActiveModel::Attribute
156
+ local_id, id_shard = Shard.local_id_for(right.value_before_type_cast)
157
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
155
158
  else
156
159
  local_id, id_shard = Shard.local_id_for(right)
157
- id_shard ||= Shard.current(klass.connection_classes) if local_id
160
+ id_shard ||= Shard.current(klass.connection_class_for_self) if local_id
158
161
  end
159
162
 
160
163
  return if !id_shard || id_shard == primary_shard
@@ -193,9 +196,9 @@ module Switchman
193
196
  reflection = model.send(:reflection_for_integer_attribute, column)
194
197
  break if reflection
195
198
  end
196
- return Shard.current(klass.connection_classes) if reflection.options[:polymorphic]
199
+ return Shard.current(klass.connection_class_for_self) if reflection.options[:polymorphic]
197
200
 
198
- Shard.current(reflection.klass.connection_classes)
201
+ Shard.current(reflection.klass.connection_class_for_self)
199
202
  end
200
203
 
201
204
  def relation_and_column(attribute)
@@ -239,6 +242,10 @@ module Switchman
239
242
  connection.with_local_table_name { super }
240
243
  end
241
244
 
245
+ def table_name_matches?(from)
246
+ connection.with_global_table_name { super }
247
+ end
248
+
242
249
  def transpose_predicates(predicates,
243
250
  source_shard,
244
251
  target_shard,
@@ -266,11 +273,11 @@ module Switchman
266
273
  right_node = or_expr.right
267
274
  new_left_predicates = transpose_single_predicate(left_node, source_shard,
268
275
  target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
269
- or_expr.instance_variable_set(:@left, new_left_predicates) if new_left_predicates != left_node
270
276
  new_right_predicates = transpose_single_predicate(right_node, source_shard,
271
277
  target_shard, remove_nonlocal_primary_keys: remove_nonlocal_primary_keys)
272
- or_expr.instance_variable_set(:@right, new_right_predicates) if new_right_predicates != right_node
273
- return predicate
278
+ return predicate if new_left_predicates == left_node && new_right_predicates == right_node
279
+
280
+ return ::Arel::Nodes::Grouping.new ::Arel::Nodes::Or.new(new_left_predicates, new_right_predicates)
274
281
  end
275
282
  return predicate unless predicate.is_a?(::Arel::Nodes::Binary) || predicate.is_a?(::Arel::Nodes::HomogeneousIn)
276
283
  return predicate unless predicate.left.is_a?(::Arel::Attributes::Attribute)
@@ -287,7 +294,7 @@ module Switchman
287
294
  if source_shard
288
295
  source_shard
289
296
  elsif type == :primary
290
- Shard.current(klass.connection_classes)
297
+ Shard.current(klass.connection_class_for_self)
291
298
  elsif type == :foreign
292
299
  source_shard_for_foreign_key(relation, column)
293
300
  end
@@ -328,8 +335,9 @@ module Switchman
328
335
  end
329
336
 
330
337
  def transpose_predicate_value(value, current_shard, target_shard, attribute_type, remove_non_local_ids)
331
- if value.is_a?(::Arel::Nodes::BindParam)
332
- query_att = value.value
338
+ case value
339
+ when ::Arel::Nodes::BindParam, ::ActiveModel::Attribute
340
+ query_att = value.is_a?(::ActiveModel::Attribute) ? value : value.value
333
341
  current_id = query_att.value_before_type_cast
334
342
  if current_id.is_a?(::ActiveRecord::StatementCache::Substitute)
335
343
  current_id.sharded = true # mark for transposition later
@@ -342,7 +350,12 @@ module Switchman
342
350
  # make a new bind param
343
351
  value
344
352
  else
345
- ::Arel::Nodes::BindParam.new(query_att.class.new(query_att.name, local_id, query_att.type))
353
+ new_att = query_att.class.new(query_att.name, local_id, query_att.type)
354
+ if value.is_a?(::ActiveModel::Attribute)
355
+ new_att
356
+ else
357
+ ::Arel::Nodes::BindParam.new(new_att)
358
+ end
346
359
  end
347
360
  end
348
361
  else
@@ -5,7 +5,7 @@ module Switchman
5
5
  module Reflection
6
6
  module AbstractReflection
7
7
  def shard(owner)
8
- if polymorphic? || klass.connection_classes == owner.class.connection_classes
8
+ if polymorphic? || klass.connection_class_for_self == owner.class.connection_class_for_self
9
9
  # polymorphic associations assume the same shard as the owning item
10
10
  owner.shard
11
11
  else