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