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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1224320ec4f3ff0d8f05d1c5537ce06f33932e4c9da069d82183aa7562e1cab4
4
- data.tar.gz: 1cd6f52c82fbdea9c3aac4c569b291428d2476abc4a2198c09beb1da64700473
3
+ metadata.gz: 48b27124d5233adf585aed105d0b2522c76249bdd1e13fbcfd7e904241d3a624
4
+ data.tar.gz: fea1ccc0ad63f00633fb90bee1d7bd4606f6de0e18564250865c1b1dde61ef8d
5
5
  SHA512:
6
- metadata.gz: 052e1675cb5d6b07844dab24c802c6f98b8e75fe4c0146a407e275d89633b5c3058801538a2537f82a7798260ad9d5dd2989536ad88d06a37bb33cf9082444dd
7
- data.tar.gz: ebaa91ea8f6be5f878ac2f8b57a7954853603cb20770f083263fd369774cedc4b33c8127a8b17746bbf4e5b91e1e742df2970be8dab08aa708baae3bc130b988
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]
@@ -2,7 +2,7 @@
2
2
 
3
3
  class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
4
4
  def change
5
- Switchman::Shard.where(default: nil).update_all(default: false)
5
+ Switchman::Shard.where(default: nil).update_all(default: false) if Switchman::Shard.current.default?
6
6
  change_column_default :switchman_shards, :default, false
7
7
  change_column_null :switchman_shards, :default, false
8
8
  options = if connection.adapter_name == 'PostgreSQL'
@@ -4,7 +4,7 @@ class AddTimestampsToShards < ActiveRecord::Migration[4.2]
4
4
  disable_ddl_transaction!
5
5
 
6
6
  def change
7
- add_timestamps :switchman_shards, null: true
7
+ add_timestamps :switchman_shards, null: true, if_not_exists: true
8
8
  now = Time.now.utc
9
9
  Switchman::Shard.update_all(updated_at: now, created_at: now) if Switchman::Shard.current.default?
10
10
  change_column_null :switchman_shards, :updated_at, false
@@ -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,20 +33,11 @@ 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
42
40
  end
43
-
44
- private
45
-
46
- def id_value_for_database(value)
47
- return super unless value.class.sharded_primary_key?
48
-
49
- # do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
50
- quote(value.id)
51
- end
52
41
  end
53
42
  end
54
43
  end
@@ -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,13 +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
- ::Switchman::Shard.global_id_for(original_#{attr_name}, shard)
44
- end
45
- 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
46
73
  else
47
74
  define_method_unsharded_column(attr_name, 'global', owner)
48
75
  end
@@ -50,96 +77,150 @@ module Switchman
50
77
 
51
78
  def define_method_local_attribute(attr_name, owner:)
52
79
  if sharded_column?(attr_name)
53
- owner << <<-RUBY
54
- def local_#{attr_name}
55
- ::Switchman::Shard.local_id_for(original_#{attr_name}).first
56
- end
57
- 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
58
89
  else
59
90
  define_method_unsharded_column(attr_name, 'local', owner)
60
91
  end
61
92
  end
62
93
 
63
- # see also Base#connection_classes_for_reflection
94
+ # see also Base#connection_class_for_self_for_reflection
64
95
  # the difference being this will output static strings for the common cases, making them
65
96
  # more performant
66
- def connection_classes_code_for_reflection(reflection)
97
+ def connection_class_for_self_code_for_reflection(reflection)
67
98
  if reflection
68
99
  if reflection.options[:polymorphic]
69
100
  # a polymorphic association has to be discovered at runtime. This code ends up being something like
70
- # context_type.&.constantize&.connection_classes
71
- "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"
72
103
  else
73
104
  # otherwise we can just return a symbol for the statically known type of the association
74
- "::#{reflection.klass.connection_classes.name}"
105
+ "::#{reflection.klass.connection_class_for_self.name}"
75
106
  end
76
107
  else
77
- "::#{connection_classes.name}"
108
+ "::#{connection_class_for_self.name}"
78
109
  end
79
110
  end
80
111
 
81
- # just a dummy class with the proper interface that calls module_eval immediately
82
- class CodeGenerator
83
- def initialize(mod, line)
84
- @module = mod
85
- @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
86
128
  end
129
+ end
87
130
 
88
- def <<(string)
89
- @module.module_eval(string, __FILE__, @line)
90
- 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
91
152
  end
92
153
 
93
- def define_method_original_attribute(attr_name, owner:)
154
+ def define_method_attribute=(attr_name, owner:)
94
155
  if sharded_column?(attr_name)
95
156
  reflection = reflection_for_integer_attribute(attr_name)
96
- if attr_name == 'id'
97
- 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
98
172
 
99
- 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)
100
177
  end
178
+ RUBY
179
+ end
101
180
 
102
- owner << <<-RUBY
103
- # rename the original method to original_*
104
- alias_method 'original_#{attr_name}', '#{attr_name}'
105
- # and replace with one that transposes the id
106
- def #{attr_name}
107
- ::Switchman::Shard.relative_id_for(original_#{attr_name}, shard, ::Switchman::Shard.current(#{connection_classes_code_for_reflection(reflection)}))
108
- 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
109
194
 
110
- alias_method 'original_#{attr_name}=', '#{attr_name}='
111
- def #{attr_name}=(new_value)
112
- self.original_#{attr_name} = ::Switchman::Shard.relative_id_for(new_value, ::Switchman::Shard.current(#{connection_classes_code_for_reflection(reflection)}), shard)
195
+ def define_method_original_attribute=(attr_name, owner:)
196
+ return unless sharded_column?(attr_name)
197
+
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)
113
202
  end
114
203
  RUBY
115
- else
116
- define_method_unsharded_column(attr_name, 'global', owner)
117
204
  end
118
205
  end
119
206
 
120
207
  def define_method_unsharded_column(attr_name, prefix, owner)
121
- return if columns_hash["#{prefix}_#{attr_name}"]
208
+ return if columns_hash["#{prefix}_#{attr_name}"] || attr_name == 'id'
122
209
 
123
- owner << <<-RUBY
124
- def #{prefix}_#{attr_name}
125
- raise NoMethodError, "undefined method `#{prefix}_#{attr_name}'; are you missing an association?"
126
- end
127
- 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
128
217
  end
129
218
  end
130
219
 
131
- def self.included(klass)
132
- klass.singleton_class.include(ClassMethods)
220
+ def self.prepended(klass)
221
+ klass.singleton_class.prepend(ClassMethods)
133
222
  klass.attribute_method_prefix 'global_', 'local_', 'original_'
134
- end
135
-
136
- # ensure that we're using the sharded attribute method
137
- # and not the silly one in AR::AttributeMethods::PrimaryKey
138
- def id
139
- return super if is_a?(Shard)
140
-
141
- self.class.define_attribute_methods
142
- super
223
+ klass.attribute_method_affix prefix: 'original_', suffix: '='
143
224
  end
144
225
 
145
226
  # these are called if the specific methods haven't been defined yet
@@ -147,7 +228,7 @@ module Switchman
147
228
  return super unless self.class.sharded_column?(attr_name)
148
229
 
149
230
  reflection = self.class.send(:reflection_for_integer_attribute, attr_name)
150
- ::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)))
151
232
  end
152
233
 
153
234
  def attribute=(attr_name, new_value)
@@ -157,7 +238,7 @@ module Switchman
157
238
  end
158
239
 
159
240
  reflection = self.class.send(:reflection_for_integer_attribute, attr_name)
160
- 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))
161
242
  end
162
243
 
163
244
  def global_attribute(attr_name)
@@ -178,15 +259,15 @@ module Switchman
178
259
 
179
260
  private
180
261
 
181
- def connection_classes_for_reflection(reflection)
262
+ def connection_class_for_self_for_reflection(reflection)
182
263
  if reflection
183
264
  if reflection.options[:polymorphic]
184
- read_attribute(reflection.foreign_type)&.constantize&.connection_classes
265
+ read_attribute(reflection.foreign_type)&.constantize&.connection_class_for_self
185
266
  else
186
- reflection.klass.connection_classes
267
+ reflection.klass.connection_class_for_self
187
268
  end
188
269
  else
189
- self.class.connection_classes
270
+ self.class.connection_class_for_self
190
271
  end
191
272
  end
192
273
  end