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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -9
  3. data/README.md +48 -0
  4. data/lib/neo4j-core.rb +23 -0
  5. data/lib/neo4j-core/helpers.rb +8 -0
  6. data/lib/neo4j-core/query.rb +23 -20
  7. data/lib/neo4j-core/query_clauses.rb +18 -32
  8. data/lib/neo4j-core/query_find_in_batches.rb +3 -1
  9. data/lib/neo4j-core/version.rb +1 -1
  10. data/lib/neo4j-embedded/cypher_response.rb +4 -0
  11. data/lib/neo4j-embedded/embedded_database.rb +3 -5
  12. data/lib/neo4j-embedded/embedded_node.rb +4 -4
  13. data/lib/neo4j-embedded/embedded_session.rb +21 -10
  14. data/lib/neo4j-embedded/embedded_transaction.rb +4 -10
  15. data/lib/neo4j-server/cypher_node.rb +5 -4
  16. data/lib/neo4j-server/cypher_relationship.rb +3 -3
  17. data/lib/neo4j-server/cypher_response.rb +4 -0
  18. data/lib/neo4j-server/cypher_session.rb +31 -22
  19. data/lib/neo4j-server/cypher_transaction.rb +23 -15
  20. data/lib/neo4j-server/resource.rb +3 -4
  21. data/lib/neo4j/core/cypher_session.rb +17 -9
  22. data/lib/neo4j/core/cypher_session/adaptors.rb +116 -33
  23. data/lib/neo4j/core/cypher_session/adaptors/bolt.rb +331 -0
  24. data/lib/neo4j/core/cypher_session/adaptors/bolt/chunk_writer_io.rb +76 -0
  25. data/lib/neo4j/core/cypher_session/adaptors/bolt/pack_stream.rb +288 -0
  26. data/lib/neo4j/core/cypher_session/adaptors/embedded.rb +60 -29
  27. data/lib/neo4j/core/cypher_session/adaptors/has_uri.rb +63 -0
  28. data/lib/neo4j/core/cypher_session/adaptors/http.rb +123 -119
  29. data/lib/neo4j/core/cypher_session/responses.rb +17 -2
  30. data/lib/neo4j/core/cypher_session/responses/bolt.rb +135 -0
  31. data/lib/neo4j/core/cypher_session/responses/embedded.rb +46 -11
  32. data/lib/neo4j/core/cypher_session/responses/http.rb +49 -40
  33. data/lib/neo4j/core/cypher_session/transactions.rb +33 -0
  34. data/lib/neo4j/core/cypher_session/transactions/bolt.rb +36 -0
  35. data/lib/neo4j/core/cypher_session/transactions/embedded.rb +32 -0
  36. data/lib/neo4j/core/cypher_session/transactions/http.rb +52 -0
  37. data/lib/neo4j/core/instrumentable.rb +2 -2
  38. data/lib/neo4j/core/label.rb +182 -0
  39. data/lib/neo4j/core/node.rb +8 -3
  40. data/lib/neo4j/core/relationship.rb +12 -4
  41. data/lib/neo4j/entity_equality.rb +1 -1
  42. data/lib/neo4j/session.rb +4 -5
  43. data/lib/neo4j/transaction.rb +108 -72
  44. data/neo4j-core.gemspec +6 -6
  45. 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
- fail ArgumentError, "Invalid path: #{path}" if !File.directory?(path)
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
- transaction do
31
- self.class.instrument_transaction do
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
- end
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
- # @transaction_state valid states
11
- # nil, :open_requested, :open, :close_requested
11
+ attr_reader :requestor, :url
12
12
 
13
- def initialize(url, _options = {})
13
+ def initialize(url, options = {})
14
14
  @url = url
15
- @url_components = url_components!(url)
16
- @transaction_state = nil
15
+ @options = options
17
16
  end
18
17
 
19
18
  def connect
20
- @connection ||= connection
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
- fail 'Query attempted without a connection' if @connection.nil?
24
+ def query_set(transaction, queries, options = {})
25
+ setup_queries!(queries, transaction)
27
26
 
28
- statements_data = queries.map do |query|
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
- return unless url = full_transaction_url
29
+ faraday_response = @requestor.post(path, queries)
38
30
 
39
- faraday_response = self.class.instrument_request(url, request_data) do
40
- @connection.post(url, request_data)
41
- end
31
+ transaction.apply_id_from_url!(faraday_response.env[:response_headers][:location])
42
32
 
43
- store_transaction_id!(faraday_response)
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 start_transaction
49
- case @transaction_state
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
- def end_transaction
60
- if @transaction_state.nil?
61
- fail 'Cannot close transaction without starting one'
62
- end
41
+ # Schema inspection methods
42
+ def indexes(_session)
43
+ response = @requestor.get('db/data/schema/index')
63
44
 
64
- # This needs thought through more...
65
- @transaction_state = :close_requested
66
- query_set([])
67
- @transaction_state = nil
68
- @transaction_id = nil
45
+ list = response.body || []
69
46
 
70
- true
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
- def transaction_started?
74
- !!@transaction_id
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 version
78
- @version ||= @connection.get(db_data_url).body[:neo4j_version]
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
- response = @connection.get(url)
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
- " #{ANSI::BLUE}HTTP REQUEST:#{ANSI::CLEAR} #{ANSI::YELLOW}#{ms.round}ms#{ANSI::CLEAR} #{payload[:url]}"
83
+ def connected?
84
+ !!@requestor
109
85
  end
110
86
 
111
- private
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
- def store_transaction_id!(faraday_response)
114
- location = faraday_response.env[:response_headers][:location]
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
- return if !location
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
- @transaction_id = location.split('/').last.to_i
119
- end
123
+ # Convenience method to #request(:post, ...)
124
+ def post(path, body = '', options = {})
125
+ request(:post, path, body, options)
126
+ end
120
127
 
121
- def full_transaction_url
122
- path = ''
123
- path << "/#{@transaction_id}" if [:open, :close_requested].include?(@transaction_state)
124
- path << '/commit' if [nil, :close_requested].include?(@transaction_state)
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
- db_data_url + 'transaction' + path if path
128
- end
133
+ private
129
134
 
130
- def db_data_url
131
- url_base + 'db/data/'
132
- end
135
+ def faraday_connection
136
+ require 'faraday'
137
+ require 'faraday_middleware/multi_json'
133
138
 
134
- def url_base
135
- "#{scheme}://#{host}:#{port}/"
136
- end
139
+ Faraday.new(url) do |c|
140
+ c.request :basic_auth, user, password
141
+ c.request :multi_json
137
142
 
138
- def connection
139
- Faraday.new(@url) do |c|
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
- c.response :multi_json, symbolize_keys: true, content_type: 'application/json'
144
- c.use Faraday::Adapter::NetHttpPersistent
146
+ # c.response :logger, ::Logger.new(STDOUT), bodies: true
145
147
 
146
- c.headers['Content-Type'] = 'application/json'
147
- c.headers['User-Agent'] = user_agent_string
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
- def url_components!(url)
152
- @uri = URI(url || 'http://localhost:7474')
153
+ def request_body(body)
154
+ return body if body.is_a?(String)
153
155
 
154
- if !@uri.is_a?(URI::HTTP)
155
- fail ArgumentError, "Invalid URL: #{url.inspect}"
156
- end
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
- true
159
- end
163
+ body
164
+ else
165
+ {statements: [self.class.statement_from_query(body)]} if body.respond_to?(:cypher)
166
+ end
167
+ end
160
168
 
161
- URI_DEFAULTS = {
162
- scheme: 'http',
163
- user: 'neo4j', password: 'neo4j',
164
- host: 'localhost', port: 7474
165
- }
169
+ class << self
170
+ private
166
171
 
167
- URI_DEFAULTS.each do |method, value|
168
- define_method(method) do
169
- @uri.send(method) || value
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
- "#{gem}-gem/#{version} (https://github.com/neo4jrb/#{gem})"
183
+ def url_from_path(path)
184
+ url_base + (path[0] != '/' ? '/' + path : path)
185
+ end
182
186
  end
183
187
  end
184
188
  end