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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a432a1b2f9b30601701af0c1464c3c93c14c94da044bad69e531bd3a9f6b26d
4
- data.tar.gz: 42f9ce782a67034c5d64c28c02e44d31272945073a1a868c521d7f4213346fc1
3
+ metadata.gz: 7866e9db857e11a68ddbcf8eef90b839ddc2e6fac110edfc87bfec3dd54edec7
4
+ data.tar.gz: 1c911439cd1f3f815db5a42f348e3a67449ef9aae128b4f48f9ee36934d2874d
5
5
  SHA512:
6
- metadata.gz: fc2c9b7a5d6653fcc997807c994ec55f9be3f514f510c20d9b4569e9d14faedf112462c68e7127e5c7310b7b444d84dfe14b2b8d0332f5a884d3d7c096d5e85e
7
- data.tar.gz: 7e031759d2edc7d7577b8df4db41ae055a35f5f46fdb434cf11b36aa7874e3f297c5512164c5e739a56f3ce5eba5200f0a18af9b9eeb0a8feeda09f61e8e73f3
6
+ metadata.gz: 78a668833d8a16ba61fca88f0b58e4d6ebac5e33b68e6698c03649c5fb66bb13ff33437ac7bcb9265792059d745999148dfee47d85d5d1ae96265f650ef5f068
7
+ data.tar.gz: 85a15bfdb886959114ccb13eed27adc9b3e83e7a77783be1d5cf65d4c33c40a27501e34d5a5af82350a3c43e08178d159e7732da3b9ad86f4944930751a5e8f9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.1.4]
2
+
3
+ - add NodeModel mixin for a basic ActiveRecord-like syntax
4
+ - add Graph#merge_node and Graph#merge_edge
5
+ - edge and node properties are now a HashWithIndifferentAccess
6
+
1
7
  ## [0.1.3] - 2021-04-13
2
8
 
3
9
  - allow custom queries
data/Gemfile.lock CHANGED
@@ -1,14 +1,24 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redgraph (0.1.3)
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
- !id.nil?
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
@@ -10,8 +10,12 @@ module Redgraph
10
10
 
11
11
  attr_accessor :connection, :graph_name
12
12
 
13
- def initialize(graph, redis_options = {})
14
- @graph_name = graph
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
- # Returns an array of existing graphs
42
+ # @return [Array] Existing graph names
39
43
  #
40
44
  def list
41
45
  @connection.call("GRAPH.LIST")
42
46
  end
43
47
 
44
- # Returns an array of existing labels
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
- # Returns an array of existing properties
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
- # Returns an array of existing relationship types
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
- result = _query("MATCH (src), (dest)
8
- WHERE ID(src) = #{edge.src.id} AND ID(dest) = #{edge.dest.id}
9
- CREATE (src)-[e:`#{edge.type}` #{quote_hash(edge.properties)}]->(dest) RETURN ID(e)")
10
- return false if result.stats[:relationships_created] != 1
11
- id = result.resultset.first["ID(e)"]
12
- edge.id = id
13
- edge
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
- cmd = "MATCH (src)-[edge#{_type} #{_props}]->(dest) #{_where}
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
- result = _query("CREATE (n:`#{node.label}` #{quote_hash(node.properties)}) RETURN ID(n)")
8
- return false if result.stats[:nodes_created] != 1
9
- id = result.resultset.first["ID(n)"]
10
- node.id = id
11
- node
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
- cmd = "MATCH (node#{_label} #{_props}) RETURN node #{_order} #{_skip} #{_limit}"
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
- _label = ":`#{label}`" if label
63
- _props = quote_hash(properties) if properties
68
+ node = Node.new(label: label, properties: properties)
64
69
 
65
- cmd = "MATCH (node#{_label} #{_props}) RETURN COUNT(node)"
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:, properties: {})
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
- !id.nil?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Redgraph
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.4"
5
5
  end
data/redgraph.gemspec CHANGED
@@ -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
@@ -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
@@ -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.3
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-13 00:00:00.000000000 Z
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