File Coverage

blib/lib/DBIx/Class/Tree/Mobius.pm
Criterion Covered Total %
statement 9 134 6.7
branch 0 54 0.0
condition 0 6 0.0
subroutine 3 34 8.8
pod n/a
total 12 228 5.2


line stmt bran cond sub pod time code
1             package DBIx::Class::Tree::Mobius;
2             # ABSTRACT: Manage trees of data using the Möbius encoding (nested intervals with continued fraction)
3              
4 1     1   679 use strict;
  1         2  
  1         27  
5 1     1   4 use warnings;
  1         1  
  1         35  
6              
7 1     1   13 use base qw/DBIx::Class/;
  1         1  
  1         2171  
8              
9             __PACKAGE__->mk_classdata( 'parent_virtual_column' => 'parent' );
10              
11             __PACKAGE__->mk_classdata( '_mobius_a_column' => 'mobius_a' );
12             __PACKAGE__->mk_classdata( '_mobius_b_column' => 'mobius_b' );
13             __PACKAGE__->mk_classdata( '_mobius_c_column' => 'mobius_c' );
14             __PACKAGE__->mk_classdata( '_mobius_d_column' => 'mobius_d' );
15             __PACKAGE__->mk_classdata( '_lft_column' => 'lft' );
16             __PACKAGE__->mk_classdata( '_rgt_column' => 'rgt' );
17             __PACKAGE__->mk_classdata( '_is_inner_column' => 'is_inner' );
18              
19             sub add_mobius_tree_columns {
20 0     0     my $class = shift;
21 0           my %column_names = @_;
22              
23 0           foreach my $name (qw/ mobius_a mobius_b mobius_c mobius_d lft rgt is_inner /) {
24 0 0         next unless exists $column_names{$name};
25 0           my $accessor = "_${name}_column";
26 0           $class->$accessor( $column_names{$name} );
27             }
28              
29             $class->add_columns(
30 0           $class->_mobius_a_column => { data_type => 'INT', size => 11, is_nullable => 1, extra => { unsigned => 1} },
31             $class->_mobius_b_column => { data_type => 'INT', size => 11, is_nullable => 1, extra => { unsigned => 1} },
32             $class->_mobius_c_column => { data_type => 'INT', size => 11, is_nullable => 1, extra => { unsigned => 1} },
33             $class->_mobius_d_column => { data_type => 'INT', size => 11, is_nullable => 1, extra => { unsigned => 1} },
34             $class->_lft_column => { data_type => 'DOUBLE', is_nullable => 0, default_value => 1, extra => { unsigned => 1} },
35             $class->_rgt_column => { data_type => 'DOUBLE', is_nullable => 1, default_value => undef, extra => { unsigned => 1} },
36             $class->_is_inner_column => { data_type => "BOOLEAN", default_value => 0, is_nullable => 0 },
37             );
38              
39 0           $class->add_unique_constraint( $class->_mobius_a_column . $class->_mobius_c_column, [ $class->_mobius_a_column, $class->_mobius_c_column ] );
40              
41 0 0         if ($class =~ /::([^:]+)$/) {
42              
43 0           $class->belongs_to( 'parent' => $1 => {
44             "foreign.".$class->_mobius_a_column => "self.".$class->_mobius_b_column,
45             "foreign.".$class->_mobius_c_column => "self.".$class->_mobius_d_column,
46             });
47              
48 0           $class->has_many( 'children' => $1 => {
49             "foreign.".$class->_mobius_b_column => "self.".$class->_mobius_a_column,
50             "foreign.".$class->_mobius_d_column => "self.".$class->_mobius_c_column,
51             }, { cascade_delete => 0 });
52            
53             }
54              
55             }
56              
57             sub root_cond {
58 0     0     my $self = shift;
59 0           return ( $self->_mobius_b_column => undef, $self->_mobius_d_column => undef );
60             }
61              
62             sub inner_cond {
63 0     0     my $self = shift;
64 0           return $self->_is_inner_column => 1 ;
65             }
66              
67             sub leaf_cond {
68 0     0     my $self = shift;
69 0           return $self->_is_inner_column => 0 ;
70             }
71              
72             sub _rational {
73 0     0     my $i = shift;
74              
75 0 0         return unless ($i);
76 0 0         return ($i, 1) unless (scalar @_ > 0);
77              
78 0           my ($num, $den) = _rational(@_);
79 0           return ($num * $i + $den, $num);
80             }
81              
82             sub _euclidean {
83 0     0     my ($a, $c) = @_;
84              
85 0 0         return unless ($c);
86 0           my $res = $a % $c;
87 0 0         return $res == 0 ? int($a / $c) : (int($a / $c), _euclidean($c, $res));
88             }
89              
90             sub _mobius {
91 0     0     my $i = shift;
92              
93 0 0         return (1, 0, 0, 1) unless ($i);
94 0           my ($a, $b, $c, $d) = _mobius(@_);
95 0           return ($i * $a + $c, $i * $b + $d, $a, $b);
96             }
97              
98             sub _mobius_encoding {
99 0     0     my ($a, $b, $c, $d) = _mobius(@_);
100 0 0         return wantarray ? ($a, $b, $c, $d) : sprintf("(${a}x + $b) / (${c}x + $d)");
101             }
102              
103             sub _mobius_path {
104 0     0     my ($a, $b, $c, $d) = @_;
105 0           my @path = _euclidean($a, $c);
106 0 0         return wantarray ? @path : join('.', @path);
107             }
108              
109             sub _left_right {
110 0     0     my ($a, $b, $c, $d) = @_;
111 0           my ($x, $y) = (($a+$b)/($c+$d), $a / $c);
112 0 0         my ($left, $right) = $x > $y ? ($y, $x) : ($x, $y);
113 0 0         warn("DBIx::Class::Tree::Mobius max depth has been reached.") if ($left == $right);
114 0 0         return wantarray ? ($left, $right) : sprintf("l=%.3f, r=%.3f", $left, $right);
115             }
116              
117             sub new {
118 0     0     my ($class, $attrs) = @_;
119 0 0         $class = ref $class if ref $class;
120            
121 0 0         if (my $parent = delete($attrs->{$class->parent_virtual_column})) {
122             # store aside explicitly parent
123 0           my $new = $class->next::method($attrs);
124 0           $new->{_explicit_parent} = $parent;
125 0           return $new;
126             } else {
127 0           return $class->next::method($attrs);
128             }
129             }
130              
131             # always use the leftmost index available
132              
133             sub _available_mobius_index {
134 0     0     my @children = @_;
135              
136 0           my $count = scalar @children + 2;
137 0           foreach my $child (@children) {
138 0           my @mpath = $child->mobius_path();
139 0           my $index = pop @mpath;
140 0 0         last if ($count > $index);
141 0           $count--;
142             }
143 0           return $count;
144             }
145              
146             sub available_mobius_index {
147 0     0     my $self = shift;
148 0           return _available_mobius_index( $self->children()->search({}, { order_by => $self->_mobius_a_column. ' DESC' } ) );
149             }
150              
151             sub insert {
152 0     0     my $self = shift;
153              
154 0 0 0       if (exists $self->{_explicit_parent}
155             and my $parent = $self->result_source->resultset->find($self->{_explicit_parent}) ) {
156              
157 0           my ($a, $b, $c, $d, $left, $right) = $parent->child_encoding( $parent->available_mobius_index );
158              
159 0           $self->store_column( $self->_mobius_a_column => $a );
160 0           $self->store_column( $self->_mobius_b_column => $b );
161 0           $self->store_column( $self->_mobius_c_column => $c );
162 0           $self->store_column( $self->_mobius_d_column => $d );
163 0           $self->store_column( $self->_lft_column => $left );
164 0           $self->store_column( $self->_rgt_column => $right );
165              
166 0           my $r = $self->next::method(@_);
167 0           $parent->update({ $self->_is_inner_column => 1 } );
168 0           return $r;
169              
170             } else { # attaching to root
171              
172 0           my $x = _available_mobius_index( $self->result_source->resultset->search( { $self->root_cond } )->search({}, { order_by => $self->_mobius_a_column. ' DESC' } ) );
173              
174 0           $self->store_column( $self->_mobius_a_column => $x );
175 0           $self->store_column( $self->_mobius_c_column => 1 );
176             # normal value are b => 1 and c => 0 but it cannot work for SQL join
177 0           $self->store_column( $self->_mobius_b_column => undef );
178 0           $self->store_column( $self->_mobius_d_column => undef );
179 0           $self->store_column( $self->_lft_column => $x );
180 0           $self->store_column( $self->_rgt_column => $x + 1 );
181 0           return $self->next::method(@_);
182              
183             }
184              
185             }
186              
187             sub mobius_path {
188 0     0     my $self = shift;
189 0           my ($b, $d) = ($self->get_column($self->_mobius_b_column), $self->get_column($self->_mobius_d_column));
190 0 0         my @path = _mobius_path(
    0          
191             $self->get_column($self->_mobius_a_column), defined $b ? $b : 1,
192             $self->get_column($self->_mobius_c_column), defined $d ? $d : 0,
193             );
194 0 0         return wantarray ? @path : join('.', @path);
195             }
196              
197             sub depth {
198 0     0     my $self = shift;
199 0           my @path = $self->mobius_path();
200 0           return scalar @path;
201             }
202              
203             sub child_encoding {
204 0     0     my $self = shift;
205 0           my $x = shift;
206 0           my ($pb, $pd) = ($self->get_column($self->_mobius_b_column), $self->get_column($self->_mobius_d_column));
207 0 0         my ($a, $b, $c, $d) = (
    0          
208             $self->get_column($self->_mobius_a_column) * $x + ( defined $pb ? $pb : 1),
209             $self->get_column($self->_mobius_a_column),
210             $self->get_column($self->_mobius_c_column) * $x + ( defined $pd ? $pd : 0),
211             $self->get_column($self->_mobius_c_column)
212             );
213 0 0         return wantarray ? ($a, $b, $c, $d, _left_right($a, $b, $c, $d)) : sprintf("(${a}x + $b) / (${c}x + $d)");
214             }
215              
216             sub root {
217 0     0     my $self = shift;
218 0           return $self->result_source->resultset->search( { $self->root_cond } )->search({
219             $self->result_source->resultset->current_source_alias.'.'.$self->_lft_column => { '<' => $self->get_column($self->_rgt_column) },
220             $self->result_source->resultset->current_source_alias.'.'.$self->_rgt_column => { '>' => $self->get_column($self->_lft_column) },
221             });
222             }
223              
224             sub is_root {
225 0     0     my $self = shift;
226 0 0         return $self->parent ? 0 : 1;
227             }
228              
229             sub is_inner {
230 0     0     my $self = shift;
231 0 0         return $self->get_column($self->_is_inner_column) ? 1 : 0;
232             }
233              
234             sub is_branch {
235 0     0     my $self = shift;
236 0 0 0       return ($self->parent && $self->get_column($self->_is_inner_column)) ? 1 : 0;
237             }
238              
239             sub is_leaf {
240 0     0     my $self = shift;
241 0 0         return $self->get_column($self->_is_inner_column) ? 0 : 1;
242             }
243              
244             sub siblings {
245 0     0     my $self = shift;
246 0 0         if (my $parent = $self->parent) {
247 0           return $parent->children->search({
248             -or => {
249             $self->result_source->resultset->current_source_alias.'.'.$self->_mobius_a_column => { '!=' => $self->get_column($self->_mobius_a_column) },
250             $self->result_source->resultset->current_source_alias.'.'.$self->_mobius_c_column => { '!=' => $self->get_column($self->_mobius_c_column) },
251             },
252             });
253             } else {
254 0           return $self->result_source->resultset->search({
255             -or => {
256             $self->result_source->resultset->current_source_alias.'.'.$self->_mobius_a_column => { '!=' => $self->get_column($self->_mobius_a_column) },
257             $self->result_source->resultset->current_source_alias.'.'.$self->_mobius_c_column => { '!=' => $self->get_column($self->_mobius_c_column) },
258             },
259             $self->result_source->resultset->current_source_alias.'.'.$self->_mobius_b_column => undef,
260             $self->result_source->resultset->current_source_alias.'.'.$self->_mobius_d_column => undef
261             });
262             }
263             }
264              
265             sub leaf_children {
266 0     0     my $self = shift;
267 0           return $self->children->search({ $self->result_source->resultset->current_source_alias.'.'.$self->_is_inner_column => 0 });
268             }
269              
270             sub inner_children {
271 0     0     my $self = shift;
272 0           return $self->children->search({ $self->result_source->resultset->current_source_alias.'.'.$self->_is_inner_column => 1 });
273             }
274              
275             sub descendants {
276 0     0     my $self = shift;
277              
278 0           return $self->result_source->resultset->search({
279             $self->result_source->resultset->current_source_alias.'.'.$self->_lft_column => { '>' => $self->get_column($self->_lft_column) },
280             $self->result_source->resultset->current_source_alias.'.'.$self->_rgt_column => { '<' => $self->get_column($self->_rgt_column) },
281             });
282             }
283              
284             sub leaves {
285 0     0     my $self = shift;
286 0           return $self->descendants->search({ $self->result_source->resultset->current_source_alias.'.'.$self->_is_inner_column => 0 });
287             }
288              
289             sub inner_descendants {
290 0     0     my $self = shift;
291              
292 0           return $self->descendants->search({ $self->result_source->resultset->current_source_alias.'.'.$self->_is_inner_column => 1 });
293             }
294              
295             sub ancestors {
296 0     0     my $self = shift;
297            
298 0           return $self->result_source->resultset->search({
299             -and => {
300             $self->result_source->resultset->current_source_alias.'.'.$self->_lft_column => { '<' => $self->get_column($self->_lft_column) },
301             $self->result_source->resultset->current_source_alias.'.'.$self->_rgt_column => { '>' => $self->get_column($self->_rgt_column) },
302             },
303             $self->result_source->resultset->current_source_alias.'.'.$self->_lft_column => { '<' => $self->get_column($self->_rgt_column) },
304             $self->result_source->resultset->current_source_alias.'.'.$self->_rgt_column => { '>' => $self->get_column($self->_lft_column) },
305             $self->result_source->resultset->current_source_alias.'.'.$self->_mobius_a_column => { '!=' => $self->get_column($self->_mobius_a_column) },
306             $self->result_source->resultset->current_source_alias.'.'.$self->_mobius_c_column => { '!=' => $self->get_column($self->_mobius_c_column) },
307             },{ order_by => $self->_lft_column.' DESC' });
308             }
309              
310 0     0     sub ascendants { return shift(@_)->ancestors(@_) }
311              
312             sub attach_child {
313 0     0     my $self = shift;
314 0           my $child = shift;
315              
316 0           my ($a, $b, $c, $d, $left, $right) = $self->child_encoding( $self->available_mobius_index );
317              
318 0           my @grandchildren = $child->children()->all();
319 0           foreach my $grandchild (@grandchildren) {
320 0           $grandchild->update( { $self->_mobius_b_column => undef, $self->_mobius_d_column => undef });
321             }
322              
323             $child->update({
324 0           $self->_mobius_a_column => $a,
325             $self->_mobius_b_column => $b,
326             $self->_mobius_c_column => $c,
327             $self->_mobius_d_column => $d,
328             $self->_lft_column => $left,
329             $self->_rgt_column => $right,
330             });
331              
332 0           foreach my $grandchild (@grandchildren) {
333 0           $child->attach_child( $grandchild );
334             }
335              
336             }
337              
338              
339             1;
340              
341             =head1 SYNOPSIS
342              
343             Create a table for your tree data with the 7 special columns used by Tree::Mobius.
344             By default, these columns are mobius_a mobius_b mobius_b and mobius_d (integer),
345             lft and rgt (float) and inner (boolean). See the add_mobius_tree_columns method
346             to change the default names.
347              
348             CREATE TABLE employees (
349             name TEXT NOT NULL
350             mobius_a integer(11) unsigned,
351             mobius_b integer(11) unsigned,
352             mobius_c integer(11) unsigned,
353             mobius_d integer(11) unsigned,
354             lft FLOAT unsigned NOT NULL DEFAULT '1',
355             rgt FLOAT unsigned,
356             inner boolean NOT NULL DEFAULT '0',
357             );
358              
359             In your Schema or DB class add Tree::Mobius in the component list.
360              
361             __PACKAGE__->load_components(qw( Tree::Mobius ... ));
362              
363             Call add_mobius_tree_columns.
364              
365             package My::Employee;
366             __PACKAGE__->add_mobius_tree_columns();
367              
368             That's it, now you can create and manipulate trees for your table.
369              
370             #!/usr/bin/perl
371             use My::Employee;
372            
373             my $big_boss = My::Employee->create({ name => 'Larry W.' });
374             my $boss = My::Employee->create({ name => 'John Doe' });
375             my $employee = My::Employee->create({ name => 'No One' });
376            
377             $big_boss->attach_child( $boss );
378             $boss->attach_child( $employee );
379              
380             =head1 DESCRIPTION
381              
382             This module provides methods for working with trees of data using a
383             Möbius encoding, a variant of 'Nested Intervals' tree encoding using
384             continued fraction. This a model to represent hierarchical information
385             in a SQL database. This model takes a complementary approach of both
386             the 'Nested Sets' model and the 'Materialized Path' model.
387              
388             The implementation has been heavily inspired by a Vadim Tropashko's
389             paper available online at http://arxiv.org/pdf/cs.DB/0402051 about
390             the Möbius encoding.
391              
392             A 'Nested Intervals' model has the same advantages that 'Nested Sets'
393             over the 'Adjacency List', that is to say that obtaining all
394             descendants requires only one query rather than recursive queries.
395              
396             Additionally, a 'Nested Intervals' model has two advantages over 'Nested Sets' :
397              
398             - Encoding is not volatile (no other node should be relabeled whenever
399             a new node were inserted).
400              
401             - There are no difficulties associated with querying ancestors.
402              
403             The Möbius encoding is a particular encoding schema of the 'Nested
404             Intervals' model that uses integer numbers economically to allow
405             better tree scaling and directly encode the material path of a node
406             using continued fraction (thus this model also relates somewhat with
407             the 'Materialized Path' model).
408              
409             The tradeoffs over other models is in this implementation the use of 7
410             SQL columns to encode each node.
411              
412             Since the encoding is not volatile, the depth is constraint by the
413             precision of FLOAT in the right and left column. The maximum depth
414             reachable is 8 levels with a simple SQL FLOAT, and 21 with a SQL DOUBLE.
415              
416             This implementation allows you to have several trees in your database.
417              
418             =head1 METHODS
419              
420             =head2 add_mobius_tree_columns
421              
422             Declare the name of the columns for tree encoding and add them to the schema.
423              
424             None of these columns should be modified outside if this module.
425              
426             Multiple trees are allowed in the same table, each tree will have a unique value in the mobius_a_column.
427              
428             =head2 attach_child
429              
430             Attach a new child to a node.
431              
432             If the child has descendants, the entire sub-tree is moved recursively.
433              
434             =head2 insert
435              
436             This method is an override of the DBIx::Class' method.
437              
438             The method is not meant to not be used directly but it allows one to
439             add a parent virtual column when calling the DBIx::Class method create.
440              
441             This virtual column should be set with the primary key value of the parent.
442              
443             My::Employee->create({ name => 'Another Intern', parent => $boss->id });
444              
445             =head2 parent
446              
447             Returns a DBIx::Class Row of the parent of a node.
448              
449             =head2 children
450              
451             Returns a DBIx::Class resultset of all children (direct descendants) of a node.
452              
453             =head2 leaf_children
454              
455             Returns a DBIx::Class resultset of all children (direct descendants) of a node that do not possess any child themselves.
456              
457             =head2 inner_children
458              
459             Returns a DBIx::Class resultset of all children (direct descendants) of a node that possess one or more child.
460              
461             =head2 descendants
462              
463             Returns a DBIx::Class resultset of all descendants of a node (direct or not).
464              
465             =head2 leaves
466              
467             Returns a DBIx::Class resultset of all descendants of a node that do not possess any child themselves.
468              
469             =head2 inner_descendants
470              
471             Returns a DBIx::Class resultset of all descendants of a node that possess one or more child.
472              
473             =head2 ancestors
474              
475             Returns a DBIx::Class resultset of all ancestors of a node.
476              
477             =head2 ascendants
478              
479             An alias method for ancestors.
480              
481             =head2 root
482              
483             Returns a DBIx::Class resultset containing the root ancestor of a given node.
484              
485             =head2 siblings
486              
487             Returns a DBIx::Class resultset containing all the nodes with the same parent of a given node.
488              
489             =head2 is_root
490              
491             Returns 1 if the node has no parent, and 0 otherwise.
492              
493             =head2 is_inner
494              
495             Returns 1 if the node has at least one child, and 0 otherwise.
496              
497             =head2 is_branch
498              
499             Returns 1 if the node has at least one child and is not a root node, 0 otherwise.
500              
501             =head2 is_leaf
502              
503             Returns 1 if the node has no child, and 0 otherwise.
504              
505             =head2 available_mobius_index
506              
507             Returns the smallest mobius index available in the subtree of a given node.
508              
509             =head2 child_encoding
510              
511             Given a mobius index, return the mobius a,b,c,d column values.
512              
513             =head2 depth
514            
515             Return the depth of a node in a tree (depth of a root node is 1).
516            
517             =for Pod::Coverage new mobius_path root_cond inner_cond leaf_cond
518