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.
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