acts_as_oqgraph 0.1.5 → 0.1.6
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/VERSION +1 -1
- data/acts_as_oqgraph.gemspec +1 -1
- data/lib/acts_as_oqgraph.rb +7 -6
- data/lib/graph_edge.rb +57 -40
- data/test/test_acts_as_oqgraph.rb +8 -5
- metadata +2 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.6
|
data/acts_as_oqgraph.gemspec
CHANGED
data/lib/acts_as_oqgraph.rb
CHANGED
@@ -60,7 +60,10 @@ module OQGraph
|
|
60
60
|
# Path Finding:
|
61
61
|
# foo.shortest_path_to(baz)
|
62
62
|
# returns [foo, bar,baz]
|
63
|
-
#
|
63
|
+
#
|
64
|
+
# With breadth first edge weights are not taken into account:
|
65
|
+
# foo.shortest_path_to(baz, :method => :breadth_first)
|
66
|
+
#
|
64
67
|
# All these methods return the node object with an additional weight field.
|
65
68
|
# This enables you to query the weights associated with the edges found.
|
66
69
|
#
|
@@ -139,9 +142,7 @@ module OQGraph
|
|
139
142
|
end
|
140
143
|
return result
|
141
144
|
rescue ActiveRecord::StatementInvalid => e
|
142
|
-
|
143
|
-
# TODO: Raise a sensible error message here.
|
144
|
-
return false
|
145
|
+
raise "MySQL or MariaDB 5.1 or above with the OQGRAPH engine is required for the acts_as_oqgraph gem.\nThe following error was raised: #{e.inspect}"
|
145
146
|
end
|
146
147
|
end
|
147
148
|
end
|
@@ -178,7 +179,7 @@ module OQGraph
|
|
178
179
|
|
179
180
|
# Returns an array of all nodes which can trace to this node
|
180
181
|
def originating
|
181
|
-
edge_class.
|
182
|
+
edge_class.originating_nodes(self)
|
182
183
|
end
|
183
184
|
|
184
185
|
# true if the other node can reach this node.
|
@@ -188,7 +189,7 @@ module OQGraph
|
|
188
189
|
|
189
190
|
# Returns all nodes reachable from this node.
|
190
191
|
def reachable
|
191
|
-
edge_class.
|
192
|
+
edge_class.reachable_nodes(self)
|
192
193
|
end
|
193
194
|
|
194
195
|
# true if the other node is reachable from this one
|
data/lib/graph_edge.rb
CHANGED
@@ -6,12 +6,13 @@ class GraphEdge < ActiveRecord::Base
|
|
6
6
|
after_destroy :remove_from_graph
|
7
7
|
after_update :update_graph
|
8
8
|
|
9
|
-
|
10
9
|
# Creates the OQgraph table if it does not exist.
|
11
10
|
# Deletes all entries if it does exist and then repopulates with
|
12
|
-
# current edges. TODO Optimise this so that it only does so if the
|
13
|
-
# DB server has been restarted.
|
11
|
+
# current edges. TODO: Optimise this so that it only does so if the
|
12
|
+
# DB server has been restarted rather than when the app is restarted.
|
14
13
|
def self.create_graph_table
|
14
|
+
connection.execute "DROP TABLE IF EXISTS #{oqgraph_table_name}"
|
15
|
+
|
15
16
|
connection.execute <<-EOS
|
16
17
|
CREATE TABLE IF NOT EXISTS #{oqgraph_table_name} (
|
17
18
|
latch SMALLINT UNSIGNED NULL,
|
@@ -25,13 +26,17 @@ class GraphEdge < ActiveRecord::Base
|
|
25
26
|
) ENGINE=OQGRAPH;
|
26
27
|
EOS
|
27
28
|
|
28
|
-
connection.execute
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
29
|
+
connection.execute <<-EOS
|
30
|
+
INSERT INTO #{oqgraph_table_name} (origid, destid, weight)
|
31
|
+
SELECT #{from_key}, #{to_key}, #{weight_column} FROM #{table_name}
|
32
|
+
EOS
|
33
33
|
end
|
34
34
|
|
35
|
+
# Returns the shortest path from node to node.
|
36
|
+
# +options+ A hash containing options.
|
37
|
+
# The only option is :method which can be
|
38
|
+
# :breadth_first to do a breadth first search.
|
39
|
+
# It defaults to using Djikstra's algorithm.
|
35
40
|
def self.shortest_path(from, to, options)
|
36
41
|
latch = case options[:method]
|
37
42
|
when :breadth_first then 2
|
@@ -48,73 +53,85 @@ class GraphEdge < ActiveRecord::Base
|
|
48
53
|
node_class.find_by_sql select_for_node << sql
|
49
54
|
end
|
50
55
|
|
51
|
-
|
56
|
+
# Finds all the nodes that lead to the node
|
57
|
+
def self.originating_nodes(node)
|
52
58
|
sql = <<-EOS
|
53
|
-
WHERE latch = 1 AND destid = #{
|
59
|
+
WHERE latch = 1 AND destid = #{node.id}
|
54
60
|
ORDER BY seq;
|
55
61
|
EOS
|
56
62
|
|
57
63
|
node_class.find_by_sql select_for_node << sql
|
58
64
|
end
|
59
65
|
|
60
|
-
|
66
|
+
# Finds all the nodes that are reachable from the node
|
67
|
+
def self.reachable_nodes(node)
|
61
68
|
sql = <<-EOS
|
62
|
-
WHERE latch = 1 AND origid = #{
|
69
|
+
WHERE latch = 1 AND origid = #{node.id}
|
63
70
|
ORDER BY seq;
|
64
71
|
EOS
|
65
72
|
|
66
73
|
node_class.find_by_sql select_for_node << sql
|
67
74
|
end
|
68
75
|
|
76
|
+
# Finds the edges leading directly into the node
|
69
77
|
# FIXME: Note this currently does not work.
|
70
78
|
# I suspect a bug in OQGRaph engine.
|
71
|
-
|
79
|
+
# Using the node classes incoming_nodes is equivalent to this.
|
80
|
+
def self.in_edges(node)
|
72
81
|
sql = <<-EOS
|
73
|
-
WHERE latch = 0 AND destid = #{
|
82
|
+
WHERE latch = 0 AND destid = #{node.id}
|
74
83
|
EOS
|
75
84
|
|
76
85
|
node_class.find_by_sql select_for_node << sql
|
77
86
|
end
|
78
87
|
|
79
|
-
|
88
|
+
# Finds the edges leading directly out of the node
|
89
|
+
def self.out_edges(node)
|
80
90
|
sql = <<-EOS
|
81
|
-
WHERE latch = 0 AND origid = #{
|
91
|
+
WHERE latch = 0 AND origid = #{node.id}
|
82
92
|
EOS
|
83
93
|
|
84
94
|
node_class.find_by_sql select_for_node << sql
|
85
95
|
end
|
86
|
-
|
87
|
-
def add_to_graph
|
88
|
-
connection.execute <<-EOS
|
89
|
-
REPLACE INTO #{oqgraph_table_name} (origid, destid, weight)
|
90
|
-
VALUES (#{self.send(self.class.from_key)}, #{self.send(self.class.to_key)}, #{weight || 1.0})
|
91
|
-
EOS
|
92
|
-
end
|
93
96
|
|
94
97
|
private
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
WHERE origid = #{self.send(self.class.from_key
|
110
|
-
|
111
|
-
|
98
|
+
|
99
|
+
# Callback to add new graph edges to the OQGraph table
|
100
|
+
def add_to_graph
|
101
|
+
connection.execute <<-EOS
|
102
|
+
REPLACE INTO #{oqgraph_table_name} (origid, destid, weight)
|
103
|
+
VALUES (#{self.send(self.class.from_key)}, #{self.send(self.class.to_key)}, #{self.send(self.class.weight_column) || 1.0})
|
104
|
+
EOS
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
# Callback to remove deleted graph edges from the OQGraph table
|
109
|
+
def remove_from_graph
|
110
|
+
# Ignores trying to delete nonexistent records
|
111
|
+
connection.execute <<-EOS
|
112
|
+
DELETE IGNORE FROM #{oqgraph_table_name} WHERE origid = #{self.send(self.class.from_key)} AND destid = #{self.send(self.class.to_key)};
|
113
|
+
EOS
|
114
|
+
end
|
115
|
+
|
116
|
+
# Callback to update graph edges in the OQGraph table
|
117
|
+
def update_graph
|
118
|
+
connection.execute <<-EOS
|
119
|
+
UPDATE #{oqgraph_table_name}
|
120
|
+
SET origid = #{self.send(self.class.from_key)},
|
121
|
+
destid = #{self.send(self.class.to_key)},
|
122
|
+
weight = #{self.send(self.class.weight_column)}
|
123
|
+
WHERE origid = #{self.send(self.class.from_key + '_was')} AND destid = #{self.send(self.class.to_key + '_was')};
|
124
|
+
EOS
|
125
|
+
end
|
126
|
+
|
127
|
+
# nodoc
|
112
128
|
def self.select_for_node
|
113
129
|
sql = "SELECT "
|
114
130
|
sql << node_class.columns.map{|column| "#{node_table}.#{column.name}"}.join(",")
|
115
131
|
sql << ", #{oqgraph_table_name}.weight FROM #{oqgraph_table_name} JOIN #{node_table} ON (linkid=id) "
|
116
132
|
end
|
117
133
|
|
134
|
+
# Returns the table containing the nodes for these edges.
|
118
135
|
def self.node_table
|
119
136
|
node_class.table_name
|
120
137
|
end
|
@@ -2,7 +2,6 @@ require 'helper'
|
|
2
2
|
|
3
3
|
class TestActsAsOqgraph < ActiveSupport::TestCase
|
4
4
|
def setup
|
5
|
-
|
6
5
|
ActiveRecord::Base.establish_connection(
|
7
6
|
:adapter => "mysql",
|
8
7
|
:host => "localhost",
|
@@ -13,8 +12,9 @@ class TestActsAsOqgraph < ActiveSupport::TestCase
|
|
13
12
|
|
14
13
|
ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS test_models(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) );")
|
15
14
|
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);")
|
16
|
-
ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS custom_edges(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY,
|
15
|
+
ActiveRecord::Base.connection.execute("CREATE TABLE IF NOT EXISTS custom_edges(id INTEGER DEFAULT NULL AUTO_INCREMENT PRIMARY KEY, orig_id INTEGER, dest_id INTEGER, length DOUBLE);")
|
17
16
|
|
17
|
+
# These requires need the tables to be created.
|
18
18
|
require File.join(File.dirname(__FILE__),'models/custom_test_model')
|
19
19
|
require File.join(File.dirname(__FILE__),'models/test_model')
|
20
20
|
|
@@ -24,7 +24,6 @@ class TestActsAsOqgraph < ActiveSupport::TestCase
|
|
24
24
|
@test_4 = TestModel.create(:name => 'd')
|
25
25
|
@test_5 = TestModel.create(:name => 'e')
|
26
26
|
@test_6 = TestModel.create(:name => 'f')
|
27
|
-
|
28
27
|
end
|
29
28
|
|
30
29
|
def teardown
|
@@ -33,7 +32,6 @@ class TestActsAsOqgraph < ActiveSupport::TestCase
|
|
33
32
|
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS custom_edges;")
|
34
33
|
ActiveRecord::Base.connection.execute("DELETE FROM test_model_oqgraph;")
|
35
34
|
ActiveRecord::Base.connection.execute("DELETE FROM custom_test_model_oqgraph;")
|
36
|
-
|
37
35
|
end
|
38
36
|
|
39
37
|
def test_edge_table_and_class_names
|
@@ -70,6 +68,11 @@ class TestActsAsOqgraph < ActiveSupport::TestCase
|
|
70
68
|
assert_equal 'dest_id', CustomEdge.to_key
|
71
69
|
assert_equal 'orig_id', CustomEdge.from_key
|
72
70
|
assert_equal 'length', CustomEdge.weight_column
|
71
|
+
mysql = Mysql.new('localhost', 'root', '', 'test')
|
72
|
+
fields = mysql.list_fields('custom_edges').fetch_fields.map{|f| f.name}
|
73
|
+
assert fields.include?('orig_id')
|
74
|
+
assert fields.include?('dest_id')
|
75
|
+
assert fields.include?('length')
|
73
76
|
end
|
74
77
|
|
75
78
|
def test_test_model_edge_creation
|
@@ -222,7 +225,7 @@ class TestActsAsOqgraph < ActiveSupport::TestCase
|
|
222
225
|
def test_deletion_of_nonexistent_edge_raises_error
|
223
226
|
edge = @test_1.create_edge_to @test_2
|
224
227
|
ActiveRecord::Base.connection.execute("DELETE FROM test_model_oqgraph WHERE destid = #{edge.to_id} AND origid = #{edge.from_id}")
|
225
|
-
|
228
|
+
assert_nothing_raised do
|
226
229
|
edge.destroy
|
227
230
|
end
|
228
231
|
end
|