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/db/connection.rb
ADDED
@@ -1,8 +1,9 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require 'active_record'
|
3
2
|
require 'logger'
|
4
3
|
|
5
|
-
|
4
|
+
puts 'Using PostgreSQL adapter'
|
5
|
+
|
6
|
+
ActiveRecord::Base.logger = Logger.new('debug.log')
|
6
7
|
|
7
8
|
ActiveRecord::Base.configurations = {
|
8
9
|
'ActiveRecord::Base' => {
|
@@ -10,8 +11,8 @@ ActiveRecord::Base.configurations = {
|
|
10
11
|
:database => 'dagnabit_test',
|
11
12
|
:username => 'dagnabit_test',
|
12
13
|
:password => 'dagnabit_test',
|
13
|
-
:
|
14
|
+
:host => 'localhost'
|
14
15
|
}
|
15
|
-
}
|
16
|
+
}
|
16
17
|
|
17
18
|
ActiveRecord::Base.establish_connection('ActiveRecord::Base')
|
data/db/models/edge.rb
ADDED
data/db/models/vertex.rb
ADDED
data/db/schema.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
extend Dagnabit::Migration
|
3
|
+
|
4
|
+
execute 'DROP LANGUAGE IF EXISTS plpgsql CASCADE'
|
5
|
+
execute 'CREATE TRUSTED LANGUAGE plpgsql'
|
6
|
+
|
7
|
+
[:edges, :other_edges].each do |table|
|
8
|
+
create_table table, :force => true do |t|
|
9
|
+
t.references :parent, :null => false
|
10
|
+
t.references :child, :null => false
|
11
|
+
end
|
12
|
+
|
13
|
+
add_index table, [:parent_id, :child_id], :unique => true
|
14
|
+
|
15
|
+
create_cycle_check_trigger table
|
16
|
+
end
|
17
|
+
|
18
|
+
create_table :vertices, :force => true do |t|
|
19
|
+
t.integer :datum
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table :other_vertices, :force => true do |t|
|
23
|
+
t.integer :datum
|
24
|
+
end
|
25
|
+
|
26
|
+
[ [:vertices, :edges],
|
27
|
+
[:other_vertices, :other_edges]
|
28
|
+
].each do |vt, et|
|
29
|
+
execute %Q{ALTER TABLE #{et} ADD CONSTRAINT FK_#{et}_parent_id_#{vt}_id FOREIGN KEY (parent_id) REFERENCES #{vt} ("id") MATCH FULL}
|
30
|
+
execute %Q{ALTER TABLE #{et} ADD CONSTRAINT FK_#{et}_child_id_#{vt}_id FOREIGN KEY (child_id) REFERENCES #{vt} ("id") MATCH FULL}
|
31
|
+
end
|
32
|
+
end
|
data/lib/dagnabit.rb
CHANGED
@@ -1,19 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
require 'dagnabit/link/cycle_prevention'
|
8
|
-
require 'dagnabit/link/named_scopes'
|
9
|
-
require 'dagnabit/link/transitive_closure_recalculation'
|
10
|
-
require 'dagnabit/link/transitive_closure_link_model'
|
11
|
-
|
12
|
-
require 'dagnabit/node/configuration'
|
13
|
-
require 'dagnabit/node/class_methods'
|
14
|
-
require 'dagnabit/node/associations'
|
15
|
-
require 'dagnabit/node/neighbors'
|
16
|
-
|
17
|
-
require 'dagnabit/activation'
|
18
|
-
|
19
|
-
ActiveRecord::Base.extend(Dagnabit::Activation)
|
1
|
+
module Dagnabit
|
2
|
+
autoload :Edge, 'dagnabit/edge'
|
3
|
+
autoload :Graph, 'dagnabit/graph'
|
4
|
+
autoload :Migration, 'dagnabit/migration'
|
5
|
+
autoload :Vertex, 'dagnabit/vertex'
|
6
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w(.. edge))
|
2
|
+
|
3
|
+
module Dagnabit::Edge
|
4
|
+
##
|
5
|
+
# This module provides a method to set up all of the Edge modules in a class.
|
6
|
+
module Activation
|
7
|
+
##
|
8
|
+
# Sets up dagnabit's edge modules in an edge class.
|
9
|
+
def acts_as_edge
|
10
|
+
extend Associations
|
11
|
+
extend Connectivity
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w(.. edge))
|
2
|
+
|
3
|
+
module Dagnabit::Edge
|
4
|
+
##
|
5
|
+
# Methods for querying connectivity of edges.
|
6
|
+
module Connectivity
|
7
|
+
##
|
8
|
+
# Finds all edges connecting the given vertices.
|
9
|
+
#
|
10
|
+
# More specifically, finds all edges such that the edge's parent and child
|
11
|
+
# is one of the given vertices.
|
12
|
+
#
|
13
|
+
# This means that should vertices belong to disjoint subgraphs be selected,
|
14
|
+
# e.g.
|
15
|
+
#
|
16
|
+
# (a) (d)
|
17
|
+
# | |
|
18
|
+
# (b) e
|
19
|
+
# | |
|
20
|
+
# (c) f
|
21
|
+
#
|
22
|
+
# where () denotes a selected vertex, then only the edges for which both
|
23
|
+
# the parent and child vertices are present will be in the result set. In
|
24
|
+
# the above example, this means that while the a->b and b->c edges will be
|
25
|
+
# present, the d->e edge will not. To include the d->e edge, e would have
|
26
|
+
# to be in the vertex list.
|
27
|
+
#
|
28
|
+
# @param [Array<ActiveRecord::Base>] vertices the vertices to consider
|
29
|
+
# @return [Array<ActiveRecord::Base>] a list of edges
|
30
|
+
def connecting(*vertices)
|
31
|
+
ids = vertices.map(&:id)
|
32
|
+
|
33
|
+
find_by_sql([%Q{
|
34
|
+
SELECT * FROM #{table_name} WHERE parent_id IN (:ids) AND child_id IN (:ids)
|
35
|
+
}, { :ids => ids }])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w(.. dagnabit))
|
2
|
+
|
3
|
+
module Dagnabit
|
4
|
+
##
|
5
|
+
# This class is a representation of a directed graph. It deviates from the
|
6
|
+
# definition of a directed graph in a few ways; here's a couple:
|
7
|
+
#
|
8
|
+
# * It represents the vertex and edge sets using data structures more akin to
|
9
|
+
# bags than sets.
|
10
|
+
# * It imposes restrictions on the form of vertices and edges; namely, they
|
11
|
+
# must be ActiveRecord::Base subclasses that extend {Vertex::Connectivity}.
|
12
|
+
#
|
13
|
+
# Despite these flaws, it is hoped that {Graph} is still useful for
|
14
|
+
# performing queries on graphs built with dagnabit.
|
15
|
+
#
|
16
|
+
# {Graph} is not compatible with RGL's Graph concept. This is because
|
17
|
+
# {Graph} defines {#vertices} and {#edges} as accessors, whereas RGL provides
|
18
|
+
# implementations of {#vertices} and {#edges} in terms of methods called
|
19
|
+
# `each_vertex` and `each_edge`, respectively. Nevertheless, RGL
|
20
|
+
# compatibility would be nice to have, so it is a to-do item.
|
21
|
+
#
|
22
|
+
# Graphs may be constructed from a set of source nodes with {.from_vertices}.
|
23
|
+
class Graph
|
24
|
+
##
|
25
|
+
# The vertices of this graph.
|
26
|
+
#
|
27
|
+
# @return [Array<ActiveRecord::Base>]
|
28
|
+
attr_accessor :vertices
|
29
|
+
|
30
|
+
##
|
31
|
+
# The edges of this graph.
|
32
|
+
#
|
33
|
+
# @return [Array<ActiveRecord::Base>]
|
34
|
+
attr_accessor :edges
|
35
|
+
|
36
|
+
##
|
37
|
+
# The vertex model used by this graph.
|
38
|
+
#
|
39
|
+
# This must be defined before calling {#load_descendants!}.
|
40
|
+
#
|
41
|
+
# @return [Class]
|
42
|
+
attr_accessor :vertex_model
|
43
|
+
|
44
|
+
##
|
45
|
+
# The vertex model used by this graph.
|
46
|
+
#
|
47
|
+
# This must be defined before calling {#load_descendants!}.
|
48
|
+
#
|
49
|
+
# @return [Class]
|
50
|
+
attr_accessor :edge_model
|
51
|
+
|
52
|
+
##
|
53
|
+
# Given a set of `vertices`, builds a subgraph from those vertices and their
|
54
|
+
# descendants.
|
55
|
+
#
|
56
|
+
# This method is intended to be used in the case where `vertices` contains
|
57
|
+
# only source vertices (vertices having indegree zero), but can be used with
|
58
|
+
# non-source vertices as well.
|
59
|
+
#
|
60
|
+
# If your vertices may be one of many subclasses (i.e. you're using single
|
61
|
+
# table inheritance in your vertices table), then you should use the base
|
62
|
+
# class for {#vertex_model}.
|
63
|
+
#
|
64
|
+
# @param [Array<ActiveRecord::Base>] vertices the vertices to start from
|
65
|
+
# @param [Class] vertex_model the model class used for representing vertices
|
66
|
+
# @param [Class] edge_model the model class used for representing edges
|
67
|
+
# @return [Graph] a subgraph
|
68
|
+
def self.from_vertices(vertices, vertex_model, edge_model)
|
69
|
+
new.tap do |g|
|
70
|
+
g.vertex_model = vertex_model
|
71
|
+
g.edge_model = edge_model
|
72
|
+
g.vertices = vertices
|
73
|
+
g.load_descendants!
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def initialize
|
78
|
+
self.vertices = []
|
79
|
+
self.edges = []
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Loads all descendants of the vertices in {#vertices}.
|
84
|
+
#
|
85
|
+
# {#vertex_model} and {#edge_model} must be set before calling this method.
|
86
|
+
# If either are not set, this method raises `RuntimeError`.
|
87
|
+
#
|
88
|
+
# Once vertices are loaded, load_descendants! loads all edges that connect
|
89
|
+
# vertices in the working vertex set.
|
90
|
+
#
|
91
|
+
# Vertices and edges that were present before a load_descendants! call will
|
92
|
+
# remain in {#vertices} and {#edges}, respectively.
|
93
|
+
#
|
94
|
+
# @raise [RuntimeError] if {#vertex_model} or {#edge_model} are unset
|
95
|
+
def load_descendants!
|
96
|
+
raise 'vertex_model and edge_model must be set' unless vertex_model && edge_model
|
97
|
+
|
98
|
+
self.vertices += vertex_model.descendants_of(*vertices)
|
99
|
+
self.edges += edge_model.connecting(*vertices)
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# Returns the source vertices in the graph. Sources are not returned in any
|
104
|
+
# particular order.
|
105
|
+
#
|
106
|
+
# This method is implemented using a hash anti-join on vertex ID and edge
|
107
|
+
# child ID. If a vertex is a child of some edge, then it is rejected, and
|
108
|
+
# thus only vertices that are not children of any edge (viz. have indegree
|
109
|
+
# zero and are therefore sources) remain.
|
110
|
+
#
|
111
|
+
# If this method is run on a subgraph of a larger graph, then the source
|
112
|
+
# determination is done relative to the subgraph. Consider the following
|
113
|
+
# graph:
|
114
|
+
#
|
115
|
+
# a b
|
116
|
+
# \ /
|
117
|
+
# c
|
118
|
+
# |
|
119
|
+
# d
|
120
|
+
#
|
121
|
+
# When considering the vertex set `{ a, b, c, d }` and corresponding edge
|
122
|
+
# set, `c` is clearly not a source. However, in the subgraph `S`
|
123
|
+
#
|
124
|
+
# S = ({c, d}, {(c, d)})
|
125
|
+
#
|
126
|
+
# `c` _is_ a source, and if S were reified as a {Graph} instance (using, for
|
127
|
+
# example, {.from_vertices}), `[c]` would be the output of calling `sources`
|
128
|
+
# on that instance.
|
129
|
+
#
|
130
|
+
# This method expects all vertices and edges to have been persisted, and
|
131
|
+
# will return incorrect results if there exist unpersisted vertices or edges
|
132
|
+
# in the graph. For this reason, it is advised that you ensure that all
|
133
|
+
# vertices and edges in a graph are saved before calling {#sources}.
|
134
|
+
#
|
135
|
+
# @return [Array<vertex_model>] an array of source vertices
|
136
|
+
def sources
|
137
|
+
ids = vertices.inject({}) { |r, v| r.update(v.id => v) }
|
138
|
+
children = edges.inject({}) { |r, e| r.update(e.child_id => true) }
|
139
|
+
|
140
|
+
ids.reject { |k, _| children[k] }.map { |_, v| v }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w(.. dagnabit))
|
2
|
+
|
3
|
+
module Dagnabit
|
4
|
+
##
|
5
|
+
# Contains shorthand for setting up database triggers and constraints for
|
6
|
+
# maintaining dagnabit's invariants. See the README for more information.
|
7
|
+
#
|
8
|
+
# This module is intended to be extended by subclasses of
|
9
|
+
# ActiveRecord::Migration.
|
10
|
+
module Migration
|
11
|
+
##
|
12
|
+
# Instantiates a cycle check trigger on `edge_table`. The trigger is
|
13
|
+
# executed on a per row basis for every insert or update.
|
14
|
+
#
|
15
|
+
# @param [Symbol, String] edge_table the table for the trigger
|
16
|
+
# @return [void]
|
17
|
+
def create_cycle_check_trigger(edge_table)
|
18
|
+
create_cycle_check_function(edge_table)
|
19
|
+
|
20
|
+
execute %Q{
|
21
|
+
CREATE TRIGGER #{trigger_name} AFTER INSERT OR UPDATE ON #{edge_table}
|
22
|
+
FOR EACH ROW EXECUTE PROCEDURE #{function_name}_#{edge_table}();
|
23
|
+
}.strip
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Drops a trigger created by {#create_cycle_check_trigger}.
|
28
|
+
#
|
29
|
+
# @param [Symbol, String] edge_table the table owning the trigger
|
30
|
+
# @return [void]
|
31
|
+
def drop_cycle_check_trigger(edge_table)
|
32
|
+
execute %Q{
|
33
|
+
DROP TRIGGER #{trigger_name} ON #{edge_table};
|
34
|
+
}.strip
|
35
|
+
|
36
|
+
drop_cycle_check_function(edge_table)
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Builds a PL/pgSQL function for performing cycle checks.
|
41
|
+
#
|
42
|
+
# If the function already exists, then calling this method will overwrite
|
43
|
+
# it.
|
44
|
+
#
|
45
|
+
# A `CREATE TRUSTED LANGUAGE plpgsql` declaration must have been made in the
|
46
|
+
# database prior to invocation of {#create_cycle_check_function}.
|
47
|
+
#
|
48
|
+
# @param [Symbol, String] edge_table the table to check
|
49
|
+
# @return [void]
|
50
|
+
def create_cycle_check_function(edge_table)
|
51
|
+
execute %Q{
|
52
|
+
CREATE OR REPLACE FUNCTION #{function_name}_#{edge_table}() RETURNS trigger AS $#{function_name}$
|
53
|
+
DECLARE
|
54
|
+
cyclic bool;
|
55
|
+
BEGIN
|
56
|
+
WITH RECURSIVE cycles(id, path, cycle) AS (
|
57
|
+
SELECT e.child_id, ARRAY[]::integer[], false
|
58
|
+
FROM #{edge_table} e WHERE e.parent_id = NEW.parent_id
|
59
|
+
UNION ALL
|
60
|
+
SELECT e.child_id, c.path || c.id, c.id = ANY(c.path)
|
61
|
+
FROM cycles c INNER JOIN #{edge_table} e ON e.parent_id = c.id AND NOT cycle
|
62
|
+
)
|
63
|
+
SELECT true FROM cycles WHERE cycle = true INTO cyclic;
|
64
|
+
|
65
|
+
IF cyclic = true THEN
|
66
|
+
RAISE EXCEPTION 'Edge (%, %) introduces a cycle', NEW.child_id, NEW.parent_id;
|
67
|
+
END IF;
|
68
|
+
|
69
|
+
RETURN NULL;
|
70
|
+
END;
|
71
|
+
$#{function_name}$ LANGUAGE plpgsql;
|
72
|
+
}.strip
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Drops the function created by {#create_cycle_check_function}.
|
77
|
+
#
|
78
|
+
# It is safe to call this method if the function was not previously
|
79
|
+
# created.
|
80
|
+
#
|
81
|
+
# @return [void]
|
82
|
+
def drop_cycle_check_function(edge_table)
|
83
|
+
execute %Q{
|
84
|
+
DROP FUNCTION IF EXISTS #{function_name}_#{edge_table}();
|
85
|
+
}.strip
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def trigger_name
|
91
|
+
'dagnabit_cycle_check'
|
92
|
+
end
|
93
|
+
|
94
|
+
def function_name
|
95
|
+
'dagnabit_cycle_check'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w(.. dagnabit))
|
2
|
+
|
3
|
+
module Dagnabit::Vertex
|
4
|
+
autoload :Activation, 'dagnabit/vertex/activation'
|
5
|
+
autoload :Associations, 'dagnabit/vertex/associations'
|
6
|
+
autoload :Bonding, 'dagnabit/vertex/bonding'
|
7
|
+
autoload :Connectivity, 'dagnabit/vertex/connectivity'
|
8
|
+
autoload :Neighbors, 'dagnabit/vertex/neighbors'
|
9
|
+
autoload :Settings, 'dagnabit/vertex/settings'
|
10
|
+
end
|