switchman 2.0.13 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -5
- 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 +74 -67
- 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 -168
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +18 -19
- data/lib/switchman/active_record/query_methods.rb +172 -197
- data/lib/switchman/active_record/reflection.rb +6 -10
- data/lib/switchman/active_record/relation.rb +30 -78
- 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 -58
- 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,18 +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
|
219
|
-
with_local_table_name(false, &block)
|
220
|
-
end
|
221
|
-
|
222
|
-
def with_local_table_name(enable = true)
|
143
|
+
def with_local_table_name(enable = true) # rubocop:disable Style/OptionalBooleanParameter
|
223
144
|
old_value = @use_local_table_name
|
224
145
|
@use_local_table_name = enable
|
225
146
|
yield
|
@@ -227,10 +148,45 @@ module Switchman
|
|
227
148
|
@use_local_table_name = old_value
|
228
149
|
end
|
229
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
|
+
|
230
186
|
def add_index_options(_table_name, _column_name, **)
|
231
|
-
|
232
|
-
algorithm = nil if DatabaseServer.creating_new_shard && algorithm ==
|
233
|
-
[
|
187
|
+
index, algorithm, if_not_exists = super
|
188
|
+
algorithm = nil if DatabaseServer.creating_new_shard && algorithm == 'CONCURRENTLY'
|
189
|
+
[index, algorithm, if_not_exists]
|
234
190
|
end
|
235
191
|
|
236
192
|
def rename_table(table_name, new_name)
|