redgraph 0.1.1 → 0.2.1

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.
@@ -0,0 +1,56 @@
1
+ module Redgraph
2
+ module NodeModel
3
+ module Persistence
4
+ # Adds the node to the graph
5
+ #
6
+ # - allow_duplicates: if false it will create a node with the same type and properties only if
7
+ # not present
8
+ #
9
+ def add_to_graph(allow_duplicates: true)
10
+ raise MissingGraphError unless graph
11
+ item = allow_duplicates ? graph.add_node(to_node) : graph.merge_node(to_node)
12
+ self.id = item.id
13
+ self
14
+ end
15
+
16
+ # Creates a new record or updates the existing
17
+ #
18
+ def save
19
+ if persisted?
20
+ item = graph.update_node(to_node)
21
+ self.class.reify_from_node(item)
22
+ else
23
+ add_to_graph
24
+ end
25
+ end
26
+
27
+ def persisted?
28
+ id.present?
29
+ end
30
+
31
+ def reload
32
+ item = self.class.find(id)
33
+ @label = item.label
34
+ assign_attributes(item.attributes)
35
+ self
36
+ end
37
+
38
+ # Deletes the record from the graph
39
+ #
40
+ def destroy
41
+ @destroyed = true
42
+ if graph.destroy_node(self)
43
+ self
44
+ else
45
+ false
46
+ end
47
+ end
48
+
49
+ # Returns true if this object has been destroyed, otherwise returns false.
50
+ #
51
+ def destroyed?
52
+ !!@destroyed
53
+ end
54
+ end
55
+ end
56
+ end
@@ -10,8 +10,35 @@ module Redgraph
10
10
  # - query stats
11
11
  #
12
12
  class QueryResponse
13
- def initialize(response)
13
+ TYPES = [
14
+ UNKNOWN = 0,
15
+ NULL = 1,
16
+ STRING = 2,
17
+ INTEGER = 3,
18
+ BOOLEAN = 4,
19
+ DOUBLE = 5,
20
+ ARRAY = 6,
21
+ EDGE = 7,
22
+ NODE = 8,
23
+ PATH = 9,
24
+ MAP = 10,
25
+ POINT = 11
26
+ ].freeze
27
+
28
+ def initialize(response, graph)
14
29
  @response = response
30
+ @graph = graph
31
+
32
+ case @response.size
33
+ when 3
34
+ @header_row = @response[0]
35
+ @result_rows = @response[1]
36
+ @query_statistics = @response[2]
37
+ when 1 # queries with no RETURN clause
38
+ @header_row = []
39
+ @result_rows = []
40
+ @query_statistics = @response[0]
41
+ end
15
42
  end
16
43
 
17
44
  def stats
@@ -26,24 +53,83 @@ module Redgraph
26
53
  @resultset ||= parse_resultset
27
54
  end
28
55
 
56
+ # Wraps in custom datatypes if needed
57
+ #
58
+ def rows
59
+ @result_rows.map do |column|
60
+ column.map do |data|
61
+ reify_column_item(data)
62
+ end
63
+
64
+ end
65
+ end
66
+
29
67
  private
30
68
 
69
+ def reify_column_item(data)
70
+ value_type, value = data
71
+
72
+ case value_type
73
+ when STRING, INTEGER, BOOLEAN, DOUBLE then value
74
+ when NODE then reify_node_item(value)
75
+ when EDGE then reify_edge_item(value)
76
+ else
77
+ "other"
78
+ end
79
+ end
80
+
81
+ def reify_node_item(data)
82
+ (node_id, labels, props) = data
83
+
84
+ label = @graph.get_label(labels[0]) # Only one label is currently supported
85
+
86
+ node = Node.new(label: label)
87
+ node.id = node_id
88
+
89
+ props.each do |(prop_id, prop_type, prop_value)|
90
+ prop_name = @graph.get_property(prop_id)
91
+ node.properties[prop_name] = prop_value
92
+ end
93
+
94
+ node
95
+ end
96
+
97
+ def reify_edge_item(data)
98
+ (edge_id, type_id, src_id, dest_id, props) = data
99
+
100
+ type = @graph.get_relationship_type(type_id)
101
+
102
+ edge = Edge.new(type: type)
103
+ edge.id = edge_id
104
+ edge.src_id = src_id
105
+ edge.dest_id = dest_id
106
+
107
+ props.each do |(prop_id, prop_type, prop_value)|
108
+ prop_name = @graph.get_property(prop_id)
109
+ edge.properties[prop_name] = prop_value
110
+ end
111
+
112
+ edge
113
+ end
114
+
31
115
  # The header lists the entities described in the RETURN clause. It is an
32
116
  # array of [ColumnType (enum), name (string)] elements. We can ignore the
33
117
  # enum, it is always 1 (COLUMN_SCALAR).
34
118
  def parse_header
35
- @response[0].map{|item| item[1]}
119
+ @header_row.map{|item| item[1]}
36
120
  end
37
121
 
38
122
  def parse_stats
39
123
  stats = {}
40
124
 
41
- @response[2].each do |item|
125
+ @query_statistics.each do |item|
42
126
  label, value = item.split(":")
43
127
 
44
128
  case label
45
129
  when /^Nodes created/
46
130
  stats[:nodes_created] = value.to_i
131
+ when /^Nodes deleted/
132
+ stats[:nodes_deleted] = value.to_i
47
133
  when /^Relationships created/
48
134
  stats[:relationships_created] = value.to_i
49
135
  when /^Properties set/
@@ -58,8 +144,8 @@ module Redgraph
58
144
 
59
145
  # The resultset has one element per entity (as described by the header)
60
146
  def parse_resultset
61
- @response[1].map do |item|
62
- out = {}
147
+ @result_rows.map do |item|
148
+ out = HashWithIndifferentAccess.new
63
149
 
64
150
  item.each.with_index do |(type, value), i|
65
151
  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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Redgraph
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.1"
5
5
  end
data/redgraph.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
 
16
16
  spec.metadata["homepage_uri"] = spec.homepage
17
17
  spec.metadata["source_code_uri"] = spec.homepage
18
- spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
18
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
19
19
 
20
20
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
21
  `git ls-files -z`.split("\x0")
@@ -23,4 +23,5 @@ Gem::Specification.new do |spec|
23
23
  spec.require_paths = ["lib"]
24
24
 
25
25
  spec.add_dependency "redis", "~> 4"
26
+ spec.add_dependency "activesupport", ">= 3.0.0"
26
27
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class GraphEdgeMethodsTest < Minitest::Test
6
+ include TestHelpers
7
+
8
+ def setup
9
+ @graph = Redgraph::Graph.new("movies", url: $REDIS_URL)
10
+
11
+ @al = quick_add_node(label: 'actor', properties: {name: "Al Pacino"})
12
+ @john = quick_add_node(label: 'actor', properties: {name: "John Travolta"})
13
+ end
14
+
15
+ def teardown
16
+ @graph.delete
17
+ end
18
+
19
+ def test_find_edge
20
+ quick_add_edge(type: 'FRIEND_OF', src: @al, dest: @john, properties: {since: 1980})
21
+ edge = @graph.edges.first
22
+
23
+ assert_equal('FRIEND_OF', edge.type)
24
+ assert_equal(1980, edge.properties["since"])
25
+ assert_equal(@al, edge.src)
26
+ assert_equal(@john, edge.dest)
27
+ end
28
+
29
+ def test_find_all_edges
30
+ marlon = quick_add_node(label: 'actor', properties: {name: "Marlon Brando"})
31
+ film = quick_add_node(label: 'film', properties: {name: "The Godfather"})
32
+ quick_add_edge(type: 'ACTOR_IN', src: marlon, dest: film, properties: {role: 'Don Vito'})
33
+ quick_add_edge(type: 'ACTOR_IN', src: @al, dest: film, properties: {role: 'Michael'})
34
+
35
+ edges = @graph.edges
36
+ assert_equal(2, edges.size)
37
+ end
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
+
48
+ def test_filter_edges
49
+ marlon = quick_add_node(label: 'actor', properties: {name: "Marlon Brando"})
50
+ film = quick_add_node(label: 'film', properties: {name: "The Godfather"})
51
+ other_film = quick_add_node(label: 'film', properties: {name: "Carlito's Way"})
52
+ e_donvito = quick_add_edge(type: 'ACTOR_IN', src: marlon, dest: film, properties: {role: 'Don Vito'})
53
+ e_michael = quick_add_edge(type: 'ACTOR_IN', src: @al, dest: film, properties: {role: 'Michael'})
54
+ e_carlito = quick_add_edge(type: 'ACTOR_IN', src: @al, dest: other_film, properties: {role: 'Carlito'})
55
+ quick_add_edge(type: 'FRIEND_OF', src: @al, dest: marlon, properties: {since: 1980})
56
+
57
+ edges = @graph.edges(type: "FRIEND_OF")
58
+ assert_equal(1, edges.size)
59
+
60
+ edges = @graph.edges(type: "ACTOR_IN")
61
+ assert_equal(3, edges.size)
62
+
63
+ edges = @graph.edges(type: "ACTOR_IN", limit: 2)
64
+ assert_equal(2, edges.size)
65
+
66
+ edges = @graph.edges(type: "ACTOR_IN", skip: 2, limit: 10)
67
+ assert_equal(1, edges.size)
68
+
69
+ edges = @graph.edges(properties: {role: "Carlito"})
70
+ assert_equal([e_carlito], edges)
71
+
72
+ edges = @graph.edges(src: marlon)
73
+ assert_equal([e_donvito], edges)
74
+
75
+ edges = @graph.edges(type: 'ACTOR_IN', dest: film)
76
+ assert_equal(2, edges.size)
77
+ assert_includes(edges, e_donvito)
78
+ assert_includes(edges, e_michael)
79
+
80
+ edges = @graph.edges(src: @al, dest: marlon)
81
+ assert_equal(1, edges.size)
82
+ edge = edges[0]
83
+ assert_equal('FRIEND_OF', edge.type)
84
+ assert_equal(1980, edge.properties["since"])
85
+ end
86
+
87
+ def test_order_edges
88
+ marlon = quick_add_node(label: 'actor', properties: {name: "Marlon Brando"})
89
+
90
+ e1 = quick_add_edge(type: 'FRIEND_OF', src: @al, dest: marlon, properties: {since: 1980})
91
+ e2 = quick_add_edge(type: 'FRIEND_OF', src: @al, dest: @john, properties: {since: 2000})
92
+ e3 = quick_add_edge(type: 'FRIEND_OF', src: marlon, dest: @john, properties: {since: 1990})
93
+
94
+ edges = @graph.edges(type: 'FRIEND_OF', order: "edge.since ASC")
95
+ assert_equal([e1, e3, e2], edges)
96
+
97
+ edges = @graph.edges(type: 'FRIEND_OF', order: "edge.since DESC")
98
+ assert_equal([e2, e3, e1], edges)
99
+ end
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
@@ -17,6 +19,35 @@ class GraphManipulationTest < Minitest::Test
17
19
  assert_predicate result, :persisted?
18
20
  end
19
21
 
22
+ def test_add_node_with_special_chars
23
+ [
24
+ "apo'str",
25
+ "two''apos",
26
+ "Foø'bÆ®",
27
+ "aa\nbb",
28
+ 'aaa "bbb" ccc'
29
+ ].each do |name|
30
+
31
+ node = Redgraph::Node.new(label: 'actor', properties: {name: name})
32
+ result = @graph.add_node(node)
33
+ assert_predicate result, :persisted?
34
+
35
+ item = @graph.find_node_by_id(node.id)
36
+
37
+ assert_equal(name, item.properties["name"])
38
+ end
39
+ end
40
+
41
+ def test_add_node_with_nil_value
42
+ node = Redgraph::Node.new(label: 'actor', properties: {name: nil})
43
+ result = @graph.add_node(node)
44
+ assert_predicate result, :persisted?
45
+
46
+ item = @graph.find_node_by_id(node.id)
47
+
48
+ assert_equal("", item.properties["name"])
49
+ end
50
+
20
51
  def test_add_edge
21
52
  actor = Redgraph::Node.new(label: 'actor', properties: {name: "Al Pacino"})
22
53
  @graph.add_node(actor)
@@ -29,4 +60,32 @@ class GraphManipulationTest < Minitest::Test
29
60
 
30
61
  assert_predicate result, :persisted?
31
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
32
91
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class GraphNodeMethodsTest < Minitest::Test
6
+ include TestHelpers
7
+
8
+ def setup
9
+ @graph = Redgraph::Graph.new("movies", url: $REDIS_URL)
10
+
11
+ @al = quick_add_node(label: 'actor', properties: {name: "Al Pacino"})
12
+ @john = quick_add_node(label: 'actor', properties: {name: "John Travolta"})
13
+ end
14
+
15
+ def teardown
16
+ @graph.delete
17
+ end
18
+
19
+ def test_find_node_by_id
20
+ node = @graph.find_node_by_id(@al.id)
21
+
22
+ refute_nil(node)
23
+ assert_equal("actor", node.label)
24
+ assert_equal("Al Pacino", node.properties["name"])
25
+ assert_equal(@al.id, node.id)
26
+ end
27
+
28
+ def test_find_node_by_wrong_id
29
+ node = @graph.find_node_by_id("-1")
30
+
31
+ assert_nil(node)
32
+ end
33
+
34
+ def test_find_all_nodes
35
+ actors = @graph.nodes
36
+
37
+ assert_equal(2, actors.size)
38
+ assert_includes(actors, @al)
39
+ assert_includes(actors, @john)
40
+ end
41
+
42
+ def test_find_all_nodes_by_label
43
+ film = quick_add_node(label: 'film', properties: {name: "Scarface"})
44
+
45
+ actors = @graph.nodes(label: 'actor')
46
+ assert_equal(2, actors.size)
47
+ assert_includes(actors, @al)
48
+ assert_includes(actors, @john)
49
+
50
+ films = @graph.nodes(label: 'film')
51
+ assert_equal(1, films.size)
52
+ assert_includes(films, film)
53
+ end
54
+
55
+ def test_find_all_nodes_by_property
56
+ scarface = quick_add_node(label: 'film', properties: {name: "Scarface", genre: "drama"})
57
+ casino = quick_add_node(label: 'film', properties: {name: "Casino", genre: "drama"})
58
+ _mamma_mia = quick_add_node(label: 'film', properties: {name: "Mamma Mia", genre: "musical"})
59
+
60
+ dramas = @graph.nodes(properties: {genre: "drama"})
61
+
62
+ assert_equal(2, dramas.size)
63
+ assert_includes(dramas, scarface)
64
+ assert_includes(dramas, casino)
65
+ end
66
+
67
+ def test_order_nodes_by_property
68
+ scarface = quick_add_node(label: 'film', properties: {name: "Scarface", genre: "drama"})
69
+ casino = quick_add_node(label: 'film', properties: {name: "Casino", genre: "drama"})
70
+ mamma_mia = quick_add_node(label: 'film', properties: {name: "Mamma Mia", genre: "musical"})
71
+
72
+ items = @graph.nodes(label: 'film', order: "node.name")
73
+ assert_equal([casino, mamma_mia, scarface], items)
74
+
75
+ items = @graph.nodes(label: 'film', order: "node.name ASC")
76
+ assert_equal([casino, mamma_mia, scarface], items)
77
+
78
+ items = @graph.nodes(label: 'film', order: "node.name DESC")
79
+ assert_equal([scarface, mamma_mia, casino], items)
80
+ end
81
+
82
+ def test_count_nodes
83
+ quick_add_node(label: 'film', properties: {name: "Scarface", genre: "drama"})
84
+ quick_add_node(label: 'film', properties: {name: "Casino", genre: "drama"})
85
+ quick_add_node(label: 'film', properties: {name: "Mamma Mia", genre: "musical"})
86
+
87
+
88
+ assert_equal(5, @graph.count_nodes)
89
+ assert_equal(3, @graph.count_nodes(label: 'film'))
90
+ assert_equal(2, @graph.count_nodes(properties: {genre: "drama"}))
91
+ end
92
+
93
+ def test_limit_nodes
94
+ 10.times do |i|
95
+ quick_add_node(label: 'token', properties: {number: i})
96
+ end
97
+
98
+ items = @graph.nodes(label: 'token', limit: 5)
99
+ assert_equal(5, items.size)
100
+ assert_equal([0,1,2,3,4], items.map{|item| item.properties["number"]})
101
+ end
102
+
103
+ def test_skip_nodes
104
+ 10.times do |i|
105
+ quick_add_node(label: 'token', properties: {number: i})
106
+ end
107
+
108
+ items = @graph.nodes(label: 'token', limit: 3, skip: 3)
109
+ assert_equal(3, items.size)
110
+ assert_equal([3,4,5], items.map{|item| item.properties["number"]})
111
+ end
112
+ end