redgraph 0.1.0 → 0.2.0

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: 80c7cce1d1d19897882873e07e7a5a1fadabbb52d14717e16ff113f83511b32a
4
- data.tar.gz: 72be15f7b95a7b45ec9ec21adb54f54676e0f960938c602fbb8381ae3ca1c682
3
+ metadata.gz: a5a64b5cffa36368ac0664e813ab61f0329a055237b7e4acd473820d9c73409d
4
+ data.tar.gz: 371cde906c0a80370e39763a9b3ba9f3fcb83cdd2c19e97f635bfea6e35c5540
5
5
  SHA512:
6
- metadata.gz: cce480b50ada0fb75bb1491d730668a8ac7f92be766dd161b1b3c3fa8082d579d7276383adb2966894388f6c95fb7e444abd9eb2935e50f6659d35be938df450
7
- data.tar.gz: 806e2105e78b9c6a188dd271ed7b6315ec9afbe0bbd6d1008eb2c4f991e756512e7b4a83dbb7808b944ab2c8b66671f270cb1a4008a476945c9f20818209dad9
6
+ metadata.gz: ac0481573ed2d41bb20de03ab1dc36d6781f5a121a865358e296654dc09abe168868104df92deeb623594b6d68363a0c9333268a059c7b8caf362504039e1431
7
+ data.tar.gz: dcb4c293c0a1003926d8776930123c81a5d35981a0896d447a03cc8ee1082f837ce1d73a1f7ca664478980fb485eefb39dad120f703b4584d40ffd6a3ff509c2
@@ -5,6 +5,12 @@ on: [push,pull_request]
5
5
  jobs:
6
6
  build:
7
7
  runs-on: ubuntu-latest
8
+
9
+ services:
10
+ redis:
11
+ image: redislabs/redisgraph
12
+ ports: ["6379:6379"]
13
+
8
14
  steps:
9
15
  - uses: actions/checkout@v2
10
16
  - name: Set up Ruby
@@ -12,6 +18,8 @@ jobs:
12
18
  with:
13
19
  ruby-version: 3.0.0
14
20
  - name: Run the default task
21
+ env:
22
+ TEST_REDIS_URL: redis://localhost:6379/0
15
23
  run: |
16
24
  gem install bundler -v 2.2.15
17
25
  bundle install
data/CHANGELOG.md CHANGED
@@ -1,4 +1,29 @@
1
- ## [Unreleased]
1
+ ## [0.2.0]
2
+
3
+ - revamp the NodeModel mixin, the Node to model mapping is now handled by the `_type` property
4
+
5
+ ## [0.1.4]
6
+
7
+ - add NodeModel mixin for a basic ActiveRecord-like syntax
8
+ - add Graph#merge_node and Graph#merge_edge
9
+ - edge and node properties are now a HashWithIndifferentAccess
10
+
11
+ ## [0.1.3] - 2021-04-13
12
+
13
+ - allow custom queries
14
+ - nodes and edges query now allow the `order` option
15
+
16
+ ## [0.1.2] - 2021-04-12
17
+
18
+ - Add Graph#relationship_types
19
+ - Add Graph#count_nodes
20
+ - Add Graph#edges
21
+
22
+ ## [0.1.1] - 2021-04-11
23
+
24
+ - Graph#nodes:
25
+ - filter by properties
26
+ - skip and limit options
2
27
 
3
28
  ## [0.1.0] - 2021-04-08
4
29
 
data/Gemfile CHANGED
@@ -9,3 +9,5 @@ gem "rake", "~> 13.0"
9
9
 
10
10
  gem "minitest", "~> 5.0"
11
11
  gem "pry", "~> 0.14.0"
12
+ gem "simplecov", "~> 0.21.2"
13
+
data/Gemfile.lock CHANGED
@@ -1,13 +1,24 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redgraph (0.1.0)
4
+ redgraph (0.2.0)
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)
19
+ docile (1.3.5)
20
+ i18n (1.8.10)
21
+ concurrent-ruby (~> 1.0)
11
22
  method_source (1.0.0)
12
23
  minitest (5.14.4)
13
24
  pry (0.14.0)
@@ -15,6 +26,15 @@ GEM
15
26
  method_source (~> 1.0)
16
27
  rake (13.0.3)
17
28
  redis (4.2.5)
29
+ simplecov (0.21.2)
30
+ docile (~> 1.1)
31
+ simplecov-html (~> 0.11)
32
+ simplecov_json_formatter (~> 0.1)
33
+ simplecov-html (0.12.3)
34
+ simplecov_json_formatter (0.1.2)
35
+ tzinfo (2.0.4)
36
+ concurrent-ruby (~> 1.0)
37
+ zeitwerk (2.4.2)
18
38
 
19
39
  PLATFORMS
20
40
  x86_64-darwin-20
@@ -24,6 +44,7 @@ DEPENDENCIES
24
44
  pry (~> 0.14.0)
25
45
  rake (~> 13.0)
26
46
  redgraph!
47
+ simplecov (~> 0.21.2)
27
48
 
28
49
  BUNDLED WITH
29
50
  2.2.15
data/README.md CHANGED
@@ -20,33 +20,97 @@ Or install it yourself as:
20
20
 
21
21
  ## Usage
22
22
 
23
+ The gem assumes you have a recent version of [RedisGraph](https://oss.redislabs.com/redisgraph/) up and running.
24
+
23
25
  Basic usage:
24
26
 
25
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>
26
29
 
27
30
  Create a couple nodes:
28
31
 
29
- actor = Redgraph::Node.new(label: 'actor', attributes: {name: "Al Pacino"})
32
+ actor = Redgraph::Node.new(label: 'actor', properties: {name: "Al Pacino"})
33
+ => #<Redgraph::Node:0x00007f8d5f95cf88 @label="actor", @properties={:name=>"Al Pacino"}>
30
34
  graph.add_node(actor)
31
- film = Redgraph::Node.new(label: 'film', attributes: {name: "Scarface"})
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"}>
32
38
  graph.add_node(film)
39
+ => #<Redgraph::Node:0x00007f8d5f85ccc8 @id=1, @label="film", @properties={:name=>"Scarface"}>
33
40
 
34
41
  Create an edge between those nodes:
35
42
 
36
43
  edge = Redgraph::Edge.new(src: actor, dest: film, type: 'ACTOR_IN', properties: {role: "Tony Montana"})
37
- result = @graph.add_edge(edge)
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">
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">
38
57
 
39
58
  Find a node by id:
40
59
 
41
- @graph.find_node_by_id(123)
60
+ @graph.find_node_by_id(1)
61
+ => #<Redgraph::Node:0x00007f8d5c2c6e88 @id=1, @label="film", @properties={"name"=>"Scarface"}>
42
62
 
43
63
  To get all nodes:
44
64
 
45
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"}>]
46
67
 
47
- Optional filters:
68
+ Optional filters that can be combined:
48
69
 
49
70
  @graph.nodes(label: 'actor')
71
+ @graph.nodes(properties: {name: "Al Pacino"})
72
+ @graph.nodes(limit: 10, skip: 20)
73
+
74
+ Counting nodes
75
+
76
+ @graph.count_nodes(label: 'actor')
77
+ => 1
78
+
79
+ Getting edges:
80
+
81
+ @graph.edges
82
+ @graph.edges(src: actor, dest: film)
83
+ @graph.edges(kind: 'FRIEND_OF', limit: 10, skip: 20)
84
+ @graph.count_edges
85
+
86
+ Running custom queries
87
+
88
+ @graph.query("MATCH (src)-[edge:FRIEND_OF]->(dest) RETURN src, edge")
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"})
106
+
107
+ `NodeModel` models will automatically set a `_type` property to keep track of the object class.
108
+
109
+ You will then be able to run custom queries such as:
110
+
111
+ Actor.query("MATCH (node) RETURN node ORDER BY node.name")
112
+
113
+ And the result rows object will be instances of the classes defined by the `_type` attribute.
50
114
 
51
115
  ## Development
52
116
 
@@ -54,7 +118,7 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
54
118
 
55
119
  TEST_REDIS_URL=YOUR-REDIS-URL rake test
56
120
 
57
- to run the tests.
121
+ to run the tests. Test coverage will be enabled if you set the `COVERAGE` environment variable to any value.
58
122
 
59
123
  You can use a `TEST_REDIS_URL` such as `redis://localhost:6379/1`. Make sure you're not overwriting important databases.
60
124
 
@@ -62,8 +126,6 @@ You can also run `bin/console` for an interactive prompt that will allow you to
62
126
 
63
127
  To install this gem onto your local machine, run `bundle exec rake install`.
64
128
 
65
- Run `bin/console` for an interactive prompt.
66
-
67
129
  ## Contributing
68
130
 
69
131
  Bug reports and pull requests are welcome on GitHub at https://github.com/pzac/redgraph.
data/lib/redgraph.rb CHANGED
@@ -1,13 +1,24 @@
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
12
18
  class ServerError < Error; end
19
+ class MissingAliasPrefixError < Error
20
+ def message
21
+ "The order clause requires the node/edge alias prefix, ie order('node.foo') instead order('foo')"
22
+ end
23
+ end
13
24
  end
data/lib/redgraph/edge.rb CHANGED
@@ -2,17 +2,30 @@
2
2
 
3
3
  module Redgraph
4
4
  class Edge
5
- attr_accessor :id, :src, :dest, :type, :properties
5
+ include Util
6
6
 
7
- def initialize(src:, dest:, type:, properties: {})
7
+ attr_accessor :id, :src, :dest, :src_id, :dest_id, :type, :properties
8
+
9
+ def initialize(src: nil, dest: nil, type: nil, properties: {})
8
10
  @src = src
11
+ @src_id = @src.id if @src
9
12
  @dest = dest
13
+ @dest_id = @dest.id if @dest
10
14
  @type = type
11
- @properties = properties
15
+ @properties = (properties || {}).with_indifferent_access
12
16
  end
13
17
 
14
18
  def persisted?
15
- !id.nil?
19
+ id.present?
20
+ end
21
+
22
+ def ==(other)
23
+ super || other.instance_of?(self.class) && !id.nil? && other.id == id
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})"
16
29
  end
17
30
  end
18
31
  end
@@ -1,11 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "graph/node_methods"
4
+ require_relative "graph/edge_methods"
5
+
3
6
  module Redgraph
4
7
  class Graph
8
+ include NodeMethods
9
+ include EdgeMethods
10
+
5
11
  attr_accessor :connection, :graph_name
6
12
 
7
- def initialize(graph, redis_options = {})
8
- @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
9
19
  @connection = Redis.new(redis_options)
10
20
  @module_version = module_version
11
21
  raise ServerError unless @module_version
@@ -29,112 +39,67 @@ module Redgraph
29
39
  raise e
30
40
  end
31
41
 
32
- # Returns an array of existing graphs
42
+ # @return [Array] Existing graph names
33
43
  #
34
44
  def list
35
45
  @connection.call("GRAPH.LIST")
36
46
  end
37
47
 
38
- # Returns an array of existing labels
48
+ # @return [Array] Existing labels
39
49
  #
40
50
  def labels
41
- result = query("CALL db.labels()")
51
+ result = _query("CALL db.labels()")
42
52
  result.resultset.map(&:values).flatten
43
53
  end
44
54
 
45
- # Returns an array of existing properties
55
+ # @return [Array] Existing properties
46
56
  #
47
57
  def properties
48
- result = query("CALL db.propertyKeys()")
58
+ result = _query("CALL db.propertyKeys()")
49
59
  result.resultset.map(&:values).flatten
50
60
  end
51
61
 
52
- # Adds a node. If successul it returns the created object, otherwise false
53
- #
54
- def add_node(node)
55
- result = query("CREATE (n:`#{node.label}` #{quote_hash(node.properties)}) RETURN ID(n)")
56
- return false if result.stats[:nodes_created] != 1
57
- id = result.resultset.first["ID(n)"]
58
- node.id = id
59
- node
60
- end
61
-
62
- def find_node_by_id(id)
63
- result = query("MATCH (node) WHERE ID(node) = #{id} RETURN node")
64
- return nil if result.resultset.empty?
65
- (node_id, labels, properties) = result.resultset.first["node"]
66
- attrs = {}
67
-
68
- properties.each do |(index, type, value)|
69
- attrs[get_property(index)] = value
70
- end
71
- Node.new(label: get_label(labels.first), properties: attrs).tap do |node|
72
- node.id = node_id
73
- end
74
- end
75
-
76
- def nodes(label: nil)
77
- node_or_node_with_label = label ? "node:`#{label}`" : "node"
78
-
79
- cmd = "MATCH (#{node_or_node_with_label}) RETURN node"
80
- result = query(cmd)
81
-
82
- result.resultset.map do |item|
83
- (node_id, labels, properties) = item["node"]
84
- attrs = {}
85
-
86
- properties.each do |(index, type, value)|
87
- attrs[get_property(index)] = value
88
- end
89
- Node.new(label: get_label(labels.first), properties: attrs).tap do |node|
90
- node.id = node_id
91
- end
92
- end
93
- end
94
-
95
- # Adds an edge. If successul it returns the created object, otherwise false
62
+ # @return [Array] Existing relationship types
96
63
  #
97
- def add_edge(edge)
98
- result = query("MATCH (src), (dest)
99
- WHERE ID(src) = #{edge.src.id} AND ID(dest) = #{edge.dest.id}
100
- CREATE (src)-[e:`#{edge.type}` #{quote_hash(edge.properties)}]->(dest) RETURN ID(e)")
101
- return false if result.stats[:relationships_created] != 1
102
- id = result.resultset.first["ID(e)"]
103
- edge.id = id
104
- edge
64
+ def relationship_types
65
+ result = _query("CALL db.relationshipTypes()")
66
+ result.resultset.map(&:values).flatten
105
67
  end
106
68
 
107
- private
108
-
69
+ # You can run custom cypher queries
109
70
  def query(cmd)
110
- data = @connection.call("GRAPH.QUERY", graph_name, cmd, "--compact")
111
- QueryResponse.new(data)
112
- end
113
-
114
- def quote_hash(hash)
115
- out = "{"
116
- hash.each do |k,v|
117
- out += "#{k}:#{escape_value(v)}"
118
- end
119
- out + "}"
120
- end
121
-
122
- def escape_value(x)
123
- case x
124
- when Integer then x
125
- else
126
- "'#{x}'"
127
- end
71
+ _query(cmd).rows
128
72
  end
129
73
 
74
+ # @param id [Integer] label id
75
+ # @return [String, nil] label
76
+ #
130
77
  def get_label(id)
131
78
  @labels ||= labels
132
79
  @labels[id] || (@labels = labels)[id]
133
80
  end
134
81
 
82
+ # @param id [Integer] property id
83
+ # @return [String, nil] property
84
+ #
135
85
  def get_property(id)
136
86
  @properties ||= properties
137
87
  @properties[id] || (@properties = properties)[id]
138
88
  end
89
+
90
+ # @param id [Integer] relationship type id
91
+ # @return [String, nil] relationship type
92
+ #
93
+ def get_relationship_type(id)
94
+ @relationship_types ||= relationship_types
95
+ @relationship_types[id] || (@relationship_types = relationship_types)[id]
96
+ end
97
+
98
+ private
99
+
100
+ def _query(cmd)
101
+ data = @connection.call("GRAPH.QUERY", graph_name, cmd, "--compact")
102
+ QueryResponse.new(data, self)
103
+ end
139
104
  end
140
105
  end