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 +2 -0
- data/Gemfile.lock +35 -0
- data/LICENCE +22 -0
- data/README.md +243 -0
- data/Rakefile +11 -0
- data/lib/generators/oqgraph/oqgraph_generator.rb +40 -0
- data/lib/generators/oqgraph/templates/graph_edge.rb +4 -0
- data/lib/generators/oqgraph/templates/graph_edge_migration.rb +12 -0
- data/lib/generators/oqgraph/templates/graph_initializer.rb +3 -0
- data/lib/generators/oqgraph/templates/graph_node.rb +4 -0
- data/lib/generators/oqgraph/templates/graph_oqgraph_migration.rb +21 -0
- data/lib/oqgraph/edge.rb +34 -0
- data/lib/oqgraph/edge_class_methods.rb +52 -0
- data/lib/oqgraph/edge_instance_methods.rb +35 -0
- data/lib/oqgraph/node.rb +102 -0
- data/lib/oqgraph_rails/version.rb +3 -0
- data/lib/oqgraph_rails.rb +2 -0
- data/oqgraph_rails.gemspec +20 -0
- data/test/graph_edge_test.rb +64 -0
- data/test/graph_node_test.rb +215 -0
- data/test/oqgraph_generator_test.rb +55 -0
- data/test/performance_test.rb +52 -0
- data/test/test_helper.rb +11 -0
- metadata +109 -0
data/Gemfile
ADDED
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,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,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,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
|
data/lib/oqgraph/edge.rb
ADDED
@@ -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
|
data/lib/oqgraph/node.rb
ADDED
@@ -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,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
|
data/test/test_helper.rb
ADDED
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
|