oqgraph_rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,35 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ oqgraph_rails (0.0.1)
5
+ activerecord
6
+ minitest
7
+ mysql2
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ activemodel (3.2.6)
13
+ activesupport (= 3.2.6)
14
+ builder (~> 3.0.0)
15
+ activerecord (3.2.6)
16
+ activemodel (= 3.2.6)
17
+ activesupport (= 3.2.6)
18
+ arel (~> 3.0.2)
19
+ tzinfo (~> 0.3.29)
20
+ activesupport (3.2.6)
21
+ i18n (~> 0.6)
22
+ multi_json (~> 1.0)
23
+ arel (3.0.2)
24
+ builder (3.0.0)
25
+ i18n (0.6.0)
26
+ minitest (3.1.0)
27
+ multi_json (1.3.6)
28
+ mysql2 (0.3.11)
29
+ tzinfo (0.3.33)
30
+
31
+ PLATFORMS
32
+ ruby
33
+
34
+ DEPENDENCIES
35
+ oqgraph_rails!
data/LICENCE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Stuart Coyle
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # OQGraph Rails
2
+
3
+ This gem can be used with ActiveRecord to access the features of the OQGraph
4
+ MySQL plugin.
5
+
6
+ Please take a look at the Open Query Graph engine page at http://openquery.com/products/graph-engine
7
+
8
+ This provides fast graph operations on the database. It does a similar thing to the
9
+ acts_as_graph gem, which does all the graph work on the application server. There are
10
+ pros and cons of both approaches, It really depends on your needs. Both libraries
11
+ use the C++ Boost graph library at the bottom layers. OQGraph has expensive
12
+ insert operations but is very fast at delivering the graph data and finding paths in a single SQL query.
13
+
14
+ This gem requires the use of MySQL or MariaDB with the OQGraph engine plugin.
15
+ For details of this see: http://openquery.com/products/graph-engine
16
+
17
+ ## Concepts
18
+
19
+ The term graph we are using here is a mathematical one not a pretty picture (sorry designers).
20
+ For more see: http://en.wikipedia.org/wiki/Graph_(mathematics)
21
+ A graph consists of nodes (aka vertices) connected by edges (aka links, paths).
22
+ In a directed graph an edge has a direction. That is, it has an 'from' node and and a 'to' node.
23
+ When tracing a path on the graph you can only go in the direction of the edge.
24
+ The OQGraph gem operates on directed graphs. To create a non directed graph you have to create two edges,
25
+ one each way between each node.
26
+
27
+ Edges can be assigned positive floating point values called weights. Weights default to 1.0
28
+ The weights are used in shortest path calculations, a path is shorter if the sum of weights over each edge is smaller.
29
+
30
+ ## What you can do with OQGraph?
31
+
32
+ Imagine your shiny new social networking app, FarceBook.
33
+ You have lots and lots of users each with several friends. How do you find the friends of friends?
34
+ Or even the friends of friends of friends...right up to the six degrees of separation perhaps?
35
+
36
+ Well you can do it, with some really slow and nasty SQL queries. Relational databases are good at set
37
+ based queries but no good at graph or tree based queries. The OQGraph engine is good at graph based queries,
38
+ it enables you in one simple SQL query to find all friends of friends.
39
+
40
+ Do this:
41
+
42
+ ```
43
+ user.reachable
44
+ ```
45
+
46
+ If you really want to you can rename the reachable method so you can do this in your User model:
47
+
48
+ ```
49
+ alias :friends, :reachable
50
+ ```
51
+
52
+ Then I can call:
53
+
54
+ ```
55
+ user.friends
56
+ ```
57
+
58
+ and I get the whole tree of friends of friends etc...
59
+
60
+ Imagine you have a maze to solve. With OQGraph the solution is as simple as:
61
+
62
+ ```
63
+ start_cell.shortest_path_to(finish_cell)
64
+ ```
65
+
66
+ It's good for representing tree structures, networks, routes between cities etc.
67
+
68
+ ## Usage
69
+
70
+ Use the generators to create your skeleton node and edge classes.
71
+
72
+ ```
73
+ rails g oqgraph
74
+ ```
75
+
76
+ This will create a node and an edge model as well as a migration to
77
+ create the edge model's table.
78
+
79
+ A node model should look like this:
80
+
81
+ ```
82
+ class Foo < ActiveRecord::Base
83
+ include OQGraph::Node
84
+ end
85
+ ```
86
+
87
+ An edge model should look like this:
88
+
89
+ ```
90
+ class FooEdge < ActiveRecord::Base
91
+ include OQGraph::Edge
92
+ end
93
+ ```
94
+
95
+ The edge model schema should be like this:
96
+
97
+ ```
98
+ create_table :foo_edges do |t|
99
+ t.integer :from_id
100
+ t.integer :to_id
101
+ t.float :weight, :limit => 25
102
+ end
103
+ ```
104
+
105
+ The associations defined on the edge and node model are:
106
+
107
+ ```
108
+ node_model.outgoing_edges
109
+ node_model.incoming_edges
110
+ node_model.outgoing_nodes
111
+ node_model.incoming_nodes
112
+
113
+ edge_model.to
114
+ edge_model.from
115
+ ```
116
+
117
+ You should be able also to extend the edge and node models as you wish.
118
+ The gem will automatically create the OQgraph table and the associations to it from your node model.
119
+
120
+ ## Examples of Use
121
+
122
+ ### Creating edges:
123
+
124
+ ```
125
+ foo.create_edge_to(bar)
126
+ foo.create_edge_to_and_from(bar)
127
+ ```
128
+
129
+ Edge creation using ActiveRecord associations:
130
+
131
+ ```
132
+ foo.outgoing_nodes << bar
133
+ ```
134
+
135
+ or equivalently:
136
+ ```
137
+ bar.incoming_nodes << foo
138
+ ```
139
+
140
+ At the moment you cannot add weights to edges with this style of notation.
141
+
142
+ Create a edge with weight:
143
+
144
+ ```
145
+ bar.create_edge_to(baz, 2.0)
146
+ ```
147
+
148
+ Removing a edge:
149
+
150
+ ```
151
+ foo.remove_edge_to(bar)
152
+ foo.remove_edge_from(bar)
153
+ ```
154
+
155
+ Note that these calls remove ALL edges to bar from foo
156
+
157
+ ### Examining edges:
158
+
159
+ What nodes point to this one?
160
+ Gives us an array of nodes that are connected to this one in the inward (from) direction.
161
+ Includes the node calling the method.
162
+
163
+ ```
164
+ foo.originating
165
+ bar.originating?(baz)
166
+ ```
167
+
168
+ What nodes can I reach from this one?
169
+ Gives us an array of nodes that are connected to this one in the outward (to) direction.
170
+ Includes the node calling the method.
171
+
172
+ ```
173
+ bar.reachable
174
+ foo.reachable?(baz)
175
+ ```
176
+
177
+ ### Path Finding:
178
+
179
+ ```
180
+ foo.shortest_path_to(baz)
181
+ -> [foo, bar,baz]
182
+ ```
183
+
184
+ The shortest path to can take a :method which can either be :dijikstra (the default)
185
+ or :breadth_first. The breadth first method does not take weights into account.
186
+ It is faster in some cases.
187
+
188
+ ```
189
+ foo.shortest_path_to(baz, :method => :breadth_first)
190
+ ```
191
+
192
+ All these methods return the node object with an additional weight field.
193
+ This enables you to query the weights associated with the edges found.
194
+
195
+ ## Behind the Scenes
196
+
197
+ The OQGraph table will also get created if it does not exist. The OQGraph table is volatile, it holds data in
198
+ memory only. The table structure is not volatile and gets stored in the db.
199
+ When your application starts up it will put all the edges into the graph table and update them as
200
+ they are created, deleted or modified. This could slow things down at startup but caching classes in production
201
+ means it does not happen on every request. The graph table is only rewritten now when the DB has been restarted.
202
+ You can use this code to force the graph to be rebuilt:
203
+ NodeModel.rebuild_graph
204
+
205
+ ### How fast is it?
206
+ I've tested with an application with 10000 nodes and 0 to 9 links from each.
207
+
208
+ For a node connected to most of the network finding all reachable nodes averages at about 300ms.
209
+ This is strongly dependent on how well connected the graph is.
210
+
211
+ To find shortest paths between nodes takes about 5 to 10ms per request.
212
+ To find all connected nodes takes about 30 to 50ms. The slow bit is the instatiation of
213
+ the models.
214
+
215
+ Here's an example request:
216
+
217
+ ```
218
+ Processing OqgraphUsersController#show_path (for 127.0.0.1 at 2010-07-21 17:09:59) [GET]
219
+ Parameters: {"id"=>"223", "other_id"=>"2333"}
220
+ OqgraphUser Load (0.3ms) SELECT * FROM `oqgraph_users` WHERE (`oqgraph_users`.`id` = 223)
221
+ OqgraphUser Load (0.1ms) SELECT * FROM `oqgraph_users` WHERE (`oqgraph_users`.`id` = 2333)
222
+ OqgraphUser Load (2.2ms) SELECT oqgraph_users.id,oqgraph_users.first_name,oqgraph_users.last_name, oqgraph_user_oqgraph.weight FROM oqgraph_user_oqgraph JOIN oqgraph_users ON (linkid=id) WHERE latch = 1 AND origid = 223 AND destid = 2333
223
+ ORDER BY seq;
224
+
225
+ Rendering oqgraph_users/show_path
226
+ Completed in 6ms (View: 2, DB: 3) | 200 OK [http://localhost/oqgraph_users/223/path_to/2333]
227
+ ```
228
+
229
+ Of course YMMV.
230
+
231
+ ### Hairy bits, bugs and gotchas
232
+
233
+ To keep the oqgraph table up to date the edge model copies all of it records in when first instantiated.
234
+ This means that on first startup the app can be slow to respond until the whole graph has been written.
235
+ This should not need to happen again unless the DB is restarted. You can get the MySQL server to update the graph
236
+ by using the --init-file=<SQLfile> option in my.cnf with the appropriate SQL in it.
237
+
238
+ I've encountered a bug where the oqgraph table occasionally needs to be dropped and rebuilt. It's being tracked down.
239
+ If you are not getting any results from the oqgraph table try dropping it and restarting the app.
240
+
241
+ I'm working on a way to tell if the oqgraph table is stale other than by the current count of rows. Suggestions would be welcome.
242
+
243
+ Copyright (c) 2010 - 2012 Stuart Coyle, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << 'lib'
7
+ t.libs << 'test'
8
+ t.pattern = 'test/**/*_test.rb'
9
+ t.verbose = false
10
+ end
11
+
@@ -0,0 +1,40 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module OQGraph
4
+ module Generators
5
+ class OQGraphGenerator < ActiveRecord::Generators::Base
6
+ include Rails::Generators::ResourceHelpers
7
+
8
+ namespace "oqgraph"
9
+ source_root File.expand_path("../templates", __FILE__)
10
+
11
+ desc "Generates a model with the given NAME with oqgraph and and edge model called <NAME>Edge" <<
12
+ "plus a migration file for the edge model."
13
+
14
+ def create_edge_model
15
+ @node_class = file_name.camelize
16
+ @edge_class = "#{@node_class}Edge"
17
+ template "graph_edge.rb", File.join('app/models', "#{file_name}_edge.rb")
18
+ end
19
+
20
+ def create_node_model
21
+ @node_class = file_name.camelize
22
+ template "graph_node.rb", File.join('app/models', "#{file_name}.rb")
23
+ end
24
+
25
+ def create_edge_table_migration
26
+ @edge_table_name = @edge_class.pluralize.underscore
27
+ migration_template "graph_edge_migration.rb", "db/migrate/create_#{@edge_table_name}"
28
+ end
29
+
30
+ def create_initializer
31
+ template "graph_initializer.rb", File.join("config/initializers/#{file_name}_oqgraph.rb")
32
+ end
33
+
34
+ def create_oqgraph_migration
35
+ @oqgraph_table_name = "#{file_name}_oqgraph"
36
+ migration_template "graph_oqgraph_migration.rb", "db/migrate/create_#{@oqgraph_table_name}"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,4 @@
1
+ class <%= @edge_class %> < ActiveRecord::Base
2
+ include OQGraph::Edge
3
+
4
+ end
@@ -0,0 +1,12 @@
1
+ class Create<%= @edge_class.pluralize %> < ActiveRecord::Migration
2
+ # This migration creates the persistent database for the oqgraph table store.
3
+ # The edge class will create it's own OQGraph table called: <%= @edge_table_name %>_oqgraph
4
+
5
+ def change
6
+ create_table :<%= @edge_table_name %> do |t|
7
+ t.integer :from_id, :null => false
8
+ t.integer :to_id, :null => false
9
+ t.float :weight, :default => 1.0
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ # Sets up the OQGraph in memory table.
2
+ # Copies all the existing nodes to the graph table if the counts seem out.
3
+ #<%= @edge_class %>.update_graph_table
@@ -0,0 +1,4 @@
1
+ class <%= @node_class %> < ActiveRecord::Base
2
+ include OQGraph::Node
3
+
4
+ end
@@ -0,0 +1,21 @@
1
+ class Create<%= @node_class %>Oqgraph < ActiveRecord::Migration
2
+
3
+ def up
4
+ ActiveRecord::Base.connection.execute <<-EOS
5
+ CREATE TABLE IF NOT EXISTS <%= @oqgraph_table_name %> (
6
+ latch SMALLINT UNSIGNED NULL,
7
+ origid BIGINT UNSIGNED NULL,
8
+ destid BIGINT UNSIGNED NULL,
9
+ weight DOUBLE NULL,
10
+ seq BIGINT UNSIGNED NULL,
11
+ linkid BIGINT UNSIGNED NULL,
12
+ KEY (latch, origid, destid) USING HASH,
13
+ KEY (latch, destid, origid) USING HASH
14
+ ) ENGINE=OQGRAPH;
15
+ EOS
16
+ end
17
+
18
+ def down
19
+ drop_table "<%= @oqgraph_table_name %>"
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ require 'oqgraph/edge_instance_methods'
2
+ require 'oqgraph/edge_class_methods'
3
+
4
+ module OQGraph
5
+ module Edge
6
+ def self.included base
7
+ base.instance_eval do
8
+ def self.node_class_name
9
+ self.name.gsub(/Edge$/,'')
10
+ end
11
+
12
+ def self.node_class
13
+ node_class_name.constantize
14
+ end
15
+
16
+ self.table_name = name.underscore.pluralize
17
+
18
+ after_create :add_to_graph
19
+ after_destroy :remove_from_graph
20
+ after_update :update_graph
21
+
22
+ belongs_to :from, :class_name => node_class_name, :foreign_key => :from_id
23
+ belongs_to :to, :class_name => node_class_name, :foreign_key => :to_id
24
+
25
+ attr_accessible :from_id, :to_id, :weight
26
+
27
+ include OQGraph::EdgeInstanceMethods
28
+ extend OQGraph::EdgeClassMethods
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+
@@ -0,0 +1,52 @@
1
+ module OQGraph
2
+ module EdgeClassMethods
3
+
4
+ ##
5
+ # Creates the OQGraph table for the edge model.
6
+ # This table exists in memory only and needs to
7
+ # be updated on every database restart.
8
+ def create_graph_table
9
+ connection.execute <<-EOS
10
+ CREATE TABLE IF NOT EXISTS #{oqgraph_table_name} (
11
+ latch SMALLINT UNSIGNED NULL,
12
+ origid BIGINT UNSIGNED NULL,
13
+ destid BIGINT UNSIGNED NULL,
14
+ weight DOUBLE NULL,
15
+ seq BIGINT UNSIGNED NULL,
16
+ linkid BIGINT UNSIGNED NULL,
17
+ KEY (latch, origid, destid) USING HASH,
18
+ KEY (latch, destid, origid) USING HASH
19
+ ) ENGINE=OQGRAPH;
20
+ EOS
21
+
22
+ # if the DB server has restarted then there will be no records in the oqgraph table.
23
+ update_graph_table
24
+ end
25
+
26
+ def update_graph_table
27
+ update_oqgraph unless up_to_date?
28
+ end
29
+
30
+ def node_table
31
+ node_class.table_name
32
+ end
33
+
34
+ private
35
+ def oqgraph_table_name
36
+ "#{node_table.singularize}_oqgraph"
37
+ end
38
+
39
+ def up_to_date?
40
+ # Really need a better way to do this.
41
+ self.count == connection.select_value("SELECT COUNT(*) FROM #{oqgraph_table_name}")
42
+ end
43
+
44
+ def update_oqgraph
45
+ connection.execute <<-EOS
46
+ REPLACE INTO #{oqgraph_table_name} (origid, destid, weight)
47
+ SELECT from_id, to_id, weight FROM #{table_name}
48
+ WHERE from_id IS NOT NULL AND to_id IS NOT NULL
49
+ EOS
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,35 @@
1
+ module OQGraph
2
+ module EdgeInstanceMethods
3
+ private
4
+ def add_to_graph
5
+ connection.execute <<-EOS
6
+ REPLACE INTO #{oqgraph_table_name} (origid, destid, weight)
7
+ SELECT #{from_id || 0}, #{to_id || 0}, #{weight || 1.0}
8
+ EOS
9
+ end
10
+
11
+ def remove_from_graph
12
+ if self.class.where(:from_id => from_id, :to_id => to_id).count == 0
13
+ connection.execute <<-EOS
14
+ DELETE IGNORE FROM #{oqgraph_table_name}
15
+ WHERE origid = #{from_id}
16
+ AND destid = #{to_id};
17
+ EOS
18
+ end
19
+ end
20
+
21
+ def update_graph
22
+ connection.execute <<-EOS
23
+ UPDATE #{oqgraph_table_name}
24
+ SET origid = #{from_id},
25
+ destid = #{to_id},
26
+ weight = #{weight}
27
+ WHERE origid = #{from_id_was} AND destid = #{to_id_was};
28
+ EOS
29
+ end
30
+
31
+ def oqgraph_table_name
32
+ "#{self.class.node_table.singularize}_oqgraph"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,102 @@
1
+ module OQGraph
2
+ module Node
3
+ def self.included base
4
+ base.instance_eval do
5
+ def self.edge_class_name
6
+ "#{self.name}Edge"
7
+ end
8
+
9
+ def self.edge_class
10
+ edge_class_name.constantize
11
+ end
12
+
13
+ def self.rebuild_graph
14
+ edge_class.create_graph_table
15
+ end
16
+
17
+ def oqgraph_table_name
18
+ "#{table_name.singularize}_oqgraph"
19
+ end
20
+
21
+ has_many :outgoing_edges, {
22
+ :class_name => edge_class_name,
23
+ :foreign_key => :from_id,
24
+ :include => :to,
25
+ :dependent => :destroy
26
+ }
27
+
28
+ has_many :incoming_edges, {
29
+ :class_name => edge_class_name,
30
+ :foreign_key => :to_id,
31
+ :include => :from,
32
+ :dependent => :destroy
33
+ }
34
+
35
+ has_many :outgoing_nodes, :through => :outgoing_edges, :source => :to
36
+ has_many :incoming_nodes, :through => :incoming_edges, :source => :from
37
+
38
+ # Joins the oqgraph edge table. The select * ensures we get the weights.
39
+ scope :join_oqgraph, select('*').joins("JOIN #{oqgraph_table_name} ON #{oqgraph_table_name}.linkid=id")
40
+ scope :order_oqgraph, order("#{oqgraph_table_name}.seq")
41
+ # Latch: 0 = only find direct connections, 1 = use Djikstra algorithm, 2 = use Breadth first algorithm
42
+ scope :originating, lambda{|latch, destid| join_oqgraph.where("#{oqgraph_table_name}.latch = ? AND #{oqgraph_table_name}.destid = ?", latch, destid).order_oqgraph}
43
+ scope :reachable, lambda{|latch, origid| join_oqgraph.where("#{oqgraph_table_name}.latch = ? AND #{oqgraph_table_name}.origid = ?", latch, origid).order_oqgraph}
44
+ scope :shortest_path, lambda{|latch, origid, destid| join_oqgraph.where("#{oqgraph_table_name}.latch = ? AND #{oqgraph_table_name}.origid = ? AND #{oqgraph_table_name}.destid = ?", latch, origid, destid).order_oqgraph}
45
+ end
46
+ end
47
+
48
+ # Creates a one way edge from this node to another with a weight.
49
+ def create_edge_to(other, weight = 1.0)
50
+ self.class.edge_class.create!(:from_id => id, :to_id => other.id, :weight => weight)
51
+ end
52
+
53
+ # +other+ graph node to edge to
54
+ # +weight+ positive float denoting edge weight
55
+ # Creates a two way edge between this node and another.
56
+ def create_edge_to_and_from(other, weight = 1.0)
57
+ self.class.edge_class.create!(:from_id => id, :to_id => other.id, :weight => weight)
58
+ self.class.edge_class.create!(:from_id => other.id, :to_id => id, :weight => weight)
59
+ end
60
+
61
+ # +other+ The target node to find a route to
62
+ # +options+ A hash of options: Currently the only option is
63
+ # :method => :djiskstra or :breadth_first
64
+ # Returns an array of nodes in order starting with this node and ending in the target
65
+ # This will be the shortest path from this node to the other.
66
+ # The :djikstra method takes edge weights into account, the :breadth_first does not.
67
+ def shortest_path_to(other, options = {:method => :djikstra})
68
+ latch = options[:method] == :breadth_first ? 2 : 1
69
+ self.class.shortest_path(latch, id, other.id)
70
+ end
71
+
72
+ # +other+ The target node to find a route to
73
+ # Gives the path weight as a float of the shortest path to the other
74
+ def path_weight_to(other)
75
+ shortest_path_to(other,:method => :djikstra).map{|edge| edge.weight.to_f}.sum
76
+ end
77
+
78
+ # Returns an array of all nodes which can trace to this node
79
+ def originating
80
+ self.class.originating(2, id)
81
+ end
82
+
83
+ def originating_neighbours
84
+ self.class.originating(0, id)
85
+ end
86
+
87
+ # true if the other node can reach this node.
88
+ def originating? other
89
+ originating.include? other
90
+ end
91
+
92
+ # Returns all nodes reachable from this node.
93
+ def reachable
94
+ self.class.reachable(2, id)
95
+ end
96
+
97
+ # Returns all nodes reachable from this node.
98
+ def reachable? other
99
+ reachable.include? other
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,3 @@
1
+ module OqgraphRails
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,2 @@
1
+ require 'oqgraph/edge'
2
+ require 'oqgraph/node'
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/oqgraph_rails/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Stuart Coyle"]
6
+ gem.email = ["stuart.coyle@gmail.com"]
7
+ gem.description = %q{Graph engine interface for active record.}
8
+ gem.summary = %q{Enables the use of OpenQuery\'s OQGraph engine with active record. OQGraph is a graph database engine for MySQL.}
9
+ gem.homepage = "https://github.com/stuart/oqgraph_rails"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "oqgraph_rails"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = OqgraphRails::VERSION
17
+ gem.add_runtime_dependency 'mysql2'
18
+ gem.add_runtime_dependency 'activerecord'
19
+ gem.add_runtime_dependency 'minitest'
20
+ end
@@ -0,0 +1,64 @@
1
+ require 'test_helper'
2
+ require 'oqgraph_rails'
3
+
4
+ class TestModel < ActiveRecord::Base
5
+ include OQGraph::Node
6
+ end
7
+ class TestModelEdge < ActiveRecord::Base
8
+ include OQGraph::Edge
9
+ end
10
+
11
+ class GraphEdgeTest < ActiveSupport::TestCase
12
+ def setup
13
+ ActiveRecord::Base.establish_connection(
14
+ :adapter => "mysql2",
15
+ :host => "localhost",
16
+ :username => "root",
17
+ :password => "",
18
+ :database => "test"
19
+ )
20
+
21
+ ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS test_model_edges(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY, from_id INTEGER, to_id INTEGER, weight DOUBLE);")
22
+ TestModelEdge.create_graph_table
23
+ end
24
+
25
+ def teardown
26
+ ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS test_model_edges;")
27
+ ActiveRecord::Base.connection.execute("DELETE FROM test_model_oqgraph;")
28
+ end
29
+
30
+ test "the oqgraph table is created" do
31
+ assert_includes ActiveRecord::Base.connection.select_values("SHOW TABLES;"), 'test_model_oqgraph'
32
+ end
33
+
34
+ test "the oqgraph is updated when there are records that need to be added" do
35
+ test_model_edge = TestModelEdge.create!(:from_id => 1, :to_id => 2, :weight => 1.0)
36
+ TestModelEdge.create_graph_table
37
+ assert_equal 1, ActiveRecord::Base.connection.select_value("SELECT count(*) FROM test_model_oqgraph;")
38
+ end
39
+
40
+ test "adding a new edge to graph" do
41
+ test_model_edge = TestModelEdge.create!(:from_id => 1, :to_id => 2, :weight => 1.0)
42
+ assert_equal 1, ActiveRecord::Base.connection.select_value("SELECT count(*) FROM test_model_oqgraph;")
43
+ end
44
+
45
+ test "removing an edge from the graph" do
46
+ test_model_edge = TestModelEdge.create!(:from_id => 1, :to_id => 2, :weight => 1.0)
47
+ test_model_edge.destroy
48
+ assert_equal 0, ActiveRecord::Base.connection.select_value("SELECT count(*) FROM test_model_oqgraph;")
49
+ end
50
+
51
+ test "removing an edge does not remove from graph table if other edges with the same nodes exist" do
52
+ test_model_edge = TestModelEdge.create!(:from_id => 3, :to_id => 4, :weight => 1.0)
53
+ test_model_edge_2 = TestModelEdge.create!(:from_id => 3, :to_id => 4, :weight => 1.0)
54
+ test_model_edge.destroy
55
+ assert_equal 1, ActiveRecord::Base.connection.select_value("SELECT count(*) FROM test_model_oqgraph;")
56
+ end
57
+
58
+ test "updating an edge" do
59
+ test_model_edge = TestModelEdge.create!(:from_id => 3, :to_id => 4, :weight => 1.0)
60
+ test_model_edge = test_model_edge.update_attributes(:from_id => 4, :to_id => 1, :weight => 2.0)
61
+ assert_equal 1, ActiveRecord::Base.connection.select_value("SELECT count(*) FROM test_model_oqgraph WHERE origid = 4 AND destid = 1 AND weight = 2.0;")
62
+ assert_equal 0, ActiveRecord::Base.connection.select_value("SELECT count(*) FROM test_model_oqgraph WHERE origid = 3 AND destid = 4 AND weight = 1.0;")
63
+ end
64
+ end
@@ -0,0 +1,215 @@
1
+ require 'test_helper'
2
+ require 'oqgraph_rails'
3
+
4
+ class TestNode < ActiveRecord::Base
5
+ include OQGraph::Node
6
+ end
7
+
8
+ class TestNodeEdge < ActiveRecord::Base
9
+ include OQGraph::Edge
10
+ end
11
+
12
+ class GraphNodeTest < ActiveSupport::TestCase
13
+ def setup
14
+ ActiveRecord::Base.establish_connection(
15
+ :adapter => "mysql2",
16
+ :host => "localhost",
17
+ :username => "root",
18
+ :password => "",
19
+ :database => "test"
20
+ )
21
+
22
+ ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS test_nodes(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) );")
23
+ ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS test_node_edges(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY, from_id INTEGER, to_id INTEGER, weight DOUBLE);")
24
+ TestNodeEdge.create_graph_table
25
+
26
+ @test_1 = TestNode.create(:name => 'a')
27
+ @test_2 = TestNode.create(:name => 'b')
28
+ @test_3 = TestNode.create(:name => 'c')
29
+ @test_4 = TestNode.create(:name => 'd')
30
+ @test_5 = TestNode.create(:name => 'e')
31
+ @test_6 = TestNode.create(:name => 'f')
32
+ end
33
+
34
+ def teardown
35
+ ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS test_nodes;")
36
+ ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS test_node_edges;")
37
+ ActiveRecord::Base.connection.execute("DELETE FROM test_node_oqgraph;")
38
+ end
39
+
40
+ test "I can connect two nodes" do
41
+ @test_1.create_edge_to @test_2
42
+ assert_equal 1, TestNodeEdge.where(:from_id => @test_1.id, :to_id => @test_2.id, :weight => 1.0).count
43
+ assert_includes @test_1.outgoing_nodes, @test_2
44
+ end
45
+
46
+ test "creation of edges by association" do
47
+ @test_1.outgoing_nodes << @test_2
48
+ assert_not_nil edge = TestNodeEdge.find(:first, :conditions => {:from_id => @test_1.id, :to_id => @test_2.id})
49
+ end
50
+
51
+ test "creation of unidirectional edges" do
52
+ @test_1.create_edge_to_and_from(@test_2)
53
+ assert_not_nil edge = TestNodeEdge.find(:first, :conditions => {:from_id => @test_1.id, :to_id => @test_2.id, :weight => 1.0})
54
+ assert_not_nil edge = TestNodeEdge.find(:first, :conditions => {:from_id => @test_2.id, :to_id => @test_1.id, :weight => 1.0})
55
+ assert @test_1.outgoing_nodes.include?(@test_2)
56
+ assert @test_1.incoming_nodes.include?(@test_2)
57
+ end
58
+
59
+ test "adding of weights to edges" do
60
+ @test_1.create_edge_to(@test_2, 2.0)
61
+ assert_not_nil edge = TestNodeEdge.find(:first, :conditions => {:from_id => @test_1.id, :to_id => @test_2.id})
62
+ assert edge.weight = 2.0
63
+ end
64
+
65
+ test "edge model creation creates oqgraph edge" do
66
+ @test_1.create_edge_to(@test_2, 2.5)
67
+ oqedge = ActiveRecord::Base.connection.execute("SELECT * FROM test_node_oqgraph WHERE origid=#{@test_1.id} AND destid=#{@test_2.id};")
68
+ assert_equal [nil,1,2,2.5,nil,nil], oqedge.first
69
+ end
70
+
71
+ test "edge model deletion removes oqgraph edge" do
72
+ @test_1.outgoing_nodes << @test_2
73
+ edge = @test_1.outgoing_edges.find(:first, :conditions => {:to_id => @test_2.id})
74
+ edge.destroy
75
+ oqedge = ActiveRecord::Base.connection.execute("SELECT * FROM test_node_oqgraph WHERE origid=#{@test_1.id} AND destid=#{@test_2.id};")
76
+ assert_equal nil, oqedge.first
77
+ end
78
+
79
+ test "edge model update updates oqgraph edge" do
80
+ edge = @test_1.create_edge_to(@test_2, 2.5)
81
+ edge.update_attributes(:weight => 3.0)
82
+ oqedge = ActiveRecord::Base.connection.execute("SELECT * FROM test_node_oqgraph WHERE origid=#{@test_1.id} AND destid=#{@test_2.id};")
83
+ assert_equal [nil,1,2,3,nil,nil], oqedge.first
84
+ edge.update_attributes(:to_id => 3)
85
+ oqedge = ActiveRecord::Base.connection.execute("SELECT * FROM test_node_oqgraph WHERE origid=#{@test_1.id} AND destid=3;")
86
+ assert_equal [nil,1,3,3,nil,nil], oqedge.first
87
+ end
88
+
89
+ test "getting the shortest path" do
90
+ # a -> b -> c -> d
91
+ @test_1.create_edge_to @test_2
92
+ @test_2.create_edge_to @test_3
93
+ @test_3.create_edge_to @test_4
94
+ assert_equal [@test_1, @test_2, @test_3, @test_4], @test_1.shortest_path_to(@test_4)
95
+ assert_equal ['a','b','c','d'], @test_1.shortest_path_to(@test_4).map(&:name)
96
+ end
97
+
98
+ test "complex getting the shortest path" do
99
+ #
100
+ # a -> b -> c -> d
101
+ # | /
102
+ # e-> f
103
+ @test_1.create_edge_to @test_2
104
+ @test_2.create_edge_to @test_3
105
+ @test_3.create_edge_to @test_4
106
+ @test_2.create_edge_to @test_5
107
+ @test_5.create_edge_to @test_6
108
+ @test_4.create_edge_to @test_6
109
+ assert_equal [@test_1, @test_2, @test_5, @test_6], @test_1.shortest_path_to(@test_6)
110
+ end
111
+
112
+ test "shortest path returns the weight" do
113
+ # a -- b -- c -- d
114
+ @test_1.create_edge_to @test_2, 2.0
115
+ @test_2.create_edge_to @test_3, 1.5
116
+ @test_3.create_edge_to @test_4, 1.2
117
+ assert_equal [nil, 2.0 ,1.5, 1.2], @test_1.shortest_path_to(@test_4).map(&:weight)
118
+ end
119
+
120
+ test "getting the path weight total" do
121
+ # a -- b -- c -- d
122
+ @test_1.create_edge_to @test_2, 2.0
123
+ @test_2.create_edge_to @test_3, 1.5
124
+ @test_3.create_edge_to @test_4, 1.2
125
+ assert_equal 4.7, @test_1.path_weight_to(@test_4)
126
+ end
127
+
128
+ test "finding the path with breadth first" do
129
+ @test_1.outgoing_nodes << @test_2
130
+ @test_2.outgoing_nodes << @test_3
131
+ @test_3.outgoing_nodes << @test_4
132
+ assert_equal [@test_1, @test_2, @test_3, @test_4], @test_1.shortest_path_to(@test_4, :method => :breadth_first)
133
+ end
134
+
135
+ # 1 -> 2 -> 3
136
+ test "getting originating nodes" do
137
+ @test_1.create_edge_to @test_2
138
+ @test_2.create_edge_to @test_3
139
+ assert_equal [@test_2, @test_1], @test_2.originating
140
+ end
141
+
142
+ test "getting the reachable nodes" do
143
+ @test_1.create_edge_to @test_2
144
+ @test_2.create_edge_to @test_3
145
+ assert_equal [@test_2, @test_3] , @test_2.reachable
146
+ end
147
+
148
+ test "testing if the node is originating" do
149
+ @test_1.create_edge_to @test_2
150
+ @test_2.create_edge_to @test_3
151
+ assert @test_2.originating?(@test_1)
152
+ refute @test_2.originating?(@test_3)
153
+ end
154
+
155
+ test "testing if the node is reachable" do
156
+ @test_1.create_edge_to @test_2
157
+ @test_2.create_edge_to @test_3
158
+ assert @test_2.reachable?(@test_3)
159
+ refute @test_2.reachable?(@test_1)
160
+ end
161
+
162
+ test "get the incoming nodes" do
163
+ @test_1.create_edge_to @test_2
164
+ @test_2.create_edge_to @test_3
165
+ assert_equal [@test_1], @test_2.incoming_nodes
166
+ end
167
+
168
+ test "get the outgoing nodes" do
169
+ @test_1.create_edge_to @test_2
170
+ @test_2.create_edge_to @test_3
171
+ assert_equal [@test_3], @test_2.outgoing_nodes
172
+ end
173
+
174
+ test "duplicate links are ignored" do
175
+ @test_1.create_edge_to @test_2
176
+ assert_nothing_raised do
177
+ @test_1.create_edge_to @test_2
178
+ end
179
+ end
180
+
181
+ test "duplicate link error" do
182
+ ActiveRecord::Base.connection.execute("INSERT INTO test_node_oqgraph (destid, origid, weight) VALUES (99,99,1.0);")
183
+ assert_raises ActiveRecord::StatementInvalid do
184
+ ActiveRecord::Base.connection.execute("INSERT INTO test_node_oqgraph (destid, origid, weight) VALUES (99,99,1.0);")
185
+ end
186
+ end
187
+
188
+ test "duplicate link error fix" do
189
+ ActiveRecord::Base.connection.execute("REPLACE INTO test_node_oqgraph (destid, origid, weight) VALUES (99,99,1.0);")
190
+ assert_nothing_raised do
191
+ ActiveRecord::Base.connection.execute("REPLACE INTO test_node_oqgraph (destid, origid, weight) VALUES (99,99,1.0);")
192
+ end
193
+ end
194
+
195
+ # There's an odd case here where MySQL would raise an error only when using Rails.
196
+ test "deletion of a nonexistent edge does no raise an error" do
197
+ edge = @test_1.create_edge_to @test_2
198
+ ActiveRecord::Base.connection.execute("DELETE FROM test_node_oqgraph WHERE destid = #{edge.to_id} AND origid = #{edge.from_id}")
199
+ assert_nothing_raised do
200
+ edge.destroy
201
+ end
202
+ end
203
+
204
+ test "rebuilding of edge graph" do
205
+ @test_1.create_edge_to @test_2
206
+ @test_2.create_edge_to @test_3
207
+ @test_3.create_edge_to @test_4
208
+ # Simulate the DB restart
209
+ ActiveRecord::Base.connection.execute("DELETE FROM test_node_oqgraph;")
210
+ TestNode.rebuild_graph
211
+
212
+ assert_equal [@test_1, @test_2, @test_3, @test_4], @test_1.shortest_path_to(@test_4)
213
+ assert_equal ['a','b','c','d'], @test_1.shortest_path_to(@test_4).map(&:name)
214
+ end
215
+ end
@@ -0,0 +1,55 @@
1
+ require 'test_helper'
2
+ require 'generators/oqgraph/oqgraph_generator'
3
+ class OQgraphGeneratorTest < Rails::Generators::TestCase
4
+ tests OQGraph::Generators::OQGraphGenerator
5
+
6
+ destination File.expand_path("../tmp", __FILE__)
7
+ setup :prepare_destination
8
+
9
+ test "creates the correct migration file" do
10
+ run_generator %w(funky)
11
+ assert_migration 'db/migrate/create_funky_edges', /create_table :funky_edges do/
12
+ end
13
+
14
+ test "creates the edge model class" do
15
+ run_generator %w(funky)
16
+ assert_file 'app/models/funky_edge.rb', /class FunkyEdge < ActiveRecord::Base/
17
+ end
18
+
19
+ test "created edge model class is parseable ruby" do
20
+ run_generator %w(funky)
21
+ assert_nothing_raised do
22
+ require 'tmp/app/models/funky_edge'
23
+ end
24
+ end
25
+
26
+ test "creates the node model class" do
27
+ run_generator %w(funky)
28
+ assert_file 'app/models/funky.rb', /class Funky < ActiveRecord::Base/
29
+ end
30
+
31
+ test "created node model class is parseable ruby" do
32
+ run_generator %w(funky)
33
+ assert_nothing_raised do
34
+ require 'tmp/app/models/funky'
35
+ end
36
+ end
37
+
38
+ test "creates the initializer" do
39
+ run_generator %w(funky)
40
+ assert_file 'config/initializers/funky_oqgraph.rb', /FunkyEdge.update_graph_table/
41
+ end
42
+
43
+ test "the initializer is parseable ruby" do
44
+ run_generator %w(funky)
45
+ assert_nothing_raised do
46
+ require 'tmp/config/initializers/funky_oqgraph.rb'
47
+ end
48
+ end
49
+
50
+ test "creates the oqgraph table migration" do
51
+ run_generator %w(funky)
52
+ assert_migration 'db/migrate/create_funky_oqgraph.rb'
53
+ end
54
+
55
+ end
@@ -0,0 +1,52 @@
1
+ require 'test_helper'
2
+ #require 'active_support/testing/performance'
3
+
4
+ class TestNode < ActiveRecord::Base
5
+ include OQGraph::Node
6
+ end
7
+
8
+ class TestNodeEdge < ActiveRecord::Base
9
+ include OQGraph::Edge
10
+ end
11
+
12
+ class PerformanceTest < ActiveSupport::TestCase
13
+ #include ActiveSupport::Testing::Performance
14
+ def setup
15
+ ActiveRecord::Base.establish_connection(
16
+ :adapter => "mysql2",
17
+ :host => "localhost",
18
+ :username => "root",
19
+ :password => "",
20
+ :database => "test"
21
+ )
22
+
23
+ ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS test_nodes(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) );")
24
+ ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS test_node_edges(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY, from_id INTEGER, to_id INTEGER, weight DOUBLE);")
25
+ TestNodeEdge.create_graph_table
26
+
27
+ puts "Creating 10000 test nodes"
28
+ @test_nodes = [1..10000].map{|i| TestNode.create(:name => "node#{i}")}
29
+ end
30
+
31
+ def teardown
32
+ ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS test_nodes;")
33
+ ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS test_node_edges;")
34
+ ActiveRecord::Base.connection.execute("DELETE FROM test_node_oqgraph;")
35
+ end
36
+
37
+ test "joining nodes" do
38
+ @test_nodes.each do |node|
39
+
40
+ end
41
+ end
42
+
43
+ test "finding the shortest path" do
44
+ end
45
+
46
+ test "finding connected nodes" do
47
+ end
48
+
49
+ test "removing connections" do
50
+ end
51
+
52
+ end
@@ -0,0 +1,11 @@
1
+ # Configure Rails Envinronment
2
+ ENV["RAILS_ENV"] = "test"
3
+
4
+ gem 'minitest'
5
+ require 'rails'
6
+ require "rails/test_help"
7
+
8
+ # For generators
9
+ require 'rails/generators/test_case'
10
+ require 'active_record'
11
+
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oqgraph_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Stuart Coyle
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-24 00:00:00.000000000 +10:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mysql2
17
+ requirement: &2158956720 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *2158956720
26
+ - !ruby/object:Gem::Dependency
27
+ name: activerecord
28
+ requirement: &2158955900 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *2158955900
37
+ - !ruby/object:Gem::Dependency
38
+ name: minitest
39
+ requirement: &2158946220 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: *2158946220
48
+ description: Graph engine interface for active record.
49
+ email:
50
+ - stuart.coyle@gmail.com
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - Gemfile
56
+ - Gemfile.lock
57
+ - LICENCE
58
+ - README.md
59
+ - Rakefile
60
+ - lib/generators/oqgraph/oqgraph_generator.rb
61
+ - lib/generators/oqgraph/templates/graph_edge.rb
62
+ - lib/generators/oqgraph/templates/graph_edge_migration.rb
63
+ - lib/generators/oqgraph/templates/graph_initializer.rb
64
+ - lib/generators/oqgraph/templates/graph_node.rb
65
+ - lib/generators/oqgraph/templates/graph_oqgraph_migration.rb
66
+ - lib/oqgraph/edge.rb
67
+ - lib/oqgraph/edge_class_methods.rb
68
+ - lib/oqgraph/edge_instance_methods.rb
69
+ - lib/oqgraph/node.rb
70
+ - lib/oqgraph_rails.rb
71
+ - lib/oqgraph_rails/version.rb
72
+ - oqgraph_rails.gemspec
73
+ - test/graph_edge_test.rb
74
+ - test/graph_node_test.rb
75
+ - test/oqgraph_generator_test.rb
76
+ - test/performance_test.rb
77
+ - test/test_helper.rb
78
+ has_rdoc: true
79
+ homepage: https://github.com/stuart/oqgraph_rails
80
+ licenses: []
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ! '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 1.6.2
100
+ signing_key:
101
+ specification_version: 3
102
+ summary: Enables the use of OpenQuery\'s OQGraph engine with active record. OQGraph
103
+ is a graph database engine for MySQL.
104
+ test_files:
105
+ - test/graph_edge_test.rb
106
+ - test/graph_node_test.rb
107
+ - test/oqgraph_generator_test.rb
108
+ - test/performance_test.rb
109
+ - test/test_helper.rb