dagnabit 2.2.6 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.md +105 -0
- data/LICENSE +1 -1
- data/MIGRATION.md +56 -0
- data/README.md +222 -0
- data/bin/dagnabit-test +11 -31
- data/db/connection.rb +3 -0
- data/{test/connections/native_postgresql → db/connections/postgresql}/connection.rb +6 -5
- data/db/models/edge.rb +6 -0
- data/db/models/other_edge.rb +6 -0
- data/db/models/other_vertex.rb +6 -0
- data/db/models/vertex.rb +6 -0
- data/db/schema.rb +32 -0
- data/lib/dagnabit.rb +6 -19
- data/lib/dagnabit/edge.rb +7 -0
- data/lib/dagnabit/edge/activation.rb +14 -0
- data/lib/dagnabit/edge/associations.rb +10 -0
- data/lib/dagnabit/edge/connectivity.rb +38 -0
- data/lib/dagnabit/graph.rb +143 -0
- data/lib/dagnabit/migration.rb +98 -0
- data/lib/dagnabit/version.rb +3 -0
- data/lib/dagnabit/vertex.rb +10 -0
- data/lib/dagnabit/vertex/activation.rb +19 -0
- data/lib/dagnabit/vertex/associations.rb +24 -0
- data/lib/dagnabit/vertex/bonding.rb +43 -0
- data/lib/dagnabit/vertex/connectivity.rb +130 -0
- data/lib/dagnabit/vertex/neighbors.rb +50 -0
- data/lib/dagnabit/vertex/settings.rb +56 -0
- metadata +94 -143
- data/.autotest +0 -5
- data/.document +0 -5
- data/.gitignore +0 -7
- data/Gemfile +0 -15
- data/Gemfile.lock +0 -38
- data/History.txt +0 -81
- data/README.rdoc +0 -202
- data/Rakefile +0 -52
- data/VERSION.yml +0 -5
- data/dagnabit.gemspec +0 -142
- data/init.rb +0 -1
- data/lib/dagnabit/activation.rb +0 -60
- data/lib/dagnabit/link/associations.rb +0 -18
- data/lib/dagnabit/link/class_methods.rb +0 -43
- data/lib/dagnabit/link/configuration.rb +0 -40
- data/lib/dagnabit/link/cycle_prevention.rb +0 -31
- data/lib/dagnabit/link/named_scopes.rb +0 -65
- data/lib/dagnabit/link/transitive_closure_link_model.rb +0 -86
- data/lib/dagnabit/link/transitive_closure_recalculation.rb +0 -17
- data/lib/dagnabit/link/transitive_closure_recalculation/on_create.rb +0 -104
- data/lib/dagnabit/link/transitive_closure_recalculation/on_destroy.rb +0 -125
- data/lib/dagnabit/link/transitive_closure_recalculation/on_update.rb +0 -17
- data/lib/dagnabit/link/transitive_closure_recalculation/utilities.rb +0 -56
- data/lib/dagnabit/link/validations.rb +0 -26
- data/lib/dagnabit/node/associations.rb +0 -84
- data/lib/dagnabit/node/class_methods.rb +0 -74
- data/lib/dagnabit/node/configuration.rb +0 -26
- data/lib/dagnabit/node/neighbors.rb +0 -73
- data/test/connections/native_sqlite3/connection.rb +0 -24
- data/test/dagnabit/link/test_associations.rb +0 -61
- data/test/dagnabit/link/test_class_methods.rb +0 -102
- data/test/dagnabit/link/test_configuration.rb +0 -38
- data/test/dagnabit/link/test_cycle_prevention.rb +0 -64
- data/test/dagnabit/link/test_named_scopes.rb +0 -32
- data/test/dagnabit/link/test_transitive_closure_link_model.rb +0 -69
- data/test/dagnabit/link/test_transitive_closure_recalculation.rb +0 -178
- data/test/dagnabit/link/test_validations.rb +0 -39
- data/test/dagnabit/node/test_associations.rb +0 -147
- data/test/dagnabit/node/test_class_methods.rb +0 -49
- data/test/dagnabit/node/test_configuration.rb +0 -29
- data/test/dagnabit/node/test_neighbors.rb +0 -91
- data/test/helper.rb +0 -26
- data/test/models/beta_node.rb +0 -3
- data/test/models/custom_data_link.rb +0 -4
- data/test/models/customized_link.rb +0 -7
- data/test/models/customized_link_node.rb +0 -4
- data/test/models/link.rb +0 -4
- data/test/models/node.rb +0 -3
- data/test/schema/schema.rb +0 -51
data/History.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
3.0.0 2011-01-10
|
2
|
+
================
|
3
|
+
|
4
|
+
Major changes
|
5
|
+
-------------
|
6
|
+
|
7
|
+
* Completely new data model: no more post-insert processing, no more transitive
|
8
|
+
closure table. Downside: only PostgreSQL is presently supported.
|
9
|
+
|
10
|
+
2.2.6 2010-01-01
|
11
|
+
================
|
12
|
+
|
13
|
+
Bugfixes
|
14
|
+
--------
|
15
|
+
|
16
|
+
* Fixed version requirements.
|
17
|
+
|
18
|
+
2.2.5 2010-10-01
|
19
|
+
================
|
20
|
+
|
21
|
+
Minor changes
|
22
|
+
-------------
|
23
|
+
|
24
|
+
* Ported development code to Bundler 1.0.
|
25
|
+
* Removed usage of `Object#returning`, which was deprecated in ActiveSupport
|
26
|
+
2.3.9.
|
27
|
+
|
28
|
+
2.2.4 2010-03-15
|
29
|
+
================
|
30
|
+
|
31
|
+
Bugfixes
|
32
|
+
--------
|
33
|
+
|
34
|
+
* Ensure that ActiveRecord is loaded before dagnabit's extensions are loaded.
|
35
|
+
This fixes a problem that would occasionally occur when using dagnabit in
|
36
|
+
Bundler.
|
37
|
+
|
38
|
+
2.2.3 2010-02-17
|
39
|
+
================
|
40
|
+
|
41
|
+
Bugfixes
|
42
|
+
--------
|
43
|
+
|
44
|
+
* It was not possible to delete links having custom data attributes in dagnabit
|
45
|
+
versions <= 2.3.2. This has been fixed.
|
46
|
+
|
47
|
+
2.2.2 2010-02-04
|
48
|
+
================
|
49
|
+
|
50
|
+
Minor enhancements
|
51
|
+
------------------
|
52
|
+
|
53
|
+
* Slightly smarter transitive closure maintenance: if a link was saved but
|
54
|
+
neither its ancestor nor descendant changed, the transitive closure will not
|
55
|
+
be recalculated.
|
56
|
+
* Removed usage of deprecated `require 'activesupport'`.
|
57
|
+
|
58
|
+
2.2.1 2009-12-07
|
59
|
+
================
|
60
|
+
|
61
|
+
Minor enhancements
|
62
|
+
------------------
|
63
|
+
|
64
|
+
* Added NUBIC provenance.
|
65
|
+
|
66
|
+
2.2.0 2009-09-14
|
67
|
+
================
|
68
|
+
|
69
|
+
Minor enhancements
|
70
|
+
------------------
|
71
|
+
|
72
|
+
* Commonized models used in dagnabit's tests.
|
73
|
+
* Switched to named scopes to implement Link#linking and TransitiveClosureLink#linking.
|
74
|
+
|
75
|
+
2.1.0 2009-09-04
|
76
|
+
================
|
77
|
+
|
78
|
+
Bugfixes
|
79
|
+
--------
|
80
|
+
|
81
|
+
* Fixed overzealous on-destroy transitive closure maintenance in situations like these:
|
82
|
+
|
83
|
+
<pre>
|
84
|
+
n1 -> n2 -> n3
|
85
|
+
|__________|
|
86
|
+
</pre>
|
87
|
+
|
88
|
+
Previously, deleting the edges (n1, n2) and (n2, n3) would also delete the
|
89
|
+
edge (n1, n3).
|
90
|
+
|
91
|
+
2.0.0 2009-08-17
|
92
|
+
================
|
93
|
+
|
94
|
+
Major changes
|
95
|
+
-------------
|
96
|
+
|
97
|
+
* Link models now validate their ancestor and descendant. This means that
|
98
|
+
Link#connect will fail if a link cannot be made due to node validation
|
99
|
+
failures.
|
100
|
+
|
101
|
+
Unfortunately, this also means you can no longer write (say)
|
102
|
+
`node1.links_as_parent.build(:descendant => node2)` or
|
103
|
+
`node2.links_as_child.build(:ancestor => node1)`.
|
104
|
+
|
105
|
+
See commit `61d6e3841b63fdcaad7fcecd05b24f4b9f217ba2` for more details.
|
data/LICENSE
CHANGED
data/MIGRATION.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
Migrating from dagnabit 2 to dagnabit 3
|
2
|
+
=======================================
|
3
|
+
|
4
|
+
1. Polymorphic associations are no longer supported. If you need a polymorphic
|
5
|
+
graph, convert your data model to use single-table inheritance.
|
6
|
+
|
7
|
+
2. The `*_transitive_closure` tables are no longer used, and can be dropped.
|
8
|
+
|
9
|
+
3. dagnabit is no longer mixed into `ActiveRecord::Base` by default. You need
|
10
|
+
to augment the models you want to use as vertices:
|
11
|
+
|
12
|
+
class Vertex < ActiveRecord::Base
|
13
|
+
extend Dagnabit::Vertex::Activation
|
14
|
+
|
15
|
+
acts_as_vertex
|
16
|
+
end
|
17
|
+
|
18
|
+
4. The default name of the edge table is now `edges`, and the name of the vertex
|
19
|
+
table is taken from the model.
|
20
|
+
|
21
|
+
So, for example, if you had the following models:
|
22
|
+
|
23
|
+
class Vertex < ActiveRecord::Base
|
24
|
+
extend Dagnabit::Vertex::Activation
|
25
|
+
|
26
|
+
acts_as_vertex
|
27
|
+
end
|
28
|
+
|
29
|
+
class BetaVertex < Vertex
|
30
|
+
end
|
31
|
+
|
32
|
+
class DifferentGraphVertex < ActiveRecord::Base
|
33
|
+
extend Dagnabit::Vertex::Activation
|
34
|
+
|
35
|
+
acts_as_vertex
|
36
|
+
set_edge_table 'different_edges'
|
37
|
+
end
|
38
|
+
|
39
|
+
then `Vertex` and `BetaVertex` would be stored in the `vertices` table and use
|
40
|
+
the `edges` table, and `DifferentGraphVertex` instances would be created from rows
|
41
|
+
in the `different_graph_vertices` table and be linked with edges from the
|
42
|
+
`different_edges` table.
|
43
|
+
|
44
|
+
5. The default edge column names have changed. `ancestor_id` is now `parent_id`
|
45
|
+
and `descendant_id` is now `child_id`. `ancestor_type` and `descendant_type`
|
46
|
+
are now unused.
|
47
|
+
|
48
|
+
6. Technically, you no longer need an edge model to use dagnabit. It can,
|
49
|
+
however, occasionally be useful. To make an edge model, create a model
|
50
|
+
resembling
|
51
|
+
|
52
|
+
class Edge < ActiveRecord::Base
|
53
|
+
extend Dagnabit::Edge::Activation
|
54
|
+
|
55
|
+
acts_as_edge
|
56
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,222 @@
|
|
1
|
+
So I lied
|
2
|
+
=========
|
3
|
+
|
4
|
+
dagnabit is actually still alive. I still think you should be using something
|
5
|
+
like Sequel if you really want to do graphs in a SQL database, but if you're
|
6
|
+
using ActiveRecord for whatever reason, dagnabit might still be useful.
|
7
|
+
|
8
|
+
Version 3 is a rework of dagnabit as a PostgreSQL-specific ActiveRecord plugin.
|
9
|
+
It's blatantly incompatible with the 2.x series of dagnabit. There are some
|
10
|
+
migration notes in MIGRATION.md.
|
11
|
+
|
12
|
+
dagnabit
|
13
|
+
========
|
14
|
+
|
15
|
+
dagnabit is (yet another) ActiveRecord plugin for directed acyclic graphs. It
|
16
|
+
stores directed acyclic graphs as an adjacency list, using recursive common
|
17
|
+
table expressions to perform fast reachability queries.
|
18
|
+
|
19
|
+
dagnabit was developed at the [Northwestern University Biomedical Informatics
|
20
|
+
Center](http://www.nucats.northwestern.edu/centers/nubic/index.html) for a Ruby
|
21
|
+
on Rails application designed for management and querying of
|
22
|
+
large-and-rapidly-growing biospecimen banks (i.e. for cancer research). The
|
23
|
+
application uses directed acyclic graphs for knowledge representation (storage
|
24
|
+
and application of workflows) and representing biospecimen heritage.
|
25
|
+
|
26
|
+
dagnabit is hosted at Gitorious and Github:
|
27
|
+
|
28
|
+
* <http://gitorious.org/dagnabit/dagnabit>
|
29
|
+
* <http://github.com/yipdw/dagnabit>
|
30
|
+
|
31
|
+
Installation
|
32
|
+
============
|
33
|
+
|
34
|
+
gem install dagnabit
|
35
|
+
|
36
|
+
Related work
|
37
|
+
============
|
38
|
+
|
39
|
+
This plugin was inspired by [Matthew Leventi's acts-as-dag
|
40
|
+
plugin](http://github.com/mleventi/acts-as-dag/tree/master). Indeed, Leventi's
|
41
|
+
acts-as-dag plugin was originally used in the application from which this plugin
|
42
|
+
was extracted.
|
43
|
+
|
44
|
+
The primary differences between dagnabit and acts-as-dag are:
|
45
|
+
|
46
|
+
* dagnabit does not maintain a separate transitive closure table, which speeds
|
47
|
+
up insertion.
|
48
|
+
|
49
|
+
* acts-as-dag does not permit linking of unpersisted nodes, i.e.
|
50
|
+
|
51
|
+
n1 = Vertex.new
|
52
|
+
n2 = Vertex.new
|
53
|
+
e1 = Edge.new(:parent => n1, :child => n2)
|
54
|
+
... other code ...
|
55
|
+
|
56
|
+
[n1, n2, e1].map { |o| o.save }
|
57
|
+
|
58
|
+
With acts-as-dag, one must save the nodes _before_ creating the edge.
|
59
|
+
The above code segment works in dagnabit.
|
60
|
+
|
61
|
+
Database compatibility
|
62
|
+
======================
|
63
|
+
|
64
|
+
PostgreSQL. That's all I know that'll work with dagnabit, anyway.
|
65
|
+
|
66
|
+
It's possible other SQL databases will work, but I have no tests to demonstrate
|
67
|
+
that situation.
|
68
|
+
|
69
|
+
Using dagnabit
|
70
|
+
==============
|
71
|
+
|
72
|
+
Database schema
|
73
|
+
---------------
|
74
|
+
|
75
|
+
You'll need at least one table for storing your graph's vertices. Additionally,
|
76
|
+
for every vertex table you create, you will need one edge table.
|
77
|
+
|
78
|
+
Polymorphic vertices are supported via single table inheritance.
|
79
|
+
|
80
|
+
Here's an example schema written as an ActiveRecord schema definition:
|
81
|
+
|
82
|
+
create_table :vertices do |t|
|
83
|
+
t.integer :ordinal
|
84
|
+
t.string :type # only if you're using STI
|
85
|
+
end
|
86
|
+
|
87
|
+
create_table :edges do |t|
|
88
|
+
t.references :parent, :null => false
|
89
|
+
t.references :child, :null => false
|
90
|
+
end
|
91
|
+
|
92
|
+
add_index :edges, [:parent_id, :child_id], :unique => true
|
93
|
+
|
94
|
+
Maintaining a directed acyclic simple graph and some words on validation
|
95
|
+
------------------------------------------------------------------------
|
96
|
+
|
97
|
+
dagnabit is designed to operate on directed, acyclic, simple graphs. That means:
|
98
|
+
|
99
|
+
1. each edge has a direction (_directed_),
|
100
|
+
2. there cannot exist any edges that create a cycle (_acyclic_),
|
101
|
+
3. any edge must connect two and only two vertices (no _hyperedges_), and
|
102
|
+
4. there may be only zero or one edges connecting any two vertices (_simple_).
|
103
|
+
|
104
|
+
dagnabit is set up to make the database enforce these invariants:
|
105
|
+
|
106
|
+
1. The directionality of each edge is implicit in the edge table structure from
|
107
|
+
parent to child.
|
108
|
+
2. dagnabit provides a PL/pgSQL trigger that you can use to abort saving edges
|
109
|
+
that, when inserted, causes a cycle. More on this below.
|
110
|
+
3. As each edge may only address one parent and one child, the maximum of two
|
111
|
+
vertices is guaranteed. `NOT NULL` constraints on the `parent_id` and
|
112
|
+
`child_id` columns guarantee the minimum of two. (It is recommended that you
|
113
|
+
also set up foreign key constraints from edges to vertices, though that
|
114
|
+
addresses a different issue.)
|
115
|
+
4. The `(parent_id, child_id)` index prevents multiple edges connecting the same
|
116
|
+
vertices.
|
117
|
+
|
118
|
+
You may, of course, relax invariants 2-4 by omitting indices or constraints;
|
119
|
+
however, if you do that, you risk problems such as
|
120
|
+
{Dagnabit::Vertex::Connectivity} methods generating infinite loops.
|
121
|
+
|
122
|
+
Note that dagnabit currently does not provide a way to catch data that would
|
123
|
+
violate these invariants via ActiveRecord's validation subsystem. Violations,
|
124
|
+
therefore, will result in quite nasty (from the ActiveRecord perspective of
|
125
|
+
things) `PGError`s.
|
126
|
+
|
127
|
+
If you design your application code such that these invariants cannot be
|
128
|
+
violated via typical user actions, then this is probably fine, because in that
|
129
|
+
case the `PGError` exception (which, in this case, you probably don't want to
|
130
|
+
handle) indicates either the existence of an error in the code and/or malicious
|
131
|
+
activity that was prevented.
|
132
|
+
|
133
|
+
On the other hand, if users of your application will be building dags as part of
|
134
|
+
their interactions with your application, then it is a very real possibility
|
135
|
+
that violation of the above invarints may occur in the course of normal user
|
136
|
+
activity. In this case, you must trap these errors and provide adequate
|
137
|
+
feedback to your users so that they can correct the problem. This, like many UI
|
138
|
+
problems, is not a trivial problem to solve, and I do not have any general
|
139
|
+
solution for it.
|
140
|
+
|
141
|
+
dagnabit's cycle-checking trigger
|
142
|
+
---------------------------------
|
143
|
+
|
144
|
+
dagnabit ships with a PL/pgSQL trigger that can be installed on edge tables.
|
145
|
+
The trigger algorithm is run per inserted or updated row, and may be described
|
146
|
+
as
|
147
|
+
|
148
|
+
trigger check_cycle(seen = [], edge = (a, b)):
|
149
|
+
if a != b
|
150
|
+
if b has no children
|
151
|
+
ok
|
152
|
+
else
|
153
|
+
for each child c of b
|
154
|
+
check_cycle(seen + [b], (a, c))
|
155
|
+
end
|
156
|
+
else
|
157
|
+
abort
|
158
|
+
end
|
159
|
+
|
160
|
+
The implementation uses a `WITH RECURSIVE` query.
|
161
|
+
|
162
|
+
The {Dagnabit::Migration} module provides methods for creating and dropping
|
163
|
+
triggers on edge tables:
|
164
|
+
|
165
|
+
class CreateEdges < ActiveRecord::Migration
|
166
|
+
extend Dagnabit::Migration
|
167
|
+
|
168
|
+
def self.up
|
169
|
+
create_table :edges do |t|
|
170
|
+
...
|
171
|
+
end
|
172
|
+
|
173
|
+
create_cycle_check_trigger :edges
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.down
|
177
|
+
drop_cycle_check_trigger :edges
|
178
|
+
|
179
|
+
drop_table :edges
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
Using dagnabit in your application
|
184
|
+
----------------------------------
|
185
|
+
|
186
|
+
dagnabit is activated on vertex and edge models by extending vertex and edge
|
187
|
+
classes with {Dagnabit::Vertex::Activation} and {Dagnabit::Edge::Activation},
|
188
|
+
respectively:
|
189
|
+
|
190
|
+
class Vertex < ActiveRecord::Base
|
191
|
+
extend Dagnabit::Vertex::Activation
|
192
|
+
|
193
|
+
acts_as_vertex
|
194
|
+
connected_by 'Edge'
|
195
|
+
end
|
196
|
+
|
197
|
+
class Edge < ActiveRecord::Base
|
198
|
+
extend Dagnabit::Edge::Activation
|
199
|
+
|
200
|
+
acts_as_edge
|
201
|
+
connects 'Vertex'
|
202
|
+
end
|
203
|
+
|
204
|
+
By default, the vertex connectivity queries expect the edge table to be called
|
205
|
+
"edges", but that is by no means required. Just associate the vertex with a
|
206
|
+
different edge model:
|
207
|
+
|
208
|
+
class OtherVertex < ActiveRecord::Base
|
209
|
+
extend Dagnabit::Vertex::Activation
|
210
|
+
|
211
|
+
acts_as_vertex
|
212
|
+
connected_by 'OtherEdge'
|
213
|
+
end
|
214
|
+
|
215
|
+
For further information, see the library API documentation. Also see the
|
216
|
+
listing of the `dagnabit-test` program.
|
217
|
+
|
218
|
+
Copyright
|
219
|
+
=========
|
220
|
+
|
221
|
+
Copyright (c) 2009, 2010 David Yip. Released under the MIT License; see
|
222
|
+
LICENSE for details.
|
data/bin/dagnabit-test
CHANGED
@@ -1,46 +1,26 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
# ActiveRecord models.
|
6
|
-
#
|
7
|
-
#
|
8
|
-
require 'rubygems'
|
9
|
-
require 'bundler'
|
10
|
-
Bundler.setup
|
3
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), %w(.. lib)))
|
4
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), %w(.. db)))
|
11
5
|
|
12
6
|
require 'active_record'
|
13
|
-
require 'sqlite3'
|
14
7
|
require 'irb'
|
15
8
|
|
16
|
-
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), %w(.. lib)))
|
17
|
-
|
18
9
|
require 'dagnabit'
|
19
10
|
|
11
|
+
require 'connection'
|
20
12
|
ActiveRecord::Base.logger = Logger.new("#{File.basename(__FILE__)}.log")
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
create_table table do |t|
|
26
|
-
t.integer :ancestor_id
|
27
|
-
t.integer :descendant_id
|
28
|
-
t.string :ancestor_type
|
29
|
-
t.string :descendant_type
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
create_table :nodes do |t|
|
34
|
-
t.string :data
|
35
|
-
end
|
36
|
-
end
|
13
|
+
require 'schema'
|
14
|
+
|
15
|
+
require 'models/edge'
|
16
|
+
require 'models/vertex'
|
37
17
|
|
38
|
-
|
39
|
-
|
18
|
+
def v(datum)
|
19
|
+
Vertex.find_or_create_by_datum(datum)
|
40
20
|
end
|
41
21
|
|
42
|
-
|
43
|
-
|
22
|
+
def e(a, b)
|
23
|
+
Edge.create(:parent => a, :child => b)
|
44
24
|
end
|
45
25
|
|
46
26
|
IRB.start
|