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/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
|