fixed typo in test
[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
e9188247 20Optionally, add one or more columns to specify groupings, allowing you
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
a8492531 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
45Specify the column that stores the position number for
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;
118e6b96 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 });
118e6b96 67
a733c37f 68 my $rs = $item->siblings();
69 my @siblings = $item->siblings();
118e6b96 70
71 my $sibling;
a733c37f 72 $sibling = $item->first_sibling();
73 $sibling = $item->last_sibling();
74 $sibling = $item->previous_sibling();
75 $sibling = $item->next_sibling();
118e6b96 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
a733c37f 89This module provides a simple interface for modifying the ordered
90position of DBIx::Class objects.
118e6b96 91
133dd22a 92=head1 AUTO UPDATE
93
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
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
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
a8492531 115This method specifies a column to limit all queries in
133dd22a 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
130that any unique constrainst can not be violated. This value defaults
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
147a resultset with no ordering applied use L</_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
218Returns the first sibling object, or 0 if the first sibling
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
259Returns the last sibling, or 0 if the last sibling is this
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
82a8f76f 275# an optimised method to get the last sibling position without inflating a row object
276sub _last_sibling_pos {
277 my $self = shift;
278 my $position_column = $self->position_column;
279
280 my $cursor = $self->next_siblings->search(
281 {},
282 { rows => 1, order_by => { '-desc' => $position_column }, columns => $position_column },
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;
316 return 0 unless $self->next_siblings->count;
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;
344 return $self->move_to( $self->_group_rs->count );
118e6b96 345}
346
347=head2 move_to
348
a733c37f 349 $item->move_to( $position );
118e6b96 350
a8492531 351Moves the object to the specified position. Returns 1 on
352success, and 0 if the object is already at the specified
353position.
118e6b96 354
355=cut
356
357sub move_to {
358 my( $self, $to_position ) = @_;
133dd22a 359 return 0 if ( $to_position < 1 );
118e6b96 360
bd7ca9e8 361 my $from_position = $self->_position;
362 return 0 if ( $from_position == $to_position );
363
364 my $position_column = $self->position_column;
365
bd7ca9e8 366 {
8f535707 367 my $guard = $self->result_source->schema->txn_scope_guard;
368
bd7ca9e8 369 my ($direction, @between);
370 if ( $from_position < $to_position ) {
371 $direction = -1;
372 @between = map { $self->_position_value ($_) } ( $from_position + 1, $to_position );
373 }
374 else {
375 $direction = 1;
376 @between = map { $self->_position_value ($_) } ( $to_position, $from_position - 1 );
377 }
378
379 my $new_pos_val = $self->_position_value ($to_position); # record this before the shift
b250066f 380
381 # we need to null-position the moved row if the position column is part of a constraint
382 if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $self->result_source->unique_constraints }} ) ) ) {
383 $self->_ordered_internal_update({ $position_column => $self->null_position_value });
384 }
385
bd7ca9e8 386 $self->_shift_siblings ($direction, @between);
387 $self->_ordered_internal_update({ $position_column => $new_pos_val });
fa6b598f 388
8f535707 389 $guard->commit;
390
bd7ca9e8 391 return 1;
392 }
393}
fa6b598f 394
79dc353a 395=head2 move_to_group
396
397 $item->move_to_group( $group, $position );
398
399Moves the object to the specified position of the specified
400group, or to the end of the group if $position is undef.
4011 is returned on success, and 0 is returned if the object is
402already at the specified position of the specified group.
403
1d941d67 404$group may be specified as a single scalar if only one
405grouping column is in use, or as a hashref of column => value pairs
406if multiple grouping columns are in use.
fa6b598f 407
79dc353a 408=cut
409
410sub move_to_group {
411 my( $self, $to_group, $to_position ) = @_;
fa6b598f 412
bd7ca9e8 413 $self->throw_exception ('move_to_group() expects a group specification')
414 unless defined $to_group;
415
fa6b598f 416 # if we're given a string, turn it into a hashref
417 unless (ref $to_group eq 'HASH') {
bd7ca9e8 418 my @gcols = $self->_grouping_columns;
419
420 $self->throw_exception ('Single group supplied for a multi-column group identifier') if @gcols > 1;
421 $to_group = {$gcols[0] => $to_group};
fa6b598f 422 }
423
79dc353a 424 my $position_column = $self->position_column;
79dc353a 425
79dc353a 426 return 0 if ( defined($to_position) and $to_position < 1 );
bd7ca9e8 427 if ($self->_is_in_group ($to_group) ) {
428 return 0 if not defined $to_position;
429 return $self->move_to ($to_position);
79dc353a 430 }
431
bd7ca9e8 432 {
8f535707 433 my $guard = $self->result_source->schema->txn_scope_guard;
434
bd7ca9e8 435 # Move to end of current group to adjust siblings
436 $self->move_last;
437
438 $self->set_inflated_columns({ %$to_group, $position_column => undef });
439 my $new_group_count = $self->_group_rs->count;
440
441 if ( not defined($to_position) or $to_position > $new_group_count) {
442 $self->set_column(
443 $position_column => $new_group_count
82a8f76f 444 ? $self->_next_position_value ( $self->_last_sibling_pos )
bd7ca9e8 445 : $self->_initial_position_value
446 );
447 }
448 else {
449 my $bumped_pos_val = $self->_position_value ($to_position);
450 my @between = ($to_position, $new_group_count);
451 $self->_shift_siblings (1, @between); #shift right
452 $self->set_column( $position_column => $bumped_pos_val );
453 }
454
455 $self->_ordered_internal_update;
456
8f535707 457 $guard->commit;
458
bd7ca9e8 459 return 1;
460 }
79dc353a 461}
462
118e6b96 463=head2 insert
464
465Overrides the DBIC insert() method by providing a default
466position number. The default will be the number of rows in
467the table +1, thus positioning the new record at the last position.
468
469=cut
470
471sub insert {
472 my $self = shift;
473 my $position_column = $self->position_column;
bd7ca9e8 474
475 unless ($self->get_column($position_column)) {
82a8f76f 476 my $lsib_pos = $self->_last_sibling_pos;
bd7ca9e8 477 $self->set_column(
82a8f76f 478 $position_column => (defined $lsib_pos
479 ? $self->_next_position_value ( $lsib_pos )
bd7ca9e8 480 : $self->_initial_position_value
481 )
482 );
483 }
484
0a298c73 485 return $self->next::method( @_ );
118e6b96 486}
487
79dc353a 488=head2 update
489
490Overrides the DBIC update() method by checking for a change
491to the position and/or group columns. Movement within a
492group or to another group is handled by repositioning
493the appropriate siblings. Position defaults to the end
494of a new group if it has been changed to undef.
495
496=cut
497
498sub update {
499 my $self = shift;
500
bd7ca9e8 501 # this is set by _ordered_internal_update()
502 return $self->next::method(@_) if $self->{_ORDERED_INTERNAL_UPDATE};
79dc353a 503
bd7ca9e8 504 my $upd = shift;
505 $self->set_inflated_columns($upd) if $upd;
79dc353a 506 my %changes = $self->get_dirty_columns;
507 $self->discard_changes;
508
bd7ca9e8 509 my $position_column = $self->position_column;
fa6b598f 510
bd7ca9e8 511 # if nothing group/position related changed - short circuit
512 if (not grep { exists $changes{$_} } ($self->_grouping_columns, $position_column) ) {
513 return $self->next::method( \%changes, @_ );
514 }
fa6b598f 515
bd7ca9e8 516 {
8f535707 517 my $guard = $self->result_source->schema->txn_scope_guard;
518
bd7ca9e8 519 # if any of our grouping columns have been changed
520 if (grep { exists $changes{$_} } ($self->_grouping_columns) ) {
521
522 # create new_group by taking the current group and inserting changes
523 my $new_group = {$self->_grouping_clause};
524 foreach my $col (keys %$new_group) {
525 if (exists $changes{$col}) {
526 $new_group->{$col} = delete $changes{$col}; # don't want to pass this on to next::method
527 }
fa6b598f 528 }
bd7ca9e8 529
530 $self->move_to_group(
531 $new_group,
532 (exists $changes{$position_column}
533 # The FIXME bit contradicts the documentation: when changing groups without supplying explicit
534 # positions in move_to_group(), we push the item to the end of the group.
535 # However when I was rewriting this, the position from the old group was clearly passed to the new one
536 # Probably needs to go away (by ribasushi)
537 ? delete $changes{$position_column} # means there was a position change supplied with the update too
538 : $self->_position # FIXME!
539 ),
540 );
541 }
542 elsif (exists $changes{$position_column}) {
543 $self->move_to(delete $changes{$position_column});
fa6b598f 544 }
545
8f535707 546 my @res;
547 my $want = wantarray();
548 if (not defined $want) {
549 $self->next::method( \%changes, @_ );
550 }
551 elsif ($want) {
552 @res = $self->next::method( \%changes, @_ );
553 }
554 else {
555 $res[0] = $self->next::method( \%changes, @_ );
556 }
557
558 $guard->commit;
559 return $want ? @res : $res[0];
79dc353a 560 }
79dc353a 561}
562
118e6b96 563=head2 delete
564
565Overrides the DBIC delete() method by first moving the object
bd7ca9e8 566to the last position, then deleting it, thus ensuring the
118e6b96 567integrity of the positions.
568
569=cut
570
571sub delete {
572 my $self = shift;
8f535707 573
574 my $guard = $self->result_source->schema->txn_scope_guard;
575
576 $self->move_last;
577
578 my @res;
579 my $want = wantarray();
580 if (not defined $want) {
581 $self->next::method( @_ );
bd7ca9e8 582 }
8f535707 583 elsif ($want) {
584 @res = $self->next::method( @_ );
585 }
586 else {
587 $res[0] = $self->next::method( @_ );
588 }
589
590 $guard->commit;
591 return $want ? @res : $res[0];
bd7ca9e8 592}
593
b250066f 594=head1 METHODS FOR EXTENDING ORDERED
bd7ca9e8 595
596You would want to override the methods below if you use sparse
597(non-linear) or non-numeric position values. This can be useful
598if you are working with preexisting non-normalised position data,
599or if you need to work with materialized path columns.
600
601=head2 _position
602
603 my $num_pos = $item->_position;
604
b250066f 605Returns the B<absolute numeric position> of the current object, with the
bd7ca9e8 606first object being at position 1, its sibling at position 2 and so on.
607By default simply returns the value of L</position_column>.
608
609=cut
610sub _position {
611 my $self = shift;
612
613# #the right way to do this
614# return $self->previous_siblings->count + 1;
615
616 return $self->get_column ($self->position_column);
617}
618
619=head2 _position_value
620
621 my $pos_value = $item->_position_value ( $pos )
622
b250066f 623Returns the B<value> of L</position_column> of the object at numeric
bd7ca9e8 624position C<$pos>. By default simply returns C<$pos>.
625
626=cut
627sub _position_value {
628 my ($self, $pos) = @_;
629
630# #the right way to do this (not optimized)
631# my $position_column = $self->position_column;
632# return $self -> _group_rs
633# -> search({}, { order_by => $position_column })
634# -> slice ( $pos - 1)
635# -> single
636# -> get_column ($position_column);
637
638 return $pos;
639}
640
641=head2 _initial_position_value
642
643 __PACKAGE__->_initial_position_value(0);
644
b250066f 645This method specifies a B<value> of L</position_column> which is assigned
bd7ca9e8 646to the first inserted element of a group, if no value was supplied at
647insertion time. All subsequent values are derived from this one by
648L</_next_position_value> below. Defaults to 1.
649
650=cut
651
652__PACKAGE__->mk_classdata( '_initial_position_value' => 1 );
653
654=head2 _next_position_value
655
656 my $new_value = $item->_next_position_value ( $position_value )
657
b250066f 658Returns a position B<value> that would be considered C<next> with
bd7ca9e8 659regards to C<$position_value>. Can be pretty much anything, given
660that C<< $position_value < $new_value >> where C<< < >> is the
661SQL comparison operator (usually works fine on strings). The
662default method expects C<$position_value> to be numeric, and
663returns C<$position_value + 1>
664
665=cut
666sub _next_position_value {
667 return $_[1] + 1;
668}
669
670=head2 _shift_siblings
671
672 $item->_shift_siblings ($direction, @between)
673
b250066f 674Shifts all siblings with B<positions values> in the range @between
675(inclusive) by one position as specified by $direction (left if < 0,
676 right if > 0). By default simply increments/decrements each
677L<position_column> value by 1, doing so in a way as to not violate
678any existing constraints.
679
680Note that if you override this method and have unique constraints
681including the L<position_column> the shift is not a trivial task.
682Refer to the implementation source of the default method for more
683information.
bd7ca9e8 684
685=cut
686sub _shift_siblings {
687 my ($self, $direction, @between) = @_;
688 return 0 unless $direction;
689
690 my $position_column = $self->position_column;
691
692 my ($op, $ord);
693 if ($direction < 0) {
694 $op = '-';
695 $ord = 'asc';
696 }
697 else {
698 $op = '+';
699 $ord = 'desc';
700 }
701
702 my $shift_rs = $self->_group_rs-> search ({ $position_column => { -between => \@between } });
703
704 # some databases (sqlite) are dumb and can not do a blanket
705 # increment/decrement. So what we do here is check if the
706 # position column is part of a unique constraint, and do a
707 # one-by-one update if this is the case
708
82a8f76f 709 my $rsrc = $self->result_source;
710
711 if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $rsrc->unique_constraints }} ) ) ) {
712
713 my @pcols = $rsrc->primary_columns;
714 my $cursor = $shift_rs->search ({}, { order_by => { "-$ord", $position_column }, columns => \@pcols } )->cursor;
715 my $rs = $self->result_source->resultset;
716
717 while (my @pks = $cursor->next ) {
718
719 my $cond;
720 for my $i (0.. $#pcols) {
721 $cond->{$pcols[$i]} = $pks[$i];
722 }
bd7ca9e8 723
82a8f76f 724 $rs->search($cond)->update ({ $position_column => \ "$position_column $op 1" } );
bd7ca9e8 725 }
726 }
727 else {
728 $shift_rs->update ({ $position_column => \ "$position_column $op 1" } );
729 }
118e6b96 730}
731
7a76f44c 732=head1 PRIVATE METHODS
733
734These methods are used internally. You should never have the
735need to use them.
736
bd7ca9e8 737=head2 _group_rs
738
b250066f 739This method returns a resultset containing all members of the row
bd7ca9e8 740group (including the row itself).
741
742=cut
743sub _group_rs {
744 my $self = shift;
745 return $self->result_source->resultset->search({$self->_grouping_clause()});
746}
747
748=head2 _siblings
749
750Returns an unordered resultset of all objects in the same group
751excluding the object you called this method on.
752
753=cut
754sub _siblings {
755 my $self = shift;
756 my $position_column = $self->position_column;
757 return $self->_group_rs->search(
758 { $position_column => { '!=' => $self->get_column($position_column) } },
759 );
760}
761
a733c37f 762=head2 _grouping_clause
118e6b96 763
bd7ca9e8 764This method returns one or more name=>value pairs for limiting a search
765by the grouping column(s). If the grouping column is not
133dd22a 766defined then this will return an empty list.
118e6b96 767
7a76f44c 768=cut
a733c37f 769sub _grouping_clause {
169bb185 770 my( $self ) = @_;
fa6b598f 771 return map { $_ => $self->get_column($_) } $self->_grouping_columns();
772}
773
fa6b598f 774=head2 _get_grouping_columns
775
776Returns a list of the column names used for grouping, regardless of whether
1d941d67 777they were specified as an arrayref or a single string, and returns ()
778if there is no grouping.
fa6b598f 779
780=cut
781sub _grouping_columns {
782 my( $self ) = @_;
a733c37f 783 my $col = $self->grouping_column();
fa6b598f 784 if (ref $col eq 'ARRAY') {
785 return @$col;
786 } elsif ($col) {
787 return ( $col );
788 } else {
789 return ();
133dd22a 790 }
7a76f44c 791}
792
bd7ca9e8 793=head2 _is_in_group
fa6b598f 794
795 $item->_is_in_group( {user => 'fred', list => 'work'} )
796
797Returns true if the object is in the group represented by hashref $other
bd7ca9e8 798
fa6b598f 799=cut
800sub _is_in_group {
801 my ($self, $other) = @_;
802 my $current = {$self->_grouping_clause};
bd7ca9e8 803
804 no warnings qw/uninitialized/;
805
806 return 0 if (
807 join ("\x00", sort keys %$current)
808 ne
809 join ("\x00", sort keys %$other)
810 );
fa6b598f 811 for my $key (keys %$current) {
fa6b598f 812 return 0 if $current->{$key} ne $other->{$key};
813 }
814 return 1;
815}
816
b250066f 817=head2 _ordered_internal_update
818
819This is a short-circuited method, that is used internally by this
820module to update positioning values in isolation (i.e. without
821triggering any of the positioning integrity code).
822
f045efad 823Some day you might get confronted by datasets that have ambiguous
824positioning data (i.e. duplicate position values within the same group,
b250066f 825in a table without unique constraints). When manually fixing such data
826keep in mind that you can not invoke L<DBIx::Class::Row/update> like
f045efad 827you normally would, as it will get confused by the wrong data before
b250066f 828having a chance to update the ill-defined row. If you really know what
f045efad 829you are doing use this method which bypasses any hooks introduced by
b250066f 830this module.
831
832=cut
833
bd7ca9e8 834sub _ordered_internal_update {
835 my $self = shift;
836 local $self->{_ORDERED_INTERNAL_UPDATE} = 1;
837 return $self->update (@_);
838}
fa6b598f 839
7a76f44c 8401;
118e6b96 841
bd7ca9e8 842__END__
dc66dea1 843
bd7ca9e8 844=head1 CAVEATS
dc66dea1 845
133dd22a 846=head2 Race Condition on Insert
847
118e6b96 848If a position is not specified for an insert than a position
bd7ca9e8 849will be chosen based either on L</_initial_position_value> or
850L</_next_position_value>, depending if there are already some
851items in the current group. The space of time between the
852necessary selects and insert introduces a race condition.
853Having unique constraints on your position/group columns,
854and using transactions (see L<DBIx::Class::Storage/txn_do>)
855will prevent such race conditions going undetected.
118e6b96 856
133dd22a 857=head2 Multiple Moves
858
859Be careful when issueing move_* methods to multiple objects. If
860you've pre-loaded the objects then when you move one of the objects
861the position of the other object will not reflect their new value
bd7ca9e8 862until you reload them from the database - see
863L<DBIx::Class::Row/discard_changes>.
133dd22a 864
dc66dea1 865There are times when you will want to move objects as groups, such
133dd22a 866as changeing the parent of several objects at once - this directly
867conflicts with this problem. One solution is for us to write a
868ResultSet class that supports a parent() method, for example. Another
869solution is to somehow automagically modify the objects that exist
870in the current object's result set to have the new position value.
871
58755bba 872=head2 Default Values
873
874Using a database defined default_value on one of your group columns
875could result in the position not being assigned correctly.
876
118e6b96 877=head1 AUTHOR
878
8f535707 879 Original code framework
880 Aran Deltac <bluefeet@cpan.org>
881
882 Constraints support and code generalisation
883 Peter Rabbitson <ribasushi@cpan.org>
118e6b96 884
885=head1 LICENSE
886
887You may distribute this code under the same terms as Perl itself.
888