neo4j_bolt 0.1.1

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.
data/lib/neo4j_bolt.rb ADDED
@@ -0,0 +1,880 @@
1
+ require "neo4j_bolt/version"
2
+ require 'socket'
3
+ require 'json'
4
+ require 'yaml'
5
+
6
+ module Neo4jBolt
7
+ NEO4J_DEBUG = 0
8
+
9
+ module ServerState
10
+ DISCONNECTED = 0
11
+ CONNECTED = 1
12
+ DEFUNCT = 2
13
+ READY = 3
14
+ STREAMING = 4
15
+ TX_READY = 5
16
+ TX_STREAMING = 6
17
+ FAILED = 7
18
+ INTERRUPTED = 8
19
+ end
20
+ SERVER_STATE_LABELS = Hash[ServerState::constants.map { |value| [ServerState::const_get(value), value] }]
21
+
22
+ module BoltMarker
23
+ BOLT_HELLO = 0x01
24
+ BOLT_GOODBYE = 0x02
25
+ BOLT_RESET = 0x0F
26
+ BOLT_RUN = 0x10
27
+ BOLT_BEGIN = 0x11
28
+ BOLT_COMMIT = 0x12
29
+ BOLT_ROLLBACK = 0x13
30
+ BOLT_DISCARD = 0x2F
31
+ BOLT_PULL = 0x3F
32
+ BOLT_NODE = 0x4E
33
+ BOLT_RELATIONSHIP = 0x52
34
+ BOLT_SUCCESS = 0x70
35
+ BOLT_RECORD = 0x71
36
+ BOLT_IGNORED = 0x7E
37
+ BOLT_FAILURE = 0x7F
38
+ end
39
+ BOLT_MAKER_LABELS = Hash[BoltMarker::constants.map { |value| [BoltMarker::const_get(value), value] }]
40
+
41
+ class Error < StandardError; end
42
+ class IntegerOutOfRangeError < Error; end
43
+ class SyntaxError < Error; end
44
+ class ExpectedOneResultError < Error; end
45
+ class UnexpectedServerResponse < Error
46
+ def initialize(token)
47
+ @token = token
48
+ end
49
+
50
+ def to_s
51
+ BOLT_MAKER_LABELS[@token]
52
+ end
53
+ end
54
+
55
+ class State
56
+ def initialize()
57
+ @state = nil
58
+ self.set(ServerState::DISCONNECTED)
59
+ end
60
+
61
+ def set(state)
62
+ @state = state
63
+ # STDERR.puts " > #{SERVER_STATE_LABELS[@state]}"
64
+ end
65
+
66
+ def ==(other)
67
+ @state == other
68
+ end
69
+
70
+ def to_i
71
+ @state
72
+ end
73
+ end
74
+
75
+ class CypherError < StandardError
76
+ def initialize(message, buf = nil)
77
+ @message = message
78
+ @buf = buf
79
+ end
80
+
81
+ def to_s
82
+ @buf.nil? ? "#{@message}" : "#{@message} at buffer offset #{sprintf('0x%x', @buf.offset)}"
83
+ end
84
+ end
85
+
86
+ class BoltBuffer
87
+ def initialize(socket)
88
+ @socket = socket
89
+ @stream_ended = false
90
+ @data = []
91
+ @offset = 0
92
+ end
93
+
94
+ attr_reader :offset
95
+
96
+ # make sure we have at least n bytes in the buffer
97
+ def request(n)
98
+ while @offset + n > @data.length
99
+ length = @socket.read(2).unpack('n').first
100
+ # STDERR.puts "Reading next chunk at offset #{@offset}, got #{length} bytes (requested #{n} bytes)"
101
+ if length == 0
102
+ @stream_ended = true
103
+ else
104
+ if @offset > 0
105
+ @data = @data[@offset, @data.size - @offset]
106
+ @offset = 0
107
+ end
108
+ chunk = @socket.read(length).unpack('C*')
109
+ @data += chunk
110
+ if NEO4J_DEBUG >= 3
111
+ dump()
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ def flush()
118
+ # STDERR.write "Flushing buffer: "
119
+ loop do
120
+ length = @socket.read(2).unpack('n').first
121
+ # TODO: length should be 0, otherwise we're out of protocol
122
+ # or we encountered features not yet implemented here
123
+ # STDERR.write "#{length} "
124
+ break if length == 0
125
+ @socket.read(length)
126
+ end
127
+ # STDERR.puts
128
+ end
129
+
130
+ def next
131
+ request(1)
132
+ v = @data[@offset]
133
+ @offset += 1
134
+ v
135
+ end
136
+
137
+ def next_s(length)
138
+ request(length)
139
+ s = @data[@offset, length].pack('C*')
140
+ s.force_encoding('UTF-8')
141
+ @offset += length
142
+ s
143
+ end
144
+
145
+ def next_uint8()
146
+ request(1)
147
+ i = @data[@offset]
148
+ @offset += 1
149
+ i
150
+ end
151
+
152
+ def next_uint16()
153
+ request(2)
154
+ i = @data[@offset, 2].pack('C*').unpack('S>').first
155
+ @offset += 2
156
+ i
157
+ end
158
+
159
+ def next_uint32()
160
+ request(4)
161
+ i = @data[@offset, 4].pack('C*').unpack('L>').first
162
+ @offset += 4
163
+ i
164
+ end
165
+
166
+ def next_uint64()
167
+ request(8)
168
+ i = @data[@offset, 8].pack('C*').unpack('Q>').first
169
+ @offset += 8
170
+ i
171
+ end
172
+
173
+ def next_int8()
174
+ request(1)
175
+ i = @data[@offset, 1].pack('C*').unpack('c').first
176
+ @offset += 1
177
+ i
178
+ end
179
+
180
+ def next_int16()
181
+ request(2)
182
+ i = @data[@offset, 2].pack('C*').unpack('s>').first
183
+ @offset += 2
184
+ i
185
+ end
186
+
187
+ def next_int32()
188
+ request(4)
189
+ i = @data[@offset, 4].pack('C*').unpack('l>').first
190
+ @offset += 4
191
+ i
192
+ end
193
+
194
+ def next_int64()
195
+ request(8)
196
+ i = @data[@offset, 8].pack('C*').unpack('q>').first
197
+ @offset += 8
198
+ i
199
+ end
200
+
201
+ def next_float()
202
+ request(8)
203
+ f = @data[@offset, 8].pack('C*').unpack('G').first
204
+ @offset += 8
205
+ f
206
+ end
207
+
208
+ def peek
209
+ request(1)
210
+ @data[@offset]
211
+ end
212
+
213
+ def eof?
214
+ @stream_ended
215
+ end
216
+
217
+ def dump
218
+ offset = 0
219
+ last_offset = 0
220
+ while offset < @data.size
221
+ if offset % 16 == 0
222
+ STDERR.write sprintf('%04x | ', offset)
223
+ end
224
+ STDERR.write sprintf("%02x ", @data[offset])
225
+ offset += 1
226
+ if offset % 16 == 0
227
+ STDERR.write ' ' * 4
228
+ (last_offset...offset).each do |i|
229
+ b = @data[i]
230
+ STDERR.write (b >= 32 && b < 128) ? b.chr : '.'
231
+ end
232
+ STDERR.puts
233
+ last_offset = offset
234
+ end
235
+ end
236
+ (16 - offset + last_offset).times { STDERR.write ' ' }
237
+ STDERR.write ' ' * 4
238
+ (last_offset...offset).each do |i|
239
+ b = @data[i]
240
+ STDERR.write (b >= 32 && b < 128) ? b.chr : '.'
241
+ end
242
+ STDERR.puts
243
+ end
244
+ end
245
+
246
+ class Node < Hash
247
+ def initialize(id, labels, properties)
248
+ @id = id
249
+ @labels = labels
250
+ properties.each_pair { |k, v| self[k] = v }
251
+ end
252
+ attr_reader :id, :labels
253
+ end
254
+
255
+ class Relationship < Hash
256
+ def initialize(id, start_node_id, end_node_id, type, properties)
257
+ @id = id
258
+ @start_node_id = start_node_id
259
+ @end_node_id = end_node_id
260
+ @type = type
261
+ properties.each_pair { |k, v| self[k] = v }
262
+ end
263
+
264
+ attr_reader :id, :start_node_id, :end_node_id, :type
265
+ end
266
+
267
+ class BoltSocket
268
+
269
+ def initialize(host = 'localhost', port = 7687)
270
+ @host = host
271
+ @port = port
272
+ @socket = nil
273
+ @transaction = 0
274
+ @transaction_failed = false
275
+ @state = State.new()
276
+ @neo4j_version = nil
277
+ end
278
+
279
+ def assert(condition)
280
+ raise "Assertion failed" unless condition
281
+ end
282
+
283
+ def _append(s)
284
+ @buffer += (s.is_a? String) ? s.unpack('C*') : [s]
285
+ end
286
+
287
+ def append_uint8(i)
288
+ _append([i].pack('C'))
289
+ end
290
+
291
+ def append_token(i)
292
+ # STDERR.puts BOLT_MAKER_LABELS[i]
293
+ append_uint8(i)
294
+ end
295
+
296
+ def append_uint16(i)
297
+ _append([i].pack('S>'))
298
+ end
299
+
300
+ def append_uint32(i)
301
+ _append([i].pack('L>'))
302
+ end
303
+
304
+ def append_uint64(i)
305
+ _append([i].pack('Q>'))
306
+ end
307
+
308
+ def append_int8(i)
309
+ _append([i].pack('c'))
310
+ end
311
+
312
+ def append_int16(i)
313
+ _append([i].pack('s>'))
314
+ end
315
+
316
+ def append_int32(i)
317
+ _append([i].pack('l>'))
318
+ end
319
+
320
+ def append_int64(i)
321
+ _append([i].pack('q>'))
322
+ end
323
+
324
+ def append_s(s)
325
+ s = s.to_s
326
+ if s.bytesize < 16
327
+ append_uint8(0x80 + s.bytesize)
328
+ elsif s.bytesize < 0x100
329
+ append_uint8(0xD0)
330
+ append_uint8(s.bytesize)
331
+ elsif s.bytesize < 0x10000
332
+ append_uint8(0xD1)
333
+ append_uint16(s.bytesize)
334
+ elsif s.bytesize < 0x100000000
335
+ append_uint8(0xD2)
336
+ append_uint32(s.bytesize)
337
+ else
338
+ raise "string cannot exceed 4GB!"
339
+ end
340
+ _append(s)
341
+ end
342
+
343
+ def append(v)
344
+ if v.is_a? Array
345
+ append_array(v)
346
+ elsif v.is_a? Hash
347
+ append_dict(v)
348
+ elsif v.is_a? String
349
+ append_s(v)
350
+ elsif v.is_a? Symbol
351
+ append_s(v.to_s)
352
+ elsif v.is_a? NilClass
353
+ append_uint8(0xC0)
354
+ elsif v.is_a? TrueClass
355
+ append_uint8(0xC3)
356
+ elsif v.is_a? FalseClass
357
+ append_uint8(0xC2)
358
+ elsif v.is_a? Integer
359
+ if v >= -16 && v <= -1
360
+ append_uint8(0x100 + v)
361
+ elsif v >= 0 && v < 0x80
362
+ append_uint8(v)
363
+ elsif v >= -0x80 && v < 0x80
364
+ append_uint8(0xC8)
365
+ append_int8(v)
366
+ elsif v >= -0x8000 && v < 0x8000
367
+ append_uint8(0xC9)
368
+ append_int16(v)
369
+ elsif v >= -0x80000000 && v < 0x80000000
370
+ append_uint8(0xCA)
371
+ append_int32(v)
372
+ elsif v >= -0x8000000000000000 && v < 0x8000000000000000
373
+ append_uint8(0xCB)
374
+ append_int64(v)
375
+ else
376
+ raise Neo4jBolt::IntegerOutOfRangeError.new()
377
+ end
378
+ elsif v.is_a? Float
379
+ append_uint8(0xC1)
380
+ _append([v].pack('G'))
381
+ else
382
+ raise "Type not supported: #{v.class}"
383
+ end
384
+ end
385
+
386
+ def append_dict(d)
387
+ if d.size < 16
388
+ append_uint8(0xA0 + d.size)
389
+ elsif d.size < 0x100
390
+ append_uint8(0xD8)
391
+ append_uint8(d.size)
392
+ elsif d.size < 0x10000
393
+ append_uint8(0xD9)
394
+ append_uint16(d.size)
395
+ elsif d.size < 0x100000000
396
+ append_uint8(0xDA)
397
+ append_uint32(d.size)
398
+ else
399
+ raise "dict cannot exceed 4G entries!"
400
+ end
401
+ d.each_pair do |k, v|
402
+ append_s(k)
403
+ append(v)
404
+ end
405
+ end
406
+
407
+ def append_array(a)
408
+ if a.size < 16
409
+ append_uint8(0x90 + a.size)
410
+ elsif a.size < 0x100
411
+ append_uint8(0xD4)
412
+ append_uint8(a.size)
413
+ elsif a.size < 0x10000
414
+ append_uint8(0xD5)
415
+ append_uint16(a.size)
416
+ elsif a.size < 0x100000000
417
+ append_uint8(0xD6)
418
+ append_uint32(a.size)
419
+ else
420
+ raise "list cannot exceed 4G entries!"
421
+ end
422
+ a.each do |v|
423
+ append(v)
424
+ end
425
+ end
426
+
427
+ def flush()
428
+ size = @buffer.size
429
+ offset = 0
430
+ while size > 0
431
+ chunk_size = [size, 0xffff].min
432
+ @socket.write([chunk_size].pack('n'))
433
+ @socket.write(@buffer[offset, chunk_size].pack('C*'))
434
+ offset += chunk_size
435
+ size -= chunk_size
436
+ end
437
+ @socket.write([0].pack('n'))
438
+ @buffer = []
439
+ end
440
+
441
+ def parse_s(buf)
442
+ f = buf.next
443
+ if f >= 0x80 && f <= 0x8F
444
+ buf.next_s(f & 0xF)
445
+ elsif f == 0xD0
446
+ buf.next_s(buf.next_uint8())
447
+ elsif f == 0xD1
448
+ buf.next_s(buf.next_uint16())
449
+ elsif f == 0xD2
450
+ buf.next_s(buf.next_uint32())
451
+ else
452
+ raise CypherError.new(sprintf("unknown string format %02x", f), buf)
453
+ end
454
+ end
455
+
456
+ def parse_dict(buf)
457
+ f = buf.next
458
+ count = 0
459
+ if f >= 0xA0 && f <= 0xAF
460
+ count = f & 0xF
461
+ elsif f == 0xD8
462
+ count = buf.next_uint8()
463
+ elsif f == 0xD9
464
+ count = buf.next_uint16()
465
+ elsif f == 0xDA
466
+ count = buf.next_uint32()
467
+ else
468
+ raise sprintf("unknown string dict %02x", f)
469
+ end
470
+ # STDERR.puts "Parsing dict with #{count} entries"
471
+ v = {}
472
+ (0...count).map do
473
+ key = parse_s(buf)
474
+ value = parse(buf)
475
+ # STDERR.puts "#{key.to_s}: #{value.to_s}"
476
+ v[key] = value
477
+ end
478
+ v
479
+ end
480
+
481
+ def parse_list(buf)
482
+ f = buf.next
483
+ count = 0
484
+ if f >= 0x90 && f <= 0x9F
485
+ count = f & 0x0F
486
+ elsif f == 0xD4
487
+ count = buf.next_uint8()
488
+ elsif f == 0xD5
489
+ count = buf.next_uint16()
490
+ elsif f == 0xD6
491
+ count = buf.next_uint32()
492
+ else
493
+ raise sprintf("unknown list format %02x", f)
494
+ end
495
+ v = {}
496
+ (0...count).map do
497
+ parse(buf)
498
+ end
499
+ end
500
+
501
+ def parse(buf)
502
+ f = buf.peek
503
+ if f >= 0x80 && f <= 0x8F || f == 0xD0 || f == 0xD1 || f == 0xD2
504
+ parse_s(buf)
505
+ elsif f >= 0x90 && f <= 0x9F || f == 0xD4 || f == 0xD5 || f == 0xD6
506
+ parse_list(buf)
507
+ elsif f >= 0xA0 && f <= 0xAF || f == 0xD8 || f == 0xD9 || f == 0xDA
508
+ parse_dict(buf)
509
+ elsif f >= 0xB0 && f <= 0xBF
510
+ count = buf.next & 0xF
511
+ # STDERR.puts "Parsing #{count} structures!"
512
+ marker = buf.next
513
+
514
+ response = {}
515
+
516
+ if marker == BoltMarker::BOLT_SUCCESS
517
+ response = {:marker => BoltMarker::BOLT_SUCCESS, :data => parse(buf)}
518
+ elsif marker == BoltMarker::BOLT_FAILURE
519
+ response = {:marker => BoltMarker::BOLT_FAILURE, :data => parse(buf)}
520
+ elsif marker == BoltMarker::BOLT_IGNORED
521
+ response = {:marker => BoltMarker::BOLT_IGNORED}
522
+ elsif marker == BoltMarker::BOLT_RECORD
523
+ response = {:marker => BoltMarker::BOLT_RECORD, :data => parse(buf)}
524
+ elsif marker == BoltMarker::BOLT_NODE
525
+ response = {
526
+ :marker => BoltMarker::BOLT_NODE,
527
+ :id => parse(buf),
528
+ :labels => parse(buf),
529
+ :properties => parse(buf),
530
+ }
531
+ elsif marker == BoltMarker::BOLT_RELATIONSHIP
532
+ response = {
533
+ :marker => BoltMarker::BOLT_RELATIONSHIP,
534
+ :id => parse(buf),
535
+ :start_node_id => parse(buf),
536
+ :end_node_id => parse(buf),
537
+ :type => parse(buf),
538
+ :properties => parse(buf),
539
+ }
540
+ else
541
+ raise sprintf("Unknown marker: %02x", marker)
542
+ end
543
+ response
544
+ elsif f == 0xC0
545
+ buf.next
546
+ nil
547
+ elsif f == 0xC1
548
+ buf.next
549
+ buf.next_float()
550
+ elsif f == 0xC2
551
+ buf.next
552
+ false
553
+ elsif f == 0xC3
554
+ buf.next
555
+ true
556
+ elsif f == 0xC8
557
+ buf.next
558
+ buf.next_int8()
559
+ elsif f == 0xC9
560
+ buf.next
561
+ buf.next_int16()
562
+ elsif f == 0xCA
563
+ buf.next
564
+ buf.next_int32()
565
+ elsif f == 0xCB
566
+ buf.next
567
+ buf.next_int64()
568
+ elsif f >= 0xF0 && f <= 0xFF
569
+ buf.next
570
+ f - 0x100
571
+ elsif f >= 0 && f <= 0x7F
572
+ buf.next
573
+ f
574
+ else
575
+ raise sprintf("Unknown marker: %02x", f)
576
+ end
577
+ end
578
+
579
+ def bolt_error(code, message)
580
+ if code == 'Neo.ClientError.Statement.SyntaxError'
581
+ SyntaxError.new(message)
582
+ else
583
+ Error.new("#{code}\n#{message}")
584
+ end
585
+ end
586
+
587
+ def read_response(&block)
588
+ loop do
589
+ buffer = BoltBuffer.new(@socket)
590
+ response_dict = parse(buffer)
591
+ buffer.flush()
592
+ if response_dict[:marker] == BoltMarker::BOLT_FAILURE
593
+ # STDERR.puts "RESETTING CONNECTION"
594
+ append_uint8(0xb1)
595
+ append_token(BoltMarker::BOLT_RESET)
596
+ flush()
597
+ @state.set(ServerState::READY)
598
+ read_response()
599
+ # BoltBuffer.new(@socket).flush()
600
+ raise bolt_error(response_dict[:data]['code'], response_dict[:data]['message'])
601
+ end
602
+ # STDERR.puts response_dict.to_json
603
+ yield response_dict if block_given?
604
+ break if [BoltMarker::BOLT_SUCCESS, BoltMarker::BOLT_FAILURE, BoltMarker::BOLT_IGNORED].include?(response_dict[:marker])
605
+ end
606
+ end
607
+
608
+ def connect()
609
+ # STDERR.write "Connecting to Neo4j via Bolt..."
610
+ @socket = TCPSocket.new(@host, @port)
611
+ # @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
612
+ # @socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 50)
613
+ # @socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
614
+ # @socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 5)
615
+ # The line below is important, otherwise we'll have to wait 40ms before every read
616
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
617
+ @buffer = []
618
+ @socket.write("\x60\x60\xB0\x17")
619
+ @socket.write("\x00\x00\x04\x04")
620
+ @socket.write("\x00\x00\x00\x00")
621
+ @socket.write("\x00\x00\x00\x00")
622
+ @socket.write("\x00\x00\x00\x00")
623
+ version = @socket.read(4).unpack('N').first
624
+ if version != 0x00000404
625
+ raise "Unable to establish connection to Neo4j using Bolt protocol version 4.4!"
626
+ end
627
+ @state.set(ServerState::CONNECTED)
628
+ data = {
629
+ :routing => nil,
630
+ :scheme => 'none',
631
+ :user_agent => 'qts/0.1'
632
+ }
633
+ append_uint8(0xb1)
634
+ append_token(BoltMarker::BOLT_HELLO)
635
+ append_dict(data)
636
+ flush()
637
+ read_response() do |data|
638
+ if data[:marker] == BoltMarker::BOLT_SUCCESS
639
+ @state.set(ServerState::READY)
640
+ @neo4j_version = data[:data]['server']
641
+ elsif data[:marker] == BoltMarker::BOLT_FAILURE
642
+ @state.set(ServerState::DEFUNCT)
643
+ else
644
+ raise UnexpectedServerResponse.new(data[:marker])
645
+ end
646
+ end
647
+
648
+ @transaction = 0
649
+ @transaction_failed = false
650
+ end
651
+
652
+ def disconnect()
653
+ append_uint8(0xb1)
654
+ append_token(BOLT_GOODBYE)
655
+ flush()
656
+ @state.set(ServerState::DEFUNCT)
657
+ end
658
+
659
+ def transaction(&block)
660
+ connect() if @socket.nil?
661
+ if @transaction == 0
662
+ # STDERR.puts '*' * 40
663
+ # STDERR.puts "#{SERVER_STATE_LABELS[@state.to_i]} (#{@state.to_i})"
664
+ # STDERR.puts '*' * 40
665
+ assert(@state == ServerState::READY)
666
+ append_uint8(0xb1)
667
+ append_token(BoltMarker::BOLT_BEGIN)
668
+ append_dict({})
669
+ flush()
670
+ read_response() do |data|
671
+ if data[:marker] == BoltMarker::BOLT_SUCCESS
672
+ @state.set(ServerState::TX_READY)
673
+ @transaction_failed = false
674
+ elsif data[:marker] == BoltMarker::BOLT_FAILURE
675
+ @state.set(ServerState::FAILED)
676
+ end
677
+ end
678
+ end
679
+ @transaction += 1
680
+ begin
681
+ yield
682
+ rescue
683
+ @transaction_failed = true
684
+ raise
685
+ ensure
686
+ @transaction -= 1
687
+ if @transaction == 0 && @transaction_failed &&
688
+ # TODO: Not sure about this, read remaining response but don't block
689
+ # read_response()
690
+ # STDERR.puts "!!! Rolling back transaction !!!"
691
+ if @state == ServerState::TX_READY
692
+ assert(@state == ServerState::TX_READY)
693
+ append_uint8(0xb1)
694
+ append_token(BoltMarker::BOLT_ROLLBACK)
695
+ flush()
696
+ read_response do |data|
697
+ if data[:marker] == BoltMarker::BOLT_SUCCESS
698
+ @state.set(ServerState::READY)
699
+ elsif data[:marker] == BoltMarker::BOLT_FAILURE
700
+ @state.set(ServerState::FAILED)
701
+ else
702
+ raise UnexpectedServerResponse.new(data[:marker])
703
+ end
704
+ end
705
+ end
706
+ end
707
+ end
708
+ if (@transaction == 0) && (!@transaction_failed)
709
+ append_uint8(0xb1)
710
+ append_token(BoltMarker::BOLT_COMMIT)
711
+ flush()
712
+ read_response()
713
+ @transaction = 0
714
+ @transaction_failed = false
715
+ @state.set(ServerState::READY)
716
+ end
717
+ end
718
+
719
+ def fix_value(value)
720
+ if value.is_a? Hash
721
+ if value[:marker] == BoltMarker::BOLT_NODE
722
+ Node.new(value[:id], value[:labels], fix_value(value[:properties]))
723
+ elsif value[:marker] == BoltMarker::BOLT_RELATIONSHIP
724
+ Relationship.new(value[:id], value[:start_node_id], value[:end_node_id], value[:type], fix_value(value[:properties]))
725
+ else
726
+ Hash[value.map { |k, v| [k.to_sym, fix_value(v)] }]
727
+ end
728
+ elsif value.is_a? Array
729
+ value.map { |v| fix_value(v) }
730
+ else
731
+ value
732
+ end
733
+ end
734
+
735
+ def run_query(query, data = {}, &block)
736
+ if NEO4J_DEBUG >= 1
737
+ STDERR.puts query
738
+ STDERR.puts data.to_json
739
+ STDERR.puts '-' * 40
740
+ end
741
+ transaction do
742
+ assert(@state == ServerState::TX_READY || @state == ServerState::TX_STREAMING || @state == ServerState::FAILED)
743
+ append_uint8(0xb1)
744
+ append_token(BoltMarker::BOLT_RUN)
745
+ append_s(query)
746
+ # Because something might go wrong while filling the buffer with
747
+ # the request data (for example if the data contains 2^100 anywhere)
748
+ # we catch any errors that happen here and if things go wrong, we
749
+ # clear the buffer so that the BOLT_RUN stanza never gets sent
750
+ # to Neo4j - instead, the transaction gets rolled back
751
+ begin
752
+ append_dict(data)
753
+ rescue
754
+ @buffer = []
755
+ raise
756
+ end
757
+ append_dict({}) # options
758
+ flush()
759
+ read_response do |data|
760
+ if data[:marker] == BoltMarker::BOLT_SUCCESS
761
+ @state.set(ServerState::TX_STREAMING)
762
+ keys = data[:data]['fields']
763
+ assert(@state == ServerState::TX_STREAMING)
764
+ append_uint8(0xb1)
765
+ append_token(BoltMarker::BOLT_PULL)
766
+ append_dict({:n => -1})
767
+ flush()
768
+ read_response do |data|
769
+ if data[:marker] == BoltMarker::BOLT_RECORD
770
+ entry = {}
771
+ keys.each.with_index do |key, i|
772
+ entry[key] = fix_value(data[:data][i])
773
+ end
774
+ if NEO4J_DEBUG >= 1
775
+ STDERR.puts ">>> #{entry.to_json}"
776
+ STDERR.puts '-' * 40
777
+ end
778
+ yield entry
779
+ elsif data[:marker] == BoltMarker::BOLT_SUCCESS
780
+ # STDERR.puts data.to_yaml
781
+ @state.set(ServerState::TX_READY)
782
+ end
783
+ end
784
+ elsif data[:marker] == BoltMarker::BOLT_FAILURE
785
+ @state.set(ServerState::FAILED)
786
+ else
787
+ raise UnexpectedServerResponse.new(data[:marker])
788
+ end
789
+ end
790
+ end
791
+ end
792
+
793
+ def neo4j_query(query, data = {}, &block)
794
+ rows = []
795
+ run_query(query, data) do |row|
796
+ if block_given?
797
+ yield row
798
+ else
799
+ rows << row
800
+ end
801
+ end
802
+ return block_given? ? nil : rows
803
+ end
804
+
805
+ def neo4j_query_expect_one(query, data = {})
806
+ rows = neo4j_query(query, data)
807
+ if rows.size != 1
808
+ raise ExpectedOneResultError.new("Expected one result, but got #{rows.size}.")
809
+ end
810
+ rows.first
811
+ end
812
+ end
813
+
814
+ def connect_bolt_socket(host, port)
815
+ @bolt_socket ||= BoltSocket.new(host, port)
816
+ end
817
+
818
+ def transaction(&block)
819
+ @bolt_socket ||= BoltSocket.new()
820
+ @bolt_socket.transaction { yield }
821
+ end
822
+
823
+ def rollback()
824
+ @bolt_socket.rollback
825
+ end
826
+
827
+ def neo4j_query(query, data = {}, &block)
828
+ @bolt_socket ||= BoltSocket.new()
829
+ @bolt_socket.neo4j_query(query, data, &block)
830
+ end
831
+
832
+ def neo4j_query_expect_one(query, data = {})
833
+ @bolt_socket ||= BoltSocket.new()
834
+ @bolt_socket.neo4j_query_expect_one(query, data)
835
+ end
836
+
837
+ def wait_for_neo4j
838
+ delay = 1
839
+ 10.times do
840
+ begin
841
+ neo4j_query("MATCH (n) RETURN n LIMIT 1;")
842
+ rescue
843
+ debug "Waiting #{delay} seconds for Neo4j to come up..."
844
+ sleep delay
845
+ delay += 1
846
+ end
847
+ end
848
+ end
849
+
850
+ def dump_database(&block)
851
+ tr_id = {}
852
+ id = 0
853
+ neo4j_query("MATCH (n) RETURN n ORDER BY ID(n);") do |row|
854
+ tr_id[row['n'].id] = id
855
+ node = {
856
+ :id => id,
857
+ :labels => row['n'].labels,
858
+ :properties => row['n']
859
+ }
860
+ yield "n #{node.to_json}"
861
+ id += 1
862
+ end
863
+ neo4j_query("MATCH ()-[r]->() RETURN r;") do |row|
864
+ rel = {
865
+ :from => tr_id[row['r'].start_node_id],
866
+ :to => tr_id[row['r'].end_node_id],
867
+ :type => row['r'].type,
868
+ :properties => row['r']
869
+ }
870
+ yield "r #{rel.to_json}"
871
+ end
872
+ end
873
+
874
+ def cleanup_neo4j
875
+ if @bolt_socket
876
+ @bolt_socket.disconnect()
877
+ @bolt_socket = nil
878
+ end
879
+ end
880
+ end