neo4j-cypher 1.0.0.rc1
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 +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
|