Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bls neptune spike #17

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
7 changes: 2 additions & 5 deletions lib/neo4j/http/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,8 @@ def initialize(options = ENV)
def transaction_path
# v3.5 - /db/data/transaction/commit
# v4.x - /db/#{database_name}/tx/commit
if database_name
"/db/#{database_name}/tx/commit"
else
"/db/data/transaction/commit"
end
# https://docs.aws.amazon.com/neptune/latest/userguide/access-graph-opencypher-queries.html
"/openCypher"
end

def auth_token
Expand Down
42 changes: 16 additions & 26 deletions lib/neo4j/http/cypher_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,17 @@ def execute_cypher(cypher, parameters = {})
# for improved routing performance on read only queries
access_mode = parameters.delete(:access_mode) || @configuration.access_mode

# https://docs.aws.amazon.com/neptune/latest/userguide/opencypher-parameterized-queries.html
request_body = {
statements: [
{
statement: cypher,
parameters: parameters.as_json
}
]
query: cypher,
parameters: parameters.as_json
}

@connection = @injected_connection || connection(access_mode)
response = @connection.post(transaction_path, request_body)
results = check_errors!(cypher, response, parameters)

Neo4j::Http::Results.parse(results&.first || {})
Neo4j::Http::Results.parse(results || [])
end

def connection(access_mode)
Expand All @@ -49,24 +46,21 @@ def connection(access_mode)
delegate :auth_token, :transaction_path, to: :@configuration
def check_errors!(cypher, response, parameters)
raise Neo4j::Http::Errors::InvalidConnectionUrl, response.status if response.status == 404
if response.body["errors"].any? { |error| error["message"][/Routing WRITE queries is not supported/] }
raise Neo4j::Http::Errors::ReadOnlyError
end
# Neptune error format:
# {"requestId":"c1470a57-7bd0-4b88-b86b-71e64e7e6a2d","code":"MalformedQueryException",
# "detailedMessage":"It is not allowed to refer to variables in LIMIT (line 1, column 54 (offset: 53))",
# "message":"It is not allowed to refer to variables in LIMIT (line 1, column 54 (offset: 53))"}

body = response.body || {}
errors = body.fetch("errors", [])
return body.fetch("results", {}) unless errors.present?
error = body.fetch("message", [])
return body.fetch("results", {}) unless error.present?

error = errors.first
raise_error(error, cypher, parameters)
end

def raise_error(error, cypher, parameters = {})
code = error["code"]
message = error["message"]
klass = find_error_class(code)
parameters = JSON.pretty_generate(parameters.as_json)
raise klass, "#{code} - #{message}\n cypher: #{cypher} \n parameters given: \n#{parameters}"
raise Neo4j::Http::Errors::Neo4jCodedError, "#{error}\n cypher: #{cypher} \n parameters given: \n#{parameters}"
end

def find_error_class(code)
Expand All @@ -77,8 +71,9 @@ def find_error_class(code)

def build_connection(access_mode)
# https://neo4j.com/docs/http-api/current/actions/transaction-configuration/
headers = build_http_headers.merge({"access-mode" => access_mode})
Faraday.new(url: @configuration.uri, headers: headers, request: build_request_options) do |f|
headers = build_http_headers
# TODO we disable SSL verification for development env
Faraday.new(url: @configuration.uri, headers: headers, request: build_request_options, ssl: { verify: false }) do |f|
f.request :json # encode req bodies as JSON
f.request :retry # retry transient failures
f.response :json # decode response bodies as JSON
Expand All @@ -95,16 +90,11 @@ def build_request_options
end

def build_http_headers
{
"User-Agent" => @configuration.user_agent,
"Accept" => "application/json"
}.merge(authentication_headers)
return {}
end

def authentication_headers
return {} if auth_token.blank?

{"Authentication" => "Basic #{auth_token}"}
return {}
end
end
end
Expand Down
31 changes: 14 additions & 17 deletions lib/neo4j/http/results.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,23 @@
module Neo4j
module Http
class Results
# Example result set:
# [{"columns"=>["n"],
# "data"=>
# [{"row"=>[{"name"=>"Foo", "uuid"=>"8c7dcfda-d848-4937-a91a-2e6debad2dd6"}],
# "meta"=>[{"id"=>242, "type"=>"node", "deleted"=>false}]}]}]
#
def self.parse(results)
columns = results["columns"]
data = results["data"]
if results.is_a?(Array)
# Recuse on arrays
return results.map { |a| self.parse(a) }
elsif results.is_a?(Hash)
# Recurse on hashes
# Hoist ~properties key
new_hash = {}
results = results.select {|k, _| k[0] != "~" }.merge(results["~properties"]) if results.key?("~properties")

data.map do |result|
row = result["row"] || []
meta = result["meta"] || []
compacted_data = row.each_with_index.map do |attributes, index|
row_meta = meta[index] || {}
attributes["_neo4j_meta_data"] = row_meta if attributes.is_a?(Hash)
attributes
results.each_pair do |k,v|
new_hash[k] = self.parse(v)
end

columns.zip(compacted_data).to_h.with_indifferent_access
return new_hash
else
# Primative value
return results
end
end
end
Expand Down
136 changes: 136 additions & 0 deletions spec/neo4j/http/results_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# frozen_string_literal: true

require "spec_helper"

# Examples taken from
# https://docs.aws.amazon.com/neptune/latest/userguide/access-graph-opencypher-queries.html
RSpec.describe Neo4j::Http::Results, type: :uses_neo4j do
it "returns node response" do
results = JSON.parse %q(
{
"results": [
{
"a": {
"~id": "22",
"~entityType": "node",
"~labels": [
"airport"
],
"~properties": {
"desc": "Seattle-Tacoma",
"lon": -122.30899810791,
"runways": 3,
"type": "airport",
"country": "US",
"region": "US-WA",
"lat": 47.4490013122559,
"elev": 432,
"city": "Seattle",
"icao": "KSEA",
"code": "SEA",
"longest": 11901
}
}
}
]
}
)

output = described_class.parse(results["results"])
expected = {
"desc" => "Seattle-Tacoma",
"lon" => -122.30899810791,
"runways" => 3,
"type" => "airport",
"country" => "US",
"region" => "US-WA",
"lat" => 47.4490013122559,
"elev" => 432,
"city" => "Seattle",
"icao" => "KSEA",
"code" => "SEA",
"longest" => 11901,
}
expect(output[0]["a"]).to match(expected)
end

it "returns relationship response" do
results = JSON.parse %q(
{
"results": [
{
"r": {
"~id": "7389",
"~entityType": "relationship",
"~start": "22",
"~end": "151",
"~type": "route",
"~properties": {
"dist": 956
}
}
}
]
}
)

output = described_class.parse(results["results"])
expected = {
"dist"=>956,
}
expect(output[0]["r"]).to eq(expected)
end

it "returns value response" do
results = JSON.parse %q(
{
"results": [
{
"count(a)": 121
}
]
}
)

output = described_class.parse(results["results"])
expect(output[0]["count(a)"]).to eq(121)
end

it "returns user network query results" do
results = [
{
"colleagues" => [
[{
"~id" => "457e8e96-3cbc-4122-97a5-4dc9fe32dbc4",
"~entityType" => "node",
"~labels" => ["User"],
"~properties" => {
"name" => "Dolores Abernathy", "uuid" => "USER", "specialty" => "Immunology", "city" => "Westworld", "state" => "Utah"
}
}, {
"~id" => "387fb59e-9e7d-4507-ba17-7de691d3e13e",
"~entityType" => "relationship",
"~start" => "457e8e96-3cbc-4122-97a5-4dc9fe32dbc4",
"~end" => "48e1f64c-48d2-421f-8b10-38ebd58df761",
"~type" => "COLLEAGUES_WITH",
"~properties" => {
"state" => "invited"
}
}, {
"~id" => "48e1f64c-48d2-421f-8b10-38ebd58df761",
"~entityType" => "node",
"~labels" => ["User"],
"~properties" => {
"name" => "Maeve Millay", "uuid" => "OTHER-USER", "specialty" => "Immunology", "city" => "Westworld", "state" => "Utah"
}
}]
], "medschool_classmates" => nil, "co_residents" => nil, "co_workers" => nil, "investigator_of" => nil, "full_name" => "Maeve Millay", "co_fellows" => nil, "co_authors" => nil, "paschool_classmates" => nil, "user_uuid" => "OTHER-USER"
}
]

output = described_class.parse(results)
expect(output[0]["colleagues"][0][0]["name"]).to eq("Dolores Abernathy")
expect(output[0]["colleagues"][0][1]["state"]).to eq("invited")
expect(output[0]["colleagues"][0][2]["name"]).to eq("Maeve Millay")
end
end