Restore ability to handle underdefined root (t/prefetch/incomplete.t)
[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
8273e845 23Optionally, add one or more columns to specify groupings, allowing you
e9188247 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
8273e845 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
8273e845 48Specify the column that stores the position number for
118e6b96 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
8273e845 92This module provides a simple interface for modifying the ordered
a733c37f 93position of DBIx::Class objects.
118e6b96 94
133dd22a 95=head1 AUTO UPDATE
96
8273e845 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
133dd22a 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
8273e845 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
8273e845 118This method specifies a column to limit all queries in
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
8273e845 221Returns the first sibling object, or 0 if the first sibling
bd7ca9e8 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
8273e845 262Returns the last sibling, or 0 if the last sibling is this
bd7ca9e8 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
fb13a49f 278# an optimized method to get the last sibling position value without inflating a result object
d7c0e320 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
8273e845 418$group may be specified as a single scalar if only one
1d941d67 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
8273e845 492Overrides the DBIC insert() method by providing a default
493position number. The default will be the number of rows in
118e6b96 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
1abccf54 584 $self->next::method( @_ );
8f535707 585
586 $guard->commit;
1abccf54 587
588 return $self;
bd7ca9e8 589}
590
5ef76b8b 591# add the current position/group to the things we track old values for
592sub _track_storage_value {
593 my ($self, $col) = @_;
594 return $self->next::method($col) || defined first { $_ eq $col } ($self->position_column, $self->_grouping_columns);
595}
596
b250066f 597=head1 METHODS FOR EXTENDING ORDERED
bd7ca9e8 598
599You would want to override the methods below if you use sparse
600(non-linear) or non-numeric position values. This can be useful
601if you are working with preexisting non-normalised position data,
602or if you need to work with materialized path columns.
603
d7c0e320 604=head2 _position_from_value
605
69cd8a7f 606 my $num_pos = $item->_position_from_value ( $pos_value )
d7c0e320 607
608Returns the B<absolute numeric position> of an object with a B<position
609value> set to C<$pos_value>. By default simply returns C<$pos_value>.
610
611=cut
612sub _position_from_value {
613 my ($self, $val) = @_;
614
615 return 0 unless defined $val;
616
617# #the right way to do this
618# return $self -> _group_rs
619# -> search({ $self->position_column => { '<=', $val } })
620# -> count
621
622 return $val;
623}
624
bd7ca9e8 625=head2 _position_value
626
627 my $pos_value = $item->_position_value ( $pos )
628
b250066f 629Returns the B<value> of L</position_column> of the object at numeric
bd7ca9e8 630position C<$pos>. By default simply returns C<$pos>.
631
632=cut
633sub _position_value {
634 my ($self, $pos) = @_;
635
636# #the right way to do this (not optimized)
637# my $position_column = $self->position_column;
638# return $self -> _group_rs
639# -> search({}, { order_by => $position_column })
640# -> slice ( $pos - 1)
641# -> single
642# -> get_column ($position_column);
643
644 return $pos;
645}
646
647=head2 _initial_position_value
648
649 __PACKAGE__->_initial_position_value(0);
650
b250066f 651This method specifies a B<value> of L</position_column> which is assigned
bd7ca9e8 652to the first inserted element of a group, if no value was supplied at
653insertion time. All subsequent values are derived from this one by
654L</_next_position_value> below. Defaults to 1.
655
656=cut
657
658__PACKAGE__->mk_classdata( '_initial_position_value' => 1 );
659
660=head2 _next_position_value
661
662 my $new_value = $item->_next_position_value ( $position_value )
663
b250066f 664Returns a position B<value> that would be considered C<next> with
bd7ca9e8 665regards to C<$position_value>. Can be pretty much anything, given
666that C<< $position_value < $new_value >> where C<< < >> is the
667SQL comparison operator (usually works fine on strings). The
668default method expects C<$position_value> to be numeric, and
669returns C<$position_value + 1>
670
671=cut
672sub _next_position_value {
673 return $_[1] + 1;
674}
675
676=head2 _shift_siblings
677
678 $item->_shift_siblings ($direction, @between)
679
b250066f 680Shifts all siblings with B<positions values> in the range @between
681(inclusive) by one position as specified by $direction (left if < 0,
682 right if > 0). By default simply increments/decrements each
f92a9d79 683L</position_column> value by 1, doing so in a way as to not violate
b250066f 684any existing constraints.
685
686Note that if you override this method and have unique constraints
f92a9d79 687including the L</position_column> the shift is not a trivial task.
b250066f 688Refer to the implementation source of the default method for more
689information.
bd7ca9e8 690
691=cut
692sub _shift_siblings {
693 my ($self, $direction, @between) = @_;
694 return 0 unless $direction;
695
696 my $position_column = $self->position_column;
697
698 my ($op, $ord);
699 if ($direction < 0) {
700 $op = '-';
701 $ord = 'asc';
702 }
703 else {
704 $op = '+';
705 $ord = 'desc';
706 }
707
375c84bb 708 my $shift_rs = $self->_group_rs-> search ({ $position_column => { -between => \@between } });
709
710 # some databases (sqlite, pg, perhaps others) are dumb and can not do a
711 # blanket increment/decrement without violating a unique constraint.
712 # So what we do here is check if the position column is part of a unique
713 # constraint, and do a one-by-one update if this is the case.
714 my $rsrc = $self->result_source;
715
716 # set in case there are more cascades combined with $rs->update => $rs_update_all overrides
717 local $rsrc->schema->{_ORDERED_INTERNAL_UPDATE} = 1;
718 my @pcols = $rsrc->primary_columns;
719 if (
720 first { $_ eq $position_column } ( map { @$_ } (values %{{ $rsrc->unique_constraints }} ) )
721 ) {
722 my $cursor = $shift_rs->search (
723 {}, { order_by => { "-$ord", $position_column }, select => [$position_column, @pcols] }
724 )->cursor;
725 my $rs = $rsrc->resultset;
726
727 my @all_data = $cursor->all;
728 while (my $data = shift @all_data) {
729 my $pos = shift @$data;
730 my $cond;
731 for my $i (0.. $#pcols) {
732 $cond->{$pcols[$i]} = $data->[$i];
733 }
734
735 $rs->find($cond)->update ({ $position_column => $pos + ( ($op eq '+') ? 1 : -1 ) });
736 }
737 }
738 else {
739 $shift_rs->update ({ $position_column => \ "$position_column $op 1" } );
740 }
118e6b96 741}
742
7a76f44c 743
4815bb2c 744# This method returns a resultset containing all members of the row
745# group (including the row itself).
bd7ca9e8 746sub _group_rs {
747 my $self = shift;
748 return $self->result_source->resultset->search({$self->_grouping_clause()});
749}
750
4815bb2c 751# Returns an unordered resultset of all objects in the same group
752# excluding the object you called this method on.
bd7ca9e8 753sub _siblings {
754 my $self = shift;
755 my $position_column = $self->position_column;
867f1b28 756 my $pos;
757 return defined ($pos = $self->get_column($position_column))
5ef76b8b 758 ? $self->_group_rs->search(
867f1b28 759 { $position_column => { '!=' => $pos } },
5ef76b8b 760 )
761 : $self->_group_rs
762 ;
bd7ca9e8 763}
764
4815bb2c 765# Returns the B<absolute numeric position> of the current object, with the
766# first object being at position 1, its sibling at position 2 and so on.
69cd8a7f 767sub _position {
768 my $self = shift;
769 return $self->_position_from_value ($self->get_column ($self->position_column) );
770}
771
4815bb2c 772# This method returns one or more name=>value pairs for limiting a search
773# by the grouping column(s). If the grouping column is not defined then
774# this will return an empty list.
a733c37f 775sub _grouping_clause {
169bb185 776 my( $self ) = @_;
fa6b598f 777 return map { $_ => $self->get_column($_) } $self->_grouping_columns();
778}
779
4815bb2c 780# Returns a list of the column names used for grouping, regardless of whether
781# they were specified as an arrayref or a single string, and returns ()
782# if there is no grouping.
fa6b598f 783sub _grouping_columns {
784 my( $self ) = @_;
a733c37f 785 my $col = $self->grouping_column();
fa6b598f 786 if (ref $col eq 'ARRAY') {
787 return @$col;
788 } elsif ($col) {
789 return ( $col );
790 } else {
791 return ();
133dd22a 792 }
7a76f44c 793}
794
4815bb2c 795# Returns true if the object is in the group represented by hashref $other
fa6b598f 796sub _is_in_group {
797 my ($self, $other) = @_;
798 my $current = {$self->_grouping_clause};
bd7ca9e8 799
800 no warnings qw/uninitialized/;
801
802 return 0 if (
803 join ("\x00", sort keys %$current)
804 ne
805 join ("\x00", sort keys %$other)
806 );
fa6b598f 807 for my $key (keys %$current) {
fa6b598f 808 return 0 if $current->{$key} ne $other->{$key};
809 }
810 return 1;
811}
812
4815bb2c 813# This is a short-circuited method, that is used internally by this
814# module to update positioning values in isolation (i.e. without
815# triggering any of the positioning integrity code).
816#
817# Some day you might get confronted by datasets that have ambiguous
818# positioning data (e.g. duplicate position values within the same group,
819# in a table without unique constraints). When manually fixing such data
820# keep in mind that you can not invoke L<DBIx::Class::Row/update> like
821# you normally would, as it will get confused by the wrong data before
822# having a chance to update the ill-defined row. If you really know what
823# you are doing use this method which bypasses any hooks introduced by
824# this module.
bd7ca9e8 825sub _ordered_internal_update {
826 my $self = shift;
0c342f8e 827 local $self->result_source->schema->{_ORDERED_INTERNAL_UPDATE} = 1;
bd7ca9e8 828 return $self->update (@_);
829}
fa6b598f 830
7a76f44c 8311;
118e6b96 832
bd7ca9e8 833__END__
dc66dea1 834
bd7ca9e8 835=head1 CAVEATS
dc66dea1 836
65285cf7 837=head2 Resultset Methods
838
839Note that all Insert/Create/Delete overrides are happening on
840L<DBIx::Class::Row> methods only. If you use the
841L<DBIx::Class::ResultSet> versions of
842L<update|DBIx::Class::ResultSet/update> or
843L<delete|DBIx::Class::ResultSet/delete>, all logic present in this
844module will be bypassed entirely (possibly resulting in a broken
845order-tree). Instead always use the
846L<update_all|DBIx::Class::ResultSet/update_all> and
847L<delete_all|DBIx::Class::ResultSet/delete_all> methods, which will
848invoke the corresponding L<row|DBIx::Class::Row> method on every
849member of the given resultset.
850
133dd22a 851=head2 Race Condition on Insert
852
65285cf7 853If a position is not specified for an insert, a position
bd7ca9e8 854will be chosen based either on L</_initial_position_value> or
855L</_next_position_value>, depending if there are already some
856items in the current group. The space of time between the
857necessary selects and insert introduces a race condition.
858Having unique constraints on your position/group columns,
859and using transactions (see L<DBIx::Class::Storage/txn_do>)
860will prevent such race conditions going undetected.
118e6b96 861
133dd22a 862=head2 Multiple Moves
863
8273e845 864Be careful when issuing move_* methods to multiple objects. If
865you've pre-loaded the objects then when you move one of the objects
866the position of the other object will not reflect their new value
bd7ca9e8 867until you reload them from the database - see
868L<DBIx::Class::Row/discard_changes>.
133dd22a 869
8273e845 870There are times when you will want to move objects as groups, such
871as changing the parent of several objects at once - this directly
872conflicts with this problem. One solution is for us to write a
873ResultSet class that supports a parent() method, for example. Another
874solution is to somehow automagically modify the objects that exist
133dd22a 875in the current object's result set to have the new position value.
876
58755bba 877=head2 Default Values
878
879Using a database defined default_value on one of your group columns
880could result in the position not being assigned correctly.
881
118e6b96 882=head1 AUTHOR
883
8f535707 884 Original code framework
885 Aran Deltac <bluefeet@cpan.org>
886
887 Constraints support and code generalisation
888 Peter Rabbitson <ribasushi@cpan.org>
118e6b96 889
890=head1 LICENSE
891
892You may distribute this code under the same terms as Perl itself.
893