switchman 3.0.6 → 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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/lib/switchman/action_controller/caching.rb +2 -2
  4. data/lib/switchman/active_record/abstract_adapter.rb +2 -4
  5. data/lib/switchman/active_record/associations.rb +223 -0
  6. data/lib/switchman/active_record/attribute_methods.rb +144 -84
  7. data/lib/switchman/active_record/base.rb +71 -31
  8. data/lib/switchman/active_record/calculations.rb +12 -5
  9. data/lib/switchman/active_record/connection_pool.rb +2 -4
  10. data/lib/switchman/active_record/database_configurations.rb +18 -2
  11. data/lib/switchman/active_record/finder_methods.rb +2 -2
  12. data/lib/switchman/active_record/model_schema.rb +1 -1
  13. data/lib/switchman/active_record/persistence.rb +3 -5
  14. data/lib/switchman/active_record/postgresql_adapter.rb +1 -1
  15. data/lib/switchman/active_record/query_methods.rb +23 -14
  16. data/lib/switchman/active_record/reflection.rb +1 -1
  17. data/lib/switchman/active_record/relation.rb +15 -18
  18. data/lib/switchman/active_record/statement_cache.rb +2 -2
  19. data/lib/switchman/active_record/table_definition.rb +1 -1
  20. data/lib/switchman/active_support/cache.rb +16 -0
  21. data/lib/switchman/arel.rb +28 -6
  22. data/lib/switchman/database_server.rb +28 -20
  23. data/lib/switchman/default_shard.rb +0 -2
  24. data/lib/switchman/engine.rb +63 -125
  25. data/lib/switchman/errors.rb +4 -2
  26. data/lib/switchman/guard_rail/relation.rb +6 -9
  27. data/lib/switchman/guard_rail.rb +5 -0
  28. data/lib/switchman/parallel.rb +68 -0
  29. data/lib/switchman/r_spec_helper.rb +8 -0
  30. data/lib/switchman/rails.rb +1 -4
  31. data/{app/models → lib}/switchman/shard.rb +30 -131
  32. data/lib/switchman/sharded_instrumenter.rb +1 -1
  33. data/lib/switchman/standard_error.rb +10 -11
  34. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  35. data/lib/switchman/version.rb +1 -1
  36. data/lib/switchman.rb +22 -2
  37. data/lib/tasks/switchman.rake +16 -9
  38. metadata +19 -18
  39. data/lib/switchman/active_record/association.rb +0 -206
  40. data/lib/switchman/open4.rb +0 -80
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81aeec21e056026642c6aa15bda349e97c8194212f8b79222b4581d5cb2a9909
4
- data.tar.gz: 77bfd14eaf245a399ec6a5b48a9f3106daac6df393d8c55d7dad5eae56966f15
3
+ metadata.gz: 48b27124d5233adf585aed105d0b2522c76249bdd1e13fbcfd7e904241d3a624
4
+ data.tar.gz: fea1ccc0ad63f00633fb90bee1d7bd4606f6de0e18564250865c1b1dde61ef8d
5
5
  SHA512:
6
- metadata.gz: bfc71a265b6aea2a48350bcce77dcdcb24cea206c2dbcd4b4e3e113f0615917b314233da2038197d394d2ae961dbc8be18f12d48ccc240d57ca96eb39d4db9e6
7
- data.tar.gz: d392e78986ef050800ad61e064c702d3cb3dc98b97e48cbf42c9a973ae52226672a33ade5944119be7365f9d7f2f17e05097e0e1f2a46b1adab27dfe5169bf87
6
+ metadata.gz: c05b445111dd0e4b16ecbfaea6ab84557fa2cbadc6102fa0af6fb43cff2f6bbb6606e825f76ed20f6b8a4a2a2966e31babadf66c5eb065a6c4d8a7eea6a8b92a
7
+ data.tar.gz: 0af9e15706142f90772d24e2187db93ec83331997bccbb25c1b89cec2dde1bd2414d2774fec3785c7bea1a1b06d3fa5ad49e15b716f0aa35794e2e5734c848a8
data/Rakefile CHANGED
@@ -35,4 +35,4 @@ RuboCop::RakeTask.new do |task|
35
35
  task.options = ['-S']
36
36
  end
37
37
 
38
- task default: %i[spec rubocop]
38
+ task default: %i[spec]
@@ -13,8 +13,8 @@ module Switchman
13
13
  # disallow assigning to ActionController::Base.cache_store or
14
14
  # ActionController::Base#cache_store for the same reasons we disallow
15
15
  # assigning to Rails.cache
16
- def cache_store=(_cache)
17
- raise NoMethodError
16
+ def cache_store=(cache)
17
+ raise NoMethodError unless cache == ::Rails.cache
18
18
  end
19
19
  end
20
20
 
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'switchman/sharded_instrumenter'
4
-
5
3
  module Switchman
6
4
  module ActiveRecord
7
5
  module AbstractAdapter
8
6
  module ForeignKeyCheck
9
7
  def add_column(table, name, type, limit: nil, **)
10
- Engine.foreign_key_check(name, type, limit: limit)
8
+ Switchman.foreign_key_check(name, type, limit: limit)
11
9
  super
12
10
  end
13
11
  end
@@ -35,7 +33,7 @@ module Switchman
35
33
 
36
34
  protected
37
35
 
38
- def log(*args, &block)
36
+ def log(...)
39
37
  super
40
38
  ensure
41
39
  @last_query_at = Time.now
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module Associations
6
+ module Association
7
+ def shard
8
+ reflection.shard(owner)
9
+ end
10
+
11
+ def build_record(*args)
12
+ shard.activate { super }
13
+ end
14
+
15
+ def load_target
16
+ shard.activate { super }
17
+ end
18
+
19
+ def scope
20
+ shard_value = @reflection.options[:multishard] ? @owner : shard
21
+ @owner.shard.activate { super.shard(shard_value, :association) }
22
+ end
23
+ end
24
+
25
+ module CollectionAssociation
26
+ def find_target
27
+ shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [shard]
28
+ # activate both the owner and the target's shard category, so that Reflection#join_id_for,
29
+ # when called for the owner, will be returned relative to shard the query will execute on
30
+ Shard.with_each_shard(shards, [klass.connection_class_for_self, owner.class.connection_class_for_self].uniq) do
31
+ super
32
+ end
33
+ end
34
+
35
+ def _create_record(*)
36
+ shard.activate { super }
37
+ end
38
+ end
39
+
40
+ module BelongsToAssociation
41
+ def replace_keys(record, force: false)
42
+ if record&.class&.sharded_column?(reflection.association_primary_key(record.class))
43
+ foreign_id = record[reflection.association_primary_key(record.class)]
44
+ owner[reflection.foreign_key] = Shard.relative_id_for(foreign_id, record.shard, owner.shard)
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def shard
51
+ if @owner.class.sharded_column?(@reflection.foreign_key) &&
52
+ (foreign_id = @owner[@reflection.foreign_key])
53
+ Shard.shard_for(foreign_id, @owner.shard)
54
+ else
55
+ super
56
+ end
57
+ end
58
+ end
59
+
60
+ module ForeignAssociation
61
+ # significant change:
62
+ # * transpose the key to the correct shard
63
+ def set_owner_attributes(record) # rubocop:disable Naming/AccessorMethodName
64
+ return if options[:through]
65
+
66
+ key = owner._read_attribute(reflection.join_foreign_key)
67
+ key = Shard.relative_id_for(key, owner.shard, shard)
68
+ record._write_attribute(reflection.join_primary_key, key)
69
+
70
+ record._write_attribute(reflection.type, owner.class.polymorphic_name) if reflection.type
71
+ end
72
+ end
73
+
74
+ module Extension
75
+ def self.build(_model, _reflection); end
76
+
77
+ def self.valid_options
78
+ [:multishard]
79
+ end
80
+ end
81
+
82
+ ::ActiveRecord::Associations::Builder::Association.extensions << Extension
83
+
84
+ module Preloader
85
+ module Association
86
+ module LoaderQuery
87
+ def load_records_in_batch(loaders)
88
+ # While in theory loading multiple associations that end up being effectively the same would be nice
89
+ # it's not very switchman compatible, so just don't bother trying to use that logic
90
+ # raw_records = records_for(loaders)
91
+
92
+ loaders.each do |loader|
93
+ loader.load_records(nil)
94
+ loader.run
95
+ end
96
+ end
97
+ end
98
+
99
+ # Copypasta from Activerecord but with added global_id_for goodness.
100
+ def records_for(ids)
101
+ scope.where(association_key_name => ids).load do |record|
102
+ global_key = if model.connection_class_for_self == UnshardedRecord
103
+ convert_key(record[association_key_name])
104
+ else
105
+ Shard.global_id_for(record[association_key_name], record.shard)
106
+ end
107
+ owner = owners_by_key[convert_key(global_key)].first
108
+ association = owner.association(reflection.name)
109
+ association.set_inverse_instance(record)
110
+ end
111
+ end
112
+
113
+ # significant changes:
114
+ # * partition_by_shard the records_for call
115
+ # * re-globalize the fetched owner id before looking up in the map
116
+ # TODO: the ignored param currently loads records; we should probably not waste effort double-loading them
117
+ # Change introduced here: https://github.com/rails/rails/commit/c6c0b2e8af64509b699b782aadfecaa430700ece
118
+ def load_records(raw_records = nil)
119
+ # owners can be duplicated when a relation has a collection association join
120
+ # #compare_by_identity makes such owners different hash keys
121
+ @records_by_owner = {}.compare_by_identity
122
+
123
+ if ::Rails.version < '7.0' && owner_keys.empty?
124
+ raw_records ||= []
125
+ else
126
+ # determine the shard to search for each owner
127
+ if reflection.macro == :belongs_to
128
+ # for belongs_to, it's the shard of the foreign_key
129
+ partition_proc = lambda do |owner|
130
+ if owner.class.sharded_column?(owner_key_name)
131
+ Shard.shard_for(owner[owner_key_name], owner.shard)
132
+ else
133
+ Shard.current
134
+ end
135
+ end
136
+ elsif !reflection.options[:multishard]
137
+ # for non-multishard associations, it's *just* the owner's shard
138
+ partition_proc = ->(owner) { owner.shard }
139
+ end
140
+
141
+ raw_records ||= Shard.partition_by_shard(owners, partition_proc) do |partitioned_owners|
142
+ relative_owner_keys = partitioned_owners.map do |owner|
143
+ key = owner[owner_key_name]
144
+ if key && owner.class.sharded_column?(owner_key_name)
145
+ key = Shard.relative_id_for(key, owner.shard,
146
+ Shard.current(klass.connection_class_for_self))
147
+ end
148
+ convert_key(key)
149
+ end
150
+ relative_owner_keys.compact!
151
+ relative_owner_keys.uniq!
152
+ records_for(relative_owner_keys)
153
+ end
154
+ end
155
+
156
+ @preloaded_records = raw_records.select do |record|
157
+ assignments = false
158
+
159
+ owner_key = record[association_key_name]
160
+ if owner_key && record.class.sharded_column?(association_key_name)
161
+ owner_key = Shard.global_id_for(owner_key,
162
+ record.shard)
163
+ end
164
+
165
+ owners_by_key[convert_key(owner_key)].each do |owner|
166
+ entries = (@records_by_owner[owner] ||= [])
167
+
168
+ if reflection.collection? || entries.empty?
169
+ entries << record
170
+ assignments = true
171
+ end
172
+ end
173
+
174
+ assignments
175
+ end
176
+ end
177
+
178
+ # significant change: globalize keys on sharded columns
179
+ def owners_by_key
180
+ @owners_by_key ||= owners.each_with_object({}) do |owner, result|
181
+ key = owner[owner_key_name]
182
+ key = Shard.global_id_for(key, owner.shard) if key && owner.class.sharded_column?(owner_key_name)
183
+ key = convert_key(key)
184
+ (result[key] ||= []) << owner if key
185
+ end
186
+ end
187
+
188
+ # significant change: don't cache scope (since it could be for different shards)
189
+ def scope
190
+ build_scope
191
+ end
192
+ end
193
+ end
194
+
195
+ module CollectionProxy
196
+ def initialize(*args)
197
+ super
198
+ self.shard_value = scope.shard_value
199
+ self.shard_source_value = :association
200
+ end
201
+
202
+ def shard(*args)
203
+ scope.shard(*args)
204
+ end
205
+ end
206
+
207
+ module AutosaveAssociation
208
+ def record_changed?(reflection, record, key)
209
+ record.new_record? ||
210
+ (record.has_attribute?(reflection.foreign_key) && record.send(reflection.foreign_key) != key) || # have to use send instead of [] because sharding
211
+ record.attribute_changed?(reflection.foreign_key)
212
+ end
213
+
214
+ def save_belongs_to_association(reflection)
215
+ # this seems counter-intuitive, but the autosave code will assign to attribute bypassing switchman,
216
+ # after reading the id attribute _without_ bypassing switchman. So we need Shard.current for the
217
+ # category of the associated record to match Shard.current for the category of self
218
+ shard.activate(connection_class_for_self_for_reflection(reflection)) { super }
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -25,6 +25,16 @@ module Switchman
25
25
  @sharded_column_values[column_name]
26
26
  end
27
27
 
28
+ def define_attribute_methods
29
+ super
30
+ # ensure that we're using the sharded attribute method
31
+ # and not the silly one in AR::AttributeMethods::PrimaryKey
32
+ return unless sharded_column?(@primary_key)
33
+
34
+ class_eval(build_sharded_getter('id', '_read_attribute(@primary_key)', "::#{connection_class_for_self.name}"), __FILE__, __LINE__)
35
+ class_eval(build_sharded_setter('id', @primary_key, "::#{connection_class_for_self.name}"), __FILE__, __LINE__)
36
+ end
37
+
28
38
  protected
29
39
 
30
40
  def reflection_for_integer_attribute(attr_name)
@@ -36,17 +46,30 @@ module Switchman
36
46
  raise if connection.open_transactions.positive?
37
47
  end
38
48
 
49
+ # rubocop:disable Naming/MethodParameterName
50
+ def define_cached_method(owner, name, namespace:, as:, &block)
51
+ if ::Rails.version < '7.0'
52
+ yield owner
53
+ owner.rename_method(as, name)
54
+ else
55
+ owner.define_cached_method(name, namespace: namespace, as: as, &block)
56
+ end
57
+ end
58
+ # rubocop:enable Naming/MethodParameterName
59
+
39
60
  def define_method_global_attribute(attr_name, owner:)
40
61
  if sharded_column?(attr_name)
41
- owner << <<-RUBY
42
- def global_#{attr_name}
43
- raw_value = original_#{attr_name}
44
- return nil if raw_value.nil?
45
- return raw_value if raw_value > ::Switchman::Shard::IDS_PER_SHARD
46
-
47
- ::Switchman::Shard.global_id_for(raw_value, shard)
48
- end
49
- RUBY
62
+ define_cached_method(owner, "global_#{attr_name}", as: "sharded_global_#{attr_name}", namespace: :switchman) do |batch|
63
+ batch << <<-RUBY
64
+ def sharded_global_#{attr_name}
65
+ raw_value = original_#{attr_name}
66
+ return nil if raw_value.nil?
67
+ return raw_value if raw_value > ::Switchman::Shard::IDS_PER_SHARD
68
+
69
+ ::Switchman::Shard.global_id_for(raw_value, shard)
70
+ end
71
+ RUBY
72
+ end
50
73
  else
51
74
  define_method_unsharded_column(attr_name, 'global', owner)
52
75
  end
@@ -54,113 +77,150 @@ module Switchman
54
77
 
55
78
  def define_method_local_attribute(attr_name, owner:)
56
79
  if sharded_column?(attr_name)
57
- owner << <<-RUBY
58
- def local_#{attr_name}
59
- raw_value = original_#{attr_name}
60
- return nil if raw_value.nil?
61
- return raw_value % ::Switchman::Shard::IDS_PER_SHARD
62
- end
63
- RUBY
80
+ define_cached_method(owner, "local_#{attr_name}", as: "sharded_local_#{attr_name}", namespace: :switchman) do |batch|
81
+ batch << <<-RUBY
82
+ def sharded_local_#{attr_name}
83
+ raw_value = original_#{attr_name}
84
+ return nil if raw_value.nil?
85
+ return raw_value % ::Switchman::Shard::IDS_PER_SHARD
86
+ end
87
+ RUBY
88
+ end
64
89
  else
65
90
  define_method_unsharded_column(attr_name, 'local', owner)
66
91
  end
67
92
  end
68
93
 
69
- # see also Base#connection_classes_for_reflection
94
+ # see also Base#connection_class_for_self_for_reflection
70
95
  # the difference being this will output static strings for the common cases, making them
71
96
  # more performant
72
- def connection_classes_code_for_reflection(reflection)
97
+ def connection_class_for_self_code_for_reflection(reflection)
73
98
  if reflection
74
99
  if reflection.options[:polymorphic]
75
100
  # a polymorphic association has to be discovered at runtime. This code ends up being something like
76
- # context_type.&.constantize&.connection_classes
77
- "read_attribute(:#{reflection.foreign_type})&.constantize&.connection_classes"
101
+ # context_type.&.constantize&.connection_class_for_self
102
+ "read_attribute(:#{reflection.foreign_type})&.constantize&.connection_class_for_self"
78
103
  else
79
104
  # otherwise we can just return a symbol for the statically known type of the association
80
- "::#{reflection.klass.connection_classes.name}"
105
+ "::#{reflection.klass.connection_class_for_self.name}"
81
106
  end
82
107
  else
83
- "::#{connection_classes.name}"
108
+ "::#{connection_class_for_self.name}"
84
109
  end
85
110
  end
86
111
 
87
- # just a dummy class with the proper interface that calls module_eval immediately
88
- class CodeGenerator
89
- def initialize(mod, line)
90
- @module = mod
91
- @line = line
112
+ def define_method_attribute(attr_name, owner:)
113
+ if sharded_column?(attr_name)
114
+ reflection = reflection_for_integer_attribute(attr_name)
115
+ class_name = connection_class_for_self_code_for_reflection(reflection)
116
+ safe_class_name = class_name.unpack1('h*')
117
+ define_cached_method(owner, attr_name, as: "sharded_#{safe_class_name}_#{attr_name}", namespace: :switchman) do |batch|
118
+ batch << build_sharded_getter("sharded_#{safe_class_name}_#{attr_name}", "original_#{attr_name}", class_name)
119
+ end
120
+ else
121
+ define_cached_method(owner, attr_name, as: "plain_#{attr_name}", namespace: :switchman) do |batch|
122
+ batch << <<-RUBY
123
+ def plain_#{attr_name}
124
+ _read_attribute("#{attr_name}") { |n| missing_attribute(n, caller) }
125
+ end
126
+ RUBY
127
+ end
92
128
  end
129
+ end
93
130
 
94
- def <<(string)
95
- @module.module_eval(string, __FILE__, @line)
96
- end
131
+ def build_sharded_getter(attr_name, raw_expr, attr_connection_class)
132
+ <<-RUBY
133
+ def #{attr_name}
134
+ raw_value = #{raw_expr}
135
+ return nil if raw_value.nil?
136
+
137
+ abs_raw_value = raw_value.abs
138
+ current_shard = ::Switchman::Shard.current(#{attr_connection_class})
139
+ same_shard = shard == current_shard
140
+ return raw_value if same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
141
+
142
+ value_shard_id = abs_raw_value / ::Switchman::Shard::IDS_PER_SHARD
143
+ # this is a stupid case when someone stuffed a global id for the current shard in instead
144
+ # of a local id
145
+ return raw_value % ::Switchman::Shard::IDS_PER_SHARD if value_shard_id == current_shard.id
146
+ return raw_value if !same_shard && abs_raw_value > ::Switchman::Shard::IDS_PER_SHARD
147
+ return shard.global_id_for(raw_value) if !same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
148
+
149
+ ::Switchman::Shard.relative_id_for(raw_value, shard, current_shard)
150
+ end
151
+ RUBY
97
152
  end
98
153
 
99
- def define_method_original_attribute(attr_name, owner:)
154
+ def define_method_attribute=(attr_name, owner:)
100
155
  if sharded_column?(attr_name)
101
156
  reflection = reflection_for_integer_attribute(attr_name)
102
- if attr_name == 'id'
103
- return if method_defined?(:original_id)
157
+ class_name = connection_class_for_self_code_for_reflection(reflection)
158
+ safe_class_name = class_name.unpack1('h*')
159
+ define_cached_method(owner, "#{attr_name}=", as: "sharded_#{safe_class_name}_#{attr_name}=", namespace: :switchman) do |batch|
160
+ batch << build_sharded_setter("sharded_#{safe_class_name}_#{attr_name}", attr_name, class_name)
161
+ end
162
+ else
163
+ define_cached_method(owner, "#{attr_name}=", as: "plain_#{attr_name}=", namespace: :switchman) do |batch|
164
+ batch << <<-RUBY
165
+ def plain_#{attr_name}=(new_value)
166
+ _write_attribute('#{attr_name}', new_value)
167
+ end
168
+ RUBY
169
+ end
170
+ end
171
+ end
104
172
 
105
- owner = CodeGenerator.new(self, __LINE__ + 4)
173
+ def build_sharded_setter(attr_name, attr_field, attr_connection_class)
174
+ <<-RUBY
175
+ def #{attr_name}=(new_value)
176
+ self.original_#{attr_field} = ::Switchman::Shard.relative_id_for(new_value, ::Switchman::Shard.current(#{attr_connection_class}), shard)
106
177
  end
178
+ RUBY
179
+ end
107
180
 
108
- owner << <<-RUBY
109
- # rename the original method to original_*
110
- alias_method 'original_#{attr_name}', '#{attr_name}'
111
- # and replace with one that transposes the id
112
- def #{attr_name}
113
- raw_value = original_#{attr_name}
114
- return nil if raw_value.nil?
115
-
116
- abs_raw_value = raw_value.abs
117
- current_shard = ::Switchman::Shard.current(#{connection_classes_code_for_reflection(reflection)})
118
- same_shard = shard == current_shard
119
- return raw_value if same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
120
-
121
- value_shard_id = abs_raw_value / ::Switchman::Shard::IDS_PER_SHARD
122
- # this is a stupid case when someone stuffed a global id for the current shard in instead
123
- # of a local id
124
- return raw_value % ::Switchman::Shard::IDS_PER_SHARD if value_shard_id == current_shard.id
125
- return raw_value if !same_shard && abs_raw_value > ::Switchman::Shard::IDS_PER_SHARD
126
- return shard.global_id_for(raw_value) if !same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
127
-
128
- ::Switchman::Shard.relative_id_for(raw_value, shard, current_shard)
129
- end
181
+ def define_method_original_attribute(attr_name, owner:)
182
+ if sharded_column?(attr_name)
183
+ define_cached_method(owner, "original_#{attr_name}", as: "sharded_original_#{attr_name}", namespace: :switchman) do |batch|
184
+ batch << <<-RUBY
185
+ def sharded_original_#{attr_name}
186
+ _read_attribute("#{attr_name}") { |n| missing_attribute(n, caller) }
187
+ end
188
+ RUBY
189
+ end
190
+ else
191
+ define_method_unsharded_column(attr_name, 'global', owner)
192
+ end
193
+ end
194
+
195
+ def define_method_original_attribute=(attr_name, owner:)
196
+ return unless sharded_column?(attr_name)
130
197
 
131
- alias_method 'original_#{attr_name}=', '#{attr_name}='
132
- def #{attr_name}=(new_value)
133
- self.original_#{attr_name} = ::Switchman::Shard.relative_id_for(new_value, ::Switchman::Shard.current(#{connection_classes_code_for_reflection(reflection)}), shard)
198
+ define_cached_method(owner, "original_#{attr_name}=", as: "sharded_original_#{attr_name}=", namespace: :switchman) do |batch|
199
+ batch << <<-RUBY
200
+ def sharded_original_#{attr_name}=(new_value)
201
+ _write_attribute('#{attr_name}', new_value)
134
202
  end
135
203
  RUBY
136
- else
137
- define_method_unsharded_column(attr_name, 'global', owner)
138
204
  end
139
205
  end
140
206
 
141
207
  def define_method_unsharded_column(attr_name, prefix, owner)
142
- return if columns_hash["#{prefix}_#{attr_name}"]
208
+ return if columns_hash["#{prefix}_#{attr_name}"] || attr_name == 'id'
143
209
 
144
- owner << <<-RUBY
145
- def #{prefix}_#{attr_name}
146
- raise NoMethodError, "undefined method `#{prefix}_#{attr_name}'; are you missing an association?"
147
- end
148
- RUBY
210
+ define_cached_method(owner, "#{prefix}_#{attr_name}", as: "unsharded_#{prefix}_#{attr_name}", namespace: :switchman) do |batch|
211
+ batch << <<-RUBY
212
+ def unsharded_#{prefix}_#{attr_name}
213
+ raise NoMethodError, "undefined method `#{prefix}_#{attr_name}'; are you missing an association?"
214
+ end
215
+ RUBY
216
+ end
149
217
  end
150
218
  end
151
219
 
152
- def self.included(klass)
153
- klass.singleton_class.include(ClassMethods)
220
+ def self.prepended(klass)
221
+ klass.singleton_class.prepend(ClassMethods)
154
222
  klass.attribute_method_prefix 'global_', 'local_', 'original_'
155
- end
156
-
157
- # ensure that we're using the sharded attribute method
158
- # and not the silly one in AR::AttributeMethods::PrimaryKey
159
- def id
160
- return super if is_a?(Shard)
161
-
162
- self.class.define_attribute_methods
163
- super
223
+ klass.attribute_method_affix prefix: 'original_', suffix: '='
164
224
  end
165
225
 
166
226
  # these are called if the specific methods haven't been defined yet
@@ -168,7 +228,7 @@ module Switchman
168
228
  return super unless self.class.sharded_column?(attr_name)
169
229
 
170
230
  reflection = self.class.send(:reflection_for_integer_attribute, attr_name)
171
- ::Switchman::Shard.relative_id_for(super, shard, ::Switchman::Shard.current(connection_classes_for_reflection(reflection)))
231
+ ::Switchman::Shard.relative_id_for(super, shard, ::Switchman::Shard.current(connection_class_for_self_for_reflection(reflection)))
172
232
  end
173
233
 
174
234
  def attribute=(attr_name, new_value)
@@ -178,7 +238,7 @@ module Switchman
178
238
  end
179
239
 
180
240
  reflection = self.class.send(:reflection_for_integer_attribute, attr_name)
181
- super(::Switchman::Shard.relative_id_for(new_value, ::Switchman::Shard.current(connection_classes_for_reflection(reflection)), shard))
241
+ super(::Switchman::Shard.relative_id_for(new_value, ::Switchman::Shard.current(connection_class_for_self_for_reflection(reflection)), shard))
182
242
  end
183
243
 
184
244
  def global_attribute(attr_name)
@@ -199,15 +259,15 @@ module Switchman
199
259
 
200
260
  private
201
261
 
202
- def connection_classes_for_reflection(reflection)
262
+ def connection_class_for_self_for_reflection(reflection)
203
263
  if reflection
204
264
  if reflection.options[:polymorphic]
205
- read_attribute(reflection.foreign_type)&.constantize&.connection_classes
265
+ read_attribute(reflection.foreign_type)&.constantize&.connection_class_for_self
206
266
  else
207
- reflection.klass.connection_classes
267
+ reflection.klass.connection_class_for_self
208
268
  end
209
269
  else
210
- self.class.connection_classes
270
+ self.class.connection_class_for_self
211
271
  end
212
272
  end
213
273
  end