Optimize some Ordered.pm code
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Ordered.pm
1 package DBIx::Class::Ordered;
2 use strict;
3 use warnings;
4 use base qw( DBIx::Class );
5
6 =head1 NAME
7
8 DBIx::Class::Ordered - Modify the position of objects in an ordered list.
9
10 =head1 SYNOPSIS
11
12 Create a table for your ordered data.
13
14   CREATE TABLE items (
15     item_id INTEGER PRIMARY KEY AUTOINCREMENT,
16     name TEXT NOT NULL,
17     position INTEGER NOT NULL
18   );
19
20 Optionally, add one or more columns to specify groupings, allowing you 
21 to maintain independent ordered lists within one table:
22
23   CREATE TABLE items (
24     item_id INTEGER PRIMARY KEY AUTOINCREMENT,
25     name TEXT NOT NULL,
26     position INTEGER NOT NULL,
27     group_id INTEGER NOT NULL
28   );
29
30 Or even
31
32   CREATE TABLE items (
33     item_id INTEGER PRIMARY KEY AUTOINCREMENT,
34     name TEXT NOT NULL,
35     position INTEGER NOT NULL,
36     group_id INTEGER NOT NULL,
37     other_group_id INTEGER NOT NULL
38   );
39
40 In your Schema or DB class add "Ordered" to the top 
41 of the component list.
42
43   __PACKAGE__->load_components(qw( Ordered ... ));
44
45 Specify the column that stores the position number for 
46 each row.
47
48   package My::Item;
49   __PACKAGE__->position_column('position');
50
51 If you are using one grouping column, specify it as follows:
52
53   __PACKAGE__->grouping_column('group_id');
54
55 Or if you have multiple grouping columns:
56
57   __PACKAGE__->grouping_column(['group_id', 'other_group_id']);
58
59 That's it, now you can change the position of your objects.
60
61   #!/use/bin/perl
62   use My::Item;
63   
64   my $item = My::Item->create({ name=>'Matt S. Trout' });
65   # If using grouping_column:
66   my $item = My::Item->create({ name=>'Matt S. Trout', group_id=>1 });
67   
68   my $rs = $item->siblings();
69   my @siblings = $item->siblings();
70   
71   my $sibling;
72   $sibling = $item->first_sibling();
73   $sibling = $item->last_sibling();
74   $sibling = $item->previous_sibling();
75   $sibling = $item->next_sibling();
76   
77   $item->move_previous();
78   $item->move_next();
79   $item->move_first();
80   $item->move_last();
81   $item->move_to( $position );
82   $item->move_to_group( 'groupname' );
83   $item->move_to_group( 'groupname', $position );
84   $item->move_to_group( {group_id=>'groupname', 'other_group_id=>'othergroupname'} );
85   $item->move_to_group( {group_id=>'groupname', 'other_group_id=>'othergroupname'}, $position );
86
87 =head1 DESCRIPTION
88
89 This module provides a simple interface for modifying the ordered 
90 position of DBIx::Class objects.
91
92 =head1 AUTO UPDATE
93
94 All of the move_* methods automatically update the rows involved in 
95 the query.  This is not configurable and is due to the fact that if you 
96 move a record it always causes other records in the list to be updated.
97
98 =head1 METHODS
99
100 =head2 position_column
101
102   __PACKAGE__->position_column('position');
103
104 Sets and retrieves the name of the column that stores the 
105 positional value of each record.  Defaults to "position".
106
107 =cut
108
109 __PACKAGE__->mk_classdata( 'position_column' => 'position' );
110
111 =head2 grouping_column
112
113   __PACKAGE__->grouping_column('group_id');
114
115 This method specifies a column to limit all queries in 
116 this module by.  This effectively allows you to have multiple 
117 ordered lists within the same table.
118
119 =cut
120
121 __PACKAGE__->mk_classdata( 'grouping_column' );
122
123 =head2 null_position_value
124
125   __PACKAGE__->null_position_value(undef);
126
127 This method specifies a value of L</position_column> which B<would
128 never be assigned to a row> during normal operation. When
129 a row is moved, its position is set to this value temporarily, so
130 that any unique constrainst can not be violated. This value defaults
131 to 0, which should work for all cases except when your positions do
132 indeed start from 0.
133
134 =cut
135
136 __PACKAGE__->mk_classdata( 'null_position_value' => 0 );
137
138 =head2 siblings
139
140   my $rs = $item->siblings();
141   my @siblings = $item->siblings();
142
143 Returns an B<ordered> resultset of all other objects in the same
144 group excluding the one you called it on.
145
146 The ordering is a backwards-compatibility artifact - if you need
147 a resultset with no ordering applied use L</_siblings>
148
149 =cut
150 sub siblings {
151     my $self = shift;
152     return $self->_siblings->search ({}, { order_by => $self->position_column } );
153 }
154
155 =head2 previous_siblings
156
157   my $prev_rs = $item->previous_siblings();
158   my @prev_siblings = $item->previous_siblings();
159
160 Returns a resultset of all objects in the same group
161 positioned before the object on which this method was called.
162
163 =cut
164 sub previous_siblings {
165     my $self = shift;
166     my $position_column = $self->position_column;
167     my $position = $self->get_column ($position_column);
168     return ( defined $position
169         ? $self->_siblings->search ({ $position_column => { '<', $position } })
170         : $self->_siblings
171     );
172 }
173
174 =head2 next_siblings
175
176   my $next_rs = $item->next_siblings();
177   my @next_siblings = $item->next_siblings();
178
179 Returns a resultset of all objects in the same group
180 positioned after the object on which this method was called.
181
182 =cut
183 sub next_siblings {
184     my $self = shift;
185     my $position_column = $self->position_column;
186     my $position = $self->get_column ($position_column);
187     return ( defined $position
188         ? $self->_siblings->search ({ $position_column => { '>', $position } })
189         : $self->_siblings
190     );
191 }
192
193 =head2 previous_sibling
194
195   my $sibling = $item->previous_sibling();
196
197 Returns the sibling that resides one position back.  Returns 0
198 if the current object is the first one.
199
200 =cut
201
202 sub previous_sibling {
203     my $self = shift;
204     my $position_column = $self->position_column;
205
206     my $psib = $self->previous_siblings->search(
207         {},
208         { rows => 1, order_by => { '-desc' => $position_column } },
209     )->single;
210
211     return defined $psib ? $psib : 0;
212 }
213
214 =head2 first_sibling
215
216   my $sibling = $item->first_sibling();
217
218 Returns the first sibling object, or 0 if the first sibling 
219 is this sibling.
220
221 =cut
222
223 sub first_sibling {
224     my $self = shift;
225     my $position_column = $self->position_column;
226
227     my $fsib = $self->previous_siblings->search(
228         {},
229         { rows => 1, order_by => { '-asc' => $position_column } },
230     )->single;
231
232     return defined $fsib ? $fsib : 0;
233 }
234
235 =head2 next_sibling
236
237   my $sibling = $item->next_sibling();
238
239 Returns the sibling that resides one position forward. Returns 0
240 if the current object is the last one.
241
242 =cut
243
244 sub next_sibling {
245     my $self = shift;
246     my $position_column = $self->position_column;
247     my $nsib = $self->next_siblings->search(
248         {},
249         { rows => 1, order_by => { '-asc' => $position_column } },
250     )->single;
251
252     return defined $nsib ? $nsib : 0;
253 }
254
255 =head2 last_sibling
256
257   my $sibling = $item->last_sibling();
258
259 Returns the last sibling, or 0 if the last sibling is this 
260 sibling.
261
262 =cut
263
264 sub last_sibling {
265     my $self = shift;
266     my $position_column = $self->position_column;
267     my $lsib = $self->next_siblings->search(
268         {},
269         { rows => 1, order_by => { '-desc' => $position_column } },
270     )->single;
271
272     return defined $lsib ? $lsib : 0;
273 }
274
275 # an optimised method to get the last sibling position without inflating a row object
276 sub _last_sibling_pos {
277     my $self = shift;
278     my $position_column = $self->position_column;
279
280     my $cursor = $self->next_siblings->search(
281         {},
282         { rows => 1, order_by => { '-desc' => $position_column }, columns => $position_column },
283     )->cursor;
284
285     my ($pos) = $cursor->next;
286     return $pos;
287 }
288
289 =head2 move_previous
290
291   $item->move_previous();
292
293 Swaps position with the sibling in the position previous in
294 the list.  Returns 1 on success, and 0 if the object is
295 already the first one.
296
297 =cut
298
299 sub move_previous {
300     my $self = shift;
301     return $self->move_to ($self->_position - 1);
302 }
303
304 =head2 move_next
305
306   $item->move_next();
307
308 Swaps position with the sibling in the next position in the
309 list.  Returns 1 on success, and 0 if the object is already
310 the last in the list.
311
312 =cut
313
314 sub move_next {
315     my $self = shift;
316     return 0 unless $self->next_siblings->count;
317     return $self->move_to ($self->_position + 1);
318 }
319
320 =head2 move_first
321
322   $item->move_first();
323
324 Moves the object to the first position in the list.  Returns 1
325 on success, and 0 if the object is already the first.
326
327 =cut
328
329 sub move_first {
330     return shift->move_to( 1 );
331 }
332
333 =head2 move_last
334
335   $item->move_last();
336
337 Moves the object to the last position in the list.  Returns 1
338 on success, and 0 if the object is already the last one.
339
340 =cut
341
342 sub move_last {
343     my $self = shift;
344     return $self->move_to( $self->_group_rs->count );
345 }
346
347 =head2 move_to
348
349   $item->move_to( $position );
350
351 Moves the object to the specified position.  Returns 1 on
352 success, and 0 if the object is already at the specified
353 position.
354
355 =cut
356
357 sub move_to {
358     my( $self, $to_position ) = @_;
359     return 0 if ( $to_position < 1 );
360
361     my $from_position = $self->_position;
362     return 0 if ( $from_position == $to_position );
363
364     my $position_column = $self->position_column;
365
366     {
367         my $guard = $self->result_source->schema->txn_scope_guard;
368
369         my ($direction, @between);
370         if ( $from_position < $to_position ) {
371             $direction = -1;
372             @between = map { $self->_position_value ($_) } ( $from_position + 1, $to_position );
373         }
374         else {
375             $direction = 1;
376             @between = map { $self->_position_value ($_) } ( $to_position, $from_position - 1 );
377         }
378
379         my $new_pos_val = $self->_position_value ($to_position);                              # record this before the shift
380
381         # we need to null-position the moved row if the position column is part of a constraint
382         if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $self->result_source->unique_constraints }} ) ) ) {
383             $self->_ordered_internal_update({ $position_column => $self->null_position_value });
384         }
385
386         $self->_shift_siblings ($direction, @between);
387         $self->_ordered_internal_update({ $position_column => $new_pos_val });
388
389         $guard->commit;
390
391         return 1;
392     }
393 }
394
395 =head2 move_to_group
396
397   $item->move_to_group( $group, $position );
398
399 Moves the object to the specified position of the specified
400 group, or to the end of the group if $position is undef.
401 1 is returned on success, and 0 is returned if the object is
402 already at the specified position of the specified group.
403
404 $group may be specified as a single scalar if only one 
405 grouping column is in use, or as a hashref of column => value pairs
406 if multiple grouping columns are in use.
407
408 =cut
409
410 sub move_to_group {
411     my( $self, $to_group, $to_position ) = @_;
412
413     $self->throw_exception ('move_to_group() expects a group specification')
414         unless defined $to_group;
415
416     # if we're given a string, turn it into a hashref
417     unless (ref $to_group eq 'HASH') {
418         my @gcols = $self->_grouping_columns;
419
420         $self->throw_exception ('Single group supplied for a multi-column group identifier') if @gcols > 1;
421         $to_group = {$gcols[0] => $to_group};
422     }
423
424     my $position_column = $self->position_column;
425
426     return 0 if ( defined($to_position) and $to_position < 1 );
427     if ($self->_is_in_group ($to_group) ) {
428         return 0 if not defined $to_position;
429         return $self->move_to ($to_position);
430     }
431
432     {
433         my $guard = $self->result_source->schema->txn_scope_guard;
434
435         # Move to end of current group to adjust siblings
436         $self->move_last;
437
438         $self->set_inflated_columns({ %$to_group, $position_column => undef });
439         my $new_group_count = $self->_group_rs->count;
440
441         if ( not defined($to_position) or $to_position > $new_group_count) {
442             $self->set_column(
443                 $position_column => $new_group_count
444                     ? $self->_next_position_value ( $self->_last_sibling_pos )
445                     : $self->_initial_position_value
446             );
447         }
448         else {
449             my $bumped_pos_val = $self->_position_value ($to_position);
450             my @between = ($to_position, $new_group_count);
451             $self->_shift_siblings (1, @between);   #shift right
452             $self->set_column( $position_column => $bumped_pos_val );
453         }
454
455         $self->_ordered_internal_update;
456
457         $guard->commit;
458
459         return 1;
460     }
461 }
462
463 =head2 insert
464
465 Overrides the DBIC insert() method by providing a default 
466 position number.  The default will be the number of rows in 
467 the table +1, thus positioning the new record at the last position.
468
469 =cut
470
471 sub insert {
472     my $self = shift;
473     my $position_column = $self->position_column;
474
475     unless ($self->get_column($position_column)) {
476         my $lsib_pos = $self->_last_sibling_pos;
477         $self->set_column(
478             $position_column => (defined $lsib_pos
479                 ? $self->_next_position_value ( $lsib_pos )
480                 : $self->_initial_position_value
481             )
482         );
483     }
484
485     return $self->next::method( @_ );
486 }
487
488 =head2 update
489
490 Overrides the DBIC update() method by checking for a change
491 to the position and/or group columns.  Movement within a
492 group or to another group is handled by repositioning
493 the appropriate siblings.  Position defaults to the end
494 of a new group if it has been changed to undef.
495
496 =cut
497
498 sub update {
499     my $self = shift;
500
501     # this is set by _ordered_internal_update()
502     return $self->next::method(@_) if $self->{_ORDERED_INTERNAL_UPDATE};
503
504     my $upd = shift;
505     $self->set_inflated_columns($upd) if $upd;
506     my %changes = $self->get_dirty_columns;
507     $self->discard_changes;
508
509     my $position_column = $self->position_column;
510
511     # if nothing group/position related changed - short circuit
512     if (not grep { exists $changes{$_} } ($self->_grouping_columns, $position_column) ) {
513         return $self->next::method( \%changes, @_ );
514     }
515
516     {
517         my $guard = $self->result_source->schema->txn_scope_guard;
518
519         # if any of our grouping columns have been changed
520         if (grep { exists $changes{$_} } ($self->_grouping_columns) ) {
521
522             # create new_group by taking the current group and inserting changes
523             my $new_group = {$self->_grouping_clause};
524             foreach my $col (keys %$new_group) {
525                 if (exists $changes{$col}) {
526                     $new_group->{$col} = delete $changes{$col}; # don't want to pass this on to next::method
527                 }
528             }
529
530             $self->move_to_group(
531                 $new_group,
532                 (exists $changes{$position_column}
533                     # The FIXME bit contradicts the documentation: when changing groups without supplying explicit
534                     # positions in move_to_group(), we push the item to the end of the group.
535                     # However when I was rewriting this, the position from the old group was clearly passed to the new one
536                     # Probably needs to go away (by ribasushi)
537                     ? delete $changes{$position_column}     # means there was a position change supplied with the update too
538                     : $self->_position                      # FIXME!
539                 ),
540             );
541         }
542         elsif (exists $changes{$position_column}) {
543             $self->move_to(delete $changes{$position_column});
544         }
545
546         my @res;
547         my $want = wantarray();
548         if (not defined $want) {
549             $self->next::method( \%changes, @_ );
550         }
551         elsif ($want) {
552             @res = $self->next::method( \%changes, @_ );
553         }
554         else {
555             $res[0] = $self->next::method( \%changes, @_ );
556         }
557
558         $guard->commit;
559         return $want ? @res : $res[0];
560     }
561 }
562
563 =head2 delete
564
565 Overrides the DBIC delete() method by first moving the object 
566 to the last position, then deleting it, thus ensuring the
567 integrity of the positions.
568
569 =cut
570
571 sub delete {
572     my $self = shift;
573
574     my $guard = $self->result_source->schema->txn_scope_guard;
575
576     $self->move_last;
577
578     my @res;
579     my $want = wantarray();
580     if (not defined $want) {
581         $self->next::method( @_ );
582     }
583     elsif ($want) {
584         @res = $self->next::method( @_ );
585     }
586     else {
587         $res[0] = $self->next::method( @_ );
588     }
589
590     $guard->commit;
591     return $want ? @res : $res[0];
592 }
593
594 =head1 METHODS FOR EXTENDING ORDERED
595
596 You would want to override the methods below if you use sparse
597 (non-linear) or non-numeric position values. This can be useful
598 if you are working with preexisting non-normalised position data,
599 or if you need to work with materialized path columns.
600
601 =head2 _position
602
603   my $num_pos = $item->_position;
604
605 Returns the B<absolute numeric position> of the current object, with the
606 first object being at position 1, its sibling at position 2 and so on.
607 By default simply returns the value of L</position_column>.
608
609 =cut
610 sub _position {
611     my $self = shift;
612
613 #    #the right way to do this
614 #    return $self->previous_siblings->count + 1;
615
616     return $self->get_column ($self->position_column);
617 }
618
619 =head2 _position_value
620
621   my $pos_value = $item->_position_value ( $pos )
622
623 Returns the B<value> of L</position_column> of the object at numeric
624 position C<$pos>. By default simply returns C<$pos>.
625
626 =cut
627 sub _position_value {
628     my ($self, $pos) = @_;
629
630 #    #the right way to do this (not optimized)
631 #    my $position_column = $self->position_column;
632 #    return $self -> _group_rs
633 #                 -> search({}, { order_by => $position_column })
634 #                 -> slice ( $pos - 1)
635 #                 -> single
636 #                 -> get_column ($position_column);
637
638     return $pos;
639 }
640
641 =head2 _initial_position_value
642
643   __PACKAGE__->_initial_position_value(0);
644
645 This method specifies a B<value> of L</position_column> which is assigned
646 to the first inserted element of a group, if no value was supplied at
647 insertion time. All subsequent values are derived from this one by
648 L</_next_position_value> below. Defaults to 1.
649
650 =cut
651
652 __PACKAGE__->mk_classdata( '_initial_position_value' => 1 );
653
654 =head2 _next_position_value
655
656   my $new_value = $item->_next_position_value ( $position_value )
657
658 Returns a position B<value> that would be considered C<next> with
659 regards to C<$position_value>. Can be pretty much anything, given
660 that C<< $position_value < $new_value >> where C<< < >> is the
661 SQL comparison operator (usually works fine on strings). The
662 default method expects C<$position_value> to be numeric, and
663 returns C<$position_value + 1>
664
665 =cut
666 sub _next_position_value {
667     return $_[1] + 1;
668 }
669
670 =head2 _shift_siblings
671
672   $item->_shift_siblings ($direction, @between)
673
674 Shifts all siblings with B<positions values> in the range @between
675 (inclusive) by one position as specified by $direction (left if < 0,
676  right if > 0). By default simply increments/decrements each
677 L<position_column> value by 1, doing so in a way as to not violate
678 any existing constraints.
679
680 Note that if you override this method and have unique constraints
681 including the L<position_column> the shift is not a trivial task.
682 Refer to the implementation source of the default method for more
683 information.
684
685 =cut
686 sub _shift_siblings {
687     my ($self, $direction, @between) = @_;
688     return 0 unless $direction;
689
690     my $position_column = $self->position_column;
691
692     my ($op, $ord);
693     if ($direction < 0) {
694         $op = '-';
695         $ord = 'asc';
696     }
697     else {
698         $op = '+';
699         $ord = 'desc';
700     }
701
702     my $shift_rs = $self->_group_rs-> search ({ $position_column => { -between => \@between } });
703
704     # some databases (sqlite) are dumb and can not do a blanket
705     # increment/decrement. So what we do here is check if the
706     # position column is part of a unique constraint, and do a
707     # one-by-one update if this is the case
708
709     my $rsrc = $self->result_source;
710
711     if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $rsrc->unique_constraints }} ) ) ) {
712
713         my @pcols = $rsrc->primary_columns;
714         my $cursor = $shift_rs->search ({}, { order_by => { "-$ord", $position_column }, columns => \@pcols } )->cursor;
715         my $rs = $self->result_source->resultset;
716
717         while (my @pks = $cursor->next ) {
718
719           my $cond;
720           for my $i (0.. $#pcols) {
721             $cond->{$pcols[$i]} = $pks[$i];
722           }
723
724           $rs->search($cond)->update ({ $position_column => \ "$position_column $op 1" } );
725         }
726     }
727     else {
728         $shift_rs->update ({ $position_column => \ "$position_column $op 1" } );
729     }
730 }
731
732 =head1 PRIVATE METHODS
733
734 These methods are used internally.  You should never have the 
735 need to use them.
736
737 =head2 _group_rs
738
739 This method returns a resultset containing all members of the row
740 group (including the row itself).
741
742 =cut
743 sub _group_rs {
744     my $self = shift;
745     return $self->result_source->resultset->search({$self->_grouping_clause()});
746 }
747
748 =head2 _siblings
749
750 Returns an unordered resultset of all objects in the same group
751 excluding the object you called this method on.
752
753 =cut
754 sub _siblings {
755     my $self = shift;
756     my $position_column = $self->position_column;
757     return $self->_group_rs->search(
758         { $position_column => { '!=' => $self->get_column($position_column) } },
759     );
760 }
761
762 =head2 _grouping_clause
763
764 This method returns one or more name=>value pairs for limiting a search
765 by the grouping column(s).  If the grouping column is not
766 defined then this will return an empty list.
767
768 =cut
769 sub _grouping_clause {
770     my( $self ) = @_;
771     return map {  $_ => $self->get_column($_)  } $self->_grouping_columns();
772 }
773
774 =head2 _get_grouping_columns
775
776 Returns a list of the column names used for grouping, regardless of whether
777 they were specified as an arrayref or a single string, and returns ()
778 if there is no grouping.
779
780 =cut
781 sub _grouping_columns {
782     my( $self ) = @_;
783     my $col = $self->grouping_column();
784     if (ref $col eq 'ARRAY') {
785         return @$col;
786     } elsif ($col) {
787         return ( $col );
788     } else {
789         return ();
790     }
791 }
792
793 =head2 _is_in_group
794
795     $item->_is_in_group( {user => 'fred', list => 'work'} )
796
797 Returns true if the object is in the group represented by hashref $other
798
799 =cut
800 sub _is_in_group {
801     my ($self, $other) = @_;
802     my $current = {$self->_grouping_clause};
803
804     no warnings qw/uninitialized/;
805
806     return 0 if (
807         join ("\x00", sort keys %$current)
808             ne
809         join ("\x00", sort keys %$other)
810     );
811     for my $key (keys %$current) {
812         return 0 if $current->{$key} ne $other->{$key};
813     }
814     return 1;
815 }
816
817 =head2 _ordered_internal_update
818
819 This is a short-circuited method, that is used internally by this
820 module to update positioning values in isolation (i.e. without
821 triggering any of the positioning integrity code).
822
823 Some day you might get confronted by datasets that have ambiguous
824 positioning data (i.e. duplicate position values within the same group,
825 in a table without unique constraints). When manually fixing such data
826 keep in mind that you can not invoke L<DBIx::Class::Row/update> like
827 you normally would, as it will get confused by the wrong data before
828 having a chance to update the ill-defined row. If you really know what
829 you are doing use this method which bypasses any hooks introduced by
830 this module.
831
832 =cut
833
834 sub _ordered_internal_update {
835     my $self = shift;
836     local $self->{_ORDERED_INTERNAL_UPDATE} = 1;
837     return $self->update (@_);
838 }
839
840 1;
841
842 __END__
843
844 =head1 CAVEATS
845
846 =head2 Race Condition on Insert
847
848 If a position is not specified for an insert than a position 
849 will be chosen based either on L</_initial_position_value> or
850 L</_next_position_value>, depending if there are already some
851 items in the current group. The space of time between the
852 necessary selects and insert introduces a race condition.
853 Having unique constraints on your position/group columns,
854 and using transactions (see L<DBIx::Class::Storage/txn_do>)
855 will prevent such race conditions going undetected.
856
857 =head2 Multiple Moves
858
859 Be careful when issueing move_* methods to multiple objects.  If 
860 you've pre-loaded the objects then when you move one of the objects 
861 the position of the other object will not reflect their new value 
862 until you reload them from the database - see
863 L<DBIx::Class::Row/discard_changes>.
864
865 There are times when you will want to move objects as groups, such 
866 as changeing the parent of several objects at once - this directly 
867 conflicts with this problem.  One solution is for us to write a 
868 ResultSet class that supports a parent() method, for example.  Another 
869 solution is to somehow automagically modify the objects that exist 
870 in the current object's result set to have the new position value.
871
872 =head2 Default Values
873
874 Using a database defined default_value on one of your group columns
875 could result in the position not being assigned correctly.
876
877 =head1 AUTHOR
878
879  Original code framework
880    Aran Deltac <bluefeet@cpan.org>
881
882  Constraints support and code generalisation
883    Peter Rabbitson <ribasushi@cpan.org>
884
885 =head1 LICENSE
886
887 You may distribute this code under the same terms as Perl itself.
888