Skip to content

Commit 1b29596

Browse files
committed
Initial commit
0 parents  commit 1b29596

10 files changed

+325
-0
lines changed

Gemfile

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
source 'https://rubygems.org'
2+
3+
gemspec
4+
5+
gem 'redis', '~> 4'
6+
gem 'terminal-table', '~> 1', '>= 1.8'
7+

LICENSE

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) 2018, Redis Labs
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
* Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
* Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
* Neither the name of the copyright holder nor the names of its
17+
contributors may be used to endorse or promote products derived from
18+
this software without specific prior written permission.
19+
20+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# redisgraph-rb
2+
3+
`redisgraph-rb` is a Ruby gem client for the RedisGraph module. It relies on `redis-rb` for Redis connection management and provides support for graph QUERY, EXPLAIN, and DELETE commands.
4+
5+
## Usage
6+
```
7+
require 'redisgraph'
8+
9+
graphname = "sample"
10+
11+
r = RedisGraph.new(graphname)
12+
13+
cmd = """CREATE (:person {name: 'Jim', age: 29})-[:works]->(:employer {name: 'Dunder Mifflin'})"""
14+
response = r.query(cmd)
15+
response.stats
16+
=> {:labels_added=>2, :nodes_created=>2, :properties_set=>3, :relationships_created=>1, :internal_execution_time=>0.705541}
17+
18+
cmd = """MATCH ()-[:works]->(e:employer) RETURN e"""
19+
20+
response = r.query(cmd)
21+
22+
response.print_resultset
23+
+----------------+
24+
| e.name |
25+
+----------------+
26+
| Dunder Mifflin |
27+
+----------------+
28+
29+
r.delete
30+
=> "Graph removed, internal execution time: 0.416024 milliseconds"
31+
```
32+
33+
## Specifying Redis options
34+
RedisGraph connects to an active Redis server, defaulting to `host: localhost, port: 6379`. To provide custom connection parameters, instantiate a RedisGraph object with a `redis_options` hash:
35+
36+
`r = RedisGraph.new("graphname", redis_options= { host: "127.0.0.1", port: 26380 })`
37+
38+
These parameters are described fully in the documentation for https://github.com/redis/redis-rb
39+
40+
## Running tests
41+
A simple test suite is provided, and can be run with:
42+
`ruby test/test_suite.rb`
43+
These tests expect a Redis server with the Graph module loaded to be available at localhost:6379
44+

lib/redisgraph.rb

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
require 'redis'
2+
require 'terminal-table'
3+
4+
require_relative 'redisgraph/errors.rb'
5+
require_relative 'redisgraph/query_result.rb'
6+
require_relative 'redisgraph/connection.rb'
7+
8+
class RedisGraph
9+
attr_accessor :connection
10+
attr_accessor :graphname
11+
12+
# The RedisGraph constructor instantiates a Redis connection
13+
# and validates that the graph module is loaded
14+
def initialize(graph, redis_options = {})
15+
@graphname = graph
16+
connect_to_server(redis_options)
17+
end
18+
19+
# Execute a command and return its parsed result
20+
def query(command)
21+
begin
22+
resp = @connection.call("GRAPH.QUERY", @graphname, command)
23+
rescue Redis::CommandError => e
24+
raise QueryError, e
25+
end
26+
27+
QueryResult.new(resp)
28+
end
29+
30+
# Return the execution plan for a given command
31+
def explain(command)
32+
begin
33+
resp = @connection.call("GRAPH.EXPLAIN", @graphname, command)
34+
rescue Redis::CommandError => e
35+
raise QueryError, e
36+
end
37+
end
38+
39+
# Delete the graph and all associated keys
40+
def delete
41+
resp = @connection.call("GRAPH.DELETE", @graphname)
42+
end
43+
end
44+

lib/redisgraph/connection.rb

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class RedisGraph
2+
def connect_to_server(options)
3+
@connection = Redis.new(options)
4+
self.verify_module()
5+
end
6+
7+
# Ensure that the connected Redis server supports modules
8+
# and has loaded the RedisGraph module
9+
def verify_module()
10+
redis_version = @connection.info["redis_version"]
11+
major_version = redis_version.split('.').first.to_i
12+
raise ServerError, "Redis 4.0 or greater required for RedisGraph support." unless major_version >= 4
13+
resp = @connection.call("MODULE", "LIST")
14+
raise ServerError, "RedisGraph module not loaded." unless resp.first && resp.first.include?("graph")
15+
end
16+
end

lib/redisgraph/errors.rb

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class RedisGraph
2+
class RedisGraphError < RuntimeError
3+
end
4+
5+
class ServerError < RedisGraphError
6+
end
7+
8+
class QueryError < RedisGraphError
9+
end
10+
end

lib/redisgraph/query_result.rb

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
class QueryResult
2+
attr_accessor :columns
3+
attr_accessor :resultset
4+
attr_accessor :stats
5+
6+
def print_resultset
7+
pretty = Terminal::Table.new headings: columns do |t|
8+
resultset.each { |record| t << record }
9+
end
10+
puts pretty
11+
end
12+
13+
def parse_resultset(response)
14+
# Any non-empty result set will have multiple rows (arrays)
15+
return nil unless response[0].length > 1
16+
# First row is return elements / properties
17+
@columns = response[0].shift
18+
# Subsequent rows are records
19+
@resultset = response[0]
20+
end
21+
22+
# Read metrics about internal query handling
23+
def parse_stats(response)
24+
return nil unless response[1]
25+
26+
stats = {}
27+
28+
response[1].each do |stat|
29+
line = stat.split(': ')
30+
val = line[1].split(' ')[0]
31+
32+
case line[0]
33+
when /^Labels added/
34+
stats[:labels_added] = val.to_i
35+
when /^Nodes created/
36+
stats[:nodes_created] = val.to_i
37+
when /^Nodes deleted/
38+
stats[:nodes_deleted] = val.to_i
39+
when /^Relationships deleted/
40+
stats[:relationships_deleted] = val.to_i
41+
when /^Properties set/
42+
stats[:properties_set] = val.to_i
43+
when /^Relationships created/
44+
stats[:relationships_created] = val.to_i
45+
when /^Query internal execution time/
46+
stats[:internal_execution_time] = val.to_f
47+
end
48+
end
49+
stats
50+
end
51+
52+
def initialize(response)
53+
# The response for any query is expected to be a nested array.
54+
# The only top-level values will be the result set and the statistics.
55+
@resultset = parse_resultset(response)
56+
@stats = parse_stats(response)
57+
end
58+
end
59+

lib/redisgraph/version.rb

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class RedisGraph
2+
VERSION = '1.0.0'
3+
end

redisgraph.gemspec

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
require "./lib/redisgraph/version"
2+
3+
Gem::Specification.new do |s|
4+
s.name = "redisgraph"
5+
6+
s.version = RedisGraph::VERSION
7+
8+
s.license = 'BSD-3-Clause'
9+
10+
s.homepage = 'https://github.com/redislabs/redisgraph-rb'
11+
12+
s.summary = 'A client for RedisGraph'
13+
14+
s.description = 'A client that extends redis-rb to provide explicit support for the RedisGraph module.'
15+
16+
s.authors = ['Redis Labs']
17+
18+
s.email = '[email protected]'
19+
20+
s.files = `git ls-files`.split("\n")
21+
22+
s.add_runtime_dependency('redis', '~> 4')
23+
s.add_runtime_dependency('terminal-table', '~> 1', '>= 1.8')
24+
end

test/test_suite.rb

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
require_relative '../lib/redisgraph.rb'
2+
require "test/unit"
3+
include Test::Unit::Assertions
4+
5+
# Helper functions
6+
# TODO it would be nice to have something like DisposableRedis
7+
8+
# Connect to a Redis server on localhost:6379
9+
def connect_test
10+
begin
11+
@r = RedisGraph.new("rubytest")
12+
rescue Redis::BaseError => e
13+
puts e
14+
puts "RedisGraph tests require that a Redis server with the graph module loaded be running on localhost:6379"
15+
exit 1
16+
end
17+
end
18+
19+
# Ensure that the graph "rubytest" does not exist
20+
def delete_graph
21+
@r.delete
22+
end
23+
24+
# Test functions - each validates one or more EXPLAIN and QUERY calls
25+
26+
def validate_node_creation
27+
query_str = """CREATE (t:node {name: 'src'})"""
28+
x = @r.query(query_str)
29+
plan = @r.explain(query_str)
30+
assert(plan =~ /Create/)
31+
assert(x.resultset.nil?)
32+
assert(x.stats[:labels_added] == 1)
33+
assert(x.stats[:nodes_created] == 1)
34+
assert(x.stats[:properties_set] == 1)
35+
puts "Create node - PASSED"
36+
end
37+
38+
def validate_node_deletion
39+
query_str = """MATCH (t:node) WHERE t.name = 'src' DELETE t"""
40+
plan = @r.explain(query_str)
41+
assert(plan =~ /Delete/)
42+
x = @r.query(query_str)
43+
assert(x.resultset.nil?)
44+
assert(x.stats[:nodes_deleted] == 1)
45+
query_str = """MATCH (t:node) WHERE t.name = 'src' RETURN t"""
46+
assert(x.resultset.nil?)
47+
puts "Delete node - PASSED"
48+
end
49+
50+
def validate_edge_creation
51+
query_str = """CREATE (p:node {name: 'src1'})-[:edge]->(:node {name: 'dest1'}), (:node {name: 'src2'})-[:edge]->(q:node_type_2 {name: 'dest2'})"""
52+
plan = @r.explain(query_str)
53+
assert(plan =~ /Create/)
54+
x = @r.query(query_str)
55+
assert(x.resultset.nil?)
56+
assert(x.stats[:nodes_created] == 4)
57+
assert(x.stats[:properties_set] == 4)
58+
assert(x.stats[:relationships_created] == 2)
59+
puts "Add edges - PASSED"
60+
end
61+
62+
def validate_edge_traversal
63+
query_str = """MATCH (a)-[:edge]->(b:node) RETURN a, b"""
64+
plan = @r.explain(query_str)
65+
assert(plan.include?("Traverse"))
66+
x = @r.query(query_str)
67+
assert(x.resultset)
68+
assert(x.columns.length == 2)
69+
assert(x.resultset.length == 1)
70+
assert(x.resultset[0] == ["src1", "dest1"])
71+
puts "Traverse edge - PASSED"
72+
end
73+
74+
def test_suite
75+
puts "Running RedisGraph tests..."
76+
connect_test
77+
delete_graph # Clear the graph
78+
79+
# Test basic functionalities
80+
validate_node_creation
81+
validate_node_deletion
82+
validate_edge_creation
83+
validate_edge_traversal
84+
85+
delete_graph # Clear the graph again
86+
puts "RedisGraph tests passed!"
87+
end
88+
89+
test_suite

0 commit comments

Comments
 (0)