dagnabit 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
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