Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/dev_manual_todo_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

result = driver.execute_query(
'MATCH (p:Person {age: $age}) RETURN p.name AS name',
nil, # auth_token not specified - nil may be omitted
{ database: 'neo4j' }, # default value may be omitted
nil, # auth_token - positional argument can't be omitted when config is provided
{ database: 'neo4j' }, # config - default value may be omitted
age: 42
)

Expand All @@ -26,7 +26,7 @@
# Create two nodes and a relationship
result = driver.execute_query(
'CREATE (a:Person {name: $name})
CREATE (b:Person {friend: $name})
CREATE (b:Person {name: $friend})
CREATE (a)-[:KNOWS]->(b)',
name: 'Alice',
friend: 'David'
Expand Down Expand Up @@ -104,6 +104,7 @@
# Database selection
driver.execute_query(
'MATCH (p:Person) RETURN p.name',
nil,
{ database: 'neo4j' },
age: 42
)
Expand Down Expand Up @@ -157,7 +158,7 @@
puts notifications

# Notifications with GQL status codes
result = execute_query(
result = driver.execute_query(
"MATCH p=shortestPath((:Person {name: $start})-[*]->(:Person {name: $end}))
RETURN p",
start: 'Alice',
Expand Down
2 changes: 2 additions & 0 deletions jruby/neo4j/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module Driver

include_package 'org.neo4j.driver'

EagerResult = Java::OrgNeo4jDriverInternal::EagerResultValue
Record = Java::OrgNeo4jDriverInternal::InternalRecord
Result = Java::OrgNeo4jDriverInternal::InternalResult
Transaction = Java::OrgNeo4jDriverInternal::InternalTransaction
Expand Down Expand Up @@ -39,6 +40,7 @@ module Types
Java::OrgNeo4jDriver::Bookmark.singleton_class.prepend Neo4j::Driver::Ext::Bookmark::ClassMethods
Java::OrgNeo4jDriver::GraphDatabase.singleton_class.prepend Neo4j::Driver::Ext::GraphDatabase
Java::OrgNeo4jDriver::Query.prepend Neo4j::Driver::Ext::Query
Java::OrgNeo4jDriverInternal::EagerResultValue.prepend Neo4j::Driver::Ext::Internal::EagerResultValue
Java::OrgNeo4jDriverInternal::InternalBookmark.prepend Neo4j::Driver::Ext::Internal::InternalBookmark
Java::OrgNeo4jDriverInternal::InternalDriver.prepend Neo4j::Driver::Ext::InternalDriver
Java::OrgNeo4jDriverInternal::InternalEntity.include Neo4j::Driver::Ext::InternalEntity
Expand Down
17 changes: 17 additions & 0 deletions jruby/neo4j/driver/ext/internal/eager_result_value.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Neo4j
module Driver
module Ext
module Internal
module EagerResultValue
include InternalKeys

def records
super.to_a
end
end
end
end
end
end
10 changes: 10 additions & 0 deletions jruby/neo4j/driver/ext/internal_driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ module InternalDriver

auto_closeable :session

def execute_query(query, auth_token = nil, config = {}, **parameters)
check do
executable_query(query)
.with_auth_token(auth_token)
.with_config(to_java_config(Neo4j::Driver::QueryConfig, **config))
.with_parameters(to_neo(parameters))
.execute
end
end

def session(**session_config)
java_method(:session, [org.neo4j.driver.SessionConfig])
.call(to_java_config(Neo4j::Driver::SessionConfig, **session_config))
Expand Down
1 change: 1 addition & 0 deletions ruby/neo4j/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module Neo4j
module Driver
Loader.load

EagerResult = Neo4j::Driver::Internal::EagerResultValue
Record = Neo4j::Driver::Internal::InternalRecord
Result = Neo4j::Driver::Internal::InternalResult
Transaction = Neo4j::Driver::Internal::InternalTransaction
Expand Down
5 changes: 5 additions & 0 deletions ruby/neo4j/driver/internal/eager_result_value.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Neo4j::Driver
module Internal
EagerResultValue = Struct.new(:keys, :records, :summary)
end
end
9 changes: 9 additions & 0 deletions ruby/neo4j/driver/internal/internal_driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ def async_session(**session_config)
InternalAsyncSession.new(new_session(**session_config))
end

def execute_query(query, auth_token = nil, config = {}, **parameters)
# TODO: auth_token not implemented yet
session(**config.slice(:bookmarks, :database, :impersonated_user, :default_access_mode, :fetch_size)) do |session|
session.run(query, parameters, config.slice(:timeout, :metadata)).then do
EagerResultValue.new(it.keys, it.to_a, it.consume)
end
end
end

def encrypted?
assert_open!
@security_plan.requires_encryption?
Expand Down
157 changes: 157 additions & 0 deletions spec/neo4j/driver/execute_query_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# frozen_string_literal: true

RSpec.describe Neo4j::Driver do

describe '#execute_query', version: '>=5' do
context 'when querying the database' do
it 'accepts query with default auth_token, config hash and parameters' do
expect {
driver.execute_query(
'MATCH (p:Person {age: $age}) RETURN p.name AS name',
nil,
{ database: 'neo4j' },
age: 42
)
}.not_to raise_error
end
end

context 'when writing to the database' do
it 'accepts query with only keyword parameters' do
expect {
driver.execute_query(
'CREATE (a:Person {name: $name}) CREATE (b:Person {name: $friend}) CREATE (a)-[:KNOWS]->(b)',
name: 'Alice',
friend: 'David'
)
}.not_to raise_error
end
end

context 'when reading from the database' do
it 'accepts query with no additional parameters' do
expect {
driver.execute_query('MATCH (p:Person)-[:KNOWS]->(:Person) RETURN p.name AS name')
}.not_to raise_error
end
end

context 'when updating the database' do
it 'accepts update query with keyword parameters' do
expect {
driver.execute_query(
'MATCH (p:Person {name: $name}) SET p.age = $age',
name: 'Alice',
age: 42
)
}.not_to raise_error
end

it 'accepts relationship creation with keyword parameters' do
expect {
driver.execute_query(
'MATCH (alice:Person {name: $name}) MATCH (bob:Person {name: $friend}) CREATE (alice)-[:KNOWS]->(bob)',
name: 'Alice',
friend: 'Bob'
)
}.not_to raise_error
end
end

context 'when deleting from the database' do
it 'accepts delete query with keyword parameters' do
expect {
driver.execute_query(
'MATCH (p:Person {name: $name}) DETACH DELETE p',
name: 'Alice'
)
}.not_to raise_error
end
end

context 'when using query configuration' do
it 'accepts nil auth_token with config hash and parameters' do
expect {
driver.execute_query(
'MATCH (p:Person) RETURN p.name',
nil,
{ database: 'neo4j' },
age: 42
)
}.not_to raise_error
end

it 'accepts auth_token with keyword parameters' do
auth_token = Neo4j::Driver::AuthTokens.basic(neo4j_user, neo4j_password)

expect {
driver.execute_query(
'MATCH (p:Person) RETURN p.name',
auth_token,
age: 42
)
}.not_to raise_error
end
end

context 'when working with query summaries' do
it 'accepts UNWIND query without parameters' do
expect {
driver.execute_query("UNWIND ['Alice', 'Bob'] AS name MERGE (p:Person {name: name})")
}.not_to raise_error
end

it 'accepts MERGE query with keyword parameters' do
expect {
driver.execute_query(
"MERGE (p:Person {name: $name}) MERGE (p)-[:KNOWS]->(:Person {name: $friend})",
name: 'Mark',
friend: 'Bob'
)
}.not_to raise_error
end

it 'accepts EXPLAIN query with keyword parameters' do
expect {
driver.execute_query(
'EXPLAIN MATCH (p {name: $name}) RETURN p',
name: 'Alice'
)
}.not_to raise_error
end

it 'accepts shortestPath query with keyword parameters' do
expect {
driver.execute_query(
"MATCH p=shortestPath((:Person {name: $start})-[*]->(:Person {name: $end})) RETURN p",
start: 'Alice',
end: 'Bob'
)
}.not_to raise_error
end
end

context 'parameter handling' do
it 'handles mixed positional and keyword arguments correctly' do
expect {
driver.execute_query(
'MATCH (p:Person {age: $age, name: $name}) RETURN p',
nil,
{ database: 'neo4j' },
age: 42,
name: 'Alice'
)
}.not_to raise_error
end

it 'handles only keyword arguments' do
expect {
driver.execute_query(
'MATCH (p:Person {name: $name}) RETURN p',
name: 'Alice'
)
}.not_to raise_error
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module Testkit::Backend::Messages
module Requests
class ExecuteQuery < Request
def process
fetch(driver_id).execute_query(cypher, auth_token, config, **decode(params)).then do |er|
named_entity(
'EagerResult',
key: er.keys,
records: er.records.map { Responses::Record.new(it).data },
summary: Responses::Summary.new(er.summary).data
)
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class GetFeatures < Request
{
'Feature:API:BookmarkManager' => 'a',
'Feature:API:ConnectionAcquisitionTimeout' => 'ja',
'Feature:API:Driver.ExecuteQuery' => 'a',
'Feature:API:Driver.ExecuteQuery' => 'ja',
'Feature:API:Driver:GetServerInfo' => '',
'Feature:API:Driver.IsEncrypted' => 'jar',
'Feature:API:Driver:NotificationsConfig' => 'ja',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,7 @@
module Testkit::Backend::Messages
module Requests
class ResultConsume < Request
def process
named_entity('Summary',
**{
serverInfo: to_map(summary.server, :protocol_version, :address, :agent),
counters: to_map(summary.counters, *%w[constraints_added constraints_removed contains_system_updates? contains_updates? indexes_added
indexes_removed labels_added labels_removed nodes_created nodes_deleted properties_set relationships_created
relationships_deleted system_updates]),
query: { text: summary.query.text, parameters: summary.query.parameters.transform_values(&method(:to_testkit)) },
database: summary.database.name,
queryType: summary.query_type,
notifications: summary.notifications&.then(&method(:notifications)),
plan: (plan_to_h(summary.plan) if summary.has_plan?),
profile: summary.has_profile? ? summary.profile.then { |p| { db_hits: p.db_hits } } : nil,
}.merge!(to_map(summary, *%w[result_available_after result_consumed_after])))
end

private

def summary
@object ||= fetch(result_id).consume
end

def to_map(o, *methods)
methods.map { |name| [key(name), o.send(name).then { |obj| block_given? ? yield(obj) : obj }] }.to_h
end

def key(name)
name.to_s.gsub('?', '').camelize(:lower).to_sym
end

def map_entry(n, method, *methods)
n.send(method)&.then { |o| { key(method) => to_map(o, *methods) } } || {}
end

def notifications(ns)
ns.map do |n|
to_map(n, *%w[code title description raw_category severity raw_severity_level])
.merge(to_map(n, *%w[category severity_level]) { |o| o&.name || 'UNKNOWN' })
.merge(map_entry(n, :position, :column, :line, :offset))
end
end

def plan_to_h(plan)
plan.to_h.transform_keys(&method(:key)).tap {|hash| hash[:children]&.map!(&method(:plan_to_h))}
end
def response = Responses::Summary.new(fetch(result_id).consume)
end
end
end
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
module Testkit::Backend::Messages
module Requests
class ResultList < Request
def process
result = fetch(result_id)
named_entity('RecordList', records: result.to_a.map do |record|
{ values: record.values.map do |value|
to_testkit(value)
end }
end
)
end
def process = named_entity('RecordList', records: fetch(result_id).to_a.map { Responses::Record.new(it).data })
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module Requests
class ResultNext < Request
def process
result = fetch(result_id)
result.has_next? ? named_entity('Record', values: result.next.values.map(&method(:to_testkit))) : named_entity('NullRecord')
result.has_next? ? Responses::Record.new(result.next).to_testkit : named_entity('NullRecord')
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module Requests
class ResultPeek < Request
def process
result = fetch(result_id)
result.has_next? ? named_entity('Record', values: result.peek.values.map(&method(:to_testkit))) : named_entity('NullRecord')
result.has_next? ? Responses::Record.new(result.peek).to_testkit : named_entity('NullRecord')
end
end
end
Expand Down
Loading
Loading