File Coverage

blib/lib/Alzabo/Runtime/ForeignKey.pm
Criterion Covered Total %
statement 18 66 27.2
branch 0 22 0.0
condition 0 24 0.0
subroutine 6 12 50.0
pod 3 4 75.0
total 27 128 21.0


line stmt bran cond sub pod time code
1             package Alzabo::Runtime::ForeignKey;
2              
3 11     11   65 use strict;
  11         20  
  11         475  
4 11     11   57 use vars qw( $VERSION %DELETED );
  11         22  
  11         522  
5              
6 11     11   61 use Alzabo::Runtime;
  11         23  
  11         331  
7 11     11   73 use Alzabo::Exceptions ( abbr => 'params_exception' );
  11         27  
  11         99  
8              
9 11     11   71 use Params::Validate qw( validate ARRAYREF OBJECT );
  11         26  
  11         1248  
10             Params::Validate::validation_options
11             ( on_fail => sub { params_exception join '', @_ } );
12              
13 11     11   67 use base qw(Alzabo::ForeignKey);
  11         25  
  11         10825  
14              
15             $VERSION = 2.0;
16              
17             1;
18              
19             # FIXME - needs docs
20             sub new
21             {
22 0     0 0   my $proto = shift;
23 0   0       my $class = ref $proto || $proto;
24              
25 0           validate( @_, { columns_from => { type => ARRAYREF | OBJECT },
26             columns_to => { type => ARRAYREF | OBJECT },
27             } );
28 0           my %p = @_;
29              
30 0           my $self = bless {}, $class;
31              
32             # XXX - needs a little more validation, like that both "sides"
33             # have the same number of columns
34 0           $self->{columns_from} = $p{columns_from};
35 0           $self->{columns_to} = $p{columns_to};
36              
37 0           return $self;
38             }
39              
40             sub register_insert
41             {
42 0     0 1   shift->_insert_or_update( 'insert', @_ );
43             }
44              
45             sub register_update
46             {
47 0     0 1   shift->_insert_or_update( 'update', @_ );
48             }
49              
50             sub _insert_or_update
51             {
52 0     0     my $self = shift;
53 0           my $type = shift;
54 0           my %vals = @_;
55              
56 0           my $driver = $self->table_from->schema->driver;
57              
58 0           my @one_to_one_where;
59             my @one_to_one_vals;
60              
61 0           my $has_nulls = grep { ! defined } values %vals;
  0            
62              
63 0           foreach my $pair ( $self->column_pairs )
64             {
65             # if we're inserting into a table we don't check if its primary
66             # key exists elsewhere, no matter what the cardinality of the
67             # relation. Otherwise, we end up in cycles where it is impossible
68             # to insert things into the table.
69 0 0 0       next if $type eq 'insert' && $pair->[0]->is_primary_key;
70              
71             # A table is always allowed to make updates to its own primary
72             # key columns ...
73 0 0 0       if ( ( $type eq 'update' || $pair->[1]->is_primary_key )
      0        
74             && ! $pair->[0]->is_primary_key )
75             {
76 0 0         $self->_check_existence( $pair->[1] => $vals{ $pair->[0]->name } )
77             if defined $vals{ $pair->[0]->name };
78             }
79              
80             # Except when the PK has a one-to-one relationship to some
81             # other table, and the update would cause a duplication in the
82             # other table.
83 0 0 0       if ( $self->is_one_to_one && ! $has_nulls )
84             {
85 0           push @one_to_one_where, [ $pair->[0], '=', $vals{ $pair->[0]->name } ];
86 0           push @one_to_one_vals, $pair->[0]->name . ' = ' . $vals{ $pair->[0]->name };
87             }
88             }
89              
90 0 0 0       if ( $self->is_one_to_one && ! $has_nulls )
91             {
92 0 0 0       if ( @one_to_one_where &&
93             $self->table_from->row_count( where => \@one_to_one_where ) )
94             {
95 0           my $err = '(' . (join ', ', @one_to_one_vals) . ') already exists in the ' . $self->table_from->name . ' table';
96 0           Alzabo::Exception::ReferentialIntegrity->throw( error => $err );
97             }
98             }
99             }
100              
101             sub _check_existence
102             {
103 0     0     my $self = shift;
104 0           my ($col, $val) = @_;
105              
106 0 0         unless ( $self->table_to->row_count( where => [ $col, '=', $val ] ) )
107             {
108 0           Alzabo::Exception::ReferentialIntegrity->throw( error => 'Foreign key must exist in foreign table. No rows in ' . $self->table_to->name . ' where ' . $col->name . " = $val" );
109             }
110             }
111              
112             sub register_delete
113             {
114 0     0 1   my $self = shift;
115 0           my $row = shift;
116              
117 0           my @update = grep { $_->nullable } $self->columns_to;
  0            
118              
119 0 0 0       return unless $self->to_is_dependent || @update;
120              
121             # Find the rows in the other table that are related to the row
122             # being deleted.
123 0           my @where = map { [ $_->[1], '=', $row->select( $_->[0]->name ) ] } $self->column_pairs;
  0            
124 0           my $cursor = $self->table_to->rows_where( where => \@where );
125              
126 0           while ( my $related_row = $cursor->next )
127             {
128             # This is a class variable so that multiple foreign key
129             # objects don't try to delete the same rows
130 0 0         next if $DELETED{ $related_row->id_as_string };
131              
132 0 0         if ($self->to_is_dependent)
    0          
133             {
134 0           local %DELETED = %DELETED;
135 0           $DELETED{ $related_row->id_as_string } = 1;
136             # dependent relationship so delete other row (may begin a
137             # chain reaction!)
138 0           $related_row->delete;
139             }
140             elsif (@update)
141             {
142             # not dependent so set the column(s) to null
143 0           $related_row->update( map { $_->name => undef } @update );
  0            
144             }
145             }
146             }
147              
148             __END__