neo4j-cypher 1.0.0.rc2 → 1.0.0
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.
- 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
|
|