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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0171849182c559f603bf4e09d1cb17eb5a3a4e3d95ddf8e6f1853434c0fc9380'
4
- data.tar.gz: 7bdff0a775ebeffffe6b842b07a990292d351ee477c4aa2ef8ec492806a0b048
3
+ metadata.gz: 2fd53776ff217146cd6ca4924da40a43f5bf929fad03b73b79f32d371ea955e8
4
+ data.tar.gz: 551afc973f8e732ad532e74395e6c73251f14d68db73f667a207b234bd64163b
5
5
  SHA512:
6
- metadata.gz: 53de618a85999bd837e1fabb458c204ac7e0f783425cef8f1d0c07e810c33f580c41d9fd8bf124015970618d6c905c8945020c06a382ae2f692384d0df856482
7
- data.tar.gz: 66866ac946e4b7456684e26587bfcec17eec93de0e4c84bc6827ee77f1f712adce2b895fc38138522c0635fe2ade930a5606e3f638eaec8ab10afc71879e612e
6
+ metadata.gz: dd7f88ae09acb7f09b6c8de949d4f31bbc52b12e4b914f035de99c7cd9b409c14eac3e592f834cc968517b796fed0cf3a085f7eba4915e630b2e66c424ae9870
7
+ data.tar.gz: 979aac39193d3150441ddf5baca14772a35c6cfd00f09df42752277aed3d955869822b2d97150209f1e71ad5ab95e566dc645d15d3e8262675dd858bd6fbb1dd
@@ -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,3 +1,29 @@
1
+ ## [0.2.1]
2
+
3
+ - Add NodeModel#destroy method
4
+ - Add NodeModel.create method
5
+
6
+ ## [0.2.0]
7
+
8
+ - revamp the NodeModel mixin, the Node to model mapping is now handled by the `_type` property
9
+
10
+ ## [0.1.4]
11
+
12
+ - add NodeModel mixin for a basic ActiveRecord-like syntax
13
+ - add Graph#merge_node and Graph#merge_edge
14
+ - edge and node properties are now a HashWithIndifferentAccess
15
+
16
+ ## [0.1.3] - 2021-04-13
17
+
18
+ - allow custom queries
19
+ - nodes and edges query now allow the `order` option
20
+
21
+ ## [0.1.2] - 2021-04-12
22
+
23
+ - Add Graph#relationship_types
24
+ - Add Graph#count_nodes
25
+ - Add Graph#edges
26
+
1
27
  ## [0.1.1] - 2021-04-11
2
28
 
3
29
  - Graph#nodes:
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.1)
4
+ redgraph (0.2.1)
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
@@ -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
@@ -20,34 +23,127 @@ Or install it yourself as:
20
23
 
21
24
  ## Usage
22
25
 
26
+ The gem assumes you have a recent version of [RedisGraph](https://oss.redislabs.com/redisgraph/) up and running.
27
+
23
28
  Basic usage:
24
29
 
25
- graph = Redgraph::Graph.new('movies', url: "redis://localhost:6379/1")
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
+ ```
26
34
 
27
35
  Create a couple nodes:
28
36
 
29
- actor = Redgraph::Node.new(label: 'actor', attributes: {name: "Al Pacino"})
30
- graph.add_node(actor)
31
- film = Redgraph::Node.new(label: 'film', attributes: {name: "Scarface"})
32
- graph.add_node(film)
37
+ ```ruby
38
+ actor = Redgraph::Node.new(label: 'actor', properties: {name: "Al Pacino"})
39
+ => #<Redgraph::Node:0x00007f8d5f95cf88 @label="actor", @properties={:name=>"Al Pacino"}>
40
+ graph.add_node(actor)
41
+ => #<Redgraph::Node:0x00007f8d5f95cf88 @id=0, @label="actor", @properties={:name=>"Al Pacino"}>
42
+ film = Redgraph::Node.new(label: 'film', properties: {name: "Scarface"})
43
+ => #<Redgraph::Node:0x00007f8d5f85ccc8 @label="film", @properties={:name=>"Scarface"}>
44
+ graph.add_node(film)
45
+ => #<Redgraph::Node:0x00007f8d5f85ccc8 @id=1, @label="film", @properties={:name=>"Scarface"}>
46
+ ```
33
47
 
34
48
  Create an edge between those nodes:
35
49
 
36
- edge = Redgraph::Edge.new(src: actor, dest: film, type: 'ACTOR_IN', properties: {role: "Tony Montana"})
37
- result = @graph.add_edge(edge)
50
+ ```ruby
51
+ edge = Redgraph::Edge.new(src: actor, dest: film, type: 'ACTOR_IN', properties: {role: "Tony Montana"})
52
+ => #<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">
53
+ @graph.add_edge(edge)
54
+ => #<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">
55
+ ```
56
+
57
+ You can merge nodes - the node will be created only if there isn't another with the same label and properties:
58
+
59
+ ```ruby
60
+ graph.merge_node(film)
61
+ => #<Redgraph::Node:0x00007f8d5f85ccc8 @id=1, @label="film", @properties={:name=>"Scarface"}>
62
+ ```
63
+
64
+ Same with edges:
65
+
66
+ ```ruby
67
+ @graph.merge_edge(edge)
68
+ => #<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">
69
+ ```
38
70
 
39
71
  Find a node by id:
40
72
 
41
- @graph.find_node_by_id(123)
73
+ ```ruby
74
+ @graph.find_node_by_id(1)
75
+ => #<Redgraph::Node:0x00007f8d5c2c6e88 @id=1, @label="film", @properties={"name"=>"Scarface"}>
76
+ ```
42
77
 
43
78
  To get all nodes:
44
79
 
45
- @graph.nodes
80
+ ```ruby
81
+ @graph.nodes
82
+ => [#<Redgraph::Node:0x00007f8d5c2ee0a0 @id=0, @label="actor", @properties={"name"=>"Al Pacino"}>, #<Redgraph::Node:0x00007f8d5c2edfd8 @id=1, @label="film", @properties={"name"=>"Scarface"}>]
83
+ ```
84
+
85
+ Optional filters that can be combined:
46
86
 
47
- Optional filters:
87
+ ```ruby
88
+ @graph.nodes(label: 'actor')
89
+ @graph.nodes(properties: {name: "Al Pacino"})
90
+ @graph.nodes(limit: 10, skip: 20)
91
+ ```
48
92
 
49
- @graph.nodes(label: 'actor')
50
- @graph.nodes(properties: {name: "Al Pacino"})
93
+ Counting nodes
94
+
95
+ ```ruby
96
+ @graph.count_nodes(label: 'actor')
97
+ => 1
98
+ ```
99
+
100
+ Getting edges:
101
+
102
+ ```ruby
103
+ @graph.edges
104
+ @graph.edges(src: actor, dest: film)
105
+ @graph.edges(kind: 'FRIEND_OF', limit: 10, skip: 20)
106
+ @graph.count_edges
107
+ ```
108
+
109
+ Running custom queries
110
+
111
+ ```ruby
112
+ @graph.query("MATCH (src)-[edge:FRIEND_OF]->(dest) RETURN src, edge")
113
+ ```
114
+
115
+ ### NodeModel
116
+
117
+ You can use the `NodeModel` mixin for a limited ActiveRecord-like interface:
118
+
119
+ ```ruby
120
+ class Actor
121
+ include Redgraph::NodeModel
122
+ self.graph = Redgraph::Graph.new("movies", url: $REDIS_URL)
123
+ attribute :name
124
+ end
125
+ ```
126
+
127
+ And this will give you stuff such as
128
+
129
+ ```ruby
130
+ Actor.count
131
+ john = Actor.new(name: "John Travolta")
132
+ john.add_to_graph # Will add the node to the graph
133
+ john.add_relation(type: "ACTED_IN", node: film, properties: {role: "Tony Manero"})
134
+ john.reload
135
+ john.destroy
136
+ Actor.create(name: "Al Pacino")
137
+ ```
138
+
139
+ `NodeModel` models will automatically set a `_type` property to keep track of the object class.
140
+
141
+ You will then be able to run custom queries such as:
142
+
143
+ ```ruby
144
+ Actor.query("MATCH (node) RETURN node ORDER BY node.name")
145
+ ```
146
+ And the result rows object will be instances of the classes defined by the `_type` attribute.
51
147
 
52
148
  ## Development
53
149
 
@@ -55,7 +151,7 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
55
151
 
56
152
  TEST_REDIS_URL=YOUR-REDIS-URL rake test
57
153
 
58
- to run the tests.
154
+ to run the tests. Test coverage will be enabled if you set the `COVERAGE` environment variable to any value.
59
155
 
60
156
  You can use a `TEST_REDIS_URL` such as `redis://localhost:6379/1`. Make sure you're not overwriting important databases.
61
157
 
@@ -63,8 +159,6 @@ You can also run `bin/console` for an interactive prompt that will allow you to
63
159
 
64
160
  To install this gem onto your local machine, run `bundle exec rake install`.
65
161
 
66
- Run `bin/console` for an interactive prompt.
67
-
68
162
  ## Contributing
69
163
 
70
164
  Bug reports and pull requests are welcome on GitHub at https://github.com/pzac/redgraph.
data/lib/redgraph.rb CHANGED
@@ -1,13 +1,29 @@
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
24
+ class MissingGraphError < Error
25
+ def message
26
+ "A graph to use is not defined"
27
+ end
28
+ end
13
29
  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,22 @@
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
+ include Util
11
+
5
12
  attr_accessor :connection, :graph_name
6
13
 
7
- def initialize(graph, redis_options = {})
8
- @graph_name = graph
14
+ # @example Graph.new("foobar", url: "redis://localhost:6379/0", logger: Logger.new(STDOUT))
15
+ # @param graph_name [String] Name of the graph
16
+ # @param redis_options [Hash] Redis client options
17
+ #
18
+ def initialize(graph_name, redis_options = {})
19
+ @graph_name = graph_name
9
20
  @connection = Redis.new(redis_options)
10
21
  @module_version = module_version
11
22
  raise ServerError unless @module_version
@@ -29,120 +40,67 @@ module Redgraph
29
40
  raise e
30
41
  end
31
42
 
32
- # Returns an array of existing graphs
43
+ # @return [Array] Existing graph names
33
44
  #
34
45
  def list
35
46
  @connection.call("GRAPH.LIST")
36
47
  end
37
48
 
38
- # Returns an array of existing labels
49
+ # @return [Array] Existing labels
39
50
  #
40
51
  def labels
41
- result = query("CALL db.labels()")
52
+ result = _query("CALL db.labels()")
42
53
  result.resultset.map(&:values).flatten
43
54
  end
44
55
 
45
- # Returns an array of existing properties
56
+ # @return [Array] Existing properties
46
57
  #
47
58
  def properties
48
- result = query("CALL db.propertyKeys()")
59
+ result = _query("CALL db.propertyKeys()")
49
60
  result.resultset.map(&:values).flatten
50
61
  end
51
62
 
52
- # Adds a node. If successul it returns the created object, otherwise false
63
+ # @return [Array] Existing relationship types
53
64
  #
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
65
+ def relationship_types
66
+ result = _query("CALL db.relationshipTypes()")
67
+ result.resultset.map(&:values).flatten
60
68
  end
61
69
 
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
70
+ # You can run custom cypher queries
71
+ def query(cmd)
72
+ _query(cmd).rows
74
73
  end
75
74
 
76
- # Returns nodes. Options:
75
+ # @param id [Integer] label id
76
+ # @return [String, nil] label
77
77
  #
78
- # - label: filter by label
79
- # - properties: filter by properties
80
- # - limit: number of items
81
- # - skip: items offset (useful for pagination)
78
+ def get_label(id)
79
+ @labels ||= labels
80
+ @labels[id] || (@labels = labels)[id]
81
+ end
82
+
83
+ # @param id [Integer] property id
84
+ # @return [String, nil] property
82
85
  #
83
- def nodes(label: nil, properties: nil, limit: nil, skip: nil)
84
- _label = ":`#{label}`" if label
85
- _props = quote_hash(properties) if properties
86
- _limit = "LIMIT #{limit}" if limit
87
- _skip = "SKIP #{skip}" if skip
88
-
89
- cmd = "MATCH (node#{_label} #{_props}) RETURN node #{_skip} #{_limit}"
90
- result = query(cmd)
91
-
92
- result.resultset.map do |item|
93
- (node_id, labels, props) = item["node"]
94
- attrs = {}
95
-
96
- props.each do |(index, type, value)|
97
- attrs[get_property(index)] = value
98
- end
99
- Node.new(label: get_label(labels.first), properties: attrs).tap do |node|
100
- node.id = node_id
101
- end
102
- end
86
+ def get_property(id)
87
+ @properties ||= properties
88
+ @properties[id] || (@properties = properties)[id]
103
89
  end
104
90
 
105
- # Adds an edge. If successul it returns the created object, otherwise false
91
+ # @param id [Integer] relationship type id
92
+ # @return [String, nil] relationship type
106
93
  #
107
- def add_edge(edge)
108
- result = query("MATCH (src), (dest)
109
- WHERE ID(src) = #{edge.src.id} AND ID(dest) = #{edge.dest.id}
110
- CREATE (src)-[e:`#{edge.type}` #{quote_hash(edge.properties)}]->(dest) RETURN ID(e)")
111
- return false if result.stats[:relationships_created] != 1
112
- id = result.resultset.first["ID(e)"]
113
- edge.id = id
114
- edge
94
+ def get_relationship_type(id)
95
+ @relationship_types ||= relationship_types
96
+ @relationship_types[id] || (@relationship_types = relationship_types)[id]
115
97
  end
116
98
 
117
99
  private
118
100
 
119
- def query(cmd)
101
+ def _query(cmd)
120
102
  data = @connection.call("GRAPH.QUERY", graph_name, cmd, "--compact")
121
- QueryResponse.new(data)
122
- end
123
-
124
- def quote_hash(hash)
125
- "{" +
126
- hash.map {|k,v| "#{k}:#{escape_value(v)}" }.join(", ") +
127
- "}"
128
- end
129
-
130
- def escape_value(x)
131
- case x
132
- when Integer then x
133
- else
134
- "'#{x}'"
135
- end
136
- end
137
-
138
- def get_label(id)
139
- @labels ||= labels
140
- @labels[id] || (@labels = labels)[id]
141
- end
142
-
143
- def get_property(id)
144
- @properties ||= properties
145
- @properties[id] || (@properties = properties)[id]
103
+ QueryResponse.new(data, self)
146
104
  end
147
105
  end
148
106
  end