Learn how to spell
[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 =head2 move_previous
276
277   $item->move_previous();
278
279 Swaps position with the sibling in the position previous in
280 the list.  Returns 1 on success, and 0 if the object is
281 already the first one.
282
283 =cut
284
285 sub move_previous {
286     my $self = shift;
287     return $self->move_to ($self->_position - 1);
288 }
289
290 =head2 move_next
291
292   $item->move_next();
293
294 Swaps position with the sibling in the next position in the
295 list.  Returns 1 on success, and 0 if the object is already
296 the last in the list.
297
298 =cut
299
300 sub move_next {
301     my $self = shift;
302     return 0 unless $self->next_siblings->count;
303     return $self->move_to ($self->_position + 1);
304 }
305
306 =head2 move_first
307
308   $item->move_first();
309
310 Moves the object to the first position in the list.  Returns 1
311 on success, and 0 if the object is already the first.
312
313 =cut
314
315 sub move_first {
316     return shift->move_to( 1 );
317 }
318
319 =head2 move_last
320
321   $item->move_last();
322
323 Moves the object to the last position in the list.  Returns 1
324 on success, and 0 if the object is already the last one.
325
326 =cut
327
328 sub move_last {
329     my $self = shift;
330     return $self->move_to( $self->_group_rs->count );
331 }
332
333 =head2 move_to
334
335   $item->move_to( $position );
336
337 Moves the object to the specified position.  Returns 1 on
338 success, and 0 if the object is already at the specified
339 position.
340
341 =cut
342
343 sub move_to {
344     my( $self, $to_position ) = @_;
345     return 0 if ( $to_position < 1 );
346
347     my $from_position = $self->_position;
348     return 0 if ( $from_position == $to_position );
349
350     my $position_column = $self->position_column;
351
352     # FIXME this needs to be wrapped in a transaction
353     {
354         my ($direction, @between);
355         if ( $from_position < $to_position ) {
356             $direction = -1;
357             @between = map { $self->_position_value ($_) } ( $from_position + 1, $to_position );
358         }
359         else {
360             $direction = 1;
361             @between = map { $self->_position_value ($_) } ( $to_position, $from_position - 1 );
362         }
363
364         my $new_pos_val = $self->_position_value ($to_position);                              # record this before the shift
365
366         # we need to null-position the moved row if the position column is part of a constraint
367         if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $self->result_source->unique_constraints }} ) ) ) {
368             $self->_ordered_internal_update({ $position_column => $self->null_position_value });
369         }
370
371         $self->_shift_siblings ($direction, @between);
372         $self->_ordered_internal_update({ $position_column => $new_pos_val });
373
374         return 1;
375     }
376 }
377
378 =head2 move_to_group
379
380   $item->move_to_group( $group, $position );
381
382 Moves the object to the specified position of the specified
383 group, or to the end of the group if $position is undef.
384 1 is returned on success, and 0 is returned if the object is
385 already at the specified position of the specified group.
386
387 $group may be specified as a single scalar if only one 
388 grouping column is in use, or as a hashref of column => value pairs
389 if multiple grouping columns are in use.
390
391 =cut
392
393 sub move_to_group {
394     my( $self, $to_group, $to_position ) = @_;
395
396     $self->throw_exception ('move_to_group() expects a group specification')
397         unless defined $to_group;
398
399     # if we're given a string, turn it into a hashref
400     unless (ref $to_group eq 'HASH') {
401         my @gcols = $self->_grouping_columns;
402
403         $self->throw_exception ('Single group supplied for a multi-column group identifier') if @gcols > 1;
404         $to_group = {$gcols[0] => $to_group};
405     }
406
407     my $position_column = $self->position_column;
408
409     return 0 if ( defined($to_position) and $to_position < 1 );
410     if ($self->_is_in_group ($to_group) ) {
411         return 0 if not defined $to_position;
412         return $self->move_to ($to_position);
413     }
414
415     # FIXME this needs to be wrapped in a transaction
416     {
417         # Move to end of current group to adjust siblings
418         $self->move_last;
419
420         $self->set_inflated_columns({ %$to_group, $position_column => undef });
421         my $new_group_count = $self->_group_rs->count;
422
423         if ( not defined($to_position) or $to_position > $new_group_count) {
424             $self->set_column(
425                 $position_column => $new_group_count
426                     ? $self->_next_position_value ( $self->last_sibling->get_column ($position_column) )    # FIXME - no need to inflate last_sibling
427                     : $self->_initial_position_value
428             );
429         }
430         else {
431             my $bumped_pos_val = $self->_position_value ($to_position);
432             my @between = ($to_position, $new_group_count);
433             $self->_shift_siblings (1, @between);   #shift right
434             $self->set_column( $position_column => $bumped_pos_val );
435         }
436
437         $self->_ordered_internal_update;
438
439         return 1;
440     }
441 }
442
443 =head2 insert
444
445 Overrides the DBIC insert() method by providing a default 
446 position number.  The default will be the number of rows in 
447 the table +1, thus positioning the new record at the last position.
448
449 =cut
450
451 sub insert {
452     my $self = shift;
453     my $position_column = $self->position_column;
454
455     unless ($self->get_column($position_column)) {
456         my $lsib = $self->last_sibling;     # FIXME - no need to inflate last_sibling
457         $self->set_column(
458             $position_column => ($lsib
459                 ? $self->_next_position_value ( $lsib->get_column ($position_column) )
460                 : $self->_initial_position_value
461             )
462         );
463     }
464
465     return $self->next::method( @_ );
466 }
467
468 =head2 update
469
470 Overrides the DBIC update() method by checking for a change
471 to the position and/or group columns.  Movement within a
472 group or to another group is handled by repositioning
473 the appropriate siblings.  Position defaults to the end
474 of a new group if it has been changed to undef.
475
476 =cut
477
478 sub update {
479     my $self = shift;
480
481     # this is set by _ordered_internal_update()
482     return $self->next::method(@_) if $self->{_ORDERED_INTERNAL_UPDATE};
483
484     my $upd = shift;
485     $self->set_inflated_columns($upd) if $upd;
486     my %changes = $self->get_dirty_columns;
487     $self->discard_changes;
488
489     my $position_column = $self->position_column;
490
491     # if nothing group/position related changed - short circuit
492     if (not grep { exists $changes{$_} } ($self->_grouping_columns, $position_column) ) {
493         return $self->next::method( \%changes, @_ );
494     }
495
496     # FIXME this needs to be wrapped in a transaction
497     {
498         # if any of our grouping columns have been changed
499         if (grep { exists $changes{$_} } ($self->_grouping_columns) ) {
500
501             # create new_group by taking the current group and inserting changes
502             my $new_group = {$self->_grouping_clause};
503             foreach my $col (keys %$new_group) {
504                 if (exists $changes{$col}) {
505                     $new_group->{$col} = delete $changes{$col}; # don't want to pass this on to next::method
506                 }
507             }
508
509             $self->move_to_group(
510                 $new_group,
511                 (exists $changes{$position_column}
512                     # The FIXME bit contradicts the documentation: when changing groups without supplying explicit
513                     # positions in move_to_group(), we push the item to the end of the group.
514                     # However when I was rewriting this, the position from the old group was clearly passed to the new one
515                     # Probably needs to go away (by ribasushi)
516                     ? delete $changes{$position_column}     # means there was a position change supplied with the update too
517                     : $self->_position                      # FIXME!
518                 ),
519             );
520         }
521         elsif (exists $changes{$position_column}) {
522             $self->move_to(delete $changes{$position_column});
523         }
524
525         return $self->next::method( \%changes, @_ );
526     }
527 }
528
529 =head2 delete
530
531 Overrides the DBIC delete() method by first moving the object 
532 to the last position, then deleting it, thus ensuring the
533 integrity of the positions.
534
535 =cut
536
537 sub delete {
538     my $self = shift;
539     # FIXME this needs to be wrapped in a transaction
540     {
541         $self->move_last;
542         return $self->next::method( @_ );
543     }
544 }
545
546 =head1 METHODS FOR EXTENDING ORDERED
547
548 You would want to override the methods below if you use sparse
549 (non-linear) or non-numeric position values. This can be useful
550 if you are working with preexisting non-normalised position data,
551 or if you need to work with materialized path columns.
552
553 =head2 _position
554
555   my $num_pos = $item->_position;
556
557 Returns the B<absolute numeric position> of the current object, with the
558 first object being at position 1, its sibling at position 2 and so on.
559 By default simply returns the value of L</position_column>.
560
561 =cut
562 sub _position {
563     my $self = shift;
564
565 #    #the right way to do this
566 #    return $self->previous_siblings->count + 1;
567
568     return $self->get_column ($self->position_column);
569 }
570
571 =head2 _position_value
572
573   my $pos_value = $item->_position_value ( $pos )
574
575 Returns the B<value> of L</position_column> of the object at numeric
576 position C<$pos>. By default simply returns C<$pos>.
577
578 =cut
579 sub _position_value {
580     my ($self, $pos) = @_;
581
582 #    #the right way to do this (not optimized)
583 #    my $position_column = $self->position_column;
584 #    return $self -> _group_rs
585 #                 -> search({}, { order_by => $position_column })
586 #                 -> slice ( $pos - 1)
587 #                 -> single
588 #                 -> get_column ($position_column);
589
590     return $pos;
591 }
592
593 =head2 _initial_position_value
594
595   __PACKAGE__->_initial_position_value(0);
596
597 This method specifies a B<value> of L</position_column> which is assigned
598 to the first inserted element of a group, if no value was supplied at
599 insertion time. All subsequent values are derived from this one by
600 L</_next_position_value> below. Defaults to 1.
601
602 =cut
603
604 __PACKAGE__->mk_classdata( '_initial_position_value' => 1 );
605
606 =head2 _next_position_value
607
608   my $new_value = $item->_next_position_value ( $position_value )
609
610 Returns a position B<value> that would be considered C<next> with
611 regards to C<$position_value>. Can be pretty much anything, given
612 that C<< $position_value < $new_value >> where C<< < >> is the
613 SQL comparison operator (usually works fine on strings). The
614 default method expects C<$position_value> to be numeric, and
615 returns C<$position_value + 1>
616
617 =cut
618 sub _next_position_value {
619     return $_[1] + 1;
620 }
621
622 =head2 _shift_siblings
623
624   $item->_shift_siblings ($direction, @between)
625
626 Shifts all siblings with B<positions values> in the range @between
627 (inclusive) by one position as specified by $direction (left if < 0,
628  right if > 0). By default simply increments/decrements each
629 L<position_column> value by 1, doing so in a way as to not violate
630 any existing constraints.
631
632 Note that if you override this method and have unique constraints
633 including the L<position_column> the shift is not a trivial task.
634 Refer to the implementation source of the default method for more
635 information.
636
637 =cut
638 sub _shift_siblings {
639     my ($self, $direction, @between) = @_;
640     return 0 unless $direction;
641
642     my $position_column = $self->position_column;
643
644     my ($op, $ord);
645     if ($direction < 0) {
646         $op = '-';
647         $ord = 'asc';
648     }
649     else {
650         $op = '+';
651         $ord = 'desc';
652     }
653
654     my $shift_rs = $self->_group_rs-> search ({ $position_column => { -between => \@between } });
655
656     # some databases (sqlite) are dumb and can not do a blanket
657     # increment/decrement. So what we do here is check if the
658     # position column is part of a unique constraint, and do a
659     # one-by-one update if this is the case
660
661     if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $self->result_source->unique_constraints }} ) ) ) {
662
663         my $rs = $shift_rs->search ({}, { order_by => { "-$ord", $position_column } } );
664         # FIXME - no need to inflate each row
665         while (my $r = $rs->next) {
666             $r->_ordered_internal_update ({ $position_column => \ "$position_column $op 1" } );
667         }
668     }
669     else {
670         $shift_rs->update ({ $position_column => \ "$position_column $op 1" } );
671     }
672 }
673
674 =head1 PRIVATE METHODS
675
676 These methods are used internally.  You should never have the 
677 need to use them.
678
679 =head2 _group_rs
680
681 This method returns a resultset containing all members of the row
682 group (including the row itself).
683
684 =cut
685 sub _group_rs {
686     my $self = shift;
687     return $self->result_source->resultset->search({$self->_grouping_clause()});
688 }
689
690 =head2 _siblings
691
692 Returns an unordered resultset of all objects in the same group
693 excluding the object you called this method on.
694
695 =cut
696 sub _siblings {
697     my $self = shift;
698     my $position_column = $self->position_column;
699     return $self->_group_rs->search(
700         { $position_column => { '!=' => $self->get_column($position_column) } },
701     );
702 }
703
704 =head2 _grouping_clause
705
706 This method returns one or more name=>value pairs for limiting a search
707 by the grouping column(s).  If the grouping column is not
708 defined then this will return an empty list.
709
710 =cut
711 sub _grouping_clause {
712     my( $self ) = @_;
713     return map {  $_ => $self->get_column($_)  } $self->_grouping_columns();
714 }
715
716 =head2 _get_grouping_columns
717
718 Returns a list of the column names used for grouping, regardless of whether
719 they were specified as an arrayref or a single string, and returns ()
720 if there is no grouping.
721
722 =cut
723 sub _grouping_columns {
724     my( $self ) = @_;
725     my $col = $self->grouping_column();
726     if (ref $col eq 'ARRAY') {
727         return @$col;
728     } elsif ($col) {
729         return ( $col );
730     } else {
731         return ();
732     }
733 }
734
735 =head2 _is_in_group
736
737     $item->_is_in_group( {user => 'fred', list => 'work'} )
738
739 Returns true if the object is in the group represented by hashref $other
740
741 =cut
742 sub _is_in_group {
743     my ($self, $other) = @_;
744     my $current = {$self->_grouping_clause};
745
746     no warnings qw/uninitialized/;
747
748     return 0 if (
749         join ("\x00", sort keys %$current)
750             ne
751         join ("\x00", sort keys %$other)
752     );
753     for my $key (keys %$current) {
754         return 0 if $current->{$key} ne $other->{$key};
755     }
756     return 1;
757 }
758
759 =head2 _ordered_internal_update
760
761 This is a short-circuited method, that is used internally by this
762 module to update positioning values in isolation (i.e. without
763 triggering any of the positioning integrity code).
764
765 Some day you might get confronted by datasets that have ambiguous
766 positioning data (i.e. duplicate position values within the same group,
767 in a table without unique constraints). When manually fixing such data
768 keep in mind that you can not invoke L<DBIx::Class::Row/update> like
769 you normally would, as it will get confused by the wrong data before
770 having a chance to update the ill-defined row. If you really know what
771 you are doing use this method which bypasses any hooks introduced by
772 this module.
773
774 =cut
775
776 sub _ordered_internal_update {
777     my $self = shift;
778     local $self->{_ORDERED_INTERNAL_UPDATE} = 1;
779     return $self->update (@_);
780 }
781
782 1;
783
784 __END__
785
786 =head1 CAVEATS
787
788 =head2 Race Condition on Insert
789
790 If a position is not specified for an insert than a position 
791 will be chosen based either on L</_initial_position_value> or
792 L</_next_position_value>, depending if there are already some
793 items in the current group. The space of time between the
794 necessary selects and insert introduces a race condition.
795 Having unique constraints on your position/group columns,
796 and using transactions (see L<DBIx::Class::Storage/txn_do>)
797 will prevent such race conditions going undetected.
798
799 =head2 Multiple Moves
800
801 Be careful when issueing move_* methods to multiple objects.  If 
802 you've pre-loaded the objects then when you move one of the objects 
803 the position of the other object will not reflect their new value 
804 until you reload them from the database - see
805 L<DBIx::Class::Row/discard_changes>.
806
807 There are times when you will want to move objects as groups, such 
808 as changeing the parent of several objects at once - this directly 
809 conflicts with this problem.  One solution is for us to write a 
810 ResultSet class that supports a parent() method, for example.  Another 
811 solution is to somehow automagically modify the objects that exist 
812 in the current object's result set to have the new position value.
813
814 =head2 Default Values
815
816 Using a database defined default_value on one of your group columns
817 could result in the position not being assigned correctly.
818
819 =head1 AUTHOR
820
821 Aran Deltac <bluefeet@cpan.org>
822
823 =head1 LICENSE
824
825 You may distribute this code under the same terms as Perl itself.
826