neo4j-core 5.1.14 → 6.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/neo4j-core.rb +1 -1
  3. data/lib/neo4j-core/helpers.rb +6 -0
  4. data/lib/neo4j-core/query.rb +88 -40
  5. data/lib/neo4j-core/query_clauses.rb +67 -67
  6. data/lib/neo4j-core/version.rb +1 -1
  7. data/lib/neo4j-embedded/embedded_label.rb +16 -14
  8. data/lib/neo4j-embedded/embedded_node.rb +115 -113
  9. data/lib/neo4j-embedded/embedded_relationship.rb +53 -51
  10. data/lib/neo4j-embedded/embedded_session.rb +5 -14
  11. data/lib/neo4j-embedded/label.rb +2 -1
  12. data/lib/neo4j-server/cypher_label.rb +8 -7
  13. data/lib/neo4j-server/cypher_relationship.rb +1 -1
  14. data/lib/neo4j-server/cypher_response.rb +3 -2
  15. data/lib/neo4j-server/cypher_transaction.rb +1 -6
  16. data/lib/neo4j-server/resource.rb +1 -1
  17. data/lib/neo4j/core/cypher_session.rb +28 -0
  18. data/lib/neo4j/core/cypher_session/adaptors.rb +108 -0
  19. data/lib/neo4j/core/cypher_session/adaptors/embedded.rb +81 -0
  20. data/lib/neo4j/core/cypher_session/adaptors/http.rb +159 -0
  21. data/lib/neo4j/core/cypher_session/responses.rb +27 -0
  22. data/lib/neo4j/core/cypher_session/responses/embedded.rb +77 -0
  23. data/lib/neo4j/core/cypher_session/responses/http.rb +104 -0
  24. data/lib/neo4j/core/cypher_session/result.rb +39 -0
  25. data/lib/neo4j/core/instrumentable.rb +36 -0
  26. data/lib/neo4j/core/node.rb +28 -0
  27. data/lib/neo4j/core/path.rb +15 -0
  28. data/lib/neo4j/core/relationship.rb +25 -0
  29. data/lib/neo4j/core/wrappable.rb +35 -0
  30. data/lib/neo4j/label.rb +7 -0
  31. data/lib/neo4j/session.rb +3 -3
  32. data/lib/neo4j/transaction.rb +1 -24
  33. data/neo4j-core.gemspec +2 -2
  34. metadata +22 -9
@@ -26,13 +26,9 @@ module Neo4j
26
26
  def properties_map
27
27
  return @properties_map if @properties_map
28
28
 
29
- props = if @config[:properties_map].is_a?(Hash)
30
- @config[:properties_map].each_with_object({}) do |(k, v), m|
31
- m[k.to_s.to_java] = v.to_s.to_java
32
- end
33
- else
34
- {}
35
- end
29
+ props = @config[:properties_map].each_with_object({}) do |(k, v), m|
30
+ m[k.to_s.to_java] = v.to_s.to_java
31
+ end
36
32
  @properties_map = java.util.HashMap.new(props)
37
33
  end
38
34
 
@@ -46,6 +42,7 @@ module Neo4j
46
42
 
47
43
  def version
48
44
  # Wow
45
+ # Yeah, agreed...
49
46
  version_string = @graph_db.to_java(Java::OrgNeo4jKernel::GraphDatabaseAPI).getDependencyResolver.resolveDependency(Java::OrgNeo4jKernel::KernelData.java_class).version.to_s
50
47
  version_string.split(' ')[-1]
51
48
  end
@@ -150,18 +147,12 @@ module Neo4j
150
147
  ActiveSupport::Notifications.instrument('neo4j.cypher_query', params: params, context: options[:context],
151
148
  cypher: query, pretty_cypher: options[:pretty_cypher], params: params) do
152
149
  @engine ||= Java::OrgNeo4jCypherJavacompat::ExecutionEngine.new(@graph_db)
153
- @engine.execute(query, indifferent_params(params))
150
+ @engine.execute(query, HashWithIndifferentAccess.new(params))
154
151
  end
155
152
  rescue StandardError => e
156
153
  raise Neo4j::Session::CypherError.new(e.message, e.class, 'cypher error')
157
154
  end
158
155
 
159
- def indifferent_params(params)
160
- return {} unless params
161
- params.each { |k, v| params[k] = HashWithIndifferentAccess.new(params[k]) if v.is_a?(Hash) && !v.respond_to?(:nested_under_indifferent_access) }
162
- HashWithIndifferentAccess.new(params)
163
- end
164
-
165
156
  def query_default_return(as)
166
157
  " RETURN #{as}"
167
158
  end
@@ -22,7 +22,8 @@ module Neo4j
22
22
  schema.get_indexes.to_a.each do |i|
23
23
  begin
24
24
  i.drop
25
- rescue Java::JavaLang::IllegalStateException; end
25
+ rescue Java::JavaLang::IllegalStateException
26
+ end
26
27
  end
27
28
  end
28
29
  end
@@ -10,16 +10,17 @@ module Neo4j
10
10
  @session = session
11
11
  end
12
12
 
13
- def create_index(*properties)
14
- response = @session._query("CREATE INDEX ON :`#{@name}`(#{properties.join(',')})")
13
+ def create_index(property, options = {}, session = Neo4j::Session.current)
14
+ validate_index_options!(options)
15
+ properties = property.is_a?(Array) ? property.join(',') : property
16
+ response = session._query("CREATE INDEX ON :`#{@name}`(#{properties})")
15
17
  response.raise_error if response.error?
16
18
  end
17
19
 
18
- def drop_index(*properties)
19
- properties.each do |property|
20
- response = @session._query("DROP INDEX ON :`#{@name}`(#{property})")
21
- response.raise_error if response.error? && !response.error_msg.match(/No such INDEX ON/)
22
- end
20
+ def drop_index(property, options = {}, session = Neo4j::Session.current)
21
+ validate_index_options!(options)
22
+ response = session._query("DROP INDEX ON :`#{@name}`(#{property})")
23
+ response.raise_error if response.error? && !response.error_msg.match(/No such INDEX ON/)
23
24
  end
24
25
 
25
26
  def indexes
@@ -85,7 +85,7 @@ module Neo4j
85
85
 
86
86
  # (see Neo4j::Relationship#props=)
87
87
  def props=(properties)
88
- @session._query_or_fail("#{match_start} SET n = { props }", false, props: properties, neo_id: neo_id)
88
+ @session._query_or_fail("#{match_start} SET n = { props }", false, props: properties, neo_id: neo_id)
89
89
  properties
90
90
  end
91
91
 
@@ -101,9 +101,10 @@ module Neo4j
101
101
  def identify_entity(data)
102
102
  self_string = data[:self]
103
103
  if self_string
104
- if self_string.include?('node')
104
+ type = self_string.split('/')[-2]
105
+ if type == 'node'
105
106
  :node
106
- elsif self_string.include?('relationship')
107
+ elsif type == 'relationship'
107
108
  :relationship
108
109
  end
109
110
  elsif [:nodes, :relationships, :start, :end, :length].all? { |k| data.key?(k) }
@@ -73,17 +73,12 @@ module Neo4j
73
73
  cypher_response.set_data(first_result)
74
74
  else
75
75
  first_error = response.body[:errors].first
76
- tx_cleanup!(first_error)
76
+ mark_expired if first_error[:message].match(/Unrecognized transaction id/)
77
77
  cypher_response.set_error(first_error)
78
78
  end
79
79
  end
80
80
  end
81
81
 
82
- def tx_cleanup!(first_error)
83
- autoclosed!
84
- mark_expired if first_error[:message].match(/Unrecognized transaction id/)
85
- end
86
-
87
82
  def empty_response
88
83
  OpenStruct.new(status: 200, body: '')
89
84
  end
@@ -47,7 +47,7 @@ module Neo4j
47
47
  end
48
48
 
49
49
  def resource_url_id(url = resource_url)
50
- url.match(/\/(\d+)$/)[1].to_i
50
+ url.match(%r{/(\d+)$})[1].to_i
51
51
  end
52
52
 
53
53
  def convert_from_json_value(value)
@@ -0,0 +1,28 @@
1
+ require 'neo4j/core/cypher_session/adaptors/http'
2
+
3
+ module Neo4j
4
+ module Core
5
+ class CypherSession
6
+ def initialize(adaptor)
7
+ fail ArgumentError, "Invalid adaptor: #{adaptor.inspect}" if !adaptor.is_a?(Adaptors::Base)
8
+
9
+ @adaptor = adaptor
10
+
11
+ @adaptor.connect
12
+ end
13
+
14
+ %w(
15
+ query
16
+ queries
17
+ start_transaction
18
+ end_transaction
19
+ version
20
+ transactions
21
+ ).each do |method|
22
+ define_method(method) do |*args|
23
+ @adaptor.send(method, *args)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,108 @@
1
+ require 'neo4j/core/instrumentable'
2
+
3
+ module Neo4j
4
+ module Core
5
+ class CypherSession
6
+ module Adaptors
7
+ MAP = {}
8
+
9
+ class Base
10
+ include Neo4j::Core::Instrumentable
11
+
12
+ def connect(*_args)
13
+ fail '#connect not implemented!'
14
+ end
15
+
16
+ Query = Struct.new(:cypher, :parameters, :pretty_cypher, :context)
17
+
18
+ class QueryBuilder
19
+ attr_reader :queries
20
+
21
+ def initialize
22
+ @queries = []
23
+ end
24
+
25
+ def append(*args)
26
+ query = case args.map(&:class)
27
+ when [String], [String, Hash]
28
+ Query.new(args[0], args[1] || {})
29
+ when [::Neo4j::Core::Query]
30
+ args[0]
31
+ else
32
+ fail ArgumentError, "Could not determine query from arguments: #{args.inspect}"
33
+ end
34
+
35
+ @queries << query
36
+ end
37
+
38
+ def query
39
+ # `nil` sessions are just a workaround until
40
+ # we phase out `Query` objects containing sessions
41
+ Neo4j::Core::Query.new(session: nil)
42
+ end
43
+ end
44
+
45
+ def query(cypher, parameters = {})
46
+ queries do
47
+ append(cypher, parameters)
48
+ end[0]
49
+ end
50
+
51
+ def queries(&block)
52
+ query_builder = QueryBuilder.new
53
+
54
+ query_builder.instance_eval(&block)
55
+
56
+ query_set(query_builder.queries)
57
+ end
58
+
59
+ def query_set(_queries)
60
+ fail '#queries not implemented!'
61
+ end
62
+
63
+ def start_transaction(*_args)
64
+ fail '#start_transaction not implemented!'
65
+ end
66
+
67
+ def end_transaction(*_args)
68
+ fail '#end_transaction not implemented!'
69
+ end
70
+
71
+ def version(*_args)
72
+ fail '#version not implemented!'
73
+ end
74
+
75
+ # Uses #start_transaction and #end_transaction to allow
76
+ # execution of queries within a block to be part of a
77
+ # full transaction
78
+ def transaction
79
+ start_transaction
80
+
81
+ yield
82
+ ensure
83
+ end_transaction
84
+ end
85
+
86
+ EMPTY = ''
87
+ NEWLINE_W_SPACES = "\n "
88
+
89
+ instrument(:query, 'neo4j.core.cypher_query', %w(query)) do |_, _start, _finish, _id, payload|
90
+ query = payload[:query]
91
+ params_string = (query.parameters && query.parameters.size > 0 ? "| #{query.parameters.inspect}" : EMPTY)
92
+ cypher = query.pretty_cypher ? NEWLINE_W_SPACES + query.pretty_cypher.gsub(/\n/, NEWLINE_W_SPACES) : query.cypher
93
+
94
+ " #{ANSI::CYAN}#{query.context || 'CYPHER'}#{ANSI::CLEAR} #{cypher} #{params_string}"
95
+ end
96
+
97
+ class << self
98
+ def instrument_queries(queries)
99
+ queries.each do |query|
100
+ instrument_query(query) {}
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,81 @@
1
+ require 'neo4j/core/cypher_session/adaptors'
2
+ require 'neo4j/core/cypher_session/responses/embedded'
3
+
4
+ module Neo4j
5
+ module Core
6
+ class CypherSession
7
+ module Adaptors
8
+ class Embedded < Base
9
+ def initialize(path, options = {})
10
+ fail 'JRuby is required for embedded mode' if RUBY_PLATFORM != 'java'
11
+ fail ArgumentError, "Invalid path: #{path}" if !File.directory?(path)
12
+
13
+ @path = path
14
+ @options = options
15
+ end
16
+
17
+ def connect
18
+ factory = Java::OrgNeo4jGraphdbFactory::GraphDatabaseFactory.new
19
+ db_service = factory.newEmbeddedDatabaseBuilder(@path)
20
+ db_service.loadPropertiesFromFile(@options[:properties_file]) if @options[:properties_file]
21
+ db_service.setConfig(@options[:properties_map]) if @options[:properties_map]
22
+
23
+ @graph_db = db_service.newGraphDatabase
24
+ end
25
+
26
+ def query_set(queries)
27
+ # I think that this is the best way to do a batch in embedded...
28
+ # Should probably do within a transaction in case of errors...
29
+
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, HashWithIndifferentAccess.new(query.parameters))
36
+ end
37
+
38
+ Responses::Embedded.new(execution_results).results
39
+ end
40
+ 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 version
57
+ if defined?(::Neo4j::Community)
58
+ ::Neo4j::Community::NEO_VERSION
59
+ elsif defined?(::Neo4j::Enterprise)
60
+ ::Neo4j::Enterprise::NEO_VERSION
61
+ else
62
+ fail 'Could not determine embedded version!'
63
+ end
64
+ end
65
+
66
+ instrument(:transaction, 'neo4j.core.embedded.transaction', []) do |_, start, finish, _id, _payload|
67
+ ms = (finish - start) * 1000
68
+
69
+ " #{ANSI::BLUE}EMBEDDED CYPHER TRANSACTION:#{ANSI::CLEAR} #{ANSI::YELLOW}#{ms.round}ms#{ANSI::CLEAR}"
70
+ end
71
+
72
+ private
73
+
74
+ def engine
75
+ @engine ||= Java::OrgNeo4jCypherJavacompat::ExecutionEngine.new(@graph_db)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,159 @@
1
+ require 'neo4j/core/cypher_session/adaptors'
2
+ require 'neo4j/core/cypher_session/responses/http'
3
+
4
+ # TODO: Work with `Query` objects
5
+ module Neo4j
6
+ module Core
7
+ class CypherSession
8
+ module Adaptors
9
+ class HTTP < Base
10
+ # @transaction_state valid states
11
+ # nil, :open_requested, :open, :close_requested
12
+
13
+ def initialize(url, _options = {})
14
+ @url = url
15
+ @url_components = url_components!(url)
16
+ @transaction_state = nil
17
+ end
18
+
19
+ def connect
20
+ @connection ||= connection
21
+ end
22
+
23
+ ROW_REST = %w(row REST)
24
+
25
+ def query_set(queries)
26
+ fail 'Query attempted without a connection' if @connection.nil?
27
+
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)
36
+
37
+ url = full_transaction_url
38
+ faraday_response = self.class.instrument_request(url, request_data) do
39
+ @connection.post(url, request_data)
40
+ end
41
+
42
+ store_transaction_id!(faraday_response)
43
+
44
+ Responses::HTTP.new(faraday_response, request_data).results
45
+ end
46
+
47
+ def start_transaction
48
+ case @transaction_state
49
+ when :open
50
+ return
51
+ when :close_requested
52
+ fail 'Cannot start transaction when a close has been requested'
53
+ end
54
+
55
+ @transaction_state = :open_requested
56
+ end
57
+
58
+ def end_transaction
59
+ if @transaction_state.nil?
60
+ fail 'Cannot close transaction without starting one'
61
+ end
62
+
63
+ # This needs thought through more...
64
+ @transaction_state = :close_requested
65
+ query_set([])
66
+ @transaction_state = nil
67
+
68
+ true
69
+ end
70
+
71
+ def version
72
+ @version ||= @connection.get(db_data_url).body[:neo4j_version]
73
+ end
74
+
75
+ instrument(:request, 'neo4j.core.http.request', %w(url body)) do |_, start, finish, _id, payload|
76
+ ms = (finish - start) * 1000
77
+
78
+ " #{ANSI::BLUE}HTTP REQUEST:#{ANSI::CLEAR} #{ANSI::YELLOW}#{ms.round}ms#{ANSI::CLEAR} #{payload[:url]}"
79
+ end
80
+
81
+ private
82
+
83
+ def store_transaction_id!(faraday_response)
84
+ location = faraday_response.env[:response_headers][:location]
85
+
86
+ return if !location
87
+
88
+ @transaction_id = location.split('/').last.to_i
89
+ end
90
+
91
+ def full_transaction_url
92
+ path = case @transaction_state
93
+ when nil then '/commit'
94
+ when :open_requested then ''
95
+ when :open then "/#{@transaction_id}"
96
+ when :close_requested then "/#{@transaction_id}/commit"
97
+ end
98
+
99
+ db_data_url + 'transaction' + path
100
+ end
101
+
102
+ def db_data_url
103
+ url_base + 'db/data/'
104
+ end
105
+
106
+ def url_base
107
+ "#{scheme}://#{host}:#{port}/"
108
+ end
109
+
110
+ def connection
111
+ Faraday.new(@url) do |c|
112
+ c.request :basic_auth, user, password
113
+ c.request :multi_json
114
+
115
+ c.response :multi_json, symbolize_keys: true, content_type: 'application/json'
116
+ c.use Faraday::Adapter::NetHttpPersistent
117
+
118
+ c.headers['Content-Type'] = 'application/json'
119
+ c.headers['User-Agent'] = user_agent_string
120
+ end
121
+ end
122
+
123
+ def url_components!(url)
124
+ @uri = URI(url || 'http://localhost:7474')
125
+
126
+ if !@uri.is_a?(URI::HTTP)
127
+ fail ArgumentError, "Invalid URL: #{url.inspect}"
128
+ end
129
+
130
+ true
131
+ end
132
+
133
+ URI_DEFAULTS = {
134
+ scheme: 'http',
135
+ user: 'neo4j', password: 'neo4j',
136
+ host: 'localhost', port: 7474
137
+ }
138
+
139
+ URI_DEFAULTS.each do |method, value|
140
+ define_method(method) do
141
+ @uri.send(method) || value
142
+ end
143
+ end
144
+
145
+ def user_agent_string
146
+ gem, version = if defined?(::Neo4j::ActiveNode)
147
+ ['neo4j', ::Neo4j::VERSION]
148
+ else
149
+ ['neo4j-core', ::Neo4j::Core::VERSION]
150
+ end
151
+
152
+
153
+ "#{gem}-gem/#{version} (https://github.com/neo4jrb/#{gem})"
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end