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