File Coverage

blib/lib/Neo4j/Cypher/Abstract.pm
Criterion Covered Total %
statement 116 162 71.6
branch 29 80 36.2
condition 19 75 25.3
subroutine 25 39 64.1
pod 18 25 72.0
total 207 381 54.3


line stmt bran cond sub pod time code
1             package Neo4j::Cypher::Abstract;
2 2     2   66724 use lib '../../../lib';
  2         6  
  2         24  
3 2     2   348 use base Exporter;
  2         7  
  2         272  
4 2     2   932 use Neo4j::Cypher::Pattern qw/pattern ptn/;
  2         10  
  2         201  
5 2     2   1536 use Neo4j::Cypher::Abstract::Peeler;
  2         11  
  2         119  
6 2     2   23 use Scalar::Util qw/blessed/;
  2         5  
  2         145  
7 2     2   13 use Carp;
  2         6  
  2         206  
8             use overload
9             '""' => as_string,
10 2     2   16 'cmp' => sub { "$_[0]" cmp "$_[1]" };
  2     56   5  
  2         22  
  56         8362  
11 2     2   198 use strict;
  2         5  
  2         54  
12 2     2   13 use warnings;
  2         5  
  2         593  
13              
14              
15             our @EXPORT_OK = qw/cypher pattern ptn/;
16             our $AUTOLOAD;
17              
18             sub puke(@);
19             sub belch(@);
20              
21             our $VERSION='0.1002';
22              
23             # let an Abstract object keep its own stacks of clauses
24             # rather than clearing an existing Abstract object, get
25             # new objects from a factory = cypher()
26              
27             # create, create_unique, match, merge - patterns for args
28             # where, set - SQL::A like expression for argument (only assignments make
29             # sense for set)
30             # for_each - third arg is a cypher write query
31              
32             # 'as' - include in the string arguments : "n.name as name"
33              
34             our %clause_table = (
35             read => [qw/match optional_match where start/],
36             write => [qw/create merge set delete remove foreach
37             detach_delete
38             on_create on_match
39             create_unique/],
40             general => [qw/return order_by limit skip with unwind union
41             return_distinct with_distinct
42             call yield/],
43             hint => [qw/using_index using_scan using_join/],
44             load => [qw/load_csv load_csv_with_headers
45             using_periodic_commit/],
46             schema => [qw/create_constraint drop_constraint
47             create_index drop_index/],
48             modifier => [qw/skip limit order_by/]
49             );
50             our @all_clauses = ( map { @{$clause_table{$_}} } keys %clause_table );
51              
52             sub new {
53 63     63 0 169 my $class = shift;
54 63         166 my $self = {};
55 63         207 $self->{stack} = [];
56 63         518 bless $self, $class;
57             }
58              
59             sub cypher {
60 63     63 0 2079 Neo4j::Cypher::Abstract->new;
61             }
62 2     2 0 17 sub available_clauses {no warnings qw/once/; @__PACKAGE__::all_clauses }
  2     0   5  
  2         6339  
  0         0  
63              
64 0 0   0 0 0 sub bind_values { $_[0]->{bind_values} && @{$_[0]->{bind_values}} }
  0         0  
65 3 50   3 1 521 sub parameters { $_[0]->{parameters} && @{$_[0]->{parameters}} }
  3         40  
66              
67             # specials
68              
69             sub where {
70 22     22 1 46 my $self = shift;
71 22 50       70 puke "Need arg1 => expression" unless defined $_[0];
72 22         42 my $arg = $_[0];
73 22         58 $self->_add_clause('where',$arg);
74             }
75              
76 1     1 1 6 sub union { $_[0]->_add_clause('union') }
77 1     1 0 28 sub union_all { $_[0]->_add_clause('union_all') }
78              
79             sub order_by {
80 7     7 1 19 my $self = shift;
81 7 50       24 puke "Need arg1 => identifier" unless defined $_[0];
82 7         14 my @args;
83 7         30 while (my $a = shift) {
84 9 100 100     51 if ($_[0] and $_[0] =~ /^(?:de|a)sc$/i) {
85 2         13 push @args, "$a ".uc(shift());
86             }
87             else {
88 7         28 push @args, $a;
89             }
90             }
91 7         22 $self->_add_clause('order_by',@args);
92             }
93              
94             sub unwind {
95 2     2 1 41 my $self = shift;
96 2 50       12 puke "need arg1 => list expr" unless $_[0];
97 2 50 33     19 puke "need arg2 => list variable" unless ($_[1] && !ref($_[1]));
98 2         15 $self->_add_clause('unwind',$_[0],'AS',$_[1]);
99             }
100              
101             sub match {
102 49     49 1 116 my $self = shift;
103             # shortcut for a single node identifier, with labels
104 49 100 66     393 if (@_==1 and $_[0] =~ /^[a-z][a-z0-9_:]*$/i) {
105 24         127 $self->_add_clause('match',"($_[0])");
106             }
107             else {
108 25         108 $self->_add_clause('match',@_);
109             }
110             }
111              
112             sub create {
113 8     8 1 24 my $self = shift;
114             # shortcut for a single node identifier, with labels
115 8 100 66     83 if (@_==1 and $_[0] =~ /^[a-z][a-z0-9_:]*$/i) {
116 3         23 $self->_add_clause('create',"($_[0])");
117             }
118             else {
119 5         26 $self->_add_clause('create',@_);
120             }
121             }
122              
123             sub foreach {
124 1     1 1 4 my $self = shift;
125 1 50 33     14 puke "need arg1 => list variable" unless ($_[0] && !ref($_[0]));
126 1 50       6 puke "need arg2 => list expr" unless $_[1];
127 1 50       9 puke "need arg3 => cypher update stmt" unless $_[2];
128 1         9 $self->_add_clause('foreach', $_[0],'IN',$_[1],'|',$_[2]);
129             }
130              
131             sub load_csv {
132 0     0 1 0 my $self = shift;
133 0 0       0 puke "need arg1 => file location" unless $_[0];
134 0 0 0     0 puke "need arg2 => identifier" if (!defined $_[1] || ref $_[1]);
135 0         0 $self->_add_clause('load_csv','FROM',$_[0],'AS',$_[1]);
136             }
137              
138             sub load_csv_with_headers {
139 0     0 1 0 my $self = shift;
140 0 0       0 puke "need arg1 => file location" unless $_[0];
141 0 0 0     0 puke "need arg2 => identifier" if (!defined $_[1] || ref $_[1]);
142 0         0 $self->_add_clause('load_csv_with_headers','FROM',$_[0],'AS',$_[1]);
143             }
144              
145             #create_constraint_exist('node', 'label', 'property')
146              
147             sub create_constraint_exist {
148 0     0 1 0 my $self = shift;
149 0 0       0 puke "need arg1 => node/reln pattern" unless defined $_[0];
150 0 0 0     0 puke "need arg2 => label" if (!defined $_[1] || ref $_[1]);
151 0 0 0     0 puke "need arg2 => property" if (!defined $_[2] || ref $_[2]);
152 0         0 $self->_add_clause('create_constraint_on', "($_[0]:$_[1])", 'ASSERT',"exists($_[0].$_[2])");
153             }
154              
155             # create_constraint_unique('node', 'label', 'property')
156             sub create_constraint_unique {
157 0     0 1 0 my $self = shift;
158 0 0       0 puke "need arg1 => node/reln pattern" unless defined $_[0];
159 0 0 0     0 puke "need arg2 => label" if (!defined $_[1] || ref $_[1]);
160 0 0 0     0 puke "need arg2 => property" if (!defined $_[2] || ref $_[2]);
161 0         0 $self->_add_clause('create_constraint_on', "($_[0]:$_[1])", 'ASSERT',
162             "$_[0].$_[2]", 'IS UNIQUE');
163             }
164              
165             # create_index('label' => 'property')
166             sub create_index {
167 0     0 1 0 my $self = shift;
168 0 0 0     0 puke "need arg1 => node label" if (!defined $_[0] || ref $_[0]);
169 0 0 0     0 puke "need arg2 => node property" if (!defined $_[1] || ref $_[1]);
170 0         0 $self->_add_clause('create_index','ON',":$_[0]($_[1])");
171             }
172              
173             # drop_index('label'=>'property')
174             sub drop_index {
175 0     0 1 0 my $self = shift;
176 0 0 0     0 puke "need arg1 => node label" if (!defined $_[0] || ref $_[0]);
177 0 0 0     0 puke "need arg2 => node property" if (!defined $_[1] || ref $_[1]);
178 0         0 $self->_add_clause('drop_index','ON',":$_[0]($_[1])");
179             }
180              
181             # using_index('identifier', 'label', 'property')
182             sub using_index {
183 0     0 1 0 my $self = shift;
184 0 0 0     0 puke "need arg1 => identifier" if (!defined $_[0] || ref $_[0]);
185 0 0 0     0 puke "need arg2 => node label" if (!defined $_[1] || ref $_[1]);
186 0 0 0     0 puke "need arg3 => node property" if (!defined $_[2] || ref $_[2]);
187 0         0 $self->_add_clause('using_index',"$_[0]:$_[1]($_[2])");
188             }
189              
190             # using_scan('identifier' => 'label')
191             sub using_scan {
192 0     0 1 0 my $self = shift;
193 0 0 0     0 puke "need arg1 => identifier" if (!defined $_[0] || ref $_[0]);
194 0 0 0     0 puke "need arg2 => node label" if (!defined $_[1] || ref $_[1]);
195 0         0 $self->_add_clause('using_scan',"$_[0]:$_[1]");
196             }
197              
198             # using_join('identifier', ...)
199             sub using_join {
200 0     0 1 0 my $self = shift;
201 0 0 0     0 puke "need arg => identifier" if (!defined $_[0] || ref $_[0]);
202 0         0 $self->_add_clause('using_join', 'ON', join(',',@_));
203             }
204              
205             # everything winds up here
206             sub _add_clause {
207 172     172   369 my $self = shift;
208 172         364 my $clause = shift;
209 172         504 $self->{dirty} = 1;
210 172         308 my @clause;
211 172         440 push @clause, $clause;
212 172 100 66     1176 if ( $clause =~ /^match|create|merge/ and
      100        
213             @_==1 and $_[0] =~ /^[a-z][a-z0-9_:]*$/i) {
214 2         12 push @clause, "($_[0])";
215             }
216             else {
217 170         473 for (@_) {
218 212 100 100     944 if (ref && !blessed($_)) {
219 30         171 my $plr = Neo4j::Cypher::Abstract::Peeler->new();
220 30         145 push @clause, $plr->express($_);
221             # kludge
222 30 100       140 if ($clause =~ /^set/) {
223             # removing enclosing parens from peel
224 8         61 $clause[-1] =~ s/^\s*\(//;
225 8         55 $clause[-1] =~ s/\)\s*$//;
226             }
227 30         167 push @{$self->{bind_values}}, $plr->bind_values;
  30         171  
228 30         71 push @{$self->{parameters}}, $plr->parameters;
  30         122  
229             }
230             else {
231 182         451 push @clause, $_;
232 182         608 my @parms = m/(\$[a-z][a-z0-9]*)/ig;
233 182         390 push @{$self->{parameters}}, @parms;
  182         605  
234             }
235             }
236             }
237 172 100       854 if ($clause =~ /^return|with|order|set|remove/) {
238             # group args in array so they are separated by commas
239 70         292 @clause = (shift @clause, [@clause]);
240             }
241 172         634 push @{$self->{stack}}, \@clause;
  172         495  
242 172         1405 return $self;
243             }
244              
245             sub as_string {
246 69     69 1 772 my $self = shift;
247 69 100 66     395 return $self->{string} if ($self->{string} && !$self->{dirty});
248 62         147 undef $self->{dirty};
249 62         129 my @c;
250 62         128 for (@{$self->{stack}}) {
  62         193  
251 172         528 my ($kws, @arg) = @$_;
252 172         430 $kws =~ s/_/ /g;
253 172         375 for (@arg) {
254 178 100       684 $_ = join(',',@$_) if ref eq 'ARRAY';
255             }
256 172 100       458 if ($kws =~ /foreach/i) { #kludge for FOREACH
257 1         12 push @c, uc($kws)." (".join(' ',@arg).")";
258             }
259             else {
260 171         759 push @c, join(' ',uc $kws, @arg);
261             }
262             }
263 62         393 $self->{string} = join(' ',@c);
264 62         748 $self->{string} =~ s/(\s)+/$1/g;
265 62         1285 return $self->{string};
266             }
267              
268             sub AUTOLOAD {
269 81     81   198 my $self = shift;
270 81         522 my ($method) = $AUTOLOAD =~ /.*::(.*)/;
271 81 50       1962 unless (grep /$method/, @all_clauses) {
272 0         0 puke "Unknown clause '$method'";
273             }
274 81         340 $self->_add_clause($method,@_);
275             }
276              
277             sub belch (@) {
278 0     0 0   my($func) = (caller(1))[3];
279 0           Carp::carp "[$func] Warning: ", @_;
280             }
281              
282             sub puke (@) {
283 0     0 0   my($func) = (caller(1))[3];
284 0           Carp::croak "[$func] Fatal: ", @_;
285             }
286              
287       0     sub DESTROY {}
288              
289             =head1 NAME
290              
291             Neo4j::Cypher::Abstract - Generate Cypher query statements
292              
293             =head1 SYNOPSIS
294              
295             =head1 DESCRIPTION
296              
297             When writing code to automate database queries, sometimes it is
298             convenient to use a wrapper that generates desired query strings. Then
299             the user can think conceptually and avoid having to remember precise
300             syntax or write and debug string manipulations. A good wrapper can
301             also allow the user to produce query statements dynamically, hide
302             dialect details, and may include some simple syntax
303             checking. C is an example of a widely-used wrapper for
304             SQL.
305              
306             The graph database L allows SQL-like
307             declarative queries through its query language
308             L. C
309             is a Cypher wrapper in the spirit of C that creates
310             very general Cypher productions in an intuitive, Perly way.
311              
312             =head2 Basic idea : stringing clauses together with method calls
313              
314             A clause is a portion of a complete query statement that plays a
315             specific functional role in the statement and is set off by one or
316             more reserved words. L
317             Cypher|https://neo4j.com/docs/developer-manual/current/cypher/clauses/>
318             include reading (e.g., MATCH), writing (CREATE), importing (LOAD CSV), and
319             schema (CREATE CONSTRAINT) clauses, among others. They have
320             arguments that define the clause's scope of action.
321              
322             L objects possess methods
323             for every Cypher clause. Each method adds its clause, with arguments,
324             to the object's internal queue. Every method returns the object
325             itself. When an object is rendered as a string, it concatenates its
326             clauses to yield the entire query statement.
327              
328             These features add up to the following idiom. Suppose we want to
329             render the Cypher statement
330              
331             MATCH (n:Users) WHERE n.name =~ 'Fred.*' RETURN n.manager
332              
333             In C, we do
334              
335             $s = Neo4j::Cypher::Abstract->new()->match('n:Users')
336             ->where("n.name =~ 'Fred.*'")->return('n.manager');
337             print "$s;\n"; # "" is overloaded by $s->as_string()
338              
339             Because you may create many such statements in a program, a short
340             alias for the constructor can be imported, and extra variable
341             assignments can be avoided.
342              
343             use Neo4j::Cypher::Abstract qw/cypher/;
344             use DBI;
345              
346             my $dbh = DBI->connect("dbi:Neo4p:http://127.0.0.1:7474;user=foo;pass=bar");
347             my $sth = $dbh->prepare(
348             cypher->match('n:Users')->where("n.name =~ 'Fred.*'")->return('n.manager')
349             );
350             $sth->execute();
351             ...
352              
353             =head2 Patterns
354              
355             L
356             are representations of subgraphs with constraints that are key
357             components of Cypher queries. They have their own syntax and are also
358             amenable to wrapping. In the example L
359             clauses together with method calls">, C uses a simple
360             built-in shortcut:
361              
362             $s->match('n:User') eq $s->match('(n:User)')
363              
364             where C<(n:User)> is the simple pattern for "all nodes with label
365             'User'". The module L handles
366             complex and arbitrary patterns. It is loaded automatically on C
367             Neo4j::Cypher::Abstract>. Abstract patterns are written in a similar
368             idiom as Cypher statements. They can be used anywhere a string is
369             allowed. For example:
370              
371             use Neo4j::Cypher::Abstract qw/cypher ptn/;
372              
373             ptn->N(':Person',{name=>'Oliver Stone'})->R("r>")->N('movie') eq
374             '(:Person {name:'Oliver Stone'})-[r]->(movie)'
375             $sth = $dbh->prepare(
376             cypher->match(ptn->N(':Person',{name=>'Oliver Stone'})->R("r>")->N('movie'))
377             ->return('type(r)')
378             );
379              
380             See L for a full description of how
381             to specify patterns.
382              
383             =head2 WHERE clauses
384              
385             As in SQL, Cypher has a WHERE clause that is used to filter returned
386             results. Rather than having to create custom strings for common WHERE
387             expressions, L provides an intuitive system for
388             constructing valid expressions from Perl data structures made up of
389             hash, array, and scalar references. L
390             contains a new implementation of the L expression
391             "compiler". If the argument to the C method (or any other
392             method, in fact) is an array or hash reference, it is interpreted as
393             an expression in L style. (The parser is a complete
394             reimplementation, so some idioms in that style may not result in
395             exactly the same productions.)
396              
397             For details on writing expressions, see
398             L.
399              
400             =head2 Parameters
401              
402             Parameters in Cypher are named, and given as alphanumeric tokens
403             prefixed (sadly) with '$'. The C object collects
404             these in the order they appear in the complete statement. The list of
405             parameters can be recovered with the C method.
406              
407             $c = cypher->match('n:Person')->return('n.name')
408             ->skip('$s')->limit('$l');
409             @p = $c->parameters; # @p is ('$s', '$l') /;
410              
411             =head1 METHODS
412              
413             =head2 Reading clauses
414              
415             =over
416              
417             =item match(@ptns)
418              
419             =item optional_match(@ptns)
420              
421             =item where($expr)
422              
423             =item start($ptn)
424              
425             =back
426              
427             =head2 Writing clauses
428              
429             =over
430              
431             =item create(@ptns), create_unique($ptn)
432              
433             =item merge(@ptns)
434              
435             =item foreach($running_var => $list, cypher->)
436              
437             =item set()
438              
439             =item delete(), detach_delete()
440              
441             =item on_create(), on_match()
442              
443             =back
444              
445             =head2 Modifiers
446              
447             =over
448              
449             =item limit($num)
450              
451             =item skip($num)
452              
453             =item order_by($identifier)
454              
455             =back
456              
457             =head2 General clauses
458              
459             =over
460              
461             =item return(@items), return_distinct(@items)
462              
463             =item with(@identifiers), with_distinct(@identifiers)
464              
465             =item unwind($list => $identifier)
466              
467             =item union()
468              
469             =item call()
470              
471             =item yield()
472              
473             =back
474              
475             =head2 Hinting
476              
477             =over
478              
479             =item using_index($index)
480              
481             =item using_scan()
482              
483             =item using_join($identifier)
484              
485             =back
486              
487             =head2 Loading
488              
489             =over
490              
491             =item load_csv($file => $identifier), load_csv_with_headers(...)
492              
493             =back
494              
495             =head2 Schema
496              
497             =over
498              
499             =item create_constraint_exist($node => $label, $property),create_constraint_unique($node => $label, $property)
500              
501             =item drop_constraint(...)
502              
503             =item create_index($label => $property), drop_index($label => $property)
504              
505             =back
506              
507             =head2 Utility Methods
508              
509             =over
510              
511             =item parameters()
512              
513             Return a list of statement parameters.
514              
515             =item as_string()
516              
517             Render the Cypher statement as a string. Overloads C<"">.
518              
519             =back
520              
521             =head1 SEE ALSO
522              
523             L, L, L, L, L
524              
525             =head1 AUTHOR
526              
527             Mark A. Jensen
528             CPAN: MAJENSEN
529             majensen -at- cpan -dot- org
530              
531             =head1 LICENSE
532              
533             This software is provided for use under the terms of Perl itself.
534              
535             =head1 COPYRIGHT
536              
537             (c) 2017 Mark A. Jensen
538              
539             =cut
540              
541             1;
542