boltless 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +8 -0
  3. data/Guardfile +44 -0
  4. data/Makefile +138 -0
  5. data/Rakefile +26 -0
  6. data/docker-compose.yml +19 -0
  7. data/lib/boltless/configuration.rb +69 -0
  8. data/lib/boltless/errors/invalid_json_error.rb +9 -0
  9. data/lib/boltless/errors/request_error.rb +24 -0
  10. data/lib/boltless/errors/response_error.rb +30 -0
  11. data/lib/boltless/errors/transaction_begin_error.rb +9 -0
  12. data/lib/boltless/errors/transaction_in_bad_state_error.rb +11 -0
  13. data/lib/boltless/errors/transaction_not_found_error.rb +11 -0
  14. data/lib/boltless/errors/transaction_rollback_error.rb +26 -0
  15. data/lib/boltless/extensions/configuration_handling.rb +37 -0
  16. data/lib/boltless/extensions/connection_pool.rb +127 -0
  17. data/lib/boltless/extensions/operations.rb +175 -0
  18. data/lib/boltless/extensions/transactions.rb +301 -0
  19. data/lib/boltless/extensions/utilities.rb +187 -0
  20. data/lib/boltless/request.rb +386 -0
  21. data/lib/boltless/result.rb +98 -0
  22. data/lib/boltless/result_row.rb +90 -0
  23. data/lib/boltless/statement_collector.rb +40 -0
  24. data/lib/boltless/transaction.rb +234 -0
  25. data/lib/boltless/version.rb +23 -0
  26. data/lib/boltless.rb +36 -0
  27. data/spec/benchmark/transfer.rb +57 -0
  28. data/spec/boltless/extensions/configuration_handling_spec.rb +39 -0
  29. data/spec/boltless/extensions/connection_pool_spec.rb +131 -0
  30. data/spec/boltless/extensions/operations_spec.rb +189 -0
  31. data/spec/boltless/extensions/transactions_spec.rb +418 -0
  32. data/spec/boltless/extensions/utilities_spec.rb +546 -0
  33. data/spec/boltless/request_spec.rb +946 -0
  34. data/spec/boltless/result_row_spec.rb +161 -0
  35. data/spec/boltless/result_spec.rb +127 -0
  36. data/spec/boltless/statement_collector_spec.rb +45 -0
  37. data/spec/boltless/transaction_spec.rb +601 -0
  38. data/spec/boltless_spec.rb +11 -0
  39. data/spec/fixtures/files/raw_result.yml +21 -0
  40. data/spec/fixtures/files/raw_result_with_graph_result.yml +48 -0
  41. data/spec/fixtures/files/raw_result_with_meta.yml +11 -0
  42. data/spec/fixtures/files/raw_result_with_stats.yml +26 -0
  43. data/spec/spec_helper.rb +89 -0
  44. metadata +384 -0
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Boltless::Extensions::Operations do
6
+ let(:described_class) { Boltless }
7
+
8
+ before { clean_neo4j! }
9
+
10
+ describe '.clear_database!' do
11
+ let(:action) { described_class.clear_database! }
12
+ let(:logger) { Logger.new(log_dev) }
13
+ let(:log_dev) { StringIO.new }
14
+ let(:log) { log_dev.string }
15
+
16
+ before do
17
+ described_class.configuration.logger = logger
18
+ described_class.add_index(name: 'user_id', for: '(n:User)', on: 'n.id')
19
+ described_class.add_constraint(name: 'uniq_user_email', for: '(n:User)',
20
+ require: 'n.email IS UNIQUE')
21
+ described_class.one_shot! do |tx|
22
+ tx.add('CREATE (n:User { name: $name })', name: 'Klaus')
23
+ tx.add('CREATE (n:User { name: $name })', name: 'Bernd')
24
+ tx.add('MATCH (a:User { name: $a_name }) ' \
25
+ 'MATCH (b:User { name: $b_name }) ' \
26
+ 'CREATE (a)-[:FRIEND_OF { since: $since }]->(b)',
27
+ a_name: 'Klaus', b_name: 'Bernd', since: Date.today.to_s)
28
+ end
29
+ end
30
+
31
+ it 'removes all indexes' do
32
+ expect { action }.to \
33
+ change { described_class.index_names.count }.from(1).to(0)
34
+ end
35
+
36
+ it 'removes all constraints' do
37
+ expect { action }.to \
38
+ change { described_class.constraint_names.count }.from(1).to(0)
39
+ end
40
+
41
+ it 'removes all nodes' do
42
+ expect { action }.to \
43
+ change { described_class.query!('MATCH (n) RETURN count(n)').value }
44
+ .from(2).to(0)
45
+ end
46
+
47
+ it 'removes all relationships' do
48
+ check = proc do
49
+ described_class.query!('MATCH (a)-[r]->(b) RETURN type(r) AS type')
50
+ .count
51
+ end
52
+ expect { action }.to change(&check).from(1).to(0)
53
+ end
54
+
55
+ it 'logs the removed indexes' do
56
+ action
57
+ expect(log).to include('Drop neo4j index user_id')
58
+ end
59
+
60
+ it 'logs the removed constraints' do
61
+ action
62
+ expect(log).to include('Drop neo4j constraint uniq_user_email')
63
+ end
64
+
65
+ it 'logs the removed nodes count' do
66
+ action
67
+ expect(log).to include('Nodes deleted: 2')
68
+ end
69
+
70
+ it 'logs the removed relationships count' do
71
+ action
72
+ expect(log).to include('Relationships deleted: 1')
73
+ end
74
+ end
75
+
76
+ describe '.component_name_present?' do
77
+ before do
78
+ described_class.add_index(name: 'user_id', for: '(n:User)', on: 'n.id')
79
+ described_class.add_constraint(name: 'uniq_user_email', for: '(n:User)',
80
+ require: 'n.email IS UNIQUE')
81
+ end
82
+
83
+ context 'with an known index' do
84
+ it 'returns true' do
85
+ expect(described_class.component_name_present?('user_id')).to \
86
+ be_eql(true)
87
+ end
88
+ end
89
+
90
+ context 'with an known constraint' do
91
+ it 'returns true' do
92
+ expect(described_class.component_name_present?('uniq_user_email')).to \
93
+ be_eql(true)
94
+ end
95
+ end
96
+
97
+ context 'with an unknown name' do
98
+ it 'returns false' do
99
+ expect(described_class.component_name_present?('unknown')).to \
100
+ be_eql(false)
101
+ end
102
+ end
103
+ end
104
+
105
+ describe '.index_names' do
106
+ before do
107
+ described_class.add_index(name: 'user_id', for: '(n:User)', on: 'n.id')
108
+ described_class.add_index(name: 'session_id', for: '(n:Session)',
109
+ on: 'n.id')
110
+ end
111
+
112
+ it 'returns the known index names' do
113
+ expect(described_class.index_names).to \
114
+ match_array(%w[user_id session_id])
115
+ end
116
+ end
117
+
118
+ describe '.constraint_names' do
119
+ before do
120
+ described_class.add_constraint(name: 'uniq_user_email',
121
+ for: '(n:User)',
122
+ require: 'n.email IS UNIQUE')
123
+ described_class.add_constraint(name: 'uniq_user_session',
124
+ for: '(n:Session)',
125
+ require: 'n.user_id IS UNIQUE')
126
+ end
127
+
128
+ it 'returns the known constraint names' do
129
+ expect(described_class.constraint_names).to \
130
+ match_array(%w[uniq_user_email uniq_user_session])
131
+ end
132
+ end
133
+
134
+ describe '.add_index' do
135
+ it 'allows to create a new index' do
136
+ described_class.add_index(name: 'user_id', for: '(n:User)', on: 'n.id')
137
+ expect(described_class.index_names).to match_array(['user_id'])
138
+ end
139
+ end
140
+
141
+ describe '.drop_index' do
142
+ before do
143
+ described_class.add_index(name: 'user_id', for: '(n:User)', on: 'n.id')
144
+ end
145
+
146
+ context 'with an existing index' do
147
+ it 'allows to drop an index' do
148
+ described_class.drop_index('user_id')
149
+ expect(described_class.index_names).to match_array([])
150
+ end
151
+ end
152
+
153
+ context 'without an existing index' do
154
+ it 'does not raise errors' do
155
+ expect { described_class.drop_index('unknown') }.not_to raise_error
156
+ end
157
+ end
158
+ end
159
+
160
+ describe '.add_constraint' do
161
+ it 'allows to create a new constraint' do
162
+ described_class.add_constraint(name: 'uniq_user_email', for: '(n:User)',
163
+ require: 'n.email IS UNIQUE')
164
+ expect(described_class.constraint_names).to \
165
+ match_array(['uniq_user_email'])
166
+ end
167
+ end
168
+
169
+ describe '.drop_constraint' do
170
+ before do
171
+ described_class.add_constraint(name: 'uniq_user_email', for: '(n:User)',
172
+ require: 'n.email IS UNIQUE')
173
+ end
174
+
175
+ context 'with an existing constraint' do
176
+ it 'allows to drop an constraint' do
177
+ described_class.drop_constraint('uniq_user_email')
178
+ expect(described_class.constraint_names).to match_array([])
179
+ end
180
+ end
181
+
182
+ context 'without an existing constraint' do
183
+ it 'does not raise errors' do
184
+ expect { described_class.drop_constraint('unknown') }.not_to \
185
+ raise_error
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,418 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Boltless::Extensions::Transactions do
6
+ let(:described_class) { Boltless }
7
+ let(:access_mode) { :write }
8
+ let(:statement_payload) do
9
+ cypher, params = statement
10
+ [cypher, (params || {}).merge(opts)]
11
+ end
12
+ let(:statement_payloads) do
13
+ statements.map do |(cypher, params)|
14
+ [cypher, (params || {}).merge(opts)]
15
+ end
16
+ end
17
+ let(:opts) { {} }
18
+ let(:create_users) do
19
+ Boltless.one_shot! do |tx|
20
+ %w[Bernd Klaus Uwe Monika].each do |name|
21
+ tx.add('CREATE (n:User { name: $name })', name: name)
22
+ end
23
+ end
24
+ end
25
+ let(:create_user_statement) do
26
+ ['CREATE (n:User { name: $name })', { name: 'Silke' }]
27
+ end
28
+ let(:fetch_users_statement) do
29
+ ['MATCH (n:User) RETURN n.name AS name', {}]
30
+ end
31
+ let(:count_users_statement) do
32
+ ['MATCH (n:User) RETURN count(n) AS count', {}]
33
+ end
34
+ let(:statement_with_syntax_errors) do
35
+ ['SOME THING!', {}]
36
+ end
37
+ let(:fetch_date_statement) do
38
+ ['RETURN date() AS date', {}]
39
+ end
40
+ let(:fetch_static_number_statement) do
41
+ ['RETURN 9867 AS number', {}]
42
+ end
43
+
44
+ before { clean_neo4j! }
45
+
46
+ describe '.execute!' do
47
+ let(:action) do
48
+ cypher, args = statement_payload
49
+ described_class.execute!(cypher, **args)
50
+ end
51
+
52
+ context 'with Cypher syntax errors' do
53
+ let(:statement) { statement_with_syntax_errors }
54
+
55
+ it 'raises an Boltless::Errors::TransactionRollbackError' do
56
+ expect { action }.to \
57
+ raise_error(Boltless::Errors::TransactionRollbackError,
58
+ /invalid input/i)
59
+ end
60
+ end
61
+
62
+ context 'with multiple rows' do
63
+ let(:statement) { fetch_users_statement }
64
+
65
+ before { create_users }
66
+
67
+ it 'returns the user names' do
68
+ expect(action.pluck(:name)).to \
69
+ match_array(%w[Bernd Klaus Uwe Monika])
70
+ end
71
+
72
+ it 'returns a Boltless::Result' do
73
+ expect(action).to be_a(Boltless::Result)
74
+ end
75
+ end
76
+
77
+ context 'with write operations on a read-only transaction' do
78
+ let(:statement) { create_user_statement }
79
+ let(:opts) { { access_mode: :read } }
80
+
81
+ it 'raises an Boltless::Errors::TransactionRollbackError' do
82
+ expect { action }.to \
83
+ raise_error(Boltless::Errors::TransactionRollbackError,
84
+ /Neo.ClientError.Request.Invalid/i)
85
+ end
86
+ end
87
+ end
88
+
89
+ describe '.execute' do
90
+ let(:action) do
91
+ cypher, args = statement_payload
92
+ described_class.execute(cypher, **args)
93
+ end
94
+
95
+ context 'with Cypher syntax errors' do
96
+ let(:statement) { statement_with_syntax_errors }
97
+
98
+ it 'returns nil' do
99
+ expect(action).to be(nil)
100
+ end
101
+ end
102
+
103
+ context 'with multiple rows' do
104
+ let(:statement) { fetch_users_statement }
105
+
106
+ before { create_users }
107
+
108
+ it 'returns the user names' do
109
+ expect(action.pluck(:name)).to \
110
+ match_array(%w[Bernd Klaus Uwe Monika])
111
+ end
112
+
113
+ it 'returns a Boltless::Result' do
114
+ expect(action).to be_a(Boltless::Result)
115
+ end
116
+ end
117
+
118
+ context 'with write operations on a read-only transaction' do
119
+ let(:statement) { create_user_statement }
120
+ let(:opts) { { access_mode: :read } }
121
+
122
+ it 'returns nil' do
123
+ expect(action).to be(nil)
124
+ end
125
+ end
126
+ end
127
+
128
+ describe '.one_shot!' do
129
+ let(:action) do
130
+ described_class.one_shot!(access_mode) do |tx|
131
+ statement_payloads.each { |cypher, args| tx.add(cypher, **args) }
132
+ end
133
+ end
134
+
135
+ context 'with an error in between' do
136
+ let(:statements) do
137
+ [
138
+ create_user_statement,
139
+ statement_with_syntax_errors,
140
+ create_user_statement,
141
+ fetch_users_statement
142
+ ]
143
+ end
144
+
145
+ it 'raises an Boltless::Errors::TransactionRollbackError' do
146
+ expect { action }.to \
147
+ raise_error(Boltless::Errors::TransactionRollbackError,
148
+ /Neo.ClientError.Statement.SyntaxError/i)
149
+ end
150
+
151
+ it 'rolls back the transaction (no data is written)' do
152
+ suppress(StandardError) { action }
153
+ cypher, args = count_users_statement
154
+ expect(described_class.execute!(cypher, **args).value).to be_eql(0)
155
+ end
156
+ end
157
+
158
+ context 'with multiple statements' do
159
+ let(:statements) do
160
+ [
161
+ create_user_statement,
162
+ create_user_statement,
163
+ create_user_statement,
164
+ count_users_statement
165
+ ]
166
+ end
167
+
168
+ it 'returns 4 results (one for each statement)' do
169
+ expect(action.count).to be_eql(4)
170
+ end
171
+
172
+ it 'returns the correct created user count' do
173
+ expect(action.last.value).to be_eql(3)
174
+ end
175
+ end
176
+ end
177
+
178
+ describe '.one_shot' do
179
+ let(:action) do
180
+ described_class.one_shot(access_mode) do |tx|
181
+ statement_payloads.each { |cypher, args| tx.add(cypher, **args) }
182
+ end
183
+ end
184
+
185
+ context 'with an error in between' do
186
+ let(:statements) do
187
+ [
188
+ create_user_statement,
189
+ statement_with_syntax_errors,
190
+ create_user_statement,
191
+ fetch_users_statement
192
+ ]
193
+ end
194
+
195
+ it 'returns nil' do
196
+ expect(action).to be(nil)
197
+ end
198
+
199
+ it 'rolls back the transaction (no data is written)' do
200
+ suppress(StandardError) { action }
201
+ cypher, args = count_users_statement
202
+ expect(described_class.execute!(cypher, **args).value).to be_eql(0)
203
+ end
204
+ end
205
+
206
+ context 'with multiple statements' do
207
+ let(:statements) do
208
+ [
209
+ create_user_statement,
210
+ create_user_statement,
211
+ create_user_statement,
212
+ count_users_statement
213
+ ]
214
+ end
215
+
216
+ it 'returns 4 results (one for each statement)' do
217
+ expect(action.count).to be_eql(4)
218
+ end
219
+
220
+ it 'returns the correct created user count' do
221
+ expect(action.last.value).to be_eql(3)
222
+ end
223
+ end
224
+ end
225
+
226
+ describe '.transaction!' do
227
+ let(:action) { described_class.transaction!(access_mode, &user_block) }
228
+
229
+ context 'with an error in between' do
230
+ let(:user_block) do
231
+ proc do |tx|
232
+ cypher, args = create_user_statement
233
+ tx.run!(cypher, **args)
234
+
235
+ cypher, args = statement_with_syntax_errors
236
+ tx.run!(cypher, **args)
237
+ end
238
+ end
239
+
240
+ it 'raises an Boltless::Errors::TransactionRollbackError' do
241
+ expect { action }.to \
242
+ raise_error(Boltless::Errors::TransactionRollbackError,
243
+ /Neo.ClientError.Statement.SyntaxError/i)
244
+ end
245
+
246
+ it 'rolls back the transaction (no data is written)' do
247
+ suppress(StandardError) { action }
248
+ cypher, args = count_users_statement
249
+ expect(described_class.execute!(cypher, **args).value).to \
250
+ be_eql(0)
251
+ end
252
+ end
253
+
254
+ context 'with manual rollback (without raised errors)' do
255
+ let(:user_block) do
256
+ proc do |tx|
257
+ cypher, args = create_user_statement
258
+ tx.run!(cypher, **args)
259
+ tx.rollback!
260
+ end
261
+ end
262
+
263
+ it 'returns true' do
264
+ expect(action).to be_eql(true)
265
+ end
266
+
267
+ it 'rolls back the transaction (no data is written)' do
268
+ suppress(StandardError) { action }
269
+ cypher, args = count_users_statement
270
+ expect(described_class.execute!(cypher, **args).value).to be_eql(0)
271
+ end
272
+ end
273
+
274
+ context 'with manual commit (without raised errors)' do
275
+ let(:user_block) do
276
+ proc do |tx|
277
+ cypher, args = create_user_statement
278
+ tx.run!(cypher, **args)
279
+ tx.commit!
280
+ end
281
+ end
282
+
283
+ it 'returns an empty array (due to no finalization statements given)' do
284
+ expect(action).to match_array([])
285
+ end
286
+
287
+ it 'completed the transaction (data is written)' do
288
+ suppress(StandardError) { action }
289
+ cypher, args = count_users_statement
290
+ expect(described_class.execute!(cypher, **args).value).to be_eql(1)
291
+ end
292
+ end
293
+
294
+ context 'with intermediate results' do
295
+ # rubocop:disable RSpec/MultipleExpectations because of the
296
+ # in-block testing
297
+ # rubocop:disable RSpec/ExampleLength dito
298
+ it 'allows direct access to each result' do
299
+ Boltless.transaction! do |tx|
300
+ cypher, args = fetch_date_statement
301
+ expect(tx.run!(cypher, **args).value).to be_eql(Date.today.to_s)
302
+
303
+ cypher, args = fetch_static_number_statement
304
+ expect(tx.run!(cypher, **args).value).to be_eql(9867)
305
+ end
306
+ end
307
+ # rubocop:enable RSpec/MultipleExpectations
308
+ # rubocop:enable RSpec/ExampleLength
309
+ end
310
+ end
311
+
312
+ describe '.transaction' do
313
+ let(:action) { described_class.transaction(access_mode, &user_block) }
314
+
315
+ context 'with an error in between (not raised)' do
316
+ let(:user_block) do
317
+ proc do |tx|
318
+ cypher, args = create_user_statement
319
+ tx.run(cypher, **args)
320
+
321
+ cypher, args = statement_with_syntax_errors
322
+ tx.run(cypher, **args)
323
+ end
324
+ end
325
+
326
+ it 'returns nil' do
327
+ expect(action).to be(nil)
328
+ end
329
+
330
+ it 'rolls back the transaction (no data is written)' do
331
+ suppress(StandardError) { action }
332
+ cypher, args = count_users_statement
333
+ expect(described_class.execute!(cypher, **args).value).to be_eql(0)
334
+ end
335
+ end
336
+
337
+ context 'with an error in between (raised)' do
338
+ let(:user_block) do
339
+ proc do |tx|
340
+ cypher, args = create_user_statement
341
+ tx.run(cypher, **args)
342
+
343
+ cypher, args = statement_with_syntax_errors
344
+ tx.run!(cypher, **args)
345
+ end
346
+ end
347
+
348
+ it 'raises an Boltless::Errors::TransactionRollbackError' do
349
+ expect { action }.to \
350
+ raise_error(Boltless::Errors::TransactionRollbackError,
351
+ /Neo.ClientError.Statement.SyntaxError/i)
352
+ end
353
+
354
+ it 'rolls back the transaction (no data is written)' do
355
+ suppress(StandardError) { action }
356
+ cypher, args = count_users_statement
357
+ expect(described_class.execute!(cypher, **args).value).to be_eql(0)
358
+ end
359
+ end
360
+
361
+ context 'with manual rollback (without raised errors)' do
362
+ let(:user_block) do
363
+ proc do |tx|
364
+ cypher, args = create_user_statement
365
+ tx.run(cypher, **args)
366
+ tx.rollback
367
+ end
368
+ end
369
+
370
+ it 'returns true' do
371
+ expect(action).to be_eql(true)
372
+ end
373
+
374
+ it 'rolls back the transaction (no data is written)' do
375
+ suppress(StandardError) { action }
376
+ cypher, args = count_users_statement
377
+ expect(described_class.execute!(cypher, **args).value).to be_eql(0)
378
+ end
379
+ end
380
+
381
+ context 'with manual commit (without raised errors)' do
382
+ let(:user_block) do
383
+ proc do |tx|
384
+ cypher, args = create_user_statement
385
+ tx.run(cypher, **args)
386
+ tx.commit
387
+ end
388
+ end
389
+
390
+ it 'returns an empty array (due to no finalization statements given)' do
391
+ expect(action).to match_array([])
392
+ end
393
+
394
+ it 'completed the transaction (data is written)' do
395
+ suppress(StandardError) { action }
396
+ cypher, args = count_users_statement
397
+ expect(described_class.execute!(cypher, **args).value).to be_eql(1)
398
+ end
399
+ end
400
+
401
+ context 'with intermediate results' do
402
+ # rubocop:disable RSpec/MultipleExpectations because of the
403
+ # in-block testing
404
+ # rubocop:disable RSpec/ExampleLength dito
405
+ it 'allows direct access to each result' do
406
+ Boltless.transaction do |tx|
407
+ cypher, args = fetch_date_statement
408
+ expect(tx.run(cypher, **args).value).to be_eql(Date.today.to_s)
409
+
410
+ cypher, args = fetch_static_number_statement
411
+ expect(tx.run(cypher, **args).value).to be_eql(9867)
412
+ end
413
+ end
414
+ # rubocop:enable RSpec/MultipleExpectations
415
+ # rubocop:enable RSpec/ExampleLength
416
+ end
417
+ end
418
+ end