blib/lib/DBIx/OO/Tree.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 12 | 322 | 3.7 |
branch | 0 | 104 | 0.0 |
condition | 0 | 18 | 0.0 |
subroutine | 4 | 18 | 22.2 |
pod | 14 | 14 | 100.0 |
total | 30 | 476 | 6.3 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package DBIx::OO::Tree; | ||||||
2 | |||||||
3 | 2 | 2 | 19989 | use strict; | |||
2 | 5 | ||||||
2 | 125 | ||||||
4 | 2 | 2 | 12 | use vars qw(@EXPORT); | |||
2 | 4 | ||||||
2 | 90 | ||||||
5 | 2 | 2 | 11 | use version; our $VERSION = qv('0.0.1'); | |||
2 | 4 | ||||||
2 | 18 | ||||||
6 | |||||||
7 | 2 | 2 | 178 | use Carp; | |||
2 | 4 | ||||||
2 | 9791 | ||||||
8 | |||||||
9 | require Exporter; | ||||||
10 | *import = \&Exporter::import; | ||||||
11 | @EXPORT = qw( | ||||||
12 | tree_append | ||||||
13 | tree_insert_before | ||||||
14 | tree_insert_after | ||||||
15 | tree_get_subtree | ||||||
16 | tree_compute_levels | ||||||
17 | tree_reparent | ||||||
18 | tree_move_after | ||||||
19 | tree_move_before | ||||||
20 | tree_delete | ||||||
21 | tree_get_path | ||||||
22 | tree_get_next_sibling | ||||||
23 | tree_get_prev_sibling | ||||||
24 | tree_get_next | ||||||
25 | tree_get_prev | ||||||
26 | ); | ||||||
27 | |||||||
28 | =head1 NAME | ||||||
29 | |||||||
30 | DBIx::OO::Tree -- manipulate hierarchical data using the "nested sets" model | ||||||
31 | |||||||
32 | =head1 SYNOPSYS | ||||||
33 | |||||||
34 | CREATE TABLE Categories ( | ||||||
35 | id INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY, | ||||||
36 | label VARCHAR(255), | ||||||
37 | |||||||
38 | -- these columns are required by DBIx::OO::Tree | ||||||
39 | parent INTEGER UNSIGNED, | ||||||
40 | lft INTEGER UNSIGNED NOT NULL, | ||||||
41 | rgt INTEGER UNSIGNED NOT NULL, | ||||||
42 | mvg TINYINT DEFAULT 0, | ||||||
43 | |||||||
44 | INDEX(lft), | ||||||
45 | INDEX(rgt), | ||||||
46 | INDEX(mvg), | ||||||
47 | INDEX(parent) | ||||||
48 | ); | ||||||
49 | |||||||
50 | * * * | ||||||
51 | |||||||
52 | package Category; | ||||||
53 | use base 'DBIx::OO'; | ||||||
54 | use DBIx::OO::Tree; | ||||||
55 | |||||||
56 | __PACKAGE__->table('Categories'); | ||||||
57 | __PACKAGE__->columns(P => [ 'id' ], | ||||||
58 | E => [ 'label', 'parent' ]); | ||||||
59 | |||||||
60 | # note it's not necessary to declare lft, rgt, mvg or parent. We | ||||||
61 | # declare parent simply because it might be useful, but | ||||||
62 | # DBIx::OO:Tree works with low-level SQL therefore it doesn't | ||||||
63 | # require that the DBIx::OO object has these fields. | ||||||
64 | |||||||
65 | # the code below creates the structure presented in [1] | ||||||
66 | |||||||
67 | my $electronics = Category->tree_append({ label => 'electronics' }); | ||||||
68 | my $tvs = $electronics->tree_append({ label => 'televisions' }); | ||||||
69 | my $tube = $tvs->tree_append({ label => 'tube' }); | ||||||
70 | my $plasma = $tvs->tree_append({ label => 'plasma' }); | ||||||
71 | my $lcd = $plasma->tree_insert_before({ label => 'lcd' }); | ||||||
72 | my $portable = $tvs->tree_insert_after({ label => 'portable electronics' }); | ||||||
73 | my $mp3 = $portable->tree_append({ label => 'mp3 players' }); | ||||||
74 | my $flash = $mp3->tree_append({ label => 'flash' }); | ||||||
75 | my $cds = $portable->tree_append({ label => 'cd players' }); | ||||||
76 | my $radios = Category->tree_append($portable->id, | ||||||
77 | { label => '2 way radios' }); | ||||||
78 | |||||||
79 | # fetch and display a subtree | ||||||
80 | |||||||
81 | my $data = $electronics->tree_get_subtree({ | ||||||
82 | fields => [qw( label lft rgt parent )] | ||||||
83 | }); | ||||||
84 | my $levels = Category->tree_compute_levels($data); | ||||||
85 | |||||||
86 | foreach my $i (@$data) { | ||||||
87 | print ' ' x $levels->{$i->{id}}, $i->{label}, "\n"; | ||||||
88 | } | ||||||
89 | |||||||
90 | ## or, create DBIx::OO objects from returned data: | ||||||
91 | |||||||
92 | my $array = Category->init_from_data($data); | ||||||
93 | print join("\n", (map { ' ' x $levels->{$_->id} . $_->label } @$array)); | ||||||
94 | |||||||
95 | # display path info | ||||||
96 | |||||||
97 | my $data = $flash->tree_get_path; | ||||||
98 | print join("\n", (map { $_->{label} } @$data)); | ||||||
99 | |||||||
100 | # move nodes around | ||||||
101 | |||||||
102 | $mp3->tree_reparent($lcd->id); | ||||||
103 | $tvs->tree_reparent($portable->id); | ||||||
104 | $cds->tree_reparent(undef); | ||||||
105 | |||||||
106 | $plasma->tree_move_before($tube->id); | ||||||
107 | $portable->tree_move_before($electronics->id); | ||||||
108 | |||||||
109 | # delete nodes | ||||||
110 | |||||||
111 | $lcd->tree_delete; | ||||||
112 | |||||||
113 | =head1 OVERVIEW | ||||||
114 | |||||||
115 | This module is a complement to DBIx::OO to facilitate storing trees in | ||||||
116 | database using the "nested sets model", presented in [1]. Its main | ||||||
117 | ambition is to be extremely fast at retrieving data (sacrificing for | ||||||
118 | this the performance of UPDATE-s, INSERT-s or DELETE-s). Currently | ||||||
119 | this module B |
||||||
120 | |||||||
121 | - id: primary key (integer) | ||||||
122 | - parent: integer, references the parent node (NULL for root nodes) | ||||||
123 | - lft, rgt: store the node position | ||||||
124 | - mvg: used only when moving nodes | ||||||
125 | |||||||
126 | "parent" and "mvg" are not esentially required by the nested sets | ||||||
127 | model as presented in [1], but they are necessary for this module to | ||||||
128 | work. In particular, "mvg" is only required by functions that move | ||||||
129 | nodes, such as tree_reparent(). If you don't want to move nodes | ||||||
130 | around you can omit "mvg". | ||||||
131 | |||||||
132 | Retrieval functions should be very fast (one SQL executed). To | ||||||
133 | further promote speed, they don't return DBIx::OO blessed objects, but | ||||||
134 | an array of hashes instead. It's easy to create DBIx::OO objects from | ||||||
135 | these, if required, by calling DBIx::OO->init_from_data() (see | ||||||
136 | DBIx::OO for more information). | ||||||
137 | |||||||
138 | Insert/delete/move functions, however, need to ensure the tree | ||||||
139 | integrity. Here's what happens currently: | ||||||
140 | |||||||
141 | - tree_append, tree_insert_before, tree_insert_after -- these execute | ||||||
142 | one SELECT and two UPDATE-s (that potentially could affect a lot of | ||||||
143 | rows). | ||||||
144 | |||||||
145 | - tree_delete: execute one SELECT, one DELETE and two UPDATE-s. | ||||||
146 | |||||||
147 | - tree_reparent -- executes 2 SELECT-s and 7 UPDATE-s. I know, this | ||||||
148 | sounds horrible--if you have better ideas I'd love to hear them. | ||||||
149 | |||||||
150 | B |
||||||
151 | untested. You just need to provide the get_dbh() method to your | ||||||
152 | packages, comply to this module's table requirements (i.e. provide the | ||||||
153 | right columns) and it should work just fine. Any success/failure | ||||||
154 | stories are welcome. | ||||||
155 | |||||||
156 | =head1 DATABASE INTEGRITY | ||||||
157 | |||||||
158 | Since the functions that update the database need to run multiple | ||||||
159 | queries in order to maintain integrity, they should normally do this | ||||||
160 | inside a transaction. However, it looks like MySQL does not support | ||||||
161 | nested transactions, therefore if I call transaction_start / | ||||||
162 | transaction_commit inside these functions they will mess with an | ||||||
163 | eventual transaction that might have been started by the calling code. | ||||||
164 | |||||||
165 | In short: you should make sure the updates happen in a transaction, | ||||||
166 | but we can't enforce this in our module. | ||||||
167 | |||||||
168 | =head1 API | ||||||
169 | |||||||
170 | =head2 tree_append($parent_id, \%values) | ||||||
171 | |||||||
172 | Appends a new node in the subtree of the specified parent. If | ||||||
173 | $parent_id is undef, it will add a root node. When you want to add a | ||||||
174 | root node you can as well omit specifying the $parent_id (our code | ||||||
175 | will realize that the first argument is a reference). | ||||||
176 | |||||||
177 | $values is a hash as required by DBIx::OO::create(). | ||||||
178 | |||||||
179 | Examples: | ||||||
180 | |||||||
181 | $node = Category->tree_append({ label => 'electronics' }); | ||||||
182 | $node = Category->tree_append(undef, { label => 'electronics' }); | ||||||
183 | |||||||
184 | $lcd = Category->tree_append($tvs->id, { label => 'lcd' }); | ||||||
185 | $lcd->tree_append({ label => 'monitors' }); | ||||||
186 | |||||||
187 | As you can see, you can call it both as a package method or as an | ||||||
188 | object method. When you call it as a package method, it will look at | ||||||
189 | the type of the first argument. If it's a reference, it will guess | ||||||
190 | that you want to add a root node. Otherwise it will add the new node | ||||||
191 | under the specified parent. | ||||||
192 | |||||||
193 | Beware of mistakes! Do NOT call it like this: | ||||||
194 | |||||||
195 | $tvs = Category->search({ label => 'televisions' })->[0]; | ||||||
196 | Category->tree_append($tvs, { label => 'lcd' }); | ||||||
197 | |||||||
198 | If you specify a parent, it MUST be its ID, not an object! | ||||||
199 | |||||||
200 | =cut | ||||||
201 | |||||||
202 | sub tree_append { | ||||||
203 | 0 | 0 | 1 | my $self = shift; | |||
204 | 0 | my ($parent, $val); | |||||
205 | 0 | 0 | if (ref $self) { | ||||
206 | 0 | $parent = $self->id; | |||||
207 | } else { | ||||||
208 | 0 | $parent = shift; | |||||
209 | 0 | 0 | if (ref $parent eq 'HASH') { | ||||
0 | |||||||
210 | # assuming $val and no parent | ||||||
211 | 0 | $val = $parent; | |||||
212 | 0 | $parent = undef; | |||||
213 | } elsif (ref $parent) { | ||||||
214 | 0 | $parent = $parent->id; | |||||
215 | } | ||||||
216 | } | ||||||
217 | 0 | 0 | $val ||= shift; | ||||
218 | |||||||
219 | 0 | my $orig = 0; | |||||
220 | 0 | my $dbh = $self->get_dbh; | |||||
221 | 0 | my $table = $self->table; | |||||
222 | |||||||
223 | 0 | 0 | if (defined $parent) { | ||||
224 | 0 | my $a = $dbh->selectrow_arrayref("SELECT rgt FROM `$table` WHERE id = $parent"); | |||||
225 | 0 | $orig = $a->[0] - 1; | |||||
226 | 0 | $dbh->do("UPDATE `$table` SET rgt = rgt + 2 WHERE rgt > $orig"); | |||||
227 | 0 | $dbh->do("UPDATE `$table` SET lft = lft + 2 WHERE lft > $orig"); | |||||
228 | } else { | ||||||
229 | 0 | my $a = $dbh->selectrow_arrayref("SELECT MAX(rgt) FROM `$table` WHERE parent IS NULL"); | |||||
230 | 0 | 0 | 0 | $orig = $a ? ($a->[0] || 0) : 0; | |||
231 | } | ||||||
232 | |||||||
233 | 0 | delete $val->{lft}; | |||||
234 | 0 | delete $val->{rgt}; | |||||
235 | 0 | delete $val->{mvg}; | |||||
236 | 0 | delete $val->{parent}; | |||||
237 | |||||||
238 | 0 | my %args = ( lft => $orig + 1, | |||||
239 | rgt => $orig + 2, | ||||||
240 | parent => $parent ); | ||||||
241 | 0 | 0 | @args{keys %$val} = values %$val | ||||
242 | if $val; | ||||||
243 | 0 | return $self->create(\%args); | |||||
244 | } | ||||||
245 | |||||||
246 | =head2 tree_insert_before, tree_insert_after ($anchor, \%values) | ||||||
247 | |||||||
248 | Similar in function to tree_append, but these functions allow you to | ||||||
249 | insert a node before or after a specified node ($anchor). | ||||||
250 | |||||||
251 | Examples: | ||||||
252 | |||||||
253 | $lcd->tree_insert_after({ label => 'plasma' }); | ||||||
254 | $lcd->tree_insert_before({ label => 'tube' }); | ||||||
255 | |||||||
256 | # Or, as a package method: | ||||||
257 | |||||||
258 | Category->tree_insert_after($lcd->id, { label => 'plasma' }); | ||||||
259 | Category->tree_insert_before($lcd->id, { label => 'tube' }); | ||||||
260 | |||||||
261 | Note that specifying the parent is not required, because it's clearly | ||||||
262 | that the new node should have the same parent as the anchor node. | ||||||
263 | |||||||
264 | =cut | ||||||
265 | |||||||
266 | sub tree_insert_before { | ||||||
267 | 0 | 0 | 1 | my $self = shift; | |||
268 | 0 | my ($pos, $val); | |||||
269 | 0 | 0 | if (ref $self) { | ||||
270 | 0 | $pos = $self->id; | |||||
271 | } else { | ||||||
272 | 0 | $pos = shift; | |||||
273 | } | ||||||
274 | 0 | $val = shift; | |||||
275 | |||||||
276 | 0 | 0 | Carp::croak('$pos MUST be a scalar (the ID of the referred node)') | ||||
277 | if ref $pos; | ||||||
278 | |||||||
279 | 0 | my $dbh = $self->get_dbh; | |||||
280 | 0 | my $table = $self->table; | |||||
281 | |||||||
282 | 0 | my $a = $dbh->selectrow_arrayref("SELECT lft, parent FROM `$table` WHERE id = $pos"); | |||||
283 | 0 | my ($orig, $parent) = @$a; | |||||
284 | |||||||
285 | 0 | $dbh->do("UPDATE `$table` SET rgt = rgt + 2 WHERE rgt >= $orig"); | |||||
286 | 0 | $dbh->do("UPDATE `$table` SET lft = lft + 2 WHERE lft >= $orig"); | |||||
287 | |||||||
288 | 0 | delete $val->{lft}; | |||||
289 | 0 | delete $val->{rgt}; | |||||
290 | 0 | delete $val->{mvg}; | |||||
291 | 0 | delete $val->{parent}; | |||||
292 | |||||||
293 | 0 | my %args = ( lft => $orig, | |||||
294 | rgt => $orig + 1, | ||||||
295 | parent => $parent ); | ||||||
296 | 0 | 0 | @args{keys %$val} = values %$val | ||||
297 | if $val; | ||||||
298 | 0 | return $self->create(\%args); | |||||
299 | } | ||||||
300 | |||||||
301 | sub tree_insert_after { | ||||||
302 | 0 | 0 | 1 | my $self = shift; | |||
303 | 0 | my ($pos, $val); | |||||
304 | 0 | 0 | if (ref $self) { | ||||
305 | 0 | $pos = $self->id; | |||||
306 | } else { | ||||||
307 | 0 | $pos = shift; | |||||
308 | } | ||||||
309 | 0 | $val = shift; | |||||
310 | |||||||
311 | 0 | 0 | Carp::croak('$pos MUST be a scalar (the ID of the referred node)') | ||||
312 | if ref $pos; | ||||||
313 | |||||||
314 | 0 | my $dbh = $self->get_dbh; | |||||
315 | 0 | my $table = $self->table; | |||||
316 | |||||||
317 | 0 | my $a = $dbh->selectrow_arrayref("SELECT rgt, parent FROM `$table` WHERE id = $pos"); | |||||
318 | 0 | my ($orig, $parent) = @$a; | |||||
319 | |||||||
320 | 0 | $dbh->do("UPDATE `$table` SET rgt = rgt + 2 WHERE rgt > $orig"); | |||||
321 | 0 | $dbh->do("UPDATE `$table` SET lft = lft + 2 WHERE lft > $orig"); | |||||
322 | |||||||
323 | 0 | delete $val->{lft}; | |||||
324 | 0 | delete $val->{rgt}; | |||||
325 | 0 | delete $val->{mvg}; | |||||
326 | 0 | delete $val->{parent}; | |||||
327 | |||||||
328 | 0 | my %args = ( lft => $orig + 1, | |||||
329 | rgt => $orig + 2, | ||||||
330 | parent => $parent ); | ||||||
331 | 0 | 0 | @args{keys %$val} = values %$val | ||||
332 | if $val; | ||||||
333 | 0 | return $self->create(\%args); | |||||
334 | } | ||||||
335 | |||||||
336 | =head2 tree_reparent($source_id, $dest_id) | ||||||
337 | |||||||
338 | This function will remove the $source node from its current parent | ||||||
339 | and append it to the $dest node. As with the other functions, you can | ||||||
340 | call it both as a package method or as an object method. When you | ||||||
341 | call it as an object method, it's not necessary to specify $source. | ||||||
342 | |||||||
343 | You can specify I |
||||||
344 | become a root node (as if it would be appended with | ||||||
345 | tree_append(undef)). | ||||||
346 | |||||||
347 | No nodes are DELETE-ed nor INSERT-ed by this function. It simply | ||||||
348 | moves I |
||||||
349 | happen to have should remain valid and point to the same nodes. | ||||||
350 | However, the tree structure is changed, so if you maintain the tree in | ||||||
351 | memory you have to update it after calling this funciton. Same | ||||||
352 | applies to tree_move_before() and tree_move_after(). | ||||||
353 | |||||||
354 | Examples: | ||||||
355 | |||||||
356 | # the following are equivalent | ||||||
357 | |||||||
358 | Category->tree_reparent($lcd->id, $plasma->id); | ||||||
359 | $lcd->tree_reparent($plasma->id); | ||||||
360 | |||||||
361 | This function does a lot of work in order to maintain the tree | ||||||
362 | integrity, therefore it might be slow. | ||||||
363 | |||||||
364 | NOTE: it doesn't do any safety checks to make sure moving the node is | ||||||
365 | allowed. For instance, you can't move a node to one of its child | ||||||
366 | nodes. | ||||||
367 | |||||||
368 | =cut | ||||||
369 | |||||||
370 | # sub _check_can_move { | ||||||
371 | # my ($src_lft, $dest_lft, $dest_rgt) = @_; | ||||||
372 | # } | ||||||
373 | |||||||
374 | sub tree_reparent { | ||||||
375 | 0 | 0 | 1 | my $self = shift; | |||
376 | 0 | my ($source, $dest); | |||||
377 | 0 | 0 | if (ref $self) { | ||||
378 | 0 | $source = $self->id; | |||||
379 | } else { | ||||||
380 | 0 | $source = shift; | |||||
381 | } | ||||||
382 | 0 | $dest = shift; | |||||
383 | |||||||
384 | 0 | 0 | 0 | Carp::croak('arguments MUST be scalars (source and destination parent node IDs)') | |||
385 | if ref $dest or ref $source; | ||||||
386 | |||||||
387 | 0 | my $dbh = $self->get_dbh; | |||||
388 | 0 | my $table = $self->table; | |||||
389 | |||||||
390 | # get source info | ||||||
391 | 0 | my $a = $dbh->selectrow_arrayref("SELECT lft, rgt FROM `$table` WHERE id = $source"); | |||||
392 | 0 | my ($orig_left, $orig_right) = @$a; | |||||
393 | 0 | my $width = $orig_right - $orig_left + 1; | |||||
394 | |||||||
395 | # hint to ignore subtree items in further computation | ||||||
396 | 0 | $dbh->do("UPDATE `$table` SET mvg = 1 WHERE lft BETWEEN $orig_left AND $orig_right"); | |||||
397 | |||||||
398 | # "collapse" tree by reducing rgt and lft for nodes after the removed one | ||||||
399 | 0 | $dbh->do("UPDATE `$table` SET rgt = rgt - $width WHERE rgt > $orig_right"); | |||||
400 | 0 | $dbh->do("UPDATE `$table` SET lft = lft - $width WHERE lft > $orig_right"); | |||||
401 | |||||||
402 | 0 | my $diff; | |||||
403 | |||||||
404 | 0 | 0 | if (defined $dest) { | ||||
405 | # get destination info (it's important to do it here as it can be modified by the UPDATE-s above) | ||||||
406 | 0 | $a = $dbh->selectrow_arrayref("SELECT rgt FROM `$table` WHERE id = $dest"); | |||||
407 | 0 | my ($dest_right) = @$a; | |||||
408 | 0 | $diff = $dest_right - $orig_left; | |||||
409 | |||||||
410 | 0 | $dbh->do("UPDATE `$table` SET rgt = rgt + $width WHERE NOT mvg AND rgt >= $dest_right"); | |||||
411 | 0 | $dbh->do("UPDATE `$table` SET lft = lft + $width WHERE NOT mvg AND lft >= $dest_right"); | |||||
412 | } else { | ||||||
413 | # appending a root node | ||||||
414 | 0 | my $a = $dbh->selectrow_arrayref("SELECT MAX(rgt) FROM `$table` WHERE parent IS NULL"); | |||||
415 | 0 | my ($dest_right) = @$a; | |||||
416 | 0 | $diff = $dest_right - $orig_left + 1; | |||||
417 | 0 | $dest = 'NULL'; | |||||
418 | } | ||||||
419 | |||||||
420 | # finally, update subtree items and remove the ignore hint | ||||||
421 | 0 | $dbh->do("UPDATE `$table` SET lft = lft + $diff, rgt = rgt + $diff, mvg = 0 WHERE mvg"); | |||||
422 | 0 | $dbh->do("UPDATE `$table` SET parent = $dest WHERE id = $source"); | |||||
423 | } | ||||||
424 | |||||||
425 | =head2 tree_move_before, tree_move_after ($source_id, $anchor_id) | ||||||
426 | |||||||
427 | These functions are similar to a reparent operation, but they allow | ||||||
428 | one to specify I |
||||||
429 | $anchor's parent. See tree_reparent(). | ||||||
430 | |||||||
431 | Examples: | ||||||
432 | |||||||
433 | $portable->tree_move_before($electronics->id); | ||||||
434 | Category->tree_move_after($lcd->id, $flash->id); | ||||||
435 | |||||||
436 | =cut | ||||||
437 | |||||||
438 | sub tree_move_before { | ||||||
439 | 0 | 0 | 1 | my ($self) = shift; | |||
440 | 0 | my ($source, $anchor); | |||||
441 | 0 | 0 | if (ref $self) { | ||||
442 | 0 | $source = $self->id; | |||||
443 | } else { | ||||||
444 | 0 | $source = shift; | |||||
445 | } | ||||||
446 | 0 | $anchor = shift; | |||||
447 | |||||||
448 | 0 | 0 | 0 | Carp::croak('arguments MUST be scalars (source and destination parent node IDs)') | |||
449 | if ref $anchor or ref $source; | ||||||
450 | |||||||
451 | 0 | my $dbh = $self->get_dbh; | |||||
452 | 0 | my $table = $self->table; | |||||
453 | |||||||
454 | # get source info | ||||||
455 | 0 | my $a = $dbh->selectrow_arrayref("SELECT lft, rgt FROM `$table` WHERE id = $source"); | |||||
456 | 0 | my ($orig_left, $orig_right) = @$a; | |||||
457 | 0 | my $width = $orig_right - $orig_left + 1; | |||||
458 | |||||||
459 | # hint to ignore subtree items in further computation | ||||||
460 | 0 | $dbh->do("UPDATE `$table` SET mvg = 1 WHERE lft BETWEEN $orig_left AND $orig_right"); | |||||
461 | |||||||
462 | # "collapse" tree by reducing rgt and lft for nodes after the removed one | ||||||
463 | 0 | $dbh->do("UPDATE `$table` SET rgt = rgt - $width WHERE rgt > $orig_right"); | |||||
464 | 0 | $dbh->do("UPDATE `$table` SET lft = lft - $width WHERE lft > $orig_right"); | |||||
465 | |||||||
466 | # get destination info (it's important to do it here as it can be modified by the UPDATE-s above) | ||||||
467 | 0 | $a = $dbh->selectrow_arrayref("SELECT lft, parent FROM `$table` WHERE id = $anchor"); | |||||
468 | 0 | my ($dest_left, $dest_parent) = @$a; | |||||
469 | 0 | 0 | if (!defined $dest_parent) { | ||||
470 | 0 | $dest_parent = 'NULL'; | |||||
471 | } | ||||||
472 | 0 | my $diff = $dest_left - $orig_left; | |||||
473 | |||||||
474 | 0 | $dbh->do("UPDATE `$table` SET rgt = rgt + $width WHERE NOT mvg AND rgt >= $dest_left"); | |||||
475 | 0 | $dbh->do("UPDATE `$table` SET lft = lft + $width WHERE NOT mvg AND lft >= $dest_left"); | |||||
476 | |||||||
477 | # finally, update subtree items and remove the ignore hint | ||||||
478 | 0 | $dbh->do("UPDATE `$table` SET lft = lft + $diff, rgt = rgt + $diff, mvg = 0 WHERE mvg"); | |||||
479 | 0 | $dbh->do("UPDATE `$table` SET parent = $dest_parent WHERE id = $source"); | |||||
480 | } | ||||||
481 | |||||||
482 | sub tree_move_after { | ||||||
483 | 0 | 0 | 1 | my ($self) = shift; | |||
484 | 0 | my ($source, $anchor); | |||||
485 | 0 | 0 | if (ref $self) { | ||||
486 | 0 | $source = $self->id; | |||||
487 | } else { | ||||||
488 | 0 | $source = shift; | |||||
489 | } | ||||||
490 | 0 | $anchor = shift; | |||||
491 | |||||||
492 | 0 | 0 | 0 | Carp::croak('arguments MUST be scalars (source and destination parent node IDs)') | |||
493 | if ref $anchor or ref $source; | ||||||
494 | |||||||
495 | 0 | my $dbh = $self->get_dbh; | |||||
496 | 0 | my $table = $self->table; | |||||
497 | |||||||
498 | # get source info | ||||||
499 | 0 | my $a = $dbh->selectrow_arrayref("SELECT lft, rgt FROM `$table` WHERE id = $source"); | |||||
500 | 0 | my ($orig_left, $orig_right) = @$a; | |||||
501 | 0 | my $width = $orig_right - $orig_left + 1; | |||||
502 | |||||||
503 | # hint to ignore subtree items in further computation | ||||||
504 | 0 | $dbh->do("UPDATE `$table` SET mvg = 1 WHERE lft BETWEEN $orig_left AND $orig_right"); | |||||
505 | |||||||
506 | # "collapse" tree by reducing rgt and lft for nodes after the removed one | ||||||
507 | 0 | $dbh->do("UPDATE `$table` SET rgt = rgt - $width WHERE rgt > $orig_right"); | |||||
508 | 0 | $dbh->do("UPDATE `$table` SET lft = lft - $width WHERE lft > $orig_right"); | |||||
509 | |||||||
510 | # get destination info (it's important to do it here as it can be modified by the UPDATE-s above) | ||||||
511 | 0 | $a = $dbh->selectrow_arrayref("SELECT rgt, parent FROM `$table` WHERE id = $anchor"); | |||||
512 | 0 | my ($dest_right, $dest_parent) = @$a; | |||||
513 | 0 | 0 | if (!defined $dest_parent) { | ||||
514 | 0 | $dest_parent = 'NULL'; | |||||
515 | } | ||||||
516 | 0 | my $diff = $dest_right + 1 - $orig_left; | |||||
517 | |||||||
518 | 0 | $dbh->do("UPDATE `$table` SET rgt = rgt + $width WHERE NOT mvg AND rgt > $dest_right"); | |||||
519 | 0 | $dbh->do("UPDATE `$table` SET lft = lft + $width WHERE NOT mvg AND lft > $dest_right"); | |||||
520 | |||||||
521 | # finally, update subtree items and remove the ignore hint | ||||||
522 | 0 | $dbh->do("UPDATE `$table` SET lft = lft + $diff, rgt = rgt + $diff, mvg = 0 WHERE mvg"); | |||||
523 | 0 | $dbh->do("UPDATE `$table` SET parent = $dest_parent WHERE id = $source"); | |||||
524 | } | ||||||
525 | |||||||
526 | =head2 tree_delete($node_id) | ||||||
527 | |||||||
528 | Removes a node (and its full subtree) from the database. | ||||||
529 | |||||||
530 | Equivalent examples: | ||||||
531 | |||||||
532 | Category->tree_delete($lcd->id); | ||||||
533 | $lcd->tree_delete; | ||||||
534 | |||||||
535 | =cut | ||||||
536 | |||||||
537 | sub tree_delete { | ||||||
538 | 0 | 0 | 1 | my ($self) = shift; | |||
539 | 0 | my $id; | |||||
540 | 0 | 0 | if (ref $self) { | ||||
541 | 0 | $id = $self->id; | |||||
542 | } else { | ||||||
543 | 0 | $id = shift; | |||||
544 | } | ||||||
545 | |||||||
546 | 0 | my $dbh = $self->get_dbh; | |||||
547 | 0 | my $table = $self->table; | |||||
548 | |||||||
549 | 0 | my $a = $dbh->selectrow_arrayref("SELECT lft, rgt FROM `$table` WHERE id = $id"); | |||||
550 | 0 | my ($left, $right) = @$a; | |||||
551 | 0 | my $width = $right - $left + 1; | |||||
552 | |||||||
553 | 0 | $dbh->do("DELETE FROM `$table` WHERE lft BETWEEN $left AND $right"); | |||||
554 | 0 | $dbh->do("UPDATE `$table` SET rgt = rgt - $width WHERE rgt > $right"); | |||||
555 | 0 | $dbh->do("UPDATE `$table` SET lft = lft - $width WHERE lft > $right"); | |||||
556 | } | ||||||
557 | |||||||
558 | =head2 tree_get_subtree(\%args) | ||||||
559 | |||||||
560 | Retrieves the full subtree of a specified node. $args is a hashref | ||||||
561 | that can contain: | ||||||
562 | |||||||
563 | - parent : the ID of the node whose subtree we want to get | ||||||
564 | - where : an WHERE clause in SQL::Abstract format | ||||||
565 | - limit : allows you to limit the results (using SQL LIMIT) | ||||||
566 | - offset : SQL OFFSET | ||||||
567 | - fields : (arrayref) allows you to specify a list of fields you're | ||||||
568 | interested in | ||||||
569 | |||||||
570 | This can be called as a package method, or as an object method. | ||||||
571 | |||||||
572 | Examples first: | ||||||
573 | |||||||
574 | $all_nodes = Category->tree_get_subtree; | ||||||
575 | |||||||
576 | $nodes = Category->tree_get_subtree({ parent => $portable->id }); | ||||||
577 | ## OR | ||||||
578 | $nodes = $portable->tree_get_subtree; | ||||||
579 | |||||||
580 | # Filtering: | ||||||
581 | $nodes = Category->tree_get_subtree({ where => { label => { -like => '%a%' }}}); | ||||||
582 | |||||||
583 | # Specify fields: | ||||||
584 | $nodes = Category->tree_get_subtree({ fields => [ 'label' ] }); | ||||||
585 | |||||||
586 | This function returns an array of hashes that contain the fields you | ||||||
587 | required. If you specify no fields, 'id' and 'parent' will be | ||||||
588 | SELECT-ed by default. Even if you do specify an array of field names, | ||||||
589 | 'id' and 'parent' would still be included in the SELECT (so you don't | ||||||
590 | want to specify them). | ||||||
591 | |||||||
592 | Using this array you can easily create DBIx::OO objects (or in our | ||||||
593 | sample, Category objects): | ||||||
594 | |||||||
595 | $arrayref = Category->init_from_data($nodes); | ||||||
596 | |||||||
597 | OK, let's get to a more real-world example. Suppose we have a forum | ||||||
598 | and we need to list all messages in a thread ($thread_id). Here's | ||||||
599 | what we're going to do: | ||||||
600 | |||||||
601 | $data = ForumMessage->tree_get_subtree({ | ||||||
602 | parent => $thread_id, | ||||||
603 | fields => [qw( subject body author date )], | ||||||
604 | }); | ||||||
605 | |||||||
606 | # the above runs one SQL query | ||||||
607 | |||||||
608 | $objects = ForumMessage->init_from_data($data); | ||||||
609 | |||||||
610 | # the above simply initializes ForumMessage objects from the | ||||||
611 | # returned data, B |
||||||
612 | # the primary key automatically selected by tree_get_subtree, and | ||||||
613 | # also have cared to select the fields we're going to use). | ||||||
614 | |||||||
615 | # compute the level of each message, to indent them easily | ||||||
616 | |||||||
617 | $levels = ForumMessage->tree_compute_levels($data); | ||||||
618 | |||||||
619 | # and now display them | ||||||
620 | |||||||
621 | foreach my $msg (@$objects) { | ||||||
622 | my $class = 'level' . $levels{$msg->id}; | ||||||
623 | print " ", $msg->subject, " ", |
||||||
624 | $msg->body, " By: ", $msg->author, ""; |
||||||
625 | } | ||||||
626 | |||||||
627 | # and indentation is now a matter of CSS. ;-) (define level0, | ||||||
628 | # level1, level2, etc.) | ||||||
629 | |||||||
630 | All this can be done with a single SQL query. Of course, note that we | ||||||
631 | didn't even need to initialize the $objects array--that's mainly | ||||||
632 | useful when you want to update the database. | ||||||
633 | |||||||
634 | =cut | ||||||
635 | |||||||
636 | sub tree_get_subtree { | ||||||
637 | 0 | 0 | 1 | my ($self, $args) = @_; | |||
638 | 0 | my ($parent, $where, $order); | |||||
639 | 0 | 0 | if (defined $args->{parent}) { | ||||
0 | |||||||
640 | 0 | $parent = $args->{parent} | |||||
641 | } elsif (ref $self) { | ||||||
642 | 0 | $parent = $self->id; | |||||
643 | } | ||||||
644 | 0 | $where = $args->{where}; | |||||
645 | 0 | 0 | $order = $args->{order} || 'TREE_NODE.lft'; | ||||
646 | 0 | 0 | if (defined $parent) { | ||||
647 | 0 | 0 | $where ||= {}; | ||||
648 | 0 | $where->{'TREE_PARENT.id'} = $parent; | |||||
649 | } | ||||||
650 | 0 | my @keys = qw(id parent lft rgt); | |||||
651 | 0 | 0 | push @keys, @{$args->{fields}} | ||||
0 | |||||||
652 | if ($args->{fields}); | ||||||
653 | 0 | my @fields = map { "TREE_NODE.`$_`" } @keys; | |||||
0 | |||||||
654 | 0 | my $sa = $self->get_sql_abstract; | |||||
655 | 0 | my @bind; | |||||
656 | 0 | 0 | if ($where) { | ||||
657 | 0 | ($where, @bind) = $sa->where($where); | |||||
658 | } else { | ||||||
659 | 0 | $where = ''; | |||||
660 | } | ||||||
661 | 0 | my $table = $self->table; | |||||
662 | 0 | my $select = 'SELECT ' . join(', ', @fields) . " FROM `$table` AS TREE_NODE INNER JOIN `$table` AS TREE_PARENT " . | |||||
663 | 'ON TREE_NODE.lft BETWEEN TREE_PARENT.lft AND TREE_PARENT.rgt' . | ||||||
664 | $where . | ||||||
665 | ' GROUP BY TREE_NODE.lft' . | ||||||
666 | $sa->order_and_limit($order, $args->{limit}, $args->{offset}); | ||||||
667 | 0 | my $sth = $self->_run_sql($select, \@bind); | |||||
668 | 0 | my @ret = (); | |||||
669 | 0 | while (my $row = $sth->fetchrow_arrayref) { | |||||
670 | 0 | my %h; | |||||
671 | 0 | @h{@keys} = @$row; | |||||
672 | 0 | push @ret, \%h; | |||||
673 | } | ||||||
674 | 0 | 0 | return wantarray ? @ret : \@ret; | ||||
675 | } | ||||||
676 | |||||||
677 | =head2 tree_get_path(\%args) | ||||||
678 | |||||||
679 | Retrieves the path of a given node. $args is an hashref that can | ||||||
680 | contain: | ||||||
681 | |||||||
682 | - id : the ID of the node whose path you're interested in | ||||||
683 | - fields : array of field names to be SELECT-ed (same like | ||||||
684 | tree_get_subtree) | ||||||
685 | |||||||
686 | This returns data in the same format as tree_get_subtree(). | ||||||
687 | |||||||
688 | =cut | ||||||
689 | |||||||
690 | sub tree_get_path { | ||||||
691 | 0 | 0 | 1 | my ($self, $args) = @_; | |||
692 | 0 | my $id; | |||||
693 | 0 | 0 | if (defined $args->{id}) { | ||||
0 | |||||||
694 | 0 | $id = $args->{id}; | |||||
695 | } elsif (ref $self) { | ||||||
696 | 0 | $id = $self->id; | |||||
697 | } | ||||||
698 | 0 | my @keys = qw(id parent lft rgt); | |||||
699 | 0 | 0 | push @keys, @{$args->{fields}} | ||||
0 | |||||||
700 | if ($args->{fields}); | ||||||
701 | 0 | my @fields = map { "TREE_PARENT.`$_`" } @keys; | |||||
0 | |||||||
702 | 0 | my $table = $self->table; | |||||
703 | 0 | my $select = 'SELECT ' . join(', ', @fields) . " FROM `$table` AS TREE_NODE INNER JOIN `$table` AS TREE_PARENT " . | |||||
704 | 'ON TREE_NODE.lft BETWEEN TREE_PARENT.lft AND TREE_PARENT.rgt' . | ||||||
705 | " WHERE TREE_NODE.id = $id ORDER BY TREE_PARENT.lft"; | ||||||
706 | 0 | my $sth = $self->_run_sql($select); | |||||
707 | 0 | my @ret = (); | |||||
708 | 0 | while (my $row = $sth->fetchrow_arrayref) { | |||||
709 | 0 | my %h; | |||||
710 | 0 | @h{@keys} = @$row; | |||||
711 | 0 | push @ret, \%h; | |||||
712 | } | ||||||
713 | 0 | 0 | return wantarray ? @ret : \@ret; | ||||
714 | } | ||||||
715 | |||||||
716 | =head2 tree_get_next_sibling, tree_get_prev_sibling | ||||||
717 | |||||||
718 | XXX: this info may be inaccurate | ||||||
719 | |||||||
720 | Return the next/previous item in the tree view. C<$args> has the same | ||||||
721 | significance as in L. $args->{id} defines the | ||||||
722 | reference node; if missing, it's assumed to be $self. | ||||||
723 | |||||||
724 | =cut | ||||||
725 | |||||||
726 | sub tree_get_next_sibling { | ||||||
727 | 0 | 0 | 1 | my ($self, $args) = @_; | |||
728 | 0 | my $id; | |||||
729 | 0 | 0 | if (defined $args->{id}) { | ||||
0 | |||||||
730 | 0 | $id = $args->{id}; | |||||
731 | } elsif (ref $self) { | ||||||
732 | 0 | $id = $self->id; | |||||
733 | } | ||||||
734 | 0 | my @keys = qw(id parent lft rgt); | |||||
735 | 0 | 0 | push @keys, @{$args->{fields}} | ||||
0 | |||||||
736 | if ($args->{fields}); | ||||||
737 | 0 | my @fields = map { "T1.`$_`" } @keys; | |||||
0 | |||||||
738 | 0 | my $table = $self->table; | |||||
739 | 0 | my $select = 'SELECT ' . join(', ', @fields) . " FROM `$table` AS T1 INNER JOIN `$table` AS T2 " . | |||||
740 | 'ON T1.lft = T2.rgt + 1' . | ||||||
741 | " WHERE T2.id = $id LIMIT 1"; | ||||||
742 | 0 | my $sth = $self->_run_sql($select); | |||||
743 | 0 | my @ret = (); | |||||
744 | 0 | my $row = $sth->fetchrow_arrayref; | |||||
745 | 0 | 0 | if ($row) { | ||||
746 | 0 | my %h; | |||||
747 | 0 | @h{@keys} = @$row; | |||||
748 | 0 | return \%h; | |||||
749 | } | ||||||
750 | 0 | return undef; | |||||
751 | } | ||||||
752 | |||||||
753 | sub tree_get_prev_sibling { | ||||||
754 | 0 | 0 | 1 | my ($self, $args) = @_; | |||
755 | 0 | my $id; | |||||
756 | 0 | 0 | if (defined $args->{id}) { | ||||
0 | |||||||
757 | 0 | $id = $args->{id}; | |||||
758 | } elsif (ref $self) { | ||||||
759 | 0 | $id = $self->id; | |||||
760 | } | ||||||
761 | 0 | my @keys = qw(id parent lft rgt); | |||||
762 | 0 | 0 | push @keys, @{$args->{fields}} | ||||
0 | |||||||
763 | if ($args->{fields}); | ||||||
764 | 0 | my @fields = map { "T1.`$_`" } @keys; | |||||
0 | |||||||
765 | 0 | my $table = $self->table; | |||||
766 | 0 | my $select = 'SELECT ' . join(', ', @fields) . " FROM `$table` AS T1 INNER JOIN `$table` AS T2 " . | |||||
767 | 'ON T1.rgt = T2.lft - 1' . | ||||||
768 | " WHERE T2.id = $id LIMIT 1"; | ||||||
769 | 0 | my $sth = $self->_run_sql($select); | |||||
770 | 0 | my @ret = (); | |||||
771 | 0 | my $row = $sth->fetchrow_arrayref; | |||||
772 | 0 | 0 | if ($row) { | ||||
773 | 0 | my %h; | |||||
774 | 0 | @h{@keys} = @$row; | |||||
775 | 0 | return \%h; | |||||
776 | } | ||||||
777 | 0 | return undef; | |||||
778 | } | ||||||
779 | |||||||
780 | =head2 tree_get_next, tree_get_prev | ||||||
781 | |||||||
782 | XXX: this info may be inaccurate | ||||||
783 | |||||||
784 | Similar to L / L but | ||||||
785 | allow $args->{where} to contain a WHERE clause (in SQL::Abstract | ||||||
786 | format) and returns the next/prev item that matches the criteria. | ||||||
787 | |||||||
788 | =cut | ||||||
789 | |||||||
790 | sub tree_get_next { | ||||||
791 | 0 | 0 | 1 | my ($self, $args) = @_; | |||
792 | 0 | my $id; | |||||
793 | 0 | 0 | if (defined $args->{id}) { | ||||
0 | |||||||
794 | 0 | $id = $args->{id}; | |||||
795 | } elsif (ref $self) { | ||||||
796 | 0 | $id = $self->id; | |||||
797 | } | ||||||
798 | 0 | my $where = $args->{where}; | |||||
799 | 0 | my @bind; | |||||
800 | 0 | my $sa = $self->get_sql_abstract; | |||||
801 | 0 | 0 | if ($where) { | ||||
802 | 0 | ($where, @bind) = $sa->where($where); | |||||
803 | } | ||||||
804 | 0 | my @keys = qw(id parent lft rgt); | |||||
805 | 0 | 0 | push @keys, @{$args->{fields}} | ||||
0 | |||||||
806 | if ($args->{fields}); | ||||||
807 | 0 | my @fields = map { "T1.`$_`" } @keys; | |||||
0 | |||||||
808 | 0 | my $table = $self->table; | |||||
809 | 0 | my $select = 'SELECT ' . join(', ', @fields) . " FROM `$table` AS T1 INNER JOIN `$table` AS T2 " . | |||||
810 | "ON T1.lft > T2.lft AND T2.id = $id $where ORDER BY T1.lft LIMIT 1"; | ||||||
811 | 0 | my $sth = $self->_run_sql($select, \@bind); | |||||
812 | 0 | my @ret = (); | |||||
813 | 0 | my $row = $sth->fetchrow_arrayref; | |||||
814 | 0 | 0 | if ($row) { | ||||
815 | 0 | my %h; | |||||
816 | 0 | @h{@keys} = @$row; | |||||
817 | 0 | return \%h; | |||||
818 | } | ||||||
819 | 0 | return undef; | |||||
820 | } | ||||||
821 | |||||||
822 | sub tree_get_prev { | ||||||
823 | 0 | 0 | 1 | my ($self, $args) = @_; | |||
824 | 0 | my $id; | |||||
825 | 0 | 0 | if (defined $args->{id}) { | ||||
0 | |||||||
826 | 0 | $id = $args->{id}; | |||||
827 | } elsif (ref $self) { | ||||||
828 | 0 | $id = $self->id; | |||||
829 | } | ||||||
830 | 0 | my $where = $args->{where}; | |||||
831 | 0 | my @bind; | |||||
832 | 0 | my $sa = $self->get_sql_abstract; | |||||
833 | 0 | 0 | if ($where) { | ||||
834 | 0 | ($where, @bind) = $sa->where($where); | |||||
835 | } | ||||||
836 | 0 | my @keys = qw(id parent lft rgt); | |||||
837 | 0 | 0 | push @keys, @{$args->{fields}} | ||||
0 | |||||||
838 | if ($args->{fields}); | ||||||
839 | 0 | my @fields = map { "T1.`$_`" } @keys; | |||||
0 | |||||||
840 | 0 | my $table = $self->table; | |||||
841 | 0 | my $select = 'SELECT ' . join(', ', @fields) . " FROM `$table` AS T1 INNER JOIN `$table` AS T2 " . | |||||
842 | "ON T1.lft < T2.lft AND T2.id = $id $where ORDER BY T1.lft DESC LIMIT 1"; | ||||||
843 | 0 | my $sth = $self->_run_sql($select, \@bind); | |||||
844 | 0 | my @ret = (); | |||||
845 | 0 | my $row = $sth->fetchrow_arrayref; | |||||
846 | 0 | 0 | if ($row) { | ||||
847 | 0 | my %h; | |||||
848 | 0 | @h{@keys} = @$row; | |||||
849 | 0 | return \%h; | |||||
850 | } | ||||||
851 | 0 | return undef; | |||||
852 | } | ||||||
853 | |||||||
854 | =head2 tree_compute_levels($data) | ||||||
855 | |||||||
856 | This is an utility function that computes the level of each node in | ||||||
857 | $data (where $data is an array reference as returned by | ||||||
858 | tree_get_subtree or tree_get_path). | ||||||
859 | |||||||
860 | This is generic, and it's simply for convenience--in particular cases | ||||||
861 | you might find it faster to compute the levels yourself. | ||||||
862 | |||||||
863 | It returns an hashref that maps node ID to its level. | ||||||
864 | |||||||
865 | In [1] we can see there is a method to compute the subtree depth | ||||||
866 | directly in SQL, I will paste the relevant code here: | ||||||
867 | |||||||
868 | SELECT node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth | ||||||
869 | FROM nested_category AS node, | ||||||
870 | nested_category AS parent, | ||||||
871 | nested_category AS sub_parent, | ||||||
872 | ( | ||||||
873 | SELECT node.name, (COUNT(parent.name) - 1) AS depth | ||||||
874 | FROM nested_category AS node, | ||||||
875 | nested_category AS parent | ||||||
876 | WHERE node.lft BETWEEN parent.lft AND parent.rgt | ||||||
877 | AND node.name = 'PORTABLE ELECTRONICS' | ||||||
878 | GROUP BY node.name | ||||||
879 | ORDER BY node.lft | ||||||
880 | )AS sub_tree | ||||||
881 | WHERE node.lft BETWEEN parent.lft AND parent.rgt | ||||||
882 | AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt | ||||||
883 | AND sub_parent.name = sub_tree.name | ||||||
884 | GROUP BY node.name | ||||||
885 | ORDER BY node.lft; | ||||||
886 | |||||||
887 | I find it horrible. | ||||||
888 | |||||||
889 | =cut | ||||||
890 | |||||||
891 | sub tree_compute_levels { | ||||||
892 | 0 | 0 | 1 | my ($self, $data) = @_; | |||
893 | 0 | my %levels = (); | |||||
894 | 0 | my @par; | |||||
895 | 0 | my $l = 0; | |||||
896 | 0 | foreach my $h (@$data) { | |||||
897 | 0 | while (@par > 0) { | |||||
898 | 0 | my $prev = $par[$#par]; | |||||
899 | 0 | 0 | if ($h->{lft} < $prev->{rgt}) { | ||||
900 | # contained | ||||||
901 | 0 | ++$l; | |||||
902 | 0 | last; | |||||
903 | } else { | ||||||
904 | 0 | pop @par; | |||||
905 | 0 | 0 | if (@par) { | ||||
906 | 0 | --$l; | |||||
907 | } | ||||||
908 | } | ||||||
909 | } | ||||||
910 | 0 | push @par, $h; | |||||
911 | 0 | $levels{$h->{id}} = $l; | |||||
912 | } | ||||||
913 | 0 | return \%levels; | |||||
914 | } | ||||||
915 | |||||||
916 | 1; | ||||||
917 | |||||||
918 | =head1 TODO | ||||||
919 | |||||||
920 | - Allow custom names for the required fields (lft, rgt, mvg, id, | ||||||
921 | parent). | ||||||
922 | |||||||
923 | - Allow custom types for the primary key (currently they MUST be | ||||||
924 | integers). | ||||||
925 | |||||||
926 | =head1 REFERENCES | ||||||
927 | |||||||
928 | [1] MySQL AB: Managing Hierarchical Data in MySQL, by Mike Hillyer | ||||||
929 | http://dev.mysql.com/tech-resources/articles/hierarchical-data.html | ||||||
930 | |||||||
931 | =head1 SEE ALSO | ||||||
932 | |||||||
933 | L |
||||||
934 | |||||||
935 | =head1 AUTHOR | ||||||
936 | |||||||
937 | Mihai Bazon, |
||||||
938 | http://www.dynarch.com/ | ||||||
939 | http://www.bazon.net/mishoo/ | ||||||
940 | |||||||
941 | =head1 COPYRIGHT | ||||||
942 | |||||||
943 | Copyright (c) Mihai Bazon 2006. All rights reserved. | ||||||
944 | |||||||
945 | This module is free software; you can redistribute it and/or modify it | ||||||
946 | under the same terms as Perl itself. | ||||||
947 | |||||||
948 | =head1 DISCLAIMER OF WARRANTY | ||||||
949 | |||||||
950 | BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY | ||||||
951 | FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT | ||||||
952 | WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER | ||||||
953 | PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, | ||||||
954 | EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE | ||||||
955 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | ||||||
956 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE | ||||||
957 | SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME | ||||||
958 | THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. | ||||||
959 | |||||||
960 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | ||||||
961 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR | ||||||
962 | REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE | ||||||
963 | TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR | ||||||
964 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE | ||||||
965 | SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING | ||||||
966 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A | ||||||
967 | FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF | ||||||
968 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH | ||||||
969 | DAMAGES. | ||||||
970 | |||||||
971 | =cut |