neo4j-cypher 1.0.0.rc2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/README.rdoc +28 -77
- data/lib/neo4j-cypher.rb +3 -1
- data/lib/neo4j-cypher/abstract_filter.rb +78 -0
- data/lib/neo4j-cypher/argument.rb +0 -1
- data/lib/neo4j-cypher/clause.rb +39 -1
- data/lib/neo4j-cypher/clause_list.rb +30 -15
- data/lib/neo4j-cypher/collection.rb +14 -0
- data/lib/neo4j-cypher/context.rb +27 -27
- data/lib/neo4j-cypher/create.rb +0 -3
- data/lib/neo4j-cypher/foreach.rb +16 -0
- data/lib/neo4j-cypher/match.rb +21 -18
- data/lib/neo4j-cypher/neography.rb +22 -0
- data/lib/neo4j-cypher/node_var.rb +0 -1
- data/lib/neo4j-cypher/operator.rb +4 -10
- data/lib/neo4j-cypher/predicate.rb +5 -56
- data/lib/neo4j-cypher/property.rb +4 -12
- data/lib/neo4j-cypher/rel_var.rb +25 -31
- data/lib/neo4j-cypher/return.rb +0 -1
- data/lib/neo4j-cypher/root.rb +44 -22
- data/lib/neo4j-cypher/start.rb +36 -40
- data/lib/neo4j-cypher/version.rb +2 -2
- data/lib/neo4j-cypher/where.rb +16 -3
- data/lib/neo4j-cypher/with.rb +1 -3
- data/lib/tasks/analyzer.rake +54 -0
- metadata +13 -6
- data/lib/neo4j-cypher/mixins.rb +0 -47
data/Gemfile
CHANGED
data/README.rdoc
CHANGED
@@ -1,98 +1,49 @@
|
|
1
|
-
|
1
|
+
= neo4j-cypher {<img src="https://secure.travis-ci.org/andreasronge/neo4j-cypher.png" />}[http://travis-ci.org/andreasronge/neo4j-cypher] {<img src="https://codeclimate.com/badge.png"/>}[https://codeclimate.com/github/andreasronge/neo4j-cypher]
|
2
2
|
|
3
3
|
A Ruby DSL for the Neo4j Cypher query language for both MRI and JRuby.
|
4
4
|
The JRuby neo4j-core gem's cypher dsl has been moved to this gem.
|
5
5
|
|
6
|
-
|
6
|
+
== Docs
|
7
7
|
|
8
|
-
|
8
|
+
* {Neo4j Wiki Cypher}[https://github.com/andreasronge/neo4j/wiki/Neo4j%3A%3ACypher]
|
9
|
+
* {Neo4j Cypher Docs}[http://docs.neo4j.org/chunked/stable/]
|
10
|
+
* RSpecs (100% test coverage)
|
9
11
|
|
10
|
-
|
12
|
+
== Why ?
|
11
13
|
|
14
|
+
Why should I write my queries using the neo4j-cypher DSL instead of using original cypher syntax ?
|
12
15
|
|
13
|
-
|
16
|
+
Let's look at a simple example using the cypher query language without the DSL.
|
17
|
+
For example: Find my friends I got 1994
|
14
18
|
|
15
|
-
|
16
|
-
|
19
|
+
START me=node(1)
|
20
|
+
MATCH (me)-[friend_rel:`friends`]->(friends)
|
21
|
+
WHERE (friend_rel.since = 1994)
|
22
|
+
RETURN friends
|
17
23
|
|
18
|
-
|
24
|
+
Instead of relying on a strict order of the clauses (+START+, +MATCH+, +WHERE+ ...)
|
25
|
+
and having to use variables (me and friends) you can write the same query using the DSL like this:
|
19
26
|
|
20
|
-
|
21
|
-
node(3) > :r > :x
|
22
|
-
end
|
27
|
+
node(1).outgoing(rel(:friends).where{|r| r[:since] == 1994})
|
23
28
|
|
29
|
+
This is more or less plain english (for me), navigate from node(1) outgoing relationships friends where friends since property is equal 1994.
|
30
|
+
Remember just like ruby, the last value evaluated is the return value which means it will return your friend.
|
24
31
|
|
25
|
-
|
32
|
+
Another example: Return the age property of all the nodes between node 1 and node 3.
|
26
33
|
|
27
|
-
|
28
|
-
node(1) > (rel(:knows)[:since] > 1994) > (node(:other)[:name] == 'foo'); :other
|
29
|
-
end
|
34
|
+
(node(1) >> node >> node(3)).nodes.extract(&:age)
|
30
35
|
|
36
|
+
Notice the cypher extract function works like the standard ruby map method.
|
37
|
+
The query above will generate the following cypher string:
|
31
38
|
|
32
|
-
|
39
|
+
START v2=node(3),v3=node(1)
|
40
|
+
MATCH v1 = (v2)-->(v4)-->(v3)
|
41
|
+
RETURN extract(x in nodes(v1) : x.age)
|
33
42
|
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
|
-
|
39
|
-
"START v1=node(2,3,4,1) RETURN count(v1.property?"
|
40
|
-
|
41
|
-
Neo4j::Cypher.query do
|
42
|
-
node(2, 3, 4, 1)[:property?].count
|
43
|
-
end
|
44
|
-
|
45
|
-
"START v1=node(42) MATCH (v1)-[:`favorite`]->(stuff)<-[:`favorite`]-(person) WHERE not((v1)-[:`friend`]-(person)) RETURN person.name,count(stuff) ORDER BY count(stuff) DESC"
|
46
|
-
|
47
|
-
Neo4j::Cypher.query do
|
48
|
-
node(42).where_not { |m| m - :friend - :person } > :favorite > :stuff < :favorite < :person
|
49
|
-
ret(node(:person)[:name], count(:stuff).desc)
|
50
|
-
end
|
51
|
-
|
52
|
-
|
53
|
-
== Complex Example
|
54
|
-
|
55
|
-
"START n=node(42) MATCH (n)-[r]->(m) WITH n,collect(type(r)) as out_types,collect(m) as outgoing MATCH (n)<-[r]-(m) RETURN n,outgoing,out_types,collect(m) as incoming,collect(type(r)) as in_types"
|
56
|
-
|
57
|
-
Neo4j::Cypher.query do
|
58
|
-
n = node(42).as(:n)
|
59
|
-
r = rel('r')
|
60
|
-
m = node(:m)
|
61
|
-
rel_types = r.rel_type.collect
|
62
|
-
end_nodes = m.collect
|
63
|
-
|
64
|
-
n.with_match(rel_types.as(:out_types), end_nodes.as(:outgoing)) { |n, _, _| n < r < m } > r > m
|
65
|
-
|
66
|
-
ret([n,
|
67
|
-
:outgoing,
|
68
|
-
:out_types,
|
69
|
-
end_nodes.as(:incoming),
|
70
|
-
rel_types.as(:in_types)])
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
|
-
=== Co-Tagged Places - Places Related through Tags
|
75
|
-
|
76
|
-
Find places that are tagged with the same tags: Determine the tags for place x. What else is tagged the same as x that is not x."
|
77
|
-
|
78
|
-
"START place=node:node_auto_index(name = "CoffeeShop1") MATCH place-[:tagged]->tag<-[:tagged]-otherPlace RETURN otherPlace.name, collect(tag.name) ORDER By otherPlace.name desc"
|
79
|
-
|
80
|
-
Can be written like this:
|
81
|
-
|
82
|
-
Neo4j::Cypher.query do
|
83
|
-
other_place = node(:otherPlace)
|
84
|
-
place = lookup('node_auto_index', 'name', 'CoffeeShop1').as(:place)
|
85
|
-
place > rel(':tagged') > node(:tag) < rel(':tagged') < other_place
|
86
|
-
ret other_place[:name].desc, node(:tag)[:name].collect
|
87
|
-
end
|
88
|
-
|
89
|
-
|
90
|
-
Or in one line:
|
91
|
-
|
92
|
-
Neo4j::Cypher.query do
|
93
|
-
lookup('node_auto_index', 'name', 'CoffeeShop1') > rel(':tagged') > node(:tag).ret { |t| t[:name].collect } < rel(':tagged') < node(:otherPlace).ret { |n| n[:name].desc }
|
94
|
-
end
|
43
|
+
So, the answer why you should use it is simply that it might improve the readability of the code for (ruby) programmers and
|
44
|
+
make it more fun to write queries.
|
95
45
|
|
46
|
+
Please read the {Neo4j Cypher Docs}[http://docs.neo4j.org/chunked/stable/] for more examples.
|
96
47
|
|
97
48
|
== License
|
98
49
|
|
data/lib/neo4j-cypher.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'neo4j-cypher/version'
|
2
2
|
|
3
3
|
require 'neo4j-cypher/context'
|
4
|
-
require 'neo4j-cypher/mixins'
|
5
4
|
require 'neo4j-cypher/clause'
|
6
5
|
require 'neo4j-cypher/clause_list'
|
6
|
+
require 'neo4j-cypher/abstract_filter'
|
7
|
+
require 'neo4j-cypher/collection'
|
8
|
+
require 'neo4j-cypher/foreach'
|
7
9
|
require 'neo4j-cypher/argument'
|
8
10
|
require 'neo4j-cypher/root'
|
9
11
|
require 'neo4j-cypher/start'
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module Cypher
|
3
|
+
class AbstractFilter
|
4
|
+
include Clause
|
5
|
+
|
6
|
+
def filter_initialize(input_context, method_name, selector_token, &block)
|
7
|
+
input = input_context.clause
|
8
|
+
fe = filter_expr(input, selector_token, &block)
|
9
|
+
@cypher = "#{method_name}#{fe}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def return_value
|
13
|
+
@cypher
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_cypher
|
17
|
+
@cypher
|
18
|
+
end
|
19
|
+
|
20
|
+
def filter_arg(input)
|
21
|
+
var = NodeVar.as_var(clause_list, 'x')
|
22
|
+
|
23
|
+
if input.is_a?(Neo4j::Cypher::Property)
|
24
|
+
filter_input = Property.new(var)
|
25
|
+
filter_input.expr = 'x'
|
26
|
+
filter_input.eval_context
|
27
|
+
else
|
28
|
+
input.referenced!
|
29
|
+
var.eval_context
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def filter_value(input)
|
34
|
+
input.is_a?(Neo4j::Cypher::Property) ? input.expr : input.return_value
|
35
|
+
end
|
36
|
+
|
37
|
+
# Used for the Ruby &: method shortcut
|
38
|
+
class FilterProp
|
39
|
+
def initialize(obj)
|
40
|
+
@obj = obj
|
41
|
+
end
|
42
|
+
|
43
|
+
def method_missing(m)
|
44
|
+
@obj[m.to_sym]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def filter_exec(arg, &block)
|
49
|
+
clause_list.push
|
50
|
+
begin
|
51
|
+
ret = RootClause::EvalContext.new(self).instance_exec(arg, &block)
|
52
|
+
rescue NoMethodError
|
53
|
+
if arg.kind_of?(Neo4j::Cypher::Context::Variable)
|
54
|
+
# Try again, maybe we are using the Ruby &: method shortcut
|
55
|
+
ret = FilterProp.new(arg).instance_eval(&block)
|
56
|
+
else
|
57
|
+
raise
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
filter = clause_list.empty? ? ret.clause.to_cypher : clause_list.to_cypher
|
62
|
+
clause_list.pop
|
63
|
+
filter
|
64
|
+
end
|
65
|
+
|
66
|
+
def filter_expr(input, selector_token, &block)
|
67
|
+
expr = "(x in #{filter_value(input)}"
|
68
|
+
arg = filter_arg(input)
|
69
|
+
filter = filter_exec(arg, &block)
|
70
|
+
expr << "#{selector_token}#{filter})"
|
71
|
+
# WHERE all(x in nodes(v1) WHERE x.age > 30)
|
72
|
+
expr
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
data/lib/neo4j-cypher/clause.rb
CHANGED
@@ -38,7 +38,7 @@ module Neo4j
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def separator
|
41
|
-
','
|
41
|
+
clause_type == :where ? ' and ' : ','
|
42
42
|
end
|
43
43
|
|
44
44
|
def match_value=(mv)
|
@@ -58,6 +58,44 @@ module Neo4j
|
|
58
58
|
NAME[clause_type]
|
59
59
|
end
|
60
60
|
|
61
|
+
def var_name
|
62
|
+
@var_name ||= @clause_list.create_variable(self)
|
63
|
+
end
|
64
|
+
|
65
|
+
def var_name=(new_name)
|
66
|
+
@var_name = new_name.to_sym if new_name
|
67
|
+
end
|
68
|
+
|
69
|
+
def referenced?
|
70
|
+
!!@referenced
|
71
|
+
end
|
72
|
+
|
73
|
+
def referenced!
|
74
|
+
@referenced = true
|
75
|
+
end
|
76
|
+
|
77
|
+
def as_alias(new_name)
|
78
|
+
@alias = new_name
|
79
|
+
self.var_name = new_name
|
80
|
+
end
|
81
|
+
|
82
|
+
def alias_name
|
83
|
+
@alias
|
84
|
+
end
|
85
|
+
|
86
|
+
def as_alias?
|
87
|
+
!!@alias && var_name != return_value
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_prop_string(props)
|
91
|
+
key_values = props.keys.map do |key|
|
92
|
+
raw = key.to_s[0, 1] == '_'
|
93
|
+
val = props[key].is_a?(String) && !raw ? "'#{props[key]}'" : props[key]
|
94
|
+
"#{raw ? key.to_s[1..-1] : key} : #{val}"
|
95
|
+
end
|
96
|
+
"{#{key_values.join(', ')}}"
|
97
|
+
end
|
98
|
+
|
61
99
|
def create_clause_args_for(args)
|
62
100
|
args.map do |arg|
|
63
101
|
case arg
|
@@ -7,7 +7,8 @@ module Neo4j
|
|
7
7
|
|
8
8
|
def initialize(variables = [])
|
9
9
|
@variables = variables
|
10
|
-
@
|
10
|
+
@lists_of_clause_list = [[]]
|
11
|
+
@curr_clause_list = @lists_of_clause_list.first
|
11
12
|
@insert_order = 0
|
12
13
|
end
|
13
14
|
|
@@ -20,34 +21,41 @@ module Neo4j
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def find(clause_type)
|
23
|
-
@
|
24
|
+
@curr_clause_list.find { |c| c.clause_type == clause_type }
|
24
25
|
end
|
25
26
|
|
26
27
|
def each
|
27
|
-
@
|
28
|
+
@curr_clause_list.each { |c| yield c }
|
28
29
|
end
|
29
30
|
|
30
31
|
def push
|
31
|
-
|
32
|
-
@
|
33
|
-
@clause_list = []
|
32
|
+
@lists_of_clause_list << []
|
33
|
+
@curr_clause_list = @lists_of_clause_list.last
|
34
34
|
self
|
35
35
|
end
|
36
36
|
|
37
37
|
def pop
|
38
|
-
@
|
39
|
-
@
|
40
|
-
@
|
38
|
+
@lists_of_clause_list.pop
|
39
|
+
@curr_clause_list = @lists_of_clause_list.last
|
40
|
+
@curr_clause_list.sort!
|
41
41
|
self
|
42
42
|
end
|
43
43
|
|
44
|
+
def return_clause
|
45
|
+
@curr_clause_list.find{|r| r.respond_to?(:return_items)}
|
46
|
+
end
|
47
|
+
|
48
|
+
def depth
|
49
|
+
@lists_of_clause_list.count
|
50
|
+
end
|
51
|
+
|
44
52
|
def insert(clause)
|
45
53
|
ctype = clause.clause_type
|
46
54
|
|
47
55
|
if Clause::ORDER.include?(ctype)
|
48
56
|
# which list should we add the cluase to, the root or the sub list ?
|
49
57
|
# ALl the start and return clauses should move to the clause_list
|
50
|
-
c = (
|
58
|
+
c = (depth > 1 && (ctype == :start || ctype == :return)) ? @lists_of_clause_list.first : @curr_clause_list
|
51
59
|
c << clause
|
52
60
|
@insert_order += 1
|
53
61
|
clause.insert_order = @insert_order
|
@@ -57,17 +65,16 @@ module Neo4j
|
|
57
65
|
end
|
58
66
|
|
59
67
|
def last
|
60
|
-
@
|
68
|
+
@curr_clause_list.last
|
61
69
|
end
|
62
70
|
|
63
71
|
def delete(clause_or_context)
|
64
72
|
c = clause_or_context.respond_to?(:clause) ? clause_or_context.clause : clause_or_context
|
65
|
-
@
|
73
|
+
@curr_clause_list.delete(c)
|
66
74
|
end
|
67
75
|
|
68
76
|
#def debug
|
69
|
-
# puts "
|
70
|
-
# @clause_list.each_with_index { |c, i| puts " #{i} #{c.clause_type.inspect}, #{c.class} id: #{c.object_id} order #{c.insert_order}" }
|
77
|
+
# @curr_clause_list.each_with_index { |c, i| puts " #{i} #{c.clause_type.inspect}, #{c.to_cypher} - #{c.class} id: #{c.object_id} order #{c.insert_order}" }
|
71
78
|
#end
|
72
79
|
|
73
80
|
def create_variable(var)
|
@@ -96,9 +103,17 @@ module Neo4j
|
|
96
103
|
end
|
97
104
|
|
98
105
|
def prefix(list)
|
99
|
-
|
106
|
+
(depth > 1) && !prefix_for_depth_2.include?(list.first.clause_type) ? '' : "#{list.first.prefix} "
|
100
107
|
end
|
101
108
|
|
109
|
+
def prefix_for_depth_2
|
110
|
+
if include?(:match) && include?(:where)
|
111
|
+
[:set, :delete, :create, :where]
|
112
|
+
else
|
113
|
+
[:set, :delete, :create]
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
102
117
|
end
|
103
118
|
end
|
104
119
|
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module Cypher
|
3
|
+
|
4
|
+
class Collection < AbstractFilter
|
5
|
+
|
6
|
+
def initialize(clause_list, method_name, input_context, &block)
|
7
|
+
super(clause_list, :return_item)
|
8
|
+
clause_list.delete(input_context)
|
9
|
+
filter_initialize(input_context, method_name, " : ", &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/neo4j-cypher/context.rb
CHANGED
@@ -99,33 +99,19 @@ module Neo4j
|
|
99
99
|
|
100
100
|
module PredicateMethods
|
101
101
|
def all?(&block)
|
102
|
-
|
103
|
-
Predicate.new(clause_list, :op => 'all', :clause => :where, :input => input, :iterable => iterable, :predicate_block => block).eval_context
|
104
|
-
end
|
105
|
-
|
106
|
-
def extract(&block)
|
107
|
-
Predicate.new(clause_list, :op => 'extract', :clause => :return_item, :input => input, :iterable => iterable, :predicate_block => block).eval_context
|
108
|
-
end
|
109
|
-
|
110
|
-
def filter(&block)
|
111
|
-
Predicate.new(clause_list, :op => 'filter', :clause => :return_item, :input => input, :iterable => iterable, :predicate_block => block).eval_context
|
102
|
+
Predicate.new(clause_list, 'all', self, &block).eval_context
|
112
103
|
end
|
113
104
|
|
114
105
|
def any?(&block)
|
115
|
-
Predicate.new(clause_list,
|
106
|
+
Predicate.new(clause_list, 'any', self, &block).eval_context
|
116
107
|
end
|
117
108
|
|
118
109
|
def none?(&block)
|
119
|
-
Predicate.new(clause_list,
|
110
|
+
Predicate.new(clause_list, 'none', self, &block).eval_context
|
120
111
|
end
|
121
112
|
|
122
113
|
def single?(&block)
|
123
|
-
Predicate.new(clause_list,
|
124
|
-
end
|
125
|
-
|
126
|
-
def foreach(&block)
|
127
|
-
Predicate.new(clause_list, :op => '', :clause => :foreach, :input => input, :iterable => iterable, :predicate_block => block, :separator => ' FOREACH ').eval_context
|
128
|
-
input.eval_context
|
114
|
+
Predicate.new(clause_list, 'single', self, &block).eval_context
|
129
115
|
end
|
130
116
|
end
|
131
117
|
|
@@ -143,15 +129,31 @@ module Neo4j
|
|
143
129
|
(self.is_a?(RootClause::EvalContext)) ? r : self
|
144
130
|
end
|
145
131
|
|
132
|
+
# To return a single property, or the value of a function from a collection of nodes or relationships, you can use EXTRACT.
|
133
|
+
# It will go through a collection, run an expression on every element, and return the results in an collection with these values.
|
134
|
+
# It works like the map method in functional languages such as Lisp and Scala.
|
135
|
+
# Will generate:
|
136
|
+
# EXTRACT( identifier in collection : expression )
|
137
|
+
def extract(&block)
|
138
|
+
Collection.new(clause_list, 'extract', self, &block).eval_context
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns all the elements in a collection that comply to a predicate.
|
142
|
+
# Will generate
|
143
|
+
# FILTER(identifier in collection : predicate)
|
144
|
+
def filter(&block)
|
145
|
+
Collection.new(clause_list, 'filter', self, &block).eval_context
|
146
|
+
end
|
147
|
+
|
148
|
+
def foreach(&block)
|
149
|
+
Foreach.new(clause_list, self, &block).eval_context
|
150
|
+
end
|
151
|
+
|
146
152
|
end
|
147
153
|
|
148
154
|
module Sortable
|
149
155
|
def _return_item
|
150
|
-
|
151
|
-
self
|
152
|
-
else
|
153
|
-
@return_item ||= ReturnItem.new(clause_list, self).eval_context
|
154
|
-
end
|
156
|
+
@return_item ||= ReturnItem.new(clause_list, self).eval_context
|
155
157
|
end
|
156
158
|
|
157
159
|
def asc(*props)
|
@@ -265,14 +267,12 @@ module Neo4j
|
|
265
267
|
|
266
268
|
module Variable
|
267
269
|
def where(&block)
|
268
|
-
|
269
|
-
Operator.new(clause_list, x.clause, nil, "").unary!
|
270
|
+
Where.new(clause_list, self, &block)
|
270
271
|
self
|
271
272
|
end
|
272
273
|
|
273
274
|
def where_not(&block)
|
274
|
-
|
275
|
-
Operator.new(clause_list, x.clause, nil, "not").unary!
|
275
|
+
Where.new(clause_list, self, &block).neg!
|
276
276
|
self
|
277
277
|
end
|
278
278
|
|