janus-ar 0.14.0 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b4012fc9260dbab0c2d86b585d800968f45dd463e39d63bc512320ce3c78665
4
- data.tar.gz: 4790e30c5f89242b39174d3ae4de12cb4c848461c494133dbdd4e3f5caee4221
3
+ metadata.gz: 32ffaeda6aafd97e8cd5b25520c543c8f142eb7dbcd0cf17ce733d8610c115ac
4
+ data.tar.gz: 3cb69a2703b091e7fe7d7a096558320f6324085ded5e6b331c26d29018b42f89
5
5
  SHA512:
6
- metadata.gz: 3d2f60f04c8e234453e61a6a549099e905389b9e845707611cfb4d3f81b93490fbfd8dd9727fa5c90700d3183ec88de4bb4b503e9782baa9b9736879f811916b
7
- data.tar.gz: b70f8246e567c80c8dba2423127f573a7988483d9e5078c7e9b7efae78d0f6ef6efeb2cbefa58c1b145291f8930b171d255dbfad43adcc85bf03ac0f8dae9d85
6
+ metadata.gz: f0d2f4dffc0e09016f1932188308d3984c6fe90c7e9d001e0c9d933ec7194082886006e8381315a0bda4f15fcf23d78a7f9ea9841d82f3194be79fbb648400e9
7
+ data.tar.gz: b28fbd3d46a9b5c4df5ba2b8f27cf28826a776a476b07e55f295e1df62e4121bfd1dcd1db3bd2654784f00e801603633cf44dd876f3efaad522cb290c5cb9d37
@@ -17,12 +17,12 @@ jobs:
17
17
  name: Ruby ${{ matrix.ruby }}
18
18
  services:
19
19
  mysql:
20
- image: mysql:5.7
20
+ image: mysql:8
21
21
  env:
22
22
  MYSQL_DATABASE: test
23
23
  MYSQL_ROOT_PASSWORD: password
24
- MYSQL_USER: primary
25
- MYSQL_PASSWORD: primary_password
24
+ MYSQL_USER: test
25
+ MYSQL_PASSWORD: test_password
26
26
  ports:
27
27
  - 3306:3306
28
28
  options: >-
@@ -37,7 +37,12 @@ jobs:
37
37
  ruby-version: ${{ matrix.ruby }}
38
38
  bundler-cache: true
39
39
  - run: |
40
- mysql -u root -p${{ env.MYSQL_PASSWORD || 'password' }} -h 127.0.0.1 -e "CREATE USER 'replica'@'%' IDENTIFIED BY 'replica_password';GRANT SELECT ON test.* TO 'replica'@'%';FLUSH PRIVILEGES;"
40
+ mysql -e "CREATE USER 'replica'@'%' IDENTIFIED WITH mysql_native_password BY 'replica_password';" -u root -p${{ env.MYSQL_PASSWORD || 'password' }} -h 127.0.0.1
41
+ mysql -e "GRANT SELECT ON test.* TO 'replica'@'%'" -u root -p${{ env.MYSQL_PASSWORD || 'password' }} -h 127.0.0.1
42
+
43
+ mysql -e "CREATE USER 'primary'@'%' IDENTIFIED WITH mysql_native_password BY 'primary_password';" -u root -p${{ env.MYSQL_PASSWORD || 'password' }} -h 127.0.0.1
44
+ mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'primary'@'%';" -u root -p${{ env.MYSQL_PASSWORD || 'password' }} -h 127.0.0.1
45
+ mysql -e "FLUSH PRIVILEGES;" -u root -p${{ env.MYSQL_PASSWORD || 'password' }} -h 127.0.0.1
41
46
  - run: |
42
47
  bundle exec rspec
43
48
  env:
data/.rubocop.yml CHANGED
@@ -19,4 +19,4 @@ Style/GlobalVars:
19
19
  - 'spec/**/*'
20
20
 
21
21
  Metrics/AbcSize:
22
- Max: 22
22
+ Max: 25
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- janus-ar (0.13.0)
4
+ janus-ar (0.15.0)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
@@ -39,7 +39,7 @@ GEM
39
39
  mutex_m (0.2.0)
40
40
  mysql2 (0.5.6)
41
41
  parallel (1.24.0)
42
- parser (3.3.0.5)
42
+ parser (3.3.1.0)
43
43
  ast (~> 2.4.1)
44
44
  racc
45
45
  pry (0.14.2)
@@ -64,7 +64,7 @@ GEM
64
64
  diff-lcs (>= 1.2.0, < 2.0)
65
65
  rspec-support (~> 3.13.0)
66
66
  rspec-support (3.13.1)
67
- rubocop (1.63.1)
67
+ rubocop (1.63.4)
68
68
  json (~> 2.3)
69
69
  language_server-protocol (>= 3.17.0)
70
70
  parallel (~> 1.10)
@@ -100,6 +100,7 @@ GEM
100
100
  rubocop (>= 0.90.0)
101
101
  ruby-progressbar (1.13.0)
102
102
  timeout (0.4.1)
103
+ trilogy (2.8.0)
103
104
  tzinfo (2.0.6)
104
105
  concurrent-ruby (~> 1.0)
105
106
  unicode-display_width (2.5.0)
@@ -121,6 +122,7 @@ DEPENDENCIES
121
122
  rubocop-rails (~> 2.24.0)
122
123
  rubocop-rspec
123
124
  rubocop-thread_safety
125
+ trilogy
124
126
 
125
127
  BUNDLED WITH
126
128
  2.4.22
data/README.md CHANGED
@@ -11,12 +11,16 @@
11
11
  [![CI](https://github.com/OLIOEX/janus-ar/actions/workflows/ci.yml/badge.svg)](https://github.com/OLIOEX/janus-ar/actions/workflows/ci.yml)
12
12
  [![Gem Version](https://badge.fury.io/rb/janus-ar.svg)](https://badge.fury.io/rb/janus-ar)
13
13
 
14
- Janus ActiveRecord is generic primary/replica proxy for ActiveRecord 7.1+ and MySQL. It handles the switching of connections between primary and replica database servers. It comes with an ActiveRecord database adapter implementation.
14
+ Janus ActiveRecord is generic primary/replica proxy for ActiveRecord 7.1+ and MySQL (via `mysql2` and `trilogy`). It handles the switching of connections between primary and replica database servers. It comes with an ActiveRecord database adapter implementation.
15
+
16
+ Note: Trilogy support is experimental at this stage.
15
17
 
16
18
  Janus is heavily inspired by [Makara](https://github.com/instacart/makara) from TaskRabbit and then Instacart. Unfortunately this project is unmaintained and broke for us with Rails 7.1. This is an attempt to start afresh on the project. It is definitely not as fully featured as Makara at this stage.
17
19
 
18
20
  Learn more about its origins: [https://tech.olioex.com/ruby/2024/04/16/introducing-janus.html](https://tech.olioex.com/ruby/2024/04/16/introducing-janus.html).
19
21
 
22
+ Notes: GEM is currently tested with MySQL 8, Ruby 3.2, ActiveRecord 7.1+
23
+
20
24
  ## Installation
21
25
 
22
26
  Use the current version of the gem from [rubygems](https://rubygems.org/gems/janus-ar) in your `Gemfile`.
@@ -48,6 +52,18 @@ development:
48
52
  password: ithappenstobedifferent
49
53
  host: replica-host.local
50
54
  ```
55
+ Note: For `trilogy` please use adapter "janus_trilogy". You'll probably need to add the following to your configuration to have it connect:
56
+
57
+ ```yml
58
+ ssl: true
59
+ ssl_mode: 'REQUIRED'
60
+ tls_min_version: 3
61
+ ```
62
+
63
+ `tls_min_version` here refers to TLS1.2.
64
+
65
+ Otherwise you will get an error like the following (see https://github.com/trilogy-libraries/trilogy/issues/26):
66
+ > trilogy_auth_recv: caching_sha2_password requires either TCP with TLS or a unix socket: TRILOGY_UNSUPPORTED"
51
67
 
52
68
  ### Forcing connections
53
69
 
data/janus-ar.gemspec CHANGED
@@ -24,6 +24,7 @@ Gem::Specification.new do |gem|
24
24
  gem.add_development_dependency 'activerecord', '>= 7.1.0'
25
25
  gem.add_development_dependency 'activesupport', '>= 7.1.0'
26
26
  gem.add_development_dependency 'mysql2'
27
+ gem.add_development_dependency 'trilogy'
27
28
  gem.add_development_dependency 'pry'
28
29
  gem.add_development_dependency 'rake'
29
30
  gem.add_development_dependency 'rspec', '~> 3'
@@ -24,15 +24,6 @@ module ActiveRecord
24
24
  module ConnectionAdapters
25
25
  class JanusMysql2Adapter < ActiveRecord::ConnectionAdapters::Mysql2Adapter
26
26
  FOUND_ROWS = 'FOUND_ROWS'
27
- SQL_PRIMARY_MATCHERS = [
28
- /\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i,
29
- /\A\s*select.+(nextval|currval|lastval|get_lock|release_lock|pg_advisory_lock|pg_advisory_unlock)\(/i,
30
- /\A\s*show/i
31
- ].freeze
32
- SQL_REPLICA_MATCHERS = [/\A\s*(select|with.+\)\s*select)\s/i].freeze
33
- SQL_ALL_MATCHERS = [/\A\s*set\s/i].freeze
34
- SQL_SKIP_ALL_MATCHERS = [/\A\s*set\s+local\s/i].freeze
35
- WRITE_PREFIXES = %w(INSERT UPDATE DELETE LOCK CREATE GRANT DROP ALTER TRUNCATE FLUSH).freeze
36
27
 
37
28
  attr_reader :config
38
29
 
@@ -56,30 +47,46 @@ module ActiveRecord
56
47
  update_config
57
48
  end
58
49
 
50
+ def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
51
+ case where_to_send?(sql)
52
+ when :all
53
+ send_to_replica(sql, connection: :all, method: :raw_execute)
54
+ super
55
+ when :replica
56
+ send_to_replica(sql, connection: :replica, method: :raw_execute)
57
+ else
58
+ Janus::Context.stick_to_primary if write_query?(sql)
59
+ Janus::Context.used_connection(:primary)
60
+ super
61
+ end
62
+ end
63
+
59
64
  def execute(sql)
60
- if should_send_to_all?(sql)
65
+ case where_to_send?(sql)
66
+ when :all
61
67
  send_to_replica(sql, connection: :all, method: :execute)
62
- return super(sql)
68
+ super(sql)
69
+ when :replica
70
+ send_to_replica(sql, connection: :replica, method: :execute)
71
+ else
72
+ Janus::Context.stick_to_primary if write_query?(sql)
73
+ Janus::Context.used_connection(:primary)
74
+ super(sql)
63
75
  end
64
- return send_to_replica(sql, connection: :replica, method: :execute) if can_go_to_replica?(sql)
65
-
66
- Janus::Context.stick_to_primary if write_query?(sql)
67
- Janus::Context.used_connection(:primary)
68
-
69
- super(sql)
70
76
  end
71
77
 
72
78
  def execute_and_free(sql, name = nil, async: false)
73
- if should_send_to_all?(sql)
74
- send_to_replica(sql, name, connection: :all)
75
- return super(sql, name, async:)
79
+ case where_to_send?(sql)
80
+ when :all
81
+ send_to_replica(sql, connection: :all, method: :execute)
82
+ super(sql, name, async:)
83
+ when :replica
84
+ send_to_replica(sql, connection: :replica, method: :execute)
85
+ else
86
+ Janus::Context.stick_to_primary if write_query?(sql)
87
+ Janus::Context.used_connection(:primary)
88
+ super(sql, name, async:)
76
89
  end
77
- return send_to_replica(sql, connection: :replica) if can_go_to_replica?(sql)
78
-
79
- Janus::Context.stick_to_primary if write_query?(sql)
80
- Janus::Context.used_connection(:primary)
81
-
82
- super(sql, name, async:)
83
90
  end
84
91
 
85
92
  def connect!(...)
@@ -108,41 +115,28 @@ module ActiveRecord
108
115
 
109
116
  private
110
117
 
111
- def should_send_to_all?(sql)
112
- SQL_ALL_MATCHERS.any? { |matcher| sql =~ matcher } && SQL_SKIP_ALL_MATCHERS.none? { |matcher| sql =~ matcher }
113
- end
114
-
115
- def can_go_to_replica?(sql)
116
- !should_go_to_primary?(sql)
117
- end
118
-
119
- def should_go_to_primary?(sql)
120
- Janus::Context.use_primary? ||
121
- write_query?(sql) ||
122
- open_transactions.positive? ||
123
- SQL_PRIMARY_MATCHERS.any? { |matcher| sql =~ matcher }
118
+ def where_to_send?(sql)
119
+ Janus::QueryDirector.new(sql, open_transactions).where_to_send?
124
120
  end
125
121
 
126
122
  def send_to_replica(sql, connection: nil, method: :exec_query)
127
123
  Janus::Context.used_connection(connection) if connection
128
124
  if method == :execute
129
125
  replica_connection.execute(sql)
126
+ elsif method == :raw_execute
127
+ replica_connection.execute(sql)
130
128
  else
131
129
  replica_connection.exec_query(sql)
132
130
  end
133
131
  end
134
132
 
135
- def write_query?(sql)
136
- WRITE_PREFIXES.include?(sql.upcase.split(' ').first)
137
- end
138
-
139
133
  def update_config
140
134
  @config[:flags] ||= 0
141
135
 
142
136
  if @config[:flags].is_a? Array
143
137
  @config[:flags].push FOUND_ROWS
144
138
  else
145
- @config[:flags] |= ::Mysql2::Client::FOUND_ROWS
139
+ @config[:flags] |= ::Janus::Client::FOUND_ROWS
146
140
  end
147
141
  end
148
142
  end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/connection_adapters/abstract_adapter'
4
+ require 'active_record/connection_adapters/trilogy_adapter'
5
+ require_relative '../../janus'
6
+
7
+ module ActiveRecord
8
+ module ConnectionHandling
9
+ def janus_trilogy_connection(config)
10
+ ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter.new(config)
11
+ end
12
+ end
13
+ end
14
+
15
+ module ActiveRecord
16
+ class Base
17
+ def self.janus_trilogy_adapter_class
18
+ ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter
19
+ end
20
+ end
21
+ end
22
+
23
+ module ActiveRecord
24
+ module ConnectionAdapters
25
+ class JanusTrilogyAdapter < ActiveRecord::ConnectionAdapters::TrilogyAdapter
26
+ FOUND_ROWS = 'FOUND_ROWS'
27
+
28
+ attr_reader :config
29
+
30
+ class << self
31
+ def dbconsole(config, options = {})
32
+ connection_config = Janus::DbConsoleConfig.new(config)
33
+
34
+ super(connection_config, options)
35
+ end
36
+ end
37
+
38
+ def initialize(*args)
39
+ args[0][:janus]['replica']['database'] = args[0][:database]
40
+ args[0][:janus]['primary']['database'] = args[0][:database]
41
+
42
+ @replica_config = args[0][:janus]['replica'].symbolize_keys
43
+ args[0] = args[0][:janus]['primary'].symbolize_keys
44
+
45
+ super(*args)
46
+ @connection_parameters ||= args[0]
47
+ update_config
48
+ end
49
+
50
+ def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
51
+ case where_to_send?(sql)
52
+ when :all
53
+ send_to_replica(sql, connection: :all, method: :execute)
54
+ super
55
+ when :replica
56
+ send_to_replica(sql, connection: :replica, method: :execute)
57
+ else
58
+ Janus::Context.stick_to_primary if write_query?(sql)
59
+ Janus::Context.used_connection(:primary)
60
+ super
61
+ end
62
+ end
63
+
64
+ def execute(sql)
65
+ case where_to_send?(sql)
66
+ when :all
67
+ send_to_replica(sql, connection: :all, method: :execute)
68
+ super(sql)
69
+ when :replica
70
+ send_to_replica(sql, connection: :replica, method: :execute)
71
+ else
72
+ Janus::Context.stick_to_primary if write_query?(sql)
73
+ Janus::Context.used_connection(:primary)
74
+ super(sql)
75
+ end
76
+ end
77
+
78
+ def execute_and_free(sql, name = nil, async: false)
79
+ case where_to_send?(sql)
80
+ when :all
81
+ send_to_replica(sql, connection: :all, method: :execute)
82
+ super(sql, name, async:)
83
+ when :replica
84
+ send_to_replica(sql, connection: :replica, method: :execute)
85
+ else
86
+ Janus::Context.stick_to_primary if write_query?(sql)
87
+ Janus::Context.used_connection(:primary)
88
+ super(sql, name, async:)
89
+ end
90
+ end
91
+
92
+ def connect!(...)
93
+ replica_connection.connect!(...)
94
+ super
95
+ end
96
+
97
+ def reconnect!(...)
98
+ replica_connection.reconnect!(...)
99
+ super
100
+ end
101
+
102
+ def disconnect!(...)
103
+ replica_connection.disconnect!(...)
104
+ super
105
+ end
106
+
107
+ def clear_cache!(...)
108
+ replica_connection.clear_cache!(...)
109
+ super
110
+ end
111
+
112
+ def replica_connection
113
+ @replica_connection ||= ActiveRecord::ConnectionAdapters::TrilogyAdapter.new(@replica_config)
114
+ end
115
+
116
+ private
117
+
118
+ def where_to_send?(sql)
119
+ Janus::QueryDirector.new(sql, open_transactions).where_to_send?
120
+ end
121
+
122
+ def send_to_replica(sql, connection: nil, method: :exec_query)
123
+ Janus::Context.used_connection(connection) if connection
124
+ if method == :execute
125
+ replica_connection.execute(sql)
126
+ elsif method == :raw_execute
127
+ replica_connection.execute(sql)
128
+ else
129
+ replica_connection.exec_query(sql)
130
+ end
131
+ end
132
+
133
+ def update_config
134
+ @config[:flags] ||= 0
135
+
136
+ if @config[:flags].is_a? Array
137
+ @config[:flags].push FOUND_ROWS
138
+ else
139
+ @config[:flags] |= ::Janus::Client::FOUND_ROWS
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module Janus
3
+ class Client
4
+ FOUND_ROWS = 2
5
+ end
6
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+ module Janus
3
+ class QueryDirector
4
+ ALL = :all
5
+ REPLICA = :replica
6
+ PRIMARY = :primary
7
+
8
+ SQL_PRIMARY_MATCHERS = [
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
+ ].freeze
13
+ SQL_REPLICA_MATCHERS = [/\A\s*(select|with.+\)\s*select)\s/i].freeze
14
+ SQL_ALL_MATCHERS = [/\A\s*set\s/i].freeze
15
+ SQL_SKIP_ALL_MATCHERS = [/\A\s*set\s+local\s/i].freeze
16
+ WRITE_PREFIXES = %w(INSERT UPDATE DELETE LOCK CREATE GRANT DROP ALTER TRUNCATE BEGIN SAVEPOINT FLUSH).freeze
17
+
18
+ def initialize(sql, open_transactions)
19
+ @_sql = sql
20
+ @_open_transactions = open_transactions
21
+ end
22
+
23
+ def where_to_send?
24
+ if should_send_to_all?
25
+ ALL
26
+ elsif can_go_to_replica?
27
+ REPLICA
28
+ else
29
+ PRIMARY
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def should_send_to_all?
36
+ SQL_ALL_MATCHERS.any? { |matcher| @_sql =~ matcher } && SQL_SKIP_ALL_MATCHERS.none? { |matcher| @_sql =~ matcher }
37
+ end
38
+
39
+ def can_go_to_replica?
40
+ !should_go_to_primary?
41
+ end
42
+
43
+ def should_go_to_primary?
44
+ Janus::Context.use_primary? ||
45
+ write_query? ||
46
+ @_open_transactions.positive? ||
47
+ SQL_PRIMARY_MATCHERS.any? { |matcher| @_sql =~ matcher }
48
+ end
49
+
50
+ def write_query?
51
+ WRITE_PREFIXES.include?(@_sql.upcase.split(' ').first)
52
+ end
53
+ end
54
+ end
data/lib/janus/version.rb CHANGED
@@ -4,8 +4,8 @@ module Janus
4
4
  unless defined?(::Janus::VERSION)
5
5
  module VERSION
6
6
  MAJOR = 0
7
- MINOR = 14
8
- PATCH = 0
7
+ MINOR = 15
8
+ PATCH = 1
9
9
  PRE = nil
10
10
 
11
11
  def self.to_s
data/lib/janus.rb CHANGED
@@ -4,6 +4,8 @@ require 'active_support'
4
4
 
5
5
  module Janus
6
6
  autoload :Context, 'janus/context'
7
+ autoload :Client, 'janus/client'
8
+ autoload :QueryDirector, 'janus/query_director'
7
9
  autoload :VERSION, 'janus/version'
8
10
  autoload :DbConsoleConfig, 'janus/db_console_config'
9
11
 
@@ -4,19 +4,6 @@ RSpec.describe ActiveRecord::ConnectionAdapters::JanusMysql2Adapter do
4
4
  subject { described_class.new(config) }
5
5
 
6
6
  it { expect(described_class::FOUND_ROWS).to eq 'FOUND_ROWS' }
7
- it { expect(described_class::SQL_SKIP_ALL_MATCHERS).to eq [/\A\s*set\s+local\s/i] }
8
- it {
9
- expect(described_class::SQL_PRIMARY_MATCHERS).to eq(
10
- [
11
- /\A\s*select.+for update\Z/i, /select.+lock in share mode\Z/i,
12
- /\A\s*select.+(nextval|currval|lastval|get_lock|release_lock|pg_advisory_lock|pg_advisory_unlock)\(/i,
13
- /\A\s*show/i
14
- ]
15
- )
16
- }
17
- it { expect(described_class::SQL_REPLICA_MATCHERS).to eq([/\A\s*(select|with.+\)\s*select)\s/i]) }
18
- it { expect(described_class::SQL_ALL_MATCHERS).to eq([/\A\s*set\s/i]) }
19
- it { expect(described_class::WRITE_PREFIXES).to eq %w(INSERT UPDATE DELETE LOCK CREATE GRANT DROP ALTER TRUNCATE FLUSH) }
20
7
 
21
8
  let(:database) { 'test' }
22
9
  let(:primary_config) do
@@ -49,14 +36,14 @@ RSpec.describe ActiveRecord::ConnectionAdapters::JanusMysql2Adapter do
49
36
  it 'creates primary connection as expected' do
50
37
  config = primary_config.dup.freeze
51
38
  expect(subject.config).to eq config.merge('database' => database,
52
- 'flags' => ::Mysql2::Client::FOUND_ROWS).symbolize_keys
39
+ 'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
53
40
  end
54
41
 
55
42
  it 'creates replica connection as expected' do
56
43
  config = replica_config.dup.freeze
57
44
  expect(
58
45
  subject.replica_connection.instance_variable_get(:@config)
59
- ).to eq config.merge('database' => database, 'flags' => ::Mysql2::Client::FOUND_ROWS).symbolize_keys
46
+ ).to eq config.merge('database' => database, 'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
60
47
  end
61
48
 
62
49
  context 'Rails sets empty database for server connection' do
@@ -66,7 +53,7 @@ RSpec.describe ActiveRecord::ConnectionAdapters::JanusMysql2Adapter do
66
53
  config = primary_config.dup.freeze
67
54
  expect(subject.config).to eq config.merge(
68
55
  'database' => nil,
69
- 'flags' => ::Mysql2::Client::FOUND_ROWS
56
+ 'flags' => ::Janus::Client::FOUND_ROWS
70
57
  ).symbolize_keys
71
58
  end
72
59
 
@@ -74,72 +61,16 @@ RSpec.describe ActiveRecord::ConnectionAdapters::JanusMysql2Adapter do
74
61
  config = replica_config.dup.freeze
75
62
  expect(
76
63
  subject.replica_connection.instance_variable_get(:@config)
77
- ).to eq config.merge('database' => nil, 'flags' => ::Mysql2::Client::FOUND_ROWS).symbolize_keys
64
+ ).to eq config.merge('database' => nil, 'flags' => ::Janus::Client::FOUND_ROWS).symbolize_keys
78
65
  end
79
66
  end
80
67
  end
81
68
 
82
69
  describe 'Integration tests' do
83
- let(:create_test_table) { ActiveRecord::Base.connection.execute('CREATE TABLE test_table (id INT);') }
70
+ describe 'Integration tests' do
71
+ let(:table_name) { 'table_name_mysql2' }
84
72
 
85
- before(:each) do
86
- $query_logger.flush_all
87
- ActiveRecord::Base.establish_connection(config)
88
- end
89
-
90
- after(:each) do
91
- ActiveRecord::Base.connection.execute(<<-SQL
92
- SELECT CONCAT('DROP TABLE IF EXISTS `', table_name, '`;')
93
- FROM information_schema.tables
94
- WHERE table_schema = '#{database}';
95
- SQL
96
- ).to_a.map { |row| ActiveRecord::Base.connection.execute(row[0]) }
97
- end
98
-
99
- it 'can list tables' do
100
- expect(ActiveRecord::Base.connection.execute('SHOW TABLES;').to_a).to eq []
101
- end
102
-
103
- it 'can create table' do
104
- create_test_table
105
- expect(ActiveRecord::Base.connection.execute('SHOW TABLES;').to_a).to eq [%w(test_table)]
106
- end
107
-
108
- describe 'SELECT' do
109
- it 'reads from `replica` by default' do
110
- create_test_table
111
- Janus::Context.release_all
112
- $query_logger.flush_all
113
- ActiveRecord::Base.connection.execute('SELECT * FROM test_table;')
114
- expect($query_logger.queries.first).to include '[replica]'
115
- end
116
-
117
- it 'will read from primary after a write operation' do
118
- create_test_table
119
- $query_logger.flush_all
120
- ActiveRecord::Base.connection.execute('SELECT * FROM test_table;')
121
- expect($query_logger.queries.first).to include '[primary]'
122
- end
123
- end
124
-
125
- describe 'INSERT' do
126
- let(:insert_query) { 'INSERT INTO test_table SET `id` = 5;' }
127
-
128
- before(:each) do
129
- create_test_table
130
- $query_logger.flush_all
131
- Janus::Context.release_all
132
- end
133
-
134
- it 'sends INSERT query to primary' do
135
- ActiveRecord::Base.connection.execute(insert_query)
136
- expect($query_logger.queries.first).to include '[primary]'
137
- end
138
-
139
- it 'ignores case when directing queries' do
140
- ActiveRecord::Base.connection.execute(insert_query.downcase)
141
- expect($query_logger.queries.first).to include '[primary]'
142
- end
73
+ it_behaves_like 'a mysql like server'
143
74
  end
144
75
  end
145
76
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe ActiveRecord::ConnectionAdapters::JanusTrilogyAdapter do
4
+ subject { described_class.new(config) }
5
+
6
+ it { expect(described_class::FOUND_ROWS).to eq 'FOUND_ROWS' }
7
+
8
+ let(:database) { 'test' }
9
+ let(:primary_config) do
10
+ {
11
+ 'username' => 'primary',
12
+ 'password' => 'primary_password',
13
+ 'host' => '127.0.0.1',
14
+ 'ssl' => true,
15
+ 'ssl_mode' => 'REQUIRED',
16
+ 'tls_min_version' => Trilogy::TLS_VERSION_12,
17
+ }
18
+ end
19
+ let(:replica_config) do
20
+ {
21
+ 'username' => 'replica',
22
+ 'password' => 'replica_password',
23
+ 'host' => '127.0.0.1',
24
+ 'pool' => 500,
25
+ 'ssl' => true,
26
+ 'ssl_mode' => 'REQUIRED',
27
+ 'tls_min_version' => Trilogy::TLS_VERSION_12,
28
+ }
29
+ end
30
+ let(:config) do
31
+ {
32
+ database:,
33
+ adapter: 'janus_trilogy',
34
+ janus: {
35
+ 'primary' => primary_config,
36
+ 'replica' => replica_config,
37
+ },
38
+ }
39
+ end
40
+
41
+ describe 'Configuration' do
42
+ it 'creates primary connection as expected' do
43
+ config = primary_config.dup.freeze
44
+ expect(subject.config).to eq config.merge('database' => database,
45
+ 'flags' => ::Janus::Client::FOUND_ROWS).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).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(
61
+ 'database' => nil,
62
+ 'flags' => ::Janus::Client::FOUND_ROWS
63
+ ).symbolize_keys
64
+ end
65
+
66
+ it 'creates replica connection as expected' do
67
+ config = replica_config.dup.freeze
68
+ expect(
69
+ subject.replica_connection.instance_variable_get(:@config)
70
+ ).to eq config.merge('database' => nil).symbolize_keys
71
+ end
72
+ end
73
+ end
74
+
75
+ describe 'Integration tests' do
76
+ let(:table_name) { 'table_name_trilogy' }
77
+
78
+ it_behaves_like 'a mysql like server'
79
+ end
80
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'janus/client'
4
+
5
+ RSpec.describe Janus::Client do
6
+ it { expect(described_class::FOUND_ROWS).to eq 2 }
7
+ end
@@ -0,0 +1,58 @@
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
+ it {
18
+ expect(described_class::WRITE_PREFIXES).to eq %w(INSERT UPDATE DELETE LOCK CREATE GRANT DROP ALTER TRUNCATE BEGIN SAVEPOINT FLUSH)
19
+ }
20
+
21
+ it { expect(described_class::ALL).to eq :all }
22
+ it { expect(described_class::REPLICA).to eq :replica }
23
+ it { expect(described_class::PRIMARY).to eq :primary }
24
+ end
25
+
26
+ describe '#where_to_send?' do
27
+ before(:each) do
28
+ Janus::Context.release_all
29
+ end
30
+
31
+ context 'when should send to all' do
32
+ it 'returns :all' do
33
+ sql = 'SET foo = bar'
34
+ open_transactions = 0
35
+ query_director = described_class.new(sql, open_transactions)
36
+ expect(query_director.where_to_send?).to eq(:all)
37
+ end
38
+ end
39
+
40
+ context 'when can go to replica' do
41
+ it 'returns :replica' do
42
+ sql = 'SELECT * FROM users'
43
+ open_transactions = 0
44
+ query_director = described_class.new(sql, open_transactions)
45
+ expect(query_director.where_to_send?).to eq(:replica)
46
+ end
47
+ end
48
+
49
+ context 'when should go to primary' do
50
+ it 'returns :primary' do
51
+ sql = 'INSERT INTO users (name) VALUES ("John")'
52
+ open_transactions = 0
53
+ query_director = described_class.new(sql, open_transactions)
54
+ expect(query_director.where_to_send?).to eq(:primary)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+ RSpec.shared_examples 'a mysql like server' do
3
+ let(:create_test_table) { ActiveRecord::Base.connection.execute("CREATE TABLE `#{table_name}` (id INT);") }
4
+
5
+ before(:each) do
6
+ $query_logger.flush_all
7
+ ActiveRecord::Base.establish_connection(config)
8
+ end
9
+
10
+ after(:each) do
11
+ ActiveRecord::Base.connection.execute(<<-SQL
12
+ SELECT CONCAT('DROP TABLE IF EXISTS `', table_name, '`;')
13
+ FROM information_schema.tables
14
+ WHERE table_schema = '#{database}';
15
+ SQL
16
+ ).to_a.map { |row| ActiveRecord::Base.connection.execute(row[0]) }
17
+ end
18
+
19
+ it 'can list tables' do
20
+ expect(ActiveRecord::Base.connection.execute('SHOW TABLES;').to_a).to eq []
21
+ end
22
+
23
+ it 'can create table' do
24
+ create_test_table
25
+ expect(ActiveRecord::Base.connection.execute('SHOW TABLES;').to_a).to eq [[table_name]]
26
+ end
27
+
28
+ describe 'SELECT' do
29
+ it 'reads from `replica` by default' do
30
+ create_test_table
31
+ Janus::Context.release_all
32
+ $query_logger.flush_all
33
+ ActiveRecord::Base.connection.execute("SELECT * FROM `#{table_name}`;")
34
+ expect($query_logger.queries.first).to include '[replica]'
35
+ end
36
+
37
+ it 'will read from primary after a write operation' do
38
+ create_test_table
39
+ $query_logger.flush_all
40
+ ActiveRecord::Base.connection.execute("SELECT * FROM `#{table_name}`;")
41
+ expect($query_logger.queries.first).to include '[primary]'
42
+ end
43
+ end
44
+
45
+ describe 'INSERT' do
46
+ let(:insert_query) { "INSERT INTO `#{table_name}` SET `id` = 5;" }
47
+
48
+ before(:each) do
49
+ create_test_table
50
+ $query_logger.flush_all
51
+ Janus::Context.release_all
52
+ end
53
+
54
+ it 'sends INSERT query to primary' do
55
+ ActiveRecord::Base.connection.execute(insert_query)
56
+ expect($query_logger.queries.first).to include '[primary]'
57
+ end
58
+
59
+ it 'ignores case when directing queries' do
60
+ ActiveRecord::Base.connection.execute(insert_query.downcase)
61
+ expect($query_logger.queries.first).to include '[primary]'
62
+ end
63
+ end
64
+
65
+ describe 'UPDATE' do
66
+ before(:each) do
67
+ create_test_table
68
+ 5.times { |i| ActiveRecord::Base.connection.execute("INSERT INTO `#{table_name}` SET `id` = #{i};") }
69
+ $query_logger.flush_all
70
+ Janus::Context.release_all
71
+ end
72
+
73
+ it 'continues to direct after bulk update' do
74
+ ActiveRecord::Base.connection.execute("UPDATE `#{table_name}` SET `id` = `id` + 2;")
75
+ expect($query_logger.queries.first).to include '[primary]'
76
+ expect(Janus::Context.last_used_connection).to eq :primary
77
+ ActiveRecord::Base.connection.execute("SELECT * FROM `#{table_name}`;")
78
+ expect($query_logger.queries.last).to include '[primary]'
79
+ Janus::Context.release_all
80
+ ActiveRecord::Base.connection.execute("SELECT * FROM `#{table_name}`;")
81
+ expect($query_logger.queries.last).to include '[replica]'
82
+ expect(Janus::Context.last_used_connection).to eq :replica
83
+ end
84
+ end
85
+ end
data/spec/spec_helper.rb CHANGED
@@ -6,6 +6,9 @@ require 'active_record'
6
6
 
7
7
  require './lib/janus'
8
8
  require './lib/active_record/connection_adapters/janus_mysql2_adapter'
9
+ require './lib/active_record/connection_adapters/janus_trilogy_adapter'
10
+
11
+ require './spec/shared_examples/a_mysql_like_server.rb'
9
12
 
10
13
  class QueryLogger
11
14
  def initialize
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: janus-ar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lloyd Watkin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-18 00:00:00.000000000 Z
11
+ date: 2024-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: trilogy
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: pry
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -189,15 +203,22 @@ files:
189
203
  - bin/release.sh
190
204
  - janus-ar.gemspec
191
205
  - lib/active_record/connection_adapters/janus_mysql2_adapter.rb
206
+ - lib/active_record/connection_adapters/janus_trilogy_adapter.rb
192
207
  - lib/janus.rb
208
+ - lib/janus/client.rb
193
209
  - lib/janus/context.rb
194
210
  - lib/janus/db_console_config.rb
195
211
  - lib/janus/logging/logger.rb
196
212
  - lib/janus/logging/subscriber.rb
213
+ - lib/janus/query_director.rb
197
214
  - lib/janus/version.rb
198
215
  - spec/lib/active_record/connection_adapters/janus_mysql_adapter_spec.rb
216
+ - spec/lib/active_record/connection_adapters/janus_trilogy_adapter_spec.rb
217
+ - spec/lib/janus/client_spec.rb
199
218
  - spec/lib/janus/context_spec.rb
200
219
  - spec/lib/janus/logging/logger_spec.rb
220
+ - spec/lib/janus/query_director_spec.rb
221
+ - spec/shared_examples/a_mysql_like_server.rb
201
222
  - spec/spec_helper.rb
202
223
  homepage: https://github.com/olioex/janus-ar
203
224
  licenses: