dagnabit 2.2.1

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 (51) hide show
  1. data/.autotest +5 -0
  2. data/.document +5 -0
  3. data/.gitignore +6 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +186 -0
  6. data/Rakefile +69 -0
  7. data/VERSION.yml +5 -0
  8. data/bin/dagnabit-test +53 -0
  9. data/dagnabit.gemspec +124 -0
  10. data/init.rb +1 -0
  11. data/lib/dagnabit.rb +17 -0
  12. data/lib/dagnabit/activation.rb +60 -0
  13. data/lib/dagnabit/link/associations.rb +18 -0
  14. data/lib/dagnabit/link/class_methods.rb +43 -0
  15. data/lib/dagnabit/link/configuration.rb +40 -0
  16. data/lib/dagnabit/link/cycle_prevention.rb +31 -0
  17. data/lib/dagnabit/link/named_scopes.rb +65 -0
  18. data/lib/dagnabit/link/transitive_closure_link_model.rb +86 -0
  19. data/lib/dagnabit/link/transitive_closure_recalculation.rb +17 -0
  20. data/lib/dagnabit/link/transitive_closure_recalculation/on_create.rb +104 -0
  21. data/lib/dagnabit/link/transitive_closure_recalculation/on_destroy.rb +125 -0
  22. data/lib/dagnabit/link/transitive_closure_recalculation/on_update.rb +13 -0
  23. data/lib/dagnabit/link/transitive_closure_recalculation/utilities.rb +56 -0
  24. data/lib/dagnabit/link/validations.rb +26 -0
  25. data/lib/dagnabit/node/associations.rb +84 -0
  26. data/lib/dagnabit/node/class_methods.rb +74 -0
  27. data/lib/dagnabit/node/configuration.rb +26 -0
  28. data/lib/dagnabit/node/neighbors.rb +73 -0
  29. data/test/connections/native_postgresql/connection.rb +17 -0
  30. data/test/connections/native_sqlite3/connection.rb +24 -0
  31. data/test/dagnabit/link/test_associations.rb +61 -0
  32. data/test/dagnabit/link/test_class_methods.rb +102 -0
  33. data/test/dagnabit/link/test_configuration.rb +38 -0
  34. data/test/dagnabit/link/test_cycle_prevention.rb +64 -0
  35. data/test/dagnabit/link/test_named_scopes.rb +32 -0
  36. data/test/dagnabit/link/test_transitive_closure_link_model.rb +69 -0
  37. data/test/dagnabit/link/test_transitive_closure_recalculation.rb +139 -0
  38. data/test/dagnabit/link/test_validations.rb +39 -0
  39. data/test/dagnabit/node/test_associations.rb +147 -0
  40. data/test/dagnabit/node/test_class_methods.rb +49 -0
  41. data/test/dagnabit/node/test_configuration.rb +29 -0
  42. data/test/dagnabit/node/test_neighbors.rb +91 -0
  43. data/test/helper.rb +27 -0
  44. data/test/models/beta_node.rb +3 -0
  45. data/test/models/custom_data_link.rb +4 -0
  46. data/test/models/customized_link.rb +7 -0
  47. data/test/models/customized_link_node.rb +4 -0
  48. data/test/models/link.rb +4 -0
  49. data/test/models/node.rb +3 -0
  50. data/test/schema/schema.rb +51 -0
  51. metadata +165 -0
@@ -0,0 +1,125 @@
1
+ module Dagnabit
2
+ module Link
3
+ module TransitiveClosureRecalculation
4
+ module OnDestroy
5
+ def after_destroy
6
+ super
7
+ update_transitive_closure_for_destroy(*quoted_dag_link_values)
8
+ end
9
+
10
+ private
11
+
12
+ def update_transitive_closure_for_destroy(aid, did, atype, dtype)
13
+ my_table = self.class.quoted_table_name
14
+ my_aid, my_did, my_atype, my_dtype = quoted_dag_link_column_names
15
+ tc = self.class.transitive_closure_table_name
16
+ tc_aid, tc_did, tc_atype, tc_dtype = quoted_dag_link_column_names
17
+
18
+ with_temporary_edge_tables('suspect', 'trusty', 'new') do |suspect, trusty, new|
19
+ connection.execute <<-END
20
+ INSERT INTO #{suspect}
21
+ SELECT * FROM (
22
+ SELECT
23
+ TC1.#{tc_aid}, TC2.#{tc_did}, TC1.#{tc_atype}, TC2.#{tc_dtype}
24
+ FROM
25
+ #{tc} AS TC1, #{tc} AS TC2
26
+ WHERE
27
+ TC1.#{tc_did} = #{aid} AND TC2.#{tc_aid} = #{did}
28
+ AND
29
+ TC1.#{tc_dtype} = #{atype} AND TC2.#{tc_atype} = #{dtype}
30
+ UNION
31
+ SELECT
32
+ TC.#{tc_aid}, #{did}, TC.#{tc_atype}, #{dtype}
33
+ FROM
34
+ #{tc} AS TC
35
+ WHERE
36
+ TC.#{tc_did} = #{aid} AND TC.#{tc_dtype} = #{atype}
37
+ UNION
38
+ SELECT
39
+ #{aid}, TC.#{tc_did}, #{atype}, TC.#{tc_dtype}
40
+ FROM
41
+ #{tc} AS TC
42
+ WHERE
43
+ TC.#{tc_aid} = #{did} AND TC.#{tc_atype} = #{dtype}
44
+ UNION
45
+ SELECT
46
+ #{aid}, #{did}, #{atype}, #{dtype}
47
+ FROM
48
+ #{tc} AS TC
49
+ WHERE
50
+ TC.#{tc_aid} = #{aid} AND TC.#{tc_did} = #{did}
51
+ AND
52
+ TC.#{tc_atype} = #{atype} AND TC.#{tc_dtype} = #{dtype}
53
+ ) AS tmp0
54
+ END
55
+
56
+ connection.execute <<-END
57
+ INSERT INTO #{trusty}
58
+ SELECT
59
+ #{tc_aid}, #{tc_did}, #{tc_atype}, #{tc_dtype}
60
+ FROM (
61
+ SELECT
62
+ #{tc_aid}, #{tc_did}, #{tc_atype}, #{tc_dtype}
63
+ FROM
64
+ #{tc} AS TC
65
+ WHERE NOT EXISTS (
66
+ SELECT *
67
+ FROM
68
+ #{suspect} AS SUSPECT
69
+ WHERE
70
+ SUSPECT.#{tc_aid} = TC.#{tc_aid} AND SUSPECT.#{tc_did} = TC.#{tc_did}
71
+ AND
72
+ SUSPECT.#{tc_atype} = TC.#{tc_atype} AND SUSPECT.#{tc_dtype} = TC.#{tc_dtype}
73
+ )
74
+ UNION
75
+ SELECT
76
+ #{my_aid}, #{my_did}, #{my_atype}, #{my_dtype}
77
+ FROM
78
+ #{my_table} AS G
79
+ WHERE
80
+ NOT (G.#{my_aid} = #{aid} AND g.#{my_atype} = #{atype}
81
+ AND
82
+ G.#{my_did} = #{did} AND g.#{my_dtype} = #{dtype})
83
+ ) AS tmp0
84
+ END
85
+
86
+ connection.execute <<-END
87
+ INSERT INTO #{new}
88
+ SELECT * FROM (
89
+ SELECT * FROM #{trusty}
90
+ UNION
91
+ SELECT
92
+ T1.#{tc_aid}, T2.#{tc_aid}, T1.#{tc_atype}, T2.#{tc_dtype}
93
+ FROM
94
+ #{trusty} T1, #{trusty} T2
95
+ WHERE
96
+ T1.#{tc_aid} = T2.#{tc_aid} AND T1.#{tc_atype} = T2.#{tc_dtype}
97
+ UNION
98
+ SELECT
99
+ T1.#{tc_aid}, T3.#{tc_aid}, T1.#{tc_atype}, T3.#{tc_dtype}
100
+ FROM
101
+ #{trusty} T1, #{trusty} T2, #{trusty} T3
102
+ WHERE
103
+ T1.#{tc_aid} = T2.#{tc_aid} AND T2.#{tc_aid} = T3.#{tc_aid}
104
+ AND
105
+ T1.#{tc_dtype} = T2.#{tc_atype} AND T2.#{tc_dtype} = T3.#{tc_atype}
106
+ ) AS tmp0
107
+ END
108
+
109
+ connection.execute <<-END
110
+ DELETE FROM #{tc} WHERE NOT EXISTS (
111
+ SELECT *
112
+ FROM
113
+ #{new} T
114
+ WHERE
115
+ T.#{tc_aid} = #{tc}.#{tc_aid} AND T.#{tc_did} = #{tc}.#{tc_did}
116
+ AND
117
+ T.#{tc_atype} = #{tc}.#{tc_atype} AND T.#{tc_dtype} = #{tc}.#{tc_dtype}
118
+ )
119
+ END
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,13 @@
1
+ module Dagnabit
2
+ module Link
3
+ module TransitiveClosureRecalculation
4
+ module OnUpdate
5
+ def after_update
6
+ old_values = dag_link_column_names.map { |n| connection.quote(changes[n].try(:first) || send(n)) }
7
+ update_transitive_closure_for_destroy(*old_values)
8
+ update_transitive_closure_for_create
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,56 @@
1
+ module Dagnabit
2
+ module Link
3
+ module TransitiveClosureRecalculation
4
+ module Utilities
5
+ private
6
+
7
+ def quoted_dag_link_values
8
+ dag_link_column_names.map { |n| connection.quote(send(n)) }
9
+ end
10
+
11
+ def quoted_dag_link_column_names
12
+ dag_link_column_names.map { |n| connection.quote_column_name(n) }
13
+ end
14
+
15
+ def dag_link_column_names
16
+ [ self.class.ancestor_id_column,
17
+ self.class.descendant_id_column,
18
+ self.class.ancestor_type_column,
19
+ self.class.descendant_type_column ]
20
+ end
21
+
22
+ def all_quoted_column_values
23
+ all_column_names.map { |n| connection.quote(send(n)) }
24
+ end
25
+
26
+ def all_quoted_column_names
27
+ all_column_names.map { |n| connection.quote_column_name(n) }
28
+ end
29
+
30
+ def all_column_names
31
+ all_columns.map { |c| c.name }
32
+ end
33
+
34
+ def with_temporary_edge_tables(*tables, &block)
35
+ tables.each do |table|
36
+ connection.create_table(table, :temporary => true, :id => false) do |t|
37
+ all_columns.each do |c|
38
+ t.send(c.type, c.name)
39
+ end
40
+ end
41
+ end
42
+
43
+ yield(tables.map { |t| connection.quote_table_name(t) })
44
+
45
+ tables.each do |table|
46
+ connection.drop_table table
47
+ end
48
+ end
49
+
50
+ def all_columns
51
+ self.class.columns.reject { |c| c.name == 'id' || c.name == 'type' }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,26 @@
1
+ module Dagnabit
2
+ module Link
3
+ #
4
+ # Basic validations on link models.
5
+ #
6
+ # This module only installs ancestor and descendant presence validations;
7
+ # the only basic requirement for a link is that it have a valid start point
8
+ # and a valid end point. We validate the presence of the +ancestor+ and
9
+ # +descendant+, instead of +ancestor_id+ and +descendant_id+, in order to
10
+ # permit scenarios like this:
11
+ #
12
+ # n1 = Node.new
13
+ # n2 = Node.new
14
+ # l = Link.new(:ancestor => n1, :descendant => n2)
15
+ # l.save # will save l, n1, and n2
16
+ #
17
+ module Validations
18
+ def self.extended(base)
19
+ base.send(:validates_presence_of, :ancestor)
20
+ base.send(:validates_presence_of, :descendant)
21
+ base.send(:validates_associated, :ancestor)
22
+ base.send(:validates_associated, :descendant)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,84 @@
1
+ module Dagnabit
2
+ module Node
3
+ #
4
+ # Association macros added to node in a dagnabit dag.
5
+ #
6
+ # == Added associations
7
+ #
8
+ # * +links_as_parent+: Links for which this node is a parent.
9
+ # * +links_as_child+: Links for which this node is a child.
10
+ # * +links_as_ancestor+: Links for which this node is an ancestor.
11
+ # * +links_as_descendant+: Links for which this node is a descendant.
12
+ #
13
+ # == Illustration
14
+ #
15
+ # Suppose we have the following graph:
16
+ #
17
+ # n1
18
+ # |
19
+ # / \
20
+ # n2 n3
21
+ # \ /
22
+ # n4
23
+ #
24
+ # Here are some example queries and outputs:
25
+ #
26
+ # n1.links_as_parent # => [#<Link ancestor=n1, descendant=n2>, #<Link ancestor=n1, descendant=n3>]
27
+ # n4.links_as_child # => [#<Link ancestor=n2, descendant=n4>, #<Link ancestor=n3, descendant=n4>]
28
+ # n1.links_as_ancestor # => [#<Link ancestor=n1, descendant=n2>, #<Link ancestor=n1, descendant=n3>, #<Link ancestor=n1, descendant=n4>]
29
+ # n4.links_as_descendant # => [#<Link ancestor=n2, descendant=n4>, #<Link ancestor=n3, descendant=n4>, #<Link ancestor=n1, descendant=n4>]
30
+ #
31
+ # In this example, we used +Link+ as the class for all links. This isn't
32
+ # actually what you get back (see Dagnabit::Link for details), but the
33
+ # objects you get back _will_ have ancestor and descendant accessors.
34
+ #
35
+ module Associations
36
+ #
37
+ # Installs associations on the node model.
38
+ #
39
+ def self.extended(base)
40
+ base.install_associations
41
+ end
42
+
43
+ def install_associations
44
+ klass = self
45
+ link_class = klass.link_class_name.constantize
46
+
47
+ klass.send(:has_many,
48
+ :links_as_parent,
49
+ :class_name => klass.link_class_name,
50
+ :foreign_key => link_class.ancestor_id_column,
51
+ :conditions => { link_class.ancestor_type_column => klass.name },
52
+ :dependent => :destroy)
53
+
54
+ klass.send(:has_many,
55
+ :links_as_child,
56
+ :class_name => klass.link_class_name,
57
+ :foreign_key => link_class.descendant_id_column,
58
+ :conditions => { link_class.descendant_type_column => klass.name },
59
+ :dependent => :destroy)
60
+
61
+ klass.send(:has_many,
62
+ :links_as_ancestor,
63
+ :class_name => link_class.transitive_closure_class.name,
64
+ :foreign_key => link_class.ancestor_id_column,
65
+ :conditions => { link_class.ancestor_type_column => klass.name },
66
+ :readonly => true)
67
+
68
+ klass.send(:has_many,
69
+ :links_as_descendant,
70
+ :class_name => link_class.transitive_closure_class.name,
71
+ :foreign_key => link_class.descendant_id_column,
72
+ :conditions => { link_class.descendant_type_column => klass.name },
73
+ :readonly => true)
74
+ end
75
+
76
+ private
77
+
78
+ def inherited(subclass)
79
+ super(subclass)
80
+ subclass.install_associations
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,74 @@
1
+ require 'ostruct'
2
+
3
+ module Dagnabit
4
+ module Node
5
+ module ClassMethods
6
+ #
7
+ # Returns a subgraph rooted at a given set of nodes.
8
+ #
9
+ # While you can retrieve all descendants of a node pretty easily (i.e.
10
+ # +node.descendants+), and all links from a node really easily (i.e.
11
+ # +node.links_as_ancestor), it's not quite as straightforward to get the
12
+ # nodes and edges (direct edges, that is) out of a graph.
13
+ #
14
+ # The subgraph is returned as an object with two properties: +nodes+ and
15
+ # +edges+. Both properties are instances of the Set class.
16
+ #
17
+ # == Examples
18
+ #
19
+ # In the following examples, the node class is called +Node+. Variables
20
+ # of the form +nM+, where M is an integer, denote Node instances.
21
+ #
22
+ # Retrieve all descendants of n1 and all direct edges "underneath" n1
23
+ # (i.e. n1 to its children and direct edges for each of n1's
24
+ # descendants):
25
+ #
26
+ # <pre>
27
+ # Node.subgraph_from(n1)
28
+ #
29
+ # => #<Graph nodes=... edges=...>
30
+ # </pre>
31
+ #
32
+ # Same as above, but builds a graph using n1 and n2 as roots.
33
+ #
34
+ # <pre>
35
+ # Node.subgraph_from(n1, n2)
36
+ # </pre>
37
+ #
38
+ # == Usage tip
39
+ #
40
+ # +subgraph_from+ forces loading of the +descendants+ and
41
+ # +links_as_parent+ +links_as_parent+ associations on Node. In some
42
+ # situations, you may experience better performance if you use
43
+ # ActiveRecord's eager-loading capabilities when using subgraph_from:
44
+ #
45
+ # <pre>
46
+ # roots = Node.find(..., :include => [:descendants, :links_as_parent])
47
+ # Node.subgraph_from(roots)
48
+ # </pre>
49
+ #
50
+ def subgraph_from(*roots)
51
+ returning(OpenStruct.new) do |g|
52
+ g.nodes = all_nodes_of(roots)
53
+ g.edges = direct_edges_of(roots)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def all_nodes_of(roots)
60
+ roots.inject(Set.new) do |r, root|
61
+ r += root.descendants
62
+ r << root
63
+ end
64
+ end
65
+
66
+ def direct_edges_of(roots)
67
+ roots.inject(Set.new) do |r, root|
68
+ r += root.links_as_ancestor.find(:all, :include => { :descendant => :links_as_parent }).map { |l| l.descendant.links_as_parent }.flatten
69
+ r += root.links_as_parent
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,26 @@
1
+ module Dagnabit
2
+ module Node
3
+ #
4
+ # Configuration mechanism for nodes in a dagnabit-managed dag.
5
+ #
6
+ module Configuration
7
+ #
8
+ # Writes accessors for configuration data into a node.
9
+ #
10
+ # The following accessors are available:
11
+ # [link_class_name]
12
+ # The name of the model used to link nodes of this class.
13
+ #
14
+ def self.extended(base)
15
+ base.class_inheritable_accessor :link_class_name
16
+ end
17
+
18
+ #
19
+ # Configure node behavior.
20
+ #
21
+ def configure_acts_as_dag_node(link_class_name)
22
+ self.link_class_name = link_class_name
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,73 @@
1
+ module Dagnabit
2
+ module Node
3
+ #
4
+ # Instance methods for finding out the neighbors of a node.
5
+ #
6
+ # These methods do _not_ behave like association proxies: they're just
7
+ # wrappers around <tt>ActiveRecord::Base#find</tt>. Therefore, they do not
8
+ # cache, do not support calculations, do not support extension modules,
9
+ # named scopes, etc.
10
+ #
11
+ # These methods aren't association proxies because a link's ancestor and
12
+ # descendant are polymorphic associations, and ActiveRecord does not
13
+ # support polymorphic has_many :through associations.
14
+ #
15
+ module Neighbors
16
+ #
17
+ # Finds the parents (immediate predecessors) of this node.
18
+ #
19
+ def parents
20
+ links_as_child.find(:all, :include => :ancestor).map { |l| l.ancestor }
21
+ end
22
+
23
+ #
24
+ # Finds the parents (immediate predecessors) of this node satisfying a given type.
25
+ #
26
+ def parents_of_type(type)
27
+ links_as_child.ancestor_type(type).find(:all, :include => :ancestor).map { |l| l.ancestor }
28
+ end
29
+
30
+ #
31
+ # Finds the children (immediate successors) of this node.
32
+ #
33
+ def children
34
+ links_as_parent.find(:all, :include => :descendant).map { |l| l.descendant }
35
+ end
36
+
37
+ #
38
+ # Finds the children (immediate successors) of this node satisfying a given type.
39
+ #
40
+ def children_of_type(type)
41
+ links_as_parent.descendant_type(type).find(:all, :include => :descendant).map { |l| l.descendant }
42
+ end
43
+
44
+ #
45
+ # Find the ancestors (predecessors) of this node.
46
+ #
47
+ def ancestors
48
+ links_as_descendant.find(:all, :include => :ancestor).map { |l| l.ancestor }
49
+ end
50
+
51
+ #
52
+ # Find the ancestors (predecessors) of this node satisfying a given type.
53
+ #
54
+ def ancestors_of_type(type)
55
+ links_as_descendant.ancestor_type(type).find(:all, :include => :ancestor).map { |l| l.ancestor }
56
+ end
57
+
58
+ #
59
+ # Finds the descendants (successors) of this node.
60
+ #
61
+ def descendants
62
+ links_as_ancestor.find(:all, :include => :descendant).map { |l| l.descendant }
63
+ end
64
+
65
+ #
66
+ # Finds the descendants (successors) of this node satisfying a given type.
67
+ #
68
+ def descendants_of_type(type)
69
+ links_as_ancestor.descendant_type(type).find(:all, :include => :descendant).map { |l| l.descendant }
70
+ end
71
+ end
72
+ end
73
+ end