neo4j-core 5.1.7 → 6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 270806b67718b4b05e99c73d1b1d2118fefe6dce
4
- data.tar.gz: 878b8d073dd7a1fbce2139524a92f195286325f4
3
+ metadata.gz: d2e549ac9b629bd47019215c2457d66c6b355a2f
4
+ data.tar.gz: 717fb2cc5363c0a7a9726ae4d686e504634d7310
5
5
  SHA512:
6
- metadata.gz: d4f9e836e2a5352c48621ef7bc0bfe40f6ec6cab15fcc7b32a003e719a11211d78e35c247ea2e51fc1c50926babe3215285a054541e511483ab9d72bfdae2707
7
- data.tar.gz: fea94937e02b32ffdd8bf6a5bbf8bee6f9e7ac6b56b1cab031ec9c5c918a03bcca21df1b2e87e5edb4b5f7c837fbca30e99b088d6981dd82352f9fc2717411fd
6
+ metadata.gz: 14df3ab1fb20692a66036201039f66861b38fc340b3c63c7051d977b2a718c09a9daa3b5d717920ddaa5228089c899f8cf9f611c2e11e6cda992af6fc6d1838a
7
+ data.tar.gz: ece61afe7b0c8c21db72322bfe5143ccc9ab7ef84dc6b01f7b7416b7978b28a082b0edb7dbc5587ed390cc9aa2da3555f4aad1fa11e96fb28e76e5de9d90e7c4
@@ -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
@@ -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,77 @@
1
+ require 'neo4j/core/cypher_session/responses'
2
+
3
+ module Neo4j
4
+ module Core
5
+ class CypherSession
6
+ module Responses
7
+ class Embedded < Base
8
+ attr_reader :results, :request_data
9
+
10
+ def initialize(execution_results)
11
+ @results = execution_results.map do |execution_result|
12
+ result_from_execution_result(execution_result)
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def result_from_execution_result(execution_result)
19
+ columns = execution_result.columns.to_a
20
+ rows = execution_result.map do |execution_result_row|
21
+ columns.map { |column| wrap_entity(execution_result_row[column]) }
22
+ end
23
+ Result.new(columns, rows)
24
+ end
25
+
26
+ def wrap_entity(entity)
27
+ if entity.is_a?(Array) ||
28
+ entity.is_a?(Java::ScalaCollectionConvert::Wrappers::SeqWrapper)
29
+ entity.to_a.map { |e| wrap_entity(e) }
30
+ elsif entity.is_a?(Java::OrgNeo4jKernelImplCore::NodeProxy)
31
+ wrap_node(entity)
32
+ elsif entity.is_a?(Java::OrgNeo4jKernelImplCore::RelationshipProxy)
33
+ wrap_relationship(entity)
34
+ elsif entity.respond_to?(:path_entities)
35
+ wrap_path(entity)
36
+ else
37
+ wrap_value(entity)
38
+ end
39
+ end
40
+
41
+ def wrap_node(entity)
42
+ ::Neo4j::Core::Node.new(entity.get_id,
43
+ entity.get_labels.map(&:to_s),
44
+ get_entity_properties(entity)).wrap
45
+ end
46
+
47
+ def wrap_relationship(entity)
48
+ ::Neo4j::Core::Relationship.new(entity.get_id,
49
+ entity.get_type.name,
50
+ get_entity_properties(entity)).wrap
51
+ end
52
+
53
+ def wrap_path(entity)
54
+ ::Neo4j::Core::Path.new(entity.nodes.map(&method(:wrap_node)),
55
+ entity.relationships.map(&method(:wrap_relationship)),
56
+ nil).wrap
57
+ end
58
+
59
+ def wrap_value(entity)
60
+ if entity.is_a?(Java::ScalaCollectionConvert::Wrappers::MapWrapper)
61
+ entity.each_with_object({}) { |(k, v), r| r[k.to_sym] = v }
62
+ else
63
+ # Convert from Java?
64
+ entity
65
+ end
66
+ end
67
+
68
+ def get_entity_properties(entity)
69
+ entity.get_property_keys.each_with_object({}) do |key, result|
70
+ result[key.to_sym] = entity.get_property(key)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,104 @@
1
+ require 'neo4j/core/cypher_session/responses'
2
+
3
+ module Neo4j
4
+ module Core
5
+ class CypherSession
6
+ module Responses
7
+ class HTTP < Base
8
+ attr_reader :results, :request_data
9
+
10
+ def initialize(faraday_response, request_data)
11
+ @request_data = request_data
12
+
13
+ validate_faraday_response!(faraday_response)
14
+
15
+ @results = faraday_response.body[:results].map do |result_data|
16
+ result_from_data(result_data[:columns], result_data[:data])
17
+ end
18
+ end
19
+
20
+ def result_from_data(columns, entities_data)
21
+ rows = entities_data.map do |entity_data|
22
+ wrap_entity entity_data[:row], entity_data[:rest]
23
+ end
24
+
25
+ Result.new(columns, rows)
26
+ end
27
+
28
+ def wrap_entity(row_data, rest_data)
29
+ row_data.each_with_index.map do |row_datum, i|
30
+ rest_datum = rest_data[i]
31
+
32
+ if rest_datum.is_a?(Array)
33
+ wrap_entity(row_datum, rest_datum)
34
+ elsif (ident = identify_entity(rest_datum))
35
+ send("wrap_#{ident}", rest_datum, row_datum)
36
+ else
37
+ row_datum
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def identify_entity(rest_datum)
45
+ self_string = rest_datum[:self]
46
+ if self_string
47
+ type = self_string.split('/')[-2]
48
+ if type == 'node'
49
+ :node
50
+ elsif type == 'relationship'
51
+ :relationship
52
+ end
53
+ elsif [:nodes, :relationships, :start, :end, :length].all? { |k| rest_datum.key?(k) }
54
+ :path
55
+ end
56
+ end
57
+
58
+ def wrap_node(rest_datum, _)
59
+ metadata_data = rest_datum[:metadata]
60
+ ::Neo4j::Core::Node.new(id_from_rest_datum(rest_datum),
61
+ metadata_data && metadata_data[:labels],
62
+ rest_datum[:data]).wrap
63
+ end
64
+
65
+ def wrap_relationship(rest_datum, _)
66
+ metadata_data = rest_datum[:metadata]
67
+ ::Neo4j::Core::Relationship.new(id_from_rest_datum(rest_datum),
68
+ metadata_data && metadata_data[:type],
69
+ rest_datum[:data]).wrap
70
+ end
71
+
72
+ def wrap_path(rest_datum, row_datum)
73
+ nodes = rest_datum[:nodes].each_with_index.map do |url, i|
74
+ Node.from_url(url, row_datum[2 * i])
75
+ end
76
+ relationships = rest_datum[:relationships].each_with_index.map do |url, i|
77
+ Relationship.from_url(url, row_datum[(2 * i) + 1])
78
+ end
79
+
80
+ ::Neo4j::Core::Path.new(nodes, relationships, rest_datum[:directions]).wrap
81
+ end
82
+
83
+ def id_from_rest_datum(rest_datum)
84
+ if rest_datum[:metadata]
85
+ rest_datum[:metadata][:id]
86
+ else
87
+ rest_datum[:self].split('/').last.to_i
88
+ end
89
+ end
90
+
91
+ def validate_faraday_response!(faraday_response)
92
+ if error = faraday_response.body[:errors][0]
93
+ fail CypherError, "#{ANSI::CYAN}#{error[:code]}#{ANSI::CLEAR}: #{error[:message]}"
94
+ end
95
+
96
+ return if (200..299).include?(status = faraday_response.status)
97
+
98
+ fail CypherError, "Expected 200-series response for #{faraday_response.env.url} (got #{status})"
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,27 @@
1
+ require 'neo4j/core/cypher_session/result'
2
+
3
+ module Neo4j
4
+ module Core
5
+ class CypherSession
6
+ module Responses
7
+ MAP = {}
8
+
9
+ class Base
10
+ class CypherError < StandardError; end
11
+
12
+ include Enumerable
13
+
14
+ def each
15
+ results.each do |result|
16
+ yield result
17
+ end
18
+ end
19
+
20
+ def results
21
+ fail '#results not implemented!'
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ require 'neo4j/core/node'
2
+ require 'neo4j/core/relationship'
3
+ require 'neo4j/core/path'
4
+
5
+ module Neo4j
6
+ module Core
7
+ class CypherSession
8
+ class Result
9
+ attr_reader :columns, :rows
10
+
11
+ def initialize(columns, rows)
12
+ @columns = columns.map(&:to_sym)
13
+ @rows = rows
14
+ @struct_class = Struct.new(*@columns)
15
+ end
16
+
17
+ include Enumerable
18
+
19
+ def each
20
+ structs.each do |struct|
21
+ yield struct
22
+ end
23
+ end
24
+
25
+ def structs
26
+ @structs ||= rows.map do |row|
27
+ @struct_class.new(*row)
28
+ end
29
+ end
30
+
31
+ def hashes
32
+ @hashes ||= rows.map do |row|
33
+ Hash[@columns.zip(row)]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -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,36 @@
1
+ require 'active_support/notifications'
2
+
3
+ module Neo4j
4
+ module Core
5
+ module Instrumentable
6
+ def self.included(base)
7
+ base.send :include, InstanceMethods
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module InstanceMethods
12
+ end
13
+
14
+ module ClassMethods
15
+ def instrument(name, label, arguments, &block)
16
+ # defining class methods
17
+ klass = class << self; self; end
18
+ klass.instance_eval do
19
+ define_method("subscribe_to_#{name}") do |&b|
20
+ ActiveSupport::Notifications.subscribe(label) do |a, start, finish, id, payload|
21
+ b.call block.call(a, start, finish, id, payload)
22
+ end
23
+ end
24
+
25
+ define_method("instrument_#{name}") do |*args, &b|
26
+ hash = arguments.each_with_index.each_with_object({}) do |(argument, i), result|
27
+ result[argument.to_sym] = args[i]
28
+ end
29
+ ActiveSupport::Notifications.instrument(label, hash) { b.call }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end