janus-ar 7.2.2 → 8.0.1

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.
@@ -1,59 +1,141 @@
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
19
- SAVEPOINT FLUSH)
20
- }
21
-
22
- it { expect(described_class::ALL).to eq :all }
23
- it { expect(described_class::REPLICA).to eq :replica }
24
- it { expect(described_class::PRIMARY).to eq :primary }
25
- end
26
-
27
- describe '#where_to_send?' do
28
- before(:each) do
29
- Janus::Context.release_all
30
- end
31
-
32
- context 'when should send to all' do
33
- it 'returns :all' do
34
- sql = 'SET foo = bar'
35
- open_transactions = 0
36
- query_director = described_class.new(sql, open_transactions)
37
- expect(query_director.where_to_send?).to eq(:all)
38
- end
39
- end
40
-
41
- context 'when can go to replica' do
42
- it 'returns :replica' do
43
- sql = 'SELECT * FROM users'
44
- open_transactions = 0
45
- query_director = described_class.new(sql, open_transactions)
46
- expect(query_director.where_to_send?).to eq(:replica)
47
- end
48
- end
49
-
50
- context 'when should go to primary' do
51
- it 'returns :primary' do
52
- sql = 'INSERT INTO users (name) VALUES ("John")'
53
- open_transactions = 0
54
- query_director = described_class.new(sql, open_transactions)
55
- expect(query_director.where_to_send?).to eq(:primary)
56
- end
57
- end
58
- end
59
- end
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
@@ -82,4 +82,74 @@ RSpec.shared_examples 'a mysql like server' do
82
82
  expect(Janus::Context.last_used_connection).to eq :replica
83
83
  end
84
84
  end
85
+
86
+ describe 'ActiveRecord compatibility' do
87
+ before(:each) do
88
+ create_test_table
89
+ Janus::Context.release_all
90
+ end
91
+
92
+ it 'accepts the optional name argument on #execute' do
93
+ expect do
94
+ ActiveRecord::Base.connection.execute("SELECT * FROM `#{table_name}`;", 'CustomName')
95
+ end.not_to raise_error
96
+ end
97
+
98
+ it 'returns a usable result through the exec_query read path' do
99
+ ActiveRecord::Base.connection.execute("INSERT INTO `#{table_name}` SET `id` = 7;")
100
+ Janus::Context.release_all
101
+ $query_logger.flush_all
102
+
103
+ result = ActiveRecord::Base.connection.exec_query("SELECT `id` FROM `#{table_name}`")
104
+
105
+ expect(result.rows).to eq [[7]]
106
+ expect($query_logger.queries.first).to include '[replica]'
107
+ end
108
+ end
109
+
110
+ describe 'Transactions' do
111
+ before(:each) do
112
+ create_test_table
113
+ Janus::Context.release_all
114
+ $query_logger.flush_all
115
+ end
116
+
117
+ it 'routes reads inside a transaction to the primary' do
118
+ ActiveRecord::Base.transaction do
119
+ ActiveRecord::Base.connection.execute("SELECT * FROM `#{table_name}`;")
120
+ end
121
+
122
+ selects = $query_logger.queries.select { |q| q.downcase.include?("select * from `#{table_name}`") }
123
+ expect(selects).not_to be_empty
124
+ expect(selects).to all(include('[primary]'))
125
+ end
126
+
127
+ it 'keeps later reads on the primary until the context is released' do
128
+ ActiveRecord::Base.transaction do
129
+ ActiveRecord::Base.connection.execute("INSERT INTO `#{table_name}` SET `id` = 1;")
130
+ end
131
+ $query_logger.flush_all
132
+
133
+ ActiveRecord::Base.connection.execute("SELECT * FROM `#{table_name}`;")
134
+ expect($query_logger.queries.last).to include '[primary]'
135
+
136
+ Janus::Context.release_all
137
+ ActiveRecord::Base.connection.execute("SELECT * FROM `#{table_name}`;")
138
+ expect($query_logger.queries.last).to include '[replica]'
139
+ end
140
+ end
141
+
142
+ describe 'SET statements' do
143
+ before(:each) { Janus::Context.release_all }
144
+
145
+ it 'sends a session SET down the broadcast (:all) path without error' do
146
+ expect do
147
+ ActiveRecord::Base.connection.execute("SET SESSION time_zone = '+00:00'")
148
+ end.not_to raise_error
149
+
150
+ # `:all` means the statement ran against the replica connection too, not
151
+ # just the primary - a write would have been marked `:primary`.
152
+ expect(Janus::Context.last_used_connection).to eq :all
153
+ end
154
+ end
85
155
  end
metadata CHANGED
@@ -1,43 +1,48 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: janus-ar
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.2
4
+ version: 8.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lloyd Watkin
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-03-06 00:00:00.000000000 Z
10
+ date: 2026-06-19 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ - - "<"
18
20
  - !ruby/object:Gem::Version
19
- version: '7.2'
21
+ version: '9.0'
20
22
  type: :runtime
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
23
25
  requirements:
24
- - - "~>"
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '8.0'
29
+ - - "<"
25
30
  - !ruby/object:Gem::Version
26
- version: '7.2'
31
+ version: '9.0'
27
32
  - !ruby/object:Gem::Dependency
28
33
  name: activesupport
29
34
  requirement: !ruby/object:Gem::Requirement
30
35
  requirements:
31
36
  - - ">="
32
37
  - !ruby/object:Gem::Version
33
- version: 7.2.0
38
+ version: '8.0'
34
39
  type: :development
35
40
  prerelease: false
36
41
  version_requirements: !ruby/object:Gem::Requirement
37
42
  requirements:
38
43
  - - ">="
39
44
  - !ruby/object:Gem::Version
40
- version: 7.2.0
45
+ version: '8.0'
41
46
  - !ruby/object:Gem::Dependency
42
47
  name: mysql2
43
48
  requirement: !ruby/object:Gem::Requirement
@@ -114,28 +119,28 @@ dependencies:
114
119
  requirements:
115
120
  - - "~>"
116
121
  - !ruby/object:Gem::Version
117
- version: 1.65.0
122
+ version: 1.88.0
118
123
  type: :development
119
124
  prerelease: false
120
125
  version_requirements: !ruby/object:Gem::Requirement
121
126
  requirements:
122
127
  - - "~>"
123
128
  - !ruby/object:Gem::Version
124
- version: 1.65.0
129
+ version: 1.88.0
125
130
  - !ruby/object:Gem::Dependency
126
131
  name: rubocop-rails
127
132
  requirement: !ruby/object:Gem::Requirement
128
133
  requirements:
129
134
  - - "~>"
130
135
  - !ruby/object:Gem::Version
131
- version: 2.26.0
136
+ version: 2.35.2
132
137
  type: :development
133
138
  prerelease: false
134
139
  version_requirements: !ruby/object:Gem::Requirement
135
140
  requirements:
136
141
  - - "~>"
137
142
  - !ruby/object:Gem::Version
138
- version: 2.26.0
143
+ version: 2.35.2
139
144
  - !ruby/object:Gem::Dependency
140
145
  name: rubocop-rspec
141
146
  requirement: !ruby/object:Gem::Requirement
@@ -202,22 +207,28 @@ files:
202
207
  - assets/.gitkeep
203
208
  - assets/janus-logo.png
204
209
  - bin/release.sh
210
+ - gemfiles/activerecord_8_0.gemfile
211
+ - gemfiles/activerecord_8_1.gemfile
205
212
  - janus-ar.gemspec
206
213
  - lib/janus-ar.rb
207
214
  - lib/janus-ar/active_record/connection_adapters/janus_mysql2_adapter.rb
208
215
  - lib/janus-ar/active_record/connection_adapters/janus_trilogy_adapter.rb
216
+ - lib/janus-ar/adapter_extensions.rb
209
217
  - lib/janus-ar/client.rb
210
218
  - lib/janus-ar/context.rb
211
219
  - lib/janus-ar/db_console_config.rb
212
220
  - lib/janus-ar/logging/logger.rb
213
221
  - lib/janus-ar/logging/subscriber.rb
214
222
  - lib/janus-ar/query_director.rb
223
+ - lib/janus-ar/railtie.rb
215
224
  - lib/janus-ar/version.rb
216
225
  - spec/lib/janus-ar/active_record/connection_adapters/janus_mysql_adapter_spec.rb
217
226
  - spec/lib/janus-ar/active_record/connection_adapters/janus_trilogy_adapter_spec.rb
218
227
  - spec/lib/janus-ar/client_spec.rb
219
228
  - spec/lib/janus-ar/context_spec.rb
229
+ - spec/lib/janus-ar/db_console_config_spec.rb
220
230
  - spec/lib/janus-ar/logging/logger_spec.rb
231
+ - spec/lib/janus-ar/logging/subscriber_spec.rb
221
232
  - spec/lib/janus-ar/query_director_spec.rb
222
233
  - spec/shared_examples/a_mysql_like_server.rb
223
234
  - spec/spec_helper.rb
@@ -226,7 +237,6 @@ licenses:
226
237
  - MIT
227
238
  metadata:
228
239
  source_code_uri: https://github.com/olioex/janus-ar
229
- post_install_message:
230
240
  rdoc_options: []
231
241
  require_paths:
232
242
  - lib
@@ -241,8 +251,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
241
251
  - !ruby/object:Gem::Version
242
252
  version: '0'
243
253
  requirements: []
244
- rubygems_version: 3.5.23
245
- signing_key:
254
+ rubygems_version: 3.6.2
246
255
  specification_version: 4
247
256
  summary: Read/Write proxy for ActiveRecord using primary/replica databases
248
257
  test_files: []