explicit int cast, prevents the result rounding up to 1000 (4 digits)
[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()
529 return $self->next::method(@_) if $self->{_ORDERED_INTERNAL_UPDATE};
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
722
82a8f76f 723 my $rsrc = $self->result_source;
724
725 if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $rsrc->unique_constraints }} ) ) ) {
726
e8fb771b 727 my @pcols = $rsrc->_pri_cols;
82a8f76f 728 my $cursor = $shift_rs->search ({}, { order_by => { "-$ord", $position_column }, columns => \@pcols } )->cursor;
729 my $rs = $self->result_source->resultset;
730
a83cdbf2 731 my @all_pks = $cursor->all;
e8fb771b 732 while (my $pks = shift @all_pks) {
82a8f76f 733 my $cond;
734 for my $i (0.. $#pcols) {
e8fb771b 735 $cond->{$pcols[$i]} = $pks->[$i];
82a8f76f 736 }
bd7ca9e8 737
82a8f76f 738 $rs->search($cond)->update ({ $position_column => \ "$position_column $op 1" } );
bd7ca9e8 739 }
740 }
741 else {
742 $shift_rs->update ({ $position_column => \ "$position_column $op 1" } );
743 }
118e6b96 744}
745
7a76f44c 746=head1 PRIVATE METHODS
747
748These methods are used internally. You should never have the
749need to use them.
750
bd7ca9e8 751=head2 _group_rs
752
b250066f 753This method returns a resultset containing all members of the row
bd7ca9e8 754group (including the row itself).
755
756=cut
757sub _group_rs {
758 my $self = shift;
759 return $self->result_source->resultset->search({$self->_grouping_clause()});
760}
761
762=head2 _siblings
763
764Returns an unordered resultset of all objects in the same group
765excluding the object you called this method on.
766
767=cut
768sub _siblings {
769 my $self = shift;
770 my $position_column = $self->position_column;
867f1b28 771 my $pos;
772 return defined ($pos = $self->get_column($position_column))
5ef76b8b 773 ? $self->_group_rs->search(
867f1b28 774 { $position_column => { '!=' => $pos } },
5ef76b8b 775 )
776 : $self->_group_rs
777 ;
bd7ca9e8 778}
779
69cd8a7f 780=head2 _position
781
782 my $num_pos = $item->_position;
783
784Returns the B<absolute numeric position> of the current object, with the
785first object being at position 1, its sibling at position 2 and so on.
786
787=cut
788sub _position {
789 my $self = shift;
790 return $self->_position_from_value ($self->get_column ($self->position_column) );
791}
792
a733c37f 793=head2 _grouping_clause
118e6b96 794
bd7ca9e8 795This method returns one or more name=>value pairs for limiting a search
87b4a877 796by the grouping column(s). If the grouping column is not defined then
797this will return an empty list.
118e6b96 798
7a76f44c 799=cut
a733c37f 800sub _grouping_clause {
169bb185 801 my( $self ) = @_;
fa6b598f 802 return map { $_ => $self->get_column($_) } $self->_grouping_columns();
803}
804
fa6b598f 805=head2 _get_grouping_columns
806
807Returns a list of the column names used for grouping, regardless of whether
1d941d67 808they were specified as an arrayref or a single string, and returns ()
809if there is no grouping.
fa6b598f 810
811=cut
812sub _grouping_columns {
813 my( $self ) = @_;
a733c37f 814 my $col = $self->grouping_column();
fa6b598f 815 if (ref $col eq 'ARRAY') {
816 return @$col;
817 } elsif ($col) {
818 return ( $col );
819 } else {
820 return ();
133dd22a 821 }
7a76f44c 822}
823
bd7ca9e8 824=head2 _is_in_group
fa6b598f 825
826 $item->_is_in_group( {user => 'fred', list => 'work'} )
827
828Returns true if the object is in the group represented by hashref $other
bd7ca9e8 829
fa6b598f 830=cut
831sub _is_in_group {
832 my ($self, $other) = @_;
833 my $current = {$self->_grouping_clause};
bd7ca9e8 834
835 no warnings qw/uninitialized/;
836
837 return 0 if (
838 join ("\x00", sort keys %$current)
839 ne
840 join ("\x00", sort keys %$other)
841 );
fa6b598f 842 for my $key (keys %$current) {
fa6b598f 843 return 0 if $current->{$key} ne $other->{$key};
844 }
845 return 1;
846}
847
b250066f 848=head2 _ordered_internal_update
849
850This is a short-circuited method, that is used internally by this
851module to update positioning values in isolation (i.e. without
852triggering any of the positioning integrity code).
853
f045efad 854Some day you might get confronted by datasets that have ambiguous
48580715 855positioning data (e.g. duplicate position values within the same group,
b250066f 856in a table without unique constraints). When manually fixing such data
857keep in mind that you can not invoke L<DBIx::Class::Row/update> like
f045efad 858you normally would, as it will get confused by the wrong data before
b250066f 859having a chance to update the ill-defined row. If you really know what
f045efad 860you are doing use this method which bypasses any hooks introduced by
b250066f 861this module.
862
863=cut
864
bd7ca9e8 865sub _ordered_internal_update {
866 my $self = shift;
867 local $self->{_ORDERED_INTERNAL_UPDATE} = 1;
868 return $self->update (@_);
869}
fa6b598f 870
7a76f44c 8711;
118e6b96 872
bd7ca9e8 873__END__
dc66dea1 874
bd7ca9e8 875=head1 CAVEATS
dc66dea1 876
65285cf7 877=head2 Resultset Methods
878
879Note that all Insert/Create/Delete overrides are happening on
880L<DBIx::Class::Row> methods only. If you use the
881L<DBIx::Class::ResultSet> versions of
882L<update|DBIx::Class::ResultSet/update> or
883L<delete|DBIx::Class::ResultSet/delete>, all logic present in this
884module will be bypassed entirely (possibly resulting in a broken
885order-tree). Instead always use the
886L<update_all|DBIx::Class::ResultSet/update_all> and
887L<delete_all|DBIx::Class::ResultSet/delete_all> methods, which will
888invoke the corresponding L<row|DBIx::Class::Row> method on every
889member of the given resultset.
890
133dd22a 891=head2 Race Condition on Insert
892
65285cf7 893If a position is not specified for an insert, a position
bd7ca9e8 894will be chosen based either on L</_initial_position_value> or
895L</_next_position_value>, depending if there are already some
896items in the current group. The space of time between the
897necessary selects and insert introduces a race condition.
898Having unique constraints on your position/group columns,
899and using transactions (see L<DBIx::Class::Storage/txn_do>)
900will prevent such race conditions going undetected.
118e6b96 901
133dd22a 902=head2 Multiple Moves
903
48580715 904Be careful when issuing move_* methods to multiple objects. If
133dd22a 905you've pre-loaded the objects then when you move one of the objects
906the position of the other object will not reflect their new value
bd7ca9e8 907until you reload them from the database - see
908L<DBIx::Class::Row/discard_changes>.
133dd22a 909
dc66dea1 910There are times when you will want to move objects as groups, such
48580715 911as changing the parent of several objects at once - this directly
133dd22a 912conflicts with this problem. One solution is for us to write a
913ResultSet class that supports a parent() method, for example. Another
914solution is to somehow automagically modify the objects that exist
915in the current object's result set to have the new position value.
916
58755bba 917=head2 Default Values
918
919Using a database defined default_value on one of your group columns
920could result in the position not being assigned correctly.
921
118e6b96 922=head1 AUTHOR
923
8f535707 924 Original code framework
925 Aran Deltac <bluefeet@cpan.org>
926
927 Constraints support and code generalisation
928 Peter Rabbitson <ribasushi@cpan.org>
118e6b96 929
930=head1 LICENSE
931
932You may distribute this code under the same terms as Perl itself.
933