redgraph 0.1.3 → 0.1.4
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/CHANGELOG.md +6 -0
- data/Gemfile.lock +14 -1
- data/README.md +27 -0
- data/lib/redgraph.rb +6 -0
- data/lib/redgraph/edge.rb +9 -2
- data/lib/redgraph/graph.rb +19 -22
- data/lib/redgraph/graph/edge_methods.rb +38 -11
- data/lib/redgraph/graph/node_methods.rb +27 -11
- data/lib/redgraph/node.rb +11 -3
- data/lib/redgraph/node_model.rb +160 -0
- data/lib/redgraph/query_response.rb +1 -1
- data/lib/redgraph/util.rb +21 -0
- data/lib/redgraph/version.rb +1 -1
- data/redgraph.gemspec +1 -0
- data/test/graph_edge_methods_test.rb +11 -11
- data/test/graph_manipulation_test.rb +30 -0
- data/test/graph_node_methods_test.rb +2 -6
- data/test/graph_queries_test.rb +2 -10
- data/test/node_model_test.rb +160 -0
- data/test/test_helper.rb +12 -0
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7866e9db857e11a68ddbcf8eef90b839ddc2e6fac110edfc87bfec3dd54edec7
|
4
|
+
data.tar.gz: 1c911439cd1f3f815db5a42f348e3a67449ef9aae128b4f48f9ee36934d2874d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 78a668833d8a16ba61fca88f0b58e4d6ebac5e33b68e6698c03649c5fb66bb13ff33437ac7bcb9265792059d745999148dfee47d85d5d1ae96265f650ef5f068
|
7
|
+
data.tar.gz: 85a15bfdb886959114ccb13eed27adc9b3e83e7a77783be1d5cf65d4c33c40a27501e34d5a5af82350a3c43e08178d159e7732da3b9ad86f4944930751a5e8f9
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,14 +1,24 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
redgraph (0.1.
|
4
|
+
redgraph (0.1.4)
|
5
|
+
activesupport (>= 3.0.0)
|
5
6
|
redis (~> 4)
|
6
7
|
|
7
8
|
GEM
|
8
9
|
remote: https://rubygems.org/
|
9
10
|
specs:
|
11
|
+
activesupport (6.1.3.1)
|
12
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
|
+
i18n (>= 1.6, < 2)
|
14
|
+
minitest (>= 5.1)
|
15
|
+
tzinfo (~> 2.0)
|
16
|
+
zeitwerk (~> 2.3)
|
10
17
|
coderay (1.1.3)
|
18
|
+
concurrent-ruby (1.1.8)
|
11
19
|
docile (1.3.5)
|
20
|
+
i18n (1.8.10)
|
21
|
+
concurrent-ruby (~> 1.0)
|
12
22
|
method_source (1.0.0)
|
13
23
|
minitest (5.14.4)
|
14
24
|
pry (0.14.0)
|
@@ -22,6 +32,9 @@ GEM
|
|
22
32
|
simplecov_json_formatter (~> 0.1)
|
23
33
|
simplecov-html (0.12.3)
|
24
34
|
simplecov_json_formatter (0.1.2)
|
35
|
+
tzinfo (2.0.4)
|
36
|
+
concurrent-ruby (~> 1.0)
|
37
|
+
zeitwerk (2.4.2)
|
25
38
|
|
26
39
|
PLATFORMS
|
27
40
|
x86_64-darwin-20
|
data/README.md
CHANGED
@@ -45,6 +45,16 @@ Create an edge between those nodes:
|
|
45
45
|
@graph.add_edge(edge)
|
46
46
|
=> #<Redgraph::Edge:0x00007f8d5f9ae3d8 @dest=#<Redgraph::Node:0x00007f8d5f85ccc8 @id=1, @label="film", @properties={:name=>"Scarface"}>, @dest_id=1, @id=0, @properties={:role=>"Tony Montana"}, @src=#<Redgraph::Node:0x00007f8d5f95cf88 @id=0, @label="actor", @properties={:name=>"Al Pacino"}>, @src_id=0, @type="ACTOR_IN">
|
47
47
|
|
48
|
+
You can merge nodes - the node will be created only if there isn't another with the same label and properties:
|
49
|
+
|
50
|
+
graph.merge_node(film)
|
51
|
+
=> #<Redgraph::Node:0x00007f8d5f85ccc8 @id=1, @label="film", @properties={:name=>"Scarface"}>
|
52
|
+
|
53
|
+
Same with edges:
|
54
|
+
|
55
|
+
@graph.merge_edge(edge)
|
56
|
+
=> #<Redgraph::Edge:0x00007f8d5f9ae3d8 @dest=#<Redgraph::Node:0x00007f8d5f85ccc8 @id=1, @label="film", @properties={:name=>"Scarface"}>, @dest_id=1, @id=0, @properties={:role=>"Tony Montana"}, @src=#<Redgraph::Node:0x00007f8d5f95cf88 @id=0, @label="actor", @properties={:name=>"Al Pacino"}>, @src_id=0, @type="ACTOR_IN">
|
57
|
+
|
48
58
|
Find a node by id:
|
49
59
|
|
50
60
|
@graph.find_node_by_id(1)
|
@@ -71,11 +81,28 @@ Getting edges:
|
|
71
81
|
@graph.edges
|
72
82
|
@graph.edges(src: actor, dest: film)
|
73
83
|
@graph.edges(kind: 'FRIEND_OF', limit: 10, skip: 20)
|
84
|
+
@graph.count_edges
|
74
85
|
|
75
86
|
Running custom queries
|
76
87
|
|
77
88
|
@graph.query("MATCH (src)-[edge:FRIEND_OF]->(dest) RETURN src, edge")
|
78
89
|
|
90
|
+
### NodeModel
|
91
|
+
|
92
|
+
You can use the `NodeModel` mixin for a limited ActiveRecord-like interface:
|
93
|
+
|
94
|
+
class Actor
|
95
|
+
include Redgraph::NodeModel
|
96
|
+
self.graph = Redgraph::Graph.new("movies", url: $REDIS_URL)
|
97
|
+
attribute :name
|
98
|
+
end
|
99
|
+
|
100
|
+
And this will give you stuff such as
|
101
|
+
|
102
|
+
Actor.count
|
103
|
+
john = Actor.new(name: "John Travolta")
|
104
|
+
john.add_to_graph # Will add the node to the graph
|
105
|
+
john.add_relation(type: "ACTED_IN", node: film, properties: {role: "Tony Manero"})
|
79
106
|
|
80
107
|
## Development
|
81
108
|
|
data/lib/redgraph.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "redis"
|
3
|
+
require "active_support/core_ext/hash/indifferent_access"
|
4
|
+
require "active_support/core_ext/object/blank"
|
5
|
+
require "active_support/core_ext/string/inflections"
|
6
|
+
require "active_support/concern"
|
3
7
|
|
4
8
|
require_relative "redgraph/version"
|
9
|
+
require_relative "redgraph/util"
|
5
10
|
require_relative "redgraph/graph"
|
6
11
|
require_relative "redgraph/node"
|
7
12
|
require_relative "redgraph/edge"
|
8
13
|
require_relative "redgraph/query_response"
|
14
|
+
require_relative "redgraph/node_model"
|
9
15
|
|
10
16
|
module Redgraph
|
11
17
|
class Error < StandardError; end
|
data/lib/redgraph/edge.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
module Redgraph
|
4
4
|
class Edge
|
5
|
+
include Util
|
6
|
+
|
5
7
|
attr_accessor :id, :src, :dest, :src_id, :dest_id, :type, :properties
|
6
8
|
|
7
9
|
def initialize(src: nil, dest: nil, type: nil, properties: {})
|
@@ -10,15 +12,20 @@ module Redgraph
|
|
10
12
|
@dest = dest
|
11
13
|
@dest_id = @dest.id if @dest
|
12
14
|
@type = type
|
13
|
-
@properties = properties
|
15
|
+
@properties = (properties || {}).with_indifferent_access
|
14
16
|
end
|
15
17
|
|
16
18
|
def persisted?
|
17
|
-
|
19
|
+
id.present?
|
18
20
|
end
|
19
21
|
|
20
22
|
def ==(other)
|
21
23
|
super || other.instance_of?(self.class) && !id.nil? && other.id == id
|
22
24
|
end
|
25
|
+
|
26
|
+
def to_query_string(item_alias: 'edge', src_alias: 'src', dest_alias: 'dest')
|
27
|
+
_type = ":#{type}" if type
|
28
|
+
"(#{src_alias})-[#{item_alias}#{_type} #{properties_to_string(properties)}]->(#{dest_alias})"
|
29
|
+
end
|
23
30
|
end
|
24
31
|
end
|
data/lib/redgraph/graph.rb
CHANGED
@@ -10,8 +10,12 @@ module Redgraph
|
|
10
10
|
|
11
11
|
attr_accessor :connection, :graph_name
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
# @example Graph.new("foobar", url: "redis://localhost:6379/0", logger: Logger.new(STDOUT))
|
14
|
+
# @param graph_name [String] Name of the graph
|
15
|
+
# @param redis_options [Hash] Redis client options
|
16
|
+
#
|
17
|
+
def initialize(graph_name, redis_options = {})
|
18
|
+
@graph_name = graph_name
|
15
19
|
@connection = Redis.new(redis_options)
|
16
20
|
@module_version = module_version
|
17
21
|
raise ServerError unless @module_version
|
@@ -35,27 +39,27 @@ module Redgraph
|
|
35
39
|
raise e
|
36
40
|
end
|
37
41
|
|
38
|
-
#
|
42
|
+
# @return [Array] Existing graph names
|
39
43
|
#
|
40
44
|
def list
|
41
45
|
@connection.call("GRAPH.LIST")
|
42
46
|
end
|
43
47
|
|
44
|
-
#
|
48
|
+
# @return [Array] Existing labels
|
45
49
|
#
|
46
50
|
def labels
|
47
51
|
result = _query("CALL db.labels()")
|
48
52
|
result.resultset.map(&:values).flatten
|
49
53
|
end
|
50
54
|
|
51
|
-
#
|
55
|
+
# @return [Array] Existing properties
|
52
56
|
#
|
53
57
|
def properties
|
54
58
|
result = _query("CALL db.propertyKeys()")
|
55
59
|
result.resultset.map(&:values).flatten
|
56
60
|
end
|
57
61
|
|
58
|
-
#
|
62
|
+
# @return [Array] Existing relationship types
|
59
63
|
#
|
60
64
|
def relationship_types
|
61
65
|
result = _query("CALL db.relationshipTypes()")
|
@@ -67,16 +71,25 @@ module Redgraph
|
|
67
71
|
_query(cmd).rows
|
68
72
|
end
|
69
73
|
|
74
|
+
# @param id [Integer] label id
|
75
|
+
# @return [String, nil] label
|
76
|
+
#
|
70
77
|
def get_label(id)
|
71
78
|
@labels ||= labels
|
72
79
|
@labels[id] || (@labels = labels)[id]
|
73
80
|
end
|
74
81
|
|
82
|
+
# @param id [Integer] property id
|
83
|
+
# @return [String, nil] property
|
84
|
+
#
|
75
85
|
def get_property(id)
|
76
86
|
@properties ||= properties
|
77
87
|
@properties[id] || (@properties = properties)[id]
|
78
88
|
end
|
79
89
|
|
90
|
+
# @param id [Integer] relationship type id
|
91
|
+
# @return [String, nil] relationship type
|
92
|
+
#
|
80
93
|
def get_relationship_type(id)
|
81
94
|
@relationship_types ||= relationship_types
|
82
95
|
@relationship_types[id] || (@relationship_types = relationship_types)[id]
|
@@ -88,21 +101,5 @@ module Redgraph
|
|
88
101
|
data = @connection.call("GRAPH.QUERY", graph_name, cmd, "--compact")
|
89
102
|
QueryResponse.new(data, self)
|
90
103
|
end
|
91
|
-
|
92
|
-
def quote_hash(hash)
|
93
|
-
"{" +
|
94
|
-
hash.map {|k,v| "#{k}:#{escape_value(v)}" }.join(", ") +
|
95
|
-
"}"
|
96
|
-
end
|
97
|
-
|
98
|
-
def escape_value(x)
|
99
|
-
case x
|
100
|
-
when Integer then x
|
101
|
-
when NilClass then "''"
|
102
|
-
else
|
103
|
-
'"' + x.gsub('"', '\"') + '"'
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
104
|
end
|
108
105
|
end
|
@@ -1,16 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Redgraph
|
2
4
|
class Graph
|
3
5
|
module EdgeMethods
|
4
6
|
# Adds an edge. If successul it returns the created object, otherwise false
|
5
7
|
#
|
6
8
|
def add_edge(edge)
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
merge_or_add_edge(edge, :create)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Merges (creates an edge unless one with the same type and properties exists) an edge.
|
13
|
+
# If successul it returns the object, otherwise false
|
14
|
+
#
|
15
|
+
def merge_edge(edge)
|
16
|
+
merge_or_add_edge(edge, :merge)
|
14
17
|
end
|
15
18
|
|
16
19
|
# Finds edges. Options:
|
@@ -24,8 +27,6 @@ module Redgraph
|
|
24
27
|
# - skip
|
25
28
|
#
|
26
29
|
def edges(type: nil, src: nil, dest: nil, properties: nil, order: nil, limit: nil, skip: nil)
|
27
|
-
_type = ":`#{type}`" if type
|
28
|
-
_props = quote_hash(properties) if properties
|
29
30
|
_order = if order
|
30
31
|
raise MissingAliasPrefixError unless order.include?("edge.")
|
31
32
|
"ORDER BY #{order}"
|
@@ -41,7 +42,9 @@ module Redgraph
|
|
41
42
|
"WHERE #{clauses}"
|
42
43
|
end
|
43
44
|
|
44
|
-
|
45
|
+
edge = Edge.new(type: type, src: src, dest: dest, properties: properties)
|
46
|
+
|
47
|
+
cmd = "MATCH #{edge.to_query_string} #{_where}
|
45
48
|
RETURN src, edge, dest #{_order} #{_skip} #{_limit}"
|
46
49
|
result = _query(cmd)
|
47
50
|
|
@@ -57,11 +60,24 @@ module Redgraph
|
|
57
60
|
end
|
58
61
|
end
|
59
62
|
|
63
|
+
# Counts edges. Options:
|
64
|
+
#
|
65
|
+
# - type: filter by type
|
66
|
+
# - properties: filter by properties
|
67
|
+
#
|
68
|
+
def count_edges(type: nil, properties: nil)
|
69
|
+
edge = Edge.new(type: type, properties: properties)
|
70
|
+
|
71
|
+
cmd = "MATCH #{edge.to_query_string} RETURN COUNT(edge)"
|
72
|
+
query(cmd).flatten[0]
|
73
|
+
end
|
74
|
+
|
75
|
+
|
60
76
|
private
|
61
77
|
|
62
78
|
def edge_from_resultset_item(item)
|
63
79
|
(edge_id, type_id, _src_id, _dest_id, props) = item
|
64
|
-
attrs =
|
80
|
+
attrs = HashWithIndifferentAccess.new
|
65
81
|
|
66
82
|
props.each do |(index, type, value)|
|
67
83
|
attrs[get_property(index)] = value
|
@@ -73,6 +89,17 @@ module Redgraph
|
|
73
89
|
edge.properties = attrs
|
74
90
|
end
|
75
91
|
end
|
92
|
+
|
93
|
+
def merge_or_add_edge(edge, verb = :create)
|
94
|
+
verb = verb == :create ? "CREATE" : "MERGE"
|
95
|
+
result = _query("MATCH (src), (dest)
|
96
|
+
WHERE ID(src) = #{edge.src.id} AND ID(dest) = #{edge.dest.id}
|
97
|
+
#{verb} #{edge.to_query_string} RETURN ID(edge)")
|
98
|
+
return false if result.stats[:relationships_created] != 1
|
99
|
+
id = result.resultset.first["ID(edge)"]
|
100
|
+
edge.id = id
|
101
|
+
edge
|
102
|
+
end
|
76
103
|
end
|
77
104
|
end
|
78
105
|
end
|
@@ -1,14 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Redgraph
|
2
4
|
class Graph
|
3
5
|
module NodeMethods
|
4
6
|
# Adds a node. If successul it returns the created object, otherwise false
|
5
7
|
#
|
6
8
|
def add_node(node)
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
merge_or_add_node(node, :create)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Merges (creates a node unless one with the same label and properties exists). If successul
|
13
|
+
# it returns the object, otherwise false
|
14
|
+
#
|
15
|
+
def merge_node(node)
|
16
|
+
merge_or_add_node(node, :merge)
|
12
17
|
end
|
13
18
|
|
14
19
|
def find_node_by_id(id)
|
@@ -36,7 +41,6 @@ module Redgraph
|
|
36
41
|
#
|
37
42
|
def nodes(label: nil, properties: nil, order: nil, limit: nil, skip: nil)
|
38
43
|
_label = ":`#{label}`" if label
|
39
|
-
_props = quote_hash(properties) if properties
|
40
44
|
_order = if order
|
41
45
|
raise MissingAliasPrefixError unless order.include?("node.")
|
42
46
|
"ORDER BY #{order}"
|
@@ -44,7 +48,9 @@ module Redgraph
|
|
44
48
|
_limit = "LIMIT #{limit}" if limit
|
45
49
|
_skip = "SKIP #{skip}" if skip
|
46
50
|
|
47
|
-
|
51
|
+
node = Node.new(label: label, properties: properties)
|
52
|
+
|
53
|
+
cmd = "MATCH #{node.to_query_string} RETURN node #{_order} #{_skip} #{_limit}"
|
48
54
|
|
49
55
|
result = _query(cmd)
|
50
56
|
|
@@ -59,10 +65,9 @@ module Redgraph
|
|
59
65
|
# - properties: filter by properties
|
60
66
|
#
|
61
67
|
def count_nodes(label: nil, properties: nil)
|
62
|
-
|
63
|
-
_props = quote_hash(properties) if properties
|
68
|
+
node = Node.new(label: label, properties: properties)
|
64
69
|
|
65
|
-
cmd = "MATCH
|
70
|
+
cmd = "MATCH #{node.to_query_string} RETURN COUNT(node)"
|
66
71
|
query(cmd).flatten[0]
|
67
72
|
end
|
68
73
|
|
@@ -72,7 +77,7 @@ module Redgraph
|
|
72
77
|
#
|
73
78
|
def node_from_resultset_item(item)
|
74
79
|
(node_id, labels, props) = item
|
75
|
-
attrs =
|
80
|
+
attrs = HashWithIndifferentAccess.new
|
76
81
|
|
77
82
|
props.each do |(index, type, value)|
|
78
83
|
attrs[get_property(index)] = value
|
@@ -81,6 +86,17 @@ module Redgraph
|
|
81
86
|
node.id = node_id
|
82
87
|
end
|
83
88
|
end
|
89
|
+
|
90
|
+
def merge_or_add_node(node, verb = :create)
|
91
|
+
verb = verb == :create ? "CREATE" : "MERGE"
|
92
|
+
result = _query("#{verb} #{node.to_query_string} RETURN ID(node)")
|
93
|
+
# Should we treat this case differently?
|
94
|
+
# return false if result.stats[:nodes_created] != 1
|
95
|
+
id = result.resultset.first["ID(node)"]
|
96
|
+
node.id = id
|
97
|
+
node
|
98
|
+
end
|
99
|
+
|
84
100
|
end
|
85
101
|
end
|
86
102
|
end
|
data/lib/redgraph/node.rb
CHANGED
@@ -2,19 +2,27 @@
|
|
2
2
|
|
3
3
|
module Redgraph
|
4
4
|
class Node
|
5
|
+
include Util
|
6
|
+
|
5
7
|
attr_accessor :id, :label, :properties
|
6
8
|
|
7
|
-
def initialize(label
|
9
|
+
def initialize(label: nil, properties: nil, id: nil)
|
10
|
+
@id = id
|
8
11
|
@label = label
|
9
|
-
@properties = properties
|
12
|
+
@properties = (properties || {}).with_indifferent_access
|
10
13
|
end
|
11
14
|
|
12
15
|
def persisted?
|
13
|
-
|
16
|
+
id.present?
|
14
17
|
end
|
15
18
|
|
16
19
|
def ==(other)
|
17
20
|
super || other.instance_of?(self.class) && !id.nil? && other.id == id
|
18
21
|
end
|
22
|
+
|
23
|
+
def to_query_string(item_alias: 'node')
|
24
|
+
_label = ":#{label}" if label
|
25
|
+
"(#{item_alias}#{_label} #{properties_to_string(properties)})"
|
26
|
+
end
|
19
27
|
end
|
20
28
|
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Redgraph
|
4
|
+
# This mixin allows you to use an interface similar to ActiveRecord
|
5
|
+
#
|
6
|
+
# class Actor
|
7
|
+
# include Redgraph::NodeModel
|
8
|
+
#
|
9
|
+
# self.graph = Redgraph::Graph.new("movies", url: $REDIS_URL)
|
10
|
+
# self.label = "actor" # optional, if missing it will be extracted from the class name
|
11
|
+
# attribute :name
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# You will then be able to
|
15
|
+
#
|
16
|
+
# john = Actor.find(123)
|
17
|
+
# total = Actor.count
|
18
|
+
#
|
19
|
+
module NodeModel
|
20
|
+
extend ActiveSupport::Concern
|
21
|
+
|
22
|
+
included do
|
23
|
+
@attribute_names = [:id]
|
24
|
+
|
25
|
+
attr_accessor :id
|
26
|
+
|
27
|
+
class << self
|
28
|
+
attr_reader :attribute_names
|
29
|
+
attr_accessor :graph
|
30
|
+
|
31
|
+
def attribute(name)
|
32
|
+
@attribute_names << name
|
33
|
+
attr_reader(name)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def inherited(subclass)
|
39
|
+
super
|
40
|
+
subclass.instance_variable_set(:@attribute_names, @attribute_names.dup)
|
41
|
+
subclass.instance_variable_set(:@graph, @graph.dup)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class_methods do
|
47
|
+
# Returns an array of nodes. Options:
|
48
|
+
#
|
49
|
+
# - properties: filter by properties
|
50
|
+
# - order: node.name ASC, node.year DESC
|
51
|
+
# - limit: number of items
|
52
|
+
# - skip: items offset (useful for pagination)
|
53
|
+
#
|
54
|
+
def all(properties: nil, limit: nil, skip: nil, order: nil)
|
55
|
+
graph.nodes(label: label, properties: properties,
|
56
|
+
limit: limit, skip: skip, order: nil).map do |node|
|
57
|
+
new(id: node.id, **node.properties)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the number of nodes with the current label. Options:
|
62
|
+
#
|
63
|
+
# - properties: filter by properties
|
64
|
+
#
|
65
|
+
def count(properties: nil)
|
66
|
+
graph.count_nodes(label: label, properties: properties)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Finds a node by id. Returns nil if not found
|
70
|
+
#
|
71
|
+
def find(id)
|
72
|
+
node = graph.find_node_by_id(id)
|
73
|
+
return unless node
|
74
|
+
new(id: node.id, **node.properties)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Sets the label for this class of nodes. If missing it will be computed from the class name
|
78
|
+
def label=(x)
|
79
|
+
@label = x
|
80
|
+
end
|
81
|
+
|
82
|
+
# Current label
|
83
|
+
#
|
84
|
+
def label
|
85
|
+
@label ||= default_label
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def default_label
|
91
|
+
name.demodulize.underscore
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def initialize(**args)
|
96
|
+
absent_attributes = args.keys.map(&:to_sym) - self.class.attribute_names
|
97
|
+
|
98
|
+
if absent_attributes.any?
|
99
|
+
raise ArgumentError, "Unknown attribute #{absent_attributes}"
|
100
|
+
end
|
101
|
+
|
102
|
+
args.each do |name, value|
|
103
|
+
instance_variable_set("@#{name}", value)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# The current graph
|
108
|
+
#
|
109
|
+
def graph
|
110
|
+
self.class.graph
|
111
|
+
end
|
112
|
+
|
113
|
+
def label
|
114
|
+
self.class.label
|
115
|
+
end
|
116
|
+
|
117
|
+
# Object attributes as a hash
|
118
|
+
#
|
119
|
+
def attributes
|
120
|
+
self.class.attribute_names.to_h { |name| [name, public_send(name)] }
|
121
|
+
end
|
122
|
+
|
123
|
+
def persisted?
|
124
|
+
id.present?
|
125
|
+
end
|
126
|
+
|
127
|
+
# Adds the node to the graph
|
128
|
+
#
|
129
|
+
# - allow_duplicates: if false it will create a node with the same type and properties only if
|
130
|
+
# not present
|
131
|
+
#
|
132
|
+
def add_to_graph(allow_duplicates: true)
|
133
|
+
item = allow_duplicates ? graph.add_node(to_node) : graph.merge_node(to_node)
|
134
|
+
self.id = item.id
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
# Adds a relation between the node and another node.
|
139
|
+
#
|
140
|
+
# - type: type of relation
|
141
|
+
# - node: the destination node
|
142
|
+
# - properties: optional properties hash
|
143
|
+
# - allow_duplicates: if false it will create a relation between two nodes with the same type
|
144
|
+
# and properties only if not present
|
145
|
+
#
|
146
|
+
def add_relation(type:, node:, properties: nil, allow_duplicates: true)
|
147
|
+
edge = Edge.new(type: type, src: to_node, dest: node.to_node, properties: properties)
|
148
|
+
allow_duplicates ? graph.add_edge(edge) : graph.merge_edge(edge)
|
149
|
+
end
|
150
|
+
|
151
|
+
def to_node
|
152
|
+
Redgraph::Node.new(id: id, label: label, properties: attributes.except(:id))
|
153
|
+
end
|
154
|
+
|
155
|
+
def ==(other)
|
156
|
+
attributes == other.attributes && id == other.id
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
@@ -136,7 +136,7 @@ module Redgraph
|
|
136
136
|
# The resultset has one element per entity (as described by the header)
|
137
137
|
def parse_resultset
|
138
138
|
@result_rows.map do |item|
|
139
|
-
out =
|
139
|
+
out = HashWithIndifferentAccess.new
|
140
140
|
|
141
141
|
item.each.with_index do |(type, value), i|
|
142
142
|
out[entities[i]] = value
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Redgraph
|
4
|
+
module Util
|
5
|
+
def properties_to_string(hash)
|
6
|
+
return if hash.empty?
|
7
|
+
"{" +
|
8
|
+
hash.map {|k,v| "#{k}:#{escape_value(v)}" }.join(", ") +
|
9
|
+
"}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def escape_value(x)
|
13
|
+
case x
|
14
|
+
when Integer then x
|
15
|
+
when NilClass then "''"
|
16
|
+
else
|
17
|
+
'"' + x.gsub('"', '\"') + '"'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/redgraph/version.rb
CHANGED
data/redgraph.gemspec
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require "test_helper"
|
4
4
|
|
5
5
|
class GraphEdgeMethodsTest < Minitest::Test
|
6
|
+
include TestHelpers
|
7
|
+
|
6
8
|
def setup
|
7
9
|
@graph = Redgraph::Graph.new("movies", url: $REDIS_URL)
|
8
10
|
|
@@ -22,7 +24,6 @@ class GraphEdgeMethodsTest < Minitest::Test
|
|
22
24
|
assert_equal(1980, edge.properties["since"])
|
23
25
|
assert_equal(@al, edge.src)
|
24
26
|
assert_equal(@john, edge.dest)
|
25
|
-
|
26
27
|
end
|
27
28
|
|
28
29
|
def test_find_all_edges
|
@@ -35,6 +36,15 @@ class GraphEdgeMethodsTest < Minitest::Test
|
|
35
36
|
assert_equal(2, edges.size)
|
36
37
|
end
|
37
38
|
|
39
|
+
def test_count_edges
|
40
|
+
marlon = quick_add_node(label: 'actor', properties: {name: "Marlon Brando"})
|
41
|
+
film = quick_add_node(label: 'film', properties: {name: "The Godfather"})
|
42
|
+
quick_add_edge(type: 'ACTOR_IN', src: marlon, dest: film, properties: {role: 'Don Vito'})
|
43
|
+
quick_add_edge(type: 'ACTOR_IN', src: @al, dest: film, properties: {role: 'Michael'})
|
44
|
+
|
45
|
+
assert_equal(2, @graph.count_edges)
|
46
|
+
end
|
47
|
+
|
38
48
|
def test_filter_edges
|
39
49
|
marlon = quick_add_node(label: 'actor', properties: {name: "Marlon Brando"})
|
40
50
|
film = quick_add_node(label: 'film', properties: {name: "The Godfather"})
|
@@ -87,14 +97,4 @@ class GraphEdgeMethodsTest < Minitest::Test
|
|
87
97
|
edges = @graph.edges(type: 'FRIEND_OF', order: "edge.since DESC")
|
88
98
|
assert_equal([e2, e3, e1], edges)
|
89
99
|
end
|
90
|
-
|
91
|
-
private
|
92
|
-
|
93
|
-
def quick_add_node(label:, properties:)
|
94
|
-
@graph.add_node(Redgraph::Node.new(label: label, properties: properties))
|
95
|
-
end
|
96
|
-
|
97
|
-
def quick_add_edge(type:, src:, dest:, properties:)
|
98
|
-
@graph.add_edge(Redgraph::Edge.new(type: type, src: src, dest: dest, properties: properties))
|
99
|
-
end
|
100
100
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require "test_helper"
|
4
4
|
|
5
5
|
class GraphManipulationTest < Minitest::Test
|
6
|
+
include TestHelpers
|
7
|
+
|
6
8
|
def setup
|
7
9
|
@graph = Redgraph::Graph.new("movies", url: $REDIS_URL)
|
8
10
|
end
|
@@ -58,4 +60,32 @@ class GraphManipulationTest < Minitest::Test
|
|
58
60
|
|
59
61
|
assert_predicate result, :persisted?
|
60
62
|
end
|
63
|
+
|
64
|
+
def test_merge_node
|
65
|
+
quick_add_node(label: 'actor', properties: {name: "Al Pacino"})
|
66
|
+
quick_add_node(label: 'actor', properties: {name: "John Travolta"})
|
67
|
+
|
68
|
+
nodes = @graph.nodes(label: 'actor')
|
69
|
+
assert_equal(2, nodes.size)
|
70
|
+
|
71
|
+
@graph.merge_node(Redgraph::Node.new(label: 'actor', properties: {name: "Joe Pesci"}))
|
72
|
+
assert_equal(3, @graph.nodes(label: 'actor').size)
|
73
|
+
|
74
|
+
@graph.merge_node(Redgraph::Node.new(label: 'actor', properties: {name: "Al Pacino"}))
|
75
|
+
assert_equal(3, @graph.nodes(label: 'actor').size)
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_merge_edge
|
79
|
+
al = quick_add_node(label: 'actor', properties: {name: "Al Pacino"})
|
80
|
+
john = quick_add_node(label: 'actor', properties: {name: "John Travolta"})
|
81
|
+
|
82
|
+
assert_equal(0, @graph.edges.size)
|
83
|
+
|
84
|
+
edge = Redgraph::Edge.new(type: 'FRIEND_OF', src: al, dest: john, properties: {since: 1990})
|
85
|
+
@graph.merge_edge(edge)
|
86
|
+
assert_equal(1, @graph.edges.size)
|
87
|
+
|
88
|
+
@graph.merge_edge(edge)
|
89
|
+
assert_equal(1, @graph.edges.size)
|
90
|
+
end
|
61
91
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require "test_helper"
|
4
4
|
|
5
5
|
class GraphNodeMethodsTest < Minitest::Test
|
6
|
+
include TestHelpers
|
7
|
+
|
6
8
|
def setup
|
7
9
|
@graph = Redgraph::Graph.new("movies", url: $REDIS_URL)
|
8
10
|
|
@@ -107,10 +109,4 @@ class GraphNodeMethodsTest < Minitest::Test
|
|
107
109
|
assert_equal(3, items.size)
|
108
110
|
assert_equal([3,4,5], items.map{|item| item.properties["number"]})
|
109
111
|
end
|
110
|
-
|
111
|
-
private
|
112
|
-
|
113
|
-
def quick_add_node(label:, properties:)
|
114
|
-
@graph.add_node(Redgraph::Node.new(label: label, properties: properties))
|
115
|
-
end
|
116
112
|
end
|
data/test/graph_queries_test.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require "test_helper"
|
4
4
|
|
5
5
|
class GraphQueriesTest < Minitest::Test
|
6
|
+
include TestHelpers
|
7
|
+
|
6
8
|
def setup
|
7
9
|
@graph = Redgraph::Graph.new("movies", url: $REDIS_URL)
|
8
10
|
|
@@ -40,14 +42,4 @@ class GraphQueriesTest < Minitest::Test
|
|
40
42
|
result = @graph.query("MATCH (src)-[edge:FRIEND_OF]->(dest) RETURN src, edge")
|
41
43
|
assert_equal([[@al, edge]], result)
|
42
44
|
end
|
43
|
-
|
44
|
-
private
|
45
|
-
|
46
|
-
def quick_add_node(label:, properties:)
|
47
|
-
@graph.add_node(Redgraph::Node.new(label: label, properties: properties))
|
48
|
-
end
|
49
|
-
|
50
|
-
def quick_add_edge(type:, src:, dest:, properties:)
|
51
|
-
@graph.add_edge(Redgraph::Edge.new(type: type, src: src, dest: dest, properties: properties))
|
52
|
-
end
|
53
45
|
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class NodeModelTest < Minitest::Test
|
6
|
+
include TestHelpers
|
7
|
+
|
8
|
+
GRAPH = Redgraph::Graph.new("movies", url: $REDIS_URL)
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@graph = GRAPH
|
12
|
+
end
|
13
|
+
|
14
|
+
def teardown
|
15
|
+
@graph.delete
|
16
|
+
end
|
17
|
+
|
18
|
+
class Animal
|
19
|
+
include Redgraph::NodeModel
|
20
|
+
attribute :name
|
21
|
+
self.graph = "pippo"
|
22
|
+
end
|
23
|
+
|
24
|
+
class Dog < Animal
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_graph_accessor
|
28
|
+
assert_equal("pippo", Animal.graph)
|
29
|
+
assert_equal("pippo", Animal.new.graph)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_class_inheritance
|
33
|
+
assert_equal("pippo", Dog.graph)
|
34
|
+
assert_equal("dog", Dog.label)
|
35
|
+
assert_equal([:id, :name], Dog.attribute_names)
|
36
|
+
end
|
37
|
+
|
38
|
+
class Actor
|
39
|
+
include Redgraph::NodeModel
|
40
|
+
self.graph = GRAPH
|
41
|
+
attribute :name
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_count
|
45
|
+
quick_add_node(label: 'actor', properties: {name: "Al Pacino"})
|
46
|
+
quick_add_node(label: 'actor', properties: {name: "John Travolta"})
|
47
|
+
assert_equal(2, Actor.count)
|
48
|
+
assert_equal(1, Actor.count(properties: {name: "Al Pacino"}))
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_all
|
52
|
+
al = Actor.new(name: "Al Pacino").add_to_graph
|
53
|
+
john = Actor.new(name: "John Travolta").add_to_graph
|
54
|
+
|
55
|
+
items = Actor.all
|
56
|
+
assert_equal(2, items.size)
|
57
|
+
assert_includes(items, al)
|
58
|
+
assert_includes(items, john)
|
59
|
+
|
60
|
+
items = Actor.all(properties: {name: "Al Pacino"})
|
61
|
+
assert_equal(1, items.size)
|
62
|
+
assert_includes(items, al)
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_find
|
66
|
+
al = quick_add_node(label: 'actor', properties: {name: "Al Pacino"})
|
67
|
+
item = Actor.find(al.id)
|
68
|
+
|
69
|
+
assert_equal(Actor, item.class)
|
70
|
+
assert_predicate(item, :persisted?)
|
71
|
+
assert_equal(al.id, item.id)
|
72
|
+
assert_equal("Al Pacino", item.name)
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_find_bad_id
|
76
|
+
quick_add_node(label: 'actor', properties: {name: "Al Pacino"})
|
77
|
+
item = Actor.find("-1")
|
78
|
+
assert_nil(item)
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_label
|
82
|
+
assert_equal("actor", Actor.label)
|
83
|
+
end
|
84
|
+
|
85
|
+
class Artist
|
86
|
+
include Redgraph::NodeModel
|
87
|
+
self.label = "person"
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_custom_label
|
91
|
+
assert_equal("person", Artist.label)
|
92
|
+
end
|
93
|
+
|
94
|
+
class Film
|
95
|
+
include Redgraph::NodeModel
|
96
|
+
self.graph = GRAPH
|
97
|
+
attribute :name
|
98
|
+
attribute :year
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_attribute_names
|
102
|
+
assert_equal([:id, :name, :year], Film.attribute_names)
|
103
|
+
|
104
|
+
film = Film.new(name: "Star Wars", year: 1977)
|
105
|
+
assert_equal("Star Wars", film.name)
|
106
|
+
assert_equal(1977, film.year)
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_attributes
|
110
|
+
film = Film.new(name: "Star Wars", year: 1977)
|
111
|
+
assert_equal({id: nil, name: "Star Wars", year: 1977}, film.attributes)
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_add_to_graph
|
115
|
+
assert_equal(0, Film.count)
|
116
|
+
film = Film.new(name: "Star Wars", year: 1977)
|
117
|
+
item = film.add_to_graph
|
118
|
+
assert_predicate(item, :persisted?)
|
119
|
+
|
120
|
+
assert_equal(1, Film.count)
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_merge_into_graph
|
124
|
+
film = Film.new(name: "Star Wars", year: 1977)
|
125
|
+
item = film.add_to_graph(allow_duplicates: false)
|
126
|
+
assert_predicate(item, :persisted?)
|
127
|
+
|
128
|
+
assert_equal(1, Film.count)
|
129
|
+
|
130
|
+
film = Film.new(name: "Star Wars", year: 1977)
|
131
|
+
item = film.add_to_graph(allow_duplicates: false)
|
132
|
+
assert_predicate(item, :persisted?)
|
133
|
+
|
134
|
+
assert_equal(1, Film.count)
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_add_relation
|
138
|
+
film = Film.new(name: "Star Wars", year: 1977).add_to_graph
|
139
|
+
actor = Actor.new(name: "Harrison Ford").add_to_graph
|
140
|
+
edge = actor.add_relation(type: "ACTED_IN", node: film, properties: {role: "Han Solo"})
|
141
|
+
|
142
|
+
assert_predicate(edge, :persisted?)
|
143
|
+
assert_equal("ACTED_IN", edge.type)
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_add_relation_with_duplicate_control
|
147
|
+
film = Film.new(name: "Star Wars", year: 1977).add_to_graph
|
148
|
+
actor = Actor.new(name: "Harrison Ford").add_to_graph
|
149
|
+
|
150
|
+
actor.add_relation(type: "ACTED_IN", node: film, properties: {role: "Han Solo"}, allow_duplicates: true)
|
151
|
+
assert_equal(1, @graph.count_edges)
|
152
|
+
|
153
|
+
actor.add_relation(type: "ACTED_IN", node: film, properties: {role: "Han Solo"}, allow_duplicates: true)
|
154
|
+
assert_equal(2, @graph.count_edges)
|
155
|
+
|
156
|
+
actor.add_relation(type: "ACTED_IN", node: film, properties: {role: "Han Solo"}, allow_duplicates: false)
|
157
|
+
assert_equal(2, @graph.count_edges)
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -19,3 +19,15 @@ unless $REDIS_URL = ENV['TEST_REDIS_URL']
|
|
19
19
|
puts "To run the tests you need to define the TEST_REDIS_URL environment variable"
|
20
20
|
exit(1)
|
21
21
|
end
|
22
|
+
|
23
|
+
module TestHelpers
|
24
|
+
private
|
25
|
+
|
26
|
+
def quick_add_node(label:, properties:)
|
27
|
+
@graph.add_node(Redgraph::Node.new(label: label, properties: properties))
|
28
|
+
end
|
29
|
+
|
30
|
+
def quick_add_edge(type:, src:, dest:, properties:)
|
31
|
+
@graph.add_edge(Redgraph::Edge.new(type: type, src: src, dest: dest, properties: properties))
|
32
|
+
end
|
33
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redgraph
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paolo Zaccagnini
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-04-
|
11
|
+
date: 2021-04-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.0.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.0.0
|
27
41
|
description:
|
28
42
|
email:
|
29
43
|
- hi@pzac.net
|
@@ -47,7 +61,9 @@ files:
|
|
47
61
|
- lib/redgraph/graph/edge_methods.rb
|
48
62
|
- lib/redgraph/graph/node_methods.rb
|
49
63
|
- lib/redgraph/node.rb
|
64
|
+
- lib/redgraph/node_model.rb
|
50
65
|
- lib/redgraph/query_response.rb
|
66
|
+
- lib/redgraph/util.rb
|
51
67
|
- lib/redgraph/version.rb
|
52
68
|
- redgraph.gemspec
|
53
69
|
- test/graph_connection_test.rb
|
@@ -56,6 +72,7 @@ files:
|
|
56
72
|
- test/graph_node_methods_test.rb
|
57
73
|
- test/graph_queries_test.rb
|
58
74
|
- test/graph_test.rb
|
75
|
+
- test/node_model_test.rb
|
59
76
|
- test/redgraph_test.rb
|
60
77
|
- test/test_helper.rb
|
61
78
|
homepage: https://github.com/pzac/redgraph
|