dagnabit 2.2.6 → 3.0.0
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/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
|