neo4j-core 3.1.1 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +7 -12
- data/README.md +7 -7
- data/lib/neo4j-core.rb +3 -2
- data/lib/neo4j-core/active_entity.rb +8 -10
- data/lib/neo4j-core/cypher_translator.rb +61 -59
- data/lib/neo4j-core/hash_with_indifferent_access.rb +31 -22
- data/lib/neo4j-core/helpers.rb +15 -17
- data/lib/neo4j-core/label.rb +7 -6
- data/lib/neo4j-core/query.rb +271 -268
- data/lib/neo4j-core/query_clauses.rb +371 -355
- data/lib/neo4j-core/query_find_in_batches.rb +26 -26
- data/lib/neo4j-core/version.rb +1 -1
- data/lib/neo4j-embedded.rb +2 -2
- data/lib/neo4j-embedded/cypher_response.rb +40 -41
- data/lib/neo4j-embedded/embedded_database.rb +21 -22
- data/lib/neo4j-embedded/embedded_ha_session.rb +13 -11
- data/lib/neo4j-embedded/embedded_impermanent_session.rb +9 -8
- data/lib/neo4j-embedded/embedded_label.rb +64 -70
- data/lib/neo4j-embedded/embedded_node.rb +68 -73
- data/lib/neo4j-embedded/embedded_relationship.rb +6 -13
- data/lib/neo4j-embedded/embedded_session.rb +128 -132
- data/lib/neo4j-embedded/embedded_transaction.rb +34 -33
- data/lib/neo4j-embedded/property.rb +84 -77
- data/lib/neo4j-embedded/to_java.rb +24 -23
- data/lib/neo4j-server.rb +1 -1
- data/lib/neo4j-server/cypher_authentication.rb +105 -103
- data/lib/neo4j-server/cypher_label.rb +25 -23
- data/lib/neo4j-server/cypher_node.rb +180 -177
- data/lib/neo4j-server/cypher_node_uncommited.rb +11 -9
- data/lib/neo4j-server/cypher_relationship.rb +101 -102
- data/lib/neo4j-server/cypher_response.rb +171 -170
- data/lib/neo4j-server/cypher_session.rb +209 -205
- data/lib/neo4j-server/cypher_transaction.rb +66 -48
- data/lib/neo4j-server/resource.rb +17 -22
- data/lib/neo4j/entity_equality.rb +3 -4
- data/lib/neo4j/label.rb +13 -16
- data/lib/neo4j/node.rb +30 -34
- data/lib/neo4j/property_container.rb +3 -3
- data/lib/neo4j/property_validator.rb +4 -5
- data/lib/neo4j/relationship.rb +17 -22
- data/lib/neo4j/session.rb +19 -21
- data/lib/neo4j/tasks/config_server.rb +2 -3
- data/lib/neo4j/tasks/neo4j_server.rake +82 -74
- data/lib/neo4j/transaction.rb +23 -22
- data/neo4j-core.gemspec +21 -16
- metadata +72 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e441fd4d968dfd3b6caf201fc60ce19c2698d29
|
4
|
+
data.tar.gz: d4e88b57b2ecd1a342eaa83c4c05faa1c4d3bdbe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 65292755970b43e9c31b886c1e3e52455094f7de61e44d483f88c15d9f4ec713a9ceeff2302b0918c02264ee957e08210850aa29ed3651905bcd5149165916dc
|
7
|
+
data.tar.gz: b3ef3555c5b6de4ef7527664b2133ecf0ca73de25245b41bf2f92817800e7403010155ef48d7be9f583d8380280fbb431016f7f239184a3782110ffc925845e6
|
data/Gemfile
CHANGED
@@ -4,22 +4,17 @@ gemspec
|
|
4
4
|
|
5
5
|
gem 'zip'
|
6
6
|
|
7
|
-
#gem 'neo4j-advanced', '>= 1.8.1', '< 2.0', :require => false
|
8
|
-
#gem 'neo4j-enterprise', '>= 1.8.1', '< 2.0', :require => false
|
9
|
-
|
10
|
-
gem 'coveralls', require: false
|
11
|
-
gem 'simplecov-html', require: false
|
7
|
+
# gem 'neo4j-advanced', '>= 1.8.1', '< 2.0', :require => false
|
8
|
+
# gem 'neo4j-enterprise', '>= 1.8.1', '< 2.0', :require => false
|
12
9
|
|
13
10
|
group 'development' do
|
14
|
-
gem '
|
15
|
-
gem 'simplecov'
|
16
|
-
gem 'pry'
|
17
|
-
gem 'pry-rescue'
|
18
|
-
gem 'pry-stack_explorer', platform: :ruby
|
11
|
+
gem 'guard-rspec', require: false
|
19
12
|
end
|
20
13
|
|
21
14
|
group 'test' do
|
22
|
-
gem
|
23
|
-
gem
|
15
|
+
gem 'coveralls', require: false
|
16
|
+
gem 'simplecov-html', require: false
|
17
|
+
gem 'rake', '>= 0.8.7'
|
18
|
+
gem 'rspec', '~> 3.0'
|
24
19
|
gem 'rspec-its'
|
25
20
|
end
|
data/README.md
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
# Neo4j-core v3.0 [![Code Climate](https://codeclimate.com/github/
|
1
|
+
# Neo4j-core v3.0 [![Code Climate](https://codeclimate.com/github/neo4jrb/neo4j-core.png)](https://codeclimate.com/github/neo4jrb/neo4j-core) [![Build Status](https://travis-ci.org/neo4jrb/neo4j-core.png)](https://travis-ci.org/neo4jrb/neo4j-core) [![Coverage Status](https://coveralls.io/repos/neo4jrb/neo4j-core/badge.png?branch=master)](https://coveralls.io/r/neo4jrb/neo4j-core?branch=master) [![PullReview stats](https://www.pullreview.com/github/neo4jrb/neo4j-core/badges/master.svg?)](https://www.pullreview.com/github/neo4jrb/neo4j-core/reviews/master)
|
2
2
|
|
3
3
|
A simple Ruby wrapper around the Neo4j graph database that works with the server and embedded Neo4j API. This gem can be used both from JRuby and normal MRI.
|
4
4
|
It can be used standalone without the neo4j gem.
|
5
5
|
|
6
6
|
## Documentation
|
7
7
|
|
8
|
-
* [3.0 Documentation](https://github.com/
|
9
|
-
* [2.x Documentation](https://github.com/
|
8
|
+
* [3.0 Documentation](https://github.com/neo4jrb/neo4j-core/wiki)
|
9
|
+
* [2.x Documentation](https://github.com/neo4jrb/neo4j-core/tree/v2.x)
|
10
10
|
|
11
11
|
|
12
12
|
## Support
|
@@ -16,20 +16,20 @@ It can be used standalone without the neo4j gem.
|
|
16
16
|
|
17
17
|
## Developers
|
18
18
|
|
19
|
-
* [Andreas Ronge](https://github.com/
|
19
|
+
* [Andreas Ronge](https://github.com/neo4jrb)
|
20
20
|
* [Brian Underwood](https://github.com/cheerfulstoic)
|
21
21
|
* [Chris Grigg](https://github.com/subvertallchris)
|
22
22
|
|
23
23
|
|
24
24
|
## Contributing
|
25
25
|
|
26
|
-
Pull request with high test coverage and good [code climate](https://codeclimate.com/github/
|
26
|
+
Pull request with high test coverage and good [code climate](https://codeclimate.com/github/neo4jrb/neo4j-core) values will be accepted faster.
|
27
27
|
Notice, only JRuby can run all the tests (embedded and server db). To run tests with coverage: `rake coverage`.
|
28
28
|
|
29
29
|
## License
|
30
|
-
* Neo4j.rb - MIT, see the LICENSE file http://github.com/
|
30
|
+
* Neo4j.rb - MIT, see the LICENSE file http://github.com/neo4jrb/neo4j-core/tree/master/LICENSE.
|
31
31
|
* Lucene - Apache, see http://lucene.apache.org/java/docs/features.html
|
32
|
-
*
|
32
|
+
* Neo4j - Dual free software/commercial license, see http://neo4j.org/
|
33
33
|
|
34
34
|
Notice there are different license for the neo4j-community, neo4j-advanced and neo4j-enterprise jar gems.
|
35
35
|
Only the neo4j-community gem is by default required.
|
data/lib/neo4j-core.rb
CHANGED
@@ -1,13 +1,11 @@
|
|
1
|
-
module Neo4j
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
1
|
+
module Neo4j
|
2
|
+
module Core
|
3
|
+
# A module to make Neo4j::Node and Neo4j::Relationship work better together with neo4j.rb's Neo4j::ActiveNode and Neo4j::ActiveRel
|
4
|
+
module ActiveEntity
|
5
|
+
# @return true
|
6
|
+
def persisted?
|
7
|
+
true
|
8
|
+
end
|
8
9
|
end
|
9
|
-
|
10
10
|
end
|
11
11
|
end
|
12
|
-
|
13
|
-
|
@@ -1,74 +1,76 @@
|
|
1
|
-
module Neo4j
|
2
|
-
module
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
1
|
+
module Neo4j
|
2
|
+
module Core
|
3
|
+
module CypherTranslator
|
4
|
+
# Cypher Helper
|
5
|
+
def escape_value(value)
|
6
|
+
if value.is_a?(String) || value.is_a?(Symbol)
|
7
|
+
"'#{escape_quotes(sanitize_escape_sequences(value.to_s))}'"
|
8
|
+
else
|
9
|
+
value
|
10
|
+
end
|
9
11
|
end
|
10
|
-
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
# Like escape_value but it does not wrap the value in quotes
|
14
|
+
def create_escape_value(value)
|
15
|
+
if value.is_a?(String) || value.is_a?(Symbol)
|
16
|
+
"#{sanitize_escape_sequences(value.to_s)}"
|
17
|
+
else
|
18
|
+
value
|
19
|
+
end
|
18
20
|
end
|
19
|
-
end
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
22
|
+
# Only following escape sequence characters are allowed in Cypher:
|
23
|
+
#
|
24
|
+
# \t Tab
|
25
|
+
# \b Backspace
|
26
|
+
# \n Newline
|
27
|
+
# \r Carriage return
|
28
|
+
# \f Form feed
|
29
|
+
# \' Single quote
|
30
|
+
# \" Double quote
|
31
|
+
# \\ Backslash
|
32
|
+
#
|
33
|
+
# From:
|
34
|
+
# http://docs.neo4j.org/chunked/stable/cypher-expressions.html#_note_on_string_literals
|
35
|
+
SANITIZE_ESCAPED_REGEXP = /(?<!\\)\\(\\\\)*(?![futbnr'"\\])/
|
36
|
+
EMPTY_PROPS = ''
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
def sanitize_escape_sequences(s)
|
39
|
+
s.gsub SANITIZE_ESCAPED_REGEXP, EMPTY_PROPS
|
40
|
+
end
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
42
|
+
def escape_quotes(s)
|
43
|
+
s.gsub("'", %q(\\\'))
|
44
|
+
end
|
44
45
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
46
|
+
# Cypher Helper
|
47
|
+
def cypher_prop_list(props)
|
48
|
+
return nil unless props
|
49
|
+
props.reject! { |_, v| v.nil? }
|
50
|
+
{props: props.each { |k, v| props[k] = create_escape_value(v) }}
|
51
|
+
end
|
51
52
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
53
|
+
# Stolen from keymaker
|
54
|
+
# https://github.com/therubymug/keymaker/blob/master/lib/keymaker/parsers/cypher_response_parser.rb
|
55
|
+
def self.translate_response(response_body, result)
|
56
|
+
Hashie::Mash.new(Hash[sanitized_column_names(response_body).zip(result)])
|
57
|
+
end
|
57
58
|
|
58
|
-
|
59
|
-
|
60
|
-
|
59
|
+
def self.sanitized_column_names(response_body)
|
60
|
+
response_body.columns.map { |column| column[/[^\.]+$/] }
|
61
|
+
end
|
61
62
|
|
62
|
-
|
63
|
-
|
64
|
-
|
63
|
+
def cypher_string(labels, props)
|
64
|
+
"CREATE (n#{label_string(labels)} #{prop_identifier(props)}) RETURN ID(n)"
|
65
|
+
end
|
65
66
|
|
66
|
-
|
67
|
-
|
68
|
-
|
67
|
+
def label_string(labels)
|
68
|
+
labels.empty? ? '' : ":#{labels.map { |k| "`#{k}`" }.join(':')}"
|
69
|
+
end
|
69
70
|
|
70
|
-
|
71
|
-
|
71
|
+
def prop_identifier(props)
|
72
|
+
'{props}' unless props.nil?
|
73
|
+
end
|
72
74
|
end
|
73
75
|
end
|
74
76
|
end
|
@@ -3,7 +3,6 @@ module Neo4j
|
|
3
3
|
# Stolen from http://as.rubyonrails.org/classes/HashWithIndifferentAccess.html
|
4
4
|
# We don't want to depend on active support
|
5
5
|
class HashWithIndifferentAccess < Hash
|
6
|
-
|
7
6
|
# Always returns true, so that <tt>Array#extract_options!</tt> finds members of this class.
|
8
7
|
def extractable_options?
|
9
8
|
true
|
@@ -103,7 +102,7 @@ module Neo4j
|
|
103
102
|
# hash.values_at("a", "b") # => ["x", "y"]
|
104
103
|
#
|
105
104
|
def values_at(*indices)
|
106
|
-
indices.collect {|key| self[convert_key(key)]}
|
105
|
+
indices.collect { |key| self[convert_key(key)] }
|
107
106
|
end
|
108
107
|
|
109
108
|
# Returns an exact copy of the hash.
|
@@ -116,7 +115,7 @@ module Neo4j
|
|
116
115
|
# Merges the instantized and the specified hashes together, giving precedence to the values from the second hash.
|
117
116
|
# Does not overwrite the existing hash.
|
118
117
|
def merge(hash)
|
119
|
-
|
118
|
+
dup.update(hash)
|
120
119
|
end
|
121
120
|
|
122
121
|
# Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
|
@@ -126,7 +125,7 @@ module Neo4j
|
|
126
125
|
end
|
127
126
|
|
128
127
|
def reverse_merge!(other_hash)
|
129
|
-
replace(reverse_merge(
|
128
|
+
replace(reverse_merge(other_hash))
|
130
129
|
end
|
131
130
|
|
132
131
|
# Removes a specified key from the hash.
|
@@ -134,11 +133,21 @@ module Neo4j
|
|
134
133
|
super(convert_key(key))
|
135
134
|
end
|
136
135
|
|
137
|
-
def stringify_keys
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
def
|
136
|
+
def stringify_keys!
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
def stringify_keys
|
141
|
+
dup
|
142
|
+
end
|
143
|
+
# undef :symbolize_keys!
|
144
|
+
def symbolize_keys
|
145
|
+
to_hash.symbolize_keys
|
146
|
+
end
|
147
|
+
|
148
|
+
def to_options!
|
149
|
+
self
|
150
|
+
end
|
142
151
|
|
143
152
|
# Convert to a Hash with String keys.
|
144
153
|
def to_hash
|
@@ -146,20 +155,20 @@ module Neo4j
|
|
146
155
|
end
|
147
156
|
|
148
157
|
protected
|
149
|
-
def convert_key(key)
|
150
|
-
key.kind_of?(Symbol) ? key.to_s : key
|
151
|
-
end
|
152
158
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
159
|
+
def convert_key(key)
|
160
|
+
key.is_a?(Symbol) ? key.to_s : key
|
161
|
+
end
|
162
|
+
|
163
|
+
def convert_value(value)
|
164
|
+
if value.is_a? Hash
|
165
|
+
value # .nested_under_indifferent_access
|
166
|
+
elsif value.is_a?(Array)
|
167
|
+
value.dup.replace(value.map { |e| convert_value(e) })
|
168
|
+
else
|
169
|
+
value
|
161
170
|
end
|
171
|
+
end
|
162
172
|
end
|
163
|
-
|
164
173
|
end
|
165
|
-
end
|
174
|
+
end
|
data/lib/neo4j-core/helpers.rb
CHANGED
@@ -1,24 +1,22 @@
|
|
1
|
-
module Neo4j
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
1
|
+
module Neo4j
|
2
|
+
module Core
|
3
|
+
module ArgumentHelper
|
4
|
+
def self.session(args)
|
5
|
+
args.last.is_a?(Neo4j::Session) ? args.pop : Neo4j::Session.current!
|
6
|
+
end
|
7
7
|
end
|
8
|
-
end
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
9
|
+
module TxMethods
|
10
|
+
def tx_methods(*methods)
|
11
|
+
methods.each do |method|
|
12
|
+
tx_method = "#{method}_in_tx"
|
13
|
+
send(:alias_method, tx_method, method)
|
14
|
+
send(:define_method, method) do |*args, &block|
|
15
|
+
session = ArgumentHelper.session(args)
|
16
|
+
Neo4j::Transaction.run(session.auto_commit?) { send(tx_method, *args, &block) }
|
17
|
+
end
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
22
|
-
|
23
|
-
|
24
22
|
end
|
data/lib/neo4j-core/label.rb
CHANGED
data/lib/neo4j-core/query.rb
CHANGED
@@ -1,344 +1,347 @@
|
|
1
1
|
require 'neo4j-core/query_clauses'
|
2
2
|
require 'active_support/notifications'
|
3
3
|
|
4
|
-
module Neo4j
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
# @method start *args
|
26
|
-
# START clause
|
27
|
-
# @return [Query]
|
28
|
-
|
29
|
-
# @method match *args
|
30
|
-
# MATCH clause
|
31
|
-
# @return [Query]
|
4
|
+
module Neo4j
|
5
|
+
module Core
|
6
|
+
# Allows for generation of cypher queries via ruby method calls (inspired by ActiveRecord / arel syntax)
|
7
|
+
#
|
8
|
+
# Can be used to express cypher queries in ruby nicely, or to more easily generate queries programatically.
|
9
|
+
#
|
10
|
+
# Also, queries can be passed around an application to progressively build a query across different concerns
|
11
|
+
#
|
12
|
+
# See also the following link for full cypher language documentation:
|
13
|
+
# http://docs.neo4j.org/chunked/milestone/cypher-query-lang.html
|
14
|
+
class Query
|
15
|
+
include Neo4j::Core::QueryClauses
|
16
|
+
include Neo4j::Core::QueryFindInBatches
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
@session = options[:session] || Neo4j::Session.current
|
20
|
+
|
21
|
+
@options = options
|
22
|
+
@clauses = []
|
23
|
+
@_params = {}
|
24
|
+
end
|
32
25
|
|
33
|
-
|
34
|
-
|
35
|
-
|
26
|
+
# @method start *args
|
27
|
+
# START clause
|
28
|
+
# @return [Query]
|
36
29
|
|
37
|
-
|
38
|
-
|
39
|
-
|
30
|
+
# @method match *args
|
31
|
+
# MATCH clause
|
32
|
+
# @return [Query]
|
40
33
|
|
41
|
-
|
42
|
-
|
43
|
-
|
34
|
+
# @method optional_match *args
|
35
|
+
# OPTIONAL MATCH clause
|
36
|
+
# @return [Query]
|
44
37
|
|
45
|
-
|
46
|
-
|
47
|
-
|
38
|
+
# @method using *args
|
39
|
+
# USING clause
|
40
|
+
# @return [Query]
|
48
41
|
|
49
|
-
|
50
|
-
|
51
|
-
|
42
|
+
# @method where *args
|
43
|
+
# WHERE clause
|
44
|
+
# @return [Query]
|
52
45
|
|
53
|
-
|
54
|
-
|
55
|
-
|
46
|
+
# @method with *args
|
47
|
+
# WITH clause
|
48
|
+
# @return [Query]
|
56
49
|
|
57
|
-
|
58
|
-
|
59
|
-
|
50
|
+
# @method order *args
|
51
|
+
# ORDER BY clause
|
52
|
+
# @return [Query]
|
60
53
|
|
61
|
-
|
62
|
-
|
63
|
-
|
54
|
+
# @method limit *args
|
55
|
+
# LIMIT clause
|
56
|
+
# @return [Query]
|
64
57
|
|
65
|
-
|
66
|
-
|
67
|
-
|
58
|
+
# @method skip *args
|
59
|
+
# SKIP clause
|
60
|
+
# @return [Query]
|
68
61
|
|
69
|
-
|
70
|
-
|
71
|
-
|
62
|
+
# @method set *args
|
63
|
+
# SET clause
|
64
|
+
# @return [Query]
|
72
65
|
|
73
|
-
|
74
|
-
|
75
|
-
|
66
|
+
# @method remove *args
|
67
|
+
# REMOVE clause
|
68
|
+
# @return [Query]
|
76
69
|
|
77
|
-
|
78
|
-
|
79
|
-
|
70
|
+
# @method unwind *args
|
71
|
+
# UNWIND clause
|
72
|
+
# @return [Query]
|
80
73
|
|
81
|
-
|
82
|
-
|
83
|
-
|
74
|
+
# @method return *args
|
75
|
+
# RETURN clause
|
76
|
+
# @return [Query]
|
84
77
|
|
85
|
-
|
86
|
-
|
87
|
-
|
78
|
+
# @method create *args
|
79
|
+
# CREATE clause
|
80
|
+
# @return [Query]
|
88
81
|
|
89
|
-
|
90
|
-
|
91
|
-
|
82
|
+
# @method create_unique *args
|
83
|
+
# CREATE UNIQUE clause
|
84
|
+
# @return [Query]
|
92
85
|
|
93
|
-
|
94
|
-
|
95
|
-
|
86
|
+
# @method merge *args
|
87
|
+
# MERGE clause
|
88
|
+
# @return [Query]
|
96
89
|
|
97
|
-
|
98
|
-
|
99
|
-
|
90
|
+
# @method on_create_set *args
|
91
|
+
# ON CREATE SET clause
|
92
|
+
# @return [Query]
|
100
93
|
|
101
|
-
|
94
|
+
# @method on_match_set *args
|
95
|
+
# ON MATCH SET clause
|
96
|
+
# @return [Query]
|
102
97
|
|
103
|
-
|
98
|
+
# @method delete *args
|
99
|
+
# DELETE clause
|
100
|
+
# @return [Query]
|
104
101
|
|
105
|
-
|
106
|
-
clause_class = CLAUSES[i]
|
102
|
+
METHODS = %w(with start match optional_match using where set create create_unique merge on_create_set on_match_set remove unwind delete return order skip limit)
|
107
103
|
|
108
|
-
|
109
|
-
def #{clause}(*args)
|
110
|
-
build_deeper_query(#{clause_class}, args)
|
111
|
-
end}, __FILE__, __LINE__)
|
112
|
-
end
|
104
|
+
CLAUSES = METHODS.map { |method| const_get(method.split('_').map(&:capitalize).join + 'Clause') }
|
113
105
|
|
114
|
-
|
115
|
-
|
106
|
+
METHODS.each_with_index do |clause, i|
|
107
|
+
clause_class = CLAUSES[i]
|
116
108
|
|
117
|
-
|
118
|
-
|
119
|
-
|
109
|
+
module_eval(%{
|
110
|
+
def #{clause}(*args)
|
111
|
+
build_deeper_query(#{clause_class}, args)
|
112
|
+
end}, __FILE__, __LINE__)
|
113
|
+
end
|
120
114
|
|
121
|
-
|
122
|
-
|
123
|
-
end
|
115
|
+
alias_method :offset, :skip
|
116
|
+
alias_method :order_by, :order
|
124
117
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
# Query.new.match(n: :Person).set_props(n: {age: 19})
|
129
|
-
def set_props(*args)
|
130
|
-
build_deeper_query(SetClause, args, set_props: true)
|
131
|
-
end
|
118
|
+
# Clears out previous order clauses and allows only for those specified by args
|
119
|
+
def reorder(*args)
|
120
|
+
query = copy
|
132
121
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
# Query.new.match(q: Person).match('r:Car').break.match('(p: Person)-->q')
|
137
|
-
def break
|
138
|
-
build_deeper_query(nil)
|
139
|
-
end
|
122
|
+
query.remove_clause_class(OrderClause)
|
123
|
+
query.order(*args)
|
124
|
+
end
|
140
125
|
|
141
|
-
|
142
|
-
|
143
|
-
#
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
def params(args)
|
149
|
-
@_params = @_params.merge(args)
|
126
|
+
# Works the same as the #set method, but when given a nested array it will set properties rather than setting entire objects
|
127
|
+
# @example
|
128
|
+
# # Creates a query representing the cypher: MATCH (n:Person) SET n.age = 19
|
129
|
+
# Query.new.match(n: :Person).set_props(n: {age: 19})
|
130
|
+
def set_props(*args)
|
131
|
+
build_deeper_query(SetClause, args, set_props: true)
|
132
|
+
end
|
150
133
|
|
151
|
-
|
152
|
-
|
134
|
+
# Allows what's been built of the query so far to be frozen and the rest built anew. Can be called multiple times in a string of method calls
|
135
|
+
# @example
|
136
|
+
# # Creates a query representing the cypher: MATCH (q:Person), r:Car MATCH (p: Person)-->q
|
137
|
+
# Query.new.match(q: Person).match('r:Car').break.match('(p: Person)-->q')
|
138
|
+
def break
|
139
|
+
build_deeper_query(nil)
|
140
|
+
end
|
153
141
|
|
154
|
-
|
155
|
-
|
156
|
-
cypher
|
157
|
-
|
158
|
-
|
142
|
+
# Allows for the specification of values for params specified in query
|
143
|
+
# @example
|
144
|
+
# # Creates a query representing the cypher: MATCH (q: Person {id: {id}})
|
145
|
+
# # Calls to params don't affect the cypher query generated, but the params will be
|
146
|
+
# # Passed down when the query is made
|
147
|
+
# Query.new.match('(q: Person {id: {id}})').params(id: 12)
|
148
|
+
#
|
149
|
+
def params(args)
|
150
|
+
@_params = @_params.merge(args)
|
151
|
+
|
152
|
+
self
|
159
153
|
end
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
154
|
+
|
155
|
+
def response
|
156
|
+
return @response if @response
|
157
|
+
cypher = to_cypher
|
158
|
+
@response = ActiveSupport::Notifications.instrument('neo4j.cypher_query', context: @options[:context] || 'CYPHER', cypher: cypher, params: merge_params) do
|
159
|
+
@session._query(cypher, merge_params)
|
160
|
+
end
|
161
|
+
if !response.respond_to?(:error?) || !response.error?
|
162
|
+
response
|
163
|
+
else
|
164
|
+
response.raise_cypher_error
|
165
|
+
end
|
164
166
|
end
|
165
|
-
end
|
166
167
|
|
167
|
-
|
168
|
+
include Enumerable
|
168
169
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
170
|
+
def each
|
171
|
+
response = self.response
|
172
|
+
if response.is_a?(Neo4j::Server::CypherResponse)
|
173
|
+
response.to_node_enumeration
|
174
|
+
else
|
175
|
+
Neo4j::Embedded::ResultWrapper.new(response, to_cypher)
|
176
|
+
end.each { |object| yield object }
|
177
|
+
end
|
177
178
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
179
|
+
# @method to_a
|
180
|
+
# Class is Enumerable. Each yield is a Hash with the key matching the variable returned and the value being the value for that key from the response
|
181
|
+
# @return [Array]
|
182
|
+
# @raise [Neo4j::Server::CypherResponse::ResponseError] Raises errors from neo4j server
|
182
183
|
|
183
184
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
185
|
+
# Executes a query without returning the result
|
186
|
+
# @return [Boolean] true if successful
|
187
|
+
# @raise [Neo4j::Server::CypherResponse::ResponseError] Raises errors from neo4j server
|
188
|
+
def exec
|
189
|
+
response
|
189
190
|
|
190
|
-
|
191
|
-
|
191
|
+
true
|
192
|
+
end
|
192
193
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
194
|
+
# Return the specified columns as an array.
|
195
|
+
# If one column is specified, a one-dimensional array is returned with the values of that column
|
196
|
+
# If two columns are specified, a n-dimensional array is returned with the values of those columns
|
197
|
+
#
|
198
|
+
# @example
|
199
|
+
# Query.new.match(n: :Person).return(p: :name}.pluck(p: :name) # => Array of names
|
200
|
+
# @example
|
201
|
+
# Query.new.match(n: :Person).return(p: :name}.pluck('p, DISTINCT p.name') # => Array of [node, name] pairs
|
202
|
+
#
|
203
|
+
def pluck(*columns)
|
204
|
+
query = return_query(columns)
|
205
|
+
columns = query.response.columns
|
206
|
+
|
207
|
+
case columns.size
|
208
|
+
when 0
|
209
|
+
fail ArgumentError, 'No columns specified for Query#pluck'
|
210
|
+
when 1
|
211
|
+
column = columns[0]
|
212
|
+
query.map { |row| row[column] }
|
213
|
+
else
|
214
|
+
query.map do |row|
|
215
|
+
columns.map do |column|
|
216
|
+
row[column]
|
217
|
+
end
|
216
218
|
end
|
217
219
|
end
|
218
220
|
end
|
219
|
-
end
|
220
221
|
|
221
|
-
|
222
|
-
|
223
|
-
|
222
|
+
def return_query(columns)
|
223
|
+
query = copy
|
224
|
+
query.remove_clause_class(ReturnClause)
|
224
225
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
226
|
+
columns = columns.map do |column_definition|
|
227
|
+
if column_definition.is_a?(Hash)
|
228
|
+
column_definition.map { |k, v| "#{k}.#{v}" }
|
229
|
+
else
|
230
|
+
column_definition
|
231
|
+
end
|
232
|
+
end.flatten.map(&:to_sym)
|
232
233
|
|
233
|
-
|
234
|
-
|
234
|
+
query.return(columns)
|
235
|
+
end
|
235
236
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
237
|
+
# Returns a CYPHER query string from the object query representation
|
238
|
+
# @example
|
239
|
+
# Query.new.match(p: :Person).where(p: {age: 30}) # => "MATCH (p:Person) WHERE p.age = 30
|
240
|
+
#
|
241
|
+
# @return [String] Resulting cypher query string
|
242
|
+
def to_cypher
|
243
|
+
cypher_string = partitioned_clauses.map do |clauses|
|
244
|
+
clauses_by_class = clauses.group_by(&:class)
|
244
245
|
|
245
|
-
|
246
|
-
|
246
|
+
cypher_parts = CLAUSES.map do |clause_class|
|
247
|
+
clauses = clauses_by_class[clause_class]
|
247
248
|
|
248
|
-
|
249
|
-
|
249
|
+
clause_class.to_cypher(clauses) if clauses
|
250
|
+
end
|
250
251
|
|
251
|
-
|
252
|
-
|
253
|
-
|
252
|
+
cypher_string = cypher_parts.compact.join(' ')
|
253
|
+
cypher_string.strip
|
254
|
+
end.join ' '
|
254
255
|
|
255
|
-
|
256
|
-
|
257
|
-
|
256
|
+
cypher_string = "CYPHER #{@options[:parser]} #{cypher_string}" if @options[:parser]
|
257
|
+
cypher_string.strip
|
258
|
+
end
|
258
259
|
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
260
|
+
# Returns a CYPHER query specifying the union of the callee object's query and the argument's query
|
261
|
+
#
|
262
|
+
# @example
|
263
|
+
# # Generates cypher: MATCH (n:Person) UNION MATCH (o:Person) WHERE o.age = 10
|
264
|
+
# q = Neo4j::Core::Query.new.match(o: :Person).where(o: {age: 10})
|
265
|
+
# result = Neo4j::Core::Query.new.match(n: :Person).union_cypher(q)
|
266
|
+
#
|
267
|
+
# @param other [Query] Second half of UNION
|
268
|
+
# @param options [Hash] Specify {all: true} to use UNION ALL
|
269
|
+
# @return [String] Resulting UNION cypher query string
|
270
|
+
def union_cypher(other, options = {})
|
271
|
+
"#{to_cypher} UNION#{options[:all] ? ' ALL' : ''} #{other.to_cypher}"
|
272
|
+
end
|
272
273
|
|
273
|
-
|
274
|
-
|
274
|
+
def &(other)
|
275
|
+
fail "Sessions don't match!" if @session != other.session
|
275
276
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
277
|
+
self.class.new(session: @session).tap do |new_query|
|
278
|
+
new_query.options = options.merge(other.options)
|
279
|
+
new_query.clauses = clauses + other.clauses
|
280
|
+
end.params(other._params)
|
281
|
+
end
|
281
282
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
283
|
+
MEMOIZED_INSTANCE_VARIABLES = [:response, :merge_params]
|
284
|
+
def copy
|
285
|
+
dup.tap do |query|
|
286
|
+
MEMOIZED_INSTANCE_VARIABLES.each do |var|
|
287
|
+
query.instance_variable_set("@#{var}", nil)
|
288
|
+
end
|
287
289
|
end
|
288
290
|
end
|
289
|
-
end
|
290
291
|
|
291
|
-
|
292
|
-
attr_accessor :session, :options, :clauses, :_params
|
292
|
+
protected
|
293
293
|
|
294
|
-
|
295
|
-
@clauses += clauses
|
296
|
-
end
|
294
|
+
attr_accessor :session, :options, :clauses, :_params
|
297
295
|
|
298
|
-
|
299
|
-
|
300
|
-
clause.is_a?(clause_class)
|
296
|
+
def add_clauses(clauses)
|
297
|
+
@clauses += clauses
|
301
298
|
end
|
302
|
-
end
|
303
|
-
private
|
304
299
|
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
300
|
+
def remove_clause_class(clause_class)
|
301
|
+
@clauses = @clauses.reject do |clause|
|
302
|
+
clause.is_a?(clause_class)
|
303
|
+
end
|
309
304
|
end
|
310
|
-
end
|
311
305
|
|
312
|
-
|
313
|
-
self.copy.tap do |new_query|
|
314
|
-
new_query.add_clauses [nil]
|
315
|
-
end
|
316
|
-
end
|
306
|
+
private
|
317
307
|
|
318
|
-
|
319
|
-
|
308
|
+
def build_deeper_query(clause_class, args = {}, options = {})
|
309
|
+
copy.tap do |new_query|
|
310
|
+
new_query.add_clauses [nil] if [nil, WithClause].include?(clause_class)
|
311
|
+
new_query.add_clauses clause_class.from_args(args, options) if clause_class
|
312
|
+
end
|
313
|
+
end
|
320
314
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
else
|
325
|
-
partitioning.last << clause
|
315
|
+
def break_deeper_query
|
316
|
+
copy.tap do |new_query|
|
317
|
+
new_query.add_clauses [nil]
|
326
318
|
end
|
327
319
|
end
|
328
320
|
|
329
|
-
|
330
|
-
|
321
|
+
def partitioned_clauses
|
322
|
+
partitioning = [[]]
|
331
323
|
|
332
|
-
|
333
|
-
|
334
|
-
|
324
|
+
@clauses.each do |clause|
|
325
|
+
if clause.nil? && partitioning.last != []
|
326
|
+
partitioning << []
|
327
|
+
else
|
328
|
+
partitioning.last << clause
|
329
|
+
end
|
330
|
+
end
|
335
331
|
|
336
|
-
|
337
|
-
passthrough_classes = [String, Numeric, Array, Regexp]
|
338
|
-
params.each do |key, value|
|
339
|
-
params[key] = value.to_s if not passthrough_classes.any? {|klass| value.is_a?(klass) }
|
332
|
+
partitioning
|
340
333
|
end
|
341
|
-
end
|
342
334
|
|
335
|
+
def merge_params
|
336
|
+
@merge_params ||= @clauses.compact.inject(@_params) { |params, clause| params.merge(clause.params) }
|
337
|
+
end
|
338
|
+
|
339
|
+
def sanitize_params(params)
|
340
|
+
passthrough_classes = [String, Numeric, Array, Regexp]
|
341
|
+
params.each do |key, value|
|
342
|
+
params[key] = value.to_s if not passthrough_classes.any? { |klass| value.is_a?(klass) }
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
343
346
|
end
|
344
347
|
end
|