redgraph 0.1.4 → 0.2.2

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: 7866e9db857e11a68ddbcf8eef90b839ddc2e6fac110edfc87bfec3dd54edec7
4
- data.tar.gz: 1c911439cd1f3f815db5a42f348e3a67449ef9aae128b4f48f9ee36934d2874d
3
+ metadata.gz: 8b51ea5b1c1c11aab62f08cfe08508130f65def1a3b527ddcaf9419140c4e12a
4
+ data.tar.gz: 6518c572f6a4c14c733653acf1282c9b8473a8737b028a5695f93f953766f0bd
5
5
  SHA512:
6
- metadata.gz: 78a668833d8a16ba61fca88f0b58e4d6ebac5e33b68e6698c03649c5fb66bb13ff33437ac7bcb9265792059d745999148dfee47d85d5d1ae96265f650ef5f068
7
- data.tar.gz: 85a15bfdb886959114ccb13eed27adc9b3e83e7a77783be1d5cf65d4c33c40a27501e34d5a5af82350a3c43e08178d159e7732da3b9ad86f4944930751a5e8f9
6
+ metadata.gz: 634966fdde2e4567654b9a2d3e77eb77b26b8d1c8def562deeef8015d0b798ef8c7a9dd993dd15eabeeac4e2cc32013b748649c3f599dcc524742edd67d0b252
7
+ data.tar.gz: 9dba064e18ded1c51b80eca7d17cf487e1748b31fcbc18be656d57dcbe70954c563f3e7ee7bcd6ed847e4d9e170b40c0a498007980642349f1a4aa1ba5f0c706
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [0.2.2] - 2022-05-15
2
+
3
+ - Nodes allow multiple labels
4
+ - Friendlier exception message when the RedisGraph module is missing
5
+
6
+ ## [0.2.1]
7
+
8
+ - Add NodeModel#destroy method
9
+ - Add NodeModel.create method
10
+
11
+ ## [0.2.0]
12
+
13
+ - revamp the NodeModel mixin, the Node to model mapping is now handled by the `_type` property
14
+
1
15
  ## [0.1.4]
2
16
 
3
17
  - add NodeModel mixin for a basic ActiveRecord-like syntax
data/Gemfile.lock CHANGED
@@ -1,23 +1,22 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redgraph (0.1.4)
4
+ redgraph (0.2.2)
5
5
  activesupport (>= 3.0.0)
6
6
  redis (~> 4)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- activesupport (6.1.3.1)
11
+ activesupport (7.0.3)
12
12
  concurrent-ruby (~> 1.0, >= 1.0.2)
13
13
  i18n (>= 1.6, < 2)
14
14
  minitest (>= 5.1)
15
15
  tzinfo (~> 2.0)
16
- zeitwerk (~> 2.3)
17
16
  coderay (1.1.3)
18
- concurrent-ruby (1.1.8)
17
+ concurrent-ruby (1.1.10)
19
18
  docile (1.3.5)
20
- i18n (1.8.10)
19
+ i18n (1.10.0)
21
20
  concurrent-ruby (~> 1.0)
22
21
  method_source (1.0.0)
23
22
  minitest (5.14.4)
@@ -25,7 +24,7 @@ GEM
25
24
  coderay (~> 1.1)
26
25
  method_source (~> 1.0)
27
26
  rake (13.0.3)
28
- redis (4.2.5)
27
+ redis (4.6.0)
29
28
  simplecov (0.21.2)
30
29
  docile (~> 1.1)
31
30
  simplecov-html (~> 0.11)
@@ -34,9 +33,9 @@ GEM
34
33
  simplecov_json_formatter (0.1.2)
35
34
  tzinfo (2.0.4)
36
35
  concurrent-ruby (~> 1.0)
37
- zeitwerk (2.4.2)
38
36
 
39
37
  PLATFORMS
38
+ arm64-darwin-21
40
39
  x86_64-darwin-20
41
40
 
42
41
  DEPENDENCIES
@@ -47,4 +46,4 @@ DEPENDENCIES
47
46
  simplecov (~> 0.21.2)
48
47
 
49
48
  BUNDLED WITH
50
- 2.2.15
49
+ 2.2.22
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Redgraph
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/redgraph.svg)](https://badge.fury.io/rb/redgraph)
4
+ [![Code Climate](https://codeclimate.com/github/pzac/redgraph.svg)](https://codeclimate.com/github/pzac/redgraph)
5
+
3
6
  A simple RedisGraph library. This gem owes **a lot** to the existing [redisgraph-rb](https://github.com/RedisGraph/redisgraph-rb) gem, but tries to provide a friendlier interface, similar to the existing [Python](https://github.com/RedisGraph/redisgraph-py) and [Elixir](https://github.com/crflynn/redisgraph-ex) clients.
4
7
 
5
8
  ## Installation
@@ -24,85 +27,130 @@ The gem assumes you have a recent version of [RedisGraph](https://oss.redislabs.
24
27
 
25
28
  Basic usage:
26
29
 
27
- graph = Redgraph::Graph.new('movies', url: "redis://localhost:6379/1")
28
- => #<Redgraph::Graph:0x00007f8d5c2b7e38 @connection=#<Redis client v4.2.5 for redis://localhost:6379/1>, @graph_name="movies", @module_version=999999>
30
+ ```ruby
31
+ graph = Redgraph::Graph.new('movies', url: "redis://localhost:6379/1")
32
+ => #<Redgraph::Graph:0x00007f8d5c2b7e38 @connection=#<Redis client v4.2.5 for redis://localhost:6379/1>, @graph_name="movies", @module_version=999999>
33
+ ```
29
34
 
30
35
  Create a couple nodes:
31
36
 
32
- actor = Redgraph::Node.new(label: 'actor', properties: {name: "Al Pacino"})
33
- => #<Redgraph::Node:0x00007f8d5f95cf88 @label="actor", @properties={:name=>"Al Pacino"}>
34
- graph.add_node(actor)
35
- => #<Redgraph::Node:0x00007f8d5f95cf88 @id=0, @label="actor", @properties={:name=>"Al Pacino"}>
36
- film = Redgraph::Node.new(label: 'film', properties: {name: "Scarface"})
37
- => #<Redgraph::Node:0x00007f8d5f85ccc8 @label="film", @properties={:name=>"Scarface"}>
38
- graph.add_node(film)
39
- => #<Redgraph::Node:0x00007f8d5f85ccc8 @id=1, @label="film", @properties={:name=>"Scarface"}>
37
+ ```ruby
38
+ actor = Redgraph::Node.new(label: 'actor', properties: {name: "Al Pacino"})
39
+ => #<Redgraph::Node:0x00007fce3baa0580 @id=nil, @labels=["actor"], @properties={"name"=>"Al Pacino"}>
40
+ graph.add_node(actor)
41
+ => #<Redgraph::Node:0x00007fce3baa0580 @id=0, @labels=["actor"], @properties={"name"=>"Al Pacino"}>
42
+ film = Redgraph::Node.new(label: 'film', properties: {name: "Scarface"})
43
+ => #<Redgraph::Node:0x00007fce3e8c6c48 @id=nil, @labels=["film"], @properties={"name"=>"Scarface"}>
44
+ graph.add_node(film)
45
+ => #<Redgraph::Node:0x00007fce3e8c6c48 @id=1, @labels=["film"], @properties={"name"=>"Scarface"}>
46
+ ```
47
+
48
+ Nodes might have multiple labels, although they're not supported by RedisGraph yet (you can track the feature progress [here](https://github.com/RedisGraph/RedisGraph/pull/1561)):
49
+
50
+ ```ruby
51
+ item = Redgraph::Node.new(labels: ['film', 'drama'], properties: {name: "Casino"})
52
+ => #<Redgraph::Node:0x00007fce3bc73308 @id=nil, @labels=["film", "drama"], @properties={"name"=>"Casino"}>
53
+ ```
40
54
 
41
55
  Create an edge between those nodes:
42
56
 
43
- edge = Redgraph::Edge.new(src: actor, dest: film, type: 'ACTOR_IN', properties: {role: "Tony Montana"})
44
- => #<Redgraph::Edge:0x00007f8d5f9ae3d8 @dest=#<Redgraph::Node:0x00007f8d5f85ccc8 @id=1, @label="film", @properties={:name=>"Scarface"}>, @dest_id=1, @properties={:role=>"Tony Montana"}, @src=#<Redgraph::Node:0x00007f8d5f95cf88 @id=0, @label="actor", @properties={:name=>"Al Pacino"}>, @src_id=0, @type="ACTOR_IN">
45
- @graph.add_edge(edge)
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">
57
+ ```ruby
58
+ edge = Redgraph::Edge.new(src: actor, dest: film, type: 'ACTOR_IN', properties: {role: "Tony Montana"})
59
+ => #<Redgraph::Edge:0x00007f8d5f9ae3d8 @dest=#<Redgraph::Node:0x00007f8d5f85ccc8 @id=1, @label="film", @properties={:name=>"Scarface"}>, @dest_id=1, @properties={:role=>"Tony Montana"}, @src=#<Redgraph::Node:0x00007f8d5f95cf88 @id=0, @label="actor", @properties={:name=>"Al Pacino"}>, @src_id=0, @type="ACTOR_IN">
60
+ graph.add_edge(edge)
61
+ => #<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">
62
+ ```
47
63
 
48
64
  You can merge nodes - the node will be created only if there isn't another with the same label and properties:
49
65
 
50
- graph.merge_node(film)
51
- => #<Redgraph::Node:0x00007f8d5f85ccc8 @id=1, @label="film", @properties={:name=>"Scarface"}>
66
+ ```ruby
67
+ graph.merge_node(film)
68
+ => #<Redgraph::Node:0x00007f8d5f85ccc8 @id=1, @label="film", @properties={:name=>"Scarface"}>
69
+ ```
52
70
 
53
71
  Same with edges:
54
72
 
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">
73
+ ```ruby
74
+ graph.merge_edge(edge)
75
+ => #<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">
76
+ ```
57
77
 
58
78
  Find a node by id:
59
79
 
60
- @graph.find_node_by_id(1)
61
- => #<Redgraph::Node:0x00007f8d5c2c6e88 @id=1, @label="film", @properties={"name"=>"Scarface"}>
80
+ ```ruby
81
+ graph.find_node_by_id(1)
82
+ => #<Redgraph::Node:0x00007f8d5c2c6e88 @id=1, @label="film", @properties={"name"=>"Scarface"}>
83
+ ```
62
84
 
63
85
  To get all nodes:
64
86
 
65
- @graph.nodes
66
- => [#<Redgraph::Node:0x00007f8d5c2ee0a0 @id=0, @label="actor", @properties={"name"=>"Al Pacino"}>, #<Redgraph::Node:0x00007f8d5c2edfd8 @id=1, @label="film", @properties={"name"=>"Scarface"}>]
87
+ ```ruby
88
+ graph.nodes
89
+ => [#<Redgraph::Node:0x00007f8d5c2ee0a0 @id=0, @label="actor", @properties={"name"=>"Al Pacino"}>, #<Redgraph::Node:0x00007f8d5c2edfd8 @id=1, @label="film", @properties={"name"=>"Scarface"}>]
90
+ ```
67
91
 
68
92
  Optional filters that can be combined:
69
93
 
70
- @graph.nodes(label: 'actor')
71
- @graph.nodes(properties: {name: "Al Pacino"})
72
- @graph.nodes(limit: 10, skip: 20)
94
+ ```ruby
95
+ graph.nodes(label: 'actor')
96
+ graph.nodes(properties: {name: "Al Pacino"})
97
+ graph.nodes(limit: 10, skip: 20)
98
+ ```
73
99
 
74
100
  Counting nodes
75
101
 
76
- @graph.count_nodes(label: 'actor')
77
- => 1
102
+ ```ruby
103
+ graph.count_nodes(label: 'actor')
104
+ => 1
105
+ ```
78
106
 
79
107
  Getting edges:
80
108
 
81
- @graph.edges
82
- @graph.edges(src: actor, dest: film)
83
- @graph.edges(kind: 'FRIEND_OF', limit: 10, skip: 20)
84
- @graph.count_edges
109
+ ```ruby
110
+ graph.edges
111
+ graph.edges(src: actor, dest: film)
112
+ graph.edges(kind: 'FRIEND_OF', limit: 10, skip: 20)
113
+ graph.count_edges
114
+ ```
85
115
 
86
116
  Running custom queries
87
117
 
88
- @graph.query("MATCH (src)-[edge:FRIEND_OF]->(dest) RETURN src, edge")
118
+ ```ruby
119
+ graph.query("MATCH (src)-[edge:FRIEND_OF]->(dest) RETURN src, edge")
120
+ ```
89
121
 
90
122
  ### NodeModel
91
123
 
92
124
  You can use the `NodeModel` mixin for a limited ActiveRecord-like interface:
93
125
 
94
- class Actor
95
- include Redgraph::NodeModel
96
- self.graph = Redgraph::Graph.new("movies", url: $REDIS_URL)
97
- attribute :name
98
- end
126
+ ```ruby
127
+ class Actor
128
+ include Redgraph::NodeModel
129
+ self.graph = Redgraph::Graph.new("movies", url: $REDIS_URL)
130
+ attribute :name
131
+ end
132
+ ```
99
133
 
100
134
  And this will give you stuff such as
101
135
 
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"})
136
+ ```ruby
137
+ Actor.count
138
+ john = Actor.new(name: "John Travolta")
139
+ john.add_to_graph
140
+ john.add_relation(type: "ACTED_IN", node: film, properties: {role: "Tony Manero"})
141
+ john.reload
142
+ john.destroy
143
+ Actor.create(name: "Al Pacino")
144
+ ```
145
+
146
+ `NodeModel` models will automatically set a `_type` property to keep track of the object class.
147
+
148
+ You will then be able to run custom queries such as:
149
+
150
+ ```ruby
151
+ Actor.query("MATCH (node) RETURN node ORDER BY node.name")
152
+ ```
153
+ And the result rows object will be instances of the classes defined by the `_type` attribute.
106
154
 
107
155
  ## Development
108
156
 
@@ -118,6 +166,15 @@ You can also run `bin/console` for an interactive prompt that will allow you to
118
166
 
119
167
  To install this gem onto your local machine, run `bundle exec rake install`.
120
168
 
169
+ ### Installing RedisGraph
170
+
171
+ If you're using an Apple silicon mac you might want to use the docker image: I've had issues compiling the module (OpenMP problems). Just do a:
172
+
173
+ docker run -p 6380:6379 -it --rm redislabs/redisgraph
174
+ TEST_REDIS_URL=redis://localhost:6380/0 be rake test
175
+
176
+ I'm using port 6380 to not interphere with the other redis instance.
177
+
121
178
  ## Contributing
122
179
 
123
180
  Bug reports and pull requests are welcome on GitHub at https://github.com/pzac/redgraph.
@@ -68,7 +68,27 @@ module Redgraph
68
68
  node = Node.new(label: label, properties: properties)
69
69
 
70
70
  cmd = "MATCH #{node.to_query_string} RETURN COUNT(node)"
71
- query(cmd).flatten[0]
71
+ # RedisGraph bug: if there are no matches COUNT returns zero rows
72
+ # https://github.com/RedisGraph/RedisGraph/issues/1455
73
+ query(cmd).flatten[0] || 0
74
+ end
75
+
76
+ def update_node(node)
77
+ return false unless node.persisted?
78
+ _set = node.properties.map do |(key, val)|
79
+ "node.#{key} = #{escape_value(val)}"
80
+ end.join(", ")
81
+
82
+ cmd = "MATCH (node) WHERE ID(node) = #{node.id} SET #{_set} RETURN node"
83
+ result = _query(cmd)
84
+ node_from_resultset_item(result.resultset.first["node"])
85
+ end
86
+
87
+ def destroy_node(node)
88
+ return false unless node.persisted?
89
+ cmd = "MATCH (node) WHERE ID(node) = #{node.id} DELETE node"
90
+ result = _query(cmd)
91
+ result.stats["nodes_deleted"] == 1
72
92
  end
73
93
 
74
94
  private
@@ -97,6 +117,7 @@ module Redgraph
97
117
  node
98
118
  end
99
119
 
120
+
100
121
  end
101
122
  end
102
123
  end
@@ -7,6 +7,7 @@ module Redgraph
7
7
  class Graph
8
8
  include NodeMethods
9
9
  include EdgeMethods
10
+ include Util
10
11
 
11
12
  attr_accessor :connection, :graph_name
12
13
 
@@ -18,7 +19,7 @@ module Redgraph
18
19
  @graph_name = graph_name
19
20
  @connection = Redis.new(redis_options)
20
21
  @module_version = module_version
21
- raise ServerError unless @module_version
22
+ raise ServerError, "Can't find RedisGraph module" unless @module_version
22
23
  end
23
24
 
24
25
  # Returns the version of the RedisGraph module
data/lib/redgraph/node.rb CHANGED
@@ -4,14 +4,19 @@ module Redgraph
4
4
  class Node
5
5
  include Util
6
6
 
7
- attr_accessor :id, :label, :properties
7
+ attr_accessor :id, :labels, :properties
8
8
 
9
- def initialize(label: nil, properties: nil, id: nil)
9
+ def initialize(label: nil, properties: nil, id: nil, labels: nil)
10
10
  @id = id
11
- @label = label
11
+ raise(Error, "You can either define a single label or a label array") if label && labels
12
+ @labels = labels || (label ? [label] : [])
12
13
  @properties = (properties || {}).with_indifferent_access
13
14
  end
14
15
 
16
+ def label
17
+ labels.first
18
+ end
19
+
15
20
  def persisted?
16
21
  id.present?
17
22
  end
@@ -21,7 +26,7 @@ module Redgraph
21
26
  end
22
27
 
23
28
  def to_query_string(item_alias: 'node')
24
- _label = ":#{label}" if label
29
+ _label = labels.map {|l| ":`#{l}`"}.join
25
30
  "(#{item_alias}#{_label} #{properties_to_string(properties)})"
26
31
  end
27
32
  end
@@ -0,0 +1,83 @@
1
+ module Redgraph
2
+ module NodeModel
3
+ module ClassMethods
4
+ # Returns an array of nodes. Options:
5
+ #
6
+ # - label: filter by label
7
+ # - properties: filter by properties
8
+ # - order: node.name ASC, node.year DESC
9
+ # - limit: number of items
10
+ # - skip: items offset (useful for pagination)
11
+ #
12
+ def all(label: nil, properties: {}, limit: nil, skip: nil, order: nil)
13
+ graph.nodes(label: label, properties: properties_plus_type(properties),
14
+ limit: limit, skip: skip, order: nil).map do |node|
15
+ reify_from_node(node)
16
+ end
17
+ end
18
+
19
+ # Returns the number of nodes with the current label. Options:
20
+ #
21
+ # - properties: filter by properties
22
+ #
23
+ def count(label: nil, properties: nil)
24
+ graph.count_nodes(label: label, properties: properties_plus_type(properties))
25
+ end
26
+
27
+ # Finds a node by id. Returns nil if not found
28
+ #
29
+ def find(id)
30
+ node = graph.find_node_by_id(id)
31
+ return unless node
32
+ reify_from_node(node)
33
+ end
34
+
35
+ # Sets the label for this class of nodes. If missing it will be computed from the class name
36
+ def label=(x)
37
+ @label = x
38
+ end
39
+
40
+ # Current label
41
+ #
42
+ def label
43
+ @label ||= default_label
44
+ end
45
+
46
+ # Converts a Node object into NodeModel
47
+ #
48
+ def reify_from_node(node)
49
+ klass = node.properties[:_type].to_s.safe_constantize || self
50
+ klass.new(id: node.id, **node.properties)
51
+ end
52
+
53
+ # Runs a query on the graph, but converts the nodes to the corresponding ActiveModel class
54
+ # if available - otherwise they stay NodeObjects.
55
+ #
56
+ # Returns an array of rows.
57
+ #
58
+ def query(cmd)
59
+ raise MissingGraphError unless graph
60
+
61
+ graph.query(cmd).map do |row|
62
+ row.map do |item|
63
+ item.is_a?(Node) ? reify_from_node(item) : item
64
+ end
65
+ end
66
+ end
67
+
68
+ def create(properties)
69
+ new(**properties).add_to_graph
70
+ end
71
+
72
+ private
73
+
74
+ def default_label
75
+ name.demodulize.underscore
76
+ end
77
+
78
+ def properties_plus_type(properties = {})
79
+ {_type: name}.merge(properties || {})
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,24 @@
1
+ module Redgraph
2
+ module NodeModel
3
+ module GraphManipulation
4
+ # Adds a relation between the node and another node.
5
+ #
6
+ # - type: type of relation
7
+ # - node: the destination node
8
+ # - properties: optional properties hash
9
+ # - allow_duplicates: if false it will create a relation between two nodes with the same type
10
+ # and properties only if not present
11
+ #
12
+ def add_relation(type:, node:, properties: nil, allow_duplicates: true)
13
+ edge = Edge.new(type: type, src: to_node, dest: node.to_node, properties: properties)
14
+ allow_duplicates ? graph.add_edge(edge) : graph.merge_edge(edge)
15
+ end
16
+
17
+ # Runs a custom query on the graph
18
+ #
19
+ def query(cmd)
20
+ self.class.query(cmd)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -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
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
+ require_relative 'node_model/class_methods'
3
+ require_relative 'node_model/graph_manipulation'
4
+ require_relative 'node_model/persistence'
2
5
 
3
6
  module Redgraph
4
7
  # This mixin allows you to use an interface similar to ActiveRecord
@@ -16,13 +19,19 @@ module Redgraph
16
19
  # john = Actor.find(123)
17
20
  # total = Actor.count
18
21
  #
22
+ # When you create a record it will automatically set the _type property with the class name.
23
+ # This allows reifying the node into the corresponding NodeModel class.
24
+ #
19
25
  module NodeModel
20
26
  extend ActiveSupport::Concern
21
27
 
22
- included do
28
+ included do |base|
29
+ include Persistence
30
+ include GraphManipulation
31
+
23
32
  @attribute_names = [:id]
24
33
 
25
- attr_accessor :id
34
+ attr_accessor :id, :_type
26
35
 
27
36
  class << self
28
37
  attr_reader :attribute_names
@@ -30,7 +39,7 @@ module Redgraph
30
39
 
31
40
  def attribute(name)
32
41
  @attribute_names << name
33
- attr_reader(name)
42
+ attr_accessor(name)
34
43
  end
35
44
 
36
45
  private
@@ -43,57 +52,8 @@ module Redgraph
43
52
  end
44
53
  end
45
54
 
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
55
  def initialize(**args)
96
- absent_attributes = args.keys.map(&:to_sym) - self.class.attribute_names
56
+ absent_attributes = args.keys.map(&:to_sym) - self.class.attribute_names - [:_type]
97
57
 
98
58
  if absent_attributes.any?
99
59
  raise ArgumentError, "Unknown attribute #{absent_attributes}"
@@ -120,36 +80,15 @@ module Redgraph
120
80
  self.class.attribute_names.to_h { |name| [name, public_send(name)] }
121
81
  end
122
82
 
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)
83
+ def assign_attributes(attrs = {})
84
+ attrs.each do |name, value|
85
+ instance_variable_set("@#{name}", value)
86
+ end
149
87
  end
150
88
 
151
89
  def to_node
152
- Redgraph::Node.new(id: id, label: label, properties: attributes.except(:id))
90
+ props = attributes.except(:id).merge(_type: self.class.name)
91
+ Redgraph::Node.new(id: id, label: label, properties: props)
153
92
  end
154
93
 
155
94
  def ==(other)
@@ -29,9 +29,16 @@ module Redgraph
29
29
  @response = response
30
30
  @graph = graph
31
31
 
32
- @header_row = @response[0]
33
- @result_rows = @response[1]
34
- @query_statistics = @response[2]
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
35
42
  end
36
43
 
37
44
  def stats
@@ -121,6 +128,8 @@ module Redgraph
121
128
  case label
122
129
  when /^Nodes created/
123
130
  stats[:nodes_created] = value.to_i
131
+ when /^Nodes deleted/
132
+ stats[:nodes_deleted] = value.to_i
124
133
  when /^Relationships created/
125
134
  stats[:relationships_created] = value.to_i
126
135
  when /^Properties set/
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Redgraph
4
- VERSION = "0.1.4"
4
+ VERSION = "0.2.2"
5
5
  end
data/lib/redgraph.rb CHANGED
@@ -21,4 +21,9 @@ module Redgraph
21
21
  "The order clause requires the node/edge alias prefix, ie order('node.foo') instead order('foo')"
22
22
  end
23
23
  end
24
+ class MissingGraphError < Error
25
+ def message
26
+ "A graph to use is not defined"
27
+ end
28
+ end
24
29
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class NodeModelClassMethodsTest < 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
+ # test classes
19
+ #
20
+
21
+ class Actor
22
+ include Redgraph::NodeModel
23
+ self.graph = GRAPH
24
+ attribute :name
25
+ end
26
+
27
+ class MissingGraphActor
28
+ include Redgraph::NodeModel
29
+ attribute :name
30
+ end
31
+
32
+ # tests
33
+ #
34
+
35
+ def test_count
36
+ quick_add_node(label: 'actor', properties: {_type: Actor.name, name: "Al Pacino"})
37
+ quick_add_node(label: 'actor', properties: {_type: Actor.name, name: "John Travolta"})
38
+ assert_equal(2, Actor.count)
39
+ assert_equal(1, Actor.count(properties: {name: "Al Pacino"}))
40
+ end
41
+
42
+ def test_all
43
+ al = Actor.new(name: "Al Pacino").add_to_graph
44
+ john = Actor.new(name: "John Travolta").add_to_graph
45
+
46
+ items = Actor.all
47
+ assert_equal(2, items.size)
48
+ assert_includes(items, al)
49
+ assert_includes(items, john)
50
+
51
+ items = Actor.all(properties: {name: "Al Pacino"})
52
+ assert_equal(1, items.size)
53
+ assert_includes(items, al)
54
+ end
55
+
56
+ def test_find
57
+ al = quick_add_node(label: 'actor', properties: {name: "Al Pacino"})
58
+ item = Actor.find(al.id)
59
+
60
+ assert_equal(Actor, item.class)
61
+ assert_predicate(item, :persisted?)
62
+ assert_equal(al.id, item.id)
63
+ assert_equal("Al Pacino", item.name)
64
+ end
65
+
66
+ def test_find_bad_id
67
+ quick_add_node(label: 'actor', properties: {name: "Al Pacino"})
68
+ item = Actor.find("-1")
69
+ assert_nil(item)
70
+ end
71
+
72
+ def test_create
73
+ assert_equal(0, Actor.count)
74
+
75
+ actor = Actor.create(name: "Harrison Ford")
76
+
77
+ assert_equal(1, Actor.count)
78
+ assert_predicate(actor, :persisted?)
79
+ assert_equal("Harrison Ford", actor.name)
80
+ assert_equal("actor", actor.label)
81
+
82
+ found = Actor.find(actor.id)
83
+ assert_equal("Harrison Ford", found.name)
84
+ assert_equal("actor", found.label)
85
+ assert_equal(Actor.name, found._type)
86
+ end
87
+
88
+ def test_create_with_missing_graph
89
+ assert_raises(Redgraph::MissingGraphError) do
90
+ MissingGraphActor.create(name: "Harrison Ford")
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class NodeModelLabelsTest < 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
+ # test classes
19
+ #
20
+
21
+ class Actor
22
+ include Redgraph::NodeModel
23
+ self.graph = GRAPH
24
+ attribute :name
25
+ end
26
+
27
+ class Artist
28
+ include Redgraph::NodeModel
29
+ self.label = "_artist"
30
+ end
31
+
32
+ class Painter < Artist
33
+ end
34
+
35
+ class Pianist < Artist
36
+ self.label = "pianist"
37
+ end
38
+
39
+ # tests
40
+ #
41
+
42
+ def test_label
43
+ assert_equal("actor", Actor.label)
44
+ end
45
+
46
+ def test_custom_label
47
+ assert_equal("_artist", Artist.label)
48
+ end
49
+
50
+ def test_default_label_when_inherited
51
+ assert_equal("painter", Painter.label)
52
+ end
53
+
54
+ def test_custom_label_when_inherited
55
+ assert_equal("pianist", Pianist.label)
56
+ end
57
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class NodeModelPersistenceTest < 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
+ # test classes
19
+ #
20
+
21
+ class Film
22
+ include Redgraph::NodeModel
23
+ self.graph = GRAPH
24
+ attribute :name
25
+ end
26
+
27
+ # tests
28
+ #
29
+
30
+ def test_save_new
31
+ assert_equal(0, Film.count)
32
+
33
+ film = Film.new(name: "Star Wars")
34
+ film.save
35
+
36
+ assert_predicate(film, :persisted?)
37
+ assert_equal(1, Film.count)
38
+ end
39
+
40
+ def test_save_existing
41
+ film = Film.create(name: "Star Wars")
42
+ film.name = "Commando"
43
+ film.save
44
+
45
+ assert_equal(1, Film.count)
46
+ item = Film.find(film.id)
47
+ assert_equal("Commando", item.name)
48
+ end
49
+
50
+ def test_reload
51
+ film = Film.create(name: "Star Wars")
52
+ copy = Film.find(film.id)
53
+
54
+ assert_equal("Star Wars", copy.name)
55
+
56
+ film.name = "Commando"
57
+ film.save
58
+
59
+ assert_equal("Star Wars", copy.name)
60
+ assert_equal("Commando", copy.reload.name)
61
+ end
62
+
63
+ def test_destroy
64
+ film = Film.create(name: "Star Wars")
65
+ assert_equal(1, Film.count)
66
+ film.destroy
67
+ assert_predicate(film, :destroyed?)
68
+ assert_equal(0, Film.count)
69
+ end
70
+ end
@@ -15,6 +15,9 @@ class NodeModelTest < Minitest::Test
15
15
  @graph.delete
16
16
  end
17
17
 
18
+ # test classes
19
+ #
20
+
18
21
  class Animal
19
22
  include Redgraph::NodeModel
20
23
  attribute :name
@@ -24,73 +27,12 @@ class NodeModelTest < Minitest::Test
24
27
  class Dog < Animal
25
28
  end
26
29
 
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
30
  class Actor
39
31
  include Redgraph::NodeModel
40
32
  self.graph = GRAPH
41
33
  attribute :name
42
34
  end
43
35
 
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
36
  class Film
95
37
  include Redgraph::NodeModel
96
38
  self.graph = GRAPH
@@ -98,6 +40,20 @@ class NodeModelTest < Minitest::Test
98
40
  attribute :year
99
41
  end
100
42
 
43
+ # tests
44
+ #
45
+
46
+ def test_graph_accessor
47
+ assert_equal("pippo", Animal.graph)
48
+ assert_equal("pippo", Animal.new.graph)
49
+ end
50
+
51
+ def test_class_inheritance
52
+ assert_equal("pippo", Dog.graph)
53
+ assert_equal("dog", Dog.label)
54
+ assert_equal([:id, :name], Dog.attribute_names)
55
+ end
56
+
101
57
  def test_attribute_names
102
58
  assert_equal([:id, :name, :year], Film.attribute_names)
103
59
 
@@ -157,4 +113,24 @@ class NodeModelTest < Minitest::Test
157
113
  assert_equal(2, @graph.count_edges)
158
114
  end
159
115
 
116
+ def test_casting_query
117
+ Film.new(name: "Star Wars", year: 1977).add_to_graph
118
+ Actor.new(name: "Harrison Ford").add_to_graph
119
+
120
+ items = Film.query("MATCH (node) RETURN node ORDER BY node.name")
121
+ assert_equal(2, items.size)
122
+ assert_kind_of(Actor, items[0][0])
123
+ assert_kind_of(Film, items[1][0])
124
+ end
125
+
126
+ def test_casting_query_from_model
127
+ film = Film.create(name: "Star Wars", year: 1977)
128
+ Actor.create(name: "Harrison Ford")
129
+
130
+ items = film.query("MATCH (node) RETURN node ORDER BY node.name")
131
+ assert_equal(2, items.size)
132
+ assert_kind_of(Actor, items[0][0])
133
+ assert_kind_of(Film, items[1][0])
134
+ end
135
+
160
136
  end
data/test/node_test.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class NodeTest < Minitest::Test
6
+ def test_single_labels
7
+ node = Redgraph::Node.new(label: "film")
8
+ assert_equal(["film"], node.labels)
9
+ assert_equal("film", node.label)
10
+ end
11
+
12
+ def test_multiple_labels
13
+ node = Redgraph::Node.new(labels: ["film", "drama"])
14
+ assert_equal(["film", "drama"], node.labels)
15
+ assert_equal("film", node.label)
16
+ end
17
+
18
+ def test_conflicting_labels
19
+ assert_raises(Redgraph::Error) do
20
+ node = Redgraph::Node.new(labels: ["film", "drama"], label: "film")
21
+ end
22
+ end
23
+ end
data/test/test_helper.rb CHANGED
@@ -16,7 +16,8 @@ require "minitest/autorun"
16
16
  require "pry"
17
17
 
18
18
  unless $REDIS_URL = ENV['TEST_REDIS_URL']
19
- puts "To run the tests you need to define the TEST_REDIS_URL environment variable"
19
+ puts "To run the tests you need to define the TEST_REDIS_URL environment variable. Ex:"
20
+ puts " TEST_REDIS_URL=redis://localhost:6379/0"
20
21
  exit(1)
21
22
  end
22
23
 
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
4
+ version: 0.2.2
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-15 00:00:00.000000000 Z
11
+ date: 2022-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -62,6 +62,9 @@ files:
62
62
  - lib/redgraph/graph/node_methods.rb
63
63
  - lib/redgraph/node.rb
64
64
  - lib/redgraph/node_model.rb
65
+ - lib/redgraph/node_model/class_methods.rb
66
+ - lib/redgraph/node_model/graph_manipulation.rb
67
+ - lib/redgraph/node_model/persistence.rb
65
68
  - lib/redgraph/query_response.rb
66
69
  - lib/redgraph/util.rb
67
70
  - lib/redgraph/version.rb
@@ -72,7 +75,11 @@ files:
72
75
  - test/graph_node_methods_test.rb
73
76
  - test/graph_queries_test.rb
74
77
  - test/graph_test.rb
78
+ - test/node_model_class_methods_test.rb
79
+ - test/node_model_labels_test.rb
80
+ - test/node_model_persistence_test.rb
75
81
  - test/node_model_test.rb
82
+ - test/node_test.rb
76
83
  - test/redgraph_test.rb
77
84
  - test/test_helper.rb
78
85
  homepage: https://github.com/pzac/redgraph
@@ -97,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
104
  - !ruby/object:Gem::Version
98
105
  version: '0'
99
106
  requirements: []
100
- rubygems_version: 3.2.3
107
+ rubygems_version: 3.2.22
101
108
  signing_key:
102
109
  specification_version: 4
103
110
  summary: A simple RedisGraph client