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
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'dagnabit'
@@ -0,0 +1,17 @@
1
+ require 'dagnabit/link/configuration'
2
+ require 'dagnabit/link/validations'
3
+ require 'dagnabit/link/associations'
4
+ require 'dagnabit/link/class_methods'
5
+ require 'dagnabit/link/cycle_prevention'
6
+ require 'dagnabit/link/named_scopes'
7
+ require 'dagnabit/link/transitive_closure_recalculation'
8
+ require 'dagnabit/link/transitive_closure_link_model'
9
+
10
+ require 'dagnabit/node/configuration'
11
+ require 'dagnabit/node/class_methods'
12
+ require 'dagnabit/node/associations'
13
+ require 'dagnabit/node/neighbors'
14
+
15
+ require 'dagnabit/activation'
16
+
17
+ ActiveRecord::Base.extend(Dagnabit::Activation)
@@ -0,0 +1,60 @@
1
+ module Dagnabit
2
+ #
3
+ # Class methods for mixing in ("activating") dag functionality.
4
+ #
5
+ module Activation
6
+ #
7
+ # Marks an ActiveRecord model as a link model.
8
+ #
9
+ # == Supported options
10
+ #
11
+ # [:ancestor_id_column]
12
+ # Name of the column in the link tables that will hold the ID of the
13
+ # ancestor object. Defaults to +ancestor_id+.
14
+ # [:descendant_id_column]
15
+ # Name of the column in the link tables that will hold the ID of the
16
+ # descendant object. Defaults to +descendant_id+.
17
+ # [:transitive_closure_table_name]
18
+ # Name of the table that will hold the tuples comprising the transitive
19
+ # closure of the dag. Defaults to the edge model's table name affixed by
20
+ # "<tt>_transitive_closure_tuples</tt>".
21
+ # [:transitive_closure_class_name]
22
+ # Name of the generated class that will represent tuples in the transitive
23
+ # closure tuple table. This class is created inside the link model class.
24
+ # Defaults to +TransitiveClosureLink+.
25
+ #
26
+ def acts_as_dag_link(options = {})
27
+ extend Dagnabit::Link::Configuration
28
+ configure_acts_as_dag_link(options)
29
+
30
+ extend Dagnabit::Link::TransitiveClosureLinkModel
31
+ generate_transitive_closure_link_model(options)
32
+
33
+ extend Dagnabit::Link::ClassMethods
34
+ extend Dagnabit::Link::Associations
35
+ extend Dagnabit::Link::NamedScopes
36
+ extend Dagnabit::Link::Validations
37
+ include Dagnabit::Link::CyclePrevention
38
+ include Dagnabit::Link::TransitiveClosureRecalculation
39
+ end
40
+
41
+ #
42
+ # Adds convenience methods to dag nodes.
43
+ #
44
+ # Strictly speaking, it's not necessary to call this method inside classes
45
+ # you want to act as nodes. +acts_as_dag_node_linked_by+ merely provides
46
+ # convenience methods for finding and traversing links from/to this node.
47
+ #
48
+ # The +link_class_name+ parameter determines the the link model to be used
49
+ # for nodes of this type.
50
+ #
51
+ def acts_as_dag_node_linked_by(link_class_name)
52
+ extend Dagnabit::Node::Configuration
53
+ configure_acts_as_dag_node(link_class_name)
54
+
55
+ extend Dagnabit::Node::ClassMethods
56
+ extend Dagnabit::Node::Associations
57
+ include Dagnabit::Node::Neighbors
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,18 @@
1
+ module Dagnabit
2
+ module Link
3
+ #
4
+ # Adds associations useful for link classes.
5
+ #
6
+ # This module mixes in the following associations to link classes:
7
+ #
8
+ # * +ancestor+: the source of this link, or where this link begins
9
+ # * +descendant+: the target of this link, or where this link ends
10
+ #
11
+ module Associations
12
+ def self.extended(base)
13
+ base.send(:belongs_to, :ancestor, :polymorphic => true, :foreign_key => base.ancestor_id_column)
14
+ base.send(:belongs_to, :descendant, :polymorphic => true, :foreign_key => base.descendant_id_column)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,43 @@
1
+ module Dagnabit
2
+ module Link
3
+ #
4
+ # Handy class methods for creating and querying paths.
5
+ #
6
+ module ClassMethods
7
+ #
8
+ # Constructs a new edge. The direction of the edge runs from +from+ to +to+.
9
+ #
10
+ def build_edge(from, to, attributes = {})
11
+ new(attributes.merge(:ancestor => from, :descendant => to))
12
+ end
13
+
14
+ #
15
+ # Like +build_edge+, but saves the edge after it is instantiated.
16
+ # Returns true if the endpoints could be connected, false otherwise.
17
+ #
18
+ # See Dagnabit::Link::Validations for more information on built-in link
19
+ # validations.
20
+ #
21
+ def connect(from, to, attributes = {})
22
+ build_edge(from, to, attributes).save
23
+ end
24
+
25
+ #
26
+ # Returns true if there is a path from +a+ to +b+, false otherwise.
27
+ #
28
+ def path?(a, b)
29
+ paths(a, b).count > 0
30
+ end
31
+
32
+ #
33
+ # Returns all paths from +a+ to +b+.
34
+ #
35
+ # These paths are returned as transitive closure links, which aren't
36
+ # guaranteed to have the same methods as your link class.
37
+ #
38
+ def paths(a, b)
39
+ transitive_closure_class.linking(a, b)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ module Dagnabit
2
+ module Link
3
+ #
4
+ # Dagnabit::Edge::Configuration - dag edge configuration
5
+ #
6
+ module Configuration
7
+ attr_accessor :ancestor_id_column
8
+ attr_accessor :descendant_id_column
9
+ attr_writer :transitive_closure_table_name
10
+ attr_accessor :transitive_closure_class_name
11
+
12
+ #
13
+ # Configure an ActiveRecord model as a dag link. See Dagnabit::Activation
14
+ # for options description.
15
+ #
16
+ def configure_acts_as_dag_link(options)
17
+ self.ancestor_id_column = options[:ancestor_id_column] || 'ancestor_id'
18
+ self.descendant_id_column = options[:descendant_id_column] || 'descendant_id'
19
+ self.transitive_closure_table_name = options[:transitive_closure_table_name] || table_name + '_transitive_closure_tuples'
20
+ self.transitive_closure_class_name = options[:transitive_closure_class_name] || 'TransitiveClosureLink'
21
+ end
22
+
23
+ def transitive_closure_table_name
24
+ connection.quote_table_name(unquoted_transitive_closure_table_name)
25
+ end
26
+
27
+ def unquoted_transitive_closure_table_name
28
+ @transitive_closure_table_name
29
+ end
30
+
31
+ def ancestor_type_column
32
+ 'ancestor_type'
33
+ end
34
+
35
+ def descendant_type_column
36
+ 'descendant_type'
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ module Dagnabit
2
+ module Link
3
+ #
4
+ # Installs a callback into the link model to check for cycles. If a cycle
5
+ # would be created by the addition of this link, prevents the link from
6
+ # being saved.
7
+ #
8
+ module CyclePrevention
9
+ #
10
+ # Performs cycle detection.
11
+ #
12
+ # Given an edge (A, B), insertion of that edge will create a cycle if
13
+ #
14
+ # * there is a path (B, A), or
15
+ # * A == B
16
+ #
17
+ def before_save
18
+ super
19
+ check_for_cycles
20
+ end
21
+
22
+ private
23
+
24
+ def check_for_cycles
25
+ if ancestor && descendant
26
+ false if self.class.path?(descendant, ancestor) || descendant == ancestor
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,65 @@
1
+ module Dagnabit
2
+ module Link
3
+ #
4
+ # Adds named scopes to Link models.
5
+ #
6
+ # This module provides two named scopes for finding links scoped by
7
+ # ancestor and descendant type. They were designed as support for node
8
+ # neighbor queries such as ancestors_of_type and descendants_as_type, but
9
+ # can be used on their own.
10
+ #
11
+ # These links are imported into the generated transitive closure link model.
12
+ # See Dagnabit::Link::TransitiveClosureLinkModel for more information.
13
+ #
14
+ # == Supplied scopes
15
+ #
16
+ # [ancestor_type]
17
+ # Returns all links having a specified ancestor type.
18
+ #
19
+ # [descendant_type]
20
+ # Returns all links having a specified descendant type.
21
+ #
22
+ # == A note on type matching
23
+ #
24
+ # Types are stored in links using ActiveRecord's polymorphic association
25
+ # typing logic, and are matched using string matching. Therefore, subclass
26
+ # matching and namespacing aren't provided.
27
+ #
28
+ # To elaborate on this, let's say you have the following model structure:
29
+ #
30
+ # class Link < ActiveRecord::Base
31
+ # acts_as_dag_link
32
+ # end
33
+ #
34
+ # module Foo
35
+ # class Bar < ActiveRecord::Base
36
+ # ...
37
+ # end
38
+ # end
39
+ #
40
+ # A link linking Foo::Bars will record Foo::Bar as ancestor or descendant
41
+ # type, not just 'Bar'. The following will therefore not work:
42
+ #
43
+ # Link.ancestor_type('Bar')
44
+ #
45
+ # You have to do:
46
+ #
47
+ # Link.ancestor_type('Foo::Bar')
48
+ #
49
+ # or, if you'd like to hide the details of deriving a full class name:
50
+ #
51
+ # Link.ancestor_type(Bar.name)
52
+ #
53
+ module NamedScopes
54
+ def self.extended(base)
55
+ base.send(:named_scope,
56
+ :ancestor_type,
57
+ lambda { |type| { :conditions => { :ancestor_type => type } } })
58
+
59
+ base.send(:named_scope,
60
+ :descendant_type,
61
+ lambda { |type| { :conditions => { :descendant_type => type } } })
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,86 @@
1
+ module Dagnabit
2
+ module Link
3
+ #
4
+ # Builds a model for transitive closure tuples.
5
+ #
6
+ # The transitive closure model is generated inside the link class.
7
+ # Therefore, if your link model class was called Link, the transitive
8
+ # closure model would be named Link::(class name here). The name of the
9
+ # transitive closure model class is determined when the link class model is
10
+ # activated; see Dagnabit::Activation#acts_as_dag_link for more information.
11
+ #
12
+ # == Model class details
13
+ #
14
+ # === Construction details
15
+ #
16
+ # The transitive closure model is constructed as a subclass of
17
+ # ActiveRecord::Base, _not_ as a subclass of your link model class. The
18
+ # transitive closure model also acts as a dag link (via
19
+ # Dagnabit::Activation#acts_as_dag_link) and is configured using the same
20
+ # configuration options as your link model class.
21
+ #
22
+ # This means:
23
+ #
24
+ # * The transitive closure tuple table and your link table must have the
25
+ # same column names for ancestor id/type and descendant id/type.
26
+ # * You will not be able to use any methods defined on your link model on
27
+ # the transitive closure model.
28
+ #
29
+ # === Available methods
30
+ #
31
+ # The following class methods are available on transitive closure link
32
+ # models:
33
+ #
34
+ # [linking(a, b)]
35
+ # Returns all links (direct or indirect) linking +a+ and +b+.
36
+ # [ancestor_type(type)]
37
+ # Behaves identically to the ancestor_type named scope defined in
38
+ # Dagnabit::Link::NamedScopes.
39
+ # [descendant_type(type)]
40
+ # Behaves identically to the descendant_type named scope defined in
41
+ # Dagnabit::Link::NamedScopes.
42
+ #
43
+ # The following instance methods are available on transitive closure link
44
+ # models:
45
+ #
46
+ # [ancestor]
47
+ # Returns the ancestor of this link. Behaves identically to the
48
+ # ancestor association defined in Dagnabit::Link::Associations.
49
+ # [descendant]
50
+ # Returns the descendant of this link. Behaves identically to the
51
+ # descendant association defined in Dagnabit::Link::Associations.
52
+ #
53
+ module TransitiveClosureLinkModel
54
+ attr_reader :transitive_closure_class
55
+
56
+ private
57
+
58
+ #
59
+ # Generates the transitive closure model.
60
+ #
61
+ def generate_transitive_closure_link_model(options)
62
+ original_class = self
63
+
64
+ klass = Class.new(ActiveRecord::Base) do
65
+ extend Dagnabit::Link::Configuration
66
+
67
+ configure_acts_as_dag_link(options)
68
+ set_table_name original_class.unquoted_transitive_closure_table_name
69
+ end
70
+
71
+ @transitive_closure_class = const_set(transitive_closure_class_name, klass)
72
+
73
+ # reflections and named scopes aren't properly created in anonymous
74
+ # models, so we need to do that work after the model has been named
75
+ @transitive_closure_class.extend(Dagnabit::Link::Associations)
76
+ @transitive_closure_class.extend(Dagnabit::Link::NamedScopes)
77
+ @transitive_closure_class.named_scope :linking, lambda { |from, to|
78
+ { :conditions => { ancestor_id_column => from.id,
79
+ ancestor_type_column => from.class.name,
80
+ descendant_id_column => to.id,
81
+ descendant_type_column => to.class.name } }
82
+ }
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,17 @@
1
+ require 'dagnabit/link/transitive_closure_recalculation/on_create'
2
+ require 'dagnabit/link/transitive_closure_recalculation/on_destroy'
3
+ require 'dagnabit/link/transitive_closure_recalculation/on_update'
4
+
5
+ module Dagnabit
6
+ module Link
7
+ #
8
+ # Code to do the heavy lifting of maintaining the transitive closure of the
9
+ # dag after edge create, destroy, and update.
10
+ #
11
+ module TransitiveClosureRecalculation
12
+ include OnCreate
13
+ include OnDestroy
14
+ include OnUpdate
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,104 @@
1
+ require 'dagnabit/link/transitive_closure_recalculation/utilities'
2
+
3
+ module Dagnabit
4
+ module Link
5
+ module TransitiveClosureRecalculation
6
+ module OnCreate
7
+ include Utilities
8
+
9
+ def after_create
10
+ super
11
+ update_transitive_closure_for_create
12
+ end
13
+
14
+ private
15
+
16
+ def update_transitive_closure_for_create
17
+ tc = self.class.transitive_closure_table_name
18
+ tc_aid, tc_did, tc_atype, tc_dtype = quoted_dag_link_column_names
19
+ aid, did, atype, dtype = quoted_dag_link_values
20
+ all_columns = all_quoted_column_names.join(',')
21
+ all_values = all_quoted_column_values.join(',')
22
+
23
+ with_temporary_edge_tables('new', 'delta') do |new, delta|
24
+ extend_connected_paths(new, tc_aid, tc_did, tc_atype, tc_dtype, tc, aid, did, atype, dtype)
25
+ append_created_edge(new, all_columns, all_values)
26
+ synchronize_transitive_closure(new, delta, all_columns, tc, tc_aid, tc_did, tc_atype, tc_dtype)
27
+ end
28
+ end
29
+
30
+ #
31
+ # determine:
32
+ # * all paths constructed by adding (a, b) to the back of paths
33
+ # ending at a (first subselect)
34
+ # * all paths constructed by adding (a, b) to the front of paths
35
+ # starting at b (second subselect)
36
+ # * all paths constructed by adding (a, b) in the middle of paths
37
+ # starting at a and ending at b (third subselect)
38
+ #
39
+ def extend_connected_paths(new, tc_aid, tc_did, tc_atype, tc_dtype, tc, aid, did, atype, dtype)
40
+ connection.execute <<-END
41
+ INSERT INTO #{new} (#{tc_aid}, #{tc_did}, #{tc_atype}, #{tc_dtype})
42
+ SELECT * FROM (
43
+ SELECT
44
+ TC.#{tc_aid}, #{did}, TC.#{tc_atype}, #{dtype}
45
+ FROM
46
+ #{tc} AS TC
47
+ WHERE
48
+ TC.#{tc_did} = #{aid} AND TC.#{tc_dtype} = #{atype}
49
+ UNION
50
+ SELECT
51
+ #{aid}, TC.#{tc_did}, #{atype}, TC.#{tc_dtype}
52
+ FROM
53
+ #{tc} AS TC
54
+ WHERE
55
+ TC.#{tc_aid} = #{did} AND TC.#{tc_atype} = #{dtype}
56
+ UNION
57
+ SELECT
58
+ TC1.#{tc_aid}, TC2.#{tc_did}, TC1.#{tc_atype}, TC2.#{tc_dtype}
59
+ FROM
60
+ #{tc} AS TC1, #{tc} AS TC2
61
+ WHERE
62
+ TC1.#{tc_did} = #{aid} AND TC1.#{tc_dtype} = #{atype}
63
+ AND
64
+ TC2.#{tc_aid} = #{did} AND TC2.#{tc_atype} = #{dtype}
65
+ ) AS tmp0
66
+ END
67
+ end
68
+
69
+ def append_created_edge(new, all_columns, all_values)
70
+ connection.execute <<-END
71
+ INSERT INTO #{new} (#{all_columns}) VALUES (#{all_values})
72
+ END
73
+ end
74
+
75
+ def synchronize_transitive_closure(new, delta, all_columns, tc, tc_aid, tc_did, tc_atype, tc_dtype)
76
+ #
77
+ # ...filter out duplicates...
78
+ #
79
+ connection.execute <<-END
80
+ INSERT INTO #{delta}
81
+ SELECT * FROM #{new} AS T
82
+ WHERE NOT EXISTS (
83
+ SELECT *
84
+ FROM
85
+ #{tc} AS TC
86
+ WHERE
87
+ TC.#{tc_aid} = T.#{tc_aid} AND TC.#{tc_did} = T.#{tc_did}
88
+ AND
89
+ TC.#{tc_atype} = T.#{tc_atype} AND TC.#{tc_dtype} = T.#{tc_dtype}
90
+ )
91
+ END
92
+
93
+ #
94
+ # ...and update the transitive closure table
95
+ #
96
+ connection.execute <<-END
97
+ INSERT INTO #{tc} (#{all_columns})
98
+ SELECT * FROM #{delta}
99
+ END
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end