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,546 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Boltless::Extensions::Utilities do
6
+ let(:described_class) { Boltless }
7
+
8
+ describe '.build_cypher' do
9
+ let(:action) { described_class.build_cypher(**replacements) { cypher } }
10
+
11
+ context 'without replacements' do
12
+ let(:cypher) { 'CREATE (n:User)' }
13
+ let(:replacements) { {} }
14
+
15
+ it 'returns the untouched input string' do
16
+ expect(action).to be_eql(cypher)
17
+ end
18
+ end
19
+
20
+ context 'with replacements (unbalanced)' do
21
+ let(:cypher) { 'CREATE (n:%<subject>s)' }
22
+ let(:replacements) { { object: 'test' } }
23
+
24
+ it 'raises a KeyError' do
25
+ expect { action }.to raise_error(KeyError, /subject.*not found/)
26
+ end
27
+ end
28
+
29
+ context 'with replacements (balanced, untouched string)' do
30
+ let(:cypher) { 'CREATE (n:%<subject>s)' }
31
+ let(:replacements) { { subject: 'User' } }
32
+
33
+ it 'returns the processed input string' do
34
+ expect(action).to be_eql('CREATE (n:User)')
35
+ end
36
+ end
37
+
38
+ context 'with replacements (balanced, single label symbol, +label+)' do
39
+ let(:cypher) { 'CREATE (n:%<label>s)' }
40
+ let(:replacements) { { label: :user } }
41
+
42
+ it 'returns the processed input string' do
43
+ expect(action).to be_eql('CREATE (n:User)')
44
+ end
45
+ end
46
+
47
+ context 'with replacements (balanced, single label symbol)' do
48
+ let(:cypher) { 'CREATE (n:%<subject_label>s)' }
49
+ let(:replacements) { { subject_label: :user } }
50
+
51
+ it 'returns the processed input string' do
52
+ expect(action).to be_eql('CREATE (n:User)')
53
+ end
54
+ end
55
+
56
+ context 'with replacements (balanced, multiple label symbols)' do
57
+ let(:cypher) { 'CREATE (n:%<subject_labels>s)' }
58
+ let(:replacements) { { subject_labels: %i[user customer] } }
59
+
60
+ it 'returns the processed input string' do
61
+ expect(action).to be_eql('CREATE (n:Customer:User)')
62
+ end
63
+ end
64
+
65
+ context 'with replacements (balanced, single type symbol, +type+)' do
66
+ let(:cypher) { 'CREATE (a)-[:%<type>s]->(b)' }
67
+ let(:replacements) { { type: :read } }
68
+
69
+ it 'returns the processed input string' do
70
+ expect(action).to be_eql('CREATE (a)-[:READ]->(b)')
71
+ end
72
+ end
73
+
74
+ context 'with replacements (balanced, single type symbol)' do
75
+ let(:cypher) { 'CREATE (a)-[:%<predicate_type>s]->(b)' }
76
+ let(:replacements) { { predicate_type: :read } }
77
+
78
+ it 'returns the processed input string' do
79
+ expect(action).to be_eql('CREATE (a)-[:READ]->(b)')
80
+ end
81
+ end
82
+
83
+ context 'with replacements (balanced, multiple type symbols)' do
84
+ let(:cypher) { 'CREATE (a)-[:%<predicate_types>s]->(b)' }
85
+ let(:replacements) { { predicate_types: %i[read write] } }
86
+
87
+ it 'returns the processed input string' do
88
+ expect(action).to be_eql('CREATE (a)-[:READ|WRITE]->(b)')
89
+ end
90
+ end
91
+
92
+ context 'with replacements (balanced, single string)' do
93
+ let(:cypher) { 'CREATE (n:User { name: %<name_str>s })' }
94
+ let(:replacements) { { name_str: 'Klaus' } }
95
+
96
+ it 'returns the processed input string' do
97
+ expect(action).to be_eql('CREATE (n:User { name: "Klaus" })')
98
+ end
99
+ end
100
+
101
+ context 'with replacements (balanced, multiple strings)' do
102
+ let(:cypher) { 'CREATE (n:User { states: [%<state_strs>s] })' }
103
+ let(:replacements) { { state_strs: %w[active locked] } }
104
+
105
+ it 'returns the processed input string' do
106
+ expect(action).to \
107
+ be_eql('CREATE (n:User { states: ["active", "locked"] })')
108
+ end
109
+ end
110
+
111
+ context 'without replacements, with comments' do
112
+ let(:cypher) do
113
+ <<~CYPHER
114
+ // Nice comment!
115
+ CREATE (n:User) // Yay.
116
+ // Other comment
117
+ CYPHER
118
+ end
119
+ let(:replacements) { {} }
120
+
121
+ it 'returns the processed input string' do
122
+ expect(action).to \
123
+ be_eql('CREATE (n:User)')
124
+ end
125
+ end
126
+ end
127
+
128
+ describe '.prepare_label' do
129
+ context 'with a single string' do
130
+ it 'returns the escaped string' do
131
+ expect(described_class.prepare_label('user')).to \
132
+ be_eql('User')
133
+ end
134
+ end
135
+
136
+ context 'with multiple strings' do
137
+ it 'returns the escaped string' do
138
+ expect(described_class.prepare_label('user', 'payment')).to \
139
+ be_eql('Payment:User')
140
+ end
141
+ end
142
+
143
+ context 'with nested strings' do
144
+ it 'returns the escaped string' do
145
+ res = described_class.prepare_label(['user'], [['payment']], 'session')
146
+ expect(res).to be_eql('Payment:Session:User')
147
+ end
148
+ end
149
+
150
+ context 'with some nasty strings' do
151
+ it 'returns the escaped string' do
152
+ expect(described_class.prepare_label('userv2', 'user+v2')).to \
153
+ be_eql('Userv2:`User+v2`')
154
+ end
155
+ end
156
+
157
+ context 'with an underscored string' do
158
+ it 'returns the escaped string' do
159
+ expect(described_class.prepare_label('payment_mandate')).to \
160
+ be_eql('PaymentMandate')
161
+ end
162
+ end
163
+
164
+ context 'with an camel-cased string' do
165
+ it 'returns the escaped string' do
166
+ expect(described_class.prepare_label('paymentMandate')).to \
167
+ be_eql('PaymentMandate')
168
+ end
169
+ end
170
+
171
+ context 'with an pascal-cased string' do
172
+ it 'returns the escaped string' do
173
+ expect(described_class.prepare_label('PaymentMandate')).to \
174
+ be_eql('PaymentMandate')
175
+ end
176
+ end
177
+
178
+ context 'with an underscored symbol' do
179
+ it 'returns the escaped string' do
180
+ expect(described_class.prepare_label(:payment_mandate)).to \
181
+ be_eql('PaymentMandate')
182
+ end
183
+ end
184
+
185
+ context 'with an kabab-cased string' do
186
+ it 'returns the escaped string' do
187
+ expect(described_class.prepare_label('payment-mandate')).to \
188
+ be_eql('PaymentMandate')
189
+ end
190
+ end
191
+
192
+ context 'with an boolean (false)' do
193
+ it 'returns the escaped string' do
194
+ expect(described_class.prepare_label(false)).to be_eql('False')
195
+ end
196
+ end
197
+
198
+ context 'with nil' do
199
+ it 'raises an ArgumentError' do
200
+ expect { described_class.prepare_label(nil) }.to \
201
+ raise_error(ArgumentError, /Bad labels: \[nil\]/)
202
+ end
203
+ end
204
+
205
+ context 'with mixed values' do
206
+ it 'returns the escaped string' do
207
+ res = described_class.prepare_label(
208
+ [:a], [true, [false, ['"nice"', "o'neil"], nil]]
209
+ )
210
+ expect(res).to be_eql("A:False:True:`\"nice\"`:`O\'neil`")
211
+ end
212
+ end
213
+
214
+ context 'with a set' do
215
+ it 'returns the escaped string' do
216
+ set = Set[:user, :payment, :session, true]
217
+ expect(described_class.prepare_label(set)).to \
218
+ be_eql('Payment:Session:True:User')
219
+ end
220
+ end
221
+ end
222
+
223
+ describe '.prepare_type' do
224
+ context 'with a single string' do
225
+ it 'returns the escaped string' do
226
+ expect(described_class.prepare_type('read')).to \
227
+ be_eql('READ')
228
+ end
229
+ end
230
+
231
+ context 'with multiple strings' do
232
+ it 'returns the escaped string' do
233
+ expect(described_class.prepare_type('read', 'write')).to \
234
+ be_eql('READ|WRITE')
235
+ end
236
+ end
237
+
238
+ context 'with nested strings' do
239
+ it 'returns the escaped string' do
240
+ res = described_class.prepare_type(['read'], [['write']], 'delete')
241
+ expect(res).to be_eql('DELETE|READ|WRITE')
242
+ end
243
+ end
244
+
245
+ context 'with some nasty strings' do
246
+ it 'returns the escaped string' do
247
+ expect(described_class.prepare_type('userv2', 'user+v2')).to \
248
+ be_eql('USERV2|`USER+V2`')
249
+ end
250
+ end
251
+
252
+ context 'with an underscored string' do
253
+ it 'returns the escaped string' do
254
+ expect(described_class.prepare_type('read_write')).to \
255
+ be_eql('READ_WRITE')
256
+ end
257
+ end
258
+
259
+ context 'with an camel-cased string' do
260
+ it 'returns the escaped string' do
261
+ expect(described_class.prepare_type('readWrite')).to \
262
+ be_eql('READ_WRITE')
263
+ end
264
+ end
265
+
266
+ context 'with an pascal-cased string' do
267
+ it 'returns the escaped string' do
268
+ expect(described_class.prepare_type('ReadWrite')).to \
269
+ be_eql('READ_WRITE')
270
+ end
271
+ end
272
+
273
+ context 'with an underscored symbol' do
274
+ it 'returns the escaped string' do
275
+ expect(described_class.prepare_type(:read_write)).to \
276
+ be_eql('READ_WRITE')
277
+ end
278
+ end
279
+
280
+ context 'with an kabab-cased string' do
281
+ it 'returns the escaped string' do
282
+ expect(described_class.prepare_type('read-write')).to \
283
+ be_eql('READ_WRITE')
284
+ end
285
+ end
286
+
287
+ context 'with an boolean (false)' do
288
+ it 'returns the escaped string' do
289
+ expect(described_class.prepare_type(false)).to be_eql('FALSE')
290
+ end
291
+ end
292
+
293
+ context 'with nil' do
294
+ it 'raises an ArgumentError' do
295
+ expect { described_class.prepare_type(nil) }.to \
296
+ raise_error(ArgumentError, /Bad types: \[nil\]/)
297
+ end
298
+ end
299
+
300
+ context 'with mixed values' do
301
+ it 'returns the escaped string' do
302
+ res = described_class.prepare_type(
303
+ [:a], [true, [false, ['"nice"', "o'neil"], nil], 1], 12.14
304
+ )
305
+ expect(res).to be_eql("1|A|FALSE|TRUE|`\"NICE\"`|`12.14`|`O\'NEIL`")
306
+ end
307
+ end
308
+
309
+ context 'with a set' do
310
+ it 'returns the escaped string' do
311
+ set = Set[:a, :b, :c, 1]
312
+ expect(described_class.prepare_type(set)).to be_eql('1|A|B|C')
313
+ end
314
+ end
315
+ end
316
+
317
+ describe '.prepare_string' do
318
+ context 'with a single string' do
319
+ it 'returns the escaped string' do
320
+ expect(described_class.prepare_string('super "cool" string')).to \
321
+ be_eql('"super \"cool\" string"')
322
+ end
323
+ end
324
+
325
+ context 'with multiple strings' do
326
+ it 'returns the escaped string' do
327
+ expect(described_class.prepare_string('a', 'b')).to \
328
+ be_eql('"a", "b"')
329
+ end
330
+ end
331
+
332
+ context 'with nested strings' do
333
+ it 'returns the escaped string' do
334
+ expect(described_class.prepare_string(['a'], [['b']], 'c')).to \
335
+ be_eql('"a", "b", "c"')
336
+ end
337
+ end
338
+
339
+ context 'with an integer' do
340
+ it 'returns the escaped string' do
341
+ expect(described_class.prepare_string(1)).to be_eql('"1"')
342
+ end
343
+ end
344
+
345
+ context 'with an boolean (true)' do
346
+ it 'returns the escaped string' do
347
+ expect(described_class.prepare_string(true)).to be_eql('"true"')
348
+ end
349
+ end
350
+
351
+ context 'with an boolean (false)' do
352
+ it 'returns the escaped string' do
353
+ expect(described_class.prepare_string(false)).to be_eql('"false"')
354
+ end
355
+ end
356
+
357
+ context 'with nil' do
358
+ it 'returns the escaped string' do
359
+ expect(described_class.prepare_string(nil)).to be_eql('""')
360
+ end
361
+ end
362
+
363
+ context 'with mixed values' do
364
+ it 'returns the escaped string' do
365
+ res = described_class.prepare_string(
366
+ [:a], [true, [false, ['"nice"', "o'neil"], nil], 1], 12.14
367
+ )
368
+ expect(res).to \
369
+ be_eql('"a", "true", "false", "\"nice\"", "o\'neil", "1", "12.14"')
370
+ end
371
+ end
372
+
373
+ context 'with a set' do
374
+ it 'returns the escaped string' do
375
+ set = Set[:a, :b, :c, 1]
376
+ expect(described_class.prepare_string(set)).to \
377
+ be_eql('"a", "b", "c", "1"')
378
+ end
379
+ end
380
+ end
381
+
382
+ describe '.to_options' do
383
+ let(:action) { described_class.to_options(obj) }
384
+ let(:obj) do
385
+ {
386
+ indexProvider: 'lucene+native-3.0',
387
+ indexConfig: {
388
+ 'spatial.wgs-84.min': [-100.0, -80.0],
389
+ 'spatial.wgs-84.max': [100.0, 80.0]
390
+ }
391
+ }
392
+ end
393
+ let(:cypher_opts) do
394
+ "{ `indexProvider`: 'lucene+native-3.0', " \
395
+ '`indexConfig`: { `spatial.wgs-84.min`: [ -100.0, -80.0 ], ' \
396
+ '`spatial.wgs-84.max`: [ 100.0, 80.0 ] } }'
397
+ end
398
+
399
+ context 'with a String' do
400
+ let(:obj) { 'test' }
401
+
402
+ it 'returns the Cypher options representation' do
403
+ expect(action).to be_eql(%('test'))
404
+ end
405
+ end
406
+
407
+ context 'with a flat Array' do
408
+ let(:obj) { ['test', 1.9235, 2, true, nil] }
409
+
410
+ it 'returns the Cypher options representation' do
411
+ expect(action).to be_eql(%([ 'test', 1.9235, 2, true ]))
412
+ end
413
+ end
414
+
415
+ context 'with a flat Hash' do
416
+ let(:obj) { { symbol: true, 'string' => 1.234 } }
417
+
418
+ it 'returns the Cypher options representation' do
419
+ expect(action).to be_eql(%({ `symbol`: true, `string`: 1.234 }))
420
+ end
421
+ end
422
+
423
+ context 'with a complex nested structure' do
424
+ it 'returns the Cypher options representation' do
425
+ expect(action).to be_eql(cypher_opts)
426
+ end
427
+ end
428
+ end
429
+
430
+ describe '.resolve_cypher' do
431
+ let(:action) { described_class.resolve_cypher(cypher, **args) }
432
+
433
+ describe 'Cypher without parameters and none given' do
434
+ let(:cypher) { 'CREATE (n:User)' }
435
+ let(:args) { {} }
436
+
437
+ it 'returns the untouched input string' do
438
+ expect(action).to be_eql(cypher)
439
+ end
440
+ end
441
+
442
+ describe 'Cypher with parameters and none given' do
443
+ let(:cypher) { 'CREATE (n:User { name: $name })' }
444
+ let(:args) { {} }
445
+
446
+ it 'returns the untouched input string' do
447
+ expect(action).to be_eql(cypher)
448
+ end
449
+ end
450
+
451
+ describe 'Cypher with parameters and parameters given (balanced)' do
452
+ let(:cypher) { 'CREATE (n:User { name: $name })' }
453
+ let(:args) { { name: 'Peter' } }
454
+
455
+ it 'returns the resolved string' do
456
+ expect(action).to be_eql('CREATE (n:User { name: "Peter" })')
457
+ end
458
+ end
459
+
460
+ describe 'Cypher with parameters and parameters given (unbalanced)' do
461
+ let(:cypher) { 'CREATE (n:User { name: $name, email: $email })' }
462
+ let(:args) { { email: 'peter@example.com' } }
463
+
464
+ it 'returns the resolved string' do
465
+ expect(action).to \
466
+ be_eql('CREATE (n:User { name: $name, email: "peter@example.com" })')
467
+ end
468
+ end
469
+ end
470
+
471
+ describe '.cypher_logging_color' do
472
+ let(:action) { described_class.cypher_logging_color(cypher) }
473
+
474
+ context 'with a transaction begin statement' do
475
+ let(:cypher) { 'BEGIN' }
476
+
477
+ it 'returns magenta' do
478
+ expect(action).to be_eql(:magenta)
479
+ end
480
+ end
481
+
482
+ context 'with a create statement' do
483
+ let(:cypher) { 'CREATE (n:User { name: $name })' }
484
+
485
+ it 'returns green' do
486
+ expect(action).to be_eql(:green)
487
+ end
488
+ end
489
+
490
+ context 'with a merge statement' do
491
+ let(:cypher) { 'MERGE (n:User { name: $name })' }
492
+
493
+ it 'returns yellow' do
494
+ expect(action).to be_eql(:yellow)
495
+ end
496
+ end
497
+
498
+ context 'with a match/set statement' do
499
+ let(:cypher) { 'MATCH (n:User { name: $name }) SET n.email = $email' }
500
+
501
+ it 'returns yellow' do
502
+ expect(action).to be_eql(:yellow)
503
+ end
504
+ end
505
+
506
+ context 'with a match/delete statement' do
507
+ let(:cypher) { 'MATCH (n:User { name: $name }) DELETE n' }
508
+
509
+ it 'returns red' do
510
+ expect(action).to be_eql(:red)
511
+ end
512
+ end
513
+
514
+ context 'with a match/remove statement' do
515
+ let(:cypher) { 'MATCH (n:User { name: $name }) REMOVE n.email' }
516
+
517
+ it 'returns red' do
518
+ expect(action).to be_eql(:red)
519
+ end
520
+ end
521
+
522
+ context 'with a transaction commit statement' do
523
+ let(:cypher) { 'COMMIT' }
524
+
525
+ it 'returns green' do
526
+ expect(action).to be_eql(:green)
527
+ end
528
+ end
529
+
530
+ context 'with a transaction rollback statement' do
531
+ let(:cypher) { 'ROLLBACK' }
532
+
533
+ it 'returns red' do
534
+ expect(action).to be_eql(:red)
535
+ end
536
+ end
537
+
538
+ context 'with a match statement' do
539
+ let(:cypher) { 'MATCH (n:User { name: $name }) RETURN n.email' }
540
+
541
+ it 'returns light blue' do
542
+ expect(action).to be_eql(:light_blue)
543
+ end
544
+ end
545
+ end
546
+ end