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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +34 -0
- data/LICENSE +674 -0
- data/README.md +50 -0
- data/Rakefile +6 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/neo4j_bolt/version.rb +3 -0
- data/lib/neo4j_bolt.rb +880 -0
- data/neo4j_bolt.gemspec +30 -0
- metadata +73 -0
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
|