boltless 1.0.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.
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