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