File Coverage

blib/lib/DBIx/Class/Ordered.pm
Criterion Covered Total %
statement 193 200 96.5
branch 68 80 85.0
condition 10 17 58.8
subroutine 33 34 97.0
pod 16 16 100.0
total 320 347 92.2


line stmt bran cond sub pod time code
1             package DBIx::Class::Ordered;
2 262     262   106259 use strict;
  262         656  
  262         7158  
3 262     262   1364 use warnings;
  262         571  
  262         6658  
4 262     262   1340 use base qw( DBIx::Class );
  262         494  
  262         24396  
5              
6 262     262   1773 use DBIx::Class::_Util qw( bag_eq fail_on_internal_call );
  262         605  
  262         16174  
7 262     262   1675 use namespace::clean;
  262         593  
  262         2246  
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_classaccessor( '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_group_accessors( inherited => 'grouping_column' );
125              
126             =head2 null_position_value
127              
128             __PACKAGE__->null_position_value(undef);
129              
130             This method specifies a value of L which B
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_classaccessor( 'null_position_value' => 0 );
140              
141             =head2 siblings
142              
143             my $rs = $item->siblings();
144             my @siblings = $item->siblings();
145              
146             Returns an B resultset of all other objects in the same
147             group excluding the one you called it on.
148              
149             Underneath calls L, and therefore returns
150             objects by implicitly invoking Lall() >>|DBIx::Class::ResultSet/all>
151             in list context.
152              
153             The ordering is a backwards-compatibility artifact - if you need
154             a resultset with no ordering applied use C<_siblings>
155              
156             =cut
157              
158             sub siblings {
159             #my $self = shift;
160              
161             DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS
162             and
163             wantarray
164             and
165 0     0 1 0 ! eval { fail_on_internal_call; 1 }
166             and
167             die "ILLEGAL LIST CONTEXT INVOCATION: $@";
168              
169             # *MUST* be context sensitive due to legacy (DO NOT call search_rs)
170 0         0 $_[0]->_siblings->search ({}, { order_by => $_[0]->position_column } );
171             }
172              
173             =head2 previous_siblings
174              
175             my $prev_rs = $item->previous_siblings();
176             my @prev_siblings = $item->previous_siblings();
177              
178             Returns a resultset of all objects in the same group
179             positioned before the object on which this method was called.
180              
181             Underneath calls L, and therefore returns
182             objects by implicitly invoking Lall() >>|DBIx::Class::ResultSet/all>
183             in list context.
184              
185             =cut
186             sub previous_siblings {
187 390     390 1 1263 my $self = shift;
188 390         10684 my $position_column = $self->position_column;
189 390         21111 my $position = $self->get_column ($position_column);
190              
191             DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS
192             and
193             wantarray
194             and
195 390         1050 ! eval { fail_on_internal_call; 1 }
196             and
197             die "ILLEGAL LIST CONTEXT INVOCATION: $@";
198              
199             # *MUST* be context sensitive due to legacy (DO NOT call search_rs)
200 390 50       2879 defined( $position )
201             ? $self->_siblings->search ({ $position_column => { '<', $position } })
202             : $self->_siblings
203             ;
204             }
205              
206             =head2 next_siblings
207              
208             my $next_rs = $item->next_siblings();
209             my @next_siblings = $item->next_siblings();
210              
211             Returns a resultset of all objects in the same group
212             positioned after the object on which this method was called.
213              
214             Underneath calls L, and therefore returns
215             objects by implicitly invoking Lall() >>|DBIx::Class::ResultSet/all>
216             in list context.
217              
218             =cut
219             sub next_siblings {
220 841     841 1 2350 my $self = shift;
221 841         19829 my $position_column = $self->position_column;
222 841         40424 my $position = $self->get_column ($position_column);
223              
224             DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS
225             and
226             wantarray
227             and
228 841         1689 ! eval { fail_on_internal_call; 1 }
229             and
230             die "ILLEGAL LIST CONTEXT INVOCATION: $@";
231              
232             # *MUST* be context sensitive due to legacy (DO NOT call search_rs)
233 841 100       5677 defined( $position )
234             ? $self->_siblings->search ({ $position_column => { '>', $position } })
235             : $self->_siblings
236             ;
237             }
238              
239             =head2 previous_sibling
240              
241             my $sibling = $item->previous_sibling();
242              
243             Returns the sibling that resides one position back. Returns 0
244             if the current object is the first one.
245              
246             =cut
247              
248             sub previous_sibling {
249 260     260 1 1377 my $self = shift;
250 260         8285 my $position_column = $self->position_column;
251              
252 260         21291 my $psib = $self->previous_siblings->search_rs(
253             {},
254             { rows => 1, order_by => { '-desc' => $position_column } },
255             )->single;
256              
257 260 100       10463 return defined( $psib ) ? $psib : 0;
258             }
259              
260             =head2 first_sibling
261              
262             my $sibling = $item->first_sibling();
263              
264             Returns the first sibling object, or 0 if the first sibling
265             is this sibling.
266              
267             =cut
268              
269             sub first_sibling {
270 130     130 1 440 my $self = shift;
271 130         4152 my $position_column = $self->position_column;
272              
273 130         11024 my $fsib = $self->previous_siblings->search_rs(
274             {},
275             { rows => 1, order_by => { '-asc' => $position_column } },
276             )->single;
277              
278 130 100       5394 return defined( $fsib ) ? $fsib : 0;
279             }
280              
281             =head2 next_sibling
282              
283             my $sibling = $item->next_sibling();
284              
285             Returns the sibling that resides one position forward. Returns 0
286             if the current object is the last one.
287              
288             =cut
289              
290             sub next_sibling {
291 260     260 1 37189 my $self = shift;
292 260         8194 my $position_column = $self->position_column;
293 260         22195 my $nsib = $self->next_siblings->search_rs(
294             {},
295             { rows => 1, order_by => { '-asc' => $position_column } },
296             )->single;
297              
298 260 100       10266 return defined( $nsib ) ? $nsib : 0;
299             }
300              
301             =head2 last_sibling
302              
303             my $sibling = $item->last_sibling();
304              
305             Returns the last sibling, or 0 if the last sibling is this
306             sibling.
307              
308             =cut
309              
310             sub last_sibling {
311 130     130 1 423 my $self = shift;
312 130         4225 my $position_column = $self->position_column;
313 130         10374 my $lsib = $self->next_siblings->search_rs(
314             {},
315             { rows => 1, order_by => { '-desc' => $position_column } },
316             )->single;
317              
318 130 100       5129 return defined( $lsib ) ? $lsib : 0;
319             }
320              
321             # an optimized method to get the last sibling position value without inflating a result object
322             sub _last_sibling_posval {
323 451     451   1074 my $self = shift;
324 451         10667 my $position_column = $self->position_column;
325              
326 451         26090 my $cursor = $self->next_siblings->search_rs(
327             {},
328             { rows => 1, order_by => { '-desc' => $position_column }, select => $position_column },
329             )->cursor;
330              
331 451         5218 ($cursor->next)[0];
332             }
333              
334             =head2 move_previous
335              
336             $item->move_previous();
337              
338             Swaps position with the sibling in the position previous in
339             the list. Returns 1 on success, and 0 if the object is
340             already the first one.
341              
342             =cut
343              
344             sub move_previous {
345 65     65 1 172 my $self = shift;
346 65         365 return $self->move_to ($self->_position - 1);
347             }
348              
349             =head2 move_next
350              
351             $item->move_next();
352              
353             Swaps position with the sibling in the next position in the
354             list. Returns 1 on success, and 0 if the object is already
355             the last in the list.
356              
357             =cut
358              
359             sub move_next {
360 65     65 1 201 my $self = shift;
361 65 100       326 return 0 unless defined $self->_last_sibling_posval; # quick way to check for no more siblings
362 51         311 return $self->move_to ($self->_position + 1);
363             }
364              
365             =head2 move_first
366              
367             $item->move_first();
368              
369             Moves the object to the first position in the list. Returns 1
370             on success, and 0 if the object is already the first.
371              
372             =cut
373              
374             sub move_first {
375 65     65 1 346 return shift->move_to( 1 );
376             }
377              
378             =head2 move_last
379              
380             $item->move_last();
381              
382             Moves the object to the last position in the list. Returns 1
383             on success, and 0 if the object is already the last one.
384              
385             =cut
386              
387             sub move_last {
388 129     129 1 347 my $self = shift;
389 129         647 my $last_posval = $self->_last_sibling_posval;
390              
391 129 100       663 return 0 unless defined $last_posval;
392              
393 84         536 return $self->move_to( $self->_position_from_value ($last_posval) );
394             }
395              
396             =head2 move_to
397              
398             $item->move_to( $position );
399              
400             Moves the object to the specified position. Returns 1 on
401             success, and 0 if the object is already at the specified
402             position.
403              
404             =cut
405              
406             sub move_to {
407 580     580 1 1850 my( $self, $to_position ) = @_;
408 580 100       2254 return 0 if ( $to_position < 1 );
409              
410 566         17098 my $position_column = $self->position_column;
411              
412 566         39910 my $rsrc = $self->result_source;
413              
414 566         9512 my $is_txn;
415 566 100       3455 if ($is_txn = $rsrc->schema->storage->transaction_depth) {
    100          
416             # Reload position state from storage
417             # The thinking here is that if we are in a transaction, it is
418             # *more likely* the object went out of sync due to resultset
419             # level shenanigans. Instead of always reloading (slow) - go
420             # ahead and hand-hold only in the case of higher layers
421             # requesting the safety of a txn
422              
423 33   33     664 $self->store_column(
424             $position_column,
425             ( $rsrc->resultset
426             ->search_rs($self->_storage_ident_condition, { rows => 1, columns => $position_column })
427             ->cursor
428             ->next
429             )[0] || $self->throw_exception(
430             sprintf "Unable to locate object '%s' in storage - object went ouf of sync...?",
431             $self->ID
432             ),
433             );
434 33         195 delete $self->{_dirty_columns}{$position_column};
435             }
436             elsif ($self->is_column_changed ($position_column) ) {
437             # something changed our position, we need to know where we
438             # used to be - use the stashed value
439 2         10 $self->store_column($position_column, delete $self->{_column_data_in_storage}{$position_column});
440 2         6 delete $self->{_dirty_columns}{$position_column};
441             }
442              
443 566         3140 my $from_position = $self->_position;
444              
445 566 100       2194 if ( $from_position == $to_position ) { # FIXME this will not work for non-numeric order
446 80         521 return 0;
447             }
448              
449 486 100       2711 my $guard = $is_txn ? undef : $rsrc->schema->txn_scope_guard;
450              
451 486         1347 my ($direction, @between);
452 486 100       1745 if ( $from_position < $to_position ) {
453 260         651 $direction = -1;
454 260         937 @between = map { $self->_position_value ($_) } ( $from_position + 1, $to_position );
  520         1805  
455             }
456             else {
457 226         673 $direction = 1;
458 226         894 @between = map { $self->_position_value ($_) } ( $to_position, $from_position - 1 );
  452         1481  
459             }
460              
461 486         1571 my $new_pos_val = $self->_position_value ($to_position); # record this before the shift
462              
463             # we need to null-position the moved row if the position column is part of a constraint
464 486 100       1183 if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $rsrc->unique_constraints }} ) ) ) {
  562         2876  
  524         2312  
  486         13034  
465 19         412 $self->_ordered_internal_update({ $position_column => $self->null_position_value });
466             }
467              
468 486         3156 $self->_shift_siblings ($direction, @between);
469 486         4566 $self->_ordered_internal_update({ $position_column => $new_pos_val });
470              
471 486 100       4588 $guard->commit if $guard;
472 486         3342 return 1;
473             }
474              
475             =head2 move_to_group
476              
477             $item->move_to_group( $group, $position );
478              
479             Moves the object to the specified position of the specified
480             group, or to the end of the group if $position is undef.
481             1 is returned on success, and 0 is returned if the object is
482             already at the specified position of the specified group.
483              
484             $group may be specified as a single scalar if only one
485             grouping column is in use, or as a hashref of column => value pairs
486             if multiple grouping columns are in use.
487              
488             =cut
489              
490             sub move_to_group {
491 19     19 1 170 my( $self, $to_group, $to_position ) = @_;
492              
493             # if we're given a single value, turn it into a hashref
494 19 100       96 unless (ref $to_group eq 'HASH') {
495 6         29 my @gcols = $self->_grouping_columns;
496              
497 6 50       28 $self->throw_exception ('Single group supplied for a multi-column group identifier') if @gcols > 1;
498 6         28 $to_group = {$gcols[0] => $to_group};
499             }
500              
501 19         454 my $position_column = $self->position_column;
502              
503 19 50 66     1026 return 0 if ( defined($to_position) and $to_position < 1 );
504              
505             # check if someone changed the _grouping_columns - this will
506             # prevent _is_in_group working, so we need to restore the
507             # original stashed values
508 19         73 for ($self->_grouping_columns) {
509 24 100       108 if ($self->is_column_changed ($_)) {
510 16         83 $self->store_column($_, delete $self->{_column_data_in_storage}{$_});
511 16         59 delete $self->{_dirty_columns}{$_};
512             }
513             }
514              
515 19 50       103 if ($self->_is_in_group ($to_group) ) {
516 0         0 my $ret;
517 0 0       0 if (defined $to_position) {
518 0         0 $ret = $self->move_to ($to_position);
519             }
520              
521 0   0     0 return $ret||0;
522             }
523              
524 19         108 my $guard = $self->result_source->schema->txn_scope_guard;
525              
526             # Move to end of current group to adjust siblings
527 19         129 $self->move_last;
528              
529 19         238 $self->set_inflated_columns({ %$to_group, $position_column => undef });
530 19         126 my $new_group_last_posval = $self->_last_sibling_posval;
531 19         107 my $new_group_last_position = $self->_position_from_value (
532             $new_group_last_posval
533             );
534              
535 19 100 100     143 if ( not defined($to_position) or $to_position > $new_group_last_position) {
536 5 50       57 $self->set_column(
537             $position_column => $new_group_last_position
538             ? $self->_next_position_value ( $new_group_last_posval )
539             : $self->_initial_position_value
540             );
541             }
542             else {
543 14         74 my $bumped_pos_val = $self->_position_value ($to_position);
544 14         47 my @between = map { $self->_position_value ($_) } ($to_position, $new_group_last_position);
  28         59  
545 14         79 $self->_shift_siblings (1, @between); #shift right
546 14         106 $self->set_column( $position_column => $bumped_pos_val );
547             }
548              
549 19         116 $self->_ordered_internal_update;
550              
551 19         175 $guard->commit;
552              
553 19         120 return 1;
554             }
555              
556             =head2 insert
557              
558             Overrides the DBIC insert() method by providing a default
559             position number. The default will be the number of rows in
560             the table +1, thus positioning the new record at the last position.
561              
562             =cut
563              
564             sub insert {
565 263     263 1 1452 my $self = shift;
566 263         6528 my $position_column = $self->position_column;
567              
568 263 100       18625 unless ($self->get_column($position_column)) {
569 238         1033 my $lsib_posval = $self->_last_sibling_posval;
570 238 100       3825 $self->set_column(
571             $position_column => (defined $lsib_posval
572             ? $self->_next_position_value ( $lsib_posval )
573             : $self->_initial_position_value
574             )
575             );
576             }
577              
578 263         1386 return $self->next::method( @_ );
579             }
580              
581             =head2 update
582              
583             Overrides the DBIC update() method by checking for a change
584             to the position and/or group columns. Movement within a
585             group or to another group is handled by repositioning
586             the appropriate siblings. Position defaults to the end
587             of a new group if it has been changed to undef.
588              
589             =cut
590              
591             sub update {
592 576     576 1 1451 my $self = shift;
593              
594             # this is set by _ordered_internal_update()
595 576 100       2272 return $self->next::method(@_) if $self->result_source->schema->{_ORDERED_INTERNAL_UPDATE};
596              
597 15         43 my $upd = shift;
598 15 100       114 $self->set_inflated_columns($upd) if $upd;
599              
600 15         337 my $position_column = $self->position_column;
601 15         782 my @group_columns = $self->_grouping_columns;
602              
603             # see if the order is already changed
604 15         55 my $changed_ordering_cols = { map { $_ => $self->get_column($_) } grep { $self->is_column_changed($_) } ($position_column, @group_columns) };
  23         80  
  35         160  
605              
606             # nothing changed - short circuit
607 15 50       100 if (! keys %$changed_ordering_cols) {
    100          
608 0         0 return $self->next::method( undef, @_ );
609             }
610 20         92 elsif (grep { exists $changed_ordering_cols->{$_} } @group_columns ) {
611             $self->move_to_group(
612             # since the columns are already re-set the _grouping_clause is correct
613             # move_to_group() knows how to get the original storage values
614             { $self->_grouping_clause },
615              
616             # The FIXME bit contradicts the documentation: POD states that
617             # when changing groups without supplying explicit positions in
618             # move_to_group(), we push the item to the end of the group.
619             # However when I was rewriting this, the position from the old
620             # group was clearly passed to the new one
621             # Probably needs to go away (by ribasushi)
622             (exists $changed_ordering_cols->{$position_column}
623 13 100       71 ? $changed_ordering_cols->{$position_column} # means there was a position change supplied with the update too
624             : $self->_position # FIXME! (replace with undef)
625             ),
626             );
627             }
628             else {
629 2         13 $self->move_to($changed_ordering_cols->{$position_column});
630             }
631              
632 15         138 return $self;
633             }
634              
635             =head2 delete
636              
637             Overrides the DBIC delete() method by first moving the object
638             to the last position, then deleting it, thus ensuring the
639             integrity of the positions.
640              
641             =cut
642              
643             sub delete {
644 45     45 1 108 my $self = shift;
645              
646 45         210 my $guard = $self->result_source->schema->txn_scope_guard;
647              
648 45         232 $self->move_last;
649              
650 45         212 $self->next::method( @_ );
651              
652 45         183 $guard->commit;
653              
654 45         142 return $self;
655             }
656              
657             # add the current position/group to the things we track old values for
658             sub _track_storage_value {
659 606     606   2086 my ($self, $col) = @_;
660             return (
661             $self->next::method($col)
662             ||
663 606   100     2762 grep { $_ eq $col } ($self->position_column, $self->_grouping_columns)
664             );
665             }
666              
667             =head1 METHODS FOR EXTENDING ORDERED
668              
669             You would want to override the methods below if you use sparse
670             (non-linear) or non-numeric position values. This can be useful
671             if you are working with preexisting non-normalised position data,
672             or if you need to work with materialized path columns.
673              
674             =head2 _position_from_value
675              
676             my $num_pos = $item->_position_from_value ( $pos_value )
677              
678             Returns the B of an object with a B
679             value> set to C<$pos_value>. By default simply returns C<$pos_value>.
680              
681             =cut
682             sub _position_from_value {
683 793     793   2340 my ($self, $val) = @_;
684              
685 793 50       2393 return 0 unless defined $val;
686              
687             # #the right way to do this
688             # return $self -> _group_rs
689             # -> search({ $self->position_column => { '<=', $val } })
690             # -> count
691              
692 793         2636 return $val;
693             }
694              
695             =head2 _position_value
696              
697             my $pos_value = $item->_position_value ( $pos )
698              
699             Returns the B of L of the object at numeric
700             position C<$pos>. By default simply returns C<$pos>.
701              
702             =cut
703             sub _position_value {
704 1500     1500   3190 my ($self, $pos) = @_;
705              
706             # #the right way to do this (not optimized)
707             # my $position_column = $self->position_column;
708             # return $self -> _group_rs
709             # -> search({}, { order_by => $position_column })
710             # -> slice ( $pos - 1)
711             # -> single
712             # -> get_column ($position_column);
713              
714 1500         4327 return $pos;
715             }
716              
717             =head2 _initial_position_value
718              
719             __PACKAGE__->_initial_position_value(0);
720              
721             This method specifies a B of L which is assigned
722             to the first inserted element of a group, if no value was supplied at
723             insertion time. All subsequent values are derived from this one by
724             L below. Defaults to 1.
725              
726             =cut
727              
728             __PACKAGE__->mk_classaccessor( '_initial_position_value' => 1 );
729              
730             =head2 _next_position_value
731              
732             my $new_value = $item->_next_position_value ( $position_value )
733              
734             Returns a position B that would be considered C with
735             regards to C<$position_value>. Can be pretty much anything, given
736             that C<< $position_value < $new_value >> where C<< < >> is the
737             SQL comparison operator (usually works fine on strings). The
738             default method expects C<$position_value> to be numeric, and
739             returns C<$position_value + 1>
740              
741             =cut
742             sub _next_position_value {
743 174     174   1262 return $_[1] + 1;
744             }
745              
746             =head2 _shift_siblings
747              
748             $item->_shift_siblings ($direction, @between)
749              
750             Shifts all siblings with B in the range @between
751             (inclusive) by one position as specified by $direction (left if < 0,
752             right if > 0). By default simply increments/decrements each
753             L value by 1, doing so in a way as to not violate
754             any existing constraints.
755              
756             Note that if you override this method and have unique constraints
757             including the L the shift is not a trivial task.
758             Refer to the implementation source of the default method for more
759             information.
760              
761             =cut
762             sub _shift_siblings {
763 500     500   2208 my ($self, $direction, @between) = @_;
764 500 50       1806 return 0 unless $direction;
765              
766 500         11537 my $position_column = $self->position_column;
767              
768 500         26294 my ($op, $ord);
769 500 100       2162 if ($direction < 0) {
770 260         624 $op = '-';
771 260         543 $ord = 'asc';
772             }
773             else {
774 240         863 $op = '+';
775 240         656 $ord = 'desc';
776             }
777              
778 500         2447 my $shift_rs = $self->_group_rs-> search_rs ({ $position_column => { -between => \@between } });
779              
780             # some databases (sqlite, pg, perhaps others) are dumb and can not do a
781             # blanket increment/decrement without violating a unique constraint.
782             # So what we do here is check if the position column is part of a unique
783             # constraint, and do a one-by-one update if this is the case.
784 500         3532 my $rsrc = $self->result_source;
785              
786             # set in case there are more cascades combined with $rs->update => $rs_update_all overrides
787 500         9470 local $rsrc->schema->{_ORDERED_INTERNAL_UPDATE} = 1;
788 500         11561 my @pcols = $rsrc->primary_columns;
789 500 100       1575 if (
790 576         2786 grep { $_ eq $position_column } ( map { @$_ } (values %{{ $rsrc->unique_constraints }} ) )
  538         1918  
  500         9315  
791             ) {
792 19         65 my $clean_rs = $rsrc->resultset;
793              
794 19         196 for ( $shift_rs->search_rs (
795             {}, { order_by => { "-$ord", $position_column }, select => [$position_column, @pcols] }
796             )->cursor->all ) {
797 37         117 my $pos = shift @$_;
798 37 50       203 $clean_rs->find(@$_)->update ({ $position_column => $pos + ( ($op eq '+') ? 1 : -1 ) });
799             }
800             }
801             else {
802 481         4372 $shift_rs->update ({ $position_column => \ "$position_column $op 1" } );
803             }
804             }
805              
806              
807             # This method returns a resultset containing all members of the row
808             # group (including the row itself).
809             sub _group_rs {
810             #my $self = shift;
811              
812             DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS
813             and
814             wantarray
815             and
816 1731     1731   3244 ! eval { fail_on_internal_call; 1 }
817             and
818             die "ILLEGAL LIST CONTEXT INVOCATION: $@";
819              
820             # *MUST* be context sensitive due to legacy (DO NOT call search_rs)
821 1731         9584 $_[0]->result_source->resultset->search({ $_[0]->_grouping_clause() });
822             }
823              
824             # Returns an unordered resultset of all objects in the same group
825             # excluding the object you called this method on.
826             sub _siblings {
827 1231     1231   2791 my $self = shift;
828 1231         29620 my $position_column = $self->position_column;
829 1231         57815 my $pos;
830              
831             DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS
832             and
833             wantarray
834             and
835 1231         2605 ! eval { fail_on_internal_call; 1 }
836             and
837             die "ILLEGAL LIST CONTEXT INVOCATION: $@";
838              
839             # *MUST* be context sensitive due to legacy (DO NOT call search_rs)
840 1231 100       6766 defined( $pos = $self->get_column($position_column) )
841             ? $self->_group_rs->search(
842             { $position_column => { '!=' => $pos } },
843             )
844             : $self->_group_rs
845             ;
846             }
847              
848             # Returns the B of the current object, with the
849             # first object being at position 1, its sibling at position 2 and so on.
850             sub _position {
851 690     690   1696 my $self = shift;
852 690         15840 return $self->_position_from_value ($self->get_column ($self->position_column) );
853             }
854              
855             # This method returns one or more name=>value pairs for limiting a search
856             # by the grouping column(s). If the grouping column is not defined then
857             # this will return an empty list.
858             sub _grouping_clause {
859 1763     1763   4904 my( $self ) = @_;
860 1763         7509 return map { $_ => $self->get_column($_) } $self->_grouping_columns();
  2525         10167  
861             }
862              
863             # Returns a list of the column names used for grouping, regardless of whether
864             # they were specified as an arrayref or a single string, and returns ()
865             # if there is no grouping.
866             sub _grouping_columns {
867 2408     2408   43542 my( $self ) = @_;
868 2408         60602 my $col = $self->grouping_column();
869 2408 100       104095 if (ref $col eq 'ARRAY') {
    100          
870 1130         5669 return @$col;
871             } elsif ($col) {
872 1126         3667 return ( $col );
873             } else {
874 152         927 return ();
875             }
876             }
877              
878             # Returns true if the object is in the group represented by hashref $other
879             sub _is_in_group {
880 19     19   61 my ($self, $other) = @_;
881 19         68 my $current = {$self->_grouping_clause};
882              
883             (
884             bag_eq(
885             [ keys %$current ],
886             [ keys %$other ],
887             )
888             and
889             ! grep {
890 19 50 33     172 (
891             defined( $current->{$_} )
892             xor
893             defined( $other->{$_} )
894             )
895             or
896             (
897             defined $current->{$_}
898             and
899             $current->{$_} ne $other->{$_}
900             )
901             } keys %$other
902             ) ? 1 : 0;
903             }
904              
905             # This is a short-circuited method, that is used internally by this
906             # module to update positioning values in isolation (i.e. without
907             # triggering any of the positioning integrity code).
908             #
909             # Some day you might get confronted by datasets that have ambiguous
910             # positioning data (e.g. duplicate position values within the same group,
911             # in a table without unique constraints). When manually fixing such data
912             # keep in mind that you can not invoke L like
913             # you normally would, as it will get confused by the wrong data before
914             # having a chance to update the ill-defined row. If you really know what
915             # you are doing use this method which bypasses any hooks introduced by
916             # this module.
917             sub _ordered_internal_update {
918 524     524   5251 local $_[0]->result_source->schema->{_ORDERED_INTERNAL_UPDATE} = 1;
919 524         2691 shift->update (@_);
920             }
921              
922             1;
923              
924             __END__