File Coverage

lib/Neo4j/Driver/Result/Jolt.pm
Criterion Covered Total %
statement 179 188 95.2
branch 89 120 74.1
condition 12 18 66.6
subroutine 24 25 96.0
pod 0 1 100.0
total 304 352 86.6


line stmt bran cond sub pod time code
1 20     20   311 use v5.12;
  20         75  
2 20     20   152 use warnings;
  20         38  
  20         1947  
3              
4             package Neo4j::Driver::Result::Jolt 1.02;
5             # ABSTRACT: Jolt result handler
6              
7              
8             # This package is not part of the public Neo4j::Driver API.
9              
10              
11 20     20   171 use parent 'Neo4j::Driver::Result';
  20         45  
  20         177  
12              
13 20     20   2108 use Carp qw(croak);
  20         45  
  20         2276  
14             our @CARP_NOT = qw(Neo4j::Driver::Net::HTTP Neo4j::Driver::Result);
15 20     20   147 use JSON::MaybeXS 1.002004 ();
  20         483  
  20         575  
16              
17 20     20   8804 use Neo4j::Driver::Type::Bytes;
  20         87  
  20         766  
18 20     20   9261 use Neo4j::Driver::Type::DateTime;
  20         61  
  20         905  
19 20     20   8817 use Neo4j::Driver::Type::Duration;
  20         77  
  20         863  
20 20     20   9080 use Neo4j::Driver::Type::Node;
  20         65  
  20         825  
21 20     20   9058 use Neo4j::Driver::Type::Path;
  20         64  
  20         790  
22 20     20   8996 use Neo4j::Driver::Type::Point;
  20         63  
  20         941  
23 20     20   9122 use Neo4j::Driver::Type::Relationship;
  20         330  
  20         1002  
24 20     20   8957 use Neo4j::Driver::Type::V1::Node;
  20         66  
  20         804  
25 20     20   9082 use Neo4j::Driver::Type::V1::Relationship;
  20         68  
  20         807  
26 20     20   134 use Neo4j::Error;
  20         33  
  20         52923  
27              
28             my ($FALSE, $TRUE) = Neo4j::Driver::Result->_bool_values;
29              
30             my $MEDIA_TYPE = "application/vnd.neo4j.jolt";
31             my $ACCEPT_HEADER = "$MEDIA_TYPE-v2+json-seq";
32             my $ACCEPT_HEADER_V1 = "$MEDIA_TYPE+json-seq";
33             my $ACCEPT_HEADER_STRICT = "$MEDIA_TYPE+json-seq;strict=true";
34             my $ACCEPT_HEADER_SPARSE = "$MEDIA_TYPE+json-seq;strict=false";
35             my $ACCEPT_HEADER_NDJSON = "$MEDIA_TYPE";
36              
37             my @CYPHER_TYPES = (
38             { # Types with legacy numeric ID (Jolt v1)
39             node => 'Neo4j::Driver::Type::V1::Node',
40             relationship => 'Neo4j::Driver::Type::V1::Relationship',
41             },
42             { # Types with element ID (Jolt v2)
43             node => 'Neo4j::Driver::Type::Node',
44             relationship => 'Neo4j::Driver::Type::Relationship',
45             },
46             );
47              
48              
49             our $gather_results = 1; # 1: detach from the stream immediately (yields JSON-style result; used for testing)
50              
51              
52             sub new {
53             # uncoverable pod (private method)
54 156     156 0 363 my ($class, $params) = @_;
55            
56 156         1531 my $jolt_v2 = $params->{http_header}->{content_type} =~ m/^\Q$MEDIA_TYPE\E-v2\b/i;
57             my $self = {
58             attached => 1, # 1: unbuffered records may exist on the stream
59             exhausted => 0, # 1: all records read by the client; fetch() will fail
60             buffer => [],
61             server_info => $params->{server_info},
62             json_coder => $params->{http_agent}->json_coder,
63             http_agent => $params->{http_agent},
64 156         710 jolt_v2 => $jolt_v2,
65             };
66 156         1356 bless $self, $class;
67            
68 156 50       2157 return $self->_gather_results($params) if $gather_results;
69            
70 0         0 die "Unimplemented"; # $gather_results 0
71             }
72              
73              
74             sub _gather_results {
75 156     156   301 my ($self, $params) = @_;
76            
77 156         243 my $error = 'Neo4j::Error';
78 156         303 my @results = ();
79 156         258 my $columns = undef;
80 156         224 my @data = ();
81 156         510 $self->{result} = {};
82 156         348 my ($state, $prev) = (0, 'in first place');
83 156         257 my ($type, $event);
84 156         468 while ( ($type, $event) = $self->_next_event ) {
85 595 100       1960 if ($type eq 'header') { # StatementStartEvent
    100          
    100          
    100          
    50          
86 145 50 33     421 croak "Jolt error: unexpected header event $prev" unless $state == 0 || $state == 3;
87 145 0       461 croak "Jolt error: expected reference to HASH, received " . (ref $event ? "reference to " . ref $event : "scalar") . " in $type event $prev" unless ref $event eq 'HASH';
    50          
88 145         244 $state = 1;
89 145         248 $columns = $event->{fields};
90             }
91             elsif ($type eq 'data') { # RecordEvent
92 138 50 66     388 croak "Jolt error: unexpected data event $prev" unless $state == 1 || $state == 2;
93 138 0       350 croak "Jolt error: expected reference to ARRAY, received " . (ref $event ? "reference to " . ref $event : "scalar") . " in $type event $prev" unless ref $event eq 'ARRAY';
    50          
94 138         193 $state = 2;
95 138         362 push @data, { row => $event };
96             }
97             elsif ($type eq 'summary') { # StatementEndEvent
98 145 50 66     658 croak "Jolt error: unexpected summary event $prev" unless $state == 1 || $state == 2;
99 145 0       388 croak "Jolt error: expected reference to HASH, received " . (ref $event ? "reference to " . ref $event : "scalar") . " in $type event $prev" unless ref $event eq 'HASH';
    50          
100 145         263 $state = 3;
101             push @results, {
102             data => [@data],
103             stats => $event->{stats},
104             plan => $event->{plan},
105 145         739 columns => $columns,
106             };
107 145         269 @data = ();
108 145         246 $columns = undef;
109             }
110             elsif ($type eq 'info') { # TransactionInfoEvent
111 156 50 66     758 croak "Jolt error: unexpected info event $prev" unless $state == 0 || $state == 3 || $state == 4;
      66        
112 156 0       379 croak "Jolt error: expected reference to HASH, received " . (ref $event ? "reference to " . ref $event : "scalar") . " in $type event $prev" unless ref $event eq 'HASH';
    50          
113 156         327 $state += 10;
114 156         309 $self->{info} = $event;
115 156         354 $self->{notifications} = $event->{notifications};
116             }
117             elsif ($type eq 'error') { # FailureEvent
118             # If a rollback caused by a failure fails as well,
119             # two failure events may appear on the Jolt stream.
120             # Otherwise, there is always one at most.
121 11 0       38 croak "Jolt error: expected reference to HASH, received " . (ref $event ? "reference to " . ref $event : "scalar") . " in $type event $prev" unless ref $event eq 'HASH';
    50          
122 11         19 $state = 4;
123 11 50       19 $error = $error->append_new(Internal => "Jolt error: Jolt $type event with 0 errors $prev") unless @{$event->{errors}};
  11         51  
124 11         20 $error = $error->append_new(Server => $_) for @{$event->{errors}};
  11         99  
125             }
126             else {
127 0         0 croak "Jolt error: unsupported $type event $prev";
128             }
129 595         21750 $prev = "after $type event";
130             }
131 156 50       405 croak "Jolt error: unexpected end of event stream $prev" unless $state >= 10;
132            
133 156 50       443 if (! $params->{http_header}->{success}) {
134             $error = $error->append_new(Network => {
135             code => $params->{http_header}->{status},
136 0         0 as_string => sprintf("HTTP error: %s %s on %s to %s", $params->{http_header}->{status}, $params->{http_agent}->http_reason, $params->{http_method}, $params->{http_path}),
137             });
138             }
139            
140 156 100       344 $self->{info}->{_error} = $error if ref $error;
141 156         303 $self->{http_agent} = undef;
142            
143 156 100       344 if (@results == 1) {
144 145         301 $self->{result} = $results[0];
145 145         366 $self->{query} = $params->{queries}->[0];
146 145         651 return $self->_as_fully_buffered;
147             }
148            
149             # If the number of Cypher queries run wasn't exactly one, provide a list
150             # of all results so that callers get a uniform interface for all of them.
151 11         28 @results = map { __PACKAGE__->_new_result($_, undef, $params) } @results;
  0         0  
152 11         40 $results[$_]->{query} = $params->{queries}->[$_] for (0 .. $#results);
153 11         23 $self->{attached} = 0;
154 11         18 $self->{exhausted} = 1;
155 11 50       27 $self->{result_list} = \@results if @results;
156 11         45 return $self;
157             }
158              
159              
160             sub _new_result {
161 0     0   0 my ($class, $result, $json, $params) = @_;
162            
163             my $self = {
164             attached => 0, # 1: unbuffered records may exist on the stream
165             exhausted => 0, # 1: all records read by the client; fetch() will fail
166             result => $result,
167             buffer => [],
168             field_names_cache => undef,
169             summary => undef,
170             server_info => $params->{server_info},
171             jolt_v2 => $params->{jolt_v2},
172 0         0 };
173 0         0 bless $self, $class;
174            
175 0         0 return $self->_as_fully_buffered;
176             }
177              
178              
179             sub _next_event {
180 751     751   1213 my ($self) = @_;
181            
182 751         1832 my $line = $self->{http_agent}->fetch_event;
183 751 100       2128 return unless length $line;
184            
185 595         3293 my $json = $self->{json_coder}->decode($line);
186            
187 595         1389 my @events = keys %$json;
188 595 50       1199 croak "Jolt error: expected exactly 1 event, received " . scalar @events unless @events == 1;
189            
190 595         2402 return ( $events[0], $json->{$events[0]} );
191             }
192              
193              
194             # Return the full list of results this object represents.
195             sub _results {
196 141     141   319 my ($self) = @_;
197            
198 141 50       332 return @{ $self->{result_list} } if $self->{result_list};
  0         0  
199 141         488 return ($self);
200             }
201              
202              
203             # Return transaction status information (if available).
204             sub _info {
205 154     154   294 my ($self) = @_;
206 154         315 return $self->{info};
207             }
208              
209              
210             # Bless and initialise the given reference as a Record.
211             sub _init_record {
212 138     138   287 my ($self, $record) = @_;
213            
214 138         288 $record->{field_names_cache} = $self->{field_names_cache};
215 138         425 $self->_deep_bless( $record->{row} );
216 136         506 return bless $record, 'Neo4j::Driver::Record';
217             }
218              
219              
220             sub _deep_bless {
221 433     433   733 my ($self, $data) = @_;
222            
223 433 100       1031 if (JSON::MaybeXS::is_bool $data) { # Boolean (sparse)
224 2 100       58 return $data ? $TRUE : $FALSE;
225             }
226 431 100       2998 if (ref $data eq 'ARRAY') { # List (sparse)
227 142         622 $_ = $self->_deep_bless($_) for @$data;
228 140         319 return $data;
229             }
230 289 100       575 if (ref $data eq '') { # Null or Integer (sparse) or String (sparse)
231 68         192 return $data;
232             }
233            
234 221 50       488 die "Assertion failed: sigil count: " . scalar keys %$data if scalar keys %$data != 1;
235 221         454 my $sigil = (keys %$data)[0];
236 221         417 my $value = $data->{$sigil};
237            
238 221 100       426 if ($sigil eq '?') { # Boolean (strict)
239 3 100       12 return $TRUE if $value eq 'true';
240 2 100       8 return $FALSE if $value eq 'false';
241 1         36 die "Assertion failed: unexpected bool value: " . $value;
242             }
243 218 100       448 if ($sigil eq 'Z') { # Integer (strict)
244 15         72 return 0 + $value;
245             }
246 203 100       362 if ($sigil eq 'R') { # Float
247 1         8 return 0 + $value;
248             }
249 202 100       416 if ($sigil eq 'U') { # String (strict)
250 133         446 return $value;
251             }
252 69 100       163 if ($sigil eq '[]') { # List (strict)
253 4         17 $_ = $self->_deep_bless($_) for @$value;
254 4         12 return $value;
255             }
256 65 100       150 if ($sigil eq '{}') { # Map
257 6         22 $_ = $self->_deep_bless($_) for values %$value;
258 6         19 return $value;
259             }
260 59 100       139 if ($sigil eq '()') { # Node
261 21 50       60 die "Assertion failed: unexpected node fields: " . scalar @$value unless @$value == 3;
262 21         37 $_ = $self->_deep_bless($_) for values %{ $value->[2] };
  21         78  
263 21         149 return bless $value, $CYPHER_TYPES[ $self->{jolt_v2} ]->{node};
264             }
265 38 100 100     166 if ($sigil eq '->' || $sigil eq '<-') { # Relationship
266 12 50       32 die "Assertion failed: unexpected rel fields: " . scalar @$value unless @$value == 5;
267 12         20 $_ = $self->_deep_bless($_) for values %{ $value->[4] };
  12         39  
268 12 100       34 @{$value}[ 3, 1 ] = @{$value}[ 1, 3 ] if $sigil eq '<-';
  4         12  
  4         9  
269 12         66 return bless $value, $CYPHER_TYPES[ $self->{jolt_v2} ]->{relationship};
270             }
271 26 100       73 if ($sigil eq '..') { # Path
272 7 50       25 die "Assertion failed: unexpected path fields: " . scalar @$value unless @$value & 1;
273 7         28 $_ = $self->_deep_bless($_) for @$value;
274 7         33 return bless $data, 'Neo4j::Driver::Type::Path';
275             }
276 19 100       51 if ($sigil eq '@') { # Spatial
277 2         10 return bless $data, 'Neo4j::Driver::Type::Point';
278             }
279 17 100       53 if ($sigil eq 'T') { # Temporal
280 15 100       153 return bless $data, $value =~ m/^-?P/
281             ? 'Neo4j::Driver::Type::Duration'
282             : 'Neo4j::Driver::Type::DateTime';
283             }
284 2 100       13 if ($sigil eq '#') { # Bytes
285 1         27 $value =~ tr/ //d; # spaces were allowed in the Jolt draft, but aren't actually implemented in Neo4j 4.2's jolt.JoltModule
286 1         8 $value = pack 'H*', $value; # see neo4j#12660
287 1         10 return bless \$value, 'Neo4j::Driver::Type::Bytes';
288             }
289            
290 1         38 die "Assertion failed: unexpected sigil: " . $sigil;
291            
292             }
293              
294              
295             sub _accept_header {
296 214     214   523 my (undef, $want_jolt, $method) = @_;
297            
298 214 100       673 return unless $method eq 'POST'; # work around Neo4j HTTP Content Negotiation bug #12644
299            
300 128 100       391 if (defined $want_jolt) {
301 17 100       50 return if ! $want_jolt;
302 15 100       56 return ($ACCEPT_HEADER_V1) if $want_jolt eq 'v1';
303 8 100       22 return ($ACCEPT_HEADER_STRICT) if $want_jolt eq 'strict';
304 6 100       18 return ($ACCEPT_HEADER_SPARSE) if $want_jolt eq 'sparse';
305 4 100       14 return ($ACCEPT_HEADER_NDJSON) if $want_jolt eq 'ndjson';
306             }
307 113         377 return ($ACCEPT_HEADER);
308             }
309              
310              
311             sub _acceptable {
312 204     204   463 my (undef, $content_type) = @_;
313            
314 204         2418 return $content_type =~ m/^\Q$MEDIA_TYPE\E\b/i;
315             }
316              
317              
318             1;