neo4j-core 6.1.6 → 7.0.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +4 -9
- data/README.md +48 -0
- data/lib/neo4j-core.rb +23 -0
- data/lib/neo4j-core/helpers.rb +8 -0
- data/lib/neo4j-core/query.rb +23 -20
- data/lib/neo4j-core/query_clauses.rb +18 -32
- data/lib/neo4j-core/query_find_in_batches.rb +3 -1
- data/lib/neo4j-core/version.rb +1 -1
- data/lib/neo4j-embedded/cypher_response.rb +4 -0
- data/lib/neo4j-embedded/embedded_database.rb +3 -5
- data/lib/neo4j-embedded/embedded_node.rb +4 -4
- data/lib/neo4j-embedded/embedded_session.rb +21 -10
- data/lib/neo4j-embedded/embedded_transaction.rb +4 -10
- data/lib/neo4j-server/cypher_node.rb +5 -4
- data/lib/neo4j-server/cypher_relationship.rb +3 -3
- data/lib/neo4j-server/cypher_response.rb +4 -0
- data/lib/neo4j-server/cypher_session.rb +31 -22
- data/lib/neo4j-server/cypher_transaction.rb +23 -15
- data/lib/neo4j-server/resource.rb +3 -4
- data/lib/neo4j/core/cypher_session.rb +17 -9
- data/lib/neo4j/core/cypher_session/adaptors.rb +116 -33
- data/lib/neo4j/core/cypher_session/adaptors/bolt.rb +331 -0
- data/lib/neo4j/core/cypher_session/adaptors/bolt/chunk_writer_io.rb +76 -0
- data/lib/neo4j/core/cypher_session/adaptors/bolt/pack_stream.rb +288 -0
- data/lib/neo4j/core/cypher_session/adaptors/embedded.rb +60 -29
- data/lib/neo4j/core/cypher_session/adaptors/has_uri.rb +63 -0
- data/lib/neo4j/core/cypher_session/adaptors/http.rb +123 -119
- data/lib/neo4j/core/cypher_session/responses.rb +17 -2
- data/lib/neo4j/core/cypher_session/responses/bolt.rb +135 -0
- data/lib/neo4j/core/cypher_session/responses/embedded.rb +46 -11
- data/lib/neo4j/core/cypher_session/responses/http.rb +49 -40
- data/lib/neo4j/core/cypher_session/transactions.rb +33 -0
- data/lib/neo4j/core/cypher_session/transactions/bolt.rb +36 -0
- data/lib/neo4j/core/cypher_session/transactions/embedded.rb +32 -0
- data/lib/neo4j/core/cypher_session/transactions/http.rb +52 -0
- data/lib/neo4j/core/instrumentable.rb +2 -2
- data/lib/neo4j/core/label.rb +182 -0
- data/lib/neo4j/core/node.rb +8 -3
- data/lib/neo4j/core/relationship.rb +12 -4
- data/lib/neo4j/entity_equality.rb +1 -1
- data/lib/neo4j/session.rb +4 -5
- data/lib/neo4j/transaction.rb +108 -72
- data/neo4j-core.gemspec +6 -6
- metadata +34 -40
@@ -1,14 +1,19 @@
|
|
1
1
|
require 'neo4j/core/cypher_session/adaptors'
|
2
2
|
require 'neo4j/core/cypher_session/responses/embedded'
|
3
|
+
require 'active_support/hash_with_indifferent_access'
|
3
4
|
|
4
5
|
module Neo4j
|
5
6
|
module Core
|
6
7
|
class CypherSession
|
7
8
|
module Adaptors
|
8
9
|
class Embedded < Base
|
10
|
+
attr_reader :graph_db, :path
|
11
|
+
|
9
12
|
def initialize(path, options = {})
|
10
13
|
fail 'JRuby is required for embedded mode' if RUBY_PLATFORM != 'java'
|
11
|
-
|
14
|
+
# TODO: Will this cause an error if a new path is specified?
|
15
|
+
fail ArgumentError, "Invalid path: #{path}" if File.file?(path)
|
16
|
+
FileUtils.mkdir_p(path)
|
12
17
|
|
13
18
|
@path = path
|
14
19
|
@options = options
|
@@ -23,38 +28,16 @@ module Neo4j
|
|
23
28
|
@graph_db = db_service.newGraphDatabase
|
24
29
|
end
|
25
30
|
|
26
|
-
def query_set(queries)
|
31
|
+
def query_set(transaction, queries, options = {})
|
27
32
|
# I think that this is the best way to do a batch in embedded...
|
28
33
|
# Should probably do within a transaction in case of errors...
|
34
|
+
setup_queries!(queries, transaction, options)
|
29
35
|
|
30
|
-
|
31
|
-
|
32
|
-
self.class.instrument_queries(queries)
|
33
|
-
|
34
|
-
execution_results = queries.map do |query|
|
35
|
-
engine.execute(query.cypher, indifferent_params(query))
|
36
|
-
end
|
37
|
-
|
38
|
-
Responses::Embedded.new(execution_results).results
|
39
|
-
end
|
36
|
+
self.class.instrument_transaction do
|
37
|
+
Responses::Embedded.new(execution_results(queries), wrap_level: options[:wrap_level] || @options[:wrap_level]).results
|
40
38
|
end
|
41
|
-
|
42
|
-
|
43
|
-
def start_transaction
|
44
|
-
@transaction = @graph_db.begin_tx
|
45
|
-
end
|
46
|
-
|
47
|
-
def end_transaction
|
48
|
-
if @transaction.nil?
|
49
|
-
fail 'Cannot close transaction without starting one'
|
50
|
-
end
|
51
|
-
|
52
|
-
@transaction.success
|
53
|
-
@transaction.close
|
54
|
-
end
|
55
|
-
|
56
|
-
def transaction_started?
|
57
|
-
!!@transaction
|
39
|
+
rescue Java::OrgNeo4jCypher::CypherExecutionException, Java::OrgNeo4jCypher::SyntaxException => e
|
40
|
+
raise CypherError.new_from(e.status.to_s, e.message) # , e.stack_track.to_a
|
58
41
|
end
|
59
42
|
|
60
43
|
def version
|
@@ -67,6 +50,41 @@ module Neo4j
|
|
67
50
|
end
|
68
51
|
end
|
69
52
|
|
53
|
+
def indexes(session, _label = nil)
|
54
|
+
Transaction.run(session) do
|
55
|
+
graph_db = session.adaptor.graph_db
|
56
|
+
|
57
|
+
graph_db.schema.get_indexes.map do |definition|
|
58
|
+
{properties: definition.property_keys.map(&:to_sym),
|
59
|
+
label: definition.label.to_s.to_sym}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
CONSTRAINT_TYPES = {
|
65
|
+
'UNIQUENESS' => :uniqueness
|
66
|
+
}
|
67
|
+
def constraints(session)
|
68
|
+
Transaction.run(session) do
|
69
|
+
all_labels(session).flat_map do |label|
|
70
|
+
graph_db.schema.get_constraints(label).map do |definition|
|
71
|
+
{label: label.to_s.to_sym,
|
72
|
+
properties: definition.property_keys.map(&:to_sym),
|
73
|
+
type: CONSTRAINT_TYPES[definition.get_constraint_type.to_s]}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.transaction_class
|
80
|
+
require 'neo4j/core/cypher_session/transactions/embedded'
|
81
|
+
Neo4j::Core::CypherSession::Transactions::Embedded
|
82
|
+
end
|
83
|
+
|
84
|
+
def connected?
|
85
|
+
!!@graph_db
|
86
|
+
end
|
87
|
+
|
70
88
|
instrument(:transaction, 'neo4j.core.embedded.transaction', []) do |_, start, finish, _id, _payload|
|
71
89
|
ms = (finish - start) * 1000
|
72
90
|
|
@@ -75,6 +93,16 @@ module Neo4j
|
|
75
93
|
|
76
94
|
private
|
77
95
|
|
96
|
+
def execution_results(queries)
|
97
|
+
queries.map do |query|
|
98
|
+
engine.execute(query.cypher, indifferent_params(query))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def all_labels(session)
|
103
|
+
Java::OrgNeo4jTooling::GlobalGraphOperations.at(session.adaptor.graph_db).get_all_labels.to_a
|
104
|
+
end
|
105
|
+
|
78
106
|
def indifferent_params(query)
|
79
107
|
params = query.parameters
|
80
108
|
params.each { |k, v| params[k] = HashWithIndifferentAccess.new(params[k]) if v.is_a?(Hash) && !v.respond_to?(:nested_under_indifferent_access) }
|
@@ -84,6 +112,9 @@ module Neo4j
|
|
84
112
|
def engine
|
85
113
|
@engine ||= Java::OrgNeo4jCypherJavacompat::ExecutionEngine.new(@graph_db)
|
86
114
|
end
|
115
|
+
|
116
|
+
def constraint_definitions_for(graph_db, label)
|
117
|
+
end
|
87
118
|
end
|
88
119
|
end
|
89
120
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Neo4j
|
4
|
+
module Core
|
5
|
+
class CypherSession
|
6
|
+
module Adaptors
|
7
|
+
# Containing the logic for dealing with adaptors which use URIs
|
8
|
+
module HasUri
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
attr_reader :default_uri
|
13
|
+
|
14
|
+
def default_url(default_url)
|
15
|
+
@default_uri = uri_from_url!(default_url)
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate_uri(&block)
|
19
|
+
@uri_validator = block
|
20
|
+
end
|
21
|
+
|
22
|
+
def uri_from_url!(url)
|
23
|
+
validate_url!(url)
|
24
|
+
|
25
|
+
@uri = url.nil? ? @default_uri : URI(url)
|
26
|
+
|
27
|
+
fail ArgumentError, "Invalid URL: #{url.inspect}" if uri_valid?(@uri)
|
28
|
+
|
29
|
+
@uri
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def validate_url!(url)
|
35
|
+
fail ArgumentError, "Invalid URL: #{url.inspect}" if !(url.is_a?(String) || url.nil?)
|
36
|
+
fail ArgumentError, 'No URL or default URL specified' if url.nil? && @default_uri.nil?
|
37
|
+
end
|
38
|
+
|
39
|
+
def uri_valid?(uri)
|
40
|
+
@uri_validator && !@uri_validator.call(uri)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def url
|
45
|
+
@uri.to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
def url=(url)
|
49
|
+
@uri = self.class.uri_from_url!(url)
|
50
|
+
end
|
51
|
+
|
52
|
+
included do
|
53
|
+
%w(scheme user password host port).each do |method|
|
54
|
+
define_method(method) do
|
55
|
+
(@uri && @uri.send(method)) || (self.class.default_uri && self.class.default_uri.send(method))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'neo4j/core/cypher_session/adaptors'
|
2
|
+
require 'neo4j/core/cypher_session/adaptors/has_uri'
|
2
3
|
require 'neo4j/core/cypher_session/responses/http'
|
3
4
|
|
4
5
|
# TODO: Work with `Query` objects
|
@@ -7,178 +8,181 @@ module Neo4j
|
|
7
8
|
class CypherSession
|
8
9
|
module Adaptors
|
9
10
|
class HTTP < Base
|
10
|
-
|
11
|
-
# nil, :open_requested, :open, :close_requested
|
11
|
+
attr_reader :requestor, :url
|
12
12
|
|
13
|
-
def initialize(url,
|
13
|
+
def initialize(url, options = {})
|
14
14
|
@url = url
|
15
|
-
@
|
16
|
-
@transaction_state = nil
|
15
|
+
@options = options
|
17
16
|
end
|
18
17
|
|
19
18
|
def connect
|
20
|
-
@
|
19
|
+
@requestor = Requestor.new(@url, USER_AGENT_STRING, self.class.method(:instrument_request))
|
21
20
|
end
|
22
21
|
|
23
22
|
ROW_REST = %w(row REST)
|
24
23
|
|
25
|
-
def query_set(queries)
|
26
|
-
|
24
|
+
def query_set(transaction, queries, options = {})
|
25
|
+
setup_queries!(queries, transaction)
|
27
26
|
|
28
|
-
|
29
|
-
{statement: query.cypher, parameters: query.parameters || {},
|
30
|
-
resultDataContents: ROW_REST}
|
31
|
-
end
|
32
|
-
request_data = {statements: statements_data}
|
33
|
-
|
34
|
-
# context option not implemented
|
35
|
-
self.class.instrument_queries(queries)
|
27
|
+
return unless path = transaction.query_path(options.delete(:commit))
|
36
28
|
|
37
|
-
|
29
|
+
faraday_response = @requestor.post(path, queries)
|
38
30
|
|
39
|
-
faraday_response
|
40
|
-
@connection.post(url, request_data)
|
41
|
-
end
|
31
|
+
transaction.apply_id_from_url!(faraday_response.env[:response_headers][:location])
|
42
32
|
|
43
|
-
|
44
|
-
|
45
|
-
Responses::HTTP.new(faraday_response, request_data).results
|
33
|
+
wrap_level = options[:wrap_level] || @options[:wrap_level]
|
34
|
+
Responses::HTTP.new(faraday_response, wrap_level: wrap_level).results
|
46
35
|
end
|
47
36
|
|
48
|
-
def
|
49
|
-
|
50
|
-
when :open
|
51
|
-
return
|
52
|
-
when :close_requested
|
53
|
-
fail 'Cannot start transaction when a close has been requested'
|
54
|
-
end
|
55
|
-
|
56
|
-
@transaction_state = :open_requested
|
37
|
+
def version
|
38
|
+
@version ||= @requestor.get('db/data/').body[:neo4j_version]
|
57
39
|
end
|
58
40
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
end
|
41
|
+
# Schema inspection methods
|
42
|
+
def indexes(_session)
|
43
|
+
response = @requestor.get('db/data/schema/index')
|
63
44
|
|
64
|
-
|
65
|
-
@transaction_state = :close_requested
|
66
|
-
query_set([])
|
67
|
-
@transaction_state = nil
|
68
|
-
@transaction_id = nil
|
45
|
+
list = response.body || []
|
69
46
|
|
70
|
-
|
47
|
+
list.map do |item|
|
48
|
+
{label: item[:label].to_sym,
|
49
|
+
properties: item[:property_keys].map(&:to_sym)}
|
50
|
+
end
|
71
51
|
end
|
72
52
|
|
73
|
-
|
74
|
-
|
53
|
+
CONSTRAINT_TYPES = {
|
54
|
+
'UNIQUENESS' => :uniqueness
|
55
|
+
}
|
56
|
+
def constraints(_session, _label = nil, _options = {})
|
57
|
+
response = @requestor.get('db/data/schema/constraint')
|
58
|
+
|
59
|
+
list = response.body || []
|
60
|
+
list.map do |item|
|
61
|
+
{type: CONSTRAINT_TYPES[item[:type]],
|
62
|
+
label: item[:label].to_sym,
|
63
|
+
properties: item[:property_keys].map(&:to_sym)}
|
64
|
+
end
|
75
65
|
end
|
76
66
|
|
77
|
-
def
|
78
|
-
|
67
|
+
def self.transaction_class
|
68
|
+
require 'neo4j/core/cypher_session/transactions/http'
|
69
|
+
Neo4j::Core::CypherSession::Transactions::HTTP
|
79
70
|
end
|
80
71
|
|
81
72
|
# Schema inspection methods
|
82
73
|
def indexes_for_label(label)
|
83
74
|
url = db_data_url + "schema/index/#{label}"
|
84
|
-
|
85
|
-
|
86
|
-
if response.body && response.body[0]
|
87
|
-
response.body[0][:property_keys].map(&:to_sym)
|
88
|
-
else
|
89
|
-
[]
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def uniqueness_constraints_for_label(label)
|
94
|
-
url = db_data_url + "schema/constraint/#{label}/uniqueness"
|
95
|
-
response = @connection.get(url)
|
96
|
-
|
97
|
-
if response.body && response.body[0]
|
98
|
-
response.body[0][:property_keys].map(&:to_sym)
|
99
|
-
else
|
100
|
-
[]
|
101
|
-
end
|
75
|
+
@connection.get(url)
|
102
76
|
end
|
103
77
|
|
104
|
-
|
105
|
-
instrument(:request, 'neo4j.core.http.request', %w(url body)) do |_, start, finish, _id, payload|
|
78
|
+
instrument(:request, 'neo4j.core.http.request', %w(method url body)) do |_, start, finish, _id, payload|
|
106
79
|
ms = (finish - start) * 1000
|
80
|
+
" #{ANSI::BLUE}HTTP REQUEST:#{ANSI::CLEAR} #{ANSI::YELLOW}#{ms.round}ms#{ANSI::CLEAR} #{payload[:method].upcase} #{payload[:url]} (#{payload[:body].size} bytes)"
|
81
|
+
end
|
107
82
|
|
108
|
-
|
83
|
+
def connected?
|
84
|
+
!!@requestor
|
109
85
|
end
|
110
86
|
|
111
|
-
|
87
|
+
# Basic wrapper around HTTP requests to standard Neo4j HTTP endpoints
|
88
|
+
# - Takes care of JSONifying objects passed as body (Hash/Array/Query)
|
89
|
+
# - Sets headers, including user agent string
|
90
|
+
class Requestor
|
91
|
+
include Adaptors::HasUri
|
92
|
+
default_url('http://neo4:neo4j@localhost:7474')
|
93
|
+
validate_uri { |uri| uri.is_a?(URI::HTTP) }
|
112
94
|
|
113
|
-
|
114
|
-
|
95
|
+
def initialize(url, user_agent_string, instrument_proc)
|
96
|
+
self.url = url
|
97
|
+
@user = user
|
98
|
+
@password = password
|
99
|
+
@user_agent_string = user_agent_string
|
100
|
+
@faraday = faraday_connection
|
101
|
+
@instrument_proc = instrument_proc
|
102
|
+
end
|
115
103
|
|
116
|
-
|
104
|
+
REQUEST_HEADERS = {'Accept'.to_sym => 'application/json; charset=UTF-8',
|
105
|
+
'Content-Type'.to_sym => 'application/json'}
|
106
|
+
|
107
|
+
# @method HTTP method (:get/:post/:delete/:put)
|
108
|
+
# @path Path part of URL
|
109
|
+
# @body Body for the request. If a Query or Array of Queries,
|
110
|
+
# it is automatically converted
|
111
|
+
def request(method, path, body = '', _options = {})
|
112
|
+
request_body = request_body(body)
|
113
|
+
url = url_from_path(path)
|
114
|
+
@instrument_proc.call(method, url, request_body) do
|
115
|
+
@faraday.run_request(method, url, request_body, REQUEST_HEADERS) do |req|
|
116
|
+
# Temporary
|
117
|
+
# req.options.timeout = 5
|
118
|
+
# req.options.open_timeout = 5
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
117
122
|
|
118
|
-
|
119
|
-
|
123
|
+
# Convenience method to #request(:post, ...)
|
124
|
+
def post(path, body = '', options = {})
|
125
|
+
request(:post, path, body, options)
|
126
|
+
end
|
120
127
|
|
121
|
-
|
122
|
-
path = ''
|
123
|
-
|
124
|
-
|
125
|
-
path = nil if @transaction_state == :close_requested && !@transaction_id
|
128
|
+
# Convenience method to #request(:get, ...)
|
129
|
+
def get(path, body = '', options = {})
|
130
|
+
request(:get, path, body, options)
|
131
|
+
end
|
126
132
|
|
127
|
-
|
128
|
-
end
|
133
|
+
private
|
129
134
|
|
130
|
-
|
131
|
-
|
132
|
-
|
135
|
+
def faraday_connection
|
136
|
+
require 'faraday'
|
137
|
+
require 'faraday_middleware/multi_json'
|
133
138
|
|
134
|
-
|
135
|
-
|
136
|
-
|
139
|
+
Faraday.new(url) do |c|
|
140
|
+
c.request :basic_auth, user, password
|
141
|
+
c.request :multi_json
|
137
142
|
|
138
|
-
|
139
|
-
|
140
|
-
c.request :basic_auth, user, password
|
141
|
-
c.request :multi_json
|
143
|
+
c.response :multi_json, symbolize_keys: true, content_type: 'application/json'
|
144
|
+
c.use Faraday::Adapter::NetHttpPersistent
|
142
145
|
|
143
|
-
|
144
|
-
c.use Faraday::Adapter::NetHttpPersistent
|
146
|
+
# c.response :logger, ::Logger.new(STDOUT), bodies: true
|
145
147
|
|
146
|
-
|
147
|
-
|
148
|
+
c.headers['Content-Type'] = 'application/json'
|
149
|
+
c.headers['User-Agent'] = @user_agent_string
|
150
|
+
end
|
148
151
|
end
|
149
|
-
end
|
150
152
|
|
151
|
-
|
152
|
-
|
153
|
+
def request_body(body)
|
154
|
+
return body if body.is_a?(String)
|
153
155
|
|
154
|
-
|
155
|
-
|
156
|
-
|
156
|
+
body_is_query_array = body.is_a?(Array) && body.all? { |o| o.respond_to?(:cypher) }
|
157
|
+
case body
|
158
|
+
when Hash, Array
|
159
|
+
if body_is_query_array
|
160
|
+
return {statements: body.map(&self.class.method(:statement_from_query))}
|
161
|
+
end
|
157
162
|
|
158
|
-
|
159
|
-
|
163
|
+
body
|
164
|
+
else
|
165
|
+
{statements: [self.class.statement_from_query(body)]} if body.respond_to?(:cypher)
|
166
|
+
end
|
167
|
+
end
|
160
168
|
|
161
|
-
|
162
|
-
|
163
|
-
user: 'neo4j', password: 'neo4j',
|
164
|
-
host: 'localhost', port: 7474
|
165
|
-
}
|
169
|
+
class << self
|
170
|
+
private
|
166
171
|
|
167
|
-
|
168
|
-
|
169
|
-
|
172
|
+
def statement_from_query(query)
|
173
|
+
{statement: query.cypher,
|
174
|
+
parameters: query.parameters || {},
|
175
|
+
resultDataContents: ROW_REST}
|
176
|
+
end
|
170
177
|
end
|
171
|
-
end
|
172
|
-
|
173
|
-
def user_agent_string
|
174
|
-
gem, version = if defined?(::Neo4j::ActiveNode)
|
175
|
-
['neo4j', ::Neo4j::VERSION]
|
176
|
-
else
|
177
|
-
['neo4j-core', ::Neo4j::Core::VERSION]
|
178
|
-
end
|
179
178
|
|
179
|
+
def url_base
|
180
|
+
"#{scheme}://#{host}:#{port}"
|
181
|
+
end
|
180
182
|
|
181
|
-
|
183
|
+
def url_from_path(path)
|
184
|
+
url_base + (path[0] != '/' ? '/' + path : path)
|
185
|
+
end
|
182
186
|
end
|
183
187
|
end
|
184
188
|
end
|