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