switchman 2.1.0 → 3.0.6

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +10 -2
  3. data/app/models/switchman/shard.rb +270 -343
  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 +8 -6
  10. data/db/migrate/20190114212900_add_unique_name_indexes.rb +5 -3
  11. data/lib/switchman/action_controller/caching.rb +2 -2
  12. data/lib/switchman/active_record/abstract_adapter.rb +0 -8
  13. data/lib/switchman/active_record/association.rb +78 -89
  14. data/lib/switchman/active_record/attribute_methods.rb +127 -52
  15. data/lib/switchman/active_record/base.rb +83 -67
  16. data/lib/switchman/active_record/calculations.rb +73 -66
  17. data/lib/switchman/active_record/connection_pool.rb +12 -59
  18. data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
  19. data/lib/switchman/active_record/database_configurations.rb +34 -0
  20. data/lib/switchman/active_record/finder_methods.rb +11 -16
  21. data/lib/switchman/active_record/log_subscriber.rb +4 -8
  22. data/lib/switchman/active_record/migration.rb +19 -45
  23. data/lib/switchman/active_record/model_schema.rb +1 -1
  24. data/lib/switchman/active_record/persistence.rb +11 -6
  25. data/lib/switchman/active_record/postgresql_adapter.rb +33 -161
  26. data/lib/switchman/active_record/predicate_builder.rb +1 -1
  27. data/lib/switchman/active_record/query_cache.rb +18 -19
  28. data/lib/switchman/active_record/query_methods.rb +178 -193
  29. data/lib/switchman/active_record/reflection.rb +7 -22
  30. data/lib/switchman/active_record/relation.rb +32 -29
  31. data/lib/switchman/active_record/spawn_methods.rb +27 -29
  32. data/lib/switchman/active_record/statement_cache.rb +18 -35
  33. data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
  34. data/lib/switchman/active_record/test_fixtures.rb +43 -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 +130 -154
  38. data/lib/switchman/default_shard.rb +52 -16
  39. data/lib/switchman/engine.rb +65 -58
  40. data/lib/switchman/environment.rb +4 -8
  41. data/lib/switchman/errors.rb +1 -0
  42. data/lib/switchman/guard_rail/relation.rb +5 -7
  43. data/lib/switchman/guard_rail.rb +6 -19
  44. data/lib/switchman/r_spec_helper.rb +29 -57
  45. data/lib/switchman/rails.rb +14 -12
  46. data/lib/switchman/sharded_instrumenter.rb +1 -1
  47. data/lib/switchman/standard_error.rb +15 -3
  48. data/lib/switchman/test_helper.rb +5 -3
  49. data/lib/switchman/version.rb +1 -1
  50. data/lib/switchman.rb +3 -3
  51. data/lib/tasks/switchman.rake +61 -72
  52. metadata +90 -48
  53. data/lib/switchman/active_record/batches.rb +0 -11
  54. data/lib/switchman/active_record/connection_handler.rb +0 -190
  55. data/lib/switchman/active_record/where_clause_factory.rb +0 -36
  56. data/lib/switchman/connection_pool_proxy.rb +0 -173
  57. data/lib/switchman/schema_cache.rb +0 -28
@@ -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
@@ -2,11 +2,11 @@
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'
9
- { unique: true, where: "\"default\"" }
9
+ { unique: true, where: '"default"' }
10
10
  else
11
11
  {}
12
12
  end
@@ -1,17 +1,19 @@
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
- add_timestamps :switchman_shards, null: true
7
+ add_timestamps :switchman_shards, null: true, if_not_exists: true
6
8
  now = Time.now.utc
7
9
  Switchman::Shard.update_all(updated_at: now, created_at: now) if Switchman::Shard.current.default?
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
@@ -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
@@ -40,14 +40,6 @@ module Switchman
40
40
  ensure
41
41
  @last_query_at = Time.now
42
42
  end
43
-
44
- private
45
-
46
- def id_value_for_database(value)
47
- return super unless value.class.sharded_primary_key?
48
- # do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
49
- quote(value.id)
50
- end
51
43
  end
52
44
  end
53
45
  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 model.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,112 +30,186 @@ 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
+ 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)
43
48
  end
44
- alias_method 'global_#{attr_name}', :__temp__
45
- undef_method :__temp__
46
49
  RUBY
47
50
  else
48
- define_method_unsharded_column(attr_name, 'global')
51
+ define_method_unsharded_column(attr_name, 'global', owner)
49
52
  end
50
53
  end
51
54
 
52
- def define_method_local_attribute(attr_name)
55
+ def define_method_local_attribute(attr_name, owner:)
53
56
  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
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
57
62
  end
58
- alias_method 'local_#{attr_name}', :__temp__
59
- undef_method :__temp__
60
63
  RUBY
61
64
  else
62
- define_method_unsharded_column(attr_name, 'local')
65
+ define_method_unsharded_column(attr_name, 'local', owner)
63
66
  end
64
67
  end
65
68
 
66
- # see also Base#shard_category_for_reflection
69
+ # see also Base#connection_classes_for_reflection
67
70
  # the difference being this will output static strings for the common cases, making them
68
71
  # more performant
69
- def shard_category_code_for_reflection(reflection)
72
+ def connection_classes_code_for_reflection(reflection)
70
73
  if reflection
71
74
  if reflection.options[:polymorphic]
72
75
  # 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"
76
+ # context_type.&.constantize&.connection_classes
77
+ "read_attribute(:#{reflection.foreign_type})&.constantize&.connection_classes"
75
78
  else
76
79
  # otherwise we can just return a symbol for the statically known type of the association
77
- reflection.klass.shard_category.inspect
80
+ "::#{reflection.klass.connection_classes.name}"
78
81
  end
79
82
  else
80
- shard_category.inspect
83
+ "::#{connection_classes.name}"
84
+ end
85
+ end
86
+
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
92
+ end
93
+
94
+ def <<(string)
95
+ @module.module_eval(string, __FILE__, @line)
81
96
  end
82
97
  end
83
98
 
84
- def define_method_original_attribute(attr_name)
99
+ def define_method_original_attribute(attr_name, owner:)
85
100
  if sharded_column?(attr_name)
86
101
  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
102
+ if attr_name == 'id'
103
+ return if method_defined?(:original_id)
104
+
105
+ owner = CodeGenerator.new(self, __LINE__ + 4)
92
106
  end
93
- owner.module_eval <<-RUBY, __FILE__, __LINE__ + 1
94
- # rename the original method to original_
107
+
108
+ owner << <<-RUBY
109
+ # rename the original method to original_*
95
110
  alias_method 'original_#{attr_name}', '#{attr_name}'
96
111
  # 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)}))
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)
99
129
  end
100
- alias_method '#{attr_name}', :__temp__
101
- undef_method :__temp__
102
130
 
103
131
  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)
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)
106
134
  end
107
- alias_method '#{attr_name}=', :__temp__
108
- undef_method :__temp__
109
135
  RUBY
110
136
  else
111
- define_method_unsharded_column(attr_name, 'global')
137
+ define_method_unsharded_column(attr_name, 'global', owner)
112
138
  end
113
139
  end
114
140
 
115
- def define_method_unsharded_column(attr_name, prefix)
141
+ def define_method_unsharded_column(attr_name, prefix, owner)
116
142
  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__
143
+
144
+ owner << <<-RUBY
145
+ def #{prefix}_#{attr_name}
146
+ raise NoMethodError, "undefined method `#{prefix}_#{attr_name}'; are you missing an association?"
147
+ end
123
148
  RUBY
124
149
  end
125
150
  end
126
151
 
127
152
  def self.included(klass)
128
- klass.extend(ClassMethods)
129
- klass.attribute_method_prefix "global_", "local_", "original_"
153
+ klass.singleton_class.include(ClassMethods)
154
+ klass.attribute_method_prefix 'global_', 'local_', 'original_'
130
155
  end
131
156
 
132
157
  # ensure that we're using the sharded attribute method
133
158
  # and not the silly one in AR::AttributeMethods::PrimaryKey
134
159
  def id
160
+ return super if is_a?(Shard)
161
+
135
162
  self.class.define_attribute_methods
136
163
  super
137
164
  end
165
+
166
+ # these are called if the specific methods haven't been defined yet
167
+ def attribute(attr_name)
168
+ return super unless self.class.sharded_column?(attr_name)
169
+
170
+ 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)))
172
+ end
173
+
174
+ def attribute=(attr_name, new_value)
175
+ unless self.class.sharded_column?(attr_name)
176
+ super
177
+ return
178
+ end
179
+
180
+ 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))
182
+ end
183
+
184
+ def global_attribute(attr_name)
185
+ if self.class.sharded_column?(attr_name)
186
+ ::Switchman::Shard.global_id_for(attribute(attr_name), shard)
187
+ else
188
+ attribute(attr_name)
189
+ end
190
+ end
191
+
192
+ def local_attribute(attr_name)
193
+ if self.class.sharded_column?(attr_name)
194
+ ::Switchman::Shard.local_id_for(attribute(attr_name), shard).first
195
+ else
196
+ attribute(attr_name)
197
+ end
198
+ end
199
+
200
+ private
201
+
202
+ def connection_classes_for_reflection(reflection)
203
+ if reflection
204
+ if reflection.options[:polymorphic]
205
+ read_attribute(reflection.foreign_type)&.constantize&.connection_classes
206
+ else
207
+ reflection.klass.connection_classes
208
+ end
209
+ else
210
+ self.class.connection_classes
211
+ end
212
+ end
138
213
  end
139
214
  end
140
215
  end