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