neo4j_bolt 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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