neo4j-core 4.0.7 → 5.0.0

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ext/kernel.rb +9 -0
  3. data/lib/neo4j/label.rb +2 -1
  4. data/lib/neo4j/node.rb +8 -11
  5. data/lib/neo4j/property_container.rb +2 -7
  6. data/lib/neo4j/property_validator.rb +1 -1
  7. data/lib/neo4j/session.rb +24 -11
  8. data/lib/neo4j/tasks/config_server.rb +4 -1
  9. data/lib/neo4j/tasks/neo4j_server.rake +86 -109
  10. data/lib/neo4j/transaction.rb +17 -16
  11. data/lib/neo4j-core/cypher_translator.rb +1 -1
  12. data/lib/neo4j-core/query.rb +103 -47
  13. data/lib/neo4j-core/query_clauses.rb +180 -112
  14. data/lib/neo4j-core/query_find_in_batches.rb +19 -11
  15. data/lib/neo4j-core/version.rb +1 -1
  16. data/lib/neo4j-core.rb +3 -0
  17. data/lib/neo4j-embedded/cypher_response.rb +20 -5
  18. data/lib/neo4j-embedded/embedded_ha_session.rb +9 -2
  19. data/lib/neo4j-embedded/embedded_impermanent_session.rb +1 -1
  20. data/lib/neo4j-embedded/embedded_node.rb +26 -28
  21. data/lib/neo4j-embedded/embedded_session.rb +14 -7
  22. data/lib/neo4j-embedded/embedded_transaction.rb +2 -2
  23. data/lib/neo4j-embedded/label.rb +65 -0
  24. data/lib/neo4j-embedded/property.rb +5 -5
  25. data/lib/neo4j-embedded/to_java.rb +7 -13
  26. data/lib/neo4j-embedded.rb +1 -0
  27. data/lib/neo4j-server/cypher_node.rb +57 -67
  28. data/lib/neo4j-server/cypher_node_uncommited.rb +1 -1
  29. data/lib/neo4j-server/cypher_relationship.rb +10 -6
  30. data/lib/neo4j-server/cypher_response.rb +96 -52
  31. data/lib/neo4j-server/cypher_session.rb +80 -93
  32. data/lib/neo4j-server/cypher_transaction.rb +42 -33
  33. data/lib/neo4j-server/label.rb +40 -0
  34. data/lib/neo4j-server/resource.rb +11 -12
  35. data/lib/neo4j-server.rb +2 -0
  36. data/neo4j-core.gemspec +3 -0
  37. metadata +47 -3
  38. data/lib/neo4j-core/graph_json.rb +0 -35
@@ -36,54 +36,87 @@ module Neo4j
36
36
  "Enumerable query: '#{@query}'"
37
37
  end
38
38
 
39
- def each(&block)
39
+ def each
40
40
  @response.each_data_row do |row|
41
- yield(row.each_with_index.each_with_object(struct.new) do |(value, i), result|
42
- result[columns[i].to_sym] = value
43
- end)
41
+ yield struct_rows(row)
42
+ end
43
+ end
44
+
45
+ def struct_rows(row)
46
+ struct.new.tap do |result|
47
+ row.each_with_index { |value, i| result[columns[i]] = value }
44
48
  end
45
49
  end
46
50
  end
47
51
 
48
- def to_struct_enumeration(cypher = '')
52
+ EMPTY_STRING = ''
53
+ def to_struct_enumeration(cypher = EMPTY_STRING)
49
54
  HashEnumeration.new(self, cypher)
50
55
  end
51
56
 
52
- def to_node_enumeration(cypher = '', session = Neo4j::Session.current)
57
+ def to_node_enumeration(cypher = EMPTY_STRING, session = Neo4j::Session.current)
53
58
  Enumerator.new do |yielder|
54
59
  @result_index = 0
55
60
  to_struct_enumeration(cypher).each do |row|
56
61
  @row_index = 0
57
- yielder << row.each_pair.each_with_object(@struct.new) do |(column, value), result|
58
- result[column] = map_row_value(value, session)
59
- @row_index += 1
60
- end
62
+ yielder << row_pair_in_struct(row, session)
61
63
  @result_index += 1
62
64
  end
63
65
  end
64
66
  end
65
67
 
68
+ def row_pair_in_struct(row, session)
69
+ @struct.new.tap do |result|
70
+ row.each_pair do |column, value|
71
+ result[column] = map_row_value(value, session)
72
+ @row_index += 1
73
+ end
74
+ end
75
+ end
76
+
66
77
  def map_row_value(value, session)
67
- if value.is_a?(Hash)
78
+ if value.is_a?(Hash) && looks_like_an_object?(value)
68
79
  hash_value_as_object(value, session)
69
80
  elsif value.is_a?(Array)
70
- value.map { |v| map_row_value(v, session) }
81
+ value.map! { |v| map_row_value(v, session) }
71
82
  else
72
83
  value
73
84
  end
74
85
  end
75
86
 
76
87
  def hash_value_as_object(value, session)
77
- return value unless value['labels'] || value['type'] || transaction_response?
88
+ data = case
89
+ when transaction_response?
90
+ add_transaction_entity_id
91
+ mapped_rest_data
92
+ when value[:labels] || value[:type]
93
+ add_entity_id(value)
94
+ value
95
+ else
96
+ return value
97
+ end
98
+ basic_obj = (node?(value) ? CypherNode : CypherRelationship).new(session, data)
99
+ unwrapped? ? basic_obj : basic_obj.wrapper
100
+ end
101
+
102
+ def looks_like_an_object?(value)
103
+ if transaction_response?
104
+ mapped_rest_data[:outgoing_relationships] || (mapped_rest_data[:start] && mapped_rest_data[:properties])
105
+ else
106
+ value[:labels] || value[:type]
107
+ end
108
+ end
109
+
110
+ def unwrapped!
111
+ @_unwrapped_obj = true
112
+ end
78
113
 
79
- is_node, data = if transaction_response?
80
- add_transaction_entity_id
81
- [!mapped_rest_data['start'], mapped_rest_data]
82
- elsif value['labels'] || value['type']
83
- add_entity_id(value)
84
- [value['labels'], value]
85
- end
86
- (is_node ? CypherNode : CypherRelationship).new(session, data).wrapper
114
+ def unwrapped?
115
+ !!@_unwrapped_obj
116
+ end
117
+
118
+ def node?(value)
119
+ transaction_response? ? !mapped_rest_data[:start] : value[:labels]
87
120
  end
88
121
 
89
122
  attr_reader :struct
@@ -95,17 +128,17 @@ module Neo4j
95
128
 
96
129
  def entity_data(id = nil)
97
130
  if @uncommited
98
- data = @data.first['row'].first
99
- data.is_a?(Hash) ? {'data' => data, 'id' => id} : data
131
+ data = @data.first[:row].first
132
+ data.is_a?(Hash) ? {data: data, id: id} : data
100
133
  else
101
134
  data = @data[0][0]
102
135
  data.is_a?(Hash) ? add_entity_id(data) : data
103
136
  end
104
137
  end
105
138
 
106
- def first_data(id = nil)
139
+ def first_data
107
140
  if @uncommited
108
- data = @data.first['row'].first
141
+ @data.first[:row].first
109
142
  # data.is_a?(Hash) ? {'data' => data, 'id' => id} : data
110
143
  else
111
144
  data = @data[0][0]
@@ -114,45 +147,57 @@ module Neo4j
114
147
  end
115
148
 
116
149
  def add_entity_id(data)
117
- data.merge!('id' => self.class.id_from_url(data['self']))
150
+ data[:id] = if data[:metadata] && data[:metadata][:id]
151
+ data[:metadata][:id]
152
+ else
153
+ self.class.id_from_url(data[:self])
154
+ end
155
+ data
118
156
  end
119
157
 
120
158
  def add_transaction_entity_id
121
- mapped_rest_data.merge!('id' => mapped_rest_data['self'].split('/').last.to_i)
159
+ mapped_rest_data[:id] = mapped_rest_data[:self].split('/').last.to_i
160
+ mapped_rest_data
122
161
  end
123
162
 
124
163
  def error?
125
164
  !!@error
126
165
  end
127
166
 
167
+ RETRYABLE_ERROR_STATUSES = %w(DeadlockDetectedException AcquireLockTimeoutException ExternalResourceFailureException UnknownFailureException)
168
+ def retryable_error?
169
+ return unless error?
170
+ RETRYABLE_ERROR_STATUSES.include?(@error_status)
171
+ end
172
+
128
173
  def data?
129
- !response.body['data'].nil?
174
+ !response.body[:data].nil?
130
175
  end
131
176
 
132
177
  def raise_unless_response_code(code)
133
- fail "Response code #{response.status}, expected #{code} for #{response.headers['location']}, #{response.body}" unless response.status == code
178
+ fail "Response code #{response.status}, expected #{code} for #{response.headers[:location]}, #{response.body}" unless response.status == code
134
179
  end
135
180
 
136
181
  def each_data_row
137
182
  if @uncommited
138
- data.each { |r| yield r['row'] }
183
+ data.each { |r| yield r[:row] }
139
184
  else
140
185
  data.each { |r| yield r }
141
186
  end
142
187
  end
143
188
 
144
- def set_data(data, columns)
145
- @data = data
146
- @columns = columns
147
- @struct = columns.empty? ? Object.new : Struct.new(*columns.map(&:to_sym))
189
+ def set_data(response)
190
+ @data = response[:data]
191
+ @columns = response[:columns]
192
+ @struct = @columns.empty? ? Object.new : Struct.new(*@columns.map(&:to_sym))
148
193
  self
149
194
  end
150
195
 
151
- def set_error(error_msg, error_status, error_core)
196
+ def set_error(error)
152
197
  @error = true
153
- @error_msg = error_msg
154
- @error_status = error_status
155
- @error_code = error_core
198
+ @error_msg = error[:message]
199
+ @error_status = error[:status] || error[:exception] || error[:code]
200
+ @error_code = error[:code] || error[:fullname]
156
201
  self
157
202
  end
158
203
 
@@ -170,9 +215,9 @@ module Neo4j
170
215
  def self.create_with_no_tx(response)
171
216
  case response.status
172
217
  when 200
173
- CypherResponse.new(response).set_data(response.body['data'], response.body['columns'])
218
+ new(response).set_data(response.body)
174
219
  when 400
175
- CypherResponse.new(response).set_error(response.body['message'], response.body['exception'], response.body['fullname'])
220
+ new(response).set_error(response.body)
176
221
  else
177
222
  fail "Unknown response code #{response.status} for #{response.env[:url]}"
178
223
  end
@@ -181,20 +226,18 @@ module Neo4j
181
226
  def self.create_with_tx(response)
182
227
  fail "Unknown response code #{response.status} for #{response.request_uri}" unless response.status == 200
183
228
 
184
- first_result = response.body['results'][0]
185
- cr = CypherResponse.new(response, true)
186
-
187
- if response.body['errors'].empty?
188
- cr.set_data(first_result['data'], first_result['columns'])
189
- else
190
- first_error = response.body['errors'].first
191
- cr.set_error(first_error['message'], first_error['status'], first_error['code'])
229
+ new(response, true).tap do |cr|
230
+ body = response.body
231
+ if body[:errors].empty?
232
+ cr.set_data(body[:results].first)
233
+ else
234
+ cr.set_error(body[:errors].first)
235
+ end
192
236
  end
193
- cr
194
237
  end
195
238
 
196
239
  def transaction_response?
197
- response.respond_to?('body') && !response.body['commit'].nil?
240
+ response.respond_to?(:body) && !response.body[:commit].nil?
198
241
  end
199
242
 
200
243
  def rest_data
@@ -203,7 +246,8 @@ module Neo4j
203
246
  end
204
247
 
205
248
  def rest_data_with_id
206
- rest_data.merge!('id' => mapped_rest_data['self'].split('/').last.to_i)
249
+ rest_data[:id] = mapped_rest_data[:self].split('/').last.to_i
250
+ rest_data
207
251
  end
208
252
 
209
253
  class << self
@@ -219,7 +263,7 @@ module Neo4j
219
263
  attr_reader :result_index
220
264
 
221
265
  def mapped_rest_data
222
- response.body['results'][0]['data'][result_index]['rest'][row_index]
266
+ response.body[:results][0][:data][result_index][:rest][row_index]
223
267
  end
224
268
  end
225
269
  end
@@ -1,8 +1,7 @@
1
1
  module Neo4j
2
2
  module Server
3
- # Plugin
4
- Neo4j::Session.register_db(:server_db) do |*url_opts|
5
- Neo4j::Server::CypherSession.open(*url_opts)
3
+ Neo4j::Session.register_db(:server_db) do |endpoint_url, url_opts|
4
+ Neo4j::Server::CypherSession.open(endpoint_url, url_opts)
6
5
  end
7
6
 
8
7
  class CypherSession < Neo4j::Session
@@ -26,9 +25,10 @@ module Neo4j
26
25
  init_params = params[:initialize] && params.delete(:initialize)
27
26
  conn = Faraday.new(url, init_params) do |b|
28
27
  b.request :basic_auth, params[:basic_auth][:username], params[:basic_auth][:password] if params[:basic_auth]
29
- b.request :json
28
+ b.request :multi_json
30
29
  # b.response :logger
31
- b.response :json, content_type: 'application/json'
30
+
31
+ b.response :multi_json, symbolize_keys: true, content_type: 'application/json'
32
32
  # b.use Faraday::Response::RaiseError
33
33
  b.use Faraday::Adapter::NetHttpPersistent
34
34
  # b.adapter Faraday.default_adapter
@@ -52,17 +52,14 @@ module Neo4j
52
52
  end
53
53
 
54
54
  def self.establish_session(root_data, connection)
55
- data_url = root_data['data']
55
+ data_url = root_data[:data]
56
56
  data_url << '/' unless data_url.nil? || data_url.end_with?('/')
57
57
  CypherSession.new(data_url, connection)
58
58
  end
59
59
 
60
60
  def self.extract_basic_auth(url, params)
61
61
  return unless url && URI(url).userinfo
62
- params[:basic_auth] = {
63
- username: URI(url).user,
64
- password: URI(url).password
65
- }
62
+ params[:basic_auth] = {username: URI(url).user, password: URI(url).password}
66
63
  end
67
64
 
68
65
  private_class_method :extract_basic_auth
@@ -80,12 +77,12 @@ module Neo4j
80
77
  end
81
78
 
82
79
  def version
83
- resource_data ? resource_data['neo4j_version'] : ''
80
+ resource_data ? resource_data[:neo4j_version] : ''
84
81
  end
85
82
 
86
83
  def initialize_resource(data_url)
87
84
  response = @connection.get(data_url)
88
- expect_response_code(response, 200)
85
+ expect_response_code!(response, 200)
89
86
  data_resource = response.body
90
87
  fail "No data_resource for #{response.body}" unless data_resource
91
88
  # store the resource data
@@ -98,43 +95,27 @@ module Neo4j
98
95
  end
99
96
 
100
97
  def begin_tx
101
- if Neo4j::Transaction.current
102
- # Handle nested transaction "placebo transaction"
103
- Neo4j::Transaction.current.push_nested!
104
- else
105
- wrap_resource(@connection)
106
- end
98
+ Neo4j::Transaction.current ? Neo4j::Transaction.current.push_nested! : wrap_resource(@connection)
107
99
  Neo4j::Transaction.current
108
100
  end
109
101
 
110
102
  def create_node(props = nil, labels = [])
111
- id = _query_or_fail(cypher_string(labels, props), true, cypher_prop_list(props))
112
- value = props.nil? ? id : {'id' => id, 'metadata' => {'labels' => labels}, 'data' => props}
103
+ id = _query_or_fail(cypher_string(labels, props), true, cypher_prop_list!(props))
104
+ value = props.nil? ? id : {id: id, metadata: {labels: labels}, data: props}
113
105
  CypherNode.new(self, value)
114
106
  end
115
107
 
116
108
  def load_node(neo_id)
117
- load_entity(CypherNode, _query("MATCH (n) WHERE ID(n) = #{neo_id} RETURN n"))
109
+ query.unwrapped.match(:n).where(n: {neo_id: neo_id}).pluck(:n).first
118
110
  end
119
111
 
120
112
  def load_relationship(neo_id)
121
- load_entity(CypherRelationship, _query("MATCH (n)-[r]-() WHERE ID(r) = #{neo_id} RETURN r"))
122
- end
123
-
124
- def load_entity(clazz, cypher_response)
125
- return nil if cypher_response.data.nil? || cypher_response.data[0].nil?
126
- data = if cypher_response.transaction_response?
127
- cypher_response.rest_data_with_id
128
- else
129
- cypher_response.first_data
130
- end
131
-
132
- if cypher_response.error?
133
- cypher_response.raise_error
134
- elsif cypher_response.error_msg =~ /not found/ # Ugly that the Neo4j API gives us this error message
135
- return nil
113
+ query.unwrapped.optional_match('(n)-[r]-()').where(r: {neo_id: neo_id}).pluck(:r).first
114
+ rescue Neo4j::Session::CypherError => cypher_error
115
+ if cypher_error.message.match(/not found$/)
116
+ nil
136
117
  else
137
- clazz.new(self, data)
118
+ raise cypher_error
138
119
  end
139
120
  end
140
121
 
@@ -143,17 +124,17 @@ module Neo4j
143
124
  end
144
125
 
145
126
  def uniqueness_constraints(label)
146
- schema_properties("#{@resource_url}schema/constraint/#{label}/uniqueness")
127
+ schema_properties("/db/data/schema/constraint/#{label}/uniqueness")
147
128
  end
148
129
 
149
130
  def indexes(label)
150
- schema_properties("#{@resource_url}schema/index/#{label}")
131
+ schema_properties("/db/data/schema/index/#{label}")
151
132
  end
152
133
 
153
134
  def schema_properties(query_string)
154
135
  response = @connection.get(query_string)
155
- expect_response_code(response, 200)
156
- {property_keys: response.body.map { |row| row['property_keys'].map(&:to_sym) }}
136
+ expect_response_code!(response, 200)
137
+ {property_keys: response.body.map! { |row| row[:property_keys].map(&:to_sym) }}
157
138
  end
158
139
 
159
140
  def find_all_nodes(label_name)
@@ -163,17 +144,15 @@ module Neo4j
163
144
  def find_nodes(label_name, key, value)
164
145
  value = "'#{value}'" if value.is_a? String
165
146
 
166
- response = _query_or_fail <<-CYPHER
167
- MATCH (n:`#{label_name}`)
168
- WHERE n.#{key} = #{value}
169
- RETURN ID(n)
170
- CYPHER
147
+ response = _query_or_fail("MATCH (n:`#{label_name}`) WHERE n.#{key} = #{value} RETURN ID(n)")
171
148
  search_result_to_enumerable_first_column(response)
172
149
  end
173
150
 
174
151
  def query(*args)
175
152
  if [[String], [String, Hash]].include?(args.map(&:class))
176
- query, params = args[0, 2]
153
+ query = args[0]
154
+ params = args[1]
155
+
177
156
  response = _query(query, params)
178
157
  response.raise_error if response.error?
179
158
  response.to_node_enumeration(query)
@@ -183,77 +162,85 @@ module Neo4j
183
162
  end
184
163
  end
185
164
 
186
- def _query_data(q)
187
- r = _query_or_fail(q, true)
188
- # the response is different if we have a transaction or not
189
- Neo4j::Transaction.current ? r : r['data']
165
+ def _query_data(query)
166
+ r = _query_or_fail(query, true)
167
+ Neo4j::Transaction.current ? r : r[:data]
190
168
  end
191
169
 
192
- def _query_or_fail(q, single_row = false, params = {})
193
- if q.is_a?(::Neo4j::Core::Query)
194
- cypher = q.to_cypher
195
- params = q.send(:merge_params).merge(params)
196
- q = cypher
170
+ DEFAULT_RETRY_COUNT = ENV['NEO4J_RETRY_COUNT'].nil? ? 10 : ENV['NEO4J_RETRY_COUNT'].to_i
171
+
172
+ def _query_or_fail(query, single_row = false, params = {}, retry_count = DEFAULT_RETRY_COUNT)
173
+ query, params = query_and_params(query, params)
174
+
175
+ response = _query(query, params)
176
+ if response.error?
177
+ _retry_or_raise(query, params, single_row, retry_count, response)
178
+ else
179
+ single_row ? response.first_data : response
197
180
  end
181
+ end
198
182
 
199
- response = _query(q, params)
200
- response.raise_error if response.error?
201
- single_row ? response.first_data : response
183
+ def query_and_params(query_or_query_string, params)
184
+ if query_or_query_string.is_a?(::Neo4j::Core::Query)
185
+ cypher = query_or_query_string.to_cypher
186
+ [cypher, query_or_query_string.send(:merge_params).merge(params)]
187
+ else
188
+ [query_or_query_string, params]
189
+ end
202
190
  end
203
191
 
204
- def _query_entity_data(q, id = nil, params = nil)
205
- response = _query(q, params)
206
- response.raise_error if response.error?
207
- response.entity_data(id)
192
+ def _retry_or_raise(query, params, single_row, retry_count, response)
193
+ response.raise_error unless response.retryable_error?
194
+ retry_count > 0 ? _query_or_fail(query, single_row, params, retry_count - 1) : response.raise_error
208
195
  end
209
196
 
210
- def _query(q, params = nil)
211
- # puts "q #{q}"
197
+ def _query_entity_data(query, id = nil, params = {})
198
+ _query_response(query, params).entity_data(id)
199
+ end
200
+
201
+ def _query_response(query, params = {})
202
+ _query(query, params).tap do |response|
203
+ response.raise_error if response.error?
204
+ end
205
+ end
206
+
207
+ def _query(query, params = {})
208
+ query, params = query_and_params(query, params)
209
+
212
210
  curr_tx = Neo4j::Transaction.current
213
211
  if curr_tx
214
- curr_tx._query(q, params)
212
+ curr_tx._query(query, params)
215
213
  else
216
- url = resource_url('cypher')
217
- q = params.nil? ? {'query' => q} : {'query' => q, 'params' => params}
218
- response = @connection.post(url, q)
214
+ url = resource_url(:cypher)
215
+ query = params.nil? ? {'query' => query} : {'query' => query, 'params' => params}
216
+ response = @connection.post(url, query)
219
217
  CypherResponse.create_with_no_tx(response)
220
218
  end
221
219
  end
222
220
 
223
221
  def search_result_to_enumerable_first_column(response)
224
222
  return [] unless response.data
225
- if Neo4j::Transaction.current
226
- search_result_to_enumerable_first_column_with_tx(response)
227
- else
228
- search_result_to_enumerable_first_column_without_tx(response)
229
- end
230
- end
231
223
 
232
- def search_result_to_enumerable_first_column_with_tx(response)
233
224
  Enumerator.new do |yielder|
234
225
  response.data.each do |data|
235
- data['row'].each do |id|
236
- yielder << CypherNode.new(self, id).wrapper
226
+ if Neo4j::Transaction.current
227
+ data[:row].each do |id|
228
+ yielder << CypherNode.new(self, id).wrapper
229
+ end
230
+ else
231
+ yielder << CypherNode.new(self, data[0]).wrapper
237
232
  end
238
233
  end
239
234
  end
240
235
  end
241
236
 
242
- def search_result_to_enumerable_first_column_without_tx(response)
243
- Enumerator.new do |yielder|
244
- response.data.each do |data|
245
- yielder << CypherNode.new(self, data[0]).wrapper
246
- end
247
- end
248
- end
249
-
250
- def map_column(key, map, data)
251
- if map[key] == :node
252
- CypherNode.new(self, data).wrapper
253
- elsif map[key] == :rel || map[:key] || :relationship
254
- CypherRelationship.new(self, data)
255
- else
256
- data
237
+ EMPTY = ''
238
+ def self.log_with
239
+ clear, yellow, cyan = %W(\e[0m \e[33m \e[36m)
240
+ ActiveSupport::Notifications.subscribe('neo4j.cypher_query') do |_, start, finish, _id, payload|
241
+ ms = (finish - start) * 1000
242
+ params_string = (payload[:params].size > 0 ? "| #{payload[:params].inspect}" : EMPTY)
243
+ yield(" #{cyan}#{payload[:context]}#{clear} #{yellow}#{ms.round}ms#{clear} #{payload[:cypher]} #{params_string}")
257
244
  end
258
245
  end
259
246
  end
@@ -2,9 +2,9 @@ module Neo4j
2
2
  module Server
3
3
  # The CypherTransaction object lifecycle is as follows:
4
4
  # * It is initialized with the transactional endpoint URL and the connection object to use for communication. It does not communicate with the server to create this.
5
- # * The first query within the transaction sets the commit and execution addresses, :commit_url and :exec_url.
5
+ # * The first query within the transaction sets the commit and execution addresses, :commit_url and :query_url.
6
6
  # * At any time, `failure` can be called to mark a transaction failed and trigger a rollback upon closure.
7
- # * `close` is called to end the transaction. It calls `_commit_tx` or `_delete_tx`.
7
+ # * `close` is called to end the transaction. It calls `commit` or `delete`.
8
8
  #
9
9
  # If a transaction is created and then closed without performing any queries, an OpenStruct is returned that behaves like a successfully closed query.
10
10
  class CypherTransaction
@@ -12,7 +12,7 @@ module Neo4j
12
12
  include Neo4j::Core::CypherTranslator
13
13
  include Resource
14
14
 
15
- attr_reader :commit_url, :exec_url, :base_url, :connection
15
+ attr_reader :commit_url, :query_url, :base_url, :connection
16
16
 
17
17
  def initialize(url, session_connection)
18
18
  @base_url = url
@@ -26,49 +26,58 @@ module Neo4j
26
26
  statement = {statement: cypher_query, parameters: params, resultDataContents: ROW_REST}
27
27
  body = {statements: [statement]}
28
28
 
29
- response = exec_url && commit_url ? connection.post(exec_url, body) : register_urls(body)
30
- _create_cypher_response(response)
29
+ response = @query_url ? query(body) : start(body)
30
+
31
+ create_cypher_response(response)
31
32
  end
32
33
 
33
- def _delete_tx
34
- _tx_query(:delete, exec_url, headers: resource_headers)
34
+ def start(body)
35
+ request(:post, @base_url, 201, body).tap do |response|
36
+ @commit_url = response.body[:commit]
37
+ @query_url = response.headers[:Location]
38
+
39
+ fail "NO ENDPOINT URL #{connection} : HEAD: #{response.headers.inspect}" if !@query_url || @query_url.empty?
40
+
41
+ init_resource_data(response.body, @base_url)
42
+ end
35
43
  end
36
44
 
37
- def _commit_tx
38
- _tx_query(:post, commit_url, nil)
45
+ def query(body)
46
+ request(:post, @query_url, 200, body)
39
47
  end
40
48
 
41
- private
49
+ def delete
50
+ return empty_response if !@commit_url || expired?
42
51
 
43
- def _tx_query(action, endpoint, headers = {})
44
- return empty_response if !commit_url || expired?
45
- response = connection.send(action, endpoint, headers)
46
- expect_response_code(response, 200)
47
- response
52
+ request(:delete, @query_url, 200, nil, resource_headers)
48
53
  end
49
54
 
50
- def register_urls(body)
51
- response = connection.post(base_url, body)
52
- @commit_url = response.body['commit']
53
- @exec_url = response.headers['Location']
54
- fail "NO ENDPOINT URL #{connection} : HEAD: #{response.headers.inspect}" if !exec_url || exec_url.empty?
55
- init_resource_data(response.body, base_url)
56
- expect_response_code(response, 201)
57
- response
55
+ def commit
56
+ return empty_response if !@commit_url || expired?
57
+
58
+ request(:post, @commit_url, 200, nil, resource_headers)
59
+ end
60
+
61
+ private
62
+
63
+ def request(action, endpoint_url, expected_code = 200, body = nil, headers = {})
64
+ connection.send(action, endpoint_url, body, headers).tap do |response|
65
+ expect_response_code!(response, expected_code)
66
+ end
58
67
  end
59
68
 
60
- def _create_cypher_response(response)
61
- first_result = response.body['results'][0]
69
+ def create_cypher_response(response)
70
+ first_result = response.body[:results][0]
62
71
 
63
- cr = CypherResponse.new(response, true)
64
- if response.body['errors'].empty?
65
- cr.set_data(first_result['data'], first_result['columns'])
66
- else
67
- first_error = response.body['errors'].first
68
- expired if first_error['message'].match(/Unrecognized transaction id/)
69
- cr.set_error(first_error['message'], first_error['code'], first_error['code'])
72
+ CypherResponse.new(response, true).tap do |cypher_response|
73
+ if response.body[:errors].empty?
74
+ cypher_response.set_data(first_result)
75
+ else
76
+ first_error = response.body[:errors].first
77
+ mark_expired if first_error[:message].match(/Unrecognized transaction id/)
78
+ cypher_response.set_error(first_error)
79
+ end
70
80
  end
71
- cr
72
81
  end
73
82
 
74
83
  def empty_response