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 CHANGED
@@ -5,6 +5,10 @@ gemspec
5
5
  group 'development' do
6
6
  gem 'pry'
7
7
  gem 'simplecov'
8
+ # gem "term-ansicolor"
9
+ gem 'flog'
10
+ gem 'flay'
11
+ gem 'reek'
8
12
  end
9
13
 
10
14
  group 'test' do
data/README.rdoc CHANGED
@@ -1,98 +1,49 @@
1
- == neo4j-cypher {<img src="https://secure.travis-ci.org/andreasronge/neo4j-cypher.png" />}[http://travis-ci.org/andreasronge/neo4j-cypher]
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
- === Docs
6
+ == Docs
7
7
 
8
- See {Neo4j Wiki Cypher}[https://github.com/andreasronge/neo4j/wiki/Neo4j::Core-Cypher]
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
- See the rspecs ! (> 99% test coverage)
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
- === Random Examples
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
- Notice the last value returned is the return value (if possible).
16
- Matching relationships can be done with operators: <,>, -, and <=> or with the both, incoming and outgoing methods.
19
+ START me=node(1)
20
+ MATCH (me)-[friend_rel:`friends`]->(friends)
21
+ WHERE (friend_rel.since = 1994)
22
+ RETURN friends
17
23
 
18
- "START v1=node(3) MATCH v2 = (v1)-[:`r`]->(x) RETURN v2"
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
- Neo4j::Cypher.query do
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
- "START v2=node(1) MATCH (v2)-[v1:`knows`]->(other) WHERE v1.since > 1994 and other.name = "foo" RETURN other"
32
+ Another example: Return the age property of all the nodes between node 1 and node 3.
26
33
 
27
- Neo4j::Cypher.query do
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
- "START v2=node(1) MATCH (v2)-[v1:`friends`]->(v3) WHERE (v1.since = 1994) RETURN v3"
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
- Neo4j::Cypher.query do
35
- node(1).outgoing(rel(:friends).where{|r| r[:since] == 1994})
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
@@ -2,7 +2,6 @@ module Neo4j
2
2
  module Cypher
3
3
 
4
4
  class Argument
5
- include Referenceable
6
5
  include Clause
7
6
 
8
7
  def initialize(clause_list, expr, var_name)
@@ -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
- @clause_list = []
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
- @clause_list.find { |c| c.clause_type == clause_type }
24
+ @curr_clause_list.find { |c| c.clause_type == clause_type }
24
25
  end
25
26
 
26
27
  def each
27
- @clause_list.each { |c| yield c }
28
+ @curr_clause_list.each { |c| yield c }
28
29
  end
29
30
 
30
31
  def push
31
- raise "Only support stack of depth 2" if @old_clause_list
32
- @old_clause_list = @clause_list
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
- @clause_list = @old_clause_list
39
- @clause_list.sort!
40
- @old_clause_list = nil
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 = (@old_clause_list && (ctype == :start || ctype == :return)) ? @old_clause_list : @clause_list
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
- @clause_list.last
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
- @clause_list.delete(c)
73
+ @curr_clause_list.delete(c)
66
74
  end
67
75
 
68
76
  #def debug
69
- # puts "ClauseList id: #{object_id}, vars: #{variables.size}"
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
- @old_clause_list && ![:set, :delete, :create].include?(list.first.clause_type) ? '' : "#{list.first.prefix} "
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
@@ -99,33 +99,19 @@ module Neo4j
99
99
 
100
100
  module PredicateMethods
101
101
  def all?(&block)
102
- self.respond_to?(:iterable)
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, :op => 'any', :clause => :where, :input => input, :iterable => iterable, :predicate_block => block).eval_context
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, :op => 'none', :clause => :where, :input => input, :iterable => iterable, :predicate_block => block).eval_context
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, :op => 'single', :clause => :where, :input => input, :iterable => iterable, :predicate_block => block).eval_context
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
- if self.is_a?(Neo4j::Cypher::ReturnItem::EvalContext)
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
- x = RootClause::EvalContext.new(self).instance_exec(self, &block)
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
- x = RootClause::EvalContext.new(self).instance_exec(self, &block)
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