NAME

DBIO::Ordered - Maintain a position column over an ordered list of rows

VERSION

version 0.900000

SYNOPSIS

package MyApp::Schema::Result::Item;
use base 'DBIO::Core';

__PACKAGE__->load_components(qw/Ordered/);
__PACKAGE__->table('items');
__PACKAGE__->add_columns(
  item_id  => { data_type => 'integer', is_auto_increment => 1 },
  name     => { data_type => 'varchar', size => 100 },
  position => { data_type => 'integer', position => 1 },
);
__PACKAGE__->set_primary_key('item_id');

With one or more grouping columns for independent ordered lists per group:

__PACKAGE__->add_columns(
  item_id  => { data_type => 'integer', is_auto_increment => 1 },
  name     => { data_type => 'varchar', size => 100 },
  position => { data_type => 'integer', position => 1 },
  group_id => { data_type => 'integer', grouping => 1 },
);

In code:

my $rs       = $item->siblings;
my $sibling  = $item->first_sibling;
$item->move_previous;
$item->move_next;
$item->move_first;
$item->move_last;
$item->move_to($position);
$item->move_to_group('groupname');
$item->move_to_group('groupname', $position);
$item->move_to_group({ group_id => 'a', other_group_id => 'b' }, $position);

DESCRIPTION

Maintains a position column over an ordered list of rows. Mark the position column with position => 1 in add_columns; mark zero or more grouping columns with grouping => 1 to maintain independent ordered lists within the same table.

The move_* methods automatically update sibling rows to keep the order contiguous. This is not configurable -- moving one row in an ordered list always shifts others.

ATTRIBUTES

_initial_position_value

__PACKAGE__->_initial_position_value(0);

Position value assigned to the first row of a group when no value is supplied. Defaults to 1.

METHODS

position_column

my $col = $self->position_column;

Returns the name of the column flagged with position => 1. Throws if no such column is defined on the result source.

grouping_column

my $col_or_aref = $self->grouping_column;

Returns the column (or arrayref of columns) flagged with grouping => 1. Returns a single scalar when exactly one column is flagged, an arrayref when multiple are flagged, and undef when no grouping is configured.

null_position_value

my $val = $self->null_position_value;

Returns the value used as a placeholder while a row is being moved, so unique constraints involving the position column are not violated. Reads from the null_position_value flag on the position column; defaults to 0.

siblings

my $rs = $item->siblings;

Returns an ordered resultset of all other rows in the same group, excluding the row this was called on.

previous_siblings

my $rs = $item->previous_siblings;

Returns a resultset of all rows in the same group positioned before this one.

next_siblings

my $rs = $item->next_siblings;

Returns a resultset of all rows in the same group positioned after this one.

previous_sibling

Returns the row one position before this one, or 0 if this is the first.

first_sibling

Returns the first row in the group, or 0 if this is the first.

next_sibling

Returns the row one position after this one, or 0 if this is the last.

last_sibling

Returns the last row in the group, or 0 if this is the last.

move_previous

Swaps with the sibling one position before this one. Returns 1 on success, 0 if already first.

move_next

Swaps with the sibling one position after this one. Returns 1 on success, 0 if already last.

move_first

Moves to the first position. Returns 1 on success, 0 if already first.

move_last

Moves to the last position. Returns 1 on success, 0 if already last.

move_to

$item->move_to($position);

Moves to the specified position. Returns 1 on success, 0 if already at that position.

move_to_group

$item->move_to_group($group, $position);

Moves to the specified position of the specified group, or to the end of the group if $position is undef. Returns 1 on success, 0 if already at that position. $group may be a single scalar (when only one grouping column is in use) or a hashref of column => value pairs.

insert

Assigns a default position (one past the current last sibling) when the position column has no value set.

update

If the position or grouping columns changed, performs the move via "move_to" or "move_to_group" to keep siblings consistent.

delete

Moves to the last position before deleting, keeping the order tree contiguous.

_position_from_value

my $num_pos = $item->_position_from_value($pos_value);

Returns the absolute numeric position of a row given a raw position value. Default: returns $pos_value unchanged.

_position_value

my $pos_value = $item->_position_value($pos);

Returns the value of "position_column" for the row at numeric position $pos. Default: returns $pos unchanged.

_next_position_value

my $new_value = $item->_next_position_value($position_value);

Returns the next position value after $position_value. Default: $position_value + 1.

_shift_siblings

$item->_shift_siblings($direction, @between);

Shifts all siblings whose position values fall within the inclusive range @between by one position in the given direction (left if < 0, right if > 0). Handles unique-constraint cases by falling back to a one-by-one update.

COLUMN FLAGS

position => 1

Marks the column that stores the integer position of each row.

grouping => 1

Marks a column as a grouping key. Multiple columns may be flagged -- in that case all of them must match for two rows to be considered siblings.

null_position_value => $value

Set on the position column. Specifies the placeholder value used while a row is mid-move, so unique constraints involving the position column are not violated. Defaults to 0; set to undef if your positions start at 0.

OVERRIDABLE METHODS

Override these in your Result class if you use sparse (non-linear) or non-numeric position values, e.g. when working with materialized path columns.

_position_from_value($value)

Maps a stored position value to an absolute numeric position. Default: identity.

_position_value($position)

Inverse of the above. Default: identity.

_next_position_value($value)

Returns the next position value after $value. Default: $value + 1.

_initial_position_value

Class data with the position value used for the first row of a group. Defaults to 1.

CAVEATS

Resultset methods

All insert/create/delete overrides happen on DBIO::Row. If you use the DBIO::ResultSet versions of update or delete, all logic in this component is bypassed. Use update_all / delete_all instead -- they invoke the row method on every member.

Race condition on insert

If no position is supplied at insert time, one is chosen based on "_initial_position_value" or "_next_position_value". The window between select and insert introduces a race. Add unique constraints on the position/group columns and use transactions to prevent silent corruption.

Multiple moves

When multiple same-group rows are loaded from storage, move_* operations on them can drift out of sync with the underlying storage. Wrapping in a transaction triggers an implicit reload; otherwise call "discard_changes" in DBIO::Row to refresh.

Default values

Database-side default values on grouping columns can result in incorrect position assignment.

AUTHOR

DBIO & DBIx::Class Authors

COPYRIGHT AND LICENSE

Copyright (C) 2026 DBIO Authors Portions Copyright (C) 2005-2025 DBIx::Class Authors Based on DBIx::Class, heavily modified.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.