neo4j-core 3.0.0.alpha.18 → 3.0.0.alpha.19

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,155 @@
1
+ module Neo4j::Server
2
+ class CypherResponse
3
+ attr_reader :data, :columns, :error_msg, :error_status, :error_code, :response
4
+
5
+ class ResponseError < StandardError
6
+ attr_reader :status, :code
7
+
8
+ def initialize(msg, status, code)
9
+ super(msg)
10
+ @status = status
11
+ @code = code
12
+ end
13
+ end
14
+
15
+
16
+ class HashEnumeration
17
+ include Enumerable
18
+ extend Forwardable
19
+ def_delegator :@response, :error_msg
20
+ def_delegator :@response, :error_status
21
+ def_delegator :@response, :error_code
22
+ def_delegator :@response, :data
23
+ def_delegator :@response, :columns
24
+
25
+ def initialize(response, map_return_procs, query)
26
+ @response = response
27
+ @map_return_procs = map_return_procs
28
+ @query = query
29
+ end
30
+
31
+ def to_s
32
+ @query
33
+ end
34
+
35
+ puts "RELOADED"
36
+ def inspect
37
+ #"Enumerable query: '#{@query}' map_return: [#{@map_return_procs.keys.join(', ')}]"
38
+ "Enumerable query: '#{@query}' map_return: [#{@map_return_procs.keys.inspect}]"
39
+ end
40
+
41
+ def each_no_mapping
42
+ data.each do |row|
43
+ hash = {}
44
+ row.each_with_index do |row, i|
45
+ key = columns[i].to_sym
46
+ hash[key] = row
47
+ end
48
+ yield hash
49
+ end
50
+ end
51
+
52
+ def each_multi_column_mapping
53
+ data.each do |row|
54
+ hash = {}
55
+ row.each_with_index do |row, i|
56
+ key = columns[i].to_sym
57
+ proc = @map_return_procs[key]
58
+ hash[key] = proc ? proc.call(row) : row
59
+ end
60
+ yield hash
61
+ end
62
+ end
63
+
64
+ def each_single_column_mapping
65
+ data.each do |row|
66
+ result = @map_return_procs.call(row.first)
67
+ yield result
68
+ end
69
+ end
70
+
71
+ def each(&block)
72
+ case @map_return_procs
73
+ when NilClass then each_no_mapping &block
74
+ when Hash then each_multi_column_mapping &block
75
+ else each_single_column_mapping &block
76
+ end
77
+ end
78
+ end
79
+
80
+ def to_hash_enumeration(map_return_procs={}, cypher='')
81
+ HashEnumeration.new(self, map_return_procs, cypher)
82
+ end
83
+
84
+ def initialize(response, uncommited = false)
85
+ @response = response
86
+ @uncommited = uncommited
87
+ end
88
+
89
+
90
+ def first_data
91
+ if uncommited?
92
+ @data.first['row'].first
93
+ else
94
+ @data[0][0]
95
+ end
96
+ end
97
+
98
+ def error?
99
+ !!@error
100
+ end
101
+
102
+ def uncommited?
103
+ @uncommited
104
+ end
105
+
106
+ def raise_unless_response_code(code)
107
+ raise "Response code #{response.code}, expected #{code} for #{response.request.path}, #{response.body}" unless response.code == code
108
+ end
109
+
110
+ def set_data(data, columns)
111
+ @data = data
112
+ @columns = columns
113
+ self
114
+ end
115
+
116
+ def set_error(error_msg, error_status, error_core)
117
+ @error = true
118
+ @error_msg = error_msg
119
+ @error_status = error_status
120
+ @error_code = error_core
121
+ self
122
+ end
123
+
124
+ def raise_error
125
+ raise "Tried to raise error without an error" unless @error
126
+ raise ResponseError.new(@error_msg, @error_status, @error_code)
127
+ end
128
+
129
+ def self.create_with_no_tx(response)
130
+ case response.code
131
+ when 200
132
+ CypherResponse.new(response).set_data(response['data'], response['columns'])
133
+ when 400
134
+ CypherResponse.new(response).set_error(response['message'], response['exception'], response['fullname'])
135
+ else
136
+ raise "Unknown response code #{response.code} for #{response.request.path.to_s}"
137
+ end
138
+ end
139
+
140
+ def self.create_with_tx(response)
141
+ raise "Unknown response code #{response.code} for #{response.request.path.to_s}" unless response.code == 200
142
+
143
+ first_result = response['results'][0]
144
+ cr = CypherResponse.new(response, true)
145
+
146
+ if (response['errors'].empty?)
147
+ cr.set_data(first_result['data'], first_result['columns'])
148
+ else
149
+ first_error = response['errors'].first
150
+ cr.set_error(first_error['message'], first_error['status'], first_error['code'])
151
+ end
152
+ cr
153
+ end
154
+ end
155
+ end
@@ -37,6 +37,10 @@ module Neo4j::Server
37
37
  Neo4j::Session._notify_listeners(:session_available, self)
38
38
  end
39
39
 
40
+ def db_type
41
+ :server_db
42
+ end
43
+
40
44
  def to_s
41
45
  "#{self.class} url: '#{@resource_url}'"
42
46
  end
@@ -64,7 +68,13 @@ module Neo4j::Server
64
68
  end
65
69
 
66
70
  def begin_tx
67
- Thread.current[:neo4j_curr_tx] = wrap_resource(self, 'transaction', CypherTransaction, nil, :post, @endpoint)
71
+ if Neo4j::Transaction.current
72
+ # Handle nested transaction "placebo transaction"
73
+ Neo4j::Transaction.current.push_nested!
74
+ else
75
+ wrap_resource(self, 'transaction', CypherTransaction, nil, :post, @endpoint)
76
+ end
77
+ Neo4j::Transaction.current
68
78
  end
69
79
 
70
80
  def create_node(props=nil, labels=[])
@@ -77,8 +87,9 @@ module Neo4j::Server
77
87
  def load_node(neo_id)
78
88
  cypher_response = _query("START n=node(#{neo_id}) RETURN n")
79
89
  if (!cypher_response.error?)
80
- CypherNode.new(self, neo_id)
81
- elsif (cypher_response.error_status == 'EntityNotFoundException')
90
+ result = cypher_response.entity_data(neo_id)
91
+ CypherNode.new(self, result)
92
+ elsif (cypher_response.error_status =~ /EntityNotFound/)
82
93
  return nil
83
94
  else
84
95
  cypher_response.raise_error
@@ -140,12 +151,24 @@ module Neo4j::Server
140
151
  end
141
152
  end
142
153
 
154
+ def _query_data(q)
155
+ r = _query_or_fail(q, true)
156
+ # the response is different if we have a transaction or not
157
+ Neo4j::Transaction.current ? r : r['data']
158
+ end
159
+
143
160
  def _query_or_fail(q, single_row = false, params=nil)
144
161
  response = _query(q, params)
145
162
  response.raise_error if response.error?
146
163
  single_row ? response.first_data : response
147
164
  end
148
165
 
166
+ def _query_entity_data(q, id=nil)
167
+ response = _query(q)
168
+ response.raise_error if response.error?
169
+ response.entity_data(id)
170
+ end
171
+
149
172
  def _query(q, params=nil)
150
173
  curr_tx = Neo4j::Transaction.current
151
174
  if (curr_tx)
@@ -1,9 +1,10 @@
1
1
  module Neo4j::Server
2
2
  class CypherTransaction
3
- attr_reader :commit_url, :exec_url
4
-
5
- include Resource
3
+ include Neo4j::Transaction::Instance
6
4
  include Neo4j::Core::CypherTranslator
5
+ include Resource
6
+
7
+ attr_reader :commit_url, :exec_url
7
8
 
8
9
  class CypherError < StandardError
9
10
  attr_reader :code, :status
@@ -20,7 +21,7 @@ module Neo4j::Server
20
21
  @exec_url = response.headers['location']
21
22
  init_resource_data(response, url)
22
23
  expect_response_code(response,201)
23
- Neo4j::Transaction.register(self)
24
+ register_instance
24
25
  end
25
26
 
26
27
  def _query(cypher_query, params=nil)
@@ -41,7 +42,10 @@ module Neo4j::Server
41
42
  end
42
43
  end
43
44
  response = @endpoint.post(@exec_url, headers: resource_headers, body: body.to_json)
45
+ _create_cypher_response(response)
46
+ end
44
47
 
48
+ def _create_cypher_response(response)
45
49
  first_result = response['results'][0]
46
50
  cr = CypherResponse.new(response, true)
47
51
 
@@ -54,29 +58,18 @@ module Neo4j::Server
54
58
  cr
55
59
  end
56
60
 
57
- def success
58
- # this is need in the Java API
59
- end
60
61
 
61
- def failure
62
- @failure = true
63
- end
64
62
 
65
- def failure?
66
- !!@failure
63
+ def _delete_tx
64
+ response = @endpoint.delete(@exec_url, headers: resource_headers)
65
+ expect_response_code(response,200)
66
+ response
67
67
  end
68
68
 
69
- def finish
70
- Neo4j::Transaction.unregister(self)
71
- if failure?
72
- response = @endpoint.delete(@exec_url, headers: resource_headers)
73
- else
74
- response = @endpoint.post(@commit_url, headers: resource_headers)
75
- end
69
+ def _commit_tx
70
+ response = @endpoint.post(@commit_url, headers: resource_headers)
76
71
  expect_response_code(response,200)
77
72
  response
78
73
  end
79
-
80
-
81
74
  end
82
75
  end
data/lib/neo4j/node.rb CHANGED
@@ -31,6 +31,11 @@ module Neo4j
31
31
  raise 'not implemented'
32
32
  end
33
33
 
34
+ # Refresh the properties by reading it from the database again next time an property value is requested.
35
+ def refresh
36
+ raise 'not implemented'
37
+ end
38
+
34
39
  # Updates the properties, keeps old properties
35
40
  # @param [Hash<Symbol, Object>] properties hash of properties that should be updated on the node
36
41
  def update_props(properties)
@@ -191,12 +196,6 @@ module Neo4j
191
196
  session.load_node(neo_id)
192
197
  end
193
198
 
194
- # Checks if the given entity node or entity id (Neo4j::Node#neo_id) exists in the database.
195
- # @return [true, false] if exist
196
- def exist?(entity_or_entity_id, session = Neo4j::Session.current!)
197
- session.node_exist?(neo_id)
198
- end
199
-
200
199
  # Find the node with given label and value
201
200
  def find_nodes(label, value=nil, session = Neo4j::Session.current!)
202
201
  session.find_nodes(label, value)
@@ -205,7 +204,6 @@ module Neo4j
205
204
 
206
205
  def initialize
207
206
  raise "Can't instantiate abstract class" if abstract_class?
208
- puts "Instantiated!"
209
207
  end
210
208
 
211
209
  private
@@ -152,6 +152,11 @@ module Neo4j
152
152
  end
153
153
 
154
154
  def load(neo_id, session = Neo4j::Session.current)
155
+ rel = _load(neo_id, session)
156
+ rel && rel.wrapper
157
+ end
158
+
159
+ def _load(neo_id, session = Neo4j::Session.current)
155
160
  session.load_relationship(neo_id)
156
161
  end
157
162
 
data/lib/neo4j/session.rb CHANGED
@@ -28,6 +28,11 @@ module Neo4j
28
28
  raise "not impl."
29
29
  end
30
30
 
31
+ # @return [:embedded_db | :server_db]
32
+ def db_type
33
+ raise "not impl."
34
+ end
35
+
31
36
  def auto_commit?
32
37
  true # TODO
33
38
  end
@@ -106,6 +111,17 @@ module Neo4j
106
111
  @@current_session = session
107
112
  end
108
113
 
114
+ # Registers a callback which will be called immediately if session is already available,
115
+ # or called when it later becomes available.
116
+ def on_session_available(&callback)
117
+ if (Neo4j::Session.current)
118
+ callback.call(Neo4j::Session.current)
119
+ end
120
+ add_listener do |event, data|
121
+ callback.call(data) if event == :session_available
122
+ end
123
+ end
124
+
109
125
  def add_listener(&listener)
110
126
  self._listeners << listener
111
127
  end
@@ -0,0 +1,202 @@
1
+ module Neo4j
2
+ class Session
3
+
4
+ @@current_session = nil
5
+ @@all_sessions = {}
6
+ @@factories = {}
7
+
8
+ # @abstract
9
+ def close
10
+ self.class.unregister(self)
11
+ end
12
+
13
+ # Only for embedded database
14
+ # @abstract
15
+ def start
16
+ raise "not impl."
17
+ end
18
+
19
+ # Only for embedded database
20
+ # @abstract
21
+ def shutdown
22
+ raise "not impl."
23
+ end
24
+
25
+ # Only for embedded database
26
+ # @abstract
27
+ def running
28
+ raise "not impl."
29
+ end
30
+
31
+ def auto_commit?
32
+ true # TODO
33
+ end
34
+
35
+ # @abstract
36
+ def begin_tx
37
+ raise "not impl."
38
+ end
39
+
40
+ class CypherError < StandardError
41
+ attr_reader :error_msg, :error_status, :error_code
42
+ def initialize(error_msg, error_code, error_status)
43
+ super(error_msg)
44
+ @error_msg = error_msg
45
+ @error_status = error_status
46
+ end
47
+ end
48
+
49
+ # Executes a Cypher Query.
50
+ # Returns an enumerable of hash values where each hash corresponds to a row unless +return+ or +map_return+
51
+ # is not an array. The search result can be mapped to Neo4j::Node or Neo4j::Relationship is your own Ruby wrapper class
52
+ # by specifying a map_return parameter.
53
+ #
54
+ # @param [Hash, String] q the cypher query, as a pure string query or a hash which will generate a cypher string.
55
+ # @option q [Hash] :params cypher parameters
56
+ # @option q [Symbol,Hash] :label the label to match. You can specify several labels by using a hash of variable names and labels.
57
+ # @option q [Symbol] :conditions key and value of properties which the label nodes must match
58
+ # @option q [Hash] :conditions key and value of properties which the label nodes must match
59
+ # @option q [String, Array] :match the cypher match clause
60
+ # @option q [String, Array] :where the cypher where clause
61
+ # @option q [String, Array, Symbol] :return the cypher where clause
62
+ # @option q [String, Hash, Symbol] :map_return mapping of the returned values, e.g. :id_to_node, :id_to_rel, or :value
63
+ # @option q [Hash<Symbol, Proc>] :map_return_procs custom mapping functions of :map_return types
64
+ # @option q [String,Symbol,Array<Hash>] :order the order
65
+ # @option q [Fixnum] :limit enables the return of only subsets of the total result.
66
+ # @option q [Fixnum] :skip enables the return of only subsets of the total result.
67
+ # @return [Enumerable] the result, an enumerable of Neo4j::Node objects unless a pure cypher string is given or return/map_returns is specified, see examples.
68
+ # @raise CypherError if invalid cypher
69
+ # @example Cypher String and parameters
70
+ # Neo4j::Session.query("START n=node({p}) RETURN ID(n)", params: {p: 42})
71
+ #
72
+ # @example label
73
+ # # If there is no :return parameter it will try to return Neo4j::Node objects
74
+ # # Default parameter is :n in the generated cypher
75
+ # Neo4j::Session.query(label: :person) # => MATCH (n:`person`) RETURN ID(n) # or RETURN n for embedded
76
+ #
77
+ # @example to_s
78
+ # # What Cypher is returned ? check with to_s
79
+ # Neo4j::Session.query(label: :person).to_s # =>
80
+ #
81
+ # @example return
82
+ # Neo4j::Session.query(label: :person, return: :age) # returns age properties
83
+ # Neo4j::Session.query(label: :person, return: [:name, :age]) # returns a hash of name and age properties
84
+ # Neo4j::Session.query(label: :person, return: 'count(n) AS c')
85
+ #
86
+ # @example map_return - an Enumerable of names (String)
87
+ # Neo4j::Session.query("START n=node(42) RETURN n.name", map_return: :value)
88
+ #
89
+ # @example map_return - Enumerable of an Hash with name property, Neo4j::Relationship and Neo4j::Node as values
90
+ # Neo4j::Session.query("START n=node(42) MATCH n-[r]->[x] RETURN n.name as N, ID(r) as R, ID(x) as X",
91
+ # map_return: {N: :value, R: :id_to_rel, X: :id_to_node})
92
+ #
93
+ # @example map_return, only for embedded_db, to_rel, and to_node allows direct mapping to Neo4j::Node and Neo4j::Relationship without ID(n)
94
+ # Neo4j::Session.query("START n=node(42) MATCH n-[r]->[x] RETURN n.name as N, r, x", map_return: {N: :value, r: :to_rel, x: :to_node})
95
+ #
96
+ # @example map_return_procs, custom mapping function
97
+ # Neo4j::Session.query(label: :person, map_return: :age_times_two, map_return_procs: {age_times_two: ->(row){(row[:age] || 0) * 2}})
98
+ #
99
+ # @example match
100
+ # Neo4j::Session.query(label: :person, match: 'n--m')
101
+ #
102
+ # @example where
103
+ # Neo4j::Session.query(label: :person, where: 'n.age > 40')
104
+ # Neo4j::Session.query(label: :person, where: 'n.age > {age}', params: {age: 40})
105
+ #
106
+ # @example condition
107
+ # Neo4j::Session.query(label: :person, conditions: {age: 42})
108
+ # Neo4j::Session.query(label: :person, conditions: {name: /foo?bar.*/})
109
+ #
110
+ # @see http://docs.neo4j.org/chunked/milestone/cypher-query-lang.html The Cypher Query Language Documentation
111
+ # @note Returns a read-once only forward iterable for the embedded database.
112
+ #
113
+ def query(q)
114
+ raise 'not implemented, abstract'
115
+ end
116
+
117
+ # Same as #query but does not accept an DSL and returns the raw result from the database.
118
+ # Notice, it might return different values depending on which database is used, embedded or server.
119
+ # @abstract
120
+ def _query(*params)
121
+ raise 'not implemented'
122
+ end
123
+
124
+ class << self
125
+ # Creates a new session to Neo4j
126
+ # @see also Neo4j::Server::CypherSession#open for :server_db params
127
+ # @param db_type the type of database, e.g. :embedded_db, or :server_db
128
+ def open(db_type=:server_db, *params)
129
+ register(create_session(db_type, params))
130
+ end
131
+
132
+ def open_named(db_type, name, default = nil, *params)
133
+ raise "Multiple sessions is currently only supported for Neo4j Server connections." unless db_type == :server_db
134
+ register(create_session(db_type, params), name, default)
135
+ end
136
+
137
+ def create_session(db_type, params = {})
138
+ unless (@@factories[db_type])
139
+ raise "Can't connect to database '#{db_type}', available #{@@factories.keys.join(',')}"
140
+ end
141
+ @@factories[db_type].call(*params)
142
+ end
143
+
144
+ def current
145
+ @@current_session
146
+ end
147
+
148
+ # @see Neo4j::Session#query
149
+ def query(*params)
150
+ current.query(*params)
151
+ end
152
+
153
+ def named(name)
154
+ @@all_sessions[name] || raise("No session named #{name}.")
155
+ end
156
+
157
+ def set_current(session)
158
+ @@current_session = session
159
+ end
160
+
161
+ def add_listener(&listener)
162
+ self._listeners << listener
163
+ end
164
+
165
+ def _listeners
166
+ @@listeners ||= []
167
+ @@listeners
168
+ end
169
+
170
+ def _notify_listeners(event, data)
171
+ _listeners.each {|li| li.call(event, data)}
172
+ end
173
+
174
+ def register(session, name = nil, default = nil)
175
+ if default == true
176
+ set_current(session)
177
+ elsif default.nil?
178
+ set_current(session) unless @@current_session
179
+ end
180
+ @@all_sessions[name] = session if name
181
+ @@current_session
182
+ end
183
+
184
+ def unregister(session)
185
+ @@current_session = nil if @@current_session == session
186
+ end
187
+
188
+ def all_sessions
189
+ @@all_sessions
190
+ end
191
+
192
+ def inspect
193
+ "Neo4j::Session available: #{@@factories && @@factories.keys}"
194
+ end
195
+
196
+ def register_db(db, &session_factory)
197
+ puts "replace factory for #{db}" if @@factories[db]
198
+ @@factories[db] = session_factory
199
+ end
200
+ end
201
+ end
202
+ end