redisgraph 1.0.0 → 2.0.3
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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +54 -0
- data/.github/release-drafter-config.yml +21 -0
- data/.github/workflows/release-drafter.yml +20 -0
- data/.gitignore +1 -0
- data/Gemfile +2 -1
- data/README.md +39 -8
- data/lib/redisgraph/connection.rb +14 -4
- data/lib/redisgraph/errors.rb +6 -6
- data/lib/redisgraph/query_result.rb +138 -26
- data/lib/redisgraph/version.rb +1 -1
- data/lib/redisgraph.rb +50 -15
- data/redisgraph.gemspec +0 -1
- data/spec/helper.rb +5 -0
- data/spec/redisgraph_quickstart_spec.rb +73 -0
- data/spec/redisgraph_spec.rb +95 -0
- metadata +10 -25
- data/test/test_suite.rb +0 -89
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6d49963c89e143060931e2fc7a20c73a0fe41a06f0002ec84494e6182ad28728
|
4
|
+
data.tar.gz: 90b9a339ae932fcea45bbf355518e54db7ddcad137739d5d2ad95bcb72b5bd99
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d4b3bc2e45a843c4328786fc1ce99e07873f9f0d09de3773ca572491867ccdc9b0f06653830167160fcf4e036349150166262890cc783e180569faecfd43bc6b
|
7
|
+
data.tar.gz: 8a0f6138c785a35ede21619c98960e3bdf96a557b98cdb3a183f1ee41ad3f99890c1e011ac6588e49efab2bcaa4f998b44cd0a44aff222155b3d0ece2b10f803
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Ruby CircleCI 2.0 configuration file
|
2
|
+
#
|
3
|
+
# Check https://circleci.com/docs/2.0/language-ruby/ for more details
|
4
|
+
#
|
5
|
+
version: 2
|
6
|
+
jobs:
|
7
|
+
build:
|
8
|
+
docker:
|
9
|
+
- image: circleci/ruby:2.4.1-node-browsers
|
10
|
+
|
11
|
+
- image: redislabs/redisgraph:edge
|
12
|
+
port: 6379:6379
|
13
|
+
|
14
|
+
working_directory: ~/repo
|
15
|
+
|
16
|
+
steps:
|
17
|
+
- checkout
|
18
|
+
|
19
|
+
- restore_cache:
|
20
|
+
keys:
|
21
|
+
- v1-dependencies-{{ checksum "Gemfile" }}
|
22
|
+
# fallback to using the latest cache if no exact match is found
|
23
|
+
- v1-dependencies-
|
24
|
+
|
25
|
+
- run:
|
26
|
+
name: install dependencies
|
27
|
+
command: |
|
28
|
+
bundle install --jobs=4 --retry=3 --path vendor/bundle
|
29
|
+
|
30
|
+
- save_cache:
|
31
|
+
paths:
|
32
|
+
- ./vendor/bundle
|
33
|
+
key: v1-dependencies-{{ checksum "Gemfile" }}
|
34
|
+
|
35
|
+
# run tests!
|
36
|
+
- run:
|
37
|
+
name: run tests
|
38
|
+
command: bundle exec rspec
|
39
|
+
|
40
|
+
workflows:
|
41
|
+
version: 2
|
42
|
+
commit:
|
43
|
+
jobs:
|
44
|
+
- build
|
45
|
+
nightly:
|
46
|
+
triggers:
|
47
|
+
- schedule:
|
48
|
+
cron: "0 0 * * *"
|
49
|
+
filters:
|
50
|
+
branches:
|
51
|
+
only:
|
52
|
+
- master
|
53
|
+
jobs:
|
54
|
+
- build
|
@@ -0,0 +1,21 @@
|
|
1
|
+
name-template: 'Version $NEXT_PATCH_VERSION'
|
2
|
+
tag-template: 'v$NEXT_PATCH_VERSION'
|
3
|
+
categories:
|
4
|
+
- title: 'Features'
|
5
|
+
labels:
|
6
|
+
- 'feature'
|
7
|
+
- 'enhancement'
|
8
|
+
- title: 'Bug Fixes'
|
9
|
+
labels:
|
10
|
+
- 'fix'
|
11
|
+
- 'bugfix'
|
12
|
+
- 'bug'
|
13
|
+
- title: 'Maintenance'
|
14
|
+
label: 'chore'
|
15
|
+
change-template: '- $TITLE (#$NUMBER)'
|
16
|
+
exclude-labels:
|
17
|
+
- 'skip-changelog'
|
18
|
+
template: |
|
19
|
+
## Changes
|
20
|
+
|
21
|
+
$CHANGES
|
@@ -0,0 +1,20 @@
|
|
1
|
+
name: Release Drafter
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
# branches to consider in the event; optional, defaults to all
|
6
|
+
branches:
|
7
|
+
- master
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
update_release_draft:
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
steps:
|
13
|
+
# Drafts your next Release notes as Pull Requests are merged into "master"
|
14
|
+
- uses: release-drafter/release-drafter@v5
|
15
|
+
with:
|
16
|
+
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
|
17
|
+
config-name: release-drafter-config.yml
|
18
|
+
env:
|
19
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
20
|
+
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
coverage/*
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,31 @@
|
|
1
|
+
[](https://github.com/RedisGraph/redisgraph-rb)
|
2
|
+
[](https://circleci.com/gh/RedisGraph/redisgraph-rb/tree/master)
|
3
|
+
[](https://github.com/RedisGraph/redisgraph-rb/releases/latest)
|
4
|
+
[](https://codecov.io/gh/RedisGraph/redisgraph-rb)
|
5
|
+
[](https://badge.fury.io/rb/redisgraph)
|
6
|
+
|
1
7
|
# redisgraph-rb
|
8
|
+
[](https://forum.redislabs.com/c/modules/redisgraph)
|
9
|
+
[](https://discord.gg/gWBRT6P)
|
10
|
+
|
11
|
+
`redisgraph-rb` is a Ruby gem client for the [RedisGraph](https://github.com/RedisLabsModules/RedisGraph) module. It relies on `redis-rb` for Redis connection management and provides support for graph QUERY, EXPLAIN, and DELETE commands.
|
12
|
+
|
13
|
+
## RedisGraph compatibility
|
14
|
+
The current version of `redisgraph-rb` is compatible with RedisGraph versions >= 1.99 (module version: 19900).
|
15
|
+
|
16
|
+
### Previous Version
|
17
|
+
For RedisGraph versions >= 1.0 and < 2.0 (ie module version: 10202), instead use and refer to
|
18
|
+
the redisgraph gem version ~> 1.0.0
|
19
|
+
|
20
|
+
which corresponds to the following docker image
|
21
|
+
`docker run -p 6379:6379 -it --rm redislabs/redisgraph:1.2.2`
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
To install, run:
|
2
25
|
|
3
|
-
|
26
|
+
`$ gem install redisgraph`
|
27
|
+
|
28
|
+
Or include `redisgraph` as a dependency in your Gemfile.
|
4
29
|
|
5
30
|
## Usage
|
6
31
|
```
|
@@ -20,11 +45,11 @@ cmd = """MATCH ()-[:works]->(e:employer) RETURN e"""
|
|
20
45
|
response = r.query(cmd)
|
21
46
|
|
22
47
|
response.print_resultset
|
23
|
-
|
24
|
-
| e
|
25
|
-
|
26
|
-
| Dunder Mifflin |
|
27
|
-
|
48
|
+
--------------------------------
|
49
|
+
| e |
|
50
|
+
--------------------------------
|
51
|
+
| [{"name"=>"Dunder Mifflin"}] |
|
52
|
+
--------------------------------
|
28
53
|
|
29
54
|
r.delete
|
30
55
|
=> "Graph removed, internal execution time: 0.416024 milliseconds"
|
@@ -38,7 +63,13 @@ RedisGraph connects to an active Redis server, defaulting to `host: localhost, p
|
|
38
63
|
These parameters are described fully in the documentation for https://github.com/redis/redis-rb
|
39
64
|
|
40
65
|
## Running tests
|
41
|
-
|
42
|
-
`
|
66
|
+
To ensure prerequisites are installed, run the following:
|
67
|
+
`bundle install`
|
68
|
+
|
43
69
|
These tests expect a Redis server with the Graph module loaded to be available at localhost:6379
|
44
70
|
|
71
|
+
The currently compatible version of the RedisGraph module may be run as follows:
|
72
|
+
`docker run -p 6379:6379 -it --rm redislabs/redisgraph:2.0-edge`
|
73
|
+
|
74
|
+
A simple test suite is provided, and can be run with:
|
75
|
+
`rspec`
|
@@ -1,16 +1,26 @@
|
|
1
1
|
class RedisGraph
|
2
2
|
def connect_to_server(options)
|
3
3
|
@connection = Redis.new(options)
|
4
|
-
|
4
|
+
check_module_version
|
5
5
|
end
|
6
6
|
|
7
7
|
# Ensure that the connected Redis server supports modules
|
8
8
|
# and has loaded the RedisGraph module
|
9
|
-
def
|
9
|
+
def check_module_version()
|
10
10
|
redis_version = @connection.info["redis_version"]
|
11
11
|
major_version = redis_version.split('.').first.to_i
|
12
12
|
raise ServerError, "Redis 4.0 or greater required for RedisGraph support." unless major_version >= 4
|
13
|
-
|
14
|
-
|
13
|
+
|
14
|
+
begin
|
15
|
+
modules = @connection.call("MODULE", "LIST")
|
16
|
+
rescue Redis::CommandError
|
17
|
+
# Ignore check if the connected server does not support the "MODULE LIST" command
|
18
|
+
return
|
19
|
+
end
|
20
|
+
|
21
|
+
module_graph = modules.detect { |_name_key, name, _ver_key, _ver| name == 'graph' }
|
22
|
+
module_version = module_graph[3] if module_graph
|
23
|
+
raise ServerError, "RedisGraph module not loaded." if module_version.nil?
|
24
|
+
raise ServerError, "RedisGraph module incompatible, expecting >= 1.99." if module_version < 19900
|
15
25
|
end
|
16
26
|
end
|
data/lib/redisgraph/errors.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
class RedisGraph
|
2
|
-
class RedisGraphError < RuntimeError
|
3
|
-
end
|
2
|
+
class RedisGraphError < RuntimeError; end
|
4
3
|
|
5
|
-
class ServerError < RedisGraphError
|
6
|
-
end
|
4
|
+
class ServerError < RedisGraphError; end
|
7
5
|
|
8
|
-
class
|
9
|
-
end
|
6
|
+
class CallError < RedisGraphError; end
|
7
|
+
class QueryError < RedisGraphError; end
|
8
|
+
class ExplainError < RedisGraphError; end
|
9
|
+
class DeleteError < RedisGraphError; end
|
10
10
|
end
|
@@ -1,59 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Data types that can be returned in result sets
|
4
|
+
module ValueType
|
5
|
+
UNKNOWN = 0
|
6
|
+
NULL = 1
|
7
|
+
STRING = 2
|
8
|
+
INTEGER = 3
|
9
|
+
BOOLEAN = 4
|
10
|
+
DOUBLE = 5
|
11
|
+
ARRAY = 6
|
12
|
+
EDGE = 7
|
13
|
+
NODE = 8
|
14
|
+
PATH = 9 # TODO: not yet implemented
|
15
|
+
end
|
16
|
+
|
1
17
|
class QueryResult
|
2
18
|
attr_accessor :columns
|
3
19
|
attr_accessor :resultset
|
4
20
|
attr_accessor :stats
|
5
21
|
|
22
|
+
def initialize(response, opts = {})
|
23
|
+
# The response for any query is expected to be a nested array.
|
24
|
+
# If compact (RedisGraph protocol v2)
|
25
|
+
# The resultset is an array w/ three elements:
|
26
|
+
# 0] Node/Edge key names w/ the ordinal position used in [1]
|
27
|
+
# en lieu of the name to compact the result set.
|
28
|
+
# 1] Node/Edge key/value pairs as an array w/ two elements:
|
29
|
+
# 0] node/edge name id from [0]
|
30
|
+
# 1..matches] node/edge values
|
31
|
+
# 2] Statistics as an array of strings
|
32
|
+
|
33
|
+
@metadata = opts[:metadata]
|
34
|
+
|
35
|
+
@resultset = parse_resultset(response)
|
36
|
+
@stats = parse_stats(response)
|
37
|
+
end
|
38
|
+
|
6
39
|
def print_resultset
|
7
|
-
|
8
|
-
|
40
|
+
return unless columns
|
41
|
+
|
42
|
+
# Compute max length of each column
|
43
|
+
column_sizes = resultset.reduce([]) do |lengths, row|
|
44
|
+
row.each_with_index.map{|iterand, index| [lengths[index] || 0, iterand.to_s.length].max}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Print column headers
|
48
|
+
puts head = '-' * (column_sizes.inject(&:+) + (3 * column_sizes.count) + 1)
|
49
|
+
row = columns.fill(nil, columns.size..(column_sizes.size - 1))
|
50
|
+
row = row.each_with_index.map{|v, i| v = v.to_s + ' ' * (column_sizes[i] - v.to_s.length)}
|
51
|
+
puts '| ' + row.join(' | ') + ' |'
|
52
|
+
puts head
|
53
|
+
|
54
|
+
# Print result set rows
|
55
|
+
resultset.each do |row|
|
56
|
+
row = row.fill(nil, row.size..(column_sizes.size - 1))
|
57
|
+
row = row.each_with_index.map{|v, i| v = v.to_s + ' ' * (column_sizes[i] - v.to_s.length)}
|
58
|
+
puts '| ' + row.join(' | ') + ' |'
|
9
59
|
end
|
10
|
-
puts
|
60
|
+
puts head
|
11
61
|
end
|
12
62
|
|
13
63
|
def parse_resultset(response)
|
64
|
+
# In the v2 protocol, CREATE does not contain an empty row preceding statistics
|
65
|
+
return unless response.length > 1
|
66
|
+
|
14
67
|
# Any non-empty result set will have multiple rows (arrays)
|
15
|
-
|
16
|
-
# First row is
|
17
|
-
|
18
|
-
|
19
|
-
@
|
68
|
+
|
69
|
+
# First row is header describing the returned records, corresponding
|
70
|
+
# precisely in order and naming to the RETURN clause of the query.
|
71
|
+
header = response[0]
|
72
|
+
@columns = header.map { |(_type, name)| name }
|
73
|
+
|
74
|
+
# Second row is the actual data returned by the query
|
75
|
+
# note handling for encountering an id for propertyKey that is out of
|
76
|
+
# the cached set.
|
77
|
+
data = response[1].map do |row|
|
78
|
+
i = -1
|
79
|
+
header.reduce([]) do |agg, (type, _it)|
|
80
|
+
i += 1
|
81
|
+
el = row[i]
|
82
|
+
case type
|
83
|
+
when 1 # Column of scalars
|
84
|
+
agg << map_scalar(el[0], el[1])
|
85
|
+
when 2 # node
|
86
|
+
props = el[2]
|
87
|
+
agg << props.sort_by { |prop| prop[0] }.map { |prop| map_prop(prop) }
|
88
|
+
when 3 # Column of relations
|
89
|
+
props = el[4]
|
90
|
+
agg << props.sort_by { |prop| prop[0] }.map { |prop| map_prop(prop) }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
data
|
96
|
+
end
|
97
|
+
|
98
|
+
def map_scalar(type, val)
|
99
|
+
map_func = case type
|
100
|
+
when ValueType::NULL
|
101
|
+
return nil
|
102
|
+
when ValueType::STRING
|
103
|
+
:to_s
|
104
|
+
when ValueType::INTEGER
|
105
|
+
:to_i
|
106
|
+
when ValueType::BOOLEAN
|
107
|
+
# no :to_b
|
108
|
+
return val == 'true'
|
109
|
+
when ValueType::DOUBLE
|
110
|
+
:to_f
|
111
|
+
when ValueType::ARRAY
|
112
|
+
return val.map { |it| map_scalar(it[0], it[1]) }
|
113
|
+
when ValueType::EDGE
|
114
|
+
props = val[4]
|
115
|
+
return props.sort_by { |prop| prop[0] }.map { |prop| map_prop(prop) }
|
116
|
+
when ValueType::NODE
|
117
|
+
props = val[2]
|
118
|
+
return props.sort_by { |prop| prop[0] }.map { |prop| map_prop(prop) }
|
119
|
+
end
|
120
|
+
val.send(map_func)
|
121
|
+
end
|
122
|
+
|
123
|
+
def map_prop(prop)
|
124
|
+
# maximally a single @metadata.invalidate should occur
|
125
|
+
|
126
|
+
property_keys = @metadata.property_keys
|
127
|
+
prop_index = prop[0]
|
128
|
+
if prop_index >= property_keys.length
|
129
|
+
@metadata.invalidate
|
130
|
+
property_keys = @metadata.property_keys
|
131
|
+
end
|
132
|
+
{ property_keys[prop_index] => map_scalar(prop[1], prop[2]) }
|
20
133
|
end
|
21
134
|
|
22
135
|
# Read metrics about internal query handling
|
23
136
|
def parse_stats(response)
|
24
|
-
|
137
|
+
# In the v2 protocol, CREATE does not contain an empty row preceding statistics
|
138
|
+
stats_offset = response.length == 1 ? 0 : 2
|
139
|
+
|
140
|
+
return nil unless response[stats_offset]
|
25
141
|
|
142
|
+
parse_stats_row(response[stats_offset])
|
143
|
+
end
|
144
|
+
|
145
|
+
def parse_stats_row(response_row)
|
26
146
|
stats = {}
|
27
147
|
|
28
|
-
|
148
|
+
response_row.each do |stat|
|
29
149
|
line = stat.split(': ')
|
30
|
-
val = line[1].split(' ')[0]
|
150
|
+
val = line[1].split(' ')[0].to_i
|
31
151
|
|
32
152
|
case line[0]
|
33
153
|
when /^Labels added/
|
34
|
-
stats[:labels_added] = val
|
154
|
+
stats[:labels_added] = val
|
35
155
|
when /^Nodes created/
|
36
|
-
stats[:nodes_created] = val
|
156
|
+
stats[:nodes_created] = val
|
37
157
|
when /^Nodes deleted/
|
38
|
-
stats[:nodes_deleted] = val
|
158
|
+
stats[:nodes_deleted] = val
|
39
159
|
when /^Relationships deleted/
|
40
|
-
stats[:relationships_deleted] = val
|
160
|
+
stats[:relationships_deleted] = val
|
41
161
|
when /^Properties set/
|
42
|
-
stats[:properties_set] = val
|
162
|
+
stats[:properties_set] = val
|
43
163
|
when /^Relationships created/
|
44
|
-
stats[:relationships_created] = val
|
164
|
+
stats[:relationships_created] = val
|
45
165
|
when /^Query internal execution time/
|
46
|
-
stats[:internal_execution_time] = val
|
166
|
+
stats[:internal_execution_time] = val
|
47
167
|
end
|
48
168
|
end
|
49
169
|
stats
|
50
170
|
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
171
|
end
|
59
|
-
|
data/lib/redisgraph/version.rb
CHANGED
data/lib/redisgraph.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'redis'
|
2
|
-
require 'terminal-table'
|
3
2
|
|
4
3
|
require_relative 'redisgraph/errors.rb'
|
5
4
|
require_relative 'redisgraph/query_result.rb'
|
@@ -8,37 +7,73 @@ require_relative 'redisgraph/connection.rb'
|
|
8
7
|
class RedisGraph
|
9
8
|
attr_accessor :connection
|
10
9
|
attr_accessor :graphname
|
10
|
+
attr_accessor :metadata
|
11
|
+
|
12
|
+
class Metadata
|
13
|
+
def initialize(opts = {})
|
14
|
+
@graphname = opts[:graphname]
|
15
|
+
@connection = opts[:connection]
|
16
|
+
|
17
|
+
# cache semantics around these labels, propertyKeys, and relationshipTypes
|
18
|
+
# defers first read and is invalidated when changed.
|
19
|
+
@labels_proc = -> { call_procedure('db.labels') }
|
20
|
+
@property_keys_proc = -> { call_procedure('db.propertyKeys') }
|
21
|
+
@relationship_types_proc = -> { call_procedure('db.relationshipTypes') }
|
22
|
+
end
|
23
|
+
|
24
|
+
def invalidate
|
25
|
+
@labels = @property_keys = @relationship_types = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def labels
|
29
|
+
@labels ||= @labels_proc.call
|
30
|
+
end
|
31
|
+
|
32
|
+
def property_keys
|
33
|
+
@property_keys ||= @property_keys_proc.call
|
34
|
+
end
|
35
|
+
|
36
|
+
def relationship_types
|
37
|
+
@relationship_types ||= @relationship_types_proc.call
|
38
|
+
end
|
39
|
+
|
40
|
+
def call_procedure(procedure)
|
41
|
+
res = @connection.call("GRAPH.QUERY", @graphname, "CALL #{procedure}()")
|
42
|
+
res[1].flatten
|
43
|
+
rescue Redis::CommandError => e
|
44
|
+
raise CallError, e
|
45
|
+
end
|
46
|
+
end
|
11
47
|
|
12
48
|
# The RedisGraph constructor instantiates a Redis connection
|
13
49
|
# and validates that the graph module is loaded
|
14
50
|
def initialize(graph, redis_options = {})
|
15
51
|
@graphname = graph
|
16
52
|
connect_to_server(redis_options)
|
53
|
+
@metadata = Metadata.new(graphname: @graphname,
|
54
|
+
connection: @connection)
|
17
55
|
end
|
18
56
|
|
19
57
|
# Execute a command and return its parsed result
|
20
58
|
def query(command)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
QueryResult.new(resp)
|
59
|
+
resp = @connection.call('GRAPH.QUERY', @graphname, command, '--compact')
|
60
|
+
QueryResult.new(resp,
|
61
|
+
metadata: @metadata)
|
62
|
+
rescue Redis::CommandError => e
|
63
|
+
raise QueryError, e
|
28
64
|
end
|
29
65
|
|
30
66
|
# Return the execution plan for a given command
|
31
67
|
def explain(command)
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
raise QueryError, e
|
36
|
-
end
|
68
|
+
@connection.call('GRAPH.EXPLAIN', @graphname, command)
|
69
|
+
rescue Redis::CommandError => e
|
70
|
+
raise ExplainError, e
|
37
71
|
end
|
38
72
|
|
39
73
|
# Delete the graph and all associated keys
|
40
74
|
def delete
|
41
|
-
|
75
|
+
@connection.call('GRAPH.DELETE', @graphname)
|
76
|
+
rescue Redis::CommandError => e
|
77
|
+
raise DeleteError, e
|
42
78
|
end
|
43
79
|
end
|
44
|
-
|
data/redisgraph.gemspec
CHANGED
data/spec/helper.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'helper.rb'
|
2
|
+
|
3
|
+
require_relative '../lib/redisgraph.rb'
|
4
|
+
|
5
|
+
# based on queries extracted from
|
6
|
+
describe RedisGraph do
|
7
|
+
before(:all) do
|
8
|
+
begin
|
9
|
+
@r = RedisGraph.new("#{described_class}_test")
|
10
|
+
create_graph
|
11
|
+
rescue Redis::BaseError => e
|
12
|
+
$stderr.puts(e)
|
13
|
+
exit 1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
after(:all) do
|
18
|
+
@r.delete if @r
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_graph()
|
22
|
+
q = "CREATE (:Rider {name:'Valentino Rossi'})-[:rides]->(:Team {name:'Yamaha'})," \
|
23
|
+
"(:Rider {name:'Dani Pedrosa'})-[:rides]->(:Team {name:'Honda'})," \
|
24
|
+
"(:Rider {name:'Andrea Dovizioso'})-[:rides]->(:Team {name:'Ducati'})"
|
25
|
+
|
26
|
+
res = @r.query(q)
|
27
|
+
|
28
|
+
expect(res.resultset).to be_nil
|
29
|
+
stats = res.stats
|
30
|
+
expect(stats).to include(:internal_execution_time)
|
31
|
+
stats.delete(:internal_execution_time)
|
32
|
+
expect(stats).to eq({
|
33
|
+
labels_added: 2,
|
34
|
+
nodes_created: 6,
|
35
|
+
properties_set: 6,
|
36
|
+
relationships_created: 3
|
37
|
+
})
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'quickstart' do
|
41
|
+
it 'should query relations, with a predicate' do
|
42
|
+
q = "MATCH (r:Rider)-[:rides]->(t:Team) WHERE t.name = 'Yamaha' RETURN r.name, t.name"
|
43
|
+
|
44
|
+
res = @r.query(q)
|
45
|
+
|
46
|
+
expect(res.columns).to eq(["r.name", "t.name"])
|
47
|
+
expect(res.resultset).to eq([["Valentino Rossi", "Yamaha"]])
|
48
|
+
end
|
49
|
+
|
50
|
+
# not in the quickstart, but demonstrates multiple rows
|
51
|
+
it 'should query relations, without a predicate' do
|
52
|
+
q = "MATCH (r:Rider)-[:rides]->(t:Team) RETURN r.name, t.name ORDER BY r.name"
|
53
|
+
|
54
|
+
res = @r.query(q)
|
55
|
+
|
56
|
+
expect(res.columns).to eq(["r.name", "t.name"])
|
57
|
+
expect(res.resultset).to eq([
|
58
|
+
["Andrea Dovizioso", "Ducati"],
|
59
|
+
["Dani Pedrosa", "Honda"],
|
60
|
+
["Valentino Rossi", "Yamaha"]
|
61
|
+
])
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should query relations, with an aggregate function' do
|
65
|
+
q = "MATCH (r:Rider)-[:rides]->(t:Team {name:'Ducati'}) RETURN count(r)"
|
66
|
+
|
67
|
+
res = @r.query(q)
|
68
|
+
|
69
|
+
expect(res.columns).to eq(["count(r)"])
|
70
|
+
expect(res.resultset).to eq([[1]])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'helper.rb'
|
2
|
+
|
3
|
+
require_relative '../lib/redisgraph.rb'
|
4
|
+
|
5
|
+
describe RedisGraph do
|
6
|
+
# TODO it would be nice to have something like DisposableRedis
|
7
|
+
# Connect to a Redis server on localhost:6379
|
8
|
+
before(:all) do
|
9
|
+
begin
|
10
|
+
@r = RedisGraph.new("#{described_class}_test")
|
11
|
+
create_graph
|
12
|
+
rescue Redis::BaseError => e
|
13
|
+
$stderr.puts(e)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Ensure that the graph "rubytest" does not exist
|
18
|
+
after(:all) do
|
19
|
+
@r.delete if @r
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_graph()
|
23
|
+
q = """CREATE (t:node {name: 'src'})"""
|
24
|
+
|
25
|
+
res = @r.query(q)
|
26
|
+
expect(res.resultset).to be_nil
|
27
|
+
|
28
|
+
plan = @r.explain(q)
|
29
|
+
expect(plan).to include("Create")
|
30
|
+
|
31
|
+
expect(res.stats[:labels_added]).to eq(1)
|
32
|
+
expect(res.stats[:nodes_created]).to eq(1)
|
33
|
+
expect(res.stats[:properties_set]).to eq(1)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Test functions - each validates one or more EXPLAIN and QUERY calls
|
37
|
+
|
38
|
+
context "bare return" do
|
39
|
+
it "should map values properly" do
|
40
|
+
q = """UNWIND [1, 1.5, null, 'strval', true, false] AS a RETURN a"""
|
41
|
+
res = @r.query(q)
|
42
|
+
expect(res.resultset).to eq([[1], [1.5], [nil], ["strval"], [true], [false]])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "nodes" do
|
47
|
+
it "should delete nodes properly" do
|
48
|
+
q = """MATCH (t:node) WHERE t.name = 'src' DELETE t"""
|
49
|
+
plan = @r.explain(q)
|
50
|
+
expect(plan).to include("Delete")
|
51
|
+
res = @r.query(q)
|
52
|
+
expect(res.resultset).to be_nil
|
53
|
+
expect(res.stats[:nodes_deleted]).to eq(1)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context "edges" do
|
58
|
+
it "should create edges properly" do
|
59
|
+
q = "CREATE (p:node {name: 'src1', color: 'cyan'})-[:edge { weight: 7.8 }]->(:node {name: 'dest1', color: 'magenta'})," \
|
60
|
+
" (:node {name: 'src2'})-[:edge { weight: 12 }]->(q:node_type_2 {name: 'dest2'})"
|
61
|
+
plan = @r.explain(q)
|
62
|
+
expect(plan).to include("Create")
|
63
|
+
res = @r.query(q)
|
64
|
+
expect(res.resultset).to be_nil
|
65
|
+
expect(res.stats[:nodes_created]).to eq(4)
|
66
|
+
expect(res.stats[:properties_set]).to eq(8)
|
67
|
+
expect(res.stats[:relationships_created]).to eq(2)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should traverse edges properly" do
|
71
|
+
q = """MATCH (a)-[e:edge]->(b:node) RETURN a.name, b, e"""
|
72
|
+
plan = @r.explain(q)
|
73
|
+
expect(plan.detect { |row| row.include?("Traverse") }).to_not be_nil
|
74
|
+
res = @r.query(q)
|
75
|
+
expect(res.columns).to eq(["a.name", "b", "e"])
|
76
|
+
expect(res.resultset).to eq([["src1", [{"name"=>"dest1"}, {"color"=>"magenta"}], [{"weight"=>7.8}]]])
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "update" do
|
81
|
+
it "should support adding new properties" do
|
82
|
+
q = """MATCH (a {name: 'src1'}) SET a.newval = true"""
|
83
|
+
plan = @r.explain(q)
|
84
|
+
expect(plan.detect { |row| row.include?("Update") }).to_not be_nil
|
85
|
+
res = @r.query(q)
|
86
|
+
expect(res.stats[:properties_set]).to eq(1)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should print property strings correctly after updates" do
|
90
|
+
q = """MATCH (a {name: 'src1'}) RETURN a"""
|
91
|
+
res = @r.query(q)
|
92
|
+
expect(res.resultset).to eq([[[{"name"=>"src1"}, {"color"=>"cyan"}, {"newval"=>true}]]])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redisgraph
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Redis Labs
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-09-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -24,26 +24,6 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '4'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: terminal-table
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '1'
|
34
|
-
- - ">="
|
35
|
-
- !ruby/object:Gem::Version
|
36
|
-
version: '1.8'
|
37
|
-
type: :runtime
|
38
|
-
prerelease: false
|
39
|
-
version_requirements: !ruby/object:Gem::Requirement
|
40
|
-
requirements:
|
41
|
-
- - "~>"
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
version: '1'
|
44
|
-
- - ">="
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: '1.8'
|
47
27
|
description: A client that extends redis-rb to provide explicit support for the RedisGraph
|
48
28
|
module.
|
49
29
|
email: jeffrey@redislabs.com
|
@@ -51,6 +31,10 @@ executables: []
|
|
51
31
|
extensions: []
|
52
32
|
extra_rdoc_files: []
|
53
33
|
files:
|
34
|
+
- ".circleci/config.yml"
|
35
|
+
- ".github/release-drafter-config.yml"
|
36
|
+
- ".github/workflows/release-drafter.yml"
|
37
|
+
- ".gitignore"
|
54
38
|
- Gemfile
|
55
39
|
- LICENSE
|
56
40
|
- README.md
|
@@ -60,7 +44,9 @@ files:
|
|
60
44
|
- lib/redisgraph/query_result.rb
|
61
45
|
- lib/redisgraph/version.rb
|
62
46
|
- redisgraph.gemspec
|
63
|
-
-
|
47
|
+
- spec/helper.rb
|
48
|
+
- spec/redisgraph_quickstart_spec.rb
|
49
|
+
- spec/redisgraph_spec.rb
|
64
50
|
homepage: https://github.com/redislabs/redisgraph-rb
|
65
51
|
licenses:
|
66
52
|
- BSD-3-Clause
|
@@ -80,8 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
66
|
- !ruby/object:Gem::Version
|
81
67
|
version: '0'
|
82
68
|
requirements: []
|
83
|
-
|
84
|
-
rubygems_version: 2.7.7
|
69
|
+
rubygems_version: 3.2.22
|
85
70
|
signing_key:
|
86
71
|
specification_version: 4
|
87
72
|
summary: A client for RedisGraph
|
data/test/test_suite.rb
DELETED
@@ -1,89 +0,0 @@
|
|
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
|