File Coverage

blib/lib/DBIx/Class/Ordered.pm
Criterion Covered Total %
statement 198 205 96.5
branch 71 82 86.5
condition 9 14 64.2
subroutine 37 38 97.3
pod 16 16 100.0
total 331 355 93.2


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