switchman 2.0.13 → 3.0.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +10 -2
  3. data/app/models/switchman/shard.rb +234 -271
  4. data/app/models/switchman/unsharded_record.rb +7 -0
  5. data/db/migrate/20130328212039_create_switchman_shards.rb +1 -1
  6. data/db/migrate/20130328224244_create_default_shard.rb +5 -5
  7. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +1 -0
  8. data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
  9. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +7 -5
  10. data/db/migrate/20190114212900_add_unique_name_indexes.rb +5 -3
  11. data/lib/switchman.rb +3 -5
  12. data/lib/switchman/action_controller/caching.rb +2 -2
  13. data/lib/switchman/active_record/abstract_adapter.rb +1 -0
  14. data/lib/switchman/active_record/association.rb +78 -89
  15. data/lib/switchman/active_record/attribute_methods.rb +58 -52
  16. data/lib/switchman/active_record/base.rb +58 -59
  17. data/lib/switchman/active_record/calculations.rb +74 -67
  18. data/lib/switchman/active_record/connection_pool.rb +14 -41
  19. data/lib/switchman/active_record/database_configurations.rb +34 -0
  20. data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
  21. data/lib/switchman/active_record/finder_methods.rb +11 -16
  22. data/lib/switchman/active_record/log_subscriber.rb +4 -8
  23. data/lib/switchman/active_record/migration.rb +6 -47
  24. data/lib/switchman/active_record/model_schema.rb +1 -1
  25. data/lib/switchman/active_record/persistence.rb +4 -6
  26. data/lib/switchman/active_record/postgresql_adapter.rb +124 -168
  27. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  28. data/lib/switchman/active_record/query_cache.rb +18 -19
  29. data/lib/switchman/active_record/query_methods.rb +172 -197
  30. data/lib/switchman/active_record/reflection.rb +6 -10
  31. data/lib/switchman/active_record/relation.rb +30 -78
  32. data/lib/switchman/active_record/spawn_methods.rb +27 -29
  33. data/lib/switchman/active_record/statement_cache.rb +18 -35
  34. data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
  35. data/lib/switchman/active_support/cache.rb +3 -5
  36. data/lib/switchman/arel.rb +13 -8
  37. data/lib/switchman/database_server.rb +121 -142
  38. data/lib/switchman/default_shard.rb +52 -16
  39. data/lib/switchman/engine.rb +61 -58
  40. data/lib/switchman/environment.rb +4 -8
  41. data/lib/switchman/errors.rb +1 -0
  42. data/lib/switchman/guard_rail.rb +6 -19
  43. data/lib/switchman/guard_rail/relation.rb +5 -7
  44. data/lib/switchman/r_spec_helper.rb +29 -37
  45. data/lib/switchman/rails.rb +14 -12
  46. data/lib/switchman/schema_cache.rb +1 -9
  47. data/lib/switchman/sharded_instrumenter.rb +1 -1
  48. data/lib/switchman/standard_error.rb +15 -3
  49. data/lib/switchman/test_helper.rb +7 -11
  50. data/lib/switchman/version.rb +1 -1
  51. data/lib/tasks/switchman.rake +54 -69
  52. metadata +87 -45
  53. data/lib/switchman/active_record/batches.rb +0 -11
  54. data/lib/switchman/active_record/connection_handler.rb +0 -172
  55. data/lib/switchman/active_record/where_clause_factory.rb +0 -36
  56. data/lib/switchman/connection_pool_proxy.rb +0 -173
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ class UnshardedRecord < ::ActiveRecord::Base
5
+ sharded_model
6
+ end
7
+ end
@@ -5,7 +5,7 @@ class CreateSwitchmanShards < ActiveRecord::Migration[4.2]
5
5
  create_table :switchman_shards do |t|
6
6
  t.string :name
7
7
  t.string :database_server_id
8
- t.boolean :default, :default => false, :null => false
8
+ t.boolean :default, default: false, null: false
9
9
  end
10
10
  end
11
11
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  class CreateDefaultShard < ActiveRecord::Migration[4.2]
4
4
  def up
5
- unless Switchman::Shard.default.is_a?(Switchman::Shard)
6
- Switchman::Shard.reset_column_information
7
- Switchman::Shard.create!(:default => true)
8
- Switchman::Shard.default(reload: true)
9
- end
5
+ return if Switchman::Shard.default.is_a?(Switchman::Shard)
6
+
7
+ Switchman::Shard.reset_column_information
8
+ Switchman::Shard.create!(default: true)
9
+ Switchman::Shard.default(reload: true)
10
10
  end
11
11
  end
@@ -8,6 +8,7 @@ class AddBackDefaultStringLimitsSwitchman < ActiveRecord::Migration[4.2]
8
8
 
9
9
  def add_string_limit_if_missing(table, column)
10
10
  return if column_exists?(table, column, :string, limit: 255)
11
+
11
12
  change_column table, column, :string, limit: 255
12
13
  end
13
14
  end
@@ -6,10 +6,10 @@ class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
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'
9
- { unique: true, where: "\"default\"" }
9
+ { unique: true, where: '"default"' }
10
10
  else
11
11
  {}
12
12
  end
13
- add_index :switchman_shards, :default, **options
13
+ add_index :switchman_shards, :default, options
14
14
  end
15
15
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class AddTimestampsToShards < ActiveRecord::Migration[4.2]
4
+ disable_ddl_transaction!
5
+
4
6
  def change
5
7
  add_timestamps :switchman_shards, null: true
6
8
  now = Time.now.utc
@@ -8,10 +10,10 @@ class AddTimestampsToShards < ActiveRecord::Migration[4.2]
8
10
  change_column_null :switchman_shards, :updated_at, false
9
11
  change_column_null :switchman_shards, :created_at, false
10
12
 
11
- if Switchman::Shard.current.default?
12
- Switchman::Shard.connection.schema_cache.clear!
13
- Switchman::Shard.reset_column_information
14
- Switchman::Shard.columns
15
- end
13
+ return unless Switchman::Shard.current.default?
14
+
15
+ Switchman::Shard.connection.schema_cache.clear!
16
+ Switchman::Shard.reset_column_information
17
+ Switchman::Shard.columns
16
18
  end
17
19
  end
@@ -2,8 +2,10 @@
2
2
 
3
3
  class AddUniqueNameIndexes < ActiveRecord::Migration[4.2]
4
4
  def change
5
- add_index :switchman_shards, [:database_server_id, :name], unique: true
6
- add_index :switchman_shards, :database_server_id, unique: true, where: "name IS NULL", name: 'index_switchman_shards_unique_primary_shard'
7
- add_index :switchman_shards, "(true)", unique: true, where: "database_server_id IS NULL AND name IS NULL", name: 'index_switchman_shards_unique_primary_db_and_shard'
5
+ add_index :switchman_shards, %i[database_server_id name], unique: true
6
+ add_index :switchman_shards, :database_server_id, unique: true, where: 'name IS NULL',
7
+ name: 'index_switchman_shards_unique_primary_shard'
8
+ add_index :switchman_shards, '(true)', unique: true, where: 'database_server_id IS NULL AND name IS NULL',
9
+ name: 'index_switchman_shards_unique_primary_db_and_shard'
8
10
  end
9
11
  end
data/lib/switchman.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "guard_rail"
4
- require "switchman/open4"
5
- require "switchman/engine"
3
+ require 'guard_rail'
4
+ require 'switchman/open4'
5
+ require 'switchman/engine'
6
6
 
7
7
  module Switchman
8
8
  def self.config
@@ -17,6 +17,4 @@ module Switchman
17
17
  def self.cache=(cache)
18
18
  @cache = cache
19
19
  end
20
-
21
- class OrderOnMultiShardQuery < RuntimeError; end
22
20
  end
@@ -13,7 +13,7 @@ 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)
16
+ def cache_store=(_cache)
17
17
  raise NoMethodError
18
18
  end
19
19
  end
@@ -21,7 +21,7 @@ module Switchman
21
21
  include ConfigMethods
22
22
 
23
23
  def self.included(base)
24
- base.extend(ConfigMethods)
24
+ base.singleton_class.prepend(ConfigMethods)
25
25
  end
26
26
  end
27
27
  end
@@ -45,6 +45,7 @@ module Switchman
45
45
 
46
46
  def id_value_for_database(value)
47
47
  return super unless value.class.sharded_primary_key?
48
+
48
49
  # do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
49
50
  quote(value.id)
50
51
  end
@@ -8,27 +8,17 @@ module Switchman
8
8
  end
9
9
 
10
10
  def build_record(*args)
11
- self.shard.activate { super }
11
+ shard.activate { super }
12
12
  end
13
13
 
14
14
  def load_target
15
- self.shard.activate { super }
15
+ shard.activate { super }
16
16
  end
17
17
 
18
18
  def scope
19
- shard_value = @reflection.options[:multishard] ? @owner : self.shard
19
+ shard_value = @reflection.options[:multishard] ? @owner : shard
20
20
  @owner.shard.activate { super.shard(shard_value, :association) }
21
21
  end
22
-
23
- def creation_attributes
24
- attributes = super
25
-
26
- # translate keys
27
- if reflection.macro.in?([:has_one, :has_many]) && !options[:through]
28
- attributes[reflection.foreign_key] = Shard.relative_id_for(owner[reflection.active_record_primary_key], owner.shard, self.shard)
29
- end
30
- attributes
31
- end
32
22
  end
33
23
 
34
24
  module CollectionAssociation
@@ -36,15 +26,19 @@ module Switchman
36
26
  shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [shard]
37
27
  # activate both the owner and the target's shard category, so that Reflection#join_id_for,
38
28
  # when called for the owner, will be returned relative to shard the query will execute on
39
- Shard.with_each_shard(shards, [klass.shard_category, owner.class.shard_category].uniq) do
29
+ Shard.with_each_shard(shards, [klass.connection_classes, owner.class.connection_classes].uniq) do
40
30
  super
41
31
  end
42
32
  end
33
+
34
+ def _create_record(*)
35
+ shard.activate { super }
36
+ end
43
37
  end
44
38
 
45
39
  module BelongsToAssociation
46
- def replace_keys(record)
47
- if record && record.class.sharded_column?(reflection.association_primary_key(record.class))
40
+ def replace_keys(record, force: false)
41
+ if record&.class&.sharded_column?(reflection.association_primary_key(record.class))
48
42
  foreign_id = record[reflection.association_primary_key(record.class)]
49
43
  owner[reflection.foreign_key] = Shard.relative_id_for(foreign_id, record.shard, owner.shard)
50
44
  else
@@ -54,7 +48,7 @@ module Switchman
54
48
 
55
49
  def shard
56
50
  if @owner.class.sharded_column?(@reflection.foreign_key) &&
57
- foreign_id = @owner[@reflection.foreign_key]
51
+ (foreign_id = @owner[@reflection.foreign_key])
58
52
  Shard.shard_for(foreign_id, @owner.shard)
59
53
  else
60
54
  super
@@ -62,9 +56,22 @@ module Switchman
62
56
  end
63
57
  end
64
58
 
65
- module Extension
66
- def self.build(_model, _reflection)
59
+ module ForeignAssociation
60
+ # significant change:
61
+ # * transpose the key to the correct shard
62
+ def set_owner_attributes(record) # rubocop:disable Naming/AccessorMethodName
63
+ return if options[:through]
64
+
65
+ key = owner._read_attribute(reflection.join_foreign_key)
66
+ key = Shard.relative_id_for(key, owner.shard, shard)
67
+ record._write_attribute(reflection.join_primary_key, key)
68
+
69
+ record._write_attribute(reflection.type, owner.class.polymorphic_name) if reflection.type
67
70
  end
71
+ end
72
+
73
+ module Extension
74
+ def self.build(_model, _reflection); end
68
75
 
69
76
  def self.valid_options
70
77
  [:multishard]
@@ -75,45 +82,35 @@ module Switchman
75
82
 
76
83
  module Preloader
77
84
  module Association
78
- if ::Rails.version >= "5.2" and ::Rails.version < "6.0"
79
- def run(preloader)
80
- associated_records_by_owner.each do |owner, records|
81
- associate_records_to_owner(owner, records)
82
- end
85
+ # Copypasta from Activerecord but with added global_id_for goodness.
86
+ def records_for(ids)
87
+ scope.where(association_key_name => ids).load do |record|
88
+ global_key = if record.class.connection_classes == UnshardedRecord
89
+ convert_key(record[association_key_name])
90
+ else
91
+ Shard.global_id_for(record[association_key_name], record.shard)
92
+ end
93
+ owner = owners_by_key[convert_key(global_key)].first
94
+ association = owner.association(reflection.name)
95
+ association.set_inverse_instance(record)
83
96
  end
84
97
  end
85
98
 
86
- if ::Rails.version >= "6.0"
87
- # Copypasta from Activerecord but with added global_id_for goodness.
88
- def records_for(ids)
89
- scope.where(association_key_name => ids).load do |record|
90
- global_key = if model.shard_category == :unsharded
91
- convert_key(record[association_key_name])
92
- else
93
- Shard.global_id_for(record[association_key_name], record.shard)
94
- end
95
- owner = owners_by_key[global_key.to_s].first
96
- association = owner.association(reflection.name)
97
- association.set_inverse_instance(record)
98
- end
99
- end
100
-
101
- def records_by_owner
102
- associated_records_by_owner
103
- end
104
- end
105
-
106
- def associated_records_by_owner(preloader = nil)
107
- return @associated_records_by_owner if defined?(@associated_records_by_owner)
108
- owners_map = owners_by_key
99
+ # significant changes:
100
+ # * partition_by_shard the records_for call
101
+ # * re-globalize the fetched owner id before looking up in the map
102
+ def load_records
103
+ # owners can be duplicated when a relation has a collection association join
104
+ # #compare_by_identity makes such owners different hash keys
105
+ @records_by_owner = {}.compare_by_identity
109
106
 
110
- if klass.nil? || owners_map.empty?
111
- records = []
107
+ if owner_keys.empty?
108
+ raw_records = []
112
109
  else
113
110
  # determine the shard to search for each owner
114
111
  if reflection.macro == :belongs_to
115
112
  # for belongs_to, it's the shard of the foreign_key
116
- partition_proc = ->(owner) do
113
+ partition_proc = lambda do |owner|
117
114
  if owner.class.sharded_column?(owner_key_name)
118
115
  Shard.shard_for(owner[owner_key_name], owner.shard)
119
116
  else
@@ -123,64 +120,56 @@ module Switchman
123
120
  elsif !reflection.options[:multishard]
124
121
  # for non-multishard associations, it's *just* the owner's shard
125
122
  partition_proc = ->(owner) { owner.shard }
126
- else
127
- # for multishard associations, it's the owner object itself
128
- # (all associated shards)
129
-
130
- # this is the default behavior of partition_by_shard, so just let it be nil
131
- # to avoid the proc call
132
- # partition_proc = ->(owner) { owner }
133
123
  end
134
124
 
135
- records = Shard.partition_by_shard(owners, partition_proc) do |partitioned_owners|
136
- # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
137
- # Make several smaller queries if necessary or make one query if the adapter supports it
138
- sliced_owners = partitioned_owners.each_slice(model.connection.in_clause_length || partitioned_owners.size)
139
- sliced_owners.map do |slice|
140
- relative_owner_keys = slice.map do |owner|
141
- key = owner[owner_key_name]
142
- if key && owner.class.sharded_column?(owner_key_name)
143
- key = Shard.relative_id_for(key, owner.shard, Shard.current(owner.class.shard_category))
144
- end
145
- key && key.to_s
125
+ raw_records = Shard.partition_by_shard(owners, partition_proc) do |partitioned_owners|
126
+ relative_owner_keys = partitioned_owners.map do |owner|
127
+ key = owner[owner_key_name]
128
+ if key && owner.class.sharded_column?(owner_key_name)
129
+ key = Shard.relative_id_for(key, owner.shard,
130
+ Shard.current(klass.connection_classes))
146
131
  end
147
- relative_owner_keys.compact!
148
- relative_owner_keys.uniq!
149
- records_for(relative_owner_keys)
132
+ convert_key(key)
150
133
  end
134
+ relative_owner_keys.compact!
135
+ relative_owner_keys.uniq!
136
+ records_for(relative_owner_keys)
151
137
  end
152
- records.flatten!
153
138
  end
154
139
 
155
- # This ivar may look unused, but remember this is an extension of
156
- # rails' AR::Associations::Preloader::Association class. It gets used
157
- # by that class (and its subclasses).
158
- @preloaded_records = records
140
+ @preloaded_records = raw_records.select do |record|
141
+ assignments = false
159
142
 
160
- # Each record may have multiple owners, and vice-versa
161
- @associated_records_by_owner = owners.each_with_object({}) do |owner,h|
162
- h[owner] = []
163
- end
164
- records.each do |record|
165
143
  owner_key = record[association_key_name]
166
- owner_key = Shard.global_id_for(owner_key, record.shard) if owner_key && record.class.sharded_column?(association_key_name)
144
+ if owner_key && record.class.sharded_column?(association_key_name)
145
+ owner_key = Shard.global_id_for(owner_key,
146
+ record.shard)
147
+ end
167
148
 
168
- owners_map[owner_key.to_s].each do |owner|
169
- owner.association(reflection.name).set_inverse_instance(record)
170
- @associated_records_by_owner[owner] << record
149
+ owners_by_key[convert_key(owner_key)].each do |owner|
150
+ entries = (@records_by_owner[owner] ||= [])
151
+
152
+ if reflection.collection? || entries.empty?
153
+ entries << record
154
+ assignments = true
155
+ end
171
156
  end
157
+
158
+ assignments
172
159
  end
173
- @associated_records_by_owner
174
160
  end
175
161
 
162
+ # significant change: globalize keys on sharded columns
176
163
  def owners_by_key
177
- @owners_by_key ||= owners.group_by do |owner|
164
+ @owners_by_key ||= owners.each_with_object({}) do |owner, result|
178
165
  key = owner[owner_key_name]
179
166
  key = Shard.global_id_for(key, owner.shard) if key && owner.class.sharded_column?(owner_key_name)
180
- key && key.to_s
167
+ key = convert_key(key)
168
+ (result[key] ||= []) << owner if key
181
169
  end
182
170
  end
183
171
 
172
+ # significant change: don't cache scope (since it could be for different shards)
184
173
  def scope
185
174
  build_scope
186
175
  end
@@ -210,7 +199,7 @@ module Switchman
210
199
  # this seems counter-intuitive, but the autosave code will assign to attribute bypassing switchman,
211
200
  # after reading the id attribute _without_ bypassing switchman. So we need Shard.current for the
212
201
  # category of the associated record to match Shard.current for the category of self
213
- shard.activate(shard_category_for_reflection(reflection)) { super }
202
+ shard.activate(connection_classes_for_reflection(reflection)) { super }
214
203
  end
215
204
  end
216
205
  end
@@ -4,22 +4,23 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module AttributeMethods
6
6
  module ClassMethods
7
-
8
7
  def sharded_primary_key?
9
- self != Shard && shard_category != :unsharded && integral_id?
8
+ !(self <= UnshardedRecord) && integral_id?
10
9
  end
11
10
 
12
11
  def sharded_foreign_key?(column_name)
13
12
  reflection = reflection_for_integer_attribute(column_name.to_s)
14
13
  return false unless reflection
14
+
15
15
  reflection.options[:polymorphic] || reflection.klass.sharded_primary_key?
16
16
  end
17
17
 
18
18
  def sharded_column?(column_name)
19
19
  column_name = column_name.to_s
20
20
  @sharded_column_values ||= {}
21
- unless @sharded_column_values.has_key?(column_name)
22
- @sharded_column_values[column_name] = (column_name == primary_key && sharded_primary_key?) || sharded_foreign_key?(column_name)
21
+ unless @sharded_column_values.key?(column_name)
22
+ @sharded_column_values[column_name] =
23
+ (column_name == primary_key && sharded_primary_key?) || sharded_foreign_key?(column_name)
23
24
  end
24
25
  @sharded_column_values[column_name]
25
26
  end
@@ -29,109 +30,114 @@ module Switchman
29
30
  def reflection_for_integer_attribute(attr_name)
30
31
  attr_name = attr_name.to_s
31
32
  columns_hash[attr_name] && columns_hash[attr_name].type == :integer &&
32
- reflections.find { |_, r| r.belongs_to? && r.foreign_key.to_s == attr_name }&.last
33
+ reflections.find { |_, r| r.belongs_to? && r.foreign_key.to_s == attr_name }&.last
33
34
  rescue ::ActiveRecord::StatementInvalid
34
35
  # this is for when models are referenced in initializers before migrations have been run
35
- raise if connection.open_transactions > 0
36
+ raise if connection.open_transactions.positive?
36
37
  end
37
38
 
38
- def define_method_global_attribute(attr_name)
39
+ def define_method_global_attribute(attr_name, owner:)
39
40
  if sharded_column?(attr_name)
40
- generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
41
- def __temp__
42
- Shard.global_id_for(original_#{attr_name}, shard)
41
+ owner << <<-RUBY
42
+ def global_#{attr_name}
43
+ ::Switchman::Shard.global_id_for(original_#{attr_name}, shard)
43
44
  end
44
- alias_method 'global_#{attr_name}', :__temp__
45
- undef_method :__temp__
46
45
  RUBY
47
46
  else
48
- define_method_unsharded_column(attr_name, 'global')
47
+ define_method_unsharded_column(attr_name, 'global', owner)
49
48
  end
50
49
  end
51
50
 
52
- def define_method_local_attribute(attr_name)
51
+ def define_method_local_attribute(attr_name, owner:)
53
52
  if sharded_column?(attr_name)
54
- generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
55
- def __temp__
56
- Shard.local_id_for(original_#{attr_name}).first
53
+ owner << <<-RUBY
54
+ def local_#{attr_name}
55
+ ::Switchman::Shard.local_id_for(original_#{attr_name}).first
57
56
  end
58
- alias_method 'local_#{attr_name}', :__temp__
59
- undef_method :__temp__
60
57
  RUBY
61
58
  else
62
- define_method_unsharded_column(attr_name, 'local')
59
+ define_method_unsharded_column(attr_name, 'local', owner)
63
60
  end
64
61
  end
65
62
 
66
- # see also Base#shard_category_for_reflection
63
+ # see also Base#connection_classes_for_reflection
67
64
  # the difference being this will output static strings for the common cases, making them
68
65
  # more performant
69
- def shard_category_code_for_reflection(reflection)
66
+ def connection_classes_code_for_reflection(reflection)
70
67
  if reflection
71
68
  if reflection.options[:polymorphic]
72
69
  # a polymorphic association has to be discovered at runtime. This code ends up being something like
73
- # context_type.&.constantize&.shard_category || :primary
74
- "read_attribute(:#{reflection.foreign_type})&.constantize&.shard_category || :primary"
70
+ # context_type.&.constantize&.connection_classes
71
+ "read_attribute(:#{reflection.foreign_type})&.constantize&.connection_classes"
75
72
  else
76
73
  # otherwise we can just return a symbol for the statically known type of the association
77
- reflection.klass.shard_category.inspect
74
+ "::#{reflection.klass.connection_classes.name}"
78
75
  end
79
76
  else
80
- shard_category.inspect
77
+ "::#{connection_classes.name}"
78
+ end
79
+ end
80
+
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
86
+ end
87
+
88
+ def <<(string)
89
+ @module.module_eval(string, __FILE__, @line)
81
90
  end
82
91
  end
83
92
 
84
- def define_method_original_attribute(attr_name)
93
+ def define_method_original_attribute(attr_name, owner:)
85
94
  if sharded_column?(attr_name)
86
95
  reflection = reflection_for_integer_attribute(attr_name)
87
- if attr_name == "id" && ::Rails.version >= '5.1.2'
88
- return if self.method_defined?(:original_id)
89
- owner = self
90
- else
91
- owner = generated_attribute_methods
96
+ if attr_name == 'id'
97
+ return if method_defined?(:original_id)
98
+
99
+ owner = CodeGenerator.new(self, __LINE__ + 4)
92
100
  end
93
- owner.module_eval <<-RUBY, __FILE__, __LINE__ + 1
94
- # rename the original method to original_
101
+
102
+ owner << <<-RUBY
103
+ # rename the original method to original_*
95
104
  alias_method 'original_#{attr_name}', '#{attr_name}'
96
105
  # and replace with one that transposes the id
97
- def __temp__
98
- Shard.relative_id_for(original_#{attr_name}, shard, Shard.current(#{shard_category_code_for_reflection(reflection)}))
106
+ def #{attr_name}
107
+ ::Switchman::Shard.relative_id_for(original_#{attr_name}, shard, ::Switchman::Shard.current(#{connection_classes_code_for_reflection(reflection)}))
99
108
  end
100
- alias_method '#{attr_name}', :__temp__
101
- undef_method :__temp__
102
109
 
103
110
  alias_method 'original_#{attr_name}=', '#{attr_name}='
104
- def __temp__(new_value)
105
- self.original_#{attr_name} = Shard.relative_id_for(new_value, Shard.current(#{shard_category_code_for_reflection(reflection)}), shard)
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)
106
113
  end
107
- alias_method '#{attr_name}=', :__temp__
108
- undef_method :__temp__
109
114
  RUBY
110
115
  else
111
- define_method_unsharded_column(attr_name, 'global')
116
+ define_method_unsharded_column(attr_name, 'global', owner)
112
117
  end
113
118
  end
114
119
 
115
- def define_method_unsharded_column(attr_name, prefix)
120
+ def define_method_unsharded_column(attr_name, prefix, owner)
116
121
  return if columns_hash["#{prefix}_#{attr_name}"]
117
- generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
118
- def __temp__
119
- raise NoMethodError, "undefined method `#{prefix}_#{attr_name}'; are you missing an association?"
120
- end
121
- alias_method '#{prefix}_#{attr_name}', :__temp__
122
- undef_method :__temp__
122
+
123
+ owner << <<-RUBY
124
+ def #{prefix}_#{attr_name}
125
+ raise NoMethodError, "undefined method `#{prefix}_#{attr_name}'; are you missing an association?"
126
+ end
123
127
  RUBY
124
128
  end
125
129
  end
126
130
 
127
131
  def self.included(klass)
128
- klass.extend(ClassMethods)
129
- klass.attribute_method_prefix "global_", "local_", "original_"
132
+ klass.singleton_class.include(ClassMethods)
133
+ klass.attribute_method_prefix 'global_', 'local_', 'original_'
130
134
  end
131
135
 
132
136
  # ensure that we're using the sharded attribute method
133
137
  # and not the silly one in AR::AttributeMethods::PrimaryKey
134
138
  def id
139
+ return super if is_a?(Shard)
140
+
135
141
  self.class.define_attribute_methods
136
142
  super
137
143
  end