switchman 3.0.1 → 4.2.5
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 +16 -15
- data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
- data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
- data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
- data/lib/switchman/action_controller/caching.rb +2 -2
- data/lib/switchman/active_record/abstract_adapter.rb +11 -18
- data/lib/switchman/active_record/associations.rb +315 -0
- data/lib/switchman/active_record/attribute_methods.rb +191 -79
- data/lib/switchman/active_record/base.rb +204 -50
- data/lib/switchman/active_record/calculations.rb +93 -50
- data/lib/switchman/active_record/connection_handler.rb +18 -0
- data/lib/switchman/active_record/connection_pool.rb +47 -34
- data/lib/switchman/active_record/database_configurations.rb +32 -6
- data/lib/switchman/active_record/finder_methods.rb +22 -16
- data/lib/switchman/active_record/log_subscriber.rb +3 -6
- data/lib/switchman/active_record/migration.rb +42 -14
- data/lib/switchman/active_record/model_schema.rb +1 -1
- data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
- data/lib/switchman/active_record/persistence.rb +37 -2
- data/lib/switchman/active_record/postgresql_adapter.rb +39 -20
- data/lib/switchman/active_record/predicate_builder.rb +2 -2
- data/lib/switchman/active_record/query_cache.rb +26 -17
- data/lib/switchman/active_record/query_methods.rb +252 -135
- data/lib/switchman/active_record/reflection.rb +10 -3
- data/lib/switchman/active_record/relation.rb +154 -32
- data/lib/switchman/active_record/spawn_methods.rb +3 -7
- data/lib/switchman/active_record/statement_cache.rb +13 -9
- data/lib/switchman/active_record/table_definition.rb +1 -1
- data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
- data/lib/switchman/active_record/test_fixtures.rb +89 -0
- data/lib/switchman/active_support/cache.rb +25 -4
- data/lib/switchman/arel.rb +20 -7
- data/lib/switchman/call_super.rb +2 -2
- data/lib/switchman/database_server.rb +123 -83
- data/lib/switchman/default_shard.rb +14 -5
- data/lib/switchman/engine.rb +85 -131
- data/lib/switchman/environment.rb +2 -2
- data/lib/switchman/errors.rb +17 -2
- data/lib/switchman/guard_rail/relation.rb +7 -10
- data/lib/switchman/guard_rail.rb +5 -0
- data/lib/switchman/parallel.rb +68 -0
- data/lib/switchman/r_spec_helper.rb +17 -28
- data/lib/switchman/rails.rb +1 -4
- data/{app/models → lib}/switchman/shard.rb +229 -246
- data/lib/switchman/sharded_instrumenter.rb +9 -3
- data/lib/switchman/shared_schema_cache.rb +11 -0
- data/lib/switchman/standard_error.rb +15 -12
- data/lib/switchman/test_helper.rb +3 -3
- data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
- data/lib/switchman/version.rb +1 -1
- data/lib/switchman.rb +46 -12
- data/lib/tasks/switchman.rake +101 -54
- metadata +34 -176
- data/lib/switchman/active_record/association.rb +0 -206
- data/lib/switchman/open4.rb +0 -80
|
@@ -1,71 +1,84 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'switchman/errors'
|
|
4
|
-
|
|
5
3
|
module Switchman
|
|
6
4
|
module ActiveRecord
|
|
7
5
|
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
6
|
def default_schema
|
|
21
|
-
connection
|
|
7
|
+
connection_method = (::Rails.version < "7.2") ? :connection : :lease_connection
|
|
8
|
+
send(connection_method) unless @schemas
|
|
22
9
|
# default shard will not switch databases immediately, so it won't be set yet
|
|
23
|
-
@schemas ||=
|
|
10
|
+
@schemas ||= send(connection_method).current_schemas
|
|
24
11
|
@schemas.first
|
|
25
12
|
end
|
|
26
13
|
|
|
27
14
|
def checkout_new_connection
|
|
28
15
|
conn = super
|
|
29
|
-
conn.shard =
|
|
16
|
+
conn.shard = current_shard
|
|
30
17
|
conn
|
|
31
18
|
end
|
|
32
19
|
|
|
33
20
|
def connection(switch_shard: true)
|
|
34
21
|
conn = super()
|
|
35
|
-
raise NonExistentShardError if
|
|
22
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
36
23
|
|
|
37
|
-
switch_database(conn) if conn.shard !=
|
|
24
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
38
25
|
conn
|
|
39
26
|
end
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
unless ::Rails.version < "7.2"
|
|
29
|
+
def active_connection(switch_shard: true)
|
|
30
|
+
conn = super()
|
|
31
|
+
return nil if conn.nil?
|
|
32
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
35
|
+
conn
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def lease_connection(switch_shard: true)
|
|
39
|
+
conn = super()
|
|
40
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
43
|
+
conn
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def with_connection(switch_shard: true, **kwargs)
|
|
47
|
+
super(**kwargs) do |conn|
|
|
48
|
+
raise Errors::NonExistentShardError if current_shard.new_record?
|
|
49
|
+
|
|
50
|
+
switch_database(conn) if conn.shard != current_shard && switch_shard
|
|
51
|
+
yield conn
|
|
55
52
|
end
|
|
56
53
|
end
|
|
57
54
|
end
|
|
58
55
|
|
|
56
|
+
def release_connection(with_id = Thread.current)
|
|
57
|
+
super
|
|
58
|
+
|
|
59
|
+
flush
|
|
60
|
+
end
|
|
61
|
+
|
|
59
62
|
def switch_database(conn)
|
|
60
|
-
|
|
63
|
+
if !@schemas && conn.adapter_name == "PostgreSQL" && !current_shard.database_server.config[:shard_name]
|
|
64
|
+
@schemas = conn.current_schemas
|
|
65
|
+
end
|
|
61
66
|
|
|
62
|
-
conn.shard =
|
|
67
|
+
conn.shard = current_shard
|
|
63
68
|
end
|
|
64
69
|
|
|
65
70
|
private
|
|
66
71
|
|
|
72
|
+
def current_shard
|
|
73
|
+
if ::Rails.version < "8.0"
|
|
74
|
+
connection_class.current_switchman_shard
|
|
75
|
+
else
|
|
76
|
+
connection_descriptor.name.constantize.current_switchman_shard
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
67
80
|
def tls_key
|
|
68
|
-
"#{object_id}_shard"
|
|
81
|
+
:"#{object_id}_shard"
|
|
69
82
|
end
|
|
70
83
|
end
|
|
71
84
|
end
|
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
module Switchman
|
|
4
4
|
module ActiveRecord
|
|
5
5
|
module DatabaseConfigurations
|
|
6
|
+
# key difference: For each env name, ensure only one writable config is returned
|
|
7
|
+
# since all should point to the same data, even if multiple are writable
|
|
8
|
+
# (Picks 'primary' since it is guaranteed to exist and switchman handles activating
|
|
9
|
+
# deploy through other means)
|
|
10
|
+
def configs_for(include_hidden: false, name: nil, **)
|
|
11
|
+
res = super
|
|
12
|
+
if name && !include_hidden
|
|
13
|
+
return nil unless name.end_with?("primary")
|
|
14
|
+
elsif !include_hidden
|
|
15
|
+
return res.select { |config| config.name.end_with?("primary") }
|
|
16
|
+
end
|
|
17
|
+
res
|
|
18
|
+
end
|
|
19
|
+
|
|
6
20
|
private
|
|
7
21
|
|
|
8
22
|
# key difference: assumes a hybrid two-tier structure; each third tier
|
|
@@ -13,19 +27,31 @@ module Switchman
|
|
|
13
27
|
return configs if configs.is_a?(Array)
|
|
14
28
|
|
|
15
29
|
db_configs = configs.flat_map do |env_name, config|
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
if config.is_a?(Hash)
|
|
31
|
+
# It would be nice to do the auto-fallback that we want here, but we haven't
|
|
32
|
+
# actually done that for years (or maybe ever) and it will be a big lift to get working
|
|
33
|
+
roles = config.keys.select do |k|
|
|
34
|
+
config[k].is_a?(Hash) || (config[k].is_a?(Array) && config[k].all?(Hash))
|
|
35
|
+
end
|
|
36
|
+
base_config = config.except(*roles)
|
|
37
|
+
else
|
|
38
|
+
base_config = config
|
|
39
|
+
roles = []
|
|
40
|
+
end
|
|
18
41
|
|
|
19
42
|
name = "#{env_name}/primary"
|
|
20
|
-
name =
|
|
43
|
+
name = "primary" if env_name == default_env
|
|
21
44
|
base_db = build_db_config_from_raw_config(env_name, name, base_config)
|
|
22
45
|
[base_db] + roles.map do |role|
|
|
23
|
-
build_db_config_from_raw_config(
|
|
24
|
-
|
|
46
|
+
build_db_config_from_raw_config(
|
|
47
|
+
env_name,
|
|
48
|
+
"#{env_name}/#{role}",
|
|
49
|
+
base_config.merge(config[role].is_a?(Array) ? config[role].first : config[role])
|
|
50
|
+
)
|
|
25
51
|
end
|
|
26
52
|
end
|
|
27
53
|
|
|
28
|
-
db_configs << environment_url_config(default_env,
|
|
54
|
+
db_configs << environment_url_config(default_env, "primary", {}) unless db_configs.find(&:for_current_env?)
|
|
29
55
|
|
|
30
56
|
merge_db_environment_variables(default_env, db_configs.compact)
|
|
31
57
|
end
|
|
@@ -4,10 +4,10 @@ module Switchman
|
|
|
4
4
|
module ActiveRecord
|
|
5
5
|
module FinderMethods
|
|
6
6
|
def find_one(id)
|
|
7
|
-
return super
|
|
7
|
+
return super unless klass.integral_id?
|
|
8
8
|
|
|
9
9
|
if shard_source_value != :implicit
|
|
10
|
-
current_shard = Shard.current(klass.
|
|
10
|
+
current_shard = Shard.current(klass.connection_class_for_self)
|
|
11
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
|
|
@@ -28,14 +28,14 @@ module Switchman
|
|
|
28
28
|
if shard
|
|
29
29
|
shard.activate { super(local_id) }
|
|
30
30
|
else
|
|
31
|
-
super
|
|
31
|
+
super
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def find_some_ordered(ids)
|
|
36
|
-
current_shard = Shard.current(klass.
|
|
36
|
+
current_shard = Shard.current(klass.connection_class_for_self)
|
|
37
37
|
ids = ids.map { |id| Shard.relative_id_for(id, current_shard, current_shard) }
|
|
38
|
-
super
|
|
38
|
+
super
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def find_or_instantiator_by_attributes(match, attributes, *args)
|
|
@@ -43,23 +43,29 @@ module Switchman
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def exists?(conditions = :none)
|
|
46
|
-
|
|
47
|
-
return false unless conditions
|
|
46
|
+
return false if @none
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
if Base === conditions
|
|
49
|
+
raise ArgumentError, <<-TEXT.squish
|
|
50
|
+
You are passing an instance of ActiveRecord::Base to `exists?`.
|
|
51
|
+
Please pass the id of the object by calling `.id`.
|
|
52
|
+
TEXT
|
|
53
|
+
end
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
return false if !conditions || limit_value == 0 # rubocop:disable Style/NumericPredicate
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
else
|
|
58
|
-
relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
|
|
57
|
+
if eager_loading?
|
|
58
|
+
relation = apply_join_dependency(eager_loading: false)
|
|
59
|
+
return relation.exists?(conditions)
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
relation = construct_relation_for_exists(conditions)
|
|
63
|
+
return false if relation.where_clause.contradiction?
|
|
64
|
+
|
|
61
65
|
relation.activate do |shard_rel|
|
|
62
|
-
return true if
|
|
66
|
+
return true if skip_query_cache_if_necessary do
|
|
67
|
+
connection.select_rows(shard_rel.arel, "#{name} Exists?").size == 1
|
|
68
|
+
end
|
|
63
69
|
end
|
|
64
70
|
false
|
|
65
71
|
end
|
|
@@ -5,29 +5,26 @@ module Switchman
|
|
|
5
5
|
module LogSubscriber
|
|
6
6
|
# sadly, have to completely replace this
|
|
7
7
|
def sql(event)
|
|
8
|
-
self.class.runtime += event.duration
|
|
9
|
-
return unless logger.debug?
|
|
10
|
-
|
|
11
8
|
payload = event.payload
|
|
12
9
|
|
|
13
10
|
return if ::ActiveRecord::LogSubscriber::IGNORE_PAYLOAD_NAMES.include?(payload[:name])
|
|
14
11
|
|
|
15
12
|
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
|
16
13
|
name = "CACHE #{name}" if payload[:cached]
|
|
17
|
-
sql = payload[:sql].squeeze(
|
|
14
|
+
sql = payload[:sql].squeeze(" ")
|
|
18
15
|
binds = nil
|
|
19
16
|
shard = payload[:shard]
|
|
20
17
|
shard = " [#{shard[:database_server_id]}:#{shard[:id]} #{shard[:env]}]" if shard
|
|
21
18
|
|
|
22
19
|
unless (payload[:binds] || []).empty?
|
|
23
20
|
casted_params = type_casted_binds(payload[:type_casted_binds])
|
|
24
|
-
binds =
|
|
21
|
+
binds = " " + payload[:binds].zip(casted_params).map do |attr, value|
|
|
25
22
|
render_bind(attr, value)
|
|
26
23
|
end.inspect
|
|
27
24
|
end
|
|
28
25
|
|
|
29
26
|
name = colorize_payload_name(name, payload[:name])
|
|
30
|
-
sql = color(sql, sql_color(sql), true)
|
|
27
|
+
sql = color(sql, sql_color(sql), bold: true)
|
|
31
28
|
|
|
32
29
|
debug " #{name} #{sql}#{binds}#{shard}"
|
|
33
30
|
end
|
|
@@ -14,38 +14,66 @@ module Switchman
|
|
|
14
14
|
|
|
15
15
|
def connection
|
|
16
16
|
conn = super
|
|
17
|
-
|
|
17
|
+
if conn.shard != ::ActiveRecord::Base.current_switchman_shard
|
|
18
|
+
::ActiveRecord::Base.connection_pool.switch_database(conn)
|
|
19
|
+
end
|
|
18
20
|
conn
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
module Migrator
|
|
23
|
-
# significant change:
|
|
25
|
+
# significant change: use the shard name instead of the database name
|
|
26
|
+
# in the lock id. Especially if you're going through pgbouncer, the
|
|
27
|
+
# database name you're accessing may not be consistent
|
|
24
28
|
def generate_migrator_advisory_lock_id
|
|
25
|
-
|
|
26
|
-
::ActiveRecord::Migrator::MIGRATOR_SALT *
|
|
29
|
+
db_name_hash = Zlib.crc32(Shard.current.name)
|
|
30
|
+
shard_name_hash = ::ActiveRecord::Migrator::MIGRATOR_SALT * db_name_hash
|
|
31
|
+
# Store in internalmetadata to allow other tools to be able to lock out migrations
|
|
32
|
+
@internal_metadata[:migrator_advisory_lock_id] = shard_name_hash.to_s
|
|
33
|
+
shard_name_hash
|
|
27
34
|
end
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
pool = ::ActiveRecord::ConnectionAdapters::ConnectionHandler.new.establish_connection(
|
|
32
|
-
::ActiveRecord::Base.connection_db_config.configuration_hash.except(:prefer_secondary)
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
pool.with_connection { |connection| yield(connection) } # rubocop:disable Style/ExplicitBlockArgument
|
|
36
|
-
ensure
|
|
37
|
-
pool&.disconnect!
|
|
36
|
+
def use_advisory_lock?
|
|
37
|
+
super && pending_migrations.any?
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
module MigrationContext
|
|
42
|
+
def migrate(...)
|
|
43
|
+
schema_cache_holder = ::ActiveRecord::Base.connection_pool
|
|
44
|
+
schema_cache_holder = schema_cache_holder.schema_reflection
|
|
45
|
+
previous_schema_cache = schema_cache_holder.instance_variable_get(:@cache)
|
|
46
|
+
|
|
47
|
+
schema_cache_holder.instance_variable_get(:@cache)
|
|
48
|
+
|
|
49
|
+
reset_column_information
|
|
50
|
+
schema_cache_holder.clear!
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
super
|
|
54
|
+
ensure
|
|
55
|
+
if ::Rails.version < "7.2"
|
|
56
|
+
schema_cache_holder.set_schema_cache(previous_schema_cache)
|
|
57
|
+
else
|
|
58
|
+
schema_cache_holder.instance_variable_set(:@cache, previous_schema_cache)
|
|
59
|
+
end
|
|
60
|
+
reset_column_information
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
42
64
|
def migrations
|
|
43
65
|
return @migrations if instance_variable_defined?(:@migrations)
|
|
44
66
|
|
|
45
67
|
migrations_cache = Thread.current[:migrations_cache] ||= {}
|
|
46
|
-
key = Digest::MD5.hexdigest(migration_files.sort.join(
|
|
68
|
+
key = Digest::MD5.hexdigest(migration_files.sort.join(","))
|
|
47
69
|
@migrations = migrations_cache[key] ||= super
|
|
48
70
|
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def reset_column_information
|
|
75
|
+
::ActiveRecord::Base.descendants.reject { |m| m <= UnshardedRecord }.each(&:reset_column_information)
|
|
76
|
+
end
|
|
49
77
|
end
|
|
50
78
|
end
|
|
51
79
|
end
|
|
@@ -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_class_for_self).id] ||= connection.quote_table_name(table_name)
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Switchman
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
module PendingMigrationConnection
|
|
6
|
+
module ClassMethods
|
|
7
|
+
def current_role
|
|
8
|
+
::ActiveRecord::Base.current_role
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def current_switchman_shard
|
|
12
|
+
::ActiveRecord::Base.current_switchman_shard
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -5,11 +5,46 @@ 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
|
-
|
|
8
|
+
writable_shadow_record_warning
|
|
9
|
+
shard.activate(self.class.connection_class_for_self) { super }
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
def update_columns(*)
|
|
12
|
-
|
|
13
|
+
writable_shadow_record_warning
|
|
14
|
+
shard.activate(self.class.connection_class_for_self) { super }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def delete
|
|
18
|
+
db = shard.database_server
|
|
19
|
+
db.unguard { super }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def destroy
|
|
23
|
+
writable_shadow_record_warning
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_or_update(**, &)
|
|
28
|
+
writable_shadow_record_warning
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reload(*)
|
|
33
|
+
res = super
|
|
34
|
+
# When a shadow record is reloaded the real record is returned. So
|
|
35
|
+
# we need to ensure the loaded_from_shard is set correctly after a reload.
|
|
36
|
+
@loaded_from_shard = @shard
|
|
37
|
+
if @readonly_from_shadow
|
|
38
|
+
@readonly_from_shadow = false
|
|
39
|
+
@readonly = false
|
|
40
|
+
end
|
|
41
|
+
res
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def writable_shadow_record_warning
|
|
45
|
+
return unless shadow_record? && Switchman.config[:writable_shadow_records]
|
|
46
|
+
|
|
47
|
+
Switchman::Deprecation.warn("writing to shadow records is not supported")
|
|
13
48
|
end
|
|
14
49
|
end
|
|
15
50
|
end
|
|
@@ -5,9 +5,9 @@ module Switchman
|
|
|
5
5
|
module PostgreSQLAdapter
|
|
6
6
|
# copy/paste; use quote_local_table_name
|
|
7
7
|
def create_database(name, options = {})
|
|
8
|
-
options = { encoding:
|
|
8
|
+
options = { encoding: "utf8" }.merge!(options.symbolize_keys)
|
|
9
9
|
|
|
10
|
-
option_string = options.sum do |key, value|
|
|
10
|
+
option_string = options.sum("") do |key, value|
|
|
11
11
|
case key
|
|
12
12
|
when :owner
|
|
13
13
|
" OWNER = \"#{value}\""
|
|
@@ -24,7 +24,7 @@ module Switchman
|
|
|
24
24
|
when :connection_limit
|
|
25
25
|
" CONNECTION LIMIT = #{value}"
|
|
26
26
|
else
|
|
27
|
-
|
|
27
|
+
""
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -32,12 +32,12 @@ 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
|
|
|
39
39
|
def current_schemas
|
|
40
|
-
select_values(
|
|
40
|
+
select_values("SELECT * FROM unnest(current_schemas(false))")
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def extract_schema_qualified_name(string)
|
|
@@ -49,13 +49,13 @@ module Switchman
|
|
|
49
49
|
# significant change: use the shard name if no explicit schema
|
|
50
50
|
def quoted_scope(name = nil, type: nil)
|
|
51
51
|
schema, name = extract_schema_qualified_name(name)
|
|
52
|
-
type =
|
|
52
|
+
type =
|
|
53
53
|
case type # rubocop:disable Style/HashLikeCase
|
|
54
|
-
when
|
|
54
|
+
when "BASE TABLE"
|
|
55
55
|
"'r','p'"
|
|
56
|
-
when
|
|
56
|
+
when "VIEW"
|
|
57
57
|
"'v','m'"
|
|
58
|
-
when
|
|
58
|
+
when "FOREIGN TABLE"
|
|
59
59
|
"'f'"
|
|
60
60
|
end
|
|
61
61
|
scope = {}
|
|
@@ -67,24 +67,43 @@ module Switchman
|
|
|
67
67
|
|
|
68
68
|
def foreign_keys(table_name)
|
|
69
69
|
super.each do |fk|
|
|
70
|
-
to_table_qualified_name =
|
|
70
|
+
to_table_qualified_name =
|
|
71
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(fk.to_table)
|
|
71
72
|
fk.to_table = to_table_qualified_name.identifier if to_table_qualified_name.schema == shard.name
|
|
72
73
|
end
|
|
73
74
|
end
|
|
74
75
|
|
|
76
|
+
module ClassMethods
|
|
77
|
+
def quote_local_table_name(name)
|
|
78
|
+
# postgres quotes tables and columns the same; just pass through
|
|
79
|
+
# (differs from quote_table_name_with_shard below by no logic to
|
|
80
|
+
# explicitly qualify the table)
|
|
81
|
+
quote_column_name(name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def quote_table_name(name, shard: nil)
|
|
85
|
+
# This looks kind of weird at first glance, but older Rails versions do not actually import
|
|
86
|
+
# these methods as class methods.
|
|
87
|
+
shard = self.shard if shard.nil? && ::Rails.version < "7.2" && !@use_local_table_name
|
|
88
|
+
|
|
89
|
+
return quote_local_table_name(name) unless shard
|
|
90
|
+
|
|
91
|
+
name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
|
|
92
|
+
name.instance_variable_set(:@schema, shard.name) unless name.schema
|
|
93
|
+
name.quoted
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
75
97
|
def quote_local_table_name(name)
|
|
76
|
-
|
|
77
|
-
# (differs from quote_table_name_with_shard below by no logic to
|
|
78
|
-
# explicitly qualify the table)
|
|
79
|
-
quote_column_name(name)
|
|
98
|
+
self.class.quote_local_table_name(name)
|
|
80
99
|
end
|
|
81
100
|
|
|
82
101
|
def quote_table_name(name)
|
|
83
|
-
|
|
102
|
+
self.class.quote_table_name(name, shard: @use_local_table_name ? nil : shard)
|
|
103
|
+
end
|
|
84
104
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
name.quoted
|
|
105
|
+
def with_global_table_name(&)
|
|
106
|
+
with_local_table_name(false, &)
|
|
88
107
|
end
|
|
89
108
|
|
|
90
109
|
def with_local_table_name(enable = true) # rubocop:disable Style/OptionalBooleanParameter
|
|
@@ -97,7 +116,7 @@ module Switchman
|
|
|
97
116
|
|
|
98
117
|
def add_index_options(_table_name, _column_name, **)
|
|
99
118
|
index, algorithm, if_not_exists = super
|
|
100
|
-
algorithm = nil if DatabaseServer.creating_new_shard && algorithm ==
|
|
119
|
+
algorithm = nil if DatabaseServer.creating_new_shard && algorithm == "CONCURRENTLY"
|
|
101
120
|
[index, algorithm, if_not_exists]
|
|
102
121
|
end
|
|
103
122
|
|
|
@@ -124,7 +143,7 @@ module Switchman
|
|
|
124
143
|
end
|
|
125
144
|
|
|
126
145
|
def columns(*)
|
|
127
|
-
|
|
146
|
+
with_global_table_name { super }
|
|
128
147
|
end
|
|
129
148
|
end
|
|
130
149
|
end
|
|
@@ -11,11 +11,11 @@ module Switchman
|
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
module
|
|
14
|
+
module PolymorphicArrayValue
|
|
15
15
|
def convert_to_id(value)
|
|
16
16
|
case value
|
|
17
17
|
when ::ActiveRecord::Base
|
|
18
|
-
value.send(primary_key)
|
|
18
|
+
value.send(primary_key(value))
|
|
19
19
|
else
|
|
20
20
|
super
|
|
21
21
|
end
|
|
@@ -8,27 +8,36 @@ module Switchman
|
|
|
8
8
|
def cache_sql(sql, name, binds)
|
|
9
9
|
# have to include the shard id in the cache key because of switching dbs on the same connection
|
|
10
10
|
sql = "#{shard.id}::#{sql}"
|
|
11
|
+
key = binds.empty? ? sql : [sql, binds]
|
|
12
|
+
result = nil
|
|
13
|
+
hit = false
|
|
14
|
+
|
|
11
15
|
@lock.synchronize do
|
|
12
|
-
|
|
13
|
-
if query_cache
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
binds: binds,
|
|
17
|
-
name: name,
|
|
18
|
-
connection_id: object_id,
|
|
19
|
-
cached: true,
|
|
20
|
-
type_casted_binds: -> { type_casted_binds(binds) }
|
|
21
|
-
}
|
|
22
|
-
::ActiveSupport::Notifications.instrument(
|
|
23
|
-
'sql.active_record',
|
|
24
|
-
args
|
|
25
|
-
)
|
|
26
|
-
query_cache[sql][binds]
|
|
16
|
+
if ::Rails.version < "7.2"
|
|
17
|
+
if (result = @query_cache.delete(key))
|
|
18
|
+
hit = true
|
|
19
|
+
@query_cache[key] = result
|
|
27
20
|
else
|
|
28
|
-
query_cache[
|
|
21
|
+
result = @query_cache[key] = yield
|
|
22
|
+
@query_cache.shift if @query_cache_max_size && @query_cache.size > @query_cache_max_size
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
hit = true
|
|
26
|
+
result = @query_cache.compute_if_absent(key) do
|
|
27
|
+
hit = false
|
|
28
|
+
yield
|
|
29
29
|
end
|
|
30
|
-
|
|
30
|
+
end
|
|
31
31
|
end
|
|
32
|
+
|
|
33
|
+
if hit
|
|
34
|
+
::ActiveSupport::Notifications.instrument(
|
|
35
|
+
"sql.active_record",
|
|
36
|
+
cache_notification_info(sql, name, binds)
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
result.dup
|
|
32
41
|
end
|
|
33
42
|
end
|
|
34
43
|
end
|