switchman 1.6.1 → 2.0.9
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.
- checksums.yaml +5 -5
- data/app/models/switchman/shard.rb +746 -11
- data/db/migrate/20130328212039_create_switchman_shards.rb +3 -1
- data/db/migrate/20130328224244_create_default_shard.rb +4 -2
- data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +13 -0
- data/db/migrate/20180828183945_add_default_shard_index.rb +15 -0
- data/db/migrate/20180828192111_add_timestamps_to_shards.rb +17 -0
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +9 -0
- data/lib/switchman/action_controller/caching.rb +2 -0
- data/lib/switchman/active_record/abstract_adapter.rb +14 -4
- data/lib/switchman/active_record/association.rb +64 -37
- data/lib/switchman/active_record/attribute_methods.rb +16 -5
- data/lib/switchman/active_record/base.rb +67 -22
- data/lib/switchman/active_record/batches.rb +3 -1
- data/lib/switchman/active_record/calculations.rb +16 -21
- data/lib/switchman/active_record/connection_handler.rb +71 -79
- data/lib/switchman/active_record/connection_pool.rb +28 -23
- data/lib/switchman/active_record/finder_methods.rb +20 -29
- data/lib/switchman/active_record/log_subscriber.rb +14 -19
- data/lib/switchman/active_record/migration.rb +80 -0
- data/lib/switchman/active_record/model_schema.rb +3 -1
- data/lib/switchman/active_record/persistence.rb +9 -1
- data/lib/switchman/active_record/postgresql_adapter.rb +166 -126
- data/lib/switchman/active_record/predicate_builder.rb +2 -0
- data/lib/switchman/active_record/query_cache.rb +22 -87
- data/lib/switchman/active_record/query_methods.rb +122 -126
- data/lib/switchman/active_record/reflection.rb +37 -20
- data/lib/switchman/active_record/relation.rb +50 -29
- data/lib/switchman/active_record/spawn_methods.rb +2 -0
- data/lib/switchman/active_record/statement_cache.rb +44 -52
- data/lib/switchman/active_record/table_definition.rb +4 -2
- data/lib/switchman/active_record/type_caster.rb +2 -0
- data/lib/switchman/active_record/where_clause_factory.rb +5 -2
- data/lib/switchman/active_support/cache.rb +18 -0
- data/lib/switchman/arel.rb +8 -25
- data/lib/switchman/call_super.rb +19 -0
- data/lib/switchman/connection_pool_proxy.rb +70 -24
- data/lib/switchman/database_server.rb +69 -59
- data/lib/switchman/default_shard.rb +3 -0
- data/lib/switchman/engine.rb +42 -40
- data/lib/switchman/environment.rb +2 -0
- data/lib/switchman/errors.rb +2 -0
- data/lib/switchman/{shackles → guard_rail}/relation.rb +7 -5
- data/lib/switchman/{shackles.rb → guard_rail.rb} +6 -4
- data/lib/switchman/open4.rb +2 -0
- data/lib/switchman/r_spec_helper.rb +14 -8
- data/lib/switchman/rails.rb +2 -0
- data/lib/switchman/schema_cache.rb +17 -0
- data/lib/switchman/sharded_instrumenter.rb +4 -2
- data/lib/switchman/standard_error.rb +4 -2
- data/lib/switchman/test_helper.rb +6 -3
- data/lib/switchman/version.rb +3 -1
- data/lib/switchman.rb +3 -1
- data/lib/tasks/switchman.rake +46 -72
- metadata +87 -41
- data/app/models/switchman/shard_internal.rb +0 -692
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateDefaultShard < ActiveRecord::Migration[4.2]
|
|
2
4
|
def up
|
|
3
5
|
unless Switchman::Shard.default.is_a?(Switchman::Shard)
|
|
4
6
|
Switchman::Shard.reset_column_information
|
|
5
7
|
Switchman::Shard.create!(:default => true)
|
|
6
|
-
Switchman::Shard.default(true)
|
|
8
|
+
Switchman::Shard.default(reload: true)
|
|
7
9
|
end
|
|
8
10
|
end
|
|
9
11
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddBackDefaultStringLimitsSwitchman < ActiveRecord::Migration[4.2]
|
|
4
|
+
def up
|
|
5
|
+
add_string_limit_if_missing :switchman_shards, :name
|
|
6
|
+
add_string_limit_if_missing :switchman_shards, :database_server_id
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def add_string_limit_if_missing(table, column)
|
|
10
|
+
return if column_exists?(table, column, :string, limit: 255)
|
|
11
|
+
change_column table, column, :string, limit: 255
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
|
|
4
|
+
def change
|
|
5
|
+
Switchman::Shard.where(default: nil).update_all(default: false)
|
|
6
|
+
change_column_default :switchman_shards, :default, false
|
|
7
|
+
change_column_null :switchman_shards, :default, false
|
|
8
|
+
options = if connection.adapter_name == 'PostgreSQL'
|
|
9
|
+
{ unique: true, where: "\"default\"" }
|
|
10
|
+
else
|
|
11
|
+
{}
|
|
12
|
+
end
|
|
13
|
+
add_index :switchman_shards, :default, **options
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddTimestampsToShards < ActiveRecord::Migration[4.2]
|
|
4
|
+
def change
|
|
5
|
+
add_timestamps :switchman_shards, null: true
|
|
6
|
+
now = Time.now.utc
|
|
7
|
+
Switchman::Shard.update_all(updated_at: now, created_at: now) if Switchman::Shard.current.default?
|
|
8
|
+
change_column_null :switchman_shards, :updated_at, false
|
|
9
|
+
change_column_null :switchman_shards, :created_at, false
|
|
10
|
+
|
|
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
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddUniqueNameIndexes < ActiveRecord::Migration[4.2]
|
|
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'
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'switchman/sharded_instrumenter'
|
|
2
4
|
|
|
3
5
|
module Switchman
|
|
4
6
|
module ActiveRecord
|
|
5
7
|
module AbstractAdapter
|
|
6
8
|
module ForeignKeyCheck
|
|
7
|
-
def add_column(table, name, type,
|
|
8
|
-
Engine.foreign_key_check(name, type,
|
|
9
|
+
def add_column(table, name, type, limit: nil, **)
|
|
10
|
+
Engine.foreign_key_check(name, type, limit: limit)
|
|
9
11
|
super
|
|
10
12
|
end
|
|
11
13
|
end
|
|
@@ -27,8 +29,8 @@ module Switchman
|
|
|
27
29
|
quote_table_name(name)
|
|
28
30
|
end
|
|
29
31
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
+
def schema_migration
|
|
33
|
+
::ActiveRecord::SchemaMigration
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
protected
|
|
@@ -38,6 +40,14 @@ module Switchman
|
|
|
38
40
|
ensure
|
|
39
41
|
@last_query_at = Time.now
|
|
40
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
|
|
41
51
|
end
|
|
42
52
|
end
|
|
43
53
|
end
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module Association
|
|
4
6
|
def shard
|
|
5
|
-
|
|
6
|
-
# polymorphic associations assume the same shard as the owning item
|
|
7
|
-
@owner.shard
|
|
8
|
-
else
|
|
9
|
-
Shard.default
|
|
10
|
-
end
|
|
7
|
+
reflection.shard(owner)
|
|
11
8
|
end
|
|
12
9
|
|
|
13
10
|
def build_record(*args)
|
|
@@ -18,12 +15,6 @@ module Switchman
|
|
|
18
15
|
self.shard.activate { super }
|
|
19
16
|
end
|
|
20
17
|
|
|
21
|
-
def association_scope
|
|
22
|
-
if klass
|
|
23
|
-
shard.activate(klass.shard_category) { super }
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
18
|
def scope
|
|
28
19
|
shard_value = @reflection.options[:multishard] ? @owner : self.shard
|
|
29
20
|
@owner.shard.activate { super.shard(shard_value, :association) }
|
|
@@ -41,9 +32,11 @@ module Switchman
|
|
|
41
32
|
end
|
|
42
33
|
|
|
43
34
|
module CollectionAssociation
|
|
44
|
-
def
|
|
45
|
-
shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [
|
|
46
|
-
|
|
35
|
+
def find_target
|
|
36
|
+
shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [shard]
|
|
37
|
+
# activate both the owner and the target's shard category, so that Reflection#join_id_for,
|
|
38
|
+
# 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
|
|
47
40
|
super
|
|
48
41
|
end
|
|
49
42
|
end
|
|
@@ -69,30 +62,49 @@ module Switchman
|
|
|
69
62
|
end
|
|
70
63
|
end
|
|
71
64
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def self.build(_model, _reflection)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def self.valid_options
|
|
78
|
-
[:multishard]
|
|
79
|
-
end
|
|
65
|
+
module Extension
|
|
66
|
+
def self.build(_model, _reflection)
|
|
80
67
|
end
|
|
81
68
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
module Builder
|
|
85
|
-
module CollectionAssociation
|
|
86
|
-
def valid_options
|
|
87
|
-
super + [:multishard]
|
|
88
|
-
end
|
|
89
|
-
end
|
|
69
|
+
def self.valid_options
|
|
70
|
+
[:multishard]
|
|
90
71
|
end
|
|
91
72
|
end
|
|
92
73
|
|
|
74
|
+
::ActiveRecord::Associations::Builder::Association.extensions << Extension
|
|
75
|
+
|
|
93
76
|
module Preloader
|
|
94
77
|
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
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
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 record.class.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
|
+
|
|
95
106
|
def associated_records_by_owner(preloader = nil)
|
|
107
|
+
return @associated_records_by_owner if defined?(@associated_records_by_owner)
|
|
96
108
|
owners_map = owners_by_key
|
|
97
109
|
|
|
98
110
|
if klass.nil? || owners_map.empty?
|
|
@@ -140,12 +152,13 @@ module Switchman
|
|
|
140
152
|
records.flatten!
|
|
141
153
|
end
|
|
142
154
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
146
159
|
|
|
147
160
|
# Each record may have multiple owners, and vice-versa
|
|
148
|
-
|
|
161
|
+
@associated_records_by_owner = owners.each_with_object({}) do |owner,h|
|
|
149
162
|
h[owner] = []
|
|
150
163
|
end
|
|
151
164
|
records.each do |record|
|
|
@@ -153,10 +166,11 @@ module Switchman
|
|
|
153
166
|
owner_key = Shard.global_id_for(owner_key, record.shard) if owner_key && record.class.sharded_column?(association_key_name)
|
|
154
167
|
|
|
155
168
|
owners_map[owner_key.to_s].each do |owner|
|
|
156
|
-
|
|
169
|
+
owner.association(reflection.name).set_inverse_instance(record)
|
|
170
|
+
@associated_records_by_owner[owner] << record
|
|
157
171
|
end
|
|
158
172
|
end
|
|
159
|
-
|
|
173
|
+
@associated_records_by_owner
|
|
160
174
|
end
|
|
161
175
|
|
|
162
176
|
def owners_by_key
|
|
@@ -174,6 +188,12 @@ module Switchman
|
|
|
174
188
|
end
|
|
175
189
|
|
|
176
190
|
module CollectionProxy
|
|
191
|
+
def initialize(*args)
|
|
192
|
+
super
|
|
193
|
+
self.shard_value = scope.shard_value
|
|
194
|
+
self.shard_source_value = :association
|
|
195
|
+
end
|
|
196
|
+
|
|
177
197
|
def shard(*args)
|
|
178
198
|
scope.shard(*args)
|
|
179
199
|
end
|
|
@@ -185,6 +205,13 @@ module Switchman
|
|
|
185
205
|
(record.has_attribute?(reflection.foreign_key) && record.send(reflection.foreign_key) != key) || # have to use send instead of [] because sharding
|
|
186
206
|
record.attribute_changed?(reflection.foreign_key)
|
|
187
207
|
end
|
|
208
|
+
|
|
209
|
+
def save_belongs_to_association(reflection)
|
|
210
|
+
# this seems counter-intuitive, but the autosave code will assign to attribute bypassing switchman,
|
|
211
|
+
# after reading the id attribute _without_ bypassing switchman. So we need Shard.current for the
|
|
212
|
+
# category of the associated record to match Shard.current for the category of self
|
|
213
|
+
shard.activate(shard_category_for_reflection(reflection)) { super }
|
|
214
|
+
end
|
|
188
215
|
end
|
|
189
216
|
end
|
|
190
217
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module AttributeMethods
|
|
@@ -27,7 +29,7 @@ module Switchman
|
|
|
27
29
|
def reflection_for_integer_attribute(attr_name)
|
|
28
30
|
attr_name = attr_name.to_s
|
|
29
31
|
columns_hash[attr_name] && columns_hash[attr_name].type == :integer &&
|
|
30
|
-
reflections.find { |_, r| r.belongs_to? && r.foreign_key.to_s == attr_name }
|
|
32
|
+
reflections.find { |_, r| r.belongs_to? && r.foreign_key.to_s == attr_name }&.last
|
|
31
33
|
rescue ::ActiveRecord::StatementInvalid
|
|
32
34
|
# this is for when models are referenced in initializers before migrations have been run
|
|
33
35
|
raise if connection.open_transactions > 0
|
|
@@ -61,12 +63,15 @@ module Switchman
|
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
# see also Base#shard_category_for_reflection
|
|
67
|
+
# the difference being this will output static strings for the common cases, making them
|
|
68
|
+
# more performant
|
|
64
69
|
def shard_category_code_for_reflection(reflection)
|
|
65
70
|
if reflection
|
|
66
71
|
if reflection.options[:polymorphic]
|
|
67
72
|
# a polymorphic association has to be discovered at runtime. This code ends up being something like
|
|
68
|
-
# context_type
|
|
69
|
-
"read_attribute(:#{reflection.foreign_type})
|
|
73
|
+
# context_type.&.constantize&.shard_category || :primary
|
|
74
|
+
"read_attribute(:#{reflection.foreign_type})&.constantize&.shard_category || :primary"
|
|
70
75
|
else
|
|
71
76
|
# otherwise we can just return a symbol for the statically known type of the association
|
|
72
77
|
reflection.klass.shard_category.inspect
|
|
@@ -79,7 +84,13 @@ module Switchman
|
|
|
79
84
|
def define_method_original_attribute(attr_name)
|
|
80
85
|
if sharded_column?(attr_name)
|
|
81
86
|
reflection = reflection_for_integer_attribute(attr_name)
|
|
82
|
-
|
|
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
|
|
92
|
+
end
|
|
93
|
+
owner.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
83
94
|
# rename the original method to original_
|
|
84
95
|
alias_method 'original_#{attr_name}', '#{attr_name}'
|
|
85
96
|
# and replace with one that transposes the id
|
|
@@ -117,7 +128,7 @@ module Switchman
|
|
|
117
128
|
klass.extend(ClassMethods)
|
|
118
129
|
klass.attribute_method_prefix "global_", "local_", "original_"
|
|
119
130
|
end
|
|
120
|
-
|
|
131
|
+
|
|
121
132
|
# ensure that we're using the sharded attribute method
|
|
122
133
|
# and not the silly one in AR::AttributeMethods::PrimaryKey
|
|
123
134
|
def id
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module Base
|
|
4
6
|
module ClassMethods
|
|
5
7
|
delegate :shard, to: :all
|
|
6
8
|
|
|
9
|
+
def find_ids_in_ranges(opts={}, &block)
|
|
10
|
+
opts.reverse_merge!(:loose => true)
|
|
11
|
+
all.find_ids_in_ranges(opts, &block)
|
|
12
|
+
end
|
|
13
|
+
|
|
7
14
|
def shard_category
|
|
8
|
-
|
|
15
|
+
connection_specification_name.to_sym
|
|
9
16
|
end
|
|
10
17
|
|
|
11
18
|
def shard_category=(category)
|
|
@@ -14,34 +21,32 @@ module Switchman
|
|
|
14
21
|
categories[shard_category].delete(self)
|
|
15
22
|
categories.delete(shard_category) if categories[shard_category].empty?
|
|
16
23
|
end
|
|
17
|
-
connection_handler.uninitialize_ar(self)
|
|
18
24
|
categories[category] ||= []
|
|
19
25
|
categories[category] << self
|
|
20
|
-
|
|
21
|
-
connection_handler.initialize_categories(superclass)
|
|
26
|
+
self.connection_specification_name = category.to_s
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
def integral_id?
|
|
25
30
|
if @integral_id == nil
|
|
26
|
-
@integral_id = columns_hash[primary_key]
|
|
31
|
+
@integral_id = columns_hash[primary_key]&.type == :integer
|
|
27
32
|
end
|
|
28
33
|
@integral_id
|
|
29
34
|
end
|
|
30
35
|
|
|
31
|
-
def transaction(
|
|
36
|
+
def transaction(**)
|
|
32
37
|
if self != ::ActiveRecord::Base && current_scope
|
|
33
38
|
current_scope.activate do
|
|
34
39
|
db = Shard.current(shard_category).database_server
|
|
35
|
-
if ::
|
|
36
|
-
db.
|
|
40
|
+
if ::GuardRail.environment != db.guard_rail_environment
|
|
41
|
+
db.unguard { super }
|
|
37
42
|
else
|
|
38
43
|
super
|
|
39
44
|
end
|
|
40
45
|
end
|
|
41
46
|
else
|
|
42
47
|
db = Shard.current(shard_category).database_server
|
|
43
|
-
if ::
|
|
44
|
-
db.
|
|
48
|
+
if ::GuardRail.environment != db.guard_rail_environment
|
|
49
|
+
db.unguard { super }
|
|
45
50
|
else
|
|
46
51
|
super
|
|
47
52
|
end
|
|
@@ -65,6 +70,14 @@ module Switchman
|
|
|
65
70
|
result
|
|
66
71
|
end
|
|
67
72
|
end
|
|
73
|
+
|
|
74
|
+
def clear_query_caches_for_current_thread
|
|
75
|
+
::ActiveRecord::Base.connection_handlers.each_value do |handler|
|
|
76
|
+
handler.connection_pool_list.each do |pool|
|
|
77
|
+
pool.connection(switch_shard: false).clear_query_cache if pool.active_connection?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
68
81
|
end
|
|
69
82
|
|
|
70
83
|
def self.included(klass)
|
|
@@ -94,22 +107,18 @@ module Switchman
|
|
|
94
107
|
end
|
|
95
108
|
end
|
|
96
109
|
|
|
97
|
-
def
|
|
98
|
-
self.class.base_class
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def save(*args)
|
|
110
|
+
def save(*, **)
|
|
102
111
|
@shard_set_in_stone = true
|
|
103
|
-
|
|
112
|
+
(self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
|
|
104
113
|
end
|
|
105
114
|
|
|
106
|
-
def save!(
|
|
115
|
+
def save!(*, **)
|
|
107
116
|
@shard_set_in_stone = true
|
|
108
|
-
|
|
117
|
+
(self.class.current_scope || self.class.default_scoped).shard(shard, :implicit).scoping { super }
|
|
109
118
|
end
|
|
110
119
|
|
|
111
120
|
def destroy
|
|
112
|
-
|
|
121
|
+
self.class.shard(shard, :implicit).scoping { super }
|
|
113
122
|
end
|
|
114
123
|
|
|
115
124
|
def clone
|
|
@@ -121,14 +130,14 @@ module Switchman
|
|
|
121
130
|
result
|
|
122
131
|
end
|
|
123
132
|
|
|
124
|
-
def transaction(
|
|
133
|
+
def transaction(**kwargs, &block)
|
|
125
134
|
shard.activate(self.class.shard_category) do
|
|
126
|
-
self.class.transaction(
|
|
135
|
+
self.class.transaction(**kwargs, &block)
|
|
127
136
|
end
|
|
128
137
|
end
|
|
129
138
|
|
|
130
139
|
def hash
|
|
131
|
-
self.class.sharded_primary_key? ? Shard.global_id_for(id).hash : super
|
|
140
|
+
self.class.sharded_primary_key? ? self.class.hash ^ Shard.global_id_for(id).hash : super
|
|
132
141
|
end
|
|
133
142
|
|
|
134
143
|
def to_param
|
|
@@ -141,6 +150,42 @@ module Switchman
|
|
|
141
150
|
@shard_set_in_stone = false
|
|
142
151
|
copy
|
|
143
152
|
end
|
|
153
|
+
|
|
154
|
+
def quoted_id
|
|
155
|
+
return super unless self.class.sharded_primary_key?
|
|
156
|
+
# do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
|
|
157
|
+
self.class.connection.quote(id)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def update_columns(*)
|
|
161
|
+
db = Shard.current(self.class.shard_category).database_server
|
|
162
|
+
if ::GuardRail.environment != db.guard_rail_environment
|
|
163
|
+
return db.unguard { super }
|
|
164
|
+
else
|
|
165
|
+
super
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
protected
|
|
170
|
+
|
|
171
|
+
# see also AttributeMethods#shard_category_code_for_reflection
|
|
172
|
+
def shard_category_for_reflection(reflection)
|
|
173
|
+
if reflection
|
|
174
|
+
if reflection.options[:polymorphic]
|
|
175
|
+
begin
|
|
176
|
+
read_attribute(reflection.foreign_type)&.constantize&.shard_category || :primary
|
|
177
|
+
rescue NameError
|
|
178
|
+
# in case someone is abusing foreign_type to not point to an actual class
|
|
179
|
+
:primary
|
|
180
|
+
end
|
|
181
|
+
else
|
|
182
|
+
# otherwise we can just return a symbol for the statically known type of the association
|
|
183
|
+
reflection.klass.shard_category
|
|
184
|
+
end
|
|
185
|
+
else
|
|
186
|
+
shard_category
|
|
187
|
+
end
|
|
188
|
+
end
|
|
144
189
|
end
|
|
145
190
|
end
|
|
146
191
|
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module Batches
|
|
4
6
|
def batch_order
|
|
5
|
-
"#{connection.quote_local_table_name(table_name)}.#{quoted_primary_key} ASC"
|
|
7
|
+
::Arel.sql("#{connection.quote_local_table_name(table_name)}.#{quoted_primary_key} ASC")
|
|
6
8
|
end
|
|
7
9
|
end
|
|
8
10
|
end
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Switchman
|
|
2
4
|
module ActiveRecord
|
|
3
5
|
module Calculations
|
|
4
|
-
CALL_SUPER = Object.new.freeze
|
|
5
|
-
private_constant :CALL_SUPER
|
|
6
6
|
|
|
7
7
|
def pluck(*column_names)
|
|
8
|
-
return super(*column_names[1..-1]) if column_names.first.equal?(CALL_SUPER)
|
|
9
8
|
target_shard = Shard.current(klass.shard_category)
|
|
10
9
|
shard_count = 0
|
|
11
10
|
result = self.activate do |relation, shard|
|
|
12
11
|
shard_count += 1
|
|
13
|
-
results = relation.pluck
|
|
12
|
+
results = relation.call_super(:pluck, Calculations, *column_names)
|
|
14
13
|
if column_names.length > 1
|
|
15
14
|
column_names.each_with_index do |column_name, idx|
|
|
16
15
|
if klass.sharded_column?(column_name)
|
|
@@ -30,13 +29,12 @@ module Switchman
|
|
|
30
29
|
result
|
|
31
30
|
end
|
|
32
31
|
|
|
33
|
-
def execute_simple_calculation(operation, column_name, distinct
|
|
34
|
-
return super(operation, column_name, distinct) if super_method
|
|
32
|
+
def execute_simple_calculation(operation, column_name, distinct)
|
|
35
33
|
operation = operation.to_s.downcase
|
|
36
34
|
if operation == "average"
|
|
37
35
|
result = calculate_simple_average(column_name, distinct)
|
|
38
36
|
else
|
|
39
|
-
result = self.activate{ |relation| relation.
|
|
37
|
+
result = self.activate{ |relation| relation.call_super(:execute_simple_calculation, Calculations, operation, column_name, distinct) }
|
|
40
38
|
if result.is_a?(Array)
|
|
41
39
|
case operation
|
|
42
40
|
when "count", "sum"
|
|
@@ -61,12 +59,12 @@ module Switchman
|
|
|
61
59
|
initial_results = relation.activate{ |rel| klass.connection.select_all(rel) }
|
|
62
60
|
if initial_results.is_a?(Array)
|
|
63
61
|
initial_results.each do |r|
|
|
64
|
-
r["average"] = type_cast_calculated_value(r["average"],
|
|
65
|
-
r["count"] = type_cast_calculated_value(r["count"],
|
|
62
|
+
r["average"] = type_cast_calculated_value(r["average"], type_for(column_name), "average")
|
|
63
|
+
r["count"] = type_cast_calculated_value(r["count"], type_for(column_name), "count")
|
|
66
64
|
end
|
|
67
65
|
result = initial_results.map{|r| r["average"] * r["count"]}.sum / initial_results.map{|r| r["count"]}.sum
|
|
68
66
|
else
|
|
69
|
-
result = type_cast_calculated_value(initial_results.first["average"],
|
|
67
|
+
result = type_cast_calculated_value(initial_results.first["average"], type_for(column_name), "average")
|
|
70
68
|
end
|
|
71
69
|
result
|
|
72
70
|
end
|
|
@@ -76,7 +74,7 @@ module Switchman
|
|
|
76
74
|
opts = grouped_calculation_options(operation.to_s.downcase, column_name, distinct)
|
|
77
75
|
|
|
78
76
|
relation = build_grouped_calculation_relation(opts)
|
|
79
|
-
target_shard = Shard.current(:
|
|
77
|
+
target_shard = Shard.current(:primary)
|
|
80
78
|
|
|
81
79
|
rows = relation.activate do |rel, shard|
|
|
82
80
|
calculated_data = klass.connection.select_all(rel)
|
|
@@ -89,14 +87,14 @@ module Switchman
|
|
|
89
87
|
|
|
90
88
|
calculated_data.map do |row|
|
|
91
89
|
row[opts[:aggregate_alias]] = type_cast_calculated_value(
|
|
92
|
-
row[opts[:aggregate_alias]],
|
|
90
|
+
row[opts[:aggregate_alias]], type_for(opts[:column_name]), opts[:operation])
|
|
93
91
|
row['count'] = row['count'].to_i if opts[:operation] == 'average'
|
|
94
92
|
|
|
95
|
-
opts[:group_columns].each do |aliaz,
|
|
93
|
+
opts[:group_columns].each do |aliaz, type, column_name|
|
|
96
94
|
if opts[:associated] && (aliaz == opts[:group_aliases].first)
|
|
97
95
|
row[aliaz] = key_records[Shard.relative_id_for(row[aliaz], shard, target_shard)]
|
|
98
96
|
elsif column_name && @klass.sharded_column?(column_name)
|
|
99
|
-
row[aliaz] = Shard.relative_id_for(type_cast_calculated_value(row[aliaz],
|
|
97
|
+
row[aliaz] = Shard.relative_id_for(type_cast_calculated_value(row[aliaz], type), shard, target_shard)
|
|
100
98
|
end
|
|
101
99
|
end
|
|
102
100
|
row
|
|
@@ -112,10 +110,6 @@ module Switchman
|
|
|
112
110
|
field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
|
|
113
111
|
end
|
|
114
112
|
|
|
115
|
-
def column_or_type_for(field)
|
|
116
|
-
::Rails.version < '4.2' ? column_for(field) : type_for(field)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
113
|
def grouped_calculation_options(operation, column_name, distinct)
|
|
120
114
|
opts = {:operation => operation, :column_name => column_name, :distinct => distinct}
|
|
121
115
|
|
|
@@ -129,9 +123,10 @@ module Switchman
|
|
|
129
123
|
group_fields = group_attrs
|
|
130
124
|
end
|
|
131
125
|
|
|
132
|
-
|
|
126
|
+
# to_s is because Rails 5 returns a string but Rails 6 returns a symbol.
|
|
127
|
+
group_aliases = group_fields.map { |field| column_alias_for(field.downcase.to_s).to_s }
|
|
133
128
|
group_columns = group_aliases.zip(group_fields).map { |aliaz, field|
|
|
134
|
-
[aliaz,
|
|
129
|
+
[aliaz, type_for(field), column_name_for(field)]
|
|
135
130
|
}
|
|
136
131
|
opts.merge!(:association => association, :associated => associated,
|
|
137
132
|
:group_aliases => group_aliases, :group_columns => group_columns,
|
|
@@ -167,7 +162,7 @@ module Switchman
|
|
|
167
162
|
'count', opts[:distinct]).as('count')
|
|
168
163
|
end
|
|
169
164
|
|
|
170
|
-
haves =
|
|
165
|
+
haves = having_clause.send(:predicates)
|
|
171
166
|
select_values += select_values unless haves.empty?
|
|
172
167
|
select_values.concat opts[:group_fields].zip(opts[:group_aliases]).map { |field,aliaz|
|
|
173
168
|
if field.respond_to?(:as)
|