neo4j-core 6.1.6 → 7.0.0.alpha.1
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.
- 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
|