Alzabo

 view release on metacpan or  search on metacpan

lib/Alzabo/Create/Table.pm  view on Meta::CPAN

package Alzabo::Create::Table;

use strict;
use vars qw($VERSION);

use Alzabo::Create;
use Alzabo::Exceptions ( abbr => 'params_exception' );

use Params::Validate qw( :all );
Params::Validate::validation_options
    ( on_fail => sub { params_exception join '', @_ } );

use Tie::IxHash;

use base qw(Alzabo::Table);

$VERSION = 2.0;

1;

sub new
{
    my $proto = shift;
    my $class = ref $proto || $proto;

    validate( @_, { schema => { isa => 'Alzabo::Create::Schema' },
                    name => { type => SCALAR },
                    attributes => { type => ARRAYREF,
                                    optional => 1 },
                    comment => { type => UNDEF | SCALAR,
                                 default => '' },
                  } );
    my %p = @_;

    my $self = bless {}, $class;

    $self->{schema} = $p{schema};

    $self->set_name($p{name});

    $self->{columns} = Tie::IxHash->new;
    $self->{pk} = [];
    $self->{indexes} = Tie::IxHash->new;

    my %attr;
    tie %{ $self->{attributes} }, 'Tie::IxHash';

    $self->set_attributes( @{ $p{attributes} } );

    $self->set_comment( $p{comment} );

    # Setting this prevents run time type errors.
    $self->{fk} = {};

    return $self;
}

sub set_name
{
    my $self = shift;

    validate_pos( @_, { type => SCALAR } );
    my $name = shift;

    params_exception "Table $name already exists in schema"
        if $self->schema->has_table($name);

    my @i;
    if ($self->{indexes})
    {
        @i = $self->indexes;
        $self->delete_index($_) foreach @i;
    }

    my $old_name = $self->{name};
    $self->{name} = $name;

    eval
    {
        $self->schema->rules->validate_table_name($self);
    };

    $self->add_index($_) foreach @i;

    if ($@)
    {
        $self->{name} = $old_name;

        rethrow_exception($@);
    }

    if ( $old_name && eval { $self->schema->table($old_name) } )
    {
        $self->schema->register_table_name_change( table => $self,
                                                   old_name => $old_name );

        foreach my $fk ($self->all_foreign_keys)
        {
            $fk->table_to->register_table_name_change( table => $self,
                                                       old_name => $old_name );
        }
    }
}

sub make_column
{
    my $self = shift;
    my %p = @_;

    my $is_pk = delete $p{primary_key};

    my %p2;
    foreach ( qw( before after ) )
    {
        $p2{$_} = delete $p{$_} if exists $p{$_};
    }
    $self->add_column( column => Alzabo::Create::Column->new( table => $self,
                                                              %p ),
                       %p2 );

    my $col = $self->column( $p{name} );
    $self->add_primary_key($col) if $is_pk;

    return $col;
}

sub add_column
{
    my $self = shift;

    validate( @_, { column => { isa => 'Alzabo::Create::Column' },
                    before => { optional => 1 },
                    after  => { optional => 1 } } );
    my %p = @_;

    my $col = $p{column};

    params_exception "Column " . $col->name . " already exists in " . $self->name
        if $self->{columns}->EXISTS( $col->name );

    $col->set_table($self) unless $col->table eq $self;

    $self->{columns}->STORE( $col->name, $col);

    foreach ( qw( before after ) )
    {
        if ( exists $p{$_} )
        {
            $self->move_column( $_ => $p{$_},
                                column => $col );
            last;
        }
    }
}

sub delete_column
{
    my $self = shift;

    validate_pos( @_, { isa => 'Alzabo::Create::Column' } );
    my $col = shift;

    params_exception"Column $col doesn't exist in $self->{name}"
        unless $self->{columns}->EXISTS( $col->name );

    $self->delete_primary_key($col) if $col->is_primary_key;

    foreach my $fk ($self->foreign_keys_by_column($col))
    {
        $self->delete_foreign_key($fk);

        foreach my $other_fk ($fk->table_to->foreign_keys( table => $self,
                                                           column => $fk->columns_to ) )
        {
            $fk->table_to->delete_foreign_key( $other_fk );
        }
    }

    foreach my $i ($self->indexes)
    {
        $self->delete_index($i) if grep { $_ eq $col } $i->columns;
    }

    $self->{columns}->DELETE( $col->name );
}

sub move_column
{
    my $self = shift;

    validate( @_, { column  => { isa => 'Alzabo::Create::Column' },
                    before  => { isa => 'Alzabo::Create::Column',
                                 optional => 1 },
                    after   => { isa => 'Alzabo::Create::Column',
                                 optional => 1 } } );
    my %p = @_;

    if ( exists $p{before} && exists $p{after} )
    {
        params_exception
            "move_column method cannot be called with both 'before' and 'after' parameters";
    }

    if ( exists $p{before} )
    {
        params_exception "Column " . $p{before}->name . " doesn't exist in schema"
            unless $self->{columns}->EXISTS( $p{before}->name );
    }
    else
    {
        params_exception "Column " . $p{after}->name . " doesn't exist in schema"
            unless $self->{columns}->EXISTS( $p{after}->name );
    }

    params_exception "Column " . $p{column}->name . " doesn't exist in schema"
        unless $self->{columns}->EXISTS( $p{column}->name );

    my @pk = $self->primary_key;

    $self->{columns}->DELETE( $p{column}->name );

    my $index;
    if ( $p{before} )
    {
        $index = $self->{columns}->Indices( $p{before}->name );
    }
    else
    {
        $index = $self->{columns}->Indices( $p{after}->name ) + 1;
    }

    $self->{columns}->Splice( $index, 0, $p{column}->name => $p{column} );

    $self->{pk} = [ $self->{columns}->Indices( map { $_->name } @pk ) ];
}

sub add_primary_key
{
    my $self = shift;

    validate_pos( @_, { isa => 'Alzabo::Create::Column' } );
    my $col = shift;

    my $name = $col->name;
    params_exception "Column $name doesn't exist in $self->{name}"
        unless $self->{columns}->EXISTS($name);

    params_exception "Column $name is already a primary key"
        if $col->is_primary_key;

    $self->schema->rules->validate_primary_key($col);

    $col->set_nullable(0);

    my $idx = $self->{columns}->Indices($name);
    push @{ $self->{pk} }, $idx;
}

sub delete_primary_key
{
    my $self = shift;

    validate_pos( @_, { isa => 'Alzabo::Create::Column' } );
    my $col = shift;

    my $name = $col->name;
    params_exception "Column $name doesn't exist in $self->{name}"
        unless $self->{columns}->EXISTS($name);

    params_exception "Column $name is not a primary key"
        unless $col->is_primary_key;

    my $idx = $self->{columns}->Indices($name);
    $self->{pk} = [ grep { $_ != $idx } @{ $self->{pk} } ];
}

sub make_foreign_key
{
    my $self = shift;

    $self->add_foreign_key( Alzabo::Create::ForeignKey->new( @_ ) );
}

sub add_foreign_key
{
    my $self = shift;

    validate_pos( @_, { isa => 'Alzabo::Create::ForeignKey' } );
    my $fk = shift;

    foreach my $c ( $fk->columns_from )
    {
        push @{ $self->{fk}{ $fk->table_to->name }{ $c->name } }, $fk;
    }

    if ( ( $fk->is_one_to_one || $fk->is_one_to_many )
         && !
         ( $self->primary_key_size == grep { $_->is_primary_key } $fk->columns_from )
       )
    {
        my $i = Alzabo::Create::Index->new( table   => $self,
                                            columns => [ $fk->columns_from ],
                                            unique  => 1 );

        # could already have a non-unique index (grr, index id()
        # method is somewhat broken)
        $self->delete_index($i) if $self->has_index( $i->id );
        $self->add_index($i);
    }
}

sub delete_foreign_key
{
    my $self = shift;

    validate_pos( @_, { isa => 'Alzabo::Create::ForeignKey' } );
    my $fk = shift;

    foreach my $c ( $fk->columns_from )
    {
        params_exception "Column " . $c->name . " doesn't exist in $self->{name}"
            unless $self->{columns}->EXISTS( $c->name );
    }

    params_exception
        "No foreign keys to " . $fk->table_to->name . " exist in $self->{name}"
            unless exists $self->{fk}{ $fk->table_to->name };

    my @new_fk;
    foreach my $c ( $fk->columns_from )
    {
        params_exception
            "Column " . $c->name . " is not a foreign key to " .
            $fk->table_to->name . " in $self->{name}"
                unless exists $self->{fk}{ $fk->table_to->name }{ $c->name };

        foreach my $current_fk ( @{ $self->{fk}{ $fk->table_to->name }{ $c->name } } )
        {
            push @new_fk, $current_fk unless $current_fk eq $fk;
        }
    }

    foreach my $c ( $fk->columns_from )
    {
        if (@new_fk)
        {
            $self->{fk}{ $fk->table_to->name }{ $c->name } = \@new_fk;
        }
        else
        {
            delete $self->{fk}{ $fk->table_to->name }{ $c->name };
        }
    }

    delete $self->{fk}{ $fk->table_to->name }
        unless keys %{ $self->{fk}{ $fk->table_to->name } };
}

sub make_index
{
    my Alzabo::Table $self = shift;

    $self->add_index( Alzabo::Create::Index->new( table => $self,
                                                  @_ ) );
}

sub add_index
{
    my Alzabo::Table $self = shift;

    validate_pos( @_, { isa => 'Alzabo::Create::Index' } );
    my $i = shift;

    my $id = $i->id;
    params_exception "Index already exists (id $id)."
        if $self->{indexes}->EXISTS($id);

    $self->{indexes}->STORE( $id, $i );

    return $i;
}

sub delete_index
{
    my Alzabo::Table $self = shift;

    validate_pos( @_, { isa => 'Alzabo::Create::Index' } );
    my $i = shift;

    params_exception "Index does not exist."
        unless $self->{indexes}->EXISTS( $i->id );

    $self->{indexes}->DELETE( $i->id );
}

sub register_table_name_change
{
    my $self = shift;

    validate( @_, { table => { isa => 'Alzabo::Create::Table' },
                    old_name => { type => SCALAR } } );
    my %p = @_;

    $self->{fk}{ $p{table}->name } = delete $self->{fk}{ $p{old_name} }
        if exists $self->{fk}{ $p{old_name} };
}

sub register_column_name_change
{
    my $self = shift;

    validate( @_, { column => { isa => 'Alzabo::Create::Column' },
                    old_name => { type => SCALAR } } );
    my %p = @_;

    my $new_name = $p{column}->name;
    my $index = $self->{columns}->Indices( $p{old_name} );
    $self->{columns}->Replace( $index, $p{column}, $new_name );

    foreach my $t ( keys %{ $self->{fk} } )
    {
        $self->{fk}{$t}{$new_name} = delete $self->{fk}{$t}{ $p{old_name} }
            if exists $self->{fk}{$t}{ $p{old_name} };
    }

    my @i = $self->{indexes}->Values;
    $self->{indexes} = Tie::IxHash->new;
    foreach my $i (@i)
    {
        $i->register_column_name_change(%p);
        $self->add_index($i);
    }
}

sub set_attributes
{
    my $self = shift;

    validate_pos( @_, ( { type => SCALAR } ) x @_ );

    %{ $self->{attributes} } = ();

    foreach ( grep { defined && length } @_ )
    {
        $self->add_attribute($_);
    }
}

sub add_attribute
{
    my $self = shift;

    validate_pos( @_, { type => SCALAR } );
    my $attr = shift;

    $attr =~ s/^\s+//;
    $attr =~ s/\s+$//;

    $self->schema->rules->validate_table_attribute( table     => $self,
                                                    attribute => $attr );

    $self->{attributes}{$attr} = 1;
}

sub delete_attribute
{
    my $self = shift;

    validate_pos( @_, { type => SCALAR } );
    my $attr = shift;

    params_exception "Table " . $self->name . " doesn't have attribute $attr"
        unless exists $self->{attributes}{$attr};

    delete $self->{attributes}{$attr};
}

sub set_comment { $_[0]->{comment} = defined $_[1] ? $_[1] : '' }

sub save_current_name
{
    my $self = shift;

    $self->{last_instantiated_name} = $self->name;

    foreach my $column ( $self->columns )
    {
        $column->save_current_name;
    }
}

sub former_name { $_[0]->{last_instantiated_name} }

__END__

=head1 NAME

Alzabo::Create::Table - Table objects for schema creation

=head1 SYNOPSIS

  use Alzabo::Create::Table;

=head1 DESCRIPTION

This class represents tables in the schema.  It contains column,
index, and foreign key objects.

=head1 INHERITS FROM

C<Alzabo::Table>

=for pod_merge merged

=head1 METHODS

=head2 new

The constructor takes the following parameters:

=over 4

=item * schema => C<Alzabo::Create::Schema> object

The schema to which this table belongs.

=item * name => $name

=item * attributes => \@attributes



( run in 0.634 second using v1.01-cache-2.11-cpan-39bf76dae61 )