mysql_framework 2.1.8 → 2.3.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.
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  describe MysqlFramework::Connector do
4
- let(:start_pool_size) { Integer(ENV.fetch('MYSQL_START_POOL_SIZE')) }
5
4
  let(:max_pool_size) { Integer(ENV.fetch('MYSQL_MAX_POOL_SIZE')) }
6
5
  let(:default_options) do
7
6
  {
@@ -11,8 +10,8 @@ describe MysqlFramework::Connector do
11
10
  username: ENV.fetch('MYSQL_USERNAME'),
12
11
  password: ENV.fetch('MYSQL_PASSWORD'),
13
12
  reconnect: true,
14
- read_timeout: ENV.fetch('MYSQL_READ_TIMEOUT', 30),
15
- write_timeout: ENV.fetch('MYSQL_WRITE_TIMEOUT', 10)
13
+ read_timeout: Integer(ENV.fetch('MYSQL_READ_TIMEOUT', 30)),
14
+ write_timeout: Integer(ENV.fetch('MYSQL_WRITE_TIMEOUT', 10))
16
15
  }
17
16
  end
18
17
  let(:options) do
@@ -25,28 +24,24 @@ describe MysqlFramework::Connector do
25
24
  reconnect: false
26
25
  }
27
26
  end
28
- let(:client) { double(close: true, ping: true, closed?: false) }
27
+ let(:client) { double(close: true, ping: true, closed?: false, abandon_results!: nil) }
29
28
  let(:gems) { MysqlFramework::SqlTable.new('gems') }
30
29
  let(:existing_client) { Mysql2::Client.new(default_options) }
31
30
  let(:connection_pooling_enabled) { 'true' }
32
31
 
33
32
  subject { described_class.new }
34
33
 
35
- before(:each) do
36
- original_fetch = ENV.method(:fetch)
37
-
38
- allow(ENV).to receive(:fetch) do |var, default|
39
- if var == 'MYSQL_CONNECTION_POOL_ENABLED'
40
- connection_pooling_enabled
41
- else
42
- original_fetch.call(var, default)
43
- end
44
- end
34
+ before do
35
+ allow(ENV).to receive(:fetch).and_call_original
36
+ allow(ENV).to receive(:fetch).with('MYSQL_CONNECTION_POOL_ENABLED', 'true')
37
+ .and_return(connection_pooling_enabled)
45
38
 
46
39
  subject.setup
47
40
  end
48
41
 
49
- after(:each) { subject.dispose }
42
+ after do
43
+ subject.dispose
44
+ end
50
45
 
51
46
  describe '#initialize' do
52
47
  context 'when options are not provided' do
@@ -66,8 +61,8 @@ describe MysqlFramework::Connector do
66
61
  username: ENV.fetch('MYSQL_USERNAME'),
67
62
  password: ENV.fetch('MYSQL_PASSWORD'),
68
63
  reconnect: false,
69
- read_timeout: ENV.fetch('MYSQL_READ_TIMEOUT', 30),
70
- write_timeout: ENV.fetch('MYSQL_WRITE_TIMEOUT', 10)
64
+ read_timeout: Integer(ENV.fetch('MYSQL_READ_TIMEOUT', 30)),
65
+ write_timeout: Integer(ENV.fetch('MYSQL_WRITE_TIMEOUT', 10))
71
66
  }
72
67
 
73
68
  expect(subject.instance_variable_get(:@options)).to eq(expected)
@@ -84,10 +79,13 @@ describe MysqlFramework::Connector do
84
79
 
85
80
  describe '#setup' do
86
81
  context 'when connection pooling is enabled' do
87
- it 'creates a connection pool with the specified number of conections' do
82
+ it 'creates a connection pool with expected stats' do
88
83
  subject.setup
89
84
 
90
- expect(subject.connections.length).to eq(start_pool_size)
85
+ expect(subject.connection_pool).to be_a(MysqlFramework::MysqlConnectionPool)
86
+ expect(subject.connection_pool.pool_stats[:size]).to eq(max_pool_size)
87
+ expect(subject.connection_pool.pool_stats[:available]).to be >= 0
88
+ expect(subject.connection_pool.pool_stats[:idle]).to be >= 0
91
89
  end
92
90
  end
93
91
 
@@ -97,182 +95,147 @@ describe MysqlFramework::Connector do
97
95
  it "doesn't create a connection pool" do
98
96
  subject.setup
99
97
 
100
- expect(subject.connections).to be_nil
98
+ expect(subject.connection_pool).to be_nil
101
99
  end
102
100
  end
103
101
  end
104
102
 
105
103
  describe '#dispose' do
106
- before do
107
- subject.connections.clear
108
- subject.connections.push(client)
109
- end
110
-
111
- it 'closes the idle connections and disposes of the queue' do
112
- expect(client).to receive(:close)
104
+ context 'when connection pooling is enabled' do
105
+ it 'disposes of the pool and clears connector reference' do
106
+ pool = instance_double(MysqlFramework::MysqlConnectionPool, dispose: true)
107
+ allow(pool).to receive(:dispose)
108
+ subject.instance_variable_set(:@connection_pool, pool)
113
109
 
114
- subject.dispose
110
+ expect(pool).to receive(:dispose)
111
+ subject.dispose
115
112
 
116
- expect(subject.connections).to be_nil
113
+ expect(subject.connection_pool).to be_nil
114
+ end
117
115
  end
118
- end
119
116
 
120
- describe '#check_out' do
121
- it 'calls synchronize on the mutex' do
122
- expect(subject.instance_variable_get(:@mutex)).to receive(:synchronize)
117
+ context 'when connection pooling is disabled' do
118
+ let(:connection_pooling_enabled) { 'false' }
123
119
 
124
- subject.check_out
120
+ it 'does not perform more actions' do
121
+ expect { subject.dispose }.not_to raise_error
122
+ expect(subject.connection_pool).to be_nil
123
+ end
125
124
  end
125
+ end
126
126
 
127
+ describe '#check_out' do
127
128
  context 'when connection pooling is enabled' do
128
- context 'when there are available connections' do
129
- before do
130
- subject.connections.clear
131
- subject.connections.push(client)
132
- end
133
-
134
- it 'returns a client instance from the pool' do
135
- expect(subject.check_out).to eq(client)
136
- end
137
-
138
- context 'and :reconnect is set to true' do
139
- let(:options) do
140
- {
141
- host: ENV.fetch('MYSQL_HOST'),
142
- port: ENV.fetch('MYSQL_PORT'),
143
- database: "#{ENV.fetch('MYSQL_DATABASE')}_2",
144
- username: ENV.fetch('MYSQL_USERNAME'),
145
- password: ENV.fetch('MYSQL_PASSWORD'),
146
- reconnect: true
147
- }
148
- end
149
-
150
- subject { described_class.new(options) }
151
-
152
- it 'pings the server to force a reconnect' do
153
- expect(client).to receive(:ping)
154
-
155
- subject.check_out
156
- end
157
- end
158
-
159
- context 'and :reconnect is set to false' do
160
- subject { described_class.new(options) }
161
-
162
- it 'pings the server to force a reconnect' do
163
- expect(client).not_to receive(:ping)
129
+ it 'checks out pooled connection' do
130
+ pool = instance_double(MysqlFramework::MysqlConnectionPool, dispose: true)
131
+ client = instance_double(Mysql2::Client)
132
+ allow(pool).to receive(:check_out).and_return(client)
133
+ subject.instance_variable_set(:@connection_pool, pool)
164
134
 
165
- subject.check_out
166
- end
167
- end
135
+ expect(pool).to receive(:check_out)
136
+ expect(subject.check_out).to eq(client)
168
137
  end
138
+ end
169
139
 
170
- context 'when connection pooling is disabled' do
171
- let(:connection_pooling_enabled) { 'false' }
140
+ context 'when pooling is disabled' do
141
+ let(:connection_pooling_enabled) { 'false' }
172
142
 
173
- it 'instantiates and returns a new connection directly' do
174
- expect(subject.connections).not_to receive(:pop)
175
- expect(Mysql2::Client).to receive(:new)
143
+ it 'returns a new client directly' do
144
+ new_client = instance_double(Mysql2::Client)
145
+ allow(Mysql2::Client).to receive(:new).and_return(new_client)
176
146
 
177
- subject.check_out
178
- end
147
+ expect(subject.check_out).to eq(new_client)
179
148
  end
180
149
  end
150
+ end
181
151
 
182
- context "when there are no available connections, and the pool's max size has not been reached" do
183
- before do
184
- subject.connections.clear
185
- subject.connections.push(client)
186
- end
187
-
188
- it 'instantiates a new connection and returns it' do
189
- subject.check_out
152
+ describe '#check_in' do
153
+ context 'when connection pooling is enabled' do
154
+ it 'checks in a pooled connection' do
155
+ pool = instance_double(MysqlFramework::MysqlConnectionPool, dispose: true)
156
+ allow(pool).to receive(:check_in)
157
+ subject.instance_variable_set(:@connection_pool, pool)
190
158
 
191
- expect(Mysql2::Client).to receive(:new).with(default_options).and_return(client)
192
- expect(subject.check_out).to eq(client)
159
+ expect(pool).to receive(:check_in)
160
+ subject.check_in(client)
193
161
  end
194
162
  end
195
163
 
196
- context "when there are no available connections, and the pool's max size has been reached" do
197
- before do
198
- subject.connections.clear
199
- subject.instance_variable_set(:@created_connections, 5)
164
+ context 'when pooling is disabled' do
165
+ let(:connection_pooling_enabled) { 'false' }
200
166
 
201
- 5.times { subject.check_in(client) }
202
- 5.times { subject.check_out }
203
- end
167
+ it 'closes the provided client' do
168
+ new_client = instance_double(Mysql2::Client, close: nil)
204
169
 
205
- it 'throws a RuntimeError' do
206
- expect { subject.check_out }.to raise_error(RuntimeError)
170
+ expect(new_client).to receive(:close)
171
+ subject.check_in(new_client)
207
172
  end
208
173
  end
209
174
  end
210
175
 
211
- describe '#check_in' do
212
- it 'calls synchronize on the mutex' do
213
- expect(subject.instance_variable_get(:@mutex)).to receive(:synchronize)
176
+ describe '#with_client' do
177
+ context 'when a provided_client is given' do
178
+ it 'yields the provided client directly' do
179
+ expect { |b| subject.with_client(client, &b) }.to yield_with_args(client)
180
+ end
214
181
 
215
- subject.check_out
216
- end
182
+ it 'does not interact with the connection pool' do
183
+ expect(subject).not_to receive(:with_new_client)
184
+ expect(subject.connection_pool).not_to receive(:with_client) if subject.connection_pool
217
185
 
218
- context 'when connection pooling is enabled' do
219
- it 'returns the provided client to the connection pool' do
220
- expect(subject.connections).to receive(:push).with(client)
186
+ subject.with_client(client) { |_c| nil }
187
+ end
221
188
 
222
- subject.check_in(client)
189
+ it 'returns the block result' do
190
+ result = subject.with_client(client) { |_c| :expected }
191
+ expect(result).to eq(:expected)
223
192
  end
193
+ end
224
194
 
225
- context 'when the connection has been closed by the server' do
226
- let(:closed_client) { double(close: true, closed?: true) }
195
+ context 'when no provided_client is given' do
196
+ context 'when connection pooling is disabled' do
197
+ let(:connection_pooling_enabled) { 'false' }
227
198
 
228
- it 'instantiates a new connection and returns it' do
229
- expect(Mysql2::Client).to receive(:new).with(default_options).and_return(client)
230
- expect(subject.connections).to receive(:push).with(client)
199
+ it 'delegates to with_new_client' do
200
+ expect(subject).to receive(:with_new_client).and_yield(client)
231
201
 
232
- subject.check_in(closed_client)
202
+ expect { |b| subject.with_client(&b) }.to yield_with_args(client)
233
203
  end
234
- end
235
- end
236
204
 
237
- context 'when connection pooling is disabled' do
238
- let(:connection_pooling_enabled) { 'false' }
239
-
240
- it 'closes the connection and does not add it to the connection pool' do
241
- expect(client).to receive(:close)
242
- expect(subject.connections).not_to receive(:push)
205
+ it 'returns the block result' do
206
+ allow(subject).to receive(:with_new_client).and_yield(client)
243
207
 
244
- subject.check_in(client)
208
+ result = subject.with_client { |_c| :expected }
209
+ expect(result).to eq(:expected)
210
+ end
245
211
  end
246
- end
247
-
248
- context 'when client is nil' do
249
- let(:client) { nil }
250
212
 
251
213
  context 'when connection pooling is enabled' do
252
- it 'does not raise an error' do
253
- expect { subject.check_in(client) }.not_to raise_error
214
+ it 'delegates to the connection pool' do
215
+ pool = instance_double(MysqlFramework::MysqlConnectionPool, dispose: true)
216
+ subject.instance_variable_set(:@connection_pool, pool)
217
+
218
+ expect(pool).to receive(:with_client).with(discard_current_pool_connection: false).and_yield(client)
219
+ expect { |b| subject.with_client(&b) }.to yield_with_args(client)
254
220
  end
255
- end
256
221
 
257
- context 'when connection pooling is disabled' do
258
- let(:connection_pooling_enabled) { 'false' }
222
+ it 'passes discard_current_pool_connection: true to the pool when requested' do
223
+ pool = instance_double(MysqlFramework::MysqlConnectionPool, dispose: true)
224
+ subject.instance_variable_set(:@connection_pool, pool)
259
225
 
260
- it 'does not raise an error' do
261
- expect { subject.check_in(client) }.not_to raise_error
226
+ expect(pool).to receive(:with_client).with(discard_current_pool_connection: true).and_yield(client)
227
+ subject.with_client(discard_current_pool_connection: true) { |_c| nil }
262
228
  end
263
- end
264
- end
265
- end
266
229
 
267
- describe '#with_client' do
268
- it 'uses the client that is provided, if passed one' do
269
- expect(subject).not_to receive(:check_out)
270
- expect { |b| subject.with_client(client, &b) }.to yield_with_args(client)
271
- end
230
+ it 'returns the block result' do
231
+ pool = instance_double(MysqlFramework::MysqlConnectionPool, dispose: true)
232
+ subject.instance_variable_set(:@connection_pool, pool)
233
+ allow(pool).to receive(:with_client).and_yield(client)
272
234
 
273
- it 'obtains a client from the pool to use, if no client is provided' do
274
- allow(subject).to receive(:check_out).and_return(client)
275
- expect { |b| subject.with_client(&b) }.to yield_with_args(client)
235
+ result = subject.with_client { |_c| :expected }
236
+ expect(result).to eq(:expected)
237
+ end
238
+ end
276
239
  end
277
240
  end
278
241
 
@@ -317,6 +280,7 @@ describe MysqlFramework::Connector do
317
280
  allow(mock_statement).to receive(:execute).and_return(mock_result)
318
281
 
319
282
  allow(mock_client).to receive(:prepare).and_return(mock_statement)
283
+ allow(mock_client).to receive(:abandon_results!)
320
284
  end
321
285
 
322
286
  it 'frees the result' do
@@ -361,7 +325,7 @@ describe MysqlFramework::Connector do
361
325
  end
362
326
 
363
327
  describe '#query' do
364
- before(:each) { allow(subject).to receive(:check_out).and_return(client) }
328
+ before(:each) { allow(subject).to receive(:with_client).and_yield(client) }
365
329
 
366
330
  it 'retrieves a client and calls query' do
367
331
  expect(client).to receive(:query).with('SELECT 1')
@@ -370,7 +334,7 @@ describe MysqlFramework::Connector do
370
334
  end
371
335
 
372
336
  it 'does not check out a new client when one is provided' do
373
- expect(subject).not_to receive(:check_out)
337
+ expect(subject).to receive(:with_client).with(existing_client).and_yield(existing_client)
374
338
  expect(existing_client).to receive(:query).with('SELECT 1')
375
339
 
376
340
  subject.query('SELECT 1', existing_client)
@@ -378,6 +342,19 @@ describe MysqlFramework::Connector do
378
342
  end
379
343
 
380
344
  describe '#query_multiple_results' do
345
+ it 'uses with_client with discard_current_pool_connection enabled' do
346
+ query_call = instance_double(Mysql2::Result, to_a: [], free: true)
347
+ allow(client).to receive(:query).and_return(query_call)
348
+ allow(client).to receive(:more_results?).and_return(false)
349
+ allow(client).to receive(:abandon_results!)
350
+
351
+ expect(subject).to receive(:with_client)
352
+ .with(nil, discard_current_pool_connection: true)
353
+ .and_yield(client)
354
+
355
+ subject.query_multiple_results('call test_procedure')
356
+ end
357
+
381
358
  it 'returns the results from the stored procedure' do
382
359
  query = 'call test_procedure'
383
360
  result = subject.query_multiple_results(query)
@@ -402,7 +379,7 @@ describe MysqlFramework::Connector do
402
379
  end
403
380
 
404
381
  describe '#transaction' do
405
- before(:each) { allow(subject).to receive(:check_out).and_return(client) }
382
+ before(:each) { allow(subject).to receive(:with_client).and_yield(client) }
406
383
 
407
384
  it 'wraps the client call with BEGIN and COMMIT statements' do
408
385
  expect(client).to receive(:query).with('BEGIN')
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe MysqlFramework::MysqlConnectionPool do
4
+ let(:options) do
5
+ {
6
+ host: ENV.fetch('MYSQL_HOST'),
7
+ port: ENV.fetch('MYSQL_PORT'),
8
+ database: ENV.fetch('MYSQL_DATABASE'),
9
+ username: ENV.fetch('MYSQL_USERNAME'),
10
+ password: ENV.fetch('MYSQL_PASSWORD'),
11
+ reconnect: true
12
+ }
13
+ end
14
+ let(:conn) do
15
+ double('Mysql2::Client',
16
+ ping: true,
17
+ abandon_results!: nil,
18
+ query: nil,
19
+ close: nil,
20
+ closed?: false)
21
+ end
22
+ let(:pool) do
23
+ double('ConnectionPool',
24
+ checkout: conn,
25
+ checkin: nil,
26
+ discard_current_connection: nil,
27
+ shutdown: nil,
28
+ size: 5,
29
+ available: 4,
30
+ idle: 1)
31
+ end
32
+
33
+ subject { described_class.new(options) }
34
+
35
+ after { subject.dispose }
36
+
37
+ describe '#initialize' do
38
+ it 'stores the provided options' do
39
+ expect(subject.instance_variable_get(:@options)).to eq(options)
40
+ end
41
+
42
+ it 'creates a setup mutex' do
43
+ expect(subject.instance_variable_get(:@setup_mutex)).to be_a(Mutex)
44
+ end
45
+ end
46
+
47
+ describe '#setup' do
48
+ it 'creates a ConnectionPool' do
49
+ subject.setup
50
+ expect(subject.connections).to be_a(ConnectionPool)
51
+ end
52
+
53
+ it 'is idempotent — calling setup twice keeps the same pool' do
54
+ subject.setup
55
+ first_pool = subject.connections
56
+ subject.setup
57
+ expect(subject.connections).to equal(first_pool)
58
+ end
59
+ end
60
+
61
+ describe '#dispose' do
62
+ before { subject.setup }
63
+
64
+ it 'shuts down the connection pool' do
65
+ subject.dispose
66
+ expect(subject.connections).to be_nil
67
+ end
68
+
69
+ it 'is safe to call when already disposed' do
70
+ subject.dispose
71
+ expect { subject.dispose }.not_to raise_error
72
+ end
73
+ end
74
+
75
+ describe '#pool_stats' do
76
+ context 'when connections have not been set up' do
77
+ it 'returns zero stats' do
78
+ expect(subject.pool_stats).to eq(size: 0, available: 0, idle: 0)
79
+ end
80
+ end
81
+
82
+ context 'when connections are set up' do
83
+ before { subject.instance_variable_set(:@connections, pool) }
84
+
85
+ it 'returns size, available, and idle metrics from the pool' do
86
+ expect(subject.pool_stats).to eq(size: 5, available: 4, idle: 1)
87
+ end
88
+ end
89
+ end
90
+
91
+ describe '#check_out' do
92
+ before do
93
+ subject.instance_variable_set(:@connections, pool)
94
+ allow(conn).to receive(:query).with('ROLLBACK')
95
+ end
96
+
97
+ it 'returns a sanitized connection from the pool' do
98
+ expect(subject.check_out).to eq(conn)
99
+ end
100
+
101
+ it 'sanitizes the connection before returning it' do
102
+ expect(conn).to receive(:ping)
103
+ expect(conn).to receive(:abandon_results!)
104
+ expect(conn).to receive(:query).with('ROLLBACK')
105
+ subject.check_out
106
+ end
107
+
108
+ context 'when sanitization raises ConnectionSanitizationError' do
109
+ before { allow(conn).to receive(:ping).and_raise(Mysql2::Error.new('gone away')) }
110
+
111
+ it 'retries the checkout once before raising' do
112
+ expect(pool).to receive(:checkout).twice.and_return(conn)
113
+ expect { subject.check_out }.to raise_error(MysqlFramework::MysqlConnectionPool::ConnectionSanitizationError)
114
+ end
115
+
116
+ it 'discards the connection on each sanitization failure' do
117
+ expect(pool).to receive(:discard_current_connection).at_least(:twice)
118
+ expect { subject.check_out }.to raise_error(MysqlFramework::MysqlConnectionPool::ConnectionSanitizationError)
119
+ end
120
+ end
121
+
122
+ context 'when checkout raises Mysql2::Error' do
123
+ before { allow(pool).to receive(:checkout).and_raise(Mysql2::Error.new('connection refused')) }
124
+
125
+ it 'discards the current connection' do
126
+ expect(pool).to receive(:discard_current_connection)
127
+ expect { subject.check_out }.to raise_error(Mysql2::Error)
128
+ end
129
+
130
+ it 're-raises the error' do
131
+ allow(pool).to receive(:discard_current_connection)
132
+ expect { subject.check_out }.to raise_error(Mysql2::Error)
133
+ end
134
+ end
135
+ end
136
+
137
+ describe '#check_in' do
138
+ before { subject.instance_variable_set(:@connections, pool) }
139
+
140
+ it 'returns immediately and does not interact with the pool when client is nil' do
141
+ expect(pool).not_to receive(:checkin)
142
+ expect(pool).not_to receive(:discard_current_connection)
143
+ subject.check_in(nil)
144
+ end
145
+
146
+ context 'when the client is closed' do
147
+ let(:closed_conn) { double('Mysql2::Client', closed?: true) }
148
+
149
+ it 'discards the current pool connection' do
150
+ expect(pool).to receive(:discard_current_connection)
151
+ allow(pool).to receive(:checkin)
152
+ subject.check_in(closed_conn)
153
+ end
154
+ end
155
+
156
+ context 'when the client is open' do
157
+ it 'checks the connection back into the pool' do
158
+ expect(pool).to receive(:checkin)
159
+ subject.check_in(conn)
160
+ end
161
+
162
+ it 'does not discard the connection' do
163
+ expect(pool).not_to receive(:discard_current_connection)
164
+ subject.check_in(conn)
165
+ end
166
+ end
167
+ end
168
+
169
+ describe '#with_client' do
170
+ before do
171
+ subject.instance_variable_set(:@connections, pool)
172
+ allow(conn).to receive(:query).with('ROLLBACK')
173
+ allow(pool).to receive(:with).and_yield(conn)
174
+ end
175
+
176
+ it 'yields a sanitized connection' do
177
+ expect { |b| subject.with_client(&b) }.to yield_with_args(conn)
178
+ end
179
+
180
+ it 'returns the block result' do
181
+ result = subject.with_client { |_c| :expected }
182
+ expect(result).to eq(:expected)
183
+ end
184
+
185
+ it 'sanitizes the connection before yielding' do
186
+ expect(conn).to receive(:ping)
187
+ expect(conn).to receive(:abandon_results!)
188
+ expect(conn).to receive(:query).with('ROLLBACK')
189
+ subject.with_client { |_c| nil }
190
+ end
191
+
192
+ context 'when discard_current_pool_connection is false (default)' do
193
+ it 'does not discard the connection after a successful block' do
194
+ expect(pool).not_to receive(:discard_current_connection)
195
+ subject.with_client { |_c| nil }
196
+ end
197
+ end
198
+
199
+ context 'when discard_current_pool_connection is true' do
200
+ it 'discards the current connection after the block completes' do
201
+ expect(pool).to receive(:discard_current_connection)
202
+ subject.with_client(discard_current_pool_connection: true) { |_c| nil }
203
+ end
204
+ end
205
+
206
+ context 'when sanitization raises ConnectionSanitizationError' do
207
+ before { allow(conn).to receive(:ping).and_raise(Mysql2::Error.new('gone away')) }
208
+
209
+ it 'retries the pool checkout once before raising' do
210
+ expect(pool).to receive(:with).twice.and_yield(conn)
211
+ expect { subject.with_client { |_c| nil } }.to raise_error(MysqlFramework::MysqlConnectionPool::ConnectionSanitizationError)
212
+ end
213
+
214
+ it 'discards the connection on each sanitization failure' do
215
+ allow(pool).to receive(:with).and_yield(conn)
216
+ expect(pool).to receive(:discard_current_connection).at_least(:once)
217
+ expect { subject.with_client { |_c| nil } }.to raise_error(MysqlFramework::MysqlConnectionPool::ConnectionSanitizationError)
218
+ end
219
+ end
220
+
221
+ context 'when the block raises Mysql2::Error' do
222
+ it 'discards the current connection' do
223
+ expect(pool).to receive(:discard_current_connection)
224
+ expect { subject.with_client { |_c| raise Mysql2::Error.new('lost connection') } }.to raise_error(Mysql2::Error)
225
+ end
226
+
227
+ it 're-raises the error' do
228
+ allow(pool).to receive(:discard_current_connection)
229
+ expect { subject.with_client { |_c| raise Mysql2::Error.new('lost connection') } }.to raise_error(Mysql2::Error)
230
+ end
231
+
232
+ it 'does not retry for Mysql2::Error raised in the block' do
233
+ expect(pool).to receive(:with).once.and_yield(conn)
234
+ allow(pool).to receive(:discard_current_connection)
235
+ expect { subject.with_client { |_c| raise Mysql2::Error.new('lost connection') } }.to raise_error(Mysql2::Error)
236
+ end
237
+ end
238
+ end
239
+ end
@@ -233,6 +233,33 @@ describe MysqlFramework::SqlQuery do
233
233
  expect(subject.sql).to eq('WHERE (`gems`.`author` = ? AND `gems`.`created_at` > ?) AND (`gems`.`name` IN (?, ?))')
234
234
  end
235
235
  end
236
+
237
+ context 'when condition is nil' do
238
+ context 'when MYSQL_FRAMEWORK_SKIP_NIL_VALUE_VALIDATION is not present' do
239
+ it 'concats the parameter collections' do
240
+ expect { subject.where(gems[:created_at].eq(nil)) }.to raise_error ArgumentError
241
+ end
242
+ end
243
+
244
+ context 'when MYSQL_FRAMEWORK_SKIP_NIL_VALUE_VALIDATION is present' do
245
+ after(:each) { ENV.delete('MYSQL_FRAMEWORK_SKIP_NIL_VALUE_VALIDATION') }
246
+
247
+ context 'when value is false' do
248
+ it 'concats the parameter collections' do
249
+ ENV['MYSQL_FRAMEWORK_SKIP_NIL_VALUE_VALIDATION'] = 'false'
250
+ expect { subject.where(gems[:created_at].eq(nil)) }.to raise_error ArgumentError
251
+ end
252
+ end
253
+
254
+ context 'when value is true' do
255
+ it 'concats the parameter collections' do
256
+ ENV['MYSQL_FRAMEWORK_SKIP_NIL_VALUE_VALIDATION'] = 'true'
257
+ query = subject.where(gems[:updated_at].eq(nil))
258
+ expect(query.params).to eq ['sage', '2018-01-01 00:00:00', nil]
259
+ end
260
+ end
261
+ end
262
+ end
236
263
  end
237
264
 
238
265
  describe '#and' do