oqgraph_rails 0.0.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.
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