switchman 2.0.9 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +10 -2
- data/app/models/switchman/shard.rb +234 -271
- data/app/models/switchman/unsharded_record.rb +7 -0
- data/db/migrate/20130328212039_create_switchman_shards.rb +1 -1
- data/db/migrate/20130328224244_create_default_shard.rb +5 -5
- data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +1 -0
- data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
- data/db/migrate/20180828192111_add_timestamps_to_shards.rb +7 -5
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +5 -3
- data/lib/switchman.rb +3 -3
- data/lib/switchman/action_controller/caching.rb +2 -2
- data/lib/switchman/active_record/abstract_adapter.rb +1 -0
- data/lib/switchman/active_record/association.rb +78 -89
- data/lib/switchman/active_record/attribute_methods.rb +58 -52
- data/lib/switchman/active_record/base.rb +58 -59
- data/lib/switchman/active_record/calculations.rb +73 -66
- data/lib/switchman/active_record/connection_pool.rb +14 -41
- data/lib/switchman/active_record/database_configurations.rb +34 -0
- data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
- data/lib/switchman/active_record/finder_methods.rb +11 -16
- data/lib/switchman/active_record/log_subscriber.rb +4 -8
- data/lib/switchman/active_record/migration.rb +6 -47
- data/lib/switchman/active_record/model_schema.rb +1 -1
- data/lib/switchman/active_record/persistence.rb +4 -6
- data/lib/switchman/active_record/postgresql_adapter.rb +124 -164
- data/lib/switchman/active_record/predicate_builder.rb +1 -1
- data/lib/switchman/active_record/query_cache.rb +18 -19
- data/lib/switchman/active_record/query_methods.rb +172 -181
- data/lib/switchman/active_record/reflection.rb +6 -10
- data/lib/switchman/active_record/relation.rb +27 -21
- data/lib/switchman/active_record/spawn_methods.rb +27 -29
- data/lib/switchman/active_record/statement_cache.rb +18 -35
- data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
- data/lib/switchman/active_support/cache.rb +3 -5
- data/lib/switchman/arel.rb +13 -8
- data/lib/switchman/database_server.rb +121 -142
- data/lib/switchman/default_shard.rb +52 -16
- data/lib/switchman/engine.rb +61 -57
- data/lib/switchman/environment.rb +4 -8
- data/lib/switchman/errors.rb +1 -0
- data/lib/switchman/guard_rail.rb +6 -19
- data/lib/switchman/guard_rail/relation.rb +5 -7
- data/lib/switchman/r_spec_helper.rb +29 -37
- data/lib/switchman/rails.rb +14 -12
- data/lib/switchman/schema_cache.rb +1 -9
- data/lib/switchman/sharded_instrumenter.rb +1 -1
- data/lib/switchman/standard_error.rb +15 -3
- data/lib/switchman/test_helper.rb +7 -11
- data/lib/switchman/version.rb +1 -1
- data/lib/tasks/switchman.rake +54 -69
- metadata +87 -45
- data/lib/switchman/active_record/batches.rb +0 -11
- data/lib/switchman/active_record/connection_handler.rb +0 -172
- data/lib/switchman/active_record/where_clause_factory.rb +0 -36
- data/lib/switchman/connection_pool_proxy.rb +0 -173
@@ -6,15 +6,18 @@ module Switchman
|
|
6
6
|
module ActiveRecord
|
7
7
|
module ConnectionPool
|
8
8
|
def shard
|
9
|
-
|
9
|
+
shard_stack.last || Shard.default
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
Thread.current
|
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
|
14
18
|
end
|
15
19
|
|
16
20
|
def default_schema
|
17
|
-
raise "Not postgres!" unless self.spec.config[:adapter] == 'postgresql'
|
18
21
|
connection unless @schemas
|
19
22
|
# default shard will not switch databases immediately, so it won't be set yet
|
20
23
|
@schemas ||= connection.current_schemas
|
@@ -22,39 +25,29 @@ module Switchman
|
|
22
25
|
end
|
23
26
|
|
24
27
|
def checkout_new_connection
|
25
|
-
conn =
|
26
|
-
|
27
|
-
# without locking anything, but if spec returns not-the-object passed
|
28
|
-
# to initialize this pool, things break
|
29
|
-
spec.config[:shard_name] = self.shard.name
|
30
|
-
|
31
|
-
super
|
32
|
-
end
|
33
|
-
conn.shard = self.shard
|
28
|
+
conn = super
|
29
|
+
conn.shard = shard
|
34
30
|
conn
|
35
31
|
end
|
36
32
|
|
37
33
|
def connection(switch_shard: true)
|
38
34
|
conn = super()
|
39
35
|
raise NonExistentShardError if shard.new_record?
|
40
|
-
|
36
|
+
|
37
|
+
switch_database(conn) if conn.shard != shard && switch_shard
|
41
38
|
conn
|
42
39
|
end
|
43
40
|
|
44
41
|
def release_connection(with_id = Thread.current)
|
45
42
|
super(with_id)
|
46
43
|
|
47
|
-
|
48
|
-
clear_idle_connections!(Time.now - spec.config[:idle_timeout].to_i)
|
49
|
-
end
|
44
|
+
flush
|
50
45
|
end
|
51
46
|
|
52
47
|
def remove_shard!(shard)
|
53
48
|
synchronize do
|
54
49
|
# The shard might be currently active, so we need to update our own shard
|
55
|
-
if self.shard == shard
|
56
|
-
self.shard = Shard.default
|
57
|
-
end
|
50
|
+
self.shard = Shard.default if self.shard == shard
|
58
51
|
# Update out any connections that may be using this shard
|
59
52
|
@connections.each do |conn|
|
60
53
|
# This will also update the connection's shard to the default shard
|
@@ -63,29 +56,9 @@ module Switchman
|
|
63
56
|
end
|
64
57
|
end
|
65
58
|
|
66
|
-
def clear_idle_connections!(since_when)
|
67
|
-
synchronize do
|
68
|
-
@connections.reject! do |conn|
|
69
|
-
if conn.last_query_at < since_when && !conn.in_use?
|
70
|
-
conn.disconnect!
|
71
|
-
true
|
72
|
-
else
|
73
|
-
false
|
74
|
-
end
|
75
|
-
end
|
76
|
-
@available.clear
|
77
|
-
@connections.each do |conn|
|
78
|
-
@available.add conn
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
59
|
def switch_database(conn)
|
84
|
-
if !@schemas && conn.adapter_name == 'PostgreSQL' && !
|
85
|
-
@schemas = conn.current_schemas
|
86
|
-
end
|
60
|
+
@schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !shard.database_server.config[:shard_name]
|
87
61
|
|
88
|
-
spec.config[:shard_name] = self.shard.name
|
89
62
|
conn.shard = shard
|
90
63
|
end
|
91
64
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Switchman
|
4
|
+
module ActiveRecord
|
5
|
+
module DatabaseConfigurations
|
6
|
+
private
|
7
|
+
|
8
|
+
# key difference: assumes a hybrid two-tier structure; each third tier
|
9
|
+
# is implicitly named, and their config is constructing by merging into
|
10
|
+
# its parent
|
11
|
+
def build_configs(configs)
|
12
|
+
return configs.configurations if configs.is_a?(DatabaseConfigurations)
|
13
|
+
return configs if configs.is_a?(Array)
|
14
|
+
|
15
|
+
db_configs = configs.flat_map do |env_name, config|
|
16
|
+
roles = config.keys.select { |k| config[k].is_a?(Hash) }
|
17
|
+
base_config = config.except(*roles)
|
18
|
+
|
19
|
+
name = "#{env_name}/primary"
|
20
|
+
name = 'primary' if env_name == default_env
|
21
|
+
base_db = build_db_config_from_raw_config(env_name, name, base_config)
|
22
|
+
[base_db] + roles.map do |role|
|
23
|
+
build_db_config_from_raw_config(env_name, "#{env_name}/#{role}",
|
24
|
+
base_config.merge(config[role]).merge(replica: true))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
db_configs << environment_url_config(default_env, 'primary', {}) unless db_configs.find(&:for_current_env?)
|
29
|
+
|
30
|
+
merge_db_environment_variables(default_env, db_configs.compact)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -7,19 +7,18 @@ module Switchman
|
|
7
7
|
return super(id) unless klass.integral_id?
|
8
8
|
|
9
9
|
if shard_source_value != :implicit
|
10
|
-
current_shard = Shard.current(klass.
|
11
|
-
result =
|
10
|
+
current_shard = Shard.current(klass.connection_classes)
|
11
|
+
result = activate do |relation, shard|
|
12
12
|
current_id = Shard.relative_id_for(id, current_shard, shard)
|
13
13
|
# current_id will be nil for non-integral id
|
14
14
|
next unless current_id
|
15
15
|
# skip the shard if the object can't be on it. unless we're only looking at one shard;
|
16
16
|
# we might be expecting a shadow object
|
17
|
-
next if current_id > Shard::IDS_PER_SHARD &&
|
17
|
+
next if current_id > Shard::IDS_PER_SHARD && all_shards.length > 1
|
18
|
+
|
18
19
|
relation.call_super(:find_one, FinderMethods, current_id)
|
19
20
|
end
|
20
|
-
if result.is_a?(Array)
|
21
|
-
result = result.first
|
22
|
-
end
|
21
|
+
result = result.first if result.is_a?(Array)
|
23
22
|
# we may have skipped all shards
|
24
23
|
raise_record_not_found_exception!(id, 0, 1) unless result
|
25
24
|
return result
|
@@ -34,8 +33,8 @@ module Switchman
|
|
34
33
|
end
|
35
34
|
|
36
35
|
def find_some_ordered(ids)
|
37
|
-
current_shard = Shard.current(klass.
|
38
|
-
ids = ids.map{|id| Shard.relative_id_for(id, current_shard, current_shard)}
|
36
|
+
current_shard = Shard.current(klass.connection_classes)
|
37
|
+
ids = ids.map { |id| Shard.relative_id_for(id, current_shard, current_shard) }
|
39
38
|
super(ids)
|
40
39
|
end
|
41
40
|
|
@@ -45,14 +44,12 @@ module Switchman
|
|
45
44
|
|
46
45
|
def exists?(conditions = :none)
|
47
46
|
conditions = conditions.id if ::ActiveRecord::Base === conditions
|
48
|
-
return false
|
47
|
+
return false unless conditions
|
49
48
|
|
50
|
-
relation =
|
51
|
-
apply_join_dependency(eager_loading: false) :
|
52
|
-
apply_join_dependency(self, construct_join_dependency)
|
49
|
+
relation = apply_join_dependency(eager_loading: false)
|
53
50
|
return false if ::ActiveRecord::NullRelation === relation
|
54
51
|
|
55
|
-
relation = relation.except(:select, :order).select(
|
52
|
+
relation = relation.except(:select, :order).select('1 AS one').limit(1)
|
56
53
|
|
57
54
|
case conditions
|
58
55
|
when Array, Hash
|
@@ -62,9 +59,7 @@ module Switchman
|
|
62
59
|
end
|
63
60
|
|
64
61
|
relation.activate do |shard_rel|
|
65
|
-
return true if
|
66
|
-
connection.select_value(shard_rel.arel, "#{name} Exists") :
|
67
|
-
connection.select_value(shard_rel, "#{name} Exists", shard_rel.bound_attributes)
|
62
|
+
return true if connection.select_value(shard_rel.arel, "#{name} Exists")
|
68
63
|
end
|
69
64
|
false
|
70
65
|
end
|
@@ -14,20 +14,16 @@ module Switchman
|
|
14
14
|
|
15
15
|
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
16
16
|
name = "CACHE #{name}" if payload[:cached]
|
17
|
-
sql = payload[:sql].squeeze(' '
|
17
|
+
sql = payload[:sql].squeeze(' ')
|
18
18
|
binds = nil
|
19
19
|
shard = payload[:shard]
|
20
20
|
shard = " [#{shard[:database_server_id]}:#{shard[:id]} #{shard[:env]}]" if shard
|
21
21
|
|
22
22
|
unless (payload[:binds] || []).empty?
|
23
|
-
|
24
|
-
|
25
|
-
[payload[:binds], payload[:type_casted_binds]] :
|
26
|
-
[payload[:type_casted_binds]]
|
27
|
-
casted_params = type_casted_binds(*args)
|
28
|
-
binds = " " + payload[:binds].zip(casted_params).map { |attr, value|
|
23
|
+
casted_params = type_casted_binds(payload[:type_casted_binds])
|
24
|
+
binds = ' ' + payload[:binds].zip(casted_params).map do |attr, value|
|
29
25
|
render_bind(attr, value)
|
30
|
-
|
26
|
+
end.inspect
|
31
27
|
end
|
32
28
|
|
33
29
|
name = colorize_payload_name(name, payload[:name])
|
@@ -4,73 +4,32 @@ module Switchman
|
|
4
4
|
module ActiveRecord
|
5
5
|
module Migration
|
6
6
|
module Compatibility
|
7
|
-
module V5_0
|
7
|
+
module V5_0 # rubocop:disable Naming/ClassAndModuleCamelCase
|
8
8
|
def create_table(*args, **options)
|
9
|
-
unless options.key?(:id)
|
10
|
-
|
11
|
-
end
|
12
|
-
if block_given?
|
13
|
-
super do |td|
|
14
|
-
yield td
|
15
|
-
end
|
16
|
-
else
|
17
|
-
super
|
18
|
-
end
|
9
|
+
options[:id] = :bigserial unless options.key?(:id)
|
10
|
+
super
|
19
11
|
end
|
20
12
|
end
|
21
13
|
end
|
22
14
|
|
23
15
|
def connection
|
24
16
|
conn = super
|
25
|
-
if conn.shard != ::ActiveRecord::Base.connection_pool.
|
26
|
-
::ActiveRecord::Base.connection_pool.current_pool.switch_database(conn)
|
27
|
-
end
|
17
|
+
::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base.connection_pool.shard
|
28
18
|
conn
|
29
19
|
end
|
30
20
|
end
|
31
21
|
|
32
22
|
module Migrator
|
33
|
-
# significant change: hash shard id, not database name
|
34
23
|
def generate_migrator_advisory_lock_id
|
35
|
-
shard_name_hash = Zlib.crc32(Shard.current.name)
|
24
|
+
shard_name_hash = Zlib.crc32("#{Shard.current.id}:#{Shard.current.name}")
|
36
25
|
::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
|
37
26
|
end
|
38
|
-
|
39
|
-
if ::Rails.version >= '6.0'
|
40
|
-
# copy/paste from Rails 6.1
|
41
|
-
def with_advisory_lock
|
42
|
-
lock_id = generate_migrator_advisory_lock_id
|
43
|
-
|
44
|
-
with_advisory_lock_connection do |connection|
|
45
|
-
got_lock = connection.get_advisory_lock(lock_id)
|
46
|
-
raise ::ActiveRecord::ConcurrentMigrationError unless got_lock
|
47
|
-
load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
|
48
|
-
yield
|
49
|
-
ensure
|
50
|
-
if got_lock && !connection.release_advisory_lock(lock_id)
|
51
|
-
raise ::ActiveRecord::ConcurrentMigrationError.new(
|
52
|
-
::ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
|
53
|
-
)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
# significant change: strip out prefer_secondary from config
|
59
|
-
def with_advisory_lock_connection
|
60
|
-
pool = ::ActiveRecord::ConnectionAdapters::ConnectionHandler.new.establish_connection(
|
61
|
-
::ActiveRecord::Base.connection_config.except(:prefer_secondary)
|
62
|
-
)
|
63
|
-
|
64
|
-
pool.with_connection { |connection| yield(connection) }
|
65
|
-
ensure
|
66
|
-
pool&.disconnect!
|
67
|
-
end
|
68
|
-
end
|
69
27
|
end
|
70
28
|
|
71
29
|
module MigrationContext
|
72
30
|
def migrations
|
73
31
|
return @migrations if instance_variable_defined?(:@migrations)
|
32
|
+
|
74
33
|
migrations_cache = Thread.current[:migrations_cache] ||= {}
|
75
34
|
key = Digest::MD5.hexdigest(migration_files.sort.join(','))
|
76
35
|
@migrations = migrations_cache[key] ||= super
|
@@ -6,7 +6,7 @@ module Switchman
|
|
6
6
|
module ClassMethods
|
7
7
|
def quoted_table_name
|
8
8
|
@quoted_table_name ||= {}
|
9
|
-
@quoted_table_name[Shard.current(
|
9
|
+
@quoted_table_name[Shard.current(connection_classes).id] ||= connection.quote_table_name(table_name)
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -5,14 +5,12 @@ module Switchman
|
|
5
5
|
module Persistence
|
6
6
|
# touch reads the id attribute directly, so it's not relative to the current shard
|
7
7
|
def touch(*, **)
|
8
|
-
shard.activate(self.class.
|
8
|
+
shard.activate(self.class.connection_classes) { super }
|
9
9
|
end
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
shard.activate(self.class.shard_category) { super }
|
14
|
-
end
|
11
|
+
def update_columns(*)
|
12
|
+
shard.activate(self.class.connection_classes) { super }
|
15
13
|
end
|
16
14
|
end
|
17
15
|
end
|
18
|
-
end
|
16
|
+
end
|
@@ -9,22 +9,22 @@ module Switchman
|
|
9
9
|
|
10
10
|
option_string = options.sum do |key, value|
|
11
11
|
case key
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
12
|
+
when :owner
|
13
|
+
" OWNER = \"#{value}\""
|
14
|
+
when :template
|
15
|
+
" TEMPLATE = \"#{value}\""
|
16
|
+
when :encoding
|
17
|
+
" ENCODING = '#{value}'"
|
18
|
+
when :collation
|
19
|
+
" LC_COLLATE = '#{value}'"
|
20
|
+
when :ctype
|
21
|
+
" LC_CTYPE = '#{value}'"
|
22
|
+
when :tablespace
|
23
|
+
" TABLESPACE = \"#{value}\""
|
24
|
+
when :connection_limit
|
25
|
+
" CONNECTION LIMIT = #{value}"
|
26
|
+
else
|
27
|
+
''
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -37,166 +37,92 @@ module Switchman
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def current_schemas
|
40
|
-
select_values(
|
40
|
+
select_values('SELECT * FROM unnest(current_schemas(false))')
|
41
|
+
end
|
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
|
41
49
|
end
|
42
50
|
|
43
51
|
def extract_schema_qualified_name(string)
|
44
52
|
name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(string.to_s)
|
45
|
-
if string && !name.schema
|
46
|
-
name.instance_variable_set(:@schema, shard.name)
|
47
|
-
end
|
53
|
+
name.instance_variable_set(:@schema, shard.name) if string && !name.schema
|
48
54
|
[name.schema, name.identifier]
|
49
55
|
end
|
50
56
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
scope[:name] = quote(name) if name
|
66
|
-
scope[:type] = type if type
|
67
|
-
scope
|
57
|
+
def view_exists?(name)
|
58
|
+
name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
|
59
|
+
return false unless name.identifier
|
60
|
+
|
61
|
+
name.instance_variable_set(:@schema, shard.name) unless name.schema
|
62
|
+
|
63
|
+
select_values(<<-SQL, 'SCHEMA').any?
|
64
|
+
SELECT c.relname
|
65
|
+
FROM pg_class c
|
66
|
+
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
67
|
+
WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
|
68
|
+
AND c.relname = '#{name.identifier}'
|
69
|
+
AND n.nspname = '#{shard.name}'
|
70
|
+
SQL
|
68
71
|
end
|
69
72
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
73
|
+
def indexes(table_name)
|
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(',')})
|
76
98
|
SQL
|
77
|
-
end
|
78
99
|
|
79
|
-
|
80
|
-
name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
|
81
|
-
return false unless name.identifier
|
82
|
-
if !name.schema
|
83
|
-
name.instance_variable_set(:@schema, shard.name)
|
84
|
-
end
|
100
|
+
column_names = columns.stringify_keys.values_at(*indkey).compact
|
85
101
|
|
86
|
-
|
87
|
-
SELECT c.relname
|
88
|
-
FROM pg_class c
|
89
|
-
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
90
|
-
WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
|
91
|
-
AND c.relname = '#{name.identifier}'
|
92
|
-
AND n.nspname = '#{shard.name}'
|
93
|
-
SQL
|
94
|
-
end
|
102
|
+
next if column_names.empty?
|
95
103
|
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
113
|
+
end
|
114
|
+
|
115
|
+
def index_name_exists?(table_name, index_name, _default = nil)
|
116
|
+
exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i.positive?
|
117
|
+
SELECT COUNT(*)
|
99
118
|
FROM pg_class t
|
100
119
|
INNER JOIN pg_index d ON t.oid = d.indrelid
|
101
120
|
INNER JOIN pg_class i ON d.indexrelid = i.oid
|
102
121
|
WHERE i.relkind = 'i'
|
103
|
-
AND
|
122
|
+
AND i.relname = '#{index_name}'
|
104
123
|
AND t.relname = '#{table_name}'
|
105
124
|
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
|
106
|
-
|
107
|
-
SQL
|
108
|
-
|
109
|
-
|
110
|
-
result.map do |row|
|
111
|
-
index_name = row[0]
|
112
|
-
unique = row[1] == true || row[1] == 't'
|
113
|
-
indkey = row[2].split(" ")
|
114
|
-
inddef = row[3]
|
115
|
-
oid = row[4]
|
116
|
-
|
117
|
-
columns = Hash[query(<<-SQL, "SCHEMA")]
|
118
|
-
SELECT a.attnum, a.attname
|
119
|
-
FROM pg_attribute a
|
120
|
-
WHERE a.attrelid = #{oid}
|
121
|
-
AND a.attnum IN (#{indkey.join(",")})
|
122
|
-
SQL
|
123
|
-
|
124
|
-
column_names = columns.stringify_keys.values_at(*indkey).compact
|
125
|
-
|
126
|
-
unless column_names.empty?
|
127
|
-
# add info on sort order for columns (only desc order is explicitly specified, asc is the default)
|
128
|
-
desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
|
129
|
-
orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
|
130
|
-
where = inddef.scan(/WHERE (.+)$/).flatten[0]
|
131
|
-
using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
|
132
|
-
|
133
|
-
if ::Rails.version >= "5.2"
|
134
|
-
::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, orders: orders, where: where, using: using)
|
135
|
-
else
|
136
|
-
::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end.compact
|
140
|
-
end
|
141
|
-
|
142
|
-
def index_name_exists?(table_name, index_name, _default = nil)
|
143
|
-
exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
|
144
|
-
SELECT COUNT(*)
|
145
|
-
FROM pg_class t
|
146
|
-
INNER JOIN pg_index d ON t.oid = d.indrelid
|
147
|
-
INNER JOIN pg_class i ON d.indexrelid = i.oid
|
148
|
-
WHERE i.relkind = 'i'
|
149
|
-
AND i.relname = '#{index_name}'
|
150
|
-
AND t.relname = '#{table_name}'
|
151
|
-
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = '#{shard.name}' )
|
152
|
-
SQL
|
153
|
-
end
|
154
|
-
|
155
|
-
def foreign_keys(table_name)
|
156
|
-
# mostly copy-pasted from AR - only change is to the nspname condition for qualified names support
|
157
|
-
fk_info = select_all <<-SQL.strip_heredoc
|
158
|
-
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
|
159
|
-
FROM pg_constraint c
|
160
|
-
JOIN pg_class t1 ON c.conrelid = t1.oid
|
161
|
-
JOIN pg_class t2 ON c.confrelid = t2.oid
|
162
|
-
JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
|
163
|
-
JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
|
164
|
-
JOIN pg_namespace t3 ON c.connamespace = t3.oid
|
165
|
-
WHERE c.contype = 'f'
|
166
|
-
AND t1.relname = #{quote(table_name)}
|
167
|
-
AND t3.nspname = '#{shard.name}'
|
168
|
-
ORDER BY c.conname
|
169
|
-
SQL
|
170
|
-
|
171
|
-
fk_info.map do |row|
|
172
|
-
options = {
|
173
|
-
column: row['column'],
|
174
|
-
name: row['name'],
|
175
|
-
primary_key: row['primary_key']
|
176
|
-
}
|
177
|
-
|
178
|
-
options[:on_delete] = extract_foreign_key_action(row['on_delete'])
|
179
|
-
options[:on_update] = extract_foreign_key_action(row['on_update'])
|
180
|
-
|
181
|
-
# strip the schema name from to_table if it matches
|
182
|
-
to_table = row['to_table']
|
183
|
-
to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(to_table)
|
184
|
-
if to_table_qualified_name.schema == shard.name
|
185
|
-
to_table = to_table_qualified_name.identifier
|
186
|
-
end
|
187
|
-
|
188
|
-
::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, to_table, options)
|
189
|
-
end
|
190
|
-
end
|
191
|
-
else
|
192
|
-
def foreign_keys(table_name)
|
193
|
-
super.each do |fk|
|
194
|
-
to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
|
195
|
-
if to_table_qualified_name.schema == shard.name
|
196
|
-
fk.to_table = to_table_qualified_name.identifier
|
197
|
-
end
|
198
|
-
end
|
199
|
-
end
|
125
|
+
SQL
|
200
126
|
end
|
201
127
|
|
202
128
|
def quote_local_table_name(name)
|
@@ -208,14 +134,13 @@ module Switchman
|
|
208
134
|
|
209
135
|
def quote_table_name(name)
|
210
136
|
return quote_local_table_name(name) if @use_local_table_name
|
137
|
+
|
211
138
|
name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
|
212
|
-
|
213
|
-
name.instance_variable_set(:@schema, shard.name)
|
214
|
-
end
|
139
|
+
name.instance_variable_set(:@schema, shard.name) unless name.schema
|
215
140
|
name.quoted
|
216
141
|
end
|
217
142
|
|
218
|
-
def with_local_table_name(enable = true)
|
143
|
+
def with_local_table_name(enable = true) # rubocop:disable Style/OptionalBooleanParameter
|
219
144
|
old_value = @use_local_table_name
|
220
145
|
@use_local_table_name = enable
|
221
146
|
yield
|
@@ -223,10 +148,45 @@ module Switchman
|
|
223
148
|
@use_local_table_name = old_value
|
224
149
|
end
|
225
150
|
|
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
|
+
|
226
186
|
def add_index_options(_table_name, _column_name, **)
|
227
|
-
|
228
|
-
algorithm = nil if DatabaseServer.creating_new_shard && algorithm ==
|
229
|
-
[
|
187
|
+
index, algorithm, if_not_exists = super
|
188
|
+
algorithm = nil if DatabaseServer.creating_new_shard && algorithm == 'CONCURRENTLY'
|
189
|
+
[index, algorithm, if_not_exists]
|
230
190
|
end
|
231
191
|
|
232
192
|
def rename_table(table_name, new_name)
|