Skip to content
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
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_closable :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
16 changes: 16 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,22 @@ def async_session(**session_config)
InternalAsyncSession.new(new_session(**session_config))
end

def execute_query(query, auth_token = nil, config = {}, **parameters)
session_config_keys = %i[bookmarks database impersonated_user default_access_mode fetch_size]
tx_config_keys = %i[timeout metadata]

session_config = config.slice(*session_config_keys)
session_config[:auth] = auth_token if auth_token
tx_config = config.slice(*tx_config_keys)

session(**session_config) do |session|
result = session.run(query, parameters, tx_config)
records = result.to_a
summary = result.consume
InternalEagerResult.new(records, summary)
end
end

def encrypted?
assert_open!
@security_plan.requires_encryption?
Expand Down
12 changes: 12 additions & 0 deletions ruby/neo4j/driver/internal/internal_eager_result.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Neo4j::Driver
module Internal
class InternalEagerResult
attr_reader :records, :summary

def initialize(records, summary)
@records = records
@summary = summary
end
end
end
end
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' 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,9 @@
module Testkit::Backend::Messages
module Requests
class ExecuteQuery < Request
def response
Responses::EagerResult.new(fetch(driver_id).execute_query(cypher, auth_token, config, **decode(params)))
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,50 +1,10 @@
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
include SummaryHelper

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))}
def process
summary_to_testkit(fetch(result_id).consume)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module Testkit
module Backend
module Messages
module Responses
class EagerResult < Response
alias_method :response_to_testkit, :to_testkit

include Conversion
alias_method :value_to_testkit, :to_testkit

include SummaryHelper

def to_testkit(*args)
if args.empty?
response_to_testkit
else
value_to_testkit(*args)
end
end
Comment on lines +6 to +19
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment I managed to make it work with this trick only.


def data
{
keys: @object.records.first&.keys || [],
records: @object.records.map do |record|
{ values: record.values.map(&method(:to_testkit)) }
end,
summary: summary_to_testkit(@object.summary)[:data]
}
end
end
end
end
end
end
Loading
Loading