neo4j-cypher 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +15 -0
- data/README.rdoc +71 -0
- data/lib/neo4j-cypher.rb +47 -0
- data/lib/neo4j-cypher/argument.rb +39 -0
- data/lib/neo4j-cypher/clause.rb +76 -0
- data/lib/neo4j-cypher/clause_list.rb +101 -0
- data/lib/neo4j-cypher/context.rb +411 -0
- data/lib/neo4j-cypher/create.rb +99 -0
- data/lib/neo4j-cypher/match.rb +343 -0
- data/lib/neo4j-cypher/mixins.rb +43 -0
- data/lib/neo4j-cypher/node_var.rb +65 -0
- data/lib/neo4j-cypher/operator.rb +130 -0
- data/lib/neo4j-cypher/predicate.rb +64 -0
- data/lib/neo4j-cypher/property.rb +106 -0
- data/lib/neo4j-cypher/rel_var.rb +128 -0
- data/lib/neo4j-cypher/result.rb +43 -0
- data/lib/neo4j-cypher/result_wrapper.rb +47 -0
- data/lib/neo4j-cypher/return.rb +124 -0
- data/lib/neo4j-cypher/root.rb +182 -0
- data/lib/neo4j-cypher/start.rb +88 -0
- data/lib/neo4j-cypher/version.rb +5 -0
- data/lib/neo4j-cypher/where.rb +16 -0
- data/lib/neo4j-cypher/with.rb +41 -0
- data/neo4j-cypher.gemspec +26 -0
- metadata +81 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module Cypher
|
3
|
+
|
4
|
+
# Generates a Cypher string from a Ruby DSL
|
5
|
+
# The result returned by #to_s and the last Cypher return columns can be found #return_names.
|
6
|
+
# The cypher query will only be generated once - in the constructor.
|
7
|
+
class Result
|
8
|
+
|
9
|
+
def initialize(*args, &dsl_block)
|
10
|
+
@root = Neo4j::Cypher::RootClause.new
|
11
|
+
eval_context = @root.eval_context
|
12
|
+
to_dsl_args = args.map do |a|
|
13
|
+
case
|
14
|
+
when a.is_a?(Array) && a.first.respond_to?(:_java_node)
|
15
|
+
eval_context.node(*a)
|
16
|
+
when a.is_a?(Array) && a.first.respond_to?(:_java_rel)
|
17
|
+
eval_context.rel(*a)
|
18
|
+
when a.respond_to?(:_java_node)
|
19
|
+
eval_context.node(a)
|
20
|
+
when a.respond_to?(:_java_rel)
|
21
|
+
eval_context.rel(a)
|
22
|
+
else
|
23
|
+
raise "Illegal argument #{a.class}"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
@root.execute(to_dsl_args, &dsl_block)
|
28
|
+
@result = @root.return_value
|
29
|
+
end
|
30
|
+
|
31
|
+
def return_names
|
32
|
+
@root.return_names
|
33
|
+
end
|
34
|
+
|
35
|
+
# Converts the DSL query to a cypher String which can be executed by cypher query engine.
|
36
|
+
def to_s
|
37
|
+
@result
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module Cypher
|
3
|
+
# Wraps the Cypher query result.
|
4
|
+
# Loads the node and relationships wrapper if possible and use symbol as column keys.
|
5
|
+
# This is typically used in the native neo4j bindings since result does is not a Ruby enumerable with symbols as keys.
|
6
|
+
# @notice The result is a once forward read only Enumerable, work if you need to read the result twice - use #to_a
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# result = Neo4j.query(@a, @b){|a,b| node(a,b).as(:n)}
|
10
|
+
# r = @query_result.to_a # can only loop once
|
11
|
+
# r.size.should == 2
|
12
|
+
# r.first.should include(:n)
|
13
|
+
# r[0][:n].neo_id.should == @a.neo_id
|
14
|
+
# r[1][:n].neo_id.should == @b.neo_id
|
15
|
+
class ResultWrapper
|
16
|
+
include Enumerable
|
17
|
+
|
18
|
+
# @return the original result from the Neo4j Cypher Engine, once forward read only !
|
19
|
+
attr_reader :source
|
20
|
+
|
21
|
+
def initialize(source)
|
22
|
+
@source = source
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Array<Symbol>] the columns in the query result
|
26
|
+
def columns
|
27
|
+
@source.columns.map { |x| x.to_sym }
|
28
|
+
end
|
29
|
+
|
30
|
+
# for the Enumerable contract
|
31
|
+
def each
|
32
|
+
@source.each { |row| yield map(row) }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Maps each row so that we can use symbols for column names.
|
36
|
+
# @private
|
37
|
+
def map(row)
|
38
|
+
out = {} # move to a real hash!
|
39
|
+
row.each do |key, value|
|
40
|
+
out[key.to_sym] = value.respond_to?(:wrapper) ? value.wrapper : value
|
41
|
+
end
|
42
|
+
out
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module Cypher
|
3
|
+
|
4
|
+
|
5
|
+
# Can be used to skip result from a return clause
|
6
|
+
class Skip
|
7
|
+
include Clause
|
8
|
+
|
9
|
+
def initialize(clause_list, value, context)
|
10
|
+
super(clause_list, :skip, context)
|
11
|
+
@value = value
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_cypher
|
15
|
+
@value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Can be used to limit result from a return clause
|
20
|
+
class Limit
|
21
|
+
include Clause
|
22
|
+
|
23
|
+
def initialize(clause_list, value, context)
|
24
|
+
super(clause_list, :limit, context)
|
25
|
+
@value = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_cypher
|
29
|
+
@value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class OrderBy
|
34
|
+
include Clause
|
35
|
+
|
36
|
+
def initialize(clause_list, context)
|
37
|
+
super(clause_list, :order_by, context)
|
38
|
+
@orders = []
|
39
|
+
end
|
40
|
+
|
41
|
+
def asc(props)
|
42
|
+
@orders << [:asc, props.map(&:clause)]
|
43
|
+
end
|
44
|
+
|
45
|
+
def desc(props)
|
46
|
+
@orders << [:desc, props.map(&:clause)]
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_cypher
|
50
|
+
@orders.map do |pair|
|
51
|
+
if pair[0] == :asc
|
52
|
+
pair[1].map(&:return_value).join(', ')
|
53
|
+
else
|
54
|
+
pair[1].map(&:return_value).join(', ') + " DESC"
|
55
|
+
end
|
56
|
+
end.join(', ')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Used for returning several values, e.g. RETURN x,y,z
|
61
|
+
class Return
|
62
|
+
include Clause
|
63
|
+
|
64
|
+
attr_reader :return_items
|
65
|
+
|
66
|
+
def initialize(clause_list, return_items, opts = {})
|
67
|
+
super(clause_list, :return, EvalContext)
|
68
|
+
@return_items = return_items.map { |ri| ri.is_a?(ReturnItem::EvalContext) ? ri.clause : ReturnItem.new(clause_list, ri) }
|
69
|
+
opts.each_pair { |k, v| self.eval_context.send(k, v) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_cypher
|
73
|
+
@return_items.map(&:return_value_with_alias).join(',')
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
class EvalContext
|
78
|
+
include Context
|
79
|
+
include ReturnOrder
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
# The return statement in the cypher query
|
85
|
+
class ReturnItem
|
86
|
+
include Clause
|
87
|
+
include Referenceable
|
88
|
+
|
89
|
+
def initialize(clause_list, name_or_ref)
|
90
|
+
super(clause_list, :return_item, EvalContext)
|
91
|
+
if name_or_ref.respond_to?(:clause)
|
92
|
+
@delegated_clause = name_or_ref.clause
|
93
|
+
@delegated_clause.referenced!
|
94
|
+
as_alias(@delegated_clause.var_name) if @delegated_clause.as_alias?
|
95
|
+
else
|
96
|
+
@return_value = name_or_ref.to_s
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def var_name
|
101
|
+
@var_name || (@delegated_clause && @delegated_clause.var_name) || @return_value.to_sym
|
102
|
+
end
|
103
|
+
|
104
|
+
def return_value_with_alias
|
105
|
+
as_alias? ? "#{return_value} as #{var_name}" : return_value
|
106
|
+
end
|
107
|
+
|
108
|
+
def return_value
|
109
|
+
@delegated_clause ? @delegated_clause.return_value : @return_value
|
110
|
+
end
|
111
|
+
|
112
|
+
class EvalContext
|
113
|
+
include Context
|
114
|
+
include Alias
|
115
|
+
include ReturnOrder
|
116
|
+
include Aggregate
|
117
|
+
include Comparable
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module Cypher
|
3
|
+
class RootClause
|
4
|
+
include Clause
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
super(ClauseList.new, :root, EvalContext)
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute(args, &cypher_dsl)
|
11
|
+
result = eval_context.instance_exec(*args, &cypher_dsl)
|
12
|
+
|
13
|
+
if ![Array, Symbol].include?(result.class)
|
14
|
+
return if clause_list.include?(:return)
|
15
|
+
return if clause_list.include?(:with)
|
16
|
+
return if clause_list.include?(:delete)
|
17
|
+
end
|
18
|
+
create_returns(result)
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_returns(last_result)
|
22
|
+
if last_result.is_a?(Array)
|
23
|
+
eval_context.ret(*last_result)
|
24
|
+
elsif last_result.nil?
|
25
|
+
eval_context.ret(clause_list.last.eval_context) unless clause_list.empty?
|
26
|
+
else
|
27
|
+
eval_context.ret(last_result)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def return_value
|
32
|
+
clause_list.to_cypher
|
33
|
+
end
|
34
|
+
|
35
|
+
def return_names
|
36
|
+
ret = clause_list.last
|
37
|
+
ret.respond_to?(:return_items) ? ret.return_items.map { |ri| ri.var_name.to_sym } : []
|
38
|
+
end
|
39
|
+
|
40
|
+
class EvalContext
|
41
|
+
include Context
|
42
|
+
include MathFunctions
|
43
|
+
include Returnable
|
44
|
+
|
45
|
+
# Does nothing, just for making the DSL easier to read (maybe).
|
46
|
+
# @return self
|
47
|
+
def match(*, &match_dsl)
|
48
|
+
instance_eval(&match_dsl) if match_dsl
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def match_not(&match_dsl)
|
53
|
+
instance_eval(&match_dsl).not
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
# Does nothing, just for making the DSL easier to read (maybe)
|
58
|
+
# @return self
|
59
|
+
def start(*)
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def where(w=nil)
|
64
|
+
Where.new(clause_list, w) if w.is_a?(String)
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
# Specifies a start node by performing a lucene query.
|
69
|
+
# @param [Class] index_class a class responsible for an index
|
70
|
+
# @param [String] q the lucene query
|
71
|
+
# @param [Symbol] index_type the type of index
|
72
|
+
# @return [NodeQuery]
|
73
|
+
def query(index_class, q, index_type = :exact)
|
74
|
+
NodeQuery.new(clause_list, index_class, q, index_type).eval_context
|
75
|
+
end
|
76
|
+
|
77
|
+
# Specifies a start node by performing a lucene query.
|
78
|
+
# @param [Class] index_class a class responsible for an index
|
79
|
+
# @param [String, Symbol] key the key we ask for
|
80
|
+
# @param [String, Symbol] value the value of the key we ask for
|
81
|
+
# @return [NodeLookup]
|
82
|
+
def lookup(index_class, key, value)
|
83
|
+
NodeLookup.new(clause_list, index_class, key, value).eval_context
|
84
|
+
end
|
85
|
+
|
86
|
+
# Creates a node variable.
|
87
|
+
# It will create different variables depending on the type of the first element in the nodes argument.
|
88
|
+
# * Fixnum - it will be be used as neo_id for start node(s) (StartNode)
|
89
|
+
# * Symbol - it will create an unbound node variable with the same name as the symbol (NodeVar#as)
|
90
|
+
# * empty array - it will create an unbound node variable (NodeVar)
|
91
|
+
#
|
92
|
+
# @param [Fixnum,Symbol,String] nodes the id of the nodes we want to start from
|
93
|
+
# @return [StartNode, NodeVar]
|
94
|
+
def node(*nodes)
|
95
|
+
if nodes.first.is_a?(Symbol)
|
96
|
+
NodeVar.new(clause_list).eval_context.as(nodes.first)
|
97
|
+
elsif !nodes.empty?
|
98
|
+
StartNode.new(clause_list, nodes).eval_context
|
99
|
+
else
|
100
|
+
NodeVar.new(clause_list).eval_context
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Similar to #node
|
105
|
+
# @return [StartRel, RelVar]
|
106
|
+
def rel(*rels)
|
107
|
+
if rels.first.is_a?(Fixnum) || rels.first.respond_to?(:neo_id)
|
108
|
+
StartRel.new(clause_list, rels).eval_context
|
109
|
+
elsif rels.first.is_a?(Symbol)
|
110
|
+
RelVar.new(clause_list, ":`#{rels.first}`", rels[1]).eval_context
|
111
|
+
elsif rels.first.is_a?(String)
|
112
|
+
RelVar.new(clause_list, rels.first, rels[1]).eval_context
|
113
|
+
elsif rels.empty?
|
114
|
+
RelVar.new(clause_list, '?').eval_context
|
115
|
+
else
|
116
|
+
raise "Unknown arg #{rels.inspect}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def rel?(*rels)
|
121
|
+
rel(*rels).clause.optionally!.eval_context
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
def shortest_path(&block)
|
126
|
+
match = instance_eval(&block)
|
127
|
+
match.shortest_path
|
128
|
+
end
|
129
|
+
|
130
|
+
def shortest_paths(&block)
|
131
|
+
match = instance_eval(&block)
|
132
|
+
match.shortest_paths
|
133
|
+
end
|
134
|
+
|
135
|
+
# @param [Symbol,nil] variable the entity we want to count or wildcard (*)
|
136
|
+
# @return [ReturnItem] a counter return clause
|
137
|
+
def count(variable='*')
|
138
|
+
operand = variable.respond_to?(:clause) ? variable.clause.var_name : variable
|
139
|
+
ReturnItem.new(clause_list, "count(#{operand})").eval_context
|
140
|
+
end
|
141
|
+
|
142
|
+
def coalesce(*args)
|
143
|
+
s = args.map { |x| x.clause.return_value }.join(", ")
|
144
|
+
ReturnItem.new(clause_list, "coalesce(#{s})").eval_context
|
145
|
+
end
|
146
|
+
|
147
|
+
def nodes(*args)
|
148
|
+
s = args.map { |x| x.clause.referenced!; x.clause.var_name }.join(", ")
|
149
|
+
ReturnItem.new(clause_list, "nodes(#{s})").eval_context
|
150
|
+
end
|
151
|
+
|
152
|
+
def rels(*args)
|
153
|
+
s = args.map { |x| x.clause.referenced!; x.clause.var_name }.join(", ")
|
154
|
+
ReturnItem.new(clause_list, "relationships(#{s})").eval_context
|
155
|
+
end
|
156
|
+
|
157
|
+
def create_path(*args, &block)
|
158
|
+
CreatePath.new(clause_list, *args, &block).eval_context
|
159
|
+
end
|
160
|
+
|
161
|
+
def create_unique_path(*args, &block)
|
162
|
+
CreatePath.new(clause_list, *args, &block).unique!.eval_context
|
163
|
+
end
|
164
|
+
|
165
|
+
def with(*args, &block)
|
166
|
+
With.new(clause_list, :where, *args, &block).eval_context
|
167
|
+
end
|
168
|
+
|
169
|
+
def with_match(*args, &block)
|
170
|
+
With.new(clause_list, :match, *args, &block).eval_context
|
171
|
+
end
|
172
|
+
|
173
|
+
def distinct(node_or_name)
|
174
|
+
operand = node_or_name.respond_to?(:clause) ? node_or_name.clause.var_name : node_or_name
|
175
|
+
ReturnItem.new(clause_list, "distinct(#{operand})").eval_context
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module Cypher
|
3
|
+
class Start
|
4
|
+
include Clause
|
5
|
+
include Referenceable
|
6
|
+
|
7
|
+
attr_accessor :entities # TODO CHECK if needed
|
8
|
+
|
9
|
+
def initialize(clause_list)
|
10
|
+
super(clause_list, :start, EvalContext)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize_entities(entities)
|
14
|
+
@entities = entities.map { |n| n.respond_to?(:neo_id) ? n.neo_id : n }
|
15
|
+
end
|
16
|
+
|
17
|
+
class EvalContext
|
18
|
+
include Context
|
19
|
+
include Variable
|
20
|
+
include Matchable
|
21
|
+
include Returnable
|
22
|
+
include Sortable
|
23
|
+
include Aggregate
|
24
|
+
include Alias
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
# Can be created from a <tt>node</tt> dsl method.
|
30
|
+
class StartNode < Start
|
31
|
+
|
32
|
+
def initialize(clause_list, nodes)
|
33
|
+
super(clause_list)
|
34
|
+
initialize_entities(nodes)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_cypher
|
38
|
+
"#{var_name}=node(#{entities.join(',')})"
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Can be created from a <tt>rel</tt> dsl method.
|
45
|
+
class StartRel < Start
|
46
|
+
def initialize(clause_list, rels)
|
47
|
+
super(clause_list)
|
48
|
+
initialize_entities(rels)
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_cypher
|
52
|
+
"#{var_name}=relationship(#{entities.join(',')})"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class NodeQuery < Start
|
57
|
+
attr_reader :index_name, :query
|
58
|
+
|
59
|
+
def initialize(clause_list, index_class, query, index_type)
|
60
|
+
super(clause_list)
|
61
|
+
@index_name = index_class.index_name_for_type(index_type)
|
62
|
+
@query = query
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_cypher
|
66
|
+
"#{var_name}=node:#{index_name}(#{query})"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class NodeLookup < Start
|
71
|
+
attr_reader :index_name, :query
|
72
|
+
|
73
|
+
def initialize(clause_list, index_class, key, value)
|
74
|
+
super(clause_list)
|
75
|
+
index_type = index_class.index_type(key.to_s)
|
76
|
+
raise "No index on #{index_class} property #{key}" unless index_type
|
77
|
+
@index_name = index_class.index_name_for_type(index_type)
|
78
|
+
@query = %Q[#{key}="#{value}"]
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_cypher
|
82
|
+
%Q[#{var_name}=node:#{index_name}(#{query})]
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|