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,601 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Boltless::Transaction do
6
+ let(:new_instance) { ->(**args) { described_class.new(connection, **args) } }
7
+ let(:instance) { new_instance.call }
8
+ let(:connection) { Boltless.connection_pool.checkout }
9
+ let(:request) { instance.instance_variable_get(:@request) }
10
+ let(:req_err) { ->(*) { raise Boltless::Errors::RequestError, 'test' } }
11
+ let(:res_err) { ->(*) { raise Boltless::Errors::ResponseError, 'test' } }
12
+ let(:raw_state) { nil }
13
+
14
+ before do
15
+ # Only force the raw state when it is configured
16
+ instance.instance_variable_set(:@raw_state, raw_state) if raw_state
17
+ end
18
+
19
+ describe 'full workflow' do
20
+ let(:opts) { {} }
21
+ let(:failed_action) do
22
+ tx = new_instance[**opts]
23
+ tx.begin
24
+ tx.run('CREATE (n:User { name: $name })', name: 'Klaus')
25
+ tx.run('SOME THING!')
26
+ tx.commit
27
+ end
28
+ let(:check_action) do
29
+ tx = new_instance[**opts]
30
+ tx.begin
31
+ tx.run('MATCH (n:User) RETURN n.name').tap do
32
+ tx.commit
33
+ end
34
+ end
35
+ let(:write_action) do
36
+ tx = new_instance[**opts]
37
+ tx.begin!
38
+ tx.run!('CREATE (n:User { name: $name })', name: 'Klaus')
39
+ tx.commit!
40
+ end
41
+
42
+ it 'rolls back the transaction on errors' do
43
+ failed_action
44
+ expect(check_action.count).to be_eql(0)
45
+ end
46
+
47
+ it 'returns a mapped result' do
48
+ expect(check_action).to be_a(Boltless::Result)
49
+ end
50
+
51
+ describe 'with raw results' do
52
+ let(:opts) { { raw_results: true } }
53
+
54
+ it 'returns an unmapped result' do
55
+ expect(check_action).to be_a(Hash)
56
+ end
57
+ end
58
+
59
+ describe 'with the read access mode' do
60
+ let(:opts) { { access_mode: :read } }
61
+
62
+ it 'does not allow us to perform write operations' do
63
+ expect { write_action }.to \
64
+ raise_error(Boltless::Errors::TransactionRollbackError,
65
+ /Neo.ClientError.Request.Invalid/i)
66
+ end
67
+ end
68
+ end
69
+
70
+ describe 'delegations' do
71
+ it 'allows to access the #build_cypher utility' do
72
+ expect(instance.respond_to?(:build_cypher)).to be_eql(true)
73
+ end
74
+ end
75
+
76
+ describe '#initialize' do
77
+ it 'passes down the connection to the request' do
78
+ expect(Boltless::Request).to \
79
+ receive(:new).with(connection, anything)
80
+ described_class.new(connection)
81
+ end
82
+
83
+ it 'passes down the access mode to the request' do
84
+ expect(Boltless::Request).to \
85
+ receive(:new).with(connection, a_hash_including(access_mode: :read))
86
+ described_class.new(connection, access_mode: :read)
87
+ end
88
+
89
+ it 'passes down the database to the request' do
90
+ expect(Boltless::Request).to \
91
+ receive(:new).with(connection, a_hash_including(database: 'test'))
92
+ described_class.new(connection, database: 'test')
93
+ end
94
+
95
+ it 'passes down the raw results flag to the request' do
96
+ expect(Boltless::Request).to \
97
+ receive(:new).with(connection, a_hash_including(raw_results: true))
98
+ described_class.new(connection, raw_results: true)
99
+ end
100
+
101
+ context 'with unknown access mode' do
102
+ it 'raises an ArgumentError' do
103
+ expect { described_class.new(connection, access_mode: :unknown) }.to \
104
+ raise_error(
105
+ ArgumentError,
106
+ /Unknown access mode 'unknown'.*use ':read' or ':write'/i
107
+ )
108
+ end
109
+ end
110
+ end
111
+
112
+ describe '#access_mode' do
113
+ let(:action) { instance.access_mode }
114
+
115
+ context 'when not explictly configured' do
116
+ it 'returns write' do
117
+ expect(action).to be_eql(:write)
118
+ end
119
+ end
120
+
121
+ context 'when initialized with read' do
122
+ let(:instance) { new_instance[access_mode: :read] }
123
+
124
+ it 'returns read' do
125
+ expect(action).to be_eql(:read)
126
+ end
127
+ end
128
+
129
+ context 'when initialized with write' do
130
+ let(:instance) { new_instance[access_mode: :write] }
131
+
132
+ it 'returns write' do
133
+ expect(action).to be_eql(:write)
134
+ end
135
+ end
136
+ end
137
+
138
+ describe '#id' do
139
+ let(:action) { instance.id }
140
+
141
+ context 'when not yet started' do
142
+ it 'returns nil' do
143
+ expect(action).to be(nil)
144
+ end
145
+ end
146
+
147
+ context 'when started' do
148
+ before { instance.begin! }
149
+
150
+ it 'returns an Integer' do
151
+ expect(action).to be_a(Integer)
152
+ end
153
+ end
154
+ end
155
+
156
+ describe '#raw_state' do
157
+ let(:action) { instance.raw_state }
158
+
159
+ it 'returns an Symbol' do
160
+ expect(action).to be_a(Symbol)
161
+ end
162
+
163
+ describe 'after initialization' do
164
+ it 'returns not_yet_started' do
165
+ expect(action).to be_eql(:not_yet_started)
166
+ end
167
+ end
168
+ end
169
+
170
+ describe '#state' do
171
+ let(:action) { instance.state }
172
+
173
+ it 'returns an ActiveSupport::StringInquirer' do
174
+ expect(action).to be_a(ActiveSupport::StringInquirer)
175
+ end
176
+
177
+ describe 'after initialization' do
178
+ it 'returns not_yet_started' do
179
+ expect(action).to be_eql('not_yet_started')
180
+ end
181
+ end
182
+ end
183
+
184
+ describe '#begin!' do
185
+ let(:action) { instance.begin! }
186
+ let(:instance) { new_instance[access_mode: :read] }
187
+
188
+ before { allow(request).to receive(:begin_transaction) }
189
+
190
+ context 'when the transaction is not in a usable state' do
191
+ let(:raw_state) { :closed }
192
+
193
+ it 'raises a Boltless::Errors::TransactionInBadStateError' do
194
+ expect { action }.to \
195
+ raise_error(Boltless::Errors::TransactionInBadStateError,
196
+ /Transaction already closed/i)
197
+ end
198
+ end
199
+
200
+ it 'call the #begin_transaction on the request' do
201
+ expect(request).to receive(:begin_transaction).once
202
+ action
203
+ end
204
+
205
+ it 'returns true' do
206
+ expect(action).to be_eql(true)
207
+ end
208
+
209
+ it 'switches the state to open' do
210
+ expect { action }.to \
211
+ change(instance, :state).from('not_yet_started').to('open')
212
+ end
213
+ end
214
+
215
+ describe '#begin' do
216
+ let(:action) { instance.begin }
217
+
218
+ context 'when the transaction is not in a usable state' do
219
+ let(:raw_state) { :closed }
220
+
221
+ it 'returns false' do
222
+ expect(action).to be_eql(false)
223
+ end
224
+ end
225
+
226
+ it 'wraps the bang-variant in a #handle_errors call' do
227
+ expect(instance).to receive(:handle_errors).with(false)
228
+ action
229
+ end
230
+
231
+ context 'with errors' do
232
+ before { allow(instance).to receive(:begin!, &res_err) }
233
+
234
+ it 'returns false' do
235
+ expect(action).to be_eql(false)
236
+ end
237
+ end
238
+
239
+ context 'without errors' do
240
+ before { allow(request).to receive(:begin_transaction) }
241
+
242
+ it 'returns true' do
243
+ expect(action).to be_eql(true)
244
+ end
245
+
246
+ it 'switches the state to open' do
247
+ expect { action }.to \
248
+ change(instance, :state).from('not_yet_started').to('open')
249
+ end
250
+ end
251
+ end
252
+
253
+ describe '#run!' do
254
+ let(:action) { instance.run!('RETURN 1') }
255
+
256
+ context 'when the transaction is not in a usable state' do
257
+ let(:raw_state) { :closed }
258
+
259
+ it 'raises a Boltless::Errors::TransactionInBadStateError' do
260
+ expect { action }.to \
261
+ raise_error(Boltless::Errors::TransactionInBadStateError,
262
+ /Transaction not open/i)
263
+ end
264
+ end
265
+
266
+ context 'with open state' do
267
+ before { instance.begin! }
268
+
269
+ it 'calls the #run_query on the request' do
270
+ expect(request).to \
271
+ receive(:run_query).with(instance.id, Hash).and_return([])
272
+ action
273
+ end
274
+
275
+ it 'returns the result' do
276
+ res = instance.run!('RETURN date() AS date')
277
+ expect(res.value).to be_eql(Date.today.to_s)
278
+ end
279
+ end
280
+ end
281
+
282
+ describe '#run' do
283
+ let(:action) { instance.run('RETURN date()') }
284
+ let(:raw_state) { :open }
285
+
286
+ context 'when the transaction is not in a usable state' do
287
+ let(:raw_state) { :closed }
288
+
289
+ it 'returns nil' do
290
+ expect(action).to be_eql(nil)
291
+ end
292
+ end
293
+
294
+ it 'wraps the bang-variant in a #handle_errors call' do
295
+ expect(instance).to receive(:handle_errors)
296
+ action
297
+ end
298
+
299
+ context 'with errors' do
300
+ before { allow(instance).to receive(:run!, &res_err) }
301
+
302
+ it 'returns nil' do
303
+ expect(action).to be_eql(nil)
304
+ end
305
+ end
306
+
307
+ context 'without errors' do
308
+ before { allow(request).to receive(:run_query).and_return([123]) }
309
+
310
+ it 'returns an the first result' do
311
+ expect(action).to be_eql(123)
312
+ end
313
+ end
314
+ end
315
+
316
+ describe '#run_in_batch!' do
317
+ let(:action) { instance.run_in_batch!(['RETURN 1'], ['RETURN date()']) }
318
+
319
+ context 'when the transaction is not in a usable state' do
320
+ let(:raw_state) { :closed }
321
+
322
+ it 'raises a Boltless::Errors::TransactionInBadStateError' do
323
+ expect { action }.to \
324
+ raise_error(Boltless::Errors::TransactionInBadStateError,
325
+ /Transaction not open/i)
326
+ end
327
+ end
328
+
329
+ context 'with open state' do
330
+ let(:statements) do
331
+ [
332
+ {
333
+ statement: 'RETURN 1',
334
+ parameters: {}
335
+ },
336
+ {
337
+ statement: 'RETURN date()',
338
+ parameters: {}
339
+ }
340
+ ]
341
+ end
342
+
343
+ before { instance.begin! }
344
+
345
+ it 'calls the #run_query on the request' do
346
+ expect(request).to receive(:run_query).with(instance.id, *statements)
347
+ action
348
+ end
349
+
350
+ it 'returns two results (one for each statement)' do
351
+ expect(action.count).to be_eql(2)
352
+ end
353
+
354
+ it 'returns the correct result (first statement)' do
355
+ expect(action.first.value).to be_eql(1)
356
+ end
357
+
358
+ it 'returns the correct result (second statement)' do
359
+ expect(action.last.value).to be_eql(Date.today.to_s)
360
+ end
361
+ end
362
+ end
363
+
364
+ describe '#run_in_batch' do
365
+ let(:action) { instance.run_in_batch(['RETURN date()'], ['RETURN 1']) }
366
+ let(:raw_state) { :open }
367
+
368
+ context 'when the transaction is not in a usable state' do
369
+ let(:raw_state) { :closed }
370
+
371
+ it 'returns nil' do
372
+ expect(action).to be_eql(nil)
373
+ end
374
+ end
375
+
376
+ it 'wraps the bang-variant in a #handle_errors call' do
377
+ expect(instance).to receive(:handle_errors)
378
+ action
379
+ end
380
+
381
+ context 'with errors' do
382
+ before { allow(instance).to receive(:run_in_batch!, &res_err) }
383
+
384
+ it 'returns nil' do
385
+ expect(action).to be_eql(nil)
386
+ end
387
+ end
388
+
389
+ context 'without errors' do
390
+ before { allow(request).to receive(:run_query).and_return([]) }
391
+
392
+ it 'returns an empty array' do
393
+ expect(action).to match_array([])
394
+ end
395
+ end
396
+ end
397
+
398
+ describe '#commit!' do
399
+ let(:action) { instance.commit! }
400
+ let(:instance) { new_instance[access_mode: :read] }
401
+
402
+ context 'when the transaction is not in a usable state' do
403
+ let(:raw_state) { :closed }
404
+
405
+ it 'raises a Boltless::Errors::TransactionInBadStateError' do
406
+ expect { action }.to \
407
+ raise_error(Boltless::Errors::TransactionInBadStateError,
408
+ /Transaction not open/i)
409
+ end
410
+ end
411
+
412
+ context 'with open state' do
413
+ before { instance.begin! }
414
+
415
+ it 'calls the #commit_transaction on the request' do
416
+ expect(request).to receive(:commit_transaction).with(instance.id)
417
+ action
418
+ end
419
+
420
+ it 'allows to send finalizing statements' do
421
+ res = instance.commit!(['RETURN date() AS date'])
422
+ expect(res.first.value).to be_eql(Date.today.to_s)
423
+ end
424
+
425
+ it 'switches the state to closed' do
426
+ expect { action }.to \
427
+ change(instance, :state).from('open').to('closed')
428
+ end
429
+ end
430
+ end
431
+
432
+ describe '#commit' do
433
+ let(:action) { instance.commit }
434
+ let(:raw_state) { :open }
435
+
436
+ context 'when the transaction is not in a usable state' do
437
+ let(:raw_state) { :closed }
438
+
439
+ it 'returns nil' do
440
+ expect(action).to be_eql(nil)
441
+ end
442
+ end
443
+
444
+ it 'wraps the bang-variant in a #handle_errors call' do
445
+ expect(instance).to receive(:handle_errors)
446
+ action
447
+ end
448
+
449
+ context 'with errors' do
450
+ before { allow(instance).to receive(:commit!, &res_err) }
451
+
452
+ it 'returns nil' do
453
+ expect(action).to be_eql(nil)
454
+ end
455
+ end
456
+
457
+ context 'without errors' do
458
+ before { allow(request).to receive(:commit_transaction).and_return([]) }
459
+
460
+ it 'returns an empty array' do
461
+ expect(action).to match_array([])
462
+ end
463
+
464
+ it 'switches the state to closed' do
465
+ expect { action }.to \
466
+ change(instance, :state).from('open').to('closed')
467
+ end
468
+ end
469
+ end
470
+
471
+ describe '#rollback!' do
472
+ let(:action) { instance.rollback! }
473
+ let(:instance) { new_instance[access_mode: :read] }
474
+
475
+ context 'when the transaction is not in a usable state' do
476
+ let(:raw_state) { :closed }
477
+
478
+ it 'raises a Boltless::Errors::TransactionInBadStateError' do
479
+ expect { action }.to \
480
+ raise_error(Boltless::Errors::TransactionInBadStateError,
481
+ /Transaction not open/i)
482
+ end
483
+ end
484
+
485
+ context 'with open state' do
486
+ before { instance.begin! }
487
+
488
+ it 'calls the #rollback_transaction on the request' do
489
+ expect(request).to receive(:rollback_transaction).with(instance.id)
490
+ action
491
+ end
492
+
493
+ it 'returns true' do
494
+ expect(action).to be_eql(true)
495
+ end
496
+
497
+ it 'switches the state to closed' do
498
+ expect { action }.to \
499
+ change(instance, :state).from('open').to('closed')
500
+ end
501
+ end
502
+ end
503
+
504
+ describe '#rollback' do
505
+ let(:action) { instance.rollback }
506
+ let(:raw_state) { :open }
507
+
508
+ context 'when the transaction is not in a usable state' do
509
+ let(:raw_state) { :closed }
510
+
511
+ it 'returns false' do
512
+ expect(action).to be_eql(false)
513
+ end
514
+ end
515
+
516
+ it 'wraps the bang-variant in a #handle_errors call' do
517
+ expect(instance).to receive(:handle_errors).with(false)
518
+ action
519
+ end
520
+
521
+ context 'with errors' do
522
+ before { allow(instance).to receive(:rollback!, &res_err) }
523
+
524
+ it 'returns false' do
525
+ expect(action).to be_eql(false)
526
+ end
527
+ end
528
+
529
+ context 'without errors' do
530
+ before { allow(request).to receive(:rollback_transaction) }
531
+
532
+ it 'returns true' do
533
+ expect(action).to be_eql(true)
534
+ end
535
+
536
+ it 'switches the state to closed' do
537
+ expect { action }.to \
538
+ change(instance, :state).from('open').to('closed')
539
+ end
540
+ end
541
+ end
542
+
543
+ describe '#handle_errors' do
544
+ let(:action) { ->(*args, &block) { instance.handle_errors(*args, &block) } }
545
+
546
+ it 'yields the user given block' do
547
+ expect { |control| action.call(&control) }.to yield_control
548
+ end
549
+
550
+ it 'rescues Boltless::Errors::RequestError' do
551
+ expect { action.call(&req_err) }.not_to raise_error
552
+ end
553
+
554
+ it 'rescues Boltless::Errors::ResponseError' do
555
+ expect { action.call(&res_err) }.not_to raise_error
556
+ end
557
+
558
+ it 'does not rescue ArgumentError' do
559
+ expect { action.call { raise ArgumentError } }.to \
560
+ raise_error(ArgumentError)
561
+ end
562
+
563
+ it 'returns the given error result in case of errors (non-proc)' do
564
+ expect(action.call(true, &res_err)).to be_eql(true)
565
+ end
566
+
567
+ it 'returns the given error result in case of errors (proc)' do
568
+ err_res = ->(e) { "Err: #{e.message}" }
569
+ expect(action.call(err_res, &res_err)).to be_eql('Err: test')
570
+ end
571
+
572
+ it 'returns the result of the user given block when no errors occur' do
573
+ expect(action.call { 123 }).to be_eql(123)
574
+ end
575
+
576
+ it 'switches the state to closed on errors' do
577
+ expect { action.call(&res_err) }.to \
578
+ change(instance, :state).from('not_yet_started').to('closed')
579
+ end
580
+
581
+ it 'calls #cleanup on errors' do
582
+ expect(instance).to receive(:cleanup).once
583
+ action.call(&res_err)
584
+ end
585
+ end
586
+
587
+ describe '#cleanup' do
588
+ let(:action) { instance.cleanup }
589
+
590
+ it 'clears the request instance variable' do
591
+ expect { action }.to \
592
+ change { instance.instance_variable_get(:@request) }
593
+ .from(Boltless::Request).to(nil)
594
+ end
595
+
596
+ it 'changes the state to cleaned' do
597
+ expect { action }.to \
598
+ change(instance, :state).from('not_yet_started').to('cleaned')
599
+ end
600
+ end
601
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Boltless do
6
+ before { described_class.reset_configuration! }
7
+
8
+ it 'has a version number' do
9
+ expect(Boltless::VERSION).not_to be nil
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ ---
2
+ columns:
3
+ - name
4
+ - birthday
5
+ - written_books
6
+ - active
7
+ data:
8
+ - row:
9
+ - Bernd
10
+ - 1971-07-28
11
+ - 2
12
+ - true
13
+ meta:
14
+ -
15
+ - row:
16
+ - Klaus
17
+ - 1998-01-03
18
+ - 0
19
+ - false
20
+ meta:
21
+ -
@@ -0,0 +1,48 @@
1
+ # CREATE (n:User { name: $name }) RETURN n [result_as_graph: true]
2
+ # MATCH (n:User) RETURN n
3
+ ---
4
+ columns:
5
+ - n
6
+ data:
7
+ - row:
8
+ - name: Kalle
9
+ meta:
10
+ - id: 149
11
+ type: node
12
+ deleted: false
13
+ graph:
14
+ nodes:
15
+ - id: '149'
16
+ labels:
17
+ - User
18
+ properties:
19
+ name: Kalle
20
+ relationships: []
21
+ - row:
22
+ - name: Klaus
23
+ meta:
24
+ - id: 147
25
+ type: node
26
+ deleted: false
27
+ graph:
28
+ nodes:
29
+ - id: '147'
30
+ labels:
31
+ - User
32
+ properties:
33
+ name: Klaus
34
+ relationships: []
35
+ - row:
36
+ - name: Bernd
37
+ meta:
38
+ - id: 148
39
+ type: node
40
+ deleted: false
41
+ graph:
42
+ nodes:
43
+ - id: '148'
44
+ labels:
45
+ - User
46
+ properties:
47
+ name: Bernd
48
+ relationships: []
@@ -0,0 +1,11 @@
1
+ # CREATE (n:User { name: $name }) RETURN n
2
+ ---
3
+ columns:
4
+ - n
5
+ data:
6
+ - row:
7
+ - name: Klaus
8
+ meta:
9
+ - id: 145
10
+ type: node
11
+ deleted: false
@@ -0,0 +1,26 @@
1
+ # CREATE (n:User { name: $name }) RETURN n [with_stats: true]
2
+ ---
3
+ columns:
4
+ - n
5
+ data:
6
+ - row:
7
+ - name: Klaus
8
+ meta:
9
+ - id: 146
10
+ type: node
11
+ deleted: false
12
+ stats:
13
+ contains_updates: true
14
+ nodes_created: 1
15
+ nodes_deleted: 0
16
+ properties_set: 1
17
+ relationships_created: 0
18
+ relationship_deleted: 0
19
+ labels_added: 1
20
+ labels_removed: 0
21
+ indexes_added: 0
22
+ indexes_removed: 0
23
+ constraints_added: 0
24
+ constraints_removed: 0
25
+ contains_system_updates: false
26
+ system_updates: 0