File Coverage

blib/lib/Alzabo/Runtime/Table.pm
Criterion Covered Total %
statement 30 254 11.8
branch 0 108 0.0
condition 0 34 0.0
subroutine 10 36 27.7
pod 18 20 90.0
total 58 452 12.8


line stmt bran cond sub pod time code
1             package Alzabo::Runtime::Table;
2              
3 11     11   69 use strict;
  11         25  
  11         473  
4 11     11   64 use vars qw($VERSION);
  11         24  
  11         562  
5              
6 11         115 use Alzabo::Exceptions ( abbr => [ qw( logic_exception not_nullable_exception
7 11     11   68 params_exception ) ] );
  11         24  
8 11     11   66 use Alzabo::Runtime;
  11         25  
  11         259  
9 11     11   73 use Alzabo::Utils;
  11         19  
  11         330  
10              
11 11     11   68 use Params::Validate qw( :all );
  11         39  
  11         3108  
12             Params::Validate::validation_options( on_fail => sub { params_exception join '', @_ } );
13              
14 11     11   126 use Scalar::Util ();
  11         28  
  11         277  
15 11     11   73 use Tie::IxHash;
  11         21  
  11         297  
16              
17 11     11   55 use base qw(Alzabo::Table);
  11         30  
  11         54199  
18              
19             $VERSION = 2.0;
20              
21             sub insert
22             {
23 0     0 1   my $self = shift;
24              
25 0 0         logic_exception "Can't make rows for tables without a primary key"
26             unless $self->primary_key;
27              
28 0           my %p = @_;
29 0           %p = validate( @_,
30 0           { ( map { $_ => { optional => 1 } } keys %p ),
31             values => { type => HASHREF, optional => 1 },
32             quote_identifiers => { type => BOOLEAN,
33             optional => 1 },
34             },
35             );
36              
37 0   0       my $vals = delete $p{values} || {};
38              
39 0           my $schema = $self->schema;
40              
41 0           my @pk = $self->primary_key;
42 0           foreach my $pk (@pk)
43             {
44 0 0         unless ( exists $vals->{ $pk->name } )
45             {
46 0 0         if ($pk->sequenced)
47             {
48 0           $vals->{ $pk->name } = $schema->driver->next_sequence_number($pk);
49             }
50             else
51             {
52 0           params_exception
53             ( "No value provided for primary key (" .
54             $pk->name . ") and no sequence is available." );
55             }
56             }
57             }
58              
59 0           foreach my $c ($self->columns)
60             {
61 0 0         next if $c->is_primary_key;
62              
63 0 0 0       unless ( defined $vals->{ $c->name } || $c->nullable || defined $c->default )
      0        
64             {
65 0           not_nullable_exception
66             ( error => $c->name . " column in " . $self->name . " table cannot be null.",
67             column_name => $c->name,
68             table_name => $c->table->name,
69             schema_name => $c->table->schema->name,
70             );
71             }
72              
73 0 0 0       delete $vals->{ $c->name }
74             if ! defined $vals->{ $c->name } && defined $c->default;
75             }
76              
77 0           my @fk;
78 0 0         @fk = $self->all_foreign_keys
79             if $schema->referential_integrity;
80              
81 0           my $sql = ( Alzabo::Runtime::sqlmaker( $self->schema, \%p )->
82             insert->
83             into($self, $self->columns( sort keys %$vals ) )->
84 0           values( map { $self->column($_) => $vals->{$_} } sort keys %$vals ) );
85              
86 0           my %id;
87              
88 0 0         $schema->begin_work if @fk;
89             eval
90 0           {
91 0           foreach my $fk (@fk)
92             {
93 0           $fk->register_insert( map { $_->name => $vals->{ $_->name } } $fk->columns_from );
  0            
94             }
95              
96 0           $sql->debug(\*STDERR) if Alzabo::Debug::SQL;
97 0           print STDERR Devel::StackTrace->new if Alzabo::Debug::TRACE;
98              
99 0           $self->schema->driver->do( sql => $sql->sql,
100             bind => $sql->bind );
101              
102 0           foreach my $pk (@pk)
103             {
104 0 0         $id{ $pk->name } = ( defined $vals->{ $pk->name } ?
105             $vals->{ $pk->name } :
106             $schema->driver->get_last_id($self) );
107             }
108              
109             # must come after call to ->get_last_id for MySQL because the
110             # id will no longer be available after the transaction ends.
111 0 0         $schema->commit if @fk;
112             };
113 0 0         if (my $e = $@)
114             {
115 0           eval { $schema->rollback };
  0            
116              
117 0           rethrow_exception $e;
118             }
119              
120 0 0 0       return unless defined wantarray || $p{potential_row};
121              
122 0           return $self->row_by_pk( pk => \%id, %p );
123             }
124              
125             sub insert_handle
126             {
127 0     0 1   my $self = shift;
128              
129 0 0         logic_exception "Can't make rows for tables without a primary key"
130             unless $self->primary_key;
131              
132 0           my %p = @_;
133 0           %p = validate( @_,
134 0           { ( map { $_ => { optional => 1 } } keys %p ),
135             columns => { type => ARRAYREF, default => [] },
136             values => { type => HASHREF, default => {} },
137             quote_identifiers => { type => BOOLEAN,
138             optional => 1 },
139             },
140             );
141              
142 0           my %func_vals;
143             my %static_vals;
144              
145 0 0         if ( $p{values} )
146             {
147 0           my $v = delete $p{values};
148 0           while ( my ( $name, $val ) = each %$v )
149             {
150 0 0         if ( Alzabo::Utils::safe_isa( $val, 'Alzabo::SQLMaker::Function' ) )
151             {
152 0           $func_vals{$name} = $val;
153             }
154             else
155             {
156 0           $static_vals{$name} = $val
157             }
158             }
159             }
160              
161 0           my $placeholder = $self->schema->sqlmaker->placeholder;
162              
163 0           my %cols;
164             my %vals;
165             # Get the unique set of columns and associated values
166 0           foreach my $col ( @{ $p{columns} }, $self->primary_key )
  0            
167             {
168 0           $vals{ $col->name } = $placeholder;
169 0           $cols{ $col->name } = 1;
170             }
171              
172 0           foreach my $name ( keys %static_vals )
173             {
174 0           $vals{$name} = $placeholder;
175 0           $cols{$name} = 1;
176             }
177              
178 0           %vals = ( %vals, %func_vals );
179              
180             # At this point, %vals has each column's name and associated
181             # value. The value may be a placeholder or SQL function.
182              
183 0           $cols{$_} = 1 foreach keys %func_vals;
184              
185 0           foreach my $c ( $self->columns )
186             {
187 0 0 0       next if $c->is_primary_key || $c->nullable || defined $c->default;
      0        
188              
189 0 0         unless ( $cols{ $c->name } )
190             {
191 0           not_nullable_exception
192             ( error => $c->name . " column in " . $self->name . " table cannot be null.",
193             column_name => $c->name,
194             table_name => $c->table->name,
195             schema_name => $c->table->schema->name,
196             );
197             }
198             }
199              
200 0           my @columns = $self->columns( keys %vals );
201              
202 0           my $sql = ( Alzabo::Runtime::sqlmaker( $self->schema, \%p )->
203             insert->
204             into( $self, @columns )->
205 0           values( map { $_ => $vals{ $_->name } } @columns ),
206             );
207              
208 0           return Alzabo::Runtime::InsertHandle->new( table => $self,
209             sql => $sql,
210             values => \%static_vals,
211             columns => \@columns,
212             %p,
213             );
214             }
215              
216             sub row_by_pk
217             {
218 0     0 1   my $self = shift;
219              
220 0 0         logic_exception "Can't make rows for tables without a primary key"
221             unless $self->primary_key;
222              
223 0           my %p = @_;
224              
225 0           my $pk_val = $p{pk};
226              
227 0           my @pk = $self->primary_key;
228              
229 0 0 0       params_exception
230             'Incorrect number of pk values provided. ' . scalar @pk . ' are needed.'
231             if ref $pk_val && @pk != scalar keys %$pk_val;
232              
233 0 0         if (@pk > 1)
234             {
235 0 0         params_exception
236             ( 'Primary key for ' . $self->name . ' is more than one column.' .
237             ' Please provide multiple key values as a hashref.' )
238             unless ref $pk_val;
239              
240 0           foreach my $pk (@pk)
241             {
242 0 0         params_exception 'No value provided for primary key ' . $pk->name . '.'
243             unless defined $pk_val->{ $pk->name };
244             }
245             }
246              
247 0           return $self->_make_row( %p,
248             table => $self,
249             );
250             }
251              
252             sub _make_row
253             {
254 0     0     my $self = shift;
255 0           my %p = @_;
256              
257 0 0         my $class = $p{row_class} ? delete $p{row_class} : $self->_row_class;
258              
259 0           return $class->new(%p);
260             }
261              
262 0     0     sub _row_class { 'Alzabo::Runtime::Row' }
263              
264             sub row_by_id
265             {
266 0     0 1   my $self = shift;
267 0           my %p = @_;
268 0           validate( @_, { row_id => { type => SCALAR },
269 0           ( map { $_ => { optional => 1 } } keys %p ) } );
270              
271 0           my (undef, undef, %pk) = split ';:;_;:;', delete $p{row_id};
272              
273 0           return $self->row_by_pk( %p, pk => \%pk );
274             }
275              
276             sub rows_where
277             {
278 0     0 1   my $self = shift;
279 0           my %p = @_;
280              
281 0           my $sql = $self->_make_sql(%p);
282              
283 0 0         Alzabo::Runtime::process_where_clause( $sql, $p{where} ) if exists $p{where};
284              
285 0           $sql->debug(\*STDERR) if Alzabo::Debug::SQL;
286 0           print STDERR Devel::StackTrace->new if Alzabo::Debug::TRACE;
287              
288 0           return $self->_cursor_by_sql( %p, sql => $sql );
289             }
290              
291             sub one_row
292             {
293 0     0 1   my $self = shift;
294 0           my %p = @_;
295              
296 0           my $sql = $self->_make_sql(%p);
297              
298 0 0         Alzabo::Runtime::process_where_clause( $sql, $p{where} ) if exists $p{where};
299              
300 0 0         Alzabo::Runtime::process_order_by_clause( $sql, $p{order_by} ) if exists $p{order_by};
301              
302 0 0         if ( exists $p{limit} )
303             {
304 0 0         $sql->limit( ref $p{limit} ? @{ $p{limit} } : $p{limit} );
  0            
305             }
306              
307 0           $sql->debug(\*STDERR) if Alzabo::Debug::SQL;
308 0           print STDERR Devel::StackTrace->new if Alzabo::Debug::TRACE;
309              
310 0 0         my @return = $self->schema->driver->one_row( sql => $sql->sql,
311             bind => $sql->bind )
312             or return;
313              
314 0           my @pk = $self->primary_key;
315              
316 0           my (%pk, %prefetch);
317              
318 0           @pk{ map { $_->name } @pk } = splice @return, 0, scalar @pk;
  0            
319              
320             # Must be some prefetch pieces
321 0 0         if (@return)
322             {
323 0           @prefetch{ $self->prefetch } = @return;
324             }
325              
326 0           return $self->row_by_pk( pk => \%pk,
327             prefetch => \%prefetch,
328             );
329             }
330              
331             sub all_rows
332             {
333 0     0 1   my $self = shift;
334              
335 0           my $sql = $self->_make_sql;
336              
337 0           $sql->debug(\*STDERR) if Alzabo::Debug::SQL;
338 0           print STDERR Devel::StackTrace->new if Alzabo::Debug::TRACE;
339              
340 0           return $self->_cursor_by_sql( @_, sql => $sql );
341             }
342              
343             sub _make_sql
344             {
345 0     0     my $self = shift;
346 0           my %p = @_;
347              
348 0 0         logic_exception "Can't make rows for tables without a primary key"
349             unless $self->primary_key;
350              
351 0 0         my $sql = ( Alzabo::Runtime::sqlmaker( $self->schema, \%p )->
352             select( $self->primary_key,
353             $self->prefetch ? $self->columns( $self->prefetch ) : () )->
354             from( $self ) );
355              
356 0           return $sql;
357             }
358              
359             sub _cursor_by_sql
360             {
361 0     0     my $self = shift;
362              
363 0           my %p = @_;
364 0           validate( @_, { sql => { isa => 'Alzabo::SQLMaker' },
365             order_by => { type => ARRAYREF | HASHREF | OBJECT,
366             optional => 1 },
367             limit => { type => SCALAR | ARRAYREF,
368             optional => 1 },
369 0           ( map { $_ => { optional => 1 } } keys %p ) } );
370              
371 0 0         Alzabo::Runtime::process_order_by_clause( $p{sql}, $p{order_by} ) if exists $p{order_by};
372              
373 0 0         if ( exists $p{limit} )
374             {
375 0 0         $p{sql}->limit( ref $p{limit} ? @{ $p{limit} } : $p{limit} );
  0            
376             }
377              
378 0           my $statement = $self->schema->driver->statement( sql => $p{sql}->sql,
379             bind => $p{sql}->bind,
380             limit => $p{sql}->get_limit );
381              
382 0           return Alzabo::Runtime::RowCursor->new( statement => $statement,
383             table => $self,
384             );
385             }
386              
387             sub potential_row
388             {
389 0     0 1   my $self = shift;
390 0           my %p = @_;
391              
392 0 0         logic_exception "Can't make rows for tables without a primary key"
393             unless $self->primary_key;
394              
395 0 0         my $class = $p{row_class} ? delete $p{row_class} : $self->_row_class;
396              
397 0           return $class->new( %p,
398             state => 'Alzabo::Runtime::RowState::Potential',
399             table => $self,
400             );
401             }
402              
403             sub row_count
404             {
405 0     0 1   my $self = shift;
406 0           my %p = @_;
407              
408 0           my $count = Alzabo::Runtime::sqlmaker( $self->schema, \%p )->COUNT('*');
409              
410 0           return $self->function( select => $count, %p );
411             }
412              
413             sub function
414             {
415 0     0 1   my $self = shift;
416 0           my %p = @_;
417              
418 0           my $sql = $self->_select_sql(%p);
419              
420             my $method =
421 0 0 0       Alzabo::Utils::is_arrayref( $p{select} ) && @{ $p{select} } > 1 ? 'rows' : 'column';
422              
423 0           $sql->debug(\*STDERR) if Alzabo::Debug::SQL;
424 0           print STDERR Devel::StackTrace->new if Alzabo::Debug::TRACE;
425              
426 0           return $self->schema->driver->$method( sql => $sql->sql,
427             bind => $sql->bind );
428             }
429              
430             sub select
431             {
432 0     0 1   my $self = shift;
433              
434 0           my $sql = $self->_select_sql(@_);
435              
436 0           $sql->debug(\*STDERR) if Alzabo::Debug::SQL;
437 0           print STDERR Devel::StackTrace->new if Alzabo::Debug::TRACE;
438              
439 0           return $self->schema->driver->statement( sql => $sql->sql,
440             bind => $sql->bind );
441             }
442              
443             use constant
444 11         14069 _SELECT_SQL_SPEC => { select => { type => SCALAR | ARRAYREF | OBJECT },
445             where => { type => ARRAYREF | OBJECT,
446             optional => 1 },
447             order_by => { type => ARRAYREF | HASHREF | OBJECT,
448             optional => 1 },
449             group_by => { type => ARRAYREF | HASHREF | OBJECT,
450             optional => 1 },
451             having => { type => ARRAYREF,
452             optional => 1 },
453             limit => { type => SCALAR | ARRAYREF,
454             optional => 1 },
455             quote_identifiers => { type => BOOLEAN,
456             optional => 1 },
457 11     11   105 };
  11         44  
458              
459             sub _select_sql
460             {
461 0     0     my $self = shift;
462              
463 0           my %p = validate( @_, _SELECT_SQL_SPEC );
464              
465 0 0         my @funcs = Alzabo::Utils::is_arrayref( $p{select} ) ? @{ $p{select} } : $p{select};
  0            
466              
467 0           my $sql = Alzabo::Runtime::sqlmaker( $self->schema, \%p )->select(@funcs)->from($self);
468              
469 0 0         Alzabo::Runtime::process_where_clause( $sql, $p{where} )
470             if exists $p{where};
471              
472 0 0         Alzabo::Runtime::process_group_by_clause( $sql, $p{group_by} )
473             if exists $p{group_by};
474              
475 0 0         Alzabo::Runtime::process_having_clause( $sql, $p{having} )
476             if exists $p{having};
477              
478 0 0         Alzabo::Runtime::process_order_by_clause( $sql, $p{order_by} )
479             if exists $p{order_by};
480              
481 0 0         $sql->limit( ref $p{limit} ? @{ $p{limit} } : $p{limit} ) if $p{limit};
  0 0          
482              
483 0           return $sql;
484             }
485              
486             sub set_prefetch
487             {
488 0     0 1   my $self = shift;
489              
490 0           $self->{prefetch} = $self->_canonize_prefetch(@_);
491             }
492              
493             sub _canonize_prefetch
494             {
495 0     0     my $self = shift;
496              
497 0           validate_pos( @_, ( { isa => 'Alzabo::Column' } ) x @_ );
498              
499 0           foreach my $c (@_)
500             {
501 0 0         params_exception "Column " . $c->name . " doesn't exist in $self->{name}"
502             unless $self->has_column( $c->name );
503             }
504              
505 0           return [ map { $_->name } grep { ! $_->is_primary_key } @_ ];
  0            
  0            
506             }
507              
508             sub prefetch
509             {
510 0     0 1   my $self = shift;
511              
512 0 0         return ref $self->{prefetch} ? @{ $self->{prefetch} } : ();
  0            
513             }
514              
515             sub add_group
516             {
517 0     0 1   my $self = shift;
518              
519 0           validate_pos( @_, ( { isa => 'Alzabo::Column' } ) x @_ );
520              
521 0           my @names = map { $_->name } @_;
  0            
522 0           foreach my $col (@_)
523             {
524 0 0         params_exception "Column " . $col->name . " doesn't exist in $self->{name}"
525             unless $self->has_column( $col->name );
526              
527 0 0         next if $col->is_primary_key;
528 0           $self->{groups}{ $col->name } = \@names;
529             }
530             }
531              
532             sub group_by_column
533             {
534 0     0 1   my $self = shift;
535 0           my $col = shift;
536              
537 0 0         return exists $self->{groups}{$col} ? @{ $self->{groups}{$col} } : $col;
  0            
538             }
539              
540             my $alias_num = '000000000';
541             sub alias
542             {
543 0     0 1   my $self = shift;
544              
545 0           my $clone;
546 0           %$clone = %$self;
547              
548 0           bless $clone, ref $self;
549              
550 0           $clone->{alias_name} = $self->name . ++$alias_num;
551 0           $clone->{real_table} = $self;
552              
553 0           $clone->{columns} = Tie::IxHash->new( map { $_->name => $_ } $self->columns );
  0            
554              
555             # Force clone of primary key columns right away.
556 0           $clone->column($_) foreach map { $_->name } $self->primary_key;
  0            
557              
558 0           return $clone;
559             }
560              
561             #
562             # Since its unlikely that a user will end up needing clones of more
563             # than 1-2 columns each time an alias is used, we only make copies as
564             # needed.
565             #
566             sub column
567             {
568 0     0 1   my $self = shift;
569              
570             # I'm an alias, make an alias column
571 0 0         if ( $self->{alias_name} )
572             {
573 0           my $name = shift;
574 0           my $col = $self->SUPER::column($name);
575              
576             # not previously cloned
577 0 0         unless ( $col->table eq $self )
578             {
579             # replace our copy of this column with a clone
580 0           $col = $col->alias_clone( table => $self );
581 0           my $index = $self->{columns}->Indices($name);
582 0           $self->{columns}->Replace( $index, $col, $name );
583              
584 0           Scalar::Util::weaken( $col->{table} );
585              
586 0 0         delete $self->{pk_array} if $col->is_primary_key;
587             }
588              
589 0           return $col;
590             }
591             else
592             {
593 0           return $self->SUPER::column(@_);
594             }
595             }
596              
597             sub alias_name
598             {
599             # intentionally don't call $_[0]->name for a noticeable
600             # performance boost
601 0   0 0 0   return $_[0]->{alias_name} || $_[0]->{name};
602             }
603              
604             sub real_table
605             {
606 0   0 0 0   return $_[0]->{real_table} || $_[0];
607             }
608              
609             # This gets called a _lot_ so doing this sort of 'memoization' helps
610             sub primary_key
611             {
612 0     0 1   my $self = shift;
613              
614 0   0       $self->{pk_array} ||= [ $self->SUPER::primary_key ];
615              
616             return ( wantarray ?
617 0 0         @{ $self->{pk_array} } :
  0            
618             $self->{pk_array}->[0]
619             );
620             }
621              
622             1;
623              
624             __END__