switchman 3.0.0 → 3.0.7
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 +4 -4
- data/app/models/switchman/shard.rb +109 -134
- data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
- data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
- data/lib/switchman/active_record/abstract_adapter.rb +0 -9
- data/lib/switchman/active_record/association.rb +1 -1
- data/lib/switchman/active_record/attribute_methods.rb +72 -3
- data/lib/switchman/active_record/base.rb +38 -21
- data/lib/switchman/active_record/calculations.rb +2 -2
- data/lib/switchman/active_record/connection_pool.rb +9 -29
- data/lib/switchman/active_record/migration.rb +18 -3
- data/lib/switchman/active_record/persistence.rb +7 -0
- data/lib/switchman/active_record/postgresql_adapter.rb +27 -111
- data/lib/switchman/active_record/predicate_builder.rb +1 -1
- data/lib/switchman/active_record/query_methods.rb +13 -3
- data/lib/switchman/active_record/relation.rb +57 -6
- data/lib/switchman/active_record/test_fixtures.rb +43 -0
- data/lib/switchman/database_server.rb +46 -49
- data/lib/switchman/engine.rb +5 -1
- data/lib/switchman/r_spec_helper.rb +2 -17
- data/lib/switchman/standard_error.rb +4 -4
- data/lib/switchman/test_helper.rb +1 -1
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +2 -0
- data/lib/tasks/switchman.rake +9 -5
- metadata +9 -8
- data/lib/switchman/schema_cache.rb +0 -20
|
@@ -5,18 +5,6 @@ require 'switchman/errors'
|
|
|
5
5
|
module Switchman
|
|
6
6
|
module ActiveRecord
|
|
7
7
|
module ConnectionPool
|
|
8
|
-
def shard
|
|
9
|
-
shard_stack.last || Shard.default
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def shard_stack
|
|
13
|
-
unless (shard_stack = Thread.current.thread_variable_get(tls_key))
|
|
14
|
-
shard_stack = Concurrent::Array.new
|
|
15
|
-
Thread.current.thread_variable_set(tls_key, shard_stack)
|
|
16
|
-
end
|
|
17
|
-
shard_stack
|
|
18
|
-
end
|
|
19
|
-
|
|
20
8
|
def default_schema
|
|
21
9
|
connection unless @schemas
|
|
22
10
|
# default shard will not switch databases immediately, so it won't be set yet
|
|
@@ -26,15 +14,15 @@ module Switchman
|
|
|
26
14
|
|
|
27
15
|
def checkout_new_connection
|
|
28
16
|
conn = super
|
|
29
|
-
conn.shard =
|
|
17
|
+
conn.shard = current_shard
|
|
30
18
|
conn
|
|
31
19
|
end
|
|
32
20
|
|
|
33
21
|
def connection(switch_shard: true)
|
|
34
22
|
conn = super()
|
|
35
|
-
raise NonExistentShardError if
|
|
23
|
+
raise NonExistentShardError if current_shard.new_record?
|
|
36
24
|
|
|
37
|
-
switch_database(conn) if conn.shard !=
|
|
25
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
38
26
|
conn
|
|
39
27
|
end
|
|
40
28
|
|
|
@@ -44,26 +32,18 @@ module Switchman
|
|
|
44
32
|
flush
|
|
45
33
|
end
|
|
46
34
|
|
|
47
|
-
def remove_shard!(shard)
|
|
48
|
-
synchronize do
|
|
49
|
-
# The shard might be currently active, so we need to update our own shard
|
|
50
|
-
self.shard = Shard.default if self.shard == shard
|
|
51
|
-
# Update out any connections that may be using this shard
|
|
52
|
-
@connections.each do |conn|
|
|
53
|
-
# This will also update the connection's shard to the default shard
|
|
54
|
-
switch_database(conn) if conn.shard == shard
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
35
|
def switch_database(conn)
|
|
60
|
-
@schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !
|
|
36
|
+
@schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !current_shard.database_server.config[:shard_name]
|
|
61
37
|
|
|
62
|
-
conn.shard =
|
|
38
|
+
conn.shard = current_shard
|
|
63
39
|
end
|
|
64
40
|
|
|
65
41
|
private
|
|
66
42
|
|
|
43
|
+
def current_shard
|
|
44
|
+
connection_klass.current_switchman_shard
|
|
45
|
+
end
|
|
46
|
+
|
|
67
47
|
def tls_key
|
|
68
48
|
"#{object_id}_shard".to_sym
|
|
69
49
|
end
|
|
@@ -14,15 +14,30 @@ module Switchman
|
|
|
14
14
|
|
|
15
15
|
def connection
|
|
16
16
|
conn = super
|
|
17
|
-
::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base.
|
|
17
|
+
::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base.current_switchman_shard
|
|
18
18
|
conn
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
module Migrator
|
|
23
|
+
# significant change: just return MIGRATOR_SALT directly
|
|
24
|
+
# especially if you're going through pgbouncer, the database
|
|
25
|
+
# name you're accessing may not be consistent. it is NOT allowed
|
|
26
|
+
# to run migrations against multiple shards in the same database
|
|
27
|
+
# concurrently
|
|
23
28
|
def generate_migrator_advisory_lock_id
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
::ActiveRecord::Migrator::MIGRATOR_SALT
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# significant change: strip out prefer_secondary from config
|
|
33
|
+
def with_advisory_lock_connection
|
|
34
|
+
pool = ::ActiveRecord::ConnectionAdapters::ConnectionHandler.new.establish_connection(
|
|
35
|
+
::ActiveRecord::Base.connection_db_config.configuration_hash.except(:prefer_secondary)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
pool.with_connection { |connection| yield(connection) } # rubocop:disable Style/ExplicitBlockArgument
|
|
39
|
+
ensure
|
|
40
|
+
pool&.disconnect!
|
|
26
41
|
end
|
|
27
42
|
end
|
|
28
43
|
|
|
@@ -11,6 +11,13 @@ module Switchman
|
|
|
11
11
|
def update_columns(*)
|
|
12
12
|
shard.activate(self.class.connection_classes) { super }
|
|
13
13
|
end
|
|
14
|
+
|
|
15
|
+
def delete
|
|
16
|
+
db = shard.database_server
|
|
17
|
+
return db.unguard { super } unless ::GuardRail.environment == db.guard_rail_environment
|
|
18
|
+
|
|
19
|
+
super
|
|
20
|
+
end
|
|
14
21
|
end
|
|
15
22
|
end
|
|
16
23
|
end
|
|
@@ -32,7 +32,7 @@ module Switchman
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# copy/paste; use quote_local_table_name
|
|
35
|
-
def drop_database(name)
|
|
35
|
+
def drop_database(name) # :nodoc:
|
|
36
36
|
execute "DROP DATABASE IF EXISTS #{quote_local_table_name(name)}"
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -40,89 +40,36 @@ module Switchman
|
|
|
40
40
|
select_values('SELECT * FROM unnest(current_schemas(false))')
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
def tables(_name = nil)
|
|
44
|
-
query(<<-SQL, 'SCHEMA').map { |row| row[0] }
|
|
45
|
-
SELECT tablename
|
|
46
|
-
FROM pg_tables
|
|
47
|
-
WHERE schemaname = '#{shard.name}'
|
|
48
|
-
SQL
|
|
49
|
-
end
|
|
50
|
-
|
|
51
43
|
def extract_schema_qualified_name(string)
|
|
52
44
|
name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(string.to_s)
|
|
53
45
|
name.instance_variable_set(:@schema, shard.name) if string && !name.schema
|
|
54
46
|
[name.schema, name.identifier]
|
|
55
47
|
end
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
result = query(<<-SQL, 'SCHEMA')
|
|
75
|
-
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
|
|
76
|
-
FROM pg_class t
|
|
77
|
-
INNER JOIN pg_index d ON t.oid = d.indrelid
|
|
78
|
-
INNER JOIN pg_class i ON d.indexrelid = i.oid
|
|
79
|
-
WHERE i.relkind = 'i'
|
|
80
|
-
AND d.indisprimary = 'f'
|
|
81
|
-
AND t.relname = '#{table_name}'
|
|
82
|
-
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
|
|
83
|
-
ORDER BY i.relname
|
|
84
|
-
SQL
|
|
85
|
-
|
|
86
|
-
result.map do |row|
|
|
87
|
-
index_name = row[0]
|
|
88
|
-
unique = row[1] == true || row[1] == 't'
|
|
89
|
-
indkey = row[2].split
|
|
90
|
-
inddef = row[3]
|
|
91
|
-
oid = row[4]
|
|
92
|
-
|
|
93
|
-
columns = Hash[query(<<-SQL, 'SCHEMA')] # rubocop:disable Style/HashConversion
|
|
94
|
-
SELECT a.attnum, a.attname
|
|
95
|
-
FROM pg_attribute a
|
|
96
|
-
WHERE a.attrelid = #{oid}
|
|
97
|
-
AND a.attnum IN (#{indkey.join(',')})
|
|
98
|
-
SQL
|
|
99
|
-
|
|
100
|
-
column_names = columns.stringify_keys.values_at(*indkey).compact
|
|
101
|
-
|
|
102
|
-
next if column_names.empty?
|
|
103
|
-
|
|
104
|
-
# add info on sort order for columns (only desc order is explicitly specified, asc is the default)
|
|
105
|
-
desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
|
|
106
|
-
orders = desc_order_columns.any? ? Hash[desc_order_columns.map { |order_column| [order_column, :desc] }] : {} # rubocop:disable Style/HashConversion
|
|
107
|
-
where = inddef.scan(/WHERE (.+)$/).flatten[0]
|
|
108
|
-
using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
|
|
109
|
-
|
|
110
|
-
::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names,
|
|
111
|
-
orders: orders, where: where, using: using)
|
|
112
|
-
end.compact
|
|
49
|
+
# significant change: use the shard name if no explicit schema
|
|
50
|
+
def quoted_scope(name = nil, type: nil)
|
|
51
|
+
schema, name = extract_schema_qualified_name(name)
|
|
52
|
+
type = \
|
|
53
|
+
case type # rubocop:disable Style/HashLikeCase
|
|
54
|
+
when 'BASE TABLE'
|
|
55
|
+
"'r','p'"
|
|
56
|
+
when 'VIEW'
|
|
57
|
+
"'v','m'"
|
|
58
|
+
when 'FOREIGN TABLE'
|
|
59
|
+
"'f'"
|
|
60
|
+
end
|
|
61
|
+
scope = {}
|
|
62
|
+
scope[:schema] = quote(schema || shard.name)
|
|
63
|
+
scope[:name] = quote(name) if name
|
|
64
|
+
scope[:type] = type if type
|
|
65
|
+
scope
|
|
113
66
|
end
|
|
114
67
|
|
|
115
|
-
def
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
INNER JOIN pg_class i ON d.indexrelid = i.oid
|
|
121
|
-
WHERE i.relkind = 'i'
|
|
122
|
-
AND i.relname = '#{index_name}'
|
|
123
|
-
AND t.relname = '#{table_name}'
|
|
124
|
-
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
|
|
125
|
-
SQL
|
|
68
|
+
def foreign_keys(table_name)
|
|
69
|
+
super.each do |fk|
|
|
70
|
+
to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
|
|
71
|
+
fk.to_table = to_table_qualified_name.identifier if to_table_qualified_name.schema == shard.name
|
|
72
|
+
end
|
|
126
73
|
end
|
|
127
74
|
|
|
128
75
|
def quote_local_table_name(name)
|
|
@@ -140,6 +87,10 @@ module Switchman
|
|
|
140
87
|
name.quoted
|
|
141
88
|
end
|
|
142
89
|
|
|
90
|
+
def with_global_table_name(&block)
|
|
91
|
+
with_local_table_name(false, &block)
|
|
92
|
+
end
|
|
93
|
+
|
|
143
94
|
def with_local_table_name(enable = true) # rubocop:disable Style/OptionalBooleanParameter
|
|
144
95
|
old_value = @use_local_table_name
|
|
145
96
|
@use_local_table_name = enable
|
|
@@ -148,41 +99,6 @@ module Switchman
|
|
|
148
99
|
@use_local_table_name = old_value
|
|
149
100
|
end
|
|
150
101
|
|
|
151
|
-
def foreign_keys(table_name)
|
|
152
|
-
# mostly copy-pasted from AR - only change is to the nspname condition for qualified names support
|
|
153
|
-
fk_info = select_all <<-SQL.strip_heredoc
|
|
154
|
-
SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
|
|
155
|
-
FROM pg_constraint c
|
|
156
|
-
JOIN pg_class t1 ON c.conrelid = t1.oid
|
|
157
|
-
JOIN pg_class t2 ON c.confrelid = t2.oid
|
|
158
|
-
JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
|
|
159
|
-
JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
|
|
160
|
-
JOIN pg_namespace t3 ON c.connamespace = t3.oid
|
|
161
|
-
WHERE c.contype = 'f'
|
|
162
|
-
AND t1.relname = #{quote(table_name)}
|
|
163
|
-
AND t3.nspname = '#{shard.name}'
|
|
164
|
-
ORDER BY c.conname
|
|
165
|
-
SQL
|
|
166
|
-
|
|
167
|
-
fk_info.map do |row|
|
|
168
|
-
options = {
|
|
169
|
-
column: row['column'],
|
|
170
|
-
name: row['name'],
|
|
171
|
-
primary_key: row['primary_key']
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
options[:on_delete] = extract_foreign_key_action(row['on_delete'])
|
|
175
|
-
options[:on_update] = extract_foreign_key_action(row['on_update'])
|
|
176
|
-
|
|
177
|
-
# strip the schema name from to_table if it matches
|
|
178
|
-
to_table = row['to_table']
|
|
179
|
-
to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(to_table)
|
|
180
|
-
to_table = to_table_qualified_name.identifier if to_table_qualified_name.schema == shard.name
|
|
181
|
-
|
|
182
|
-
::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, to_table, options)
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
102
|
def add_index_options(_table_name, _column_name, **)
|
|
187
103
|
index, algorithm, if_not_exists = super
|
|
188
104
|
algorithm = nil if DatabaseServer.creating_new_shard && algorithm == 'CONCURRENTLY'
|
|
@@ -239,6 +239,10 @@ module Switchman
|
|
|
239
239
|
connection.with_local_table_name { super }
|
|
240
240
|
end
|
|
241
241
|
|
|
242
|
+
def table_name_matches?(from)
|
|
243
|
+
connection.with_global_table_name { super }
|
|
244
|
+
end
|
|
245
|
+
|
|
242
246
|
def transpose_predicates(predicates,
|
|
243
247
|
source_shard,
|
|
244
248
|
target_shard,
|
|
@@ -281,7 +285,7 @@ module Switchman
|
|
|
281
285
|
remove = true if type == :primary &&
|
|
282
286
|
remove_nonlocal_primary_keys &&
|
|
283
287
|
predicate.left.relation.klass == klass &&
|
|
284
|
-
predicate.is_a?(::Arel::Nodes::Equality)
|
|
288
|
+
(predicate.is_a?(::Arel::Nodes::Equality) || predicate.is_a?(::Arel::Nodes::HomogeneousIn))
|
|
285
289
|
|
|
286
290
|
current_source_shard =
|
|
287
291
|
if source_shard
|
|
@@ -301,7 +305,7 @@ module Switchman
|
|
|
301
305
|
new_right_value =
|
|
302
306
|
case right
|
|
303
307
|
when Array
|
|
304
|
-
right.map { |val| transpose_predicate_value(val, current_source_shard, target_shard, type, remove) }
|
|
308
|
+
right.map { |val| transpose_predicate_value(val, current_source_shard, target_shard, type, remove).presence }.compact
|
|
305
309
|
else
|
|
306
310
|
transpose_predicate_value(right, current_source_shard, target_shard, type, remove)
|
|
307
311
|
end
|
|
@@ -315,7 +319,13 @@ module Switchman
|
|
|
315
319
|
predicate.class.new(predicate.left, right.class.new(new_right_value, right.attribute))
|
|
316
320
|
end
|
|
317
321
|
elsif predicate.is_a?(::Arel::Nodes::HomogeneousIn)
|
|
318
|
-
|
|
322
|
+
# switch to a regular In, so that Relation::WhereClause#contradiction? knows about it
|
|
323
|
+
if new_right_value.empty?
|
|
324
|
+
klass = predicate.type == :in ? ::Arel::Nodes::In : ::Arel::Nodes::NotIn
|
|
325
|
+
klass.new(predicate.attribute, new_right_value)
|
|
326
|
+
else
|
|
327
|
+
predicate.class.new(new_right_value, predicate.attribute, predicate.type)
|
|
328
|
+
end
|
|
319
329
|
else
|
|
320
330
|
predicate.class.new(predicate.left, new_right_value)
|
|
321
331
|
end
|
|
@@ -63,7 +63,7 @@ module Switchman
|
|
|
63
63
|
%I[update_all delete_all].each do |method|
|
|
64
64
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
65
65
|
def #{method}(*args)
|
|
66
|
-
result = self.activate { |relation| relation.call_super(#{method.inspect}, Relation, *args) }
|
|
66
|
+
result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation, *args) }
|
|
67
67
|
result = result.sum if result.is_a?(Array)
|
|
68
68
|
result
|
|
69
69
|
end
|
|
@@ -94,7 +94,7 @@ module Switchman
|
|
|
94
94
|
|
|
95
95
|
while ids.first.present?
|
|
96
96
|
ids.map!(&:to_i) if is_integer
|
|
97
|
-
ids << ids.first + batch_size if loose_mode
|
|
97
|
+
ids << (ids.first + batch_size) if loose_mode
|
|
98
98
|
|
|
99
99
|
yield(*ids)
|
|
100
100
|
last_value = ids.last
|
|
@@ -103,7 +103,7 @@ module Switchman
|
|
|
103
103
|
end
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
def activate(&block)
|
|
106
|
+
def activate(unordered: false, &block)
|
|
107
107
|
shards = all_shards
|
|
108
108
|
if Array === shards && shards.length == 1
|
|
109
109
|
if shards.first == DefaultShard || shards.first == Shard.current(klass.connection_classes)
|
|
@@ -112,10 +112,61 @@ module Switchman
|
|
|
112
112
|
shards.first.activate(klass.connection_classes) { yield(self, shards.first) }
|
|
113
113
|
end
|
|
114
114
|
else
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
result_count = 0
|
|
116
|
+
can_order = false
|
|
117
|
+
result = Shard.with_each_shard(shards, [klass.connection_classes]) do
|
|
118
|
+
# don't even query other shards if we're already past the limit
|
|
119
|
+
next if limit_value && result_count >= limit_value && order_values.empty?
|
|
120
|
+
|
|
121
|
+
relation = shard(Shard.current(klass.connection_classes), :to_a)
|
|
122
|
+
# do a minimal query if possible
|
|
123
|
+
relation = relation.limit(limit_value - result_count) if limit_value && !result_count.zero? && order_values.empty?
|
|
124
|
+
|
|
125
|
+
shard_results = relation.activate(&block)
|
|
126
|
+
|
|
127
|
+
if shard_results.present? && !unordered
|
|
128
|
+
can_order ||= can_order_cross_shard_results? unless order_values.empty?
|
|
129
|
+
raise OrderOnMultiShardQuery if !can_order && !order_values.empty? && result_count.positive?
|
|
130
|
+
|
|
131
|
+
result_count += shard_results.is_a?(Array) ? shard_results.length : 1
|
|
132
|
+
end
|
|
133
|
+
shard_results
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
result = reorder_cross_shard_results(result) if can_order
|
|
137
|
+
result.slice!(limit_value..-1) if limit_value
|
|
138
|
+
result
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def can_order_cross_shard_results?
|
|
143
|
+
# we only presume to be able to post-sort the most basic of orderings
|
|
144
|
+
order_values.all? { |ov| ov.is_a?(::Arel::Nodes::Ordering) && ov.expr.is_a?(::Arel::Attributes::Attribute) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def reorder_cross_shard_results(results)
|
|
148
|
+
results.sort! do |l, r|
|
|
149
|
+
result = 0
|
|
150
|
+
order_values.each do |ov|
|
|
151
|
+
if l.is_a?(::ActiveRecord::Base)
|
|
152
|
+
a = l.attribute(ov.expr.name)
|
|
153
|
+
b = r.attribute(ov.expr.name)
|
|
154
|
+
else
|
|
155
|
+
a, b = l, r
|
|
156
|
+
end
|
|
157
|
+
next if a == b
|
|
158
|
+
|
|
159
|
+
if a.nil? || b.nil?
|
|
160
|
+
result = 1 if a.nil?
|
|
161
|
+
result *= -1 if ov.is_a?(::Arel::Nodes::Descending)
|
|
162
|
+
else
|
|
163
|
+
result = a <=> b
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
result *= -1 if ov.is_a?(::Arel::Nodes::Descending)
|
|
167
|
+
break unless result.zero?
|
|
118
168
|
end
|
|
169
|
+
result
|
|
119
170
|
end
|
|
120
171
|
end
|
|
121
172
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Switchman
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
module TestFixtures
|
|
6
|
+
FORBIDDEN_DB_ENVS = %i[development production].freeze
|
|
7
|
+
def setup_fixtures(config = ::ActiveRecord::Base)
|
|
8
|
+
super
|
|
9
|
+
|
|
10
|
+
return unless run_in_transaction?
|
|
11
|
+
|
|
12
|
+
# Replace the one that activerecord natively uses with a switchman-optimized one
|
|
13
|
+
::ActiveSupport::Notifications.unsubscribe(@connection_subscriber)
|
|
14
|
+
# Code adapted from the code in rails proper
|
|
15
|
+
@connection_subscriber = ::ActiveSupport::Notifications.subscribe('!connection.active_record') do |_, _, _, _, payload|
|
|
16
|
+
spec_name = payload[:spec_name] if payload.key?(:spec_name)
|
|
17
|
+
shard = payload[:shard] if payload.key?(:shard)
|
|
18
|
+
setup_shared_connection_pool
|
|
19
|
+
|
|
20
|
+
if spec_name && !FORBIDDEN_DB_ENVS.include?(shard)
|
|
21
|
+
begin
|
|
22
|
+
connection = ::ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
|
|
23
|
+
rescue ::ActiveRecord::ConnectionNotEstablished, ::ActiveRecord::NoDatabaseError
|
|
24
|
+
connection = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if connection && !@fixture_connections.include?(connection)
|
|
28
|
+
connection.begin_transaction joinable: false, _lazy: false
|
|
29
|
+
connection.pool.lock_thread = true if lock_threads
|
|
30
|
+
@fixture_connections << connection
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def enlist_fixture_connections
|
|
37
|
+
setup_shared_connection_pool
|
|
38
|
+
|
|
39
|
+
::ActiveRecord::Base.connection_handler.connection_pool_list.reject { |cp| FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym) }.map(&:connection)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -59,7 +59,7 @@ module Switchman
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def database_servers
|
|
62
|
-
|
|
62
|
+
if !@database_servers || @database_servers.empty?
|
|
63
63
|
@database_servers = {}.with_indifferent_access
|
|
64
64
|
::ActiveRecord::Base.configurations.configurations.each do |config|
|
|
65
65
|
if config.name.include?('/')
|
|
@@ -89,13 +89,13 @@ module Switchman
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
def connects_to_hash
|
|
92
|
-
self.class.all_roles.
|
|
92
|
+
self.class.all_roles.to_h do |role|
|
|
93
93
|
config_role = role
|
|
94
94
|
config_role = :primary unless roles.include?(role)
|
|
95
95
|
config_name = :"#{id}/#{config_role}"
|
|
96
96
|
config_name = :primary if id == ::Rails.env && config_role == :primary
|
|
97
97
|
[role.to_sym, config_name]
|
|
98
|
-
end
|
|
98
|
+
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
def destroy
|
|
@@ -113,7 +113,7 @@ module Switchman
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def config(environment = :primary)
|
|
116
|
-
@configs[environment] ||=
|
|
116
|
+
@configs[environment] ||=
|
|
117
117
|
case @config[environment]
|
|
118
118
|
when Array
|
|
119
119
|
@config[environment].map do |config|
|
|
@@ -127,7 +127,6 @@ module Switchman
|
|
|
127
127
|
else
|
|
128
128
|
@config
|
|
129
129
|
end
|
|
130
|
-
end
|
|
131
130
|
end
|
|
132
131
|
|
|
133
132
|
def guard_rail_environment
|
|
@@ -187,58 +186,56 @@ module Switchman
|
|
|
187
186
|
|
|
188
187
|
name ||= "#{config[:database]}_shard_#{id}"
|
|
189
188
|
|
|
189
|
+
schema_already_existed = false
|
|
190
|
+
shard = nil
|
|
190
191
|
Shard.connection.transaction do
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if ::ActiveRecord::Base.connection.select_value("SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}")
|
|
202
|
-
schema_already_existed = true
|
|
203
|
-
raise 'This schema already exists; cannot overwrite'
|
|
204
|
-
end
|
|
205
|
-
Array(create_statement.call).each do |stmt|
|
|
206
|
-
::ActiveRecord::Base.connection.execute(stmt)
|
|
207
|
-
end
|
|
192
|
+
self.class.creating_new_shard = true
|
|
193
|
+
DatabaseServer.send(:reference_role, :deploy)
|
|
194
|
+
::ActiveRecord::Base.connected_to(shard: self.id.to_sym, role: :deploy) do
|
|
195
|
+
shard = Shard.create!(id: id,
|
|
196
|
+
name: name,
|
|
197
|
+
database_server_id: self.id)
|
|
198
|
+
if create_statement
|
|
199
|
+
if ::ActiveRecord::Base.connection.select_value("SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}")
|
|
200
|
+
schema_already_existed = true
|
|
201
|
+
raise 'This schema already exists; cannot overwrite'
|
|
208
202
|
end
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
end
|
|
203
|
+
Array(create_statement.call).each do |stmt|
|
|
204
|
+
::ActiveRecord::Base.connection.execute(stmt)
|
|
212
205
|
end
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
206
|
+
end
|
|
207
|
+
if config[:adapter] == 'postgresql'
|
|
208
|
+
old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor do
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
old_verbose = ::ActiveRecord::Migration.verbose
|
|
212
|
+
::ActiveRecord::Migration.verbose = false
|
|
213
|
+
|
|
214
|
+
unless schema == false
|
|
215
|
+
shard.activate do
|
|
216
|
+
reset_column_information
|
|
217
|
+
|
|
218
|
+
::ActiveRecord::Base.connection.transaction(requires_new: true) do
|
|
219
|
+
::ActiveRecord::Base.connection.migration_context.migrate
|
|
227
220
|
end
|
|
221
|
+
reset_column_information
|
|
222
|
+
::ActiveRecord::Base.descendants.reject do |m|
|
|
223
|
+
m <= UnshardedRecord || !m.table_exists?
|
|
224
|
+
end.each(&:define_attribute_methods)
|
|
228
225
|
end
|
|
229
|
-
ensure
|
|
230
|
-
::ActiveRecord::Migration.verbose = old_verbose
|
|
231
|
-
::ActiveRecord::Base.connection.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
232
226
|
end
|
|
233
|
-
shard
|
|
234
|
-
rescue
|
|
235
|
-
shard.destroy
|
|
236
|
-
shard.drop_database rescue nil unless schema_already_existed
|
|
237
|
-
reset_column_information unless schema == false rescue nil
|
|
238
|
-
raise
|
|
239
227
|
ensure
|
|
240
|
-
|
|
228
|
+
::ActiveRecord::Migration.verbose = old_verbose
|
|
229
|
+
::ActiveRecord::Base.connection.raw_connection.set_notice_processor(&old_proc) if old_proc
|
|
241
230
|
end
|
|
231
|
+
shard
|
|
232
|
+
rescue
|
|
233
|
+
shard&.destroy
|
|
234
|
+
shard&.drop_database rescue nil unless schema_already_existed
|
|
235
|
+
reset_column_information unless schema == false rescue nil
|
|
236
|
+
raise
|
|
237
|
+
ensure
|
|
238
|
+
self.class.creating_new_shard = false
|
|
242
239
|
end
|
|
243
240
|
end
|
|
244
241
|
|
data/lib/switchman/engine.rb
CHANGED
|
@@ -90,6 +90,7 @@ module Switchman
|
|
|
90
90
|
require 'switchman/active_record/statement_cache'
|
|
91
91
|
require 'switchman/active_record/tasks/database_tasks'
|
|
92
92
|
require 'switchman/active_record/type_caster'
|
|
93
|
+
require 'switchman/active_record/test_fixtures'
|
|
93
94
|
require 'switchman/arel'
|
|
94
95
|
require 'switchman/call_super'
|
|
95
96
|
require 'switchman/rails'
|
|
@@ -101,7 +102,7 @@ module Switchman
|
|
|
101
102
|
self.default_shard = ::Rails.env.to_sym
|
|
102
103
|
self.default_role = :primary
|
|
103
104
|
|
|
104
|
-
|
|
105
|
+
prepend ActiveRecord::Base
|
|
105
106
|
include ActiveRecord::AttributeMethods
|
|
106
107
|
include ActiveRecord::Persistence
|
|
107
108
|
singleton_class.prepend ActiveRecord::ModelSchema::ClassMethods
|
|
@@ -150,9 +151,12 @@ module Switchman
|
|
|
150
151
|
::ActiveRecord::Relation.include(CallSuper)
|
|
151
152
|
|
|
152
153
|
::ActiveRecord::PredicateBuilder::AssociationQueryValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
|
|
154
|
+
::ActiveRecord::PredicateBuilder::PolymorphicArrayValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
|
|
153
155
|
|
|
154
156
|
::ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(ActiveRecord::Tasks::DatabaseTasks)
|
|
155
157
|
|
|
158
|
+
::ActiveRecord::TestFixtures.prepend(ActiveRecord::TestFixtures)
|
|
159
|
+
|
|
156
160
|
::ActiveRecord::TypeCaster::Map.include(ActiveRecord::TypeCaster::Map)
|
|
157
161
|
::ActiveRecord::TypeCaster::Connection.include(ActiveRecord::TypeCaster::Connection)
|
|
158
162
|
|