dagnabit 2.2.6 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. data/History.md +105 -0
  2. data/LICENSE +1 -1
  3. data/MIGRATION.md +56 -0
  4. data/README.md +222 -0
  5. data/bin/dagnabit-test +11 -31
  6. data/db/connection.rb +3 -0
  7. data/{test/connections/native_postgresql → db/connections/postgresql}/connection.rb +6 -5
  8. data/db/models/edge.rb +6 -0
  9. data/db/models/other_edge.rb +6 -0
  10. data/db/models/other_vertex.rb +6 -0
  11. data/db/models/vertex.rb +6 -0
  12. data/db/schema.rb +32 -0
  13. data/lib/dagnabit.rb +6 -19
  14. data/lib/dagnabit/edge.rb +7 -0
  15. data/lib/dagnabit/edge/activation.rb +14 -0
  16. data/lib/dagnabit/edge/associations.rb +10 -0
  17. data/lib/dagnabit/edge/connectivity.rb +38 -0
  18. data/lib/dagnabit/graph.rb +143 -0
  19. data/lib/dagnabit/migration.rb +98 -0
  20. data/lib/dagnabit/version.rb +3 -0
  21. data/lib/dagnabit/vertex.rb +10 -0
  22. data/lib/dagnabit/vertex/activation.rb +19 -0
  23. data/lib/dagnabit/vertex/associations.rb +24 -0
  24. data/lib/dagnabit/vertex/bonding.rb +43 -0
  25. data/lib/dagnabit/vertex/connectivity.rb +130 -0
  26. data/lib/dagnabit/vertex/neighbors.rb +50 -0
  27. data/lib/dagnabit/vertex/settings.rb +56 -0
  28. metadata +94 -143
  29. data/.autotest +0 -5
  30. data/.document +0 -5
  31. data/.gitignore +0 -7
  32. data/Gemfile +0 -15
  33. data/Gemfile.lock +0 -38
  34. data/History.txt +0 -81
  35. data/README.rdoc +0 -202
  36. data/Rakefile +0 -52
  37. data/VERSION.yml +0 -5
  38. data/dagnabit.gemspec +0 -142
  39. data/init.rb +0 -1
  40. data/lib/dagnabit/activation.rb +0 -60
  41. data/lib/dagnabit/link/associations.rb +0 -18
  42. data/lib/dagnabit/link/class_methods.rb +0 -43
  43. data/lib/dagnabit/link/configuration.rb +0 -40
  44. data/lib/dagnabit/link/cycle_prevention.rb +0 -31
  45. data/lib/dagnabit/link/named_scopes.rb +0 -65
  46. data/lib/dagnabit/link/transitive_closure_link_model.rb +0 -86
  47. data/lib/dagnabit/link/transitive_closure_recalculation.rb +0 -17
  48. data/lib/dagnabit/link/transitive_closure_recalculation/on_create.rb +0 -104
  49. data/lib/dagnabit/link/transitive_closure_recalculation/on_destroy.rb +0 -125
  50. data/lib/dagnabit/link/transitive_closure_recalculation/on_update.rb +0 -17
  51. data/lib/dagnabit/link/transitive_closure_recalculation/utilities.rb +0 -56
  52. data/lib/dagnabit/link/validations.rb +0 -26
  53. data/lib/dagnabit/node/associations.rb +0 -84
  54. data/lib/dagnabit/node/class_methods.rb +0 -74
  55. data/lib/dagnabit/node/configuration.rb +0 -26
  56. data/lib/dagnabit/node/neighbors.rb +0 -73
  57. data/test/connections/native_sqlite3/connection.rb +0 -24
  58. data/test/dagnabit/link/test_associations.rb +0 -61
  59. data/test/dagnabit/link/test_class_methods.rb +0 -102
  60. data/test/dagnabit/link/test_configuration.rb +0 -38
  61. data/test/dagnabit/link/test_cycle_prevention.rb +0 -64
  62. data/test/dagnabit/link/test_named_scopes.rb +0 -32
  63. data/test/dagnabit/link/test_transitive_closure_link_model.rb +0 -69
  64. data/test/dagnabit/link/test_transitive_closure_recalculation.rb +0 -178
  65. data/test/dagnabit/link/test_validations.rb +0 -39
  66. data/test/dagnabit/node/test_associations.rb +0 -147
  67. data/test/dagnabit/node/test_class_methods.rb +0 -49
  68. data/test/dagnabit/node/test_configuration.rb +0 -29
  69. data/test/dagnabit/node/test_neighbors.rb +0 -91
  70. data/test/helper.rb +0 -26
  71. data/test/models/beta_node.rb +0 -3
  72. data/test/models/custom_data_link.rb +0 -4
  73. data/test/models/customized_link.rb +0 -7
  74. data/test/models/customized_link_node.rb +0 -4
  75. data/test/models/link.rb +0 -4
  76. data/test/models/node.rb +0 -3
  77. data/test/schema/schema.rb +0 -51
@@ -0,0 +1,3 @@
1
+ database = ENV['DATABASE'] || 'postgresql'
2
+
3
+ require File.join(File.dirname(__FILE__), %W(connections #{database} connection))
@@ -1,8 +1,9 @@
1
- print "Using native PostgreSQL\n"
2
-
1
+ require 'active_record'
3
2
  require 'logger'
4
3
 
5
- ActiveRecord::Base.logger = Logger.new("debug.log")
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
- :hostname => 'localhost'
14
+ :host => 'localhost'
14
15
  }
15
- };
16
+ }
16
17
 
17
18
  ActiveRecord::Base.establish_connection('ActiveRecord::Base')
@@ -0,0 +1,6 @@
1
+ class Edge < ActiveRecord::Base
2
+ extend Dagnabit::Edge::Activation
3
+
4
+ acts_as_edge
5
+ connects 'Vertex'
6
+ end
@@ -0,0 +1,6 @@
1
+ class OtherEdge < ActiveRecord::Base
2
+ extend Dagnabit::Edge::Activation
3
+
4
+ acts_as_edge
5
+ connects 'OtherVertex'
6
+ end
@@ -0,0 +1,6 @@
1
+ class OtherVertex < ActiveRecord::Base
2
+ extend Dagnabit::Vertex::Activation
3
+
4
+ acts_as_vertex
5
+ connected_by 'OtherEdge'
6
+ end
@@ -0,0 +1,6 @@
1
+ class Vertex < ActiveRecord::Base
2
+ extend Dagnabit::Vertex::Activation
3
+
4
+ acts_as_vertex
5
+ connected_by 'Edge'
6
+ end
@@ -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
@@ -1,19 +1,6 @@
1
- require 'active_record'
2
-
3
- require 'dagnabit/link/configuration'
4
- require 'dagnabit/link/validations'
5
- require 'dagnabit/link/associations'
6
- require 'dagnabit/link/class_methods'
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,7 @@
1
+ require File.join(File.dirname(__FILE__), %w(.. dagnabit))
2
+
3
+ module Dagnabit::Edge
4
+ autoload :Activation, 'dagnabit/edge/activation'
5
+ autoload :Associations, 'dagnabit/edge/associations'
6
+ autoload :Connectivity, 'dagnabit/edge/connectivity'
7
+ 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,10 @@
1
+ require File.join(File.dirname(__FILE__), %w(.. edge))
2
+
3
+ module Dagnabit::Edge
4
+ module Associations
5
+ def connects(vertex_class)
6
+ belongs_to :parent, :class_name => vertex_class
7
+ belongs_to :child, :class_name => vertex_class
8
+ end
9
+ end
10
+ 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,3 @@
1
+ module Dagnabit
2
+ VERSION = '3.0.0'
3
+ 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