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