janus-ar 8.0.1 → 8.1.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/Gemfile +1 -1
- data/Gemfile.lock +24 -24
- data/README.md +24 -1
- data/janus-ar.gemspec +6 -4
- data/lib/janus-ar/adapter_extensions.rb +5 -0
- data/lib/janus-ar/client.rb +6 -6
- data/lib/janus-ar/db_console_config.rb +17 -17
- data/lib/janus-ar/logging/logger.rb +15 -15
- data/lib/janus-ar/logging/subscriber.rb +27 -27
- data/lib/janus-ar/query_director.rb +9 -3
- data/lib/janus-ar/version.rb +2 -2
- metadata +8 -31
- data/.bundle/config +0 -5
- data/.github/dependabot.yml +0 -7
- data/.github/workflows/ci.yml +0 -68
- data/.github/workflows/publish.yml +0 -25
- data/.gitignore +0 -3
- data/.rspec +0 -3
- data/.rubocop.yml +0 -29
- data/assets/.gitkeep +0 -1
- data/assets/janus-logo.png +0 -0
- data/bin/release.sh +0 -4
- data/gemfiles/activerecord_8_0.gemfile +0 -7
- data/gemfiles/activerecord_8_1.gemfile +0 -7
- data/spec/lib/janus-ar/active_record/connection_adapters/janus_mysql_adapter_spec.rb +0 -78
- data/spec/lib/janus-ar/active_record/connection_adapters/janus_trilogy_adapter_spec.rb +0 -77
- data/spec/lib/janus-ar/client_spec.rb +0 -7
- data/spec/lib/janus-ar/context_spec.rb +0 -118
- data/spec/lib/janus-ar/db_console_config_spec.rb +0 -28
- data/spec/lib/janus-ar/logging/logger_spec.rb +0 -31
- data/spec/lib/janus-ar/logging/subscriber_spec.rb +0 -58
- data/spec/lib/janus-ar/query_director_spec.rb +0 -141
- data/spec/shared_examples/a_mysql_like_server.rb +0 -155
- data/spec/spec_helper.rb +0 -46
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
RSpec.describe ActiveRecord::ConnectionAdapters::JanusMysql2Adapter do
|
|
4
|
-
subject { described_class.new(config) }
|
|
5
|
-
|
|
6
|
-
let(:database) { 'test' }
|
|
7
|
-
let(:primary_config) do
|
|
8
|
-
{
|
|
9
|
-
'username' => 'primary',
|
|
10
|
-
'password' => 'primary_password',
|
|
11
|
-
'host' => '127.0.0.1',
|
|
12
|
-
'ssl' => true,
|
|
13
|
-
'ssl_mode' => 'REQUIRED',
|
|
14
|
-
'tls_min_version' => 3,
|
|
15
|
-
}
|
|
16
|
-
end
|
|
17
|
-
let(:replica_config) do
|
|
18
|
-
{
|
|
19
|
-
'username' => 'replica',
|
|
20
|
-
'password' => 'replica_password',
|
|
21
|
-
'host' => '127.0.0.1',
|
|
22
|
-
'pool' => 500,
|
|
23
|
-
'ssl' => true,
|
|
24
|
-
'ssl_mode' => 'REQUIRED',
|
|
25
|
-
'tls_min_version' => 3,
|
|
26
|
-
}
|
|
27
|
-
end
|
|
28
|
-
let(:config) do
|
|
29
|
-
{
|
|
30
|
-
database:,
|
|
31
|
-
adapter: 'janus_mysql2',
|
|
32
|
-
janus: {
|
|
33
|
-
'primary' => primary_config,
|
|
34
|
-
'replica' => replica_config,
|
|
35
|
-
},
|
|
36
|
-
}
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
describe 'Configuration' do
|
|
40
|
-
it 'creates primary connection as expected' do
|
|
41
|
-
config = primary_config.dup.freeze
|
|
42
|
-
expect(subject.config).to eq config.merge('database' => database,
|
|
43
|
-
'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
it 'creates replica connection as expected' do
|
|
47
|
-
config = replica_config.dup.freeze
|
|
48
|
-
expect(
|
|
49
|
-
subject.replica_connection.instance_variable_get(:@config)
|
|
50
|
-
).to eq config.merge('database' => database, 'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
context 'Rails sets empty database for server connection' do
|
|
54
|
-
let(:database) { nil }
|
|
55
|
-
|
|
56
|
-
it 'creates primary connection as expected' do
|
|
57
|
-
config = primary_config.dup.freeze
|
|
58
|
-
expect(subject.config).to eq config.merge(
|
|
59
|
-
'database' => nil,
|
|
60
|
-
'flags' => ::Janus::Client::FOUND_ROWS
|
|
61
|
-
).symbolize_keys
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
it 'creates replica connection as expected' do
|
|
65
|
-
config = replica_config.dup.freeze
|
|
66
|
-
expect(
|
|
67
|
-
subject.replica_connection.instance_variable_get(:@config)
|
|
68
|
-
).to eq config.merge('database' => nil, 'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
describe 'Integration tests' do
|
|
74
|
-
let(:table_name) { 'table_name_mysql2' }
|
|
75
|
-
|
|
76
|
-
it_behaves_like 'a mysql like server'
|
|
77
|
-
end
|
|
78
|
-
end
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
RSpec.describe ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter do
|
|
4
|
-
subject { described_class.new(config) }
|
|
5
|
-
|
|
6
|
-
let(:database) { 'test' }
|
|
7
|
-
let(:primary_config) do
|
|
8
|
-
{
|
|
9
|
-
'username' => 'primary',
|
|
10
|
-
'password' => 'primary_password',
|
|
11
|
-
'host' => '127.0.0.1',
|
|
12
|
-
'ssl' => true,
|
|
13
|
-
'ssl_mode' => 'REQUIRED',
|
|
14
|
-
'tls_min_version' => Trilogy::TLS_VERSION_12,
|
|
15
|
-
}
|
|
16
|
-
end
|
|
17
|
-
let(:replica_config) do
|
|
18
|
-
{
|
|
19
|
-
'username' => 'replica',
|
|
20
|
-
'password' => 'replica_password',
|
|
21
|
-
'host' => '127.0.0.1',
|
|
22
|
-
'pool' => 500,
|
|
23
|
-
'ssl' => true,
|
|
24
|
-
'ssl_mode' => 'REQUIRED',
|
|
25
|
-
'tls_min_version' => Trilogy::TLS_VERSION_12,
|
|
26
|
-
}
|
|
27
|
-
end
|
|
28
|
-
let(:config) do
|
|
29
|
-
{
|
|
30
|
-
database:,
|
|
31
|
-
adapter: 'janus_trilogy',
|
|
32
|
-
janus: {
|
|
33
|
-
'primary' => primary_config,
|
|
34
|
-
'replica' => replica_config,
|
|
35
|
-
},
|
|
36
|
-
}
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
describe 'Configuration' do
|
|
40
|
-
# Trilogy enables FOUND_ROWS via the `found_rows` option (not mysql2-style
|
|
41
|
-
# flags), and ActiveRecord's TrilogyAdapter forces it on. We assert it is
|
|
42
|
-
# present even though the supplied config omits it.
|
|
43
|
-
it 'creates primary connection as expected' do
|
|
44
|
-
config = primary_config.dup.freeze
|
|
45
|
-
expect(subject.config).to eq config.merge('database' => database, 'found_rows' => true).symbolize_keys
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
it 'creates replica connection as expected' do
|
|
49
|
-
config = replica_config.dup.freeze
|
|
50
|
-
expect(
|
|
51
|
-
subject.replica_connection.instance_variable_get(:@config)
|
|
52
|
-
).to eq config.merge('database' => database, 'found_rows' => true).symbolize_keys
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
context 'Rails sets empty database for server connection' do
|
|
56
|
-
let(:database) { nil }
|
|
57
|
-
|
|
58
|
-
it 'creates primary connection as expected' do
|
|
59
|
-
config = primary_config.dup.freeze
|
|
60
|
-
expect(subject.config).to eq config.merge('database' => nil, 'found_rows' => true).symbolize_keys
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
it 'creates replica connection as expected' do
|
|
64
|
-
config = replica_config.dup.freeze
|
|
65
|
-
expect(
|
|
66
|
-
subject.replica_connection.instance_variable_get(:@config)
|
|
67
|
-
).to eq config.merge('database' => nil, 'found_rows' => true).symbolize_keys
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
describe 'Integration tests' do
|
|
73
|
-
let(:table_name) { 'table_name_trilogy' }
|
|
74
|
-
|
|
75
|
-
it_behaves_like 'a mysql like server'
|
|
76
|
-
end
|
|
77
|
-
end
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'janus-ar/context'
|
|
4
|
-
require 'active_support/executor'
|
|
5
|
-
|
|
6
|
-
RSpec.describe Janus::Context do
|
|
7
|
-
after(:each) { described_class.release_all }
|
|
8
|
-
|
|
9
|
-
describe '#initialize' do
|
|
10
|
-
it 'defaults to the replica and a primary last-used connection' do
|
|
11
|
-
context = described_class.new
|
|
12
|
-
expect(context.use_primary?).to be false
|
|
13
|
-
expect(context.last_used_connection).to eq(:primary)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
it 'can be created pinned to the primary' do
|
|
17
|
-
expect(described_class.new(primary: true).use_primary?).to be true
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
describe '#stick_to_primary' do
|
|
22
|
-
it 'sets the primary flag to true' do
|
|
23
|
-
context = described_class.new
|
|
24
|
-
context.stick_to_primary
|
|
25
|
-
expect(context.use_primary?).to be true
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
describe '#release_all' do
|
|
30
|
-
it 'resets the primary flag and the last used connection' do
|
|
31
|
-
context = described_class.new(primary: true)
|
|
32
|
-
context.used_connection(:primary)
|
|
33
|
-
context.release_all
|
|
34
|
-
expect(context.use_primary?).to be false
|
|
35
|
-
expect(context.last_used_connection).to be_nil
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
describe '#used_connection' do
|
|
40
|
-
it 'sets the last used connection' do
|
|
41
|
-
context = described_class.new
|
|
42
|
-
context.used_connection(:replica)
|
|
43
|
-
expect(context.last_used_connection).to eq(:replica)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
describe 'class-level access' do
|
|
48
|
-
it 'sticks and releases the current execution state' do
|
|
49
|
-
described_class.stick_to_primary
|
|
50
|
-
expect(described_class.use_primary?).to be true
|
|
51
|
-
described_class.release_all
|
|
52
|
-
expect(described_class.use_primary?).to be false
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
it 'isolates state between threads' do
|
|
56
|
-
described_class.stick_to_primary
|
|
57
|
-
other = Thread.new { described_class.use_primary? }.value
|
|
58
|
-
expect(other).to be false
|
|
59
|
-
expect(described_class.use_primary?).to be true
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
it 'isolates the last used connection between threads' do
|
|
63
|
-
described_class.used_connection(:replica)
|
|
64
|
-
other = Thread.new do
|
|
65
|
-
described_class.used_connection(:primary)
|
|
66
|
-
described_class.last_used_connection
|
|
67
|
-
end.value
|
|
68
|
-
expect(other).to eq(:primary)
|
|
69
|
-
expect(described_class.last_used_connection).to eq(:replica)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
it 'is safe to release a context that was never touched' do
|
|
73
|
-
Thread.new do
|
|
74
|
-
expect { described_class.release_all }.not_to raise_error
|
|
75
|
-
expect(described_class.use_primary?).to be false
|
|
76
|
-
end.join
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
describe '.install_reset_hook' do
|
|
81
|
-
let(:executor) { Class.new(ActiveSupport::Executor) }
|
|
82
|
-
|
|
83
|
-
it 'releases the context at the start of every executor run' do
|
|
84
|
-
described_class.install_reset_hook(executor)
|
|
85
|
-
described_class.stick_to_primary
|
|
86
|
-
|
|
87
|
-
stuck_inside = nil
|
|
88
|
-
executor.wrap { stuck_inside = described_class.use_primary? }
|
|
89
|
-
|
|
90
|
-
expect(stuck_inside).to be false
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
it 'stops stickiness leaking from a previous run into the next' do
|
|
94
|
-
described_class.install_reset_hook(executor)
|
|
95
|
-
|
|
96
|
-
executor.wrap { described_class.stick_to_primary }
|
|
97
|
-
|
|
98
|
-
leaked = nil
|
|
99
|
-
executor.wrap { leaked = described_class.use_primary? }
|
|
100
|
-
expect(leaked).to be false
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
it 'still starts the next run clean after the previous run raised' do
|
|
104
|
-
described_class.install_reset_hook(executor)
|
|
105
|
-
|
|
106
|
-
expect do
|
|
107
|
-
executor.wrap do
|
|
108
|
-
described_class.stick_to_primary
|
|
109
|
-
raise 'boom'
|
|
110
|
-
end
|
|
111
|
-
end.to raise_error('boom')
|
|
112
|
-
|
|
113
|
-
leaked = nil
|
|
114
|
-
executor.wrap { leaked = described_class.use_primary? }
|
|
115
|
-
expect(leaked).to be false
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'janus-ar/db_console_config'
|
|
4
|
-
|
|
5
|
-
RSpec.describe Janus::DbConsoleConfig do
|
|
6
|
-
subject(:console_config) { described_class.new(db_config) }
|
|
7
|
-
|
|
8
|
-
let(:db_config) do
|
|
9
|
-
instance_double(
|
|
10
|
-
ActiveRecord::DatabaseConfigurations::HashConfig,
|
|
11
|
-
configuration_hash: {
|
|
12
|
-
database: 'my_database',
|
|
13
|
-
janus: {
|
|
14
|
-
'primary' => { 'host' => 'primary.local', 'username' => 'primary' },
|
|
15
|
-
'replica' => { 'host' => 'replica.local', 'username' => 'replica' },
|
|
16
|
-
},
|
|
17
|
-
}
|
|
18
|
-
)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
it 'exposes the replica configuration with symbol keys so dbconsole connects to a replica' do
|
|
22
|
-
expect(console_config.configuration_hash).to eq(host: 'replica.local', username: 'replica')
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
it 'exposes the database name from the top-level config' do
|
|
26
|
-
expect(console_config.database).to eq('my_database')
|
|
27
|
-
end
|
|
28
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
RSpec.describe Janus::Logging::Logger do
|
|
4
|
-
describe '.log' do
|
|
5
|
-
let(:logger) { double('logger') }
|
|
6
|
-
|
|
7
|
-
before do
|
|
8
|
-
described_class.logger = logger
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
it 'logs the message with the specified format' do
|
|
12
|
-
expect(logger).to receive(:send).with(:info, '[Janus] Test message')
|
|
13
|
-
described_class.log('Test message', :info)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
it 'does not log the message if logger is not set' do
|
|
17
|
-
described_class.logger = nil
|
|
18
|
-
expect(logger).not_to receive(:send)
|
|
19
|
-
described_class.log('Test message', :info)
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
describe '.logger=' do
|
|
24
|
-
let(:logger) { double('logger') }
|
|
25
|
-
|
|
26
|
-
it 'sets the logger' do
|
|
27
|
-
described_class.logger = logger
|
|
28
|
-
expect(described_class.logger).to eq(logger)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'janus-ar/context'
|
|
4
|
-
require 'janus-ar/logging/subscriber'
|
|
5
|
-
|
|
6
|
-
RSpec.describe Janus::Logging::Subscriber do
|
|
7
|
-
# A minimal stand-in for ActiveRecord::LogSubscriber: it records the event it
|
|
8
|
-
# is given so we can assert on the (possibly rewritten) payload name.
|
|
9
|
-
let(:base_class) do
|
|
10
|
-
Class.new do
|
|
11
|
-
attr_reader :received_event
|
|
12
|
-
|
|
13
|
-
def sql(event)
|
|
14
|
-
@received_event = event
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
let(:subscriber) { base_class.new.extend(described_class) }
|
|
19
|
-
let(:event) { instance_double(ActiveSupport::Notifications::Event, payload: { name: 'User Load' }) }
|
|
20
|
-
|
|
21
|
-
after(:each) { Janus::Context.release_all }
|
|
22
|
-
|
|
23
|
-
it 'tags the log name with the last used connection' do
|
|
24
|
-
Janus::Context.used_connection(:replica)
|
|
25
|
-
|
|
26
|
-
subscriber.sql(event)
|
|
27
|
-
|
|
28
|
-
expect(event.payload[:name]).to eq('[replica] User Load')
|
|
29
|
-
expect(subscriber.received_event).to eq(event)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
it 'reflects the primary connection' do
|
|
33
|
-
Janus::Context.used_connection(:primary)
|
|
34
|
-
|
|
35
|
-
subscriber.sql(event)
|
|
36
|
-
|
|
37
|
-
expect(event.payload[:name]).to eq('[primary] User Load')
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
it 'leaves the name unchanged when no connection has been used yet' do
|
|
41
|
-
Janus::Context.release_all
|
|
42
|
-
|
|
43
|
-
subscriber.sql(event)
|
|
44
|
-
|
|
45
|
-
expect(event.payload[:name]).to eq('User Load')
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
Janus::Logging::Subscriber::IGNORE_PAYLOAD_NAMES.each do |ignored|
|
|
49
|
-
it "does not tag #{ignored} statements" do
|
|
50
|
-
Janus::Context.used_connection(:replica)
|
|
51
|
-
ignored_event = instance_double(ActiveSupport::Notifications::Event, payload: { name: ignored })
|
|
52
|
-
|
|
53
|
-
subscriber.sql(ignored_event)
|
|
54
|
-
|
|
55
|
-
expect(ignored_event.payload[:name]).to eq(ignored)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
RSpec.describe Janus::QueryDirector do
|
|
4
|
-
describe 'Constants' do
|
|
5
|
-
it { expect(described_class::SQL_SKIP_ALL_MATCHERS).to eq [/\A\s*set\s+local\s/i] }
|
|
6
|
-
it {
|
|
7
|
-
expect(described_class::SQL_PRIMARY_MATCHERS).to eq(
|
|
8
|
-
[
|
|
9
|
-
/\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i,
|
|
10
|
-
/\A\s*select.+(nextval|currval|lastval|get_lock|release_lock|pg_advisory_lock|pg_advisory_unlock)\(/i,
|
|
11
|
-
/\A\s*show/i
|
|
12
|
-
]
|
|
13
|
-
)
|
|
14
|
-
}
|
|
15
|
-
it { expect(described_class::SQL_REPLICA_MATCHERS).to eq([/\A\s*(select|with.+\)\s*select)\s/i]) }
|
|
16
|
-
it { expect(described_class::SQL_ALL_MATCHERS).to eq([/\A\s*set\s/i]) }
|
|
17
|
-
|
|
18
|
-
it { expect(described_class::ALL).to eq :all }
|
|
19
|
-
it { expect(described_class::REPLICA).to eq :replica }
|
|
20
|
-
it { expect(described_class::PRIMARY).to eq :primary }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
describe '#where_to_send?' do
|
|
24
|
-
let(:open_transactions) { 0 }
|
|
25
|
-
|
|
26
|
-
before(:each) { Janus::Context.release_all }
|
|
27
|
-
|
|
28
|
-
context 'with reads' do
|
|
29
|
-
{
|
|
30
|
-
'plain select' => 'SELECT * FROM users',
|
|
31
|
-
'lower case select' => 'select * from users',
|
|
32
|
-
'CTE select' => 'WITH recent AS (SELECT * FROM users) SELECT * FROM recent',
|
|
33
|
-
'leading whitespace' => "\n\t SELECT 1",
|
|
34
|
-
}.each do |label, query|
|
|
35
|
-
it "routes a #{label} to the replica" do
|
|
36
|
-
expect(described_class.new(query, 0).where_to_send?).to eq(:replica)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
context 'with writes' do
|
|
42
|
-
# Every one of these used to be sent to the replica because the router
|
|
43
|
-
# defaulted unknown statements there. They are genuine writes and must
|
|
44
|
-
# reach the primary.
|
|
45
|
-
%w(
|
|
46
|
-
INSERT UPDATE DELETE REPLACE RENAME CALL LOAD OPTIMIZE ANALYZE REPAIR
|
|
47
|
-
CREATE DROP TRUNCATE ALTER
|
|
48
|
-
).each do |verb|
|
|
49
|
-
it "routes #{verb} to the primary" do
|
|
50
|
-
sql = "#{verb} something that is not a read"
|
|
51
|
-
expect(described_class.new(sql, 0).where_to_send?).to eq(:primary)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
it 'routes a write annotated with a leading comment to the primary' do
|
|
56
|
-
sql = '/* app:web,controller:orders */ INSERT INTO orders (id) VALUES (1)'
|
|
57
|
-
expect(described_class.new(sql, 0).where_to_send?).to eq(:primary)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
it 'routes a read annotated with a leading comment to the replica' do
|
|
61
|
-
sql = '/* app:web */ SELECT * FROM orders'
|
|
62
|
-
expect(described_class.new(sql, 0).where_to_send?).to eq(:replica)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
context 'with comments and whitespace around the statement' do
|
|
67
|
-
{
|
|
68
|
-
'a line (--) comment then a read' => "-- audit trail\nSELECT * FROM orders",
|
|
69
|
-
'a hash (#) comment then a read' => "# cache key\nSELECT * FROM orders",
|
|
70
|
-
'several stacked leading comments then a read' => "/* a */ /* b */\n\t SELECT 1",
|
|
71
|
-
'tabs and newlines then a read' => "\n\t SELECT 1",
|
|
72
|
-
}.each do |label, query|
|
|
73
|
-
it "routes #{label} to the replica" do
|
|
74
|
-
expect(described_class.new(query, 0).where_to_send?).to eq(:replica)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
{
|
|
79
|
-
'a line (--) comment then a write' => "-- triggered by job 42\nUPDATE orders SET state = 1",
|
|
80
|
-
'a hash (#) comment then a write' => "# backfill\nDELETE FROM orders",
|
|
81
|
-
'a block comment then a write' => "/* migration */ ALTER TABLE orders ADD COLUMN x INT",
|
|
82
|
-
}.each do |label, query|
|
|
83
|
-
it "routes #{label} to the primary" do
|
|
84
|
-
expect(described_class.new(query, 0).where_to_send?).to eq(:primary)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
context 'with locking reads' do
|
|
90
|
-
it 'routes SELECT ... FOR UPDATE to the primary' do
|
|
91
|
-
expect(described_class.new('SELECT * FROM users FOR UPDATE', 0).where_to_send?).to eq(:primary)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
it 'routes SELECT ... LOCK IN SHARE MODE to the primary' do
|
|
95
|
-
expect(described_class.new('SELECT * FROM users LOCK IN SHARE MODE', 0).where_to_send?).to eq(:primary)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
it 'routes advisory lock reads to the primary' do
|
|
99
|
-
expect(described_class.new("SELECT get_lock('x', 0)", 0).where_to_send?).to eq(:primary)
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
context 'with SHOW' do
|
|
104
|
-
it 'routes SHOW statements to the primary' do
|
|
105
|
-
expect(described_class.new('SHOW TABLES', 0).where_to_send?).to eq(:primary)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
context 'with SET' do
|
|
110
|
-
it 'routes SET to all connections' do
|
|
111
|
-
expect(described_class.new('SET sql_mode = ?', 0).where_to_send?).to eq(:all)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
it 'does not broadcast SET LOCAL to all connections' do
|
|
115
|
-
expect(described_class.new('SET LOCAL sql_mode = ?', 0).where_to_send?).to eq(:primary)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
context 'when the context is stuck to the primary' do
|
|
120
|
-
before(:each) { Janus::Context.stick_to_primary }
|
|
121
|
-
|
|
122
|
-
it 'routes an otherwise replica-bound read to the primary' do
|
|
123
|
-
expect(described_class.new('SELECT * FROM users', 0).where_to_send?).to eq(:primary)
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
context 'when inside a transaction' do
|
|
128
|
-
let(:open_transactions) { 1 }
|
|
129
|
-
|
|
130
|
-
it 'routes reads to the primary so they can see uncommitted writes' do
|
|
131
|
-
expect(described_class.new('SELECT * FROM users', open_transactions).where_to_send?).to eq(:primary)
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
context 'with an unrecognised / empty statement' do
|
|
136
|
-
it 'defaults blank input to the primary' do
|
|
137
|
-
expect(described_class.new(' ', 0).where_to_send?).to eq(:primary)
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
end
|